Repository navigation
feat(spec)!: the build doors refuse an undeclared key on a script / subflow node config, with its location - #22129
Conversation
…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>
📓 Docs Drift CheckThis PR changes 2 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 139 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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
|
…hemaless-builtin-undeclared-key
…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>
Contract reviewServed-tier: Reviewed from the card (#21982, body and all 7 comments: triage grade ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
Part of #21982
Clause-②: no (narrowing)
Draft, patch round 1 done (claim revision
6050287134). This PR lands thescript/subflowhalf of the card. The card stays open for the remainder named below.What changes
The build doors now refuse a key that a
scriptorsubflownode's executor contract does not declare. They refuse it at its location, in the existing family of flow slot refusal codes.FlowSchema.parse/defineFlow(),defineStack,objectstack validate,objectstack compile, an artifact's parse, the metadata save door'sflowtype schema, andregisterFlow, which parsesFlowSchemafirst.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.packages/spec/src/automation/flow-node-config-refusals.ts, the builtin branch offlowNodeConfigRefusals. It judges key membership wherebuiltinKeysJudged(nodeType)holds. That is a builtin contract whose type is in the spec's own schemaless class,SCHEMALESS_NODE_CONFIG_SCHEMAS.configSchema, soregisterFlow's undeclared-key walk skips it. Its executor still parses the strict contract, so it refuses the node at every run.getBuiltinNodeConfigContracts()keeps its 13 entries, and no new code joinsFLOW_SLOT_REFUSAL_CODES.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.FlowSchemaheader inflow.zod.tsand thenode-config-refused-by-contractdocblock inflow-node-expression-paths.tsnow 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), andscript/subflowkeys.service-automationsource line moves, and there is no second key check anywhere.Built-ins: which get the key refusal, and why (measured at
15ec50e528)getBuiltinNodeConfigContractsconfigSchemascriptstrictObjectscreen-nodes.tsparseNodeConfig)subflowstrictObjectsubflow-node.tsparseNodeConfig)decisionstrictObjectconditions[]rawdecisionShapeRefusalsjudges theconditionsshape, not keys.wait,connector_actionwaitEventConfig/connectorConfig,strictObjectinsideFlowNodeSchemaconfigFlowSchemaalready refuses an unknown key in those blocks.get_record,create_record,update_record,delete_record,notify,http,screen,map,loop,parallel,try_catchstrictObject(all 11)assignmentThe 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.configSchemabuiltins at the build doors:get_record,create_record,update_record,delete_record,notify,http,screen,map,loop,parallelandtry_catch.f281d801fonexamples/app-showcase, flowshowcase_task_completed, nodenotify, withconfig.bogusKey: 1:objectstack validateexits 0;objectstack compileexits 0, and the artifact carries"bogusKey":1;registerFlowrefuses the same node: "Flow 'p' rejected: 1 undeclared config key(s). … unknown config keybogusKeyat config.bogusKey".registerFlow? The spec arm pre-empts registration's descriptor walk, and with it that walk's pinned prescriptions (service-automationconfig-unknown-keys.test.ts).Census first (triage step 1): no writer found
scriptnodessubflownodesexamples/**,packages/platform-objects/**,packages/apps/**,packages/create-objectstack/**(templates),skills/**,content/docs/**15ec50e528c9678036d9FLOW_NODE_CONFIGa58626c88d(same file at objectuimain9990f9e122)function,inputs,outputVariableflowName,input,outputVariabletimeoutMsfield writes the node; the five retiredscriptkeys sit behind ashowWhenno field satisfies)defaultNodeExtrasa58626c88dconfigconfiga58626c88didandtype: 'script'/'subflow', reading the keys of theirconfig. Code fences in.md/.mdxand.jsonfiles were parsed too.conversions/registry.ts);linttests;service-automationengine.test.ts, repaired below.Doors, measured
objectstack validate/compile. Built CLI at round-0 headf281d801f,examples/app-showcasenodesummarize(script), one edit:function: 'summarizeCompletedTask' , bogusKey: 1,.bogusKey.bogusKey: validate exit 1, compile exit 2, and no artifact is written. Both print the refusal at pathnodes, 1, config, bogusKey.scripts/ablation-replace.mjs: anchor 1 → 0, then restored to the HEAD blob,git diff HEADempty.registerFlow(real builtin executors):script/subflowwithbogusKeyare refused atnodes.1.config.bogusKey;scriptfunctionNamealias registers: it is converted before the parse;httpbogusKeyis still refused by the descriptor walk.flow-builtin-node-config-keys.test.ts(19 tests):FlowSchema(also inside a region body),defineStack(STACK_SCHEMA_INVALID422 atflows.1.nodes.1.config.bogusKey),ObjectStackDefinitionSchema, the save door'sflowtype schema and an artifact parse;http,create_record,screen);decision; a retiredscriptkey keeps its tombstone path.builtinKeysJudgedmutated toreturn 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:engine.test.ts("should execute unconditional branches in parallel", "should fail when parameter type is wrong") and ininput-schema-retry-parity.test.tsnow register under the typeprobe_step, executor and nodes alike, never the builtinscript.function: 'noop'filler went with them.inputSchemareads top-level config keys, which a realscriptexecutor refuses.lint:validateStackExpressionskeeps the pre-conversion tolerance it declares.validate-expressions.tsalso hands the judge's undeclared-key refusal for ascriptnode'sfunctionNamealias to the callable check, which already reads that alias.scriptkey is still refused there (bogusKey, on a canonical and on an alias source).'@objectstack/lint': patch.Red → green. Round 0 at
f281d801fhad 4 red inservice-automationand 2 red inlint. All six now pass atef0dfb44d: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› "ascriptwith nofunctionis ONE finding, the callable check's …" ✓ADR-0087
18.flow-script-subflow-config-undeclared-keys-refused.ts.order: 87, re-read onorigin/main8fc50b764(the merged base): its highest order is 86, and open PRs feat(spec)!: refuse bare unique: true on a declared index at protocol 18 — stated scope, zero-drift conversion (ADR-0120 D2/D5a/D7) #22103 and feat(spec)!: retire the flat string-list arm of manifest.permissions — the structured ADR-0025 block is the only form (#13458) #22094 hold 86 and 85 at their heads.registry.tswas regenerated bygen:migration-registry.@objectstack/specminor, BREAKING, with theregisteredmarker, plus@objectstack/lintpatch..changeset/pre.jsonis absent onorigin/main.Merge
origin/main8fc50b764was merged byscripts/pm/os-regen-merge.shas merge commit521e16f1f, with parentsf281d801fand8fc50b764. There were no conflicts.main's side of the generated artifacts thatmainmoved, and there was nothing more to commit.check:generatedreported all 15 artifacts up to date. The delta againstmainwas exactly this PR's 5 round-0 files.gen:schemawas not run.ef0dfb44don top.Verification at
ef0dfb44dcheck:generated: all 15 artifacts up to date.flow-builtin-node-config-keys.test.tsandflow-builtin-node-config-values.test.ts: 63/63.f281d801f: 624 files, 18628 tests passed.@objectstack/spec/automationunresolvable for 17 files; it was re-run alone.@objectstack/lintexit 0 and@objectstack/service-automationexit 0, both includingcheck:test-typecheck, over a closure rebuilt with declarations.--no-inline-config --format json) over the diff's 10.tsfiles: 10 files, 0 errors, 0 warnings. The population is read from the json count.parserOptions.projectandprojectServiceare null for each file, so there is no type-aware linting and no untouched file's verdict can move.dispatch-gates --commands --repo objectstack-ai/objectstackderives 92 commands from the merged head. All ran, with exit codes captured before any pipe. The--ranreconciliation reads 91 run, 1 NOT-MEASURED, 0 UNRUN (check:dts-closurerecorded at its re-run).check:dual-build-cjs-loads: exit 3, PREREQUISITE NOT MET (packages outside this worktree's build closure have nodist). It is read from CI, as are round 0'sclipublished-subpath pins.check:dts-closurefirst exited 1, naming exactly the 19 closure packages built withOS_SKIP_DTS=1for 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