From 6b14022b56cf5010c4d6c4e4525bb8e1e534bde6 Mon Sep 17 00:00:00 2001 From: Brad Byrd Date: Sat, 12 Sep 2026 08:41:46 -0500 Subject: [PATCH 1/3] chore(governance): establish project policies and contribution templates --- .github/ISSUE_TEMPLATE/bug_report.yml | 113 +++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 + .github/pull_request_template.md | 54 ++++++ .github/workflows/governance.yml | 37 ++++ .github/workflows/update-scribe.yml | 50 ++++- .gitignore | 26 +++ .repo-hygiene.toml | 56 ++++++ AGENTS.md | 8 + AI_POLICY.md | 67 +++++++ CLAUDE.md | 49 ++++- CODE_OF_CONDUCT.md | 74 ++++++++ CONTRIBUTING.md | 205 ++++++++++++++++++++ GOVERNANCE.md | 186 ++++++++++++++++++ README.md | 46 ++++- REPOSITORY_HYGIENE.md | 111 +++++++++++ SECURITY.md | 104 ++++++++++ scripts/check_repo_hygiene.py | 216 +++++++++++++++++++++ scripts/run_tests.py | 96 ++++++++++ testing.md | 261 ++++++++++++++++++++++++++ tests/tooling/test_repo_hygiene.py | 211 +++++++++++++++++++++ tests/tooling/test_run_tests.py | 144 ++++++++++++++ 21 files changed, 2103 insertions(+), 16 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/governance.yml create mode 100644 .repo-hygiene.toml create mode 100644 AGENTS.md create mode 100644 AI_POLICY.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 GOVERNANCE.md create mode 100644 REPOSITORY_HYGIENE.md create mode 100644 SECURITY.md create mode 100644 scripts/check_repo_hygiene.py create mode 100644 scripts/run_tests.py create mode 100644 testing.md create mode 100644 tests/tooling/test_repo_hygiene.py create mode 100644 tests/tooling/test_run_tests.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..dfffaaa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,113 @@ +name: Bug report +description: Report a reproducible problem with Scriptorium. +title: "[Bug]: " +body: + - type: markdown + attributes: + value: | + Describe one problem per report and link an existing issue if it covers the same problem. + Share what you know; use "Unknown" for versions or details you cannot determine. + + **This report and its attachments will be public.** Use synthetic examples and remove + passwords, session cookies, CSRF tokens, personal information, private drafts, and site data. + Do not attach a live database, raw production logs, or your complete deployment configuration. + + Report suspected security vulnerabilities privately through + [SECURITY.md](https://github.com/WebFirstLanguage/Scriptorium/blob/main/SECURITY.md). + + - type: textarea + id: summary + attributes: + label: What is broken? + description: Describe the problem and who or what it affects. Use a title that names the failing behavior. + placeholder: "For example: After saving a published page, the public page still shows the previous title." + validations: + required: true + + - type: textarea + id: environment + attributes: + label: Versions and environment + description: | + Include what you can determine; "Unknown" is fine. From the checkout used to run the site, + `git rev-parse HEAD` identifies Scriptorium, `wfl --version` identifies WFL, and + `git submodule status lib/scribe` shows the Scribe revision and checkout status. + Include that output as-is. Describe relevant settings without credentials, + private hostnames, or private filesystem paths. + placeholder: | + Scriptorium commit or version: + WFL version: + Scribe revision and checkout status: + Operating system and version: + Browser and version (if relevant): + Deployment (local, container, reverse proxy, etc.): + Theme and site extension (stock or customized): + Data directory (default or configured): + Fresh install or upgrade: + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Steps to reproduce + description: | + Give the smallest steps that show the problem, including the route, account role, + and synthetic input when relevant. A minimal WFL or template example is welcome. + If you cannot reproduce it reliably, describe the sequence you observed. + placeholder: | + 1. Start with ... + 2. Sign in as an admin/author and open ... + 3. Submit this sample input ... + 4. Observe ... + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected behavior + description: What should happen? Link relevant documentation if it helps explain the expectation. + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual behavior + description: What happened instead? Include the exact error or HTTP status when available, with sensitive values removed. + validations: + required: true + + - type: dropdown + id: frequency + attributes: + label: How often does it happen? + options: + - Every time + - Sometimes + - Observed once + - Not sure + validations: + required: true + + - type: textarea + id: regression + attributes: + label: Last working version or recent changes + description: Optional. Note a last working revision, recent upgrade, Scribe pin change, or theme/configuration change. Say if this is a fresh install. + + - type: textarea + id: evidence + attributes: + label: Relevant logs, screenshots, or minimal example + description: | + Optional. Paste only the relevant sanitized excerpt or attach a screenshot/example using + synthetic data. Remove credentials, cookies, tokens, private content, and personal information. + Include commands and actual results for any checks you already ran; tests are not required to report a bug. + + - type: textarea + id: workaround + attributes: + label: Workaround or additional context + description: Optional. Describe any workaround, related issue, or other observation that may help reproduce the problem. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..3fcf286 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Report a security vulnerability + url: https://github.com/WebFirstLanguage/Scriptorium/blob/main/SECURITY.md + about: Use the private reporting process for vulnerabilities; do not publish exploit details or private site data in an issue. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..5d74cfe --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,54 @@ + + +## Summary + + + +## Changes + + + +## Compatibility and risk + +- **Risk class and reason:** +- **Affected contracts:** +- **Upgrade and recovery:** +- **Remaining risks or gaps:** + +## Validation + +- **Tested revision and environment:** +- **Regression evidence:** + +| Check or exact command | Result and evidence | +|---|---| +| | | +| | | + +## Checklist + +- [ ] The title, summary, and risk assessment match the final diff. +- [ ] Required validation is recorded above; failures and missing checks are explicit. +- [ ] Documentation, examples, and upgrade/recovery guidance are updated where applicable. +- [ ] I reviewed the diff for repository hygiene, secrets, and private site data. + +Follow [CONTRIBUTING.md](https://github.com/WebFirstLanguage/Scriptorium/blob/main/CONTRIBUTING.md), +[testing.md](https://github.com/WebFirstLanguage/Scriptorium/blob/main/testing.md), and +[REPOSITORY_HYGIENE.md](https://github.com/WebFirstLanguage/Scriptorium/blob/main/REPOSITORY_HYGIENE.md). + +Report undisclosed vulnerabilities privately using [SECURITY.md](https://github.com/WebFirstLanguage/Scriptorium/blob/main/SECURITY.md). diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml new file mode 100644 index 0000000..ef30132 --- /dev/null +++ b/.github/workflows/governance.yml @@ -0,0 +1,37 @@ +name: Governance + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: governance-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + repository-checks: + name: Repository checks (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + submodules: recursive + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Test repository tooling + run: python -m unittest discover -s tests/tooling -v + - name: Check repository hygiene + run: python scripts/check_repo_hygiene.py diff --git a/.github/workflows/update-scribe.yml b/.github/workflows/update-scribe.yml index aeb1ea8..6f96222 100644 --- a/.github/workflows/update-scribe.yml +++ b/.github/workflows/update-scribe.yml @@ -6,11 +6,12 @@ # lib/scribe to the tip of Scribe's main and opens a PR with the intervening # Scribe commits in the body, so the bump is reviewed rather than silent. # -# Note on CI: the PR is opened with the built-in GITHUB_TOKEN, and GitHub does -# not fire `push` / `pull_request` workflow events for anything that token -# creates. No test workflow exists in this repo today, so nothing is missed — -# but if one is added, it will NOT run on these auto-generated PRs. Swap in a -# PAT or GitHub App token at that point if you want checks on them. +# Note on CI: GITHUB_TOKEN-created PR runs may require Maintainer approval. +# Approve the pending Governance run, or use Run workflow on the PR branch; +# verify that the successful run covers the current revision before merging. +# Runtime tests also need recorded results; see testing.md. No extra token is +# needed for the manual Governance workflow. +# https://docs.github.com/en/actions/concepts/security/github_token # # To bump by hand instead, run scripts/update-scribe.sh. name: Update Scribe @@ -106,7 +107,11 @@ jobs: git push -u $force origin "$branch" { - echo "Bumps the \`lib/scribe\` submodule from \`$BEFORE\` to \`$AFTER\`." + echo '## Summary' + echo + echo "Update the \`lib/scribe\` pin from \`$BEFORE\` to \`$AFTER\` to pick up the latest upstream main changes while keeping checkouts reproducible." + echo + echo '## Changes' echo echo "Scribe commits picked up:" echo @@ -115,7 +120,38 @@ jobs: echo '```' echo echo "Opened automatically by \`.github/workflows/update-scribe.yml\`." - echo "Review Scribe's changes and run the Scriptorium suites before merging." + echo + echo '## Compatibility and risk' + echo + echo '- **Risk class and reason:** R2 (Scribe dependency pin); raise the class if the upstream diff affects a higher-risk boundary.' + echo '- **Affected contracts:** Scribe pin, template rendering, escaping and safe markers, Markdown, and theme output. Review the upstream diff to identify the changed paths.' + echo '- **Upgrade and recovery:** Compatibility and migration requirements are not yet verified. To undo the pin change, revert the bump commit and update submodules to restore the previous pin. Any migration needs its own tested recovery plan.' + echo '- **Remaining risks or gaps:** Upstream compatibility review and affected rendering journeys are pending. No policy exception has been recorded.' + echo + echo '## Validation' + echo + echo '- **Tested revision and environment:** Pending. Record the tested Scriptorium and Scribe revisions, OS/configuration, `wfl --version`, and relevant tool versions with the results.' + echo '- **Regression evidence:** Pending upstream diff review. Record the required before/after evidence for affected behavior, or explain why a check does not apply.' + echo + echo '| Check or exact command | Result and evidence |' + echo '|---|---|' + echo '| `python scripts/run_tests.py --include-scribe` | Not run — this workflow prepares the dependency bump. Record the local and upstream suite results, including failures. |' + echo '| `python -m unittest discover -s tests/tooling -v` | Pending — Governance results are not verified by this workflow. Link results for the current revision. |' + echo '| `python scripts/check_repo_hygiene.py` | Pending — Governance results are not verified by this workflow. Link results for the current revision. |' + echo '| Affected HTTP/UI rendering journeys | Not run — upstream diff review is needed to identify affected paths. Record setup, expected/actual outcome, and evidence. |' + echo + echo 'Approve any pending Governance run, or run Governance manually on this branch. Verify that successful checks cover the current revision before merging.' + echo + echo '## Checklist' + echo + echo '- [ ] The title, summary, and risk assessment match the final diff.' + echo '- [ ] Required validation is recorded above; failures and missing checks are explicit.' + echo '- [ ] Documentation, examples, and upgrade/recovery guidance are updated where applicable.' + echo '- [ ] I reviewed the diff for repository hygiene, secrets, and private site data.' + echo + echo 'Follow [CONTRIBUTING.md](https://github.com/WebFirstLanguage/Scriptorium/blob/main/CONTRIBUTING.md), [testing.md](https://github.com/WebFirstLanguage/Scriptorium/blob/main/testing.md), and [REPOSITORY_HYGIENE.md](https://github.com/WebFirstLanguage/Scriptorium/blob/main/REPOSITORY_HYGIENE.md).' + echo + echo 'Report undisclosed vulnerabilities privately using [SECURITY.md](https://github.com/WebFirstLanguage/Scriptorium/blob/main/SECURITY.md).' } > /tmp/pr-body.md gh pr create \ diff --git a/.gitignore b/.gitignore index 04379f0..f8d5afc 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,29 @@ node_modules/ # Uploaded media (runtime) static/uploads/* !static/uploads/.gitkeep + +# Local site data, backups, and test/tool outputs +/data/ +/target/ +*.sqlite +*.sqlite3 +*.db-journal +*.sqlite-wal +*.sqlite-shm +*.sqlite3-wal +*.sqlite3-shm +*.log + +# Local credentials and TLS material (never commit deployment configuration) +.env +.env.* +*.key +*.pem +*.p12 +*.pfx + +# Python tooling +__pycache__/ +*.py[cod] +.venv/ +.pytest_cache/ diff --git a/.repo-hygiene.toml b/.repo-hygiene.toml new file mode 100644 index 0000000..4c4e862 --- /dev/null +++ b/.repo-hygiene.toml @@ -0,0 +1,56 @@ +# Concrete enforcement for REPOSITORY_HYGIENE.md. Changes require review of +# the policy reason, not just a wider allowlist to make the checker pass. +schema = 1 + +[root] +allowed-files = [ + ".gitignore", ".gitmodules", ".repo-hygiene.toml", ".wflcfg", + "AGENTS.md", "AI_POLICY.md", "CLAUDE.md", "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", "GOVERNANCE.md", "LICENSE", "README.md", + "REPOSITORY_HYGIENE.md", "SECURITY.md", "main.wfl", "testing.md", +] +allowed-dirs = [ + ".github", "admin", "app", "docs", "lib", "scripts", "static", + "TestPrograms", "tests", "themes", +] + +[required] +files = [ + ".gitignore", ".gitmodules", ".repo-hygiene.toml", ".wflcfg", + "AGENTS.md", "AI_POLICY.md", "CLAUDE.md", "CODE_OF_CONDUCT.md", + "CONTRIBUTING.md", "GOVERNANCE.md", "LICENSE", "README.md", + "REPOSITORY_HYGIENE.md", "SECURITY.md", "main.wfl", "testing.md", + "docs/ARCHITECTURE.md", "docs/PROJECT-LAYOUT.md", "docs/THEMING.md", + "scripts/check_repo_hygiene.py", "scripts/run_tests.py", + "tests/tooling/test_repo_hygiene.py", "tests/tooling/test_run_tests.py", + ".github/pull_request_template.md", ".github/workflows/governance.yml", +] +# These paths must remain Git gitlinks; their contents are upstream-owned. +gitlinks = ["lib/scribe"] + +[forbidden] +# Match path components / base names case-insensitively, at any depth. +dirs = [ + "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache", + "node_modules", ".venv", "venv", "target", ".cache", +] +names = [".DS_Store", "Thumbs.db", "settings.local.json", "id_rsa", "id_ed25519"] +patterns = [ + "*.db", "*.db-*", "*.sqlite", "*.sqlite-*", "*.sqlite3", "*.sqlite3-*", + "*.log", "*.pyc", "*.pyo", "*.orig", "*.rej", "*.tmp", "*.bak", + "*_debug.txt", "*.ast.txt", "*.lex.txt", "*.exe", "*.dll", "*.msi", + ".env", ".env.*", "*.key", "*.pem", "*.p12", "*.pfx", + "credentials.json", "credentials.*.json", "secrets.json", "secrets.*.json", +] +# Mutable application state. Only the existing empty upload placeholder ships. +paths = ["data", "static/uploads"] +allowed-placeholders = ["static/uploads/.gitkeep"] + +[links] +# Check local inline links/images and reference-link definitions in these files. +# Fragments, external URLs, code fences, and paths within gitlinks are skipped. +files = [ + "README.md", "CLAUDE.md", "AGENTS.md", "GOVERNANCE.md", "CONTRIBUTING.md", + "CODE_OF_CONDUCT.md", "AI_POLICY.md", "SECURITY.md", "REPOSITORY_HYGIENE.md", + "testing.md", +] diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4217613 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,8 @@ +# Repository guidelines + +Read [CLAUDE.md](CLAUDE.md) first. It is the canonical shared agent guide for +Scriptorium, including architecture constraints, development commands, and +links to the binding root governance policies. + +This file is an adapter, not a second policy source. Update `CLAUDE.md` and the +relevant root policy when guidance changes. diff --git a/AI_POLICY.md b/AI_POLICY.md new file mode 100644 index 0000000..12626c5 --- /dev/null +++ b/AI_POLICY.md @@ -0,0 +1,67 @@ +# Scriptorium AI Policy + +## AI-assisted contributions are welcome + +Generative AI, coding agents, and other automation are legitimate tools for +contributing code, tests, documentation, reviews, and design proposals to +Scriptorium. Evaluate the contribution by its correctness, usefulness, +maintainability, and compliance with project policy. + +Do not reject a contribution or application, harass someone, or impose a higher +quality bar solely because AI was involved. Do not require contributors to +prove that every character was written by hand. These behaviors are covered by +[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). + +## The submitting person remains accountable + +The person submitting work must understand it well enough to explain the +behavior, assumptions, and tradeoffs and must address review feedback. AI use +does not waive any requirement in [GOVERNANCE.md](GOVERNANCE.md), +[CONTRIBUTING.md](CONTRIBUTING.md), or [testing.md](testing.md). + +| Responsibility | Required behavior | +|---|---| +| Correctness | Review the result and provide the required test evidence | +| Compatibility | Protect site data, routes, configuration, themes, and extension contracts | +| Licensing | Submit only work you have the right to contribute under Apache-2.0; preserve attribution | +| Honesty | Report actual checks and results; do not invent reviews, approvals, citations, or passing CI | +| Reviewability | Keep changes understandable and answer questions about them | +| Security and privacy | Protect credentials, vulnerability details, and site data | + +The same requirements apply to work produced without AI. Reviewers may request +changes or reject a contribution for concrete quality, scope, security, +licensing, or policy reasons. They may also limit automated spam or excessive +volume that prevents useful review. + +## Disclosure + +Disclosure of AI assistance is optional and welcome. A short PR note such as +`Drafted with AI assistance; I reviewed the change and ran the checks listed +above` is sufficient when accurate. Lack of an AI-use label alone is not a +reason to reject a contribution. + +Optional disclosure does not excuse false statements about authorship, +provenance, review, or validation. Attribution required by a third-party license +still applies. + +## Private information and agent authority + +Do not send live site databases, credentials, session cookies, unpublished +content, user uploads, production logs, or private vulnerability details to a +third-party AI service without the appropriate data owner's authorization. +Use synthetic examples or carefully sanitized reproductions for development +and review. Follow [SECURITY.md](SECURITY.md). + +Agents must follow [CLAUDE.md](CLAUDE.md) and the root policies. A tool's ability +to change files, deploy a site, merge a PR, or publish a release does not grant +authority to do so. Maintainers remain accountable for project decisions and +must authorize any delegation for merges, releases, access, or infrastructure. +AI-generated approval does not substitute for required Maintainer approval. + +## Changes to this policy + +Amendments follow [GOVERNANCE.md](GOVERNANCE.md#7-disputes-and-amendments). +Reports of AI-related discrimination follow +[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md#reporting). + +Effective: 2026-09-12. diff --git a/CLAUDE.md b/CLAUDE.md index e79ffae..fe25852 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,10 +1,35 @@ -# Scriptorium — instructions for Claude +# Scriptorium — shared agent instructions Scriptorium is a WordPress-style CMS written entirely in **WFL**, rendering through the **Scribe** template engine (a git submodule at `lib/scribe`) and persisting to SQLite. Start with [`README.md`](README.md), then [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md). +## Project governance + +Scriptorium is maintainer-led; Brad is the primary Maintainer. The binding +policies live at the repository root: + +| Document | Purpose | +|---|---| +| [GOVERNANCE.md](GOVERNANCE.md) | Roles, decisions, compatibility, releases, and amendments | +| [CONTRIBUTING.md](CONTRIBUTING.md) | Contribution workflow and Contributor applications | +| [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) | Community conduct and reporting | +| [AI_POLICY.md](AI_POLICY.md) | AI-assisted work is welcome; authors remain accountable | +| [SECURITY.md](SECURITY.md) | Private vulnerability reporting and security scope | +| [testing.md](testing.md) | Required test evidence, risk triggers, and current gaps | +| [REPOSITORY_HYGIENE.md](REPOSITORY_HYGIENE.md) | Content placement, runtime data, and the enforced hygiene profile | + +Protect existing databases, uploads, URLs, theme contracts, and extension +hooks. Behavioral changes need failing-then-passing test evidence and updated +docs in the same change. Documentation-only changes need relevant validation, +not artificial application tests. Do not log or commit secrets or real site +data. Maintainers own merges, releases, access grants, and policy exceptions; +AI assistance does not change that authority or the quality bar. + +`AGENTS.md` points here so agent guidance has one canonical home. Keep this +section and the root policies aligned when changing contribution workflow. + ## The one rule that catches everyone **[`docs/PROJECT-LAYOUT.md`](docs/PROJECT-LAYOUT.md) is the house standard for @@ -34,11 +59,29 @@ violate the standard. identifiers instead: `the_status`, `media_row`, `db_path`. - **Run from the repo root.** Template and asset paths resolve relative to the working directory. +- **Bug reports:** follow [CONTRIBUTING.md](CONTRIBUTING.md#report-a-bug) and + [.github/ISSUE_TEMPLATE/bug_report.yml](.github/ISSUE_TEMPLATE/bug_report.yml), + including for reports created through a CLI or API. Record observed behavior, + reproduction steps, expected/actual results, and known versions; mark unknown + details honestly. Use sanitized evidence and the private security channel + for suspected vulnerabilities. +- **Pull requests:** follow the title convention and body format in + [CONTRIBUTING.md](CONTRIBUTING.md#pull-request-format), using + [.github/pull_request_template.md](.github/pull_request_template.md) even when + creating a PR through a CLI or API. Keep all five sections, scale the detail + to the change, and update the title and body to match the final diff. Record + actual check results and explain inapplicable or unavailable evidence. - **Scribe is a submodule.** Don't edit `lib/scribe/` in place; changes go upstream to WebFirstLanguage/Scribe, then bump via `scripts/update-scribe.sh`. -- **Tests:** `wfl --test TestPrograms/.test.wfl`. There is no test workflow - in CI today — the only workflow is `update-scribe.yml`. +- **Checks:** `python scripts/run_tests.py` runs every Scriptorium WFL suite; + add `--include-scribe` for dependency updates. Run + `python -m unittest discover -s tests/tooling -v` and + `python scripts/check_repo_hygiene.py` for repository tooling and hygiene. + Python 3.11+ is needed for tooling; `wfl` is needed for application tests. + The Governance workflow runs tooling tests and hygiene on Linux and Windows; + WFL runtime suites currently require recorded local results. See + [testing.md](testing.md) for commands, coverage limits, and merge evidence. - **`data_dir` is an application convention, not a WFL runtime feature.** `main.wfl` reads `.wflcfg` itself at boot and parses the key via `config_value_from` in `app/util.wfl`. The runtime ignores it. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..86ef741 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Scriptorium Code of Conduct + +## Participation + +Scriptorium welcomes people of every background and experience level. Treat +participants with respect regardless of age, disability, race, ethnicity, +nationality, caste, religion, gender, sexual orientation, appearance, education, +or economic circumstances. Beginners and experienced contributors are equally +entitled to a constructive environment. + +AI-assisted participation is welcome. Harassment or exclusion solely because a +person uses AI tools violates this policy. See [AI_POLICY.md](AI_POLICY.md). + +## Expected conduct + +- Discuss the work with specific, constructive feedback. +- Be patient with questions and respectful when disagreeing. +- Acknowledge mistakes, correct inaccurate claims, and credit others' work. +- Respect privacy and keep vulnerability details in the reporting channels + described in [SECURITY.md](SECURITY.md). +- Keep issues and pull requests relevant, understandable, and reviewable. + +Unacceptable conduct includes harassment, intimidation, discriminatory abuse, +sexualized remarks or unwanted sexual attention, personal attacks, threats, +publication of private information without permission, and retaliation against +good-faith reporters. Deliberate malware, credential theft, sabotage, spam, and +sustained disruption of project discussions are also prohibited. + +Strong technical disagreement and requests for tests, documentation, clearer +designs, or compatibility handling are legitimate when expressed without +personal attacks. Reviewers may reject unsafe, incorrect, or unsuitable work +regardless of the tools used to create it. + +## Scope + +This policy applies in Scriptorium's repository, issues, pull requests, official +communication channels, and events, and when someone officially represents the +project elsewhere. It applies to Maintainers, Contributors, and Participants. +It does not grant the project authority over independently operated sites +built with Scriptorium. + +## Reporting + +Email **info@logbie.com** with the subject `Scriptorium Code of Conduct`. +Include what happened, relevant dates and links, and supporting material you +can safely share. Include any immediate safety or privacy concerns. Do not +publish private information in an issue to make a report. + +Maintainers handle reports as confidentially as practical, sharing details +only as needed to investigate and respond. Absolute confidentiality cannot be +promised. Retaliation against someone who reports or assists in good faith is +itself a violation. For vulnerabilities or security-related abuse, use +[SECURITY.md](SECURITY.md). + +## Enforcement and review + +Maintainers enforce this policy under [GOVERNANCE.md](GOVERNANCE.md). They may +moderate content, request a correction, issue a warning, limit participation, +remove elevated access, or impose a temporary or permanent ban. They consider +the impact, severity, and pattern of behavior; serious threats, doxxing, or +sabotage may require immediate restrictions. + +Where practical, Maintainers explain the action and its scope to the affected +person without exposing reporter information. A person may request +reconsideration by emailing the reporting address with relevant context. +Reports involving a Maintainer should be reviewed by an uninvolved Maintainer +when one is available. The project currently has a single primary Maintainer +and does not claim an independent appeals body. + +Policy amendments follow [GOVERNANCE.md](GOVERNANCE.md#7-disputes-and-amendments). +This policy adapts the maintainer-led and AI-inclusive approach used in WFL's +governance suite for Scriptorium. + +Effective: 2026-09-12. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..dac4564 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,205 @@ +# Contributing to Scriptorium + +Scriptorium welcomes fixes, tests, documentation, themes, accessibility work, +and improvements to the CMS. You can contribute through a fork and pull request +without a formal project role. AI-assisted contributions are welcome; the author +remains responsible for understanding and checking the result. + +| Document | Purpose | +|---|---| +| [GOVERNANCE.md](GOVERNANCE.md) | Roles, decisions, compatibility, and project authority | +| [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) | Community standards and reporting | +| [AI_POLICY.md](AI_POLICY.md) | Responsibilities when using AI tools | +| [SECURITY.md](SECURITY.md) | Private vulnerability reporting | +| [testing.md](testing.md) | Required tests, risk assessment, and change evidence | +| [REPOSITORY_HYGIENE.md](REPOSITORY_HYGIENE.md) | File placement and generated output | +| [CLAUDE.md](CLAUDE.md) | Shared agent instructions and development constraints | + +Participation is subject to these policies. Report suspected vulnerabilities +privately through [SECURITY.md](SECURITY.md), rather than a public issue or PR. + +## Report a bug + +Choose **Bug report** when opening a GitHub issue. The canonical form is +[.github/ISSUE_TEMPLATE/bug_report.yml](.github/ISSUE_TEMPLATE/bug_report.yml). +Use a title that names the failing behavior and describe one problem per report; +link an existing report when relevant. + +Include the problem and impact, versions and environment, reproduction steps, +expected and actual behavior, and how often it occurs. When available, add the +last working revision, recent changes, a sanitized log or screenshot, and a +workaround. The form explains how to identify the Scriptorium, WFL, and Scribe +versions. Use `Unknown` for unavailable details; you do not need a diagnosis, +failing test, or proposed fix to report a bug. + +Reports created through a CLI, API, or agent should include the same information. +Use synthetic examples and inspect attachments for secrets and private site +data. Suspected vulnerabilities follow [SECURITY.md](SECURITY.md) privately. +Blank issues remain available for Contributor applications and other topics. + +## Get a working checkout + +Install WFL and Git. Python 3.11 or newer runs the repository's development +checks; it is not an application runtime dependency. Clone your fork with the +pinned Scribe submodule: + +```sh +git clone --recurse-submodules https://github.com/YOUR-USERNAME/Scriptorium.git +cd Scriptorium +git switch -c fix/describe-the-change +``` + +For an existing clone, use `git submodule update --init --recursive`. Read +[README.md](README.md) and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md), then run +the baseline suites: + +```sh +wfl --version +python scripts/run_tests.py +``` + +Use `--wfl /absolute/path/to/wfl` if the interpreter is not on `PATH`; use +`python3` in environments where that is the Python 3 command. The test profile +records current verification limits; a successful local run does not establish +support for an untested platform or runtime version. + +Start a disposable local site with `wfl main.wfl` from the repository root and +open `http://127.0.0.1:8080/`. First run creates a database and presents the +installer. Use synthetic users and content. Set `data_dir` in a disposable +checkout's `.wflcfg` to a temporary directory when exercising persistent state; +keep the database and uploads out of commits. Stop the server when finished. + +## Make a focused change + +1. Describe the problem and observable acceptance criteria. Seek Maintainer + agreement for substantial or breaking changes to public routes, schema or + storage conventions, theme or extension contracts, or the repository layout. +2. For behavior changes, add the smallest useful regression test and run it + against the old behavior. Keep the expected failure as Red evidence. Fix the + implementation, verify Green, and exercise affected boundaries as required + by [testing.md](testing.md). +3. Preserve existing installs and defaults. Include documentation and any + migration or recovery instructions in the same PR. +4. Run the applicable checks, inspect the final diff, and open a PR with the + evidence below. Draft PRs are welcome for early review. + +Scriptorium's layout is deliberately grandfathered. Keep WFL suites in +`TestPrograms/`, the shared router and handlers in `main.wfl`, and the base +theme's existing `sections/` and `templates/` arrangement. The standard in +[docs/PROJECT-LAYOUT.md](docs/PROJECT-LAYOUT.md) applies to new projects; moving +this repository to it requires a separate, agreed migration. + +Read the architecture notes before editing `main.wfl` or `app/`. WFL includes +form a tree; preserve the `util ← db ← auth ← render ← site_ext ← main` chain +and avoid duplicate include paths. Use descriptive qualified names where WFL +reserves common words such as `content`, `status`, `file`, and `count`. + +Use [docs/THEMING.md](docs/THEMING.md) for theme work. Preserve body-template +names and fallback behavior. Site-specific routes and boot work belong behind +the contract documented in [app/site_ext.wfl](app/site_ext.wfl); changes to that +contract need tests of both an extension handling a request and stock fallback. + +## Check and submit + +Run these from the repository root, using the interpreter version recorded on +the PR: + +```sh +python scripts/check_repo_hygiene.py +python -m unittest discover -s tests/tooling -v +python scripts/run_tests.py +``` + +For a Scribe pin, rendering, or template-engine integration change, also run: + +```sh +python scripts/run_tests.py --include-scribe +``` + +The extra suite runs the pinned Scribe tests in a temporary copy because they +write fixtures. Follow [testing.md](testing.md) for HTTP, UI, security, migration, +and recovery checks triggered by the change. Prose-only changes need relevant +link, command, and hygiene checks; they do not need invented application tests. + +Keep PRs focused and use conventional commit subjects such as `fix:`, `feat:`, +`docs:`, `test:`, `refactor:`, and `chore:`. Follow the PR format below. + +Do not include passwords, session cookies, tokens, user databases, or personal +content in logs or fixtures. A review must resolve blocking findings before +merge. A bot-created dependency PR needs the same evidence as a human-authored +PR; approve any pending workflow run or run the Governance workflow manually on +the proposed branch, then verify the tested revision and runtime results. + +GitHub review and branch-protection settings are maintained on the repository +host. Adding these documents or workflows does not configure those settings. + +## Pull request format + +Use [.github/pull_request_template.md](.github/pull_request_template.md) as the +canonical body format for every PR, including those created through a CLI, +API, agent, or dependency updater. Copy it explicitly when your tool does not +load it. Template completion is a review requirement; it does not replace tests +or Maintainer approval. + +Titles use `(): `, for example +`fix(auth): reject expired sessions` or `docs: clarify theme fallback`. +Describe the final change in the title and body, updating both when scope changes. + +Keep these sections in order: + +1. **Summary** — the concrete problem, resulting behavior, observable acceptance + criteria, and related issue when one exists. +2. **Changes** — material implementation changes and reasons a reviewer needs. +3. **Compatibility and risk** — the R0–R3 classification from + [testing.md](testing.md#change-risk), affected contracts, upgrade/recovery + steps, and remaining risks or approved exceptions. +4. **Validation** — tested revision and environment, regression evidence, exact + commands and actual results, and checks of affected user or data boundaries. +5. **Checklist** — confirmations made by the author or reviewing Maintainer + after inspecting the proposed change. + +Replace prompts with evidence and keep details proportional to the change. +Use `N/A — reason` for inapplicable fields and `Not run — reason` for unavailable +checks. Drafts and automated proposals may mark evidence pending; resolve it +under [testing.md](testing.md) before merge. Never pre-check a confirmation or +invent a passing result. +AI disclosure remains optional under [AI_POLICY.md](AI_POLICY.md). + +## Scribe changes + +`lib/scribe` records an exact upstream commit. Make engine fixes in the upstream +[Scribe repository](https://github.com/WebFirstLanguage/Scribe), then update the +pin here with `scripts/update-scribe.sh` from Bash. Inspect the upstream diff and +run the Scriptorium and pinned Scribe suites before proposing the update. +Do not leave an uncommitted engine patch inside the submodule as the CMS fix. + +## Becoming a Contributor + +Contributor is an optional trusted role with explicitly delegated access; it is +not required to submit issues or PRs and does not automatically grant merge or +release authority. Maintainers consider care for existing sites, quality of +tests and reviews, respectful communication, and sustained interest. There is +no fixed PR count or credential requirement. + +Apply with a public issue titled `Scriptorium Contributor Application` in the +[Scriptorium repository](https://github.com/WebFirstLanguage/Scriptorium), or +email `info@logbie.com` with that subject when private contact details are +needed. Maintainers may also invite contributors. Use this outline: + +```text +Preferred name or handle: +GitHub username: +Why you want the role: +Links to contributions, reviews, or relevant work: +Areas of interest (CMS, themes, documentation, tests, security, etc.): +Requested access and intended responsibilities: +Confirmation that you have read the governance, conduct, AI, security, +testing, and repository-hygiene policies: +Private contact details (email applications only): +``` + +Public issues are public: omit private addresses, phone numbers, and other +non-public personal details. Maintainers review applications and explain the +scope of any granted access. Access can be adjusted or revoked under +[GOVERNANCE.md](GOVERNANCE.md); role status does not transfer project or trademark +ownership. There is no guaranteed application response deadline. diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 0000000..988040c --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,186 @@ +# Scriptorium Project Governance + +Scriptorium uses a **maintainer-led** model. This document defines project +authority, contribution roles, and the policies used to review changes. + +| Project | Value | +|---|---| +| Repository | [WebFirstLanguage/Scriptorium](https://github.com/WebFirstLanguage/Scriptorium) | +| Purpose | A WFL content management system with SQLite storage and Scribe templates | +| Stage | MVP under active development | +| Primary Maintainer | Brad | +| Project contact | info@logbie.com | +| License | [Apache-2.0](LICENSE) | + +## 1. Policy map + +These root documents are binding project policy: + +| Document | Responsibility | +|---|---| +| [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) | Respectful participation, reporting, and enforcement | +| [AI_POLICY.md](AI_POLICY.md) | AI inclusion and contributor accountability | +| [CONTRIBUTING.md](CONTRIBUTING.md) | Contribution workflow and applications for elevated access | +| [SECURITY.md](SECURITY.md) | Private vulnerability reporting, support scope, and data protection | +| [testing.md](testing.md) | Test evidence and review gates appropriate to the change | +| [REPOSITORY_HYGIENE.md](REPOSITORY_HYGIENE.md) | Layout, tracked content, temporary outputs, and exceptions | + +[CLAUDE.md](CLAUDE.md) is the canonical shared agent guidance; +[AGENTS.md](AGENTS.md) points to it. Developer instructions must stay consistent +with this policy suite. The [architecture](docs/ARCHITECTURE.md) and +[theming guide](docs/THEMING.md) describe the application contracts that changes +must account for. + +## 2. Roles and authority + +| Role | Responsibilities and access | +|---|---| +| **Maintainer** | Decides project direction, accepts changes, appoints collaborators, manages releases and security response, and approves policy amendments | +| **Contributor** | A participant explicitly granted elevated project access; helps with review, triage, or implementation within delegated responsibilities | +| **Participant** | Anyone proposing changes, reporting bugs, reviewing work, or contributing through a fork and pull request | + +Anyone may propose changes in any area. Formal Contributor status is optional +and does not determine the value of a person's work. Elevated repository access +does not by itself authorize merging to `main`, publishing releases, managing +credentials, or changing repository settings; those actions belong to +Maintainers unless explicitly delegated. + +Brad is the current primary Maintainer and has the final decision when a +technical or governance disagreement remains unresolved. Additional Maintainers +may be appointed explicitly, with their responsibilities recorded here. + +These roles describe project authority; they do not assert that GitHub branch +protections, teams, or other access settings are already configured. + +## 3. Decision process + +Ordinary changes are proposed and reviewed in pull requests. Use an issue first +for substantial design changes when practical, especially changes to stored +data, authentication, public URLs, themes, or extension interfaces. Public +discussion informs the decision; a Maintainer makes it and records significant +decisions in the associated issue, PR, or maintained documentation. + +PRs follow the title and body format in +[CONTRIBUTING.md](CONTRIBUTING.md#pull-request-format), using the canonical +[PR template](.github/pull_request_template.md). Authors and automation use +the same format; required evidence must satisfy [testing.md](testing.md) +before merge. + +Maintainers decide whether a contribution fits the project and has sufficient +review, validation, documentation, and compatibility handling. Review should +identify concrete issues and apply the same quality bar to all contributors, +including those using AI. Silence or a bot-generated review is not approval. +Explicitly delegated automation may carry out approved actions within its scope. + +Security details use the private process in [SECURITY.md](SECURITY.md). +Conduct reports use [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). Maintainers may +act immediately to contain a security incident or serious conduct violation, +then record an explanation when it is safe to do so. + +## 4. Binding technical policies + +### Protect sites and compatibility + +Existing content and working sites are the compatibility baseline. Prefer +additive changes and preserve existing behavior when configuration is absent. +Review compatibility effects on: + +- SQLite schema, stored content, account roles, and session handling; +- `data_dir`, the default database and upload locations, and asset URLs; +- public routes, published slugs, and admin form behavior; +- theme lookup and fallback, template names and context, and + `app/site_ext.wfl`'s boot and dispatch contracts; +- the WFL runtime requirements and the pinned Scribe revision. + +A breaking change needs an explicit Maintainer decision, a documented reason, +an upgrade or migration path, and relevant compatibility tests. Data migrations +must explain backup and recovery requirements and the effect on existing +databases. Do not silently reset a site, discard content, reopen the installer, +or overwrite deployment-specific configuration or extensions during an upgrade. +Security fixes may require an expedited change with the impact documented. + +Scriptorium has no fixed deprecation window or supported-release series yet. +WFL's language-level versioning and deprecation schedules do not automatically +become Scriptorium's release policy. + +### Tests and documentation accompany behavior changes + +Follow [testing.md](testing.md) for risk assessment, regression coverage, +Red-to-Green evidence, and the checks required for the affected layers. Record +the commands run and their actual results, including any unavailable checks. +Do not claim a passing check that was skipped or could not run. + +Update user-facing instructions, architecture notes, examples, and configuration +documentation in the same change as the behavior they describe. Documentation +must distinguish implemented behavior from plans and known limitations. +Commit messages follow the conventional prefixes described in +[CONTRIBUTING.md](CONTRIBUTING.md). + +### Respect the existing architecture and repository layout + +Scriptorium is deliberately grandfathered under the house layout standard for +new WFL projects. Its `main.wfl`, include chain, `TestPrograms/`, and existing +theme layout remain supported. A structural migration is a separate Maintainer +decision, not incidental cleanup. Follow [CLAUDE.md](CLAUDE.md) and +[REPOSITORY_HYGIENE.md](REPOSITORY_HYGIENE.md). + +Scribe is a pinned dependency at `lib/scribe`. Library changes belong upstream; +Scriptorium updates the submodule pin through a reviewed change with relevant +tests. Dependency update automation proposes changes and does not bypass +review or grant itself release authority. + +### Protect security and privacy + +Preserve authorization, CSRF checks, parameterized SQL, output escaping, and +safe filesystem handling. Changes at these boundaries require negative tests +and review of the affected request paths. Treat extensions and themes as +trusted deployment inputs, not a sandbox for untrusted code. + +Never commit credentials, live databases, session tokens, private content, +uploaded user files, or production logs. Use synthetic or sanitized fixtures. +Follow [SECURITY.md](SECURITY.md) for vulnerability reports and private data. + +## 5. Contributor and Maintainer appointments + +Apply for Contributor status through the process in +[CONTRIBUTING.md](CONTRIBUTING.md#becoming-a-contributor). Maintainers consider +judgment, quality, respectful communication, and willingness to maintain the +work. Credentials, contribution counts, and avoiding AI tools are not +prerequisites. There is no automatic promotion timeline. + +Maintainers may invite established Contributors to take on Maintainer duties. +Appointments, delegated responsibilities, and changes to standing access must +be explicit. Access may be reduced or withdrawn for inactivity, a changed +responsibility, security needs, or policy violations. + +## 6. Releases, assets, and licensing + +Maintainers control official releases and project publishing credentials. +Release notes must identify the source revision, relevant dependency changes, +upgrade instructions, and known limitations. The security support scope lives +in [SECURITY.md](SECURITY.md); no release cadence or backport period is implied. + +Contributions are submitted under the repository's [Apache-2.0 license](LICENSE). +Contributors must have the right to submit their work and preserve applicable +third-party licenses and attribution. Contributions do not transfer copyright +ownership to the project. No separate CLA or DCO sign-off process is established +by this policy. Any proposed license change requires a documented decision and +the necessary permissions from the relevant rights holders. + +Project contribution access does not grant access to deployed sites. Deployment +operations, credentials, site content, and backups remain under the authority +of each site's operator; follow the relevant infrastructure instructions. + +## 7. Disputes and amendments + +Resolve technical disagreements through specific evidence and discussion on +the relevant issue or PR. The primary Maintainer resolves disputes that remain. +Conduct and security concerns follow their respective reporting policies. + +Propose policy changes in a pull request and allow reasonable comment for +material changes. Maintainer approval makes an amendment effective. Update all +affected root policies and agent guidance together; editorial corrections need +no extended discussion. Record any urgent exception with its scope and reason, +keeping sensitive details private where necessary. + +Effective: 2026-09-12. diff --git a/README.md b/README.md index 30e0f0f..b9bcfff 100644 --- a/README.md +++ b/README.md @@ -207,12 +207,21 @@ docs/ Architecture notes + THEMING.md + PROJECT-LAYOUT.md + scre ## Tests +From the repository root, with Python 3.11+ and WFL on PATH: + ```sh -wfl --test TestPrograms/util.test.wfl # helpers (slugify, file_ext, parsing, …) -wfl --test TestPrograms/db.test.wfl # data layer against sqlite::memory: -wfl --test TestPrograms/auth.test.wfl # sessions + CSRF token checks +python scripts/run_tests.py # all five Scriptorium suites +python scripts/run_tests.py --include-scribe # also test the pinned Scribe engine +python -m unittest discover -s tests/tooling -v +python scripts/check_repo_hygiene.py ``` +Individual suites still run with `wfl --test TestPrograms/.test.wfl`. +The Governance workflow checks tooling and repository hygiene on Linux and +Windows. Runtime test results must currently be recorded on the PR. See +[testing.md](testing.md) for coverage, commands, and checks for changes to +HTTP routes, security, themes, or stored data. + ## Keeping Scribe current The template engine is not vendored as a copied file any more — `lib/scribe` is @@ -229,20 +238,23 @@ tested against this Scriptorium — but it also means Scribe moving forward does ```sh scripts/update-scribe.sh --check # is there a newer Scribe? (changes nothing) scripts/update-scribe.sh # bump lib/scribe to the tip of Scribe main -wfl --test TestPrograms/scribe.test.wfl # the suite a Scribe bump can break -wfl --test TestPrograms/util.test.wfl # …and the rest (see Tests), then: +python scripts/run_tests.py --include-scribe # app and upstream regression suites git commit -m "chore(scribe): update lib/scribe" ``` `.github/workflows/update-scribe.yml` does the same thing on a weekly schedule (and on demand via *Run workflow*), opening a PR with the Scribe commits it -picked up. Delete that file if you would rather bump by hand only. +picked up. Maintainers must verify Governance checks for the current revision +and record runtime test results before merging; see [testing.md](testing.md). +Delete that file if you would rather bump by hand only. Working on Scribe itself? `lib/scribe` is a normal git checkout — commit and push from inside it, then bump the pin here. ## Security notes +Report suspected vulnerabilities privately using [SECURITY.md](SECURITY.md). + - Passwords are stored only as Argon2id hashes; login uses `verify_password`. - Every SQL statement is **parameterised** — user input is never spliced into SQL. - Output is **auto-escaped** by Scribe; Markdown is rendered through a safe subset. @@ -273,6 +285,28 @@ push from inside it, then bump the pin here. See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for how the pieces fit and the WFL constraints that shaped the design. +## Governance and contributing + +Scriptorium follows a maintainer-led model with Brad as primary Maintainer. +[GOVERNANCE.md](GOVERNANCE.md) defines decision authority and binding policies; +[CONTRIBUTING.md](CONTRIBUTING.md) explains the contribution workflow and how +to apply for Contributor access. Anyone can propose a change through a PR. + +Found a bug? Follow [Report a bug](CONTRIBUTING.md#report-a-bug) and use the +[bug-report form](.github/ISSUE_TEMPLATE/bug_report.yml) to share reproduction +steps and environment details. Report vulnerabilities privately through +[SECURITY.md](SECURITY.md). + +Participation follows the [Code of Conduct](CODE_OF_CONDUCT.md) and +[AI Policy](AI_POLICY.md). AI-assisted contributions are welcome and held to +the same quality bar. The [testing policy](testing.md) and +[repository hygiene policy](REPOSITORY_HYGIENE.md) define checks and evidence. +Agent instructions are in [CLAUDE.md](CLAUDE.md), reached through +[AGENTS.md](AGENTS.md). + +The governance suite adopts WFL's approach for this CMS. Scriptorium's existing +layout remains grandfathered under [docs/PROJECT-LAYOUT.md](docs/PROJECT-LAYOUT.md). + ## License Apache-2.0. See [LICENSE](LICENSE). diff --git a/REPOSITORY_HYGIENE.md b/REPOSITORY_HYGIENE.md new file mode 100644 index 0000000..68c171d --- /dev/null +++ b/REPOSITORY_HYGIENE.md @@ -0,0 +1,111 @@ +# Repository hygiene + +This is Scriptorium's binding placement and tracking policy, governed by +[GOVERNANCE.md](GOVERNANCE.md). The concrete checker configuration is +[.repo-hygiene.toml](.repo-hygiene.toml). Change policy, configuration, and relevant +tests together when an approved exception or a new repository component requires +them. Do not widen an allowlist solely to silence a violation. + +## Preserve the current application layout + +Scriptorium predates the new-project standard in +[docs/PROJECT-LAYOUT.md](docs/PROJECT-LAYOUT.md). That standard's proposed module +and theme migrations are not part of this policy. The existing include chain, +large `main.wfl`, `TestPrograms/`, and `sections/` / `templates/` themes remain +deliberate. A migration needs its own proposal and maintainer decision. + +| Material | Home | +|---|---| +| Application boot, routing, handlers | `main.wfl` | +| Shared application actions | `app/` | +| Admin templates | `admin/templates/` | +| Public themes | `themes/` | +| Shipped CSS, fonts, logos, other source assets | `static/`, or the owning theme | +| WFL behavior tests | `TestPrograms/` | +| Python automation regression tests | `tests/tooling/` | +| Contributor and CI automation | `scripts/`, `.github/` | +| Maintained docs, designs, screenshots | `docs/` | +| Shared agent instructions | [CLAUDE.md](CLAUDE.md); [AGENTS.md](AGENTS.md) is its discovery adapter | +| Governance and contribution policies | The explicitly allowlisted root policy files | +| Scribe dependency | `lib/scribe`, pinned as a Git submodule | + +Keep the root small. Its exhaustive file and directory allowlists are in the +profile. New documentation belongs under `docs/` unless it is an approved root +policy. Keep historical design notes identifiable as historical; they do not +override current policies or maintained architecture documentation. Use issues +for the live backlog and review records for accepted decisions. + +## Track inputs; keep mutable state out + +Track source, tests, maintained documentation, required configuration, and +purposeful shipped assets. Fonts, screenshots, logos, and other product source +assets are allowed; there is no blanket ban on binary files. Review their +purpose, provenance, licensing, and size. + +Do not track site databases or sidecars, uploads, credentials, private keys, +logs, Python or dependency caches, debug dumps, compiled executables, merge +remnants, or temporary output. The only tracked upload entry is the empty +`static/uploads/.gitkeep`. Configure production `data_dir` outside the checkout; +the legacy local database and upload locations remain supported and ignored. +Tests and tools should write transient output to an OS temporary directory or +the ignored `target/` tree (`target/reports/` for local validation reports). +Ignored local runtime data is expected, and the checker does not delete it. + +Keep `.wflcfg` a safe shared default; real deployment credentials and private +configuration stay outside tracked source. The checker catches recognizable +secret filenames, not arbitrary credentials hidden in source or configuration. +Contributors and reviewers remain responsible for inspecting contents. + +Scribe is upstream-owned. Changes go to its upstream repository and arrive here +through a reviewed pin update using [scripts/update-scribe.sh](scripts/update-scribe.sh). +Do not replace the gitlink with copied files or introduce another submodule +without an explicit policy change. First-party tracked symlinks are disallowed +to keep checkout inspection portable and prevent reads outside the repository. + +## Enforcement and its limits + +Run from a checkout with Git and Python 3.11 or later: + +```sh +python scripts/check_repo_hygiene.py +python -m unittest discover -s tests/tooling -p "test_*.py" +``` + +The [governance workflow](.github/workflows/governance.yml) runs the hygiene gate +and tooling tests. [testing.md](testing.md) covers the separate WFL test runner. + +The checker inspects paths in the Git index **plus nonignored untracked files**, +using their current working-tree contents. This checks new work before staging. +An ignored file already tracked in Git remains subject to every rule. Ignored, +untracked runtime data is excluded. The check is not a staged-content audit or +a test for a clean working tree; run it again after staging changes that alter +which files Git tracks. Indexed files deleted only in the working tree are +reported until the deletion is staged. + +It fails on: + +- Root entries outside the case-sensitive allowlists; forbidden directory + components, filenames, and suffix patterns listed in the profile (matched + case-insensitively). +- Candidate files in declared runtime paths, except the named empty placeholder. +- Missing or empty required policies, documentation, and automation files. +- Missing or unapproved Git gitlinks, copied files under `lib/scribe`, and + first-party symlinks or nonregular files. Gitlink contents are not traversed; + Scribe need not be initialized for this check. +- Broken local inline links, image links, and reference-link destinations in + the root files listed in `[links]`. Targets must exist in the candidate tree; + links cannot depend on ignored local files or escape the checkout. + +Link checking handles the simple Markdown syntax used by these documents; +angle brackets delimit destinations containing spaces. It skips fenced code, +external URLs, anchors, and destinations inside the Scribe gitlink. It does not +validate heading anchors, resolve reference labels, check remote URLs, or parse +every Markdown extension. Required-file checks establish presence, not that the +contents implement sound policy. Secret detection by content, asset provenance, +test adequacy, generated-file justification, compatibility, workflow semantics, +and policy exception approval remain review responsibilities. + +The command exits `0` on success, `1` for policy violations, and `2` when the +profile or checkout cannot be inspected reliably. A missing profile, unknown +profile keys, malformed configuration, Git failure, or unresolved merge fails +closed. No check rewrites source, deletes runtime files, or changes Git history. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..3637e5c --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,104 @@ +# Scriptorium Security Policy + +## Development and support scope + +Scriptorium is an MVP under active development. Security fixes target the +current `main` branch. There is no established supported-release series, +backport schedule, response-time guarantee, or security certification. +Maintainers assess reports and coordinate fixes according to severity and +available capacity. + +Reports should identify the Scriptorium commit, WFL version, and pinned Scribe +commit because all three affect behavior. A dependency update is not applied +to existing deployments automatically; site operators must review, test, and +deploy updates. + +## Report vulnerabilities privately + +Email **info@logbie.com** with the subject `Scriptorium Security Vulnerability`. +Do not open a public issue or PR containing exploit details, credentials, or +private site data. This policy does not depend on GitHub private vulnerability +reporting being enabled. + +Include: + +- A description of the issue and its likely impact. +- Affected commits or versions and the relevant operating system and configuration. +- A minimal reproduction using synthetic accounts and data. +- The affected routes, roles, or components, and expected versus actual behavior. +- Any suggested mitigation and a contact method for follow-up. + +Redact passwords, session IDs, CSRF tokens, personal information, and sensitive +host details. Do not attach a live database or raw production logs. Ask for an +appropriate transfer method if sensitive material is necessary to reproduce an +issue. Test only systems you own or are authorized to assess. + +Maintainers investigate privately, determine the affected scope, and coordinate +mitigation, testing, and disclosure. Public security notes should include impact +and upgrade or mitigation guidance when available. Reporter credit is offered +with their consent. Please coordinate publication of details so affected +operators have an opportunity to respond; no fixed embargo period is assumed. + +## Application boundaries and current limitations + +Scriptorium stores accounts, password hashes, active sessions, content, settings, +media metadata, and login-attempt IP addresses in SQLite. It serves uploaded +files through public `/assets/uploads/*` URLs. An unpublished post does not +make an image uploaded for that post private. + +The application uses parameterized SQL, Scribe escaping, password hashing, +session checks, CSRF checks, and ownership/role checks. These controls require +continued testing; they are not a claim that the application has been audited. +Relevant current limitations include: + +- **Transport:** the default listener is local HTTP at `127.0.0.1:8080`. + Session and CSRF cookies use `HttpOnly` and `SameSite=Lax` but currently do + not set `Secure`. An external deployment must enforce HTTPS and configure + secure cookie handling at its proxy or through a reviewed application change. +- **First-run setup:** the installer creates the first administrator. Complete + setup through a controlled access path before exposing a fresh site publicly. +- **Uploads:** the application restricts filename extensions and request size; + it does not decode images or verify their file signatures. Do not treat the + extension allowlist as content inspection or malware scanning. +- **Availability:** request handling and login throttling are basic application + controls. Deployments may need additional connection, traffic, and resource + limits at the hosting or proxy layer. +- **Extensions and themes:** `app/site_ext.wfl`, theme files, and their configured + locations are trusted deployment inputs. Extensions run with application + access to the database and process permissions; this is not an isolation boundary. +- **Storage changes:** schema setup runs at boot and there is no general + migration or rollback engine. Test upgrades against a disposable copy of the + existing schema and preserve a recoverable backup before deployment. + +See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for implementation details and +[README.md](README.md) for configuration and storage paths. + +## Protect site data + +Run with the filesystem and network permissions the site needs. Protect the +database, its journal files, backups, configuration, and any TLS keys from +unauthorized access. Keep backups of both the database and uploaded files and +verify restoration; `data_dir` consolidates their location but does not create +backups. The default locations are `scriptorium.db` and `static/uploads/`. + +Keep runtime data, secrets, private drafts, and production logs out of Git, +public issues, shared screenshots, and test fixtures. Processing private data +with an AI service requires the data owner's authorization under +[AI_POLICY.md](AI_POLICY.md); use synthetic test data for development. +Site operators are responsible for their deployed accounts, access, data +retention, backups, and infrastructure. Contributing to this repository does +not authorize access to any live installation. + +## Requirements for security-sensitive changes + +Preserve authorization and ownership checks, CSRF protection on mutations, +parameterized SQL, safe output rendering, and filesystem path checks. Changes +to authentication, roles, installer state, uploads, paths, migrations, or Scribe +integration need relevant negative and regression tests under +[testing.md](testing.md). Keep sensitive reproductions private until coordinated +disclosure; public fixtures must be sanitized. + +Use [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md#reporting) for conduct reports and +[GOVERNANCE.md](GOVERNANCE.md) for authority and policy amendments. + +Effective: 2026-09-12. diff --git a/scripts/check_repo_hygiene.py b/scripts/check_repo_hygiene.py new file mode 100644 index 0000000..9c13fd3 --- /dev/null +++ b/scripts/check_repo_hygiene.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +"""Check Scriptorium's candidate tree against .repo-hygiene.toml. + +Uses indexed paths (even ignored ones) plus nonignored untracked paths, reading +their working-tree contents so new changes can be checked before staging. +Python 3.11+ standard library only. Exit: 0 clean, 1 violations, 2 setup error. +""" + +from __future__ import annotations + +import argparse +import fnmatch +import os +from pathlib import Path, PurePosixPath +import re +import subprocess +import sys +import tomllib +from urllib.parse import unquote, urlsplit + + +class SetupError(Exception): + """The checkout or policy cannot be inspected reliably.""" + + +def git(root: Path, *args: str) -> bytes: + try: + result = subprocess.run( + ["git", "-C", str(root), *args], capture_output=True, check=False + ) + except OSError as exc: + raise SetupError(f"cannot execute git: {exc}") from exc + if result.returncode: + detail = result.stderr.decode("utf-8", errors="replace").strip() + raise SetupError(f"git {' '.join(args)} failed: {detail}") + return result.stdout + + +def load_profile(root: Path) -> dict: + try: + profile = tomllib.loads((root / ".repo-hygiene.toml").read_text("utf-8")) + except (OSError, UnicodeError, tomllib.TOMLDecodeError) as exc: + raise SetupError(f"cannot read .repo-hygiene.toml: {exc}") from exc + expected = { + "root": {"allowed-files", "allowed-dirs"}, + "required": {"files", "gitlinks"}, + "forbidden": {"dirs", "names", "patterns", "paths", "allowed-placeholders"}, + "links": {"files"}, + } + if type(profile.get("schema")) is not int or profile["schema"] != 1: + raise SetupError("profile schema must be 1") + if set(profile) != {"schema", *expected}: + raise SetupError("profile has missing or unknown sections") + for section, keys in expected.items(): + table = profile.get(section) + if not isinstance(table, dict) or set(table) != keys: + raise SetupError(f"profile [{section}] has missing or unknown keys") + for key, values in table.items(): + if not isinstance(values, list) or not all( + isinstance(value, str) and value for value in values + ) or len(values) != len(set(values)): + raise SetupError(f"profile {section}.{key} must be unique nonempty strings") + if section in {"root", "required", "links"} or key in { + "paths", "allowed-placeholders" + }: + for value in values: + parts = PurePosixPath(value).parts + if (value.startswith("/") or "\\" in value or ":" in value + or ".." in parts or "." in value.split("/") + or str(PurePosixPath(value)) != value): + raise SetupError(f"profile contains non-relative path: {value}") + if section == "root" and len(parts) != 1: + raise SetupError(f"root allowlist entry must be a base name: {value}") + return profile + + +def candidate_files(root: Path) -> dict[str, str]: + """Index modes distinguish symlinks and gitlinks, including on Windows.""" + checkout = os.fsdecode(git(root, "rev-parse", "--show-toplevel")).strip() + if Path(checkout).resolve() != root: + raise SetupError(f"--root must be the checkout root ({checkout})") + entries: dict[str, str] = {} + for record in git(root, "ls-files", "--stage", "-z").split(b"\0"): + if not record: + continue + metadata, name = record.split(b"\t", 1) + mode, _, stage = metadata.split() + if stage != b"0": + raise SetupError(f"unresolved merge in {os.fsdecode(name)}") + entries[os.fsdecode(name)] = mode.decode("ascii") + for name in git(root, "ls-files", "--others", "--exclude-standard", "-z").split(b"\0"): + if name: + entries.setdefault(os.fsdecode(name), "untracked") + return entries + + +def under(path: str, prefix: str) -> bool: + return path == prefix or path.startswith(prefix + "/") + + +def markdown_destinations(source: str): + """The policy docs use simple inline links and reference definitions. + + This deliberately isn't a complete Markdown parser. Destinations with spaces + use angle brackets. Anchors and reference-label resolution aren't validated. + """ + source = re.sub(r"(?ms)^ {0,3}(`{3,}|~{3,})[^\n]*\n.*?^ {0,3}\1\s*$", "", source) + pattern = r"\]\(\s*(<[^>\n]+>|[^\s)]+)(?:\s+[\"'][^\n]*?[\"'])?\s*\)" + for match in re.finditer(pattern, source): + yield match.group(1).strip("<>") + for match in re.finditer(r"(?m)^ {0,3}\[[^\]\n]+\]:\s*(<[^>\n]+>|\S+)", source): + yield match.group(1).strip("<>") + + +def check(root: Path, profile: dict, entries: dict[str, str]) -> list[str]: + violations: list[str] = [] + + def fail(rule: str, name: str, detail: str): + violations.append(f"HYGIENE: {rule}: {name}: {detail}") + + gitlinks = set(profile["required"]["gitlinks"]) + forbidden = profile["forbidden"] + for name, mode in sorted(entries.items()): + parts = PurePosixPath(name).parts + path = root / name + if (len(parts) == 1 and name not in profile["root"]["allowed-files"]) or ( + len(parts) > 1 and parts[0] not in profile["root"]["allowed-dirs"] + ): + fail("root", name, "root entry is not in .repo-hygiene.toml") + if mode == "160000": + if name not in gitlinks: + fail("submodule", name, "unapproved gitlink") + continue + if any(under(name, link) for link in gitlinks): + fail("submodule", name, "Scribe must be an opaque gitlink, not copied files") + if mode == "120000" or path.is_symlink() or not path.resolve().is_relative_to(root): + fail("file-type", name, "first-party symlinks and paths outside the checkout are not allowed") + continue + if not path.is_file(): + fail("file-type", name, "candidate is missing or is not a regular file") + continue + lower_parts = [part.lower() for part in parts] + if any(part in {item.lower() for item in forbidden["dirs"]} for part in lower_parts[:-1]): + fail("artifact", name, "cache or generated-output directory") + if lower_parts[-1] in {item.lower() for item in forbidden["names"]} or any( + fnmatch.fnmatchcase(lower_parts[-1], pattern.lower()) + for pattern in forbidden["patterns"] + ): + fail("artifact", name, "runtime, secret, cache, or generated-output filename") + if any(under(name.lower(), prefix.lower()) for prefix in forbidden["paths"]): + if name not in forbidden["allowed-placeholders"] or path.stat().st_size != 0: + fail("runtime-state", name, "runtime paths allow only declared empty .gitkeep files") + + for name in profile["required"]["files"]: + path = root / name + if name not in entries or not path.is_file() or path.is_symlink(): + fail("required", name, "required file is absent from the candidate tree") + elif path.stat().st_size == 0: + fail("required", name, "required file is empty") + for name in sorted(gitlinks): + if entries.get(name) != "160000": + fail("submodule", name, "required Git gitlink is missing") + + for name in profile["links"]["files"]: + path = root / name + if name not in entries or not path.is_file() or path.is_symlink(): + continue + try: + source = path.read_text("utf-8") + except (OSError, UnicodeError) as exc: + fail("links", name, f"cannot read Markdown as UTF-8: {exc}") + continue + for destination in markdown_destinations(source): + url = urlsplit(destination) + if url.scheme or url.netloc or not url.path: + continue + relative = unquote(url.path) + target = path.parent / relative + resolved = target.resolve() + if not resolved.is_relative_to(root): + fail("links", name, f"local link leaves checkout: {destination}") + continue + relative_target = resolved.relative_to(root).as_posix() + if any(under(relative_target, link) for link in gitlinks): + continue + # Existence alone would let ignored, private local files hide a broken + # link in a fresh clone. Require an actual candidate or directory. + known = relative_target == "." or relative_target in entries or any( + under(candidate, relative_target) for candidate in entries + ) + if not target.exists() or not known: + fail("links", name, f"local link is missing from candidate tree: {destination}") + return violations + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1]) + args = parser.parse_args(argv) + root = args.root.resolve() + try: + profile = load_profile(root) + entries = candidate_files(root) + violations = check(root, profile, entries) + except (SetupError, OSError, ValueError) as exc: + print(f"HYGIENE-ERROR: {exc}", file=sys.stderr) + return 2 + for violation in violations: + print(violation) + if not violations: + print(f"Repository hygiene passed ({len(entries)} candidate paths).") + return 1 if violations else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_tests.py b/scripts/run_tests.py new file mode 100644 index 0000000..303810a --- /dev/null +++ b/scripts/run_tests.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Run Scriptorium's WFL test suites.""" + +import argparse +import math +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile + + +def positive_seconds(value): + try: + seconds = float(value) + except ValueError as exc: + raise argparse.ArgumentTypeError("timeout must be a positive number") from exc + if not math.isfinite(seconds) or seconds <= 0: + raise argparse.ArgumentTypeError("timeout must be finite and greater than zero") + return seconds + + +def run_suite(executable, suite, directory, timeout, label): + print(f"\n=== {label} ===", flush=True) + try: + result = subprocess.run( + [executable, "--test", str(suite)], + cwd=directory, + timeout=timeout, + check=False, + ) + except subprocess.TimeoutExpired: + print(f"FAIL {label}: timeout after {timeout:g} seconds", file=sys.stderr, flush=True) + return False + except OSError as exc: + print(f"FAIL {label}: {exc}", file=sys.stderr, flush=True) + return False + if result.returncode != 0: + print(f"FAIL {label}: exit {result.returncode}", file=sys.stderr, flush=True) + return False + print(f"PASS {label}", flush=True) + return True + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--wfl", default="wfl", help="WFL executable (default: wfl on PATH)") + parser.add_argument("--include-scribe", action="store_true", + help="also run the pinned Scribe suite in a temporary copy") + parser.add_argument("--timeout", type=positive_seconds, default=120, + help="maximum seconds per suite (default: 120)") + args = parser.parse_args() + root = Path(__file__).resolve().parent.parent + suites = sorted(path for path in (root / "TestPrograms").rglob("*.test.wfl") + if path.is_file()) + if not suites: + parser.error("no WFL test suites found under TestPrograms/") + executable = shutil.which(args.wfl) + if executable is None: + parser.error(f"WFL executable not found: {args.wfl}") + executable = str(Path(executable).resolve()) + scribe = root / "lib" / "scribe" + if args.include_scribe: + for required in (scribe / "src" / "scribe.wfl", scribe / "tests" / "scribe.test.wfl"): + if not required.is_file(): + parser.error(f"missing Scribe file: {required}; initialize the pinned submodule") + + results = [] + try: + for suite in suites: + label = suite.relative_to(root).as_posix() + results.append(run_suite(executable, suite, root, args.timeout, label)) + if args.include_scribe: + # Upstream tests write build/ fixtures. Never write those into the + # dependency checkout or carry stale output into a test run. + with tempfile.TemporaryDirectory(prefix="scriptorium-scribe-tests-") as temporary: + copy = Path(temporary) / "scribe" + shutil.copytree(scribe, copy, ignore=shutil.ignore_patterns(".git", "build", "__pycache__")) + (copy / "build").mkdir() + results.append(run_suite( + executable, copy / "tests" / "scribe.test.wfl", copy, args.timeout, + "lib/scribe/tests/scribe.test.wfl (temporary copy)", + )) + except OSError as exc: + print(f"Test setup or cleanup failed: {exc}", file=sys.stderr) + return 2 + except KeyboardInterrupt: + print("Test run interrupted", file=sys.stderr) + return 130 + failures = len(results) - sum(results) + print(f"\n{len(results)} suites: {sum(results)} passed, {failures} failed", flush=True) + return 1 if failures else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/testing.md b/testing.md new file mode 100644 index 0000000..25a1259 --- /dev/null +++ b/testing.md @@ -0,0 +1,261 @@ +# Scriptorium testing policy and project profile + +This is Scriptorium's binding testing policy, adapted from WFL's testing +governance to this WFL application. It defines both required evidence and the +limits of the tooling that exists today. It does not claim that the repository +already implements every gate required for a release. + +- Policy and profile version: 1.0. +- Adopted and reviewed: 2026-09-12. +- Test-suite and infrastructure owner: the Maintainer named in + [GOVERNANCE.md](GOVERNANCE.md). +- Next adoption-gap review: 2026-10-12, or before the next affected behavioral + change or release, whichever comes first. + +## Required evidence + +MUST means required; SHOULD means the normal expectation with a written reason +for a deviation. A change record is the PR or issue that retains scope, results, +review, and any accepted limitation. + +Every behavior change MUST have a regression test at the lowest useful layer +and exercise every affected boundary. A fix MUST reproduce the defect. Use +Red → Green → Refactor → Broaden → Record: + +1. State observable acceptance criteria and identify the affected contracts. +2. Run a new or strengthened test against the previous implementation. It must + fail for the intended behavioral reason. A syntax error or broken fixture + does not count as Red. +3. Make the implementation pass, then refactor with tests passing. +4. Run the full application suites and affected boundary and risk checks. +5. Retain exact commands, tool versions, expected failure, passing results, + tested revision, and limitations in the change record. + +For pure refactoring, characterize the affected behavior first and show Green +before and after. For prose, formatting, or other work without behavior changes, +verify the relevant links, commands, or structure; application regression tests +are not required simply to change wording. New validation or test-runner logic +is behavior and needs failure-path tests. + +Tests MUST assert outcomes rather than merely mirror implementation. A component +test that calls an action does not prove its router, real HTTP response, browser +journey, or persisted-file recovery. Use the real boundary for that claim. +Required failures MUST NOT be hidden with retries, skips, relaxed assertions, or +quarantine. Investigate infrastructure failures with evidence before rerunning; +keep the original failure visible. A flaky required test is a failure to fix. + +## Change risk + +Use the highest applicable class and have the reviewer check the assessment. + +| Class | Typical Scriptorium change | Required scope | +|---|---|---| +| R0 | Prose, comments, formatting with no behavior change | Relevant documentation and hygiene checks | +| R1 | Isolated utility or development-tool behavior | Red/Green regression, boundaries and negative cases, affected suites | +| R2 | Routing, templates, theme resolution, extension contract, Scribe pin | R1 evidence plus real affected integration and user journey | +| R3 | Auth, roles, CSRF, installer, uploads, schema, data location or recovery | R2 evidence plus adversarial cases, data integrity/recovery checks, and independent domain review | + +Independence means the reviewer did not author the implementation and inspects +the actual diff and evidence. An independent review agent can provide technical +review, but project acceptance and merge authority remain with the Maintainer. +Missing automation does not exempt new or changed behavior from these rules. + +## Runtime and environment + +The application needs WFL with its web server, SQLite, crypto, and testing +built-ins, plus the exact Scribe commit recorded at `lib/scribe`. Initialize +submodules with `git submodule update --init --recursive`. Python 3.11 or newer +is needed only for repository tooling. Run individual WFL suites from the +Scriptorium root so relative includes, templates, and assets resolve correctly. + +WFL 26.9.3 on Windows is the local adoption baseline: the five application +suites passed there on 2026-09-12. This is a recorded observation, not a complete +supported-platform matrix or a minimum-version promise. Record `wfl --version`, +OS, Scriptorium revision, and Scribe revision with runtime evidence. No Linux, +macOS, alternate WFL version, or production configuration is release-verified +merely because these suites passed on Windows. + +The five suites use in-memory SQLite and direct action calls; they need no +external credentials or service. Test data MUST be synthetic. For HTTP/UI and +file-backed tests, use a disposable checkout, a temporary `data_dir`, loopback +binding, and an isolated port. Avoid a live site's database or uploads. Keep +test output in the approved locations in +[REPOSITORY_HYGIENE.md](REPOSITORY_HYGIENE.md). + +## Executable checks + +The portable entry point discovers every `TestPrograms/**/*.test.wfl` suite: + +```sh +python scripts/run_tests.py +python scripts/run_tests.py --include-scribe +``` + +`--wfl /absolute/path/to/wfl` selects the executable. `--timeout 120` sets the +maximum seconds per suite; 120 is the default. The runner executes suites +sequentially from the proper working directory, preserves interpreter output, +reports every suite's result, and exits nonzero if any suite fails or times out. +An empty suite set, missing interpreter, or missing requested Scribe source is +an error. There are no automatic retries. + +`--include-scribe` also executes the upstream `tests/scribe.test.wfl` from a +temporary copy of the pinned submodule, with a fresh `build/` for its fixtures. +This avoids writing test output into the dependency checkout. Run it for Scribe +pin changes and changes affecting Scribe integration. CMS-specific Scribe +regressions remain in `TestPrograms/scribe.test.wfl` even when upstream tests +also cover related behavior. + +| Existing suite | Direct command from the repository root | What it currently exercises | +|---|---|---| +| Utilities | `wfl --test TestPrograms/util.test.wfl` | Slugs, number/field helpers, parsing, config values, installer validation | +| Data | `wfl --test TestPrograms/db.test.wfl` | In-memory SQLite CRUD helpers, sessions, installer state, rate-limit records, legacy CSRF-column migration | +| Auth | `wfl --test TestPrograms/auth.test.wfl` | CSRF helper acceptance/rejection and session token binding | +| Scribe integration | `wfl --test TestPrograms/scribe.test.wfl` | Markdown, safe-marker/filter propagation, escaping, nested blockquotes | +| Theme configuration | `wfl --test TestPrograms/render.test.wfl` | Theme path selection defaults and traversal rejection | + +The data suite tests migration idempotency and the legacy session-column +upgrade. The auth suite does not test the login router or complete role matrix. +The render suite tests path selection; it does not render every theme template +through HTTP. Keep these distinctions in PR descriptions. + +Repository tooling checks run independently of WFL: + +```sh +python scripts/check_repo_hygiene.py +python -m unittest discover -s tests/tooling -v +``` + +The tooling suites check the validation and runner failure paths. They do not +substitute for the application suites. Application tests stay in the existing +`TestPrograms/` layout; Python tooling tests live in `tests/tooling/`. + +## CMS boundaries and critical journeys + +When a change touches a journey below, add or extend automated coverage for +the changed behavior and verify the real affected HTTP/UI boundary. Before a +release, exercise all applicable journeys against the exact candidate and +configuration that will be deployed. Record setup, input, expected outcome, +actual outcome, and cleanup. Manual browser checks supply UI evidence while +the automated journey harness remains an explicit adoption gap. + +1. **Installation:** fresh database redirects to the installer; valid setup + creates the administrator and site settings; invalid or mismatched tokens + fail without mutation; setup locks afterward, including a repeated POST. + Existing databases start without reopening the installer. +2. **Authentication and authorization:** login, invalid credentials, expiry, + logout, admin-only operations, and author ownership. Check rejected requests + leave records and sessions unchanged; exercise the rate-limit boundary. +3. **Content publication:** admin/author create and edit posts and pages, + draft visibility, publication, navigation, pagination, and deletion. Verify + the public response and safe rendering of untrusted content. +4. **CSRF and HTTP methods:** valid and missing/wrong tokens for forms and + multipart uploads; GET requests to mutating routes cannot mutate data. +5. **Media:** accepted uploads, rejected type/size/input, generated storage + names, authorization to delete, file/database consistency, and retrieval + under `/assets/uploads/` for default and configured `data_dir`. +6. **Themes and extensions:** default base theme; configured `body/` and legacy + `templates/` layouts; partial-theme fallback and invalid paths; stock + extension fallthrough; extension-owned route and boot timing with migrated + tables; inherited post/page/404 behavior. +7. **Persistence and recovery:** restart preserves users, settings, content, + and media; upgrading a representative prior schema preserves data; repeated + migration is safe; a backup restores the database and matching uploads. + +UI changes also require keyboard navigation, focus, accessible labels, error +presentation, and narrow-screen checks for affected forms and navigation. +Screenshots can supplement these observations but cannot prove authorization, +keyboard behavior, or data integrity. + +## Risk-triggered checks + +- **Security:** test allowed and denied cases through the affected boundary, + including cross-user access, missing/expired sessions, malformed tokens, + untrusted HTML/Markdown, path traversal, oversized and malformed uploads, + and injection attempts. Test the actual implemented upload checks; the CMS + currently uses an extension allowlist, not image-content decoding. Do not + describe an unimplemented content-inspection defense as verified. +- **Schema, installer, and storage:** use synthetic file-backed databases from + the previous schema as well as a fresh database. Verify repeated boot, + partial completion, data preservation, invalid or unwritable paths, and a + concrete restore or forward-repair plan. An in-memory migration test alone + does not prove crash recovery or database/upload consistency. +- **Configuration:** exercise unset/default keys and explicit values for + `data_dir`, `theme`, and `theme_root`, including malformed values. Account for + the current whole-line-comment parsing and out-of-tree theme paths. +- **Dependencies:** inspect the actual pinned Scribe diff, run both local and + upstream suites, and exercise affected rendering paths. Runtime upgrades also + require the application suites and affected web/SQLite/crypto boundaries. +- **Lifecycle or performance:** changes to request processing or resource + limits require bounded timeouts, failure handling, restart, and representative + workloads. Set measured budgets before claiming a capacity or latency target. +- **Test infrastructure:** runner, workflow, discovery, filters, or hygiene + changes require positive and negative cases, the full tooling suite, and + Maintainer review. A change must not weaken its own validation to pass. + +## CI, merge, and release gates + +[.github/workflows/governance.yml](.github/workflows/governance.yml) runs the +repository hygiene and tooling checks. The application suites still require a +local WFL run; there is no pinned-runtime application-test CI job or automated +HTTP/browser journey suite in this repository. The scheduled Scribe updater +proposes dependency changes; it is not runtime test evidence. + +Before merge, required checks MUST pass on the final proposed revision, evidence +MUST cover the affected behavior and risk, and blocking reviews MUST be resolved. +Recheck after changes that invalidate prior results. Maintainers configure +required status checks and review protections on GitHub; repository files alone +do not activate host settings. Bot PRs follow the same review and test rules; +approve pending workflow runs or manually dispatch Governance on the proposed +branch and verify that its revision matches the proposal. + +Before a production release, the Maintainer MUST identify the immutable +Scriptorium and Scribe revisions, runtime version, deployed configuration, and +candidate artifact if packaged. Run the application suites and real critical +journeys for that candidate, including installation, upgrade, data recovery, +and the affected security cases. Keep recovery instructions and evidence with +the release record. An untested journey or platform cannot be called verified; +a failed required test blocks release. + +No coverage percentage, performance budget, accessibility conformance level, or +availability guarantee is currently measured by this profile. Establish the +measurement and passing criteria before making such a claim. + +## Adoption gaps and follow-through + +The Maintainer owns this dated register as of 2026-09-12. Each item requires an +owned issue before implementation and a link here when that issue exists. + +| Gap | Required next step and trigger | +|---|---| +| WFL application suites are manual | Pin and provision a runtime in CI; evaluate before the next behavioral PR. Until then attach exact local results to every affected PR. | +| No router/HTTP or browser automation | Add real-boundary regression coverage with each affected behavior change; plan coverage of all critical journeys before the next production release. | +| No declared compatibility matrix or release-candidate workflow | Define supported runtime/platform/configuration tuples and retain candidate results before the next production release. | +| No coverage measurement, performance budgets, or scheduled extended tests | Establish baselines and risk-based targets before claiming those properties; review at the next profile review. | +| Host protection settings are external | Maintainer verifies required checks and review rules on GitHub at adoption and after workflow changes. | + +Existing gaps do not authorize new untested behavior or a claim of full +compliance. Update this register when tooling or host settings are verified. + +## Exceptions and completion + +The Maintainer may approve a narrow temporary exception for unavailable +evidence or an emergency mitigation. Record the exact rule, revision, reason, +approver, compensating checks, recovery plan, expiry, and an owned repair issue +with a deadline. An author cannot be the only reviewer of their exception. +Ordinary exceptions expire within 30 days and R3 exceptions within seven days; +they must not silently roll forward to another release. + +An exception records missing evidence; it does not convert it into a pass. +Known authorization bypass, exposed secrets, data loss or corruption, or a +reproducible required-test failure cannot be waived into a normal release. +For an active incident, a minimal reversible mitigation may reorder Red/Green +work with Maintainer approval; complete regression coverage and affected suites +within 24 hours and before closing the incident or making another normal +deployment of the affected component. + +A change is ready to merge only when its acceptance criteria, required tests, +review, documentation, recovery instructions where applicable, and durable +evidence are complete. Preserve PR evidence with the change record; preserve +release, migration, security, and exception evidence for the supported life of +the affected release. Evidence MUST exclude secrets and uncontrolled personal +data. Unmet requirements remain visible as pending or blocked work. diff --git a/tests/tooling/test_repo_hygiene.py b/tests/tooling/test_repo_hygiene.py new file mode 100644 index 0000000..2dd3a98 --- /dev/null +++ b/tests/tooling/test_repo_hygiene.py @@ -0,0 +1,211 @@ +"""Exercise the hygiene command against isolated Git checkouts, never this index.""" + +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import tomllib +import unittest + + +PROJECT = Path(__file__).resolve().parents[2] +CHECKER = PROJECT / "scripts/check_repo_hygiene.py" +PROFILE = PROJECT / ".repo-hygiene.toml" + + +@unittest.skipUnless(shutil.which("git"), "Git is required for fixture checkouts") +class RepositoryHygieneTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix="scriptorium-hygiene-") + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.git("init", "--quiet") + self.profile_text = PROFILE.read_text("utf-8") + profile = tomllib.loads(self.profile_text) + for name in profile["required"]["files"]: + self.write(name, "Fixture file.\n") + self.write(".repo-hygiene.toml", self.profile_text) + self.write(".gitignore", "*.db\n*.private\n.env\n__pycache__/\n") + self.write("README.md", "[Architecture](docs/ARCHITECTURE.md)\n") + self.write("static/uploads/.gitkeep", "") + self.write("app/source.wfl", "display \"fixture\"\n") + self.git("add", ".") + # A gitlink records a commit object name. No remote, nested checkout, or + # object download is needed to test that the parent treats it opaquely. + self.git("update-index", "--add", "--cacheinfo", "160000", "1" * 40, "lib/scribe") + + def git(self, *args): + return subprocess.run( + ["git", "-C", str(self.root), *args], capture_output=True, check=True + ) + + def write(self, name, content): + path = self.root / name + path.parent.mkdir(parents=True, exist_ok=True) + if isinstance(content, bytes): + path.write_bytes(content) + else: + path.write_text(content, encoding="utf-8") + return path + + def run_checker(self): + return subprocess.run( + [sys.executable, str(CHECKER), "--root", str(self.root)], + capture_output=True, text=True, check=False, + ) + + def assert_result(self, code, fragment=None): + result = self.run_checker() + output = result.stdout + result.stderr + self.assertEqual(result.returncode, code, output) + if fragment is not None: + self.assertIn(fragment, output) + return output + + def test_clean_checkout_and_uninitialized_submodule_pass(self): + self.assert_result(0, "Repository hygiene passed") + + def test_new_untracked_root_file_is_checked_before_staging(self): + self.write("scratch.md", "Unapproved root note\n") + self.assert_result(1, "HYGIENE: root: scratch.md") + + def test_new_untracked_root_directory_is_rejected(self): + self.write("notes/design.md", "An unapproved root directory\n") + self.assert_result(1, "HYGIENE: root: notes/design.md") + + def test_legitimate_new_docs_and_binary_source_assets_pass(self): + self.write("docs/new-guide.md", "Maintained guide\n") + self.write("static/fonts/new.woff2", b"wOF2\x00fixture") + self.write("docs/screenshots/new.png", b"\x89PNG\r\n\x1a\n\x00fixture") + self.assert_result(0) + + def test_ignored_untracked_runtime_data_is_left_alone(self): + database = self.write("scriptorium.db", b"private runtime fixture") + self.write(".env", "TOKEN=fixture\n") + self.assert_result(0) + self.assertEqual(database.read_bytes(), b"private runtime fixture") + + def test_force_tracked_ignored_database_is_rejected(self): + self.write("app/fixture.db", b"runtime fixture") + self.git("add", "--force", "app/fixture.db") + self.assert_result(1, "HYGIENE: artifact: app/fixture.db") + + def test_case_variants_and_nested_cache_are_rejected(self): + # Windows Git may ignore __PyCache__ under the lowercase ignore rule. + # Make these candidates visible on every platform for this check. + self.write(".gitignore", "# No ignores in this test.\n") + for name in ["app/site.SQLITE3-WAL", "docs/server.LOG", "scripts/__PyCache__/bad.txt"]: + with self.subTest(name=name): + self.write(name, "fixture\n") + self.assert_result(1, f"HYGIENE: artifact: {name}") + (self.root / name).unlink() + + def test_secret_names_are_rejected_at_any_depth(self): + for name in ["app/.env.production", "docs/id_rsa", "app/secret.KEY"]: + with self.subTest(name=name): + self.write(name, "fixture\n") + self.assert_result(1, f"HYGIENE: artifact: {name}") + (self.root / name).unlink() + + def test_upload_image_cannot_use_source_asset_exception(self): + self.write("static/uploads/photo.png", b"\x89PNG\x00runtime fixture") + self.assert_result(1, "HYGIENE: runtime-state: static/uploads/photo.png") + + def test_only_empty_declared_upload_placeholder_is_allowed(self): + self.write("static/uploads/.gitkeep", "runtime bytes\n") + self.assert_result(1, "HYGIENE: runtime-state: static/uploads/.gitkeep") + + def test_removed_and_empty_required_policy_fail(self): + (self.root / "GOVERNANCE.md").unlink() + self.assert_result(1, "HYGIENE: required: GOVERNANCE.md") + self.write("GOVERNANCE.md", "") + self.assert_result(1, "required file is empty") + + def test_ignored_required_policy_cannot_pass_by_existing_locally(self): + self.git("rm", "--cached", "--force", "GOVERNANCE.md") + self.write(".gitignore", "GOVERNANCE.md\n") + self.assert_result(1, "HYGIENE: required: GOVERNANCE.md") + + def test_worktree_link_changes_are_checked_without_staging(self): + self.write("README.md", "[Missing](docs/does-not-exist.md)\n") + self.assert_result(1, "local link is missing from candidate tree") + + def test_link_to_ignored_local_file_cannot_hide_broken_clone(self): + self.write("docs/local.private", "private fixture\n") + self.write("README.md", "[Local](docs/local.private)\n") + self.assert_result(1, "local link is missing from candidate tree") + + def test_links_to_new_candidates_directories_and_encoded_paths_pass(self): + self.write("docs/new file.md", "New guide\n") + self.write("README.md", "[Guide](docs/new%20file.md#title)\n[Docs](docs/)\n" + "[Spaced]()\n[Ref][guide]\n" + "[guide]: docs/new%20file.md \"A guide\"\n") + self.assert_result(0) + + def test_reference_and_image_link_destinations_are_checked(self): + self.write("README.md", "![Image](docs/missing.png)\n[ref]: docs/missing.md\n") + output = self.assert_result(1) + self.assertIn("docs/missing.png", output) + self.assertIn("docs/missing.md", output) + + def test_anchors_urls_fences_and_submodule_links_are_skipped(self): + self.write("README.md", "[Anchor](#unknown)\n[Web](https://example.invalid/nope)\n" + "[Mail](mailto:test@example.invalid)\n" + "[Scribe](lib/scribe/uninitialized.md)\n" + "```md\n[Example](missing.md)\n```\n" + "~~~md\n[Example](missing-too.md)\n~~~\n") + self.assert_result(0) + + def test_links_cannot_escape_checkout(self): + self.write("README.md", "[Outside](../outside.md)\n") + self.assert_result(1, "local link leaves checkout") + + def test_submodule_contents_are_not_recursively_inspected(self): + self.write("lib/scribe/private.db", "upstream runtime fixture\n") + self.write("lib/scribe/.env", "upstream fixture\n") + self.assert_result(0) + + def test_scribe_cannot_be_replaced_with_copied_files(self): + self.git("update-index", "--force-remove", "lib/scribe") + self.write("lib/scribe/src/scribe.wfl", "copied upstream fixture\n") + self.assert_result(1, "required Git gitlink is missing") + + def test_another_gitlink_cannot_bypass_artifact_inspection(self): + self.git("update-index", "--add", "--cacheinfo", "160000", "2" * 40, "lib/other") + self.assert_result(1, "HYGIENE: submodule: lib/other: unapproved gitlink") + + def test_indexed_symlink_is_rejected_even_when_checked_out_as_text(self): + target = self.write("app/linked.wfl", "../README.md") + blob = self.git("hash-object", "-w", str(target)).stdout.decode().strip() + self.git("update-index", "--add", "--cacheinfo", "120000", blob, "app/linked.wfl") + self.assert_result(1, "HYGIENE: file-type: app/linked.wfl") + + def test_malformed_profile_fails_closed(self): + self.write(".repo-hygiene.toml", "schema = [\n") + self.assert_result(2, "HYGIENE-ERROR") + + def test_missing_profile_fails_closed(self): + (self.root / ".repo-hygiene.toml").unlink() + self.assert_result(2, "cannot read .repo-hygiene.toml") + + def test_unknown_profile_key_fails_closed(self): + self.write(".repo-hygiene.toml", self.profile_text.replace( + "allowed-dirs =", "allowed-directories =" + )) + self.assert_result(2, "missing or unknown keys") + + def test_profile_cannot_use_escape_paths(self): + self.write(".repo-hygiene.toml", self.profile_text.replace( + '"docs/ARCHITECTURE.md"', '"../ARCHITECTURE.md"' + )) + self.assert_result(2, "non-relative path") + + def test_git_failure_fails_closed(self): + # Rename only this fixture's .git, without touching the actual checkout. + (self.root / ".git").rename(self.root / "fixture-index-away") + self.assert_result(2, "HYGIENE-ERROR: git") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/tooling/test_run_tests.py b/tests/tooling/test_run_tests.py new file mode 100644 index 0000000..cbebb0b --- /dev/null +++ b/tests/tooling/test_run_tests.py @@ -0,0 +1,144 @@ +"""Exercise the test runner as a process with a controlled interpreter.""" + +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import unittest + + +RUNNER = Path(__file__).resolve().parents[2] / "scripts" / "run_tests.py" + + +class RunTestsTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory(prefix="scriptorium-runner-test-") + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + (self.root / "scripts").mkdir() + shutil.copy2(RUNNER, self.root / "scripts" / "run_tests.py") + (self.root / "TestPrograms").mkdir() + fake = self.root / "fake_wfl.py" + fake.write_text( + "import json, pathlib, sys, time\n" + "suite = pathlib.Path(sys.argv[2])\n" + "case = json.loads(suite.read_text())\n" + "print('RUN ' + json.dumps({'suite': str(suite), " + "'cwd': str(pathlib.Path.cwd()), 'build_exists': pathlib.Path('build').is_dir()}), flush=True)\n" + "if case.get('stderr'): print(case['stderr'], file=sys.stderr, flush=True)\n" + "time.sleep(case.get('sleep', 0))\n" + "sys.exit(case.get('exit', 0))\n", + encoding="utf-8", + ) + if os.name == "nt": + self.interpreter = self.root / "fake-wfl.cmd" + self.interpreter.write_text( + f'@echo off\n"{sys.executable}" "{fake}" %*\n', encoding="utf-8" + ) + else: + self.interpreter = self.root / "fake-wfl" + self.interpreter.write_text( + f"#!{sys.executable}\n" + fake.read_text(encoding="utf-8"), + encoding="utf-8", + ) + self.interpreter.chmod(0o755) + + def suite(self, name="one.test.wfl", **behavior): + path = self.root / "TestPrograms" / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(behavior), encoding="utf-8") + return path + + def run_runner(self, *args): + return subprocess.run( + [sys.executable, str(self.root / "scripts" / "run_tests.py"), + "--wfl", str(self.interpreter), *args], + cwd=self.root.parent, + text=True, + capture_output=True, + timeout=15, + ) + + @staticmethod + def runs(result): + return [json.loads(line[4:]) for line in result.stdout.splitlines() + if line.startswith("RUN ")] + + def test_discovers_nested_suites_in_order_and_uses_repository_cwd(self): + self.suite("z.test.wfl") + self.suite("nested/a.test.wfl") + (self.root / "TestPrograms" / "example.wfl").write_text("not a suite") + result = self.run_runner() + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + runs = self.runs(result) + self.assertEqual([Path(run["suite"]).name for run in runs], + ["a.test.wfl", "z.test.wfl"]) + self.assertTrue(all(Path(run["cwd"]) == self.root for run in runs)) + + def test_failing_suite_preserves_diagnostics_and_later_suite_runs(self): + self.suite("a.test.wfl", exit=7, stderr="intentional fixture failure") + self.suite("b.test.wfl") + result = self.run_runner() + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertEqual(len(self.runs(result)), 2) + self.assertIn("intentional fixture failure", result.stderr) + self.assertIn("a.test.wfl", result.stdout + result.stderr) + + def test_timeout_fails_and_later_suite_still_runs(self): + self.suite("a.test.wfl", sleep=2) + self.suite("b.test.wfl") + result = self.run_runner("--timeout", "1") + self.assertEqual(result.returncode, 1, result.stdout + result.stderr) + self.assertEqual(len(self.runs(result)), 2) + self.assertIn("timeout", (result.stdout + result.stderr).lower()) + + def test_empty_suite_set_is_an_error(self): + result = self.run_runner() + self.assertNotEqual(result.returncode, 0) + self.assertIn("no", (result.stdout + result.stderr).lower()) + + def test_missing_interpreter_is_an_error(self): + self.suite() + result = self.run_runner("--wfl", str(self.root / "absent-wfl")) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(self.runs(result), []) + + def test_nonpositive_and_nonfinite_timeout_are_rejected(self): + self.suite() + for value in ("0", "-1", "nan", "inf"): + with self.subTest(value=value): + result = self.run_runner("--timeout", value) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(self.runs(result), []) + + def test_missing_scribe_source_is_an_error(self): + self.suite() + result = self.run_runner("--include-scribe") + self.assertNotEqual(result.returncode, 0) + self.assertIn("scribe", (result.stdout + result.stderr).lower()) + + def test_upstream_suite_runs_in_disposable_copy_with_build_directory(self): + self.suite() + scribe = self.root / "lib" / "scribe" + (scribe / "src").mkdir(parents=True) + (scribe / "src" / "scribe.wfl").write_text("fixture") + (scribe / "tests").mkdir() + (scribe / "tests" / "scribe.test.wfl").write_text("{}") + (scribe / "build").mkdir() + (scribe / "build" / "existing.txt").write_text("preserve") + result = self.run_runner("--include-scribe") + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + runs = self.runs(result) + self.assertEqual(len(runs), 2) + upstream = Path(runs[-1]["cwd"]) + self.assertNotEqual(upstream, scribe) + self.assertTrue(runs[-1]["build_exists"]) + self.assertFalse(upstream.exists(), "temporary Scribe copy was not removed") + self.assertEqual((scribe / "build" / "existing.txt").read_text(), "preserve") + + +if __name__ == "__main__": + unittest.main() From 0b354896fb5cc2d1a20a79cf4a818c7b32e27904 Mon Sep 17 00:00:00 2001 From: Brad Byrd Date: Sat, 12 Sep 2026 08:46:36 -0500 Subject: [PATCH 2/3] test: compare fixture directories by identity on Windows --- tests/tooling/test_run_tests.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/tooling/test_run_tests.py b/tests/tooling/test_run_tests.py index cbebb0b..077e9ff 100644 --- a/tests/tooling/test_run_tests.py +++ b/tests/tooling/test_run_tests.py @@ -76,7 +76,10 @@ def test_discovers_nested_suites_in_order_and_uses_repository_cwd(self): runs = self.runs(result) self.assertEqual([Path(run["suite"]).name for run in runs], ["a.test.wfl", "z.test.wfl"]) - self.assertTrue(all(Path(run["cwd"]) == self.root for run in runs)) + # Windows TEMP can use a DOS short path that the runner resolves to its + # long spelling. Assert directory identity, not how the path is spelled. + self.assertTrue(all(Path(run["cwd"]).samefile(self.root) for run in runs), + f"Expected {self.root}, got {[run['cwd'] for run in runs]}") def test_failing_suite_preserves_diagnostics_and_later_suite_runs(self): self.suite("a.test.wfl", exit=7, stderr="intentional fixture failure") From 8787f253ee8360bdf995f6a162d91a15c483866a Mon Sep 17 00:00:00 2001 From: Brad Byrd Date: Sat, 12 Sep 2026 08:49:47 -0500 Subject: [PATCH 3/3] fix(hygiene): ignore SQLite rollback journals --- .gitignore | 2 ++ tests/tooling/test_repo_hygiene.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index f8d5afc..ab5e4cf 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,8 @@ static/uploads/* *.sqlite *.sqlite3 *.db-journal +*.sqlite-journal +*.sqlite3-journal *.sqlite-wal *.sqlite-shm *.sqlite3-wal diff --git a/tests/tooling/test_repo_hygiene.py b/tests/tooling/test_repo_hygiene.py index 2dd3a98..07bf7aa 100644 --- a/tests/tooling/test_repo_hygiene.py +++ b/tests/tooling/test_repo_hygiene.py @@ -91,6 +91,13 @@ def test_force_tracked_ignored_database_is_rejected(self): self.git("add", "--force", "app/fixture.db") self.assert_result(1, "HYGIENE: artifact: app/fixture.db") + def test_repository_ignores_sqlite_databases_and_sidecars(self): + self.write(".gitignore", (PROJECT / ".gitignore").read_text("utf-8")) + for extension in ("db", "sqlite", "sqlite3"): + for suffix in ("", "-journal", "-wal", "-shm"): + self.write(f"site.{extension}{suffix}", b"local runtime fixture") + self.assert_result(0) + def test_case_variants_and_nested_cache_are_rejected(self): # Windows Git may ignore __PyCache__ under the lowercase ignore rule. # Make these candidates visible on every platform for this check.