Skip to content

Commit a7f3557

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-17306-screen-field-keys-live
2 parents b8c2600 + 3937ad2 commit a7f3557

21 files changed

Lines changed: 1670 additions & 73 deletions
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: an agent's `structuredOutput` is JSON-only — the `regex` / `grammar` / `xml` formats and the `coerce_types` step are retired, and the block is `live`, enforced by the cloud AI runtime (#21277)
6+
7+
**BREAKING** — four members leave the agent's structured-output vocabulary:
8+
`regex`, `grammar` and `xml` from `StructuredOutputFormat` (so from
9+
`agent.structuredOutput.format` and `agent.structuredOutput.fallbackFormat`), and
10+
`coerce_types` from `TransformPipelineStep` (so from
11+
`agent.structuredOutput.transformPipeline`). ADR-0049 enforce-or-remove, ruled
12+
retire. The cloud AI runtime, the one runtime that executes agents, enforces
13+
`structuredOutput` on every final answer and refused an agent declaring any of the
14+
four before its first turn: the spec never had a key to carry the pattern or
15+
grammar a `regex` / `grammar` answer would be checked against, an answer is checked
16+
only as JSON, and no coercion engine exists. So no authored value of the four ever
17+
did what it named, and authoring now refuses them by name instead of the first
18+
live turn refusing the agent. `json_object`, `json_schema`, `trim`, `parse_json`
19+
and `validate` are unchanged.
20+
21+
### FROM → TO
22+
23+
| removed | what to write instead |
24+
| --- | --- |
25+
| `structuredOutput.format: 'regex'`, `'grammar'` or `'xml'` | `format: 'json_schema'` with a JSON Schema in `schema` when the answer must have a shape, or `format: 'json_object'`; or delete the `structuredOutput` block if the agent needs no output contract. |
26+
| `structuredOutput.fallbackFormat: 'regex'`, `'grammar'` or `'xml'` | `'json_object'` or `'json_schema'`, or delete the key. |
27+
| `'coerce_types'` in `structuredOutput.transformPipeline` | delete the step, and declare the exact types in `schema` so the answer is validated as the model wrote it. |
28+
29+
**The one-line fix: use `json_schema` with a JSON Schema; drop `coerce_types`.**
30+
`os migrate meta --from 17` lists the mechanical edits for existing sources.
31+
32+
Each retired member is refused at parse with a prescription naming the JSON
33+
formats, and in `tsc` (the members are gone from the `StructuredOutputFormat` /
34+
`TransformPipelineStep` types). Any other unknown value keeps zod's own message.
35+
36+
### The retirement kit
37+
38+
- **Value-level retirement.** Both enums are declared through
39+
`enumWithRetiredValues` (`shared/retired-key.ts`), the house mechanism for a
40+
narrowed vocabulary, with the prescriptions module-private. No authorable KEY and
41+
no def changed, so nothing lands in `RETIRED_KEYS_BY_MAJOR` and the four surface
42+
ratchets (`api-surface`, `authorable-surface`, `json-schema.manifest`,
43+
`api-surface-signatures`) are byte-identical.
44+
- **D2 conversion `agent-structured-output-refused-members-removed`** (step 18,
45+
retired from the load path): it deletes a `structuredOutput` block whose `format`
46+
was retired (the format is required, and no rewrite can say which JSON contract
47+
was meant), deletes a retired `fallbackFormat`, and drops `coerce_types` from the
48+
pipeline, keeping the other steps in order. Stored `sys_metadata` agent rows replay
49+
it at rehydration; one notice per edit.
50+
- **D3 entry `agent-structured-output-refused-members-retired`** carries the
51+
judgement the conversion cannot make: whether an agent whose block was deleted
52+
should now carry a `json_schema` contract.
53+
- **No deprecation window**, per the project's startup-stage posture.
54+
55+
### Describes and the liveness ledger
56+
57+
- `agent.structuredOutput` drops `[EXPERIMENTAL — not enforced]`: it states that the
58+
cloud AI runtime enforces it on every final answer and that the open framework
59+
edition does not run agents. Its ledger row moves `experimental` → `live`, citing
60+
the cloud readers (`agent-runtime.ts#compileStructuredOutput`,
61+
`ai-service.ts#AIService.settleFinalAnswer`) as attested by the cloud seat's
62+
reading at cloud `cb62c3ea`, `verifiedAt` 2026-10-02. `os lint` / `os validate` no
63+
longer warn `liveness-experimental-property` on an agent that sets it.
64+
- `fallbackFormat`'s describe states what the runtime does with it: once the primary
65+
format's retries are spent, the last answer is checked against the fallback.
66+
- `guardrails.blockedTopics`'s describe states the enforced match: an exact,
67+
case-sensitive match on the tool name, on `action_` plus the action type, or on
68+
the tool category.
69+
- The generated agent reference page follows.
70+
71+
⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is
72+
published, and tenant-authored agents were not measured. This repo authors no
73+
`structuredOutput` outside `packages/spec`, and the cloud seat's reading found no
74+
producer in cloud.
75+
76+
Clause-②: no (narrowing)
77+
78+
<!-- adr-0087: registered agent-structured-output-refused-members-removed, agent-structured-output-refused-members-retired -->
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"@objectstack/service-analytics": patch
3+
---
4+
5+
fix(service-analytics): the ObjectQL strategy applies a query's `order`, then its `offset` and `limit`, to the aggregated answer, as its echoed `sql` says
6+
7+
Clause-②: no
8+
9+
**Before**, the ObjectQL strategy passed none of the three keys to `engine.aggregate`, which has no ordering or window grammar, and applied none of them itself. Every date-bucketed query lands on that strategy, because the native-SQL strategy declines `granularity`. Measured through `POST /api/v1/analytics/query` on SQLite and PostgreSQL 16.14:
10+
11+
- `timeDimensions: [{ dimension: 'closed_on', granularity: 'month' }]`, `order: { closed_on: 'desc' }`, `limit: 1` answered every month, unordered (ascending on SQLite, `04, 03, 05` on PostgreSQL).
12+
- A selected dimension with `order: { note: 'desc' }`, and a selected measure with `limit: 2, offset: 1`, answered every group in the engine's order.
13+
14+
The echoed `sql` and `POST /api/v1/analytics/sql` rendered `ORDER BY … LIMIT … OFFSET …` for all three.
15+
16+
**Now** the strategy orders the answer by `order`, in the key order given, and then applies `offset` and `limit`. This happens on the direct path and on the cross-object (FK-expand) path, after the re-bucket. A bare `limit` with no `order` slices the engine's order, as `LIMIT` without `ORDER BY` does. Where the native-SQL strategy answers the same query, the two answer the same rows for numbers and for text of single-case ASCII letters. The comparison is the dataset door's own `applyOrdering`, which sorts NULL and `''` last in both directions, while SQL places NULL by driver (lowest on SQLite, highest on PostgreSQL), so the two faces can still order NULL, `''`, numeric text and mixed-case text differently.
17+
18+
**Dataset door.** `POST /api/v1/analytics/dataset/query` pushes a single query's `order`, `limit` and `offset` down to the strategy, and then windowed the answer a second time, so `offset` was applied twice. `limit: 2, offset: 1` over five groups answered one row, the third, on the native-SQL strategy. It now windows only a grid it could not push down. The ObjectQL strategy answered that page correctly before, because it dropped the window; it still does.
19+
20+
**Unchanged.** A query with no `order`, `limit` or `offset` answers exactly the engine's aggregate rows. Which `order` keys are accepted is unchanged: the analytics door still refuses a key the query does not select. The dataset door's own ordering is unchanged too: label sort keys, derived measures, the implicit dimension order for a bare `limit`, and the chronological default.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/cli': minor
3+
---
4+
5+
fix(cli)!: `os verify` runs the author-time rules first, and a stack they refuse fails `verify` with the findings `os validate` reports (#21323)
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) a CLI command's verdict, not a declaration: os verify now refuses, before it boots anything, a stack the author-time rule registry refuses or that does not parse against the protocol schema. No authorable key, spelling, export or stored shape moves: every stack parses and loads exactly as before, os validate and os build answer exactly as before, and nothing on the metadata load path is read or rewritten. What an author does about a refusal is fix the finding the message names, which os validate and os build already report for the same stack, so there is no rewrite a ledger entry could carry. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id covers a command's verdict (not registered / already-registered); and the change is CLI behaviour, not a TypeScript declaration (not runtime-interface-only / type-surface-only). -->
10+
11+
**BREAKING** — `os verify` narrows what it passes. It ships as `minor` under the launch-window convention for accept-set narrowings.
12+
13+
**What was accepted before.** `os verify` booted the app and exercised CRUD round-trip fidelity and, with `--rls`, the RLS invariant — and nothing else. A stack carrying a lookup to an object that does not exist, an action `visible` expression naming a field without `record.`, or a list column naming no field booted, round-tripped its records and printed `✓ verify passed` at exit 0, while `os validate`, `os build` and `os lint` all refused it. The documented done-bar ("`objectstack verify` is green") was green on a stack the build refuses to ship.
14+
15+
**What is refused now.** `os verify` runs two stages. The first is the author-time rule registry `os validate` runs, over the stack prepared the way `os validate` prepares it: normalized, inline handlers lowered, parsed against the protocol schema, the SDUI manifest read beside the config, judged whole and then once per package of a multi-package artifact. A gating finding, or a stack that does not parse, exits 1 with those findings and the runtime stage never starts:
16+
17+
- text face: `✗ Author-time rules failed (N issues) — the runtime stage did not run`, then each finding with its rule and location (the per-package and schema refusals have their own sentence);
18+
- `--json`: the command's failure envelope, `error` (the sentence), plus a new key, `errors`, carrying the findings in the shape `os validate --json` carries them under `errors` — rule findings (with `package` on a per-package one), or the schema issues.
19+
20+
Advisories never fail the stage; the text face counts them and points at `os validate`. On a passing stack the text face prints one step line and `✓ Author-time rules passed (N rules)` before the runtime stage, and the `--json` report of a run that reaches the runtime stage is unchanged.
21+
22+
**Who is affected.** Only a stack `os build` already refuses: the first stage runs the same gating rules over the same prepared stack, so every stack it refuses, `os build` refuses too. The remedy is the one `os validate` prints for each finding. Measured with this branch's CLI over the examples at `222ecc27f9` (unchanged on this branch): `os validate` exits 0 on `examples/app-todo`, `examples/app-crm`, `examples/app-showcase` and `examples/app-multi-package`, so none of the four is refused by the new stage.

‎content/docs/deployment/cli.mdx‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1614,6 +1614,7 @@ emits a scoped, publishable `@objectstack/plugin-<name>`, and it lands under
16141614
| Command | Description |
16151615
|---------|-------------|
16161616
| `os lint [config]` | Every author-time gate `validate`/`build` run, plus style and convention checks |
1617+
| `os verify` | The author-time rules `validate` runs, then boot the app in-process and verify it through the real HTTP stack |
16171618
| `os test [files]` | Run Quality Protocol test scenarios against a running server |
16181619
| `os doctor` | Check development environment health |
16191620
@@ -1679,6 +1680,62 @@ the flag was in effect), `failing` (the count the exit code was read from —
16791680
`errors`, or `errors + warnings` under `--strict`) and `passed` (`failing` is
16801681
`0` — the same statement the exit code makes).
16811682
1683+
#### `os verify`
1684+
1685+
The done-bar: `os verify` green means the author-time rules pass **and** the
1686+
app behaves at runtime. It runs two stages, in order, and the second starts
1687+
only when the first passes.
1688+
1689+
```bash
1690+
os verify # Author-time rules, then CRUD round-trip fidelity
1691+
os verify --app ./objectstack.config.ts --rls # Also prove the cross-owner RLS invariant
1692+
os verify --rls --multi-tenant --json # Org-scoped boot, structured report
1693+
```
1694+
1695+
1. **Author-time rules.** The rule registry `os validate` runs, over the stack
1696+
prepared the way `os validate` prepares it: normalized, inline handlers
1697+
lowered, parsed against the protocol schema, and judged whole and then once
1698+
per package of a multi-package artifact. A stack that does not parse, or
1699+
that any gating rule refuses, fails the run here with those findings — the
1700+
same ones `os validate` reports — and the app is never booted. Advisories
1701+
(`warning` and `info` findings) never fail `os verify`; the text face counts
1702+
them, and `os validate` prints them. This stage is the rule registry, not all
1703+
of `os validate`: the package-docs lint, the capability-provider check, the
1704+
picklist-reference and view-container-name checks and `--strict` stay
1705+
`os validate`'s, so run it too.
1706+
2. **Runtime.** Boot the app in-process and exercise it through the real HTTP
1707+
stack: for each object, a record derived from its fields is created, read
1708+
back and compared (CRUD round-trip fidelity), and a create, read or
1709+
fidelity failure fails the run. With `--rls`, a second fresh boot proves the
1710+
cross-owner invariant — a member must not write what it cannot read — for a
1711+
probe persona and for one persona per declared position, and a hole fails
1712+
the run. `--multi-tenant` boots org-scoped so tenant-isolation RLS policies
1713+
apply; a walled tenancy posture (`OS_TENANCY_POSTURE` set to `isolated` or
1714+
`group`) asks for the same boot.
1715+
1716+
The config is the one `--app` names, or the auto-detected one — see
1717+
[Config File Auto-Detection](#config-file-auto-detection).
1718+
1719+
**Exit status.**
1720+
1721+
| Exit | Meaning |
1722+
|---|---|
1723+
| `0` | Both stages passed: no gating author-time finding, and no runtime failure |
1724+
| `1` | The author-time stage refused the stack (the runtime stage did not run), the runtime stage found failures, or the command could not run at all (no config found, a config that does not load, a boot failure) |
1725+
1726+
**`--json`.** What the document carries depends on where the run ended:
1727+
1728+
- **Refused by the author-time stage:** `{ "error": "<sentence>", "errors": [...] }`,
1729+
where `errors` carries the findings in the shape `os validate --json` carries
1730+
them under `errors` — the gating findings (`severity`, `rule`, `where`,
1731+
`path`, `message`, `hint`, plus `package` for a finding raised inside one
1732+
package of the artifact), or the schema issues when the stack does not parse.
1733+
- **Reached the runtime stage:** the report — `app`, `config`, `multiTenant`,
1734+
`crud`, `rls` (with `--rls`) and `hardFailures`, the count the exit status is
1735+
read from.
1736+
- **Could not run:** `{ "error": "<sentence>" }`, plus `code` and `httpStatus`
1737+
when the failure carries them.
1738+
16821739
#### `os test`
16831740
16841741
Runs Quality Protocol test scenarios (JSON-based BDD) against a running ObjectStack server.

0 commit comments

Comments
 (0)