Skip to content

feat(spec)!: the build doors refuse an undeclared key on a script / subflow node config, with its location - #22129

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-21982-schemaless-builtin-undeclared-key
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-21982-schemaless-builtin-undeclared-key

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Part of #21982
Clause-②: no (narrowing)

Draft, patch round 1 done (claim revision 6050287134). This PR lands the script / subflow half of the card. The card stays open for the remainder named below.

What changes

The build doors now refuse a key that a script or subflow node's executor contract does not declare. They refuse it at its location, in the existing family of flow slot refusal codes.

  • The doors: FlowSchema.parse / defineFlow(), defineStack, objectstack validate, objectstack compile, an artifact's parse, the metadata save door's flow type schema, and registerFlow, which parses FlowSchema first.
  • The code: the existing node-config-refused-by-contract, params: { nodeType, key }. There is one refusal per undeclared key, anchored at the key (nodes.N.config.bogusKey), and its message is the contract's own sentence.
  • The edit: packages/spec/src/automation/flow-node-config-refusals.ts, the builtin branch of flowNodeConfigRefusals. It judges key membership where builtinKeysJudged(nodeType) holds. That is a builtin contract whose type is in the spec's own schemaless class, SCHEMALESS_NODE_CONFIG_SCHEMAS.
  • Why the narrow lift: a schemaless descriptor publishes no configSchema, so registerFlow's undeclared-key walk skips it. Its executor still parses the strict contract, so it refuses the node at every run.
  • Unchanged: getBuiltinNodeConfigContracts() keeps its 13 entries, and no new code joins FLOW_SLOT_REFUSAL_CODES.
  • Premise corrected: builtinValueJudged's docblock said "registration refuses an undeclared key against the descriptor". That was false for exactly these two types. The docblock now says which door owns which key.
  • Comments made true: the FlowSchema header in flow.zod.ts and the node-config-refused-by-contract docblock in flow-node-expression-paths.ts now name each arm that judges key membership: approval whole (build: objectstack validate / compile accept unknown keys in a plugin node's config (e.g. an approval node's escalation) — the build-time refusal map covers built-in node types only #21850), builtin values (service-automation: a built-in node's config value its own contract refuses still registers, then fails every run — the built-in half of #21848's class #21898), and script / subflow keys.
  • No service-automation source line moves, and there is no second key check anywhere.

Built-ins: which get the key refusal, and why (measured at 15ec50e528)

type in getBuiltinNodeConfigContracts descriptor configSchema contract strict executor parses it key refusal here
script yes none strictObject yes (screen-nodes.ts parseNodeConfig) yes
subflow yes none strictObject yes (subflow-node.ts parseNodeConfig) yes
decision no none strictObject no: its executor reads conditions[] raw no. An undeclared key fails no run. decisionShapeRefusals judges the conditions shape, not keys.
wait, connector_action no none their contracts are the FlowNode sibling blocks waitEventConfig / connectorConfig, strictObject inside FlowNodeSchema they read the sibling block, not config no. FlowSchema already refuses an unknown key in those blocks.
get_record, create_record, update_record, delete_record, notify, http, screen, map, loop, parallel, try_catch yes yes strictObject (all 11) yes no: the remainder, below
assignment no yes (keyValue map) no: open top-level variable names no single contract no

The remainder: the card stays open for it

Triage's direction step 2 covers every builtin whose executor contract is strict. All 13 are. This PR takes the two schemaless ones, by the seat's ruling 6050287134.

  • Remainder 1, the 11 descriptor-configSchema builtins at the build doors: get_record, create_record, update_record, delete_record, notify, http, screen, map, loop, parallel and try_catch.
    • They have the same gap at the build doors. Measured at this branch's round-0 head f281d801f on examples/app-showcase, flow showcase_task_completed, node notify, with config.bogusKey: 1:
      • objectstack validate exits 0;
      • objectstack compile exits 0, and the artifact carries "bogusKey":1;
      • registerFlow refuses the same node: "Flow 'p' rejected: 1 undeclared config key(s). … unknown config key bogusKey at config.bogusKey".
    • So boot drops the flow with a warning, and the build never says so.
  • Remainder 2, an open design choice, not decided here: once the spec arm covers those 11 types, who judges a builtin's undeclared key at registerFlow? The spec arm pre-empts registration's descriptor walk, and with it that walk's pinned prescriptions (service-automation config-unknown-keys.test.ts).

Census first (triage step 1): no writer found

corpus read at script nodes subflow nodes with a key outside the contract
this repo: examples/**, packages/platform-objects/**, packages/apps/**, packages/create-objectstack/** (templates), skills/**, content/docs/** 15ec50e528 6 2 0
hotcrm, whole tree c9678036d9 0 5 0
objectui flow designer FLOW_NODE_CONFIG pin a58626c88d (same file at objectui main 9990f9e122) form writes function, inputs, outputVariable form writes flowName, input, outputVariable 0 (its timeoutMs field writes the node; the five retired script keys sit behind a showWhen no field satisfies)
objectui designer seeds defaultNodeExtras a58626c88d empty config empty config 0
objectui console preview samples a58626c88d 4 0 0
  • Method: a TypeScript-AST scan for object literals carrying id and type: 'script' / 'subflow', reading the keys of their config. Code fences in .md / .mdx and .json files were parsed too.
  • Control: over this whole repo, tests included, the same scan finds 103 nodes and flags 17. All 17 are fixtures:
    • 9 in the D2 conversion fixtures (conversions/registry.ts);
    • 5 in lint tests;
    • 3 test-double keys in service-automation engine.test.ts, repaired below.

Doors, measured

  • objectstack validate / compile. Built CLI at round-0 head f281d801f, examples/app-showcase node summarize (script), one edit: function: 'summarizeCompletedTask' , bogusKey: 1,.
    • Control: validate exit 0, compile exit 0, and the artifact has no bogusKey.
    • With bogusKey: validate exit 1, compile exit 2, and no artifact is written. Both print the refusal at path nodes, 1, config, bogusKey.
    • The mutation was made with scripts/ablation-replace.mjs: anchor 1 → 0, then restored to the HEAD blob, git diff HEAD empty.
  • registerFlow (real builtin executors):
    • script / subflow with bogusKey are refused at nodes.1.config.bogusKey;
    • both controls register;
    • a script functionName alias registers: it is converted before the parse;
    • http bogusKey is still refused by the descriptor walk.
  • Pinned in flow-builtin-node-config-keys.test.ts (19 tests):
    • the refusal at FlowSchema (also inside a region body), defineStack (STACK_SCHEMA_INVALID 422 at flows.1.nodes.1.config.bogusKey), ObjectStackDefinitionSchema, the save door's flow type schema and an artifact parse;
    • the controls: no extra key; a descriptor type's key still left to registration (http, create_record, screen); decision; a retired script key keeps its tombstone path.
  • Reverse verification at round 0, builtinKeysJudged mutated to return false: 12 of 19 went red and the 7 controls held. It was restored to the HEAD blob.

Cross-lane fixtures repaired (claim revision 6050287134)

  • service-automation, test only:
    • The doubles in engine.test.ts ("should execute unconditional branches in parallel", "should fail when parameter type is wrong") and in input-schema-retry-parity.test.ts now register under the type probe_step, executor and nodes alike, never the builtin script.
    • The function: 'noop' filler went with them.
    • inputSchema reads top-level config keys, which a real script executor refuses.
  • lint: validateStackExpressions keeps the pre-conversion tolerance it declares.
    • The filter in validate-expressions.ts also hands the judge's undeclared-key refusal for a script node's functionName alias to the callable check, which already reads that alias.
    • A new pin holds that every other undeclared script key is still refused there (bogusKey, on a canonical and on an alias source).
    • The changeset gains '@objectstack/lint': patch.

Red → green. Round 0 at f281d801f had 4 red in service-automation and 2 red in lint. All six now pass at ef0dfb44d:

  • engine.test.ts › "should execute unconditional branches in parallel" ✓
  • engine.test.ts › "should fail when parameter type is wrong" ✓
  • input-schema-retry-parity.test.ts › "never executes a node whose config mis-types its declared inputSchema — on ANY attempt" ✓
  • input-schema-retry-parity.test.ts › "still retries a VALID flow normally …" ✓
  • validate-expressions.test.ts › "accepts a script node that names a callable via the functionName alias" ✓
  • validate-expressions.test.ts › "a script with no function is ONE finding, the callable check's …" ✓

ADR-0087

Merge

  • origin/main 8fc50b764 was merged by scripts/pm/os-regen-merge.sh as merge commit 521e16f1f, with parents f281d801f and 8fc50b764. There were no conflicts.
  • Step 2 took main's side of the generated artifacts that main moved, and there was nothing more to commit.
  • After a spec build on the merged tree, check:generated reported all 15 artifacts up to date. The delta against main was exactly this PR's 5 round-0 files.
  • gen:schema was not run.
  • Round 1's edits are commit ef0dfb44d on top.

Verification at ef0dfb44d

  • Spec:
    • check:generated: all 15 artifacts up to date.
    • The pin files flow-builtin-node-config-keys.test.ts and flow-builtin-node-config-values.test.ts: 63/63.
    • Round 0's whole spec suite at f281d801f: 624 files, 18628 tests passed.
  • lint: the whole suite, 123 files, 5689/5689.
  • service-automation: the whole suite, 175 files, 2120/2120. The first attempt collided with a concurrent gate run that left @objectstack/spec/automation unresolvable for 17 files; it was re-run alone.
  • Typecheck: @objectstack/lint exit 0 and @objectstack/service-automation exit 0, both including check:test-typecheck, over a closure rebuilt with declarations.
  • eslint, narrowed (--no-inline-config --format json) over the diff's 10 .ts files: 10 files, 0 errors, 0 warnings. The population is read from the json count. parserOptions.project and projectService are null for each file, so there is no type-aware linting and no untouched file's verdict can move.
  • Gates: dispatch-gates --commands --repo objectstack-ai/objectstack derives 92 commands from the merged head. All ran, with exit codes captured before any pipe. The --ran reconciliation reads 91 run, 1 NOT-MEASURED, 0 UNRUN (check:dts-closure recorded at its re-run).
    • 91 exit 0.
    • check:dual-build-cjs-loads: exit 3, PREREQUISITE NOT MET (packages outside this worktree's build closure have no dist). It is read from CI, as are round 0's cli published-subpath pins.
    • check:dts-closure first exited 1, naming exactly the 19 closure packages built with OS_SKIP_DTS=1 for the test runs. Re-run after the closure was rebuilt with declarations, it exits 0: 169/169 declaration files across 71 built packages.

Generated by Claude Code

claude added 3 commits October 7, 2026 23:55
…ubflow node config (WIP)

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
…flow door; ADR-0087 D3 entry

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
…bflow undeclared-key refusal

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

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/spec, touching 7 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/automation/flow.zod.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/releases/v17/17-0.mdx (via functionName (literal, a string literal in runStackExpressionPasses))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/automation/flow.zod.ts) — pages documenting those are invisible to this run
  • 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 — 139 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 8fc50b7647d30db2a0837877cf251c1163a3239e → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 8fc50b7647d30db2a0837877cf251c1163a3239e

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

claude added 2 commits October 8, 2026 01:29
…ance, comment truths

- service-automation tests: the input-schema and parallel-branch doubles
  register under a type of their own (probe_step), never the builtin script,
  whose contract now refuses their undeclared keys at the flow parse.
- lint: validateStackExpressions keeps its declared pre-conversion tolerance
  for a script node's functionName alias; every other undeclared script key
  is still refused there (pinned).
- spec comments: the FlowSchema header and the node-config-refused-by-contract
  docblock say which arm judges key membership.
- changeset: @objectstack/lint patch.

Claude-Session: https://claude.ai/code/session_01RPo7FUd6bSnAfkWMAKi848
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

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

Reviewed from the card (#21982, body and all 7 comments: triage grade 6015698142, unlock 6042634083, claim 6049115454, dev reports 6050249047 / 6050924694, claim revision 6050287134, ACCEPT 6050941111), the PR (#22129 body, its 11-file list, its one comment, the net diff against base 8fc50b764, 11 files +566/−39) and the check-runs on the head. Check-runs read twice: 2026-10-08T02:31:50Z and 2026-10-08T02:37:47Z, both times 46 runs, all completed: 38 success, 8 skipped, 0 failed, 0 in_progress (Check Changeset ×3, Part-of PR must not also close its card ×2, Test Core 1–6/6, Type Check ×5, Lint & Repo Gates, Spec property liveness, Governed Surface Queue Guard, Dogfood Regression Gate 1–3/3 all success). Files at the head read through the raw contents API; main read once at ef1fcb26a2 (2026-10-08T01:58Z, past the base).

① Derived judgments

  1. FlowSchema.parse narrows, and every door that parses through it narrows with it — right. A script node config carrying a key outside function / inputs / outputVariable, or a subflow node config carrying a key outside flowName / input / outputVariable, is now refused as node-config-refused-by-contract, params: { nodeType, key }, path at the key, one refusal per key, message wrapping the contract's own unrecognized-key sentence. Both contracts are strictObject (schemaless-node-config.zod.ts :285 and :372 at the head), both executors parse them before acting (the card's measured run failure), and neither descriptor publishes a configSchema, so registerFlow's validateNodeConfigKeys walk skipped them. Refused-at-save now equals refused-at-run. Doors covered and pinned in flow-builtin-node-config-keys.test.ts: FlowSchema / defineFlow(), defineStack (STACK_SCHEMA_INVALID 422 at flows.N.nodes.M.config.KEY), ObjectStackDefinitionSchema (the objectstack validate / compile parse; dev measured exit 1 / exit 2, path nodes,1,config,bogusKey), ArtifactStagePackageBodySchema, getMetadataTypeSchema('flow'), and registerFlow, which converts, then FlowSchema.parses (engine.ts :4348), then walks descriptors (:4386). Triage's three pins (validate + compile refuse with location; registerFlow refuses; control passes) are all met.
  2. The judged set is exactly script and subflow — right. builtinKeysJudged is hasOwnProperty on SCHEMALESS_NODE_CONFIG_SCHEMAS (script, subflow, decision), asked only for a type in getBuiltinNodeConfigContracts() (13 entries, unchanged); decision is not among the 13 and takes its own early-return arm (:616). Pinned by the control that filters the 13 for a bogusKey refusal and gets ['script', 'subflow'].
  3. The 11 descriptor-configSchema builtins keep their undeclared keys at registration — right for this PR, under the seat's q3 ruling. flowNodeConfigRefusals leaves unrecognized_keys alone for them (pinned on http, create_record, screen), so their build doors stay green on a key registration refuses. The shadowing argument holds structurally: the spec arm runs inside the parse that precedes the descriptor walk, so lifting it would pre-empt that walk's pinned prescriptions. The PR body names this as Remainder 1 and 2, the first line is Part of #21982, the stored body has zero closing-keyword hits and one #21982, and Part-of PR must not also close its card is success twice on the head. The card stays open.
  4. Tombstoned retired script keys stay where they were — right, and outside this card's class. retiredKey() yields invalid_type expecting never, not unrecognized_keys; builtinValueJudged still returns false for it, so the lint and the D2 conversion flow-node-script-branch-keys-removed keep them. Pinned (actionType: 'email' → no refusal from the arm, the tombstone message intact).
  5. D2 alias spellings at the direct parse door — right as a narrowing, with one disclosure note (③.6). At the converting doors (defineStack, os validate, os compile, registerFlow) functionName / input on a script and flow on a subflow are rewritten before the judge, so nothing moves there (dev measured a functionName alias registering). At a direct FlowSchema.parse or defineFlow() (flow.zod.ts :1738 is FlowSchema.parse(config) with no conversion) they are now refused as undeclared keys. For functionName and flow the accept set does not move: the required canonical key was already missing and refused since [finding] a decision branch with no label registers and validates clean, then at run time the decision takes EVERY out-edge; a non-object conditions element also registers #20316. For input beside a present function on a script, the direct-parse accept set narrows net-new (inputs is optional, so nothing refused it before). The refusal carries SCRIPT_KEY_GUIDANCE.input's prescription, the census's AST scan flags any key outside the contract (input included) and found 0 production writers, and the changeset discloses the alias refusal at direct parse. Judged right.
  6. Region bodies — right. A script / subflow inside an ADR-0031 region is refused at the author's path (nodes.N.config.body.nodes.M.config.KEY), pinned; the same walk service-automation: a built-in node's config value its own contract refuses still registers, then fails every run — the built-in half of #21848's class #21898 used.
  7. Key arm and value arm compose — right. With keysJudged true, an unrecognized_keys issue is consumed (continue), and every other issue still passes builtinValueJudged (not whole), so the {token}, run-resolved and ledger-slot carve-outs keep holding for script / subflow values. Pinned: function: '' beside bogusKey yields exactly ['bogusKey', 'function'].
  8. @objectstack/lint validateStackExpressions — right. Its public output gains one error finding at config.KEY for an undeclared script / subflow key, through the shared judge; the filter at validate-expressions.ts :1724 hands both the function-missing refusal and the new functionName-undeclared refusal to the callable check, so the declared pre-conversion tolerance holds exactly for the alias that check reads. Pinned: the judge names ['bogusKey', 'function', 'functionName'] on a raw alias source and the lint keeps only config.bogusKey. A raw input (script) or flow (subflow) handed directly to the lint is now a finding too; no real door hands the lint a pre-conversion source (os validate converts at load), the lint suite carries no such fixture (5689/5689), and the changeset's lint paragraph says every other undeclared key is refused there.
  9. service-automation — right, test files only. The doubles in engine.test.ts and input-schema-retry-parity.test.ts register under probe_step, executor and nodes alike, keeping each test's meaning (delay on parallel branches; inputSchema reading a top-level count); the function: 'noop' filler goes with them. No source line moves (file list confirms). Red at f281d801f, green at the head; Test Core green.
  10. Public surface otherwise unchanged — right. No new export, no new code in FLOW_SLOT_REFUSAL_CODES, FlowSlotRefusalParams['node-config-refused-by-contract'] keeps its type (docblock only), getBuiltinNodeConfigContracts() keeps 13 entries; the added SCHEMALESS_NODE_CONFIG_SCHEMAS import comes from a module flow-node-config-refusals.ts already imported, so no new import edge. flow.zod.ts and flow-node-expression-paths.ts edits are comment-only (diff read).
  11. ADR-0087 registration — right. MIGRATIONS_BY_MAJOR[18].semantic gains one D3 entry with no conversionIds and no RETIRED_KEYS_BY_MAJOR row (no key removed, no tombstone owed); STEP18_RATIONALE gains order: 87; the generated copy in registry.ts is byte-identical to the entry file; check:generated is inside the green Lint & Repo Gates. On main ef1fcb26a2 the highest step-18 order is 86 (now held three times, after a landing since the base) and no 87 exists; duplicates at 74, 77 and 86 pre-exist in that array, so order uniqueness is not a gate, and the PR's 87 stays the next free order. mergeable_state reads clean.
  12. Triage's value guard — right. "A template value is never refused for its key's value": the key arm judges membership only; values stay under builtinValueJudged with its {token} carve-out.

② Semver level

  • @objectstack/spec: minor, BREAKING — right. The diff narrows the accept set of FlowSchema, a published authoring surface, which the rulebook (AGENTS.md :1076) classes as BREAKING for (narrowing); this repo ships BREAKING accept-set narrowings as minor under the launch-window convention (packages/cli/src/utils/spec-release-changes.ts :17–18, the phrase in 125 repo hits including sibling changesets), and .changeset/pre.json is absent at the head and on main (HTTP 404 both). The changeset carries the migration the rulebook demands for a breaking entry: a FROM → TO table, the one-line fix, the measured who-is-affected, and exactly one ADR-0087 marker, adr-0087: registered flow-script-subflow-config-undeclared-keys-refused, matching the D3 entry id in ①.11. Check Changeset is success ×3 and check:adr-0087-registration sits in the green Lint & Repo Gates.
  • @objectstack/lint: patch — right. The lint's own diff is a tolerance-preserving filter plus a pin; the new finding class arrives through its spec dependency and is declared on spec.
  • Clause-②: no (narrowing) — right. Present at a line start in the PR body (line 2) and in the changeset (line 14), identical. Well-formed under AGENTS.md :1075–1076: the closed pair allows no (narrowing); only no (widening) is malformed, and (narrowing) is BREAKING, which the changeset states. Triage's grade 6015698142 wrote yes (narrowing); the claim 6049115454 read the line's test as widening-only (as on lint: a conditional validation rule's nested then / otherwise predicate is never validated, so os build passes and the object save door stores a predicate the top-level rule would refuse #22042). The diff accepts nothing new and exports nothing new, so no matches the diff; the arm (narrowing) is what the ADR-0087 gate reads, and the level is minor under either reading, so the discrepancy moves neither the gate nor the level. No skip-changeset anywhere: right, two released packages publish.

③ Boundary flags

  1. OQ1 (service-automation doubles) — answered. Seat ruled A in 6050287134; implemented at the head exactly as ruled (①.9).
  2. OQ2 (lint pre-conversion tolerance) — answered. Seat ruled A; implemented with the filter, the new pin and the @objectstack/lint: patch line (①.8).
  3. OQ3 (scope: 2 of 13 strict builtins) — answered. Seat ruled the narrow reading with Part of and the remainder on the card; implemented and gated (①.3). For the seat's landing act: the ACCEPT's Release: line returning the card to pm:queue must name Remainder 1 (the 11 builtins at the build doors) and Remainder 2 (which judge owns a builtin's undeclared key at registerFlow), and the merge must not close build: a script node's undeclared config key passes objectstack validate, compile and registerFlow, then fails every run — the key half of #21898's class (subflow by reading) #21982. Nothing in the stored body or the gate would close it today.
  4. Round-0 NOT MEASURED items (cli published-subpath pins, check:dts-closure, check:dual-build-cjs-loads; round-1 whole spec suite not re-run) — answered by the head's check-runs: Test Core 1–6/6, Build Core, Lint & Repo Gates, all five Type Check runs success.
  5. closingIssuesReferences GraphQL NOT MEASURED — answered: Part-of PR must not also close its card success ×2 on the head; my own whole-word scan of the stored body finds 0 closing-keyword hits.
  6. Escalated to the seat, non-blocking (disclosure wording): the changeset's sentence "Met by a direct FlowSchema.parse or defineFlow(), it is refused like any other undeclared key, as its missing canonical key already was" is exact for functionName and flow, but input beside a present function on a script is a net-new refusal at defineFlow() (①.5): no canonical key was missing, since inputs is optional. The refusal itself already carries the right prescription and the census found no writer, so the narrowing is right and disclosed; only the justification clause under-describes it. A one-clause changeset edit before landing would make the CHANGELOG sentence exact; not required for PASS.
  7. H4 live save door (draft + publish through runtime) not measured — accepted residual. Pinned at getMetadataTypeSchema('flow'), the schema that door validates against; Dogfood Regression Gate 1–3/3 success.
  8. Merge commit 521e16f1f without the session trailer pair; first commit subject carries (WIP) — not a contract matter. check:commit-card-trailers passed on push; the squash title is the PR title.
  9. service-automation first run collided with a concurrent gate (import resolution, 0 assertion failures) — answered: green alone and in CI.
  10. Out-of-scope carriers: the two comment-only staleness items were folded into this PR by the claim revision (①.10); the objectstack validate raw-ZodError print on a defineFlow source predates this PR and stays noted-not-filed, as on build: objectstack validate / compile accept unknown keys in a plugin node's config (e.g. an approval node's escalation) — the build-time refusal map covers built-in node types only #21850.
  11. Docs Drift Check (advisory): content/docs/releases/v17/17-0.mdx is release-owned and untouched; flow.zod.ts yielded no anchor, and its edit is comment-only, so no page can be falsified by it.
  12. main moved past the base (ef1fcb26a2, a third step-18 order: 86): no path of this PR is touched, mergeable_state is clean, and 87 stays free (①.11). No action.

Implemented-by: claude/issue-21982-schemaless-builtin-undeclared-key
Reviewed-by: session_01RPo7FUd6bSnAfkWMAKi848

VERDICT: PASS

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