Repository navigation
Commit f606ba2
Part of #23543
Clause-②: yes (narrowing)
Regen-provenance: since the reviewed head 0bc4f42, dce2266 rewords
the D3 entry's `reason` (prose only, one file, at the seat's request)
and 2741849 merges origin/main at f846cc2 through
os-regen-merge.sh with check:generated clean; every other file's change
equals the reviewed head's.
This PR lands family **F2** of ruling `6182001882` (item 2, the loud
form) only. F4 and F5-http stay on #23543, one PR each, in the ruled
order; #23543 remains open.
## What
Ruling F2 A: "a `job.retryPolicy` block whose `maxRetries` is omitted or
0 is refused, in the shape `flow.errorHandling` already uses for
`strategy: 'retry'` … A job with no block stays a single attempt;
`backoffMultiplier` keeps its declared default and is not refused.
`retry-policy-converged` then sets `retiredFromLoadPath`; the 17
materialization lives on in the D3 chain."
- **The refusal lives on the job, not on the shared schema.**
`RetryPolicySchema` (`shared/retry-policy.zod.ts`) is also a `try_catch`
node's `retry` region, which the ruling does not cover. So `JobSchema`
gains one root-level refine, `job.retryPolicy === undefined ||
job.retryPolicy.maxRetries >= 1`, reporting a `custom` issue at
`retryPolicy.maxRetries` with the ruling's prescription as its first
sentence: "A retry block with no attempts is no retry block: state
`maxRetries` ≥ 1, for example 3, or delete the block." It then names why
(the loader used to write `maxRetries: 3`; a retry re-runs whatever the
failed attempt did) and ends with the house `os migrate meta --from 16`
sentence. At `defineStack` this is `STACK_SCHEMA_INVALID` / 422 at
`jobs.N.retryPolicy.maxRetries`.
- **Omitted and 0 are one case by construction.** The refine reads the
parsed policy, where the default has already filled an omitted count
with 0. No pre-default hook is needed.
- **No new dropped-refinement site.** The refine sits on the job's root,
a site `dropped-refinements.baseline.json` already records for
`system/Job` (the `pull` exclusivity refine). A field-level refine would
have added a ledger site to `system/Job` and to every schema that embeds
a job.
- **The `retryPolicy` describe** no longer says "declaring it without
`maxRetries` also means no retry"; it says a block must state
`maxRetries` >= 1. `content/docs/references/system/job.mdx` is
regenerated from it.
- **The D2 entry.** `retry-policy-converged` gets `retiredFromLoadPath:
true` and `retiredAfter: '17.7.0'`, the stamp F1, F5-jsx and F3 used.
Its "recorded verdict at 18" paragraph is rewritten as the retirement
record. The fixture is unchanged: every `includeRetired` seam still
replays the whole entry.
- **The D3 entry.** `18.job-retry-policy-without-attempts-refused`
(semantic, step 18) with `conversionIds: ['retry-policy-converged']`,
already a step-17 member. It states the decision no conversion makes for
the author: keep the written count, or delete the block. It also covers
the one silent change the ruling accepts: a block that states
`maxRetries` and omits `backoffMultiplier` now gets 1 where the loader
wrote 2.
- **The verdict-list pin** in `conversions.test.ts` moves the entry to
the retired list. The kept list now holds three entries:
`flow-node-http-callout-rename`,
`page-component-visibility-to-visibleWhen` and
`view-visibleOn-to-visibleWhen`. The removal-version pin's `JOIN` gains
one `retry-policy-converged` row: the job refusal, which names
`@objectstack/spec 18` and `--from 16`.
## Mechanism assumptions, measured
1. **Who else parses `RetryPolicySchema`.** It holds. Its parsers are
`job.retryPolicy` (`system/job.zod.ts`) and a `try_catch` node's `retry`
(`automation/control-flow.zod.ts`). `flow.errorHandling` builds from
`retryPolicyShape()`, and `worker.zod.ts` has its own
`TaskRetryPolicySchema`. So the refusal is on `JobSchema`. A control
pins that `RetryPolicySchema.safeParse({ backoffMs: 5000 })` still
succeeds.
2. **Omitted versus 0.** It holds: judging the parsed value is enough
(above). Both spellings are pinned at the key.
3. **The entry and the pins.** It holds as described. The kept list is
3. The `JOIN` row is owed, because `retiredAfter` 17.7.0 is the newest
published stable release.
4. **The `retryDelayMs` half.** It needs nothing beyond the retirement.
Both surfaces carry the `retryDelayMs` tombstone, and a new case in
`conversions.test.ts` pins an authored flow-level `retryDelayMs` refused
at `defineStack` naming `backoffMs`. Two existing tests relied on the
load converting it, and both were re-pointed:
- `flow-builtin-node-config-keys.test.ts` "CONTROL: a pre-17
retryDelayMs alone is converted first where the door converts" now reads
the replaying seam (`applyConversionsToFlow(…, { includeRetired: true
})`, which parses) and the authoring door (`defineStack`, refused at
`config.retry.retryDelayMs` naming `backoffMs`).
- The cli `retry-policy-key-validate-door.test.ts` "declared" fixture
now writes `backoffMs`.
5. **The four load-path suites** (F3's record `6205630082`). The
measurement that decides them: `os migrate meta` replays from
`MIGRATION_SUPPORT_FLOOR` 16, so `--from 16` replays steps 17 and 18,
and after F2 every entry of both is retired. The only entries still live
on the load path are toMajor 11 and 15 (F4's two, F5-http's one), and
the chain never replays those. So no live entry can be both converted by
the load and listed by the chain. Per suite:
- `packages/spec/src/stack-authored-input.test.ts` gets a **synthetic
probe**. Its subject is the load pass itself, not an entry, so a
`vi.mock` of `./conversions/registry` appends a test-owned live
conversion (`stack-authored-input-probe`, the old `retryDelayMs`
rename). Every assertion is unchanged and now reads the probe's id. No
later family has to re-point this suite again.
- `packages/cli/test/migrate-meta-load-conversions.test.ts` is **partly
retired and partly re-pointed**. A probe cannot reach it: the temp
project loads the built `@objectstack/spec` through a symlinked
`node_modules`, outside vitest's module graph.
- Retired: "lists a conversion the load already applied …" (the defect's
chain reading), which has no subject on this tree. Its write / re-run /
clean-reload half is pinned at the same layer by the
two-refused-spellings case, as the file header records.
- Re-pointed: "the authored-source load reads the argument off the
default export" and "defined twice starts from the innermost argument".
They now use a discriminator that needs no conversion: a strict
`defineStack` fills the declared defaults (`backoffMultiplier: 1`,
`maxRetryDelayMs: 30000`, `jitter: false`), and the authored argument
does not have them. I measured this on the built dist before writing it.
- The old defined-twice chain case moved to the refused arm, where
`retryDelayMs` now lives. The mixed case was retitled "two refused
spellings the chain converts".
- `packages/cli/test/migrate-meta-composed-load-conversions.test.ts`
gets the same treatment, for the package body.
- `packages/cli/test/migrate-meta-composed.test.ts` (its load-path row)
is **kept and renamed**. Its assertions still hold, but its name was
false. It is now "a refused stack carrying a second refused spelling".
6. **Authored sources.** I swept `examples/**`, `packages/**`,
`content/docs/**` and `skills/**` for every object-literal `retryPolicy`
block, single- and multi-line, plus non-literal values. No job block
omits `maxRetries` or writes 0. The hits are `hook.retryPolicy` (a
separate declaration, outside the ruling) and this PR's own fixtures.
One test fixture wrote `maxRetries: 0` on a job (`job.test.ts`,
"one-time scheduled job"). It means a single attempt, so the block was
deleted. `skills/**` has nothing to report.
7. **Serial neighbours.** PR #23781 and PR #23784 had not landed.
`origin/main` moved 5 commits (none in this PR's files) and was merged
through `scripts/pm/os-regen-merge.sh` with no conflict.
`check:generated` then read "All 14 generated artifacts are up to date".
8. **The metadata save door** (`protocol.ts`) is untouched. For a
non-flow type it judges the body as stored, with no conversion replay,
so a job body meets this refusal there. `job` has not been
runtime-creatable since 17, though, so no door claim is made for it. The
flow half (`retryDelayMs`) meets the door's flow fallback exactly as
before, because that fallback already replayed this entry while it was
live. The seat's separate save-door decision covers it with the other
retired flow entries.
## Readers updated (prose made false by the retirement)
- `content/docs/automation/jobs.mdx`: "declaring `retryPolicy` without
stating a count still means *no retry*" is now the refusal rule.
- `packages/services/service-job/src/run-with-policy.ts` (comment only):
the default is reached only by a hand-built caller.
- `shared/retry-policy.zod.ts` (two comments), `liveness/flow.json` (one
note) and the `job-timeout-to-timeout-ms` docblock in `registry.ts` no
longer call the entry a load-path rewrite.
## Changeset
`.changeset/23543-f2-job-retry-policy-without-attempts-refused.md`:
- **Bumps:** `@objectstack/spec` minor; `@objectstack/service-job` patch
(comment-only).
- It is BREAKING and carries `Clause-②: yes (narrowing)`. It gives FROM
→ TO: `retryPolicy: {}` → `retryPolicy: { maxRetries: 3 }` or delete the
block, and a block with `maxRetries: 0` → delete it. It has a "What an
author who still writes it sees" paragraph, the `backoffMultiplier` 2 →
1 note, the `os migrate meta --from 16` path, and how stored rows keep
loading.
- **ADR-0087 disposition:** `registered
job-retry-policy-without-attempts-refused`.
## Verification
All readings are on head `0bc4f428` (the merge of `origin/main`
`46ca793e`) unless noted.
- **`@objectstack/spec`:** full `test` 683 files / 20888 passed (1
todo), exit 0. `typecheck` exit 0 (pre-merge `9e09e0dd`; the merge
brought no change to this PR's files). `build` + `check:generated`: "All
14 generated artifacts are up to date".
- **`@objectstack/cli`:** the full `unit` tier passed, 297 files. The
first run had 295 passing, plus the 2 build-dependent published-subpath
pins, which failed only with "packages/cli is not built" and passed 2 /
29 after `pnpm --filter @objectstack/cli build`. 4348 tests passed, 29
skipped. Integration tier: the one edited file,
`retry-policy-key-validate-door.test.ts`, 2 / 2. `typecheck` exit 0,
after building the closure with declarations.
- **`@objectstack/service-job`:** `test` 11 files / 117 passed.
`typecheck` exit 0.
- **Gates.** `dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 114 commands on `0bc4f428`. Each ran
one by one, with its exit code captured before any pipe. `--ran` reports
"114 derived famil(ies) accounted for — 111 run, 3 NOT-MEASURED (3
DERIVED from a recorded exit 3)". The three are the prerequisite
refusals F3 also recorded: `check-plugin-teardown-shape --self-test`
(shallow clone), `check:skill-examples` (client dist absent) and
`check:dual-build-cjs-loads` (full build absent). Notable greens:
`check-adr-0087-registration --base origin/main`,
`check-changeset-no-major`, `check-empty-changeset`,
`check:dts-closure`, `check:nul-bytes`, `check:liveness`,
`check:spec-changes`, `check:upgrade-guide`.
- **Ablation, the refusal** (`scripts/ablation-replace.mjs`, on
`9e09e0dd`). `job.retryPolicy.maxRetries >= 1` was replaced by `>= 0`.
- On-disk proof: anchor 1 → 0, blob `28e227b31f4e` → `c6c4d7684caa`.
- Result: 7 tests went red across `job.test.ts` (3),
`conversions.test.ts` (3, including the stored-row case's "the row as
written is refused" leg) and `retired-key-removal-version.pin.test.ts`
(the `retry-policy-converged` `JOIN` row). Direction: red.
- Restore proven: blob == HEAD and `git diff HEAD` empty. Spec tests
import `src`, so there is no dist leg.
## Acceptance notes
- Labels: this dispatch's write budget names no `label-write`, so this
PR carries none from me.
- The refusal is a root-level refine, so zod reports it only once the
job's other fields pass. A job with another error and an empty retry
block shows that error first and this one after it is fixed. A
field-level refine would report both at once, at the cost of new
dropped-refinement ledger sites in every schema that embeds a job.
- `tsc` does not see the omission: `maxRetries` stays optional in
`RetryPolicy`, because the shared input type also serves `try_catch`.
The parse-time prescription is the channel for this family.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01MT2aGLQQhH8ZsPrgqHMrwz)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent af3873b commit f606ba2
18 files changed
Lines changed: 519 additions & 202 deletions
File tree
- .changeset
- content/docs
- automation
- references/system
- packages
- cli/test
- services/service-job/src
- spec
- liveness
- src
- automation
- conversions
- migrations/entries/semantic
- system
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
385 | 385 | | |
386 | 386 | | |
387 | 387 | | |
388 | | - | |
| 388 | + | |
389 | 389 | | |
390 | | - | |
391 | | - | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
392 | 394 | | |
393 | 395 | | |
394 | | - | |
| 396 | + | |
395 | 397 | | |
396 | 398 | | |
397 | 399 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
61 | 61 | | |
62 | 62 | | |
63 | 63 | | |
64 | | - | |
| 64 | + | |
65 | 65 | | |
66 | 66 | | |
67 | 67 | | |
| |||
0 commit comments