Skip to content

Job status vocabulary has no degraded value — #5548's services half cannot compile or persist without widening JobExecutionStatus + the two sys_job* selects #7072

Description

@os-project-manager

Blocks #5548 (the services half of the 2026-08-08 B-minimal maintainer ruling). Filed by the services-lane dev agent dispatched on #5548 (session session_01USNUyHEr7uaU6MoEWXitei) after measuring that #5548's deliverable is unreachable inside its declared file surface — and unreachable inside domain:services at all.

Not a design question: the maintainer already ruled the vocabulary gets exactly one more value. This card is the mechanical execution of that ruling in the two lanes that own the declarations.

What the ruling requires

From the 2026-08-08 maintainer ruling on #5548 (comment 5224023201), verbatim:

Vocabulary stays minimal — one additional outcome meaning "completed without accomplishing the work". ⛔ Do not open an enum family; a second key would need its own pull.

and

DbJobAdapter maps a handler-resolved degraded outcome to a sys_job_run.status distinct from success; bumpJob (sys_job.last_status) in sync.

"One additional status value" is the vocabulary widening. Nothing has performed it.

What the spec half actually shipped

#6617 (closed completed 2026-08-09T12:27:24Z, commit e18a1626) delivered the producer side only:

  • packages/spec/src/contracts/job-service.ts — new JobRunOutcome { outcome: 'completed' | 'degraded'; reason?: string }, JobHandler widened to resolve void | JobRunOutcome;
  • packages/services/service-job/src/run-with-policy.ts — made generic so the resolved value is no longer erased.

Its declared file surface (claim comment on #6617) was contracts/job-service.ts + spec contract tests + the api-surface shard. packages/spec/src/system/job.zod.ts was not in it, and the shipped TSDoc says so plainly: "the shipped adapters currently discard the resolved value". Correct scoping by #6617 — the gap is simply that the consumer-side vocabulary was never filed as anyone's card.

The gap — three declaration sites, none carrying degraded

Read from origin/main @ 2c7e62d5:

# File:line Declaration Lane
1 packages/spec/src/system/job.zod.ts:178 JobExecutionStatus = z.enum(['running','success','failed','timeout']) domain:spec
2 packages/platform-objects/src/audit/sys-job-run.object.ts:49 status: Field.select(['running','success','failed','timeout'], …) domain:metadata
3 packages/platform-objects/src/audit/sys-job.object.ts:73 last_status: Field.select(['success','failed','timeout','running'], …) domain:metadata

Exhaustive — a repo-wide grep for the four-value tuple returns exactly these three plus the spec pin test.

Measured consequences (both directions, both fatal)

1. It does not compile. DbJobAdapter.finishRun's status parameter is typed JobExecution['status'], i.e. site 1. Applying the minimal #5548 mapping in wrap() and nothing else:

src/db-job-adapter.ts(169,48): error TS2345: Argument of type '"success" | "degraded"' is not assignable to parameter of type '"timeout" | "success" | "failed" | "running"'.
  Type '"degraded"' is not assignable to type '"timeout" | "success" | "failed" | "running"'.
src/db-job-adapter.ts(170,34): error TS2345: Argument of type '"success" | "degraded"' is not assignable to parameter of type '"success" | "failed"'.

(The second error is in-lane and trivially fixable; the first is site 1 and is not.)

2. Even if it compiled, the write would be refused — and refused silently. Site 2 is enforced, not merely declared: packages/objectql/src/validation/record-validator.ts:578 rejects an out-of-vocabulary select value with invalid_option, and JobServicePlugin hands DbJobAdapter the real ObjectQL engine (ctx.getService('objectql')). DbJobAdapter.finishRun wraps its update in a best-effort try/catch that logs logger.warn — so the rejection would be swallowed and the run row would stay status: 'running' forever. That is a strictly worse audit face than the defect #5548 is fixing.

The underlying defect, reproduced on unmodified origin/main

A handler resolving the shipped JobRunOutcome degraded shape, driven through DbJobAdapter.trigger:

sys_job_run row: {
  "id": "run_cf5629dc-cfb3-48de-ab18-fa5bec2c1f0d",
  "job_name": "wait_wake",
  "status": "success",
  "started_at": "2026-08-09T13:44:58.206Z",
  "trigger": "schedule",
  "attempt": 1,
  "created_at": "2026-08-09T13:44:58.206Z",
  "completed_at": "2026-08-09T13:44:58.206Z",
  "duration_ms": 0,
  "error": null
}
sys_job last_status: success / failure_count: 0

{ outcome: 'degraded', reason: 'STORE_UNAVAILABLE' } in, status: "success" / error: null out. #5548's premise holds verbatim.

Complete change set this card owes

  1. packages/spec/src/system/job.zod.ts:178 — add 'degraded' to JobExecutionStatus, with a TSDoc pointing at DbJobAdapter 把「handler 没抛错」记成 sys_job_run.status='success' —— 内部自行降级的 job(如 wait 唤醒打空)在作业审计面上仍显示成功 #5548's ruling and stating that degraded is not a failure and never retries (matching the JobHandler TSDoc spec: JobHandler 增加可选的「跑完但没干成」回报通道 —— #5548 裁决(B-minimal)的契约半边,可加性,Promise<void> handler 零改动 #6617 shipped).
  2. packages/spec/src/system/job.test.ts:399-408 and :467 — the accept/reject pins and the Array< JobExecution['status'] > round-trip list both enumerate the four values.
  3. packages/platform-objects/src/audit/sys-job-run.object.ts:49status select.
  4. packages/platform-objects/src/audit/sys-job.object.ts:73last_status select.
  5. Regenerated i18n bundles — the select options are carried per-locale in packages/platform-objects/src/apps/translations/{en,zh-CN,ja-JP,es-ES}.objects.generated.ts (sys_job_run.fields.status.options, sys_job.fields.last_status.options); node scripts/check-i18n-bundles.mjs --write, gated by check:i18n / check:i18n-coverage.
  6. Regenerated spec artifacts — JobExecutionStatus appears in packages/spec/json-schema.manifest/system.json and packages/spec/api-surface/system.json; per AGENTS.md run pnpm --filter @objectstack/spec build && check:generated.

Why one card and not two

Splitting sites 1 and 2/3 into a domain:spec card and a domain:metadata card opens a window in which the spec enum and the platform object disagree about the legal value set. That is precisely the defect shape #6036 was filed and fixed for (sys_setting.scope declared a runtime option the spec enum did not know) — its resolution note put it best: 「两边本就该是同一个真相」. Recommend one card spanning both lanes, seated wherever the PM seats cross-lane vocabulary work; if it must be split, the metadata half should land first so no window exists in which the type system permits a value the engine refuses.

Downstream note — not for this card

objectui's Studio jobs view reads sys_job_run.status / sys_job.last_status and will meet a fifth value it has no label, colour or filter option for. Out of scope here (separate repo) and premature to file there while the value does not exist; worth a follow-up in objectui once this lands.

Filed unlabeled and unassigned for PM triage per #4949 discipline. Duplicate search before filing: sys_job_run, JobExecutionStatus, "job status enum" over open issues — only #5548 itself.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions