Repository navigation
fix(spec)!: close the shared retry policy, and judge try_catch config keys at the build doors - #22380
Conversation
…pec key arm WIP: the shared retry policy becomes a strictObject, so an undeclared retry key is refused at parse with a did-you-mean; try_catch leaves the registration-time descriptor walk and joins the builtin key arm, which also refuses a retryDelayMs tombstone the conversion leaves behind. Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
…y; curate the policy's near-miss table Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
…at the build doors; changeset Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
…y-catch-retry-strict
…t the value arm's Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
…y-catch-retry-strict
📓 Docs Drift CheckThis PR changes 2 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 6 release-owned page(s) also 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 f1c8cf7fdcfc8b68ef467660d2907941e6ae964c && git checkout f1c8cf7fdcfc8b68ef467660d2907941e6ae964c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c8c803c293af7fdcba18b142e64ee9ce80bfc198 6fbc005a63133309c536215b04a403b9519627e4 && git checkout -B drift-repro c8c803c293af7fdcba18b142e64ee9ce80bfc198 && git merge --no-ff 6fbc005a63133309c536215b04a403b9519627e4
node scripts/docs-audit/affected-docs.mjs --json c8c803c293af7fdcba18b142e64ee9ce80bfc198
|
…e runs one attempt more, not fewer maxAttempts counts the first attempt and maxRetries does not, so maxAttempts: 3 is 3 runs and a bare rename to maxRetries: 3 is 4. The prescription (maxRetries one lower) was right; the consequence clause was inverted. Pinned on both surfaces: the message says "one attempt more" and never "fewer". Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #22343 (body and its four comments: triage ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL |
…narrowing) The diff widens no accept set and no public surface; a pure narrowing is spelled `Clause-②: no (narrowing)`. The parenthesis is unchanged. Claude-Session: https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs, and nothing else: card #22343 (body and its six comments: triage ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
… merging main at e75dced (step 18: 64 conversions, 326 semantic entries) main added two step-18 semantic entries since b460153: sys-view-definition-retired (#22374) and try-catch-and-retry-policy-undeclared-keys-refused (#22380), and #22380 reworded the existing entry flow-builtin-node-config-undeclared-keys-refused. At protocol 18 both generators project every step-18 entry, so both documents gain the two entries and carry the reworded text. The conversion ids are unchanged. registry.ts is current as merged (check:migration-registry exits 0), so it is not regenerated. Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
declare the current protocol, ^18 main added two manifest fixtures that declare engines.protocol '^17' and stand for a valid current app, not for an old artifact: - packages/qa/dogfood/test/security-catalog-cold-boot-environment-holder.dogfood.test.ts (#22365): at protocol 18 the load-seam handshake refuses it, and all four cases fail with ProtocolIncompatibleError before reaching their subject. - packages/cli/test/retry-policy-key-validate-door.test.ts (#22380): os validate only advises on the gap, so it stays green either way, but its subject is the retry-policy key door, not the protocol's age. Both now read '^18', like the other current-app fixtures this change moved. Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
…n in words instead of a tracker number (stage 30) (objectstack-ai#22414) Part of objectstack-ai#20749 Clause-②: no Stage 30 of this card: the class (e) remainder, the test strings shipped under the `packages/spec/src` subdirectories, as ruled in `5902360492` on objectstack-ai#20513. The census at the base reads 17 messages / 18 ids in 12 files. This stage rewrites 7 of them (6 titles and 1 expect message, 8 ids) in 5 files: each now states what its record decided, or drops the number where the title already says it. The other 10 messages / 10 ids stay, 4 because earlier stages decided they are not citations and 6 because an assertion matches the string against text this claim does not let the stage edit; both groups are named under "What stays". Text only: no assertion, identifier, test count, code comment, file name or non-test file changes. No file is deferred. ## Census (re-taken first) The instrument is stage 28's `census28.cjs`, byte-identical (md5 `31d8488b5194b8d3048e3fcaec0efaed`, the value stages 28 and 29 published): an AST walk over the `packages/spec/src` test files, one message per folded string (a lone literal, a template, or a plus chain) that matches the gate's id pattern, a title when the folded root is argument 0 of a describe / it / test / suite / bench call, comments never read. It was run against the three published readings before it was trusted, and all three reproduce exactly: 128 messages / 130 ids in 37 files at `f7b8a5932b`, 191 / 200 in 53 files at `aa09db58c9`, 73 / 76 in 24 files at `b7e01fbbd`. | reading | messages / ids | files | |:--|--:|--:| | base `11d119ab1` | 17 / 18 (titles 6 / 7, other 11 / 11) | 12 | | stage 29's landing `0ef9029da` | 17 / 18, per file equal to the base | 12 | | this head | 10 / 10 (titles 0 / 0, other 10 / 10) | 7 | | the 5 edited files, this head | 0 / 0 | 0 of 5 | Stage 29's ACCEPT carried 16 / 17 (ui 9 / 9 in 7 files, automation 2 / 3, ai 2 / 2, api 1 / 1, contracts 1 / 1, kernel 1 / 1). The base reads 1 / 1 more, all in `ui`: the title `carries no ruling date and no tracker id (objectstack-ai#22093)` at `view-submit-redirect-url.test.ts:341`, which PR objectstack-ai#22322 (`ad381fd94`, landed 2026-10-09T00:35Z) added after stage 29's head. So `ui` reads 10 / 10 in 7 files, and nothing else moved. The census at `origin/main` `e75dceddd` (8 commits past the base, none touching the 12 files) reads the same 17 / 18 in the same 12 files, so nothing regrew while this stage ran. The reading is within one message of the claim's, so there was no re-cut. Per file, messages at base: `ai/build-progress` 2, `api/meta-item-response-shapes` 1, `automation/builtin-node-config` 2 (3 ids), `contracts/approval-service` 1, `kernel/manifest` 1, `ui/action-description` 1, `ui/component-props-unknown-members.pin` 1, `ui/dashboard-chart-structure-refusal` 2, `ui/dashboard` 2, `ui/notification` 1, `ui/strictness-batch14` 1, `ui/view-submit-redirect-url` 2. Controls: - Pathspec: the 12 named paths hit the control word `describe(` in 12 of 12 files and a nonsense word in none; the census scanned 12 of 12. - Planted, in a scratch tree: an id in a describe title, a plus-chain title, an expect message, a template literal, a cross-repo spelling and a ledger-style string each read once (6 / 6); a comment, a six-digit colour, an HTML entity, a two-digit number and a hex colour with a letter read 0. - Lit and dark inside the group: the 5 edited files read 1, 2, 1, 1 and 2 messages at base and 0 at the head; the 7 untouched files read the same at both ends. ## Deferral At the census (2026-10-09T03:03Z) 17 PRs were open; at the re-scan before opening this PR (04:13Z), 13. Every file list was read through REST (605 and 593 rows). None touches any of the 12 files: lit control `api/protocol.test.ts` (PR objectstack-ai#22323) found, dark control 0. Of the four PRs the claim named, objectstack-ai#22380 has landed and objectstack-ai#22315, objectstack-ai#22323 and objectstack-ai#22215 are open; none of them touches a file in this group. Deferred files: none. ## What changed 7 literals, one line each, in 5 files: +7 / -7. Every file keeps its line count. - `api/meta-item-response-shapes.test.ts:226`: the `[objectstack-ai#22114] ` prefix goes; the title already says what objectstack-ai#22126 landed, that the read serves the version token and the 409 carries the current one as data. - `automation/builtin-node-config.test.ts:434`: "a CEL envelope beside literals; the `{token}` dialect retired". objectstack-ai#14149's ruling A made an assignment value a CEL envelope beside literals, and objectstack-ai#19939 retires the `{token}` dialect in flow value slots; the title already stated both, so only the two numbers go. - `automation/builtin-node-config.test.ts:485`: the `[objectstack-ai#19939] ` prefix goes from the REFUSES title. - `kernel/manifest.test.ts:681` and `ui/action-description.test.ts:235`: the trailing `(objectstack-ai#22093)` goes. objectstack-ai#22093 decided that author-visible help and refusals carry no service-interface name, ruling date or foreign example id, and both titles already say what their bodies pin. - `ui/view-submit-redirect-url.test.ts:341`: the trailing `(objectstack-ai#22093)` goes from the title. - `ui/view-submit-redirect-url.test.ts:118`: the expect message `states the rule, not its ruling date (objectstack-ai#22093)` drops the number. It is an assertion's failure message, so it was needle-checked first (below) and is the one declared non-title string. All seven are "drop a number the title already explains". None needed a rewrite in new words, because each title already carried the decision. ## What stays, and why 10 messages / 10 ids in 7 files, none edited. Four are CSS hex colours, not citations. `colors: ['objectstack-ai#111', 'objectstack-ai#222']` at `ui/dashboard-chart-structure-refusal.test.ts:94` and `palette: ['objectstack-ai#111', 'objectstack-ai#222']` at `ui/dashboard.test.ts:124` are fixture input the schema under test reads. Stage 21's ACCEPT (`6001279159`, decision A) kept them by file and line, and every later stage carried them forward. Six are strings that an assertion matches against text outside this stage's edit surface. Moving one at the same strength means editing a non-test source docblock (and, for the first two, its generated reference page) or the assertion that matches it. The claim forbids both and says to stop and report, so none is touched; `open_questions` in the report carries the decision. - `ai/build-progress.test.ts:236` `'cloud#2172'` and `:237` `'objectui#7388 block 2'`: `toContain` over the source text of `ai/build-progress.zod.ts` (docblock lines 8, 27 and 85), which `content/docs/references/ai/build-progress.mdx` renders. - `contracts/approval-service.test.ts:274` `'objectstack-ai#16495'`: `toContain` over the docblock above `continueRestoredRun` in `contracts/approval-service.ts` (line 999). - `ui/notification.test.ts:123` `'// [objectstack-ai#4610]'`: the locator of the tombstone note in `ui/notification.zod.ts:94`; the file's own `toMatch(/^\[objectstack-ai#4610\]/)` at `:134` reads the same note. - `ui/strictness-batch14.test.ts:395` `'objectstack-ai#5015'`: `toContain` over `ui/notification.zod.ts` and `ui/sharing.zod.ts`. - `ui/component-props-unknown-members.pin.test.ts:322` `ruling: 'decision card objectstack-ai#21704, fork 4, letter B (record 5979239990)'`: the file's own assertion at `:417` matches the value with `/objectstack-ai#21704/`. Stage 20's ACCEPT (`5998488373`) kept it for this reason and sent it to the needles' stage. Readers of the seven rewritten strings: none. `git grep -F` at HEAD over the tracked tree outside the 12 files, with the full literal, a 24-character window around each id, and the text on each side of each id (29 needles over all 17 sites): the only hits are the readers of the kept strings named above, the lit control (`composeStacks` in `stack.zod.ts`) hits and the dark control does not. The same needles searched inside the 12 files, outside each literal's own span: the only hits are two code comments beside `:322`. The five short needles (`cloud#2172`, `objectstack-ai#16495`, `// [objectstack-ai#4610]`, `objectstack-ai#5015`, `objectstack-ai#21704`) fall under the script's 12-character floor, so their readers were confirmed by direct `git grep -F` with a dark control. ## Cited records Read with their comments as the API serves them: objectstack-ai#22114 (8 of 8 comments; landed as PR objectstack-ai#22126), objectstack-ai#14149 (12 of 12; ruling A `5507504961`, landed as PR objectstack-ai#15113), objectstack-ai#19939 (15 of 15; pass 1 landed as PR objectstack-ai#22259, the card stays open), objectstack-ai#22093 (17 of 17; PRs objectstack-ai#22125, objectstack-ai#22309 and objectstack-ai#22322), and the four PRs themselves. objectstack-ai#19939 is still open: its pass 1 refuses the `{token}` dialect in flow value slots and keeps two spellings (the date macros and `{$User.*}`) until CEL can write them. The describe's PRESERVATION test still accepts those two, and the title keeps the record's own verb, "retired", as PR objectstack-ai#22259 wrote it. The title and the test body say the same thing the record says. ## Verification At head `8885dbf1c` (one commit on base `11d119ab1`): - **Text only.** `textonly28.cjs` (stage 28's, md5 `957eff6b3837d762b8e03d070155930a`) on all 12 base copies against their heads: 12 / 12 SAME. 7 changed tokens, as predicted in writing at 2026-10-09T03:08Z before any edit or test run: 6 titles and 1 declared string (`--declared 118`). Every other string token, identifier, number, punctuation mark and comment is byte-equal. 16 controls, expectations written in the script before the first run, 16 / 16 as predicted: an identifier rename, a numeric literal, a comment edit, an undeclared expect message, a rewritten title given a new id, an id-free title edited, one title reverted to base (SAME, 0 changed), a declared label without `--declared`, a declared line plus another changed string, the declared line alone (SAME, 2 changed), a title re-split into a plus chain, a test added, an untouched file (SAME, 0 changed), an id appended to a rewritten title, a kept needle rewritten, a kept hex colour rewritten. The two controls that mutate a string beside the declared line fail at the mutated line, not at 118. - **Tests, 12 files, base and head.** `--project local --project repo` with the JSON reporter, 618 tests in 88 suites each side, all passed. Per-file test count and status sequence identical in 12 / 12. 23 full names changed (4 + 14 + 1 + 3 + 1), 0 mismatches against the plan. Names carrying `#` plus digits: 23 at base, 0 at head. Duplicate full names: 3 and 3, the same three `[object Object]` it.each rows at both ends. - **Full spec unit tier at the head**, under the verify lock: `Test Files 626 passed (626)`, `Tests 18743 passed | 1 todo (18744)`. - **Build and typecheck**, under the verify lock: `turbo run build` over `packages/*` and `packages/*/*`, `Tasks: 71 successful, 71 total`; `@objectstack/spec` `typecheck` exit 0 with `check:test-typecheck` holding 52 files / 246 errors / 135 pinned signatures, the same figures as stage 29; the 12 files are all in the `tsconfig.test.json` program. - **Gates.** `dispatch-gates.mjs --commands` at the head derives 79 (stage 29's 77 plus `check:authorable-surface` and `check:yaml-examples`); all 79 exit 0, and `--ran` reconciles 79 derived, 79 run, 0 NOT-MEASURED. The five artifact-roster families that keep their roster in a directory one of the paths is in (`check:meta-url-spelling`, `check:spec-changes`, `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`) and `check:generated` (all 15 artifacts up to date) also exit 0. - **ESLint**, `--no-inline-config`, 12 files: 0 errors, 0 warnings. Population from ESLint's own config: 12 configured, 0 ignored, 0 with a type-aware parser option, so this diff cannot move the verdict of a file it does not touch. - **Skip-changeset.** `npm pack --dry-run --json --ignore-scripts` in `packages/spec`: 2069 files, 0 `*.test.ts`, 0 of the 5 edited files; controls `src/stack.zod.ts`, `dist/index.mjs` and `package.json` present. The rewritten expect message occurs in 0 files of `dist/`; the control `Unrecognized key` occurs in 42. Nothing published changes. - **Governed.** `check-governed-merges.mjs --test` on the 5 paths: 0 of 5, not governed; 14 changed lines. - **Merge.** `git merge-tree --write-tree` onto `origin/main` `e75dceddd`: clean. - **Bytes.** 0 added lines carry `#` plus digits; 0 control bytes in the changed files. Declared narrowing: the 12-file base and head comparison ran outside `os-verify-lock.sh`, after three queue turns (about 28 minutes) ended without a grant. It is a 12-file run with two workers; the workspace build, the typecheck and the full unit tier all ran under the lock. The gates are `check:*` runs, which do not use the lock. ## Acceptance notes - **Regrowth continues.** Since stage 27's landing, four PRs (objectstack-ai#22125, objectstack-ai#22126, objectstack-ai#22259 and objectstack-ai#22322) added 7 messages / 8 ids to test strings in files that already existed, one of them to a title objectstack-ai#22322 wrote while stripping a ruling date from a describe. Test files sit outside `check:doc-authoring`'s ledgered leg, and the ruling adds no gate, so the per-stage census is the only instrument. An observation about the burn-down's denominator, not a class a / b / c finding. - **Comments are untouched.** Code comments in these files still cite ids (for example `// ─── assignment (objectstack-ai#14149) ───` at `builtin-node-config.test.ts:432`); comments are objectstack-ai#20234's share. --- _Generated by [Claude Code](https://claude.ai/code/session_01DhTqaEHqPVSVnAkjG3jywn)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #22343
Clause-②: no (narrowing: an undeclared key on the shared retry policy is refused at parse wherever it is written, a job's retryPolicy and a try_catch node's retry, and an undeclared try_catch config key at the build doors and the save door, where each passed)
The claim (
6069618073) declaredyes. A pure narrowing isno(references/execution-duties.md:105), so the seat corrected the spelling after the contract review6072339018; the parenthesis is unchanged. It is worded to what the census decided: no writer relies on the strip, so the sharedRetryPolicySchemais closed and every one of its parsers narrows, not thetry_catchslot alone. Per triage6068085630, the importers named that way arejob.retryPolicyand atry_catchnode'sretry.What changes
shared/retry-policy.zod.ts.RetryPolicySchemais astrictObject(it was a plainz.object). A key it does not declare is refused at parse, naming the key, with a curated near-miss table:maxRetry,retriesandattempts→maxRetries;initialDelayMsandbaseDelayMs→backoffMs;maxDelayMs→maxRetryDelayMs. AmaxAttemptsgets a prescription instead of an alias, because it counts the first attempt. TheretryDelayMstombstone stays, with its rename. The docblock records the census.automation/flow-node-config-refusals.ts.BUILTIN_KEYS_JUDGED_AT_REGISTRATIONis deleted.try_catchwas its only member, and an empty exception set would be dead code.builtinNodeConfigKeysJudged(type)is nowgetBuiltinNodeConfigContracts().has(type), so the key arm judges all 13 builtins. The key arm also refuses a tombstoned key the author wrote, at its own path and in the tombstone's words.scriptis excluded (RETIRED_KEYS_JUDGED_ELSEWHERE), because the lint names its retired keys and no door before the run ever judged them. See "Route change" below.service-automationengine.ts: comments only.validateNodeConfigKeysalready asksbuiltinNodeConfigKeysJudged, so it now stands aside fortry_catchand judges plugin node types only. Its docblocks are corrected.try-catch-and-retry-policy-undeclared-keys-refused, plus aSTEP18_RATIONALEfragment with order 90; the highest order onmainwas 89.migrations/registry.tswas regenerated withgen:migration-registry. The step-18 entryflow-builtin-node-config-undeclared-keys-refusedand its fragment saidtry_catch"stays registration's", which this change makes false in the same unreleased step, so both now point at the new entry..changeset/22343-retry-policy-try-catch-undeclared-keys-refused.mdbumps@objectstack/specminor(BREAKING, ADR-0087registered) under the same pre-mode convention as2f70c2222(.changeset/pre.json: modepre, tagnext). It carries the Clause-② line above. It has no@objectstack/service-automationentry: that package's diff is comments plus one test, and no gate asked for one (check-changeset-no-major,check-adr-0087-registrationandcheck-empty-changesetare green).content/docs/automation/flows.mdx: theconfigrow and the strictness callout now listtry_catchamong the judged builtins, and one sentence saysretryis closed.jobs.mdxgets one sentence sayingretryPolicyis closed. The strictness ledger's audit row (docs/audits/2026-07-unknown-key-strictness-ledger.md) said the policy "is still non-strict", which is no longer true, so it is corrected.automation/flow.zod.ts(seat order6071818310).Flow.errorHandling's refusal ofmaxAttemptssaid a bare rename tomaxRetrieswould run "one attempt fewer" than asked for. It runs one more:maxAttempts: 3is 3 runs, andmaxRetries: 3is 1 + 3 = 4. The clause now says "one attempt more". The prescriptionmaxRetries: <maxAttempts - 1>is unchanged. Theflow.test.tscase is now the pin, and its comment, which carried the same inversion, is corrected. TheRetryPolicySchemapin inretry-policy.test.tsgains the same two assertions, so both surfaces assert the same four things: the prescription, "one attempt more", never "fewer", and no bare rename. The changeset gains oneAlso corrected:line.Census (H1), at
origin/mainb7fdd5085Parsers of
RetryPolicySchema:system/job.zod.ts:366JobSchema.retryPolicyjobsautomation/control-flow.zod.ts:330TryCatchConfigSchema.retrytry_catchnodesautomation/flow.zod.ts:1224Flow.errorHandlingretryPolicyShape()into its ownstrictObject, already closedintegration/connector-fetch-policy.ts:49contracts/job-service.ts:131JobRetryPolicyservice-jobrun-with-policy.ts:17Writers checked for a key outside the five (
maxRetries,backoffMs,backoffMultiplier,maxRetryDelayMs,jitter):examples/app-showcasejobs (:22) and flows (:1212,:1425),examples/app-todo,content/docs/automation/flows.mdx(:798,:1856) andjobs.mdx(:25,:243), theservice-automationandservice-jobREADMEs, every test fixture that writesretry:under atry_catchor a jobretryPolicy:(lint, service-automation, service-job, spec), and theretry-policy-convergedconversion's output and fixtures. 0 undeclared keys.a58626c88d:preview-samples.ts:331(retryPolicy: { maxRetries: 3 }); the flow inspector's fallbacktry_catchform, which writeserrorVariableonly; and the descriptor-driven form, which reads the descriptor that closesretryto the five keys. 0 undeclared keys.sys_metadatawriter of this shape was found. hotcrm and deployed metadata were not measured.Verdict: no writer relies on the strip, so the shared schema is closed. No writer sits outside
packages/spec,service-automationorservice-job, so the stop condition does not apply.Hypotheses, measured
retrystrict, the key arm judgestry_catchlike the other 12. Docblocks that named the exception were corrected inflow-node-config-refusals.ts(module header, the value-arm carve-out, the predicate, the main arm's bullet, the inline comment), inflow.zod.ts(two comments), inengine.ts(three comments), and in the two spec test headers.installBuiltinNodeslists the 17 builtin descriptors with theirconfigSchemaand judged state. Before: the walk still reached["try_catch"]. After:[]. The remaining builtins publish noconfigSchema(decision,wait,connector_action) or are exempt (assignment). "Refused once" is pinned inconfig-unknown-keys.test.ts: one parse issue, and noundeclared config key(s)text from the walk.maxRetry. It is 3 edits frommaxRetries(y→i, +e, +s) against a budget of 2 for an 8-letter key:findClosestMatches('maxRetry', …, 2)returns[]. The measured door first printed the refusal with no did-you-mean. It now answers`maxRetry` → `maxRetries`through analiasesrow, the mechanismFlow.errorHandlingalready uses for the same vocabulary. The refusal is located (nodes.N.config.retry.maxRetry, andjobs.N.retryPolicyfor a job) and uses thenode-config-refused-by-contractshape from build: ascriptnode's undeclared config key passesobjectstack validate,compileandregisterFlow, then fails every run — the key half of #21898's class (subflowby reading) #21982.Route change: the
retryDelayMstombstone (registration would otherwise have widened)The key arm judged
unrecognized_keysonly, and both arms skip a tombstone (invalid_typeexpectingnever). Theretry-policy-convergedconversion renamesretryDelayMstobackoffMs, but it keeps both spellings when the values differ, and it leaves anull. The walk refused what survived, as an undeclared key. With the walk standing aside and the arms unchanged,registerFlowwould have accepted those two variants and the run would have refused them. So the key arm now refuses a tombstoned key the author wrote on every judged type.scriptis the exception, so this change narrows nothing on any other builtin: onlyscriptandtry_catchcarryretiredKey()in their contracts, measured by grep over the five contract modules. The ruling's intent ("one judge", andregisterFlowwidens nowhere) is what decided this. It is named here for the contract review.Registration verdicts, before and after (10-variant probe on the built packages)
try_catchconfigregisterFlowregisterFlowretryretry.bogusKeynodes.1.config.retry.bogusKeyretry.maxRetrynodes.1.config.retry.maxRetryretryDelayMsaloneretryDelayMsequal tobackoffMsretryDelayMsdiffering frombackoffMs…retry.retryDelayMsretryDelayMs: null…retry.retryDelayMsbogusKeynodes.1.config.bogusKeytry.bogusKey(region)retry: 5Every variant registers or is refused exactly as before; only the judge moved. A direct
FlowSchema.parseordefineFlow()(no conversion) now meets the tombstone for a pre-17retryDelayMs, like every other retired spelling.Measured at the CLI door
Fixture projects (
defineStack(…, { strict: false })), at9f71db481, with the built CLI closure:os validate --jsontry_catchretry: { maxRetry: 2 }customatflows.0.nodes.1.config.retry.maxRetryretryPolicywithmaxRetry: 3unrecognized_keysatjobs.0.retryPolicyretryDelayMsaloneretry-policy-convergedconverted it)examples/app-showcasevalidates clean (exit 0,valid: true; 2try_catchwithretry, 1 job withretryPolicy), and so doesexamples/app-todo(exit 0).Ablation
At HEAD
9f71db481,RetryPolicySchemawas mutated back to a plainz.objectthroughscripts/ablation-replace.mjs: anchor 1 → 0, blobc06e5bd1a6→dbd76790f2. The spec was rebuilt, andablation-dist-preflightfound the marker present in 20 built files. Results:packages/cli/test/retry-policy-key-validate-door.test.ts): 1 failed, 1 passed. The refusal went red (exit 0 where exit 1 was expected) and the control held.Restore: the blob is back to
c06e5bd1a6== HEAD andgit diff HEADis empty. After a rebuild, the preflight with--absentfound the marker in none of 232 built files and the tree clean, and the door pin was green again (2 passed).Tests (real readings)
At
b7fdd5085(merged withorigin/main6a53564b9):@objectstack/spec: 626 files, 18751 passed, 1 todo.@objectstack/service-automation: 178 files, 2179 passed.@objectstack/lint: 128 files, 5853 passed.--project integration: 2 passed.spec,service-automationandcli: exit 0. The test-layer debt is unchanged (spec 52 files / 246 errors held, cli 3 / 28).At
c915191dc(the in-place fix round, no merge ofmain):@objectstack/specsrc/automationplusretry-policy.test.ts, 34 files, 1134 passed, and the spec typecheck exits 0 (debt held at 52 / 246). Ablation: putting back "fewer" withscripts/ablation-replace.mjsturned theflow.test.tspin red (1 failed). The blob was restored toc4c6c4453d== HEAD.At
a77cb02b1:@objectstack/service-job11 files, 117 passed. The@objectstack/cliunit tier, 270 files, 3968 passed; it includes the tier-partition pin, and the new door pin lands inintegrationby behaviour. It runs per PR because it is not named.e2e.Narrowed eslint (
--no-inline-config) over the 12 changed.tsfiles: 12 files, 0 errors, 0 warnings, none ignored. This config enables no type-aware linting (eslint.config.mjs:327), so the diff cannot move any untouched file's verdict. The repo-widepnpm lintis CI's.Gates
node scripts/pm/dispatch-gates.mjs --commandsatb7fdd5085derived 115 commands, the same list as ata77cb02b1, and all 115 ran with exit 0. The--ranverdict:115 derived famil(ies) accounted for — 115 run, 0 NOT-MEASURED (a DERIVED zero — all 115 recorded an exit code and none of them is 3).At
c915191dc, the round's 4 paths derived 85 commands, every one of them already among the 115. The full derivation is unchanged at 115. All 85 exited 0 (--ran: 85 run, 0 NOT-MEASURED). Ten dist-reading gates first exited 3 on a fresh worktree, then exited 0 after a build.Two gates needed a second run:
check:type-check-debthit the per-command 300 s cap. Re-run alone, it exited 0 in 123 s.a77cb02b1, five spec gates exited 3 (specdistpredated a test edit) andcheck:dual-build-cjs-loadsexited 3 (8 packages had nodist). After building them, all six exited 0.The roster gates whose roster sits under a touched directory also exited 0:
check:meta-url-spelling,check:authz-resolver,check:error-code-casingandcheck:filter-alias-parity.Acceptance notes
Flow.errorHandlinggets no did-you-mean formaxRetry, the same distance gap closed here on the shared policy. Its own alias table has no such row. It is a sibling surface outside this card's scope, and the key is still refused loudly. Carrier: none.scriptnode carrying a retired dispatch key (actionType, …) is refused at error, with exit 1, byos validateandos lint(ruleexpression-invalid, measured at6fbc005a6). The save door answers 200 and stores the body with the key removed: the retired conversionflow-node-script-branch-keys-removedreplays on the save path and logsOS_METADATA_CONVERTED. So nothing reaches the run from either door.defineStackand a directregisterFlowwere not measured.RETIRED_KEYS_JUDGED_ELSEWHEREkeeps the key arm out ofscriptdeliberately. Carrier: none (finding 9 of the contract review6072339018, measured in round 36073081304).try_catch)":packages/runtime/src/domains/automation-put-post-error-parity.test.ts:50andautomation-register-error-class.test.ts:79. After this PR, the walk judges plugin node types only. They are comments in test files that do not ship. Carrier: the next edit of either file.Line budget
17 files, +846 / −143 against the merge base (the in-place fix round: 4 files, +13 / −4; round 3: the changeset's
Clause-②:value, +1 / −1). 0 governed paths.Generated by Claude Code