Skip to content

feat(core): one by-name read of the security catalog (ADR-0131 C2, stage S1) - #22091

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-15196-s1-catalog-seam
Oct 7, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-15196-s1-catalog-seam

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #15196
Clause-②: yes (widening)

The new public surface of @objectstack/core. One value export, createSecurityCatalogReader(sources), re-exported from the root barrel through packages/core/src/security/index.ts. It takes { registry, metadata } (type SecurityCatalogSources) and returns a SecurityCatalogReader with two members. resolve(type, name) returns a promise of the SecurityCatalogEntry that the name resolves to, or undefined when no reader holds it. list(type) returns a promise of an array of SecurityCatalogEntry, one per name, each equal to what resolve answers for that name. type is SecurityCatalogType, the union of 'position', 'permission' and 'capability'. Any other value rejects with a TypeError. An empty or non-string name resolves to undefined. An entry is { type, name, definition, source, packageId? }. definition is the reader's own definition object, read-only and shared. source is a SecurityCatalogSourceName, 'registry' or 'metadata'. packageId is the definition's _packageId, present when the definition names one. The types exported with it are SecurityCatalogType, SecurityCatalogSourceName, SecurityCatalogRegistry, SecurityCatalogMetadataService, SecurityCatalogSources, SecurityCatalogEntry and SecurityCatalogReader. SecurityCatalogRegistry needs getItem(type, name), listItems(type) and isPackageDisabled(packageId); ObjectQL's SchemaRegistry satisfies it as it stands. SecurityCatalogMetadataService needs get(type, name) and list(type), plus the optional getDiagnosed and listDiagnosed; the MetadataManager satisfies it as it stands. Construction throws a TypeError when either source lacks a member the read calls. The read rejects with AuthzStoreUnavailableError (SERVICE_UNAVAILABLE, 503) when a reader throws, when a metadata by-name read lost a loader and found nothing, or when a metadata list lost a loader at all. Nothing calls the reader yet, so no grant changes.

What this stage is

Stage S1 of the C2 stage plan (ADR-0131 D2–D4): one seam that later stages switch readers onto. No consumer is switched. Files:

  • packages/core/src/security/security-catalog.ts (new): the read.
  • packages/core/src/security/index.ts: the export, one block.
  • Tests only, beyond that: packages/core/src/security/security-catalog.test.ts (new), a new describe block at the end of packages/objectql/src/protocol-boot-hydration-scoped.test.ts, and packages/qa/dogfood/test/security-catalog-showcase.dogfood.test.ts (new).
  • .changeset/15196-core-security-catalog-read.md: minor for @objectstack/core.

The reader per type, measured before choosing

Re-measured on objectstack main at 3d9188502e (the census was taken at e67ba80049; the readings are unchanged). Showcase was booted four ways: single, single with the Default Organization, walled with the real organizations package declared by a temporary host root, and walled posture-only. Names per reader, identical in all four boots:

type engine registry (SchemaRegistry.listItems) metadata service (list) metadata door (GET /api/v1/meta/:type) union of the first two equals the door
position 0 10 10 yes
permission 17 9 (a subset) 17 yes
capability 2 2 (the same names) 2 yes

Neither in-process reader holds the whole catalog. The engine registry has no stack-declared position, because the engine's stack-collection list does not decompose positions. The metadata service lacks the eight platform permission sets that plugin-security ships on its own manifest (admin_full_access among them). The door is a serving surface: it reads stored rows on each request and decorates what it serves. So the read is the union of the two in-process readers, in one order: the engine registry first, then the metadata service for the names the registry does not hold. The reader that answers on showcase, per type:

  • permission: the engine registry (17 of 17).
  • position: the metadata service (10 of 10).
  • capability: the engine registry, which answers first. Both hold the same 2.

For the 11 names both readers hold (9 permission sets and 2 capabilities), the definition bodies are identical across the registry, the metadata service and the door's by-name read. That was measured by comparing every non-underscore key. So the order changes no body on showcase.

What the read does and does not answer

  • It does not answer whether an item is in effect. Under the standing ruling (Ledger convergence: registration home + one store implementation + permission-set convergence (ADR-0126 §4/§8, maintainer-ruled bundle) #12159 Part 3), the row active flag stays authoritative, and no definition carries it. An entry says that a definition exists under a name, never that it grants. A later stage that reads definitions keeps reading the row flag with isRowActive. The module doc says so in those words.
  • It does not answer the position → permission-set binding, which stays on the junction until C3, or organization scope, because the catalog is environment-level under ADR-0131 D3.
  • A name two packages ship gets today's answer, which is the registry's by-name precedence. With no stored override, the read returns the first-registered package's body. Once one package stores an override bound to itself and boot hydrates it, the read returns that override for every caller. The metadata door's by-name read returns the same body in both cases. Q4 is with the maintainer, so this is pinned as today's answer and not as a policy.
  • A definition owned by a disabled package answers neither member. The registry's listItems already hides such items, and the by-name read applies the same rule, so the two members cannot disagree. This is the one rule the read adds on top of the readers' own answers; see the first Acceptance note.

Pins, each ablated and restored by blob

Every ablation went through scripts/ablation-replace.mjs in WRAP mode, so its anchor hit exactly once and the blob changed. The restore was proven with "blob == HEAD and git diff HEAD is empty". The dist-resolved legs also rebuilt the package, proved the marker with scripts/ablation-dist-preflight.mjs (marker present in 2 built files), and proved it gone after restore (marker absent from all … built files, working tree clean against HEAD).

  • P1.1, security-catalog-showcase.dogfood.test.ts (31 tests, green). For each type in each of the three postures (single, single with the Default Organization, walled posture-only), the read lists exactly the declared names. The expected names come from the producers, never from a registry: the showcase stack's positions, permissions and capabilities, plus securityDefaultPermissionSets. The read also lists exactly the door's names, and every listed name resolves to the entry the list gave.
    • Ablation A1, dropping the platform bootstrap-set source: plugin-security's manifest permissions: entry was replaced with an empty array, and plugin-security was rebuilt. Red in all three postures: permission: lists exactly the declared names, with admin_full_access and organization_admin missing (expected 17, received 15). The door assertion stayed green because the door lost the same names, which is why the expected sets are derived from the producers.
    • Ablation A2, the read drops its engine-registry source: the read's registry by-name call was made unreachable, and core was rebuilt. Red in all three postures, on both the declared half and the door half (received the metadata service's 9). Positions and capabilities stayed green, as they should.
  • P1.2, the new block in protocol-boot-hydration-scoped.test.ts (5 tests, green). It uses the real SchemaRegistry, the real loadMetaFromDb hydration and the file's existing pinned engine double, for both permission and position. With no override, the read returns the first-registered package's body. With an override bound to package B, it returns B's override and packageId B. The door's getMetaItem is asserted beside each case. A position name that two stacks declare shares one metadata-service slot, and the later registration holds it.
    • Ablation, swapping the override's package: the stored override was bound to package A. Red: the 2 override cases (permission, position).
  • Unit pins, security-catalog.test.ts (14 tests, green), each ablated in the core source:
    • U1: metadata service first. Red: a name both readers hold is answered by the engine registry and list gives one entry per name ….
    • U2: disabled rule dropped. Red: both disabled-package tests.
    • U3: a registry throw read as a miss. Red: a registry read that throws.
    • U4: a metadata throw read as a miss. Red: a metadata read that throws.
    • U5: a degraded miss read as a miss. Red: a metadata read that lost a loader and found nothing.
    • U6: construction no longer refuses a missing metadata source. Red: construction refuses a missing reader ….
    • The failure pins assert the envelope (code SERVICE_UNAVAILABLE, status 503, and the brand), never a bare throw.

The ablations ran at 0482468f8e. The two later commits add the changeset and turn one fixture argument into JSON text. Neither touches an ablated line or assertion.

Verification, at head b70dd032be

  • pnpm --filter @objectstack/core test: Test Files 78 passed (78), Tests 2192 passed (2192). This ran at 46fe89abad; the last commit changed only an objectql test file.
  • pnpm --filter @objectstack/core typecheck: exit 0, test layer OK, debt unchanged. The new test file is in the tsconfig.test.json program (checked with --listFiles).
  • pnpm --filter @objectstack/objectql typecheck: exit 0, test layer OK.
  • pnpm --filter @objectstack/dogfood typecheck: exit 0.
  • The P1.2 file: Tests 8 passed (8). The P1.1 file (--project isolated): Tests 31 passed (31).
  • Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack was re-derived on this change: 70 families, plus the 2 the dispatch named that the re-derivation does not (check:authz-resolver, check:dispatcher-error-vocabulary). All 72 were run with the exit code captured before any pipe, and all exit 0. check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET: 7 unbuilt packages); after building them it exited 0, with 106 published require entry point(s) across 66 package(s) load. --ran reports 70 derived famil(ies) accounted for — 70 run, 0 NOT-MEASURED. Some verdict lines: check-engine-double-contract: OK — 980 pinned, check-nul-bytes: OK, check-test-source-alias OK, check:authz-resolver: single shared authorization resolver intact, check-adr-0087-registration: … 1 non-breaking changeset(s) seen, This diff introduces no major bump.
  • ESLint, narrowed and measured: eslint --no-inline-config --format json on the 5 changed .ts files reports 5 files, 0 errors, 0 warnings, and every file matches the config. eslint.config.mjs enables no type-aware linting (no parserOptions.project). The only files it reads are two baselines this diff does not touch, so no untouched file's verdict can move. The repository-wide pnpm lint is CI's.
  • Declared to CI: the rest of the dogfood suite, the objectql suite outside the touched file, Build Core and the workspace type-check lanes.

Acceptance notes

  • The disabled-package rule is a decision for the contract review. No security code reads package disable today. The catalog rows a disabled package seeded keep granting through the row read. When S8 switches a reader onto this seam, a disabled package's sets would stop resolving, so S8's grant-equivalence goldens should state that. The alternative was to apply the rule to neither member, which leaves list and resolve disagreeing about whether a name exists, because the registry's listItems applies the rule unconditionally.
  • Stack-declared positions carry no _packageId in the metadata service, so the disabled-package rule cannot reach them. This is the same root as the positions being absent from the engine registry.
  • A waiver reason has drifted (observation, not filed). scripts/check-stack-collection-maps.mjs's waiver for positions says positions "reach the registry through the security bootstrap". Measured, they reach the metadata service and the catalog rows, never the engine registry.
  • The census's carrier, "the declared-positions seeder's registry-first / list fallback is an either-or trap", is not removed by this stage. No consumer is switched, and the seam is the place a later stage moves that seeder onto.
  • No ADR anchor file was added under scripts/adr-anchors/, because the stage's surface is the module and its export. ADR-0131 is cited in the module.
  • The walled arm of P1.1 boots posture-only. The real organizations package resolves only from a host root that declares it, and this suite's root does not. The one-off reading with a declared host root gave the same sets.

Generated by Claude Code

claude added 5 commits October 7, 2026 13:59
…registry and the metadata service

ADR-0131 D2-D4: positions, permission sets and capabilities resolve by name
from the environment registry. No single in-process reader holds the whole
catalog (the engine registry has no stack-declared position; the metadata
service lacks the platform permission sets), so the read is their union in
one order, registry first. No consumer is switched.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
… ship

Today's answer, until shared catalog names are ruled: the first-registered
package's body with no stored override; the override one package stored for
itself, for every caller, once hydrated. The metadata door's by-name read is
asserted beside each answer. A position name two stacks declare shares one
metadata-service slot; the later registration holds it.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
…ures

Per type, the read lists exactly the declared names (the showcase stack's
positions, permissions and capabilities, plus the platform's bootstrap
permission sets) and exactly the names the metadata door lists, in single,
single with the Default Organization, and walled postures; every listed name
resolves by name to the entry the list gave.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
The row's metadata column is text; passing the object added a type error the
test-typecheck ledger does not record.

Claude-Session: https://claude.ai/code/session_01WMQprn46CND82KmY8sZWBu
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

30 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json bafb58bb08aa3989b1d72a633496b361fcf0842e.

⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

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

Coarse fallback — 27 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 bafb58bb08aa3989b1d72a633496b361fcf0842e → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json bafb58bb08aa3989b1d72a633496b361fcf0842e

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b70dd032be84fbfcdcf4af58c5dbe485408a3152
Local-runs: none

① Derived judgments

Inputs: card #15196 (body and all 17 comments), PR #22091 (body, the 6-file list, the net diff against main at 3d9188502e), and the 33 check-runs on the head. Read-only: no worktree, no build, no test, no gate re-run. Classes, positions and functions only.

Public surface of @objectstack/core (the root barrel re-exports ./security/index.js wholesale): one value export, createSecurityCatalogReader, and seven type exports (SecurityCatalogType, SecurityCatalogSourceName, SecurityCatalogRegistry, SecurityCatalogMetadataService, SecurityCatalogSources, SecurityCatalogEntry, SecurityCatalogReader), exactly what the PR body and the changeset name. Right. Nothing is removed, renamed or narrowed. core gains no package dependency, because both sources are structural interfaces; objectql already depends on core, so the objectql test's import of @objectstack/core follows an existing edge, and dogfood holds core as a devDependency.

Accept-set of the new read, each judged on the diff:

  • a type outside position | permission | capability is refused with a TypeError from both members; the assertion sits inside the async member, so it surfaces as a rejection, which is what the body says. Right.
  • an empty or non-string name resolves to undefined and asks no reader. Right.
  • construction with a source lacking getItem / listItems / isPackageDisabled, or get / list, throws a TypeError. Right. SchemaRegistry (registry.ts: getItem, listItems, isPackageDisabled) and MetadataManager (metadata-manager.ts: get, getDiagnosed, list, listDiagnosed) satisfy the interfaces structurally, which the green workspace type-check on the objectql pin (a real SchemaRegistry and a real MetadataManager passed in) confirms.
  • read order registry-then-metadata, with getItem called context-free, so the registry's own by-name precedence applies (bare-slot overlay, then first composite match). Right for S1. ADR-0131 D4 says resolution reads the registry, one source, both provenances; the card's census measured that no single in-process object holds the whole catalog today (engine registry 0 / 17 / 2, metadata service 10 / 9 / 2, door 10 / 17 / 2 for position / permission / capability), so a fixed-order union is the only in-process read that answers the full catalog, and the module doc states the order and forbids metadataService.list alone. One deviation from the claim amendment 6039096736, which named "the engine registry and the metadata door" as the sources: the built seam reads the metadata SERVICE, not the door, and says why (the door is a serving surface). The dogfood pin asserts that the listed names equal the door's names per type in three postures, so the substitution is measured rather than assumed. Right.
  • list returns one entry per name, each equal to what resolve answers: list re-asks getItem per name and takes the metadata service's first body per name; MetadataManager.readListUncached admits in-memory items before loader items and get reads in-memory first, so the two reads share one precedence. Right.
  • loud failure: a throwing reader, a degraded by-name miss, or a degraded list raises AuthzStoreUnavailableError (SERVICE_UNAVAILABLE, 503). This matches getDiagnosed (degraded only when nothing answered) and listDiagnosed (degraded when any loader was lost) exactly. The error's object field carries security catalog: source (type) rather than a table name, which the message's "failed read" slot tolerates. Right, and fail-closed.
  • the entry shape { type, name, definition, source, packageId? } carries no activation member. Right: the row active flag stays authoritative under the standing ruling the seat's Q2 answer cites (6039027082), and the unit pin asserts the key set.
  • the disabled-package rule on the by-name read: servable hides a definition whose _packageId names a disabled package from BOTH members, where registry.getItem today hides nothing and registry.listItems hides it from the list only. Right as a contract (one answer to "does this name exist"; no consumer is switched, so no grant moves now), with one derived corner the pins do not cover, carried to ③.

No consumer switched, verified on the diff: the only non-test source lines are the new module and one export block. The pinned sibling checkout is unaffected, since the change is an additive export.

② Semver level

.changeset/15196-core-security-catalog-read.md declares "@objectstack/core": minor. The diff publishes one new value export and seven types from a released package and removes nothing, so minor is both the floor and the ceiling: no BREAKING banner and no ADR-0087 disposition are owed, and skip-changeset would be wrong. Clause-②: yes (widening) on PR body line 2 and again in the changeset body is right: the accept-set of the package's public API widens, yes takes at least minor, and (widening) is the arm that matches. objectql and dogfood receive tests only, so they owe no changeset. Check Changeset: success; check-adr-0087-registration saw one non-breaking changeset.

③ Boundary flags

Dev flags (the PR's Acceptance notes and the os-dev-report 6040687107), each answered:

  1. Open question: a disabled package's definition answers neither member (A) or is hidden from neither (B). Answered A, as built and as the seat read it (6040744949). Carrier it adds: with this rule and the registry's first-composite-match combined, a name shipped by a disabled first-registered package and an enabled later-registered package resolves to nothing in both members, whereas registry.listItems lists the enabled body and registry.getItem answers the disabled one. That is consistent inside the seam, and it is not today's answer either; it belongs to Q4 (6039049826, with the maintainer) and to S8's grant-equivalence goldens, which must state the disabled-package narrowing. Not a defect of S1.
  2. Stack-declared positions carry no _packageId, so the disabled rule and S10's ownership check cannot reach them. Answered: observation accepted; carrier S10, as the seat recorded.
  3. Waiver prose drift in scripts/check-stack-collection-maps.mjs for positions. Answered: an acceptance note is the right place under Prime Directive 10 (neither a reproducible defect nor a contract violation); this PR owes no filing.
  4. The declared-positions seeder's registry-first / list fallback survives S1. Answered: correct, since S1 switches no consumer; carrier S2 or S8b, as recorded.
  5. No scripts/adr-anchors/ file. Answered: accepted for S1. The module names ADR-0131 D2 to D4 in its doc and the export block names it in the barrel, so the decision is findable; the anchor becomes owed by the stage that switches resolve-authz-context.ts onto the seam (S8a), where the spot becomes load-bearing.
  6. The walled arm of P1.1 boots posture-only. Answered: acceptable. The read is environment-level (ADR-0131 D3) and takes no organization, so the wall is not its subject, and the one-off reading with the real organizations package declared gave the same sets.
  7. P1.2 appended to protocol-boot-hydration-scoped.test.ts rather than a new file. Answered: acceptable; it reuses that file's pinned engine double (check-engine-double-contract green, 980 pinned) and sits beside the loadMetaFromDb boot hydration keeps a third inline copy of the overlay→registry rule with an UNSCOPED artifact lookup (ADR-0048 gap) #4624 cases the carrier note 6034517879 names.
  8. Ablations ran at an earlier commit. Answered: accepted on the head's check-runs. The two later commits are the changeset and a fixture argument turned into JSON text, both visible in the diff, and every test lane is green on the head.

Card-level open questions: Q3 (the walled half) and Q4 remain with the maintainer (6039041675, 6039049826). S1 needs neither; its shared-name pin is explicitly today's answer until ruled, so a Q4 ruling lands as a test change, never as a silent refactor. Escalated: nothing new.

Gate verdicts on the head: 30 check-runs success, 3 skipped by path filter (Build Docs, Console Pin Gate, Packed-tarball smoke), 0 failures. Governed Surface Queue Guard: success, and the file list touches no governed surface. One roster observation for the governing seat, not a verdict input: the required-context name TypeScript Type Check that AGENTS.md lists does not appear on this head's check-run roster; the four Type Check · runs do, all green.

Implemented-by: claude/issue-15196-s1-catalog-seam
Reviewed-by: session_01WMQprn46CND82KmY8sZWBu

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 7, 2026 15:45
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 7, 2026 15:45
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Regen-provenance: 6041388058 · b70dd032be84fbfcdcf4af58c5dbe485408a3152 → e05d4692103c4d8276a5aecafb01e188158d6f5c · git diff --name-only → main's carry-over only (#22086's two workflow files, and #22089's changeset and four packages/cli files)

Seat domain:services#2 (#21118) · session_01WMQprn46CND82KmY8sZWBu · 2026-10-07T16:16Z. The head moved by a merge-forward through update-branch; the PR's own files were not touched.

Why the merge: the first CI and Lint & Type Check runs on b70dd032be concluded failure with every job green. The aggregate contexts TypeScript Type Check and Dogfood Regression Gate were never created. rerun-failed-jobs answered 403 ("this workflow run cannot be retried"), and main had since changed those workflows (#22086).

What the merge carries: the PR's own delta against merge-base origin/main is unchanged, the same six files. So the contract-review record 6041388058 carries to this head under the carry-over arm.


Generated by Claude Code

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

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

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

分类:failure —— 按下面的日志分诊。

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

  • (没拿到 job 级信息,点上面的 run 链接看)

↳ 失败原因 是判读的关键:超时(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 的同类评论。

历史信号:

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

分诊清单:

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

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

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

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants