Skip to content

Commit cf532f2

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-15317-conditional-floors
2 parents f0ac7dc + a623a15 commit cf532f2

79 files changed

Lines changed: 6967 additions & 560 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: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
---
2+
"@objectstack/runtime": patch
3+
---
4+
5+
fix(runtime): a multi-package artifact's collections are read from `packages[]`, not only from the flattened top level
6+
7+
A release artifact composed with `manifest: 'preserve'` carries every
8+
definition twice — flattened at its top level, and again under
9+
`packages[]` (ADR-0130 D4). Only two readers had ever learned the second
10+
half: `ObjectQLPlugin`'s manifest service and the metadata artifact door.
11+
Every other reader said `artifact.<collection>` and nothing else, so an
12+
artifact that carried a collection under `packages[]` alone reached them
13+
EMPTY — and nothing threw. The app booted clean having lost its
14+
declarative actions, its scheduled jobs, its seed data, its object routing
15+
or its default permission set.
16+
17+
`resolveArtifactCollections` — new, and PACKAGE-PRIVATE to
18+
`@objectstack/runtime` — is now the one way this package reads a top-level
19+
collection out of an artifact in either shape. It takes the artifact's own
20+
top-level value first and whole, then adds from each package body — in
21+
`resolveArtifactPackageOrder`'s dependency order — the items the top level
22+
did not already claim. A bundle that carries no `packages[]` is returned
23+
unchanged, by identity: every single-package artifact and every
24+
`defineStack()` config reads exactly as before. Nothing is added to any
25+
package's published surface: `@objectstack/core` is untouched by this
26+
change, and the new module is not named by
27+
`packages/runtime/src/index.ts`.
28+
29+
Where one collection key is spelled two ways inside one artifact —
30+
`functions` is `z.union([z.record(…), z.array(…)])`, so two packages can
31+
each be schema-valid and disagree — the read is REFUSED with an ADR-0112
32+
envelope (`MIXED_ARTIFACT_COLLECTION_SHAPE`, 422) rather than one spelling
33+
being skipped. `composeStacks` already refuses the same mix at compose
34+
time for the same reason.
35+
36+
Taught to use it, in `@objectstack/runtime`:
37+
38+
- `AppPlugin` — declared datasources and their auto-connect, the
39+
`datasourceMapping` object routing, the objects handed to the connection
40+
service and to the hot-reload seeder, scheduled jobs, seed datasets,
41+
translation bundles, and the ADR-0057 security collections
42+
(`positions` / `permissions` / `capabilities` / `sharingRules`). A job
43+
handler's `ctx.bundle` is now the resolved view too, so
44+
`ctx.bundle.objects` answers on a multi-package artifact.
45+
- `collectBundleActions`, `collectBundleHooks` and
46+
`collectBundleFunctionEntries` — including the object-EMBEDDED actions
47+
that ride on `objects[]` and disappeared with it.
48+
- `mergeRuntimeModule` — the declaration half. The sibling ESM module
49+
re-supplies every callable regardless of shape, so `functions` was not
50+
absent: a function declared `effect: 'writes'` simply came back as a bare
51+
callable and defaulted to `'pure'`. It registered, it ran, and its writes
52+
were counted as none.
53+
- `createStandaloneStack`'s surfaced `requires` / `objects` /
54+
`permissions` / `positions`, which drive the CLI's tier resolution, its
55+
engine and storage-driver auto-registration, and the ADR-0056 D7 default
56+
permission set.
57+
- `resolve-project-database`'s project-database tier, which opens the
58+
artifact itself and runs before any stack exists (`os dev`, `os start`,
59+
`os db clean`). Without this a multi-package project silently fell
60+
through to the unified default database instead of the datasource it
61+
declared.
62+
63+
Nothing about what the platform EMITS changes: `composeStacks` and the
64+
artifact format are untouched, and the flattened top level is still
65+
written. This is the reader half of the option-B program (#14512).
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
"@objectstack/metadata-protocol": minor
3+
---
4+
5+
fix(metadata-protocol)!: a batch ROW reports a unique-constraint refusal as `UNIQUE_VIOLATION` — the same wire spelling as the whole-request failure on the same route (#14723)
6+
7+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable moves: no spec key, export, config field or stored metadata changes spelling or shape, `packages/spec` is untouched and `objectstack migrate meta` has nothing to rewrite. What moves is the `code` value one REST response row carries for one condition — the wire report of a driver's unique-constraint refusal on the per-row surface of `POST /api/v1/data/:object/batch` — which now spells the standard-catalog member the whole-request doors and the published protocol docs already use. The consumer note below is guidance for a client branching on that row code; it prescribes no rewrite of any authored artifact. -->
8+
9+
**BREAKING** on the per-row report of `POST /api/v1/data/:object/batch` (and
10+
the multi-object `POST /api/v1/batch`, which rides the same protocol): a row
11+
refused by the engine's `DuplicateRecordError` envelope now reports
12+
`errors[].code: 'UNIQUE_VIOLATION'` where it reported `'DUPLICATE_RECORD'`.
13+
Shipped as `minor` under the repo's launch-window convention for breaking
14+
changes. Maintainer ruling 2026-09-03 on #14723 (verbatim 「同意,然后执行契约
15+
复审」), adopting option A: one wire spelling for a unique-constraint refusal on
16+
every route.
17+
18+
**Why.** `toRowApiError` put a thrown REGISTERED code on the row verbatim, and
19+
`DUPLICATE_RECORD` is registered, so a `DuplicateRecordError` row said
20+
`DUPLICATE_RECORD` while the whole-request failure on the very same route (the
21+
bulk door's classification in `@objectstack/rest`) answered `UNIQUE_VIOLATION`
22+
— the standard-catalog member `content/docs/protocol/kernel/http-protocol.mdx`
23+
documents for the 409 constraint-violation body. Since the bulk doors were
24+
restored to `UNIQUE_VIOLATION`, the two spellings of one condition sat side by
25+
side in one route's responses, which ADR-0112's one-name-per-concept and the
26+
error-code ledger's own header both forbid. The duplication is removed, not
27+
declared: no ledger waiver is added.
28+
29+
**What changes.** The row derivation recognises the engine's envelope by the
30+
same two-part gate the whole-request arm uses — the registered code AND the
31+
class name `DuplicateRecordError`, never message text — and reports
32+
`UNIQUE_VIOLATION`. Everything else on the row is unchanged: `httpStatus: 409`,
33+
the platform sentence (no driver text, no bound value — the driver's error
34+
stays on `cause` and never reaches the row), and the sibling `NOT_ATTEMPTED` /
35+
`ROLLED_BACK` rows.
36+
37+
**What does NOT change.** The engine's thrown identity: `DuplicateRecordError.code`
38+
is still `DUPLICATE_RECORD` for an in-process caller of `engine.insert` /
39+
`engine.update` (a hook, a flow node), and the objectql pins on `insert` /
40+
`insertMany` hold. The single-record `/data` door, which has answered
41+
`UNIQUE_VIOLATION` throughout, does not move. A producer that merely THROWS the
42+
registered `DUPLICATE_RECORD` from its own body without being the engine's
43+
class keeps its own code on the row, exactly as it does at the door.
44+
45+
**Consumer note.** A batch client that branched on a row's `code` reading
46+
`DUPLICATE_RECORD` reads `UNIQUE_VIOLATION` there now — the same value it
47+
already handles for the whole-request 409 on that route and on the
48+
single-record door. Measured in-repo and in the sibling repos (hotcrm, objectui,
49+
non-test sources): zero consumers branch on either spelling of a row code.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): retire `connector.errorMapping` — eleven authorable keys nothing ever read, one of them spelled like the live `userMessage` channel (#14676, ADR-0049)
6+
7+
<!-- adr-0087: registered connector-error-mapping-removed -->
8+
9+
**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep
10+
launch-window convention ships it as `minor`; the migration prescription is
11+
registered under protocol major 18, where `os migrate meta` users will look).
12+
Triage ruling 2026-09-02 on the census card: ADR-0049 enforce-or-remove decides
13+
it — declared-but-unenforced authorable surface with zero measured pull for a
14+
reader comes off.
15+
16+
`ConnectorSchema.errorMapping` carried `ErrorMappingConfig` (`rules`,
17+
`defaultCategory`, `unmappedBehavior`, `logUnmapped`) and its
18+
`ErrorMappingRule[]` (`sourceCode`, `sourceMessage`, `targetCode`,
19+
`targetCategory`, `severity`, `retryable`, `userMessage`) — eleven keys on the
20+
published authorable surface that **nothing read**: measured on `origin/main`,
21+
the only reference outside the declaring file and its unit test was a
22+
type-identity pin. No provider, dispatcher or materializer ever mapped an
23+
external error through the rules, so `unmappedBehavior` configured nothing and
24+
a rule's `userMessage` was never shown to anyone. That spelling is what made
25+
this worse than ordinary dead surface: it is the name of the **live**
26+
API-error channel (`ApiError.userMessage`, the user-facing refusal text a
27+
thrown HTTP error declares), so an author who had read that documentation and
28+
wrote a connector rule reasonably believed they were marking a refusal for an
29+
end user — and the failure was silent in both directions (it validated, it
30+
published, no message was ever shown). Removal resolves the collision by
31+
deletion; the live channel is untouched.
32+
33+
**What is refused:** authoring `errorMapping` on a connector, with any value.
34+
`ConnectorSchema` is a non-strict `z.object`, so the key is a `retiredKey()`
35+
tombstone rather than a bare deletion (a deletion would have stripped it in
36+
silence): authoring it is a `tsc` error (`never`) and a parse error carrying
37+
the prescription, on the base schema and — through
38+
`DeclarativeConnectorEntrySchema`, which `superRefine`s the same shape — on
39+
`stack.connectors[]` and the `PUT /api/v1/meta/connector/:name` door.
40+
41+
**What leaves the public surface:** `ErrorMappingConfigSchema` /
42+
`ErrorMappingConfig` / `ErrorMappingConfigParsed`, `ErrorMappingRuleSchema` /
43+
`ErrorMappingRule`, and `ConnectorErrorCategorySchema` / `ConnectorErrorCategory`
44+
(the enum's only consumers were the two removed shapes; an exported value
45+
schema with no consumer reads as a capability). `api/ErrorCategory` — the
46+
HTTP-response vocabulary — is unaffected.
47+
48+
**What stays, byte-identical:** every other connector key (`health`, `retry`,
49+
`webhooks`, `fieldMappings`, `syncConfig`, `actions`, `triggers`, `provider`,
50+
`providerConfig`, `auth`, …) with its default and its readers.
51+
52+
## FROM → TO
53+
54+
```ts
55+
// before — parsed green; nothing ever read the block, no message was ever shown
56+
defineStack({
57+
connectors: [{
58+
name: 'payments_api',
59+
label: 'Payments API',
60+
type: 'api',
61+
errorMapping: {
62+
rules: [{
63+
sourceCode: 429,
64+
targetCode: 'RATE_LIMITED',
65+
targetCategory: 'rate_limit',
66+
severity: 'medium',
67+
retryable: true,
68+
userMessage: 'The payment provider is busy; try again shortly.',
69+
}],
70+
unmappedBehavior: 'generic_error',
71+
},
72+
}],
73+
});
74+
75+
// after — delete the key; there is no replacement because no error-mapping
76+
// engine exists: a connector's failures reach callers as the provider's own
77+
// errors (ADR-0097). A user-facing refusal text is the API error envelope's
78+
// `userMessage`, declared by the code that throws — not connector metadata.
79+
defineStack({
80+
connectors: [{ name: 'payments_api', label: 'Payments API', type: 'api' }],
81+
});
82+
```
83+
84+
One-line fix: delete the `errorMapping` block; `os migrate meta --from 17`
85+
lists the mechanical edits for existing sources.
86+
87+
The retirement kit:
88+
89+
- `retiredKey()` tombstone on `ConnectorSchema.errorMapping`
90+
(`packages/spec/src/integration/connector.zod.ts`; the section comment
91+
records what the shape was), inherited by `DeclarativeConnectorEntrySchema`
92+
- ADR-0087 registration: `integration/Connector:errorMapping` and
93+
`integration/DeclarativeConnectorEntry:errorMapping` in
94+
`RETIRED_KEYS_BY_MAJOR[18]`; `integration/ErrorMappingConfig`,
95+
`integration/ErrorMappingRule`, `integration/ConnectorErrorCategory` in
96+
`RETIRED_DEFS_BY_MAJOR[18]`; the D2 conversion
97+
`connector-error-mapping-removed` (protocol 18) wired into the step-18 chain
98+
— a pure lossless strip of the block from every `connectors[]` entry, one
99+
notice per connector (the eleven nested keys leave with the block)
100+
- no liveness-ledger row: `connector` is not an enrolled ledger type, so
101+
there is no row to keep or drop
102+
- pin tests (`connector.test.ts`): refusal pins asserting the issue path,
103+
code and prescription on the base schema, the declarative entry, and the
104+
`stack.connectors[]` authoring path; the tsc `never` channel; a
105+
no-materialize pin; the conversion's strip and notice; zero holders of the
106+
seven retired names on every public entry; the ADR-0087 registration
107+
- generated baselines/docs follow the schema (`authorable-surface/`,
108+
`authorable-defaults/`, `api-surface/`, `json-schema.manifest/`,
109+
`declaration-map/`, `export-origins/`, spec-changes, upgrade guide,
110+
reference docs)
111+
- zero authored occurrences in this repo's examples, skills and docs, and
112+
zero hits in objectui at `0d8fd7c`, so no in-repo source changes ride along
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"@objectstack/objectql": patch
3+
---
4+
5+
fix(objectql): `DuplicateRecordError.developerMessage` names the wire spelling a client branches on (#14723)
6+
7+
The envelope's `developerMessage` — the remedy sentence addressed to the
8+
application author — told its reader to "branch on `code === 'DUPLICATE_RECORD'`",
9+
which is the engine's THROWN identity and holds only for an in-process caller of
10+
`engine.insert` / `engine.update`. Every REST route reports the same refusal as
11+
`UNIQUE_VIOLATION`, and since #14723 the per-row reports of the batch and import
12+
surfaces do too, so the sentence was a platform contradicting itself on the one
13+
line an author is most likely to copy. It now says both halves: over the HTTP
14+
API branch on `code === 'UNIQUE_VIOLATION'` on every route, whole-request and
15+
per-row alike; inside the engine the thrown class carries `DUPLICATE_RECORD`.
16+
The class's own docblock says the same. Nothing else about the envelope moves:
17+
`code`, `status`, `cause`, `field`, `object` and the user-facing `message` are
18+
byte-identical, and every pin on the engine's thrown code holds.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
"@objectstack/rest": minor
3+
---
4+
5+
fix(rest)!: an import ROW report spells a unique-constraint refusal `UNIQUE_VIOLATION` — the same wire code as the whole-request failure on the same route (#14723)
6+
7+
<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable moves: no spec key, export, config field or stored metadata changes spelling or shape, `packages/spec` is untouched and `objectstack migrate meta` has nothing to rewrite. What moves is the `code` value one import row report carries for one condition — the wire report of a driver's unique-constraint refusal on the per-row surface of `POST /api/v1/data/:object/import` — which now spells the standard-catalog member the whole-request door and the published protocol docs already use. The consumer note below is guidance for a client branching on that row code; it prescribes no rewrite of any authored artifact. -->
8+
9+
**BREAKING** on the per-row results of the import runner
10+
(`POST /api/v1/data/:object/import` and the import job): a row refused by the
11+
engine's `DuplicateRecordError` envelope now reports `code: 'UNIQUE_VIOLATION'`
12+
where it reported `'DUPLICATE_RECORD'`. Shipped as `minor` under the repo's
13+
launch-window convention for breaking changes. Maintainer ruling 2026-09-03 on
14+
#14723 (verbatim 「同意,然后执行契约复审」), adopting option A: one wire
15+
spelling for a unique-constraint refusal on every route.
16+
17+
**Why.** `toFailedResult` relayed the thrown error's own `code`, and the engine's
18+
envelope carries the registered `DUPLICATE_RECORD` — while the whole-request
19+
failure on the same import route answered `UNIQUE_VIOLATION` through
20+
`mapDataError`. Two spellings of one condition on one route, which ADR-0112's
21+
one-name-per-concept and the error-code ledger's header both forbid. The
22+
duplication is removed, not declared: no ledger waiver is added.
23+
24+
**What changes.** The import row derivation applies the whole-request arm's own
25+
predicate — the registered code AND the class name `DuplicateRecordError`,
26+
exported from `error-response.ts` as `isEngineDuplicateRecordEnvelope` and now
27+
shared by the arm and the row report — and reports `UNIQUE_VIOLATION`. A
28+
field-level finding still takes precedence (the envelope carries none), the
29+
row's sentence is unchanged (the platform sentence, sanitised as before; no
30+
driver text), and a producer that merely throws the registered
31+
`DUPLICATE_RECORD` without being the engine's class keeps its own code.
32+
33+
**What does NOT change.** The whole-request doors (single-record, bulk, import,
34+
metadata, UI) already answered `UNIQUE_VIOLATION` and keep doing so; the arm's
35+
logic is untouched beyond reading the shared predicate. The engine's thrown
36+
identity stays `DUPLICATE_RECORD` in-process. This package's `error-response.ts`
37+
docblock that disclosed the fork under the #14541 contract review now states
38+
the converged rule.
39+
40+
**Consumer note.** An import client that branched on a row's `code` reading
41+
`DUPLICATE_RECORD` reads `UNIQUE_VIOLATION` there now — the same value it
42+
already handles for the whole-request 409. Measured in-repo and in the sibling
43+
repos (hotcrm, objectui, non-test sources): zero consumers branch on either
44+
spelling of a row code.

0 commit comments

Comments
 (0)