Repository navigation
feat(objectql)!: positions, permission sets and capabilities hold one name per deployment — a second holder is refused at registration, naming both - #22197
Conversation
…use a second holder of a position, permission set or capability name The package door (SchemaRegistry.installPackage, ahead of every mutation) refuses a package whose declared positions, permission sets or capabilities name something an installed package, the environment catalog or a built-in already holds; the item seam (registerItem with a package id) refuses the same for direct package-bound registrations. ADR-0112 envelope: the namespace gate's code, NAMESPACE_CONFLICT, status 422; the message names both holders. Same-package reload stays allowed; no collisionPolicy downgrade; bare-slot (environment) registrations are not judged. Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
…red-name catalog pin to the refusal Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
…e one-holder refusal Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
…ckage door; changeset; ADR anchor Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
…curity-catalog-one-holder
Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
…curity-catalog-one-holder
📓 Docs Drift CheckThis PR changes 2 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 35 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin dc59f1371fb60bdc9fa1e32742c515543a0195df && git checkout dc59f1371fb60bdc9fa1e32742c515543a0195df
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fbcbcf124f2aa49259b837bc74cdbf4d08b46b3c f16fcd0c7c1c808a1397887fc4334ce804326aa8 && git checkout -B drift-repro fbcbcf124f2aa49259b837bc74cdbf4d08b46b3c && git merge --no-ff f16fcd0c7c1c808a1397887fc4334ce804326aa8
node scripts/docs-audit/affected-docs.mjs --json fbcbcf124f2aa49259b837bc74cdbf4d08b46b3c
|
…s set is not also handed to plugin-security The dogfood fixtures declared one permission set under two packages: the app's own `permissions`, and the same set handed to SecurityPlugin's `defaultPermissionSets`, which plugin-security declares on its own manifest. Under one-name-one-holder that boot is refused (NAMESPACE_CONFLICT, both holders named). `os serve` never composes it: it hands the plugin the default's NAME only (`appSecurityPluginOptions`), and the app registers the set. The fixtures now wire it the same way. A runtime pin holds the refused composition. Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs, and nothing else: card #22135 (body and all six comments: triage grade 6051220848 and its Clause-② amendment 6052924911, claim 6051656254 and its amendment 6053548364, os-dev-reports 6053492929 and 6054287813); PR #22197 (body, the 15-file list, the net diff against its merge base with ① Derived judgmentsCheck-runs on the head: 42, all Accept-set and public-surface changes the diff implies, each judged:
Kept as the ruling keeps them: same-package reload; every other metadata type ( ② Semver level
③ Boundary flagsFrom os-dev-report 6053492929, patch-round report 6054287813 and the PR's acceptance notes:
Implemented-by: VERDICT: PASS |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 37746963251 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Queue kick-out: new signature, not re-queued · 2026-10-08T08:35Z
|
…curity-catalog-one-holder
…uilt-in holder's own registration At the item seam a built-in position or capability name is held by the platform, and plugin-security's registerBuiltinPositions (SecurityPlugin.start) is that holder declaring it. The environment catalog is no longer asked for a built-in name there: an environment item under a built-in name exists only where an environment save went over the platform's name (outside the ruling), and boot hydration (ObjectQLPlugin.start) registers it before the platform declares, so asking it refused the platform's own declaration and the boot. The stored definition keeps answering first from the bare slot (S2b's shadowing). A second PACKAGE registering a built-in name is still refused, in either order; non-built-in names are judged as before. Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
…holder's own Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
…curity-catalog-one-holder
…ngine's collection loop With positions registered by ObjectQL.registerApp's collection loop (now on main), three comments this change added said the loop never registers them. The claims doc and the installPackage comment now say what the claims are for on either tree: installPackage itself registers no items, so the claim is how the package door remembers every declared name. The declared-names reader's note and the runtime pin's header no longer say positions reach no engine slot. Comments only; no behaviour change. Claude-Session: https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Read at 2026-10-08T14:09Z. Inputs, and nothing else: card #22135 (body and all eleven comments: triage 6051220848 and its Clause-② amendment 6052924911, claim 6051656254 and its amendment 6053548364, os-dev-reports 6053492929, 6054287813, 6058641140 and 6061349995, the ACCEPT 6054723926, the kick-out note 6056029522, the landing record 6056178904); the ruling record 6050490870 on #15196 and Q4's option A as the card quotes it; PR #22197 (body, the 15-file list, the net diff from merge base ① Derived judgmentsCheck-runs on the head: 42, all Accept-set and public-surface changes the diff implies, each judged:
Kept as the ruling keeps them: every other metadata type's §3.4 coexistence (pinned on Comment truth on the merged tree. The four comments round 3 rewrote read true: the ② Semver level
③ Boundary flagsFrom os-dev-reports 6058641140 (round 2) and 6061349995 (round 3), the earlier record, and the PR's acceptance notes:
Implemented-by: VERDICT: FAIL What turns this to PASS, in one push: the changeset re-graded as the card graded it ( |
Fixes #22135
Clause-②: no
Executes the maintainer's ruling Q4 = A on #15196 (ruling record 6050490870): positions, permission sets and capabilities each hold one name per deployment. A package registering a name that an installed package, the environment catalog or a built-in already holds is refused, and the error names both holders. ADR-0048 §3.4's coexistence stands for every other metadata type.
The ADR-0048 §3.4 narrowing note was Tier H and rode its own PR, #22198, now on
main. This PR carries nodocs/adr/**file.What changed
packages/objectql/src/security-catalog-namespace.ts(new). The rule in one place: the three types, the built-in names, the holder vocabulary (package/environment/built-in), and the reader of a manifest's declared names. It reads the same sources the engine's registration seams read: the manifest's ownpositions/permissions/capabilitiesand each nestedplugins[]entry's, arrays only. A manifest-stagepermissionsgrant block is never read as permission sets.SchemaRegistry.installPackage— the package door. It refuses ahead of every mutation, beside the namespace gate, so a refused package leaves no record, no namespace ownership and no claim. Every conflict is listed in one refusal. The package's claims are recorded after a successful install and released byuninstallPackage. The claims are what the door reads for names no registered item records, andinstallPackageitself registers no items. ThroughObjectQL.registerApp(every boot and hot-install door), a package's permission sets and capabilities are also registered items under the package, and so are its positions since fix(objectql): register stack-declared positions under their package so the save door refuses overrides #22262 landed onmain. There the claims agree with the items. With the claims ablated, theregisterAppdoors still refuse and the directinstallPackagedoor does not (Patch round 3), so the claims stay.SchemaRegistry.registerItem— the item seam. A package-bound registration of a catalog type over a name another holder holds is refused before anything is stamped or stored. Built-ins are not asked here: the platform registers its own built-in positions at this seam, under its own package id (the S2 stage, now onmain), and that registration is the built-in holder's own. For a built-in name the environment holder is not asked either; see Patch round 2. A registration with no package is the bare slot, which is what everysys_metadatahydration and metadata write-through writes. It is never judged: an environment save over a package-held name is outside the ruling.code: 'NAMESPACE_CONFLICT'(NAMESPACE_CONFLICT_CODE, already exported),status: 422,httpStatus: 422. The condition is the same one, a name in a deployment-wide namespace already taken, and so is the remedy: rename, or uninstall the other holder. The class (SecurityCatalogNameConflictError) stays unexported, as the registry's other refusal classes are. It is not the namespace gate's class, whose message names amanifest.namespaceand offers theOS_METADATA_COLLISION=warndowngrade. Neither is true here, andcollisionPolicy: 'warn'does not downgrade this refusal (pinned).packages/specchange.Where the doors are, measured
The card names three doors. What this PR measured is that all three are reached through ONE:
ObjectQL.registerApp→SchemaRegistry.installPackage, which every package registration hits in the kernel's Phase 1, before anystart().AppPlugin's security registrar (registerInMemory, the'app-plugin'registrar) and the artifact door (MetadataPlugin._registerArtifactBodyCollections, the'artifact-door'registrar) both run in Phase 2.AppPlugin.initregisters every package of its bundle through themanifestservice first, a multi-package artifact package by package.packages/metadata/src/plugin.tsandpackages/runtime/src/app-plugin.tsare unchanged. The runtime pins boot both registrars' real compositions and see the boot refused before either runs.Door table: base vs head
"Base" is the same tree with both gates ablated, at 1604e09 (rows 1, 2 and 6 were also measured on the untouched base 7ef50a4, with the same answers). "Head" is 8ad6385. Boots go through
@objectstack/verify'sbootStack; the artifact rows go throughcreateStandaloneStack.new AppPlugin(stack)): two stacks sharing a position, a permission set and a capability name422 NAMESPACE_CONFLICT, 3 conflicts, second stack vs first stackeveryone/manage_users/admin_full_accessadmin_full_accessanswers the APP's setbuilt-infor the first two. Foradmin_full_accessthe app registers beforeplugin-securityinbootStack, so the platform's registration is the one stopped, naming the appeveryonemanifest.registerover a held namePOST /api/v1/marketplace/install-local, inline manifest200, installed422 PLUGIN_REGISTER_FAILED, the route's own code, with this refusal's message inerror.message; no recordPOST /api/v1/packages400: the strict body refusespositions, the retiredcapabilitiesand a flatpermissionslistenvironmentPUT /api/v1/meta/permission/NAME, with or without?package=) — not covered by the ruling403 NOT_OVERRIDABLE(the packaged permission-set lock)PUT /api/v1/meta/position/NAME) — not covered by the ruling200, and the saved position then answers the by-name read ahead of the package'smain:403 NOT_OVERRIDABLE(see Acceptance notes)Named but not measured:
MetadataPlugin._reloadAndAnnounce). It re-registers into the metadata service withoutregisterApp, so a dev-loop edit giving a package a held name is served until restart. The restart's boot refuses it.install-local's cloud-sourced install. Its existing code tolerates a register failure: it warns, persists the ledger entry, and answers success. The next boot's rehydrate logs the refusal aterrorand skips the package. That is code reading only (it needs a control plane).In-repo collision census (M2)
Instrument. A tsx census over
examples/app-crm,examples/app-showcase,examples/app-todoandexamples/app-multi-package: each config's top level, itspackages[]bodies and its nestedplugins[]. Against those it reads the built-ins:BUILTIN_IDENTITY_NAMES+AUDIENCE_ANCHOR_POSITIONS,PLATFORM_CAPABILITY_NAMES, andplugin-security'ssecurityDefaultPermissionSets.Result at 1604e09: 50 declarations — crm 3 positions / 2 sets; showcase 10 / 9 / 2 capabilities; todo 0; multi-package 0; built-ins 6 positions / 10 capabilities / 8 sets. Names with more than one holder: 0. Same-holder repeats: 0.
The guard, measured with the gate in place at 8ad6385:
crm,showcaseandmulti-packageboot throughbootStack, andsecurity-catalog-showcase.dogfood.test.ts(3 postures) andmulti-package-artifact.dogfood.test.tsare green.app-tododeclares no catalog name. Deployed and marketplace packages are NOT MEASURED.The P1.2 pin, flipped
S1's shared-name pin is the
security catalog read — a name two packages shipdescribe inpackages/objectql/src/protocol-boot-hydration-scoped.test.ts. It added noP1.2label, which is why agit grepmisses it. It now pins the ruled answer at the same seams:core'ssecurity-catalog.test.tspointed at a non-existentsecurity-catalog-shared-name.test.ts. It now names that describe and the new door pins.security-catalog.ts's module doc said the shared-name answer was "pinned until it is ruled", and is rewritten to the ruled answer.engine-capability-provenance.test.tspinned two packages' same-named capabilities coexisting, the exact behaviour the ruling removes. It flips to the refusal.Tests (at 8ad6385)
@objectstack/objectql:registry-security-catalog-namespace.test.ts(new, 28 cases),protocol-boot-hydration-scoped.test.ts,engine-capability-provenance.test.ts,registry-collision-order.test.tsandregistry-artifact-co-ownership.test.ts: 5 files, 64 passed. Full objectql suite before the merges: 382 files, 7553 tests. The one red was the coexistence pin flipped above; it is green after the flip.@objectstack/runtime:standalone-stack-security-catalog-one-holder.test.ts(new, 4) andstandalone-stack-security-registrar.test.ts: 2 files, 6 passed.@objectstack/coresecurity-catalog.test.ts: 14 passed.@objectstack/plugin-securitybuiltin-positions.boot.test.ts+builtin-positions.test.ts(S2's): 18 passed.security-catalog-showcase,multi-package-artifact, plus a local door probe that is not committed: 3 files, 44 passed.objectqldist: runtime 337 files / 5465, plugin-security 172 / 3663, rest 266 / 5120, verify 18 / 133, cloud-connection 41 / 505. All green.typecheckfor objectql, core and runtime (withcheck:test-typecheck): exit 0. No new test-typecheck debt.Ablation
Both gates were ablated together through
scripts/ablation-replace.mjs, which wraps the run and restores on exit:globalThismarker write;Both mutations landed on disk: anchor 1 → 0, blob
b96099a12688→12c018d406ab.objectqlwas rebuilt andablation-dist-preflightfound both markers indist/. The DTS step failed on the now-unused private method, and the JS bundle the suites read was emitted.src): 22 failed / 21 passed of 43. Every refusal pin went red, including both flipped P1.2 cases and the flipped capability pin. The controls stayed green: same-package reload, uninstall releases the name, the environment-registration carve-out, non-catalog coexistence, the grant-block reader, and the platform's own built-in registration.dist): 3 failed / 1 passed. The control stayed green.Restore: the blob is back to
b96099a12688andgit diff HEADis empty. After a rebuild,ablation-dist-preflight --absentis green for both markers (dist and whole tree).Gates
node scripts/pm/dispatch-gates.mjs --commandsderived 78 commands on 8ad6385; all 78 were run, and--ranreconciles 78/78 with exit codes recorded. All 78 exited 0. On the pre-merge tree 053cc2e two needed a prerequisite first:check-engine-split-ratiorefused the shallow clone (deepened withgit fetch --shallow-since=2026-07-03), andcheck:dual-build-cjs-loadsanswered PREREQUISITE NOT MET until eight unrelated packages were built. On 8ad6385 both ran green with the rest. CI's own lanes (Test Core shards, Temporal Conformance, the Dogfood shards, Build Core, the workspace type-check) are declared to CI and are NOT MEASURED here. After the run,origin/mainmoved 6 commits, none of which touches a file in this PR.Acceptance notes
200), and the saved position then won the by-name read; permission sets were protected on the same door by the packaged permission-set lock (403). Since fix(objectql): register stack-declared positions under their package so the save door refuses overrides #22262 landed onmain, a package's positions are registered items under the package, and the same save answers403 NOT_OVERRIDABLE("'position' is not allowOrgOverride in the registry"), measured on f16fcd0 withPUT /api/v1/meta/position/shared_pos?package=w. Outside this ruling either way; this PR changes nothing there.ObjectQL.registerAppin Phase 1, andsys_metadatahydrates in Phase 2 (ObjectQLPlugin.start). A package added to a deployment whose environment catalog already holds one of its names is therefore NOT refused at cold boot: the env row hydrates over it, with the registry's existing collision warning. It is refused on a hot install. From the registry's seat, that arrival is indistinguishable from an environment save over a package-held name, which the ruling leaves out. TheCONTROLcase inregistry-security-catalog-namespace.test.tspins that the bare slot is not judged. A plugin's ownstart()is different: every plugin that depends on the engine starts after that hydration, so a package-bound registration it makes at the item seam DOES meet the environment holder, and is refused (holderenvironment). The exception is a built-in name, which the platform declares there itself (Patch round 2). Agit grepfor literal catalog-typeregisterItemcalls in production source finds one such registration:plugin-security's built-in positions. Raised for a decision in the report.plugin-securitydeclares the platform's sets on its own manifest (configurable throughdefaultPermissionSets), so they are package-held. When an app registers before it, asbootStackcomposes, the platform's registration is the one refused, naming the app. The boot fails either way, and both holders are named.install-localinline import answers its ownPLUGIN_REGISTER_FAILEDfor any register refusal (this one and the namespace gate's alike), soerror.codedoes not carryNAMESPACE_CONFLICTthere. The refusal's text is inerror.message. Not changed here.NAMESPACE_CONFLICTinpackages/spec/src/api/error-code-ledger.zod.tsdescribes the manifest-namespace condition only. The spelling, owner key and face are unchanged, and the provenance gate is green. A one-line comment noting the second condition is a spec-lane follow-up, not made here.packages/runtime/src/standalone-stack-security-catalog-one-holder.test.ts(new test;runtimeisdomain:cli's package);scripts/adr-anchors/packages__objectql__src__security-catalog-namespace.ts.json(new ADR anchor); and, from patch round 1, fivedomain:clidogfood files:packages/qa/dogfood/test/showcase-security.ts,showcase-d7-default-profile.dogfood.test.ts,authored-row-write-scope.dogfood.test.ts,bulk-widener-probe.dogfood.test.tsandowd-public-read-write-write-floor.dogfood.test.ts(each: theSecurityPluginconstruction, with its comment and imports).Patch round 1 — the dogfood fixtures declared one permission set twice
The Dogfood Regression Gate (all 3 shards) was red on 8ad6385. In every failing boot,
plugin-securityregistered a permission set that the app package already held.The fixtures handed an app-declared set to
SecurityPlugin'sdefaultPermissionSets, which plugin-security declares on its own manifest, while the app declared the same set too:showcase_member_default, throughshowcaseAppDefaultSecurity()and the D7 test;wscope_*,probe_widenerandowdw_*, in three fixtures.Measured:
os serve/objectstack devnever composes this. It hands the plugin only the default's NAME (appSecurityPluginOptions), and the app registers the set. A realobjectstack dev --freshboot ofexamples/app-showcasecame up with the gate in place: health 200.Fixed at the producer: each fixture declares the set once, as the app's, and wires the default by name, as the CLI does. A runtime pin holds the refused composition.
All three dogfood shards are green locally on 089b1c8 (74 + 74 + 74 files) and in CI.
Patch round 2 — the platform's built-in positions met an environment row at boot
The merge queue removed this PR (record 6056019838).
plugin-security'sregisterBuiltinPositionswas refused at the item seam: positionorg_admin, incomingcom.objectstack.plugin-security, holderenvironment. That refusal failedSecurityPlugin.start, and with it the boot. S2b's pins went red:builtin-positions.boot.test.ts, "a stored definition under a built-in name" (3 postures), andbootstrap-declared-positions.test.ts, "a stored definition shadowing a built-in name is neither seeded nor restamped".Measured on 19c86b7 (this branch with
mainmerged, before the fix):SecurityPlugindepends on the engine. SoObjectQLPlugin.starthydratessys_metadatainto the bare slot BEFORESecurityPlugin.startdeclares the built-in positions. Through a real door: withOS_METADATA_WRITABLE=position,PUT /api/v1/meta/position/org_adminanswered200, and the cold restart failed ("Plugin com.objectstack.security failed to start", with this refusal). Without that setting the save answers403 NOT_OVERRIDABLE.registerBuiltinPositionsregisters exactly the six static built-in names (BUILTIN_IDENTITY_NAMES+AUDIENCE_ANCHOR_POSITIONS), under the platform's own package id. That is the built-in holder declaring its own names, not a second holder.Fixed at the producer, the item seam in
SchemaRegistry.registerItem: for a built-in name, it no longer asks the environment holder. An environment item under a built-in name exists only because an environment save went over the platform's name, which is outside the ruling. Unchanged:built-in);environment).No same-definition exception, no
collisionPolicychange, and S2b's pins are untouched.registry-security-catalog-namespace.test.tsgained three cases, one per behaviour above (the third is aCONTROL).Reverse verification. The new condition was mutated through
scripts/ablation-replace.mjsto ask the environment holder again (blobc60d9bad21bc→eeca074e4f80).objectqlwas rebuilt, andablation-dist-preflightfound the marker indist/. The queue's signature came back: S2b's 3 boot postures and the bootstrap-declared-positions case went red withSecurityCatalogNameConflictError(org_adminheld by the environment catalog), and so did the new admit case. Restored: blob == HEAD, andgit diff HEADis empty. After a rebuild,ablation-dist-preflight --absentis green.All suites, the three dogfood shards and the 82 derived gates were green at ffa6d51, and so was CI.
Patch round 3 — #22262 landed on
mainfirst#22262 (squash 0b997ea) adds
positionsto the engine'sMETADATA_ARRAY_KEYS, soObjectQL.registerAppnow registers a package's positions under the package. This branch mergedmainat fbcbcf1. The merge touched none of this PR's files, andregistry.ts's logic is unchanged.Comments only. Four comments this PR added said a package's positions never reach the engine registry's item store. Each now reads true on
main: thesecurityCatalogClaimsdoc and theinstallPackagecomment inregistry.ts, the declared-names reader's note insecurity-catalog-namespace.ts, and the header ofstandalone-stack-security-catalog-one-holder.test.ts. No behaviour changed, so no reverse leg was re-run.Measured with #22262 in the tree (f16fcd0, this branch with
mainmerged; throughbootStack; local probes, not committed):com.example.showcaseand 6 undercom.objectstack.plugin-security, and 10 claims. Re-registering the showcase is not refused. A second package declaringcontributoris refused, holdercom.example.showcase.org_adminandeveryone(OS_METADATA_WRITABLE=position), the cold restart boots, and both names resolve to the environment's saved definitions.PUT /api/v1/meta/position/shared_pos?package=wover a package-held position answers403 NOT_OVERRIDABLE.install-localare refused, each naming both holders; the same-package reload is accepted.The claims, ablated. This was measured on a throwaway local merge of #22262's head 7ed88a6, never pushed. All seven files #22262 landed are byte-identical to that head's. The claim recording was replaced by a no-op (
scripts/ablation-replace.mjs),objectqlwas rebuilt, and the marker was proven indist/. The runtime boot pins stayed green (5 of 5): everyregisterAppdoor refuses through the registered items alone. Two objectql pins went red: the P1.2 position case and thecollisionPolicy: 'warn'pin. Both reach the package door through a directinstallPackagecall, which registers no items. So the claims stay. Restored: blob == HEAD; after a rebuild,ablation-dist-preflight --absentis green.Tests at f16fcd0, all under
os-verify-lock:@objectstack/objectql, whole suite: 383 files / 7572 passed.@objectstack/plugin-security, whole suite: 179 files / 3775 passed, 45 skipped.@objectstack/runtime, whole suite: 340 files / 5505 passed, 19 skipped.typecheckforobjectqlandruntime(tsc --noEmit+check:test-typecheck): exit 0.dispatch-gates --commandsderived 82 on f16fcd0. All 82 ran and exited 0, and--ranreconciles 82/82, 0 NOT MEASURED.CI on f16fcd0: 32 checks success, including Dogfood Regression Gate 1/3 to 3/3 and Test Core 1/6 to 6/6. Three were skipped (Build Docs, Console Pin Gate, Packed-tarball smoke).
Generated by Claude Code