Skip to content

Repair three durability swallows in metadata-protocol and service-storage — batch 7 of the #12981 worklist - #13725

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-12981-batch7-swallow-family
Aug 31, 2026
Merged

Repair three durability swallows in metadata-protocol and service-storage — batch 7 of the #12981 worklist#13725
os-steve merged 2 commits into
mainfrom
claude/issue-12981-batch7-swallow-family

Conversation

@os-steve

@os-steve os-steve commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Part of #12981 — batch 7 of the swallow-family worklist. ⛔ Not a closing keyword: the DARK list is not empty, so merging this closes nothing.

Head 88127211c, base 2cce3fd30. Three tier-1 DARK sites repaired across two packages; a fourth in scope was read and left alone, on its own recorded instruction.

The ledger is the INSTRUMENT

scripts/measure-durability-swallow-family.mjs, self-test green before every reading below (4 positive controls at their declared tier, 3 negative yield none, 2 regression controls clear).

                     before        after
MEMBERS              59 / 38 f     56 / 37 f
  [1] DARK            8 /  7 f      5 /  5 f
  [2] carries-error  25 / 20 f     24 / 19 f
  [3] channelled     26 / 13 f     27 / 14 f
  QUIET (adjacent)   95            98

Two different terminal shapes, on purpose:

  • protocol.ts stays a MEMBER and moves DARK → channelled (silent/channelled:accumulator-reported). Its catch is a loop body, and AGENTS.md requires the report be stated once rather than once per failed write, so the refusal is counted in the catch and reported after the loop. That is the shape of the census's own declared positive control (plugin-security/src/permission-set-projection.ts, the fix(security): RBAC catalog seeder swallows unique-violation write failures — 'seeded 0' reported as success while a legacy index vetoes every row #12923 accumulator), not a workaround.
  • Both storage-service-plugin.ts sites leave MEMBERS entirely for the adjacent QUIET bucket, because their reports sit inside the catch. Its third site (:532, previously silent/carries-error) left with them: the nested repair put a warn inside its block.

Site 1 — metadata-protocol/src/protocol.ts reassignOrphanedMetadata

The purest remaining member, and the only durability one in this batch. ADR-0070 D5's orphan-adoption loop dropped a refused sys_metadata update whole — not logged, not rethrown, not carried on the response — and the return reports success: reassigned.length > 0. An adoption in which 99 of 100 orphans were refused answered { success: true, reassignedCount: 1 }, a response identical in shape to a healthy run with exactly one orphan to move, while the 99 stayed orphans with nothing retrying them and no record they had been tried.

The loop now counts refusals and states the degradation once after it, naming the count, the target package, the driver's own sentence and the fix. Control flow and the response shape are untouched — no failedCount was added, and a row that cannot be rebound still must not abort the rows that can. Both halves are asserted, not assumed.

Sites 2 and 3 — service-storage/src/storage-service-plugin.ts

  • The settings-namespace binding. One try caught two outcomes and the comment named one. The settings service being absent (a bare kernel) is now resolved on its own line and stays correctly silent; a binding that fails with the service present is reported, because start() otherwise completes into a healthy-looking boot whose storage settings screen is wired to nothing — an operator's adapter change is saved and never applied.
  • The storage/test probe cleanup. catch { /* ignore */ } over a delete. The result returned beside it reports the probe's failure, a different failure; one stray __objectstack_probe__/… key accrued per failed test and the only record of its name, minted per call from a timestamp and a random suffix, died with the frame.

Site 4 — runtime/src/domains/keys.ts: READ, and deliberately NOT reopened

The [#12981] annotation it already carries records a prior decision not to repair, in its own words: "This catch is silent BY DESIGN and it is NOT a durability swallow… every path out of this catch hands the failure to the caller as a 500 envelope and no key material is returned, so nothing claims to have persisted." It names AGENTS.md's third legal answer, forbids bolting a logger.error on, and says the correct declaration is a FAILURE_PROPAGATION_SITES entry belonging to the LAST step of this programme — the one that widens DURABILITY_CRITICAL_CALLEES — because declared before that step it would go red as STALE. Not touched. Its delivery pin (packages/runtime/src/http-dispatcher.keys.test.ts) was run and is green: 2 files, 96 passed.

Level calls — decided PER SITE, with the measured export line

⛔ Batch 6's answer was not carried forward. Its load-bearing conjunction was published and the sink type does not declare error — raising the level there meant widening a published logger?: { warn(msg: string): void }, which enrols the module into the shrink-only check:optional-error-sink-contract population. That is the harm #13398's 2026-08-30 ruling refused (transcribed in PR #13592's body, since the card itself does not resolve — #13634).

site module re-exported? the measured line the SINK declares error? level shape widened?
protocol.ts reassignOrphanedMetadata yes packages/metadata-protocol/src/index.ts:3export { ObjectStackProtocolImplementation, … } from './protocol.js'; the console global yes, always error no
storage-service-plugin.ts settings binding yes packages/services/service-storage/src/index.ts:3export { StorageServicePlugin } from './storage-service-plugin.js'; ctx.logger, i.e. PluginContext.logger: Logger yes — packages/spec/src/contracts/logger.ts:9 declares error(message, error?, meta?): void non-optional (only fatal? is optional) warn no
storage-service-plugin.ts probe cleanup yes same line same same warn no

Clause ② does not fire: no published sink shape is changed at any of the three sites. No type declaration is added, removed or widened anywhere in this diff.

⚠️ One declared divergence from the dispatch, flagged rather than self-cleared. The brief compressed the rule to module re-exported ⇒ do not raise the level. All three modules are re-exported, so under the compressed test all three would be warn. I measured the sink instead, because that is what the ruling's harm mechanism turns on and what batch 6 actually recorded, and because batch 5 already raised to error on a published module (auth-plugin.ts, index.ts:11 export *) for exactly this reason — "through the kernel Logger, whose error is required." console is not a declared sink type in packages/** at all, so it cannot enrol anything. If the PM reads the compressed test as binding, the remedy is one word in protocol.ts and I will make it — I have not treated my own reading as clearance.

The error at site 1 is also decided on the merits and against this file's own recorded doctrine, not by default. console.warn is the file's overwhelming idiom (51 sites), and clientFacingRowFailureText writes down why: "console.warn and not error deliberately — nothing claimed to be persisted was silently dropped (the row reports success: false and the counters reconcile)." At reassignOrphanedMetadata neither half holds. The matching precedent is 1000 lines away in the same file: recordPackageCommit already answers console.error for a refused sys_metadata_commit write under a publish that reports success.

Both service-storage sites are warn on the merits too. Neither is a durability degradation: storage keeps serving from the adapter the plugin's own options built, and the leaked probe object is inert content no record references. AGENTS.md is explicit that escalating a functional degradation is what makes error unreadable.

⚠️ check-durability-degradation-log-level.mjs — MEASURED as NOT MEASURED

Not merely observed green after. Run both ways through the ablation harness, and its verdict line is byte-identical:

AFTER  (88127211c) ✓ 29 durability-critical catch seam(s) … (4 propagating, declared); 2 counter-guarded summary report(s) outside a catch
BEFORE (2cce3fd30) ✓ 29 durability-critical catch seam(s) … (4 propagating, declared); 2 counter-guarded summary report(s) outside a catch

⇒ The gate sees no seam at all in reassignOrphanedMetadata — its vocabulary carries no update. The new counter-guarded report is not among the 2 it counts. ⛔ Its green is NOT MEASURED for this family, never "level approved," in either direction.

Ratchet ledgers

  • scripts/durability-degradation.baseline.json — untouched, still empty. ⛔ No row added.
  • scripts/engine-double-contract.pinned.jsonuntouched, and it did not need to be. 723 pinned, 134 in the DEBT ledger, 3 exempt / 663 (file, verb) row(s) before and after: the new two-verb double does not declare IDataEngine, so the gate's population did not grow. It routes through assertEngineUpdateDispatch from @objectstack/metadata-core regardless.
  • scripts/slot-lookup-baseline.json — one line, ratcheted DOWN 6 → 5, which the gate's own verdict line asked for by name: "packages/services/service-storage/src/storage-service-plugin.ts: erasure count fell 6 → 5 — ratchet DOWN: run pnpm check:slot-lookup --update and commit the baseline."

Verification

Reverse-verified, not asserted. The repair was committed first (9dd2ba94b) so the restore leg had a real reference. The mutation reverted both sources to the merge base and was confirmed on disk before anything was measured — never by an editor's exit code, and the mutation script carries a trap … EXIT INT TERM restore with absolute paths:

PRE  hashes  A=03be73cf…  B=51f62372…   (asserted equal to the HEAD blobs first)
POST hashes  A=7e3ddb6d…  B=96f307ad…   (asserted != PRE and == the base blobs)
PRE  repaired markers  A(rebound)=1  B(binding)=1  B(probe)=1
POST repaired markers  A(rebound)=0  B(binding)=0  B(probe)=0
POST swallow  markers  A(skip-a-row)=1  B(not-present)=1  B(ignore)=1
MUTATION CONFIRMED ON DISK

Ablated run — 5 failed, and they are exactly the 5 that should redden:

metadata-protocol   3 failed | 2 passed
  × reports a PARTIAL adoption — the run that still answers success: true
  × reports a TOTAL refusal, where the response already says success: false
  × states the degradation ONCE, not once per refused row
service-storage     2 failed | 2 passed
  × a binding that FAILS with the service PRESENT is reported, and names what stops working
  × a refused probe cleanup is reported, and names the stray key

The other 4 are declared CONTROLS, not pins, and are named as such in each file's header: two absence-assertions against seams that logged nothing at all before this repair (a healthy adoption reports nothing; a bare kernel stays silent; a cleanup that succeeds says nothing) and two invariance assertions (a refused row does not abort the rest, and the response shape is unchanged). They stay green in both directions by construction. ⛔ They are not counted as ablation evidence.

Restore proved by state: git diff HEAD 0 lines, git status --porcelain 0 entries, both worktree hashes equal to their HEAD blobs, all three markers back at 1. Both pins re-run green on the restored tree (5 passed, 4 passed).

No dist leg applies to the pins — both import a relative sibling (./protocol.js, ./storage-service-plugin.js), which vitest resolves to src/. The dependency closures were built first (pnpm --filter 'PKG^...' build), and both packages plus @objectstack/service-datasource were rebuilt before the cross-package consumer tests below, which do resolve through dist/.

Suites

run result
pnpm --filter @objectstack/metadata-protocol test 147 files passed, 2 skipped · 2045 passed, 10 skipped
pnpm --filter @objectstack/service-storage test 34 files · 522 passed
consumers: objectql protocol-package-lifecycle 1 file · 10 passed
consumers: runtime packages-capability-gate + http-dispatcher.keys (the site-4 pin) 2 files · 96 passed

Typecheck — measured directly, because neither package HAS a typecheck script

pnpm --filter PKG typecheck here would match zero scripts and exit 0, which reads as a pass and measures nothing. Both tsconfig.jsons include src/**/* with no test exclusion, so tsc --noEmit -p was run directly, and --listFiles confirms it really read the new files (1 and 3 hits). Attribution:

metadata-protocol   63 pre-existing error(s); protocol.ts 0; the new test file 0
service-storage     51 pre-existing error(s); storage-service-plugin.ts 0; the new test file 0

Both counts are frozen debt this diff does not move — the ratchet that judges it, check:type-check-debt --re-measure on the built closure, reports 29 ledger entries, 1542 raw tsc errors, none above its recorded number, surplus: none. (Two TS2347s the new test file did introduce were fixed at their cause before the final commit, not baselined.)

Gates

Re-derived from the actual diff in this worktree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (both output sections read whole, per the #13642 consumption trap): 31 path-derived families — 16 pnpm, 15 direct node — plus the convention-triggered kind gates for adds a test file, edits a package owning an i18n-extract.config.ts, and carries an ADR-0112 code. All exit codes captured before any pipe; every verdict below is the gate's own line.

Green: pnpm lint (eslint . --no-inline-config, whole repo, run in full — no narrowing to declare), check:slot-lookup, check:objectql-double-limit, check:where-matcher, check:engine-double-contract, check:query-options-erasure, check:type-check-coverage, check:type-check-debt, check:cross-package-test-inputs, check:i18n (after clearing its declared build prerequisite — 9 packages, all bundles in sync), check:i18n-stale-fill, check:dual-build-cjs-loads (after the full workspace build it demands), check:durability-log-level, check:dispatcher-error-vocabulary, check:nul-bytes, check:changeset-gate-self-tests, check:doc-authoring, check:filter-alias-parity, check:logger-receiver-detach, check:objectui-changeset, check:page-declaration-shape, check:pm-half-states, check:published-files, check:test-source-alias, check:type-source-resolution, and the check-adr-0087-registration / check-changeset-no-major / check-ci-filter-parity / check-comment-mask-adoption / check-cross-package-test-inputs / check-empty-changeset / check-keyed-text-bounds / check-plugin-teardown-shape / check-shard-attestation / check-undeclared-dep-imports / docs-audit/check-affected-docs / pm/release-rehearsal-clone --self-test scripts.

Every ratchet-family gate was re-run after the final commit and reports against 88127211c on a clean tree.

NOT MEASURED — neither a pass nor a red:

  • node scripts/check-test-completeness.mjs — exit 3, PREREQUISITE NOT MET. It grades a saved turbo run test log and the derived family names it with no argument. Its own text: "running the family locally, record this gate as NOT MEASURED… it is not a red, and there is nothing here to fix." CI tees a real log.
  • check-durability-degradation-log-level.mjs on this family — measured both ways above, identical.

Remaining DARK — the next batch's ledger

5 sites in 5 files, of which 0 are outstanding repairs in this domain.

site disposition
plugin-auth/src/auth-manager.ts verifyMcpAccessToken census FALSE MEMBER, standing determination from batch 6. Its try holds no write; return null is right for an expired token. ⛔ Do not repair
plugin-auth/src/ensure-default-organization.ts FENCED — PR #13685 (#11973 L3) is editing this file
runtime/src/domains/keys.ts CLOSED by its own annotation — a recorded prior decision NOT to repair, re-read and re-confirmed this batch. Its remaining action is a FAILURE_PROPAGATION_SITES entry that belongs to the LAST step of the programme
plugin-sharing/src/share-link-service.ts batch 8 — usage telemetry, may not be a claim-to-persist
verify/src/harness.ts batch 8 — a harness whose gate answers either way, may not be a claim-to-persist

⇒ Batches 1–7 have repaired every DARK site that is a repair. What batch 8 inherits is a determination, not a repair: decide whether the last two are claims-to-persist at all, and if not, record it and move them out of DARK. After that the only open item is the programme's final step — widening DURABILITY_CRITICAL_CALLEES and declaring keys.ts::handleKeysRequest in FAILURE_PROPAGATION_SITES, which its annotation says must land together.


Authoring session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs (also on both commits as a Claude-Session: trailer).

Generated by Claude Code


Generated by Claude Code

claude added 2 commits August 31, 2026 07:37
…ree catch blocks swallowed

Three tier-1 DARK sites from the #12981 swallow-family worklist (batch 7),
across two packages. Control flow is unchanged at every one of them, and none
of them is silent any more.

metadata-protocol — reassignOrphanedMetadata. ADR-0070 D5's orphan-adoption
loop dropped a refused sys_metadata update whole. The return reports
`success: reassigned.length > 0`, so an adoption in which 99 of 100 orphans
were refused answered `{ success: true, reassignedCount: 1 }` while the 99
stayed orphans with nothing recording that they had been tried. The loop now
counts refusals and states the degradation ONCE after the loop at
console.error, naming the count, the target package, the driver's sentence and
the fix. `error` and not this file's usual console.warn, by the AGENTS.md
question: the system keeps looking normal while something it claims to have
persisted did not land. Same verdict recordPackageCommit already reaches in
this file, and the inverse of clientFacingRowFailureText's recorded reason for
choosing warn. The response shape is untouched.

service-storage — two functional sites at the tail of start(), both warn. The
settings-namespace binding caught two outcomes and named one: absence (a bare
kernel, correctly silent) is now resolved on its own line, and a binding that
fails with the service PRESENT is reported, because start() otherwise completes
into a healthy-looking boot whose storage settings screen is wired to nothing.
The storage/test probe cleanup swallowed its own failure; the result beside it
reports the PROBE's failure, a different one, so a stray probe key accrued per
failed test with the only record of its name lost. Both are warn on the merits:
neither is a durability degradation, and AGENTS.md is explicit that escalating
these is what makes error unreadable.

No sink type is changed at any of the three sites — PluginContext.logger
declares error non-optional, and metadata-protocol reports on console — so no
published sink shape is widened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
Three gate verdicts, each repaired at its own cause rather than at a ledger:

- check:slot-lookup — splitting the settings lookup from its declaration is
  the rule's own FOURTH erasure shape (`let x: any; try { x = getService(…) }`).
  Named the slice this plugin actually uses as StorageSettingsSlot, which also
  retires the pre-existing `getService<any>` there, so the file's count falls
  6 -> 5. The gate's verdict line asked for the DOWNWARD ratchet by name:
  "erasure count fell 6 -> 5 — ratchet DOWN: run `pnpm check:slot-lookup
  --update` and commit the baseline." One line in the baseline, shrink only.
  Two `as any` service lookups in the new test are typed at their source.

- check:objectql-double-limit — the new `find` double filtered on a `where`
  it only half implemented, so the control probe could not seat it (UNJUDGED).
  It now REFUSES both the `where` combinators and the `limit` it does not
  implement, which is the convention the gate's own remedy names and the
  stronger of the two: every case here adopts env-wide orphans, so the
  producer passes `{}` and no bound.

- tsc — `ctx.getService<T>(…)` on the `any`-typed fake ctx is a type argument
  on an untyped call (TS2347). The storage service is typed once, at the
  harness, by plain assignment: neither a type argument nor an `as any`.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/service-storage, touching 5 documentable anchor(s).

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

  • content/docs/protocol/kernel/config-resolution.mdx (via createClient (symbol))
  • content/docs/ui/actions.mdx (via registerAction (symbol))
What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 13 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 cc837dbfec53401c03b2d89071c8b4bee1e5ac97packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json cc837dbfec53401c03b2d89071c8b4bee1e5ac97

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

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