From a4861b2a7a51d34e70c4722a8841f962d9af3184 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 11:45:41 +0000 Subject: [PATCH 1/3] docs(spec): import job `failed` also covers a job whose every row failed The REST import worker now ends a job that had rows and created, updated or skipped none of them `failed` (maintainer ruling B, record 6509036166). `ImportJobStatus` and `ImportJobProgressSchema.error` described `failed` as a fatal abort only; reword them to the ruled meaning. No enum value, key or type changes. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L9qibz3A9wkHngU9obhv1g --- .changeset/27734-import-job-failed-wording.md | 13 +++++++++++++ packages/spec/src/api/export.zod.ts | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 .changeset/27734-import-job-failed-wording.md diff --git a/.changeset/27734-import-job-failed-wording.md b/.changeset/27734-import-job-failed-wording.md new file mode 100644 index 00000000000..166303d22d9 --- /dev/null +++ b/.changeset/27734-import-job-failed-wording.md @@ -0,0 +1,13 @@ +--- +'@objectstack/spec': patch +--- + +docs(spec): `ImportJobStatus` and `ImportJobProgressSchema.error` describe `failed` as the REST server now ends it: a job aborted on a fatal error, or a job whose every row failed (#27734) + +Clause-②: no +Metadata-protocol: none + +Only the declared meaning changes; no enum value, key, type or export does, and the wire is unchanged. + +- **`ImportJobStatus`.** `failed` covered only a job aborted on a fatal error. Since the async import worker in `@objectstack/rest` ends a job that had at least one row and created, updated or skipped none of them `failed` (maintainer ruling B, record 6509036166), the docblock and the value comments say so. `succeeded` still covers a job with no rows, a job whose every row was skipped and a job whose rows only partly failed; a dry run follows the same rule, and a cancel wins over both. +- **`ImportJobProgressSchema.error`.** Described as the fatal error only; it now also says that, when every row failed, it carries one sentence pointing at the job's `results`, which hold each row's reason. The reference page `api/export` is regenerated from it. diff --git a/packages/spec/src/api/export.zod.ts b/packages/spec/src/api/export.zod.ts index b42d34ec602..a1baff4e68b 100644 --- a/packages/spec/src/api/export.zod.ts +++ b/packages/spec/src/api/export.zod.ts @@ -488,12 +488,19 @@ export const IMPORT_JOB_MAX_ROWS = 50_000; /** * Import Job Status — the states the import worker actually moves a job * through (`succeeded`, not `completed`, is the success terminal). + * + * `succeeded` and `failed` split on whether the job accomplished anything, not + * on whether some row failed (maintainer ruling B, record 6509036166). A job + * that had at least one row and created, updated or skipped none of them ends + * `failed`, as does a job aborted on a fatal error. A job with no rows, a job + * whose every row was skipped and a job whose rows only partly failed end + * `succeeded`. A dry run follows the same rule, and a cancel wins over both. */ export const ImportJobStatus = z.enum([ 'pending', // Row persisted, worker not yet started 'running', // Worker streaming through the batch - 'succeeded', // Finished (rows may still have per-row errors) - 'failed', // Aborted on a fatal error + 'succeeded', // Finished with a row created, updated or skipped, or with no rows (other rows may still have per-row errors) + 'failed', // Aborted on a fatal error, or every row failed (rows existed and none was created, updated or skipped) 'cancelled', // Cancelled by the caller before completion ]); export type ImportJobStatus = z.input; @@ -539,7 +546,10 @@ export const ImportJobProgressSchema = lazySchema(() => z.object({ percentComplete: z.number().min(0).max(100).describe('processed / total as a percentage'), undoable: z.boolean().describe('Whether this job can still be logically rolled back (undo log captured, terminal state, not yet reverted)'), revertedAt: z.string().optional().describe('When the job was undone / rolled back (ISO 8601): set only once every row is reverted. An undo that left rows unreverted sets nothing, and the job stays undoable for those rows.'), - error: z.string().optional().describe('Fatal error message (when status = failed)'), + error: z.string().optional().describe( + 'Why the job failed (when status = failed): the fatal error that aborted it, or, when every row failed, ' + + 'one sentence pointing at the job\'s `results`, which carry each row\'s reason', + ), startedAt: z.string().optional().describe('Processing start timestamp (ISO 8601)'), completedAt: z.string().optional().describe('Completion timestamp (ISO 8601)'), createdAt: z.string().describe('Job creation timestamp (ISO 8601)'), From 4765d36c14172e30c64464551d309cb19a70ffc8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 11:49:35 +0000 Subject: [PATCH 2/3] docs(spec): regenerate the export reference for the import job `error` describe Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L9qibz3A9wkHngU9obhv1g --- content/docs/references/api/export.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/docs/references/api/export.mdx b/content/docs/references/api/export.mdx index 7394590f09a..bb87d4b987c 100644 --- a/content/docs/references/api/export.mdx +++ b/content/docs/references/api/export.mdx @@ -186,7 +186,7 @@ const result = CreateImportJobRequestSchema.parse(data); | **percentComplete** | `number` | ✅ | processed / total as a percentage | | **undoable** | `boolean` | ✅ | Whether this job can still be logically rolled back (undo log captured, terminal state, not yet reverted) | | **revertedAt** | `string` | optional | When the job was undone / rolled back (ISO 8601): set only once every row is reverted. An undo that left rows unreverted sets nothing, and the job stays undoable for those rows. | -| **error** | `string` | optional | Fatal error message (when status = failed) | +| **error** | `string` | optional | Why the job failed (when status = failed): the fatal error that aborted it, or, when every row failed, one sentence pointing at the job's `results`, which carry each row's reason | | **startedAt** | `string` | optional | Processing start timestamp (ISO 8601) | | **completedAt** | `string` | optional | Completion timestamp (ISO 8601) | | **createdAt** | `string` | ✅ | Job creation timestamp (ISO 8601) | @@ -214,7 +214,7 @@ const result = CreateImportJobRequestSchema.parse(data); | **percentComplete** | `number` | ✅ | processed / total as a percentage | | **undoable** | `boolean` | ✅ | Whether this job can still be logically rolled back (undo log captured, terminal state, not yet reverted) | | **revertedAt** | `string` | optional | When the job was undone / rolled back (ISO 8601): set only once every row is reverted. An undo that left rows unreverted sets nothing, and the job stays undoable for those rows. | -| **error** | `string` | optional | Fatal error message (when status = failed) | +| **error** | `string` | optional | Why the job failed (when status = failed): the fatal error that aborted it, or, when every row failed, one sentence pointing at the job's `results`, which carry each row's reason | | **startedAt** | `string` | optional | Processing start timestamp (ISO 8601) | | **completedAt** | `string` | optional | Completion timestamp (ISO 8601) | | **createdAt** | `string` | ✅ | Job creation timestamp (ISO 8601) | From 154d26cb727ffed16cc80d1522d604c2a68e7695 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 13:08:58 +0000 Subject: [PATCH 3/3] docs(spec): a cancel wins over the every-row-failed rule, not over a fatal abort The worker's catch writes `failed` unconditionally, so an error thrown after the cancel route stamped the job `cancelled` still ends it `failed`. Narrow the docblock and the changeset to say so. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01L9qibz3A9wkHngU9obhv1g --- .changeset/27734-import-job-failed-wording.md | 2 +- packages/spec/src/api/export.zod.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.changeset/27734-import-job-failed-wording.md b/.changeset/27734-import-job-failed-wording.md index 166303d22d9..2a54b66980f 100644 --- a/.changeset/27734-import-job-failed-wording.md +++ b/.changeset/27734-import-job-failed-wording.md @@ -9,5 +9,5 @@ Metadata-protocol: none Only the declared meaning changes; no enum value, key, type or export does, and the wire is unchanged. -- **`ImportJobStatus`.** `failed` covered only a job aborted on a fatal error. Since the async import worker in `@objectstack/rest` ends a job that had at least one row and created, updated or skipped none of them `failed` (maintainer ruling B, record 6509036166), the docblock and the value comments say so. `succeeded` still covers a job with no rows, a job whose every row was skipped and a job whose rows only partly failed; a dry run follows the same rule, and a cancel wins over both. +- **`ImportJobStatus`.** `failed` covered only a job aborted on a fatal error. Since the async import worker in `@objectstack/rest` ends a job that had at least one row and created, updated or skipped none of them `failed` (maintainer ruling B, record 6509036166), the docblock and the value comments say so. `succeeded` still covers a job with no rows, a job whose every row was skipped and a job whose rows only partly failed; a dry run follows the same rule. A cancel wins over that counting rule, so over both of its outcomes, but not over a fatal abort: an error thrown after the job was cancelled still ends it `failed`. - **`ImportJobProgressSchema.error`.** Described as the fatal error only; it now also says that, when every row failed, it carries one sentence pointing at the job's `results`, which hold each row's reason. The reference page `api/export` is regenerated from it. diff --git a/packages/spec/src/api/export.zod.ts b/packages/spec/src/api/export.zod.ts index a1baff4e68b..0eb7fd71450 100644 --- a/packages/spec/src/api/export.zod.ts +++ b/packages/spec/src/api/export.zod.ts @@ -494,7 +494,9 @@ export const IMPORT_JOB_MAX_ROWS = 50_000; * that had at least one row and created, updated or skipped none of them ends * `failed`, as does a job aborted on a fatal error. A job with no rows, a job * whose every row was skipped and a job whose rows only partly failed end - * `succeeded`. A dry run follows the same rule, and a cancel wins over both. + * `succeeded`. A dry run follows the same rule. A cancel wins over that + * counting rule, so over both of its outcomes, but not over a fatal abort: an + * error thrown after the job was cancelled still ends it `failed`. */ export const ImportJobStatus = z.enum([ 'pending', // Row persisted, worker not yet started