Skip to content

docs(adr): ADR-0007 D6 — a null clear of an env-pinned key is not refused (#30385) - #30698

Merged
os-zhuang merged 2 commits into
mainfrom
claude/project-thread-myd9f0
Nov 6, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/project-thread-myd9f0

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread
domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

维护者速读

Refs #30385

Clause-②: no
Metadata-protocol: none

What changes

ADR-0007 D6 said every UI write to an env-pinned key is refused with HTTP 409 locked-by-env. PR #30448 lets exactly one write through the pin: a clear (null / absent), which nulls the stored row beneath the override. The value in force does not move — the resolver still answers source='env', locked=true — and every other write stays 409 (settings-service.ts pre-flight: const clears = patch[key] === null || typeof patch[key] === 'undefined'; if (!clears && this.effectiveEnvOverride(...)) throw new SettingsLockedError(...)).

Per AGENTS.md (a decision change amends the original ADR's status line), and per contract review 6671549520's three should-fixes:

  1. Status line — · **Amended (2026-11-05, #30385)** — D6: a clear (null) of an env-pinned key is not refused; it clears the stored value beneath the override and changes nothing in force.
  2. D6 — the exception sentence (the pin does not refuse a clear; the row locks and the key's write predicates still judge it like any clear), with the superseded wording kept inline as an Amended note.
  3. REST table — PUT /api/settings/:namespace row: (409 if any key locked-by-env, except a null clear).
  4. Architecture sketch (L86) and the service-surface comment (L303) — the same exception, (a null clear excepted — D6), so the ADR does not contradict D6.

Out of scope (known drift, untouched): resetNamespace wording (reset still skips pinned keys; widening it is #30664), D-tier drift, env-var prefixes.

ADR anchor

New scripts/adr-anchors/packages__services__service-settings__src__settings-service.ts.json (ADR-0007), as ruling 6670154789 item 3 names. Its invariant: an env override in force refuses every write but a clear; the clear nulls the stored row and changes nothing in force. settings-service.ts already cites ADR-0007 on main, so pnpm check:adr-anchors is green on main and on this branch.

Verification

At head 27a3e1c9: derived gates for the two files, 29 of 29 exit 0; --ran reconcile 0 NOT-MEASURED. The first head (26da6f27) ran 21 of 21. Contract review PASS 6671549520 on the first head; the delta review covers the fix commit.

No changeset: docs/adr only (skip-changeset).

@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Nov 5, 2026
@github-actions github-actions Bot added size/xs documentation Improvements or additions to documentation labels Nov 5, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

Contract-review-claim: session_01Utp6Cbvcmv8YWFhFJwzjKL · head 26da6f2 · 2026-11-05T22:42Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 26da6f272d44273eba58b22b2d8b1005ddf8c371
Local-runs: none
Scope: docs/adr/0007-settings-manifest-and-kv-store.md + packages/services/service-settings/src/settings-service.ts (setMany pre-flight and write loop, resetNamespace, at PR #30448 head 4153daa426dfe5c6c5413c815e56b97e4bc6de53 and on main), packages/services/service-settings/src/settings-routes.ts (the PUT /:namespace route and its 409 mapping, main), packages/services/service-settings/src/settings-service.types.ts (SettingsLockedError, #30448 head), packages/services/service-settings/src/settings-service.test.ts and packages/plugins/plugin-auth/src/allowed-ip-ranges.pin.test.ts (the #30385 pins, #30448 head), docs/adr/README.md and scripts/adr-index.mjs (the generated index row), scripts/adr-anchors/ (read for an ADR-0007 anchor)

Inputs read: card #30385 body and all 8 comments (triage 6653992606, claim 6654828319, dev report 6657238833, ruling 6670154789, release 6670837769, re-claim 6671137072, ADR dev report 6671431750); PR #30698 body, file list (1 file) and net diff against main at a2437b560bd8872feece6ec8b57c6fdf21cff1f4 (+3/−3); the check-runs on the head; the code the hunks make claims about, read at PR #30448's head (unmerged; it lands after this PR by ruling 6670154789 item 4).

① Derived judgments

  1. Accept-set change the ADR text records — right. D6 as amended: an env-pinned key refuses every write 409 locked-by-env except a clear (null), which sets the stored value beneath the override to null and changes nothing in force. The cited code at 4153daa4 does exactly that: setMany pre-flight const clears = patch[key] === null || typeof patch[key] === 'undefined'; if (!clears && this.effectiveEnvOverride(reg, namespace, key)) throw new SettingsLockedError(namespace, key); — only a null/undefined clear passes the pin; '' and every other value still throw. The write loop then takes the isNull branch (storedValue = null) and calls upsertRow, so the row is nulled, not deleted — the ADR's "sets the stored value … to null" and the status line's "clears the stored value" are the right words (ruling item 1: never "deletes the row"). The answer stays { source: 'env', locked: true } (pinned by settings-service.test.ts "a clear clears the stored value beneath the pin …" and by the end-to-end allowed-ip-ranges.pin.test.ts case: '' → 409 locked-by-env, null → 200 with source: 'env', locked: true, then default after the variable is removed).
  2. REST row — right. settings-routes.ts makes no env-lock check of its own: the PUT route unwraps the body and calls service.setMany; its refuse maps SettingsLockedError to 409 SETTINGS_LOCKED with reason. So "(409 if any key locked-by-env, except a null clear)" is the route's behaviour once fix(service-settings): a clear goes through an env pin, so the allowed-IP lockout recovery survives removing the variable #30448 lands, and null is the exact wire spelling (JSON carries no undefined; an absent key is no write at all).
  3. What the amendment leaves alone — right. resetNamespace is byte-identical between main and 4153daa4 (it still builds its null patch only from global/tenant/user-sourced keys, so a pinned key is skipped); ADR-0007 on main never mentions it, so there is no reset sentence in the ADR to true up, and ruling 6670154789 item 2 sends that half to Settings reset: an env-pinned key keeps its stored value beneath the pin, while the reset says the namespace is back on environment/default configuration #30664. The ADR's silence on reset matches letter A. The badge sentence ("never silently overwritten") still holds: the pinned field reads locked before and after a clear.
  4. Status line — parses. The line still starts **Status**: with Accepted as its first keyword, which is what scripts/adr-index.mjs reads (STATUS_LINE), so the generated index keeps the record under Accepted. The index cell is cut at 64 characters after the keyword, so the docs/adr/README.md row is unchanged by this amendment — no stale index to regenerate. Spelling nit, not a finding: the house pattern in ADR-0005 and ADR-0087 is · **Amended** (date, #card — …) (bold on the word only); this PR bolds **Amended (2026-11-05, #30385)**. Both parse; harmonise if touched again.
  5. Public surface published by this PR — none. One docs file; no package, schema, export or error code moves here. The behaviour change itself is fix(service-settings): a clear goes through an env pin, so the allowed-IP lockout recovery survives removing the variable #30448's and is declared there (see ②).
  6. Gate verdicts on the head (read 2026-11-05T22:46Z): green — Governed Surface Queue Guard, Check Changeset, Check PR Size, PR-body guards, Check Documentation Links, Lint · docs & code guards (the ADR gates' family), Lint · repo & tooling gates, Lint · PM dispatch-gates, Lint · ESLint, Lint · CI/release/engine gates, Lint · scoped ×3, Type Check · source gates and debt ledger, Test UI, Dogfood Regression Gate, Auto Label, filter; no failure. Still in_progress at the time of this record: Test Core (1/8), Type Check · consumer gates, Type Check · workspace. None of the three reads docs/adr/**; this is a Tier H PR, so the landing is the maintainer's hand on a fully green head either way — the owning seat confirms the three before asking for the merge.
  7. Order of landing — right. PR docs(adr): ADR-0007 D6 — a null clear of an env-pinned key is not refused (#30385) #30698 is draft, Tier H (docs/adr/**), reviewers per ruling os-zhuang and hotlong — not yet requested on the PR at this head (requested_reviewers empty); that is the seat's next stroke, not a defect of the diff. fix(service-settings): a clear goes through an env pin, so the allowed-IP lockout recovery survives removing the variable #30448 waits for this merge per ruling item 4.

② Semver level

Right. Docs-only diff under docs/adr/; skip-changeset label present and Check Changeset green; PR body Clause-②: no and Metadata-protocol: none are correct for a PR that publishes nothing. The widening the ADR now records (a clear through an env pin on set / setMany / PUT) is carried by #30448's own changeset .changeset/30385-clear-env-locked-setting.md (@objectstack/service-settings patch, @objectstack/plugin-auth patch, Clause-②: yes (widening)), which is where it belongs; the ADR PR owes no changeset of its own. Clause-②: no — holds.

③ Boundary flags

④ Design judgment

File: docs/adr/0007-settings-manifest-and-kv-store.md (the only hits path), judged against its first consumer (settings-service.ts / settings-routes.ts at #30448 head) and the execution plan (ruling 6670154789 items 1–4, order ADR first then #30448).

The three edits say what the code does, in the words the ruling prescribes, and nothing the code does not do: only null passes the pin on the wire, the row is nulled rather than deleted, the resolver still answers env/locked, every other write (empty string included) stays 409, reset is untouched. I tried to break it on four axes:

  • Could a reader take the ADR as permitting a clear that the code refuses? Yes, on one reading — see should-fix 2. Not a contradiction of behaviour, a precision gap.
  • Could a reader take the ADR as still forbidding what the code now allows? Yes, in two sentences the amendment did not reach — should-fix 1.
  • Does the amendment move the value in force, the badge, or the audit row? No; D6's "changes nothing in force" is exactly the pinned test's assertion, the badge rule (render rule 3, locked === true → read-only + badge) is unchanged, and the write loop still audits the nulled row like any clear.
  • Does it widen resetNamespace by implication? No; the ADR has no reset text, the code is unchanged, and the ruling parks it on Settings reset: an env-pinned key keeps its stored value beneath the pin, while the reset says the namespace is back on environment/default configuration #30664.

Must-fix: none.

Should-fix 1 — the ADR still states the old rule twice, unqualified. The architecture sketch (line 86, "- reject if effective source='env' (env wins, UI shows locked)") and the service-surface comment (line 303, "// Write (rejected if effective source='env')") were not amended. PD #13's reader greps the ADR for the surface and may land on either line and read "every write refused" with no exception; D6 governs, but an ADR that contradicts itself three sentences apart is the drift this amendment exists to close. The ruling's item 3 enumerated three edits, so this is a should-fix, not a breach: add "(a null clear excepted — D6)" to both, in this PR or the next touch.

Should-fix 2 — D6's "is not refused" is broader than the code. The clear is not refused by the env pin; the Phase 2 row locks and the write predicates still judge it, and the pins in settings-service.test.ts show a clear refused locked-by-global under an upper-rung locked row and a non-platform clear refused at the global rung. Suggested wording: "A clear (null) is the one exception: the pin does not refuse it — the row locks and the key's write predicates still judge it like any clear — it sets the stored value beneath the override to null and changes nothing in force …". One clause; keeps the ADR from being read as "null always goes through".

Should-fix 3 — the anchor the ruling named. Ruling 6670154789 item 3 ordered the amendment "with its scripts/adr-anchors/ anchor file"; none exists for packages/services/service-settings/src/settings-service.ts and neither PR adds one. The file already cites ADR-0007 on main (format rows) and gains the ADR-0007 D6 citation at the pre-flight in #30448, so a one-file anchor (packages__services__service-settings__src__settings-service.ts.json, invariant: an env override in force refuses every write but a null/undefined clear; the clear nulls the stored row and changes nothing in force) is green on either tree. It belongs with #30448 (the commit that writes the D6 comment) or here; the seat picks, before the card lands.

Implemented-by: claude/project-thread-myd9f0
Reviewed-by: session_01Utp6Cbvcmv8YWFhFJwzjKL

VERDICT: PASS

@github-actions github-actions Bot added size/s and removed size/xs labels Nov 5, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

Contract review

Delta-of: 6671549520
Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 27a3e1c9adb9a566da39ea25321f928b734d33fd
Local-runs: none
Scope: docs/adr/0007-settings-manifest-and-kv-store.md + scripts/adr-anchors/packages__services__service-settings__src__settings-service.ts.json (new in this delta), scripts/adr-anchors/README.md and scripts/check-adr-anchors.mjs (the shape and the gate the new anchor is judged against), scripts/adr-anchors.mjs (shardNameFor), packages/services/service-settings/src/settings-service.ts (ADR-0007 citations on main at 4bf9a2d7eeeec7c07971aba1a8734c227acaaa3a; setMany pre-flight, write loop and upsertRow at PR #30448 head 4153daa426dfe5c6c5413c815e56b97e4bc6de53), packages/services/service-settings/src/settings-service.test.ts (the #30385 pins, #30448 head)

Delta review: one commit, 26da6f27 → 27a3e1c9, 2 files, +10/−3 — three hunks in the ADR and one new anchor file. Everything 6671549520 judged on the first head stands unless named below; this record judges the delta only.

Inputs read: comment 6671549520 in full; the PR body and comment list at the new head; git diff 26da6f27..27a3e1c9; the new commit's message and trailers; PR #30448's file list against main (10 files, no docs/adr/**, no scripts/adr-anchors/**); the check-runs on 27a3e1c9.

① Derived judgments

  1. Should-fix 1 (the two unqualified sketch lines) — resolved. Line 86 (- reject if effective source='env' (env wins, UI shows locked) (a null clear excepted — D6)) and line 303 (// Write (rejected if effective source='env'; a null clear excepted — D6)) both now carry the exception and point at D6. The ADR no longer states the pre-amendment rule anywhere unqualified: the status line, D6, the REST-table row, the sketch and the service-surface comment all agree.
  2. Should-fix 2 (D6 broader than the code) — resolved, in the suggested words. D6 now reads "the pin does not refuse it — the row locks and the key's write predicates still judge it like any clear — it sets the stored value beneath the override to null and changes nothing in force". That is the code's order at 4153daa4: the env check is the only judgment a clear skips (if (!clears && this.effectiveEnvOverride(...)) throw), the Phase 2 lockThatRefuses walk runs on every key after it (locked-by-${own.scope} / locked-by-${lock.scope}), and refuseByWritePredicates judges the whole patch with submitted ?? null as next. The settings-service.test.ts pins (locked-by-global under an upper-rung lock at a clear; the non-platform clear refused at the global rung) are what the clause now describes.
  3. Should-fix 3 (the anchor) — resolved, and the file is well-formed. scripts/adr-anchors/packages__services__service-settings__src__settings-service.ts.json is exactly shardNameFor('packages/services/service-settings/src/settings-service.ts') (/ → __, extension kept, .json appended), so the gate's filename-derivation check holds. It carries the three and only three keys the README and check-adr-anchors.mjs require (file, adrs non-empty array, invariant non-empty string), 2-space indent and multi-line array like its siblings, trailing newline present, parses as JSON. ADR-0007 names one live record (not a tombstone, not on KNOWN_NUMBER_COLLISIONS). No other file is touched — no index to register in, as the README's first rule demands.
  4. The anchor's presence check is green on both trees. settings-service.ts on main (4bf9a2d7) cites ADR-0007 six times (the format-bearing specifier rows, lines 640–721, 2951, 3294) and at 4153daa4 eight times (those six plus the setMany docblock and the pre-flight comment, ADR-0007 D6). The gate asks only that the id appear in the file, so it passes on main today, on this branch, and after fix(service-settings): a clear goes through an env pin, so the allowed-IP lockout recovery survives removing the variable #30448 lands. The ruling's ordering (this PR first, fix(service-settings): a clear goes through an env pin, so the allowed-IP lockout recovery survives removing the variable #30448 after) is unaffected: fix(service-settings): a clear goes through an env pin, so the allowed-IP lockout recovery survives removing the variable #30448 does not add an anchor at this path (no add/add conflict) and does not touch docs/adr/**.
  5. The invariant is true of 4153daa4. Each clause checked against the code: "the resolver answers it with source='env', locked=true" — effectiveEnvOverride and the pinned test's assertion; "the setMany pre-flight refuses every write to that key with SettingsLockedError (HTTP 409 locked-by-env) except a clear (null / undefined)" — const clears = patch[key] === null || typeof patch[key] === 'undefined', the default reason of SettingsLockedError is locked-by-env, the route maps it to 409; "the pin does not refuse a clear; the row locks and the key's write predicates still judge it like any clear" — item 2 above; "a clear nulls the stored row beneath the override (never deletes it)" — the write loop takes isNull with storedValue = null and calls upsertRow, whose engine branch is find → update or insert, never a delete (the only deletes in the file belong to the rotated-secret reaper); "changes nothing in force" — the pinned test's "the answer still reads env, locked". On main the pre-flight still refuses a clear, so the invariant describes the state the ruling orders fix(service-settings): a clear goes through an env pin, so the allowed-IP lockout recovery survives removing the variable #30448 to produce — the same relation the ADR text itself now has to main, and the gate reads only the citation, so nothing is red in the interval.
  6. Gate verdicts on the head (all 47 check-runs completed by 2026-11-05T23:10Z; none failed): success — Lint & Repo Gates, TypeScript Type Check, Test Core (and shards 1/8–3/8), Governed Surface Queue Guard, Test UI, Dogfood Regression Gate, Check Documentation Links, Check PR Size, PR-body guards, Lint · repo & tooling gates (the job that runs pnpm check:adr-anchors, so the new anchor passed the gate's own filename, id and presence checks in CI), Lint · docs & code guards, Lint · CI/release/engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · scoped ×3, Type Check · source gates / debt ledger / consumer gates / workspace, Plan Test Core shards, Auto Label, filter. Skipped by path filter or label: the UI/Build/Docs/Dogfood-shard family, Check Changeset (skip-changeset), opt-in harnesses. Of the ten queue-required contexts, the five this PR's paths trigger are green and the rest are path-skipped. The three runs 6671549520 left in_progress on the first head are green here.
  7. The commit. Message names the three fixes; trailers are the model-free pair (a generic Co-Authored-By with the noreply address, and Claude-Session: …session_01Utp6CaQzHSoRvseiSpfmLn). No model identifier in the diff, the body or the anchor. PR body updated at this head (the "## ADR anchor" section replaces the first head's "none added"; verification line names 27a3e1c9). Still draft, Tier H, reviewers os-zhuang and hotlong not yet requested — the seat's next stroke, unchanged from 6671549520 item 7.
  8. Nothing else moved. The status line is byte-identical to the first head, so docs/adr/README.md's generated row is unchanged and check:adr-index has nothing to regenerate. resetNamespace is still unmentioned in the ADR and untouched in code (parked on Settings reset: an env-pinned key keeps its stored value beneath the pin, while the reset says the namespace is back on environment/default configuration #30664). The REST-table row from the first head stands. The #30385 in the anchor's invariant follows ten sibling anchors that cite a card the same way; scripts/adr-anchors/** is outside check:doc-authoring's roots (.claude, docs, skills, content).

② Semver level

Unchanged from 6671549520. The delta adds one JSON file under scripts/ and three ADR lines; no package, schema, export or error code moves. skip-changeset still correct (Check Changeset skipped by label at this head); Clause-②: no, Metadata-protocol: none hold. The widening remains declared on #30448's changeset.

③ Boundary flags

④ Design judgment

File: docs/adr/0007-settings-manifest-and-kv-store.md (hits), with its new anchor, judged against the first consumer (settings-service.ts at 4153daa4) and the execution plan (ruling 6670154789 items 1–4).

The three ADR hunks close the two precision gaps 6671549520 named without opening a new one: no sentence in the record now says more than the code does (the clear is judged by locks and predicates) or less (the sketch and the interface comment carry the exception). The anchor earns its failure mode in the README's sense — a reader of the pre-flight alone could "fix" !clears && back to an unconditional refusal and be reverting a decision — and its invariant teaches that decision rather than merely naming the id. I tried to break the anchor on three axes: wrong filename (no — matches shardNameFor), an id the gate refuses (no — live, uncollided, cited in the file on both trees), an invariant that overstates (no — every clause has a line of code or a pin behind it at 4153daa4).

Must-fix: none.

Should-fix 1 — give ADR-0007's principles a machine-readable decision index on a later touch. Because the record declares no Dk letters, ADR-0007 D6 resolves only by convention; the anchor gate prints it as unverifiable on every run. A ### D6 — Env override is first-class heading per principle (grammar 1), or a decision table (grammar 3), would let the letter resolve and would make the fifth check real for this file. Governed surface, so a deliberate amendment — not this PR's; the ruling named three edits and an anchor, all delivered.

Implemented-by: claude/project-thread-myd9f0
Reviewed-by: session_01Utp6Cbvcmv8YWFhFJwzjKL

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

维护者速读

席位核对 — ACCEPT · domain:server#2 · head 27a3e1c9adb9a566da39ea25321f928b734d33fd

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 23:13
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 6, 2026
Merged via the queue into main with commit da9b5f7 Nov 6, 2026
48 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-myd9f0 branch November 6, 2026 01:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants