Skip to content

Commit 932cbd2

Browse files
committed
Merge origin/main into claude/issue-17158-export-job-family-retired (regenerate contract.mdx)
Brings the branch up to origin/main 03d6cb0, which already carries the VALUE_TOO_LONG / VALUE_TOO_SHORT catalog edit. The os-regen driver deferred content/docs/references/api/contract.mdx and content/docs/references/api/error-code-ledger.mdx (both sides edited them); this commit carries origin/main's side of both, per scripts/pm/os-regen-merge.sh step 2, and the regeneration of the merged tree is the next commit. content/docs/api/error-catalog.mdx merged clean: main's VALUE_TOO_LONG / VALUE_TOO_SHORT entries plus this branch's removal and count line. Claude-Session: https://claude.ai/code/session_01AsCNgFBs8HCjwhyHQsFbx3 Co-authored-by: Claude <noreply@anthropic.com>
2 parents e9f9b06 + 03d6cb0 commit 932cbd2

1,055 files changed

Lines changed: 80607 additions & 24021 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.changeset/15178-translation-bundle-split-settings-platform-only.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,9 @@ and the rejection carries the prescription above.
6666

6767
### Unchanged
6868

69-
The registered `translation` metadata type (`TranslationItemSchema`) still
70-
declares `settings` — this ruling covers the file-authored bundle. `GET
69+
The registered `translation` metadata type (`TranslationItemSchema`) is not
70+
changed by THIS entry — this ruling covers the file-authored bundle. (Superseded
71+
in the same release: #19620 narrows the item door too; see its own changeset.) `GET
7172
/api/v1/i18n/translations/:locale` still declares it on its response, because the
7273
served document is the merged tree; `GetTranslationsResponseSchema` is typed
7374
against the platform face for exactly that reason.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@objectstack/plugin-hono-server": patch
3+
---
4+
5+
The UI auto-discovery block in `HonoServerPlugin.start()` no longer names the retired plugin type `ui-plugin`: the `plugin.type === 'ui-plugin'` disjunct and its "Support legacy" comment are removed, and the block mounts `type: 'ui'` plugins only (#15638).
6+
7+
Clause-②: no
8+
9+
- **Why nothing a plugin declares moves.** `ui-plugin` is not a member of the closed plugin-type set (`'standard'` plus `CORE_PLUGIN_TYPES`), and `kernel.use()` already refuses it on both published kernels, before the block can see it, with `PLUGIN_CONTRACT_VIOLATION ... at 'type'`. `LiteKernel.use()` throws it as-is; `ObjectKernel.use()` throws it behind its `Failed to load plugin: NAME - ` prefix. The disjunct was reachable through neither kernel's `use()`, so the deletion changes no accept or reject verdict for any declared value, and the refusal is the generic closed-set one. There is no message specific to this spelling.
10+
- **The one object that stops mounting.** The contract validates at `use()` and stores the plugin object by reference, so an object admitted as `ui` that then rewrites its own `type` to `ui-plugin` before `start()` used to be mounted by the removed disjunct. It no longer is.
11+
- **Fix**: declare `type: 'ui'`, with `staticPath` and `slug` (both required for that type).

‎.changeset/15705-mcp-resume-run.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/mcp': minor
3+
'@objectstack/runtime': minor
4+
---
5+
6+
feat(mcp): `resume_run` continues a flow run that paused on a screen, behind the same gates as `run_action` (#15705)
7+
8+
Clause-②: yes
9+
10+
**What changed.** `run_action` on a flow action whose flow stops on a `screen` node answers `status: "paused"` with a `runId` and the `screen` to fill in. Until now nothing on the MCP surface could submit that screen, so the run stayed parked: an agent could start such an action but never finish it. The new MCP tool `resume_run({ runId, values?, confirm? })` submits the screen's field values (keyed by the names in `screen.fields`) and the run continues. It answers with `run_action`'s envelope, `{ ok, action, objectName, recordId?, result }`. A run that pauses on its next screen comes back paused again, so a multi-screen wizard is walked by calling `resume_run` once per screen.
11+
12+
**Which runs it continues, and no others.** The runtime's bridge admits a call only where `run_action` would admit starting the same flow on the same record for this caller now:
13+
14+
- **Only the caller's own run.** The run's trigger identity must be the caller. Another user's run, an unknown id and a finished run all answer the same `404 RESOURCE_NOT_FOUND`. A resumed run continues under the identity of the user who started it, so only that user may continue it.
15+
- **`run_action`'s gates, with `run_action`'s helpers.** A `type: 'flow'` action whose `target` is the run's flow, on the run's object, must be AI-exposed (`ai.exposed`), must pass the caller's `requiredPermissions` and must not be switched off (`ACTION_DISABLED`, `409`). An action flagged `ai.requiresConfirmation` needs `confirm: true` on the resume too (`ACTION_CONFIRMATION_REQUIRED`, `428`), because the flow's writes happen after the screen. The exposure and permission refusals answer `403 PERMISSION_DENIED`. So does a run that no flow action targets.
16+
- **The subject record is read again as the caller.** A record the caller can no longer read is refused `404 RECORD_NOT_FOUND`, which is how `run_action` refuses it.
17+
- **Screen pauses only.** A run waiting on anything else (a timer `wait`, an approval) is refused `409 RESOURCE_CONFLICT` and left as it is.
18+
19+
Every refusal happens before the engine is asked, so the run stays parked. The engine's own answers (a screen submission missing a required field, a concurrent resume, a run that resumed and then failed) reach the caller with the code, status, message and `details` that `POST /api/v1/automation/:name/runs/:runId/resume` gives for the same result. The two doors now share one classification of the engine's answer. It was moved out of the REST route unchanged, and the route's answers are byte-identical.
20+
21+
**For hosts.** `McpActionBridge` gains an OPTIONAL member, `resumeRun(runId, { values?, confirm? })`. A bridge that implements it gets `resume_run` beside `run_action`, under the same `actions:execute` OAuth scope, on both the HTTP and the stdio transport. A bridge without it is unchanged and does not list the tool. `run_action`'s description names `resume_run` only where it is registered. `@objectstack/runtime`'s MCP bridge implements the member.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: `composeStacks` `objectConflict: 'merge'` refuses a fixed-shape config object both objects declare with different values (#16075)
6+
7+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is renamed, retired or re-typed: every object key, every `composeStacks` option and the `ConflictStrategySchema` enum parse exactly as before, so `objectstack migrate meta` has nothing to rewrite. What narrows is the ACCEPT SET of one option value at composition time, one step past the #14848 narrowing that answered the same question the same way: two stacks whose same-name objects both declare a fixed-shape config object (`enable`, `access`, `protection`, ...) with different values are now refused under `'merge'` where they used to compose with the earlier declaration silently replaced. The refusal text names the object, the key and both stacks and carries its own fix, no stored metadata row or authored file changes shape, and the repository measures zero non-test call sites passing `objectConflict` at all, so there is no document for a migration to act on. -->
8+
9+
**BREAKING** accept-set narrowing on `composeStacks({ objectConflict: 'merge' })`
10+
— shipped as `minor` under the repo's launch-window convention for breaking
11+
changes. Maintainer ruling on #16075 (ruling record 5563452716, director
12+
decision batch #61, option 1, verbatim 「同意」): the #14848 refusal extends to
13+
fixed-shape config objects.
14+
15+
**What changed.** #14848 made `'merge'` refuse every object-level
16+
**collection** two stacks declare differently, and left everything else on
17+
later-wins. "Everything else" included eight **fixed-shape config objects** on
18+
`ObjectSchema` — `userActions`, `external`, `tenancy`, `access`, `lifecycle`,
19+
`enable`, `publicSharing`, `protection`. Measured on `main` @ `44ce049a8`
20+
before this change, each of the eight composed to the LATER object's
21+
declaration wholesale, with nothing said: `enable: { trackHistory: true }`
22+
beside `enable: { apiEnabled: true }` lost `trackHistory`, and an add-on
23+
package's `access: { default: 'public' }` switched a core package's
24+
`access: { default: 'private' }` off — the posture downgrade `composeStacks`
25+
already refuses at the top level for `api` / `server`.
26+
27+
Now, when both objects declare one of them with different values,
28+
`composeStacks` throws the refusal it throws for a collection — same code
29+
(`STACK_COMPOSE_COLLECTION_CONFLICT`), same `status: 422`, same three-line
30+
shape — naming the object, the key and both stacks by manifest id:
31+
32+
```
33+
composeStacks conflict: object 'shared' is defined in multiple stacks and its 'access' is declared with different values by 'com.example.a' (stack #0) and 'com.example.b' (stack #1).
34+
objectConflict: 'merge' shallow-merges 'fields' only. Any other object-level collection (indexes, fieldGroups, requiredPermissions, validations, activityMilestones, highlightFields, listViews, searchableFields, actions) is not merged, and neither is a fixed-shape config object (userActions, external, tenancy, access, lifecycle, enable, publicSharing, protection): the later declaration would replace the earlier one wholesale, silently dropping every member 'com.example.a' (stack #0) set.
35+
Fix: declare 'access' on 'shared' in exactly one of the two stacks, make the two declarations identical, or use { objectConflict: 'override' } to hand the whole object to the later stack.
36+
```
37+
38+
The config-object half of the refusal set is **derived from `ObjectSchema`'s
39+
shape**, like the collection half — every key whose declared type, through
40+
optional/default wrappers, a `lazy` or a `pipe`'s authored side, is a plain
41+
object and not a collection — so a config object added to the object schema
42+
joins the refusal without an edit to the composer. The collection refusal's
43+
message now lists both kinds; its first and last lines are unchanged.
44+
45+
**What did not change.**
46+
47+
- `fields` keeps its documented shallow merge (later fields win, earlier
48+
fields kept).
49+
- **Identical** declarations on both sides pass through and are carried once
50+
— the reading `'merge'` already gives an identical collection. Because the
51+
strict parse fills a config object's member defaults, "identical" is judged
52+
on the parsed objects: `enable: { apiEnabled: true }` and
53+
`enable: { apiEnabled: true, trackHistory: false }` are the same declaration.
54+
- A config object only the earlier object declares is kept; a later object
55+
that does not declare it (or declares it `undefined`) leaves it in place.
56+
- A **scalar** the later object declares (`label`, `sharingModel`, …) still
57+
replaces the earlier one. So does a key whose type is a **union** admitting
58+
an object beside a non-object form — `systemFields` (`false` or an options
59+
object) and `titleFormat` (a template string or an expression object): a
60+
union is not a fixed shape, and the ruling covers the fixed-shape keys only.
61+
- The default `'error'` and `'override'` are untouched, message for message.
62+
63+
**Who is affected.** Measured on `origin/main` @ `44ce049a8`: **zero**
64+
non-test call sites in `packages/**`, `examples/**`, `apps/**` pass
65+
`objectConflict` at all — the one non-test `composeStacks` call
66+
(`examples/app-multi-package`) passes `{ manifest: 'preserve' }` and takes the
67+
default `'error'`. An external author who opted into `'merge'` and relied on
68+
the later package's config object winning silently now gets the refusal above;
69+
the fix is the one it names.
70+
71+
Clause-②: yes
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+
docs(spec): the `organizationId` describes on `GetMetaItemRequestSchema`, `GetMetaItemLayeredRequestSchema` and `GetMetaItemCachedRequestSchema` no longer promise that a supplied organization is always consulted (#16524)
6+
7+
Clause-②: no
8+
9+
Each of the three published `describe()`s opened with "Selects the org partition in the ADR-0005 overlay read order" and closed with an absent-only statement ("Absent = environment-wide read …"). Read together, an integrator completes that as *present ⇒ consulted*, and it is not: on `getMetaItem`, `getMetaItemLayered` and `getMetaItemCached` a supplied organization is dropped wherever no org partition applies, and the read resolves environment-wide exactly as if none had been sent.
10+
11+
The corrected text takes the same shape as the sibling `GetMetaItemsRequestSchema.organizationId` describe: the parameter selects the org partition **when an org partition applies**, and supplying a value "does not by itself guarantee an org partition is consulted; where none applies, and whenever it is absent, the read is environment-wide". On `GetMetaItemCachedRequestSchema` the ETag sentence ("Also folded into the ETag, so a scope switch never returns a stale 304 from another scope's cached representation") is true and is kept byte-for-byte.
12+
13+
Prose only. No key is added, removed or renamed, no export moves, no accept set changes and no runtime behaviour changes — a supplied `organizationId` is still accepted on all three requests, and the runtime still drops it where no org partition applies. What ships is the JSON-Schema `description` of the existing `organizationId` key on the three requests and the matching rows in the generated API reference.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
Corrects the ADR-0087 D2/D3 house-rule prose across the migration chain's doc comments (`packages/spec/src/migrations/types.ts`, `packages/spec/src/migrations/registry.ts` and `packages/spec/src/migrations/spec-changes.ts`) so it states the ruled convention: every retirement family gets one D3 `semantic` entry, even when a lossless D2 conversion also exists for it, and D2 carries the mechanical data repair only (#17152).
6+
7+
Clause-②: no
8+
9+
The shape ruled superseded (director-seat class-one self-adjudication on #17152, authority: the maintainer's ruling on #15954) was stated in several places, each in its own words: `types.ts`'s module docblock said "breaks with no lossless mapping"; `types.ts`'s `SemanticMigration`/`MigrationStep`/`reason` doc comments said "A non-lossless change", "Why this is not losslessly convertible" and "Non-lossless changes authored for this major"; `registry.ts`'s module docblock said "the non-lossless residue D2 could not express"; and `spec-changes.ts`'s `SpecMigratedSchema` said "A semantic (non-lossless) migration" with a `rationale` `.describe()` of "Why it is not losslessly convertible". All are corrected to the same meaning: a D3 entry is owed per retirement family regardless of whether D2 is lossless, and `reason`/`rationale` now say why the consumer still owes a judgment rather than why the change cannot be losslessly converted.
10+
11+
No entry, gate or runtime behaviour changes; this is a doc-comment correction, filed as `patch` because the `types.ts` prose ships verbatim into the published `dist/index.d.ts` / `dist/index.d.mts` (measured: `grep` after a real build finds each corrected sentence there, with `MigrationStep` and `MIGRATION_SUPPORT_FLOOR` as positive controls proving `dist` is readable). `registry.ts`'s docblock does not ship to `dist` at this head (same positive controls, zero hits either wording). `spec-changes.ts`'s `.describe()` text ships in the runtime bundles (`dist/index.js`, `dist/index.mjs`, `dist/browser/*`), not in the `.d.ts`, `json-schema/**` or `spec-changes.json` (measured the same way, with `SpecMigratedSchema` as the `.d.ts` positive control). `check:spec-changes` / `check:upgrade-guide` / `check:authorable-surface` all report their generated artifacts unchanged. Both are corrected for the same reason — each is prose the card and the ruling target.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
**BREAKING** — the export-job API family, the `IExportService` contract and `ScheduleState` leave the public surface (#17158).
6+
7+
A `major`-class change, recorded as `minor` under the launch-window convention. Maintainer ruling A (decision batch #122 item 3, 「同意」), landing route A (decision batch #221 item 2, 「同意」: objectui retired its side first, in objectui#10247), and a scope note (「同意」) that puts the export-job list pair in; ADR-0049 enforce-or-remove.
8+
9+
**Why.** `@objectstack/spec` declared a complete asynchronous export API — create a job, poll its progress, fetch a download link, list jobs, schedule a recurring export, cancel — and nothing on the platform served any of it. `@objectstack/rest` mounts no `/api/v1/data/export` route and no `POST` on `/api/v1/data/:object/export`; `IExportService` had no provider; and no package, example, app or skill in this repository, in objectui at the pinned sha, or in cloud read any of the names. An AI following the generated API reference wrote calls that answer `404`. The scheduled-export shapes were worse than unserved: after the cron positions were deleted earlier in this release line, `ScheduledExport.schedule` and `ScheduleExportRequest.schedule` were REQUIRED blocks that could hold no schedule, so an author who filled in the `timezone` believed they had scheduled something. `ScheduleState` described the runtime state of a scheduled flow that no scheduler ever wrote or read.
10+
11+
### FROM → TO
12+
13+
| removed | from | what to write instead |
14+
| --- | --- | --- |
15+
| `ExportJobStatus`, `CreateExportJobRequestSchema` / `CreateExportJobResponseSchema`, `ExportJobProgressSchema` (with their types and `…Parsed` aliases) | `@objectstack/spec/api` | nothing — no route ever created or tracked an export job. To export records, call the served synchronous door `GET /api/v1/data/:object/export` (the SDK's `data.export`), which answers the file itself as CSV, JSON or XLSX. |
16+
| `GetExportJobDownloadRequestSchema` / `GetExportJobDownloadResponseSchema`, `ListExportJobsRequestSchema` / `ListExportJobsResponseSchema`, `ExportJobSummarySchema` (with their types and `…Parsed` aliases) | `@objectstack/spec/api` | nothing — no job ever existed to download or list. |
17+
| `ScheduledExportSchema`, `ScheduleExportRequestSchema` / `ScheduleExportResponseSchema` (with their types and `…Parsed` aliases) | `@objectstack/spec/api` | a `Job` (`system/job.zod.ts`) whose handler performs the export, with its cadence on `Job.schedule.expression` — the one cron slot the platform evaluates. |
18+
| `ExportApiContracts` | `@objectstack/spec/api` | nothing — every route it named was unserved. |
19+
| `IExportService`, `CreateExportJobInput`, `CreateExportJobResult`, `ExportJobDownload`, `ListExportJobsOptions`, `ExportJobListResult`, `ScheduleExportInput` | `@objectstack/spec/contracts` | nothing — no provider ever bound the contract. |
20+
| `ScheduleStateSchema`, `ScheduleState`, `ScheduleStateParsed` | `@objectstack/spec/automation` | nothing — a scheduled flow declares its cadence on its start node (`config.schedule`), and its run history is `ExecutionLog` / `FlowRunSummary`. |
21+
22+
**The one-line fix: delete every import of the names above, and every request to `/api/v1/data/export/…` or `POST /api/v1/data/:object/export`.** The compiler finds the imports (`TS2305: Module '"@objectstack/spec/api"' has no exported member …`); a hard-coded path has to be searched for. No behaviour is lost — none of those requests was ever answered.
23+
24+
**What stays.** `ExportFormat`, `ExportImportTemplateSchema`, the import validation shapes and the whole import-job family in the same module — `ImportJobStatus`, `CreateImportJob…`, `ImportJobProgress…`, `ListImportJobs…`, `ImportJobApiContracts` — are served and unchanged, as is `GET /api/v1/data/:object/export`.
25+
26+
**Read together with the cron-positions retirement in this release.** That entry says `ScheduledExport.schedule` / `ScheduleExportRequest.schedule` keep their `timezone` and `ScheduleState` keeps `timezone`, `status` and `nextRunAt`; this retirement removes those defs whole, so none of those shapes remains to carry them.
27+
28+
⚠️ Runtime behaviour is deliberately **unchanged**: nothing ever mounted a retired path or parsed a retired shape, so every request answers exactly as before. The removal retracts a false claim, not a capability. **No deprecation window** (startup-stage posture: retirements take effect immediately).
29+
30+
⚠️ **The out-of-repo consumer population is NOT MEASURED.** Inside this repository the names occurred only in their declarations, their own tests, generated artifacts and prose; objectui at the pinned sha names none of them in code (it retired its unimplemented async-export path in objectui#10247), and cloud names none; `@objectstack/spec` is published, so readers elsewhere were not measured.
31+
32+
The ADR-0087 D3 semantic entry `export-job-family-retired` carries the judgement, and the thirteen defs are registered in `RETIRED_DEFS_BY_MAJOR[18]`: none of these shapes is a stack collection or a metadata type, so there is no source for a D2 conversion to rewrite and no carrier key for a tombstone.
33+
34+
Clause-②: no
35+
36+
<!-- adr-0087: registered export-job-family-retired -->

0 commit comments

Comments
 (0)