Skip to content

Census the catch { return null; } durability swallow family, and repair the first two members (#12981) - #13397

Merged
os-elon merged 3 commits into
mainfrom
claude/issue-12981-swallow-family-census
Aug 30, 2026
Merged

Census the catch { return null; } durability swallow family, and repair the first two members (#12981)#13397
os-elon merged 3 commits into
mainfrom
claude/issue-12981-swallow-family-census

Conversation

@os-elon

@os-elon os-elon commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Part of #12981

⛔ Deliberately NOT a closing keyword. #12981 is a WORKLIST card covering the whole family — the ruling's own shape is "step one is a mechanical census … then batched repairs". This PR repairs 2 of the 28 tier-1 sites and enumerates the other 26 in §4; closing the card on merge would orphan that tail, leaving the worklist alive only inside a merged PR body.

Step one of the repair-first worklist the maintainer ruled on 2026-08-29 (verbatim 「同意」, option A): a mechanical census with positive controls enumerating the full membership, plus the first repair batch. The gate vocabulary is untouched in either direction, and scripts/durability-degradation.baseline.json keeps its designed empty steady state — not one entry added.


1. The census — scripts/measure-durability-swallow-family.mjs

A measurement instrument, not a gate: not wired into any workflow, exits 0 on any membership count, and deliberately not named check:* or gen:* so the #4203 script ledger has nothing to classify (the shape measure-partial-retirement-annotation.mjs established).

The predicate is the deliverable, not the list. A try/catch is a MEMBER when all three hold:

  1. SILENT CATCH — no log call at any level is reachable from the catch, following same-file helpers transitively.
  2. NO RETHROW — some path leaves the catch normally.
  3. AN AWAITED WRITE IS REACHED IN THE try — a callee from the declared WRITE_SHAPED_CALLEES vocabulary, directly or through same-file helpers.

Conjunct 3 is the durability filter, and it is what the card's "15" always was without ever being written down. Written down, it is measurable — see the ablation below.

Why a census may use a vocabulary the GATE may not

The gate's header excludes find/findOne/count as "names too generic to declare repo-wide", and insert/update/delete are in the same class. That exclusion is right FOR A GATE, because a gate BLOCKS and a disabled gate is worse than none. A census REPORTS, and its failure mode is the opposite: a member it cannot see is a member nobody repairs. So it over-collects on purpose, labels every bucket, and prints what it dropped.

AWAITED, the narrowing that earns its keep

temporalRewriteCache.delete(first) in formula/src/cel-engine.ts is a Map eviction spelled exactly like a driver delete. Type information would separate them; a full Program is not worth its cost here. The structural discriminator is that every driver and ObjectQL write in this repo is async and awaited, and no Map/Set/Array mutation ever is. 105 calls are dropped by it, and the count plus the per-name split is printed on every run — a narrowing nobody can see is indistinguishable from a matcher that stopped matching.

Measured, on 541f65eae

scanned                        2120 non-test source files under packages/
try/catch statements           2734
...guarding an awaited write    220
...of those, rethrowing          31   the failure propagates — not a member
...answering LOUD                32   error/fatal — already correct

MEMBERS                          72 sites in 42 files
  [1] DARK                       28 sites in 14 files   no error binding, or bound and never read
  [2] carries-error              25 sites in 20 files   the caught value leaves the catch
  [3] channelled                 19 sites in 10 files   #12923 refusal log / #5241 propagation

ADJACENT, not members: QUIET     85 sites   warn/info/debug over a swallowed write
dropped by AWAITED              105 calls   delete×59, upsert×21, update×19, insert×4, create×2

Tier 1 is the card's family and is mechanically decided. Tier 2 is where the judgement lives and it is not pretended otherwise: "the caller still reports success" is inter-procedural, and deciding it needs the call graph of every caller across package boundaries plus a judgement about whether the counter the caller increments is the one it later prints. Instead of faking that, the census prints the EVIDENCE per site — the enclosing function, and whether the file carries a quiet-level summary log outside any catch (the "RBAC catalog seeded" line that makes a zero-row pass read healthy) — and leaves the verdict to a person.

Controls, declared rather than derived from a run

A control read off the output only proves the instrument agrees with itself, so every one of these was chosen by reading code first. --self-test asserts them.

  • 4 positive, one per tier and one per channel: permission-set-projection.ts (channelled, fix(security): RBAC catalog seeder swallows unique-violation write failures — 'seeded 0' reported as success while a legacy index vetoes every row #12923's shared accumulator), primary-bu-projection.ts (channelled via the weakest channel, accumulator-reported), crud-nodes.ts (carries-error), bootstrap-system-capabilities.ts (dark — the card's shape verbatim, still standing).
  • 3 negative, every one of which matches the RAW syntactic shape the card's number came from: cel-engine.ts, pinyin-search-plugin.ts, zod-graph.ts. A membership number with no negative control is the same false green this whole card is about.
  • 1 regression, pinned at (file, function) rather than at a file: db-job-adapter.ts :: cancel. Its try block is await this.cron.cancel(name) — the CronJobAdapter's method. Resolving a dotted callee to a same-file body by its LAST segment walked into the file's OWN cancel(), reached setActive()'s engine.update(...) two frames on, and reported a cron-registry cleanup as a swallowed durability write. sameFileCallee now resolves only bare foo(...) and this.foo(...).

Reverse verification — direction predicted BEFORE the run

Prediction: ablating conjunct 3 (the durability filter) turns --self-test RED with all three negative controls firing.

Mutation proven on disk before anything was read. if (!write) return;if (!write) write = { callee: 'ABLATED', ... };, restore trapped on EXIT INT TERM against an absolute path:

HEAD blob    66b22bd29cc42289306fe514ed8d5714aa300ab5
before       66b22bd29cc42289306fe514ed8d5714aa300ab5   (tree confirmed at HEAD first)
guard count  1 → 0        marker 'ABLATED'  0 → 1
after        82a22028252639bbb9303d3f3ef265f69689c058   (≠ before, so the edit landed)

Observed: exactly the predicted direction. ABLATED_SELFTEST_EXIT=1, and the negative controls yielded 23 / 2 / 3 members respectively (plus the regression control). The durability filter is load-bearing and now measurably so.

Restore proven by state, not by exit code: blob back to 66b22bd2…, marker count 0, guard count 1, git diff HEAD empty, git status clean. No dist/ participates — the instrument is executed from source by node, so there is no rebuild leg to prove.


2. The repair batch — 2 files, and why only 2

plugin-sharingbackfillPrimaryBu

The exact shape #12970 repaired in permission-set-drift.ts, still standing here: a per-row catch { } that counted nothing, and a report gated on updated > 0. Together they made a pass in which EVERY write was refused print byte-identical output to a pass with nothing to do, while every affected user kept a stale or absent primary_business_unit_id and every sharing rule keyed on the primary business unit evaluated against the wrong value. Refusals are now counted, reported once with the consequence and the remedy, and the branch is updated > 0 || refused > 0.

⚠️ The report stays on warn and that is a decision, not an oversight. OptionalSharingLogger's own header ⛔ forbids growing an error (it would enrol every module on that type into check:optional-error-sink-contract's population at once, and that ledger is shrink-only). Giving this function a stricter sink instead means requiring warn on a publicly exported shape — the break scripts/optional-error-sink-contract.baseline.json records in as many words, citing the maintainer ruling of 2026-08-24 and the minor that shipped it for external hosts.

⚠️ That ledger names #10556 as the card carrying the contract call, and this PR originally routed the level there. #10556 is closed (completed 2026-08-24) — nothing in the ledger says so, which is a trap in the source rather than a reading error, but a card in that state cannot carry new work. The level question is now #13398, which carries the A/B/C option table and recommendation A, with B refused by OptionalSharingLogger's own header and C recorded as the end state needing a CONTRACT_REVIEW_TIER seat. #10556 remains the right citation for the PRECEDENT — how that break was shipped — and is not the route.

What is fixed here is the SILENCE, which needed no contract at all; the LEVEL is #13398's.

plugin-authensureDefaultOrganization

A refused sys_organization or sys_member insert leaves the platform admin with no organization: multi-org RLS filters their console to zero rows, single-org has no active org to invite into (ADR-0081 D1). Reported at warn. Both lines now report at error, each naming the consequence AND the remedy. BootstrapLogger gains an OPTIONAL error beside its already-required warn — additive, so a host passing { info, warn } compiles unchanged and gets the same line on the warn channel.

The fallback is spelled if (logger?.error) logger.error(m, undefined, meta); else logger?.warn?.(m, meta), re-derived from logSeedDurabilityFailure with both of its measured prohibitions: ⛔ never logger?.error?.(…) (prints nothing against a reduced sink), ⛔ never (logger.error ?? logger.warn)(…) (detaches the receiver and throws against a class sink while surviving every plain-closure double). check:logger-receiver-detach and check:optional-error-sink both pass.

The batch is 2 files because the census says the channel does not reach further — see §3.


3. PM mechanism assumptions — results

① Is the predicate mechanically expressible? PARTLY, and the split is now explicit rather than hidden. Conjuncts 1–3 are fully mechanical and give tier 1 (28 sites / 14 files). "The caller keeps reporting success" is NOT, and is reported as tier 2 with per-site evidence. No regex was tuned to return 15.

② Does #12923's shared reporter fit every member? NO — FALSIFIED, and more sharply than expected. createSeedWriteRefusals / reportSeedWriteRefusals / logSeedDurabilityFailure all live in packages/plugins/plugin-security/src/per-organization-catalog.ts and none of them is exported from that package's index.ts (its only seed-adjacent export is claimSeedOwnership). They are intra-package helpers by design, and the header of logSeedDurabilityFailure says so in as many words. So the channel does not merely "not fit" a non-seeder swallow — it is not reachable from any package outside plugin-security at all, and reaching it would mean a new runtime dependency on the security plugin plus an export change inside the package this card is fenced out of.

What IS transferable is the spelling, not the accumulator, and that is what the plugin-auth repair uses.

③ Do the 52 and the 15 differ only by the durability filter? YES. The raw multiline shape re-measured 52 files on 541f65eae — the same 52 as on aa16721b6, so the tree did not grow members. The gap is entirely the filter, now written down as conjunct 3, and the ablation above shows what removing it does.

Bonus falsification, for the record. Triage's confidence gap ③ ("I could not count the 18-entry vocabulary; my regex returned 0, which is my query being broken, not the vocabulary being empty") — it is 18, machine-counted.


4. The remainder, enumerated honestly

The 26 tier-1 sites this PR does not repair, and why each is left:

files why not here
plugin-securitybootstrap-system-capabilities.ts ×2, suggested-audience-bindings.ts ×4, cleanup-package-permissions.ts ×1 Serial fence. #13176 is in flight on this package's tsc program. Two PRs racing one package's typecheck program is how a green PR merges into a red main. These are the highest-value members left — bootstrap-system-capabilities.ts is the card's shape verbatim and the shared accumulator IS reachable in-package.
runtimeaction-execution.ts, domains/keys.ts Fenced (PR #13391).
plugin-authadmin-user-endpoints.ts, admin-import-users.ts A measured misfit, not a skip. Both swallow a sys_audit_log insert, and both carry a documented rationale: plugin-audit is OPTIONAL, so with it uninstalled there is no sys_audit_log table and a bare error fires on EVERY admin action — the mirror-image failure AGENTS.md names. The correct repair discriminates with the shipped isMissingTableError predicate, which is exported from @objectstack/metadata — a package plugin-auth does not depend on, at all. Adding that dependency is an architecture decision, not a mechanical repair. Routed: #13399, which records recommendation B (re-export the predicate from a package plugin-auth already depends on) together with the note that doing so widens a published surface and is therefore above the implementing seat.
plugin-authauth-manager.ts ×9, auth-plugin.ts ×2 Judgement-heavy: nine sites each carrying an explicit "best-effort — never break the auth response" rationale. Several deserve escalation on their own merits (recordSignInOutcome's lockout accounting silently not landing means brute-force protection silently does not work), and each needs its own argument against its author's. Nine at once inside a census PR is the failure mode triage warned about.
service-storagestorage-service-plugin.ts ×2 Composition branches (settings service absent) mixed with a probe cleanup — reads functional, not durability; wants a per-site read.
metadata-protocolprotocol.ts (reassignOrphanedMetadata) Real, and in a 17k-line file in a high-traffic package. Its own card.
plugin-sharingshare-link-service.ts Judged OUT: the swallowed write is a use_count / last_used_at telemetry stamp. Escalating a functional degradation to error is the over-application AGENTS.md forbids.
verifyharness.ts Test harness.

5. Verification — all on 541f65eae, the merged head this PR carries

  • pnpm --filter '@objectstack/plugin-auth^...' --filter '@objectstack/plugin-sharing^...' build, then both packages' own build — VERDICT command-exit 0.
  • plugin-sharing: typecheck + vitest run30 files / 678 tests passed; the new primary-bu-projection.test.ts run targeted — 1 file / 5 tests passed.
  • plugin-auth: typecheck (both legs, examples included) + vitest run85 files / 1729 tests passed, one locked run joined with && so the VERDICT covers every part.
  • ⚠️ Both packages' typecheck scripts exclude **/*.test.ts, so their green says NOTHING about the two test files this PR touches. Measured separately with a test-inclusive tsc --noEmit --listFiles: both files are confirmed IN the program (--listFiles grep, 1 hit each) and neither produces a single error. The 97 errors that run does report are pre-existing debt in other test files — 94 in plugin-auth, 3 in plugin-sharing, none in anything this PR wrote.
  • Full-repo pnpm lint (eslint . --no-inline-config), not a narrowed scan — VERDICT command-exit 0.
  • Gate families re-derived at edit time on the merged tree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (the first derivation warned STALE TREE; after merging origin/main the derivation is clean and the matched list is byte-identical). 37 families run locally, all green, including the two this change most plausibly moves: check:logger-receiver-detach and check:optional-error-sink. check:durability-log-level — the gate this card is about — is green and, per the standing premise, that green is NOT MEASURED for every file listed in §4.
  • Two results are NOT MEASURED (prerequisite not met), neither a red nor a finding: pm/check-half-states exits 3 for want of a GitHub token ("nothing was swept"), and check:i18n exited 1 with "PREREQUISITE NOT MET — the workspace CLI is not built / nothing was checked". The i18n prerequisite was then cleared with its own named turbo closure and re-run green (9 packages, all bundles in sync).
  • Exit codes captured before any pipe throughout (cmd > log 2>&1; EXIT=$?), and every verdict quoted above is the gate's own printed line, never a bare $?.

check:type-check-debt --re-measure is left to CI: it refuses without the whole workspace closure built, and a refusal is NOT MEASURED rather than a pass. Its structural half, check:type-check-coverage, is green here.

Authoring session (durable copy — the footer below is rewritten to its bare form by every body edit): https://claude.ai/code/session_012WkdHQwHr2KQmaX7P1BHzi

Generated by Claude Code

os-elon and others added 3 commits August 30, 2026 08:22
Adds `scripts/measure-durability-swallow-family.mjs`, a MEASUREMENT
instrument (not a gate, not in any workflow, not named `check:*`) that
enumerates the `catch { return null; }` family the durability log-level
gate is structurally blind to.

The predicate, not the list, is the deliverable: a silent catch, no
rethrow, and an AWAITED write-shaped callee reached in the guarded block.
Members split into three tiers by what survives the catch — dark,
carries-error, channelled — because "the caller still reports success" is
inter-procedural and is not pretended to be mechanical.

Controls are declared, not derived from a run: 3 positive (one per tier),
3 negative (files matching the raw syntactic shape with no durability
seam), 1 regression.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012WkdHQwHr2KQmaX7P1BHzi
…ing as a clean one

plugin-auth `ensureDefaultOrganization`: a refused sys_organization or
sys_member insert leaves the platform admin with no organization while the
boot goes on looking healthy. Both lines move from `warn` to `error` and name
the consequence and the remedy. `BootstrapLogger` gains an OPTIONAL `error`
beside its required `warn`, with the mandatory fallback in one helper.

plugin-sharing `backfillPrimaryBu`: per-row refusals were uncounted and the
report was gated on `updated > 0`, so a fully refused backfill printed nothing
at all. Refusals are counted, reported once, and the gate is now
`updated > 0 || refused > 0` — the #12970 `permission-set-drift.ts` repair.

The census instrument learns the accumulator channel so a caller-side repair
is visible to a re-run.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-auth, @objectstack/plugin-sharing, touching 6 documentable anchor(s).

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

  • content/docs/deployment/seed-tenancy-repair.mdx (via sys_organization (literal))
  • content/docs/deployment/tenancy-modes.mdx (via sys_member (literal))
  • content/docs/permissions/administrator-guide.mdx (via sys_organization (literal))
  • content/docs/permissions/authentication.mdx (via sys_member (literal))
  • content/docs/permissions/delegated-administration.mdx (via sys_member (literal))
  • content/docs/permissions/permission-sets.mdx (via sys_member (literal))
  • content/docs/permissions/positions.mdx (via sys_member (literal))
  • content/docs/protocol/kernel/config-resolution.mdx (via sys_organization (literal))
  • content/docs/protocol/objectql/schema.mdx (via sys_organization (literal))

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

  • content/docs/releases/implementation-status.mdx (via sys_member (literal), sys_organization (literal))
  • content/docs/releases/v16.mdx (via sys_member (literal))
  • content/docs/releases/v17.mdx (via sys_member (literal), sys_organization (literal))

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
  • 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 — 16 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 83546d97053009cc84b90e1addeee7163d99911cpackageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 83546d97053009cc84b90e1addeee7163d99911c

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 30, 2026
@os-elon
os-elon marked this pull request as ready for review August 30, 2026 09:17
@os-elon
os-elon enabled auto-merge August 30, 2026 09:17
@os-elon
os-elon added this pull request to the merge queue Aug 30, 2026
Merged via the queue into main with commit 35202f1 Aug 30, 2026
40 checks passed
@os-elon
os-elon deleted the claude/issue-12981-swallow-family-census branch August 30, 2026 09:36
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/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants