Skip to content

fix(service-settings): refuse a settings write issued before the engine is bound - #10251

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-10159-settings-write-before-bind
Aug 21, 2026
Merged

os-warren merged 3 commits into
mainfrom
claude/issue-10159-settings-write-before-bind

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes #10159

SettingsService.upsertRow picks its store on if (this.engine), and the engine is bound in exactly one place: SettingsServicePlugin registers a kernel:ready hook from its start() and calls bindEngine inside it. Hooks fire in registration order (hooks.get(name).push(...) in packages/core/src/kernel-base.ts, dispatched in array order) and every plugin's init() runs before any plugin's start() — so every kernel:ready hook registered from an init() runs inside that window. A set() from there landed in the in-process memory fallback, re-resolved off that same array, and handed the caller a fully resolved value while sys_setting received nothing and both audit ledgers stayed silent. Nothing was logged at any level, because the write did not fail: it succeeded against the wrong store.

Premise re-established on this branch before anything changed

A real LiteKernel, a real ObjectQL over the real SysSetting/SysSecret schemas, the real SettingsServicePlugin, and a probe plugin registering its kernel:ready hook from init():

                            BEFORE (origin/main)                    AFTER
serviceResolvableAtReady  : true                                  true
engineBoundAtReady        : false                                 false
writeAtReady              : resolved:"written-at-kernel-ready"    threw:SETTINGS_ENGINE_NOT_BOUND:503
readAtReady               : resolved:"written-at-kernel-ready"    resolved:"never"   (manifest default)
rowsInDbAfterBoot         : []                                    []
engineBoundAfterBoot      : true                                  true
rowsInDbAfterControlWrite : [ { receipt_probe, last_run, … } ]    [ { receipt_probe, last_run, … } ]
auditRowsAfterControlWrite: 1                                     1

One column is sourced differently and is flagged rather than blended in: readAtReady was not a field on the original pre-fix probe (it was added when the probe became the shipped test), so its BEFORE value is the ablation reading — the guard neutered on the fixed tree, which restores this code path to origin/main's behaviour. Every other BEFORE cell was measured directly on origin/main before anything changed.

The control write moments later lands a real sys_setting row and a real sys_setting_audit row on the same connection, so the store existed throughout — the timing is the whole cause. The named population is real and static: assembleMetadataProtocol registers the three platform migrations' kernel:ready hook from ObjectQLPlugin.init() (packages/objectql/src/plugin.ts, init = async at 303, well before start at 385 → packages/metadata-protocol/src/plugin.ts:306).

Loud refusal, not buffered replay — argued from measured consequences

Who is in the window today. Swept the repo for settings writers: zero .set( / .setMany( call sites against the settings service outside packages/services/service-settings itself, and the two inside it are the HTTP PUT handler (settings-routes.ts:137, which cannot run before kernel:listening) and setMany's own internal delegate. Counter-check on the same corpus with the same regex family: .get( / .getNamespace( returns ten shipped call sites, so the sweep works and the zero is a measurement rather than a broken pattern. No shipped startup sequence becomes an error.

Why a buffer cannot keep the promise the resolved value makes:

  • Pre-flight is evaluated against the wrong store. setMany's env-lock and upper-scope-lock checks read through loadRows, which in the window reads memory — an in-window write is validated against a store that does not contain the persisted locks. Replaying it would commit a write a real pre-flight would have refused with SETTINGS_LOCKED. That is a correctness failure, not a bookkeeping one.
  • Encrypted specifiers cannot be buffered safely. cryptoProvider and secretStore arrive on the same bindEngine call, so a buffer would have to hold plaintext in process memory until bind. In-window encrypted writes already refuse today (the plugin's default crypto is NoopCryptoAdapter, providesConfidentiality returns false, assertEncryptionAvailable throws) — buffering would make the write door less uniform, in the direction SETTINGS_CRYPTO_UNAVAILABLE exists to prevent.
  • A replay has nobody left to report to. The caller was told "resolved" during boot; a replay failing at bind time re-creates the silent loss this card is about, one phase later.

What the refused caller does instead is a shipped, documented phase, named in the error message: kernel:bootstrapped, which plugin-lifecycle-events.ts already describes as the "all synchronous bootstrap has settled" anchor for exactly this class of work.

Clause-② determination: no — non-window callers observe nothing new

Decided before the fix was written, and pinned by tests rather than asserted. The refusal is armed only by the new opt-in SettingsServiceOptions.engineBindPending, which only SettingsServicePlugin sets (in init()) and which both branches of its kernel:ready hook clear — bindEngine when objectql resolves, the new SettingsService.settleWithoutEngine() when it does not. So the guard covers a declared, pending bind and nothing else:

population before after
write inside the window resolves, nothing persists SETTINGS_ENGINE_NOT_BOUND / 503
write after bind persists to sys_setting + audit unchanged
directly constructed SettingsService (unit test / bootstrap / control-plane mock) resolves into memory unchanged — declares no pending bind, guard never arms
lean kernel with no objectql resolves into memory unchanged after its kernel:ready hook settles the question (now with a warn that those values are lost on restart)
reads, in every state resolve unchanged

One surface addition is unavoidable for any refusal-shaped fix and is called out rather than buried: SETTINGS_ENGINE_NOT_BOUND is registered in ERROR_CODE_LEDGER (packages/spec/src/api/error-code-ledger.zod.ts) per ADR-0112. That widens the ErrorCode union by one member; it changes no existing envelope and no existing caller's accept/reject behaviour. The status (503, not SETTINGS_CRYPTO_UNAVAILABLE's 500 — this one is temporal, the identical write succeeds one phase later) is declared on the error class rather than at a sendError site because no HTTP door can reach it: the window closes at kernel:ready and sockets open at kernel:listening, strictly after.

Tests

packages/services/service-settings/src/settings-engine-bind-window.test.ts, six cases, driving a real kernel boot. Cases 4–6 are the Clause-② pins above.

The silent-loss direction is pinned explicitly. On the behaviour this replaces, sys_setting was also empty after the in-window write — so a case asserting only "no row landed" would have passed against the defect and tested nothing. The load-bearing assertion is the refusal: the probe records the write's outcome as a string, resolved:… on the old behaviour and threw:SETTINGS_ENGINE_NOT_BOUND:503 on the new one.

Ablation. Predicted signature stated first: neutering assertEngineBound to an unconditional return restores the silent accept, so the two in-window write cases and the standalone bindEngine case go red. Observed 4 red, not the predicted 3 — the extra one is real and is reported rather than smoothed over: case 2 (reads in the window are NOT gated) also reddened, with expected 'resolved:"written-at-kernel-ready"' to be 'resolved:"never"', because under the ablation the in-window write lands in memory and the in-window read then reads the phantom value straight back out. That is a second silent-loss witness, and case 2 now documents the coupling deliberately instead of carrying it by accident.

git hash-object packages/services/service-settings/src/settings-service.ts: ff35b55c7d25959350d185ea4db5ccffb8c953b2 before → 52ce662edcda238953c83bb543a5e6139a1bfc4e neutered → ff35b55c7d25959350d185ea4db5ccffb8c953b2 restored, byte-identical, with the ablation marker absent from the tree and git status clean.

Rebuild statement, argued from the files rather than recited. No rebuild was required for this ablation, and the reason is that dist is nowhere in the resolution path of the mutated file. The tests import the subject through relative specifiers (./settings-service.js), which vitest resolves to src/*.ts in the same directory — no package exports involved. Every cross-package specifier they do use is aliased to source by this package's own vitest.config.ts (@objectstack/core, @objectstack/objectql, @objectstack/platform-objects[/system], @objectstack/spec[/*], @objectstack/types), which is why this package carries no KNOWN_UNALIASED_TEST_IMPORTS entry. The decisive check rather than the argument: packages/services/service-settings/dist/ does not exist in this worktree, and the new cases pass asserting new behaviour — there is no build artifact they could have been reading.

Gates

node scripts/pm/dispatch-gates.mjs (no paths passed — the script derives the change set from the merge base itself) run after the final commit, on a clean worktree. Exit codes captured by redirecting each gate to a file first and reading $? before any pipe, never through tail.

All at cd161cb2d (git rev-parse --short HEAD), which is also the sha the package suite and the ablation above were measured on.

gate exit verdict line (the gate's own)
check:nul-bytes 0 —
check:changeset-gate-self-tests 0 —
check:cross-package-test-inputs (both spellings) 0 —
check:dispatcher-error-vocabulary 0 —
lint check:doc-formula-expressions 0 —
spec check:empty-state 0 —
check:error-code-casing 0 —
spec check:liveness 0 —
check:merge-driver 0 —
check:objectui-changeset 0 —
check:slot-lookup 0 —
check:spec-parsed-alias 0 —
spec check:strictness-ledger 0 —
check:test-source-alias 0 —
check:type-source-resolution 0 —
spec check:variant-docs 0 —
check-adr-0087-registration 0 —
check-changeset-no-major 0 —
check-empty-changeset 0 —
docs-audit/check-affected-docs 0 —
check:query-options-erasure 0 —
check:type-check-coverage 0 check-type-check-coverage: OK — 64/77 workspace packages type-checked (plus the root), 13 in the DEBT ledger
check:type-check-debt (--re-measure) 0 check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured in 238.5s, 1924 raw tsc error(s) total, none above its recorded number.
check:engine-double-contract 0 —
check:where-matcher 0 —
check-dev-prereqs 1 → 0 first run: ✗ The workspace is not built — 1 unmet precondition, not a list of problems. An unbuilt-worktree precondition, not this diff — after turbo run build (exit 0) it re-ran green: ✓ 67 package build artifacts present.

Package suite, at the same sha: pnpm --filter @objectstack/service-settings test → Test Files 24 passed (24) · Tests 462 passed (462) (456 before, +6 new); pnpm --filter @objectstack/service-settings typecheck → exit 0.

check:type-check-coverage ledger entries were not raised — the --re-measure verdict above says none is above its recorded number.

Deliberately not done

  • Reads in the window are not addressed here and remain a separate question: an in-window get() resolves from manifest defaults / the memory fallback rather than from persisted sys_setting rows, so a boot-time reader can legitimately see a stale value. That is a different defect from this card's silent write loss, and closing it would change what a real population observes. Filed as [finding] A settings READ in the pre-bind window silently resolves to manifest defaults instead of the persisted sys_setting row #10250 (unassigned, finding — the population is not yet measured).
  • No content/docs/releases/ edit, no check-type-check-coverage ledger movement, and no attempt to make the window smaller — a narrower silent-loss window would still be a silent-loss window.

Generated by Claude Code

…ne is bound

`upsertRow` picks its store on `if (this.engine)`, and the engine is bound in
exactly one place: `SettingsServicePlugin` registers a `kernel:ready` hook from
its `start()` and calls `bindEngine` inside it. Hooks fire in registration order
and every plugin's `init()` runs before any plugin's `start()`, so every
`kernel:ready` hook registered from an `init()` runs inside that window — an
ordinarily occupied position (`assembleMetadataProtocol` registers the three
platform migrations' hook from `ObjectQLPlugin.init()`).

A `set()` from there landed in the in-process memory fallback, re-resolved off
that same array, and handed the caller a fully resolved value while
`sys_setting` received nothing and both audit ledgers stayed silent. No log line
at any level: the write did not fail, it succeeded against the wrong store.

A write in the window now raises `SettingsEngineNotBoundError`
(`SETTINGS_ENGINE_NOT_BOUND`, 503) naming `kernel:bootstrapped` as the earliest
safe phase. The refusal is armed only by the new opt-in
`SettingsServiceOptions.engineBindPending`, set by the plugin in `init()` and
cleared on BOTH branches of its `kernel:ready` hook — `bindEngine` when
`objectql` is present, the new `settleWithoutEngine()` when it is not. Every
other engine-less reading of the memory fallback, and every read in any state,
is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
@github-actions

github-actions Bot commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-settings, @objectstack/spec, touching 10 documentable anchor(s).

7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via ERROR_CODE_LEDGER (symbol))
  • content/docs/api/error-handling-server.mdx (via ERROR_CODE_LEDGER (symbol))
  • content/docs/data-modeling/drivers.mdx (via SettingsService (symbol))
  • content/docs/kernel/contracts/data-engine.mdx (via ERROR_CODE_LEDGER (symbol))
  • content/docs/kernel/runtime-services/settings-service.mdx (via setMany (symbol))
  • content/docs/protocol/kernel/config-resolution.mdx (via SettingsService (symbol), SettingsServiceOptions (symbol), setMany (symbol))
  • content/docs/protocol/kernel/index.mdx (via SettingsService (symbol), SettingsServicePlugin (symbol))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx (via setMany (symbol))
  • content/docs/releases/v17.mdx (via ERROR_CODE_LEDGER (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/services/service-settings/src/index.ts) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 124 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 359f5956d7910aed7ae9f8fccc9fbb988b3e4882 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from bffafa173c08a0603eecb300b6ac11a0dcd9310b — the merge of head 8b2c585286a2402f5cf25f618e5c2681dfe70e06 into base 359f5956d7910aed7ae9f8fccc9fbb988b3e4882, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin bffafa173c08a0603eecb300b6ac11a0dcd9310b && git checkout bffafa173c08a0603eecb300b6ac11a0dcd9310b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 359f5956d7910aed7ae9f8fccc9fbb988b3e4882 8b2c585286a2402f5cf25f618e5c2681dfe70e06 && git checkout -B drift-repro 359f5956d7910aed7ae9f8fccc9fbb988b3e4882 && git merge --no-ff 8b2c585286a2402f5cf25f618e5c2681dfe70e06

node scripts/docs-audit/affected-docs.mjs --json 359f5956d7910aed7ae9f8fccc9fbb988b3e4882

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 359f5956d7910aed7ae9f8fccc9fbb988b3e4882 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

os-warren and others added 2 commits August 21, 2026 10:34
…ENGINE_NOT_BOUND

`content/docs/references/api/` is generated from `packages/spec` and was never
regenerated after this branch registered `SETTINGS_ENGINE_NOT_BOUND` in
`packages/spec/src/api/error-code-ledger.zod.ts`, so the `Type Check - source
gates` job failed with both pages reported out of date.

Produced by exactly the two commands the gate names, in that order:

  pnpm --filter @objectstack/spec gen:schema && pnpm --filter @objectstack/spec gen:docs

The whole diff is the one new ledger member and its consequences:
`error-code-ledger.mdx` gains the `SETTINGS_ENGINE_NOT_BOUND` row, and
`contract.mdx` gains the same row plus the enum-summary count it carries
(`+283 more` to `+284 more`). No source file, no changeset, no behaviour change.
`gen:schema` rewrote no tracked file, and of the 229 files `gen:docs` renders
only these two moved.

`pnpm --filter @objectstack/spec check:docs` now exits 0:
`OK 229 generated files in sync with packages/spec`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PnJHU45vPJj5UQrxe946Bx
@os-warren
os-warren added this pull request to the merge queue Aug 21, 2026
Merged via the queue into main with commit 1ec36b7 Aug 21, 2026
33 checks passed
@os-warren
os-warren deleted the claude/issue-10159-settings-write-before-bind branch August 21, 2026 03:30
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…ommits that decided them (objectstack-ai#20836)

Part of objectstack-ai#20596
Clause-②: no

## What changed

This is the fifteenth stage of the `domain:services` lane of the
dead-citation sweep. It covers
`packages/services/service-settings/src/**` and nothing else. By the
seat's claim (`5908460751`), it is the largest package left in the lane.
Later stages cover the other packages, so this PR says `Part of` and the
card stays open.

Every comment or docblock site in scope that cited a tracker number
answering 404 has been rewritten in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123), by the method of stages 1 to 14 (the latest is PR
objectstack-ai#20816, landed as `73155fedc`). That is **11 sites on 11 lines in 9
files, covering 4 numbers**:

- 4 census sites (every census site this package has at the base);
- 7 sites in test comments, which the census defers. One of their
numbers, `objectstack-ai#11318`, stands only in test files here; it was read on its
own and answers 404.

Each rewritten line now cites the commit in this repository that decided
what the line describes, and says in its own words what was decided: **4
distinct commit shas**. No ADR records any of the four decisions (see
the per-number table), so ruling C's commit rung applies. No number was
dropped.

Only comments changed. Every touched source file keeps its line count
(11 lines out, 11 in, over 9 files), so no line citation into these
files moves. All 11 changed lines carried a dead citation. No code token
moves (see the guard below).

**No citation number is added.** The only tracker number on an added
line is the live `objectstack-ai#10251`, once, in
`settings-prebind-read-warning.test.ts:17`. It already stood on that
line, and it now sits beside the sha as the convenience link ruling C
allows: 「(commit 1ec36b7, PR objectstack-ai#10251)」. `1ec36b730` is that pull
request's squash commit.

2 dead sites are left on purpose: a test title and a test assertion
message (see the list below).

One more file: a `patch` changeset for `@objectstack/service-settings`,
because the rewritten prose ships (see Changeset below).

## Census: `service-settings`, before and after

**Instrument (A1).** The gate's own `node
scripts/check-issue-citations.mjs --census --json`, read-only and
unchanged. The count below is its `allocated-but-absent` findings under
`packages/services/service-settings/`. Each run counts as a reading only
because its board frontier equals the newest issue or pull-request
number, read by a separate request just before and just after the run.

| reading | tree | board | whole-repo `allocated-but-absent` |
service-settings sites | lines | files | numbers |
|---|---|---|---|---|---|---|---|
| before | base `73155fedc`, run 2026-09-30T09:39:38Z to 09:43:17Z |
enumerated, 187 pages, frontier objectstack-ai#20830 (newest objectstack-ai#20830 before and after)
| 752 | **4** | 4 | 4 | 3 |
| after | head `ac05607d6`, run 10:02:17Z to 10:06:00Z | enumerated, 187
pages, frontier objectstack-ai#20834 (newest objectstack-ai#20834 before and after) | 748 | **0** |
0 | 0 | 0 |

The whole-repo drop is 4, exactly this diff's census sites. The
`resolves` tally is 33,134 in both runs, and `resolves-as-pull-request`
(1,985) and `cross-repo-unjudged` (1,018) did not move either. Neither
run was truncated or discarded: both enumerations read 187 pages at the
newest frontier. The seat's census counted 4 here at `6bff748b`, and the
base agrees: `6bff748b` is an ancestor of the base, and no commit
between them touches this package's `src`.

**Supplementary instrument, the whole scope.** The census does not read
test files or strings, and this stage's scope includes test comments. So
a second reading runs the gate's own exported `extractCitations`
(whole-file and comment-prose projections) and `namesThisRepository`
over every `.ts` file under `service-settings/src` (64 files). It takes
its verdicts from the before census's own board reading rather than from
a second enumeration: a number is dead when that census reported it
`allocated-but-absent`, and alive when the gate's own census-scope
extraction judged it and the census did not report it. 10 numbers are
covered by neither, because they stand only in test files, or as the
second number of an `#A/#B` pair. Each was read on its own through the
read-only tools. 1 answers 404 (`objectstack-ai#11318`, on the issue and the
pull-request endpoint alike); 6 answer as issues; 3 answer as pull
requests (`objectstack-ai#7554`, `objectstack-ai#10251`, `objectstack-ai#5133`). The probe's control: the known
issue `objectstack-ai#11352` answers 404 on the pull-request endpoint.

| reading | citations | dead | src comment | test comment | src string |
test string |
|---|---|---|---|---|---|---|
| before, `73155fedc` | 534 | **13** | 4 | 7 | 0 | 2 |
| after, `ac05607d6` | 523 | **2** | 0 | 0 | 0 | 2 |

Its src-comment column equals the census's 4, which is the control on
the second instrument. The 519 live citations and 2 cross-repo citations
are the same in both readings, and the drop of 11 citations is exactly
the rewritten sites. A third, raw reading (every `#` followed by 2 to 6
digits, whatever surrounds it) finds 547 occurrences before and 536
after, the same drop of 11. The 13 tokens beyond the gate's grammar are
the same before and after, and none is dead (see Acceptance notes).

## Per-number table

Sites and files count every dead occurrence in scope at the base
(comments and strings, tests included). `rewritten / left` counts the
sites rewritten and the sites left. Each anchor was read in its message
and diff, not only its subject.

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#13279` | 4/4 | 4/0 | `6a180e42d` (PR objectstack-ai#13475): `resolveAuthzContext`
raises `AuthzStoreUnavailableError` (`SERVICE_UNAVAILABLE`, 503) when a
permission-store read throws, instead of answering an outage as a caller
with zero capabilities, and each production transport's fail-closed
`catch` re-raises that brand. The settings plugin's
`verifiedContextFromRequest` is one of them. Its message names `objectstack-ai#13279`
4 times and its diff 54 times; `git blame` puts
`settings-service-plugin.ts:307` in it, and the other three lines were
written by `ac9376a74` (PR objectstack-ai#16580), a descendant, which describes that
re-raise. Stage 6's anchor, reused by the storage, datasource and
analytics stages |
| `objectstack-ai#10159` | 3/2 | 3/0 | `1ec36b730` (PR objectstack-ai#10251): a settings write
issued before the engine is bound is refused with
`SETTINGS_ENGINE_NOT_BOUND` (503). Its message states that every read in
any state is unchanged, which is the "left reads open" all three lines
describe. The message does not name `objectstack-ai#10159`, but its own diff does,
once, in its changeset ("refused loudly instead of resolving
successfully while nothing reaches `sys_setting` (objectstack-ai#10159)"), and
`settings-prebind-read-warning.test.ts:17` already paired the two
numbers. `git blame` puts the three lines in `a24b7fa4d` (PR objectstack-ai#11044),
the later read-half fix, a descendant. New to the sweep |
| `objectstack-ai#17062` | 3/2 | 2/1 | `50b6f17d4` (PR objectstack-ai#17071): adds the package-local
route-ledger conformance guard beside the dogfood live-mount parity
gate, and updates the ledger header that had said such a guard was
deliberately omitted. Its message does not name `objectstack-ai#17062`; its diff does,
on 3 added lines, which are the three sites here (`git blame` puts all
three in it). New to the sweep |
| `objectstack-ai#11318` | 3/1 | 2/1 | `99ccbb9c8` (PR objectstack-ai#11467): the Settings, AI "Test
connection" fallback keeps its mount instruction on all three
real-provider branches and gains the cloud-only boundary read from
`PLATFORM_CAPABILITY_PROVIDERS.ai`. Its own changeset states that "the
embedder hint at the fourth site is deliberately left alone ... and
pinned by a contrast test", which is the fence `:339` describes. Its
message's trailer names `objectstack-ai#11318` as the issue it answers, its diff names
the number 3 times, and `git blame` puts all three lines in it. New to
the sweep |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1 for each of the 4), and all 4 are ancestors of
the base (`merge-base --is-ancestor`, exit 0 for each; reverse leg, base
against each anchor, exit 1 for each; control legs exit 0: stage 1's
landing `422db788a`, and the repository's root commit, which lies deeper
than every anchor; the history is complete, `--is-shallow-repository`
false, 15,193 commits). Each of the 4 numbers answers 404 on the issues
endpoint, which serves pull requests too, read one by one.

No ADR, `scripts/adr-anchors/` file or other `docs/` page records the
decision of any of the 4: `docs/adr` names none of the numbers, and none
of their mechanisms (`AuthzStoreUnavailableError`,
`SETTINGS_ENGINE_NOT_BOUND`, `engineBindPending`, the settings route
ledger, the AI hint's edition boundary).

## Wordings to check

- **Tag swaps in place.** 「[objectstack-ai#13279]」 became 「[commit 6a180e4]」
(`settings-service-plugin.ts:307`); 「(objectstack-ai#13279)」 became 「(commit
6a180e4)」 on 2 lines; 「objectstack-ai#13279's permission-store re-raise」 became
「commit 6a180e4's permission-store re-raise」. These are the forms the
storage and datasource stages used for the same sha.
- **`objectstack-ai#10159`.** 「is why objectstack-ai#10159's fix deliberately left reads open」
became 「is why commit 1ec36b7's write refusal deliberately left reads
open」; 「(objectstack-ai#10159's fix left reads open on purpose)」 became 「(commit
1ec36b7 left reads open on purpose)」; 「(objectstack-ai#10159 / PR objectstack-ai#10251)」 became
「(commit 1ec36b7, PR objectstack-ai#10251)」, with the pull-request number kept as
the convenience link beside its own squash commit.
- **`objectstack-ai#17062`.** 「Two layers, since objectstack-ai#17062.」 became 「Two layers, since
commit 50b6f17.」 The docblock goes on to describe the conformance test
that commit added as the second layer.
- **Two headers keep the antecedent of the prose below them**, the form
stage 14 used:
- `settings-route-ledger.conformance.test.ts:4`: 「Settings route-ledger
conformance (objectstack-ai#17062)」 became 「Settings route-ledger conformance (the
issue behind commit 50b6f17)」, because `:25` of the same docblock says
「(per the issue)」.
- `manifests/ai.manifest.test.ts:277`: 「objectstack-ai#11318 —」 became 「The card
behind commit 99ccbb9:」, because `:288` 「the very claim this card is
about」 and `:329` 「The un-followable form this card retired」 speak of
that card.
- **`ai.manifest.test.ts:339`.** 「deliberately not edited — objectstack-ai#11318
fences this site out by name」 became 「... — commit 99ccbb9 fences this
site out by name」. The commit's own changeset names that site (quoted in
the table).

## The 2 sites left

- **Test strings, 2 sites on 2 lines**, left as stages 1 to 14 left
theirs:
  - `manifests/ai.manifest.test.ts:292`, a `describe` title (`objectstack-ai#11318`);
- `settings-route-ledger.conformance.test.ts:88`, the assertion message
a failing run prints (`objectstack-ai#17062`). It is a string, not a comment, and form
C does not touch strings.
- No operator log string, runtime refusal, quoted maintainer ruling or
generated file in this package carries a dead number.
- **Outside `src`, listed and left, not edited in this stage:**
  - the shipping `README.md` names only the live `objectstack-ai#8026`;
- `vitest.config.ts` names only live numbers (`objectstack-ai#8020`, `objectstack-ai#8030`, `objectstack-ai#8063`,
`objectstack-ai#8104`, `objectstack-ai#10374`);
  - `tsconfig.json` and `package.json` name none;
- the release-owned `CHANGELOG.md` names `objectstack-ai#13279` and `objectstack-ai#10159` on 2
lines, the entries of `6a180e4` and `1ec36b7`, which are this PR's
anchors.

## Mechanical guard: no code token moves

The guard compares, base `73155fedc` against head, over all 9 touched
`.ts` files:

- **Reading 1**, the TypeScript parser's leaf nodes (a `forEachChild`
walk, so comments are trivia and JSDoc nodes are never visited). String
and template literals are therefore read in full.
- **Reading 2**, the full token stream in parser context (a
`getChildren` walk, so punctuation and keywords are included; JSDoc
nodes skipped).

Results:

- Real run at the final head `ac05607d6`: 9,999 base leaf tokens, **0
files with a token change** on either reading (exit 0). The first commit
`ff7ef46f4` gave the same, and no `.ts` path changed after it.
- Comment control in `settings-service.ts` (「deliberately left reads
open.」 to 「deliberately kept reads open.」): 0 files changed, as expected
(exit 0).
- Positive control, a code token renamed in `settings-service-plugin.ts`
(`isAuthzStoreUnavailableError(err)` to
`isAuthzStoreUnavailableErrorX(err)`): DIFFER on the identifier (exit
1).
- Positive control, one digit changed inside the kept test title
`ai.manifest.test.ts:292` (`objectstack-ai#11318` to `objectstack-ai#11319`): DIFFER on the string
literal (exit 1).

Every mutation went through `scripts/ablation-replace.mjs` (wrap mode)
under a shell trap that restores by absolute path, and each landed
(anchor 1 to 0, blob changed). Each restore was proven byte-identical to
the HEAD blob (`1248149a428d`, `a2fac9ad1f0b`, `79c2b40a48a6`), with
`git diff HEAD` empty and a clean tree afterwards.

## Changeset

This change ships bytes, so a `patch` changeset for
`@objectstack/service-settings`
(`.changeset/20596-service-settings-provenance-anchors.md`) is included.
Its body is stages 12 and 13's commit-anchor text, word for word, with
the package name changed.

Measured on the built package (A3), after a full workspace build in
which this package was a cache miss (71 of 71 tasks, 0 cached, at
`ff7ef46f4`, which holds every source-line change): `files[]` is `dist`,
`README.md` and `CHANGELOG.md`, and the package is not private.

- The rewritten `settings-service.ts:685` docblock, on the pre-bind read
reporter, is in all four entries: `dist/index.js`, `dist/index.cjs`,
`dist/index.d.ts` and `dist/index.d.cts` (once each).
- The other three rewrites (`settings-route-ledger.ts:17`,
`settings-routes.ts:72`, `settings-service-plugin.ts:307`) are stripped
by the bundle, and the other seven sit in test files.
- Positive controls, the unchanged line beside each rewrite, land
exactly where their neighbours do: the line before
`settings-service.ts:685` once in each of the four entries, and the
neighbours of the three stripped rewrites 0 everywhere.
- A never-written negative phrase appears nowhere in `dist`, and none of
the four old numbers is left there.

The later commit adds only the changeset.

## Gates (final head `ac05607d6`)

- **Citation judging, as CI runs it:** `pnpm check:issue-citations`
exits 0 (self-test, 114 cases, 8 batteries). `node
scripts/check-issue-citations.mjs` exits 0: 「no issue citations added
against 73155fe (4 file(s) read)」.
- **Doc authoring:** `pnpm check:doc-authoring` exits 0 (the
sibling-package prose-id baseline holds, no growth).
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at `ac05607d6` derived 63 commands,
the same 63 as at dispatch.
- Each ran with its exit code captured before any pipe, and all 63 exit
0; none exited 3.
- `--ran`, fed each command with its exit code, reports 63 run, 0 NOT
MEASURED (a derived zero), 0 unrun, and exits 0.
- The full `turbo run build` above ran first under the shared verify
lock, so no gate hit an unbuilt workspace.
- **Roster families the derivation lists outside its commands** (their
rosters sit in directories this diff touches): `node
scripts/check-changeset-fixed.mjs`, `pnpm check:authz-resolver`, `pnpm
check:error-code-casing` and `pnpm check:filter-alias-parity`, each exit
0.
- **Tests and typecheck, under the verify lock, at `ac05607d6`:**
- `pnpm --filter @objectstack/service-settings test`: 33 files pass and
584 tests pass, which is every tracked test file under `src/`, the 5
touched ones included.
- `pnpm --filter @objectstack/service-settings typecheck` (`tsc
--noEmit`) exits 0, and `tsc --listFiles` puts all 9 touched files in
the program.
- **Lint, as a proven narrowing:** eslint with inline config disabled,
over the 9 touched `.ts` files, gives 9 files, 0 errors and 0 warnings
(its `--format json` output). All 9 are in eslint's own population (none
reported ignored; `dist/index.js`, the control, reads ignored).
`eslint.config.mjs` never enables type-aware linting (no
`parserOptions.project`, as its own lines 327-328 state), so a comment
edit here cannot move the verdict on any untouched file. The repo-wide
`pnpm lint` is CI's run.
- **Control bytes:** `pnpm check:nul-bytes` exits 0, and a raw scan of
the 10 changed files for control bytes finds none.

## Acceptance notes

- **The gate-invisible spellings, grepped as the claim asked** (objectstack-ai#20636,
including the `clause #N` position). At the base, `#N-word` is on 0
lines. `#A/#B` is on 11 lines (12 second numbers), and every second
number is live: `objectstack-ai#6580`, `objectstack-ai#5094`, `objectstack-ai#11230`, `objectstack-ai#5480`, `objectstack-ai#5932`, `objectstack-ai#6199`
and `objectstack-ai#5204` by the census's own judgement, and `objectstack-ai#5133` read on its own
as a pull request. `option #N`, `clause #N` and URL-spelled links are on
0 lines. So the claim's 0 / 11 / 0 / 0 hold, and nothing dead hides
behind them. The one other raw token beyond the grammar is the colour
literal `'#6366f1'` in `manifests/branding.manifest.ts:32`.
- **「The card」 phrases.** 38 lines in 18 files under this package's
`src` speak of 「the card」 or 「this card」. They carry no number, and
neither instrument sees them. The ones whose antecedent this diff would
have removed are handled above; the rest are unchanged, as in stages 8
to 14.
- **The census instrument did not truncate in this stage.** Both
enumerations read 187 pages at the newest frontier.
- **Anchors the next stages can reuse**, each checked here: `objectstack-ai#10159` →
`1ec36b730`; `objectstack-ai#17062` → `50b6f17d4`; `objectstack-ai#11318` → `99ccbb9c8`; and the
reused `objectstack-ai#13279` → `6a180e42d`.
- **Base.** The branch is on `main` at `73155fedc`. `main` has since
moved two commits (`4b45afaed`, `15b586dcf`). Neither touches
`packages/services/service-settings`,
`scripts/check-issue-citations.mjs`, `.changeset/config.json` or a path
in this diff. `15b586dcf` moves `packages/spec/liveness/**`, a gate
input this comment-only diff cannot interact with. No merge was taken;
the merge queue rebuilds on the merged generation.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants