diff --git a/README.md b/README.md index 645572b..9b3e3b0 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,17 @@ # Project Template A working documentation skeleton, the tracker convention that goes with it, and -the scripts that enforce both. Copy `docs/` into a new project, replace the -angle-bracket placeholders, delete the paragraphs that do not apply — then -create the labels and the first milestone in the repository's issue tracker. +the scripts that enforce both. Adopt it with +`docs/architecture/scripts/scaffold.sh`, which never overwrites a file that is +already there — the two *Start Here* documents below say which path to take. +Then replace the angle-bracket placeholders, delete the paragraphs that do not +apply, and create the labels and the first milestone in the repository's issue +tracker. ``` Status: Current Owner: _null -Last reviewed: 2026-08-12 +Last reviewed: 2026-08-22 Governs: README.md, START-HERE-New-Project.md, START-HERE-Existing-Project.md Review trigger: Any change to how a project adopts this template — scaffold.sh's file list, the tracker conventions or the label names, or what @@ -151,26 +154,30 @@ and a row on the site only when a human puts it there. ### Where the tracker is -**[dream.scheller.ltd](https://dream.scheller.ltd)** — a Forgejo instance, and -naming it is not a detail. The convention below is portable: milestones, issues -and labels exist on GitHub, GitLab and Jira, and every rule in it would work -there unchanged. **The reporting is not portable at all.** Every figure the -Command Center shows — the headline percentage, the milestone count, milestone -coverage, the alert an opened `P0` raises — is read from a Forgejo repository -mapped to the project. A project that follows every rule here in a different -tracker satisfies the convention completely and appears on privacyllc.dev as -having no tracker at all. +**A Forgejo instance**, and that it is Forgejo is not a detail. The convention +below is portable: milestones, issues and labels exist on GitHub, GitLab and +Jira, and every rule in it would work there unchanged. **The reporting is not +portable at all.** Every figure the Command Center shows — the headline +percentage, the milestone count, milestone coverage, the alert an opened `P0` +raises — is read from a Forgejo repository mapped to the project. A project that +follows every rule here in a different tracker satisfies the convention +completely and appears on privacyllc.dev as having no tracker at all. -Credentials are in **`~/.openclaw/docker-registry.env`**, which carries -`FORGEJO_REGISTRY`, `FORGEJO_REGISTRY_USER` and `FORGEJO_REGISTRY_TOKEN`: +The instance and its credentials are operator configuration, not repository +content: they live in this machine's credential store and are loaded into the +environment before the call. **Nothing here names either**, and neither should +anything you add — a template is copied into repositories that may end up +public, and a document naming a host, a credential file and what that token can +reach is a map even when it holds no secret. ```bash -set -a; . ~/.openclaw/docker-registry.env; set +a +# Load the tracker credentials from wherever this machine keeps them, then: python3 docs/architecture/scripts/forgejo-issue.py list ``` -Despite its name that token is not registry-scoped — it works across the whole -API, with push and pull. Never print it into a log or a chat. +Whatever that token turns out to reach, treat it as reaching everything until +somebody has checked: an API token named for one job is routinely scoped for +none. Never print it into a log or a chat. Two things that cost an hour each otherwise. **Cloudflare fronts the instance and 1010-blocks clients that do not look like a browser or curl** — Python's @@ -186,10 +193,6 @@ cannot file into another project's tracker by accident. `--repo owner/name` overrides that deliberately, and `--dry-run` prints the payloads and changes nothing. -> This block names one instance and one path on the machine that runs it. If -> this repository will be public, replace both with your own before the first -> push. - ### The rules - **Milestone = batch.** Name it `Batch 02 — Payments`, and **start it with a @@ -202,6 +205,14 @@ nothing. inventing one. - **Issue = deliverable.** One per item, in its milestone, ending with a `Verify:` line stating the acceptance check. +- **An issue that produces a data shape names its surface.** A schema, a stored + figure, a computed field or an API response is not a deliverable by itself — + somebody has to be able to see it. Say which screen shows it, link the issue + that provides one, or record the decision that it is deliberately not shown. + Skip it and the `Verify:` line drifts toward *"a caller can retrieve it"*, + which is satisfiable with nothing visible: a batch of twenty-eight issues + closed that way leaves the product exactly as it was, and every figure it + built is reachable only by whoever knows the JSON. - **Labels:** `P0` ships broken or loses data · `P1` materially wrong but shippable · `P2` cosmetic or low impact · `release-blocker` means a release built today would be wrong rather than merely incomplete. Exactly these diff --git a/docs/architecture/scripts/forgejo-issue.py b/docs/architecture/scripts/forgejo-issue.py index d3d8dfc..a64d659 100755 --- a/docs/architecture/scripts/forgejo-issue.py +++ b/docs/architecture/scripts/forgejo-issue.py @@ -214,6 +214,36 @@ def check_verify_line(body: str, title: str) -> str: return body +def check_surface_named(body: str, title: str) -> str: + """An issue that produces a data shape names its surface. + + Both tests read the `Verify:` line, not the body — bodies say "page" and + "renders" incidentally all the time, and the acceptance check is the thing + that decides whether the issue can close with nothing visible. + + Deliberately narrow. It fires only when the acceptance check's subject is + the data itself — retrievable, returned, reported, recorded — and nothing in + it requires a person to see the result. A warning that fires on most issues + is one nobody reads, and it would take the `Verify:`-not-last warning down + with it. + + A warning rather than a refusal: API-only is a legitimate decision, it just + has to be a decision rather than an omission.""" + lines = [l for l in body.strip().splitlines() if l.strip()] + verify = next((l for l in reversed(lines) if l.strip().startswith("Verify:")), "") + data_is_the_subject = re.search( + r"(retrievable|returns the|--json\b|a caller|is stored|recorded with|" + r"\breports\b|available (?:from|via) the API)", verify, re.I) + a_person_sees_it = re.search( + r"\b(admin|screen|page|shown|shows|displayed|visible|sees|operator|" + r"renders|dialog|button|UI)\b", verify, re.I) + if data_is_the_subject and not a_person_sees_it: + print(f'warning: "{title}" accepts on the data being retrievable and never on\n' + " anyone seeing it. Name the screen, link the issue that provides\n" + " one, or state that it is deliberately API-only.", + file=sys.stderr) + return body + def resolve_labels(names, available: dict[str, int]) -> list[int]: """Names → ids, failing loudly. A typo'd severity label is reported by the Command Center as *not adopted*, not as zero defects — silently dropping it @@ -390,6 +420,7 @@ def create_one(api: Api, spec: dict, labels_map, ms_map, existing, allow_dup=False) -> dict | None: title = spec["title"].strip() body = check_verify_line(spec.get("body", ""), title) + body = check_surface_named(body, title) dup = find_duplicate(title, existing) if dup and not allow_dup: