Skip to content

feat(objectql,metadata-protocol,rest,spec): the object read names the objects a delete can cascade into - #28451

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-qxo0yv
Oct 31, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-qxo0yv

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

Requested by Leehom · project thread

Part of #28001 (the server half of ruling B; the ui half, the delete confirmations that read this key, is its own domain:ui card, blocked by this PR).

Clause-②: yes (widening) — a new optional key served beside the object read (cascadeDeleteObjects, both item doors and the app bundle's object entries), a new optional IObjectQLEngine member and a new public ObjectQL method. Nothing is refused that ran before.
Metadata-protocol: added — the served key cascadeDeleteObjects moves the api authorable-surface shard (packages/spec/authorable-surface/api.json), which scripts/check-changeset-no-major.mjs reads as an added protocol key; @objectstack/spec is graded minor (the #27842 / #27964 shape). Nothing an author can write moves: the key is served beside item, never inside it, and has no authorable spelling.
Skills-docs: none — a served-only key; no skills/** text names cascadeDeleteObjects or disabledActions.

维护者速读

  • 改了什么:GET /api/v1/meta/object/:name(缓存默认门与非缓存门,以及 app bundle 里每个对象条目)在 item 旁边多返回一个 cascadeDeleteObjects:删除这个对象的一条记录时会被级联一起删掉的对象名列表(主从子对象默认算,lookup/tree 只有声明 deleteBehavior: 'cascade' 才算,孙辈对象也算,restrict / set_null 不算)。列表由引擎 ObjectQL.getCascadeDeleteObjects 给出,它和删除流程共用同一个「哪些关系会级联」的扫描函数。
  • 为什么改:Delete confirm: an object can word its Delete confirmation, or the confirmation names what a cascade will delete #28001 裁决 B:删除确认框要加一行说明「这还会删掉哪些子对象」,列表必须来自服务端、不能在前端复制一份引擎规则;服务端这一半先落。HotCRM 里删一条线索会悄悄删掉它的活动成员记录。
  • 风险与回滚:只加不减;删除本身行为不变(现有级联删除测试照旧绿)。只读注册表、不读任何行,所以不会泄露记录或数量;只给对象名、不给字段名(字段可能被另一对象的 ADR-0106 掩码隐藏)。缓存门把列表折进 ETag,后注册的级联对象会让 ETag 变。回滚即 revert。
  • 先看:packages/objectql/src/engine-delete-cascade.ts(cascadeRelationsInto 抽出与 collectCascadeDeleteObjects);packages/metadata-protocol/src/protocol.ts(resolveCascadeDeleteObjects 与 getMetaItemCached 的 304 退让);packages/rest/src/meta-item-read-routes.ts(ETag 折叠)。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/engine-cascade-delete-objects.test.ts → 7 passed,其中 cd_parent 答 ['cd_child', 'cd_grandchild'],restrict / set_null 兄弟不在列。
  • 需要你做的:无,席位落地(触及 packages/spec/**,席位先做合约评审)。

What changes

The shape, measured and chosen

  • Object-level, not record-level. The confirmation names child objects. ObjectQL.getCascadeDeleteObjects(object) reads the registry and no row, so it says what a delete MAY remove, never which records or how many. inspectCascadeDelete (record-level, reads as SYSTEM with no organization) is untouched and is not exposed anywhere.
  • Transitive, the walk's own closure. Breadth-first from the object through every relation whose resolved behaviour is cascade, each object named once at the first depth that reaches it and expanded once, so a cycle (a self-cascading tree, two objects cascading into each other) terminates. The object itself is named only when a cascading relation leads back to it.
  • One rule. The collection walk's scan of "which relations cascade into this object" (cascadingInto inside collectCascadeDeleteSet) is lifted out unedited as cascadeRelationsInto, and both collectCascadeDeleteSet and the new collectCascadeDeleteObjects call it. It reads cascadeRelationBehavior(target, child, fieldName, fdef) === 'cascade', the predicate the delete runs. No copy of the rule lives in rest, runtime, metadata-protocol or spec.
  • Where it is served: beside the object item read, on disabledActions' (meta: GET /meta/object serves which packaged actions are switched off beside the item, and the disabled-action refusal reads as a plain sentence #27842 / rest: a cached GET /meta/object/:name that carries disabledActions is never answered 304 — fold the switched-off names into the ETag so object reads revalidate again #28044) terms. The console's grid and record page read GET /meta/object/:name (ObjectStackAdapter.getObjectSchema) or the app bundle's object entries, so a key on that answer needs no second request; a new read route would add a route-ledger row and a second round trip. Precisely as disabledActions does:
    • the protocol reads the engine's optional IObjectQLEngine.getCascadeDeleteObjects through the kernel's service table by name (objectql), per call, and imports no engine;
    • served face only: absent on the execution face and on a draft read; absent on the list read (GET /meta/object), since the confirmations read the item;
    • absent where no engine answered (no engine, an engine without the member, an object the registry does not hold), [] where the engine answered that nothing cascades;
    • on the cached default door too: getMetaItemCached carries it beside data and stands its own 304 down for a read that carries it; the REST door folds the served list into the object read's ETag, so an unchanged object with an unchanged cascade revalidates to 304, and an object registered later that cascades into it moves the ETag. An object no engine answered for keeps its old validator byte for byte.
    • The app bundle's object entries are item answers (getMetaItem through createMetaItemAnswer), so they carry the key with no change to the bundle route.
  • Object names, not fields. The order's hypothesis was one entry per object with the field carrying the edge. Measured: a field of another object can be withheld from this caller by that object's ADR-0106 mask (D1 removes an unreadable field whole, its existence included), while an object read withholds no object (createMetaItemReadGate's object arm prunes only listViews; measured on this tree, a member with no nav reach to secret_deal is answered 200 on GET /meta/object/secret_deal). Serving field names would disclose masked fields; serving object names discloses nothing an object read does not. One entry per object also absorbs an object reached by two fields. The ui half shows object labels, which the client resolves from names; no translated text is served.
  • Typed in spec. GetMetaItemResponseSchema and GetMetaItemCachedResponseSchema gain the optional cascadeDeleteObjects: string[], documented beside disabledActions. IObjectQLEngine gains the optional getCascadeDeleteObjects?(objectName): string[] | undefined.

Files

  • packages/objectql/src/engine-delete-cascade.ts: cascadeRelationsInto (lifted from collectCascadeDeleteSet), collectCascadeDeleteObjects.
  • packages/objectql/src/engine.ts: public getCascadeDeleteObjects.
  • packages/spec/src/contracts/objectql-engine.ts, packages/spec/src/api/protocol.zod.ts: the member and the key.
  • packages/metadata-protocol/src/protocol.ts: resolveCascadeDeleteObjects, the item read's envelope, getMetaItemCached.
  • packages/rest/src/meta-item-read-routes.ts: the cached arm carries it and folds it into the ETag.
  • content/docs/references/api/protocol.mdx, app-bundle.mdx: regenerated (gen:docs).
  • Tests: packages/objectql/src/engine-cascade-delete-objects.test.ts, packages/metadata-protocol/src/protocol.object-cascade-delete-objects.test.ts, packages/rest/src/meta-object-cached-cascade-delete-objects.test.ts, a [#28001] block in packages/rest/src/meta-app-bundle-routes.test.ts and in packages/spec/src/api/meta-item-response-shapes.test.ts.
  • .changeset/28001-cascade-delete-objects-beside-item.md: minor for spec; patch for objectql, metadata-protocol, rest.

Tests

  • Red first (production sources restored to origin/main 4bc077d2, objectql and metadata-protocol dist/ rebuilt from them, tests kept): engine-cascade-delete-objects.test.ts 7/7 failed; protocol.object-cascade-delete-objects.test.ts 5 failed, 4 passed (the four controls: no key without an engine, other types, execution face, the list); meta-object-cached-cascade-delete-objects.test.ts 3 failed, 3 passed (the no-key control and the unchanged-validator 304 controls); meta-item-response-shapes.test.ts 4 failed, 28 passed. The restore leg's git diff HEAD was empty.
  • Green on head: the fixture cd_parent ← cd_child (master_detail, default cascade) ← cd_grandchild (lookup, deleteBehavior: 'cascade'), with a restrict sibling (cd_pin), a set_null sibling (cd_tag) and a default-set_null lookup on the child (cd_note): cd_parent answers ['cd_child', 'cd_grandchild'], cd_child answers ['cd_grandchild']; the self-cascading cd_folder answers ['cd_folder']; cd_loner answers []; an unknown object undefined. The real protocol over the real engine serves the same list on both doors, [] for cd_loner, nothing on the execution face, and an object registered later that cascades into cd_loner is on the next read.
  • Agreement with the delete: a real delete() of a cd_parent record empties exactly cd_child and cd_grandchild, leaves the set_null holders standing, and equals the served list.
  • Ablation (scripts/ablation-replace.mjs, src-resolved suite, restore proven blob == HEAD, git diff HEAD empty): replacing cascadeRelationsInto.call(this, target, objects) in collectCascadeDeleteObjects with a hand-written lookup-only filter (type === 'lookup' && deleteBehavior === 'cascade') reds the master_detail case (expected [] to deeply equal [ 'cd_child', 'cd_grandchild' ]) and the self-cascading tree case, while engine-cascade-delete.test.ts (the delete itself) stays green. The protocol-door rows of that run read a stale base dist/ from the red leg and are not counted.
  • Controls: the delete is unchanged (engine-cascade-delete.test.ts, engine-inspect-cascade-delete.test.ts green); an object with no cascading children answers []; no engine answer serves no key and keeps the old ETag.

Acceptance notes

  • Metadata-protocol: none with patch everywhere [Edit by the seat: the contract review (6546633648) ruled option B. The changeset gate refuses none whenever an authorable-surface shard moves, so head 7f3638c declares Metadata-protocol: added and grades @objectstack/spec minor, as the disabledActions precedent did.]
  • The list read (GET /meta/object) does not carry the key. The confirmations read the item; adding it to the list is a widening with no reader yet.
  • Order-of-reach is the walk's breadth-first order; the ui half may sort by label.

Verification

Local runs. The package suites and the first 88 derived gates ran on 81f57815; the last commit, 8bfac916, adds only the engine-double ledger pin, after which check:engine-double-contract and the 30 families it added to the derivation ran on 8bfac916:

  • pnpm --filter @objectstack/objectql run test exit 0, 9836 passed; typecheck exit 0.
  • pnpm --filter @objectstack/metadata-protocol run test exit 0, 21881 passed, 26 skipped; typecheck exit 0.
  • pnpm --filter @objectstack/rest run test exit 0, 6870 passed, 332 skipped; typecheck exit 0.
  • pnpm --filter @objectstack/spec run test exit 0, 22834 passed, 1 todo; typecheck exit 0.
  • @objectstack/runtime src/domains/meta-list-projection-parity.test.ts and src/domains/meta*: 29 files, 1425 passed.
  • pnpm --filter @objectstack/spec check:generated: check:docs stale before gen:docs, regenerated and committed; the other 13 artifacts up to date.
  • Derived gates (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack): reconciled with --ran, 118 derived famil(ies) accounted for — 117 run, 1 NOT-MEASURED. Every run family exit 0, after two fixes in this PR: check:engine-double-contract first answered exit 1 (RETAINED [findOne] for the new metadata-protocol double: new coverage the ledger must learn), fixed by node scripts/check-engine-double-contract.mjs --write (scripts/engine-double-contract.pinned.json, +1 row) and re-run exit 0; check-plugin-teardown-shape.mjs --self-test first answered exit 3 on the shallow clone and exit 0 after fetching its positive-control commit.
  • NOT MEASURED: pnpm check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET: it needs every package's dist/, this worktree built only the touched closures). CI runs it.
  • pnpm exec eslint --no-inline-config packages/objectql packages/metadata-protocol packages/rest packages/spec: exit 0.
  • Dogfood battery: not run (no ! in the title, no (narrowing) arm). test:repo projects: declared to CI (Test Core).

Generated by Claude Code

…e objects a delete can cascade into

The delete walk's own relation scan (cascadeRelationsInto) now also feeds an
object-level closure, ObjectQL.getCascadeDeleteObjects, which the object item
read serves beside the item as cascadeDeleteObjects (both doors; folded into
the cached REST door's ETag).

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pEcJ17rvFVfE3NXzN
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 31, 2026
@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/rest, @objectstack/spec, touching 14 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/kernel/contracts/data-engine.mdx (via IObjectQLEngine (symbol, a top-level interface))
  • content/docs/kernel/services-checklist.mdx (via getMetaItemCached (symbol, a method of class ObjectStackProtocolImplementation))

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

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-5.mdx (via IObjectQLEngine (symbol, a top-level interface))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 79 pages)
  • the SDK route bridge reached 60 of 212 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 173 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 776c1d875b2d140b4c2419103a7ad8681c1d2610 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2efa81586180b3ac7b7dcae7dcb3ab45e5367454 — the merge of head 7f3638c5bcacab92670113fbc367a58c1e211eab into base 776c1d875b2d140b4c2419103a7ad8681c1d2610, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2efa81586180b3ac7b7dcae7dcb3ab45e5367454 && git checkout 2efa81586180b3ac7b7dcae7dcb3ab45e5367454
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 776c1d875b2d140b4c2419103a7ad8681c1d2610 7f3638c5bcacab92670113fbc367a58c1e211eab && git checkout -B drift-repro 776c1d875b2d140b4c2419103a7ad8681c1d2610 && git merge --no-ff 7f3638c5bcacab92670113fbc367a58c1e211eab

node scripts/docs-audit/affected-docs.mjs --json 776c1d875b2d140b4c2419103a7ad8681c1d2610

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 776c1d875b2d140b4c2419103a7ad8681c1d2610 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8bfac916b83df754c3e712655a85048ae8d980f6
Local-runs: none
Scope: packages/spec/src/api/protocol.zod.ts, packages/spec/src/contracts/objectql-engine.ts + packages/spec/authorable-surface/api.json, packages/objectql/src/engine-delete-cascade.ts, packages/objectql/src/engine.ts, packages/metadata-protocol/src/protocol.ts, packages/rest/src/meta-item-read-routes.ts, packages/rest/src/meta-item-read-gate.ts (on main, the object arm), .changeset/28001-cascade-delete-objects-beside-item.md, scripts/check-changeset-no-major.mjs (the gate the changeset is judged by), content/docs/references/api/protocol.mdx, content/docs/references/api/app-bundle.mdx, scripts/engine-double-contract.pinned.json

Face: published schema (packages/spec/src/**, non-test). Nothing narrows, so no narrowing face is added. Card #28001, ruling record 6545218189 (B), seat claim 6545641008, dev report 6546528911. PR author is objectstack-fleet[bot], so no human login is named under ③.

① Derived judgments

Each acceptance-set and public-surface change the diff makes, judged on the committed tree against origin/main (merge base 4bc077d2):

  1. GetMetaItemResponseSchema.cascadeDeleteObjects: string[] optional, no default (protocol.zod.ts). CORRECT. Widening only: an absent key still parses, [] survives as [], a non-string entry is refused at ['cascadeDeleteObjects', 0]; absence is never defaulted to [], so "no engine answered" and "nothing cascades" stay distinct, as the describe text promises. Pinned in meta-item-response-shapes.test.ts, type-level and runtime.
  2. GetMetaItemCachedResponseSchema.cascadeDeleteObjects: string[] optional (protocol.zod.ts). CORRECT. Same shape; its describe text says "never part of etag" and that the method never answers notModified for a read that carries it, which is what getMetaItemCached does on this head (the ifNoneMatch stand-down now also requires cascadeDeleteObjects === undefined).
  3. IObjectQLEngine.getCascadeDeleteObjects?(objectName: string): string[] | undefined (contracts/objectql-engine.ts). CORRECT as an optional member on the getFlowRegistration? / getDisabledActions? shape: an engine that lacks it still satisfies the contract, and the only caller (resolveCascadeDeleteObjects) type-checks typeof read !== 'function' before calling. The docblock's "three members are optional by ruling" sentence is updated to match.
  4. packages/spec/authorable-surface/api.json gains api/GetMetaItemCachedResponse:cascadeDeleteObjects and api/GetMetaItemResponse:cascadeDeleteObjects. CORRECT as the mechanical companion of 1 and 2 (the shard already lists disabledActions the same way). But see ②: this file moving is what the changeset gate reads as a metadata-protocol change, and it decides the level.
  5. Public ObjectQL.getCascadeDeleteObjects(object) (engine.ts). CORRECT. undefined when _registry.getObject(object) is falsy, otherwise collectCascadeDeleteObjects over resolveObjectName(object) and getAllObjects(). The delete walk's cascadeRelationBehavior matches the target "by raw or resolved name", so the resolved form here and the form delete() passes reach the same relations.
  6. The served key on GET /meta/object/:name, both doors, and on the app bundle's object entries. CORRECT and in scope of the ruling ("through object metadata"). On the non-cached door it is produced in getMetaItem under the same condition as disabledActions (!execution && decorated present), so it is absent on the execution face and on a draft read, and absent on every type other than object (resolveCascadeDeleteObjects returns undefined before touching the service table). The list read (GET /meta/object) deliberately carries none; declared in the PR body and the changeset, pinned by a test.
  7. No change to the acceptance set of any authorable document: item is served as stored, the key sits beside it, and no schema under packages/spec/src/data/** moves. The object save body is parsed by the .strict() ObjectSchema, so a cascadeDeleteObjects an author writes inside an object definition is refused as an unrecognised key, and the envelope key has no write address at all (the PUT body is the item). This is the same posture disabledActions has; no new refusal is needed and none is added.

② Semver level

Declared: Clause-②: yes (widening) on the PR body and the changeset. CORRECT: a new optional key on two response schemas, a new optional contract member and a new public engine method; nothing that ran before is refused.

Declared: Metadata-protocol: none, with patch on @objectstack/spec, @objectstack/objectql, @objectstack/metadata-protocol, @objectstack/rest in one changeset. WRONG on this repo's rules, and the head is already red on it. This answers the dev's open question (option B, not A):

Ruling the seat can adopt: Metadata-protocol: added, @objectstack/spec at minor; objectql, metadata-protocol, rest stay patch (either as one changeset with mixed levels or split as #27842 did; both read discharged). The lockstep fixed group versions every package at the highest bump anyway, so the choice carries no release cost.

MUST-FIX (② declaration): change the Metadata-protocol: line to added in the PR body and in .changeset/28001-cascade-delete-objects-beside-item.md, and grade '@objectstack/spec': minor there. A kind other than none also owes the PR body the Skills-docs: line the pm-dispatch declaration row names; Skills-docs: none — a served-only key, nothing an author writes or a skill teaches is the honest value here. Re-read Check Changeset green on the new head before ACCEPT. This is a declaration and changeset edit only; no source moves, so the resulting head is a delta re-review, not a new full round.

③ Boundary flags

  • Dev flag open_questions[0] (changeset level, status: for_contract_review): answered above, option B. Must-fix recorded.
  • Order ruling "one rule": HOLDS. cascadeRelationsInto is the former inline cascadingInto body lifted out byte-for-byte in logic (same loop, same cascadeRelationBehavior(target, child, fieldName, fdef) === 'cascade' test, same result shape); collectCascadeDeleteSet now calls it through the same per-target memo it had, so the delete's behaviour is unchanged. The grep of protocol.zod.ts, protocol.ts and meta-item-read-routes.ts on the head finds cascade / deleteBehavior / master_detail only in prose and describe text. No second copy.
  • Order ruling "object-level, never inspectCascadeDelete or row counts at a user door": HOLDS. getCascadeDeleteObjects reads getAllObjects() and no row; inspectCascadeDelete is untouched and is not reachable from any door on this diff.
  • Order ruling "no authorable key" and "no ui/**": HOLDS. No ui/** path in the diff; no authorable schema moves (① item 7).
  • Dev deviation "object names, not fields": ACCEPTED as within the order's "pick one shape" latitude. The reason is real on main: createMetaItemReadGate's object arm serves the whole document to any caller with a resolved context and prunes only listViews, while a field of another object can be removed whole by that object's ADR-0106 mask; a field name in the list could therefore disclose a masked field, an object name discloses nothing an object read does not.
  • Dev deviation "two single-file vitest runs outside os-verify-lock": process note, no contract effect.
  • Out-of-scope note "the list read carries none": agreed, and the acceptance set says so. The ui card the seat files at landing is the consumer.
  • Dev's "NOT MEASURED: check:dual-build-cjs-loads": CI runs it; nothing in this diff touches a build entry.

④ Design judgment

protocol.zod.ts against ruling B and its first consumer: the key is what the ruling asked for, a server-served list the delete confirmation names objects from, with no engine rule copied client-side. Its position beside item is the right one: it is derived from other objects' definitions, so it is no part of this object's document and can never be saved back. The prose on both schemas is precise about the three states (absent, [], names) and matches the code on every count I read: production in getMetaItem, the cached door's 304 stand-down, the REST door's ETag fold.

contracts/objectql-engine.ts against the engine: the member is optional on the established shape and its contract text (transitive, each object once, breadth-first first-reach order, self only on a cycle back, restrict/set_null not named, undefined for an unknown object) is what collectCascadeDeleteObjects does. Cycles: expanded is seeded with the root and each child is expanded once, so a self-cascading tree names itself once and terminates (pinned, cd_folder), and a two-object cycle terminates by the same set. Transitivity: it is the object-level closure of collectCascadeDeleteSet, which expands only objects whose rows were actually reached; the served list is therefore the superset "what a delete MAY remove", which is the right semantics for a confirmation. The agreement test (a real delete() of cd_parent empties exactly the named objects) pins the two walks to each other.

Visibility (order: "follow the rule the metadata answer already applies"): the served list is caller-independent, and the object read on main withholds no object by permission, so the list cannot name an object the caller could not read by name. Not a leak and not a must-fix. One reading for the ui half: the list is the process registry's closure, the same getAllObjects() the delete itself walks, so any scoping question is the delete's own, not one this key introduces.

ETag and cache: the REST door folds cascadeDeleteObjectsFingerprint as the outermost fold after disabledActionsFingerprint, through cachedDisabledActionsFingerprint ('' for absent, a hash of the JSON list otherwise); foldVisibilityFingerprintIntoEtag(etag, '') is the identity, so an object no engine answered for keeps its validator byte-for-byte (pinned). The fold order of the five existing dimensions is unchanged; the restructured nesting is the same sequence with one more fold. The list is the same for every caller, so folding it into a shared validator discloses nothing per-caller. The protocol-level getMetaItemCached stands its own 304 down when the key is present, exactly as for disabledActions, and the REST door's own comparison decides. Correct.

content/docs/references/** (AGENTS.md: never hand-edited): the PR's diff there is only the seven new table rows (five in app-bundle.mdx, two in protocol.mdx), zero other lines; each row's text equals, character for character, the corresponding .describe() string on the head (the item describe for the five bundle rows and one protocol row, the cached describe for the other), and the row counts match disabledActions' footprint on main minus the list-read row this PR does not add. That is the generator's output for this spec change, not a hand edit; check:generated in CI is the mechanical confirmation.

scripts/engine-double-contract.pinned.json: one row, the new metadata-protocol test's findOne double, beside the identical row for protocol.object-disabled-actions.test.ts. Expected mechanical companion.

Implemented-by: claude/project-thread-qxo0yv
Reviewed-by: session_0186vfjHHMsRXC9EUT9hTFMo

VERDICT: FAIL

One must-fix, in ②: the Metadata-protocol: none + patch declaration is refused by the changeset gate because packages/spec/authorable-surface/api.json moves; declare added, grade @objectstack/spec minor (others stay patch, the #27842 shape), add the Skills-docs: line, and the record on the new head is a delta review. Every other judgment ①–④ passes on this head.


Generated by Claude Code

…ne (contract review 6546633648)

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pEcJ17rvFVfE3NXzN
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7f3638c5bcacab92670113fbc367a58c1e211eab
Local-runs: probe — the two changeset gates (scripts/check-changeset-no-major.mjs, scripts/check-empty-changeset.mjs) were run on a throwaway worktree of this head against origin/main, the level axis driven by the live PR body through --event, because the only must-fix was a declaration the gate reads and the head's Check Changeset run was still in progress when this record was drafted; nothing else was built or run
Scope: packages/spec/src/api/protocol.zod.ts, packages/spec/src/contracts/objectql-engine.ts + .changeset/28001-cascade-delete-objects-beside-item.md, the live PR body, scripts/check-changeset-no-major.mjs (the gate the changeset is judged by)

Delta review. The full review of record on this PR is comment 6546633648 on head 8bfac916b83df754c3e712655a85048ae8d980f6 (same reviewer, same face, FAIL with one must-fix in ②). The new head is one commit on top of it, 7f3638c5, "chore(changeset): spec minor, Metadata-protocol added, Skills-docs none (contract review 6546633648)". git diff 8bfac916 7f3638c5 touches exactly one file, .changeset/28001-cascade-delete-objects-beside-item.md (3 insertions, 2 deletions); no path under packages/**, content/** or scripts/** moves between the two heads, and the file list against the merge base (4bc077d2) is the same sixteen paths the full review judged. The seat also edited the PR body (a body edit does not move the head): the Metadata-protocol: line, the changeset bullet under "Files" and the first acceptance note. This record re-judges ② and the parts of ③ the must-fix touched, and inherits ①, ③ (the rest) and ④ from 6546633648 because the tree they were judged on is byte-for-byte the tree on this head outside .changeset/**. Card #28001, ruling record 6545218189 (B), seat claim 6545641008, dev report 6546528911. PR author is objectstack-fleet[bot], so no human login is named under ③.

① Derived judgments

Inherited from 6546633648, items 1 to 7, unchanged: the published schema (protocol.zod.ts, contracts/objectql-engine.ts), the api.json shard, the engine method, the served key and the authorable acceptance set are identical on both heads (git diff 8bfac916 7f3638c5 -- packages content scripts is empty). Nothing to re-judge.

② Semver level

Re-judged on this head. The must-fix asked for three things; all three are present, in both carriers:

  1. Metadata-protocol: added. The changeset line reads Metadata-protocol: added (was none). The live PR body reads Metadata-protocol: added — the served key cascadeDeleteObjects moves the api authorable-surface shard …, which scripts/check-changeset-no-major.mjs reads as an added protocol key; @objectstack/spec is graded minor (the #27842 / #27964 shape). Nothing an author can write moves …. CORRECT, and the reason it gives is the gate's own reason (a moved packages/spec/authorable-surface/ shard is a key added that the ratchet can see), not a claim that the key is authorable; the body says plainly that it is not.
  2. '@objectstack/spec': minor in the changeset frontmatter (was patch); @objectstack/objectql, @objectstack/metadata-protocol, @objectstack/rest stay patch. CORRECT: one changeset with mixed levels is one of the two shapes the full review named as discharged, and the lockstep fixed group versions every package at the highest bump anyway.
  3. Skills-docs: line. Present on the PR body (Skills-docs: none — a served-only key; no skills/** text names cascadeDeleteObjects or disabledActions) and, more fully, on the changeset (Skills-docs: none — cascadeDeleteObjects is a served-only key beside the object read with no authorable spelling; no file under skills/** teaches the object read's envelope keys …, and every taught deleteBehavior use stays true). CORRECT: a kind other than none owes the line, and none is the honest value for a key with no authorable spelling and no skill that teaches the envelope. I confirmed the grep claim on this head: git grep -n -e cascadeDeleteObjects -e disabledActions -- skills/ names nothing.

Clause-②: yes (widening) is unchanged on both carriers and still correct (nothing that ran before is refused).

Gate runs, on a temporary worktree of 7f3638c5 with --base origin/main (merge base 4bc077d2):

  • node scripts/check-changeset-no-major.mjs --base origin/main: exit 0, "no major bump"; the level axis reports NOT APPLICABLE without a pull_request payload, as its header says a local run must.
  • The same with --event pointing at a payload built from the live PR (gh api …/pulls/28451 wrapped as {pull_request: …}, the shape the script documents as "the WHOLE declaration input"): exit 0, LEVEL AXIS: this PR declares clause-② yes (widening) and metadata-protocol added, and it grades a package whose published source it moves at minor or above — the declared widening is accounted for: @objectstack/spec: minor; objectql, metadata-protocol and rest listed as patch and "NOT refused". This is the discharged reading the full review asked for. The script's named residual (a second widening graded patch beside the minor would not be seen by the axis) is answered by ① of the full review: the only widening in this diff is the one key, the one member and the one method, all in @objectstack/spec's source or its consumers.
  • node scripts/check-empty-changeset.mjs --base origin/main: exit 0, one declaring changeset added, no changeset from the merge base modified or deleted.
  • Check Changeset check-run on 7f3638c5 (job 124604719336, run 41224167803, re-triggered by the body edit): completed / success, read from the commit's check-runs API after it finished; its only annotation is the runner-image migration notice. The red the full review recorded on 8bfac916 (none-baseline-moved) is cleared on this head.

The must-fix in ② is discharged.

③ Boundary flags

  • Dev flag open_questions[0] (changeset level, status: for_contract_review): answered in 6546633648 as option B and now acted on; the delta commit's message cites the record. CLOSED.
  • The acceptance note the full review read as the dev's option-A reading (Metadata-protocol: none with patch everywhere) is struck through on the live body with the seat's edit naming record 6546633648 and head 7f3638c5; the "Files" bullet for the changeset now reads minor for spec, patch for the other three. The body is consistent with the changeset and with the tree.
  • Every other flag and deviation judged in 6546633648 (one rule, object-level, no authorable key, no ui/**, object names not fields, the two single-file vitest runs, the list read carrying none, check:dual-build-cjs-loads NOT MEASURED) is inherited unchanged: none of the code those flags are about moved.

④ Design judgment

Inherited from 6546633648 in full: protocol.zod.ts against ruling B and its first consumer, contracts/objectql-engine.ts against the engine, the visibility reading, the ETag fold, the regenerated content/docs/references/** rows and the engine-double ledger row are all judged on a tree this head reproduces byte-for-byte outside .changeset/**. The delta commit makes no design claim; a changeset level is not a design surface.

Implemented-by: claude/project-thread-qxo0yv
Reviewed-by: session_0186vfjHHMsRXC9EUT9hTFMo

VERDICT: PASS

The one must-fix of 6546633648 is discharged on this head: Metadata-protocol: added, @objectstack/spec at minor with the other three at patch, and a Skills-docs: none line, on both the changeset and the PR body; both changeset gates answer exit 0 locally with the live declaration, and Check Changeset on this head is as recorded above. ①, ③ and ④ are inherited because nothing outside .changeset/** moved. No new finding; the seat may ACCEPT on this head once the PR's checks are all green.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

席位核对

ACCEPT · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-10-31T20:39Z · head 7f3638c5 · #28001

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

  1. Shape:
    • A draft against main with no assignees. It says Part of #28001, because the ui half (the delete confirms that read this key) is its own domain:ui card, which the seat files at landing.
    • check-governed-merges --pr 28451 reads NOT governed, 974 changed lines. Landing tier S.
    • The claim (6545641008) names this PR's branch, claude/project-thread-qxo0yv.
  2. Against the order:
    • One rule. The delete walk's relation scan is lifted out unedited as cascadeRelationsInto, and both the delete and the new ObjectQL.getCascadeDeleteObjects call it. The delete's behaviour is unchanged.
    • Object-level only. The served list reads no row. inspectCascadeDelete is untouched and is reachable from no door.
    • Served as cascadeDeleteObjects beside the object item read, on disabledActions' terms: both doors, folded into the cached door's ETag, and carried on the app bundle's object entries. It is derived, with no authorable spelling.
    • Object names, not fields. This is a measured departure the review accepts: a field name could disclose a field another object's ADR-0106 mask removes.
    • No visibility leak. The object read on main withholds no object by permission, so the list cannot name an object the caller could not read.
    • The reference rows in content/docs/references/** are generator output, matching the .describe() text character for character.
  3. Contract review: owed at CONTRACT_REVIEW_TIER (published schema: packages/spec touched, Clause-②: yes (widening)).
    • The full review was FAIL 6546633648 on 8bfac916, with one must-fix: the Metadata-protocol: none + patch declaration.
    • Rework head 7f3638c5 declares Metadata-protocol: added, grades @objectstack/spec minor, and adds Skills-docs: none. The seat patched the body's declaration line to match.
    • Delta PASS on this head: 6546719471.
  4. Gates:
    • Locally, the dev accounted for 118 derived families: 117 exit 0, plus 1 NOT MEASURED (check:dual-build-cjs-loads, which CI runs). Red-first and ablation were recorded, and scoped lint exits 0.
    • CI on head 7f3638c5: 42 success, 11 skipped. The one red row is the Check Changeset run superseded by its re-run after the body edit (124604719336, success). The state is clean.
  5. Overlap: open drafts feat(spec,lint): a form's thank-you panel names the record's engine-generated fields, and the public submit answer has a declared shape #28440 and feat(spec): one override declaration for code and Studio, with the .custom pairing helpers and the closed overridable-type list (ADR-0146) #28467 also touch packages/spec/authorable-surface/api.json (and feat(spec,lint): a form's thank-you panel names the record's engine-generated fields, and the public submit answer has a declared shape #28440 protocol.zod.ts). git merge-tree reports no conflict with either, or with main 52306d40.
  6. Landing: pr_ready + automerge_enable through the relay. After it merges, the landing record goes on the card, the ui card is filed, and the claim is released.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 31, 2026 20:40
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 31, 2026 20:40
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit 6e0b16a Oct 31, 2026
54 of 55 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-qxo0yv branch October 31, 2026 21:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant