在 #4538 的三仓 import 级扫描中发现的运行时缺陷(与该 issue 的类型收敛无关,按 Prime Directive #10 单独立案)。
现象
defineJob(JobSchema.parse)产出的 cron schedule 里,expression 已被 CronExpressionInputSchema 的 transform 变换成 ADR 表达式信封:
// packages/spec (built dist) 实测:
defineJob ( { name : 'x_job' , schedule : { type : 'cron' , expression : '0 1 * * *' } , handler : 'h' } ) . schedule
// => {"type":"cron","expression":{"dialect":"cron","source":"0 1 * * *"},"timezone":"UTC"}
而 AppPlugin(packages/runtime/src/app-plugin.ts,kernel:ready 钩子里的 declarative-jobs 注册段)把 job.schedule 原样 传给 IJobService.schedule,CronJobAdapter(packages/services/service-job/src/cron-job-adapter.ts)又把 schedule.expression 原样 传给 croner:
new Cron ( { dialect :'cron' , source :'0 1 * * *' } , { timezone :'UTC' } , ...)
// => throws: "CronPattern: Pattern has to be of type string."
该异常被 AppPlugin 的 per-job try/catch 吞掉,只留一条 warn('[AppPlugin] Failed to schedule job', …) —— 通过 defineJob 授权的 cron job 永远不会真正被调度 ,而作者看到的是一次成功的 build 和一次成功的 boot。examples/app-showcase 的 HealthSweepJob(src/automation/jobs/index.ts,schedule: { type: 'cron', expression: '0 1 * * *' })正中此路径。
interval/once 分支不受影响(无 transform);trigger-schedule 一侧自己归一化出裸字符串 expression,也不受影响。
成因判定
declared ≠ enforced 的典型分裂:JobSchema.schedule 的解析产物(信封)与 IJobService.schedule 的消费约定(裸字符串,见 @objectstack/spec/contracts 的 JobSchedule — #4538 后是该名字唯一声明)之间没有任何一层做信封 → 字符串的降解,adapter 也从未声明它能吃信封。
修复方向(按 contract-first,不建议在 adapter 里加 typeof === 'object' 容错)
候选:
AppPlugin 注册声明式 job 时(与 retryPolicy/timeout 同一处)把 Schedule(authoring tier)降解为 contracts JobSchedule(boundary tier):信封 expression.source → 裸字符串。降解点单一、两个 tier 的边界正好是 [#4535·A3] contracts 手写 interface 与域内 zod 推导类型收敛(3 簇 9 条) #4538 划清的那条线。
或者让 IJobService.schedule 正式接受 authoring Schedule 并由各 adapter 统一经 @objectstack/formula cron engine 解包/校验 —— 改动面大,但把信封作为一等公民贯穿到执行层。
无论取哪条,失败路径都应从 warn 升级为更响亮的信号(至少 error + 计数),"调度失败"不该和"缺 handler"共用一条静默 warn。
复现:任一 defineJob cron job → boot → 观察 warn 日志与 listJobs()。
在 #4538 的三仓 import 级扫描中发现的运行时缺陷(与该 issue 的类型收敛无关,按 Prime Directive #10 单独立案)。
现象
defineJob(JobSchema.parse)产出的 cron schedule 里,expression已被CronExpressionInputSchema的 transform 变换成 ADR 表达式信封:而
AppPlugin(packages/runtime/src/app-plugin.ts,kernel:ready钩子里的 declarative-jobs 注册段)把job.schedule原样传给IJobService.schedule,CronJobAdapter(packages/services/service-job/src/cron-job-adapter.ts)又把schedule.expression原样传给 croner:该异常被 AppPlugin 的 per-job try/catch 吞掉,只留一条
warn('[AppPlugin] Failed to schedule job', …)—— 通过defineJob授权的 cron job 永远不会真正被调度,而作者看到的是一次成功的 build 和一次成功的 boot。examples/app-showcase的HealthSweepJob(src/automation/jobs/index.ts,schedule: { type: 'cron', expression: '0 1 * * *' })正中此路径。interval/once 分支不受影响(无 transform);
trigger-schedule一侧自己归一化出裸字符串 expression,也不受影响。成因判定
declared ≠ enforced 的典型分裂:
JobSchema.schedule的解析产物(信封)与IJobService.schedule的消费约定(裸字符串,见@objectstack/spec/contracts的JobSchedule— #4538 后是该名字唯一声明)之间没有任何一层做信封 → 字符串的降解,adapter 也从未声明它能吃信封。修复方向(按 contract-first,不建议在 adapter 里加
typeof === 'object'容错)候选:
Schedule(authoring tier)降解为 contractsJobSchedule(boundary tier):信封expression.source→ 裸字符串。降解点单一、两个 tier 的边界正好是 [#4535·A3] contracts 手写 interface 与域内 zod 推导类型收敛(3 簇 9 条) #4538 划清的那条线。IJobService.schedule正式接受 authoringSchedule并由各 adapter 统一经@objectstack/formulacron engine 解包/校验 —— 改动面大,但把信封作为一等公民贯穿到执行层。无论取哪条,失败路径都应从 warn 升级为更响亮的信号(至少 error + 计数),"调度失败"不该和"缺 handler"共用一条静默 warn。
复现:任一 defineJob cron job → boot → 观察 warn 日志与
listJobs()。