Skip to content

fix(objectql)!: a caller-supplied formula value is stripped on every write path and reported as computed, and engine.validate runs the write doors (#20805) - #20834

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-20805-formula-write-strip
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-20805-formula-write-strip

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20805
Clause-②: yes (narrowing)

A caller-supplied value for a formula field is now stripped by the engine on every write path, in every context, and reported through droppedFields / onFieldsDropped under a new reason arm, computed. ObjectQL.validate now runs the write's own field doors, so a dry run built on it predicts what the write does. The REST half (the import runner's row report) stays on #20701; #20701 remains open.

Reproduction first (base 5bed1f6caf, real ObjectQL + ObjectStackProtocolImplementation + RestServer routes, SQLite :memory: and the in-memory driver)

A throwaway probe (never committed) wrote to an object with n: number, doubled: formula (record.n * 2), ro: readonly text, task_count: summary, as a non-system caller unless noted. The raw row was read from the driver, below the engine's formula hydration.

door SQLite, before memory, before both drivers, after (b21d13b05b)
protocol.createData with doubled: 5 throws SqliteError SQLITE_ERROR, no status, no field ok, droppedFields null, raw row stores doubled: 5 ok, droppedFields: [{ fields: ['doubled'], reason: 'computed' }], raw row has no doubled
POST /api/v1/data/:object with doubled: 5 400 INVALID_FIELD "Unknown field 'doubled'" (REST's driver-string branch, for a declared field) 201, raw row stores doubled: 5 201, the same droppedFields, header doubled;reason=computed
engine.update by id with doubled: 99 throws SqliteError ok, no drop, raw row stores doubled: 99 ok, one computed event, raw row has no doubled, read returns doubled: 8 from n: 4
PATCH /api/v1/data/:object/:id with doubled: 7 400 INVALID_FIELD 200, raw row stores doubled: 7 200, the same droppedFields
engine.insert, isSystem, doubled: 5 throws SqliteError ok, raw row stores doubled: 5 ok, one computed event, raw row has no doubled
full read of a row, written back (GET then PUT) throws SqliteError ok ok, computed for doubled beside the existing readonly drops
engine.validate with doubled: 5 valid: true valid: true valid: true, and the listener receives the computed event
engine.validate with nope: 5 valid: true while insert refused it same throws INVALID_FIELD / 400 / field: 'nope', the envelope insert throws
engine.insert / update with task_count: 42 / 7 (summary, H4) stored 42, then 7, then recomputed to 1 by a child write, no drop same unchanged: summary is persisted by the engine (see Acceptance notes)

What changed

  • packages/objectql/src/engine.ts
    • stripComputedWriteFields — a new module function beside undeclaredWriteFieldErrors: takes every own key naming a declared formula field out of the payload (copy, never mutating the caller's object) and returns the union of what it took. Keyed on type === 'formula' literally, the test fieldHasColumn and the read projection use.
    • insert() — runs it right after the declared-field door, over the rows that door did not refuse, with no isSystem gate; opCtx.data carries the stripped rows, so the snapshot, the defaults and the hooks all see the payload that is stored. The report joins the existing site (insertDrops: one computed event, then the existing readonly event), so strictReadonlyWrites refuses it by the derived coverage its contract states.
    • update() — the same door after the pre-hook declared-field door (before the read-only hide pass and the hook recording); reported with reportDroppedFields(..., 'computed') at the confluence after the post-hook door, beside the other strips' reports. ctx.submitted still carries the submission as sent.
    • validate() — runs, by the same functions and in the write's order: the declared-field door (throws, as insert does), the computed strip, and the caller-write strips under the write's isSystem gate (insert mode: stripRuntimeOwnedFields, then the static-readonly strip; update mode: stripReadonlyFields, with a supplied id read as the address). Drops go to a new optional onFieldsDropped option. The docblock's "second named limit" (a readonly reference field) is closed and rewritten.
    • staticReadonlyCreateStrip — the create path's static-readonly strip with its re-default, lifted out of insert()'s loop unchanged in behaviour so validate calls the same door rather than a copy (one function per door).
  • packages/spec/src/data/data-engine.zod.ts — the computed arm on DroppedFieldsEventSchema.reason, its docblock bullet and describe string. packages/spec/src/contracts/data-engine.ts — the WriteObservabilityOptions docblock named "all three arms"; it now names four (a comment-only edit, outside the claim's one-arm surface, because the sentence would otherwise be false).
  • Consumers of reason (H3) — the two exhaustive maps tsc re-checks: REASON_PHRASE in packages/objectql/src/readonly-strict-errors.ts (with a remedy clause spoken only when a computed drop is present, so every existing message is byte-identical; computed is deliberately not in READONLY_CLASS_REASONS) and DROPPED_REASON_LABEL in packages/services/service-automation/src/builtin/crud-nodes.ts. Reason-agnostic pass-throughs need no change: REST's X-ObjectStack-Dropped-Fields echo, metadata-protocol's mergeDroppedFieldEvents, the client SDK types. objectui is NOT MEASURED (not checked out in this container).
  • Docs — content/docs/references/** regenerated by check:generated --fix (the enum and describe string only); the two hand-written pages that enumerate the arms (kernel/contracts/data-engine.mdx, protocol/objectql/security.mdx); permissions/system-context.mdx gains row 21b for the preview's isSystem gate and amends row 22 (strict still refuses a formula value under isSystem), with the census counts regenerated by gen:system-context-census.
  • Changeset .changeset/20805-formula-write-strip.md: @objectstack/spec minor, @objectstack/objectql minor with the BREAKING banner, @objectstack/service-automation patch, one ADR-0087 disposition (not-required (no-migration-prescription)).

Zone 2 hypotheses, as measured

  • H1 — falsified in its "one place" half. The existing strips live in several places: insert() has two strip passes (stripRuntimeOwnedFields, and the static strip over staticReadonlyInsertSubject) behind one isSystem gate, reporting at one site (insertDropped, reason readonly); update() has per-branch passes (primary key, readonlyWhen, static) on by-id and multi, reporting through the reportDroppedFields closure. So there are two report sites. The new strip's REPORT joins both; its STRIP sits at the declared-field door instead, because those passes are post-hook and isSystem-gated, while this one must run for system writers, must not hand hooks a value that will not be stored, and must be callable by validate, which runs no hooks. The function is stripComputedWriteFields.
  • H2 — held: undeclaredWriteFieldErrors judges undeclared keys only and validate called no door. Served as one function per door (undeclaredWriteFieldErrors, stripComputedWriteFields, stripRuntimeOwnedFields, staticReadonlyCreateStrip, stripReadonlyFields), each called by the write and by validate, not one combined judge.
  • H3 — listed above; both exhaustive maps updated, and tsc reddens when one is not (reverse check below).
  • H4 — summary is persisted by the engine, measured on both drivers (table above), so it takes no arm and no pin here.

Tests

  • New packages/objectql/src/engine-formula-write-strip.test.ts (19 cases, recording driver): insert single / batch / insertMany (a refused row counts toward nothing) / isSystem; update by id / multi / isSystem; a formula also declared readonly reported once as computed; controls (static readonly still readonly, writable fields land); the hooks' view and ctx.submitted; strict refusal on both verbs; validate in both modes, under isSystem, the unknown-key envelope equal on validate and insert, the caller-write strips' report, and the update-mode id address rule on an object whose id is declared readonly.
  • packages/runtime/src/sandbox/undeclared-field-write-driver-split.integration.test.ts gains a #20805 block, one assertion run on both families (SQLite on a real table, in-memory): insert and update succeed, report computed, store no such key, isSystem included; validate reports the same and refuses an unknown key as insert does; controls. The memory arm lives in this file because it is a ruled driver-memory consumer, and the census counts declarations, so no ledger row moves. PostgreSQL is not a cell here: the one live-PostgreSQL job for @objectstack/runtime (Temporal Conformance, step "Run the runtime cascade-delete matrix against live PostgreSQL") runs only cascade-delete-multivalue-lookup-real-driver, and this file is not in it (sentence corrected by the seat after review 5909189785).
  • Runs (all VERDICT command-exit 0 under the shared lock): objectql local 344 files / 6769 tests at 55c81da438 (objectql unchanged to b21d13b05b); spec local 578 files / 17065 tests and service-automation 157 files / 1974 tests at 06e16e23f0 (both unchanged to b21d13b05b); consumer suites at c8b823cf16 — metadata-protocol validate-data + dropped-fields 4 files / 33, REST every import-* file + rest-dropped-fields + rest-batch-endpoint 22 files / 529, verify handle.test.ts 24; the runtime file 28 / 28 at b21d13b05b; typecheck (tsc + test layer) green for spec, objectql, service-automation and runtime.
  • Reverse verification (fix committed first; scripts/ablation-replace.mjs in wrap mode, restore trap armed): stripComputedWriteFields made to return before stripping (anchor 1 to 0, blob 2719ac6bc184 to 15ace97894c2), objectql rebuilt, ablation-dist-preflight found the marker in 4 built files. Result: objectql pins 17 of 19 red (the two strip-independent cases — unknown key, nothing to strip — stayed green), runtime 4 of 28 red (both families' write and validate cells; the controls and every pre-existing case green). Restore: blob equal to HEAD, git diff HEAD empty, rebuilt, marker absent from all 14 built files, whole tree clean, pins 19 / 19 and 28 / 28.
  • Type reverse check: deleting computed from DROPPED_REASON_LABEL made service-automation tsc exit 2 with TS2741: Property 'computed' is missing — it reads the rebuilt spec declarations; restored byte-identical.
  • Gates at b21d13b05b: node scripts/pm/dispatch-gates.mjs --commands derived 116; all 116 run, each exit captured before any pipe, all exit 0; --ran reconciliation: 116 derived, 116 run, 0 NOT-MEASURED, 0 UNRUN. Three that first answered PREREQUISITE NOT MET (check:skill-examples, check:dual-build-cjs-loads, check:type-check-debt) were re-run green after turbo run build --filter='./packages/*' --filter='./packages/*/*'; check:system-context-census first reddened on the new isSystem read and is green with row 21b.
  • Lint, narrowed: eslint --no-inline-config --format json over the 7 TS files this diff touches reports 7 files, 0 errors, 0 warnings; --print-config shows no parserOptions.project / projectService on any of them (the config enables no type-aware linting), so the diff cannot move a verdict on an untouched file. The repo-wide pnpm lint is CI's.

Acceptance notes

  • summary (H4): a caller-supplied roll-up value is stored as sent and overwritten by the next child write, on both drivers, with no drop. That is the contract RUNTIME_OWNED_FIELD_TYPES and its docblock in rule-validator.ts record on purpose; not changed here.
  • FLS runs before the strip: plugin-security's field-write gate judges opCtx.data before the engine body, so a permission set that marks a formula field not editable would 403 a round trip before the strip runs — the same order a readonly field already has. Read, not measured.
  • A hook-written formula key: the door strips the caller's value before the hooks; a before* hook that writes a formula key itself passes the post-hook declared-field door (the field is declared) and still reaches the driver. Read, not measured; no producer found.
  • Neighbouring describe strings: the droppedFields describe strings in api/batch.zod.ts / api/protocol.zod.ts name only the read-only strips (already incomplete since primary_key); left as they are, outside this surface.
  • REST's driver-string branch still labels a missing column "Unknown field" for schema drift; the import runner's use of the new listener and its row report are rest(import): a column for a formula field passes the dry run, then fails the row at commit with the driver's SQL error, where the create door answers 400 INVALID_FIELD #20701's.

Generated by Claude Code

…ath and run the write doors in validate

Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
Co-authored-by: Claude <noreply@anthropic.com>
…d validate's doors, on memory and SQLite

Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
Co-authored-by: Claude <noreply@anthropic.com>
…ame the computed strip's exemption from it

Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/kernel/contracts/data-engine.mdx (via WriteObservabilityOptions (symbol, a top-level interface), onFieldsDropped (literal, a string literal in validate), primary_key (literal, a string literal in DroppedFieldsEventSchema), readonly_when (literal, a string literal in DroppedFieldsEventSchema))
  • content/docs/protocol/objectql/security.mdx (via onFieldsDropped (literal, a string literal in validate), primary_key (literal, a string literal in DroppedFieldsEventSchema), readonly_when (literal, a string literal in DroppedFieldsEventSchema))

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

  • content/docs/releases/v17/17-4.mdx (via onFieldsDropped (literal, a string literal in validate))

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 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 70 pages)
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 138 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 4b45afaed5e2a1afdbc8c4fd36005dce210baa4e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 26ac685b88680bf880090cbebe057513fa215d1a — the merge of head b21d13b05b35ebae2e9cf60725a081ae5349e56c into base 4b45afaed5e2a1afdbc8c4fd36005dce210baa4e, 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 26ac685b88680bf880090cbebe057513fa215d1a && git checkout 26ac685b88680bf880090cbebe057513fa215d1a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4b45afaed5e2a1afdbc8c4fd36005dce210baa4e b21d13b05b35ebae2e9cf60725a081ae5349e56c && git checkout -B drift-repro 4b45afaed5e2a1afdbc8c4fd36005dce210baa4e && git merge --no-ff b21d13b05b35ebae2e9cf60725a081ae5349e56c

node scripts/docs-audit/affected-docs.mjs --json 4b45afaed5e2a1afdbc8c4fd36005dce210baa4e

⚠️ 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 4b45afaed5e2a1afdbc8c4fd36005dce210baa4e → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b21d13b05b35ebae2e9cf60725a081ae5349e56c
Local-runs: none

Inputs read: card #20805 (body, claim 5906928968, os-dev-report 5908848440); #20701 (body, triage 5897601448, claim 5905999986, dev report 5906449929, seat review 5906485725, the split ruling 5906762084); the cross-seat declaration 5906954118 on #18549; PR #20834 (body, 14-file list, the net diff against main at the head above, read with git); the head's check-runs. The head was still the sha above when read. Nothing built, run or re-run; the template came from record-recognisers --template and the post went through post-stamped.

① Derived judgments

Every accept-set and public-surface change the diff implies, each judged against the claim's surface and stop lines:

  1. Engine write doors — insert (one row, a batch), insertMany, update by id and by predicate; every driver; every context, isSystem included. A caller-supplied formula key used to reach the driver (SQL: the driver's own error, no status, no field; schemaless: stored as a shadow column). Now stripComputedWriteFields (one module function beside undeclaredWriteFieldErrors, keyed on type === 'formula' literally, pure, index-aligned, skipping rows the declared-field door refused) takes it out and the write completes with one { reason: 'computed' } event per call. A WIDENING on SQL (failure to success), a behaviour change on schemaless (silent store to reported strip). Right — triage's ruling verbatim ("strip and report, never refuse"); not a 400, not silent, not readonly. The changeset states both halves truly.
  2. REST POST /api/v1/data/:object and PATCH /api/v1/data/:object/:id. The answer changes: on SQL from 400 INVALID_FIELD "Unknown field" (REST's driver-string relabel of a driver error) to 201/200 with droppedFields and the X-ObjectStack-Dropped-Fields header; on memory from a silent 201 to 201 with droppedFields. The header formatter (error-response.ts, field;reason=reason per field) and metadata-protocol's mergeDroppedFieldEvents are reason-agnostic and untouched. This is within THIS card: the split ruling put the engine verdict here and said in so many words that the strip turns SQL's driver error into a success; rest(import): a column for a formula field passes the dry run, then fails the row at commit with the driver's SQL error, where the create door answers 400 INVALID_FIELD #20701 keeps the import row report, toFailedResult and the driver-string wording, and stays open. No packages/rest or packages/metadata-protocol source moved. Right.
  3. engine.validate throws INVALID_FIELD / 400 on an undeclared key — a NARROWING (it answered valid: true). Same function, same envelope (status, httpStatus, code, field, fields, object), same first-refusal-thrown batch rule as insert; id / created_at / updated_at stay tolerated on both, so an update-mode preview carrying the address is not refused where the write is not. metadata-protocol's validateData relays engine.validate unchanged, so the protocol door and the import dry run now refuse what the commit refuses. Declared: BREAKING banner, (narrowing) arm. Right.
  4. engine.validate runs the caller-write strips in the preview, under the write's isSystem gate — a behaviour change on the preview's payload (the docblock's former "second named limit", now closed and rewritten). Insert mode calls stripRuntimeOwnedFields then staticReadonlyCreateStrip, the write's own functions, with the options a hook-less, write-less preview cannot have (no logger, strictReadonlyWrites: false, no hookWrittenKeys); update mode calls stripReadonlyFields with supplied minus id, which is equivalent to the write's addressKey: 'id' because that function judges only keys supplied owns (rule-validator.ts) and mirrors the write's own pre-hook hide pass, which also passes suppliedDataOnly. readonly_when and primary_key do not run in the preview (no prior record, no dispatch) — a named limit in the code; the card's direction was "the same doors as insert", which is met in full, and the changeset enumerates exactly what runs in update mode, so it does not overclaim. One function per door, no copy for validate: held. Right.
  5. validate gains an optional onFieldsDropped — a widening of @objectstack/objectql's public surface (the exported ObjectQL class). validate is declared on neither IDataEngine nor IObjectQLEngine in packages/spec/src/contracts, so no spec signature moves, and validateData does not relay the listener — rest(import): a column for a formula field passes the dry run, then fails the row at commit with the driver's SQL error, where the create door answers 400 INVALID_FIELD #20701's carrier, correctly left there. Right.
  6. strictReadonlyWrites refuses a formula value, isSystem included — a NARROWING of what a strict caller may send, derived by the contract's own sentence ("a new reason therefore adds a new REFUSAL, by construction"). On insert the computed event joins insertDrops before the strict throw; on update reportDroppedFields(..., 'computed') at the confluence feeds strictDrops, refused by assertNoStrictDrops() before any driver call. computed is deliberately not in READONLY_CLASS_REASONS; the remedy clause is spoken only when a computed drop is present, so every pre-existing message is byte-identical. Pinned on both verbs. Right, and stated.
  7. before* hooks no longer see the key in ctx.input.data; ctx.submitted keeps it. Insert replaces opCtx.data right after the declared-field door, before the snapshot, the defaults and the hooks; update replaces it before the Update-side: a readonly field is stripped from persistence but still reaches beforeUpdate, so hook-derived columns persist values computed from data the row never contains #16344 hide pass, after suppliedValues was captured. The Update-side: a readonly field is stripped from persistence but still reaches beforeUpdate, so hook-derived columns persist values computed from data the row never contains #16344 invariant (a hook is handed the payload that will be stored), pinned on both verbs. Right, and stated.
  8. DroppedFieldsEventSchema.reason gains computed — a WIDENING of a published OUTPUT enum. Reachable: packages/spec/src/data/index.ts re-exports data-engine.zod, ./data is a subpath in spec's exports, and DroppedFieldsEvent also rides ./contracts through WriteObservabilityOptions.onFieldsDropped. The docblock demands exhaustive branching; every non-test consumer in this repo that branches on the arm is exactly two maps, REASON_PHRASE (objectql) and DROPPED_REASON_LABEL (service-automation), both updated, and the dev's type reverse check reddened the second. The arm's NAME, left to this review by triage and the claim: computed is settled — it names the field's role in the register the other three arms use, its docblock line pins it to formula and says why summary is not it. Right.
  9. summary — measured on both drivers: persisted by the engine and overwritten by the next child write, no drop. No arm, no pin, reported in out_of_scope_findings as the card instructed; the door is keyed on formula alone. Right.
  10. staticReadonlyCreateStrip — the create path's static-readonly strip lifted out of insert() with behaviour unchanged, by reading: the same stripReadonlyFields call and options, the same skip when nothing was taken, the same push of taken keys into insertDropped / preserveAuditIgnored BEFORE the re-default error is judged, the same partial-mode culling with the row left as it arrived, the same re-default copy-back over the stripped row. Right.
  11. insertMany partial mode — a row the declared-field door refused is left as it is and counts toward nothing; pinned. Right.

Review faces, each factual sentence judged: the changeset — true throughout; the PR body — true, except one sentence named in ③ (PostgreSQL); packages/spec/src/data/data-engine.zod.ts docblock and describe string — true; packages/spec/src/contracts/data-engine.ts — the comment-only edit is outside the claim's one-arm surface and is accepted: without it "all three arms" would be false, and "all four" is true; content/docs/kernel/contracts/data-engine.mdx (new table row, interface snippet), content/docs/protocol/objectql/security.mdx (the two non-lock arms), content/docs/permissions/system-context.mdx (row 21b anchors the one new isSystem read in validate; row 22's exception is true) — true; content/docs/references/** — every hunk in the three files is the Enum literal or the describe string, byte-equal to the zod source, in a commit of its own: generator output, not hand edits. Neighbouring describe strings in api/batch.zod.ts / api/protocol.zod.ts remain incomplete (they already omitted primary_key); noted by the dev, not false.

② Semver level

  • @objectstack/spec minor: the enum widening; yes takes at least minor. Right.
  • @objectstack/objectql minor with the **BREAKING** banner: the validate narrowing and the strict narrowing ship as minor under the launch-window convention that check-changeset-no-major.mjs states (a major would promote the whole fixed group), with the banner the ADR-0087 gate reads. Right.
  • @objectstack/service-automation patch: one entry in an exhaustive map, a new worded step warning, no new export; patch is the right floor and the changeset-fixed gate requires an entry because its src/** moved. Right.
  • Other published packages: rest and metadata-protocol answer differently (item 2 and 3 above) but moved no src/**; by this repo's rule they owe no entry, they ride the fixed group with updateInternalDependencies: patch, and the objectql entry names the REST routes and validateData by name, so a consumer of either finds the change in the train's changelog. runtime moved a test file only. Not owed. Check Changeset on the head: success.
  • Clause-②: yes (narrowing): the key at line start, yes the first thing after the colon, the arm the next non-blank thing — the reader takes it as "widens one surface and narrows another", which is exactly the diff: the enum and the validate option widen; validate's refusal and strict's refusal narrow. The same line stands on the claim, the cross-seat declaration, the PR body and the changeset.
  • ADR-0087: exactly one marker, not-required (no-migration-prescription), in the gate's comment spelling. The body carries no FROM/TO table, no arrow rewrite between code spans, no migration heading (the only arrow-shaped bytes are the comment closer). The category is right: nothing objectstack migrate meta could rewrite moves — no authorable key, spelling, export or stored shape; runtime-interface-only / type-surface-only would need a path-qualified symbol referenced by no code, and DroppedFieldsEvent is a referenced zod surface; the packages publish; no registry id covers a write payload or a strip report. The gate itself runs in Lint & Repo Gates, in progress at read time.

③ Boundary flags

Dev flags, each answered:

  • H1 placement (strip at the declared-field door, pre-hook, ungated; report at the post-hook sites): right, for three reasons the code states — the strip must run for system writers, must not hand a hook a value that will not be stored, and must be callable by validate, which runs no hooks; joining the existing report sites is what makes strict's derived coverage hold without a second rule.
  • staticReadonlyCreateStrip extracted with behaviour unchanged: confirmed by reading (① item 10).
  • validate update mode — id is the address; readonlyWhen and primary-key strips not run: answered in ① item 4; a documented limit, not a copy and not a divergence from the card's "same doors as insert".
  • Memory-driver pins in the runtime integration file: right — @objectstack/runtime already dev-depends on driver-memory, the file is the standing driver-split consumer, and no ledger or census names the file, so no row moves.
  • main merged twice: confirmed (the first merge and the head are both merge commits); the net diff against main is the 14 files and nothing else.
  • NOT MEASURED — PostgreSQL: the pin obligation reads "where the package's matrix runs it". The one live-PostgreSQL job for @objectstack/runtime (Temporal Conformance, step "Run the runtime cascade-delete matrix against live PostgreSQL") selects a single file, cascade-delete-multivalue-lookup-real-driver, so this file has no PostgreSQL cell and the omission is within the card's clause; the strip is engine-side and never reaches a driver. ⚠️ The sentence "PostgreSQL is not a cell: no CI job provisions one for this package" — in the PR body and in the committed docblock of packages/runtime/src/sandbox/undeclared-field-write-driver-split.integration.test.ts — is FALSE as written: a CI job does provision one for this package. Escalated to the seat: correct it to "no CI job runs this file against PostgreSQL" in a follow-up, or add the file to that job's selection. Not a contract defect.
  • NOT MEASURED — objectui consumers of reason: ESCALATED. Console Pin Gate was skipped on this head because its filter watches only .objectui-sha, the console scripts and ci.yml, never packages/spec; so nothing on this PR measured whether the pinned objectui carries an exhaustive branch on DroppedFieldsEvent['reason']. Before landing, the owning seat greps ../objectui at the pinned .objectui-sha for DroppedFieldsEvent and readonly_when (the pre-merge check AGENTS.md prescribes for the pinned sibling). A Record keyed on the arm there reddens the pinned build the moment this merges and needs the sibling fix and the pin bump in the same landing; a reason-agnostic pass-through needs nothing.
  • Out-of-scope notes, each carrier: none:
  • open_questions: none in the report; none arise.

Check-runs on the head as read for this record (2026-09-30T10:00Z): 32 runs — 15 success (among them Check Changeset, Type Check · source gates, Build Docs, Spec property liveness, Governed Surface Queue Guard, the claim / branch / issue guards, Check PR Size, Check Documentation Links, Flag docs affected by code changes), 2 skipped (Console Pin Gate, the opt-in packed-tarball smoke), 15 in progress (Test Core 1–6, Dogfood Regression Gate 1–3, Dogfood Verify CLI, Temporal Conformance, Build Core, Lint & Repo Gates, Type Check consumer gates / debt ledger / workspace), 0 failed. Not polled. The in-progress families carry the gate verdicts for the tests, the lint gates (ADR-0087, the system-context census, the generated references) and the consumer typecheck; this record judges the contract, and the landing still needs every check green.

Implemented-by: claude/issue-20805-formula-write-strip
Reviewed-by: session_01DEvba2nBuD4tWzfq8r8NFY

VERDICT: PASS


Generated by Claude Code

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 protocol:data size/xl tests tooling

Projects

None yet

2 participants