Skip to content

[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

Closed

Description

@os-project-manager

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 D3 etl-pipeline-layer-retired semantic migration, which states the consequence directly: "tsc reports TS2724/TS2305" for the ETL factory from @objectstack/spec/automation.
  • retry-policy.zod.ts:30-35 itself — 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 ("⚠️ This note used to send readers on to 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

Provenance

Activity

  1. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    Contributor

    Triage: finding + domain:spec-surface.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    CollaboratorAuthor

    Claim: PM loop round 7 (domain:spec-surface seat #6298)
    Session: session_018ffcE95NaMJcL9XJ9VDYgk (GitHub os-project-manager)
    Branch: claude/issue-6630-etl-retry-stale-prose
    Worktree: objectstack-issue-6630
    Domain: domain:spec-surface
    File surface: packages/spec/src/automation/retry-policy.zod.ts (the six stale ETLPipeline.retry prose sites, incl. the retryDelayMs tombstone error string and the dead maxAttempts-tombstone pointer), its pin tests, .changeset/*.md (stop on breach; explain in the report)
    Serial constraints cleared: automation/retry-policy.zod.ts is NOT in #5593's declared 14-file surface (its automation entries are approval.zod.ts and flow.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) are pm:blocked with body lines as of this round.
    Container weight: S/M (six prose sites + tombstone string in one file), mode:subagent shared container.


    Generated by Claude Code

  3. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    CollaboratorAuthor

    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:


    Generated by Claude Code

  4. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    CollaboratorAuthor

    ACCEPT (part 1) — PR #6701 (spec-surface seat #6298, session session_018ffcE95NaMJcL9XJ9VDYgk; early review from the diff and body at head 6f26acf. Ready-flip waits for the dev's final report per flip precondition E6. This issue stays OPEN for part 2.)

    Split boundaries held exactly:

    Verified against the diff:

    • 6 files: shared/retry-policy.zod.ts (sites 1-5), conversions/registry.ts (the retry-policy-converged stanza), one new pin in shared/retry-policy.test.ts, two regenerated reference pages, one changeset. No content/docs/releases/, no acceptance-surface bytes — retryPolicyShape() / RetryPolicySchema keep their key sets, bounds and defaults; retryDelayMs is still rejected with a prescription.
    • The tombstone fix is the right shape: the enumeration drops the deleted fourth surface, the prescription (→ backoffMs, value semantics, os migrate meta --from 16) is byte-preserved.
    • The maxAttempts paragraph now states the stronger truth rather than deleting the claim: no tombstone exists because the shape that carried it is gone, and RETIRED_DEFS_BY_MAJOR + the D3 etl-pipeline-layer-retired entry are the declaration — quoted from the migration registry's own words, not invented.
    • The new pin asserts both directions (every live surface named / no retired surface named / prescription intact), with reverse verification in the predicted direction: restoring the deleted item reds exactly one assertion, 8844 others green — which is itself the measurement that no pre-existing assertion could see this drift (they all matched the prescription, never the enumeration).
    • Deliberate absence of an ADR-0112 code/status assertion, explained in the test: retiredKey() is a Zod never whose issue carries the guidance as its message, 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's ETL grep 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 by check:docs and committed unedited. Seat rule updated by measurement — a retiredKey() guidance string IS a render input, unlike ordinary guidance/error-map text.


    Generated by Claude Code

  5. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    CollaboratorAuthor

    Part 2 claim — the file is released and re-anchored (spec-surface seat #6298, session session_018ffcE95NaMJcL9XJ9VDYgk).

    Unblocking fact: #5593 MERGED (PR #6595, e0f300ba5), so automation/flow.zod.ts is 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:

    site audit anchor now text
    ETL prose :665-667 :640 "job.retryPolicy, a try_catch node's retry and an ETL pipeline's …"
    arithmetic :745-747 :720 "policy lands on all four surfaces at once"
    arithmetic :752-753 :727 "the other three do not"

    Claim: PM loop round 8b (domain:spec-surface seat #6298)
    Session: session_018ffcE95NaMJcL9XJ9VDYgk (GitHub os-project-manager)
    Branch: claude/issue-6630-part2-flow-etl-prose
    Worktree: objectstack-issue-6630-part2
    Domain: domain:spec-surface
    File surface: packages/spec/src/automation/flow.zod.ts (those three prose sites only), its pin tests if any assert them, .changeset/*.md
    Serial 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 #6630 line 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:subagent shared container.


    Generated by Claude Code

  6. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    CollaboratorAuthor

    ACCEPT — part 2, PR #6753 (spec-surface seat #6298, session session_018ffcE95NaMJcL9XJ9VDYgk). This closes the card: part 1 landed as PR #6701 (Part of, no Fixes), part 2 carries the Fixes line.

    Reviewed from the PR diff and the dev's final report, with the load-bearing claims re-verified independently rather than taken on trust:

    • The four→three arithmetic was recounted by this seat via git on origin/main: retryPolicyShape() / RetryPolicySchema have exactly three consumers — system/job.zod.ts:141, automation/control-flow.zod.ts:318, and the spread at automation/flow.zod.ts:722. system/worker.zod.ts's TaskRetryPolicySchema is a separate lazySchema declaration and was correctly never counted; contracts/job-service.ts:53 is a contract-layer mirror comment, correctly excluded.
    • Zero acceptance-surface bytes, zero regen (all 10 generated artifacts in sync) — consistent with the seat's render-input criterion: a TSDoc block and two // comments are not render inputs, unlike part 1's retiredKey() describe which was.
    • No new pin — endorsed, not waived. The only runtime-observable string reachable through FlowSchema is the tombstone part 1 already pinned in both directions in shared/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-changeset applied 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:977 still teaches an ETL pipeline's retry as 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 Changeset rerun after the label (the gate reads live labels, #5625), then ready + auto-merge.


    Generated by Claude Code

  7. added a commit that references this issue on Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions