Skip to content

Commit 9d91f58

Browse files
docs(spec): JobSchema.body's describe says an enabled pull job installs when its pull binds and is refused when it does not (#21708)
Fixes #21703 Clause-②: no Built to triage `5977869121` (direction: the card's own one-sentence fix), dispatched under claim `5977933303`. One sentence of `JobSchema.body`'s published describe, the reference page generated from it, and one `@objectstack/spec` patch changeset. No schema, key, export or behaviour change. ## Measured first: what install-local does with a `pull` job at the base Base `16d241a6af` (`origin/main` when this branch was cut). Install-local is the door after PR #21683. - `packages/runtime/src/app-artifact-handlers.ts:389-412`, `collectJobsWithoutBody`. A disabled job is skipped (`:393`, `if (job.enabled === false) continue;`). An enabled job that declares `pull` is judged by `judgeJobPull(job, bundle)` (`:397`). `if (judged.binds) continue;` (`:398`) leaves a binding pull unnamed; otherwise the job is named with `pullRefusal = judged.refusal;` (`:399`). A job with no `pull` and a `body` is named only when `judgeJobBody` refuses that body (`:401-403`). A job with neither is named with no refusal. - `:457-491`, `judgeJobPull`, the one judge (the binder calls it too, `:837`). It binds only when no `body` or `handler` sits beside the `pull` (`:461`), the `pull` parses (`:468`), and the artifact declares the named mapping (`:475`) with a `connectorSource` (`:483`); then `return { binds: true, mapping };` (`:490`). - `packages/cloud-connection/src/marketplace-install-local-plugin.ts:1006-1014`. The install route answers `422` `VALIDATION_ERROR` (`UNRUNNABLE_REFUSAL_CODE` and `UNRUNNABLE_REFUSAL_STATUS`, `:192-193`) whenever `unrunnable.jobs.length` is non-zero (`:1007`). A job carrying `pullRefusal` gets `describeUnrunnable`'s pull clause (`:218`, `:242`). So at the base, an enabled `pull` job installs when its `pull` binds. It is refused with the same `422` when it does not, as is a job whose `body` the declaration refuses. A disabled one is not judged. The old sentence was "`os package install` therefore refuses an enabled job with no `body` (a `pull` job excepted: it is data too)". It reads as "a `pull` job is never refused", which those lines contradict. ## The change In `packages/spec/src/system/job.zod.ts`, `JobSchema.body`'s describe, the old clause becomes: > `os package install` therefore refuses an enabled job with no `body`. A `pull` is data too, so an enabled `pull` job is judged by its `pull` instead: it installs when the `pull` binds (it names a mapping the package declares, with a `connectorSource`) and is refused when it does not, as is a job whose `body` the declaration refuses. The parenthesis names the condition an author controls. Code beside a `pull` stays with the describe's next sentence, "Refused beside `pull`." **Neighbouring describes, read for any other sentence PR #21683 made false: none, so none is touched.** - `handler` ("When both are present `body` wins; refused beside `pull`. A job must declare one of …") says nothing about install. - `pull` ("Data like `body`, so every door schedules it. Refused beside `body` or `handler`.") and `pull.mapping` ("refused at `defineStack` / `os validate` otherwise"). PR #21683 did not change which doors schedule a binding pull, and the mapping sentence does not claim those are the only refusals. - `organization` (`JOB_ORGANIZATION_DESCRIPTION`) is judged at bind by the posture rule. PR #21683 left that alone. - The `pull` and `mapping` TSDoc blocks ("binds on every door, as a `body` does"; "`defineStack` (and so `os validate`) refuses a name that resolves to neither") are true at the base too. ## Readers of the old wording `git grep -n -F` over tracked files at the base, for each of these substrings: ```text job excepted it is data too excepted: it is data pull` job excepted data too) job with no `body` (a ``` Each has two hits: `packages/spec/src/system/job.zod.ts:322` and the generated `content/docs/references/system/job.mdx:61`. No test, gate, skill or hand-written doc matches the old sentence, so nothing else moves. The broader substring `refuses an enabled job with no` also hits a pending changeset and a test `describe` title. Both are about the no-`body` refusal, which is unchanged. ## Regenerated with the repo's tooling - **Before the regeneration:** `pnpm --filter @objectstack/spec check:generated` ran after the describe commit `b58c6f819b`. 14 of 15 artifacts were current, and `check:docs` was stale on `content/docs/references/**`. - **The regeneration:** `check:generated --fix` rebuilt `packages/spec` first, regenerated only that artifact, and re-checked it green. The diff is the one table row in `job.mdx`. - No other generated artifact carries the describe. ## Changeset `.changeset/21703-job-body-describe-pull.md` names `'@objectstack/spec': patch`, since the describe ships in `src/**/*.zod.ts` and in the build. It carries `Clause-②: no` at line start. Text only. ## Verification at `c31367a1ec` (the final commit) - `pnpm --filter @objectstack/spec build`, under `os-verify-lock`: exit 0. - `pnpm --filter @objectstack/spec typecheck` (tsc, `check:scripts-typecheck`, `check:test-typecheck`): exit 0. - `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2`: 612 files passed. 18185 tests passed, 1 todo. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, with no paths: 3 paths against merge base `16d241a6a`, 104 commands. All 104 exit 0. - Six first exited 3 (`PREREQUISITE NOT MET`), because packages outside this diff were unbuilt. They were lint's `check:doc-formula-expressions` and `check:doc-security-posture`, `check:skill-examples`, `check:docs-transcript-drift`, `check:dual-build-cjs-loads` and `check:lean-entry-closure`. - A full `turbo run build --filter=!@objectstack/docs --concurrency=2` then ran under the lock (72 of 72 tasks). Each of the six exited 0 on rerun, and the record carries the reruns. - `--ran`: 104 derived, 104 run, 0 NOT-MEASURED, 0 UNRUN. - **ESLint, narrowed to the diff:** - `pnpm exec eslint --no-inline-config --format json` over the three diff paths reports 3 files. - ESLint's own config matches neither Markdown file ("File ignored because no matching configuration was supplied"). `job.zod.ts` has 0 errors and 0 warnings. - The config never enables type-aware linting: every `parserOptions` is `{ ecmaVersion: 'latest', sourceType: 'module' }`, and `eslint.config.mjs` says so at its query-options note. So a string edit in one file moves no untouched file's verdict. The full `pnpm lint` is left to CI. - `origin/main` has moved one commit since the base (`7e0066af7a`, tracker citations in spec test files). It touches none of this PR's paths, so the branch was not merged again. ## Acceptance notes - `pull`'s describe says "Data like `body`, so every door schedules it", and `body`'s says "Every door that brings an artifact in schedules a job's `body`". Both describe a run form that binds; one that does not bind is scheduled on no door. That was as true before PR #21683 as after it, so it is outside this card. Not filed. --- _Generated by [Claude Code](https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5259a35 commit 9d91f58

3 files changed

Lines changed: 16 additions & 2 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
`JobSchema.body`'s description now says an enabled `pull` job installs through `os package install` when its `pull` binds and is refused when it does not
6+
7+
Clause-②: no
8+
9+
The description said `os package install` refuses an enabled job with no `body`, "a `pull` job excepted: it is data too". Read plainly, that says the install never refuses a `pull` job. That stopped being true when the install-local door (`os package install`, `POST /api/v1/marketplace/install-local`) began refusing an enabled job whose `pull` does not bind, with the same `422 VALIDATION_ERROR` it gives a job whose `body` the declaration refuses.
10+
11+
The sentence now reads: a `pull` is data too, so an enabled `pull` job is judged by its `pull` instead. It installs when the `pull` binds (it names a mapping the package declares, with a `connectorSource`) and is refused when it does not, as is a job whose `body` the declaration refuses. The generated reference page for `job` carries the same text.
12+
13+
Text only: no key, schema shape, condition, error code or status moves. A tool or test that matches the old sentence needs the new one.

‎content/docs/references/system/job.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const result = CronScheduleSchema.parse(data);
5858
| **description** | `string` | optional | Job description / purpose |
5959
| **schedule** | `{ type: 'cron'; expression: string \| object; timezone?: string } \| { type: 'interval'; intervalMs: integer } \| { type: 'once'; at: string }` | ✅ | Job schedule configuration |
6060
| **handler** | `string` | optional | Handler function name (must match a key in `defineStack({ functions })`) — DEPRECATED, prefer `body`. When both are present `body` wins; refused beside `pull`. A job must declare one of `body`, `handler` or `pull`. |
61-
| **body** | `{ language: 'js'; source: string; capabilities?: Enum<'api.read' \| 'api.write' \| 'api.transaction' \| 'crypto.uuid' \| 'log'>[]; timeoutMs?: integer; … }` | optional | Job body — a sandboxed JS (L2) body, the same shape hooks and actions use; an expression (L1) body is refused, because a job runs for its effects and an expression has none. Preferred over `handler`: when both are present `body` wins. It runs in the QuickJS sandbox with no module scope (no imports, no helpers or constants from the surrounding file): it reaches data only through `ctx.api` under its declared `capabilities` (`api.read` / `api.write` / `api.transaction`) and logs through `ctx.log` (`log`); the in-process handler context (`ql`, `logger`, `bundle`) does not exist there. Its time limit is the job's `timeoutMs` (see there): long-running work declares a `timeoutMs` that covers it, or splits into bounded runs that each finish within it. Every door that brings an artifact in schedules a job's `body` — the boot, and `os package install` on install and on every restart — while a `handler` is code that travels only in the artifact's runtime module and runs only on a boot that loads it (a config, or `os start --artifact`); `os package install` therefore refuses an enabled job with no `body` (a `pull` job excepted: it is data too). Refused beside `pull`. |
61+
| **body** | `{ language: 'js'; source: string; capabilities?: Enum<'api.read' \| 'api.write' \| 'api.transaction' \| 'crypto.uuid' \| 'log'>[]; timeoutMs?: integer; … }` | optional | Job body — a sandboxed JS (L2) body, the same shape hooks and actions use; an expression (L1) body is refused, because a job runs for its effects and an expression has none. Preferred over `handler`: when both are present `body` wins. It runs in the QuickJS sandbox with no module scope (no imports, no helpers or constants from the surrounding file): it reaches data only through `ctx.api` under its declared `capabilities` (`api.read` / `api.write` / `api.transaction`) and logs through `ctx.log` (`log`); the in-process handler context (`ql`, `logger`, `bundle`) does not exist there. Its time limit is the job's `timeoutMs` (see there): long-running work declares a `timeoutMs` that covers it, or splits into bounded runs that each finish within it. Every door that brings an artifact in schedules a job's `body` — the boot, and `os package install` on install and on every restart — while a `handler` is code that travels only in the artifact's runtime module and runs only on a boot that loads it (a config, or `os start --artifact`); `os package install` therefore refuses an enabled job with no `body`. A `pull` is data too, so an enabled `pull` job is judged by its `pull` instead: it installs when the `pull` binds (it names a mapping the package declares, with a `connectorSource`) and is refused when it does not, as is a job whose `body` the declaration refuses. Refused beside `pull`. |
6262
| **pull** | `{ mapping: string }` | optional | Pull run form: on each run the platform pulls the named mapping's `connectorSource` (one action call, one response) and writes the rows through the import runner — no code. A refused pull records the run `failed` (retried per `retryPolicy`); a pull whose rows the import runner refused records it `degraded`. Data like `body`, so every door schedules it. Refused beside `body` or `handler`. |
6363
| **organization** | `string` | optional | Organization id (sys_organization.id) this job runs as — its `body`'s `ctx.api`, the execution context its `handler` is handed, and its `pull`'s reads and writes alike, as a system run carrying that organization. A scheduled run has no session to inherit one from. Judged at bind by the posture rule scheduled flows use: required under the isolated tenancy posture (a job that declares none is not scheduled); optional under group (undeclared, the run carries no organization and a tenant-scoped write it makes is refused); not required under single (the install's one organization is resolved beneath each write). Where declared, it is the organization the run acts as on every posture. |
6464
| **retryPolicy** | `{ maxRetries?: integer; backoffMs?: integer; backoffMultiplier?: number; maxRetryDelayMs?: integer; … }` | optional | Retry policy: failed runs (including timeouts) are retried with exponential backoff (delay = min(backoffMs * backoffMultiplier^(retry-1), maxRetryDelayMs), optionally jittered) up to maxRetries retries after the initial attempt. Omit the block for a single attempt; declaring it without `maxRetries` also means no retry since 17.0.0 — state a count to opt in. |

‎packages/spec/src/system/job.zod.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,8 @@ export const JobSchema = lazySchema(() => strictObject({
319319
+ 'Preferred over `handler`: when both are present `body` wins. '
320320
+ 'It runs in the QuickJS sandbox with no module scope (no imports, no helpers or constants from the surrounding file): it reaches data only through `ctx.api` under its declared `capabilities` (`api.read` / `api.write` / `api.transaction`) and logs through `ctx.log` (`log`); the in-process handler context (`ql`, `logger`, `bundle`) does not exist there. '
321321
+ "Its time limit is the job's `timeoutMs` (see there): long-running work declares a `timeoutMs` that covers it, or splits into bounded runs that each finish within it. "
322-
+ "Every door that brings an artifact in schedules a job's `body` — the boot, and `os package install` on install and on every restart — while a `handler` is code that travels only in the artifact's runtime module and runs only on a boot that loads it (a config, or `os start --artifact`); `os package install` therefore refuses an enabled job with no `body` (a `pull` job excepted: it is data too). "
322+
+ "Every door that brings an artifact in schedules a job's `body` — the boot, and `os package install` on install and on every restart — while a `handler` is code that travels only in the artifact's runtime module and runs only on a boot that loads it (a config, or `os start --artifact`); `os package install` therefore refuses an enabled job with no `body`. "
323+
+ 'A `pull` is data too, so an enabled `pull` job is judged by its `pull` instead: it installs when the `pull` binds (it names a mapping the package declares, with a `connectorSource`) and is refused when it does not, as is a job whose `body` the declaration refuses. '
323324
+ 'Refused beside `pull`.',
324325
),
325326
/**

0 commit comments

Comments
 (0)