Repository navigation
[finding][spec] #6414 retired the L2 ETL layer, but retry-policy.zod.ts still teaches ETLPipeline.retry as a live surface in six places — including the retryDelayMs tombstone an upgrading author actually reads #6630
Description
Found during a read-only truth sweep of packages/spec's text surfaces against the mechanisms they name. Filed unassigned for triage.
This is the same shape as #5554 / #6383 (prose still advertising connector Rate Limiting after #4911 retired it), applied to the #6414 ETL retirement — and one of the six sites is an error template, not a comment.
The defect
#6414 removed automation/etl.zod.ts outright: ETLPipeline, ETLPipelineRun, their source/destination/transformation vocabulary, four enums and the ETL factory — 9 defs, 27 exported names. The absence is pinned in packages/spec/src/automation/sync-retirement.test.ts:147-158:
for (const alsoRetired of [
'ETLPipeline', 'ETLPipelineSchema', 'ETLPipelineRun', 'ETLPipelineRunSchema',
'ETLSource', 'ETLSourceSchema', 'ETLDestination', 'ETLDestinationSchema',
'ETLTransformation', 'ETLTransformationSchema', 'ETLEndpointTypeSchema',
'ETLTransformationTypeSchema', 'ETLSyncModeSchema', 'ETLRunStatusSchema',
'ETL',
]) {
expect(
automationNames,
`./automation must not export ${alsoRetired} (#6414, L2 retired on L1's reading)`,
).not.toContain(alsoRetired);
}packages/spec/src/shared/retry-policy.zod.ts knows this — exactly one stanza in the file was updated (L30-35):
* - `automation/etl.zod.ts` → `ETLPipeline.retry` (#4962; the whole L2 layer
* was retired at #6414, so this surface no longer exists — the convergence
* is recorded because it is how the divergence was FOUND) — spelled the count
* `maxAttempts`, defaulted it to **3** ...
Every other mention in the file stayed in the present tense.
1. The retryDelayMs tombstone — the error text, retry-policy.zod.ts:120-126:
retryDelayMs: retiredKey(
'`retryDelayMs` was removed in @objectstack/spec 17.0.0 (#4661, #4964) — the retry policy now ' +
'has ONE spelling for its base delay across every surface that carries it: `job.retryPolicy`, ' +
"a `try_catch` node's `retry`, `flow.errorHandling` and an ETL pipeline's `retry`. " +
'Rename the key to `backoffMs`; the value (milliseconds before the first retry) ' +
'is unchanged. `os migrate meta --from 16` rewrites it for you.',
),2. RetryPolicySchema's own TSDoc, retry-policy.zod.ts:130-134 (present tense, and {@link}-bearing, so it reaches generated reference docs):
* Exponential-backoff retry policy — the named schema for `job.retryPolicy` and
* a `try_catch` node's `retry` region. `Flow.errorHandling` and
* `ETLPipeline.retry` carry the same contract via {@link retryPolicyShape},
* which they must, being `.strict()` (see that function's note).
3. retry-policy.zod.ts:80-82:
* Two of the four surfaces that carry this policy cannot simply reference
* {@link RetryPolicySchema}: `Flow.errorHandling` and `ETLPipeline.retry` are
* `.strict()` (`strictObject`, the #4001 campaign standard) ...
4. retry-policy.zod.ts:149-155 — present tense, and it points at a tombstone that no longer exists:
* The ETL half needed no conversion branch and deliberately has none: an ETL
* pipeline is not a `defineStack` collection and `etl.zod.ts` has no parse site
* anywhere in objectstack / objectui / cloud (批 12's measurement), so there is
* no stored document for a D2 walker to reach. Writing one anyway would be a
* conversion advertising coverage it does not have. The `maxAttempts` tombstone
* on that block is the whole migration channel, and it reaches the only door
* that exists — `tsc` at the authoring site, and the parse.
There is no maxAttempts tombstone. Grepping maxAttempts across packages/spec/src (excluding tests) returns only prose references in this file and in conversions/registry.ts, plus an unrelated local in shared/resilient-fetch.ts:54 and an unrelated alias in ai/agent.zod.ts:85 (maxAttempts: 'maxRetries'). The tombstone went with the block it sat on.
5. The arithmetic, retry-policy.zod.ts:42 — "the four surfaces share one declaration". Three now.
6. automation/flow.zod.ts:665-667:
* The retry knobs are the converged `RetryPolicySchema` contract, shared with
* `job.retryPolicy`, a `try_catch` node's `retry` and an ETL pipeline's
* `retry` (#4661 + #4964 — see `shared/retry-policy.zod.ts`).
with the same stale arithmetic twice more in the same file — flow.zod.ts:745-747 ("A key added to the policy lands on all four surfaces at once") and flow.zod.ts:752-753 ("the flow surface has a reading the other three do not").
Adjacent, same cause, likely same fix: conversions/registry.ts:3736 ("The two surfaces this entry grew to cover") and :3750-3761, which still says "Its maxAttempts tombstone carries the rename AND the default change, and the tombstone reaches the only doors that exist". Note the registry's step list WAS updated at #6414 (registry.ts:1227-1228 records etl-retry-converged-onto-retry-policy as "ABSORBED by etl-pipeline-layer-retired"), so this is the same partial-sweep, not a separate decision.
The authority
packages/spec/src/automation/sync-retirement.test.ts:147-158— the export-absence pin quoted above.packages/spec/src/automation/index.ts:15-32— the retirement note: "etl.zod.ts… was removed here (L2 ETLPipeline 在本仓无任何执行侧消费者(仅 spec 自身 + 生成文档),而 SYNC_ARCHITECTURE.md 把它作为 L1 退役后的推荐去处 #6414, ADR-0049 enforce-or-remove, protocol 17) … Multi-source, multi-stage movement has no protocol surface at all now — deliberately, because it had no executor".packages/spec/src/migrations/registry.ts:2623-2674— the D3etl-pipeline-layer-retiredsemantic migration, which states the consequence directly: "tscreports TS2724/TS2305" for theETLfactory from@objectstack/spec/automation.retry-policy.zod.ts:30-35itself — the one stanza that got the update, and therefore the in-file proof that the other five are drift and not a deliberate historical framing.
Contrast with the sites that were correctly re-aimed, which is what shows the intended pattern: integration/connector.zod.ts:105 ("This used to point at automation/etl.zod.ts; L2 was retired…") and automation/index.ts:49 ("etl.zod.ts … that pointer is gone rather than re-aimed").
Why it matters — the authoring path
Site 1 is the load-bearing one. retiredKey() is not a comment; it is the upgrade channel, and shared/retired-key.ts:15-32 says so in its own words:
* A tombstone keeps the key declared but makes it unwritable, so the removal
* lands in the two channels an upgrading author — very often an AI (ADR-0033) —
* actually reads:
* 1. **`tsc`.** ...
* 2. **The parse.** A value reaching the runtime raises the prescription
* itself — not a generic "unrecognized key". ...
* ... an agent bumping `@objectstack/spec` sees THIS string, not our docs site.
So the one string guaranteed to reach an author or agent migrating off retryDelayMs currently enumerates four surfaces where the converged spelling applies, and one of them was deleted in the same major. Concretely, an AI author reading that message is told an ETL pipeline's retry is a live place to write backoffMs. If it acts on that — writes or keeps an ETLPipeline — the failure is tsc TS2724/TS2305 on a name the spec's own upgrade prescription just recommended. That is the message spending its credibility to send the reader somewhere that no longer exists.
Sites 2 and 3 additionally flow into generated reference pages (the {@link} and module-TSDoc surfaces #6085 and #6383 were filed about), so the retired layer keeps a published footprint.
Site 4 is the subtler harm: it tells a future maintainer that a migration channel exists for the ETL rename ("the maxAttempts tombstone … is the whole migration channel"). It does not. Anyone auditing "is the #4962 convergence fully migrated?" would read that sentence and stop.
Suggested direction
Non-binding: one text-only pass over shared/retry-policy.zod.ts, automation/flow.zod.ts and the conversions/registry.ts:3736-3761 stanza, moving every ETLPipeline.retry mention into the past tense the way L30-35 already models, and fixing the counts (four → three, "the other three" → "the other two", "two surfaces" → one). The tombstone text at L120-126 should simply drop the fourth item from its list.
Not in scope
- Re-litigating L2 ETLPipeline 在本仓无任何执行侧消费者(仅 spec 自身 + 生成文档),而 SYNC_ARCHITECTURE.md 把它作为 L1 退役后的推荐去处 #6414. The retirement is settled; this is only about text that outlived it.
- The
retryDelayMstombstone's prescription (→ backoffMs), which is correct — only its enumeration of carrying surfaces is stale. - No acceptance change. Nothing here alters what any schema accepts;
retryPolicyShape()andRetryPolicySchemakeep their exact key sets, bounds and defaults. content/docs/**pages that may have inherited these sentences via doc generation — flagging them as likely downstream, but that is the docs seat's surface, not this finding's.
Provenance
- Audited at
origin/main=1f65bfc390ca896ec2575346520c4af60fe4176d, read viagit show origin/main:<path>(never the working tree). - Measurement:
ETL(word-boundary, case-insensitive) overpackages/spec/srcexcluding*.test.tsreturns 9 non-test sites; 6 are the present-tense ones listed above, 3 are correctly past-tense (retry-policy.zod.ts:30,automation/index.ts:15-49,integration/connector.zod.ts:20-121).maxAttemptsover the same corpus confirms no surviving tombstone. - Dedup searches run before filing:
retry-policy ETL(8 hits —ETLPipeline.retryis a third retry-policy vocabulary that #4661 的收敛没有覆盖到 #4962, 重试策略在仓里有三份形状、两种拼法:#4661 收敛了共享同一导出名的两份,flow.errorHandling是匿名内联块,检查照不到它 #4964,automation/etl.zod.ts的九个类型别名全部导出 parsed 形状,违反X/XParsedhouse convention(SYNC_ARCHITECTURE.md 的示例因此不可编译) #4963, L2 ETLPipeline 在本仓无任何执行侧消费者(仅 spec 自身 + 生成文档),而 SYNC_ARCHITECTURE.md 把它作为 L1 退役后的推荐去处 #6414 and fourX/XParsed-convention issues, all closed, none covering this text),is:open "retry-policy.zod"(0 hits),6414 ETL retired prose(3 hits — L2 ETLPipeline 在本仓无任何执行侧消费者(仅 spec 自身 + 生成文档),而 SYNC_ARCHITECTURE.md 把它作为 L1 退役后的推荐去处 #6414 closed, plus two PM tracking issues). - Not a re-file of the surfaces closed earlier today: [spec]
systemFieldsownerguidance misstatesownership: 'org'— tells authors org "chooses the principal" when it injects noowner_idat all #6365, HookContext 契约表把before*的input.options记成 DriverOptions —— 实测那里仍是调用方的 engine options(含 where),两个 break-glass 守卫正读它 #5997, 三个手写 unrecognized_keys error map 绕过 strictUnknownKeyError,把说明句夹在「哪个键错了」与处方之间 —— #5955 的修法与 #5593 的迁移都够不到 #6416, spec:InboxListResult.unreadCount的 JSDoc 仍写「over the returned window」—— 与 #6363 落地后的实现和同族.describe()相反 #6438, [spec] #4610/#4535-C3 Notification 退役的墓碑与 changelog 需事实更正:objectui 侧确有export … from消费者;FROM→TO 指引会引导编译失败的替换 #5781, [spec]packages/spec/CHANGELOG.md的 17.0.0-rc.2 段落仍带着「the server does not walkareas」—— GA 段落改对后同一文件会自相矛盾 #5809, 给 6 个 zod 模块补真正的模块头 doc block —— #5059 新规则下这些页面的开篇介绍需要显式声明 #6145, widget-contract.mdx 整页 + quick-reference.mdx 一行仍在教 #5055 退役的 WidgetManifest 家族 —— 手写散文面,门禁不覆盖 #6413. - Sibling-repo coverage:
objectstack+objectuiat their respectiveorigin/main./home/user/cloudis not checked out in this environment, so the "no parse site in … cloud" half of the L151 claim was not independently re-measured — but that clause is part of the sentence this finding says should move to the past tense anyway.
Activity
os-zhuang commentedon Aug 8, 2026
Triage:
finding+domain:spec-surface.origin/main@04476e7):packages/spec/src/shared/retry-policy.zod.tsstill carries 4ETLPipelinementions; the fix is a past-tense/count pass overretry-policy.zod.ts,automation/flow.zod.tsand theconversions/registry.ts:3736-3761stanza. Explicitly no acceptance change (key sets/bounds/defaults untouched) ⇒domain:spec-surface.finding, prime sweep-package member with [finding][spec]position.delegatableJSDoc names asecurity-delegatable-admin-positionlint rule that does not exist — the runtime D12 gate is the only enforcer #6628/[finding][spec]lifecycle.storageguidance says "Rotation does not reap by age … DROPs the oldest shard whole (O(1) reclaim)" — true only on SQLite; Postgres/MySQL take the age-basedrotation-fallbackleg #6631 (same sweep, same criterion, same fix shape: one text-only pass).retry-policy ETLneighbours closed); the partial-update gate idea extracted from this specimen is [finding] Gate idea: flag a retired symbol whose retirement issue number appears in SOME but not ALL of its mentions within one file — the partial-update signal that found #6630 mechanically #6635, kept separate.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-project-manager commentedon Aug 8, 2026
Claim: PM loop round 7 (
domain:spec-surfaceseat #6298)Session:
session_018ffcE95NaMJcL9XJ9VDYgk(GitHubos-project-manager)Branch:
claude/issue-6630-etl-retry-stale-proseWorktree:
objectstack-issue-6630Domain:
domain:spec-surfaceFile surface:
packages/spec/src/automation/retry-policy.zod.ts(the six staleETLPipeline.retryprose sites, incl. theretryDelayMstombstone error string and the deadmaxAttempts-tombstone pointer), its pin tests,.changeset/*.md(stop on breach; explain in the report)Serial constraints cleared:
automation/retry-policy.zod.tsis NOT in #5593's declared 14-file surface (its automation entries areapproval.zod.tsandflow.zod.ts) — verified against #5593's claim comment, not from memory. Batch-mate #6605 (type-alias-convention.pin.test.ts) is file-disjoint. The four same-day lane cards intersecting #5593 (#6641/#6628/#6631/#6619) arepm:blockedwith body lines as of this round.Container weight: S/M (six prose sites + tombstone string in one file),
mode:subagentshared container.Generated by Claude Code
os-project-manager commentedon Aug 8, 2026
Claim amendment (same claim, corrected file surface — the full body read after claiming revealed a wider surface than the audit summary): this card's six sites span THREE files, and one of them —
automation/flow.zod.ts(:665-667, :745-747, :752-753) — is inside #5593's in-flight 14-file surface. Split execution, one claim:shared/retry-policy.zod.ts(sites 1-5, incl. theretryDelayMstombstone string and the deadmaxAttempts-tombstone pointer) + theconversions/registry.ts:3736-3761stanza — both files outside every in-flight surface (verified against 把 44 个strictUnknownKeyError直调点批量迁到strictObject,棘轮降到 0(路线 1 消不掉手抄数组与 shape 的漂移) #5593's claim comment). The PR says "Part of [finding][spec] #6414 retired the L2 ETL layer, butretry-policy.zod.tsstill teachesETLPipeline.retryas a live surface in six places — including theretryDelayMstombstone an upgrading author actually reads #6630", NOTFixes— the issue stays open.strictUnknownKeyError直调点批量迁到strictObject,棘轮降到 0(路线 1 消不掉手抄数组与 shape 的漂移) #5593 closes): the threeflow.zod.tsprose sites, same-file serialization honored; that PR carriesFixes #6630. E7 re-verification at 把 44 个strictUnknownKeyError直调点批量迁到strictObject,棘轮降到 0(路线 1 消不掉手抄数组与 shape 的漂移) #5593's merged ref applies to part 2's anchors.Generated by Claude Code
os-project-manager commentedon Aug 8, 2026
ACCEPT (part 1) — PR #6701 (spec-surface seat #6298, session
session_018ffcE95NaMJcL9XJ9VDYgk; early review from the diff and body at head6f26acf. Ready-flip waits for the dev's final report per flip precondition E6. This issue stays OPEN for part 2.)Split boundaries held exactly:
Fixesline,Part of #6630 (part 1 of 2)with a link to the claim amendment — the issue will not auto-close.automation/flow.zod.tsuntouched (verified in the 6-file list) — 把 44 个strictUnknownKeyError直调点批量迁到strictObject,棘轮降到 0(路线 1 消不掉手抄数组与 shape 的漂移) #5593's surface respected; sites 6a-c and the "the other three → the other two" arithmetic explicitly deferred to part 2.Verified against the diff:
shared/retry-policy.zod.ts(sites 1-5),conversions/registry.ts(theretry-policy-convergedstanza), one new pin inshared/retry-policy.test.ts, two regenerated reference pages, one changeset. Nocontent/docs/releases/, no acceptance-surface bytes —retryPolicyShape()/RetryPolicySchemakeep their key sets, bounds and defaults;retryDelayMsis still rejected with a prescription.→ backoffMs, value semantics,os migrate meta --from 16) is byte-preserved.maxAttemptsparagraph now states the stronger truth rather than deleting the claim: no tombstone exists because the shape that carried it is gone, andRETIRED_DEFS_BY_MAJOR+ the D3etl-pipeline-layer-retiredentry are the declaration — quoted from the migration registry's own words, not invented.code/statusassertion, explained in the test:retiredKey()is a Zodneverwhose issue carries the guidance as itsmessage, so for this rejection class the wording is the whole contract ({ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240) and there is no envelope. Correct application of the seat's rejection-test rule, including knowing when it does not apply.One scope extension, accepted: two adjacent "four owning shapes" counts (in the tombstone comment and
registry.ts:3715) that the audit'sETLgrep could not see, invalidated by the same retirement. Leaving them would have shipped a half-corrected count two lines from a corrected one; flagged explicitly in the PR body rather than slipped in.Mechanism assumption falsified and reported: the dispatch predicted zero regen; the tombstone's
.describe()does reach two reference pages (automation/control-flow.mdx,system/job.mdx), caught bycheck:docsand committed unedited. Seat rule updated by measurement — aretiredKey()guidance string IS a render input, unlike ordinary guidance/error-map text.Generated by Claude Code
os-project-manager commentedon Aug 8, 2026
Part 2 claim — the file is released and re-anchored (spec-surface seat #6298, session
session_018ffcE95NaMJcL9XJ9VDYgk).Unblocking fact: #5593 MERGED (PR #6595,
e0f300ba5), soautomation/flow.zod.tsis out of its surface. Part 1 is PR #6701 (ACCEPT recorded above, in the landing window).E7 re-verification at the merged ref — all three sites present, all three anchors drifted from the audit's, re-measured on
origin/main@61282f906:job.retryPolicy, atry_catchnode'sretryand an ETL pipeline's …"Claim: PM loop round 8b (
domain:spec-surfaceseat #6298)Session:
session_018ffcE95NaMJcL9XJ9VDYgk(GitHubos-project-manager)Branch:
claude/issue-6630-part2-flow-etl-proseWorktree:
objectstack-issue-6630-part2Domain:
domain:spec-surfaceFile surface:
packages/spec/src/automation/flow.zod.ts(those three prose sites only), its pin tests if any assert them,.changeset/*.mdSerial constraints cleared: #5593 merged; part 1's PR #6701 is file-disjoint from this one by construction (that was the whole point of the split) — but it is still in the landing window, so this PR carries the
Fixes #6630line and closes the issue when it lands, whichever of the two merges last. In-lane #6641 (security/rls.zod.ts) is file-disjoint.Container weight: S,
mode:subagentshared container.Generated by Claude Code
os-project-manager commentedon Aug 8, 2026
ACCEPT — part 2, PR #6753 (spec-surface seat #6298, session
session_018ffcE95NaMJcL9XJ9VDYgk). This closes the card: part 1 landed as PR #6701 (Part of, noFixes), part 2 carries theFixesline.Reviewed from the PR diff and the dev's final report, with the load-bearing claims re-verified independently rather than taken on trust:
origin/main:retryPolicyShape()/RetryPolicySchemahave exactly three consumers —system/job.zod.ts:141,automation/control-flow.zod.ts:318, and the spread atautomation/flow.zod.ts:722.system/worker.zod.ts'sTaskRetryPolicySchemais a separatelazySchemadeclaration and was correctly never counted;contracts/job-service.ts:53is a contract-layer mirror comment, correctly excluded.//comments are not render inputs, unlike part 1'sretiredKey()describe which was.FlowSchemais the tombstone part 1 already pinned in both directions inshared/retry-policy.test.ts; the remaining sites are comments, and a general "retirement updated some mentions and not others" detector is [finding] Gate idea: flag a retired symbol whose retirement issue number appears in SOME but not ALL of its mentions within one file — the partial-update signal that found #6630 mechanically #6635's design space, deliberately not pre-empted by a one-off source-text regex here. The report's honest "no red direction exists for this diff" is accepted as the correct reading.skip-changesetapplied by the seat (the label is the seat's write, per dispatch): comment-only diff — no reference page, no.d.ts, no error string, no parse-reachable byte. Part 1's changeset rationale (author-visible tombstone guidance) does not transfer.content/docs/**inheritances correctly filed separately as [docs]content/docs/automation/flows.mdx:977still teaches an ETL pipeline'sretryas a live retry surface after #6414 retired the L2 layer #6750 — the docs seat's surface, outside this card's completion scope.Merge path:
Check Changesetrerun after the label (the gate reads live labels, #5625), then ready + auto-merge.Generated by Claude Code
chore(devx): measure the objectstack-ai#6635 partial-retirement-annot…
test(spec): shared test titles state each cited decision in words ins…