Skip to content

feat(spec)!: publish refuses an anonymous flow endpoint whose target flow runs as system, in both directions (ADR-0138 D2b) - #23411

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-23225-d2b-publish-refusal
Oct 14, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-23225-d2b-publish-refusal

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #23225
Clause-②: yes (narrowing)

ADR-0138 card E2, D2b: the card's last leg (slices 1 and 2, D2, landed as 01d503432b and f24574ae0). Publish refuses an authRequired: false endpoint of type: 'flow' whose target flow declares runAs: 'system', in both directions, each refusal naming both items and carrying the record's prescription, with one registered ADR-0087 semantic entry.

The Clause-② line above is the claim's, copied verbatim as the order requires. The measured reading is yes (narrowing), and the changeset says so. The judge has to be callable from two packages outside @objectstack/spec (the metadata layer's package publish and the metadata protocol's flow publish), so it is exported from @objectstack/spec/api: five functions and three interfaces join the published surface (api-surface/api.json +8). Every landed narrowing that also added exports declares yes (narrowing) (for example the 23015, 23072 and 23165 changesets). No new error code is minted. All three packages are graded minor, so either value passes check-changeset-no-major. The seat may amend the line.

Doors, measured on origin/main 6eb6f7d65 (the order's H1 and H2)

item publish door what it holds D2b leg
endpoint (api, code-only: allowRuntimeCreate: false) the stack parse, ObjectStackDefinitionSchema (defineStack, os validate, os build, the artifact load) the stack's apis and flows together both directions as one input, in applyApiEndpointGates
endpoint and flow MetadataManager.publishPackage (POST /api/v1/packages/:id/publish), @objectstack/metadata the package's api and flow items, and the manager's index both directions
flow (allowRuntimeCreate: true) the metadata protocol: saveMetaItem in publish mode, promoteDraftForPublish (the per-item publish and publishPackageDrafts) the flow being published the flow direction: the declared endpoints are read from the engine registry and the metadata service
  • H1 partly falsified. endpoint-publish-gate.ts's entry is validateApiEndpointDeclarations (:155) with policyGate at :499, as stated. But other packages do import the gate: @objectstack/metadata calls validateApiEndpointDeclarations in gateApiItemsForPublish (the package publish) and identityFreeEndpointGateFailure in the endpoint matcher's load-time backstop. The package publish is the second endpoint publish door, so D2b's endpoint direction lands there too. The order's clause for a measured producer elsewhere covers it; packages/metadata was not on the claim's file surface.
  • H2 falsified for @objectstack/service-automation. It judges no publish. It registers credential and mutation hooks on the protocol and arms flows after they are published (registerFlow, the mutation sync). A stored flow's publish is judged in metadata-protocol (saveMetaItem, promoteDraftForPublish). So this PR changes nothing in service-automation, and its suite is run as a consumer reading.
  • H3 holds. Existing envelopes only: at the parse, a zod custom issue at apis[i].authRequired (defineStack throws STACK_SCHEMA_INVALID / 422); at the package publish, the existing validationErrors result (success: false); at the protocol, INVALID_METADATA / 422 with issues[0].path: 'runAs', the envelope of that door's other publish-time contract refusals.
  • H4: the runAs description misled. It prescribed system for user-less triggers, api included, which is the path the record names. It now names the exception (an anonymous flow endpoint) and what serves that caller. This is a text change; content/docs/references/** is regenerated.

What the refusal reads

One pure judge, anonymousElevatedFlowPairs in endpoint-publish-gate.ts's new section (f), beside policyGate. It is not part of the per-endpoint firstFailure chain, because no single declaration carries both halves. It pairs an endpoint with type: 'flow', authRequired: false (exactly; an absent key is the schema default, true) and a non-empty target with a flow of that name declaring runAs: 'system'. Two refusal sentences word it per direction: anonymousEndpointElevatedFlowMessage when the endpoint is being published, elevatedFlowAnonymousEndpointsMessage when the flow is. Both carry the record's prescription: an authenticated endpoint for a partner that can hold a credential, the flow's signed inbound hook (ADR-0041) for a partner webhook, or a public form for anonymous intake. runAs: 'automation' is not prescribed until ADR-0073 M2 adds the value.

Not judged, on purpose: a draft save (a draft does not execute; its publish is judged); a migrate-stored re-save of an existing row (refusing it would leave a credential the move takes out of the row in cleartext); and, by the record's own words, the indirect path where a guest-written row fires a record-change flow.

Where it composes with #23375 (eeb95f7ff, merged into this branch): the D2b leg runs inside applyApiEndpointGates, before the per-endpoint gates. The new .superRefine(applyPermissionSetOwnerBindingGate) is chained after it, unchanged and unreordered. The protocol edits are in the save and promotion blocks beside the default-record-page refusal. permissionSetOwnerBindingRefusal, assertLockAllowsDelete and restoreArtifactRegistryView are untouched. #23375 covers none of D2b.

Tests (at 1bd1ba175, the merged tree)

  • Spec, src/api/anonymous-elevated-flow-gate.test.ts, 14 cases:
    • each direction is the edit that completes the pair from an accepted stack, asserted as code: 'custom' at ['apis', 0, 'authRequired'];
    • defineStack throws STACK_SCHEMA_INVALID / 422;
    • the controls: an authenticated endpoint on a system flow, and an anonymous endpoint on a user flow or a default-runAs flow;
    • the judge's edges and the prescription in both sentences.
  • Metadata, src/publish-anonymous-elevated-flow-gate.test.ts, 8 cases:
    • the endpoint direction with the flow in the package and in another package, and the flow direction, each refused at its own item with nothing published;
    • validate: false does not disable it;
    • a publish-envelope body is peeled first;
    • two controls.
  • Metadata protocol, section 5 of src/protocol.item-name-every-door.test.ts, on that file's pinned double (no new engine double, so no ledger change), 10 cases:
    • the publish-mode save, the per-item publish and publishPackageDrafts, each with its control;
    • the endpoint read from the metadata service as well as the registry;
    • a failing metadata service read fails the publish;
    • draft and migrate-stored saves are not judged.
  • Consumer readings at the merged head (each package's own exit 0, run through the shared lock):
    • @objectstack/spec pnpm test: 674 files / 20475 tests;
    • @objectstack/metadata: 62 / 888;
    • @objectstack/metadata-protocol: 235 files passed (3 skipped) / 28373 tests;
    • @objectstack/service-automation: 186 / 2486;
    • @objectstack/lint: 138 / 6699;
    • @objectstack/cli --project unit: 287 / 4307. The integration layer is declared to CI.
  • Typecheck: spec, metadata and metadata-protocol exit 0. Spec's test layer holds its pinned debt, so the new test file adds no error.
  • Ablations: four, through scripts/ablation-replace.mjs with its restore trap, run at the pre-merge head f66d8c10c, where the merge touched none of the four sites. The expected direction (red) was declared before each run. No build leg: each suite reads the mutated file from src.
    • A1, the stack leg fed no flows: 5 failed / 9 passed, exactly the five stack-parse refusals.
    • A2, the save-door call removed: 3 failed / 36 passed, the three save-door refusals.
    • A3, the promotion call removed: 2 failed / 37 passed, the per-item and batch publish refusals.
    • A4, the package-publish call removed: 5 failed / 3 passed, the five package refusals.
    • Every anchor went 1 to 0, and every restore was proven: blob equals HEAD and git diff HEAD is empty.

Gates (at 1bd1ba175)

  • Derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths): 125 commands at 1bd1ba175, the same list as at the pre-merge head.
  • At the moment this PR opened, 71 had run:
    • 70 exit 0;
    • check:skill-examples answered PREREQUISITE NOT MET (exit 3: @objectstack/client-react has no built declarations). It is NOT MEASURED until it re-runs after that build.
  • The rest is reported on ADR-0138 E2: the closed list of doors recorded in the conformance matrix, denial by domain, and D2b's publish refusal (D2, D2b) #23225, in the os-dev-report comment, not here (this body is written once): the remaining commands, that re-run, and the --ran reconciliation.
  • Changeset level axis: node scripts/check-changeset-no-major.mjs --base origin/main --event over this PR's title and body exits 0 ("LEVEL AXIS: … declares clause-② no (narrowing)"). The same run with the line amended to yes (narrowing) also exits 0.
  • Governed paths: check-governed-merges --test over the 16 paths: 0 hit the register, NOT governed.

Lint, as a proven narrowing: eslint --no-inline-config --format json over the 10 changed .ts files reports 10 files, 0 errors, 0 warnings and 0 ignored. eslint.config.mjs enables no type-aware linting (no parserOptions.project), so a change here cannot move a verdict on an untouched file. The repo-wide pnpm lint is left to CI.

Consumer sweep

  • examples/**: zero authRequired: false declarations (one comment mentions the key).
  • In-tree fixtures: every anonymous flow endpoint in a test names a runAs: 'user' flow (anonymous-door-sweep, declarative-endpoint-anonymous-guest, the runtime endpoint fallback tests).
  • guest-owner-stamp.dogfood.test.ts pairs anonymous object_operation endpoints with a system record-change flow. That is the record's indirect path, not D2b's pair, and it is untouched.
  • showcase_inbound_task_webhook takes the signed inbound hook with runAs: 'system' and is untouched.
  • No hit needed a fate.

Acceptance notes

  • Restore doors are not judged. rollbackMetaItem and revertCommit re-activate a stored flow version through the repository's synchronous deriveRestoredBody, which has no async read to ask the endpoints. The record says "the publish path", and no cross-item refusal sits on the restore doors today (the default-record-page refusal does not either). Carrier: none; noted, not filed.
  • Not publish doors, so not judged here. The endpoint matcher's load-time index (buildEndpointIndex) holds no flows. A flow registered straight into the engine (registerFlow) passes no publish. A pair whose endpoint and flow live in two packages of one composed artifact is judged at the package publish, but not at the stack parse, which reads only top-level apis and flows, the same scope as the per-endpoint gates.
  • No full runtime counterpart. The executor's refusesElevatedSelfTriggeredStart covers self-triggered types only. The record places D2b at publish, and the module header now says so.
  • The protocol reads endpoints as its own read API does. It reads the registry's and the metadata service's api items as bodies, as getMetaItems does. A wrapped publish envelope registered by a direct metadata.register() is read only by the package publish, which peels.

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

28 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 5656c5638abfcee5081571e6c7c5f5c8998a1812.

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

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/export-origins/api.json) — pages documenting those are invisible to this run
  • 5 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 — 145 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 5656c5638abfcee5081571e6c7c5f5c8998a1812 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 5656c5638abfcee5081571e6c7c5f5c8998a1812

⚠️ 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 5656c5638abfcee5081571e6c7c5f5c8998a1812 → 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: 1bd1ba175673e6f4d194a556fb71b0ae23b1c051
Local-runs: none

Inputs read: card #23225 (body and all 13 comments, the D2b claim 6160651537 as amended, the os-dev-report 6162772892, the ACCEPT 6162852069); PR #23411's body, its 16-file list and the net diff against the merge base e4ec6ec1a (+1218 / -14); ADR-0138 at origin/main (D2, D2b, the four axes, the enforcement map, the unsettled items, the E2 row); the 47 check-runs on the head. Committed source was read with git show / git grep against the two fetched refs only. Nothing was built, run or re-run.

① Derived judgments

The eight new @objectstack/spec/api exports — right. Five functions (anonymousFlowDoorTarget, anonymousElevatedFlowPairs, anonymousEndpointElevatedFlowMessage, elevatedFlowAnonymousEndpointsMessage, validateAnonymousElevatedFlows) and three interfaces (AnonymousFlowDoorCandidate, ElevatedFlowCandidate, AnonymousElevatedFlowPair), all declared in endpoint-publish-gate.ts section (f) and re-exported by src/api/index.ts; api-surface/api.json and export-origins/api.json each gain exactly those eight, each origin pointing at that file. They exist because the record puts the cross-item read "beside policyGate" in spec while two doors outside spec (MetadataManager.publishPackage, the metadata protocol's flow publish) must call the same judge: one judge, three doors, no second implementation (PD #12). The judge is pure and total: anonymousFlowDoorTarget reads type === 'flow', authRequired === false exactly (an absent key is the schema default true, so never anonymous) and a non-empty string target; anonymousElevatedFlowPairs indexes flows with runAs === 'system' by name and pairs in endpoint order; a flow with an absent runAs reads as the default user. The pinned edges in anonymous-elevated-flow-gate.test.ts (an object_operation endpoint sharing the flow's name, an absent authRequired, a system flow of another name, an unnamed endpoint reported as apis[i]) match the code. This is a public-surface widening and is declared as one (②).

The stack-schema refinement — right, and every embedder inherits it. applyApiEndpointGates now takes flows and runs validateAnonymousElevatedFlows(config.apis, config.flows) before the per-endpoint validateApiEndpointDeclarations loop, raising one custom issue per pair at ['apis', i, 'authRequired'] with the endpoint-direction sentence. Because superRefine reads the parsed output, the schema defaults have already applied, so the judge's exact-value reads are sound at this door. Both items are in the one input, so the stack is refused whichever was edited last, which is the record's "both directions" at this door; the spec test pins each direction as the edit that completes the pair from an accepted stack (the two accepted stacks are the negative controls), plus defineStack raising STACK_SCHEMA_INVALID / 422 with the issue located. A pair is reported beside, not instead of, a missing armed budget (ADR-0121 D6), so D2b is not a cascade of D6: right, a pair is a fact about two declarations. Embedders: ObjectStackSchema is the same lazy schema (stack.zod.ts:1888) and EnvironmentArtifactSchema.metadata embeds ObjectStackDefinitionSchema itself (system/environment-artifact.zod.ts:132), so the artifact load, defineStack, os validate, os build, the lint scorer and the metadata plugin's artifact ingestion (plugin.ts:1321) all run the leg with no copy to drift. ArtifactPackageSchema.manifest is AssembledPackageBodySchema, which never carried the endpoint gates, so a per-package artifact entry is judged exactly as before; a pair whose two halves sit in two packages of one composition is outside the stack door's reach, the same scope the per-endpoint gates have always had, and the PR's acceptance notes say so.

The composition with applyPermissionSetOwnerBindingGate — unchanged and unreordered. At the head, stack.zod.ts:1853 reads }).superRefine(applyApiEndpointGates).superRefine(applyPermissionSetOwnerBindingGate));, byte-identical to origin/main:1833. The diff touches applyApiEndpointGates's body and signature only; the D2b leg runs inside it, before the per-endpoint gates; the owner-binding gate still runs second. permissionSetOwnerBindingRefusal, assertLockAllowsDelete and restoreArtifactRegistryView are not in the diff.

MetadataManager.publishPackage's new refusal — right, both directions. gateAnonymousElevatedFlowsForPublish runs right after gateApiItemsForPublish, outside the shouldValidate branch (a security refusal with an opt-out flag is no refusal; validate: false is pinned not to disable it). It reads both types through listForIndex (the registry and every loader, rethrowing a loader failure, so a publish fails instead of answering "nothing names it" from a read that did not happen), peels the stored envelope as gateApiItemsForPublish and buildEndpointIndex do, and reports: an api item of the package naming a system flow anywhere in the index at the endpoint (a pair inside the package once, at the endpoint); a flow item of the package declaring system that an anonymous endpoint outside the package names at the flow. The errors ride the existing validationErrors / success: false result, nothing is published (pinned: itemsPublished 0, state stays draft), and each message is the direction's sentence with the prescription. packageNamesOf keeps both the registry key and the body's name, so a disagreement cannot hide an item from its own package.

The metadata protocol's flow-publish refusal — right, and the one-direction shape is right. anonymousElevatedFlowRefusal judges flow items declaring runAs: 'system' only, reads the engine registry's api items and the metadata service's list('api') (the two sources getMetaItems reads), hands them to the one judge with [{ name, runAs: 'system' }], and throws INVALID_METADATA / 422 with issues[0].path: 'runAs', the envelope that door's other publish-time contract refusals carry; the error's message is the flow-direction sentence with the prescription, the per-key issue carries the reason and the endpoint names. It is asked at every door a flow body reaches active through: saveMetaItem when mode !== 'draft' && writeSource !== 'migrate-stored', placed after the schema parse and before the authoring gate and every write (beside the default-record-page refusal); and promoteDraftForPublish on draftForGate.body before repo.promoteDraft, which publishMetaItem (protocol.ts:23676) and publishPackageDrafts (:25061) share, so a batch aborts on it (pinned, with the draft kept). Only the flow direction sits here, and that is right: api is allowRuntimeCreate: false (metadata-plugin.zod.ts:1130) and the protocol's code-only inlet refuses its write, so no endpoint is ever published through this door; the endpoint direction lives at the two doors that publish endpoints (the stack parse and the package publish). The saves it deliberately does not judge: a draft save (a draft does not execute, and its promotion is judged, both pinned in section 5) and a migrate-stored re-save (it rewrites a row that already exists, and the credential move exists to take a secret out of that row). The migrate-stored skip is not a wire bypass: source is stated only by migrateStoredMetadata, and rest-server.ts builds the save request field by field without a client-supplied source. A row that already pairs keeps executing as published until its next publish, which the semantic entry's acceptance criteria say to an upgrading author.

Both directions at every door the diff names, each refusal with the prescription — yes. Stack parse: both, as one input. Package publish: both, at the package's own item. Protocol: the flow direction, the only direction that door can hold. The record's prescription, "an authenticated endpoint for a partner that can hold a credential; the signed inbound-hook channel (ADR-0041) for a partner webhook; a public form for anonymous intake", is carried verbatim in substance by both sentences (ANONYMOUS_ELEVATED_FLOW_PRESCRIPTION), the prescription before the reason so a truncated 4xx keeps it. runAs: 'automation' is deliberately not prescribed: the enum has no such value, and the record's F3 adds it to D2b's prescription when ADR-0073 M2 lands; the semantic entry's replacement names it under that condition. The spec tests assert all three channels and the absence of 'automation' in both sentences; the metadata and protocol tests assert the three channels on their own envelopes.

The runAs description change on flow.zod.ts — right. The record itself names the description as the hazard ("prescribes system for user-less triggers, so an author who follows it opens an anonymous system-level door"). The diff keeps the existing text and appends the exception and the three serving channels; it cites an ADR id, not a tracker number. Text only; content/docs/references/api/automation-api.mdx (five tables) and automation/flow.mdx (two tables) are regenerated to the same sentence. The hook runAs text is untouched, which is right: a hook is not endpoint-triggered.

The ADR anchor (scripts/adr-anchors/packages__spec__src__api__endpoint-publish-gate.ts.json, new, naming ADR-0121 and ADR-0138 with the invariant) is PD #13's "one new JSON file per anchor", data for the existing check:adr-anchors, not a gate script; the record's "no new gate script" holds (no scripts/check-* is added).

② Semver level

The changeset .changeset/23225-d2b-anonymous-elevated-flow-refused.md grades @objectstack/spec, @objectstack/metadata and @objectstack/metadata-protocol minor; all three are published packages (no private flag) and each one's shipped code moves in the diff, so each owes the entry. It carries the BREAKING line (an accept-set narrowing shipped as minor under the launch-window convention, the grade slices 1 and 2 used for their narrowings), the FROM → TO table per door, the one-line fix, and Clause-②: yes (narrowing). The PR title feat(spec)!: matches the changeset's first line. Clause-②: yes (narrowing) is right for this diff. The value answers whether the card widens an accept set or the public surface: the diff adds eight exports to @objectstack/spec/api (api-surface/api.json +8), so yes; the arm says the diff also narrows a published accept set, so (narrowing). The fleet's one reader (scripts/pm/clause2-line.mjs) names yes (narrowing) as the combination for "a diff that widens one surface and narrows another: both facts are true and both are read". The triage's no and the claim's original line predate the measurement that the judge must be exported; the seat's in-place amendment of the claim (6160651537) and of PR body line 2 to yes (narrowing) makes the three carriers (claim, body, changeset) agree, and check-changeset-no-major admits either value because every moved package is minor. The ADR-0087 registered entry exists in the diff and resolves. The changeset carries one disposition marker (an HTML comment, spelled adr-0087: registered api-endpoint-anonymous-elevated-flow-refused), and the diff adds packages/spec/src/migrations/entries/semantic/18.api-endpoint-anonymous-elevated-flow-refused.ts with exactly that id, in the directory the gate reads ids from (check-adr-0087-registration.mjs, the semantic entries dir); the id is new in the diff, so the gate's "named id must be new" rule is met. registered, one new semantic entry, is the card row's disposition verbatim, and the entry's reason states why no conversion can choose a posture for the author. The head's Check Changeset and Lint & Repo Gates check-runs are success.

③ Boundary flags

Every dev flag (the os-dev-report 6162772892: nine deviations, one open question, two out-of-scope findings) and the PR's four acceptance notes, each answered or escalated:

  • Clause-② conflict (deviation 1; open question, options A/B). Answered A by the seat, in place; judged right above. Nothing open.
  • File surface widened to @objectstack/metadata (deviation 2). MetadataManager.publishPackage already calls validateApiEndpointDeclarations, so it is a measured endpoint publish door; leaving it out would have left the endpoint direction unenforced at a door the record's "every publish door that holds the pair" covers. The claim was widened at ACCEPT and the cross-lane declaration made on [PM seat] domain:engine — 🟢 sunhaolin #6367. Inside the record.
  • @objectstack/service-automation unchanged (H2 falsified). The claim and the ADR's Consumers line name that package for D2b; the dev measured that it judges no publish (it arms flows after the protocol publishes them), and said so in the report and the PR body rather than choosing silently. The record's enforcer is "the publish path", not a package name, and the stored-flow publish is the protocol's. Right; nothing the record requires is missing because of it.
  • New ADR anchor (deviation 3), section-5 pins on the existing double (deviation 4), body written at 71 of 125 gates (deviation 5), lock-queue re-runs (6), label and worktree notes (8, 9). Noted; none moves the judgment. The report's final figures (125 derived, 125 run, 0 unmeasured) are the dev's; the check-runs on the head are the gate verdicts this record reads.
  • Merge commits without the trailer pair (deviation 7). Outside this record's inputs; the PR lands as a squash whose subject is the title.
  • Restore doors not judged (out-of-scope finding 1; acceptance note 1). rollbackMetaItem and revertCommit's restore limb can re-activate a stored runAs: 'system' flow version while an anonymous endpoint names it, through the repository's synchronous deriveRestoredBody. The record decides "publish refuses ... in both directions" and its enforcement-map row reads "the publish path"; a restore is a third door the record did not name, and no cross-item refusal sits on it today. So the PR stays inside the decision and is not short of anything the record requires; whether D2b should reach the restore doors is an amendment question for the maintainer, and the ACCEPT says the seat carries it in its round report. Escalated, accepted as such.
  • Non-publish paths (out-of-scope finding 2; acceptance notes 2 and 3). The endpoint matcher's load-time index holds no flows, a flow registered straight into the engine passes no publish, a cross-package pair is outside the stack door's scope, and there is no full runtime counterpart (refusesElevatedSelfTriggeredStart covers self-triggered types only, which the module header now states). Each is where the record's own words put it ("the publish path"); the module header's "every per-endpoint gate has a runtime counterpart" rewording keeps the header true. Acceptance notes, rightly.
  • The protocol reads endpoints without peeling (acceptance note 4). A code-declared or package-published api row is flat (its body keys at the top level beside the bookkeeping), and the judge reads it correctly; a row registered directly through metadata.register() in the wrapped envelope shape is served by the matcher (which peels) but is not seen by the protocol's flow-direction read. The matcher's own header calls such a write "not a publish", so this is outside the record's path and rightly an acceptance note. Named for the seat: peeling at that read would bring the protocol's view of the declared endpoints to parity with the index the matcher serves from. Not a blocker.
  • The indirect path (a guest-written row firing a record-change flow) is "recorded and not decided" in the record and is left alone by the diff, as the section (f) header and the semantic entry say.

Nothing the record's E2 row requires of D2b is missing: the refusal in both directions at every publish door the diff names, each refusal carrying the prescription, each pin with a negative control (the controls are in all three test files), the registered semantic entry, and the changeset carrying the row's disposition. The diff touches no governed surface (Governed Surface Queue Guard success; 16 paths, none in the register) and is under the 3,000-line class.

Check-runs on the head, as found at the read (47 runs): the five required contexts: Lint & Repo Gates success; TypeScript Type Check success; Build Core success; Governed Surface Queue Guard success; Test Core shards 1, 2, 4, 5 and 6 of 6 success, shard Test Core (3/6) in_progress at the read (not polled). All other completed runs are success or roster skipped (Console Pin Gate, Dogfood Verify CLI, Temporal Conformance, the dogfood shard matrix, Packed-tarball smoke, the second Auto Label and Check PR Size); no run has a failure conclusion. The landing waits on that shard's success as it waits on every check; this verdict does not substitute for it.

Implemented-by: claude/issue-23225-d2b-publish-refusal
Reviewed-by: session_01YRp5z4kK6vgYkrdH97ztsy

VERDICT: PASS


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0138 E2: the closed list of doors recorded in the conformance matrix, denial by domain, and D2b's publish refusal (D2, D2b)

2 participants