Skip to content

Commit 93cf173

Browse files
committed
Merge branch 'main' into claude/issue-14299-skills-platform-optimization
Nine sibling skills flights landed on main after this branch's base. Both conflicts fell inside the BEGIN/END GENERATED: skills region of the two mixed generated files (skills/README.md, content/docs/ai/skills-reference.mdx); resolved by clearing the markers in place — preserving main's text-merged prose outside the region — and regenerating both wholesale with `pnpm --filter @objectstack/spec gen:skill-docs` on the merged tree. scripts/check-skills-token-ratchet.mjs auto-merged (each flight edited its own package's ceiling rows). No behaviour or content change to the platform package. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01489YWhZEoHT9oXshiyywQy
2 parents 2501218 + 7085f90 commit 93cf173

599 files changed

Lines changed: 44113 additions & 6514 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.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
Startup `[action-governance]` resolves declarations through the same rungs the router does
6+
7+
The boot inventory built its declaration set from object-embedded `actions[]` plus the
8+
metadata service's `action` rows. `resolveRouteActionDeclaration` resolves through a third
9+
source between those two — the engine registry's standalone `action` items,
10+
`registry.getItem('action', name)`, accepted when the item owns the route. On the in-process
11+
boot (`new AppPlugin(...)` then `kernel.bootstrap()`), where the metadata plane holds no
12+
`action` rows at all, every object-less `defineAction` was therefore reported as a
13+
"registered handler with NO declaration — REFUSED at dispatch (ADR-0110 D3) and there is no
14+
opt-out" in the same boot in which the router resolved it at that rung and dispatched it.
15+
Both remedies the message offered were wrong for that shape: the action was already declared
16+
with `defineAction`, and dropping the registration would have broken a working endpoint under
17+
a green `pnpm validate`.
18+
19+
The registry rung is now injected into the audit by `ObjectQLPlugin` — the one caller holding
20+
the engine, because objectql cannot import the router — and judged by the same ownership test
21+
the router applies. The warning also stops asserting a dispatch outcome it never checked: it
22+
names the three sources it read, says it did not dispatch, and points an author whose action
23+
IS declared at the real bug instead of at deleting the registration. The other finding in the
24+
block, `declared script actions with NO handler`, is unchanged in wording and in population.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): refuse undeclared keys on `address` and `location` values — `AddressSchema` / `LocationValueSchema` are strict (#13802)
6+
7+
<!-- adr-0087: registered address-location-value-unknown-keys-refused -->
8+
9+
**BREAKING** accept-set narrowing on two ADR-0104 D1 value contracts, shipped
10+
as `minor` under the repo's launch-window convention for breaking changes; the
11+
migration prescription is registered under protocol major 18. Maintainer
12+
ruling 2026-09-01 on #13802 (director decision batch #26, verbatim 「同意」):
13+
option A.
14+
15+
`LocationValueSchema` and `AddressSchema` (`AddressValueSchema` is the same
16+
schema) were all-optional **stripping** `z.object`s. Every member being
17+
optional meant a value with a completely wrong key set still parsed green,
18+
and the wrong keys vanished from the parse output — the showcase seed wrote
19+
`postal_code`, the platform accepted it, dropped it, and rendered an empty ZIP
20+
box (#13388), while a stored-value scan over either class could only ever
21+
report a clean count it had no way to earn. Both are now `strictObject`s.
22+
`FileValueSchema` stays `z.looseObject` — the one deliberate loose site,
23+
untouched.
24+
25+
**What is refused:** any key the shape does not declare, with a prescriptive
26+
message naming the surface, the key, and a rename where one is known
27+
(`postal_code` / `zipCode` / `zip` / `postcode``postalCode`;
28+
`latitude``lat`, `longitude``lng`). The zod issue is
29+
`unrecognized_keys` and its `keys` name the offending spellings.
30+
31+
**What stays accepted:** every declared key byte-identically —
32+
`street`, `city`, `state`, `postalCode`, `country`, `countryCode`, `formatted`
33+
on an address; `lat`, `lng`, `altitude`, `accuracy` on a location.
34+
35+
**Where the refusal bites — and where it deliberately does not** (the
36+
ADR-0104 posture is unchanged; this changeset narrows the contract, not the
37+
write path's evidence gate):
38+
39+
- **Authoring, hard reject, unconditional:** a `location` / `address` field's
40+
literal `defaultValue` (`FieldSchema`, #7127) and an action param of those
41+
types (`validateActionParams`, strict by default since 17.0).
42+
- **Record writes, per deployment:** objectql's `validateRecord` rejects the
43+
value (`400 VALIDATION_FAILED`, field code `invalid_type`, message naming
44+
the key) **only** on a deployment that has attested `adr-0104-value-shapes`
45+
or set `OS_DATA_VALUE_SHAPE_STRICT_ENABLED=1` (`OS_ALLOW_LAX_VALUE_SHAPES=1`
46+
re-opens). Everywhere else the write is **admitted** warn-first, logged once
47+
per field, and reported to the admitted-violation sink — exactly as before.
48+
- **`os migrate value-shapes`** now counts an undeclared key as a violation,
49+
so a deployment holding such values cannot attest until they are cleaned at
50+
the producer. That scan is what keeps the strict flip from stranding stored
51+
data.
52+
- **Read paths: none.** No consumer parses these shapes on read; a stored
53+
`{ …, postal_code }` reads back as it was written. No read path was
54+
narrowed, and no consumer-side alias is introduced — `postal_code` is
55+
refused, never read.
56+
57+
## FROM → TO
58+
59+
```ts
60+
// before — parsed green; `postal_code` silently gone from the parsed output
61+
valueSchemaFor({ type: 'address' }, 'stored').safeParse(
62+
{ street: '1 Main St', city: 'Seattle', state: 'WA', postal_code: '98101', country: 'US' })
63+
// => { success: true, data: { street, city, state, country } }
64+
65+
// after — refused, naming the key and the declared spelling
66+
// => { success: false, error: { issues: [{ code: 'unrecognized_keys', keys: ['postal_code'],
67+
// message: 'Unrecognized key(s) on this address value: `postal_code`. Did you mean `postal_code` → `postalCode`? …' }] } }
68+
```
69+
70+
Fix: spell the key as the contract declares it — `postal_code``postalCode`
71+
in the producer (seed, importer, geocoder adapter, widget). For a location,
72+
`latitude` / `longitude``lat` / `lng`; drop device extras such as
73+
`heading` / `speed` or model them as fields of their own. Run
74+
`os migrate value-shapes` to find stored values that carry undeclared keys.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
"@objectstack/objectql": minor
3+
"@objectstack/runtime": patch
4+
---
5+
6+
feat(objectql): admit same-artifact co-owners at the namespace gate, and refuse two of them defining one object name (#14163)
7+
8+
ADR-0130 **D1 + D3**, landed as the single change D3 specifies as a machine
9+
constraint. Neither half may ship alone, in either order.
10+
11+
- **D1 — the gate's question is corrected.** `SchemaRegistry.installPackage`'s
12+
ADR-0048 namespace gate asked *"is this the same package id?"*; it now asks
13+
*"are these co-owners within one artifact?"*. Everything inside one release
14+
artifact is built, versioned, downloaded and installed as one atomic act by
15+
one publisher, and ADR-0130 D1 makes that joint delivery the ownership proof.
16+
`RESERVED_NAMESPACES` / `isShareableNamespace` are unchanged, and two packages
17+
from **different** artifacts sharing a namespace are still refused with the
18+
existing `NamespaceConflictError`.
19+
- **D3 — the guarantee the gate was silently carrying is now checked directly.**
20+
Namespace exclusivity has been proxying for *"no two packages define the same
21+
object name"* (ADR-0048 §3.2 grounds it on exactly that). So `installPackage`
22+
now refuses, **at install time and ahead of any DDL**, a package whose object
23+
name is already owned by a co-owner from the same artifact —
24+
`ArtifactObjectNameConflictError`, an ADR-0112 envelope (`code:
25+
'DUPLICATE_ARTIFACT_OBJECT_NAME'`, `status: 422`) naming both packages and the
26+
object. Without it, two co-owning packages defining `crm_account` would reach
27+
the DB as a duplicate `CREATE TABLE` or — driver-dependent — one silently
28+
overwriting the other's table definition.
29+
30+
**How the gate learns "same artifact".** An optional third argument on
31+
`installPackage` (`ArtifactInstallScope`, the artifact's own package-id list),
32+
threaded from the ADR-0130 D4/D5 load path through `ObjectQL.registerApp`. ⛔ No
33+
owner/publisher field on the manifest — ADR-0130 D8 defers that deliberately,
34+
and nothing is persisted, so a co-ownership claim cannot outlive or drift from
35+
the artifact that IS the claim.
36+
37+
**Not a compatibility break** (#14122 §6.2): the new refusal can only reject a
38+
configuration that would have failed at the DB anyway, and it rejects it earlier
39+
and more legibly. Every caller that installs one package —
40+
`protocol.installPackage`, `POST /packages`, a bare `registerApp` — passes no
41+
scope, so both halves are structurally no-ops there; a single-`manifest`
42+
artifact passes a one-element scope whose only member is the installing package,
43+
which the gate excludes anyway (D7's bit-identity pin covers it).
44+
45+
`@objectstack/runtime` carries the classification row for the new error code in
46+
the dispatcher error-code vocabulary (verdict `boot-refusal`: the refusal cannot
47+
be raised by either HTTP install site, which pass no artifact scope).
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
docs(spec): `ApprovalEscalation.timeoutHours` names its clock — calendar (wall-clock) hours
6+
7+
The `timeoutHours` describe text now states that the hours are calendar
8+
(wall-clock) hours: nights, weekends and holidays count, because the platform
9+
ships no business-hours calendar, so a request opened at 17:00 on a Friday with
10+
`timeoutHours: 4` escalates at 21:00 that same Friday. The sentence is published
11+
contract text — it is what `gen:schema` emits to the JSON schema `description`
12+
and what the reference page carries — so the unit is part of the declaration an
13+
author reads at authoring time rather than prose beside it. No key is added,
14+
renamed or defaulted differently; the approvals service's arithmetic is
15+
unchanged and is now pinned by a wall-clock test (Friday 17:00 + 4 h, a 168-hour
16+
deadline across a weekend, a DST transition).
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
"@objectstack/plugin-approvals": patch
3+
---
4+
5+
fix(approvals): the #3424 override reaches a `pending` request only — override-recall of a `returned` request is now refused (#12775)
6+
7+
**Behaviour change, declared explicitly (maintainer ruling 2026-09-02) so that
8+
one revert of this changeset's PR restores the previous behaviour.**
9+
10+
`ApprovalService.recall` admits two callers: the submitter, and a platform or
11+
tenant admin releasing a stuck request (the #3424 privileged override). Recall
12+
is also valid on the LATEST `returned` request of a run — the ADR-0044 revise
13+
window, where the submitter abandons the revision instead of resubmitting.
14+
Those two rules met above the state check: the override short-circuit carried
15+
no status test of its own, so an override actor could recall a `returned`
16+
request too. Nothing else on the platform said so — `isOverrideActor`'s doc
17+
block names a PENDING request, `attachViewers` computes
18+
`viewer.can_override` as `status === 'pending' && isOverrideActor(...)`, and
19+
the `approval_recall` action's override arm reads that flag — so the reach was
20+
API-only, never offered by any UI, and pinned by nothing.
21+
22+
What changes:
23+
24+
- **Override-recall of a `returned` request is refused.** The override
25+
short-circuit in `recall` now applies only while the request is `pending`,
26+
spelled exactly as the viewer flag is computed. On `returned` an override
27+
actor is judged exactly as any other non-submitter and receives the existing
28+
refusal: `403 FORBIDDEN` over REST, with the operation catalog's
29+
`approval_recall_not_submitter` sentence. No new error code, no new envelope.
30+
- **Only `pending` requests are override-recallable** — the same scope as the
31+
other three override levers (approve / reject / reassign are pending-only at
32+
their endpoints), and the same scope the viewer flag has always declared.
33+
34+
What does not change:
35+
36+
- The submitter's own recall of a `returned` request (the ADR-0044 revise
37+
window) is untouched; so is the submitter's recall of a `pending` one.
38+
- The override actor's recall of a `pending` request — the #3424 rescue of a
39+
request routed to an unstaffed position — is untouched, lock release and all.
40+
- The refusal's message, wire code and developer log line keep their shape; the
41+
log line now also names the request status it refused on.
42+
43+
Why: the gate now agrees with the viewer flag and the documented contract at
44+
one point instead of disagreeing with both. If a real operator workflow
45+
depended on override-recalling a `returned` request, this is the change to
46+
revert; the `returned` record lock is already released, so the stuck-record
47+
rescue motive that justifies the override does not apply on that status.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
"@objectstack/runtime": minor
3+
---
4+
5+
fix(runtime): one registrar for the artifact boot's security collections — the artifact door, not `AppPlugin` (#12892 step 2)
6+
7+
On an artifact boot (`createStandaloneStack`: `os serve` / `os dev` / `os start` /
8+
`os migrate` booting from `dist/objectstack.json`) the stack-declared
9+
`positions` / `permissions` / `capabilities` / `sharingRules` used to reach the
10+
metadata service through TWO registrars over the same bytes: the artifact door
11+
(`MetadataPlugin`, which forward-converts, strict-parses and stamps ADR-0010
12+
provenance) and `AppPlugin`'s ADR-0057 block (which does none of that). Because
13+
`AppPlugin` started last its copy won, so a consumer of
14+
`metadata.list('sharing_rule')` read a sharing rule whose `condition` was a bare
15+
STRING where the door's copy carries `{ dialect, source }` (reading
16+
`.condition.source` gave `undefined`), and a capability with no `scope` default
17+
and no `_packageVersion` / `_provenance`. Which shape a reader saw depended on
18+
plugin start order.
19+
20+
Maintainer ruling on #12892 (2026-08-29): the door owns the route. Now:
21+
22+
- `AppPlugin` takes `securityMetadataRegistrar: 'app-plugin' | 'artifact-door'`
23+
(constructor `opts`, default `'app-plugin'`). Under `'artifact-door'` the
24+
ADR-0057 block registers nothing and logs a `debug` line naming the
25+
collections left to the door; an unknown value is refused loudly.
26+
- `createStandaloneStack` — the one composition that runs the door over the
27+
same artifact — declares `'artifact-door'` on the `AppPlugin` it composes. On
28+
that boot the metadata service holds exactly ONE copy of every security item:
29+
the door's, parsed, defaulted and provenance-stamped, in either start order.
30+
- Every door-less composition (`new AppPlugin(config)` over a `defineStack()`
31+
module in `os serve` / `os migrate`, `DevPlugin`, `@objectstack/verify`'s
32+
`bootStack`, embedders) is unchanged: the default keeps registering all four
33+
collections exactly as before.
34+
35+
`minor` rather than `patch`: a public constructor option is added, and what the
36+
metadata service serves for these four kinds on an artifact boot changes shape
37+
(door copy instead of raw copy) — a fix, but one a reader of `GET /meta/<kind>`
38+
or `metadata.list(<kind>)` on an artifact boot can observe.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/objectql': minor
3+
---
4+
5+
Load a release artifact's packages in dependency-topological order (ADR-0130 D5).
6+
7+
The `manifest` service now reads both artifact shapes ADR-0130 D4 declares — `packages: [...]` when present, and the singular `manifest` treated as a one-element list when absent — and registers the packages inside one artifact in dependency-topological order, resolved by `resolvePluginOrder`, the platform's single topological sorter (ADR-0116). A package that extends another package's object therefore registers after the package it extends, whatever slot the artifact's array put it in.
8+
9+
An existing single-`manifest` artifact takes the second branch, by reference and unrewritten, and its registration state is unchanged (ADR-0130 D7). New: `resolveArtifactPackageOrder` is exported so every other door that grows an artifact-loading seam reads both shapes and orders them the same way.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
'@objectstack/lint': patch
3+
---
4+
5+
lint: `dashboard-filter-field-unknown` resolves dotted dashboard-filter fields on the object graph, and answers system columns per object
6+
7+
A dashboard-level filter (`dateRange`, or a `globalFilters[]` entry) is ANDed into
8+
**every** widget's analytics query, so its effective field — after any
9+
`filterBindings` re-target — has to resolve on each bound widget's dataset object.
10+
The rule that enforces that shipped with two holes, and this closes both by
11+
migrating the check onto the shared `resolveFieldPath` / `joinablePrefixes` seam
12+
the widget's own `filter` keys already use one position over.
13+
14+
- **Dotted paths are no longer skipped.** The branch carried
15+
`if (field.includes('.')) continue;`, accurate when nothing in the package could
16+
walk relationship hops and false since the object-graph seam landed. A filter
17+
re-targeted to `account.signed_at` was unjudged whether or not `account` existed,
18+
whether or not `signed_at` existed on it, and whether or not `account` was
19+
declared in the dataset's `include`. It is now walked hop by hop, and a miss
20+
names **which** hop failed.
21+
- **System columns are resolved per object, not through the flat union.** The old
22+
test was `objectFields.has(field) || SYSTEM_FIELDS.has(field)`, which answers
23+
"could this be a system column *anywhere*". On an `ownership: 'none'` object the
24+
platform injects no `owner_id`, and on `systemFields: { audit: false }` no
25+
`created_at` — both were answered as resolvable and are now reported.
26+
27+
New error id **`dashboard-filter-field-not-included`**: the effective field
28+
resolves, but its relationship prefix is not declared in the bound dataset's
29+
`include`, so ADR-0021 compiles no join and the column is out of the broadcast
30+
query's reach. It mirrors `widget-filter-field-not-included` one level down, and is
31+
its own id because the fix is a different edit (declare the join, versus point the
32+
filter at something real).
33+
34+
This **narrows the accept set of a shipped gating rule**. Both new answers are
35+
error-tier, so like the rule's other errors they fail `os validate` / `os build`
36+
and the runtime publish gate for `dashboard` writes. Measured over the shipped
37+
dashboard corpus — the three example apps plus the platform's own
38+
`system_overview` — the change is 0 findings before and 0 after; the
39+
`dashboard-filter-field-unprovisioned` warning is unchanged and now travels with
40+
the verdict, so it answers a dotted path landing on an ADR-0015 `external` object
41+
too.

0 commit comments

Comments
 (0)