Skip to content

fix(plugin-sharing): let system writes materialize sharing rules — drop the isSystem skips in bindRuleHooks (#13533) - #14528

Merged
os-sales merged 10 commits into
mainfrom
claude/issue-13533-system-write-sharing-materialization
Sep 3, 2026
Merged

fix(plugin-sharing): let system writes materialize sharing rules — drop the isSystem skips in bindRuleHooks (#13533)#14528
os-sales merged 10 commits into
mainfrom
claude/issue-13533-system-write-sharing-materialization

Conversation

@claude

@claude claude Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #13533

A criteria sharing rule declares a promise — status == "approved" means the named recipients can see the record. bindRuleHooks did not keep that promise when the platform was the writer, so an approval write-back left a teammate unable to see an approved record until somebody ran POST /api/v1/sharing/rules/:id/evaluate or restarted the server. This removes the skips that caused it.

Implements the maintainer ruling of 2026-08-31 (issue comment 5472662038), quoted verbatim and untranslated:

裁定:系统写参与逐记录共享物化 —— 删除 plugin-sharing 两个钩子里的 isSystem 跳过,⛔ 不加声明式开关、不以文档代修。

The approval path, cited

The write the card is about is mirrorStatusField in packages/plugins/plugin-approvals/src/approval-service.ts (called from the decision, recall, reject and return paths). It writes:

const context = actorId ? { ...SYSTEM_CTX, userId: actorId } : SYSTEM_CTX;
await this.engine.update(object, { id: recordId, [field]: status }, { context });

SYSTEM_CTX is { isSystem: true, positions: [], permissions: [] }. Elevation is not incidental here: an approval node with lockRecord: true means only a platform write can land while the record is locked, so this is the only write that can carry the decision onto the record. plugin-approvals is read-only in this PR — cited, not edited.

What changed

Three early returns are gone from bindRuleHooks, not two.

site hook why
afterInsert the ruling's first named skip a system insert into a rule's criteria owes the grant
afterUpdate the ruling's second named skip this is the approval write-back
stashAffectedRows (beforeUpdate / beforeDelete) the second gate on the same path see below

The third one is load-bearing, and removing only the two named skips would have shipped a different defect. afterUpdate reads its row set through readAffectedRows, which answers { kind: 'unbounded', reason: 'resolve-failed', detail: 'no before-hook stash' } when no before hook stashed. With the stash still skipping system writes, every single-row system update — every approval — would have taken revokeThenQueueRegrant: an object-wide revoke of every rule grant plus an asynchronous re-grant, with a warn per approval. Ablation leg B below measures exactly that. Resolving is nearly free on this shape anyway: a write that names its row short-circuits in resolveAffectedRows step 1 without querying.

The SYSTEM_WRITE_SKIP_NOTICE channel retires with the skips. No declarative switch was added and no documentation substituted for the fix, per the ruling.

What deliberately did not change

afterDelete keeps its isSystem skip. Its payload is revocation, not materialisation, and record-share-cascade.ts delivers that on every sharing-capable object, stashing for system writes on its own account (#5103), with the boot orphan sweep behind it. This is one subscriber declining work another subscriber owns, not elevation used as a blanket silencer — the distinction the ruling itself draws.

bu-tree-recompute.ts is untouched. Its docblock describes the opposite disposition and is a different subject: BU-tree recompute deliberately does not skip system writes, because its payload is revocation on a path whose realistic production trigger (an HRIS or directory sync) is itself a system write. Nothing there was a materialisation skip, so nothing there was in the ruling's scope.

The kernel:bootstrapped boot backfill stays. It is no longer needed for seed rows, but it still reaches rows no hook saw — written before an object's hooks were bound, or while a rule was inactive — and it is the only pass that purges a deactivated rule's grants (#4433).

Acceptance anchor: the member perspective, without viewAllRecords

Triage's binding note (issue comment 5479116055) is that this defect is observable only to a principal without viewAllRecords who depends on the sharing rule. A manager or admin reads through the profile path, never consults the rule, and sees the record either way — "the manager sees it within 18 s" is true and is not a counter-proof.

The acceptance pins therefore end at SharingService.buildReadFilter for a plain member context and then run that filter against the table, so the assertion is that the teammate can see the record, not merely that a row appeared in sys_record_share:

  • before the write-back, buildReadFilter for the member is { owner_id: 'rep2' } and the record is not admitted;
  • after it, { $or: [ { owner_id: 'rep2' }, { id: { $in: ['lr1'] } } ] } and the record is admitted — additive access, the owner match still present;
  • and a companion pin asserts the approver's filter is null both before and after, which is the reproduction constraint stated as a test: verifying this fix from a manager view proves nothing.

Controls in the same file: a non-system write still materialises (the user path did not move); a system write that does not satisfy the criteria grants nothing; a system write on an object no active rule covers is a no-op; a recall revokes again.

The bulk-path census (ruling point 2), measured before disposal

The ruling forbids keeping the skip on unmeasured performance fear, and asks for the bulk system-write paths to be censused first. Measured on the merged tree:

path system write? reaches rule-covered objects? bound trailing batch already?
SeedLoader (packages/metadata-protocol/src/seed-loader.ts, SEED_OPTIONS = isSystem + skipTriggers + seedReplay) yes yes, any author-declared dataset BULK_BATCH_SIZE 200 per flush, dataset unbounded yesbackfillRuleGrants at kernel:bootstrapped, after every kernel:ready handler settles
claimSeedOwnership (plugin-security) yes yes, every non-sys_, non-managed, non-external object declaring owner_id two scans at limit: 10_000, written as single-id updates in a loop at boot, yes (same backfill); on its runtime replay, no
REST import runner (packages/rest/src/import-runner.ts) nowriteCtx is the importer's own context n/a n/a already per-record today; untouched by this card
data migrations (metadata-protocol/src/migrations/*) yes nosys_/cloud_/ai_ namespace only n/a n/a
plugin-sharing's own grant writes yes no — hooks bind only on objects carrying an active rule, and sys_record_share carries none n/a n/a

skipTriggers does not suppress these hooks: engine.ts skips only metadata-bound hooks and only under skipAutomations (if (skipAutomations && entry.meta)), and seed writes set skipTriggers. So the seed path really does reach bindRuleHooks.

Disposition: no new batch code. The ruling's remedy — replace per-record with one trailing batch on the bulk path — already exists for both shapes the hooks can see, and removing the skips is what makes system writes reach it:

  • a write the hooks can see as bulk (predicate/multi) over RULE_RECOMPUTE_ROW_CAP (1000) already takes revokeThenQueueRegrant into evaluateAllRulesForObject;
  • boot-phase seeding already has backfillRuleGrants reconciling every rule behind it.

What remains is per-record cost on single-row writes, and that is not new amplification: it is the cost a user write of the same shape has always paid. An executable pin (#13533 census: a system write costs exactly what the same user write costs) measures the two populations against each other rather than asserting an absolute — a 25-row system insert batch performs exactly the reads a 25-row admin batch performs, and the cost is linear in the batch. Adding a batch shortcut keyed on isSystem would reintroduce precisely the special case the ruling condemned. The one path the hook layer cannot help is claimSeedOwnership's loop of single-id writes, which is a writer-side shape in another package; it is reported to the PM rather than folded in.

Reverse verification

Both legs mutate the committed tree, prove the mutation on disk before reading anything, and prove the restore afterwards. No rebuild is involved or needed: the suites import ./rule-hooks.js, a same-package relative specifier vitest resolves to src/, and the mutation turning tests red with no rebuild is itself the proof that no dist/ sits in the resolution path.

Leg A — restore the afterUpdate skip. Marker present x1, blob moves to cb675403… off HEAD 23baa61d…. Result: Tests 5 failed | 44 passed (49) — the two approval write-back acceptance pins, both bulk-update directions, and the reversed bulk-recompute case. Every control stayed green.

Leg B — restore only the before* stash skip, leaving both after skips removed. Marker present x1, blob b8e9c653…. Result: Tests 4 failed | 45 passed (49), and the discriminating one is takes the BOUNDED per-record branch, never the object-wide revoke, which reds only in leg B. That is the measurement behind the third removal.

Baseline unmutated: Tests 49 passed (49). After every leg the restore is proved by blob equality with the HEAD blob plus an empty git diff HEAD, with an empty hash treated as failure rather than as "nothing to compare".

Verification

All readings at 376c04e00, after the last commit and after merging origin/main (7251bdb30). Re-run in full for patch round 1 — see that section for what moved.

  • pnpm --filter @objectstack/plugin-sharing testTest Files 30 passed (30), Tests 718 passed (718) (712 before patch round 1; main's fix(plugin-sharing): report a refused resolveToken usage stamp once as a durability degradation — #12981 batch 9 #14383 added six share-link cases).
  • pnpm --filter @objectstack/plugin-sharing typecheck — clean, and it now genuinely covers the test layer: check:test-typecheck: OK — @objectstack/plugin-sharing's test layer compiles under packages/plugins/plugin-sharing/tsconfig.test.json; 2 file(s) / 3 error(s) / 3 pinned signature(s) held in test-typecheck-debt.json. Those 3 are pre-existing entries in two files this PR does not touch.
  • Gate families derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands after the last commit — 69 commands over 9 paths. 64 green. Five are NOT MEASURED, every one a "build the whole repo first" prerequisite on packages this diff never touches, and each says so in its own verdict: check-test-completeness (exit 3, "Nothing was measured"), check:skill-examples ("packages/client-react/dist holds no .d.ts declarations"), check:dual-build-cjs-loads (exit 3, "PREREQUISITE NOT MET"), check:i18n ("PREREQUISITE NOT MET — the workspace CLI is not built"), check:type-check-debt (exit 3, "This is NOT a pass and NOT a finding"). CI builds the closure before those steps. Every exit code was captured before any pipe.
  • check:system-context-censusOK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read.
  • check:engine-double-contract — green after --write repathed the renamed test file: "692 (file, verb) row(s), 2 added or grown, 2 lost. No seam losses". The script's own --self-test passes too.
  • Lint, narrowed — and the narrowing is measured, not assumed. (1) Population read from ESLint's own config: eslint --print-config claims all 5 lintable changed files, none ignored. (2) File count read from --format json: 5 files reported, 0 errors, 0 warnings, exit 0. (3) Invariance for untouched files: this repo runs one eslint.config.mjs which "never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file" — the config states this itself, with a measured positive control — so no verdict on a file this diff does not touch can move. The repo-wide pnpm lint remains CI's run.

Documentation

content/docs/permissions/system-context.mdx is edited because a required gate demands it, not as a rider. check:system-context-census holds that page to a census of every isSystem read site — every site must be anchored at its exact file:line, and the page's census-derived counts must equal the census. Three sites vanished, so seven counts moved (109 → 106 elevation reads, 20 → 17 for plugin-sharing's share, 115 → 112 property reads, 105 → 102 behaviour-bearing). regen-artifacts.mjs records that the --fix path refuses a population change, so the rows were rewritten by hand: row 30 now describes the surviving afterDelete skip alone, and rough edge 2 records the reversal instead of the retired INFO notice.

Reversal register, not deletion

The two pins of the old behaviour are reversed and registered. system-write-skip-notice.test.ts becomes system-write-materialisation.test.ts carrying a row-by-row table of every #6783 expectation against its #13533 counterpart, including the four that are unchanged and why. bulk-recompute.test.ts keeps its case under a docblock recording what it used to assert, what reversed it, and the ruling's own words. Neither pin was a mistake; both pinned a real, separately-ruled behaviour that a later ruling overturned.

Contract review (Clause ②)

Declared yes, re-derived from the actual diff, which flips the dispatch's claimed no. git diff -U0 origin/main...HEAD | grep export reports exactly one exported symbol removed and none added:

-export const SYSTEM_WRITE_SKIP_NOTICE =

The published package surface is unchanged: plugin-sharing declares a single "." entry point resolving to src/index.ts, and index.ts re-exports bindRuleHooks, unbindAllRuleHooks, bindRuleCriteriaGuard, ruleRegrantQueue, SHARING_RULE_HOOK_PACKAGE and RULE_CRITERIA_GUARD_PACKAGE from this module — never SYSTEM_WRITE_SKIP_NOTICE. Its only consumer was the pin file renamed here. The declaration is yes because the dispatch's rule is mechanical about any exported symbol, and being over-conservative costs a label; the substantive review question is the behaviour change, which the ruling already settled. needs:contract-review is hung on this PR and on #13533.

Changeset is patch for that reason — no published export moves — and the change is a bug fix, not a declared-breaking one, so it carries no ADR-0087 marker. No new error-level log site through a published sink shape; the retirement removes an info line and adds none.

Patch round 1

origin/main moved to 7251bdb30 while this PR was open and the merge went dirty on exactly one path: content/docs/permissions/system-context.mdx. No code file conflicted.

What conflicted, and why it was not a real overlap. That page is routed to the os-regen merge driver, which refuses to text-merge it — so git stops there whenever both sides move it, regardless of whether the edits touch the same lines. They did not. Main's side (#14383, the #12981 batch 9 share-link work) moved row 37's share-link-service.ts anchors by +6. This branch had moved row 36's sharing-plugin.ts anchor by +11 and rewritten row 30, rough edge 2 and the counts. The resolution therefore takes row 37 from main and row 36 from this branch — each side's own row, neither discarded.

How it was verified. Not by inspection: the page was re-derived against the merged tree, and check:system-context-census reports OK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read. The census total is unchanged at 106 because main added and removed no elevation read sites.

Re-run on the merged head 376c04e00, after pnpm install --frozen-lockfile and a rebuild of the dependency closure (main moved packages/objectql/src, which this package builds against):

  • pnpm --filter @objectstack/plugin-sharing testTest Files 30 passed (30), Tests 718 passed (718).
  • pnpm --filter @objectstack/plugin-sharing typecheck — clean; check:test-typecheck: OK — 2 file(s) / 3 error(s) / 3 pinned signature(s), the same pre-existing entries.
  • Gate families re-derived on the new base — still 69 commands over 9 paths, an identical set (nothing arrived, nothing left). 64 green, the same five NOT MEASURED for the same repo-build prerequisites.
  • Both ablation legs re-run and reproduce exactly: leg A Tests 5 failed | 44 passed (49), leg B Tests 4 failed | 45 passed (49) with the bounded-branch pin still the discriminating failure, baseline Tests 49 passed (49), and the restore proved by blob equality plus an empty git diff HEAD after every leg.

Patch round 2 — documentation only, after the A ruling

The maintainer ruled A on 2026-09-02 (verbatim, untranslated: 「#13564 转维护者处理;其他同意」 — decision batch #11, this card item 1, recommendation A; recorded at 13533#issuecomment-5511791709). Ruling point 2 is closed by the census plus the cost-equivalence measurement already in this body: no boot-phase skip predicate is added, and meta resync stays per row, carried by #14530. The three removed skips, the reversed pins and the changeset level are untouched by this round.

This round carries the contract review's non-blocking §5 notes 2, 3 and 4, plus this body's note-5 omissions. New head e9b612a7a.

No code path changed. The round's own diff is three files — the changeset, one census-page cell and one docblock — and every added or removed line in the only .ts file among them is a JSDoc body line (*): zero non-comment lines. No ablation is owed and none was run; with no behaviour change there is no red for a mutation to produce, and running one would measure the previous round's code.

  • note 2 — content/docs/permissions/system-context.mdx, row 30. "Lose: nothing" glossed the one shape where the cascade's delivery is the queued orphan sweep rather than a synchronous revoke. The cell now opens "Lose: nothing permanently — the revoke is delivered, but deferred on the unbounded shape" and says which shape: when the deleted ids are enumerable the cascade revokes inline; when they are not — a predicate delete whose row set the stash could not resolve — it hands the reclaim to a queued background orphan sweep, so the share rows outlive the deleted records until that sweep runs. No surviving record loses access either way, and a restart re-runs the same sweep. Prose only: no anchor and no count was hand-edited, the generator owns those.
  • note 3 — packages/plugins/plugin-sharing/src/bu-tree-recompute.ts docblock. It still described bindRuleHooks' materialisation skip as current ("…is about grant MATERIALISATION, which the boot backfill re-does anyway"), a sentence this PR makes false. It now says that bindRuleHooks no longer skips system writes — the afterInsert / afterUpdate materialisation skips and the before* stash skip that fed them are gone, and the one skip it keeps is afterDelete revocation, which record-share-cascade.ts delivers instead — and that this file's own hooks never carried an isSystem branch to skip with. That last claim is checked against the file's whole history, not just the head: the only isSystem line ever added to it is its SYSTEM_CTX constant. Nothing else in the file changed.
  • note 4 — .changeset/system-write-sharing-materialization.md. One operator sentence added: seed- and import-time system writes on rule-covered objects now pay per-record sharing evaluation at write time — the cost a user write of the same shape has always paid, with the kernel:bootstrapped backfill still reconciling behind it. And "unexported" now reads "not exported from the package entry point", matching what the file already said correctly further down. Level stays patch; no ADR-0087 marker is owed.

note 5 — what this body omitted

Comment-only edits this PR carries. sharing-plugin.ts (docblock corrections at the rule-hook binding and at the kernel:bootstrapped handler) — its whole diff against origin/main has zero non-comment changed lines. boot-backfill.test.ts (file header rewritten) — likewise zero. bulk-recompute.test.ts carries an ADMIN_SESSION explanatory comment alongside the reversed pin this body already describes.

A behavioural side effect this body did not state. With the before* stash skip removed, the beforeDelete stash now runs for system deletes on rule-covered objects, so hook ordering flips: the rule package (priority: 180, rule-hooks.ts:177) resolves the affected row set first, and the cascade (priority: 190, record-share-cascade.ts:283) then reads the stashed answer instead of resolving its own (bulk-recompute.ts:304-305). Cost-neutral — one resolve either way; only which subscriber pays for it changes.

The merge

origin/main had moved to 20b883918 and the PR read mergeable_state: dirty on the same single path as round 1: content/docs/permissions/system-context.mdx. No code file conflicted; sharing-plugin.ts and scripts/engine-double-contract.pinned.json auto-merged.

That page is routed to the os-regen merge driver, which for this MIXED file — generated anchors plus hand-written prose — refuses to keep either side whole and text-merges instead, so it conflicts whenever both sides move it. One hunk conflicted, two rows wide, and the two rows belong to opposite sides: row 36 is this branch's (it moved the sharing-plugin.ts anchor by the +11 comment lines this PR added above it), row 37 is main's (#14580, landing #14033, rewrote its prose — link creation is bypassed, redemption is not — and moved its five share-link-service.ts anchors). The resolution takes each side's own row; neither is discarded.

Proven by the gate, not by inspection. The merge was committed first — never regenerate in MERGE state — then pnpm gen:system-context-census ran on the merged tree and rewrote 0 anchors, i.e. the hand resolution already agreed with the merged tree, and check:system-context-census reports OK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read. Both sides' content survives on the merged page: main's row-37 sentence about redemption, and this branch's row-30 rewrite plus rough edge 2.

Verification on the final head e9b612a7a

Every command ran through scripts/pm/os-verify-lock.sh, each exit code captured before any pipe; the lines quoted are the gates' own verdicts.

  • pnpm --filter @objectstack/plugin-sharing testTest Files 30 passed (30), Tests 729 passed (729). The delta from round 1's 718 is entirely main's: this round's diff touches no test file at all, and main's fix(plugin-sharing): hold publicSharing.enabled at redemption, not only at mint (#14033) #14580 added a net 11 cases across share-link-eligibility.test.ts and share-link-service.test.ts (15 it( lines added, 4 removed). 718 + 11 = 729.
  • pnpm --filter @objectstack/plugin-sharing typecheck — clean, and the test layer is genuinely covered: check:test-typecheck: OK — @objectstack/plugin-sharing's test layer compiles under packages/plugins/plugin-sharing/tsconfig.test.json; 2 file(s) / 3 error(s) / 3 pinned signature(s) held in test-typecheck-debt.json — the same pre-existing entries in two files this PR does not touch.
  • pnpm check:system-context-censuscheck-system-context-census --self-test: all cases passed, then check-system-context-census: OK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read.
  • pnpm check:doc-authoring — clean on all four legs: 388 files with no bare metadata literals, 41 published skill files with no internal issue-id references, 14500 customer-facing strings across 710 spec sources clean, and 831 pinned sibling-prose sites across 231 files with "no growth, no burn-down unrecorded".
  • node scripts/check-changeset-no-major.mjs — "This diff introduces no major bump."
  • node scripts/check-adr-0087-registration.mjscheck-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).
  • node scripts/check-empty-changeset.mjs — "No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)."
  • pnpm check:nul-bytescheck-nul-bytes: OK (scanned 7996 text file(s) -- 7996 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes).
  • pnpm check:engine-double-contractcheck-engine-double-contract: OK — 754 pinned, 134 in the DEBT ledger, 3 exempt. (753 in round 1; main added one pinned row, this round added none.)
  • Gate family re-derived on the final head with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands69 commands over 10 paths (round 1: the same count over 9 paths; the tenth is bu-tree-recompute.ts, which brought no additional command). Run in seven foreground batches under the container's cap: 64 green, and the same five NOT MEASURED as round 1, each declaring its own unmet prerequisite rather than a finding — check-test-completeness (exit 3, "Nothing was measured"), check:skill-examples ("packages/client-react/dist holds no .d.ts declarations"), check:dual-build-cjs-loads (exit 3, "PREREQUISITE NOT MET"), check:i18n ("PREREQUISITE NOT MET — the workspace CLI is not built"), check:type-check-debt (exit 3, "This is NOT a pass and NOT a finding"). CI builds the closure before those steps.

Clause ② stays yes — this round adds and removes no export — and needs:contract-review remains hung on this PR and on #13533 for the seat to clear.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8

Generated by Claude Code


Generated by Claude Code

`bindRuleHooks` returned early on `ctx.session.isSystem` in `afterInsert` and
`afterUpdate`, so a system-context write that moved a record into a sharing
rule's criteria materialized no `sys_record_share` row. Approval write-back is
exactly that write — an approval node with `lockRecord: true` mirrors the
decision onto the subject record under a system context, because while the
record is locked no other write can land — so a teammate who depended on the
criteria rule could not see an approved record until somebody re-evaluated the
rule or restarted the server.

Removes three early returns: the two named above, and the one on the
`beforeUpdate` / `beforeDelete` row-set stash they depended on. That third one
is load-bearing: without a stash `readAffectedRows` reports the row set as
`unbounded`, which would have sent every single-row system update down the
object-wide revoke plus asynchronous re-grant branch instead of the per-record
recompute. Retires the `SYSTEM_WRITE_SKIP_NOTICE` channel that announced the
skip.

`afterDelete` keeps its skip on separate grounds — its payload is revocation,
which `record-share-cascade.ts` delivers for system writes on its own account.

The two pins of the old behaviour are reversed and registered rather than
deleted: `system-write-skip-notice.test.ts` is renamed to
`system-write-materialisation.test.ts` and carries a row-by-row register of
every old expectation against its new counterpart, and `bulk-recompute.test.ts`
keeps its case under a docblock recording what it used to assert and why.

`content/docs/permissions/system-context.mdx` is updated because
`check:system-context-census` requires it: the page anchors every `isSystem`
read site and states census-derived counts, and three sites vanished.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…uble ledger after merging main

Discharges the os-regen deferral the merge commit recorded.

`content/docs/permissions/system-context.mdx` is os-regen-driven and both sides
moved it, so the merge sequence took main's side and this commit re-applies the
branch's hand-written half on top: row 30 now describes the one surviving skip
(`afterDelete`, whose payload `record-share-cascade.ts` delivers) instead of all
four, rough edge 2 records the reversal rather than the retired INFO notice, and
the seven census-derived counts move with the population. Re-derived on the
merged tree rather than carried over — `check:system-context-census` reports
"OK — 106 elevation read sites in 20 packages across 45 files, all anchored;
140 anchors resolve, 27 declared non-read".

`scripts/engine-double-contract.pinned.json` learns the test file's new path.
The two rows are repathed, not renumbered: `--write` reports "692 (file, verb)
row(s), 2 added or grown, 2 lost. No seam losses — this regeneration only
records new seams."

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…ain merge

Patch round 1. `content/docs/permissions/system-context.mdx` is os-regen-driven
and both sides moved it again, so the merge stopped there and neither side could
simply win: main's #14383 (#12981 batch 9) moved row 37's `share-link-service.ts`
anchors by +6, while this branch moved row 36's `sharing-plugin.ts` anchor by +11
and rewrote row 30 plus rough edge 2. The resolution takes row 37 from main and
row 36 from this branch — the two sides moved different rows, so there was no
real overlap, only the driver's refusal to text-merge a generated page.

Verified by re-deriving rather than by inspection: `check:system-context-census`
reports "OK — 106 elevation read sites in 20 packages across 45 files, all
anchored; 140 anchors resolve, 27 declared non-read".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/plugins/plugin-sharing/src/bu-tree-recompute.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/plugins/plugin-sharing/src/bu-tree-recompute.ts) — pages documenting those are invisible to this run
  • 1 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 — 8 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 a392dbf329854c3dfa549f1723e5c39c1d7a83bepackageMentionDocs.

Which tree this was computed on

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

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

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

…stem-write-sharing-materialization

# Conflicts:
#	content/docs/permissions/system-context.mdx
…block, operator cost sentence

Documentation-only follow-up on the contract review's non-blocking §5 notes
2, 3 and 4. No code path changes: the only .ts edit is a docblock body.

- system-context.mdx row 30: "Lose: nothing" glossed the one shape where the
  cascade's delivery is the queued orphan sweep rather than a synchronous
  revoke. The cell now says the revoke is delivered but deferred on the
  unbounded shape.
- bu-tree-recompute.ts: the docblock still described bindRuleHooks'
  materialisation skip as current. It now says what bindRuleHooks does after
  this change and why this file's own hooks never skipped system writes.
- changeset: one sentence on the operator-visible cost of seed- and
  import-time system writes on rule-covered objects, and "unexported" becomes
  "not exported from the package entry point". Level stays patch.

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

os-sales commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Landing provenance — ready + auto-merge at head e9b612a7a


Generated by Claude Code

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33719408288 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   integration  test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
      ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1800
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 5 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 74 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

os-sales commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

The dependency I wrote on this PR has resolved — and it resolved to "the fix does not work". Correcting it, and replacing the rule that produced it.

What I committed to, and why it is now void

At 23:48Z (issuecomment-5518115525) I wrote that this PR lands behind #14715, that when #14715 merged I would merge origin/main in and re-queue, and — explicitly — that there would be no third arming. I justified that as "a named, dated dependency, not a wait of indefinite length."

#14715 merged at 03:52:24Z, closing #14648. The trigger fired. But the thing it was supposed to deliver did not arrive, and the 05:59Z ejection is the proof:

when failure reason line
every earlier ejection of this PR AssertionError: expected 'SIGKILL' to be null
queue build 33719408288, 05:59Z AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1800…

That second message is #14715's own wording. So this build contained the fix, ran the test against the raised 180 s ceiling, and the test failed anyway — the child now burns the full 180 000 ms where it used to burn 40 000. Measured elsewhere on the same signature with the raised ceiling in force: the child ran 180 105 ms against a sibling's 6 125 ms on the same runner, and again 180 097 ms against 8 199 ms. The child stalls; it is not slow under load. No constant ceiling closes a stall, which is why raising it changed only the sentence.

So my dependency did not become satisfied — it became indefinite, which is the exact property I promised it did not have.

The rule I am replacing, and why the old one was wrong

The old rule — this PR gets one manual re-queue, then it waits — made a finished PR's ability to land depend on somebody else's fix arriving. Auto-merge is cleared by every dequeue (measured on this PR at 23:02Z), so "waiting" here does not mean sitting in a queue: it means sitting outside the queue forever, with nothing armed to put it back. A green, ruled, conflict-free PR that can never enter the queue is not waiting, it is abandoned. That is what the rule actually produced, and it is worse than the queue slot it was protecting.

The replacement rule, which I can hold consistently: while this PR is green, mergeable and unarmed, it gets armed. An ejection caused by a repo-wide signature is the queue's defect, tracked on its own card — it is not a reason to leave my own PR unable to land. This PR's presence in a batch does not make the flake more likely to fire; it only decides whether this PR is present when a batch does succeed, and batches are succeeding — #14804, #14790 and #14799 merged at 04:51:09Z, and this seat's own #14813 and #14815 merged at 05:34:30Z, all through the same queue on the same night.

No merge round is owed. The reason I named for merging origin/main in was to carry #14715's fix without a single-writer collision. That fix does not fix this, so there is nothing to carry. Head stays 645b14e9e with its 34/34 green checks, its adopted A ruling, and both Clause-② carriers already cleared at check-clause2-carriers --pair 14528 exit 0.

Where the defect now lives

#14822 is reopened, by this seat, at 06:0xZ. It had been closed as a duplicate at 04:05:26Z — thirteen minutes after #14648 closed, when #14715 still looked like the fix — and the issue it was marked duplicate of is closed, so this signature had no open home while it went on ejecting. The merge-queue-triage workflow states in this PR's own 05:59Z comment that it will not file a replacement and names reopening #14822 as the route. The full evidence, including the untried axis (why the child never exits, rather than how long we wait for it), is at 14822#issuecomment-5521277456.

packages/cli is domain:cli; this seat writes no code there and has graded nothing. This PR now waits on #14822 rather than on a closed card — and it waits inside the queue, not outside it.

Auto-merge re-armed.


Generated by Claude Code

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33721319707 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   integration  test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
      ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1800
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 6 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 78 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 3, 2026

os-sales commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Not re-arming. My "this failure is not this PR's" reasoning has a hole in it, and 7/7 is the reason to test it instead of rolling again.

The arming at 06:0xZ worked — the PR enqueued, built as 33721319707, and was dequeued at 06:19:44Z with CI_FAILURE on the same assertion. That resolves one open question (the arm's empty confirmation echo was cosmetic; it really did arm) and it opens a worse one.

The anomaly I have been explaining away

This PR is now 7 for 7: every queue build it has ever entered has failed on this one test. Meanwhile the same queue merged #14804, #14790 and #14799 at 04:51:09Z and this seat's own #14813 and #14815 at 05:34:30Z — through the identical full suite on the same night. So the flake does not fire in every batch. A break that fires often enough to take this PR seven consecutive times, while other batches sail through, is not well explained by "unlucky bystander".

I have asserted three times on this PR that the failure is not this PR's. The argument each time was package-level: the failing test is in @objectstack/cli, this diff is plugin-sharing plus a docs page, therefore unrelated. That argument is exactly the one this repo's own standing correction says is insufficient — when the changed code sits on a path the failing test can reach, the package boundary is not the test, and the FAIL has to be read against the change.

The hypothesis, stated as a hypothesis

run-dev-unbuilt-workspace.e2e.test.ts boots a dev server in an unbuilt workspace. A dev boot seeds. This PR's whole subject is that seed-time system writes now pay per-record sharing evaluation — its own body says so: "seed- and import-time system writes on rule-covered objects now pay per-record sharing evaluation at write time — the cost a user write of the same shape has always paid." The census in the body confirms SeedLoader writes are system writes that reach rule-covered objects, and that skipTriggers does not suppress these hooks.

So there is a plausible mechanism by which this PR makes that child slower to reach its exit, and the failing case is precisely "gives up and exits instead of waiting forever" — a case that asserts the child is not still alive at the ceiling.

⚠️ This is not a claim that the PR is at fault. The repo-wide break is real and independently evidenced: 25 PRs on this signature in 24 hours, most of which cannot reach plugin-sharing, and a measured stall of 180 105 ms against a 6 125 ms sibling on PR #14827, which touches service-automation. Both things can be true — a real repo-wide stall, plus a PR-specific cost that makes this PR lose the race every time. What I can say is that I have never tested the second half, and 7/7 says I should have.

What happens instead of another queue roll

A measurement round is dispatched on this branch. It runs the failing test file repeatedly against origin/main and against head 645b14e9e on the same machine, records the child's runtime distribution on each, and looks at what the child is actually doing when it is still alive at the ceiling.

⛔ It stops at the measurement. It changes no test, ports no fix, touches no file under packages/cli, and does not "make it green". If the two distributions are indistinguishable, that exonerates this PR and strengthens #14822 with a real control — which is worth as much as the other answer.

⛔ No re-arm until that number exists. This is not the old "wait for someone else's fix" rule that I replaced two hours ago — that rule was wrong because it made this PR's liveness depend on work I was not doing. This is the opposite: the work is mine, it is dispatched, and it is dated by a running agent rather than by a queue lottery.

The repo-wide anchor stays #14822 (reopened 06:0xZ; the triage workflow has already re-adopted it as this signature's aggregate). Nothing about this PR's content, its adopted A ruling, or its cleared Clause-② carriers is reopened by any of this.


Generated by Claude Code

os-sales commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

The number exists, and it exonerates this PR. My hypothesis was wrong, and it was wrong at the root.

At 06:2xZ I stopped re-arming this PR and dispatched a measurement round, on the hypothesis that its own change was making the child slower and losing it the race — 7 ejections out of 7 looked anomalous for a shared flake. The measurement is done (full report on the card, 13533#issuecomment-5522251046) and the verdict is REFUTED.

The premise I handed the dev was false. I wrote that this test "boots a dev server in an unbuilt workspace, and a dev boot seeds", and reasoned from there that this PR's seed-time sharing materialisation could reach it. The case-5 child is tsx packages/cli/bin/run-dev.js i18n extract nope.ts — the run-the-CLI-from-source shim. It never boots a dev server and never seeds. Measured: zero occurrences of seed, SeedLoader or sharing in the child's 144 706 bytes of stderr. I had read the test file's name as its behaviour.

And the change is not reachable from that child at all. V8 coverage on the real case-5 child reports plugin-sharing/dist: 0 script entries, 0 executed — against a positive control (await import('@objectstack/plugin-sharing') in the same tree) of 1 entry with execution count 8, so the instrument can see the module when it runs. This PR's built artefact is also 943 bytes smaller than baseline, not larger.

The 7-for-7 was not evidence about this PR. 74 interleaved out-of-band children across two trees differing by exactly this PR's 10 files — the A/B used the merge's own second parent 4d0d9445a rather than current main, so 46 unrelated commits stayed out of the comparison:

  • cold tsx cache: baseline 6 hangs / 7 runs, head 7 / 7 — the break reproduces on this PR's own merge base at the same rate;
  • every fast run on both trees absorbed byte-identical output (144 706 bytes, 116 ModuleLoadError blocks), and truncated runs land on the same values on both.

The real mechanism — measured, and now on #14822 (14822#issuecomment-5522294431): with a cold tsx cache, an inherited-stderr esbuild service process leaves fd 2 in blocking mode (flags: 02000002 vs 02004002 when warm), and the worker parks forever in write(2) on a full stderr socketpair, below the event loop where the shim's own 15 s no-progress bound — a setInterval — can never fire. CPU frozen at ~0.16 s across 172 s. No ceiling closes it; 40 s, 45 s, 120 s and 180 s all give the identical picture.

What this changes here

Auto-merge is re-armed. The condition I set at 06:2xZ was "no re-arm until that number exists"; it exists, and it says this PR is a bystander. Ejections on this signature are the queue's defect, tracked on #14822 with a named mechanism and a recommended fix that belongs to domain:cli.

I am also not repeating the arming argument on this PR again — the rule and its bound are stated, and a fourth comment about re-queue policy would be volume rather than record.

Nothing about this PR's content moved: head stays 645b14e9e with its 34/34 green checks, its adopted A ruling, and both Clause-② carriers cleared at check-clause2-carriers --pair 14528 exit 0.


Generated by Claude Code

@os-sales
os-sales added this pull request to the merge queue Sep 3, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Sep 3, 2026
…ain merge

Discharges the os-regen deferral recorded by the merge commit. Pure
re-anchoring: 20 insertions / 20 deletions, all of them line numbers the
generator re-derived from the merged tree (main moved engine.ts,
sharing-service.ts, auth-plugin.ts, rest-server.ts and last-admin-guard.ts).
No prose changed on either side -- normalising every digit away leaves the
before/after byte-identical.

The engine-double-contract ledger needed no repair: its textual auto-merge was
already byte-exact, and `--write` reported 697 rows with 0 added, 0 grown and
0 lost.

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

os-sales commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Merge round complete — and it falsified the premise this seat dispatched it on

domain:services seat, session session_01AUF1NoViznQK32gqpK8wS8, R22. Merge commit e7c756aca, regeneration commit f7d1386a3, pushed as a fast-forward 645b14e9e..f7d1386a3. No rebase, no amend, no force-push, no stash. PR content untouched — this round is the merge and the regenerated artefacts, nothing else.

My dispatch order expected a hand prose resolution. There was none, and that is the right outcome.

I wrote that content/docs/permissions/system-context.mdx would conflict and prescribed the hand resolution for it. The real merge had zero conflicts. git merge ran the os-regen driver down its DEFER branch — keeping the branch's bytes and recording the path in os-regen-pending — rather than the text-merge branch that has bitten this PR three times before.

And nothing was lost by deferring: main's side of that file carried no prose at all. With every digit normalised away, main-vs-base is byte-identical, so main's entire contribution was anchor re-numbering — which the generator re-derives. The regeneration then rewrote exactly the 30 anchors main had moved (engine.ts, sharing-service.ts, auth-plugin.ts, rest-server.ts, last-admin-guard.ts).

scripts/pm/os-regen-merge.sh was deliberately NOT run, and the reason is worth recording

Its step 2 takes main's whole side of any os-regen path both sides changed. For this MIXED file that would have discarded the branch prose the driver exists to protect. The dev proved step 2 had no work to do — the intersection of both-sides-edited os-regen paths is exactly that one file, and the driver handles it itself.

That is consistent with #14671, filed earlier this shift against the same script for misreporting a MIXED conflict.

Readings, all on the final commit f7d1386a3

check reading
merge clean git merge-tree --write-tree --name-only origin/main HEAD — exit 0, bare tree oid, 0 CONFLICT lines. Run twice, the second time against 13b520069 after main moved mid-round
census page merge committed first, then pnpm gen:system-context-census: 20 insertions / 20 deletions, and with digits normalised the prose diff is 0 lines — the generator's signature. Gate: OK — 106 elevation read sites in 20 packages across 45 files, all anchored
pinned ledger not os-regen routed, so git text-merged it; regenerating with the gate's own --write changed zero bytes ⇒ the auto-merge was already byte-exact. 697 rows before and after, seam rows 6 → 6
tests plugin-sharing 32 files / 772 passed
gates 69 commands over 10 paths — 66 green, 3 NOT MEASURED, 0 red. Three that first read non-zero were unmet build prerequisites and were cleared by building rather than reported as red

The test-count delta is attributed rather than reported bare, which is what this seat asked for: the two sides touch disjoint test files (verified by name-status diff, no overlap). Main brought ~+43 cases; this branch's own net is +3. The counts for the four modified files are static it(/test( declaration counts and are labelled as such; the added/deleted files' counts are run-measured.

Two notes

⚠️ scripts/pm/check-half-states.mjs timed out (exit 124) at both a 180 s and a 280 s cap. It is a live GitHub-API backlog sweep, unrelated to this diff, and its offline self-test sibling check:pm-half-states is green. This seat has been hitting sustained GitHub rate limiting since 08:56Z, so a live-API sweep timing out is consistent with that rather than with anything on this branch. Recorded as NOT MEASURED.

The PR was already draft: false when the round started — set before it — and the dev left it as found rather than reverting another actor's flip. Correct call.

Landing: GitHub reports mergeable: true again (was MERGE_CONFLICT); mergeable_state: blocked is pending checks, not a conflict. CI is running on f7d1386a3 — 13 of 31 complete, zero failures so far. ⛔ Not armed yet: this seat reads job-level conclusions, not an aggregate, so auto-merge goes on when the shards finish green.


Generated by Claude Code

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

2 participants