Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions .abcd/development/brief/04-surfaces/22-site.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ copied from abcd's own; a workflow that renders the site from each published
release with abcd's checksum- and attestation-verified binary and deploys the
rendered archive from a second job; and the provider's host configuration. The
composition and the static inputs are the repository's own once they exist, so
a later run keeps them as they are. The workflow and the host configuration are
a later run keeps them as they are, with one exception
([adr-2609301720596683](../../decisions/adrs/2609301720596683-abcd-adds-a-missing-site-label-to-an-existing-ui-json-and.md)):
an interface-string file that lacks a label the allowlist declares, because it
was written before the label existed, gains that label with abcd's own words
for it, and the run names each added label. Nothing the file already says is
rewritten. The workflow and the host configuration are
abcd's: a copy that differs refuses the whole run, with nothing written and
no remote change attempted, unless the run is told to replace it.

Expand Down Expand Up @@ -133,6 +138,15 @@ block carries a `data-src` attribute naming the file and heading it came from, s
each block names its own source in the markup. And the interface-string file is
decoded against a closed struct with unknown fields refused, so a word added there
which no field reads fails the build rather than reaching a reader unreviewed.
A label the struct declares and the file leaves blank fails the build by name. A
label the file does not carry at all, which is how a file written before that
label existed reads, is added to the file by the build and by setting up, with
the words abcd's own interface-string file gives it: each added label is named
on standard error, every byte already in the file stays, and a file carrying a
key no field reads is left untouched and refused as before. The render the
site gate makes of an empty output directory writes only inside that directory,
so it never completes the file and refuses an incomplete one by name
([adr-2609301720596683](../../decisions/adrs/2609301720596683-abcd-adds-a-missing-site-label-to-an-existing-ui-json-and.md)).

Every picture is a committed asset under `docs/assets/img/`, referenced from a
docs page like any other image. SVGs are inlined so their colours follow the
Expand Down Expand Up @@ -176,7 +190,9 @@ behind the contributors page;
and `docs/` with its committed assets. It writes the landing page, the record explorer, the machine-readable
record export, the install script from its committed template, the redirect and
header maps, the stylesheets and scripts, every referenced raster, and its own
build marker. Nothing else, nowhere else.
build marker. The one write outside the output directory is the missing-label
completion of the interface-string file above, made only when a declared label
is absent. Nothing else, nowhere else.

One input reaches past the durable record into the working tier, and it is off
unless a repository asks for it. The composition declaration carries an
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
---
id: adr-2609301720596683
slug: abcd-adds-a-missing-site-label-to-an-existing-ui-json-and
status: accepted
date: 2026-09-30
supersedes: null
superseded_by: null
refines: [adr-47]
related_intents: [itd-2609212103568351, itd-2609212103572513]
related_rfcs: []
related_adrs: [adr-47]
---

# ADR-2609301720596683: abcd adds a missing site label to an existing ui.json and never rewrites one

Typed links: `refines` [adr-47](0047-abcdev-app-rendered-from-this-repository-alone.md)
(decision 2's closed allowlist is untouched: this adds a declared label to the
file, and adds no fallback at render time).

## Context

`site-src/ui.json` is the closed allowlist of interface strings adr-47
decision 2 permits the site generator to add. `LoadUI` decodes it with unknown
keys refused and refuses a declared label the file leaves empty or absent,
naming it (`no text for status.target`), so a blank button never reads as a
rendering fault. `abcd site setup` seeds the file once and never rewrites it,
under the rule its chapter states: a file the repository owns once it exists
is kept.

Two intents of this release cycle each declared new required labels:
itd-2609212103568351 the six `status.*` labels of the Now / Next / Later
block, and itd-2609212103572513 `status.target`. Both carry
`impact: additive`, but a managed repository whose `ui.json` predates them
would see a green `site build` refuse on upgrade until it added the lines by
hand, which is breaking for that surface. The review of the second intent's
lane raised it as a ruling owed before the v0.12.0 cut.

## Decision

The product thinker ruled on 2026-09-30 (ruling TG1), verbatim as relayed:
"(b) ABCD ADDS THE MISSING LABELS: on the next site setup or site build, abcd
adds only the missing required labels (with the default words); the
project's own wording elsewhere in ui.json is never changed. No failure, no
manual step; both intents stay impact: additive."

We therefore make one exception to "a file the repository owns once it exists
is kept": `abcd site setup` and `abcd site build` add to an existing
`ui.json` each label the allowlist declares and the file does not carry, with
the words abcd's own bundled `ui.json` gives it, and name each added label on
stderr, one line per label. Nothing else in the file changes:

- A label the file carries keeps its wording byte for byte. A label declared
blank is the project's declaration, so it is not rewritten and `LoadUI`
still refuses it by name.
- The added members go at the end of their block, in the block's own
indentation and line style; a whole declared block the file lacks is added
with every label in it. Every byte already in the file stays where it was.
- A file that does not decode against the allowlist, an unknown key included,
is not touched: the closed allowlist refuses it exactly as before, and
adding applies to declared keys only. The `forge_names` map is not required,
so nothing is added to it, and `_purpose` is never rendered, so it is never
added.
- The file is read as the site's other reads are, so a symlinked or
non-regular `ui.json` is refused, and it is written atomically through the
canonical writer, keeping its mode.

## Alternatives Considered

- **(a) A breaking impact.** Keep the refusal and declare both intents
`breaking`, so the cut derives a major-shaped version and every adopter adds
the lines by hand. Rejected by the ruling: a manual step for words abcd
already knows.
- **(b) abcd adds the missing labels.** Chosen: no failure and no manual step,
both intents stay additive, and the project's wording is never touched.
- **A fallback at render time.** Render a missing label from the bundled
words without writing the file. Rejected: adr-47 decision 2 keeps the
allowlist closed and the file the one place the site's added words live; a
silent fallback would render words the repository's file does not hold.

## Consequences

- An older `ui.json` keeps building across a release that declares a new
label, and the change it takes is visible: the verb says which labels it
added, and the file is left modified in the working tree for the person to
commit (`site setup` names it in its commit step).
- `site build` writes one file outside its output directory, the repository's
`ui.json`, and only when a declared label is absent.
- A future label is additive for adopters by construction, provided the
bundled `ui.json` declares its words; a test holds the bundled file to
declaring every label.
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ Fidelity review OWED (receipt rcp-4d55b6f29ab8).

Changed on 2026-09-29 by the product thinker's rulings BV1 and BV2 of that day (DECISIONS.md entry landing with lane recRulings), recorded as iss-2609292011569133: the text board gives Later as a count alone while `--json` and the site's Status page keep its rows (criterion 1), and an intent in a lane is listed under Now only, never also under Next or Later, so without the state file that intent returns to the list the gate places it in (criteria 1 and 3). The criterion text above stands as shipped; adr-2609292012006845 supersedes adr-2609212115255771 and restates decision 2.

Changed on 2026-09-30 by the product thinker's ruling TG1 of that day, recorded as adr-2609301720596683: a managed repository's `site-src/ui.json` written before this intent's `status.*` labels existed keeps building, because `abcd site setup` and `abcd site build` add each declared label the file lacks with abcd's default words, name it on stderr, and change nothing else in the file. The impact stays `additive`.

## Grounds

- pursued: phases are retired today and the record needs a place a person looks to see what is next; we expect the computed block to be read where the phase documents were not; shown wrong if Now is found naming an intent neither in a lane nor next
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ _None open._
Fidelity review OWED (receipt rcp-47e25ab4498e).
<!-- abcd-review-end receipt=rcp-47e25ab4498e -->

Changed on 2026-09-30 by the product thinker's ruling TG1 of that day, recorded as adr-2609301720596683: a managed repository's `site-src/ui.json` written before this intent's `status.target` label existed keeps building, because `abcd site setup` and `abcd site build` add each declared label the file lacks with abcd's default words, name it on stderr, and change nothing else in the file. The impact stays `additive`.

## Grounds

- pursued: milestones are retired today and this is the only place must-land-by survives; we expect the cut's report to be read and the moved target to be acted on; shown wrong if targets are never set or carried past two cuts unremarked
1 change: 1 addition & 0 deletions .abcd/work/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2622,3 +2622,4 @@ together (the script's header says why there is no escape hatch).
- 2026-09-30 — The drain reads the drained repository's own eligibility record, which may loosen abcd's floors loudly, and it hands back every record still waiting on a person (the product thinker's rulings BX2 and H11 of 2026-09-29, applied by lane drainOwnRule of autonomous run A; partial of itd-82, whose spec stays open for the host judgement, the lane, the hand-back writes and the pace). BX2, verbatim: "the PROJECT MUST HOLD the eligibility decision in its own record (e.g. added at setup); drain refuses there until it does." H11, verbatim: "MAY LOOSEN abcd's floors (a project may let drain take major/critical and security issues). NOTE for the lane: make a loosened floor loud (drain --dry-run and the drain start name every floor the project loosened), and keep abcd's own repository at the stricter default." As built: the record is the one accepted decision record in the repository's `.abcd/development/decisions/adrs/` whose frontmatter carries `drain_categories` (an inline list, a subset of the fixable set), `drain_severities` (an inline list of severities), `drain_security` (`handback` or `take`) and `drain_remedy` (`required`, its only value, since the remedy is the brief a lane works from); abcd's own adr-2609291342092738 carries the strict baseline, which the binary also bundles as the measure a loosening is named against, and a test fails if abcd's record loosens anything. A repository without such a record, with one that is only proposed or superseded, with two accepted, or with one that misses, misspells, repeats or mis-values a field, is refused by `drain --dry-run` and bare `drain` alike, exit 2 and nothing written, never falling back to the baseline or a looser rule; widening the categories past the fixable set is refused as a decision by kind, which H11 does not name. Every loosened floor (`severity major`, `severity critical`, `security`) is named in the dry run's text, on stderr in both output modes, in `--json` as `loosened`, and in the start's refusal. `ahoy install` offers the baseline as an accepted record, written through the decision store's mint only on an answered yes; `--yes` skips it and reports `drain_rule.offered` under `optional_skipped`, as the routing offers are. The gap the remedy lanes found (50 of 54 dry-run-eligible records waiting on a ruling) is closed by BOTH hand-backs, each its own rule: a remedy opening "Waits on" (compared case-folded) is handed back as `waits-on-ruling`, because taking it would make the ruling the remedy waits on; and a record whose `deferred_after` names the current anchor tag is handed back as `deferred`, because a person carried it past this release and the waiver is that person's decision for the cycle. Both hold whatever the repository's record says. `capture defer` writes a deferral only onto a `major` or `critical` record, which H11 now lets a record take, but this ledger also carries hand-written deferrals on minor records (62 of the 231 open records on this branch are handed back as `deferred`), and the rule holds them back the same way. They are asked after the category and severity hand-backs, whose fix a ruling or a lapse would not change, and the ruling before the deferral, because it names which decision is owed; a record carrying both waits on both. The release tags are read only when an open record carries a deferral, and a failure to read them refuses the plan rather than letting a live deferral through. The threat is stated in the drain brief chapter: the record is a repository-authored file deciding what an unattended agent may do, so a contributor's pull request can loosen it; what guards it is that the record is committed history reviewed like code, a loosening is loud on every run, abcd's own repository keeps the baseline under a test, and the store is read inside the checkout so a symlink leaving it is refused.
- 2026-09-30 — Correcting three points of the entry above after its review (lane fix-drainOwnRule of autonomous run A). The drain-rule offer of `ahoy install` is asked only of a person at a terminal, the itd-131 precedent the git identity question set, rather than behind a named opt-in flag: off a terminal neither its category question nor the offer is asked, so a piped answer stream keeps the order it had before the offer existed and a scripted yes never writes the record, and the run reports `drain_rule.offered` under `optional_skipped` naming the terminal as the way to be asked. The terminal gate was chosen over a `--drain-rule` flag because the record decides what an unattended agent may do, which a scripted answer is not a person's yes to, and a flag would hide the offer from the person at a terminal it is for. A checkout holding no release tag (a shallow clone fetches none) marks the anchor unknown rather than reading every deferral as lapsed: every record carrying a deferral is handed back as `deferred`, naming the missing tags and `git fetch --tags`, which keeps the rest of the dry run readable where refusing the whole plan would not. The rule's reader refuses, as malformed, a record that states any frontmatter key twice (not only a `drain_` key) and one whose frontmatter `id` disagrees with its file name, and reads each record through the capped trust-boundary reader, so a record that is a symlink or past the size cap refuses; every refusal of the rule exits 2 on the dry run as on the bare verb.
- 2026-09-30 — Six entries above appear twice, verbatim: the five dated 2026-09-29 from "Two itd-111 follow-ups from its fidelity audit" to "Ruling J13", and the 2026-09-30 entry beginning "The 2026-09-29 itd111Follow entry above". Two histories carried them in opposite order relative to the 2026-09-30 BU1/BT1 entry (main below them, the implement-loop lanes above them), so joining them in integration 24b-3 kept main's order and repeated the six after BU1 in the lanes' order, the one merge result the append-only gate admits (every parent's lines kept in their order, DA002; no line beyond what the merge base held plus what each side added, DA003). Each pair is one decision recorded once: the first copy is the record, and the second repeats it (recorded by the integration lane of autonomous run A).
- 2026-09-30 — An older site interface-string file keeps building: `abcd site setup` and `abcd site build` add to `site-src/ui.json` each label the allowlist declares and the file does not carry, with abcd's default words, name each on stderr, and change nothing else in it (the product thinker's ruling TG1 of 2026-09-30, relayed verbatim: "(b) ABCD ADDS THE MISSING LABELS: on the next site setup or site build, abcd adds only the missing required labels (with the default words); the project's own wording elsewhere in ui.json is never changed. No failure, no manual step; both intents stay impact: additive."). It is the one exception to "a file the repository owns once it exists is kept", recorded as adr-2609301720596683, which refines adr-47 and leaves decision 2's closed allowlist untouched: a blank declared label and an unknown key are still refused, and the site gate's own render never completes the file. itd-2609212103568351 and itd-2609212103572513 keep `impact: additive` (lane tgLabels of autonomous run A).
20 changes: 16 additions & 4 deletions commands/site.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,16 @@ the root or in `docs/` — and `CITATION.cff` for the footer),
`.claude-plugin/plugin.json` (the forge URL, licence and author the links and
footer use) — and writes the landing page, the record
export, the redirect and header maps, the stylesheet, the two scripts, the
`install.sh`, and every referenced raster into the output directory, and
nowhere else. It reaches no network. The default output directory is `site`,
`install.sh`, and every referenced raster into the output directory. The one
write outside it is `site-src/ui.json` itself, and only when the file lacks a
label abcd declares (a file written before that label existed): the build adds
each such label with abcd's default words, prints one stderr line per label
(`abcd site build: added the missing label status.target to site-src/ui.json
with its default words`), lists them in `added_labels`, and changes nothing
else in the file. A label the file carries keeps its wording, a blank one is
still refused by name, and a key abcd does not declare is still refused. The
render `abcd lint site` makes of an empty output directory never completes the
file, so the gate refuses an incomplete one by name. It reaches no network. The default output directory is `site`,
which the repository does not track.

The last two are declared deviations from the generic input contract: a repo
Expand Down Expand Up @@ -95,15 +103,19 @@ the fix is an edit to the page.
```

sets up the site of a repository abcd manages, in three stages, and emits
`{ "status": …, "files": […], "environments": […], "host": {…}, "remaining": […], "notes": […] }`:
`{ "status": …, "files": […], "environments": […], "host": {…}, "remaining": […], "notes": […] }`
(with `added_labels` and `labels_file` when a label was added):

- `files` — the repository half, each `written`, `current`, `kept` or
`refused`: `.abcd/site.json` (derived from the identity block and
`docs/README.md`), the static inputs under `site-src/`, the workflow
`.github/workflows/site.yml` (render on each published release with abcd's
verified binary, deploy from the rendered archive) and `wrangler.jsonc`. The
composition and the static inputs are the repository's own once they exist
and are `kept`; a workflow or host configuration that differs from what setup
and are `kept`, with one exception: a `site-src/ui.json` lacking a label abcd
declares gains it with abcd's default words and is `written`, each added
label named on stderr and in `added_labels`, and nothing it already says is
rewritten; a workflow or host configuration that differs from what setup
writes is `refused`, the whole run writes nothing, and `--confirm` replaces it.
- `environments` — the forge's `site-render` and `site` deployment
environments, each admitting only the default branch and tags `v*`, created
Expand Down
Loading
Loading