Uh oh!
There was an error while loading. Please reload this page.
Repair three durability swallows in metadata-protocol and service-storage — batch 7 of the #12981 worklist - #13725
Conversation
…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📓 Docs Drift CheckThis PR changes 2 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
Uh oh!
There was an error while loading. Please reload this page.
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, base2cce3fd30. 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).Two different terminal shapes, on purpose:
protocol.tsstays 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.storage-service-plugin.tssites leave MEMBERS entirely for the adjacent QUIET bucket, because their reports sit inside the catch. Its third site (:532, previouslysilent/carries-error) left with them: the nested repair put awarninside its block.Site 1 —
metadata-protocol/src/protocol.tsreassignOrphanedMetadataThe purest remaining member, and the only durability one in this batch. ADR-0070 D5's orphan-adoption loop dropped a refused
sys_metadataupdate whole — not logged, not rethrown, not carried on the response — and the return reportssuccess: 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
failedCountwas 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.tstrycaught 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, becausestart()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.storage/testprobe 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 reopenedThe
[#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 alogger.erroron, and says the correct declaration is aFAILURE_PROPAGATION_SITESentry belonging to the LAST step of this programme — the one that widensDURABILITY_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 publishedandthe sink type does not declare
error— raising the level there meant widening a publishedlogger?: { warn(msg: string): void }, which enrols the module into the shrink-onlycheck:optional-error-sink-contractpopulation. 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).error?protocol.tsreassignOrphanedMetadatapackages/metadata-protocol/src/index.ts:3—export { ObjectStackProtocolImplementation, … } from './protocol.js';consoleglobalerrorstorage-service-plugin.tssettings bindingpackages/services/service-storage/src/index.ts:3—export { StorageServicePlugin } from './storage-service-plugin.js';ctx.logger, i.e.PluginContext.logger: Loggerpackages/spec/src/contracts/logger.ts:9declareserror(message, error?, meta?): voidnon-optional (onlyfatal?is optional)warnstorage-service-plugin.tsprobe cleanupwarnClause ② 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.
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 toerroron a published module (auth-plugin.ts,index.ts:11export *) for exactly this reason — "through the kernel Logger, whoseerroris required."consoleis not a declared sink type inpackages/**at all, so it cannot enrol anything. If the PM reads the compressed test as binding, the remedy is one word inprotocol.tsand I will make it — I have not treated my own reading as clearance.The
errorat site 1 is also decided on the merits and against this file's own recorded doctrine, not by default.console.warnis the file's overwhelming idiom (51 sites), andclientFacingRowFailureTextwrites down why: "console.warnand noterrordeliberately — nothing claimed to be persisted was silently dropped (the row reportssuccess: falseand the counters reconcile)." AtreassignOrphanedMetadataneither half holds. The matching precedent is 1000 lines away in the same file:recordPackageCommitalready answersconsole.errorfor a refusedsys_metadata_commitwrite under a publish that reports success.Both
service-storagesites arewarnon 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 makeserrorunreadable.check-durability-degradation-log-level.mjs— MEASURED as NOT MEASUREDNot merely observed green after. Run both ways through the ablation harness, and its verdict line is byte-identical:
⇒ The gate sees no seam at all in
reassignOrphanedMetadata— its vocabulary carries noupdate. 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.json— untouched, 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 declareIDataEngine, so the gate's population did not grow. It routes throughassertEngineUpdateDispatchfrom@objectstack/metadata-coreregardless.scripts/slot-lookup-baseline.json— one line, ratcheted DOWN6 → 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: runpnpm check:slot-lookup --updateand 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 atrap … EXIT INT TERMrestore with absolute paths:Ablated run — 5 failed, and they are exactly the 5 that should redden:
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 HEAD0 lines,git status --porcelain0 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
distleg applies to the pins — both import a relative sibling (./protocol.js,./storage-service-plugin.js), which vitest resolves tosrc/. The dependency closures were built first (pnpm --filter 'PKG^...' build), and both packages plus@objectstack/service-datasourcewere rebuilt before the cross-package consumer tests below, which do resolve throughdist/.Suites
pnpm --filter @objectstack/metadata-protocol testpnpm --filter @objectstack/service-storage testobjectqlprotocol-package-lifecycleruntimepackages-capability-gate+http-dispatcher.keys(the site-4 pin)Typecheck — measured directly, because neither package HAS a
typecheckscriptpnpm --filter PKG typecheckhere would match zero scripts and exit 0, which reads as a pass and measures nothing. Bothtsconfig.jsonsincludesrc/**/*with no test exclusion, sotsc --noEmit -pwas run directly, and--listFilesconfirms it really read the new files (1 and 3 hits). Attribution:Both counts are frozen debt this diff does not move — the ratchet that judges it,
check:type-check-debt --re-measureon 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 — 16pnpm, 15 directnode— plus the convention-triggered kind gates for adds a test file, edits a package owning ani18n-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 thecheck-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-testscripts.Every ratchet-family gate was re-run after the final commit and reports against
88127211con 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 savedturbo run testlog 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.mjson 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.
plugin-auth/src/auth-manager.tsverifyMcpAccessTokentryholds no write;return nullis right for an expired token. ⛔ Do not repairplugin-auth/src/ensure-default-organization.tsruntime/src/domains/keys.tsFAILURE_PROPAGATION_SITESentry that belongs to the LAST step of the programmeplugin-sharing/src/share-link-service.tsverify/src/harness.ts⇒ 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_CALLEESand declaringkeys.ts::handleKeysRequestinFAILURE_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