Skip to content

finding: the insert-path readonly strip is a protocol-boundary guard only — engine.insert applies none of it, and create_record's onFieldsDropped channel can never fire for a readonly drop #14147

Closed

Description

@os-warren

Observation from an application (objectstack-ai/duly), filed for triage rather than as a defect — the asymmetry may be intended, but the engine's own prose says otherwise and one consumer is wired for a signal it cannot receive.

Measured against @objectstack/runtime 17.2.0 / @objectstack/objectql 17.2.0 / @objectstack/metadata-protocol 17.2.0, on a real booted kernel with the in-memory driver.

What was measured

duly_task.completed_at is readonly: true.

write face context forged completed_at
MetadataProtocolService.createData none stripped (row then refused by a validation rule)
MetadataProtocolService.createData { isSystem: false } stripped
MetadataProtocolService.createData { isSystem: true } preserved (intended — the documented exemption)
engine.insert (the data service) none preserved

The last row is the finding: a non-system caller reaching engine.insert directly writes the read-only column, with no refusal, no WARN, and no onFieldsDropped event.

Why it happens

The insert-path strip is stripReadonlyForInsert, which lives in @objectstack/metadata-protocol and is called from exactly two places — MetadataProtocolService.createData and cloneData:

function stripReadonlyForInsert(schema, data, context) {
  if (context?.isSystem) return data;
  ...
}

The update path is not like this: stripReadonlyFields is inside ObjectQL and runs on every engine.update, isSystem-gated. So readonly is enforced in-engine on update and only at the API boundary on insert.

Two things that look unintended

1. The engine's own doc comment claims otherwise. assertReferencesResolve in objectql/dist/core.js describes its non-system scoping as being

like every other write-path guard in this engine (stripReadonlyFields, stripReadonlyForInsert).

stripReadonlyForInsert is not in that engine and is not on engine.insert's path at all. An application author reading that reasonably concludes the engine enforces readonly on insert.

2. create_record is wired for a drop event it can never get. The automation engine's create_record executor (@objectstack/service-automation) calls data.insert — the engine, not the protocol — and passes onFieldsDropped, then surfaces the result as output.droppedFields and node warnings:

const created = await data.insert(objectName, fields, {
  context: dataCtx,
  onFieldsDropped: (e) => { dropped.push(e); }
});

Since no readonly strip runs on that path, that channel cannot report a readonly drop. A flow without runAs: 'system' can set a readonly column at insert and the run reports a clean success.

(In the application that found this, the relevant flow does declare runAs: 'system', so nothing there is currently affected — this is reported as a seam, not an incident.)

The question for triage

Is the insert-side strip deliberately a user-context / API-boundary concern (which #3425's framing suggests), with engine.insert trusted by construction? If so the fix is prose plus the dead onFieldsDropped wiring on create_record. If not, the strip belongs on the engine insert path where its update-side twin already lives.

Either way readonly currently means something different on insert than on update, and the difference is not written down anywhere an application author would look.

Reproduction

Two duly_task inserts with an identical payload carrying a forged completed_at, one through protocol.createData and one through data.insert, both with no context. Pinned as a tripwire in objectstack-ai/duly — test/seed-history.test.ts, the insert-path strip is a BOUNDARY guard block — which is written to go red and be deleted when this is closed.

Activity

  1. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    Triage (R+89, triage seat, session session_019kDRpB7D2XzVzkaLp57T5D): pm:queue · priority:p2 · domain:engine · type Bug.

    The card's question is answered by a standing meta-rule, not a new ruling. Maintainer-approved meta-criterion (2026-08-07): one operation, two implementations, inconsistent behaviour ⇒ the governed side wins by default; the other is re-bound, not aligned and not double-written. Here readonly on update is enforced in-engine (stripReadonlyFields, isSystem-gated) and on insert only at the protocol boundary (stripReadonlyForInsert in metadata-protocol) — so the governed side is the in-engine strip, and engine.insert is re-bound to it. Consequences the PR owes: the insert-path strip runs inside engine.insert for non-system callers with the same onFieldsDropped / warn behaviour as update, create_record's dead onFieldsDropped wiring starts receiving readonly drops, the boundary copy is deleted rather than kept as a second implementation, and assertReferencesResolve's doc sentence becomes true. Premise check binding on the dev: if #3425 (the card the reporter cites) recorded an explicit maintainer ruling that insert is a boundary-only concern, stop and report the fork — a ruling outranks the meta-rule. Absent that, proceed.

    Clause-②: yes for the dispatch's tier decision — a published engine write path changes what it does with a caller-supplied readonly field (strip + report instead of write), which is an accept-behaviour change on a public path even though no wire schema moves. Landing files: packages/objectql/src/engine.ts (insert), the strip's home, metadata-protocol's createData/cloneData callers.

    Family: #14259 (provenance, the same strip's update-side twin), #14088 (in flight). Hot file: packages/objectql/src/engine.ts — the engine seat answers fold-or-serial.

    Size/model suggestion: M, claude-fable-5 per clause ②.


    Generated by Claude Code

  2. added theissue type on Sep 2, 2026
  3. os-project-manager commented on Sep 3, 2026

    @os-project-manager
    Collaborator

    Maintainer ruling recorded — C: readonly is enforced in-engine on INSERT for non-system callers, exactly as on UPDATE; the 2026-07-24 "INSERT (all callers) exempt" row of #3425 is superseded

    Director seat, summon #11, session session_01ShyhexkB2d1AeRZ85tgAAe, 2026-09-03.

    Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, 2026-09-03, replying to decision batch #17 in which this card was item 3 with the recommendation C (fallback A), presented explicitly as overturning the maintainer's own 2026-07-24 ruling row on #3425 ("INSERT(所有调用方)豁免"). Verbatim reply: 「同意」 — adopts C, and with it the supersession.

    Ruled: C — one semantics, one enforcement point. A static readonly field is stripped from a non-system caller's INSERT payload inside engine.insert, by the same in-engine strip (stripReadonlyFields, isSystem-gated) that already runs on engine.update, with the same onFieldsDropped / warn behaviour. Seeding a readonly column at create time is done under system context (runAs: 'system', system hooks, seeds) — the exemption the 2026-07-24 table already grants — which is what "create legitimately seeds readonly columns" needed all along. Consequences the PR owes, all in one change: the boundary copy stripReadonlyForInsert in @objectstack/metadata-protocol is deleted rather than kept as a second implementation; create_record's onFieldsDropped wiring starts receiving readonly drops; assertReferencesResolve's doc sentence becomes true; packages/lint/src/validate-readonly-action-writes.ts drops its "INSERT is exempt BY DECISION" premise and its insert/create scan gap, with its GREEN control case reversed into a refusal pin; and the prose in skills/objectstack-data/SKILL.md and skills/objectstack-automation/SKILL.md says "any non-system write", not "an UPDATE payload". Not taken: A (delete the boundary strip, INSERT exempt for everyone — rejected as loosening a data-integrity guard), B (boundary-only, engine trusted — a two-implementation shape), D (measure existing dependents first — 「不考虑存量」).

    Supersession, for the record: the #3425 ruling table's row "INSERT (all callers): exempt" is superseded by this ruling; its other rows (user-context UPDATE strips; runAs: 'user' strips; runAs: 'system' / system hooks / seeds pass; no fail-the-node, no onDroppedFields: 'fail' knob) stand unchanged. A cross-reference is posted on #3425.

    Execution: domain:engine lane, M–L. Clause-②: yes (a published engine write path changes what it does with a caller-supplied readonly field) ⇒ contract-review tier, needs:contract-review on the PR, in-seat review before ready. Two landing paths: the engine + lint + metadata-protocol change through the merge queue (packages/objectql/src/engine.ts is a hot file — the engine seat answers fold-or-serial); the two skills/** prose edits are a governed surface and land as a separate draft PR requesting review from os-zhuang and hotlong, human merge, cross-linked from the code PR. Changeset: @objectstack/objectql and @objectstack/metadata-protocol, minor with a BREAKING note per the repo's launch-window convention — a non-system create that used to write a readonly column now has it stripped and reported.

    State transition, same stroke: needs-user-decision → pm:queue. Ledger: director seat post #12708, summon #11.


    Generated by Claude Code

  4. self-assigned this
    on Sep 4, 2026
  5. zhuangjianguo commented on Sep 4, 2026

    @zhuangjianguo
    Collaborator

    Claim: PM loop round R17
    Session: session_01ARYe3yQTQCUFm5qPYNgKaJ
    Branch: claude/issue-14147-engine-insert-readonly-strip
    Worktree: objectstack-issue-14147
    Domain: domain:engine
    File surface: packages/objectql/src/engine.ts (the insert path) · packages/metadata-protocol/src/protocol.ts (delete the boundary copy + its callers) · packages/lint/src/validate-readonly-action-writes.ts · packages/services/service-automation (create_record's onFieldsDropped) · pins + changesets. Separately: two skills/** prose edits as their own draft PR — §4.
    Container & model: M–L, mode:subagent, model: opus under the measured quota exemption (authority and measurement identical to my claim on #14423, comment 5539903476 — eight 429 readings today, last 11:27Z; ⛔ the exemption downgrades the dev, never the review).
    Clause-②: yes (triage's, 5503036931): a published engine write path changes what it does with a caller-supplied readonly field.
    Serial constraints: ⚠️ engine.ts co-tenancy — read §3 before touching it.


    1. The ruling — C, and it OVERTURNS a maintainer's own earlier row

    Maintainer via the director seat (summon #11, comment 5522734749, 2026-09-03), verbatim 「同意」, adopting C and presented explicitly as overturning their own 2026-07-24 row on #3425.

    Ruled: a static readonly field is stripped from a non-system caller's INSERT payload inside engine.insert, by the same in-engine stripReadonlyFields (isSystem-gated) that already runs on engine.update, with the same onFieldsDropped / warn behaviour. Seeding a readonly column at create time is done under system context — the exemption the 2026-07-24 table already grants.

    Everything the PR owes, in one change:

    • the boundary copy stripReadonlyForInsert in @objectstack/metadata-protocol is DELETED, ⛔ not kept as a second implementation;
    • create_record's dead onFieldsDropped wiring starts receiving readonly drops;
    • assertReferencesResolve's doc sentence becomes true;
    • packages/lint/src/validate-readonly-action-writes.ts drops its "INSERT is exempt BY DECISION" premise and its insert/create scan gap, with its GREEN control case reversed into a refusal pin.

    ⭐ The supersession is part of the deliverable, not background. #3425's table row 「INSERT(所有调用方)豁免」 is superseded; its other rows stand unchanged. ⚠️ A dev who reads #3425 without this ruling will implement the exact opposite of what was ruled.

    Changeset: @objectstack/objectql + @objectstack/metadata-protocol, minor with a BREAKING note — a non-system create that used to write a readonly column now has it stripped and reported.

    2. Anchors on origin/main @ 6e67b86c0, 11:39Z — ⛔ locate by symbol

    site reading
    engine.insert engine.ts:9724 (async insert(object, data, options?))
    stripReadonlyFields import engine.ts:196 from ./validation/rule-validator.js
    the doc sentence to make true engine.ts:5939-5940 — 「like every other write-path guard in this engine (stripReadonlyFields, stripReadonlyForInsert)」
    existing update-path call sites (the shape to mirror) :11389, :11569
    the lint premise validate-readonly-action-writes.ts:57 and :154 (「absent BY DECISION, not by omission」)

    ⚠️ stripReadonlyForInsert's definition did not surface in my quick grep (the hits were CHANGELOGs and a test) — the card places it at metadata-protocol/src/protocol.ts with ~six call sites. Locating it and enumerating every caller is your first deliverable, ⛔ not something to take from this table.

    3. ⚠️ engine.ts co-tenancy — a second open PR is on this file RIGHT NOW

    PR #15319 (#14666) is open and edits engine.ts, at the boot-loop registrar around :5298. Your site is engine.insert at :9724 — ~4,400 lines away, different function, no symbol overlap.

    The lane's convention is one engine.ts PR at a time; I am deliberately running two, because #15319 is finished work parked on a review queue rather than a moving branch, and the lane's own precedent is that symbol-level fencing works on a file this size (three concurrent PRs on protocol.ts this week, no collision).

    ⇒ What you owe because of that decision:

    • ⭐ content/docs/permissions/system-context.mdx is a GENERATED line-anchor table into engine.ts, and it is the one artifact you and fix(objectql): the boot loop refuses a view container whose name disagrees with its derived object key (#14666) #15319 both move. If check:system-context-census reds, repair it with node scripts/check-system-context-census.mjs --fix and nothing else. ⛔ Never hand-edit it, ⛔ never "take ours"/"take theirs" in a conflict — both are wrong; re-derive.
    • Merge origin/main as you go and re-run the ratchet families after each merge.
    • ⛔ Do not touch anything near :5298. If your change requires it, stop and report.

    4. ⛔ TWO landing paths, and the second is NOT this lane's to land

    The engine + lint + metadata-protocol change goes through the merge queue as one PR.

    The two skills/** prose edits — skills/objectstack-data/SKILL.md and skills/objectstack-automation/SKILL.md, whose text must say "any non-system write", not "an UPDATE payload" — are a governed surface. They land as a separate draft PR requesting review from os-zhuang and hotlong, human merge, cross-linked from the code PR. ⛔ You do not mark that one ready, enqueue it, or approve it. ⛔ Neither do I.

    5. Zone 2 — measure these; falsifying one is a result I want

    A2.1 — the premise check the triage ruling made binding, now discharged, and you should confirm that. Triage wrote: 「if #3425 recorded an explicit maintainer ruling that insert is a boundary-only concern, stop and report the fork」. The predecessor seat did stop, found exactly that, and the fork went to the maintainer — who overturned their own row. ⇒ the stop condition is spent, and finding #3425's old table is ⛔ not a reason to stop again. Confirm you are reading ruling 5522734749 and not only #3425.

    A2.2 — enumerate every stripReadonlyForInsert call site before deleting it. The card says "at least six". A deletion that misses one is a compile error at best and a silently unstripped door at worst.

    A2.3 — the lint GREEN control must be REVERSED, not deleted. validate-readonly-action-writes.ts carries a passing case asserting 「an elevated insert seeding a readonly AND a readonlyWhen-locked column keeps both values」. The ruling says that case becomes a refusal pin. ⛔ Deleting it loses the coverage; ⛔ leaving it green means the lint still encodes the superseded premise.

    A2.4 — gate families, derived on your ACTUAL surface, reported per family with exit codes, ⛔ never as an aggregate. Treat exit 3 as NOT MEASURED, ⛔ never a pass — build the prerequisite and re-run.
    ⭐ And know what the tool cannot see: it maps paths→gates, so convention-scoped gates (firing on a property of the change) are invisible by construction. Four devs hit this in this round alone. Ask separately what your change's kind trips — a new refusal code, a deleted exported function, a lint-rule premise change, and the engine.ts line shift are all candidates.
    ⚠️ ALSO: derive your local suite list from turbo ls --affected against the merge base, ⛔ not from "packages I changed" — that exact gap cost a sibling PR two CI cycles today (#15323).

    A2.5 — content/docs/releases/** is release-owned and read-only. If a drift check names one, ⛔ do not edit it; report it.

    ⛔ PR body first line: Fixes #14147. Draft. ⛔ Not ready, ⛔ not enqueued, ⛔ no auto-merge — it lands only after a PASS at CONTRACT_REVIEW_TIER.


    Generated by Claude Code

  6. 14 remaining items

  7. zhuangjianguo commented on Sep 5, 2026

    @zhuangjianguo
    Collaborator

    Maintainer ruling on E3 — the standard: 已发布必修,其余立卡. The patch round is narrowed accordingly, and this binds future clause-② reviews, not just this card.

    domain:engine execution seat, session_01ARYe3yQTQCUFm5qPYNgKaJ. Provenance: maintainer, live PM chat, 2026-09-05, answering the escalation E3 raised by the third CONTRACT_REVIEW_TIER review (5550434842) — 「Three tier reviews have upheld the engine change and failed the PR on prose residue each time … If the maintainer would rather land the engine change and carry non-published residue as a card, that is a ruling on the standard」. Presented with four options and the recommendation; the maintainer chose 「已发布必修,其余立卡」.

    The standard, as ruled

    For the class 「a superseded architecture still stated as live」:

    • Published surface wrong ⇒ fixed in the round. packages/spec .describe() / TSDoc that ships in .d.ts, the generated content/docs/references/** projections, and customer-facing documentation pages.
    • Non-published residue ⇒ one finding card; the PR lands. Test headers, in-source code notes that reach no published artefact, and scripts/ provenance text.

    ⚠️ Which of the nine N5 sites is 「published」 is a measurement, not a reading of this comment. The reviewer named exactly one as published — N5(a), packages/spec/src/security/public-form.ts:21-22, TSDoc on an exported const — and distinguished N5(b) (「the docblock of the module that defines the published error code」) and N5(c) (「shipped source」) from it. ⇒ The patch round measures which sites actually land in a published artefact rather than sorting them by intuition, and the ruling is applied to that measurement.

    What this changes on this card

    The verdict's patch list is narrowed from seven items to two, ⛔ not by this seat's judgment but by the ruling:

    • Item 1 — N5(a), and any other site the measurement shows reaches a published artefact.
    • Item 7 — re-bound the sweep by claim, not spelling. ⭐ Kept deliberately and it is now the more important half: the ruling makes the card the destination for residue, so the card is only as good as the sweep that finds it. The claim-level patterns (insert…exempt, exempt…insert, readonly-exempt, may seed|can seed, seed…readonly, #3413), the content-only co-occurrence pass, every hit classified, each pattern proven to fire on 485a2d525 before it is claimed to return zero.

    Items 2–6 (N5 b–g) become the card, filed with the reviewer's own file:line enumeration and extended by whatever item 7's re-bound sweep adds.

    What the ruling does ⛔ NOT change

    • The three FAILs stand as correct. Each found real, present-tense false statements; the first two were adopted under the standard in force at the time and are ⛔ not retroactively softened.
    • The engine change was never the problem — three independent tier reviews upheld it line by line, and this one re-read it from scratch and confirmed it is exactly ruling C.
    • F2 stands: origin/main is still not merged this round; the both-sides set is six files.
    • ⚠️ F3 stands and is the sharper risk now that landing is nearer: CI has never run on this head (0 check-runs on 485a2d525, e78c95cb4, 66c580b0c — GitHub runs no pull_request workflow on a conflicted PR). The last CI-measured head, bd598e803, predates R2's one behaviour change. ⇒ The landing must read the merged head's checks, ⛔ never bd598e803's, and 「the local farm was green」 is not a substitute.
    • E1 is discharged ([decision] readonly on create vs update is asymmetric on every managedBy object — including the admin/user-writable system-data bucket #15719 exists); E2 remains recorded for the maintainer, unchanged.

    ⚠️ Where this ruling needs to live

    It is a standard for every clause-② review, not a fact about #14147. Its home is the contract-review reference, which is a governed surface — ⛔ draft-only, human merge, and ⛔ not something this seat writes. Recorded here and carried in the dispatch briefs meanwhile; a governed-surface PR to make it durable is a separate, human-merged change.


    Generated by Claude Code

  8. claude commented on Sep 5, 2026

    @claude
    Contributor

    os-dev-report
    Claim: PM loop patch round R4 — Session: session_01ARYe3yQTQCUFm5qPYNgKaJ — Branch: claude/issue-14147-engine-insert-readonly-strip — head a73dd85db — PR #15395 (draft, needs:contract-review on both carriers) — residue card #15819 (finding, unassigned)
    Clause-②: yes

    {
      "issue": 14147,
      "status": "done",
      "branch": "claude/issue-14147-engine-insert-readonly-strip",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15395",
      "premise_still_valid": true,
      "summary": "Patch round R4 on PR #15395 under the maintainer's E3 ruling (5550450281, 「已发布必修,其余立卡」) applied to the third CONTRACT_REVIEW_TIER verdict (5550434842, FAIL). New head a73dd85db: ONE commit appended on 485a2d525 (no rebase/amend/force-push); PR still draft; needs:contract-review still on both carriers; origin/main NOT merged (F2 — #15225 / PR #15687 first; mergeable_state dirty as expected; the both-sides set is now SEVEN files: settings-service.ts gained one main commit, 6b8c67778 #15434, in hunks disjoint from R4's). ITEM 1 — 'published' was MEASURED per site, not sorted: the three packages rebuilt, each N5 sentence grepped as a fixed string in dist/**/*.d.ts, dist/**/*.{js,mjs,cjs} and the .map files plus content/docs/**, exit captured before any pipe, a firing control (the neighbouring exported identifier) per package, before at 485a2d525 and after at a73dd85db. Result: (a) public-form.ts:21-22 reaches dist/security/index.d.ts:278 — published, fixed; the same docblock's :10-11 ('the static-readonly strip only covers UPDATE — so nothing downstream guards these columns') is the same claim in a spelling none of the reviewer's patterns carried, ships at :266, fixed in the same edit (R4 added sweep pattern p6 for it). (b) is HALF: the module docblock :21-22 reaches no .d.ts and no .js (a floating docblock ahead of the non-exported READONLY_CLASS_REASONS; only the .map sourcesContent) — carded; the `operation` property TSDoc :170-172 of the exported ReadonlyFieldRejectedError reaches dist/index.d.ts:362 and :373 — published, fixed (buildRefusalMessage untouched). (c) settings-service.ts:2279, the upsertRow docblock, reaches dist/index.d.ts:1150 AND dist/index.js:1722 / index.cjs:1803 — published, fixed with the live mechanism (sys_setting is sys_-prefixed and managedBy engine-owned, outside staticReadonlyInsertSubject; UPDATE applies no carve-out — #15719), and the changeset gained '@objectstack/service-settings': patch so the corrected .d.ts ships. (d) (e) (f) (g) reach nothing in any dist, liveness/, json-schema/, prompts/, api-surface/ or content/docs — carded. Boundary declared in the PR: every shipped .js.map embeds its whole source, so by that reading nothing is non-published; that contradicts the ruling's own examples, so the .d.ts/docs measurement is applied and the .map column is reported, not scored. ZONE 2: (1) FALSIFIED — N5(a) is not the only published site; (b2) and (c) are too, so both moved into item 1 and the card shrank to seven sites; (2) held — check:generated after the rebuild reports all 15 artifacts up to date and git status is empty, no projection regenerated; (3) held — check:api-surface 'public API surface + factory signatures unchanged'. ITEM 2 — sweep re-bound by CLAIM: sweep-r4.sh runs pass A (twelve spellings), pass B content-only co-occurrence (one regex over the line, never a filter over grep -n output — 32 lines, the reviewer's number; the R3 naive form's 42 includes 10 filename-admitted sites, now named), the five claim patterns of item 7, and p6 (covers UPDATE / strips readonly from an UPDATE payload / from an UPDATE payload). Positive controls: every added pattern fires at 485a2d525 on the N5 sites it was meant to catch (p1 60 lines incl. b/c/d/e/f/g, p2 3, p3 8 incl. (a):22, p4 41 incl. (a):22, p5 17, p6 6 incl. (a):11); after: union 147 unique file:lines (150 before), EVERY one classified in the PR body (H 44, U 40, R 33, RW 10, #7823 8, L 8 lines = 7 sites, H+R 6?, D 1, P 2, F 1, S 1, G 1 — the exact counts head the table); live-class hits after = the 7 non-published sites, all on #15819; published live-class = 0. The class is stated in one sentence in the PR body. The R3 list's two imprecisions are struck and corrected in place (the 10 filename-admitted rows named; 'five listed twice' → one, #23/#53). ITEM 3 — residue card #15819 filed: finding label only, ungraded/unrouted/unassigned; seven sites by file:line with what each says and why it is the class ((b1), (d) x3, (e), (f), (g)), rule-validator.ts:1021 (stripReadonlyFields docblock names UPDATE only — stale by omission) as a marked secondary entry, SKILL.md:274 excluded by name as the governed half (#15382), the measurement method, the templates to close it, and the plain statement that the maintainer ruled these carded rather than fixed; cross-links #14147, PR #15395, ruling C, the three verdicts, the E3 ruling, #15719, #15382, #15394. Dedupe: REST /search was 403 this session (control search also 403 — channel, not result) → one targeted MCP search_issues (27 results, nearest #15394 and #15703, neither this) plus the open finding list (one page of 100) grepped locally. CI REALITY, stated: GET /commits/a73dd85db/check-runs → total_count 0; mergeable false / dirty — no pull_request workflow runs on a conflicted PR, so no CI has run on any head since bd598e803 (which predates R2's behaviour change); the local farm is the only evidence and the landing must read the MERGED head's checks. PR body PATCHed via REST (200) and read back in full: identical to what was sent except that the platform dropped the trailing bare attribution footer block this time (the R3 PATCH had kept it bare) — attribution stands in the R4 section's prose per the AGENTS.md create-only rule, no re-append loop; every section probe 1x; draft still true; labels unchanged. Process note: the before-reading of the sweep ran in a temporary detached worktree at 485a2d525 that I removed with --force seconds after creating it (it held no uncommitted work — read-only greps only); the R4 worktree itself was removed without force after node_modules deletion.",
      "tests": "All at a73dd85db (the final commit; nothing changed after it — no regenerated artefact, no census re-anchor was needed); builds/tests through scripts/pm/os-verify-lock.sh (slot dev-14147-r4; --status read first: free), every exit captured before any pipe, VERDICT lines read. Lock A (waited 284s, held 165s — shared-box seconds): spec build 0 (34/34 .d.ts emitted), objectql build 0, service-settings build 0, git status empty; check:generated 0 'All 15 generated artifacts are up to date'; check:api-surface 0 'public API surface + factory signatures unchanged'; check:docs 0 '230 generated files in sync'; check:liveness 0 (36 rows current); git status empty. Lock B (waited 332s, held 53s): objectql typecheck 0 (check:test-typecheck OK, 44 files / 69 pinned); 6 suites engine-insert-static-readonly-strip + engine-autonumber-runtime-owned + validation/rule-validator + engine-lookup-referential-integrity + engine-repo-execute-elevation + engine-strict-readonly-warning-truthful: 6 files / 266 passed (the strict block — a static completed_at refused with ERR_READONLY_FIELD_REJECTED, 0 creates — is the pin the old operation TSDoc denied); service-settings typecheck 0; service-settings test 29 files / 519 passed (behaviour unchanged; the rotation and orphan-report suites green). Lock C (waited 0s, held 424s): turbo build client-react... + driver-turso... 0 (35 tasks); check:skill-examples 0 (257 examples); check:type-check-debt 0 (13 entries, 143 raw, none above record); .examples-build removed → check:docs-audit-scope 0; turbo build ./packages/** 0 (71/71 cached); check:dual-build-cjs-loads 0 (103/66/619/1 vs floors 90/58/520/1). Published-artefact measurement: before — (a) 2 .d.ts lines (+2 for :10-11), (b2) 4 .d.ts lines, (c) 2 .d.ts + 2 .js lines; (b1) (d) (e) (f) (g) 0 in every shipped file; controls 6/4/2/4/34/4 lines; after the rebuild — (a) (b2) (c) 0 in .d.ts/.js/.map, the new sentences present (dist/security/index.d.ts:270, dist/index.d.ts:365 and :379, service-settings dist/index.d.ts:1156), controls unchanged. Gate union derived from THIS worktree at a73dd85db (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths, 52-path change set from the merge-base) under its own STALE TREE warning (139 commits behind origin/main, 60 gate files changed — F4): 112 commands executed sequentially unlocked with per-command exit capture: 107 at 0 first run; check:cross-package-test-inputs 1 → 0 — red only under my runner's NODE_USE_ENV_PROXY=1 (Node's experimental-undici warning printed on import fails its own 'importing this module prints NOTHING' self-test; the first 84 commands ran with that env, only that self-test is sensitive; 0 with it unset — a runner artefact, declared); check:docs-audit-scope 1 → 0 with the gitignored packages/spec/.examples-build left by check:skill-examples removed (#15328, pre-existing); check:skill-examples 1 / check:type-check-debt 3 / check:dual-build-cjs-loads 3 = PREREQUISITE NOT MET → 0 after lock C's builds. Among the 0s: check:system-context-census + self-test ('105 sites / 19 packages / 44 files; 139 anchors resolve' — the one anchor in an edited file, readonly-strict-errors.ts:66, is above the edit, so no re-anchor), check:doc-authoring ('14753 customer-facing strings clean; 829 prose ids hold'), check:nul-bytes (7497 files), check:engine-double-contract, check:corpus-claim-drift, check:pm-dispatch-gates. The 3 workflow-valued changeset gates --base origin/main: 0/0/0 (valid no-migration-prescription; no major; 1 declaring changeset). Sweep: before/after with positive controls per pattern (numbers in summary); every edit proven on disk by grep -c of the deleted anchors (0) and the injected anchors (1), not by the edit script's exit. NOT MEASURED, declared: CI on this head (0 check-runs, structural); check:react-declaration-parity (objectui manifest, the on-demand pin-bump gate); the 10 workflow-valued $RUNNER_TEMP / matrix.shard families; the two main-only families absent from this branch; spec typecheck (comment-only edit compiled by the build's DTS pass); the lint / metadata-protocol / rest / service-automation / dogfood / example-todo suites of R3 (no file in those packages changed this round; R3's readings at the parent commit stand). No ablation (prose only; no behaviour changed).",
      "mcp_calls": "1 — one targeted search_issues for the residue card's dedupe after the REST /search endpoint answered 403 (control search also 403 — the channel, not the result). Every other GitHub read and write went through repo-scoped REST via curl with the container token: the card page payload (zero-quota), the three comments, the PR, related cards, the open finding list, the check-runs, the issue creation (201) and its read-back, the PR body PATCH (200) and its read-back, and this comment.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #15819: seven non-published sites still state the superseded readonly-on-INSERT contract as live — readonly-strict-errors.ts:21-22 (module docblock), settings-secret-rotation.test.ts:13 and :189, sys-secret-orphan-report.test.ts:190, rest-batch-endpoint.test.ts:308-312, crud-dropped-fields.test.ts:144-146, proof-registry.mts:156 — carded rather than fixed by the 2026-09-05 E3 ruling; plus rule-validator.ts:1021 (stripReadonlyFields docblock names UPDATE only; stale by omission) as a marked secondary entry. finding label only, unassigned, ungraded."
      ]
    }

    Generated by Claude Code

  9. zhuangjianguo commented on Sep 5, 2026

    @zhuangjianguo
    Collaborator

    Contract re-review (fourth) at CONTRACT_REVIEW_TIER — PASS, adopted verbatim. ⛔ Content limb only — this is not a landing.

    domain:engine execution seat, session_01ARYe3yQTQCUFm5qPYNgKaJ. Adoption record first, then the verdict unaltered.

    Transcript tier verification: 102 occurrences of "model":"claude-fable-5-1", no other model, over 794,739 bytes. ⇒ adoptable ⇒ adopted verbatim.

    Standard applied: the maintainer's E3 ruling 5550450281 (「已发布必修,其余立卡」). Reviews 1–3 applied the earlier standard and the verdict explicitly does not re-argue them — they were right under the standard in force.

    ⭐ What made this round different: 「published」 stopped being a judgment call

    R4 did not accept the dispatch's assumption that only N5(a) ships. It rebuilt three packages and grepped each sentence as a fixed string across dist/**/*.d.ts, dist/**/*.{js,mjs,cjs}, the .map files and content/docs/**, with a firing control per package, before and after. The reviewer reproduced that measurement in its own worktree. Result: three published sites, not one — and a fourth sentence in public-form.ts:10-11 making the same claim in a spelling none of verdict 3's patterns carried, found and fixed with a new pattern added for it.

    ⭐ And the honest boundary that could have quietly voided the whole ruling: every shipped .js.map embeds whole source, so on that reading nothing is ever non-published — which would make the ruling's own residue class empty. The dev reported the .map column and did not score it; the reviewer judged that line correct, in its words: 「a reading that empties the category the ruling created cannot be the ruling's meaning」.

    ⚠️ The verdict is explicit that a PASS here is not a merge

    ⛔ This verdict clears the content limb only — it is not a landing: the merge round (F2, now unblocked) and the merged head's CI (F3) are still ahead of it.

    ⇒ needs:contract-review stays hung on both carriers and the PR stays draft. ⛔ Nothing flipped, ⛔ nothing armed.

    F2's precondition is now met, and the reviewer caught it before this seat did. The dev's both-sides set said seven; between its measurement and the review, main took 4b3955ead (#15687 — the p0 #15225), putting packages/objectql/src/engine.ts on both sides. Re-measured here: eight files. ⇒ 「the deferral's stated precondition being met, not a defect」. The merge round is dispatched now.

    ⚠️ CI is structurally NOT MEASURED on every head since bd598e803 — total_count: 0 check-runs on a73dd85db, because GitHub runs no pull_request workflow on a conflicted PR. The merge round's push is the first head CI will see; the landing reads that head's checks, ⛔ never an older sha.

    One new escalation — E4, for the standard's home

    The reviewer found two boundaries the E3 ruling needs written down, exposed by actually performing the measurement: (i) .js.map sourcesContent is not a published artefact for this purpose; (ii) a package that ships source in files — @objectstack/spec's src/**/*.zod.ts — publishes floating comments in those files, which a .d.ts-and-docs method cannot see. ⚠️ Neither changes this PR (no live-class site sits in a .zod.ts — all such lines classify as R or U), but the next card that applies the standard will hit them. Its home is the contract-review reference, a governed surface — ⛔ draft-only, human merge, ⛔ not this seat's to write.


    Contract re-review (fourth) at CONTRACT_REVIEW_TIER — PASS — PR #15395 @ a73dd85dbf9c8a48f78143f24e647613b94d99e8 (merge-base a06faebbe)

    Standard applied: the maintainer's E3 ruling 5550450281 (「已发布必修,其余立卡」), as instructed — published surface wrong ⇒ fixed in the round; non-published residue ⇒ one finding card, and the PR lands. Reviews 1–3 applied the earlier standard and are not re-argued.

    Read: card #14147 body and all 20 comments (ruling C 5522734749; the E3 ruling; verdict 3 5550434842 in full; the R4 dev report 5550821479; verdict 2, the R3 report and receipt); PR #15395 body (949 lines — R4 section, R3 sweep section, narrowings, changeset derivation, cross-lane table), 10 comments, 26 commits; residue card #15819 (no comments); the R4 diff 485a2d525..a73dd85db (4 files, +28/−11) line by line; at head: engine.ts:10186-10345, rule-validator.ts (stripReadonlyFields, stripRuntimeOwnedFields, staticReadonlyInsertSubject and their docblocks), readonly-strict-errors.ts whole, public-form.ts whole, settings-service.ts:2255-2310, sys-setting.object.ts, sys-secret.object.ts, the seven carded sites, rule-validator.ts:1021, the R3 templates (rest-server.ts:12355-12376, crud-nodes.ts:318-332), .changeset/tidy-cups-smile.md, the bump-level rule text as b337a1308 introduced it, clause ② at .claude/skills/pm-dispatch/SKILL.md:588-600; the built dist of the three packages and 19 others; the GitHub check-runs and combined status on the head.

    Short form. 「Published」 was measured, not sorted, and the measurement reproduces here from a rebuilt dist with a firing control per package: three sites ship in a .d.ts — (a) both sentences, (b2) the operation TSDoc, (c) the upsertRow docblock which also ships in the .js — and all three now carry the ruled state; (b1), (d), (e), (f), (g) reach nothing shipped. The sweep is bound by claim: my reconstruction returns the same 147 unique file:line at head and 150 at 485a2d525, both comm directions against the published list are empty, every added pattern fires on its N5 target before and returns zero published live-class hits after, and every one of the 147 classifications holds on the line it carries. A claim-bound grep over all 212 built .d.ts files finds zero live-class sentences. #15819 is a complete carrier. The engine change is exactly ruling C, re-read from the tree; clause ② is yes on the right ground; every level is right and the changeset gates are 0/0/0. R4's three docblocks state no wrong fact. Under the ruled standard this is a PASS, and I say so plainly.


    Readings

    Locked runs: one acquisition, slot review4-15395-fable, --status read first (free, queue empty, arriving depth 0); held 150s, waited 0s (shared-box seconds). Every step wrote its own exit before any pipe; the wrapper's VERDICT line reads command-exit 0. Unlocked rows are check:* scripts, git grep, git merge-file and REST reads. Review worktree /home/user/objectstack-review4-15395, fresh, detached at a73dd85db.

    # command exit result
    1 pnpm install --frozen-lockfile (unlocked, per os-dev.md:123-124) 0 4.9s warm store; git status empty
    2 pnpm turbo run build --filter='@objectstack/{spec,objectql,service-settings,service-automation}...' 0 22/22 tasks, 22 cached (FULL TURBO) — the content-keyed hits are legitimate only if the restored dist carries this head's text, which readings 12–16 verify (old sentence absent AND new sentence present, control firing); git status empty after
    3 pnpm --filter @objectstack/spec check:generated 0 «All 15 generated artifacts are up to date» — no projection regenerates from a TSDoc on a const
    4 pnpm --filter @objectstack/spec check:api-surface 0 «public API surface + factory signatures unchanged ✓»
    5 pnpm --filter @objectstack/spec check:docs 0 «230 generated files in sync with packages/spec»
    6 pnpm --filter @objectstack/spec check:liveness 0 ledger rows current
    7 pnpm --filter @objectstack/spec typecheck (the dev declared this NOT MEASURED; measured here) 0 tsc clean; check:test-typecheck OK 54 files / 145 pinned
    8 pnpm --filter @objectstack/objectql typecheck 0 tsc clean; test layer OK 44 files / 69 pinned
    9 objectql vitest: engine-insert-static-readonly-strip, engine-autonumber-runtime-owned, validation/rule-validator, engine-lookup-referential-integrity, engine-repo-execute-elevation, engine-strict-readonly-warning-truthful 0 6 files, 266 passed
    10 pnpm --filter @objectstack/service-settings typecheck · test 0 · 0 tsc clean · 29 files, 519 passed
    11 service-automation vitest: create-record-readonly-drop, crud-dropped-fields 0 2 files, 7 passed
    12 (a) built spec .d.ts/.d.mts (128 files): old insert may seed \readonly` columns· oldcovers UPDATE(p6 form, reading 20) · newnever a column by NAME· newso an import seeds read-only· controlPUBLIC_FORM_SERVER_MANAGED_FIELDS` 1 · 1 · 0 · 0 · 0 0 · 0 · 2 (dist/security/index.d.ts:270, .d.mts) · 2 (:283) · 6 lines. Old text also 0 in the 44 .js/.mjs and the 44 .map
    13 (b1) built objectql .d.ts (6) / .js (4) / .map (4): INSERT only the runtime-owned ones 1 / 1 / 0 0 / 0 / 4 — the module docblock reaches only sourcesContent; the two .d.ts hits on deliberately exempt are a different sentence («deliberately exempted isSystem writes», util-*.d.ts:2434)
    14 (b2) objectql .d.ts: old can only ever be about a runtime-owned value · new since the 2026-09-03 ruling (#14147), about · control ReadonlyFieldRejectedError 1 · 0 · 0 0 · 4 (dist/index.d.ts:364, :378, .d.mts ×2 — the constructor parameter and the property declaration) · 4
    15 (c) service-settings .d.ts/.d.cts · .js/.cjs: old INSERT path is deliberately exempt · new staticReadonlyInsertSubject · control Why the update is a SYSTEM write 1/1 · 0/0 · 0 0/0 · .d.ts:1154, .d.cts:1154 / .js:1726, .cjs:1807 · 2
    16 (d)(e)(f) the four test-file sentences over all 22 present dist dirs; find … -name '*.test.*' inside any dist 1 ×4 0 hits; 0 test files in any dist (structural: files: ["dist"], tests are not entry points). ⚠️ packages/rest/dist was not built here — (e) is a test file, so the structural argument carries it
    17 (g) insert exempt) over real HTTP over packages/spec/{dist,liveness,json-schema,prompts,api-surface} + content/docs; control readonly-static-write in liveness/ 1 · 0 0 · 2 (field.json:101, README.md:492). scripts/ is outside spec's files (read: ["dist","json-schema","liveness","prompts","llms.txt","README.md","src/**/*.zod.ts","CHANGELOG.md","api-surface","spec-changes.json"])
    18 Before-state, read-only corroboration from the prior reviewer's untouched tree /home/user/objectstack-rereview2-15395 (detached at 485a2d525, clean, dist mtime 07:38 — before R4's 08:25 commit) 0 old sentences at exactly the dev's line numbers: spec dist/security/index.d.ts:278, objectql dist/index.d.ts:362, :373, service-settings dist/index.d.ts:1150, dist/index.js:1722. Not built by me; the primary evidence for «before» is the R4 diff itself, which changes only text inside the same three docblocks readings 12/14/15 show shipping
    19 The shipped #3413 residual: spec/dist/analytics.zod-*.d.ts:1318-1320 0 contracts/data-engine.ts:145-146 — «true when written, SUPERSEDED since: engine.insert now runs the static-readonly strip» — class H, published, true
    20 Claim-bound grep over all 212 built .d.ts/.d.mts/.d.cts: A · B · p1 · p2 · p3 · p4 · p5 · p6 1 · 0 · 0 · 1 · 0 · 0 · 0 · 1 0 · 2 · 6 · 0 · 6 · 6 · 2 · 0 lines; deduped: SEED_OPTIONS (U), engine.ts:4610 adoption (U), platform-objects «Layer 2 ingress» (U), service-settings:1151 «"INSERT exempt" row (superseded…» (H+R, R4's), data-engine.ts:145-146 (H, reading 19), share-link-service / seed-settlement / temporal-conformance (U). Zero live-class in any shipped declaration
    21 Sweep reconstruction (sweep.sh: scope packages content docs skills scripts, :!**/CHANGELOG.md :!content/docs/releases/** :!content/docs/references/**, git grep -n -iE, pass B content-only, one regex per line) at a73dd85db A 0 · B 0 · p1 0 · p2 0 · p3 0 · p4 0 · p5 0 · p6 0 31 · 32 · 59 · 3 · 7 · 40 · 16 · 5; union 147 unique file:line
    22 Same at 485a2d525 (positive control, via git grep … <tree>) all 0 31 · 32 · 60 · 3 · 8 · 41 · 17 · 6; union 150. Before-only: readonly-strict-errors.ts:170, settings-service.ts:2279, public-form.ts:11, :22; head-only: settings-service.ts:2280 — exactly the dev's delta
    23 comm my head union ↔ the PR body's 147 rows (parsed: 147 rows, 147 unique keys, class counts H 42 · R 33 · U 33 · RW 11 · #7823 8 · L 8 · H+R 6 · P 2 · D 1 · S 1 · F 1 · G 1 = the table's header) — both directions empty
    24 Claim patterns over content/docs/references/** at head (published, excluded from the sweep by construction); control droppedFields A 1 · B 0 · p1 0 · p2 1 · p3 1 · p4 0 · p5 1 · p6 1 0 · 13 · 18 · 0 · 0 · 1 · 0 · 0 — every hit is the preserveAudit describe («does not apply on INSERT»), the readonly describe («BOTH write paths») or the droppedFields describe; control 20 lines across 3 files. Zero live-class
    25 node scripts/check-adr-0087-registration.mjs · check-changeset-no-major.mjs · check-empty-changeset.mjs, each --base origin/main 0 · 0 · 0 valid no-migration-prescription · «no major bump» · «1 declaring changeset(s) added»
    26 node scripts/check-doc-authoring.mjs 0 46 skill files clean; 14753 customer-facing strings clean; 829 prose ids hold
    27 pnpm run check:cross-package-test-inputs with NODE_USE_ENV_PROXY unset (it is unset in this runner) · with NODE_USE_ENV_PROXY=1 0 · 1 «117 self-test cases passed; 26 packages read outside themselves, all declared» · 1/117 fails: importing this module prints NOTHING — the case asserts stderr === '' (scripts/check-cross-package-test-inputs.mjs:2433-2434) and Node 22.22.2 prints [UNDICI-EHPA] Warning: EnvHttpProxyAgent is experimental on import under that env (probe: NODE_USE_ENV_PROXY=1 node -e "" prints it, exit 0). The dev's explanation holds; it hides nothing
    28 GET check-runs on a73dd85db · combined status HTTP 200 total_count: 0 · success = Vercel only, «Canceled by Ignored Build Step». PR mergeable: null, mergeable_state: dirty, draft: true ⇒ CI on this head: NOT MEASURED (structural)
    29 Both-sides set by content: diff --name-only a06faebbe...HEAD ∩ a06faebbe..origin/main (origin/main = 9140c7681, branch 146 commits behind) — EIGHT files — the dev's seven plus packages/objectql/src/engine.ts, which gained 4b3955ead «fix(objectql): a published BulkDataEvent names the one organization… (#15687)» — i.e. the p0 #15225 has landed
    30 git merge-file -p (ours=head, base, theirs=main) per both-sides file — informational, ⛔ not a pass system-context.mdx 10 · the other seven 0 the .mdx is merge=os-regen (.gitattributes:154) — regenerate, never hand-merge; settings-service.ts merged carries R4's sentence (1) and main's value_domain (5); main's hunks there are at base :33, :1810, :2050-2077, R4's at :2275-2288 — disjoint by content
    31 engine.ts hunk sets read side by side — main's: imports, a new bulkEventOrganizationId helper (:2239-2385), the bulk-event publisher (:5860, :5896); the PR's: an import, two doc comments in assertReferencesResolve (:6014, :6058), the bulk-strip note (:9611), the insert strip (:10179-10240). Different functions; textual merge clean (reading 30) — semantic disjointness read, not merely diffed
    32 git diff --name-only a06faebbe...HEAD -- 'content/docs/releases/**' 0 files control content/docs/**: 10 files
    33 git status --porcelain in the review worktree, final · verify lock --status 0 lines · free, queue empty the primary checkout and every other worktree untouched

    NOT MEASURED, and why: CI on this head (reading 28 — structural; the landing must read the merged head's checks, never bd598e803's); check:react-declaration-parity (objectui manifest); the @objectstack/rest suites and dist (no file in that package changed in R4; (e) is a test file — structural); the lint / metadata-protocol / dogfood / example-todo suites (R3's readings at the parent commit 485a2d525 stand — R4 is comment-only in three files plus a changeset, and the three packages it touched are typechecked and tested above); the 112-command gate union beyond the rows above; a full rebuild of 485a2d525 (reading 18 is corroborative, read-only, from another agent's tree).


    A. Item 1 — was 「published」 measured, and is every published site fixed?

    Measured, and the measurement reproduces. Readings 12–17 are my own greps over a dist restored in my worktree, each with a control that fires. Verdict 3's assumption (only N5(a) is published) is falsified exactly as the dev reports:

    site ships in evidence R4
    (a) public-form.ts:21-22 and :10-11 dist/security/index.d.ts (+.d.mts) reading 12; the :10-11 claim («only covers UPDATE — so nothing downstream guards these columns») was the same claim in a spelling none of verdict 3's patterns carried — p6 now catches it (reading 22: 6 → 5, the removed line is public-form.ts:11) fixed, both sentences
    (b1) readonly-strict-errors.ts:21-22 nothing but .map sourcesContent reading 13 — a floating docblock ahead of the non-exported READONLY_CLASS_REASONS; declaration emit drops it, esbuild drops it carded (#15819 row 1) — correct under the ruling
    (b2) readonly-strict-errors.ts:170-172 dist/index.d.ts:364, :378 reading 14 — the operation parameter property of the exported class emits twice fixed
    (c) settings-service.ts:2279 dist/index.d.ts:1154 and dist/index.js:1726 / .cjs:1807 reading 15 — tsup keeps the comment in the JS fixed; '@objectstack/service-settings': patch added so the corrected .d.ts ships
    (d) three service-settings test docblocks · (e) rest-batch-endpoint.test.ts:308-312 · (f) crud-dropped-fields.test.ts:144-146 · (g) proof-registry.mts:156 nothing readings 16–17 carded (rows 2–7)

    The .map boundary — the dev drew the right line. sourcemap: true in every tsup config and files: ["dist"] with no .npmignore means the shipped .js.map embeds whole sources (reading 13: sourcesContent present, 2.4 MB for objectql). On that reading no source comment is ever non-published, which would make the ruling's own examples («test headers, in-source notes that reach no published artefact, scripts/ provenance») an empty class — a reading that empties the category the ruling created cannot be the ruling's meaning. A consumer reads .d.ts in the IDE and .js on disk; nobody reads a .map's sourcesContent as documentation. Reporting the .map column without scoring it is the honest form. ⚠️ One boundary the dev's method does not cover and should be written where the standard lives (F5): @objectstack/spec ships src/**/*.zod.ts as source, so a floating comment in a .zod.ts is published even though it reaches no .d.ts. No live-class site lives in one (the sweep's spec .zod.ts lines — protocol.zod.ts:1975, field.zod.ts:1581, execution-context.zod.ts:392 — are all R; stack.zod.ts:1427 is U), so nothing changes here.

    Zone 2 — (1) falsified as the dev says; (2) held (reading 3, empty git status); (3) held (reading 4).

    B. Item 2 — is the sweep bound by claim rather than spelling?

    Yes. Reading 21 reproduces the dev's per-pattern counts and the 147-line union to the line; reading 23's comm is empty in both directions; reading 22 proves every added pattern fires at 485a2d525 on the N5 sites it was meant to catch (p1 60→59 drops :170/:2279, p3 8→7 and p4 41→40 drop public-form.ts:22, p5 17→16 drops :2279, p6 6→5 drops public-form.ts:11) and that the only line added is R4's own «superseded row named as superseded» (settings-service.ts:2280, H+R). Pass B is content-only (32, the reviewer's number; the R3 naive form's 10 filename-admitted rows are named and struck in place). The class is stated in one sentence in the PR body.

    Classification audit — all 147 lines read with their content (readings 21/23 dumped every line beside its declared class). The classes holding exactly one line each hold: D protocol.readonly-insert.test.ts:106 is a describe title stating delegation to engine.insert — true; S rule-validator.ts:1021 «from an UPDATE payload» names UPDATE only while the function runs on both verbs (engine.ts:10264 insert, :11561/:11741 update) — stale by omission, carded as secondary; F evidence.test.ts:33 is a two-pointer fixture string fed to a parser — inert; G SKILL.md:274 — governed, #15382. The L class is 8 lines = 7 sites = #15819 rows 1–7 exactly. Every H line I read is past-tense or quotation («lived», «used to», «was», «when this note was written», «SUPERSEDED since»); every RW line is the readonlyWhen strip (engine.ts:9632, :11687, rule-validator.ts:643, :872, field.json:87, data-engine.mdx:311/:377); every R line states the in-engine strip or a runtime-owned exemption that is still live; the U lines are the TypeScript readonly keyword beside a seed* identifier, autonumber adoption, the #3433 initialStates exemption, or posture exemptions. The one imprecision verdict 3 already noted (validate-readonly-action-writes.ts:64 «no longer engine-exempt from anything», qualified by its next paragraph) is unchanged and not the class. Honest list.

    Two further published-surface checks the sweep excludes by construction, both clean: the built .d.ts directly (reading 20) and content/docs/references/** (reading 24).

    C. Item 3 — is #15819 an honest carrier?

    Yes, and recoverable. Seven sites by file:line at a pinned sha, each with the sentence verbatim (I read all seven at head — reading table above — and they say what the card says), why each is the class, and for (d) and (e) the mechanism that keeps the object's behaviour true while the sentence is false (sys_setting/sys_secret: sys_-prefixed, managedBy: 'engine-owned' — verified in both object files — carved out by staticReadonlyInsertSubject, #15719). The measurement method is stated with its boundary (.map declared, not scored). rule-validator.ts:1021 is a marked secondary entry with the right characterisation. SKILL.md:274 is excluded by name to #15382; data-engine.mdx:371-373 is excluded as historical. The ruling is stated plainly («the maintainer ruled these sites are carded rather than fixed»). How-to-close names the three R3/R4 templates (which are correct — I read all three) and asks for the sweep re-run with the same positive controls. Cross-links are complete. The one dependency on a mutable surface — the sweep's full list lives in the PR body — is acceptable because the card carries the patterns in prose and the PR head is pinned. Dedupe was done against a 403'd REST search with a working MCP fallback; the nearest neighbours named (#15394, #15703) are indeed different findings.

    D. The contract limb, semver, clause ② — afresh

    Engine = ruling C, re-read from the tree. engine.ts:10193 if (!opCtx.context?.isSystem) gates both passes; runtime-owned first (:10210, with preserveAudit and hookWrittenKeys); then staticReadonlyInsertSubject (:10259) — readonly: true fields minus runtime-owned types, null on managedBy or sys_ (rule-validator.ts:1519-1535) — fed to the same stripReadonlyFields update runs (:10264) with verb: 'insert', the strict flag and hookWrittenKeys, no preserveAudit (preserveAuditIgnoredOnInsertWarning at :10307); defaultValue re-derived over the stripped row for taken keys only (:10295-10300); one insertDropped union → one readonly event or one ReadonlyFieldRejectedError(…, 'insert') before any driver dispatch (:10324-10336). The UPDATE path calls stripReadonlyFields over the full updateSchema with no subject view (:11561, :11741) — the asymmetry (c)'s sentence and #15719 describe. Pinned by reading 9 (266). The three carried narrowings (runtime-owned pass kept separate; preserveAudit not forwarded on create; platform-object carve-out) are each outside what ruling C superseded and each stated where the code lives — sound, as three reviews found; nothing new to add.

    Clause ② — yes, on independent grounds. engine.insert is on @objectstack/objectql's . export and the five DataProtocol create faces are published; their accept-set for a caller-supplied readonly field moves. That is a published contract face (SKILL.md:595 「条款②只指已发布契约面」). The 2026-08-28 negative boundary (:593-595 — runtime permission/security behaviour changes go to the human floor) does not pull it out: no who-may gate, RLS, FLS or permission moves. needs:contract-review on both carriers is right.

    Semver, against b337a1308's text (read from the commit: «a purely additive widening … takes at least minor; the commit type may raise a bump but never lower it below what the act requires; a fix( that changes no public surface stays patch; major stays refused; breaking-ness carried by the banner plus the ADR-0087 disposition»). No package widens a surface (reading 4; verdict 3's reading 39 stands, unchanged files). objectql / metadata-protocol minor — ruling C prescribed «minor with a BREAKING note», the rule permits raising, refuses major; banner present, no-migration-prescription valid (reading 25). service-automation, lint, spec patch — prose/text face. service-settings patch — prose only; the package is released (private unset, 17.3.0); a .d.ts comment change is «no public surface» ⇒ patch is the floor and the ceiling. The bullet added to the changeset says exactly what R4 did. F6 (a behaviour narrowing with no surface change is not decided by the rule's sentences) stands as verdict 3 wrote it.

    E2 — unchanged; engine-repo-execute-elevation is among the six suites (reading 9). Recorded for the maintainer, not re-litigated.

    E. New defects R4 introduced — none

    Each corrected sentence checked as a fact against the tree it describes:

    Things that were not taken on trust


    Verdict

    PASS, pinned to a73dd85dbf9c8a48f78143f24e647613b94d99e8.

    Under the E3 standard: every site the measurement shows reaching a published artefact is corrected and the correction is true; the sweep is bound by claim and its list is exactly reproducible; the residue is carded on #15819 completely enough to be closed from the card alone; the engine change, the narrowings, clause ② and the levels are right; R4 introduced no wrong stated fact. No patch round is owed. ⛔ This verdict clears the content limb only — it is not a landing: the merge round (F2, now unblocked) and the merged head's CI (F3) are still ahead of it, and the governed half remains #15382.

    Escalations — not patch items

    • E2 — unchanged, recorded for the maintainer (buildActionExecutionContext pinned isSystem: true; two reviews before this one accepted the reasoned refusal, and so does this one).
    • E4 — for the standard's home (governed surface, human merge): the E3 ruling's «published» needs two written boundaries the measurement exposed: (i) .js.map sourcesContent is not a published artefact for this purpose (otherwise the ruling's own residue class is empty); (ii) a package that ships source in files — @objectstack/spec's src/**/*.zod.ts — publishes floating comments in those files, which the .d.ts/docs method does not see. Neither changes this PR's outcome.

    Boundary flags

    Nothing was posted to GitHub. Review worktree /home/user/objectstack-review4-15395, detached at a73dd85db, git status clean; the primary checkout and every other worktree were not edited; no git stash; the verify lock is free.


    Generated by Claude Code

  10. claude commented on Sep 5, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 14147,
      "status": "done",
      "branch": "claude/issue-14147-engine-insert-readonly-strip",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15395",
      "premise_still_valid": true,
      "merge_commit": "b88b8f70bdb2452f44fe46418514136b9ae9eab0 (parents a73dd85db + 2de7e5352; no history rewritten)",
      "head": "2f0262ca14e2dcccebda6ba0a9abba68e911f69d (the census re-anchor commit on top of the merge; every gate and suite below ran on this tree, git rev-parse --short HEAD = 2f0262ca1)",
      "summary": "F2 merge round for PR #15395 on card #14147. Merged origin/main (actual second parent 2de7e5352, 151 commits) into the branch in a dedicated worktree; no rebase, amend or force-push. The both-sides set recomputed to exactly the eight files in A2 (merge base a06faebbe). Seven auto-merged with every hunk of both sides present, verified line-for-line against each parent's diff from the base. The eighth, content/docs/permissions/system-context.mdx (merge=os-regen, the MIXED row), was NOT silently dropped by the driver: the #14064 line-anchors comparator refused to defer because both sides carried prose, text-merged it and conflicted loudly in 10 regions. I hand-merged the prose (branch's merged INSERT row 20 kept, main's new bulk-event row inserted as row 29, numbering from Sharing onward back to the base's, counts 106 sites / 19 packages / 44 files) and regenerated the anchors with pnpm gen:system-context-census in a second commit. Two NON_READ_ANCHORS why strings the branch had renumbered (row 33, row 59) were set to row 34 / row 60 for the merged numbering in the merge commit. Both commits pushed; PR left draft, labels untouched, no auto-merge.",
      "both_sides_resolution": [
        "content/docs/permissions/system-context.mdx — BRANCH: collapsed base rows 20+21 (INSERT engine pass + INSERT protocol ingress) into one INSERT row 20, removed the metadata-protocol site (106 sites/20 packages/45 files to 105/19/44), renumbered rows 22-65 by -1, updated the prose row refs (row 30 to 29, Row 22 to 21, Row 34 to 33, row 50 to 49) and the section-5 decomposition (112 reads to 111, rows 1-61 to 1-60, rows 62-65 to 61-64). MAIN (6 commits, last #15687): added row 30 'Bulk data event organizationId OMITTED' (106 to 107 sites, 20 packages), renumbered rows 30-65 by +1, refs row 30 to 31 and row 50 to 51, decomposition 112 to 113 / rows 1-62 / 63-66, all anchors re-derived on main. RESOLUTION: hand-merged prose — branch's row 20 text, main's new row as row 29, rows 30-65 at base numbering, counts 106/19/44, reads 112, behaviour-bearing rows 1-61 = 102, carry-onward rows 62-65 = 4, 'Row 21' from the branch, 'Row 34' (revoke) correct again; anchors regenerated, never hand-numbered (proof below).",
        "packages/lint/src/validate-readonly-flow-writes.ts — BRANCH: header scope bullet rewritten (the INSERT exemption is superseded; create_record readonly writes are a recorded scan gap). MAIN (#15751): local asArray replaced by recordsOf from object-graph.js. Disjoint hunks; git auto-merged; both hunk sets verified present; lint validator suite 124/124 pass.",
        "packages/lint/src/validate-readonly-hook-writes.ts — BRANCH: header scope bullet and the STRIP_SUBJECT_METHODS docblock rewritten (insert/create absent as a scan gap, not a decision). MAIN (#15751): asArray to recordsOf. Disjoint; auto-merged; verified; same suite pass.",
        "packages/objectql/src/engine.ts — BRANCH: imports staticReadonlyInsertSubject / preserveAuditIgnoredOnInsertWarning, two comment rewrites in the referential-integrity check, the insert() observability docblock, autonumberDropped renamed insertDropped and the new static-readonly strip block inside insert(). MAIN (#15687): tenancy imports, eventOrganizationValue + bulkEventOrganizationId helpers, the enforcedTenancyPosture() accessor, organizationId stamped on BulkDataEvent. Line ranges disjoint (branch at ~193/6014/6058/9611/10179-10330, main at ~138/170/2239/3896/5860/5896); auto-merged; verified; objectql typecheck exit 0; touched objectql tests pass including main's engine-data-events.test.ts.",
        "packages/rest/src/rest-server.ts — BRANCH: two comment rewrites only (POST /data/:object droppedFields header note; batch-create routing rationale). MAIN (5 commits: #15707, #15726, #15689, #15619, #15436): i18n fallback-locale lookup, /meta/diagnostics and /references organization forwarding, a docs comment, the stranded-approval 500 envelope. Disjoint; auto-merged; verified; check:route-envelope exit 0; rest touched tests pass.",
        "packages/services/service-settings/src/settings-service.ts — BRANCH: one docblock in the secret-rotation note (sys_setting INSERT sits outside the strip via the staticReadonlyInsertSubject platform carve-out, #15719, not the superseded exemption). MAIN (#15434): value_domain refusal code rendered from the ADR-0114 catalog, new imports. Disjoint; auto-merged; verified; full service-settings suite 30 files / 533 tests pass.",
        "packages/spec/liveness/field.json — BRANCH: the readonly row's evidence/note (engine.insert is the one enforcement point). MAIN (#15316): the valueDomain row flipped planned to live. Different keys; auto-merged; verified; spec check:generated (which runs the spec package's check:liveness over liveness/state-counts.md) exit 0; proof-registry.test.ts 39/39; check-reference-carrier-shape.mjs --self-test exit 0.",
        "scripts/engine-double-contract.pinned.json — BRANCH: +1 entry (protocol.readonly-insert.test.ts findOne). MAIN: +8 entries (mcp stdio-tenancy-posture-api-key-matrix x3, protocol-publish-drafts-object-field-refs x3, decision-strand-envelope x2). Different positions; auto-merged; verified; check:engine-double-contract exit 0.",
        "Ninth file differing from both parents: scripts/check-system-context-census.mjs — branch-only edit (seven why strings renumbered); in the merge commit two of them (row 33, row 59) became row 34 / row 60 because main's inserted row restores base numbering from Sharing onward. These strings are diagnostic-only (not asserted by the gate)."
      ],
      "census_page_proof": "Three independent readings. (1) Anchor-normalized comparison using blankAnchorLineNumbers from scripts/doc-line-anchors.mjs: the merged page vs the BRANCH parent differs only by main's contribution (count 105 to 106, the new bulk-event row, the +1 renumbering from Sharing onward, row 29 to 30 refs, Row 33 to Row 34, decomposition 111 to 112 / 105 to 106 / rows 1-61 / 62-65); vs the MAIN parent it differs only by the branch's contribution (107 to 106, 20 to 19 packages, rows 20+21 to the merged row 20, the -1 renumbering, 17 of the 106, row 31 to 30 refs, Row 22 to Row 21, ruling 106/19, 113 to 112, 45 to 44 files). Zero conflict markers. (2) The generator as the instrument: the first pnpm gen:system-context-census refused engine.ts ('page anchors 16 distinct lines, tree holds 15 anchorable: 11 census sites + 4 NON_READ_ANCHORS') — diagnosed as one anchorable line under two coordinate systems (row 20 carried the branch tree's 10193 for the INSERT isSystem site, the validation-row range carried main's 10356 for the same line). Putting row 20 on main's coordinate (the driver's own 'take either side' instruction) made the page one coordinate system; the rerun rewrote 11 engine.ts anchors (6223 to 6225, 6921 to 6924, 10339 to 10349, 10356 to 10366, 10404 to 10499, 11623 to 11718, 11806 to 11901, 12351 to 12446, 12422 to 12517, 14866 to 14961) and REFUSED ZERO files; pnpm check:system-context-census exit 0 with '106 elevation read sites in 19 packages across 44 files, all anchored; 140 anchors resolve, 27 declared non-read' — the census re-derived from the tree equals the hand-derived counts, i.e. the population is exactly base minus the deleted ingress site plus main's new bulk-event site. (3) The re-anchor commit's anchor-normalized diff against the merge commit is empty: it moved numbers and nothing else.",
      "tests": "All at head 2f0262ca1, exit codes captured by redirecting output to a file before any pipe; lock runs cite the VERDICT line. BUILD/INSTALL: pnpm install exit 0; pnpm install --frozen-lockfile (post-merge, lockfile moved on main) exit 0; os-verify-lock -c 'pnpm --filter @objectstack/spec build' VERDICT command-exit 0 held 107s; os-verify-lock -c 'pnpm turbo run build --filter=@objectstack/objectql... --concurrency=2' VERDICT command-exit 0 (15/15 tasks); os-verify-lock -c 'pnpm turbo run build --filter=@objectstack/rest... --filter=@objectstack/service-automation... --filter=@objectstack/lint... --filter=@objectstack/mcp... --filter=@objectstack/service-settings... --filter=@objectstack/metadata-protocol... --filter=@objectstack/client-react... --concurrency=2' VERDICT command-exit 0 (36/36, 15 cached). GATES: pnpm gen:system-context-census run 1 exit 1 (refused engine.ts as described; re-anchored field.zod.ts:1580 to 1581 and stdio-data-bridge.ts:246 to 250), run 2 exit 0 (11 rewritten, 0 refused); pnpm check:system-context-census exit 0 (106/19/44, 140 anchors, 27 non-read); os-verify-lock -c 'pnpm --filter @objectstack/spec run check:generated' VERDICT command-exit 0 ('All 15 generated artifacts are up to date', incl. check:docs, check:api-surface, check:authorable-surface, check:liveness, check:skill-refs); root gates batch 1a (each 'pnpm CHECK'): check:nul-bytes=0, check:engine-double-contract=0, check:merge-driver=0, check:route-envelope=0, check:stack-collection-maps=0, check:swallow-census-controls=0, check:error-code-casing=0, check:filter-alias-parity=0, check:durability-log-level=0, check:dispatcher-error-vocabulary=0, check:objectql-double-limit=0, check:where-matcher=0, check:slot-lookup=0, check:test-source-alias=0, check:cross-package-test-inputs=0, check:entry-guard=0, check:parse-guard=0, check:ratchet-remedy-authority=0, check:pnpm-filter-targets=0, check:cli-command-ids=0, check:changeset-gate-self-tests=0, check:objectui-changeset=0, check:authz-resolver=0, check:spec-parsed-alias=0; batch 1b (root gates as 'pnpm CHECK', spec ones as 'pnpm --filter @objectstack/spec run CHECK', plus 'node packages/lint/scripts/check-reference-carrier-shape.mjs --self-test'): check:doc-anchors=0, check:doc-authoring=0, check:docs-single-h1=0, check:corpus-claim-drift=0, check:docs-audit-scope=0, check:role-word=0, check:docs-transcript-drift=0, check:docs-redirects=0, check:quick-reference-counts=0, check:skill-identifier-liveness=0, check:vendor-version-stamps=0, check:published-readme-links=0, check:react-page-adapter-contract=0, check:agent-test-spelling=0, check:bash32-floor=0, check:examples-live-imports=0, check:logger-receiver-detach=0, check:org-identifier=0, check:page-declaration-shape=0, check:published-files=0, spec:check:llms-txt=0, spec:check:skill-examples=1, spec:check:yaml-examples=0, spec:check:empty-state=0, spec:check:variant-docs=0, spec:check:objectui-pin-citations=0, spec:check:dual-source-exports=0, spec:check:entry-nameability=0, spec:check:exported-any=0, spec:check:browser-reachable-entries=0, carrier-shape-self-test=0 — the one non-zero, spec check:skill-examples=1, was PREREQUISITE NOT MET (wanted @objectstack/client-react built), re-run after that build: exit 0 ('257 prose examples type-check across 3 surface(s)'). TYPECHECK: pnpm --filter @objectstack/objectql typecheck exit 0; pnpm --filter @objectstack/spec typecheck exit 0. SUITES (one os-verify-lock call, VERDICT command-exit 0, held 82s; per-suite exits captured inside): pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-readonly-flow-writes.test.ts src/validate-readonly-hook-writes.test.ts src/validate-readonly-action-writes.test.ts src/validate-flow-node-writes.test.ts exit 0 (4 files, 124 tests); pnpm --filter @objectstack/service-settings exec vitest run --maxWorkers=2 exit 0 (30 files, 533 tests, the whole package); pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/rest-dropped-fields.test.ts src/import-runner-historical-readonly-insert.test.ts exit 0 (2 files, 7 tests); pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/engine-insert-static-readonly-strip.test.ts src/engine-autonumber-runtime-owned.test.ts src/engine-lookup-referential-integrity.test.ts src/validation/rule-validator.test.ts src/engine-data-events.test.ts exit 0 (5 passed (5) / 292 passed (292)); pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 src/protocol.readonly-insert.test.ts src/protocol.dropped-fields.test.ts src/protocol.dropped-fields.bulk.test.ts exit 0 (3 files, 25 tests); pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 scripts/liveness/proof-registry.test.ts exit 0 (1 file, 39 tests). DERIVATION: node scripts/pm/dispatch-gates.mjs exit 0, 'derived from objectstack-ai/objectstack at commit 2f0262ca1', 52 paths vs merge base 2de7e5352. NOT RUN LOCALLY (declared narrowing, the farm is CI's): the full vitest suites of objectql (273 files), rest (183), lint (97), metadata-protocol (165), service-automation (111), mcp (26) and spec (473); every dogfood suite; typecheck for rest, lint, service-settings, metadata-protocol, service-automation, mcp; pnpm lint; the dist-dependent root gates check:dts-closure, check:dual-build-cjs-loads, check:type-source-resolution; the spec-package check:api-surface etc. only as run inside check:generated. No ablation in this round (nothing was implemented).",
      "zone2_readings": [
        "A1 matched: mergeable_state dirty, mergeable false, 0 check-runs on a73dd85db. One legacy commit STATUS existed (Vercel, state success, 'Canceled by Ignored Build Step') — not a check-run, and not a signal.",
        "A2 matched exactly: the eight files, recomputed at merge base a06faebbe against both 3e3ecb0e8 (origin/main at my first read) and 2de7e5352 (the parent actually merged).",
        "A3 reading: 150 behind at 3e3ecb0e8; the shared refs/remotes/origin/main advanced to 2de7e5352 (#15801, one tooling commit) between that read and my git merge — a sibling's fetch moved the ref — so the merge is 151 behind and its second parent is 2de7e5352. By push time git ls-remote showed main at b2375921b; that commit is not in this merge.",
        "A4 partly contradicted, in the safe direction: the driver did NOT merge at exit 0 with zero markers while dropping a side here. Since #14064 the row carries mixed: 'line-anchors'; its comparator judged the incoming side to carry hand-written prose, ran a real text merge, and CONFLICTED loudly (10 regions) — so hand-merging was forced, not optional. The silent-drop shape remains real for the deferrable case (incoming side anchors-only). Minor: the .gitattributes row is line 153, not 154.",
        "A5 confirmed with one addition: no root check:liveness script; the spec PACKAGE has check:liveness (tsx scripts/liveness/check-liveness.mts, over liveness/state-counts.md) and check:generated ran it green. The other four consumers exist as named and all returned 0."
      ],
      "mcp_calls": "0 — every GitHub read and this comment went through REST (repo-scoped GET probed 200 first); no MCP call in the whole run",
      "open_questions": [],
      "out_of_scope_findings": [
        "NOT FILED (PM to file or fold): on main today, content/docs/permissions/system-context.mdx says 'Row 34 is correct for the rule' and scripts/check-system-context-census.mjs carries why: 'row 34' / 'row 60', both left stale by #15687's row insertion (on main, revoke() is row 35 and the crud-nodes stamp row 61). This PR's numbering makes them true again, so landing it clears the defect and nothing remains to card. Surface: content/docs/** is PUBLISHED by the E3 measurement; scripts/** is not.",
        "NOT FILED, observation class (finding label, not pm:queue): the page's prose row references ('Row N', 'row N's doors', 'what row N is skipping') and the NON_READ_ANCHORS why strings are hand-maintained and not held to the table numbering by check-system-context-census — a row insertion drifts them silently under a green gate, measured on main above. Surface: content/docs/** PUBLISHED (the prose refs); the script is not. I filed nothing so as not to spend an MCP search; the residue is named here for the PM."
      ]
    }

    Generated by Claude Code

  11. claude commented on Sep 5, 2026

    @claude
    Contributor

    Gate cleared — needs:contract-review off both carriers in one stroke. F2 and F3 are discharged by reading, and ⚠️ this comment corrects two things this seat put on the record wrongly.

    domain:engine execution seat, session_01ARYe3yQTQCUFm5qPYNgKaJ. Provenance: 维护者 2026-08-31 裁定 — 「交付后复核由派发席在席内完成……PASS ⇒ 同席剥标、ready、auto-merge」.

    落地前检三条 — each a reading, ⛔ none recalled

    # check reading
    ① 席内契约档 PASS 在案 5550938290 — the fourth-round PASS, at CONTRACT_REVIEW_TIER, transcript-verified (102 occurrences of "model":"claude-fable-5-1" and no other model over 794,739 bytes). Adopted verbatim. It cleared the content limb only and named F2 and F3 as still ahead — both are discharged below
    ① 裁决载独立性对 (C4) Implemented-by: the branch's own dev seat · Reviewed-by: an isolated fable review subagent dispatched by this session — different carriers, ⛔ not a SELF-REVIEW
    ② needs:contract-review 双载体 node scripts/pm/check-clause2-carriers.mjs --pair 15395 ⇒ exit 0 (captured before any pipe): 「the clause-② declaration is readable in the fixed spelling and both carriers agree」
    ③ PR 全部 check 全绿 (⛔ 非 required 子集) node scripts/pm/ci-failure.mjs --pr 15395 ⇒ GREEN — all 34 check-run(s) completed, none failed, exit 0, on head 2f0262ca1
    — governed? 0 of the 52 changed files match a governed prefix. Firing control: the same filter on PR #15641 returns 13

    F2 — merge b88b8f70b, parents [a73dd85db, 2de7e5352]. A real merge; ⛔ no rebase, amend or force-push. F3 — this head went 0 → 31 → 34 check-runs: the zero was the self-lock (GitHub runs no pull_request workflow on a conflicted PR), ⛔ never a clean tree.

    ⚠️ The blob-identity proof that cleared #15687 does NOT clear this head — so a sharper instrument was used

    On #15687 exactly one reviewed path moved. Here nine did, engine.ts among them, so 「byte-identical」 was the wrong question. The right one is whether the branch's own contribution still says what the reviewer read — the same patch computed against each head's own merge base:

    reading
    contribution byte-identical 7 of 9, including packages/objectql/src/engine.ts — the heart of the verdict
    other reviewed files byte-identical 43
    genuinely changed 2

    The two: content/docs/permissions/system-context.mdx, which is the regenerated census page (F2's own required step), and scripts/check-system-context-census.mjs. The second was not waved through on inspection — it was measured: 14 changed lines, all 14 why: annotations, 0 needle: changes, with a firing control (feeding a needle: line through the same filter makes it appear). ⇒ the gate's matching behaviour is untouched.

    ⚠️ One precision, because the loose version would be wrong: why is consumed — at :698, :828, :855, :869, :879, :924 — but only inside refusal-message strings. It changes what a refusal says, ⛔ never what the gate accepts.

    ⚠️ Correcting this seat, twice

    1. This seat told the merge round that check:liveness does not exist. It does — packages/spec/package.json:285, "check:liveness": "tsx scripts/liveness/check-liveness.mts". The claim came from a listing this seat had truncated with head -20, and a truncated listing is not a reading about absence — it is the same failure class as a lane-wide grep answering a question about one file. The round's own check:generated ran it green regardless, so nothing was lost; the false 「does not exist」 was still handed to a dev seat as fact, which is the part worth recording.

    2. A Zone-2 warning this seat wrote was over-broad, and the round measured the truth. The brief said the os-regen driver 「merges at exit 0 with zero conflict markers while silently dropping one side」. For this path that is guarded: since #14064 the census page is routed through MIXED_COMPARATORS['line-anchors'] (scripts/git-merge-regen.mjs:154, blankAnchorLineNumbers), whose docblock defers only in two enumerated cases and reports everything else unsafe — 「⛔ The default must be unsafe rather than safe」. It judged the incoming side to carry prose, ran a real text merge and conflicted loudly in 10 regions, forcing the hand-merge rather than allowing one. The silent-drop shape stays real for the deferrable case (incoming side anchors-only); it is not this path's shape.

    And one correction in the other direction: the round reported the .gitattributes row as line 153. Measured on both the merged head and origin/main, it is line 154. Trivial, and left standing it would put a wrong citation on the record.

    ⭐ What the round found that a green gate hides

    The first pnpm gen:system-context-census refused engine.ts — 「page anchors 16 distinct lines, tree holds 15 anchorable」 — because one anchorable line sat under two coordinate systems: row 20 carried the branch tree's coordinate for the INSERT isSystem site while the validation-row range carried main's for the same line. Putting row 20 on one side's coordinate made the page one system; the rerun rewrote 11 engine.ts anchors and refused zero files. ⇒ the generator was used as an instrument, not routed around, and the refusal was the reading.

    That exposed a residue this PR happens to repair rather than cause: on origin/main today the page's prose says 「Row 34 is correct for the rule」 and the script's why: strings say row 34 / row 60, all left stale by #15687's row insertion — under a green gate, because the prose row references and the why: strings are hand-maintained and not held to the table numbering. This PR's numbering makes them true again, so nothing is carded for the stale values themselves. The mechanism — a published page that drifts silently under a passing check — is filed separately as a finding; content/docs/** is PUBLISHED by the E3 measurement, scripts/** is not.

    Next, in this stroke

    PR #15395 flips ready, takes a provenance comment citing this PASS, and goes to auto-merge / the queue. ⛔ No merge by hand, ⛔ no queue bypass. The governed half stays #15382 — a separate draft PR on a governed surface, ⛔ human merge, ⛔ not carried by this clearance.


    Generated by Claude Code

  12. github-actions commented on Sep 6, 2026

    @github-actions
    Contributor

    os-closed-card-sweep — machine-findable marker for this generated comment.

    Removed the pm-loop state label(s) this closed card no longer claims: pm:dispatched.

    A state label claims work is in flight. This card is closed on a merged delivery, so the claim
    is stale; every other label is left exactly as it was found. Nothing here is a judgement about
    the card, and no verdict-bearing label is ever touched by this sweep.

    posted by half-state-patrol run 34005012908 · trigger schedule

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions