Skip to content

spec: declare confirmedBlueprintIdentity on the protocol's ToolExecutionContext - #18676

Merged
os-litant merged 2 commits into
mainfrom
claude/issue-15937-confirmed-blueprint-identity-protocol
Sep 17, 2026
Merged

os-litant merged 2 commits into
mainfrom
claude/issue-15937-confirmed-blueprint-identity-protocol

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes #15937

ToolExecutionContext in packages/spec/src/contracts/ai-service.ts is the contract a tool handler may rely on. A published handler already authorizes on confirmedBlueprintIdentity — cloud's apply_blueprint gate makes a matching blueprint-identity digest one clause of the decision to build a whole app (cloud#1954 / cloud PR #2005) — while the member it reads was declared only on cloud's own augmented ToolExecutionContext and reached by a structural cast. This declares it where the contract lives.

Clause-②: yes (widening) — one new OPTIONAL member on a published interface, so the shape a consumer writes against grows. Nothing previously admitted is refused, no member is renamed or retired, and no producer is required to write it. Contract-review tier. The needs:contract-review carrier is the seat's on both sides and this PR touches neither.

The ruling this implements

The maintainer's ruling is recorded on #15937 at comment 5553002827 (2026-09-05), on option 1, verbatim 「同意」. A triage stroke the next morning re-hung needs-user-decision on an already-decided card, and the director seat reversed it at 5559258277 with "Nothing to re-decide". So the implementer's brief is the ruling comment, not the card body's two-option analysis.

Ruling items 1 and 2 are implemented here. Item 3 is a read-and-report and is answered below. Item 4 — delete cloud's augmentation at tool-registry.ts:45, replace the cast in confirm-gate.ts with the typed read — is a cloud follow-up card blocked on this field being published and pinned. It is not in this diff, and this seat neither touched cloud nor filed that card.

What landed

ToolExecutionContext.confirmedBlueprintIdentity?: string, placed next to userMessageText because that is the same class of field: route-owner-populated, advisory-to-nobody, never client-derived. Its docblock carries the four things item 1 asks for.

confirmed-blueprint-identity-contract.pin.test.ts — a new pin, following the file's existing action-confirmation-contract.pin.test.ts convention. ToolExecutionContext had no test naming it anywhere in this repository before this PR (git grep -l ToolExecutionContext over the tree lit 8 files: two CHANGELOGs, two generated artefacts, one consumer, the contract itself — and zero test files). Six legs, each negative one paired with a lit positive one on the same helper:

leg what it pins what makes it able to fail
declared on ToolExecutionContext the member, and the digest round-trips removing the member makes the literal an excess property
reaches a handler it travels on ChatWithToolsOptions.toolExecutionContext declaring it on the wrong type compiles nowhere
optional, absent reads undefined absence is the resting state of a non-replay turn the lit control reads the same property on a context that carries it
reads as string | undefined a handler cannot compile a path that assumes a confirmation making the member required leaves the directive unused — TS2578
typed string, not any the member did not arrive untyped typing it any/unknown leaves the directive unused — TS2578
the spelling is load-bearing a near-miss is not the confirmation field renaming the real member reddens the first leg instead

Changeset: @objectstack/spec minor, per ruling item 2.

Item 3 — systemInvocation: read, reported, NOT folded in

Measured on this base, repo-wide with same-scope controls:

target files reading
systemInvocation 0 the protocol does not declare it either
lit control userMessageText 1 the grep reaches, and finds the analogue field
lit control isSystem 853 the wide control
dark control 0 —

Half of the class holds and half is not measurable from here. The class this card names has two halves: (a) absent from the protocol, and (b) load-bearing for an authorization decision a published handler makes through a structural cast. (a) is confirmed above. (b) lives in objectstack-ai/cloud, which is outside this card's scope and which this seat did not open — so it is reported as unmeasured rather than assumed.

What the protocol side does say is an asymmetry worth handing to whoever picks this up: confirmedBlueprintIdentity had no protocol counterpart at all, whereas systemInvocation sits beside isSystem, which this same interface declares and whose docblock calls it "the ONLY way to obtain system behaviour from the tool loop". So the first question for systemInvocation is not "declare it" but "does it duplicate isSystem" — and only a "no" makes it this card's case. That is a different first question, so it is not plainly the same class of omission, it needs its own card, and this PR does not widen into it.

Premise re-check on this base

The dispatch flagged three premises. All three were re-measured rather than inherited.

  1. The gap is still open. On base 30be2ac0bb: confirmedBlueprintIdentity across packages/ = 0 files (grep exit 1); lit control userMessageText = 1 file; lit control ToolExecutionContext = 8 files; dark control = 0. The one-file control proves the grep reaches packages/ and finds the analogue field and nothing more — the 8-file control is the wide one.
  2. The file moved after the ruling, the interface did not. scripts/pm/git-history.mjs touch answers fe0d9a4241ab782628e5a7bc4ac61baeb505fbd1, 2026-09-07T10:00:37Z, +134 lines — two days after the 2026-09-05 ruling, and proved without fetching, the touch and its parent sitting above the shallow floor. Reading that commit's own diff: not one added or removed line names a ToolExecutionContext member. The +134 is the action-confirmation contract (spec: define the confirmation contract behind action.ai.requiresConfirmation #16531) elsewhere in the file. So the interface and userMessageText's docblock were re-read on this base and the docblock matched in shape is the one on the tree; the ruling's quoted field list still describes it, its "UI-context block" being currentObjectName / currentViewName / surfaceContext.
  3. The card's comparison holds. cloud PR chore(showcase): seed Field Zoo with all field types + guard budget hook #2005's body placed confirmedBlueprintIdentity "alongside userMessageText and systemInvocation". Of those three, userMessageText's single repo-wide hit is this spec file — the protocol, not an augmentation — while systemInvocation has zero. The three were never alike in where they are declared, which is the whole reason this card exists.

Verification

Readings are cited at the commit they were taken on. pnpm lint (the repo-wide eslint . --no-inline-config) is CI's run; what is below is a declared narrowing with its evidence.

  • Build — pnpm --filter @objectstack/spec build, exit 0. packages/spec/dist/contracts/index.d.ts carries the member, so the readings below are against a rebuilt tree, not a cached one. git status clean after the build: no generator moved a tracked artefact, and api-surface/contracts.json is unmoved because it records exported symbol names, not interface members.
  • Type-check — pnpm --filter @objectstack/spec typecheck, exit 0 (that is tsc --noEmit, check:scripts-typecheck and check:test-typecheck in series). The test-layer ratchet holds at 54 files / 259 errors / 144 pinned signatures, unchanged.
  • Tests — pnpm --filter @objectstack/spec test, exit 0: 486 files, 13856 tests passed. The new pin, run on its own, is 6 passed.
  • Ablation, direction predicted before the run: RED. From the committed state, the declared member was renamed on disk (delete-anchor 1 to 0, inject-anchor 0 to 1, blob hash moved bf79a0cc to a646979419), and check:test-typecheck went from exit 0 to exit 1, naming 10 type errors in the new pin file. The restore leg put HEAD's bytes back — git hash-object back to bf79a0cc3f6fb0505e0edf6c5665c3f004e371f4, git diff HEAD empty, git status --porcelain empty — and the same gate returned to exit 0. Both legs ran under a trap ... EXIT INT TERM restore with absolute paths. No dist preflight: the pin imports ./ai-service relatively, so it resolves to src, and a dist reading would be about the wrong artefact.
  • Lint, narrowed and declared, at 5ddd6f45ee. ① The population comes from eslint's own config: the base block is files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'], so the changeset .md is outside it entirely — eslint says so itself, "File ignored because no matching configuration was supplied". ② --format json counts 3 entries, of which 2 are in-population: 0 errors, 0 warnings on both .ts files. ③ Invariance, quoted from eslint.config.mjs: "this repo runs one eslint.config.mjs, which never enables type-aware linting (no parserOptions.project, no typed @typescript-eslint rules) for ANY file, test or not." With no type-aware rule, nothing in this diff can move the verdict on a file the diff does not touch; the config's file rosters are likewise untouched.
  • Control bytes — check:nul-bytes exit 0 over 8798 files, plus a direct grep -naP sweep of the three changed files, which found none.

The full re-derived gate sweep, every exit code, and the reconciliation against dispatch-gates.mjs --ran are in the os-dev-report comment on #15937.

Acceptance notes

Noted while in the file, not filed and not fixed here:

  • userMessageText's docblock, the line directly above the new member, reads "(cloud, post-ADR-0025)" — a bare ADR number for what is, in context, cloud's record. AGENTS.md Prime Directive 13 is explicit: an ADR lives in the repository whose code it governs, cloud decisions are cited "as cloud ADR-NNNN — ⛔ never as a bare number, which scripts/check-adr-anchors.mjs resolves against this registry (the two number independently)". This repository's own ADR-0025 is docs/adr/0025-plugin-package-distribution.md, and every other bare ADR-0025 in the tree means that one, so a reader following this citation lands on a real page about plugin packaging. The new member spells it cloud ADR-0025, which is why the two lines differ. Not folded in: it is a different defect class from this card's, so the bounded in-place exemption does not open, and check:adr-anchors does not read source docblocks at all (it checks only the entries under scripts/adr-anchors/), so nothing is currently red. Reported with dedupe words in the report comment for the triage seat to file or discard.
  • ToolExecutionContext carrying no test at all before this PR is now half-closed: the new pin asserts the new member and the thread it travels on, deliberately not the other twelve members. Widening it to the whole interface would be scope this card does not carry.

Provenance: authored by an os-dev seat under the PM dispatch on #15937, session session_01LvwGppdonww4zGLWZo5rho (https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho).


Generated by Claude Code

…cutionContext`

A published handler already authorizes on this consent digest — cloud's
`apply_blueprint` gate makes a matching blueprint identity one clause of
the decision to build a whole app (cloud#1954 / cloud PR #2005) — while
the member it reads was declared only on cloud's augmented context and
reached by a structural cast. The protocol is the baseline, so the field
is declared here.

- `ToolExecutionContext.confirmedBlueprintIdentity?: string`, with the
  provenance docblock `userMessageText` carries: stamped by whichever
  layer owns the agent route, only by in-process server code on that
  route, never from a request body; `undefined` means "no confirmed
  identity on this turn" and authorizes nothing.
- `confirmed-blueprint-identity-contract.pin.test.ts` pins the
  declaration half — the member, its thread through
  `ChatWithToolsOptions.toolExecutionContext`, its optionality and its
  `string` type — each negative leg paired with a lit positive one.
- Changeset: `@objectstack/spec` minor. `Clause-②: yes` (widening).

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 2 documentable anchor(s).

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

  • content/docs/protocol/knowledge.mdx (via ToolExecutionContext (symbol, a top-level interface))

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

  • content/docs/releases/v15.mdx (via ToolExecutionContext (symbol, a top-level interface))
  • content/docs/releases/v9.mdx (via ToolExecutionContext (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
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 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; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 136 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 7f7b8557dfb459719e1eb473cd9ea5f10a7322b9 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 7f7b8557dfb459719e1eb473cd9ea5f10a7322b9

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

Copy link
Copy Markdown
Collaborator Author

契约复核记录 — 交付后复核,档位 CONTRACT_REVIEW_TIER

由 domain:spec 席位(session_01LvwGppdonww4zGLWZo5rho)转录落档。本席服务档读数在 CONTRACT_REVIEW_TIER 之下,故复核走转录核验的隔离子代理跑在该档,⛔ 不在席内判、⛔ 不作额度降档。以下为复核代理产出,逐字落档,⛔ 未经本席编辑。

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5ddd6f4

Method: own blobless clone at the head SHA (merge-base with main 7f7b8557d; head is 5 commits behind origin/main); the shared checkout was not used for any reading. Every gate script and every config I ran was blob-compared to origin/main first (git rev-parse HEAD:<path> vs origin/main:<path>; all SAME except one import noted in ③). Every exit code was read from $? after redirection to a file. Diff vs merge-base: 3 files, +160/−0; the merge commit vs its own main parent shows the same 3 files and nothing else.

① Derived judgments

  1. ADR-0049 enforce-or-remove — what enforces the four provenance clauses: exactly one, and only its handler-side half.

    • Enforced by the type: undefined is a legal state and a handler cannot compile a path that assumes presence. Ablation L2 (member made required) → check:test-typecheck exit 1; raw tsc: TS2345 ×2 (pin lines 77, 86) + TS2578 at line 88 (leg 4's directive unused). Baseline: gate exit 0, 0 diagnostics in the pin.
    • Enforced by nothing: confirm-replay origin, in-process-route-owner stamping, never-from-request-body/tool-argument/transcript. Measured: producers of toolExecutionContext in packages/ outside spec = 0 files (lit control isSystem = 716 files); readers of confirmedBlueprintIdentity anywhere = the interface, the pin and the changeset only (control userMessageText: the same 2-file footprint); zod mirrors of ToolExecutionContext = 0 (control: 4 zod files carry isSystem: z.). So an optional string accepts a request-body value exactly as it accepts a digest, and nothing in this tree would stop it.
    • Verdict: acceptable documented-invariant, not an ADR-0049 violation. Reasons: (a) ADR-0049's population and its gate (check:liveness, header: "authors write metadata against these schemas… SOURCE OF TRUTH: the metadata-type registry", walks Zod) are authorable metadata; contracts/ai-service.ts is a TS runtime contract no author writes, and the ADR's harm model (an admin sets a key and believes a boundary exists) has no author to mislead here. (b) The clauses are producer obligations of the same shape isSystem on the same interface has carried since Security: AI ToolExecutionContext contract documents system-level as the missing-actor default — a contract-level fall-open across all data tools #2991 ("MUST be set by trusted server code only, never derived from request input"), which ADR-0118 D2/D3 elevated to a platform rule with enforcement placed at the entry point (D3, tech-debt/tracking: 让「特权内部写入必须显式声明 isSystem」成为可检查契约(先立约束 → 增量迁移 → 收敛强制) #3166 lint/audit), not in the type — and isSystem gates an RLS bypass, strictly more than a consent digest. (c) The entry point for this field is the agent route, which lives in cloud post cloud ADR-0025; there is no producer in this repo to enforce at. Caveat: ADR-0049 state 1 ("a runtime consumer reads it and changes a decision") rests on cloud confirm-gate.ts, asserted by the ruling's freshness re-read and NOT MEASURABLE here. If that consumer did not exist this would be a dangling declaration and the verdict would flip; ruling item 4 (cloud replaces the cast with the typed read) is what keeps it honest.
  2. "authorizes nothing" when undefined: honoured in this tree trivially and by type. No code here reads the member (0 readers, control above), so no in-repo path can authorize on absence; the string | undefined read type is pinned (L2). The authorizing consumer is cloud (cloud#1954 / cloud PR chore(showcase): seed Field Zoo with all field types + guard budget hook #2005): NOT MEASURABLE, not speculated on in either direction.

  3. isSystem duplication/conflict: none. isSystem answers "with what authority does this run" (boolean, RLS bypass, "ignored when an actor is present"); confirmedBlueprintIdentity answers "which exact proposal did the human approve" (digest; grants no authority; "one clause of the decision"). No member on the interface carries that fact today. systemInvocation, by its name, would be a second spelling of the privilege fact isSystem already carries — that is why the dev's deferral reason applies to it and does not apply to this member. I also checked the file's other confirmation surface, AIActionConfirmation.confirm?: boolean (spec: define the confirmation contract behind action.ai.requiresConfirmation #16531, ~line 299): a caller-written attestation on the action request, for action.ai.requiresConfirmation doors, explicitly "no server-side approval record". Different carrier (request vs execution context), different shape (boolean vs digest), opposite trust class, different door — not a duplicate. Finding (non-blocking, listed in ③): neither docblock references the other, and the new sentence "anything the caller can write is not a confirmation" contradicts AIActionConfirmation when read in isolation. One sentence in the new docblock closes it, inside the PR's own diff: "This is not the request-side AIActionConfirmation.confirm attestation: confirm: true on a request never substitutes for this member."

  4. Widening, empirically: holds. Probe compiled against the merge-base interface (git show 7f7b8557:… imported as OldCtx) and head: old→new value, new→old value, both options bags both ways, a literal carrying every merge-base member, and a handler typed against OldCtx placed in a new-typed slot (strictFunctionTypes) — tsc exit 0. Instrument control: dropping the directive on an excess-member literal → exactly one TS2353, exit 2. exactOptionalPropertyTypes: 0 occurrences tree-wide (control: root "strict": true at tsconfig.json:9). No zod schema mirrors the interface (0 / control 4), so no parse path accepts differently. api-surface/contracts.json records ToolExecutionContext (interface) by name only. NOT MEASURABLE: cloud's declaration-merged augmentation — an identical ?: string merges silently; any other type would surface as TS2717 at cloud's next pin bump.

  5. See ②.

  6. Pin test: re-derived; the pairing is real and every negative leg fails under the mutation it names. Gate check:test-typecheck (script, tsconfig.test.json and debt ledger all blob-identical to origin/main; the new file has 0 ledger entries, control: 55 src/ entries): baseline exit 0 (40 s). L1 remove member → exit 1, 10 diagnostics in the pin (dev reported 10): TS2353/TS2339 at lines 57, 60, 67, 70, 79, 82×2, 99, 111, 122 — all six legs red. L2 required → exit 1, 3 diagnostics: TS2345 at 77, 86 + TS2578 at 88 (leg 4). L3 any → exit 1: TS2578 at 88 and 105 (legs 4, 5). L4 unknown → exit 1, 2 diagnostics (gate only). L5 rename to the typo spelling → exit 1, 11 diagnostics incl. TS2578 at 116 (leg 6). Each mutation proven on disk by anchor count and git hash-object (bf79a0cc → 11d9c21e / af563cf9 / 6dc13541 / 180c62e3 / 955387ca) and restored byte-identical to bf79a0cc3f6fb0505e0edf6c5665c3f004e371f4; tree clean at the end. Raw tsc -p tsconfig.test.json baseline: 259 diagnostics, all ledgered, 0 in the pin — the gate is the ledger-aware instrument it claims to be. Runtime legs: vitest run --project local <pin> exit 0, 6/6 (control: sibling action-confirmation-contract.pin.test.ts 6/6 under the same invocation).

  7. Bare ADR-0025 on the adjacent userMessageText line: reading confirmed. This repo's ADR-0025 is docs/adr/0025-plugin-package-distribution.md; cloud's is cloud/docs/adr/0025-service-ai-to-cloud-open-mcp-only.md (2026-06-25), cited as cloud ADR-0025 in five ADR callouts (0033/0038/0040/0063/0064) and packages/mcp/src/skill-prompts.ts. userMessageText's docblock landed 2026-06-25 (feat(spec): add ToolExecutionContext.userMessageText contract field #2328, 7db9a733f) — the day of that cloud record — and its sense (which layer owns the agent route after the service-ai move) is cloud's. So the existing line is a Prime Directive 13 violation and the new line is spelled correctly. Nothing is red: check-adr-anchors reads only scripts/adr-anchors/ (its own header, lines 78/450, says bare references in packages/** are not checked) and no anchor names ai-service.ts. Disposition: acceptable to leave for this PR — a pre-existing defect of a different class, reported with dedupe words; not a reason to REWORK. It must be filed (seat), and if this PR takes any further push (e.g. item 3's sentence) fold the one-token fix in then: two adjacent lines citing one record two ways is the shape the next reader "corrects" in the wrong direction.

② Semver level

minor is correct; the changeset needs no BREAKING banner and no ADR-0087 marker.

  • Rule: AGENTS.md Post-Task §3 (Clause-②: yes takes at least minor; only (narrowing) is BREAKING), pr-automation "WHICH LEVEL" (additive widening ≥ minor), and ruling item 2.
  • Measured: check-changeset-no-major.mjs (blob = origin/main; --self-test 299 assertions exit 0): real run exit 0; with the real PR payload as --event (labels incl. needs:contract-review; Clause-②: yes (widening) on body line 5): exit 0, "declares clause-② yes… direction arm: widening". Control: same payload with the changeset committed as patch → exit 1 "declares clause-② YES, and it grades NO package… minor". (A first control with an uncommitted edit stayed green because the gate scans the committed diff — that was my instrument, not the gate.) CI's second Check Changeset run (14:34:59Z, after the carrier label at 14:34:53Z) is green on this head.
  • check-adr-0087-registration.mjs (blob = origin/main; --self-test 384 assertions exit 0): real run exit 0, "adds no declared-breaking changeset (1 non-breaking changeset seen)". It classifies breaking on **BREAKING / major / feat! / Clause-② … narrowing; its header states widening "adds NO signal". A BREAKING banner on this widening would be wrong: it would turn a non-breaking changeset into one that demands an ADR-0087 disposition.

③ Boundary flags

out_of_scope_findings:

  • (dev-reported, confirmed) bare ADR-0025 in userMessageText's docblock, packages/spec/src/contracts/ai-service.ts — PD 13; file it, or fold in on the next push if there is one.
  • (new, non-blocking) the two confirmation surfaces in ai-service.ts (AIActionConfirmation.confirm, request-side; confirmedBlueprintIdentity, context-side) do not reference each other; one sentence in the new docblock (wording in ①.3) closes it, inside this PR's diff if folded in.
  • (noted, not a PR defect) scripts/pm/check-half-states.mjs on the PR head differs from origin/main (+246/−10); the only symbol the no-major gate imports from it (CONTRACT_REVIEW_LABEL) is identical at both, so the reading stands. Head is 5 commits behind main; a merge before landing is the queue's business.

gate accounting — ran here: check-changeset-no-major (self-test, real, --event, committed control), check-adr-0087-registration (self-test, real), check:test-typecheck (baseline + 5 ablation legs), raw tsc -p tsconfig.test.json (baseline + 4 legs), widening probe + control, vitest on the pin + sibling control. NOT run here (no spec build, no root install): pnpm --filter @objectstack/spec build, check:generated / check:api-surface / check:docs, eslint, the full vitest suite, check:cross-package-test-inputs, check:adr-anchors. For those the evidence is CI on this head, all success: Type Check · source gates, Type Check · workspace, Type Check · consumer gates, Type Check · debt ledger, Lint & Repo Gates, Test Core (1–6/6), Spec property liveness, Check Changeset ×2; Console Pin Gate / Build Docs skipped by path filter.
NOT MEASURABLE: everything in objectstack-ai/cloud — the augmentation's declared type, the confirm-gate.ts read, whether undefined is honoured there.
escalation: none. No governed surface in the file list (.changeset/, packages/spec/src/contracts/).

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Contract review

Head: 5ddd6f45eea88e13fdd75fa0120638d1c2331f21

Tier: CONTRACT_REVIEW_TIER — served by an isolated transcript-verified review subagent at that tier, because this seat's own served tier (claude-opus-5) reads below it. ⛔ Not an in-seat review, ⛔ not a quota downgrade.

Verdict: PASS

Full record, transcribed verbatim and unedited by this seat: comment 5716513724.

Reviewed-by: session_01LvwGppdonww4zGLWZo5rho
Implemented-by: claude/issue-15937-confirmed-blueprint-identity-protocol

⭐ 本条是补记,且补记的原因是本席的顺序错误 —— 记下来而不是抹掉

本席先摘了两侧 needs:contract-review 载体,后才复跑落地前检查,结果 check-clause2-carriers --pair 18676 报 exit 4 / 行 C6:载体已清,而这一 head 上没有符合机器可读形态的复核记录。

闸门要的三个记号,本席 14:5xZ 发的那条记录一个都不满足(已实测,⛔ 不是推断):

记号 那条记录
二级标题以 ## Contract review 起头 ❌ 写的是 ## 契约复核记录
所审 head sha 独占一个码段 ❌ 写成 Head-sha: 5ddd… 纯文本
Reviewed-by: 行 ❌ 缺

⇒ 从闸门的视角,那正是它点名的半态:「a cleared gate with nothing behind it, indistinguishable from never reviewing」。闸门是对的,本席是错的。

⛔ 本条不是对复核代理记录的编辑。 记录按「逐字落档,要么整条采信要么整条作废,⛔ 永不编辑」处理,原样留在 5716513724。本条是闸门 remedy 明写的那件事 ——「the owning seat writes down the review it already performed, in that shape」—— 由席位补写形态并引那条逐字记录。

正确顺序应是:先验记录形态 → 再摘载体 → 再复跑检查。 本席做成了摘在前。下一张卡按前者走。

⚠️ 同一形态缺陷也在 PR #18638 的记录上(5715671761,标题同为 ## 契约复核记录,sha 非独占码段)。那张 PR 仍在补丁轮,落地前会撞上同一行 C6;已记在案,届时同法补记,⛔ 不到那时才发现。


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: 80/80 CONTRACT_REVIEW_TIER
Head-sha: 5ddd6f45eea88e13fdd75fa0120638d1c2331f21

档位读数是本席自己 grep 复核那一轮转录里 harness 盖的 served-model 字段所得:80 个带戳的 assistant 轮次,80 个在档,0 个脱档。⛔ 不是派发时传的 model 参数 —— 那是配置,不是读数。

① Derived judgments

逐字全文见本 PR 评论 5716513724,原样落档、未经本席编辑(记录按「要么整条采信,要么整条作废,⛔ 永不编辑」处理)。要点:

  1. ADR-0049 enforce-or-remove —— 四条来源条款里,只有「undefined 是合法态、handler 不能编译出假定其存在的路径」这半条被类型强制(消融 L2:成员改必填 ⇒ 闸门 exit 1)。另外三条(confirm-replay 来源、仅由持有 agent route 的进程内服务端代码盖戳、绝不取自请求体/工具参数/转录)无任何东西强制:本仓 packages/ 内 toolExecutionContext 的生产者 0 个文件(LIT 对照 isSystem = 716 个文件),ToolExecutionContext 的 zod 镜像 0 个(对照:4 个 zod 文件带 isSystem: z.)。裁定为可接受的成文不变量,不是 ADR-0049 违规,理由三条(ADR-0049 的人群是可编写的元数据;这些是与 isSystem 同形的生产者义务,ADR-0118 D2/D3 把强制点放在入口而非类型;本仓根本没有生产者可强制)。⚠️ 该裁定的前提「cloud 侧真有消费者读它」本仓不可测。
  2. undefined 不授权:本仓内无人读该成员(0 个读者),故无路径能凭缺失授权;cloud 侧 NOT MEASURABLE,⛔ 两个方向都未臆测。
  3. 与 isSystem 不重复:前者答「以何权限运行」(布尔、RLS 绕过),后者答「人类批准了哪一份提案」(摘要、不授予权限)。另核了同文件的 AIActionConfirmation.confirm:请求侧、布尔、相反信任类、不同门 —— 不重复。
  4. 加宽成立(实测):对 merge-base 接口与 head 双向编译,含 strictFunctionTypes 下的 handler 代入;仪器对照(去掉指令的多余成员字面量)恰好一条 TS2353。exactOptionalPropertyTypes 全树 0 次(对照:根 "strict": true)。
  5. pin 可证伪:5 条消融腿全部变红(L1 移除成员 ⇒ 10 条诊断,六条腿全红;L5 改名 ⇒ 11 条),每次变异以 git hash-object 在盘上证明,并逐字节还原回 bf79a0cc3f6fb0505e0edf6c5665c3f004e371f4。

② Semver level

minor 正确,且该 changeset 不需要 BREAKING banner、不需要 ADR-0087 条目。check-changeset-no-major.mjs 以真实 PR payload 跑 --event exit 0(direction arm: widening);受控对照(同 payload、changeset 提交为 patch)exit 1。check-adr-0087-registration.mjs exit 0。给一个 widening 挂 BREAKING banner 会是错的:那会把一条非破坏性 changeset 变成需要 ADR-0087 处置的条目。

③ Boundary flags

Implemented-by: claude/issue-15937-confirmed-blueprint-identity-protocol
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

VERDICT: PASS


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants