Skip to content

fix(metadata-protocol): stop persisting derived provenance keys, and restate tenant authorship at hydration for every type - #16796

Merged
os-zhuang merged 8 commits into
mainfrom
claude/issue-16702-strip-derived-provenance-keys
Sep 8, 2026
Merged

fix(metadata-protocol): stop persisting derived provenance keys, and restate tenant authorship at hydration for every type#16796
os-zhuang merged 8 commits into
mainfrom
claude/issue-16702-strip-derived-provenance-keys

Conversation

@os-musk

@os-musk os-musk commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Fixes #16702

Clause-②: yes

The defect

saveMetaItem persisted the caller's _packageId / _packageVersion / _provenance verbatim, and for every non-object type boot hydration read that stored body back as truth. metadata-read-decorations.ts deliberately does not strip _provenance from a served document, so the ordinary Studio GET /meta/app/pet_hospital then PUT /meta/app/pet_hospital round trip wrote _provenance: 'package' into the tenant's own sys_metadata row. From there the row's own bytes decided: isCodeArtifactBody accepted a truthy non-sentinel _packageId with non-org provenance, SchemaRegistry.getArtifactItem's bare-key fallback returned the overlay as an artifact, isArtifactBacked turned true, and every later write was refused NOT_OVERRIDABLE — permanently, since the next boot re-derived the same verdict from the same row.

The failure also misattributes: the operator is told the item is "provided by a code package" when no code package publishes it at all.

Both doors, because they cover different populations

Door 1 — the write door. saveMetaItem now drops exactly _packageId / _packageVersion / _provenance from the body it persists, one beat after the existing stripReadDecorations call and for the same reason. The _lock* family is deliberately untouched: a lock is author-declarable and dropping one is the fail-open direction, the line cloud PR #2065 drew at its own producer.

This is a new, write-door-scoped strip and not a new member of METADATA_READ_DECORATIONS. That list is shared with every consumer that re-parses a served document, and its own header states on purpose that the protection envelope stays on a served body so provenance survives a re-parse.

Door 2 — hydration. hydrateOverlayIntoRegistry — the one choke point boot, read-side and write-through hydration already share — now states _provenance: 'org' on a copy before mergeArtifactProtection runs. That is the same sentence applyObjectRegistryMutation and the boot object limb have always written, said once for every other type. The order is the whole contract: where a real artifact exists its envelope still overwrites _provenance (and _packageId / _packageVersion / _lock*) on the way out, so ADR-0010 section 3.3 precedence is unchanged in both directions.

Door 2 is not belt-and-braces. Measured, not argued: with door 2 ablated and door 1 live, a row whose stored body carries no protection key at all is still hydrated as a code artifact, because getMetaItems re-stamps _packageId onto the body from the row's package_id column a few frames before handing it to the hydrator. The write-door strip cannot reach that path; the restatement can.

⚠️ What door 2 does not fix: the SERVED document

Door 2 corrects the authorization verdict, not the served document. getMetaItem and getMetaItems return the overlay row's own body — _packageId stamped from the column, no _provenance restatement — and mergeArtifactProtection only fires where an artifact exists. So a row already poisoned at rest becomes editable again while GET /meta/app/x keeps serving _provenance: 'package' and the UI keeps badging it as package-provided, until that row is re-saved through door 1 or backfilled. The objectstack-ai/cloud#2069 backfill stays load-bearing for that population; this PR does not retire it, and the criterion-7 reply owes that sentence so cloud does not read "the door is shut" as "the backfill is no longer needed".

Measured on this head, one poisoned-at-rest app row with no artifact, through a throwaway probe deleted in the same round:

[F2 probe] hydrated registry entry _provenance: "org"
[F2 probe] getMetaItem RAW: {"type":"app","name":"pet_hospital","item":{"name":"pet_hospital","label":"Pet Hospital","_packageId":"app.sdbh","_packageVersion":"1.0.0","_provenance":"package","_diagnostics":{"valid":true}},"lock":"none","provenance":"package","packageId":"app.sdbh","packageVersion":"1.0.0","editable":true,"deletable":true,"resettable":false}
[F2 probe] getMetaItems RAW: {"type":"app","items":[{"name":"pet_hospital","label":"Pet Hospital","_packageId":"app.sdbh","_packageVersion":"1.0.0","_provenance":"package","_diagnostics":{"valid":true}}]}

editable: true is door 2 working; "provenance":"package" in the same envelope is the residual. The stale _packageVersion rides along on the same rows and is cosmetic until the backfill.

Acceptance criteria, and where each is asserted

New file packages/objectql/src/protocol-derived-provenance-doors.test.ts — a real ObjectQL engine, a real SchemaRegistry, sys_metadata registered, no code package loaded.

criterion assertion
1 — the card's reproduction leg, run as written #16702 — the card's reproduction leg, run as written. Writes the app carrying _provenance: 'package', boots a fresh engine + protocol over the same driver, calls loadMetaFromDb(), then saves again. Output pasted below.
2 — strip exactly three keys, never _lock* two cases under door 1: one asserts the three keys are absent from the persisted body and name / label survive; the other sends _lock / _lockReason / _lockSource / _lockDocsUrl alongside them and asserts all four survive the write.
3 — negative control two cases under criterion 3. A real artifact registered under the composite key app.sdbh:pet_hospital is still refused NOT_OVERRIDABLE / 403 — once with no overlay, and once after a boot that hydrated a tenant overlay of the same name.
4 — both branches two cases under criterion 4. The object branch keeps stamping _provenance: 'org' and stays editable across a restart, and it still registers through registerObject (contributor layers) rather than the shared registerItem path — getObject and getAllObjects(packageId) both answer. The first case also asserts the stored object row lost all three keys, pinning door 1's type-agnostic scope rather than implying it (the strip runs before saveMetaItem's type branch). The object branch source is untouched by this PR.
5 — mergeArtifactProtection precedence its own two cases. With a real artifact present the hydrated entry carries the artifact's _lock: 'full', _lockReason, _packageId, _packageVersion and _provenance: 'package' while the overlay's authored label still wins; with no artifact the restatement stands and nothing is invented.
6 — the row.package_id reading below. Reported, not fixed here, exactly as triage fenced it.
7 — close the loop with cloud see the report on this branch's dispatch. The reply on objectstack-ai/cloud#2069 owes the SERVED-document residual named above, so cloud does not read "the door is shut" as "the backfill is no longer needed". objectstack-ai/cloud is not reachable from the sessions that have worked this branch, so posting that reply is handed to a seat that can reach it.

Criterion 1, run as written

[#16702 repro] stored row: {"name":"pet_hospital","label":"Pet Hospital"}
[#16702 repro] loadMetaFromDb(): {"loaded":1,"errors":0,"invalid":0,"storeUnavailable":false}
[#16702 repro] second saveMetaItem receipt: {"success":true,"version":"sha256:6e4970111e11bb918a3b5514b20901d9e7ca9244b4434e29ecd1270ad2e0fd26","seq":2,"state":"active","message":"Saved app 'pet_hospital' (env-wide, state=active) [seq=2]"}

On origin/main 73053ed27b, the same path, byte for byte the card's own output:

[#16702 repro] stored row: {"name":"pet_hospital","label":"Pet Hospital","_packageId":"app.sdbh","_packageVersion":"1.0.0","_provenance":"package"}
[#16702 repro] loadMetaFromDb(): {"loaded":1,"errors":0,"invalid":0,"storeUnavailable":false}
Error: [not_overridable] Metadata item 'app/pet_hospital' is provided by a code package and the type has not opted into per-org overlay writes (allowOrgOverride=false).

Criterion 6 — the reading on the row.package_id column path

Triage named one site and asked whether the column is a second poisoning path. The dispatch measured three. Re-derived by symbol on origin/main 73053ed27b (protocol.ts, 21,885 lines; firing controls on the same file: _packageId 34 lines, _provenance 12 lines, a nonsense control 0):

  • :4922 const packageId = row.package_id; — in foldStoredCollection. Stamps _packageId onto a freshly parsed, ephemeral body that is pushed into the runtime authoring gate's reference-resolution universe and returned. Nothing persists it and nothing registers it. Not a poisoning path.
  • :16054 const packageId: string | null = row.package_id ?? null; — in migrateStoredMetadata. Reads the column into the migration report row and passes it as the packageId parameter of the re-save, never as a body key. Not a poisoning path — and after door 1 an --apply migration pass now actively cleans the rows it rewrites.
  • :19972 packageId: row.package_id, — in revertCommit, where row is a commit row and the value goes to recordPackageCommit as a commit column. Not a metadata body at all. Not a poisoning path.

But the pattern row.package_id cannot express the shapes that matter. A whole-file census of the package_id spelling (61 occurrences) finds the cast and destructured forms it misses, and four of those DO write the column into a body: :7208, :7354, :7736, :7805 — all in getMetaItems / getMetaItem, all (record as ...).package_id, each if (recPkg && body._packageId === undefined) body._packageId = recPkg;.

:7208 is the live one. The same overlays array it stamps is handed to hydrateOverlayIntoRegistry at :7302 on an unscoped kernel, so an ordinary list read re-injects _packageId from the column into the in-memory registry entry. That is a genuine second path to isArtifactBacked, it needs no poisoned bytes at rest, and door 1 alone cannot close it. Door 2 does, because isTenantAuthored asks _provenance, not _packageId. Asserted by the read-side hydration seam (getMetaItems) is covered by the same restatement.

No fix for the column path is folded into this PR. None is needed for the defect this card names: the stamp is a legitimate read-side surfacing of the row's package binding (the sidebar package filter consumes it), and door 2 makes it non-load-bearing for the authorization verdict.

Verification

Ablation — two legs, PLANT mode, blob-hash-verified restore. Each leg mutated protocol.ts, rebuilt @objectstack/metadata-protocol (the suite resolves it through dist/), and proved the marker reached the built artifact with scripts/ablation-dist-preflight.mjs before any colour was read.

  • Leg A, door 1 neutered (DERIVED_PROVENANCE_KEYS swapped for a nonsense key): marker present in 2 built files; 3 cases red — the two door-1 pins and the object stored-row pin added by the contract-review round (expected { name: 'pet_visit', …(6) } to not have property "_packageId"). It reddened 2 before that pin existed, which is the measurement that the pin discriminates.
  • Leg B, door 2 neutered (_provenance: 'org' swapped for a non-org literal): marker present in 2 built files; 3 cases red — the two at-rest/read-side pins and the no-artifact restatement pin.
  • Both restores: restored blob equals the HEAD blob byte for byte, whole-tree git status --porcelain empty, rebuild plus --absent confirms the marker is gone from all 24 built files, restored suite 11 of 11 green.

One thing stayed green under both legs, and that is the finding, not a defect in the ablation: the reproduction leg itself. Either door alone closes it, which is exactly triage's "they cover different time windows". The layer holding it is the other door. The repro leg's own discriminating power is proven by the pre-fix run with neither door present, which reproduced the 403 verbatim.

Suites. @objectstack/metadata-protocol 169 files / 2,433 tests pass, 2 files / 10 cases skipped — named and counted: the describe.skipIf(!MYSQL_URL) limbs of src/migrations/sys-setting-identity-index.live-mysql.test.ts (5) and src/migrations/seed-tenancy-backfill.live-mysql.test.ts (5), pre-existing and unrelated. @objectstack/objectql 286 files / 4,819 tests pass, 0 skipped. Consumer sweep, because the served shape can change: @objectstack/rest 192 files / 3,241 tests and @objectstack/runtime 240 files / 3,340 tests, both fully green, 0 skipped.

Typecheck legs, read out of each package.json rather than assumed. metadata-protocol: tsc --noEmit, 1 leg. objectql: tsc --noEmit, then tsc --noEmit -p tsconfig.scripts.json, then check:test-typecheck (its own --self-test plus --package packages/objectql --project tsconfig.test.json) — 3 legs. All green.

Existing pins updated, deliberately. Six cases asserted deep equality on a hydrated non-object registry entry and now see one more key. Each was updated to toEqual against the exact new shape rather than relaxed to toMatchObject, so an unexpected extra key still reds them: protocol-meta.test.ts (4), plugin.integration.test.ts (1), and protocol-boot-hydration-scoped.test.ts, whose "registers the row unchanged" case is renamed to what it actually pins — no artifact envelope is grafted, while _lock and _packageId stay absent and _provenance is now the server's own statement.

Gates. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 59 families; all 59 were run and reconciled with --ran:

Run reconciliation — 59 derived, 59 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 59 derived famil(ies) accounted for — 59 run, 0 NOT-MEASURED.

57 exit 0. Two exit 3, which is PREREQUISITE NOT MET and is neither a pass nor a finding — check:dual-build-cjs-loads and check:type-check-debt both refuse to measure without a whole-workspace build closure, and both say so in their own output. They are declared to CI, which builds first. check:objectql-double-limit caught a real defect in the new stub driver on its first run (a find double ignoring the caller's bound); corrected in bbe4590c and re-run green.

Lint, as a proven narrowing rather than a skipped run. Population read from eslint's own config: 6,348 tracked files carry a lintable extension and none is removed by the single global-ignore block, whose whole content is node_modules / dist / build / .next / .turbo — untracked build output that a tracked-file census matches 0 files under, as the config's own header records. Files linted, counted from --format json: 5, the entire lintable half of the change set, eslint --no-inline-config exit 0, 0 errors and 0 warnings. Invariance: this repo runs one eslint.config.mjs and it never enables type-aware linting for any file (no parserOptions.project, no typed rules), so no verdict on any of the 6,343 untouched files can depend on this diff. Measured at bbe4590c.

Changeset. minor, on @objectstack/metadata-protocol. The floor comes from the DECLARATION, not from the direction of the change. check-changeset-no-major's level axis refuses any PR that declares Clause-②: yes while grading a package whose packages/*/src/** it moves as patch, and it never asks which way that surface moved. This PR declares Clause-②: yes (line 3 of this body) and moves packages/metadata-protocol/src/**, so minor is owed whatever the direction. (The act is not additive — no export is added and no key or value is newly accepted — but direction does not set this floor, and an earlier revision of this paragraph wrongly argued patch from it.) Route 1 of the two the gate names is taken: "The declaration is right and the level is wrong -> raise it to minor." Route 2 — correcting the declaration at the producer — is the review seat's and is deliberately not exercised: the Clause-②: line is untouched and no tolerance was added to the gate. The release effect of the raise is nil: @objectstack/metadata-protocol sits in the single fixed version group in .changeset/config.json (one group, 70 members), so it already moves in lockstep with every other package in that group.

none / the skip-changeset label was rejected and stays rejected: AGENTS.md is a floor against none for a bug fix in a released package, not a ceiling, and this diff does publish from a released package. No BREAKING banner and therefore no ADR-0087 disposition: nothing an author can write is removed or renamed, no export or config field changes, the three keys are still accepted on input, and where a real artifact exists the read side recomputes all three from it on every read — so there is no FROM-TO mapping to ship and no migration to prescribe. check-adr-0087-registration agrees on this diff.

验收备注

Triage's seven, adopted verbatim (5578409434, quoted unchanged):

  1. 复现腿原样跑通并贴出:写入带 _provenance: 'package'app → 新引擎 → loadMetaFromDb() → 再次 saveMetaItem 不再 403。⛔ 不要用改写后的等价脚本代替 —— 要验的正是卡面那条路径。
  2. 只剥三个键:_packageId / _packageVersion / _provenance。⛔ 不要碰 _lock* 家族 —— 锁是作者可声明的,剥掉一个锁是 fail-open 方向,这条线 cloud PR feat(security): ADR-0056 D1 (canonical OWD vocab) + D2 (anonymous deny posture) #2065 在它的生产者侧已经划过,本卡沿用。
  3. 阴性对照必测:一个真的由代码包提供的 app(真 artifact 在场),在修复后必须仍然NOT_OVERRIDABLE 拒绝。一个"无条件放行"的实现会让第 1 条绿掉,同时把包保护整层删掉。
  4. object 与非 object 都要有断言:object 分支今天是对的,回归后必须仍然对。修法若是"给非 object 分支也重述一次",⛔ 不得顺手把 object 分支改写成共用路径而不测它。
  5. mergeArtifactProtection 的优先级不变:真 artifact 在场时,artifact 的信封仍须压过存储副本(卡面在修法 2 的 ⚠️ 里已点名)。这条要单独一个断言。
  6. row.package_id 列那条相邻面给出读数(见上)。
  7. 与 cloud 侧对齐:cloud#2050 / PR feat(security): ADR-0056 D1 (canonical OWD vocab) + D2 (anonymous deny posture) #2065(堵住一个生产者)与 cloud#2069 / PR examples/app-showcase: add an end-to-end external datasource + federated object example #2111(回填)已在路上。本 PR 落地后请在 cloud#2069 上回帖说明门已关,⛔ 不要让两边各自以为对方在管。

Out-of-scope notes, filed nowhere

  • getMetaItem / getMetaItems stamp _packageId onto a served body from the package_id column at four sites (:7208, :7354, :7736, :7805) and never stamp _provenance beside it. Noted, not filed: with door 2 in place the asymmetry no longer decides an authorization verdict, so it is an observation about symmetry, not a reproducible defect.
  • publishMetaItem, rollbackMetaItem (restoreVersion) and revertCommit write bodies that did not come through saveMetaItem in this call. Each originates from a body saveMetaItem already wrote (a draft, a history entry, a commit item) — but door 1 covers them transitively only for bodies written AFTER this PR. A rollback to a pre-PR history version re-persists the three keys into the active row, so the stored corpus does not converge through those paths; door 2 is what keeps that harmless, by making the stored copy non-load-bearing for the verdict. Noted, not filed: no repro, and the residual is the same one the section above names.

Draft only. Not ready, not queued, no auto-merge.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 5 documentable anchor(s).

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

  • content/docs/concepts/metadata-lifecycle.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/deployment/validating-metadata.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/cluster.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/services-checklist.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/permissions/authorization.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))

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

  • content/docs/releases/v17.mdx (via saveMetaItem (symbol, a method of class ObjectStackProtocolImplementation))

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 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 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 — 10 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 625b0c32b4431ba5c04d7f898152a03a1f8a0251packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 625b0c32b4431ba5c04d7f898152a03a1f8a0251

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

Copy link
Copy Markdown
Contributor

Contract review (CONTRACT_REVIEW_TIER, isolated seat) — PR #16796 @ bbe4590c0

Verdict: CHANGES REQUIRED

Ruling implemented: no ruling — no maintainer ## Ruling recorded exists on card #16702 or on this PR. The PR implements the triage seat's preference (os-zhuang, 5578409434: "建议两条都做") as adopted by the PM seat (5578971143), which the card itself left open as "Which one — or both — is a maintainer call." Both seats explicitly declined the decision box on the "zero-cost narrowing" argument. The only maintainer ruling this PR cites is the batch #35 WHICH LEVEL ruling (.github/workflows/pr-automation.yml:667-682), which governs the changeset level, not the fix shape — and the PR's level choice is where it breaks (F1).

Quoted verbatim, the closest thing to a ruling on the card (triage, seat preference, not maintainer):

而且两条修法不互斥,卡面自己也这么说("which one — or both"):(1) 让存量语料向读侧已经在重算的那个真相收敛,(2) 让今天已经写坏的行在不重写的情况下无害。建议两条都做,理由是它们覆盖的是不同的时间窗

Scope vs that preference: exactly — both doors, three keys only, _lock* untouched, object branch source untouched (the only three non-comment code lines that change in protocol.ts are stripDerivedProvenance at the write door, stateTenantAuthorship inside hydrateOverlayIntoRegistry, and the two module-private helpers).

1. Files in the diff vs merge-base 8b37a0973d (6 files, +604/−8)

file status
.changeset/derived-provenance-write-door-and-hydration.md added
packages/metadata-protocol/src/protocol.ts +106/−1
packages/objectql/src/protocol-derived-provenance-doors.test.ts added (440)
packages/objectql/src/protocol-meta.test.ts 4 pins updated
packages/objectql/src/plugin.integration.test.ts 1 pin updated
packages/objectql/src/protocol-boot-hydration-scoped.test.ts 1 pin updated + renamed

Governed paths touched: NO (docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md, content/docs/releases/** — none). The documentation / tooling labels are the auto-labeler's, not a governed touch. Maintainer-only merge on governed grounds: no.

2. Clause-② re-derived independently: yes — and the body's own patch argument contradicts its own declaration

Re-derived from code, not from the body:

  • Persisted metadata shape narrows. saveMetaItem (protocol.ts:15006) now drops _packageId / _packageVersion / _provenance from every body it persists, for every type including object (the strip sits before the type branch). The stored sys_metadata body is a persisted shape; it changed.
  • Hydrated registry-entry shape widens. hydrateOverlayIntoRegistry (:14131) now registers {...data, _provenance: 'org'} for every non-object row with no artifact. That entry is what SchemaRegistry.getItem / getAllApps / listItems answer — an exported objectql surface — and the PR's own six updated deep-equality pins are the measurement of that change.
  • The accept set of PUT /meta/TYPE/NAME is unchanged (the three keys are still accepted and silently dropped), so the request contract is not narrowed.

Clause-②: yes is correct. The body's argument that the act is "the opposite of additive" and therefore patch is where it goes wrong: the WHICH LEVEL ruling says a fix( that changes no public surface stays patch; this PR declares — correctly — that it changes one.

3. Changeset

  • Package name @objectstack/metadata-protocol: correct (the only package whose src/** moves; it is in the fixed group, version 17.3.0, not private).
  • Level patch: wrong per the gate that enforces the batch [WIP] Add query enhancements and advanced validation features #35 ruling — see F1. scripts/check-changeset-no-major.mjs judgeLevel: "enforce — declared yes, patch on a grown package → exit 1". Both Check Changeset runs on the head are red on exactly this.
  • No **BREAKING** banner and no ADR-0087 disposition: correct — nothing an author can write is removed or renamed; check-adr-0087-registration passes.
  • FROM/TO: the body states what stops being persisted and what is restated; it does not state the served-document residual (F2). Nothing deliberately removed is silently dropped.

4. Tests

  • New file protocol-derived-provenance-doors.test.ts: real ObjectQL + real SchemaRegistry + stub driver (not an engine double). No .skip / .only / .todo / skipIf in any of the four changed test files.
  • Would redden on revert, reasoned from code: the door-1 pins assert not.toHaveProperty('_packageId'|'_packageVersion'|'_provenance') on the stored body; without stripDerivedProvenance the repository's put persists JSON.stringify(body) verbatim (sys-metadata-repository.ts:666) → red. The door-2 pins seed a row with _provenance: 'package' and assert the hydrated entry is 'org'; without stateTenantAuthorship the hydrator registers data as-is → red. The repro leg: with both doors absent, isCodeArtifactBody (metadata-core/src/code-artifact-provenance.ts) accepts truthy non-sentinel _packageId + non-org provenance, getArtifactItem's bare-key fallback (registry.ts:3759/3777) returns the overlay, NOT_OVERRIDABLE follows → red. Not vacuous.
  • Negative control (criterion 3) registers a real artifact under the composite key and asserts 403 both without and with a hydrated overlay: it discriminates an "unconditional allow" implementation.
  • Criterion 5 asserts every artifact envelope key wins over both the stored copy and the 'org' stamp — the ordering contract of door 2 is pinned.
  • Typecheck program: packages/objectql/tsconfig.test.json includes src/**/*; the new file has no entry in the shrink-only debt ledger, so it must compile at zero errors — Type Check · workspace is green on the head.
  • Caveat the body admits: the suite resolves @objectstack/metadata-protocol through dist/, so locally it reddens only after a rebuild. CI orders Build Core before Test Core, so the gate is real there.

5. Persistence / hydration specifics

  • Stops being persisted: exactly _packageId, _packageVersion, _provenance, on the saveMetaItem door only, for all types (object included). _lock / _lockReason / _lockSource / _lockDocsUrl are persisted unchanged (pinned).
  • Restated at hydration: _provenance: 'org' on a copy, for every non-object row reaching hydrateOverlayIntoRegistry (boot loadMetaFromDb, read-side getMetaItems on the unscoped kernel, and the meta overlay: a just-saved overlay is listed but not dispatchable for a short window — a cache between saveMeta and resolveRouteActionDeclaration lags the write #4521 write-through), applied before mergeArtifactProtection so an artifact's _provenance/_packageId/_packageVersion/_lock* still overwrite it. The object limbs (applyObjectRegistryMutation, boot registerObject branch) are byte-for-byte untouched.
  • Backward read compatibility: a row written before this PR still parses and still hydrates (no schema change, no migration of at-rest bytes). It does not hydrate identically: its registry entry's _provenance becomes 'org' where it was previously the stored value or absent — that is the fix, and the six updated pins are the measurement. But the served document for an at-rest poisoned row is not corrected by this PR — see F2. That is a backfill question, not a migration of this PR, and it must be stated.

6. Criterion-6 reading, spot-checked

The body's claim that getMetaItems re-stamps _packageId from the package_id column onto the body handed to the hydrator is confirmed at :7288-7290 ((data as any)._packageId = recPkg) → :7380 (hydrateOverlayIntoRegistry). Door 1 cannot reach it; door 2 makes it non-load-bearing because isTenantAuthored asks _provenance, not _packageId. The "both doors are not redundant" reasoning holds.

7. CI on head bbe4590c0

37 check runs: 29 success · 2 failure · 6 skipped · 0 in progress. Red: Check Changeset ×2 (pr-automation on opened and on labeled), both on the level axis above. Skipped: Packed-tarball smoke ×2 (opt-in), Auto Label, Check PR Size, Build Docs, Console Pin Gate. mergeable_state: unstable (the red gate). Head is 11 commits behind origin/main at review time (base recorded at CI: b38821d1; merge-base now 8b37a097). No reviews on the PR; one bot comment (docs-drift, advisory).

Findings

F1 — CHANGES REQUIRED. Changeset level patch is refused by the gate that enforces the batch #35 ruling, and the refusal is correct. The PR declares Clause-②: yes (and carries the needs:contract-review carrier) while grading @objectstack/metadata-protocol patch; check-changeset-no-major.mjs judgeLevel returns enforce, exit 1 — both Check Changeset runs are red. This seat's independent re-derivation agrees the surface changes (persisted body narrows; hydrated registry-entry shape widens). Expectation: take the gate's route 1 — change the one word in .changeset/derived-provenance-write-door-and-hydration.md to minor, and rewrite the body's "Changeset" paragraph so it no longer argues patch from "not additive" while declaring yes. ⛔ Do not take route 2 (flipping the declaration to no) — the declaration is the accurate half.

F2 — Must be stated in the PR body and changeset: door 2 corrects the authorization verdict, not the served document. getMetaItem step 1 (:7860-7875) serves the raw overlay row body with only _packageId stamped from the column and no _provenance restatement; mergeArtifactProtection only fires when an artifact exists. getMetaItems likewise serves the overlay data (which wins the merge over the restated registry entry). So a row poisoned at rest keeps being served as _provenance: 'package' by GET /meta/app/x — the UI keeps badging it as package-provided — until the row is re-saved (door 1) or backfilled by cloud#2069 / cloud PR #2111. The refusal is gone; the misattribution in the served body is not, for that population. Expectation: one sentence in the changeset and the body naming this residual and that the cloud backfill is load-bearing for it; carry it into the criterion-7 reply on cloud#2069 so cloud does not read "door is shut" as "backfill no longer needed".

F3 — Non-blocking, confirm the body's "noted, not filed" is complete. promoteDraft (:16942) and restoreVersion (rollbackMetaItem :20331, revertCommit :19941) hand a stored draft/history body straight to repo.put without stripDerivedProvenance. A rollback to a pre-PR history version therefore re-persists the three keys into the active row. Door 2 keeps that harmless for hydration; the corpus does not converge on those doors. Expectation: acceptable as scoped, but the body's claim "door 1 covers them transitively" is only true for bodies written after this PR — say so.

F4 — Minor. Door 2 restates only _provenance; a poisoned row's stale _packageVersion survives onto the hydrated entry (non-load-bearing for authorization, cosmetic until backfill). Expectation: none beyond F2's sentence.

F5 — Minor test gap. The strip is type-agnostic, so object bodies also lose the three keys at rest; the criterion-4 object case writes _packageId/_provenance into the request but asserts only the hydrated entry, not the stored body. Expectation: optional one-line not.toHaveProperty on the stored object row, so the type-agnostic scope of door 1 is pinned rather than implied.

F6 — Process, not a defect. The "both doors" decision was taken by triage + PM as a determinacy call, with an explicit invitation to rebut; the card's own text called it a maintainer call. This seat finds the determinacy argument sound (a stored copy is never load-bearing on the read side; only the lie is negated) and the ablation reasoning that the two doors cover different populations verified from code. Expectation: none — recorded so the maintainer sees the decision was a seat's, not theirs.

Summary for the maintainer: the fix is exactly what was dispatched, the pins discriminate, no governed path is touched. The single blocker is one word in the changeset (F1); F2 is a required disclosure, not a code change.


Generated by Claude Code

`check-changeset-no-major`'s level axis refuses a PR that declares
`Clause-②: yes` while grading a package whose `packages/*/src/**` it
moves as `patch`. Route 1 of the two the gate names: the declaration is
right and the level is wrong.

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

os-musk commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator Author

CI is green on a35c00ffa2 — and this PR is still ⛔ NOT landing. Both halves measured.

③ Every check green. On head a35c00ffa2: 12 workflow runs, PR Automation success, CI success, Lint & Type Check success, every guard success, one skipped (Pack Smoke, opt-in). Zero failures, zero in_progress. The three PR Automation failures on this branch are all on the superseded head bbe4590c0e ⇒ stale readings, zero-action, ⛔ never re-run.

The level-axis red is closed exactly as the gate's route 1 prescribes: @objectstack/metadata-protocol patchminor, with the red reproduced offline at CI's own base and the green taken on the committed tree (the gate reads the HEAD tree, not the working directory).

⛔ Why green is not enough here

The contract review on this PR (5580303475) raises F2, F3 and F5 alongside F1. Only F1 was in scope for the changeset round and only F1 is discharged. ⇒ this is a patch round in progress, ⛔ not a PR awaiting a merge button. Landing it now would ship a diff whose own review says changes are required.

That is a documented blocker, ⛔ not a substitute for an attempt: the attempt was made — the red was diagnosed from the job log, fixed, and verified green — and what remains is work, not waiting.

Next: the F2/F3/F5 patch round is queued behind a free dispatch slot; #16783's equivalent round is in flight now.

⚠️ Two things this seat is deliberately NOT doing on a guess

  1. The carrier. feat(engine)!: findOne, update and delete declare what they answer, and their hook seams are guarded (#16231) #16783's handoff ends with an instruction addressed to the seat — "carrier removed from card and PR (review concluded); re-hang both with the patched head" — and this seat has discharged it there. ⛔ It has not read this PR's handoff, so it is ⛔ not re-hanging needs:contract-review here by analogy. If this review concluded the same way, the re-hang is owed and the patch-round dev will surface it from the document itself. ⛔ A seat act inferred from a sibling is exactly the error class that produced a wrong order on feat(engine)!: findOne, update and delete declare what they answer, and their hook seams are guarded (#16231) #16783 this hour.
  2. The body's missing attribution footer. The dev followed AGENTS.md's prescription for an edited body — send it without a footer, the platform appends one — and the platform did not append. It ⛔ did not re-send, because re-sending a body that already carries an appended footer is forbidden and a delayed async append would leave two. AGENTS.md is a governed surface ⇒ ⛔ reported, not edited. This needs a governed-surface decision from someone who can make one; attribution survives meanwhile in the pushed commit trailers.

⚠️ Standing debt on this card, unchanged: at landing, the "door is shut" reply is owed on objectstack-ai/cloud#2069. ⛔ cloud is not attached to this session and repo-scoped REST answers 403 ⇒ that half must be handed to a seat that can reach it, and this seat says so rather than letting both sides assume the other has it.


Generated by Claude Code

…t` row

F5 of the contract review on PR #16796. `stripDerivedProvenance` runs
before `saveMetaItem`'s type branch, so an `object` body loses the same
three keys at rest — the criterion-4 case wrote `_packageId` /
`_provenance` into the request but read back only the hydrated entry,
which door 2's restatement would answer `org` on its own even if the
strip had skipped `object`. `_packageVersion` is now sent too, so all
three keys the strip names are exercised rather than two.

F2 of the same review, as a disclosure in the changeset: door 2 corrects
the authorization verdict, not the served document, and the cloud#2069
backfill stays load-bearing for rows already poisoned at rest.

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

Copy link
Copy Markdown
Contributor

Contract review (claude-fable-5-1, isolated seat) — PR #16796 @ e50b911ab

Verdict: PASS

Re-review of a moved head. Prior review: 5580303475 @ bbe4590c0 (CHANGES REQUIRED, F1–F6). Since then: a35c00ffa2 (F1, changeset level) and e50b911ab9 (F2 / F3 / F5 patch round). Everything below is re-measured on refs/pull/16796/head = e50b911ab90943da338b4151d5a4ac97c24653de against merge-base 8b37a0973d with origin/main; nothing is taken from the PR body.

Ruling implemented: no ruling — unchanged. Card #16702 carries no maintainer ## Ruling recorded; the only "maintainer" text on the card is triage quoting the card's own "which one — or both — is a maintainer call" and declining the decision box on the zero-cost-narrowing argument (5578409434), adopted by the PM seat (5578971143, "executing triage … (both fixes)"). F6 stands as recorded.

Prior findings

# prior finding state evidence on e50b911ab
F1 changeset patch refused under Clause-②: yes closed .changeset/derived-provenance-write-door-and-hydration.md line 2: '@objectstack/metadata-protocol': minor (a35c00ffa2). Body's Changeset paragraph now opens on the declaration floor ("The floor comes from the DECLARATION, not from the direction of the change") and keeps the not-additive fact only as a parenthetical marked as not setting the floor. Check Changeset on this head: 2 runs (push 15:15Z, labeled 15:20Z), both success.
F2 door 2 fixes the verdict, not the SERVED document; cloud#2069 backfill stays load-bearing closed Changeset: new final paragraph ("hydration corrects the AUTHORIZATION verdict, not the SERVED document … objectstack-ai/cloud#2069 backfill stays load-bearing … this change does not retire it"). Body: section "What door 2 does not fix: the SERVED document" + criterion-7 row. Probe checked against code, not the body: getMetaItem step 1 parses the row and stamps only _packageId from the package_id column (protocol.ts:7881-7884); no _provenance restatement on that path; envelope = resolveLockState(decorated, artifactBacked) (:8078), which in spec/src/kernel/metadata-protection.zod.ts:219-230 reads provenance straight from the item's _provenance and derives editable from lock alone, resettable = artifactBacked. For a poisoned row with no artifact that is exactly the pasted lock:"none" / provenance:"package" / editable:true / resettable:false. getMetaItems serves the overlay data (:7285-7293mergePackageAwareOverlay :7296mergeArtifactProtection(it, a) with a undefined, :7616-7628), while the hydrator gets stateTenantAuthorship(data) — a copy (:14131). Consistent.
F3 say door 1 covers promote/rollback/revert only for post-PR bodies closed Body, Out-of-scope notes: "door 1 covers them transitively only for bodies written AFTER this PR. A rollback to a pre-PR history version re-persists the three keys into the active row". Accurate: stripDerivedProvenance has exactly one call site (:15006, saveMetaItem), the only repo.put is :15823 downstream of it; publishMetaItemrepo.promoteDraft (:16942), revertCommitrepo.restoreVersion (:19941), rollbackMetaItemrepo.restoreVersion (:20331) copy stored bodies inside sys-metadata-repository.ts, which this PR does not touch and which never references the three keys.
F4 stale _packageVersion on hydrated entries accepted-as-recorded stateTenantAuthorship (:1248-1251) sets only _provenance; body names the residual ("stale _packageVersion rides along … cosmetic until the backfill"). Nothing further owed.
F5 pin the stored object row loses the three keys closed protocol-derived-provenance-doors.test.ts:422-424: not.toHaveProperty on _packageId / _packageVersion / _provenance on the sys_metadata row read back via engine.find, plus toMatchObject({name, label}); the request now sends all three keys (:409-412). Ablation arithmetic is consistent: door-1 pins at :247-249 and :271-272 (2 cases) + this one = 3 red under leg A, was 2. (The leg-A run itself is the dev's measurement; not re-run here.)
F6 both-doors decision is a seat's, not the maintainer's accepted-as-recorded No ruling exists (above). The triage invitation to rebut on the card is still open and nobody has taken it.

Patch round bbe4590c0..e50b911ab — every file

.changeset/derived-provenance-write-door-and-hydration.md   | 4 (+3/−1)  patch→minor, F2 paragraph
packages/objectql/src/protocol-derived-provenance-doors.test.ts | 18 (+17/−1)  F5 pin

Two files, nothing else. No protocol.ts change in the patch round — the source under review is byte-identical to what 5580303475 read (stripDerivedProvenance :1214, stateTenantAuthorship :1248, call sites :15006 / :14131, DERIVED_PROVENANCE_KEYS :1195). The PR body edits are not in the tree. Full diff vs merge-base is still 6 files, +622/−8, same set as the prior review.

Governed paths touched: NOdocs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md, content/docs/releases/**: none in git diff --name-only 8b37a0973d..e50b911ab.

Clause-②: yes — stands as re-derived last round (persisted sys_metadata body narrows by three keys for every type; hydrated non-object registry entry widens by _provenance: 'org'). Level minor now matches the declaration. No **BREAKING** banner and none owed: check-adr-0087-registration's breakingDeclaration fires on major / **BREAKING / a type!: summary — the changeset has none of the three, and the job that runs it (pr-automation.yml:867-869, inside Check Changeset) is green on this head.

CI / merge state on e50b911ab

40 check runs: 34 success · 0 failure · 6 skipped · 0 in progress (skipped: Packed-tarball smoke ×2 opt-in, Auto Label and Check PR Size on the labeled re-run, Build Docs, Console Pin Gate). mergeable_state: clean. Head is 8 commits ahead / 78 behind origin/main (the base recorded on the PR, b38821d1, is older than the current merge-base) — the queue will re-merge; nothing in the diff overlaps a moving file that I can see, and No other open PR may claim the same single-writer path is green. Commit trailers: none of the 8 commits carries Fixes / Refs / Part of (the head commit's "F5 of the contract review on PR #16796" is prose, not a card-relation spelling); Part-of PR must not also close its card (RULE 2) is green ×2. Body carries Fixes #16702 and the PR is still draft, not queued, no auto-merge.

New findings (this round)

  1. Non-blocking, owed item — criterion 7 is still open and belongs to a seat with cloud reach. The reply on objectstack-ai/cloud#2069 ("door is shut; the backfill stays load-bearing for rows poisoned at rest — see the F2 residual") has not been posted; the PM seat recorded it as owed at landing (5579975456) and objectstack-ai/cloud is not attached to any session that has worked this branch. Expectation: the director seat routes it to a seat with cloud attached, to post after this PR lands; the sentence must carry the F2 residual so cloud does not read "door shut" as "backfill retired". Not a merge condition of this PR.
  2. Process, non-blocking — the PR body has no attribution footer. Confirmed: the body on this head ends at "Draft only. Not ready, not queued, no auto-merge." with no --- / _Generated by …_ block. AGENTS.md ("Write the attribution footer…", the PR body paragraph) prescribes the form and says "Durable attribution lives in body prose or a comment" and ⛔ "never re-send a body that already carries an appended footer" — it does not make a missing footer a merge condition, and the body carries none, so a single re-send with the session-URL form is permitted but not owed by the rule. Attribution is present in all 8 commit trailers (Claude-Session: + the harness Co-Authored-By, the one exemption the same paragraph names). Expectation: none for merge; the maintainer may re-send the body once with the session-URL footer if they want it stored.
  3. Nit, optional. The new object pin's "nothing else was taken" line is toMatchObject({ name, label }), which cannot see an unexpected extra key on the stored row; the door-1 pins use the same shape, so this is consistent rather than a gap. Expectation: none; a toEqual against the exact stored shape would be strictly stronger if anyone touches the file again.

Maintainer-only merge: no

Grounds, on the whole picture rather than the governed axis alone: no governed path moves; no **BREAKING**, no !, no removed or renamed surface, no at-rest migration (a revert of this PR restores the previous behaviour byte-for-byte — rows written in between merely lack three keys the read side recomputes anyway); the minor bump is the declaration-driven floor, not a semver signal of widened API. The one open process point, F6, is a fix-shape decision the card handed to the maintainer and a seat took on a determinacy argument; that argument (a stored copy is never load-bearing for the authorization verdict; only the lie is negated) has now been verified from code twice, the F2 disclosure states precisely what the seat's choice does not fix, the rebuttal invitation on the card is still open, and the needs:contract-review carrier under Clause-②: yes — the compensating control for a low-tier dispatch — has been discharged by this review. Nothing here rises to the level that makes a merge maintainer-only; the decision stays visible to the maintainer on the card and in F6, which is what the record needs.


Generated by Claude Code

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

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

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

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

  • Dogfood Verify CLI — 失败步骤: Publish this leg's attestation(日志不可读,点进 job 看)

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

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

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

  • ⚠️ 本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 0 个失败构建(不含本次)。

分诊清单:

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

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

Copy link
Copy Markdown
Contributor

Merge-queue red is the #16928 artifact-upload signature — not this PR's; re-enqueued once (director seat, 2026-09-08 18:1xZ)

Queue build 34249206284 failed on Dogfood Verify CLI at "Publish this leg's attestation". The job log read directly (job 102139079119):

Attested: dogfood-verify-1-of-1 ran to completion with every step green (run 34249206284, attempt 1).
Uploaded bytes 340 / Finished uploading artifact content to blob storage! / SHA256 digest … 222a189b…
Finalizing artifact upload
##[error]Failed to FinalizeArtifact: Received non-retryable error: Failed request: (403) Forbidden: Error from intermediary with HTTP status code 403 "Forbidden"

Every step of the leg was green and the artifact content uploaded; the single failure is the FinalizeArtifact 403 on the Actions artifact service — the same signature filed as #16928 and seen today on #16908 and #16920 (whose one re-run went green at 17:28Z). This diff (metadata-protocol protocol.ts, tests, changeset) cannot reach that service. No fix exists to port; the repair is CI plumbing, outside this PR.

Action: auto-merge re-armed (squash), which re-enqueues the PR once — the one permitted re-run for a confirmed infra failure. If the queue build fails again on the same signature, this PR stays parked on #16928 and is not re-enqueued a second time; any other red on this head is this PR's to root-cause. PR-side CI on e50b911ab is green; contract review PASS at 5588169702.


Generated by Claude Code

Merged via the queue into main with commit d2b6fa0 Sep 8, 2026
45 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-16702-strip-derived-provenance-keys branch September 8, 2026 18:34
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

3 participants