docs(tracker): an issue that produces a data shape names its surface

A batch of twenty-eight issues in one adopting project could close in full —
every P1 included — with nothing new visible to anybody using the product.
Cost per project, hours with coverage bounds, model attribution and an
algorithm version on every stored figure: all of it verifiable through a JSON
flag, none of it required to appear on a screen.

Two of those issues exist specifically to correct what somebody believes when
they look at the admin — wall-clock is not summed effort, and figures computed
by different algorithm versions are not comparable. Satisfied in JSON, they
correct nobody.

So the rule, and the check that carries it:

  README.md — a schema, a stored figure, a computed field or an API response
  is not a deliverable by itself. Say which screen shows it, link the issue
  that provides one, or record that it is deliberately not shown.

  forgejo-issue.py — warns when the acceptance check's subject is the data
  (retrievable, returned, reported, recorded) and nothing in it requires a
  person to see the result.

Both tests read the `Verify:` line rather than the body. A first attempt read
the whole body and fired on 44 of 73 real open issues, because bodies say
"page" and "renders" incidentally; a warning at that rate is one nobody reads,
and it would have taken the `Verify:`-not-last warning down with it. Narrowed
to the acceptance line, it fires on 8 of the same 73 — all eight in the batch
that prompted this, none anywhere else.

A warning and not a refusal. API-only is a legitimate decision; it has to be a
decision rather than an omission.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
null 2026-08-22 14:05:11 -05:00
parent c839f1eb72
commit 694b161bb1
2 changed files with 64 additions and 22 deletions

View File

@ -1,14 +1,17 @@
# Project Template # Project Template
A working documentation skeleton, the tracker convention that goes with it, and 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 the scripts that enforce both. Adopt it with
angle-bracket placeholders, delete the paragraphs that do not apply — then `docs/architecture/scripts/scaffold.sh`, which never overwrites a file that is
create the labels and the first milestone in the repository's issue tracker. 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 Status: Current
Owner: _null 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 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 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 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 ### Where the tracker is
**[dream.scheller.ltd](https://dream.scheller.ltd)** — a Forgejo instance, and **A Forgejo instance**, and that it is Forgejo is not a detail. The convention
naming it is not a detail. The convention below is portable: milestones, issues below is portable: milestones, issues and labels exist on GitHub, GitLab and
and labels exist on GitHub, GitLab and Jira, and every rule in it would work Jira, and every rule in it would work there unchanged. **The reporting is not
there unchanged. **The reporting is not portable at all.** Every figure the portable at all.** Every figure the Command Center shows — the headline
Command Center shows — the headline percentage, the milestone count, milestone percentage, the milestone count, milestone coverage, the alert an opened `P0`
coverage, the alert an opened `P0` raises — is read from a Forgejo repository raises — is read from a Forgejo repository mapped to the project. A project that
mapped to the project. A project that follows every rule here in a different follows every rule here in a different tracker satisfies the convention
tracker satisfies the convention completely and appears on privacyllc.dev as completely and appears on privacyllc.dev as having no tracker at all.
having no tracker at all.
Credentials are in **`~/.openclaw/docker-registry.env`**, which carries The instance and its credentials are operator configuration, not repository
`FORGEJO_REGISTRY`, `FORGEJO_REGISTRY_USER` and `FORGEJO_REGISTRY_TOKEN`: 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 ```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 python3 docs/architecture/scripts/forgejo-issue.py list
``` ```
Despite its name that token is not registry-scoped — it works across the whole Whatever that token turns out to reach, treat it as reaching everything until
API, with push and pull. Never print it into a log or a chat. 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 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 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 overrides that deliberately, and `--dry-run` prints the payloads and changes
nothing. 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 ### The rules
- **Milestone = batch.** Name it `Batch 02 — Payments`, and **start it with a - **Milestone = batch.** Name it `Batch 02 — Payments`, and **start it with a
@ -202,6 +205,14 @@ nothing.
inventing one. inventing one.
- **Issue = deliverable.** One per item, in its milestone, ending with a - **Issue = deliverable.** One per item, in its milestone, ending with a
`Verify:` line stating the acceptance check. `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 - **Labels:** `P0` ships broken or loses data · `P1` materially wrong but
shippable · `P2` cosmetic or low impact · `release-blocker` means a release shippable · `P2` cosmetic or low impact · `release-blocker` means a release
built today would be wrong rather than merely incomplete. Exactly these built today would be wrong rather than merely incomplete. Exactly these

View File

@ -214,6 +214,36 @@ def check_verify_line(body: str, title: str) -> str:
return body 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]: def resolve_labels(names, available: dict[str, int]) -> list[int]:
"""Names → ids, failing loudly. A typo'd severity label is reported by the """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 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: allow_dup=False) -> dict | None:
title = spec["title"].strip() title = spec["title"].strip()
body = check_verify_line(spec.get("body", ""), title) body = check_verify_line(spec.get("body", ""), title)
body = check_surface_named(body, title)
dup = find_duplicate(title, existing) dup = find_duplicate(title, existing)
if dup and not allow_dup: if dup and not allow_dup: