diff --git a/.changeset/advisory-boot-path-aggregation.md b/.changeset/advisory-boot-path-aggregation.md deleted file mode 100644 index 682e37fdeb..0000000000 --- a/.changeset/advisory-boot-path-aggregation.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"@objectstack/core": minor -"@objectstack/objectql": minor -"@objectstack/metadata-protocol": minor ---- - -Advisory validation rules no longer flood the startup log, and no longer count a row twice on a clean first boot. - -A `severity: 'warning'` (or `'info'`) validation rule is advisory: it never blocks a write, and its message is written for a person filling in a form. Evaluated across a seed load it produced one `WARN` line per row, so a clean-database first boot opened with a wall of form hints re-cast as boot diagnostics — and an app could reach "zero warnings" only by bending its data or deleting the rule. - -Two changes, and neither moves what a rule evaluates to: - -- **Aggregated reporting on the seed/boot path.** `SeedLoaderService.load()` now runs inside an advisory aggregation scope, and reports one summary line per rule — the rule, the object, the row count, the rule's own message and example rows — instead of one line per row. Off that path (an ordinary interactive write) nothing changes: the same per-write line is emitted verbatim. The new scope is `runWithAdvisoryAggregation` / `recordAdvisoryHit` in `@objectstack/core`. -- **Advisory rules are counted by row, not by write.** An `update` whose payload touches only platform-injected system columns — the shape `claimSeedOwnership` writes when it hands seeded rows to the first admin, `{ owner_id }` — changes no business field, so it no longer re-evaluates the object's advisory rules. Previously a seeded row rang once on insert and again when the claim scan rewrote `owner_id`, so anyone counting startup warnings over-estimated by the number of claimed objects. - -`error`-severity rules are untouched by both changes: an invariant is still enforced on every write, whoever issued it and however little it moved. Membership of the "system column" set is resolved per object by `resolveInjectedSystemColumns`, so an object that declares `ownership: 'org'` (no `owner_id`) or `systemFields: false` is judged on its own columns rather than a fixed list. diff --git a/.changeset/analytics-text-family-case-exact-per-dialect.md b/.changeset/analytics-text-family-case-exact-per-dialect.md deleted file mode 100644 index 54df4f9997..0000000000 --- a/.changeset/analytics-text-family-case-exact-per-dialect.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@objectstack/service-analytics": minor -"@objectstack/driver-sql": minor ---- - -The analytics SQL compilers compile the case-sensitive text family per dialect, so a `$contains` policy on SQLite stops admitting rows it excludes (#15684) - -`$contains` / `$notContains` / `$startsWith` / `$endsWith` are case-SENSITIVE on every backend (#4706 Q2 = A). All three of `service-analytics`' SQL compilers emitted `col LIKE ? ESCAPE ?` on every dialect, and SQLite's `LIKE` folds ASCII case unconditionally — the fold cannot be turned off per statement, because `PRAGMA case_sensitive_like` is a connection-global switch. Measured on sql.js over the shared `FILTER_TEXT_ROWS` fixture, `{ name: { $contains: 'acme' } }` answered `['1','2']` — `ACME Corp` **and** `acme corp` — where `FILTER_TEXT_CASES` says `['2']`. - -On two of the three compilers that is a wrong chart. The third is `read-scope-sql.ts`, the ADR-0021 D-C read scope: a scope that **admits** rows the policy's case-sensitive predicate excludes is over-reach, not a loose filter — the same reading that file already applied to its own `LIKE` escaping. The `/analytics/sql` echo was wrong in a third way: it printed `LIKE` while the statement it claims to reproduce ran through a driver that has emitted `GLOB` on the SQLite dialects since #6518. - -What changed: - -- **The construct is chosen per dialect** (`text-match-sql.ts`), arm for arm with `driver-sql`'s own table: `GLOB` on SQLite (case-exact by definition, with its own `*` / `?` / `[` escaped class and no `ESCAPE` clause), `LIKE` over `CAST(… AS BINARY)` on MySQL, and `LIKE` **unchanged** on Postgres, where it is already exactly the ruled semantics. There is no single construct that is case-exact and parses on all three, so the dialect had to become an input rather than a guess. -- **The dialect arrives from the driver that will execute the statement.** New optional `AnalyticsServiceConfig.sqlDialect`, wired by `AnalyticsServicePlugin` from `IDataEngine.getDriverForObject`. `SqlDriver.dialectName` is now public so that answer can be read without a second dialect-resolution table drifting behind the driver's own knex spellings; it is derived and read-only. -- **A host that answers no dialect keeps the `LIKE` it always got** — "cannot answer, do not block". Postgres deployments see byte-identical SQL. - -`$icontains` is untouched: it keeps its own ASCII-only fold on both sides, and collapsing the two families onto one path would hand the case-exact family back the fold the ruling took away from it. `LIKE` escaping is unchanged wherever a `LIKE` is still emitted. diff --git a/.changeset/app-plugin-flat-bundle-seed-double-collect.md b/.changeset/app-plugin-flat-bundle-seed-double-collect.md deleted file mode 100644 index f64edf62b9..0000000000 --- a/.changeset/app-plugin-flat-bundle-seed-double-collect.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -"@objectstack/runtime": patch ---- - -fix(runtime): a flat-manifest bundle no longer collects every seed dataset twice - -`AppPlugin.start()` collects seed data from two locations — the top-level -`data` field, then the legacy `manifest.data` for backward compatibility. The -legacy read resolves its base as `this.bundle.manifest || this.bundle`, so on a -FLAT bundle — manifest fields written directly on the bundle rather than nested -under `manifest:`, a shape `AppPlugin` supports by design and this repo's own -tests construct — it re-read the very array the top-level read had just -contributed. Every dataset landed in the collection twice. - -`mergeSeedDatasets` is a plain `push` with no de-duplication, so both copies -reached the shared `seed-datasets` registry, the inline boot seed, and every -later per-org replay. For an `upsert` dataset with an `externalId` the second -pass is idempotent and the cost is doubled work; for a `mode: 'insert'` dataset -it is the dataset APPLIED TWICE per boot — measured here as two `insert` calls -for one record. - -The legacy read now carries the same reference guard its sibling collector has -always carried: `loadTranslations()` performs the identical two-location read -and skips the legacy half when `manifest.translations` IS the array the top -level already contributed. That asymmetry between the two collectors was the -whole defect, so the repair is the sibling's guard rather than a third spelling -of the same idea. - -⛔ Not a removal of the legacy read: a bundle whose `manifest.data` is a -genuinely different array from its top-level `data` still contributes both, and -a bundle that nests its manifest is unaffected either way. Nothing is added to -or removed from any published surface. diff --git a/.changeset/approval-recall-docstring-override-scope.md b/.changeset/approval-recall-docstring-override-scope.md deleted file mode 100644 index 82dfc4d475..0000000000 --- a/.changeset/approval-recall-docstring-override-scope.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -`IApprovalService.recall`'s contract prose names every actor who may recall, and scopes each one by status (#14670) - -**Documentation only — no key, no accepted value, no runtime behaviour moves.** The implementation has been correct since #12775; only the contract's description of it was stale. - -The docstring said *"Only the submitter (or a system context) may recall"*, then widened to `returned` requests in a second paragraph. Both halves were wrong, in opposite directions: - -- **The list was not exhaustive.** A #3424 override actor — a platform or tenant admin holding no approver slot — may recall a `pending` request. That is the in-product recovery path for an approval routed to an unstaffed position, and this same file already documented it 387 lines above the sentence denying it: the docblock on `ApprovalRequestRow.viewer.can_override` spells the override's levers as `(approve / reject / reassign / recall it)`. One file, two contradicting sentences about the same verb. -- **The ADR-0044 widening read as though it applied to that whole list.** It does not. The override and system arms are ANDed with `status === 'pending'` where they are computed, so neither reaches a `returned` request; an override actor is refused there exactly as any other non-submitter (#12775, maintainer ruling 2026-09-02). Abandoning a revision window is the submitter's alone. - -The rewrite makes **status** the axis instead of appending a caveat, so the second defect cannot come back on a re-read: each status carries its own admitted set, and the `returned` bullet says outright that the submitter is alone in it. - -`ApprovalRecallInput.actorId` carried the same stale sentence (*"Must be the request's submitter (or a system context)"*) and is corrected with it. Fixing only the method docstring would have left the contradiction alive on the very input type the corrected method takes. - -The two sibling docstrings sharing that phrasing are **correct and unchanged**: `ApprovalSendBackInput.actorId` and `ApprovalResubmitInput.actorId`. `isOverrideActor` is called from exactly five places in `plugin-approvals` — `decideNode`, `reassign`, `recall`, `attachViewers` and `visibleRequestIds` — and neither `sendBack` nor `resubmit` is among them, so no override actor reaches either. - -The published prose already described the corrected rule (`content/docs/automation/approvals.mdx`: an admin "may act on any `pending` request — approve, reject, reassign it to a real approver, or recall it"). This docstring was the one surface that had not kept up. diff --git a/.changeset/artifact-packages-collection-reads.md b/.changeset/artifact-packages-collection-reads.md deleted file mode 100644 index e306662df5..0000000000 --- a/.changeset/artifact-packages-collection-reads.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -"@objectstack/runtime": patch ---- - -fix(runtime): a multi-package artifact's collections are read from `packages[]`, not only from the flattened top level - -A release artifact composed with `manifest: 'preserve'` carries every -definition twice — flattened at its top level, and again under -`packages[]` (ADR-0130 D4). Only two readers had ever learned the second -half: `ObjectQLPlugin`'s manifest service and the metadata artifact door. -Every other reader said `artifact.` and nothing else, so an -artifact that carried a collection under `packages[]` alone reached them -EMPTY — and nothing threw. The app booted clean having lost its -declarative actions, its scheduled jobs, its seed data, its object routing -or its default permission set. - -`resolveArtifactCollections` — new, and PACKAGE-PRIVATE to -`@objectstack/runtime` — is now the one way this package reads a top-level -collection out of an artifact in either shape. It takes the artifact's own -top-level value first and whole, then adds from each package body — in -`resolveArtifactPackageOrder`'s dependency order — the items the top level -did not already claim. A bundle that carries no `packages[]` is returned -unchanged, by identity: every single-package artifact and every -`defineStack()` config reads exactly as before. Nothing is added to any -package's published surface: `@objectstack/core` is untouched by this -change, and the new module is not named by -`packages/runtime/src/index.ts`. - -Where one collection key is spelled two ways inside one artifact — -`functions` is `z.union([z.record(…), z.array(…)])`, so two packages can -each be schema-valid and disagree — the read is REFUSED with an ADR-0112 -envelope (`MIXED_ARTIFACT_COLLECTION_SHAPE`, 422) rather than one spelling -being skipped. `composeStacks` already refuses the same mix at compose -time for the same reason. - -Taught to use it, in `@objectstack/runtime`: - -- `AppPlugin` — declared datasources and their auto-connect, the - `datasourceMapping` object routing, the objects handed to the connection - service and to the hot-reload seeder, scheduled jobs, seed datasets, - translation bundles, and the ADR-0057 security collections - (`positions` / `permissions` / `capabilities` / `sharingRules`). A job - handler's `ctx.bundle` is now the resolved view too, so - `ctx.bundle.objects` answers on a multi-package artifact. -- `collectBundleActions`, `collectBundleHooks` and - `collectBundleFunctionEntries` — including the object-EMBEDDED actions - that ride on `objects[]` and disappeared with it. -- `mergeRuntimeModule` — the declaration half. The sibling ESM module - re-supplies every callable regardless of shape, so `functions` was not - absent: a function declared `effect: 'writes'` simply came back as a bare - callable and defaulted to `'pure'`. It registered, it ran, and its writes - were counted as none. -- `createStandaloneStack`'s surfaced `requires` / `objects` / - `permissions` / `positions`, which drive the CLI's tier resolution, its - engine and storage-driver auto-registration, and the ADR-0056 D7 default - permission set. -- `resolve-project-database`'s project-database tier, which opens the - artifact itself and runs before any stack exists (`os dev`, `os start`, - `os db clean`). Without this a multi-package project silently fell - through to the unified default database instead of the datasource it - declared. - -Nothing about what the platform EMITS changes: `composeStacks` and the -artifact format are untouched, and the flattened top level is still -written. This is the reader half of the option-B program (#14512). diff --git a/.changeset/assembled-package-body-plugins-envelope.md b/.changeset/assembled-package-body-plugins-envelope.md deleted file mode 100644 index 985cb214fb..0000000000 --- a/.changeset/assembled-package-body-plugins-envelope.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -feat(spec)!: `plugins` / `devPlugins` are artifact envelope keys — excluded from the assembled package body and refused inside `packages[]` (#15219) - - - -**BREAKING** accept-set narrowing on `AssembledPackageBodySchema` — the body -under `packages[i].manifest` of a release artifact (ADR-0130 D4): a body that -carries `plugins` or `devPlugins` is now **refused** at the manifest's strict -close (`unrecognized_keys`, naming the key), where it used to parse. Shipped as -`minor` under the repo's launch-window convention for breaking changes; the -hand-migration prescription is registered under protocol major 18. Maintainer -ruling 2026-09-04 on #15219 (director decision batch #32, verbatim 「同意」): -option A for both keys. - -`plugins` and `devPlugins` were members of the assembled-body key set by the -same derivation every other collection uses (`COMPOSE_KEY_DISPOSITIONS` gives -both `concat`). They are the only members whose values are **runtime assembly -instructions** rather than serialisable metadata: `plugins` holds what a host -hands to `kernel.use()` — live plugin instances, manifests or package names — -and `devPlugins` is the `os dev` load list. Inside an artifact a package body -is inert JSON, so a plugin under `packages[i].manifest` could never be -constructed by a loader; every reader reads the top level. The classification -is corrected rather than special-cased: an artifact carries metadata, a host -assembles plugins. - -**What changes** (`packages/spec/src/stack.zod.ts`): - -- `plugins` / `devPlugins` are **envelope keys** — top level only, never inside - `packages[]`. `ASSEMBLED_PACKAGE_BODY_ENVELOPE_KEYS` (`packages`, `plugins`, - `devPlugins`) is declared once and feeds both the `AssembledPackageBodyKey` - derivation and `assembledPackageBodyShape()`. -- Both keys stay `concat`: a live stack still concatenates its plugins to the - top level under `composeStacks`, and `manifest: 'preserve'` no longer folds - them into any package body. -- The two declarations on the stack schema are unchanged. - -**What does NOT change:** `os serve` / `os migrate` / `os dev` keep reading the -top level (now correct by construction); no CLI, core or runtime code moves. - -## FROM → TO - -```ts -// before — a package body inside an artifact could carry plugins nobody could load -{ packages: [{ manifest: { id: 'com.example.crm', /* … */ plugins: [{ name: 'plugin.x' }] } }] } - -// after — plugins live on the artifact envelope only; the body above is refused: -// packages.0.manifest: unrecognized_keys ['plugins'] -{ plugins: [new CrmPlugin()], packages: [{ manifest: { id: 'com.example.crm', /* … */ } }] } -``` - -**Migration.** Declare `plugins` / `devPlugins` at the stack top level and -delete them from every `packages[i].manifest`. An existing multi-package -artifact that carries `packages[i].manifest.plugins` (if `os build` ever wrote -one — not directly measured) is refused on load after this change and must be -rebuilt from source; a hand-written `packages[]` entry drops the keys. Stacks -that only ever declared the two keys at the top level parse byte-identically. diff --git a/.changeset/assignment-value-cel-envelope-executor.md b/.changeset/assignment-value-cel-envelope-executor.md deleted file mode 100644 index 34b69590b9..0000000000 --- a/.changeset/assignment-value-cel-envelope-executor.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -"@objectstack/service-automation": minor -"@objectstack/lint": minor ---- - -feat(service-automation): an `assignment` value may be a CEL envelope — evaluated at run time, validated at `registerFlow`, `objectstack validate` and the runtime publish gate (#15137, the executor half of #14149) - - - -**BREAKING** in the accept-set sense, landing in the launch window as `minor` -(the lockstep convention; the level also follows the 2026-09-04 bump ruling — -this adds `AutomationEngine.evaluateValueEnvelope` to a published surface, and an -additive widening is at least `minor`). No ADR-0087 conversion: no authorable key -is renamed or retired, and the shape this refuses was never a shape any surface -offered. - -The maintainer's 2026-09-02 ruling on #14149 made an assignment value able to be -a CEL **value** expression, so the declared stdlib (`joinNonEmpty`, `map`, `size` -…) is finally reachable from metadata — until now CEL was only ever asked for a -boolean. The spec half landed the contract (PR #15113); this is the half that -makes it do something. - -```yaml -# before: written into the variable verbatim, and rendered by `notify` as -# {"dialect":"cel","source":"joinNonEmpty(...)"} -# now: evaluated — digest is "Renewal due\nInvoice overdue" -assignments: - digest: { dialect: cel, source: 'joinNonEmpty(rows.map(r, r.subject), "\n")' } -``` - -- **Evaluated at run time.** The built-in `assignment` executor evaluates a - `value`-role envelope with the expression engine and assigns the result, in the - same CEL scope a flow predicate is evaluated in (one shared scope builder, so a - predicate and a value expression cannot disagree about what `rows` means). A - plain string keeps today's `{token}` interpolation, and every other literal is - still assigned as data. -- **Refused at three doors.** A malformed envelope now stops the flow registering - (`registerFlow` throws, the severity a malformed predicate gets) and surfaces as - a located `error` finding naming the node and the author's own variable — - `config.assignments.digest` — both at `objectstack validate` and at the runtime - publish gate a Studio / REST / MCP flow write goes through - (`validateStackExpressions` is registered `CLI_AND_RUNTIME`, `runtimeTypes: - ['flow']`). Malformed is a composition, not a fixed list: whatever - `AssignmentValueSchema` refuses in the envelope's shape — among them a missing, - empty or non-string `source`, a dialect other than `cel`, a non-object `meta` — - and then CEL that does not parse. All three doors derive that set from the same - two published validators, so none refuses a shape the executor would have run, - and a registered flow never faults for a shape those validators judge malformed. - Two shapes sit outside what either validator can judge — an `ast`-only envelope - and a whitespace-only `source` (it passes `min(1)` and reads as "not authored" - to the validator, while the CEL engine parses it untrimmed) — and those fault - loudly at run time rather than assigning a value. Both are pinned and tracked in - #15430. -- **Only the canonical map.** The ledger declares `assignment.assignments.*` and - nothing else, so the two legacy shapes the executor still normalizes — the - `assignments: [{ variable, value }]` array and the bare `{ : }` - config — keep every meaning they had, envelope-shaped values included. - `AssignmentConfigSchema` is deliberately NOT wired into `parseNodeConfig` for the - array form: refusing it would break flows that register today, and that refusal - is a maintainer ruling rather than a lane's call (#15137 ask 3). - -**What changes silently, and how far it reaches.** A flow that today authors an -envelope-shaped object *as data* in the canonical `assignments` map now evaluates -it — no error on either side, a different value. The discriminator is the spec's -own `isExpressionEnvelopeShaped`: a plain object naming a **string** `dialect`, -in the declared map only. Data that names no `dialect`, names a non-string one, -nests the envelope one level down, or sits in either legacy shape is untouched -and byte-identical. The remaining overlap — a well-formed -`{ dialect: 'cel', source: … }` written as data in the canonical map — is exactly -the spelling the ruling reinterprets; every near-miss the two validators can -judge now refuses loudly at registration instead of changing value in silence. diff --git a/.changeset/audit-router-keyed-identity-and-listnames-parity.md b/.changeset/audit-router-keyed-identity-and-listnames-parity.md deleted file mode 100644 index 2ccbfb14de..0000000000 --- a/.changeset/audit-router-keyed-identity-and-listnames-parity.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -'@objectstack/metadata': minor -'@objectstack/objectql': minor ---- - -feat(metadata,objectql): a keyed plural read on `MetadataManager`, `listNames` fault parity, and an action audit that answers from the same identity and sources as the router - -Two plural reads of one metadata plane could disagree with a by-name read of -that same plane, and the ADR-0110 D5 action-governance audit stood on the -disagreement — reporting `registered handler with NO declaration … REFUSED at -dispatch` about a route the router was resolving and dispatching in the same -boot. - -**`MetadataManager.listNames` gains the per-loader `try`/`catch` that -`loadMany` and `list()` have carried since #5108.** One loader fault used to -produce two different facts depending only on which plural read a caller -reached for: `loadMany` swallowed it and answered short, `listNames` threw. It -now degrades the same way, through the same `reportLoaderReadFailure` / -`reportLoaderReadRecovered` helpers — one outage, one line, one vocabulary. -Callers that relied on `listNames` throwing to detect an outage should read -`listDiagnosed()`, which reports `degraded` explicitly. - -**New: `MetadataManager.loadManyKeyed(type, options?)`** — `loadMany` read under -the identity the STORE holds each item by, returning `{ name, data }` pairs. It -delegates to a loader's own `loadManyKeyed` where one is offered (on -`DatabaseLoader` that shares `loadMany`'s single query, so it costs nothing -extra) and otherwise falls back to that loader's `list()` + per-name `load()`. -⛔ **`loadMany`'s published return shape does not change**, and no existing -consumer is touched: the key travels *beside* the body, never inside it, so a -body that deliberately carries no `name` stays byte-identical to what was -stored (#14205). - -**The action-governance audit now mirrors the router on both halves of the D5 -bijection.** The declaration half enumerates the plane keyed -(`loadStandaloneActionsKeyed`), so a row whose body does not name itself — a -`sys_metadata` row keyed by its `name` column, or a `FilesystemLoader` file -whose identity is its path — is a declaration to the audit exactly as it is to -the router; the handler half also probes the plane BY NAME -(`lookupMetadataAction`, `loadDiagnosed`/`load`, injected like the existing -registry rung), so a loader fault a plural read swallows can no longer turn a -dispatchable handler into an accusation. Both probes stay conservative in one -direction only: a source that throws leaves the handler on the list. - -Additive on every published signature. `runActionGovernanceInventory` and -`collectEngineActionDeclarations` gain optional parameters and keep their old -ones working unchanged; declaration rows gain an optional `storeKey` (the new -exported `ActionDeclarationRow`). - -**Population change, reported:** `unboundDeclarations` now sees declarations -whose identity is the store key. Its BEFORE was **0, structurally rather than -by sampling** — a nameless row was dropped before reconciliation ran, so it -could never be reported however many a plane held. Its one deliberate -subtraction: a row with neither an own `name` nor a store key is no longer -reported as `actionName: undefined`, which read as a parse failure in the -warning rather than as a finding. - -Known boundary, stated in the audit's docblock rather than left to be -rediscovered: a boot-time audit runs outside any request scope, so if a -composition ever registered `metadata` as `SCOPED` the audit could not reach -that instance at all — before any read method runs. No shipped composition does -(`packages/metadata/src/plugin.ts` registers a static instance), and reaching a -request-scoped service from a boot-time audit is a separate change. diff --git a/.changeset/batch-row-unique-violation-metadata-protocol.md b/.changeset/batch-row-unique-violation-metadata-protocol.md deleted file mode 100644 index 918ad7a355..0000000000 --- a/.changeset/batch-row-unique-violation-metadata-protocol.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -"@objectstack/metadata-protocol": minor ---- - -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) - - - -**BREAKING** on the per-row report of `POST /api/v1/data/:object/batch` (and -the multi-object `POST /api/v1/batch`, which rides the same protocol): a row -refused by the engine's `DuplicateRecordError` envelope now reports -`errors[].code: 'UNIQUE_VIOLATION'` where it reported `'DUPLICATE_RECORD'`. -Shipped as `minor` under the repo's launch-window convention for breaking -changes. Maintainer ruling 2026-09-03 on #14723 (verbatim 「同意,然后执行契约 -复审」), adopting option A: one wire spelling for a unique-constraint refusal on -every route. - -**Why.** `toRowApiError` put a thrown REGISTERED code on the row verbatim, and -`DUPLICATE_RECORD` is registered, so a `DuplicateRecordError` row said -`DUPLICATE_RECORD` while the whole-request failure on the very same route (the -bulk door's classification in `@objectstack/rest`) answered `UNIQUE_VIOLATION` -— the standard-catalog member `content/docs/protocol/kernel/http-protocol.mdx` -documents for the 409 constraint-violation body. Since the bulk doors were -restored to `UNIQUE_VIOLATION`, the two spellings of one condition sat side by -side in one route's responses, which ADR-0112's one-name-per-concept and the -error-code ledger's own header both forbid. The duplication is removed, not -declared: no ledger waiver is added. - -**What changes.** The row derivation recognises the engine's envelope by the -same two-part gate the whole-request arm uses — the registered code AND the -class name `DuplicateRecordError`, never message text — and reports -`UNIQUE_VIOLATION`. Everything else on the row is unchanged: `httpStatus: 409`, -the platform sentence (no driver text, no bound value — the driver's error -stays on `cause` and never reaches the row), and the sibling `NOT_ATTEMPTED` / -`ROLLED_BACK` rows. - -**What does NOT change.** The engine's thrown identity: `DuplicateRecordError.code` -is still `DUPLICATE_RECORD` for an in-process caller of `engine.insert` / -`engine.update` (a hook, a flow node), and the objectql pins on `insert` / -`insertMany` hold. The single-record `/data` door, which has answered -`UNIQUE_VIOLATION` throughout, does not move. A producer that merely THROWS the -registered `DUPLICATE_RECORD` from its own body without being the engine's -class keeps its own code on the row, exactly as it does at the door. - -**Consumer note.** A batch client that branched on a row's `code` reading -`DUPLICATE_RECORD` reads `UNIQUE_VIOLATION` there now — the same value it -already handles for the whole-request 409 on that route and on the -single-record door. Measured in-repo and in the sibling repos (hotcrm, objectui, -non-test sources): zero consumers branch on either spelling of a row code. diff --git a/.changeset/blueprint-strict-mirror-value-parity.md b/.changeset/blueprint-strict-mirror-value-parity.md deleted file mode 100644 index 2cb54a4e52..0000000000 --- a/.changeset/blueprint-strict-mirror-value-parity.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -The model-facing solution-blueprint mirror can no longer generate an identifier the applier rejects. - -`SolutionBlueprintSchema` (what `apply_blueprint` validates against) and `SolutionBlueprintStrictSchema` (the OpenAI-strict structured-output contract the design model generates against) are two declarations of one shape. Their KEYS were pinned by an existing parity test; their VALUES had never been. Every identifier in the lenient schema carried `.regex(/^[a-z_][a-z0-9_]*$/)` and not one identifier in the strict mirror carried it — 20 leaves apart, measured. - -The consequence was a build whose approval did nothing. Asked for a CRM, the design model emitted a `company_size` select whose option values came straight off the labels — `1_49` for 「1-49人」. Generating that was legal. Applying it was not: on the turn the user clicked 「确认,开始搭建」 the deterministic confirm replay handed that exact blueprint to `apply_blueprint`, which refused it wholesale (`objects.0.fields.2.options.0.value: Invalid string: must match pattern /^[a-z_][a-z0-9_]*$/`) and staged nothing. The app appeared only because the model noticed the error card and retried with a repaired blueprint the user had never seen. - -Every identifier leaf in the strict mirror now carries the same `SNAKE_CASE` constraint the lenient schema enforces — object / field / view / dashboard / widget / app / nav names, `reference`, `nameField`, `columns`, `groupBy`, `measure`, roll-up `object` / `field` / `relationshipField`, condition `field`, and select option `value`. The constraint is emitted into the JSON Schema the model is given (`pattern`), so an out-of-pattern identifier is refused at generation instead of after approval. Option `value` additionally spells out the case that produced the incident: it may never start with a digit, so 「1-49人」 is authored as `size_1_49` — the `label` keeps the human wording untouched, and only the stored value is an identifier. - -A new `strict mirror ↔ lenient schema — VALUE parity` test walks both schemas leaf by leaf and fails on any future divergence, the value-side twin of the key-parity gate that already guards this pair. - -Refs cloud#1967. diff --git a/.changeset/boot-sign-in-report-remedy-text.md b/.changeset/boot-sign-in-report-remedy-text.md deleted file mode 100644 index 2f330fb333..0000000000 --- a/.changeset/boot-sign-in-report-remedy-text.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@objectstack/plugin-auth": patch ---- - -The `no_sign_in_account_at_boot` report now names a remedy that works — and warns off the one that silences the report itself. - -That boot line fires on the deployment nobody can sign in to: human `sys_user` rows, zero `sys_account` rows. It ended with two remedies, and measured on the exact population it fires on, neither did what its sentence said: - -- **"Open the audience posture so an existing person can register their own login"** produced no login, and for an existing person it never can: self-registration is a user-creation path, so it cannot attach a login to an address that already carries a `sys_user` row, whatever the posture. Widening only ever admits a *new* address — and then every posture other than `invite_only` forces `requireEmailVerification` on, so that login is refused `EMAIL_NOT_VERIFIED` at its first sign-in, and a locked-out self-hosted install is usually the shape with no mail transport wired. -- **"Write a `sys_account` credential row directly against the store"** was worse than useless. The `password` column carries a secret in the platform's own hash format, so a plaintext one authenticates nothing — and the probe behind this report asks only whether *any* `sys_account` row exists, so writing one turns the report off. The operator's first attempt at the named remedy turned the loud dead end back into the silent one the report was written to end. - -The line now names the path that was measured to work: write one pending `sys_invitation` row directly against the store — a lowercase address the directory does not already hold, `status` `pending`, a future `expires_at`, `inviter_id` of any existing `sys_user` — then register through the ordinary sign-up endpoint. The invitation carve-out admits that one creation under every posture, so no door needs widening. It is an admission verdict and not a verification bypass, though, so the line scopes what follows from that: only under the default `invite_only` posture is the recovery mail-transport-free, and it tells the operator to close a widened posture back to `invite_only` before the invited person registers — otherwise the invited login is created, refused `EMAIL_NOT_VERIFIED` at first sign-in, and has silenced this report on the way past. On the `single` tenancy posture that account holder is then promoted to platform admin. The other two are still named, as the two things that look like remedies and are not, because an operator who is going to hand-write a credential row anyway needs to know it blinds the probe. - -**Message text only — no admission semantics move.** Nothing widens, nothing narrows, no accept set changes, and the probe is untouched: this changes what an operator *reads*, not what the platform *admits*. The long form of the same three facts is on the self-hosting deployment page. diff --git a/.changeset/bulk-event-batch-organization.md b/.changeset/bulk-event-batch-organization.md deleted file mode 100644 index e82c5e2340..0000000000 --- a/.changeset/bulk-event-batch-organization.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -"@objectstack/objectql": patch ---- - -fix(objectql): a published `BulkDataEvent` now names the ONE organization the tenant wall named for the batch - -`BulkDataEventSchema.organizationId` (`@objectstack/spec/api`, declared by the -contract half) is one organization for a whole predicate write, or absent. The -only bulk producer — `publishBulkDataEvent`, behind the `multi: true` branches -of `update()` / `delete()` — never set it, so every `data.records.updated` / -`data.records.deleted` event read "not asserted" and a tenant-scoped consumer -could deliver nothing per organization on the bulk path. This is the bulk half -of the cross-tenant webhook fan-out leak; the single-record half (`DataEvent`) -landed separately. - -The producer now stamps the key from what it already holds — no second query -on the publish path: under `isolated` the caller's active organization (the -Layer 0 wall's equality term), under `group` the caller's membership set when -it names exactly one organization. It is OMITTED — never the caller's active -organization standing in — on a `single`-posture deployment, on an `isSystem` -context (no wall composed), on a multi-membership `group` sweep, when no -enforcement layer injected a posture (the `OS_TENANCY_POSTURE` env fallback is -deliberately not consulted), when the caller may have crossed the wall as a -`PLATFORM_ADMIN` or carries no resolved posture rung, and on an object the wall -does not key on. `absent` here means "the producer did not assert one -organization for the batch", deliberately NOT the `DataEvent` reading -"belongs to no organization". - -Which objects "the wall does not key on", stated exactly rather than claimed as -a mirror: plugin-security's Layer 0 composes no wall when its `tenancyDisabled` -input is true or the object carries no `organization_id`, and it folds THREE -clauses into `tenancyDisabled` — `tenancy.enabled === false`, -`systemFields.tenant === false`, and the deployment's `platformGlobalObjects` -carve-out. The producer reads the registry's binding of that predicate -(`carriesTenantScopeColumn`: the first two clauses plus the column clause) and -answers absent on a federated (`external`) object; a custom -`tenancy.tenantField` is therefore not an exit by itself — the object is walled -iff it carries `organization_id`, and the key follows the wall. The third -clause is deployment-declared and not readable by the engine: a -deployment-exempted object under an armed wall is still stamped with the -caller's organization by this producer alone, and that population's exact -answer is decided by the seam ruled on in #15706. - -`patch`, not `minor`: the act adds no member to this package's published -surface. `carriesTenantScopeColumn` is exported at module level inside -`registry.ts` only — `@objectstack/objectql`'s entries (`.`, `./core`) re-export -named members and never `export *`, so `dist/index.d.ts`, `dist/core.d.ts` and -both entries' runtime export lists are unchanged (measured on the built `dist`, -with a firing control) — and the emitted event's member was declared, typed -and paid for at `minor` by the spec half. Producer conformance to an existing -optional member under `fix(` changes no public surface of this package. diff --git a/.changeset/chart-config-missing-overreach.md b/.changeset/chart-config-missing-overreach.md deleted file mode 100644 index c02a356bf6..0000000000 --- a/.changeset/chart-config-missing-overreach.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/lint': patch ---- - -`chart-config-missing` no longer fires on a widget whose binding the renderer derives - -The rule warned on every chart-family widget that declared no `chartConfig`, on the -stated grounds that "the renderer cannot determine which measure to plot, so the series -renders empty". Measured against the `@object-ui` revision this repo pins -(`.objectui-sha`), that consequence is false: `DatasetWidget` derives the x-axis key and -one series per measure from the widget's own `dimensions` / `values` via -`buildChartSeries`, and refuses an authored `ChartAxis.field` / `ChartSeries.name` -outright — `chartConfig` carries presentation only. The renderer pins this by name: -"ignores an authored axis `field` and keeps the derived axis binding", "ignores an -authored series and keeps one derived series per measure", "emits none of the -presentation keys when no chartConfig is declared". - -The false finding was landing on this platform's own shipped metadata — the -`system_overview` dashboard's pie and bar tiles, on the Setup board every customer opens -first — which is the ADR-0072 D1 cost the rule family exists to avoid. - -The rule id is unchanged and keeps one true arm: a `combo` widget with no `chartConfig`, -whose per-series mark is authored as `chartConfig.series[].type` and has no other -channel, so every measure draws with the same default mark and the chart is not a -combination at all. Its message now names that consequence instead of the binding. -An existing `suppressWarnings: ['chart-config-missing']` entry stays valid. diff --git a/.changeset/chart-empty-selection-rules.md b/.changeset/chart-empty-selection-rules.md deleted file mode 100644 index 0a290341c1..0000000000 --- a/.changeset/chart-empty-selection-rules.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -'@objectstack/lint': minor ---- - -Two new widget-binding rule ids for a chart widget with an empty selection - -`validateWidgetBindings` reported nothing about two dataset-bound chart shapes that the -`@object-ui` revision this repo pins (`.objectui-sha`) visibly degrades. Both are now -warnings, suppressible per widget with `suppressWarnings: ['']`: - -- `chart-measures-missing` — a chart-family widget selects no measures (`values` empty or - absent). `DatasetWidget.tsx:683` returns the authoring placeholder "Pick measures - (values) for this dataset widget." before any query runs, above every family branch, so - no chart is drawn at all. -- `chart-dimensions-missing` — a chart-family widget selects at least one measure but no - dimensions. `DatasetWidget.tsx:423` reads - `const isMetric = METRIC_TYPES.has(widgetType) || dimensions.length === 0;`, so the - widget renders as a single KPI number and the declared chart family is silently ignored. - The hint steers the author to a dimension, or to the `metric`/`kpi` family that matches - what actually renders. - -Warning tier rather than error for both: an empty selection is a work-in-progress state a -build must tolerate, and erroring would gate the `sys_metadata` publish path on a -half-authored widget. Neither shape is folded into `chart-config-missing` — neither is -caused by, nor repairable with, `chartConfig`, which carries presentation only. - -"Chart family" is derived, not hand-listed: every declared `ChartTypeSchema` option that -the pinned renderer routes to its chart branch — the taxonomy minus the renderer's own -`METRIC_TYPES` (`metric`, `kpi`, `gauge`, `solid-gauge`, `bullet`) and its `table`/`pivot` -tabular test. A `metric` tile with no dimensions, such as the shipped `system_overview` -board's own KPI tiles, is therefore not a finding. diff --git a/.changeset/chart-field-unknown-refused-binding-tier.md b/.changeset/chart-field-unknown-refused-binding-tier.md deleted file mode 100644 index f18c3f48ca..0000000000 --- a/.changeset/chart-field-unknown-refused-binding-tier.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -'@objectstack/lint': minor ---- - -`chart-field-unknown` drops to `warning` on the three `chartConfig` binding keys the pinned renderer refuses, and says what actually happens - -The rule id covers exactly three positions, and the `@object-ui` revision this repo pins (`.objectui-sha`) refuses all three as bindings, so none of them can produce the data failure the messages described: - -- `chartConfig.xAxis.field` — `axisPresentation` (`@object-ui/core` `src/utils/chart-presentation.ts`) builds the axis presentation **minus** its `field`. The x-axis key is `buildChartSeries`' `xAxisKey`, i.e. the widget's `dimensions[0]`; an authored `field` re-points nothing. -- `chartConfig.yAxis[].field` — the same call, per entry. The entry keeps its slot (the count is what turns on a secondary axis) and its scale and chrome; only the binding is dropped. -- `chartConfig.series[].name` — `mergeAuthoredSeries` pairs an authored entry with the derived series whose `dataKey` it equals, one per entry of `values`. An entry naming no derived series is ignored whole, so the presentation hung on it — the mark, the colour, the stack, the axis side — lands on nothing. - -The renderer pins this by name in `DatasetWidget.chartConfig.test.tsx` ("ignores an authored axis `field` and keeps the derived axis binding", "ignores an authored series and keeps one derived series per measure"). - -So the old message — "the query result will not contain it" — named a query failure that never happens, and `error` blocked a build and a Studio publish for a key that changes nothing at runtime. That is the class `widget-legacy-analytics-shape` reports at `warning` in the same file ("the dashboard renderer ignores them … a silent no-op"), and this id now carries the same tier, the same suppressibility (`suppressWarnings: ['chart-field-unknown']` per widget) and the same kind of sentence. Each message states its own consequence, because the axis positions and the series position are refused for different reasons. - -The finding is **kept**, not deleted: unlike the `chart-config-missing` over-reach this measurement came from, the metadata really is wrong — the author wrote a binding and believes it is in force. - -## Migration - -**A publish that used to be refused now succeeds.** Ruled 2026-08-15, `validateWidgetBindings` put its whole error set on the `sys_metadata` publish door (Studio / REST `/meta` / MCP) as one "this board cannot render" reference-integrity class. That class was six ids and is now five — `chart-field-unknown` has left it. A dashboard write whose only reference-integrity problem is a refused `chartConfig` binding key is no longer a 422 `INVALID_METADATA`; it publishes, and the finding rides the non-blocking `advisories` channel on the 2xx response instead. The other five (`widget-dataset-unknown`, `widget-dimension-unknown`, `widget-measure-unknown`, `widget-legacy-analytics-unrenderable`, `dashboard-filter-field-unknown`) are unchanged. - -Same direction on the CLI: `os validate` / `os build` / `os lint` report the finding at `warning`, so a stack that used to fail the build over one of these keys now exits 0 with an advisory. If you were relying on the build to stop on it, add the key to your own gate, or fix the binding — the fix has not changed: - -- point `xAxis.field` at a dimension the widget selects (or drop the key — `xAxis` carries presentation only); -- point `yAxis[].field` at a selected measure (or drop it — `yAxis[]` carries presentation only); -- name a selected measure in `series[].name`, remembering that post-cutover (ADR-0021) result rows are keyed by the dataset's measure **name** (`sum_amount`), not the base column (`amount`). - -A deliberately inert key can be silenced per widget with `suppressWarnings: ['chart-field-unknown']`. diff --git a/.changeset/chart-measure-unknown-presentation-positions.md b/.changeset/chart-measure-unknown-presentation-positions.md deleted file mode 100644 index 4f1d16e175..0000000000 --- a/.changeset/chart-measure-unknown-presentation-positions.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"@objectstack/lint": patch ---- - -`chart-measure-unknown` no longer blocks a build over a chart `series[].name` (or a page chart's `yAxis[].field`) that names nothing — those positions are presentation, and the message now says so. - -The rule fired at `error` on every measure position of the three chart surfaces it covers, with one consequence sentence: *"result rows are keyed by MEASURE NAME … so this series comes back empty"*. Read at the `@object-ui` revision this repo pins (`.objectui-sha`), that is true only where the position feeds the dataset query, and the three surfaces do not agree: - -- **Report charts** run the chart's own query out of the two axis strings (`useDatasetRows(dataset, [xAxis], [yAxis], …)` — *"the embedded chart queries only `chart.xAxis` × `chart.yAxis`"*), so `chart.xAxis`/`chart.yAxis` are the binding. `chart.series[]` is *"the author's per-chart override for ONE measure's display name"*, lowered through `mergeAuthoredSeries`, where *"an authored entry naming a measure that is NOT in the dataset selection is **ignored** — membership belongs to the dataset"*. -- **List-view charts** have no presentation position at all: `ListChartConfigSchema` is a strict object of `chartType`/`dataset`/`dimensions`/`values`, and `values[]` is handed to the chart as the dataset measures. -- **Dataset-bound page chart components** query `{ dimensions, measures: values }` and then replace the authored series wholesale with one derived entry per selected measure, so `properties.series[].name` reaches the renderer not at all and `properties.yAxis[].field` re-points nothing. - -**Behaviour change users see:** the three presentation positions — report `chart.series[].name`, page-component `properties.series[].name` and `properties.yAxis[].field` — drop from `error` to `warning`. A build or a metadata publish that used to be refused because of one of them now succeeds, with the finding on the advisory channel. The finding is KEPT, not deleted: the metadata really is wrong — the author wrote a key and believes it is in force. Every query position (report `chart.yAxis`, and `values[]` on all three surfaces) keeps `error` and its existing message verbatim. - -Two smaller corrections ride along, both from the same read: - -- The page surface's `yAxis[].field` refs are no longer concatenated into the `series[]` limb before the measure walk, so an axis position no longer takes the series message. Reading both shapes on that surface stays deliberate; giving them one sentence was not. -- `chart-axis-not-selected` (a declared measure outside the selection) took the same one-size consequence, *"the query does not return it, so the series plots nothing"*. It keeps that wording at a query position and states the real one at a presentation position, where no series is derived for the name in the first place. - -Note that none of these three surfaces declares `suppressWarnings` — it is a dashboard-widget key — so the new advisories cannot be individually silenced; the hint says so instead of pointing at a key that does not exist. diff --git a/.changeset/cli-hook-body-subpath-export.md b/.changeset/cli-hook-body-subpath-export.md deleted file mode 100644 index af97e82057..0000000000 --- a/.changeset/cli-hook-body-subpath-export.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@objectstack/cli': minor ---- - -Ratify `./hook-body` as a public subpath export — `extractHookBody`, `HookBodyExtractionError`, `HookBodyRefusalKind` and `ExtractedBody` were reachable as a deep `dist/utils/extract-hook-body.js` import until #13123 sealed the surface, and an app's hook-body fidelity harness (hotcrm's `test/helpers/action-sandbox.ts`) consumes them to run the SAME body-only lowering `os build` ships through the real QuickJS runner, so a test executes what production executes rather than a lookalike. The #13123 body names exactly this remedy for an out-of-repo consumer — ratify the subpath as public surface rather than read `dist/` paths — and 17.3.0 applied it to `./console` for cloud's `objectos-runtime`; this applies it to the second consumer (#15325). `@objectstack/cli/hook-body` is a dedicated entry that re-exports those four names and nothing else; the deep `dist/` path stays sealed. Also admits `./package.json`, so the ordinary tooling idiom of reading a dependency's own manifest resolves again. - -`minor`, not `patch`: a new subpath on a published package's `exports` map is a purely additive widening of its public surface — a new accepted key — which takes at least `minor` under the maintainer's 2026-09-04 rule (decision batch #35, on #15294) in the Check Changeset step's "WHICH LEVEL" prose; the commit type never lowers it. diff --git a/.changeset/cli-package-publish-help-local-dev-example.md b/.changeset/cli-package-publish-help-local-dev-example.md deleted file mode 100644 index 6d22c35109..0000000000 --- a/.changeset/cli-package-publish-help-local-dev-example.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -`os package publish --help` no longer points its local-dev example at a directory this repo does not have. - -The last line of the command's `EXAMPLES` block read: - -``` -$ OS_CLOUD_URL=http://localhost:4000 os package publish # local dev (apps/cloud) -``` - -`apps/cloud` was deleted from this repository — the reference cloud host now lives in `objectstack-ai/cloud` — so the parenthetical sent a reader to a path that is not in the tree they cloned. This is help text, not a source comment: it is printed verbatim to anyone who runs the command. - -The parenthetical is dropped rather than re-pointed at the other repo. The example is about `OS_CLOUD_URL` overriding the control-plane URL, which the `--server` flag already documents in the same output; which directory happens to serve `localhost:4000` was never part of what the example teaches, and a `--help` reader is not looking for a file in a monorepo. `# local dev` alone carries it, and it now matches how the CLI reference docs have long published the same example. - -No behaviour changes: `examples` is a static help string, and no flag, argument, default or exit code moves. diff --git a/.changeset/client-oauth-family-wire-shape-binding.md b/.changeset/client-oauth-family-wire-shape-binding.md deleted file mode 100644 index 48bc5fa893..0000000000 --- a/.changeset/client-oauth-family-wire-shape-binding.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -"@objectstack/client": minor ---- - -fix(client)!: the `oauth.*` family declares the wire shapes better-auth actually sends — four published `Promise< any >` returns narrowed (#14312) - -**BREAKING** for a typed caller, and it breaks nothing that ever worked at runtime. No request bytes, no URL and no response handling change: this is a declaration catching up with what the routes have always answered. It ships as `minor` under the lockstep launch-window convention (`scripts/check-changeset-no-major.mjs`) — the version number is not the migration signal here, this entry is. - - - -Card 1 of 3 of the #12104 family, under the maintainer's 2026-08-31 ruling: the wire contract is the only source of truth, and better-auth's own `Date`-typed fields are the pre-serialization SERVER shape, not the wire fact. - -## What changed - -Four methods ended `return res.json()` with no return annotation, so `lib.dom`'s `Response.json(): Promise< any >` was their published type. Each now declares the shape its route serves, and its `exported-any-returns.json` entry is deleted in the same change: - -| method | resolved to (before) | resolves to (now) | -|:--|:--|:--| -| `client.oauth.applications.register(req)` | `any` | `OAuthApplicationRegistration` | -| `client.oauth.applications.get(id)` | `any` | `OAuthApplication` | -| `client.oauth.applications.getPublic(id)` | `any` | `OAuthApplicationPublic` | -| `client.oauth.consent(req)` | `any` | `OAuthConsentResult` | - -`OAuthApplication`, `OAuthApplicationRegistration`, `OAuthApplicationPublic` and `OAuthConsentResult` are newly exported from `@objectstack/client`. These four routes are served BARE by better-auth (`auth-route-ledger.ts` records them `source: 'better-auth'`) — there is no `{ success, data }` envelope to unwrap, and none is introduced. - -## The exact reads that stop compiling - -Everything below compiled before only because `any` is assignable to, and indexable by, everything. - -```ts -const app = await client.oauth.applications.get('c_1'); -app.data; // was fine; now TS2339 — these routes carry NO envelope -app.anythingAtAll; // was fine; now TS2339 - -const pub = await client.oauth.applications.getPublic('c_1'); -pub.client_secret; // now TS2339 — the public projection hand-picks 7 columns -pub.grant_types; // now TS2339 — same reason -pub.disabled; // now TS2339 — same reason - -const decision = await client.oauth.consent({ accept: true }); -decision.client_id; // now TS2339 — consent answers `{ redirect, url }` - -// Timestamps are RFC 7591 NUMBERS (Unix epoch seconds), so a caller that -// guessed `Date` or ISO `string` now fails: -new Date(app.client_id_issued_at!).toISOString(); // TS2769: number is not a Date arg -app.client_id_issued_at!.slice(0, 10); // TS2339: not a string -new Date(app.client_id_issued_at! * 1000); // the correct rewrite -``` - -A caller that only read `client_id`, `client_secret`, `redirect_uris` or `url` needs no change. - -## Timestamps: `number`, not `Date` and not ISO-8601 - -The ruling ordered every `Date`-typed field declared as an ISO `string` and forbade both a `Date` declaration and a runtime revival layer. **This family has no `Date` field to convert.** RFC 7591 carries `client_id_issued_at` and `client_secret_expires_at` as Unix-epoch SECONDS, and the provider converts its stored `Date` to a number before serialising, so the wire sends neither a `Date` nor an ISO string. Both are declared `number`, and a type-level pin holds them there. The ruling's prohibitions are satisfied: nothing declares a `Date`, and no revival layer exists. - -## Two places better-auth's own types were the wrong answer - -Read off the wire against a real server, not off the vendor's `.d.ts`: - -- `getPublic` is declared `OAuthClient` — the full row — but its handler hand-picks seven columns. `OAuthApplicationPublic` is that projection, derived with `Pick` so it cannot drift from its parent. Its `redirect_uris` is always `[]` on this route and carries no information. -- `user_id` and `application_type` are declared nullable by the vendor, but the serialiser folds a null column to `undefined`, so `null` is unreachable and is not declared. - -## `oauth.applications.delete` is deliberately NOT bound - -The fifth method of the family keeps its `Promise< any >` and its ledger entry. Its route answers HTTP 200 with a zero-byte body, so its `res.json()` rejects with a `SyntaxError` on every successful delete. No annotation can be honest while that call stands, and binding it needs a behaviour change — a decision beyond this card's type-narrowing scope. That the shrink-only ledger still carries exactly this one entry is the mechanism working. diff --git a/.changeset/colorfield-derive-describe.md b/.changeset/colorfield-derive-describe.md deleted file mode 100644 index 099854c9e0..0000000000 --- a/.changeset/colorfield-derive-describe.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -`colorField` now documents what it means: a field to DERIVE a colour from, not a field holding one. - -`TimelineConfigSchema`, `CalendarConfigSchema` and `GanttConfigSchema` each declare a `colorField`, and all three `.describe()` strings said only that the field "determines"/"drives" the colour — `'Field to determine item color'`, `'Field whose value determines the event color'`, `'Field that drives the bar color'`. Read literally, that invites pointing the key at a field whose stored value *is* a colour, which is the one case the renderers need the least: the common author intent is `colorField: 'status'`, a select field whose options already carry the colours. - -The renderers resolve it as a derivation ladder (objectui#7243, shared as `createFieldColorResolver` in `@object-ui/core`): - -1. the option `color` the field declares for the record's stored value; -2. else the value itself, when it already is a colour literal (hex 3/6/8-digit, `rgb(...)`, `hsl(...)`); -3. else each renderer's own last rung — the gantt derives a semantic colour token, the calendar hashes onto its theme-aware palette, the timeline draws its default marker. - -The three strings now say that, each naming its own last rung. **Nothing in the accept set moves**: all three keys stay `z.string().optional()`, and a config pointing `colorField` at a plain hex field is still exactly as valid as before — that is rung 2. This is prose on a declared key, so the only regenerated follower is `content/docs/references/ui/view.mdx`. diff --git a/.changeset/config-miss-refusal-to-stderr.md b/.changeset/config-miss-refusal-to-stderr.md deleted file mode 100644 index ea723170f8..0000000000 --- a/.changeset/config-miss-refusal-to-stderr.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -`os validate|info|diff|lint|compile|build|verify|migrate meta|i18n check|i18n extract --json` no longer print human text on stdout when the config file is missing. - -`resolveConfigPath()` emitted both of its refusals — the explicit-path miss and the auto-detect miss — through `printError` and `console.log`, **both of which write to stdout**, and then called `process.exit(1)` directly. Ten published `--json` faces reach that helper, so a missing config file answered them with exit 1, an unparseable stdout and an **empty stderr**: 206 bytes of prose on the one stream `--json` reserves for the machine. And because the exit was called rather than thrown, every command's catch-all `--json` error exit — all of which sit downstream of a throw — never ran. - -The diagnostic now goes to stderr, where the rest of this CLI's diagnostics already go. Nothing else moves: - -- **the exit code is still 1**, so a consumer branching on exit status sees no change at all; -- **the wording is unchanged**, hints included, so a human reading a terminal sees the same three lines; -- **nothing is accepted or rejected differently** — no config that loaded before fails now, and none that failed now loads. - -⚠️ **No error payload is invented on this path.** What a `--json` consumer should *receive* when the config file is missing is an envelope question that touches ten published faces at once, and it is deliberately left open here — this change settles only that the machine's channel no longer carries prose. `--json` on this path emits nothing on stdout; a consumer must still read the exit status, exactly as it must today. - -A new pin (`config-miss-stdout-purity.e2e.test.ts`) drives all ten faces on both branches of the helper. The existing purity pin could not: it discovers its family as the commands that call `bootSchemaStack`, and these fail before any kernel boots. diff --git a/.changeset/connector-error-mapping-retired.md b/.changeset/connector-error-mapping-retired.md deleted file mode 100644 index 3893d2d28a..0000000000 --- a/.changeset/connector-error-mapping-retired.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -feat(spec): retire `connector.errorMapping` — eleven authorable keys nothing ever read, one of them spelled like the live `userMessage` channel (#14676, ADR-0049) - - - -**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep -launch-window convention ships it as `minor`; the migration prescription is -registered under protocol major 18, where `os migrate meta` users will look). -Triage ruling 2026-09-02 on the census card: ADR-0049 enforce-or-remove decides -it — declared-but-unenforced authorable surface with zero measured pull for a -reader comes off. - -`ConnectorSchema.errorMapping` carried `ErrorMappingConfig` (`rules`, -`defaultCategory`, `unmappedBehavior`, `logUnmapped`) and its -`ErrorMappingRule[]` (`sourceCode`, `sourceMessage`, `targetCode`, -`targetCategory`, `severity`, `retryable`, `userMessage`) — eleven keys on the -published authorable surface that **nothing read**: measured on `origin/main`, -the only reference outside the declaring file and its unit test was a -type-identity pin. No provider, dispatcher or materializer ever mapped an -external error through the rules, so `unmappedBehavior` configured nothing and -a rule's `userMessage` was never shown to anyone. That spelling is what made -this worse than ordinary dead surface: it is the name of the **live** -API-error channel (`ApiError.userMessage`, the user-facing refusal text a -thrown HTTP error declares), so an author who had read that documentation and -wrote a connector rule reasonably believed they were marking a refusal for an -end user — and the failure was silent in both directions (it validated, it -published, no message was ever shown). Removal resolves the collision by -deletion; the live channel is untouched. - -**What is refused:** authoring `errorMapping` on a connector, with any value. -`ConnectorSchema` is a non-strict `z.object`, so the key is a `retiredKey()` -tombstone rather than a bare deletion (a deletion would have stripped it in -silence): authoring it is a `tsc` error (`never`) and a parse error carrying -the prescription, on the base schema and — through -`DeclarativeConnectorEntrySchema`, which `superRefine`s the same shape — on -`stack.connectors[]` and the `PUT /api/v1/meta/connector/:name` door. - -**What leaves the public surface:** `ErrorMappingConfigSchema` / -`ErrorMappingConfig` / `ErrorMappingConfigParsed`, `ErrorMappingRuleSchema` / -`ErrorMappingRule`, and `ConnectorErrorCategorySchema` / `ConnectorErrorCategory` -(the enum's only consumers were the two removed shapes; an exported value -schema with no consumer reads as a capability). `api/ErrorCategory` — the -HTTP-response vocabulary — is unaffected. - -**What stays, byte-identical:** every other connector key (`health`, `retry`, -`webhooks`, `fieldMappings`, `syncConfig`, `actions`, `triggers`, `provider`, -`providerConfig`, `auth`, …) with its default and its readers. - -## FROM → TO - -```ts -// before — parsed green; nothing ever read the block, no message was ever shown -defineStack({ - connectors: [{ - name: 'payments_api', - label: 'Payments API', - type: 'api', - errorMapping: { - rules: [{ - sourceCode: 429, - targetCode: 'RATE_LIMITED', - targetCategory: 'rate_limit', - severity: 'medium', - retryable: true, - userMessage: 'The payment provider is busy; try again shortly.', - }], - unmappedBehavior: 'generic_error', - }, - }], -}); - -// after — delete the key; there is no replacement because no error-mapping -// engine exists: a connector's failures reach callers as the provider's own -// errors (ADR-0097). A user-facing refusal text is the API error envelope's -// `userMessage`, declared by the code that throws — not connector metadata. -defineStack({ - connectors: [{ name: 'payments_api', label: 'Payments API', type: 'api' }], -}); -``` - -One-line fix: delete the `errorMapping` block; `os migrate meta --from 17` -lists the mechanical edits for existing sources. - -The retirement kit: - -- `retiredKey()` tombstone on `ConnectorSchema.errorMapping` - (`packages/spec/src/integration/connector.zod.ts`; the section comment - records what the shape was), inherited by `DeclarativeConnectorEntrySchema` -- ADR-0087 registration: `integration/Connector:errorMapping` and - `integration/DeclarativeConnectorEntry:errorMapping` in - `RETIRED_KEYS_BY_MAJOR[18]`; `integration/ErrorMappingConfig`, - `integration/ErrorMappingRule`, `integration/ConnectorErrorCategory` in - `RETIRED_DEFS_BY_MAJOR[18]`; the D2 conversion - `connector-error-mapping-removed` (protocol 18) wired into the step-18 chain - — a pure lossless strip of the block from every `connectors[]` entry, one - notice per connector (the eleven nested keys leave with the block) -- no liveness-ledger row: `connector` is not an enrolled ledger type, so - there is no row to keep or drop -- pin tests (`connector.test.ts`): refusal pins asserting the issue path, - code and prescription on the base schema, the declarative entry, and the - `stack.connectors[]` authoring path; the tsc `never` channel; a - no-materialize pin; the conversion's strip and notice; zero holders of the - seven retired names on every public entry; the ADR-0087 registration -- generated baselines/docs follow the schema (`authorable-surface/`, - `authorable-defaults/`, `api-surface/`, `json-schema.manifest/`, - `declaration-map/`, `export-origins/`, spec-changes, upgrade guide, - reference docs) -- zero authored occurrences in this repo's examples, skills and docs, and - zero hits in objectui at `0d8fd7c`, so no in-repo source changes ride along diff --git a/.changeset/console-a472b07167a3.md b/.changeset/console-a472b07167a3.md deleted file mode 100644 index 1cda964017..0000000000 --- a/.changeset/console-a472b07167a3.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -"@objectstack/console": minor ---- - -Console (objectui) refreshed to `a472b07167a3`. Frontend changes in this range: - -Derived from the changesets objectui declared over the range — 15 releasing of 18 changesets added across 29 non-merge commits; omitted: 3 release-nothing changesets, 11 commits carrying no changeset (they ship no package code). - -- **minor** — **BREAKING** — Converge the lookup/user widget metadata on the spec's camelCase — one concept, one spelling (objectui#7155, maintainer ruling A′ of 2026-09-03, director decision batch #19). (objectui `351eb3181`) -- **minor** — **BREAKING** — One authority for `KanbanSchema` / `KanbanColumn` / `KanbanCard`: the bare names now belong to `@object-ui/plugin-kanban` (objectui#6172, closing the cross-package half of objectu… (objectui `2c71482ea`) -- **minor** — Retire `ComponentInput.inputType` — the fifth and last key objectui#5905 named (ADR-0049 enforce-or-remove, maintainer ruling 2026-08-31, option B). (objectui `1ec291c0d`) -- **minor** — `@object-ui/core` publishes `resolveRecordSourceObjectName`, the ONE reader for "which object is this block bound to" (objectui#7627). (objectui `b041b9c0c`) -- **minor** — **Published TS surface narrowed:** `DashboardComponentSchema` no longer declares the dashboard-root `title` member (objectui#7623). (objectui `5d0876c5c`) -- **minor** — **BREAKING** — BREAKING (`@object-ui/components`): the chart primitives — `ChartContainer`, `ChartTooltip`, `ChartTooltipContent`, `ChartLegend`, `ChartLegendContent`, `ChartStyle` and the `Char… (objectui `7bf244bea`) -- **minor** — ListView: fold `data={{ provider: 'object', object }}` onto `objectName`, and read the author's view kind from `specType` / `type` (objectui#7477 — step 6 of #2890, released by th… (objectui `00d2fa682`) -- **minor** — Retire the dashboard-**root** `title` read across all five surfaces (objectui#7509, maintainer ruling 2026-09-04, decision batch #29, option C, under ADR-0049). (objectui `1cca678ba`) -- **minor** — **BREAKING** — Re-home the breakpoint layout vocabulary and delete the two dead responsive implementations (objectui#7580, maintainer ruling 2026-09-04, option A). (objectui `e62c44e7e`) -- **minor** — `@object-ui/types/zod`: the zod const `StylePropsSchema` is renamed to `ClassNameStylePropsSchema` (objectui#5928). **The old name is gone** — there is no deprecated alias and no… (objectui `24e027e93`) -- **patch** — Fix `extractToc` eating the underscores out of a `SCREAMING_SNAKE` heading, so its `#id` links resolve to the heading they name again (objectui#7667). (objectui `a472b0716`) -- **patch** — Remove `src/ui/toast.tsx`, an unreferenced primitive, and the dependency only it imported (objectui `2f61238b9`) -- **patch** — Fix `extractToc` deleting tag-shaped text that lives INSIDE an inline code span, so its `#id` links resolve to the heading they name again (objectui#7658). (objectui `90c6d090d`) -- **patch** — A record-page URL now names the object the clicked rows actually came from, in `ObjectTree` and `ObjectCalendar` (objectui#7638). (objectui `2ce2612df`) -- **patch** — fix(app-shell): the object-field options editor no longer drops `default` and `visibleWhen` on save (objectui `97c3e1972`) - -⚠️ 4 of these carry a breaking change: 4 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record. - -**In this console build, declared nowhere** — objectui merged 11 commits in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared them, so they appear in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387. - -- _(no changeset)_ fix(scripts): check-doc-links resolves the #fragment, not just the file (objectui#7644) (#7657) (objectui `f7cf7e8a9`) -- _(no changeset)_ docs(plugin-chatbot): document chatbot-floating's seven declared inputs keys (objectui#7594) (#7656) (objectui `8e501cb97`) -- _(no changeset)_ docs(agents): record the never-approve seat rule beside the governed never-list (#7630) (objectui `2e99852ca`) -- _(no changeset)_ refactor(examples): drop the inert root `title` from six catalog dashboards (#7634) (objectui `46cde8264`) -- _(no changeset)_ docs(check-skill-examples): drop the stale zero-jsonc-fences claim (#7631) (objectui `0b24d7f85`) -- _(no changeset)_ docs(governed-guard): replace the retired sha pin with the ruled approval-record predicate (#7616) (objectui `11edab88f`) -- _(no changeset)_ docs(skills): split multi-document JSON fences, drop the `...` elisions, mark every parsing fence (#7608) (objectui `89d6adf37`) -- _(no changeset)_ fix(scripts): judge spec citations at member granularity, and stop the header teaching a retired filter (objectui#7513) (#7617) (objectui `d28d87bf4`) -- _(no changeset)_ fix(governed-guard): an authorised approval record satisfies the queue leg on any commit (#7606) (objectui `0d8fd7ce3`) -- _(no changeset)_ chore(deps): Bump fumadocs-core from 16.14.4 to 16.15.4 (#7059) (objectui `1bae75bb8`) -- _(no changeset)_ docs(claude-md): collapse the two AGENTS.md excerpts to rule + hook + pointer (#7600) (objectui `c70ebaaeb`) - - - -objectui range: `00d3f09c500c...a472b07167a3` diff --git a/.changeset/contained-failure-visibility.md b/.changeset/contained-failure-visibility.md deleted file mode 100644 index fd9080a7bf..0000000000 --- a/.changeset/contained-failure-visibility.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"@objectstack/service-automation": minor ---- - -A contained per-iteration failure is now visible at run level, attributed to its iteration, and bound to its row. - -`loop { body: [ try_catch { try, catch } ] }` is the containment spelling for a per-iteration failure that must not end the sweep (there is deliberately no `loop.config.onIterationError` key). Containment already worked — the failure was caught, the loop went on and the run completed — but nothing said what it had contained: a sweep that lost two rows out of five reported `status=completed selected=5 acted=9 skipped=0` and was indistinguishable from one that lost none. The failure was in the step log and in `nodes[].failures`; no run-level number carried it, the failing step named no row, and `$error` bound no row identity. - -Four changes populate the contract `@objectstack/spec` already declares: - -- **`FlowRunSummary.failed`** — `summarizeRun` now folds `failed = Σ nodes[].failures` over the per-node array it publishes, so the run-level count can never disagree with the breakdown it summarizes. It counts every node execution that failed, contained or fatal; on a run that completed, all of them were contained. -- **`failed=N` on the run summary line** — `formatRunSummaryLine` prints the token whenever the count is present, `failed=0` included. That is the opposite of the `unmeasured` rule beside it and deliberate: `unmeasured` qualifies `acted`, while `failed` answers a question a completed run's line otherwise cannot be asked at all. Read `failed=0` precisely: **no node execution of this run failed**. It is the node fold and only that, so a `subflow` child's own contained failures stay on the child's summary rather than rolling up the way `acted` does — see #15617, where the declaration's two paragraphs are being reconciled. -- **Iteration through `try_catch`** — a step that ran in a `try` or `catch` region inside a loop body now carries the enclosing loop's `iteration`, with `regionKind` still `try` / `catch`. The step says which region ran it *and* which row it ran for. `parallel` branch tagging is unchanged. -- **`$error` binds the row** — the value bound to `errorVariable` (default `$error`) is the declared `TryCatchErrorValue`: `nodeId` and `message` as before, plus `iteration` and the loop's current `item` when the failure happened inside a loop body. A `subflow` / `map` child run has its own variable scope and therefore binds neither, so a parent's row identity never leaks into a child's `$error`. - -**`failed` absent means "not tracked", never `0`.** Runs recorded before this change keep it absent — no migration and no default, the same convention `unmeasured` carries. Defaulting it to zero would tell an operator "nothing failed" about a run nobody measured. Absent, the summary line prints no `failed=` token at all; present-and-zero prints `failed=0`. The count rides in the persisted `summary_json`, including on a summary compacted past the size cap, where the per-node `failures` it folds are exactly what gets dropped. diff --git a/.changeset/core-private-keys-pin-extension-boundary.md b/.changeset/core-private-keys-pin-extension-boundary.md deleted file mode 100644 index 1f35a21fb9..0000000000 --- a/.changeset/core-private-keys-pin-extension-boundary.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -"@objectstack/core": patch ---- - -fix(core): narrow the operation-private-keys pin's scanner to `.ts`, so it judges exactly the population turbo re-runs it for (#15090) - -`packages/core/src/security/operation-private-keys.pin.test.ts` filtered its -candidate set with `/\.tsx?$/` — `.ts` **and** `.tsx` — while this package's -declared radius in the cross-package declaration table is a `packages/**` -subtree glob ending in `.ts`. So the pin judged a population **strictly wider** -than the one either scoping layer of `check:cross-package-test-inputs` knows -about: Layer A never unions this package into the test shard when a `.tsx` file -changes, and Layer B never moves the `test` task's cache hash for one. A `.tsx` -file under `packages/` declaring its own `OPERATION_PRIVATE_KEY_PREFIX` or -`withoutOperationPrivateKeys` was therefore scanned by the pin and invisible to -CI's scoping — landing on `main` with every PR green and then reddening whichever -unrelated PR next touched a `.ts` file. That is the #7802 shape the declaration -table exists to close, one extension wide. - -Repaired by narrowing the **scanner**, not by widening the **glob** — and that -asymmetry is measured rather than assumed. On `b548e438d`, adding a `.tsx` glob -to this package's roster entry and re-deriving `check:cross-package-test-inputs`' -watch hints flips the dispatch-gates self-test case *"nor a .tsx test file inside -it"* from true to false, with the added glob itself as the covering hint. That -case is a live specimen for "a test class the hint route cannot reach", so the -red is real and re-pointing it is a decision in another lane, not a fixup. - -What the boundary costs, measured on the pin's own surface (tracked **plus** -untracked, ignored paths excluded) at `b548e438d`: **5408** `.ts` files scanned, -8 of them mentioning a guarded symbol; **8** `.tsx` files excluded, **0** of them -mentioning either symbol. The loss is empty today — and that reading is no longer -transcribed and trusted. A new case re-measures it on every run: it asserts the -excluded `.tsx` population is non-empty (so the boundary is an exclusion and not -an empty tree describing itself), that the filter really drops those files, and -that none of them declares either symbol. Ablation, with the restore proven by -blob hash rather than by exit code: re-widening the scanner reddens it while the -offender assertion stays green — which is precisely the failure mode, since a -wider scanner reads as coverage CI never runs — and planting a `.tsx` -redeclaration reddens it with a message that says the choice is a second-gate -trade, not a one-line widening. - -The correspondence between scanner and glob is now stated at **both** ends: the -pin's header and the declaration table's entry for this package. No published -surface moves — the only source file edited is a test. diff --git a/.changeset/core-time-zone-domain-repoint.md b/.changeset/core-time-zone-domain-repoint.md deleted file mode 100644 index ccfbd34775..0000000000 --- a/.changeset/core-time-zone-domain-repoint.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -'@objectstack/core': patch ---- - -refactor(core): the authz context's time-zone probe is now the shared value-domain predicate, not a third copy of it - -`resolve-authz-context.ts` carried a module-private `isValidTimeZone` — the -`Intl.DateTimeFormat` probe, re-stated. It was the third copy of one -definition, alongside `@objectstack/spec/shared`'s `isValueDomainMember` and -`service-settings`' own re-statement. `coerceTimeZone` now calls -`isValueDomainMember('iana_time_zone', …)` and the copy is gone. - -**No behavioural change, measured rather than asserted.** The two predicates -were run over a shared 4,058-input corpus — the zones -`Intl.supportedValuesOf('timeZone')` omits (`UTC`, `Asia/Kolkata`, -`Europe/Kyiv`, `Asia/Ho_Chi_Minh`, `US/Eastern`, `GMT`), every member of that -enumeration plus its case- and space-padded variants, refusals, `Etc/` and -offset spellings, legacy aliases, and fuzz — with **zero disagreements**, and -the same zero at the `coerceTimeZone` level. The call site's own -pre-processing (trim, stringify a non-string, refuse blank) is unchanged. - -What this buys is drift resistance, not a fix: core's time-zone acceptance now -sits under the shared pins, so a future "modernisation" to -`Intl.supportedValuesOf('timeZone')` — which would silently narrow what the -authz context accepts, since that enumeration omits this platform's own -default `UTC` — turns a test red instead of shipping. diff --git a/.changeset/dashboard-gap-author-vocabulary.md b/.changeset/dashboard-gap-author-vocabulary.md deleted file mode 100644 index 2b8464be53..0000000000 --- a/.changeset/dashboard-gap-author-vocabulary.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -"@objectstack/spec": patch -"@objectstack/platform-objects": patch ---- - -fix(spec): the dashboard `gap` field no longer describes itself to app authors in Tailwind vocabulary - -`ui/dashboard`'s `gap` key told app authors its value in the vocabulary of a CSS -library they never chose and cannot act on. **Two** independent producer strings -carried that wording, and they feed two independent customer-facing surfaces: - -- `dashboardForm`'s `helpText` — `Grid gap (Tailwind units)` — rendered verbatim in - the Studio property panel, which is spec-driven and feeds this form straight into - the generic form renderer. -- `DashboardSchema.gap`'s `.describe()` — `Grid gap in Tailwind spacing units` — - rendered as this field's row in the published reference page - `content/docs/references/ui/dashboard.mdx`. The reference corpus renders - `.describe()`, never `helpText`. - -Both now read **Space between widgets, in steps of 0.25rem (4 = 1rem)**: what the -author decides, plus the magnitude, stated in a CSS unit instead of a framework's -scale. The magnitude had to survive the rewrite rather than be dropped with the -framework name — the number is a spacing step, so `4` means `1rem` and not `4px`, -and an author who lost that would come away knowing less than before. - -The step size is stated as measured rather than inferred: the dashboard renderer -sets the grid gap as an inline style computed from this key, so every accepted -value is linear and one step is exactly `0.25rem`. "Tailwind units" was doubly -wrong — it named an implementation dependency, and it named one the consumer of -this key does not have. - -**No schema change.** `gap` stays `z.number().int().min(0).optional()` and accepts -exactly what it accepted before; nothing is added to or removed from any public -surface. `columns` is deliberately untouched on both of its producer lines — -`12` is an author-visible fact about the grid being laid out, not a framework -detail — and this is one field's two strings, not a sweep for framework words. - -The `en` metadata-forms translation bundle is a mechanical copy of the form source, -so it is regenerated to match. Translated locales are not touched: regeneration -fills gaps only and never overwrites an existing leaf. diff --git a/.changeset/data-driver-aggregate-declared.md b/.changeset/data-driver-aggregate-declared.md deleted file mode 100644 index 1563cf344c..0000000000 --- a/.changeset/data-driver-aggregate-declared.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -`IDataDriver` now declares `aggregate?` — the one engine-reached driver verb that had no signature to match against. - -The engine has always dispatched native aggregation by presence (`typeof driver.aggregate === 'function'`) and called `driver.aggregate(object, query, options)`, but the interface never spelled the member, so a custom driver's `aggregate` was checked in neither direction: swapped arguments or a non-row result compiled clean and surfaced only after the engine's `having` filter silently matched nothing. The member is declared optional, matching the presence test — a driver without native aggregation omits it and stays conformant, served by the `find()` + in-memory fallback. - -Additive: every in-repo driver already satisfies the declared signature (`(object: string, query: DriverQuery, options?: DriverOptions) => Promise[]>`); a wider parameter union or a looser return type stays assignable. What is newly refused is a wrong argument order or a non-array result. No `DriverCapabilities` bit is added — presence remains the capability test, as `data/driver.zod.ts` rules. diff --git a/.changeset/decision-predicate-envelope-refused.md b/.changeset/decision-predicate-envelope-refused.md deleted file mode 100644 index 2d29f4e39b..0000000000 --- a/.changeset/decision-predicate-envelope-refused.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"@objectstack/spec": minor -"@objectstack/service-automation": minor -"@objectstack/lint": minor ---- - -A flow predicate authored as a CEL envelope is now refused at build time, instead of running unread by either validator. - -A `predicate`-role expression slot holds **bare CEL text** — `DecisionConditionSchema.expression` is declared `z.string()`, and so is a screen field's `visibleWhen`. An author who instead wrote the `{ dialect, source }` expression *envelope* there reached a shape nothing could see: a flow node's `config` is an open `z.record(z.unknown())` that no Zod schema is parsed against, the unknown-key walk exempts the schemaless node types on purpose (`decision` publishes no descriptor `configSchema`), and the expression ledger's `predicate` arm skipped every non-string as "a type violation for the schema pass to report" — a schema pass that, for those node types, does not exist. `registerFlow` accepted the flow, `objectstack validate` reported nothing, and the evaluator was the only layer that ever read the predicate. - -- `resolveFlowNodeExpressions` now emits a non-string sitting in a `predicate` slot, and the new `predicateSlotRefusal` / `PREDICATE_SLOT_STRING_REFUSAL` say why it is refused — one notion, derived once, read by both validators so build time and author time cannot disagree about the shape. `flow-template` slots keep the old rule: no validator implements that dialect, so a finding there is one nobody could judge. -- `registerFlow` throws, naming the node, the slot and the index, and attributing the finding to the envelope's own `source`. `objectstack validate` reports the same refusal as a located `error`. - -**String predicates are untouched, deliberately.** A whitespace-only string still means "not authored" on both sides, exactly as before; what a non-empty string *says* is still judged by `validateExpression('predicate', …)`, brace trap and all. Only the shape moved. - -An app that authored an envelope in one of these slots now fails to register with a message naming the slot; the fix is to write the predicate as bare CEL text (`record.rating >= 4`). The `{ dialect, source }` envelope remains the `value`-role spelling, on the `assignment` node's `assignments` map. diff --git a/.changeset/diagnostics-untyped-sweep-organization-forwarding.md b/.changeset/diagnostics-untyped-sweep-organization-forwarding.md deleted file mode 100644 index 259062b790..0000000000 --- a/.changeset/diagnostics-untyped-sweep-organization-forwarding.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@objectstack/rest": patch ---- - -An organization-scoped caller's own items now appear in the untyped metadata diagnostics sweep. - -`GET /api/v1/meta/diagnostics` has two arms. The `?type=` arm has stated the caller's organization since #13753; the untyped whole-registry sweep passed none, so the Studio governance summary reported clean tiles over a partition it never read — undercounting relative to the per-type drill-down screen you reach by clicking into it. A summary whose whole job is surfacing problems, and which structurally cannot see a class of them while its own drill-down can, issues a false all-clear. The untyped arm now forwards the caller's organization, so items that organization authored on the five `allowOrgOverride: true` types (`view`, `dashboard`, `report`, `translation`, `email_template`) are counted in `stats`, `total` and `scannedItems`. - -The organization is passed RAW, deliberately, and that is the whole of the change — no new parameter, response field, status code or contract surface. There is no single type to fold on for a whole-registry sweep, and folding on any one of them would suppress the organization for every type at once; instead `getMetaDiagnostics` reads each swept type through `getMetaItems`, which applies the `allowOrgOverride` read gate to its own request type, so every type is scoped on its own registry flag. A non-overridable type (`object`, `flow`, `app`, …) is still read environment-wide and no pre-#6190 organization-scoped row is resurrected into the report. An anonymous or organization-less caller reads exactly what it read before, and the `stats` / `total` / `scannedTypes` arithmetic is unchanged in shape. diff --git a/.changeset/driver-memory-find-findone-create-honest-types.md b/.changeset/driver-memory-find-findone-create-honest-types.md deleted file mode 100644 index dd41c9d4c0..0000000000 --- a/.changeset/driver-memory-find-findone-create-honest-types.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -'@objectstack/driver-memory': minor ---- - -fix(driver-memory): `find()`, `findOne()` and `create()` publish their declared types (#14435) - -**BREAKING** for TypeScript consumers — a published TYPE-surface narrowing, the same shape #13878 landed on `update()` / `upsert()` one door over, shipped as `minor` under the launch-window convention (`major` is refused by `check-changeset-no-major`, so the BREAKING banner and the ADR-0087 disposition are the carriers, not the level). - -`IDataDriver` has always declared `Promise[]>`, `Promise | null>` and `Promise>` on these three doors. The emitted `.d.ts` published `Promise`, `Promise` and `Promise>`: the return types of `find` and `findOne` were INFERRED through the backing store's `any[]` rows (`private db: Record` to `getTable()`), and `create` carried an explicit annotation that itself spelled `Record`. They are now declared as the contract declares them. - -What this asks of a consumer holding a concrete `InMemoryDriver`: a caller that reads fields off a `findOne()` result narrows the `null` arm first — the arm the driver has always been able to answer with (`results[0] || null`) and that no caller was ever asked to handle; and a caller that leaned on `any` to read a member off a `find()` row or a `create()` result now types it, since the rows are `Record`. A consumer whose receiver is typed as `IDataDriver` sees no change at all — that declaration already said this. - -The parameters are deliberately untouched: `create(data: Record)` stays as it is, because narrowing an INPUT would be a second, unrelated break, and method parameters compare bivariantly against the contract's `Record`. No runtime behaviour changes; the store keeps its `any[]` rows, which the card measured to cascade if re-typed. - - diff --git a/.changeset/driver-memory-notcontains-non-string-value.md b/.changeset/driver-memory-notcontains-non-string-value.md deleted file mode 100644 index 8c67de7e7e..0000000000 --- a/.changeset/driver-memory-notcontains-non-string-value.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@objectstack/driver-memory": patch ---- - -fix(driver-memory): the reference matcher's `$notContains` arm answers the predicate, not a type test, for a stored non-string value - -`match()` used to answer `{ n: { $notContains: '5' } }` with NO for `{ n: 5 }` — the arm read `typeof value !== 'string' || value.includes(target)`, so a number failed `$contains` (correct) AND its negation (wrong: for the very reason a number cannot contain the substring, it does not contain it). This package's own live mingo path admitted the row, so one filter answered two ways depending on which face was asked; on this face the failure mode was silently dropped rows. - -The arm now answers what `FILTER_TEXT_CASES`' new `score` rows declare on every face (maintainer ruling 2026-09-05 on the contract card): a stored value that is not a string never satisfies a positive text operator and always satisfies `$notContains`. The no-value cells keep their #13166 answer; nothing else in the matcher moved. diff --git a/.changeset/driver-mongodb-test-tsc-program.md b/.changeset/driver-mongodb-test-tsc-program.md deleted file mode 100644 index 79d24ac974..0000000000 --- a/.changeset/driver-mongodb-test-tsc-program.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -"@objectstack/driver-mongodb": patch ---- - -fix(driver-mongodb): put the test layer in front of tsc, so the package's own typecheck reports a PASS and not a NUMBER (#14917) - -`packages/drivers/driver-mongodb`'s `tsconfig.json` excluded `**/*.test.ts`, and -its `typecheck` script is `tsc --noEmit` against that very config. Measured at -`6ed4b811af` with the dependency closure built: that program admits **0** of the -package's 30 `src/**/*.test.ts` files while all **10** of its non-test `src/**` -files ARE there, so `pnpm --filter @objectstack/driver-mongodb typecheck` -exiting 0 was a true sentence carrying no information about any test file. - -The filing's headline — that a compile-time `Equals` / `IsAny` pin here is -"checked by nothing" — is **false**, and the correction on the card is right: a -second program does compile these files. `check-type-check-coverage.mjs`'s -`remeasureProject` drops only the test glob and compares the result against its -`TEST_DEBT` ledger. Confirmed here by ablation rather than argued: a -deliberately false `Equals` pin added to `mongodb-driver.test.ts` takes that -program from 10 errors to 11, above the ledger's recorded 10, which reddens it. -The pins were never phantoms. What was true is narrower, and is what this change -closes: the only program reading this layer was a **debt ratchet** — an -instrument that reports a number and fails when the number moves, not a gate -that reports a pass. - -Gives the package the #5286 sibling shape (`packages/rest`, `runtime`, -`objectql`, `core`): a `tsconfig.test.json` with module semantics only — -`esnext` / `bundler` / `lib: ES2022`, matching how vitest actually executes -these files — strictness inherited and untouched, named by the `typecheck` -script via `check:test-typecheck`. - -Measured: **10** errors under the ratchet's shape (matching its recorded number, -and its recorded composition `TS1309 x7, TS2550 x3`, class for class), and **0** -under the split. All 10 were config-tier in full — 7 `TS1309` (`await` at module -scope in a program NodeNext compiles as CJS, because this package has no `"type": -"module"`) and 3 `TS2550` (`Array.prototype.at` against a `lib` older than -es2022). Neither class says anything about a test, and nothing was exposed -behind them: there was no unresolved-import cascade here to collapse, so there -is no `+n` term. `noUnusedLocals` / `noUnusedParameters` are live for this -package (unlike `driver-turso`, which switches both off) and neither fires. - -The `TEST_DEBT` entry (10 errors) is **deleted**, not lowered — the graduation -this ratchet's invariant requires. No `test-typecheck-debt.json` is added: -residue is 0, so none is owed (#5286, maintainer-only to open). That leaves all -30 files unledgered, so any error any one of them gains is red on arrival. - -`check:type-source-resolution` went red from onboarding the new program (the -documented onboarding-limb case, #11490): a registry entry is added rather than -`paths`, with its numbers stated in place — 123 tsc programs / 309 pairs before, -124 / 310 after. The single new pair is `@objectstack/objectql`, a devDependency -that no non-test file in `src/` imports. - -No runtime code changes: not one test file and not one source file is edited, so -no shipped behaviour moves — the suite reports the same 552 passed / 147 skipped -across 30 files as before. The `patch` level reflects the published -`package.json` gaining `typecheck` / `check:test-typecheck` scripts and a `tsx` -devDependency. diff --git a/.changeset/driver-sql-text-operator-non-text-column.md b/.changeset/driver-sql-text-operator-non-text-column.md deleted file mode 100644 index 723cd1d38e..0000000000 --- a/.changeset/driver-sql-text-operator-non-text-column.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@objectstack/driver-sql": minor ---- - -A text operator over a column whose declared type stores no text (`Field.number` and its numeric siblings, `Field.boolean`) now compiles to the contract's declared answer on every dialect, instead of a dialect accident. - -Before: `{ score: { $contains: '5' } }` over a numeric column compiled `col GLOB '*5*'` on SQLite and coerced the REAL in its storage class's spelling (`5` as `'5.0'`, so `$endsWith: '0'` matched every row), `col LIKE $1 ESCAPE $2` on Postgres and was refused at query time with SQLSTATE 42883 (`operator does not exist: real ~~ text` — a 500 for a filter the spec accepts), and `CAST(col AS BINARY) LIKE ?` on MySQL. - -Now (`FILTER_TEXT_CASES`' `score` rows, maintainer ruling 2026-09-05): the positive operators (`$contains` / `$startsWith` / `$endsWith` / `$icontains` / `$like` / `$ilike`) compile to `1 = 0` and `$notContains` to `1 = 1` — the same row set as every JS face, decided from the declared type at compile time because the stored value is not visible until run time. Postgres: a 500 becomes a result. The gate reads the `numericFields` / `booleanFields` registries `initObjects` and `registerExternalObject` already fill; a table this driver was never told about keeps the `LIKE` / `GLOB` it always compiled, every comparand refusal still runs first, and the constants compose with the NULL-safe rules (`$notContains` admits a NULL row already) and the `$not` rewrite. Temporal columns are untouched: their stored value IS text on SQLite, so the contract declares nothing for them. - -`driver-sqlite-wasm` and `driver-turso`'s local transport inherit this compiler. diff --git a/.changeset/driver-sql-update-declared-null.md b/.changeset/driver-sql-update-declared-null.md deleted file mode 100644 index 65b737e404..0000000000 --- a/.changeset/driver-sql-update-declared-null.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -'@objectstack/driver-sql': minor ---- - -feat(driver-sql): `update()` publishes its honest type — the contract's `Record | null`, not `any` (#14438) - -**BREAKING** for TypeScript consumers — a published TYPE-surface narrowing, shipped as `minor` under the launch-window convention (the one PR #14434 used for the same door on `@objectstack/driver-memory`). `SqlDriver.update()` was written out with an explicit `Promise` while it has always answered a missing id with `null` (`formatOutput(...) || null` on the un-rotated path, `null` once every rotation shard has been probed). `IDataDriver.update()` declares `Promise | null>`, and an explicit `any` satisfies that structurally — so the emitted `.d.ts` read `Promise` and no caller holding a `SqlDriver`, or a `SqliteWasmDriver` (which inherits the door unchanged), was ever asked to narrow. It is now declared as the contract declares it, and the protected rotation-path producer `rotatedUpdateById()` carries the same type. A caller that read fields off `update()`'s result through the `any` now narrows the `null` arm first; a caller that leaned on `any` to read undeclared members now types them. No runtime behaviour changes. - -`@objectstack/driver-sqlite-wasm` re-declares no `update` member of its own (measured on its emitted `.d.ts`), so it carries no entry: the narrowing reaches its consumers through this package's `.d.ts`. `@objectstack/driver-turso` overrides the door and carries its own entry. - - diff --git a/.changeset/driver-turso-remote-text-operator-non-text-column.md b/.changeset/driver-turso-remote-text-operator-non-text-column.md deleted file mode 100644 index 34882ade6d..0000000000 --- a/.changeset/driver-turso-remote-text-operator-non-text-column.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@objectstack/driver-turso": minor ---- - -The remote transport compiles a text operator over a declared numeric or boolean column to the contract's declared answer, in step with the local transport. - -`RemoteTransport.buildWhereSQL` compiles filters independently of `SqlDriver` and keeps no schema, so a text operator over a `Field.number` used to compile `"col" GLOB ?` and coerce the REAL in the storage class's spelling (`5` as `'5.0'`). `TursoDriver` now hands the transport its declared-type rule (`setNonTextColumnResolver`, the same shape as the temporal `setFilterColumnSql` rule), answered from the registries `registerRemoteFieldMetadata` already fills at schema sync — so a positive text operator over such a column compiles to `1 = 0` and `$notContains` to `1 = 1` on BOTH transports (`FILTER_TEXT_CASES`' `score` rows, maintainer ruling 2026-09-05), instead of a dialect accident. A transport nobody handed the rule to compiles exactly as before, and every comparand refusal still runs ahead of the constant. diff --git a/.changeset/driver-turso-update-declared-null.md b/.changeset/driver-turso-update-declared-null.md deleted file mode 100644 index 28cea18aae..0000000000 --- a/.changeset/driver-turso-update-declared-null.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/driver-turso': minor ---- - -feat(driver-turso): the `update()` override publishes its honest type — `Record | null`, not `any` (#14438) - -**BREAKING** for TypeScript consumers — a published TYPE-surface narrowing, shipped as `minor` under the launch-window convention. `TursoDriver` overrides `update()` rather than inheriting it, and the override was written out with its own explicit `Promise` — so this package's emitted `.d.ts` re-declared the door as `any` on its own and would not have picked up the `@objectstack/driver-sql` narrowing. Both of its branches already answered the contract's type: the local branch forwards to `SqlDriver.update()` (narrowed alongside, #14438) and the remote branch passes `RemoteTransport.update()`'s `Record | null` (#14428) through the generic `formatRemoteRow`. The override now declares what it answers. A caller that read fields off the result through the `any` now narrows the `null` arm first. No runtime behaviour changes. - - diff --git a/.changeset/duplicate-record-error-developer-message-wire-spelling.md b/.changeset/duplicate-record-error-developer-message-wire-spelling.md deleted file mode 100644 index 06f899585a..0000000000 --- a/.changeset/duplicate-record-error-developer-message-wire-spelling.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@objectstack/objectql": patch ---- - -fix(objectql): `DuplicateRecordError.developerMessage` names the wire spelling a client branches on (#14723) - -The envelope's `developerMessage` — the remedy sentence addressed to the -application author — told its reader to "branch on `code === 'DUPLICATE_RECORD'`", -which is the engine's THROWN identity and holds only for an in-process caller of -`engine.insert` / `engine.update`. Every REST route reports the same refusal as -`UNIQUE_VIOLATION`, and since #14723 the per-row reports of the batch and import -surfaces do too, so the sentence was a platform contradicting itself on the one -line an author is most likely to copy. It now says both halves: over the HTTP -API branch on `code === 'UNIQUE_VIOLATION'` on every route, whole-request and -per-row alike; inside the engine the thrown class carries `DUPLICATE_RECORD`. -The class's own docblock says the same. Nothing else about the envelope moves: -`code`, `status`, `cause`, `field`, `object` and the user-facing `message` are -byte-identical, and every pin on the engine's thrown code holds. diff --git a/.changeset/evaluated-expression-slot-requires-source.md b/.changeset/evaluated-expression-slot-requires-source.md deleted file mode 100644 index bb7db75b05..0000000000 --- a/.changeset/evaluated-expression-slot-requires-source.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -feat(spec)!: an evaluated expression slot requires a non-blank `source` — `EvaluatedExpressionSchema`, composed by the `assignment` value envelope (#15430) - - - -**BREAKING** in the accept-set sense, landing in the launch window as `minor` -(the lockstep convention): on the schemas that type an EVALUATED expression -slot — today the `assignment` node's value envelope, -`AssignmentExpressionValueSchema` — an envelope with no `source` the engine can -evaluate is now **refused at authoring**, where it used to parse, register, -pass `objectstack validate`, and then fault at run time. - -Two spellings of one seam, refused by ONE rule with one message at `source` -(`EVALUATED_EXPRESSION_SOURCE_REQUIRED`): - -```yaml -assignments: - digest: { dialect: cel, ast: { kind: const } } # `ast` only — no engine evaluates it - greeting: { dialect: cel, source: ' ' } # blank after trimming — parses to EOF -``` - -> An expression in an evaluated slot needs a non-blank `source`: the expression -> engine evaluates `source` (the canonical persisted form of phase M9.1) and -> cannot evaluate `ast` alone, so an envelope carrying only `ast`, or a `source` -> that is blank after trimming, would validate and register and then fault at -> run time. Write `{ dialect: 'cel', source: '…' }`. - -- **`ExpressionSchema` is NOT narrowed.** It is the persistence contract — - `source` OR `ast` — and its docblock declares that `ast` becomes required in - build output at phase M9.2. The new export `EvaluatedExpressionSchema` (and - its type `EvaluatedExpression`) is a sibling: the same envelope with `source` - required and non-blank, spelled once and composed by every evaluated slot, so - when AST-only evaluation lands the flip is one edit there rather than a - per-slot unwinding. The rule is worded as "an evaluated slot requires whatever - the engine can actually evaluate"; what that is today is `source`. -- **The notion of blank is the engine's own** — `.trim()`, which - `cel-engine.ts`'s helpers already apply — not a third one beside the shape - rule's `min(1)` and `validateExpression`'s trim. -- **Three doors agree.** `registerFlow` refuses the flow, `objectstack validate` - and the runtime publish gate report a located `error` at the author's own - variable (`config.assignments..source`), and the executor's own shape - pass refuses the same set — all through the spec schema, so none of them - grew a rule of its own. - -**What an author does with a refused envelope.** An assignment value that -carried only `ast` has no evaluable form under M9.1: author its `source`. A -whitespace-only `source` was never an expression: delete the entry, or write -the expression. Every envelope with a non-blank `source` is unchanged, and -nothing is renamed, retired or rewritten — the refusal itself carries the -prescription. - -Not touched here: the `predicate` half of the same seam — `evaluateCondition`'s -silent `false` on an envelope without a `source` — is a behaviour change on a -live path with its own card, and the edge-condition schema that carries that -envelope is narrowed in a follow-up once the in-flight change to -`automation/flow.zod.ts` lands. diff --git a/.changeset/field-value-domain-write-path.md b/.changeset/field-value-domain-write-path.md deleted file mode 100644 index 8cbcd8dfa1..0000000000 --- a/.changeset/field-value-domain-write-path.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -'@objectstack/objectql': minor -'@objectstack/spec': minor ---- - -feat(objectql,spec): `Field.valueDomain` binds at the write seam — a non-member is refused with `value_domain` (maintainer ruling 2026-09-02 on #14168, engine half) - -**BREAKING** accept-set narrowing on the ObjectQL record write path, shipped as -`minor` under the repo's launch-window convention for breaking changes. - -The key is **already published, and published unenforced**. The version-packages -cut `8a1bad8b8` (2026-09-04 10:20Z) consumed the spec half's changeset -`field-value-domain-slot.md` and released `@objectstack/spec@17.3.0`, which -declares `Field.valueDomain`, parses it, and refuses it on any type other than -`text` — and never reads it when a record is written. The 17.3.0 liveness ledger -states the gap in its own words: "a non-member WRITTEN to a `text` field -declaring a domain is accepted today". That write is accepted on 17.3.0 and is -refused from this release on. - -**Refused shape**, precisely: a record write that supplies a value for a `text` -field whose definition declares `valueDomain`, where the WRITTEN value is not a -member of the named standard. It fails with the field error code `value_domain`, -carrying `constraint: { valueDomain }` and a message that names the standard in -all four platform locales. Nothing else narrows — a field that declares no -`valueDomain` is untouched, and so is every other field type, because the schema -accepts the key on `text` alone and the validator judges exactly that set. - -**Remedy: write a member of the declared standard.** `iana_time_zone` admits -`UTC` and refuses `Mars/Olympus`; `iso_4217_currency` admits `CHF` and refuses -`chf`; `iso_3166_alpha2` admits `CH` and refuses `ZZ`. Dropping the -`valueDomain` declaration from the field lifts the refusal entirely, for an -author who declared a domain they did not mean. - -**No stored row is touched, and none becomes invalid.** This is the `min` / -`max` / `maxLength` transition-gate class: a value stored before the domain was -declared — or before this release — is never re-read, and it survives an edit of -another field on the same record. An absent or empty value follows the field's -`required` handling, not this check. - - - -- The membership test is the spec's shared `isValueDomainMember` — the same - predicate, over the same closed vocabulary, that a settings specifier's - `valueDomain` uses. A time zone accepted in Settings is the time zone - accepted in a field. -- The two authoring forms (`fieldForm`, `objectForm`) gain a `valueDomain` - control, shown on exactly the types the schema accepts the key on. The - object-form control's choices are derived from the vocabulary, not re-typed. diff --git a/.changeset/filter-text-non-string-stored-value.md b/.changeset/filter-text-non-string-stored-value.md deleted file mode 100644 index 5632728f89..0000000000 --- a/.changeset/filter-text-non-string-stored-value.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -`FILTER_TEXT_CASES` declares what a text operator answers over a stored value that is NOT a string, and the fixture gains its first non-string column. - -Measured before this row existed, one filter over one numeric column answered four ways across the platform: `driver-memory`'s reference matcher said NO to `$contains` and to `$notContains` for the same row; its live mingo path, `formula`, objectql's `having`, `driver-mongodb` and the analytics face type-gated (`$contains` NO, `$notContains` YES); the SQLite family coerced the number to text in its storage class's spelling (REAL renders `5` as `'5.0'`); and live Postgres refused at query time with SQLSTATE 42883 — a 500. - -The maintainer ruled the cell on 2026-09-05 (option A, type-gate): a stored value that is not a string never satisfies a positive text operator (`$contains` / `$startsWith` / `$endsWith` / `$icontains` / `$like` / `$ilike`) and satisfies `$notContains` — complementarity holds, on every face. Coercion was refused on the measurement; a declared-type door that refuses the filter before any backend runs is deferred to its own decision card, not rejected. - -- `FilterTextRow` is now `{ id, name, score }` — `score` is a NUMBER on every row (a `0` among them), chosen so a coercing backend answers a visibly non-empty set and a truthiness guard drops a row. -- Five new evaluated rows over `score`: the four positive operators the table can carry answer `[]`, `$notContains` answers all nine. (`$like` / `$ilike` follow the same rule and are pinned on the faces that answer them — the table is a driver's enrolment and `driver-mongodb` refuses those two.) -- `NON_TEXT_STORED_VALUE_TYPES` (`field-value.zod.ts`) — the numeric and boolean value classes, i.e. the declared field types whose stored value is never text — is the list the SQL faces classify a column by at compile time, since they cannot read the value. Temporal types are deliberately absent: their stored form is a dialect question (ADR-0053) the row does not decide. - -Every suite that materialises the fixture adds the column (SQL `initObjects` DDL included). diff --git a/.changeset/filter-text-operator-declared-type-door.md b/.changeset/filter-text-operator-declared-type-door.md deleted file mode 100644 index 5a20137cef..0000000000 --- a/.changeset/filter-text-operator-declared-type-door.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -feat(spec)!: a text operator over a field whose DECLARED type can never store a string is refused at the engine's field-aware door — the contract rows (#15661) - - - -**BREAKING** accept-set narrowing, declared here and enforced at the engine door: a text operator (`$contains` / `$notContains` / `$startsWith` / `$endsWith` / `$icontains` / `$like` / `$ilike`) over a field whose DECLARED type is numeric, boolean, temporal (`date` / `datetime` / `time`) or structured JSON is refused before any driver runs — `INVALID_FILTER` / 400, naming the field and its declared type — instead of answering `[]` or a dialect accident. Shipped as `minor` under the repo's launch-window convention for breaking changes. Maintainer ruling 2026-09-05 on #15661 (director decision batch #43, verbatim 「同意」): option C-deny. - -The refused set is the union of six EXISTING classes in `field-value.zod.ts`, by reference — `NUMERIC_VALUE_TYPES` ∪ `BOOLEAN_VALUE_TYPES` ∪ `CALENDAR_DATE_TYPES` ∪ `INSTANT_TYPES` ∪ `CLOCK_TIME_TYPES` ∪ `STRUCTURED_JSON_TYPES` — so no new vocabulary is minted and a member added to one of those sets later is refused without a change here. String-valued classes pass: `STRING_VALUE_TYPES`, `autonumber`, the option-code classes (single and multi — `tags` included), the record-id classes, and the file classes. `formula` is judged as the field type its declared `returnType` names (`text` passes; `number` / `boolean` / `date` are refused) and is deferred — not judged — when `returnType` is absent. A dotted path into a structured-JSON field stays unjudged, as `filter-dotted-head` already declares. - -New on `@objectstack/spec/data` (`filter-text-operator-declared-type.ts`): `TEXT_FILTER_OPERATORS` (pinned equal to `StringOperatorSchema`'s keys), `TEXT_OPERATOR_DOOR_REFUSED_TYPES` / `TEXT_OPERATOR_DOOR_PASSING_TYPES`, `FORMULA_RETURN_TYPE_AS_FIELD_TYPE`, the pure verdict `textOperatorDoorVerdict`, the class table `TEXT_OPERATOR_DOOR_TYPE_CLASSES` (every `FieldType` member exactly once — pinned as a census), the fixture object `TEXT_OPERATOR_DOOR_FIXTURE`, and the derived case table `TEXT_OPERATOR_DOOR_CASES` the engine suite consumes. - -The door itself lands in `@objectstack/objectql` under its own engine-lane card (beside the `INVALID_FIELD` unknown-field door, judged against the object's real field map, before any driver dispatch); this changeset is the contract half. Beneath the door nothing moves: a direct driver call — and every evaluator no door fronts — keeps answering `FILTER_TEXT_CASES`' stored-value row (#14079), and the SQL faces' compile-time type-gate set `NON_TEXT_STORED_VALUE_TYPES` stays numeric + boolean, deliberately narrower than the door's set. - -What an author sees after the door lands: a condition such as `{ amount: { $contains: '5' } }` over a `number` field, which used to answer an empty list with no signal, is refused with a message naming `amount`, `number` and `$contains`. The condition was a mistake in every measured occurrence (a substring over a number can never match); drop it, or aim it at the text field that was meant. diff --git a/.changeset/flow-action-record-load-signal.md b/.changeset/flow-action-record-load-signal.md deleted file mode 100644 index 3fad885942..0000000000 --- a/.changeset/flow-action-record-load-signal.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -"@objectstack/runtime": minor -"@objectstack/spec": patch ---- - -feat(runtime): a flow action's run context now carries `recordLoadDenied` (#15168) - -The previous release declared `AutomationContext.recordLoadDenied?: true` and -said so plainly: **declared, not yet populated on the flow face.** The -script/body face of both action doors emitted the signal, but -`dispatchFlowAction` handed `automation.execute` a context without it, so a -`runAs: 'system'` flow that guarded on the documented key was inert — never -`true`, never wrong, and indistinguishable from a flow whose caller could read -the row. - -**This release populates it, on both doors in one stroke** — REST -`POST /api/v1/actions/...` and the MCP `run_action` bridge: - -```js -// a runAs:'system' flow, guarding before it acts on the subject row -if (context.recordLoadDenied === true) { /* the invoker cannot read this row */ } -``` - -- **The exact producer shape, unchanged.** The one shared producer - (`loadActionSubjectRecord` → `actionRecordLoadSignal`) already returns - `{ recordLoadDenied?: true }`, and the flow door now spreads it as a - **sibling of `record`** — never a key on the record, and **absent**, never - `false`, when nothing was refused. So a flow reads it exactly as a handler - does, `recordLoadDenied === true`. -- **Both doors, structurally.** `dispatchFlowAction`'s wiring now takes the - load OUTCOME (`subject`) instead of a bare `record`, and derives both the - record and the signal from it. A caller can no longer forward the row while - dropping the verdict that says the caller could not read it — the omission is - a compile error rather than a guard silently inert one door over, which is - the defect the handler-face signal was filed for. -- **Purely additive.** Nothing is refused that was not refused before, no - existing key changes value, and the `recordId` stamp is deliberately kept: - `record.id` still arrives exactly as it did, which is why the flag — and not - `record.id` — is the authorization predicate. Whether the automation engine - *acts* on the key (a flow-level refusal, a step condition) is a separate - decision and is deliberately not part of this change. -- **`@objectstack/spec` (docs only).** The contract's "not yet populated on the - flow face" sentence is retired; no type changes. diff --git a/.changeset/flow-edge-id-uniqueness.md b/.changeset/flow-edge-id-uniqueness.md deleted file mode 100644 index a69e7ad3c7..0000000000 --- a/.changeset/flow-edge-id-uniqueness.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -feat(spec)!: `FlowSchema` refuses a flow whose `edges[]` declares the same id twice (#14964) - - - -**BREAKING** accept-set narrowing on `FlowSchema` — a flow whose `edges[]` -carries two edges with the same `id` is now **refused at parse time** — by -`FlowSchema.parse` / `safeParse`, `defineFlow`, and every door that validates a -flow through the schema (`objectstack validate`, the runtime publish gate, a -stack's `flows[]`) — where it used to parse on green. Shipped as `minor` under -the repo's launch-window convention for breaking changes. Maintainer ruling -2026-09-05 on #14964 (director decision batch #40, verbatim 「同意」): option -A — an `error`, not a `warning`; no opt-out, no transition window. - -Every reader of an edge id assumes the ids in a flow are unique — a designer, -a BPMN export, a flow diff, any traversal that dedupes by id — and nothing -enforced it. A real duplicate (`id: 'e20'` on two edges of one flow) shipped -through two releases of green CI in a downstream app and was inert only -because the engine keys out-edges by `source`, never by `id`: the collision is -invisible until something keys on ids, and then silently wrong rather than -loudly broken. The id space is hand-authored, so the next author picking a -"free" id from the sequence had no way to know it was taken. - -**What changes** (`packages/spec/src/automation/flow.zod.ts`): a `superRefine` -on the flow's `edges[]`. Each later occurrence of an already-declared id raises -one `custom` issue, anchored at `edges[N].id` of the *later* edge and naming -both positions, so the formatted error points at the edge to renumber: - -```text -✗ edges.7.id: Duplicate edge id `e20` — `edges[7]` reuses the id already declared by `edges[3]`; every edge id in a flow must be unique. Renumber one of them: … -``` - -**What does NOT change:** `edges[].id` keeps its name, type and describe; the -node vocabulary, the edge `type` enum and every other refusal are untouched; -a flow with unique edge ids (or no edges) parses exactly as before. Node ids -are not covered by this change. - -The shape that is refused, and what the author does about it — a two-edge -excerpt, the later edge renumbered: - -```ts -// before — parsed on green, both edges keyed 'e20' -edges: [ - { id: 'e20', source: 'qualify', target: 'convert' }, - { id: 'e20', source: 'convert', target: 'end' }, -] - -// after — refused at parse (edges.1.id: Duplicate edge id `e20` …); renumber the later one: -edges: [ - { id: 'e20', source: 'qualify', target: 'convert' }, - { id: 'e21', source: 'convert', target: 'end' }, -] -``` - -**Remedy.** Renumber the later edge to an id no other edge in that flow -carries; nothing else in the flow needs to move. The census over this -repository found no flow to migrate, so this is a release note, not a -migration: no shipped example, fixture or seed in `packages/**` or -`examples/**` declares a duplicate edge id, and the pinned objectui tree -carries none in its authored flows. The one known downstream instance was -renumbered before this change (hotcrm PR #1571). diff --git a/.changeset/flow-record-decoupled-from-batch-payload.md b/.changeset/flow-record-decoupled-from-batch-payload.md deleted file mode 100644 index c22cd1cfe5..0000000000 --- a/.changeset/flow-record-decoupled-from-batch-payload.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -"@objectstack/trigger-record-change": patch ---- - -fix(trigger-record-change)!: the record handed to a record-change flow no longer aliases the write's payload (#14744) - - - -**BREAKING** for a flow whose `script` node mutates a NESTED value of the -triggering record IN PLACE: that mutation no longer affects the write the flow -was triggered by. Shipped as `patch` — this change moves no public surface (no -exported symbol, no accepted key or value), and under the maintainer's -2026-09-04 rule (decision batch #35, on #15294) a `fix(` that changes no public -surface stays `patch`, with breaking-ness carried by this banner and the -ADR-0087 disposition rather than by the level. Maintainer ruling 2026-09-04 on -#14744 (decision batch #38, verbatim 「同意」), adopting option A. - -**Why.** `buildContext` builds the flow's `record` as a shallow overlay of the -pre-image, the mutation payload and the after-row. The top-level object was -new, so a flow ASSIGNING a top-level key reached nothing — but every nested -value in it was the engine's own object, shared by reference. One of those is -`ctx.input.data`, and on a `multi: true` update ADR-0058 Addendum II D3 hands -every per-row context that same payload object, which is the SET clause of the -single `updateMany`. A registered function doing `record.tags.push(...)` -therefore wrote the SET clause without assigning any key: every dispatch's -contribution landed on EVERY matched row, including values derived from another -row's pre-image, and #14099's key-set refusal could not see it because no key -was assigned. Measured end to end on the memory driver and on -`@objectstack/driver-sql` (#15356). - -**What changes.** Both flow-facing roots — `record` (and the `params` alias of -it) and `previous` — are decoupled from the engine's state before the flow -runs. Arrays, plain objects, `Date`, `RegExp`, `Map` and `Set` are copied; -primitives, functions and other class instances are shared, which is the -documented and pinned boundary. A flow still mutates its roots freely and still -observes its own writes for the rest of the run; those writes simply reach -nothing outside it. `previous` is decoupled in the same stroke because it is the -engine's single pre-image object and the same hook context reaches every other -flow bound to the same write. - -**What does NOT change.** The engine's write shape. ADR-0058 Addendum II D3 -stands untouched: one payload still serves N rows and every per-row context is -still handed that one object. #14099's key-set refusal is untouched and is not -widened — a hook that assigns the same key with per-row values still passes it, -and divergent key sets are still refused whole. Flow metadata with no registered -function reached nothing before this change and reaches nothing after it: -assignment nodes write the run's variable map, and `update_record` issues its own -by-id write. Lookup expansion (`config.expand`) still grafts onto the record the -flow holds. - -**Consumer note.** A flow that relied on an in-place nested mutation to persist -— which on a by-id write did persist, and on a `multi: true` write corrupted -every other matched row — writes the record with the `update_record` node -instead. That node is the supported per-row write and is unaffected by this -change. diff --git a/.changeset/generated-migration-id-column-shape.md b/.changeset/generated-migration-id-column-shape.md deleted file mode 100644 index 2d557f03d6..0000000000 --- a/.changeset/generated-migration-id-column-shape.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -`os generate migration` gives a table's own `id` column the shape the platform actually creates. - -Both migration generators hardcoded the primary key as a UUID — `"id" UUID PRIMARY KEY DEFAULT gen_random_uuid()` in the SQL format, `table.uuid('id').primary().defaultTo(db.fn.uuid())` in the TypeScript one (the default format). The platform's SQL driver emits `table.string('id').primary()`, which is knex's `varchar(255)`. A platform id is a string, not a uuid, so on Postgres the generated table refused the platform's very first insert with `22P02 invalid input syntax for type uuid`. - -The quieter half is the `DEFAULT`, and it is why this was worth correcting rather than working around. The driver emits no database-side default at all — its insert path always supplies the id itself — so `gen_random_uuid()` never fired for a platform write, only for an out-of-band one, handing that row a 36-character uuid this platform's id generator would never mint. One table would then hold two incompatible id shapes, with nothing said. - -Both generators now emit the driver's own answer: `"id" VARCHAR(255) PRIMARY KEY` and `table.string('id').primary()`. The correction also closes a contradiction inside the generator file, whose prose already stated that a reference column takes the width of the target's `id` column *because* the driver emits `table.string('id').primary()` — a few hundred lines above the two lines that emitted `uuid`. - -`generate-builtin-id-column.pin.test.ts` reads the width from the driver's own `DEFAULT_STRING_VARCHAR_CHARS` rather than transcribing `255`, so the generators cannot drift away from the driver again without a named failure. diff --git a/.changeset/great-clouds-repair.md b/.changeset/great-clouds-repair.md deleted file mode 100644 index 082e4063dd..0000000000 --- a/.changeset/great-clouds-repair.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@objectstack/plugin-hono-server': patch ---- - -`GET /auth/me/localization` answers the deployment's resolved `currency` and `timezone` instead of `null` - -The handler read both off the request `ExecutionContext`, citing ADR-0053, but the resolver serving this surface is a hand-rolled envelope that never carried them — so every authenticated caller was answered `currency: null, timezone: null` whatever the `localization` settings said, and the console's regional-formatting seed was fed nulls. All three values now come from one reading of the same `resolveLocalizationContext` cascade the dispatcher's shared assembler uses. `locale` resolution is unchanged. `timezone` now always answers (cascade floor `UTC`); `currency` still answers `null` when the deployment configures none — that value has no floor. diff --git a/.changeset/hono-me-localization-user-locale.md b/.changeset/hono-me-localization-user-locale.md deleted file mode 100644 index 21d883db39..0000000000 --- a/.changeset/hono-me-localization-user-locale.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -"@objectstack/plugin-hono-server": minor ---- - -feat(hono-server): `GET /auth/me/localization` → `locale` is now the signed-in user's language — `sys_user.locale` when set, then the request's `Accept-Language`, then the deployment default (#14788) - -Maintainer ruling 2026-09-03 (option D on #14788): this endpoint is the ONE -read face for "what language is this user", now that `sys_user.locale` is a -user-stated preference (#13881 / #14787) and the never-produced -`SessionUser.language` is retired from the session contract -(`@objectstack/spec`, same release). - -What changed, for an authenticated caller: - -- `locale` resolves **the user's own `sys_user.locale`** first — read under a - system context by the caller's own id and accepted only when it passes the - column's OWN `locale_bcp47_shape` rule as the registry declares it (the - endpoint evaluates that rule; it carries no second locale parser). A - malformed, blank or unverifiable value falls through, it is never served. -- then **the request's `Accept-Language`** preference (`preferredLocaleFromHeader`, - the same parse REST and the runtime dispatcher feed `execCtx.locale` from); -- then **the deployment default** (`resolveLocalizationContext` — the - `localization.locale` settings cascade, floor `en-US`). - -Before, the resolver behind this endpoint assembled no localization at all, so -`locale` was `null` for every authenticated caller; it is now always a string -for an authenticated caller. The response shape is unchanged -(`{ authenticated, currency, locale, timezone }`), `currency` / `timezone` -are untouched, and the unauthenticated answer (`{ authenticated: false }`) is -unchanged. `resolveSignedInUserLocale` is exported for hosts that compose the -current-user endpoints directly. diff --git a/.changeset/host-importer-aliased-dual-publish-entry.md b/.changeset/host-importer-aliased-dual-publish-entry.md deleted file mode 100644 index 69d7c1c852..0000000000 --- a/.changeset/host-importer-aliased-dual-publish-entry.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/types": patch ---- - -`createHostImporter` now loads the `import` build of an ALIASED dual-published package, instead of silently keeping its `require` build. - -An alias declaration — `{"dependencies": {"foo": "npm:bar@1"}}` — installs a package whose manifest is named `bar` under the key `foo`. On the path where CommonJS resolution SUCCEEDS, the importer re-decides only the CONDITION (it asks the package which entry an `import()` gets, so the caller's ESM chain and this load share one instance). That re-decision recognised the package root by walking up from the resolved entry until it found a manifest named after the DECLARATION KEY — `foo` — while an aliased install's manifest is named `bar`. The walk therefore never matched, the re-decision produced nothing, and the load fell back to whatever the CommonJS resolver had answered: the `require` condition. - -For an aliased dual publish that left the process holding two live copies of one package — the CommonJS build behind the host importer, the `import` build in the caller's own chain — which is exactly the split the condition re-decision exists to remove: a plugin registry, a singleton kernel, a module-level cache, one copy each. - -The expectation now comes from the host's own declaration (`npm:name@range`, aliased `workspace:name@range`), the same reading the ESM-only fallback finder has used since it learned about aliases. Nothing about the check's strictness moves: an alias naming one package still does not license a directory holding another, and a non-aliased declaration is still verified against its key. Declarations that name a LOCATION rather than a package (`link:`, `file:`) carry no name to expect, so they keep today's behaviour unchanged. - -Measured population for the behaviour change: zero aliased declarations exist across this workspace's 875 dependency declarations, and 867 of 867 installed declarations already match their key — no ordinary, non-aliased install reaches this path. diff --git a/.changeset/i18n-declared-fallback-chain-rest.md b/.changeset/i18n-declared-fallback-chain-rest.md deleted file mode 100644 index fb056b1e3e..0000000000 --- a/.changeset/i18n-declared-fallback-chain-rest.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -"@objectstack/rest": patch ---- - -fix(rest): metadata label lookup honours the stack's declared `i18n.fallbackLocale` / `defaultLocale` instead of falling through to the `en` bundle (#14882) - -On a workspace whose labels are authored in `zh-CN` (`defaultLocale: 'zh-CN'`, -`fallbackLocale: 'zh-CN'`) and which ships only a courtesy `en` translation bundle, -`GET /api/v1/meta/object/:name`, the `/meta/:type` list, `GET /api/v1/meta` and the -public-form schema served the ENGLISH bundle labels to a `zh-CN` request (`Entry Sheet` -for an authored `填报单`, `KPI Assessment` for `KPI 考核管理`). The document translators walk -`requested locale → fallback chain → authored label` and default the chain to a literal -`['en']`; every REST seam passed none, so the declared fallback never reached the chain -and `en` was consulted before the authored label. - -Every metadata translation seam now passes `fallbackChain: [i18n.getFallbackLocale()]` — -the locale the i18n service's own `t()` falls back to, which `I18nServicePlugin` receives -from the stack config as `fallbackLocale || defaultLocale || 'en'`. For the workspace -above a `zh-CN` request now resolves `zh-CN → zh-CN → authored label` (the authored -Chinese labels), an `en` request still gets the `en` bundle, and a `zh-CN` bundle, when one -is shipped, still wins over the authored label. - -Feature-detected: an i18n service that does not declare a fallback (the method is -optional on `II18nService`; the core in-memory fallback has none) gets no chain and the -resolver's own default applies exactly as before. A stack declaring `defaultLocale: 'zh-CN'` -with `fallbackLocale: 'en'` is likewise unchanged — the declared `en` is honoured as it -reads. diff --git a/.changeset/i18n-declared-fallback-chain-service.md b/.changeset/i18n-declared-fallback-chain-service.md deleted file mode 100644 index 1979908b5f..0000000000 --- a/.changeset/i18n-declared-fallback-chain-service.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@objectstack/service-i18n": minor ---- - -feat(service-i18n): `FileI18nAdapter.getFallbackLocale()` reports the `fallbackLocale` the adapter was constructed with (#14882) - -Implements the new optional `II18nService.getFallbackLocale()`. `I18nServicePlugin` -already receives `fallbackLocale || defaultLocale || 'en'` from the stack's `i18n` -config on both boot paths (`os serve`, the dev plugin); this makes that declaration -readable, so the REST metadata reads pass the document translators the same fallback -locale `t()` itself consults. Returns `undefined` when no `fallbackLocale` was given. diff --git a/.changeset/i18n-declared-fallback-chain-spec.md b/.changeset/i18n-declared-fallback-chain-spec.md deleted file mode 100644 index a030769f8b..0000000000 --- a/.changeset/i18n-declared-fallback-chain-spec.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -feat(spec): `II18nService.getFallbackLocale()` — the declared fallback locale is readable, so the metadata-document translators can be handed the chain the deployment declared (#14882) - -`ResolveOptions.fallbackChain` on the `@objectstack/spec/system` label -resolvers (`translateMetadataDocument`, `translateObject`, `translateApp`, -`resolveViewLabel`, …) is the ordered list of locales consulted after the -requested one and BEFORE the authored label. Nothing on `II18nService` -exposed the deployment's declared fallback (`i18n.fallbackLocale`, else -`defaultLocale`), so no serving layer could thread it, and every caller fell -to the resolver's literal `['en']` default. A `zh-CN` workspace that shipped a -courtesy `en` bundle therefore served English bundle text to a `zh-CN` -request ahead of its own authored Chinese labels. - -- New optional contract member `II18nService.getFallbackLocale?(): string | undefined` - — the locale the service's own `t()` consults second. `undefined` (or the - method absent) means nothing was declared, and a serving layer must then - leave the resolver's default in place rather than invent a chain. -- The `fallbackChain` documentation now states who supplies it (the serving - layer, from `getFallbackLocale()`) and that the `['en']` default applies - only when a caller declares no chain at all. The resolver's behaviour for - a caller that passes nothing is unchanged. - -Additive: no existing implementation or caller changes shape. diff --git a/.changeset/i18n-walk-one-key-one-demand.md b/.changeset/i18n-walk-one-key-one-demand.md deleted file mode 100644 index 6255ad9acc..0000000000 --- a/.changeset/i18n-walk-one-key-one-demand.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -`os lint` and `os i18n extract` no longer count one translation key twice. - -A translation key is derived from *where a string is addressed*, not from *which declaration was being read* when the walk reached it — and two declarations can address one bundle slot. `collectExpectedEntries` emitted one entry per declaration, so a key reachable twice became two expected entries. Two families were measured, with different causes: - -- **Two carriers, one action.** The normalized config attaches an object's actions to `obj.actions` *and* to the top-level `actions` list — the same object reference, not a copy — so both action branches emitted `objects.OBJECT._actions.ACTION.*`. This is the family the coverage report shows: 70 of 691 baselined units across `app-todo` (40), `app-showcase` (29) and `app-crm` (1). -- **Two declarations, one form field.** `deleteBehavior` is declared twice in each of the `field` and `object` metadata forms, gated on `visibleWhen` (`lookup` vs `master_detail`); both render into one key. Config-independent — it duplicated six entries on every config, including an empty one. - -Neither is an authoring mistake, and neither is fixable where it originates: both are two correct declarations of one displayed string. So the walker now collapses entries that address the same path, keeping the first emission. - -What that corrects, in both directions: - -- **`os lint`'s i18n findings.** The same missing key was reported twice, byte-identically. `pnpm check:i18n-coverage` ratchets the finding *count* while its report calls the number "untranslated declared strings", so translating one key moved the ratchet by two and the frozen debt was ~11% larger than the work it described. The three coverage baselines are regenerated in this change and fall by exactly 70 (691 to 621): `app-crm` 102 to 101, `app-showcase` 443 to 414, `app-todo` 146 to 106. The ratchet's direction, monotonicity and failure text are unchanged — only the population it counts. -- **`os i18n extract`'s reported counts.** `totalExpected` and the per-locale `counts` counted emissions while the skeleton itself had already collapsed the duplicates on the way in, so extract over-reported what it wrote — 1632 claimed against 1531 keys written on `app-showcase`, 894 against 870 on `app-todo`, 930 against 925 on `app-crm`. Those numbers now match the skeleton. - -No generated bundle changes: every duplicate pair measured carries a byte-identical record, so de-duplication removes copies and never a demand. All nine `translations/*.generated.ts` packages stay in sync. diff --git a/.changeset/import-row-unique-violation-rest.md b/.changeset/import-row-unique-violation-rest.md deleted file mode 100644 index ce6d84a8b5..0000000000 --- a/.changeset/import-row-unique-violation-rest.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -"@objectstack/rest": minor ---- - -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) - - - -**BREAKING** on the per-row results of the import runner -(`POST /api/v1/data/:object/import` and the import job): a row refused by the -engine's `DuplicateRecordError` envelope now reports `code: 'UNIQUE_VIOLATION'` -where it reported `'DUPLICATE_RECORD'`. Shipped as `minor` under the repo's -launch-window convention for breaking changes. Maintainer ruling 2026-09-03 on -#14723 (verbatim 「同意,然后执行契约复审」), adopting option A: one wire -spelling for a unique-constraint refusal on every route. - -**Why.** `toFailedResult` relayed the thrown error's own `code`, and the engine's -envelope carries the registered `DUPLICATE_RECORD` — while the whole-request -failure on the same import route answered `UNIQUE_VIOLATION` through -`mapDataError`. Two spellings of one condition on one route, which ADR-0112's -one-name-per-concept and the error-code ledger's header both forbid. The -duplication is removed, not declared: no ledger waiver is added. - -**What changes.** The import row derivation applies the whole-request arm's own -predicate — the registered code AND the class name `DuplicateRecordError`, -exported from `error-response.ts` as `isEngineDuplicateRecordEnvelope` and now -shared by the arm and the row report — and reports `UNIQUE_VIOLATION`. A -field-level finding still takes precedence (the envelope carries none), the -row's sentence is unchanged (the platform sentence, sanitised as before; no -driver text), and a producer that merely throws the registered -`DUPLICATE_RECORD` without being the engine's class keeps its own code. - -**What does NOT change.** The whole-request doors (single-record, bulk, import, -metadata, UI) already answered `UNIQUE_VIOLATION` and keep doing so; the arm's -logic is untouched beyond reading the shared predicate. The engine's thrown -identity stays `DUPLICATE_RECORD` in-process. This package's `error-response.ts` -docblock that disclosed the fork under the #14541 contract review now states -the converged rule. - -**Consumer note.** An import client that branched on a row's `code` reading -`DUPLICATE_RECORD` reads `UNIQUE_VIOLATION` there now — the same value it -already handles for the whole-request 409. Measured in-repo and in the sibling -repos (hotcrm, objectui, non-test sources): zero consumers branch on either -spelling of a row code. diff --git a/.changeset/inert-deadline-keys-retired.md b/.changeset/inert-deadline-keys-retired.md deleted file mode 100644 index decce162bc..0000000000 --- a/.changeset/inert-deadline-keys-retired.md +++ /dev/null @@ -1,140 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -feat(spec): retire the fourteen inert deadline keys of the incident-response, training and change-management schemas (#14477, ADR-0049) - - - -**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep -launch-window convention ships it as `minor`; the migration prescriptions are -registered under protocol major 18, where `os migrate meta` users will look). -Maintainer ruling 2026-09-02 on the census card (ruled A: retire per family): -ADR-0049 enforce-or-remove decides it — declared-but-unenforced deadline -surface with zero measured readers comes off. - -Fourteen hour/minute/day-shaped deadline, SLA and duration key sites — twelve -distinct names, because `durationMinutes` and `estimatedMinutes` each occur at -two sites — sat on the exported incident-response, training and -change-management schemas and in the generated reference docs, and **nothing -read them**: the schemas are exported from `@objectstack/spec/system`, mounted -by no stack key, registered as no metadata type, absent from the 2026-06 -liveness ledgers, and the reader census over every package outside -`packages/spec` (tests and changelogs excluded) and over objectui at the -pinned sha returned zero hits for every key. An author could write -`triageDeadlineHours: 4`, `validityDays: 365` or `regulatorDeadlineHours: 72` -and reasonably expect the platform to escalate, expire or notify — it never -did, and it never said so. Six of the keys carried defaults (30 minutes, -1 hour, 2555 days; 365, 30 and 14 days) that were materialized into every -parsed document without ever being consulted. A compliance-shaped deadline -that fails silently is the worst form of the shape ADR-0049 names. - -**What is refused:** authoring any of the keys below, with any value, on the -base schema and through every carrier that nests it (`Incident.responsePhases[]`, -`IncidentResponsePolicy.notificationMatrix`, `TrainingPlan.courses[]`, -`ChangeRequest.impact` / `.rollbackPlan` / `.implementation`). None of the -schemas is `.strict()`, so each key is a `retiredKey()` tombstone rather than a -bare deletion (a deletion would have stripped it in silence): authoring it is a -`tsc` error (`never`) and a parse error carrying the prescription -(`invalid_type` at the path of the key). - -| schema | retired keys | -|:--|:--| -| `IncidentResponsePhase` | `targetHours` | -| `IncidentNotificationRule` | `withinMinutes`, `regulatorDeadlineHours` | -| `IncidentNotificationMatrix` | `escalationTimeoutMinutes` (default 30) | -| `IncidentResponsePolicy` | `triageDeadlineHours` (default 1), `retentionDays` (default 2555) | -| `TrainingCourse` | `durationMinutes`, `validityDays` | -| `TrainingPlan` | `recertificationIntervalDays` (default 365), `gracePeriodDays` (default 30), `reminderDaysBefore` (default 14) | -| `ChangeImpact` | `downtime.durationMinutes` | -| `RollbackPlan` | `steps[].estimatedMinutes` | -| `ChangeRequest` | `implementation.steps[].estimatedMinutes` | - -**What stays, byte-identical:** every other key of the three families with its -default and its (absent) readers, and every export — no def leaves the public -surface. Parsed documents no longer carry the six former defaults. - -**Held, not touched:** the `ESignatureConfig` pair (`expirationDays`, -`reminderDays` in `data/document.zod.ts`) — the ruling left that branch open -pending the e-signature roadmap answer; it stays on the card. - -## FROM → TO - -```ts -// before — parsed green; no engine ever read a single one of these numbers -const policy: IncidentResponsePolicy = { - notificationMatrix: { - rules: [{ severity: 'critical', channels: ['pagerduty'], recipients: ['security_team'], - withinMinutes: 15, notifyRegulators: true, regulatorDeadlineHours: 72 }], - escalationTimeoutMinutes: 45, - }, - defaultResponseTeam: 'security_team', - triageDeadlineHours: 2, - retentionDays: 3650, -}; -const course: TrainingCourse = { - id: 'COURSE-SEC-001', title: 'Security Fundamentals', description: '…', - category: 'security_awareness', targetRoles: ['all_employees'], - durationMinutes: 60, validityDays: 365, -}; -const rollback: RollbackPlan = { - description: 'Restore from backup', - steps: [{ order: 1, description: 'Restore backup', estimatedMinutes: 15 }], -}; - -// after — delete the keys; there is no replacement because no incident-response, -// training-management or change-management engine exists to keep a deadline. -// Record retention is the object-level `lifecycle` block (ADR-0057), declared on -// the object that stores the records. -const policy: IncidentResponsePolicy = { - notificationMatrix: { - rules: [{ severity: 'critical', channels: ['pagerduty'], recipients: ['security_team'], - notifyRegulators: true }], - }, - defaultResponseTeam: 'security_team', -}; -const course: TrainingCourse = { - id: 'COURSE-SEC-001', title: 'Security Fundamentals', description: '…', - category: 'security_awareness', targetRoles: ['all_employees'], -}; -const rollback: RollbackPlan = { - description: 'Restore from backup', - steps: [{ order: 1, description: 'Restore backup' }], -}; -``` - -One-line fix: delete the key wherever it is authored. There is no -`os migrate meta` edit list for these keys — none of the schemas is a stack -collection member, so the conversion chain has no seam to walk (the -`MetadataPluginConfig.additionalTypes` precedent); the tombstone prescription -and the protocol-18 upgrade guide are the channels. - -The retirement kit: - -- `retiredKey()` tombstones at all fourteen sites (`packages/spec/src/system/ - incident-response.zod.ts`, `training.zod.ts`, `change-management.zod.ts`; - each file's section comment records what the shape was and why no D2 - conversion exists) -- ADR-0087 registration: fourteen `RETIRED_KEYS_BY_MAJOR[18]` entries (the - three nested change-management sites spelled `ChangeImpact:downtime.durationMinutes`, - `RollbackPlan:steps.estimatedMinutes`, `ChangeRequest:implementation.steps.estimatedMinutes`) - and three D3 semantic entries, one per family -- no liveness-ledger row: none of the three families is an enrolled ledger - type, so there is no row to keep or drop -- pin tests (`deadline-keys-retirement.test.ts`): a refusal pin per site - asserting the issue path, code and prescription on the base schema and - through the nesting carriers; the tsc `never` channel; no-materialize pins - for the six former defaults; the ADR-0087 registration; and a tree-scoped - absence pin over every authored source in the repo -- generated baselines and docs follow the schema: `authorable-surface/` gains - eleven `[RETIRED]` rows, `authorable-defaults/` loses six rows, the three - system reference pages are regenerated, and the gitignored `json-schema/` - output is re-emitted on the next build -- `json-schema.manifest/` is unchanged, and correctly so: it ratchets def - *names*, and retiring keys removes no def from the published surface -- `spec-changes.json` and the protocol upgrade guide are unchanged too: both - project the migration chain at the current protocol major (17), so these - protocol-18 registrations reach them at the 18 cut -- zero authored occurrences in this repo's examples, skills and hand-written - docs, and zero hits in objectui at the pinned sha, so no in-repo source - changes ride along beyond the three families' own unit tests diff --git a/.changeset/injected-system-column-labels-localised.md b/.changeset/injected-system-column-labels-localised.md deleted file mode 100644 index 3aff300a37..0000000000 --- a/.changeset/injected-system-column-labels-localised.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -The tenant-scope and owning-business-unit system columns now render a localised display name on the `/meta` read exits, as the other platform-injected columns already did. - -`translateObject` carries a built-in label table for the columns the platform injects onto every eligible object, applied while a column still carries its injected English default, so a `zh-CN` / `ja-JP` / `es-ES` request never sees the English label on a custom object that ships no translation entries of its own. The table covered `owner_id`, `created_at`, `created_by`, `updated_at` and `updated_by` but not the two remaining injected columns, `organization_id` (`Organization`) and `owning_business_unit_id` (`Owning Business Unit`), so those two leaked English on every locale. Both rows are added, with the wording the platform bundles already use for the same columns on platform objects. The identity-stable column definitions are untouched, no new authorable key is introduced, and a label a tenant or author customised is still never overridden. diff --git a/.changeset/install-local-admission-tenancy-posture.md b/.changeset/install-local-admission-tenancy-posture.md deleted file mode 100644 index 80a23c5216..0000000000 --- a/.changeset/install-local-admission-tenancy-posture.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/cloud-connection': patch ---- - -Fix: the marketplace install-local routes now supply the effective tenancy posture to the shared authorization resolver, so both posture-conditional API-key refusals apply at these doors. - -Under a wall-enforcing posture (`isolated`), an API key stamped with an organization its owner has left is refused, as is a key carrying no organization at all. Previously neither guard ran here, because both are conditional on a posture the caller supplies and this seam supplied none — the key's tenant was its own stored `active_organization_id`, never checked against current membership. - -The posture is read from the kernel's `tenancy` service, so it is the posture in force rather than the one requested through `OS_TENANCY_POSTURE`. A deployment that registers no `tenancy` service is unchanged: there is no wall there, and no posture-conditional refusal applies. A `tenancy` service that is registered and fails to build is an outage and answers 503 rather than admitting the caller. diff --git a/.changeset/job-service-replay-force.md b/.changeset/job-service-replay-force.md deleted file mode 100644 index 8d8c5aaf91..0000000000 --- a/.changeset/job-service-replay-force.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -feat(spec): `IJobService.replay` gains an optional third argument, `options?: JobReplayOptions`, carrying `force: true` (#14766 — the contract half of the #14501 A+a2 ruling) - -Additive: the argument is optional, an existing two-argument `replay(name, data?)` implementation keeps compiling and behaving as before, and omitting it is the pre-#14766 call exactly. `JobReplayOptions` is exported from `@objectstack/spec` (`contracts`), with one member, `force?: boolean`. - -**What the contract now declares** (`packages/spec/src/contracts/job-service.ts`, the `replay` TSDoc), for a scheduled (cron) flow whose tick window takes a `(flow, tick-window)` dispatch claim in `sys_flow_dispatch`: - -- `replay(name, data)` on a window whose claim is **absent or failed** re-runs the window — unchanged behaviour, and every job that never takes a claim is this row; -- `replay(name, data)` on a window whose claim **succeeded** is **refused loudly**: the promise rejects with an ADR-0112 envelope — `code: 'RESOURCE_CONFLICT'` (the standard-catalog member HTTP 409 derives; no new extension code) and `status: 409` — whose message names the window asked for and the claim that refused it. Never a silent no-op; -- `replay(name, data, { force: true })` sends anyway; the duplicate is the operator's, taken knowingly. - -**Declared here, enforced by #14501.** This release changes the contract text and the signature only. The refusal semantics are implemented by the services half (#14501: the `(flow, tick-window)` claim through `sys_flow_dispatch`, and `DbJobAdapter.replay` reading it); until that lands, shipped adapters still accept the third argument and ignore it, re-running the window as before. A third-party `IJobService` implementation that already declares `replay` needs no change to keep compiling; one that wants the once-only guarantee implements the table above. diff --git a/.changeset/lint-eval-throwing-generator-unscorable.md b/.changeset/lint-eval-throwing-generator-unscorable.md deleted file mode 100644 index 83a6bdfb3a..0000000000 --- a/.changeset/lint-eval-throwing-generator-unscorable.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -`os lint --eval` no longer scores a failed generation as a perfect one: a generator that throws now counts 0 toward `meanScore` instead of 100. - -The harness has always handled a throwing `--generator` by substituting an empty stack and scoring that. An empty stack is **100 / grade `A` / `valid: true`** — it has nothing wrong with it because it has nothing in it. So a live eval in which every single generation failed reported the best possible headline number: - -``` -os lint --eval --json --generator ./throws.mjs -exit 1 · ok: false · passed: 0 · failed: 5 · meanScore: 100 -every case: score 100 · grade A · valid true · generationError "model unavailable" -``` - -`meanScore` is the first number a human scanning that report reads, and it read perfect precisely when the model under test produced nothing. - -**What was NOT wrong: `passed`.** It carries its own guard (`!generationError && …`), so the failed cases were reported as failed and `ok` was `false` throughout. A reader who cross-read `ok`/`passed` was safe; a reader who checked the mean and moved on got exactly the wrong impression. That is the whole defect, and nothing about `passed`, `ok`, `total`, `failed` or the exit code changes here. - -The repair is the verdict the sibling failure path already used. A generator that *returns* a value nobody can walk was already scored `0 / F / valid: false`, with the reason written into the module: a stack that cannot be walked is not an empty stack, and `valid: true` for one that was never parsed is simply false. A stack that was never produced is not an empty stack either — so both now answer the same: - -```json -{ "id": "invoice_with_line_items", - "generationError": "model unavailable", - "passed": false, - "score": { "score": 0, "grade": "F", "valid": false } } -``` - -and the run above now reports `meanScore: 0`. - -`meanScore`'s denominator is unchanged and is now stated in the payload's own documentation: the mean is over every case **attempted**, so a failed case contributes its 0 and is counted. The alternative — averaging only over cases that could be scored — is a different metric that would report the quality of the generations that arrived while staying silent about how many never did; a `meanScore` that switched denominators without saying so would be a worse defect than the one being fixed. - -No key is added to or removed from the `--json` payload, and nothing a generator can return is newly accepted or rejected: an off-shape stack is still a **scored** case whose schema errors are why it fails, never a generation error. diff --git a/.changeset/lint-eval-unscorable-stack-json-face.md b/.changeset/lint-eval-unscorable-stack-json-face.md deleted file mode 100644 index d1b95ca0f5..0000000000 --- a/.changeset/lint-eval-unscorable-stack-json-face.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -`os lint --eval --json` reports an unscorable generated stack as a failed case instead of crashing with no JSON at all. - -The eval harness promised totality in writing — *"Never throws — generation failures become failed cases"* — and the promise was false as written. Its `try` wrapped only the call to your `--generator` module; the `scoreMetadata(stack)` call that follows sat outside it. So a generator that **threw** became a failed case, exactly as documented, while a generator that **returned** a value nobody could walk took the whole process down: - -``` -os lint --eval --json --generator ./g.mjs -exit 1 · stdout 0 bytes · stderr " Error: poison getter" -``` - -A caller that asked for `--json` got the framework's human error text on stderr and no document at all to parse. Eval mode dispatches above the project-lint `try`, so the catch-all JSON exit that mode has could never see it either. - -Scoring a stack means walking it, and there are two walks: the normalizer spreads the stack's top level, and the schema parse walks everything below it. A throw from **either** now becomes that case's `generationError` — the same per-case channel a throwing generator already used — so the report exit that was always there emits its JSON, names the cause, and still exits non-zero: - -```json -{ "id": "invoice_with_line_items", - "generationError": "Failed to score the generated stack: poison getter", - "passed": false, - "score": { "score": 0, "grade": "F", "valid": false } } -``` - -Nothing new appears on the `--json` face: no new key, no new payload shape. The failing exit was already reachable for a throwing generator; it is now reachable for a poisonous one too. - -The failed case is scored `0 / F / valid: false` rather than as an empty stack. An empty stack scores 100 / A / valid, and stamping that on a stack nobody could parse would have put a clean-looking verdict next to a failure — the crash replaced by a quiet wrong answer. - -Unchanged: offline mode, and every off-shape stack a generator can return. Bad metadata is still **scored**, with its schema errors as the reason it fails — it is not rerouted into the failure channel. diff --git a/.changeset/lint-non-record-collection-entry.md b/.changeset/lint-non-record-collection-entry.md deleted file mode 100644 index 57da994b25..0000000000 --- a/.changeset/lint-non-record-collection-entry.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@objectstack/lint": patch ---- - -No authoring rule throws on a non-record entry of any stack collection. - -A collection is authored either as a list or as a name-keyed map, so every rule that reads one coerces `unknown` into an array of records first. That coercion had been hand-copied into 39 modules, and 23 of the copies spelled the array branch as an unchecked cast — every member was asserted to be a record. A YAML list item left empty deserialises to `null`, so a single stray `-` under `flows:`, `pages:`, `dashboards:`, `datasets:`, `apps:`, `permissions:`, `capabilities:`, `data:`, `hooks:`, `views:`, `actions:`, `translations:` (or a per-object `fields:` / `actions:` / `views:`) reached a property read on `null` and threw a stack trace out of `os lint` / `os validate` instead of reporting a finding. The rules are pure `(stack) => Finding[]` running on the raw path, so nothing upstream had judged the entry's shape. - -Twenty-two of those readers now read through the shared, guarded `recordsOf`, which drops a non-record member of the array shape whole and keeps the author's key on the map shape. Nothing else about what the rules judge changes: a valid entry standing beside a junk one is still read, and still draws exactly the findings it drew before. - -The remaining copies are pinned by a new source-text test in the package, so the predicate cannot be pasted back in: it asserts that `recordsOf` is the only collection coercion, that every module still holding a private one is named in a dated ledger that is exact in both directions, and that no coercion outside a dated single-file allowance casts its array branch unchecked. diff --git a/.changeset/lint-non-record-objects-readers.md b/.changeset/lint-non-record-objects-readers.md deleted file mode 100644 index 81d217ab75..0000000000 --- a/.changeset/lint-non-record-objects-readers.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -"@objectstack/lint": patch ---- - -fix(lint): every `stack.objects` reader skips a non-record entry, so no authoring rule throws on the publish door - -A `null` member of `stack.objects` — what an empty YAML list item -deserialises to, and what a partial editor write leaves behind — crashed -13 of the 42 `AUTHORING_RULES` with -`TypeError: Cannot read properties of null (reading 'name')`. The -authoring rules are pure `(stack) => Finding[]` (ADR-0019) and run on the -RAW `lint` path as well as the parsed one, so nothing upstream had judged -the entry's shape. At the runtime publish gate they are called inside the -gate rather than behind a try/catch of their own, so the throw was an -exception on a WRITE path, not a skipped finding; on the CLI, `os lint` / -`os validate` / `os compile` died on the first one instead of reporting -the stack. - -The repair before this one guarded ONE seam — the object-graph index every -field-path rule opens with. The crash stood at fourteen more readers of -the same collection, each a hand-copied `asArray` whose array branch was -an unchecked `v as AnyRec[]`. Copies are why: the defensive spelling was -already present in about a dozen siblings and absent in the rest, so -fixing one left the others answering the old way. - -So the copies are gone. `recordsOf` — the guarded reader, exported from -`object-graph.ts` and package-private — is now the one coercion from a -collection authored as an array OR as a name-keyed map into the records it -holds, and fifteen files call it: - -- `validate-expressions.ts`, `validate-list-view-mode.ts`, - `validate-widget-bindings.ts`, `filter-walk.ts`, - `validate-object-references.ts`, `validate-record-title.ts`, - `validate-form-layout.ts`, `lint-autonumber-formats.ts`, - `lint-view-refs.ts`, `validate-org-axis-red-lines.ts`, - `validate-sharing-rule-enforceability.ts` — the eleven sites that threw. -- `validate-searchable-fields.ts`'s `indexObjectSearchTargets` and - `validate-page-field-bindings.ts`'s `indexObjectFields` — two shared - indexers inside the reference-integrity suite, each in front of two - rules and both hidden behind whichever suite member threw first. -- `object-field-groups.ts`'s `indexObjectFieldGroups`, which the - re-measure surfaced only once the eleven above stopped throwing. -- `validate-security-posture.ts`, the one that never threw: an `[]` - member passed its `typeof v === 'object'` read and drew a second - `security-owd-unset` at `object "(object 0)"` — an `error` about an - entry no author wrote. - -The verdict is a SKIP, not a finding, matching the seam it extends: a junk -`objects` member is a SHAPE defect and belongs to the schema, every rule -already re-answers the question in its own per-object guard, and reporting -it at the reader would emit one finding per member for one bad entry. On -the name-keyed map shape a member whose VALUE is unreadable keeps its key -(`{ name }`) — the author named it, only its body is illegible. - -No rule tier, id, message or accept-set changes. A valid object standing -beside a junk one is judged exactly as it is judged alone; only a path -index moves, and only for the rules that index `objects` raw, where -`objects[1]` is the honest position. diff --git a/.changeset/list-view-grouping-server-side-contract.md b/.changeset/list-view-grouping-server-side-contract.md deleted file mode 100644 index 109fe84521..0000000000 --- a/.changeset/list-view-grouping-server-side-contract.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -feat(spec): list-view grouping is server-side — the group header query and the per-group row page compile from the view (#14556) - -Maintainer ruling A on objectui#7189 (2026-09-02): grouping on a list view is -server-side. The set of groups and every number in a group header — the count -and any per-group aggregation — are properties of the query, not of the fetched -page; rows inside a group are paged. Grouping one fetched window (the interim -behaviour) rendered two headers (86, 14) or five (31/31/30/7/1) for the same -186 rows in five units depending on row order, and left the rows past the -first window unreachable. - -The contract reuses the query shapes the platform already has — no new query -shape, no new engine verb, no new envelope: - -1. **The group keys and every header number are ONE aggregate query** - (`EngineAggregateOptions`, executed by `IDataEngine.aggregate`): `groupBy` - is `grouping.fields[].field` in nesting order (a multi-level grouping is a - multi-column `groupBy`), `aggregations` is a `count` node (the group's total - row count, alias `count`) plus the view's declared column summaries mapped - onto `AggregationFunction` — the one aggregation vocabulary datasets already - use — and `where` is the view's composed filter. -2. **The rows inside a group are the existing paged `find`** - (`EngineQueryOptions`) with the group's key predicate AND-ed into the view - filter, `limit` / `offset` per group. - -New on the `ui` entry, `view-grouping-query.ts`: - -- `compileListViewGroupQuery(view, { where?, depth? })` → the header query; - `compileListViewGroupRowsQuery(view, groupKey, { where?, limit?, offset?, orderBy?, fields? })` - → the row page; `listViewGroupKeyPredicate` (the empty group is spelled with - the `$null` predicate — the spelling the view filter dialect's `is_empty` - lowers to). -- `COLUMN_SUMMARY_AGGREGATION` — the `ColumnSummary` → aggregation table, - exhaustive by type: `count` → a fieldless `count` (`COUNT(*)`), - `count_unique` → `count_distinct`, `sum` / `avg` / `min` / `max` → the same - name, `none` → nothing; `count_filled` / `count_empty` / `percent_filled` / - `percent_empty` map by derivation — one `{ function: 'count', field }` node - (`COUNT(field)`, the non-null count, header column `count_`), from - which `deriveColumnSummary(row, summary, field)` computes all four on the - header row (`count_filled` = `count_`, `count_empty` = `count − - count_`, `percent_filled` = `count_ / count`, 0 when the count - is 0, `percent_empty` = `1 − percent_filled`). Server-side "empty" is `null` - on every face; the footer's client-side reading of `''` / `[]` as empty is - the renderer's to converge. A future member with no counterpart is refused - loudly at compile time (`ListViewGroupQueryError`, `NOT_IMPLEMENTED` / 501, - the summary's path — `UNMAPPED_COLUMN_SUMMARIES`, empty today); a value that - is no member at all is `INVALID_QUERY` / 400. -- Result-column naming on a header row: each grouped field under its own name - (raw stored value, `null` for the empty group; group keys are scalar), `count`, - and each summary under `_` (`columnSummaryAlias`). - -`GroupingConfigSchema` / `GroupingFieldSchema` / `ColumnSummarySchema` now say -this in their docs, with the shape's recorded limits (a date grouping field -groups per distinct stored instant; header cardinality is unbounded). Nothing -changes in what parses: no key is added, removed or re-shaped. `minor` because -a new exported helper and a declared contract semantics ship; not breaking — -the page-scoped behaviour was never declared. Both queries ride the existing -`POST /data/:object/query` door (`protocol.findData` → `engine.aggregate`, -answering `{ object, records, total, hasMore }`); the grid consuming the header -rows is objectui#7189. diff --git a/.changeset/map-node-progress-state-lifetime.md b/.changeset/map-node-progress-state-lifetime.md deleted file mode 100644 index 6f841aa239..0000000000 --- a/.changeset/map-node-progress-state-lifetime.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/service-automation": patch ---- - -A `map` node inside a `loop` body now runs its collection on every iteration, not just the first. - -`map` tracks its progress through the collection in the flow variable `.$mapState`, and wrote it into the flow's **shared** variable scope without ever removing it. A `loop` body region runs in that same scope by construction — that is what makes the iterator variable and the body's mutations visible to the rest of the flow — so the state written by iteration 1 was still there when iteration 2 entered the map. It read back `started === collection.length`, correctly concluded there was nothing left to start, and returned. - -The result was silent partial work reported as success: measured on the engine, **5 iterations x 2 items produced 2 child runs instead of 10**, the map step reported `success` on all five iterations, and the run finished `completed`. Nothing threw and nothing was caught, so `FlowRunSummary.failed` — the run-level counter that exists to expose contained failures — reported `failed = 0` over it. An operator reading that counter was told the run was clean while it had done a fifth of its work. - -The fix is a lifetime correction, not a new key: `$mapState` is now removed once the collection is exhausted, so its lifetime is one execution of the collection rather than the enclosing scope's. - -**The durable-pause path is deliberately unchanged.** A `map` whose per-item subflow pauses still writes its progress before suspending, and still reads it back when the engine re-enters the node — that write is the mechanism resume depends on, because a resume rebuilds the variable scope from the snapshot taken at the suspend and so can never see any later write. Only the node's terminal path clears the key. A `map` resumed mid-collection continues where it left off, exactly as before, and no item is re-run. diff --git a/.changeset/mcp-stdio-tenancy-posture.md b/.changeset/mcp-stdio-tenancy-posture.md deleted file mode 100644 index 8f4288c510..0000000000 --- a/.changeset/mcp-stdio-tenancy-posture.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@objectstack/mcp": patch ---- - -The MCP stdio transport now vets an API key's organization against the deployment's tenancy posture, instead of trusting the key's own stored claim. - -`resolveStdioExecutionContext` — the whole of this transport's authorization, since every caller on it is an API key by construction and there is no session path — built its own header map and called `resolveAuthzContext` with no `tenancyPosture`. Both posture-conditional API-key refusals are gated on the caller supplying one (`organization_required` at admission, `organization_membership_ended` after grants), so a door that supplied none ran neither: the key's `sys_api_key.active_organization_id`, never re-checked against current membership, became the request's tenant. Under a wall-enforcing posture a key stamped with an organization its owner had left read and wrote that organization's rows through this door. - -The posture is now derived in the plugin's `start()`, where the kernel is reachable, and threaded into the resolver. What changes for a deployment: - -- Under `isolated` or `group`, a stdio transport configured with a key whose owner is no longer a member of the organization the key names refuses to start, and a key already live is refused on its next call. Under `isolated`, an organization-less key is refused the same way. Both refusals are logged server-side naming the key, principal, organization and reason; nothing about them reaches the caller. -- A kernel that registers no `tenancy` service is unaffected: no organization wall exists there, so no posture-conditional refusal is made. That is the supported composition, not a degraded one. -- A `tenancy` service that is registered and **fails to build** now raises `SERVICE_UNAVAILABLE` (503) rather than reading as "no posture". A posture that could not be read is not a posture that is absent, and admitting on one is the permissive-on-failure shape this repair exists to avoid. - -The posture is re-read per call, on the same schedule as the identity beside it (ADR-0101 D1), so a wall that comes up or a membership that ends mid-session takes effect on the next call rather than at the next restart. diff --git a/.changeset/metadata-view-container-leaf-subpath.md b/.changeset/metadata-view-container-leaf-subpath.md deleted file mode 100644 index ca89281928..0000000000 --- a/.changeset/metadata-view-container-leaf-subpath.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -'@objectstack/metadata': minor -'@objectstack/objectql': patch ---- - -feat(metadata): `deriveViewContainerObject` gets a leaf `/view-container` subpath, so objectql's lean ADR-0076 entry stops loading the manager, chokidar, glob and js-yaml for a six-line pure function - -`packages/objectql/src/engine.ts` reached `deriveViewContainerObject` through -`@objectstack/metadata`'s ROOT entry. `core.ts` — the ADR-0076 lean entry — -re-exports `engine.ts`, so `@objectstack/objectql/core`'s module-init closure -inherited the whole root entry: `MetadataPlugin` -> `NodeMetadataManager` -> -`chokidar`, plus `glob`, `js-yaml` and `readdirp`. - -The same file already carried the answer 79 lines above, at its -`@objectstack/metadata/errors` import: that leaf subpath exists "precisely so a -cross-package consumer gets the predicate without the manager, the loaders or -the YAML/filesystem machinery behind the root entry". This is that pattern, -taken a second time. - -**Measured on the built artifacts, not asserted** — every module Node actually -evaluates when `@objectstack/objectql/core` is loaded in a fresh process, -recorded through a `module.registerHooks` load hook (ESM and CJS) plus -`require.cache`, byte sizes from `statSync`: - -| `@objectstack/objectql/core` | modules | bytes | -|:---|---:|---:| -| before (ESM `dist/core.mjs`) | 190 | 12,348,424 | -| after (ESM `dist/core.mjs`) | 185 | 11,849,808 | -| **delta** | **-5** | **-498,616 (-486.9 KiB)** | -| before (CJS `dist/core.js`) | 188 | 12,654,238 | -| after (CJS `dist/core.js`) | 183 | 12,141,034 | -| **delta** | **-5** | **-513,204 (-501.2 KiB)** | - -Six modules stop loading — `packages/metadata/dist/index.js` (237,747 B), -`js-yaml` (114,610 B), `glob` (82,749 B), `chokidar` (2 files, 54,220 B) and -`readdirp` (9,836 B) — and one 469-byte module takes their place. Marginal -module-init time for that root entry, measured on a warm lean closure, was -~22 ms (median of 7; 20.4-27.5 ms) out of ~630 ms. - -⚠️ The figure the finding was argued on — "~3.6 KB to ~450 KB" — is right about -the delta and wrong about the baseline: the lean entry's closure was already -~11.5 MiB before this import existed, dominated by `@objectstack/spec` -(9,587,914 B) and `zod` (567,918 B), neither of which the metadata root entry -contributes. What the root import cost was ~487 KiB *on top of* that, not a -closure of 450 KB. - -The derivation itself moves to `packages/metadata/src/view-container.ts`, a -module with **no imports at all**, and `view-container-expansion.ts` imports -and re-exports it, so `index.ts`'s root export and `plugin.ts` keep their -spelling and the symbol stays on the root entry — this subpath is an additional -door, not a relocation. A re-export shim onto `view-container-expansion.ts` was -tried first and rejected on measurement: esbuild tree-shakes the unused -`expandRuntimeViewContainer` but keeps its two `@objectstack/spec` import -statements, so that shim's own closure was 84 modules / 3,035 KiB. The real -leaf's is 1 module / 469 B. - -`expandRuntimeViewContainer` is deliberately not exported from the new subpath: -`metadata-manager.ts` is its only caller, the root entry does not export it -either, and it is the half that carries the spec machinery. diff --git a/.changeset/object-door-searchable-listview-refusal.md b/.changeset/object-door-searchable-listview-refusal.md deleted file mode 100644 index 3dd84b8d08..0000000000 --- a/.changeset/object-door-searchable-listview-refusal.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -"@objectstack/lint": minor -"@objectstack/metadata-protocol": minor ---- - -The object publish door now refuses an object whose `searchableFields` entry, or whose built-in list view's `columns` (and every other field-naming position on that list view), names a field the object does not have. - -`#15254` closed this one key over: it crossed the reference-integrity suite onto the object write door for the object's own field-name **lists** (`highlightFields`, `publicSharing.redactFields`). The two members that read the *other* field surfaces an object carries — its ADR-0061 search set and its built-in `listViews` — still declared `runtimeTypes: ['flow', 'view']`, so on the only door a Studio, REST `/meta` or MCP author has they never judged the snapshot that arrived. An object could publish clean with `searchableFields: ['gone_field']` or a list-view column resolving to nothing, and both fail the same silent way downstream: the engine filters a stale search entry out without a word (`resolveSearchFields`), so `$search` scans a narrower set than declared — or, once every entry is stale, the auto-default set the author never chose — and a dangling column renders one field short. - -- **`validateSearchableFields` and `validateListViewFieldRefs` gain `object`** in their suite-member `runtimeTypes`. No new rule and no new finding class: the rule ids (`searchable-field-unknown`, `searchable-field-unsearchable`, `list-view-field-unknown`, `list-view-field-dotted`) and their severities are unchanged — they now reach the door where the author actually is. -- **The crossing carries the #9313 precondition.** Both members resolve only against `stack.objects`, the one collection every per-write snapshot carries, so neither opens a missing-collection false-positive channel; their `views[]` rungs simply find no `stack.views` on an object snapshot. -- **Measured before crossing**, at the door's own snapshot shape and differential, over every shipped object definition in the monorepo: 116 objects (platform-objects 48, showcase 24, plugins 19, services 12, crm 6, metadata-core 5, todo 1, qa 1), 105 built-in list views on 40 objects, 666 list-view field-naming positions and 5 `searchableFields` entries judged — **0 findings for both members, precision 1.0**, against synthetic probes that are refused. -- **`validateSortableFields`, the third sibling, is deliberately not crossed** — it measured equally clean, but that crossing is its own adjudication. - -## Migration - -**A publish that used to succeed can now be refused (HTTP 422, `INVALID_METADATA`).** The receipt names the rule id and the offending path, name-keyed on the wire — for example `objects.proj_task.searchableFields[1]` or `objects.proj_task.listViews.all.columns[1]` — plus the string that was written and the fields the object actually has. - -To fix a refusal, do one of: - -- rewrite the entry to the field's current API name (after a Studio label edit the derived name is the one to use — `field_10` becomes `health_score`); or -- drop the entry from the declaration; or, for `searchable-field-unsearchable`, target a text-like stored column instead of a virtual or non-scannable one. - -`os validate` / `os build` / `os lint` already reported these findings at the same severity, so a code-authored stack can be repaired before it reaches a publish. Objects that name a platform-injected system column are unaffected — both members resolve those per object and stay silent where the platform really provisions them. diff --git a/.changeset/object-graph-null-entry-guard.md b/.changeset/object-graph-null-entry-guard.md deleted file mode 100644 index 851c7d6b73..0000000000 --- a/.changeset/object-graph-null-entry-guard.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -'@objectstack/lint': patch -'@objectstack/metadata-protocol': patch ---- - -A junk entry in `stack.objects` no longer crashes the reference-integrity rules, and a probe rule that throws is reported instead of read as "nothing wrong". - -`indexObjectGraph` is the first statement of every rule that resolves a field path, and it read each `stack.objects` member without checking it was a record — so a `null` entry (an empty YAML list item, a partial editor write) threw `TypeError: Cannot read properties of null (reading 'name')` before any rule's own per-object guard could run. Because these rules also run inside the runtime publish gate, that was an exception on a write path rather than a missed finding. The seam now drops non-record entries — silently, matching every sibling collection reader in the package — and the valid objects beside them are judged exactly as before. - -On the publish receipt, `runBuildProbes`' object plane wrapped its rule call in a catch that produced an empty finding list, so a crashed rule was indistinguishable from a clean object while `checked.objects` had already counted it. A rule that throws now surfaces as a `runtime`-layer `object_field_ref_rule_failed` error carrying the thrown message, so an unverified object never reads as a verified one. Probes still never fail the publish they verify. diff --git a/.changeset/objectql-boot-loop-refuses-divergent-view-name.md b/.changeset/objectql-boot-loop-refuses-divergent-view-name.md deleted file mode 100644 index 6b07355ea3..0000000000 --- a/.changeset/objectql-boot-loop-refuses-divergent-view-name.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -"@objectstack/objectql": minor ---- - -fix(objectql): the boot loop refuses a view container whose `name` disagrees with the object it binds to, instead of silently rewriting the author's field (#14666) - -**BREAKING** accept-set narrowing on the ObjectQL boot loop's SOURCE registrar -(`registerMetadataCollections`), shipped as `minor` under the repo's -launch-window convention for breaking changes. Ruled on #14666 (2026-09-03, -direction 2). - -An aggregated `defineView` container is keyed by the OBJECT it binds to, not -by its own row identity, and `ViewSchema` declares an optional `name` whose -own description says that for an object-scoped container it *is* the object -name. Nothing enforced that. A container written as -`{ name: 'lead_views', object: 'crm_lead', list: { ... } }` therefore reached -the two SOURCE registrars and got opposite answers: this boot loop overwrote -`name` with the derived key `crm_lead` and registered it, discarding the -author's field with no diagnostic, while the artifact/HMR loader -(`MetadataPlugin._parseAndRegisterArtifact`) refused the whole artifact load -through `assertMetadataRegisterContract` (#7378 row 1, `VALIDATION_ERROR` / -400). Same document, and whether it loaded at all depended on how the package -was loaded. - -The boot loop now **refuses loudly**, with the same `VALIDATION_ERROR` / 400 -envelope the artifact door raises, naming the container's own `name`, the -object key it derived, and both remedies: drop `name`, or set it to that -derived key. #7378 row 1 already ruled that resolving such a disagreement -silently, in either direction, files the item under a key the caller never -wrote, so the two registrars converge on the refusal rather than on the -rewrite; the artifact door is unchanged. - -**Refused shape**, precisely: an aggregated view container in a stack `views:` -collection that carries a non-empty top-level `name` AND derives a different -object key from its own `object` (or, failing that, `list.data.object` / -`form.data.object`). - -Scope, which the ruling names as this change's main risk. A container with no -`name` is untouched, and still registers under its derived key. So is a -container whose `name` already equals that key, and one that declares no -binding anywhere else, since the derivation then falls back to that same -`name` and cannot disagree with itself. No other metadata kind changes -behaviour: the refusal is gated inside the `views` branch of the generic -registration loop. Standalone ViewItems and flattened overlays travelling in -the assembled `viewItems:` channel are untouched, because a container cannot -reach that channel at all. Every one of these has a control test. - - diff --git a/.changeset/os-create-emits-an-installable-project.md b/.changeset/os-create-emits-an-installable-project.md deleted file mode 100644 index f4da992bd5..0000000000 --- a/.changeset/os-create-emits-an-installable-project.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -"@objectstack/cli": minor ---- - -`os create` now emits a project that installs outside this monorepo. - -Every project the command scaffolded declared its `@objectstack/*` dependencies -with pnpm's `workspace:*` protocol, extended a `tsconfig.json` two directories -above itself, and was written into this repository's own `packages/plugins/` or -`examples/` by default — so a developer following the documented command got a -project `pnpm install` refuses. The default emission is now standalone: - -- `@objectstack/*` dependencies are published semver ranges pinned to the - version of the CLI that generated them; -- the emitted `tsconfig.json` is self-contained and extends nothing; -- the project is written to `./` in the current directory (or `--dir`); -- a `pnpm-workspace.yaml` carries the build approvals a fresh `pnpm install` - needs on pnpm 11. - -The `plugin` template also emits `init` where it used to emit `initialize`. -`initialize` is not part of the `Plugin` contract, so the scaffold did not -type-check under its own `strict` config (TS7006 on the untyped `context` -parameter) and `kernel.use()` refused the plugin at load with -`Plugin init function is required` — a defect the kernel protocol docs -previously carried a warning about instead of a fix. - -The previous monorepo-internal placement is still available for ObjectStack -platform work as the explicit `--in-repo` flag, which keeps the `workspace:*` -specs and writes into `packages/plugins/` or `examples/`. diff --git a/.changeset/plain-unique-index-duplicate-preflight.md b/.changeset/plain-unique-index-duplicate-preflight.md deleted file mode 100644 index 174193bede..0000000000 --- a/.changeset/plain-unique-index-duplicate-preflight.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@objectstack/driver-sql": patch ---- - -A plain unique index over existing duplicate rows no longer kills the boot with the database's raw error, and `os migrate plan` no longer calls that op `safe`. - -Declaring a column unique over a table that already holds duplicates had two very different outcomes depending on one branch in the SQL driver, and only one of them was survivable. - -- **An organization-scoped unique** (the `unique: 'organization'` default, materialised as the NULL-safe `COALESCE(organization_id, '__global__')` composite) kept the boot up: the driver logged at `error` naming the index, the constraint that is not enforced and the remedy, and the ADR-0120 D4 duplicate pre-flight reported the blocked `create_index` as `category: 'destructive'` / `severity: 'error'` with the conflicting key groups and their row counts. -- **A plain unique** — no organization key part at all, reached by an object with `tenancy: { enabled: false }` or by any explicit `unique: 'global'` — took the process down: `initObjects` threw the database's own error, which names the index and the column and no rows and no remedy, nothing reached the durability channel, and `detectManagedDrift` (what `os migrate plan` reports) classified the very same op `category: 'safe'`, `severity: 'warning'`, so `os migrate apply` and dev `autoMigrate: 'safe'` walked straight into the raw failure. - -The plain path now reaches the same posture as the scoped one: - -- **The boot survives and says what is not enforced.** `syncDeclaredIndexes` absorbs a uniqueness violation on a plain unique index the way it already absorbed one on the NULL-safe composite: the failure is logged on the durability channel (`error`) naming the index, the conflicting key groups with their row counts, the constraint that is NOT enforced, and `os migrate plan` as the way out. A non-unique index and any failure that is not a uniqueness violation still surface as before. -- **The duplicate pre-flight covers it.** The ADR-0120 D4 probe no longer skips ops whose NULL-safe column set is empty, so a plain unique `create_index` over dirty data is reported `destructive` / `error` with the same row report instead of `safe`. Nothing new probes it: the existing probe already groups by the bare columns when there is no NULL-safe key part, so both key shapes share one pre-flight rather than a second copy that can drift from the first. - -Consumers of the classification see the op move from the "Safe" group to "Destructive (requires --allow-destructive)" in `os migrate plan` and `os diff`; `os migrate apply` defers it instead of attempting it; the artifact boot gate refuses with a named destructive-drift refusal instead of crashing; and dev `autoMigrate: 'safe'` leaves it alone. Clean data is unaffected — the probe finds nothing and the index is created exactly as before. diff --git a/.changeset/platform-object-tenancy-census-derived.md b/.changeset/platform-object-tenancy-census-derived.md deleted file mode 100644 index 7855601bcb..0000000000 --- a/.changeset/platform-object-tenancy-census-derived.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/objectql": patch ---- - -The platform-object tenancy census is derived and gated instead of hand-written in a comment. Documentation only — no runtime behaviour changes. - -`PLATFORM_OBJECT_TENANCY`'s header explained why the reclassification needs a ledger rather than a schema read, and backed the argument with three hand-written digits and a parenthetical attributing them. Nothing re-derived any of it, so it was true only until the population moved and failed silently when it did — in both of the directions a prose count can. - -The parenthetical mis-attributed the exclusion: it named `sys_sso_provider`'s `tenancy.enabled: false` as an addition to the `managedBy: 'better-auth'` set that object was already in, and left `sys_api_key`'s identical opt-out unnamed. The arithmetic stayed right, which is why no reader and no gate caught it — a wrong reason producing a right total is the shape that survives longest. The digits then went stale when an object opted out of the tenant column through a third mechanism the parenthetical's taxonomy had no slot for (`systemFields: { tenant: false }`), while the gated page next door was updated in the same commit. - -The digits and the parenthetical are deleted rather than corrected. The header now points at `scripts/platform-object-tenancy-census.json` and states the PREDICATE it was missing: an object is inside the machinery when `resolveTenantFieldName` answers non-null on the **registered** schema — after `applySystemFields` has injected the tenant column, because the injected column is what the engine sees, not what the author typed. Counting `managedBy` as if the resolver read it is the mistake that produced the wrong reason. - -The artefact is derived by `scripts/platform-object-tenancy-census.mjs`, which loads `resolveTenantFieldName` and `resolveInjectedSystemColumns` from source and executes them rather than re-spelling what they decide, and is held to the tree by `scripts/check-platform-object-tenancy-census.mjs`. It records per object the declaration on that object's own schema that puts it outside the reach; declarations are not mutually exclusive and an object carrying two keeps both. An excluded object with no declared mechanism is an error, not a default: the generator refuses to commit the row and the gate reds, so a new exclusion mechanism is adjudicated rather than absorbed into an existing total. diff --git a/.changeset/plugin-auth-find-envelope-limbs.md b/.changeset/plugin-auth-find-envelope-limbs.md deleted file mode 100644 index 2eeba4b964..0000000000 --- a/.changeset/plugin-auth-find-envelope-limbs.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"@objectstack/plugin-auth": patch ---- - -A self-registration grant is refused, not silently redirected, when a permission-set row is malformed — and the fourteen dead `{ records }` / `{ data }` normalizer limbs behind that code are gone. - -`plugin-auth` carried fourteen array-or-envelope normalizer blocks of the shape `Array.isArray(x) ? x : x.records ?? []` (thirteen on a `records` limb, one on a `data` limb, four of them written as a guard clause rather than a ternary). All fourteen read the same concrete engine — the `ObjectQL` instance the kernel registers as the `objectql` / `data` service — which answers a bare array on every path, populated or empty. The envelope limb was unreachable code that read as a contract, so the next author writing a defensive normalizer here believed an envelope was possible. The limbs are removed, and the three local engine ports that declared `Promise` (`BootProbeEngine`, `DevAdminSeedProbeEngine`, `PhoneSmsTemplateEngine`) now declare the array they always returned. - -The user-visible change is in `settleSelfRegistrationGrant`, which carried the opposite defect. Its candidate filter dropped any permission-set row whose `id` was missing or blank, silently, before choosing which row to grant: - -- When the malformed row was the only one, the operator was told `no active sys_permission_set row named 'X' resolves` — false, since an active row named exactly that was present. That report is the only signal this path emits, and nothing retries it. -- When the malformed row was the **organization-scoped** one and a global row also carried the declared name, dropping it let the `organization_id == null` arm match instead, and the self-registrant was granted the **global** permission set their organization never declared — with a success log and no other trace. - -`active !== false` remains a selection predicate: a deactivated set still reports the ordinary "does not resolve". A malformed row is no longer a selection at all — the grant is refused and the report names the malformed row, so the ambiguity is surfaced instead of resolved by accident. A well-formed family grants exactly as before. - -**Upgrade note — one family now gets a refusal where it previously got a grant.** If a deployment's `sys_permission_set` already contains a row that is active and carries the declared name but whose `id` is missing or blank, self-registration grants against that name now stop and report, including the case where the malformed row is one nobody was relying on: a malformed **global** row sitting alongside a well-formed **organization-scoped** row used to be dropped silently, letting the org row be granted, and is now refused. This is deliberate — the old behaviour could not tell that family apart from the one where the silent drop granted the *wrong* set — and it is fully reversible without a code change: repair or delete the malformed row and the grant proceeds exactly as before. The refusal is loud and names the row, so it is visible rather than something to discover later; nothing is written while it stands. diff --git a/.changeset/plugin-describe-ui-type-spelling.md b/.changeset/plugin-describe-ui-type-spelling.md deleted file mode 100644 index cca7dfd3ab..0000000000 --- a/.changeset/plugin-describe-ui-type-spelling.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -The `PluginSchema` describe strings for `staticPath`, `slug` and `default` now name `ui`, the plugin type the enum actually accepts. - -`PluginSchema.type` is `z.enum(['standard', ...CORE_PLUGIN_TYPES])`, and `CORE_PLUGIN_TYPES` spells the frontend member `ui`. The three describe strings beside it still named `ui-plugin` — a value the same schema refuses two lines above. They are not merely stale: they read as instructions ("Required for `type="ui-plugin"`"), so an author or an agent following the field's own documentation writes a value that is then rejected, with the correct spelling nowhere in the sentence that sent them there. - -The strings now read `(Required for type="ui")`, `(Required for type="ui")` and `(Only one "ui" plugin can be default)`. Because these describes compile into the published JSON Schema and into the generated reference page, the correction reaches every consumer that reads field documentation out of the spec rather than out of the source file — the generated `content/docs/references/kernel/plugin.mdx` table now agrees with the `type` row printed directly above it, which previously listed `'ui'` among the accepted members while the three rows underneath told the reader to write `ui-plugin`. - -No accept/reject behaviour moves: `type: 'ui-plugin'` is refused before and after, `type: 'ui'` is accepted before and after, and no key is added, renamed or removed. The closed-set pin tests that name `ui-plugin` as a non-member are deliberately unchanged — they are the reason this correction is provable. diff --git a/.changeset/plugin-dev-i18n-detect-packages-reader.md b/.changeset/plugin-dev-i18n-detect-packages-reader.md deleted file mode 100644 index 40375119b5..0000000000 --- a/.changeset/plugin-dev-i18n-detect-packages-reader.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -"@objectstack/plugin-dev": patch ---- - -fix(plugin-dev): the i18n auto-detect resolves `translations` from `packages[]`, not only the flattened top level (#15232) - -`DevPlugin.init`'s 3b block read `options.stack.translations` and nothing else. -For a multi-package app under the ADR-0130 D4 option-B shape — where -`packages[]` carries each definition exactly once and the flattened top-level -copy is gone — that read returns `undefined`, the detection concludes "this app -declared no copy", and the boot continues. Nothing throws and nothing logs. - -What the developer gets instead is the wrong strings. `I18nServicePlugin` -(`@objectstack/service-i18n`) is never registered, so the `i18n` slot keeps the -core in-memory fallback: `os dev` serves message KEYS, or last release's copy, -for an app that declared real translations. It reads as "the translations are -broken", not as "a collection went missing", which is why it is a reader fix -rather than a footnote. - -The detection now reads the flattened top level FIRST and then each package -body, in the order `resolveArtifactPackageOrder` (`@objectstack/core`, -ADR-0130 D4+D5) registers them: - -- **Every artifact the platform emits today answers bit-identically.** The - flattened level still answers first and short-circuits, so the `packages[]` - pass can only supply a declaration the top level did not have. This is the - reader half of the ruled order (readers first, emitter last, the artifact - additive throughout), so it lands with no change to what any command emits. -- **The caller's original expression is preserved, not re-expressed.** - `Array.isArray(t) && t.length > 0` still decides the top level, per package - body as well — re-expressing a gate as a resolved-and-counted traversal is - what silently changes the verdict for a stack that declares the key empty. -- **⛔ `stack.packages` is not iterated directly.** - `resolveArtifactPackageOrder` is the platform's one traversal and also the - GATE that parses each entry, so a second traversal would disagree with the - load path about which artifacts are loadable. An artifact with no `packages` - key is left entirely on the old path — the key's absence is checked before - the call, because D4's second branch would otherwise hand the caller's own - object back and read the same `translations` twice. -- **A malformed `packages` is refused, not skipped.** A non-array `packages`, - an entry inlined instead of wrapped under `manifest:`, or a duplicate package - id raises the same ADR-0112 envelope (`code` + `status: 422`) that - `ObjectQL.registerApp` raises for the same object later in the same boot. - -The decision — detection plus the locales it derives — is now one exported -function, `devI18nPluginOptions`, so the #15004 option-B acceptance pin -measures it by CALLING it rather than re-implementing the read. `DevPlugin` -keeps the dynamic import and its degradation: those are about the optional -package being installed, which is a different question from what the stack -declares. diff --git a/.changeset/plugin-security-default-set-answer-not-container.md b/.changeset/plugin-security-default-set-answer-not-container.md deleted file mode 100644 index a128ef51d6..0000000000 --- a/.changeset/plugin-security-default-set-answer-not-container.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -"@objectstack/plugin-security": patch ---- - -fix(plugin-security): the app default permission set resolves from the first level that NAMES one (#15298) - -`declaredPermissionSets` carried a docblock stating a short-circuit its code did -not have: - -> The `packages[]` pass only supplies a set where the top level had none — which -> is precisely the option-B artifact. - -The code pushed the flattened top level and then **every** package body -unconditionally, so on today's additive artifact (flattened level *and* -`packages[]` both present) every permission set was collected twice. Nothing -observable came of it — the sole caller is private and takes the first -`isDefault` set, which the flattened copy still supplied — so this corrects a -false written contract on a security-path reader, not a live defect. That -distinction is the point: the sentence was load-bearing, because it was the -stated reason the reader half was revertible on its own and safe to land before -the emitter half (#14512), and the next reader would have believed the mechanism -was there. - -⚠️ Release-notes note: this supersedes one sentence of the #15226 entry in this same -unreleased batch — "The resolution now reads the flattened top level FIRST and then each -package body". That described #15226 accurately when it landed; after this change the -`packages[]` pass runs only where the top level named no default. The earlier entry is -left as written rather than retro-edited, so whoever compiles the notes collapses the two -deliberately instead of reading a contradiction. - -The reader now walks the discipline the docblock claims — start from the -expression this program replaced, `appDefaultPermissionSetName(config.permissions)`, -and consult `packages[]` only where it came back `undefined`. - -- **The condition is the resolved NAME, never the `permissions` container.** - Branching on the container re-creates the silent loss the reader program - exists to remove, one shape further along: a flattened level that carries - permission sets but marks none of them `isDefault` is legal today and - hand-authorable in any `objectstack.config.ts`, and a container-shaped - condition (`Array.isArray(flattened)`, with or without `&& length > 0`) shorts - it past the whole `packages[]` pass and answers `undefined` — nothing thrown, - nothing logged, every member of the app back down to the platform floor alone. - Reading the answer also retires the `[]`-is-truthy trap rather than patching - around it. -- **The package order is resolved BEFORE the top level is consulted.** - `resolveArtifactPackageOrder` refuses a malformed `packages` — not an array, - an entry inlined instead of wrapped under `manifest:`, a duplicate package id - — with an ADR-0112 envelope this reader does not catch, and that refusal must - not become conditional on whether the flattened level happened to name a - default first. An artifact is either loadable or refused; which level answered - is not part of that question. -- **No emitted artifact changes its answer.** Measured, not argued: 26 shapes — - the composed additive artifact, its option-B derivative, the collection-zoo - fixtures behind the #15004 acceptance pin, every config the unit suite drives, - the three malformed-`packages` refusals, and the hand-authored mixed shapes — - return byte-identical results before and after, with `@objectstack/plugin-security` - rebuilt and the change proven present in `dist/` on each leg. diff --git a/.changeset/published-cli-stderr-nonblocking-guard.md b/.changeset/published-cli-stderr-nonblocking-guard.md deleted file mode 100644 index 60ce0d2ac3..0000000000 --- a/.changeset/published-cli-stderr-nonblocking-guard.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -The published `os` binary no longer freezes in the kernel when whatever is reading its output stops draining. - -Node puts the CLI's stderr on the non-blocking write path when it opens the pipe, so a write to a reader that has stopped is buffered rather than parking the thread. libuv clears that flag again in the pre-exec of every child spawned with **inherited** stdio — and inheriting is `dup2`, so the flag lives on an open file description the spawner shares. Clearing it for the child clears it for the CLI too. - -Measured on the built binary, `os dev --verbose` with its output piped to a reader that stopped draining: `os dev` spawns `os serve --dev` with inherited stdio at 2.8 s, that child spawns the esbuild service with inherited stderr at 5.2 s, and fd 2 stays blocking for the rest of the run. 3.1 s after the reader stopped, the main thread sat in `write(2)` (`wchan=sock_alloc_send_pskb`), 4 of 4 runs — parked 28.9 s, **ignoring SIGINT while parked**, and released only when the consumer resumed. Not a crash and not a timeout: alive, idle, unresponsive, with an empty log. Anything that pipes `os dev` and reads it slowly — a CI log collector, a backgrounded runner, a supervisor that stops draining while it does work — could park the CLI this way. - -`bin/run.js` now installs `keepStderrNonBlocking()` before oclif can write a byte. The guard re-asserts `O_NONBLOCK` immediately ahead of each write, which is what the measurement requires: the clearing that persisted was made by a **grandchild** the CLI does not spawn and cannot see, so a one-shot at startup would be undone silently and no change to the CLI's own spawn sites would have prevented it. - -The guard itself is not new — it shipped in no published install. It lived at `packages/cli/bin/stderr-nonblocking.mjs`, and `files` names only `dist`, `README.md` and `CHANGELOG.md`; npm packs a `bin` **target** regardless of `files`, which is why `bin/run.js` reached every install and the module beside it reached none. It now compiles from `src/utils/stderr-nonblocking.ts` into `dist/`, under the whitelist that was already there. - -Nothing about which arguments the CLI accepts, what it prints, or what it exits with changes. The refusal of `setBlocking(true)` in `src/utils/format.ts` stands and is untouched — this is its inverse, and what keeps its premise true. diff --git a/.changeset/record-picker-filter-rule-array.md b/.changeset/record-picker-filter-rule-array.md deleted file mode 100644 index 67f9ef2f67..0000000000 --- a/.changeset/record-picker-filter-rule-array.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -feat(spec)!: `ComponentPropsMap['element:record_picker'].filter` converges onto the `ViewFilterRule` array form — the last record-form `filter` in the map (#14406, objectui#6206 Option B) - - - -**BREAKING** accept-set change on one props-map entry, shipped as `minor` under -the repo's launch-window convention for breaking changes; the migration -prescription is registered under protocol major 18. - -One filter orthography platform-wide (maintainer batch adjudication 2026-08-25, -verbatim 「同意」, Option B): after `element:number` converged (#12039 Key 2), -`element:record_picker`'s `filter` was the one `filter` input in -`ComponentPropsMap` still declared as the MongoDB-style record -(`FilterConditionSchema`) while the three array-declared siblings -(`record:related_list`, its nested Add-affordance picker, `element:number`) -declared `z.array(ViewFilterRuleSchema)` — the four `object-*` doors declare -`filter` as `z.unknown()`, #15449 — so the filter a list view stores and -renders was refused by the picker beside it. The entry now declares the same -array form those siblings do, and the `FilterConditionSchema` import that existed for this -one site leaves the file with it. - -Sequenced measurement-first, as that convergence had to be: the `record_picker` -read path was measured at the objectui pin before the declaration moved. The -renderer hands `filter` to `query.$filter` and calls `adapter.find()`, whose -`convertQueryParams` lowers a rule array through `translateFilterArray` into -filter AST tuples — the door every list view's stored rule array already takes -— and nothing on that path parses `properties` against the installed spec. - -**Migration** (`element-record-picker-filter-rule-array` — listed by -`os migrate meta --from 17` once the protocol major is 18): a record-form `filter: { status: 'active' }` becomes -`filter: [{ field: 'status', operator: 'equals', value: 'active' }]`; an operator -object `{ amount: { $gt: 100 } }` becomes -`[{ field: 'amount', operator: 'greater_than', value: 100 }]`; several keys -become several rules (they AND). The record form is refused at `filter` -(`invalid_type`, expected array). The binding-level `dataSource.filter` on the -same node is a different key and is unchanged by this release. - -`ElementRecordPickerPropsParsed` is declared (ADR-0122): the entry's parsed -state now differs from its authored state on `filter` (`operator` normalizes on -parse), so the bare alias is no longer isomorphic. diff --git a/.changeset/reference-page-block-tag-payload-render.md b/.changeset/reference-page-block-tag-payload-render.md deleted file mode 100644 index 9ad26d1bdd..0000000000 --- a/.changeset/reference-page-block-tag-payload-render.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -Reference pages no longer print `@example` and `@category` tag lines as literal text. - -A module docblock is JSDoc, so its header carries block tags, and the reference-docs -renderer emitted a tag written on a prose line verbatim — 18 such lines reached 14 -customer-facing pages, as `@example Basic field mapping` above a code fence and -`@category Security` at the foot of four `system/` pages. `#13796` removed `@module` -from the page and left these two open, because a blanket `^@\w+` line filter would -have taken reader prose off the page and orphaned the fences below it. - -The verdict is per tag, and the axis is the payload rather than the spelling: - -- **`@example CAPTION` is REWRITTEN** into that caption, in bold, above the block it - captions — the shape `@see` already had (`See also: …`). 12 lines across 10 pages. - Bold rather than a heading because heading renumbering has already run by then, so - an emitted heading would carry a level chosen blind of the page, add entries to the - pages' tables of contents, and put a caption in reach of `check:docs-single-h1`. -- **A bare `@example` is DROPPED.** With no payload it is the `@module` case exactly, - and the fence beneath it is visibly an example without a line announcing one. 2 - lines (`studio/plugin`, `studio/object-designer`), both sitting against the - `check:skill-examples` opt-in marker that was already dropped there. -- **`@category VALUE` is DROPPED.** 4 lines, all reading `Security`, on four pages that - already sit under a `system/` section saying as much — and nothing in the repo reads - the tag: no typedoc or api-extractor (neither is used here), no search index, no - gate. Routing it into page frontmatter instead would publish a field with no - consumer. The tag stays in the source, where it is a legitimate JSDoc tag; only the - rendered page drops it. - -No schema behavior changes. The pins assert on the rendered fragment rather than on the -emitted `.mdx`, because `check:docs` compares the artifact against the source and -reproduced all 18 tag lines faithfully. diff --git a/.changeset/references-door-organization-forwarding.md b/.changeset/references-door-organization-forwarding.md deleted file mode 100644 index a642db13da..0000000000 --- a/.changeset/references-door-organization-forwarding.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@objectstack/rest": patch ---- - -The admin "Used by" panel no longer clears a delete when the caller's own organization is using the item. - -`GET /api/v1/meta/:type/:name/references` backs that panel, whose empty case reads "Nothing in the metadata graph points at this item. Safe to delete." — advice given to an operator about to delete something. The door supplied no organization, so the reference sweep read the environment partition only: an organization-scoped `view` (or `dashboard`, `report`, `translation`, `email_template`) pointing straight at the object being deleted was invisible, and the panel issued a false clearance. It now passes the caller's organization, and those references are returned. - -The organization is passed RAW, deliberately, and that is the whole of the change — no new parameter, response field or contract surface. `req.params.type` is the reference TARGET, while the sweep spends the organization on the SOURCES it reads per type; `getMetaItems` applies the `allowOrgOverride` read gate to its own request type, so each source is scoped on its own registry flag. A non-overridable source (`object`, `flow`, `app`, …) is still read environment-wide and no pre-#6190 organization-scoped row is resurrected into a delete clearance. An anonymous or organization-less caller reads exactly what it read before, and no status code or response shape moves. diff --git a/.changeset/report-chart-axis-own-selection.md b/.changeset/report-chart-axis-own-selection.md deleted file mode 100644 index 1ddd27ef6e..0000000000 --- a/.changeset/report-chart-axis-own-selection.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -"@objectstack/lint": patch ---- - -`chart-axis-not-selected` resolves a report chart against its own `chart.yAxis`, not `report.values` (#15734) - -**Behaviour change — one false finding removed on the report surface.** A report chart whose `chart.yAxis` names a declared measure that `report.values` does not select no longer raises a `chart-axis-not-selected` warning. Nothing else about the rule moves, and no other surface moves at all. - -The warning stated a query consequence the renderer refutes. Read at the `@object-ui` revision this repo pins (`.objectui-sha`), `plugin-report/src/DatasetReportRenderer.tsx` does not query `report.values` for the chart at all — it runs the chart's own, narrower query out of the two axis strings: - -``` -const state = useDatasetRows( - dataset, - plan.kind === 'series' && xAxis ? [xAxis] : [], - wantsQuery && yAxis ? [yAxis] : [], -``` - -and says so in that file's own words at the `scopeOrder` docblock: *"the embedded chart queries only `chart.xAxis` × `chart.yAxis`"*. So the measure the warning said "the query does not return" is exactly the one the query asks for, and the chart plots it. `report.values` is the selection of the TABLE beneath the chart. - -Both limbs follow from that one measurement: - -- **No not-selected check at the report `chart.yAxis`.** That position IS the chart's query, so it cannot fail to select itself. `chart-measure-unknown` there is untouched: an UNDECLARED measure is still no column at all, and still an `error`. -- **`chart.series[].name` resolves against the singleton `{ chart.yAxis }`.** The entry is a display-name override paired with a DERIVED series, and the chart derives exactly one (`buildChartSeries(…, [xAxis], [yAxis], …)`). An entry naming `chart.yAxis` now lands however the table is selected, and one naming any other declared measure is still reported — including a measure `report.values` does select, which it could not reach before. - -The list-view and page-component surfaces are unchanged, and carry firing controls that say so: on both, `values` IS the measure set the query asks for (`ObjectView` hands it to the chart; `ObjectChart` queries `{ dimensions: schema.dimensions, measures: schema.values }`), so the existing resolution is the right one there. - -The per-position tier and consequence wording is untouched — only the SET the report surface resolves against moves. diff --git a/.changeset/rest-generic-passthrough-object-key.md b/.changeset/rest-generic-passthrough-object-key.md deleted file mode 100644 index b24c8e33b9..0000000000 --- a/.changeset/rest-generic-passthrough-object-key.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -"@objectstack/rest": minor ---- - -fix(rest): the generic declared-status passthrough names its object on both error doors (#14725) - -**Response-body change on the published bulk / metadata / UI doors: one optional -key is added, `object`.** Nothing is removed, no status moves, and no `code` -value changes spelling. - -#14541 made the two REST error doors agree for every refusal a *bespoke* arm -classifies. They still disagreed for every refusal that reached the *generic* -declared-status passthrough, because the two copies of that one passthrough -differed by exactly one key: `classifyDataError`'s copy ends -`...(object ? { object } : {})` and `resolveErrorResponse`'s 4xx arm had no such -limb. Measured on `main` @ `a12b15e394` — one error object, both doors: - -| door | before | -|---|---| -| `mapDataError(err, 'duly_note')` (single-record `/data`) | `409 {"error":"…","code":"DUPLICATE_RECORD","object":"duly_note"}` | -| `sendThrownError(res, err, 'duly_note')` (bulk / metadata / UI) | `409 {"error":"…","code":"DUPLICATE_RECORD"}` | - -One refusal, two bodies, decided by which route caught it — the #14541 shape one -arm over. The bulk door now answers the first row too. - -It closes the same card's second residue with it. `recordNotFoundError` -(`@objectstack/core`) declares `code`, `status = 404` **and** `object`, so that -declared status carries a record-level not-found past the `RECORD_NOT_FOUND` arm -into this same generic passthrough on every route reporting through -`handleRouteError` / `sendThrownError`, while the single-record `/data` door -reached the generic arm in `classifyDataError` and shipped the name. Both doors -now agree for that producer in every combination of declared status and -door-supplied object. - -**Who sees the new key.** The name comes from the door's `object` *argument*, -never from `error.object`, so only a route that supplies one is widened. Of 35 -route call sites of this door, **9** pass an argument that can be a non-empty -object name — `POST /data/:object/batch`, `/createMany`, `/updateMany`, -`/deleteMany`, `POST /data/:object/:id/clone`, `POST /data/:object/import`, -`POST /data/:object/import/jobs`, `GET /data/:object/export`, and -`GET /ui/view/:object/:type`. The other 26 (21 passing nothing, 5 passing the -literal `''`) answer byte-identical bodies. `classifiedRefusalAnswer` — the -entry point the analytics dataset face and the record-share family re-dress — -calls this door with no `object` argument at all, so those envelopes' key sets -do not move. - -**What deliberately does not change.** The declared-**5xx** arm gains nothing: -its sibling `declaredServerFaultAnswer` names no object either, so the two doors -already agreed in that band and adding the limb there would *create* a -divergence, on top of putting a caller-supplied name into a body whose whole -rule is that a declared server fault says nothing beyond status and code. The -`RECORD_NOT_FOUND` arm's message-**text** limb -(`/^Record \S+ not found in \S+/i`) is not lifted above the passthrough either — -that boundary is #14541's, and it is now pinned behaviourally and positionally -rather than described. - -Consumer note: a client that key-counts or exact-matches an error body from a -bulk, import, export, clone or UI-view route will see `object` alongside `error` -and `code` where the equivalent single-record `/data` response has carried it all -along. A client that reads named fields is unaffected. diff --git a/.changeset/runtime-declarative-row-update-executor.md b/.changeset/runtime-declarative-row-update-executor.md deleted file mode 100644 index d4aca34c61..0000000000 --- a/.changeset/runtime-declarative-row-update-executor.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -"@objectstack/runtime": minor ---- - -feat(runtime): the platform action route executes the declarative row-level `operation: 'update'` action (#14092) - -The spec half (#15077) made `operation: 'update'` + `patch` parse; nothing performed the -write, so an authored update action reached the action route with no handler and collected -the registry's loud not-registered answer. It now performs the write. - -`POST /api/v1/actions///` — and the MCP `run_action` bridge, through -the same shared executor — performs exactly ONE data-plane update of the current record: - -- **As the caller.** The write carries the caller's own `ExecutionContext`, never the - `isSystem`-elevated context a `type: 'script'` BODY runs under. There is no author body here - to trust, so the data plane's own gate is the only gate — the object's permissions, its hooks - and its validations fire exactly as for a user edit, and their refusals reach the caller with - their own `code` and `status`. This consumes the `runAs: 'user'` direction ruled on #14010; no - `runAs` key is added. -- **A caller who cannot read the row is refused before anything is written** (404 - `RECORD_NOT_FOUND`, the platform's one existence-non-disclosing envelope), by consuming the - caller-scope load's verdict rather than re-deriving it from the stamped `record.id` — the - #14143 class: a swallowed load must never become an implicit grant. -- **The write is `{ ...patch, ...collectedParams }`** — static values under the dialog's, so a - param of the same name wins. Nothing else from the action is merged, and the ADR-0104 D2 param - contract still bounds what the wire can add. -- **No current record ⇒ a located refusal**, never a silent no-op: no `recordId` on the route or - in the body, an action addressed at the object-less key, or an empty write bag each answer 400 - naming the action and the fix. -- **`undoable: true`** returns `undo: { type, objectName, recordId, undoData, redoData }` — the - prior values of exactly the fields written, `null` for a field the row did not carry, so the - existing Undo readers can restore. The three remaining `UndoableOperation` keys (`id`, - `timestamp`, `description`) stay the client's. -- `visible` is deliberately unread here: it is a per-record renderer predicate, and the - authorization is the point above. - -`operation` is read BEFORE `type` at every reader, so the HTTP door and the MCP bridge agree: -`isHeadlessInvokableAction` now accepts a declarative update (it has neither `target` nor `body` -by construction), `headlessActionTypeError` hands it no client-side-type prescription, and -`summarizeAction` reports `operation` and `requiresRecord: true`. - -Unchanged: a handler-less `type: 'script'` action WITHOUT `operation` still gets today's -not-registered 404 — the script path is not widened. diff --git a/.changeset/sandbox-writeback-entry-snapshot-normalised.md b/.changeset/sandbox-writeback-entry-snapshot-normalised.md deleted file mode 100644 index 247a4a7410..0000000000 --- a/.changeset/sandbox-writeback-entry-snapshot-normalised.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -'@objectstack/runtime': patch ---- - -fix(runtime): a sandboxed hook body no longer launders an untouched `readonly` field onto the row - -A `beforeUpdate`/`beforeInsert` body running in the sandbox made the engine believe it had -written payload keys it never named, and a `readonly` field the caller supplied then survived -the readonly strip and landed. Measured end to end: with `locked_at` declared -`{ type: 'datetime', readonly: true }` and seeded to `2020-01-01`, a caller sending -`locked_at: new Date('2099-12-31…')` alongside a body whose whole source is -`ctx.input.touched_by = 'hook'` stored the caller's 2099 value — while the same object's -readonly `text` field was correctly stripped in the same request. - -The cause was a comparison of unlike things. The write-back decides whether a body wrote -*through* an object-valued key by comparing the host payload value against the VM's exit dump, -and the dump has been through `JSON.stringify`/`JSON.parse` while the host value has not. A -`Date` therefore never compared equal to its own ISO projection, took the documented -"cannot prove equal ⇒ carry it back" path, and was re-asserted onto the proxy that records -which keys a hook wrote. The class was every object-valued value a JSON round-trip cannot -prove equal — an object carrying an `undefined` member included, a `Date` being only its most -reachable member. - -The entry value is now normalised through the same round-trip the VM saw before it is -compared. The same change ends a fidelity loss on non-readonly fields: an untouched key is no -longer carried at all, so a host `Date` is no longer replaced by an ISO string on its way to -the driver. - -Fail-open behaviour is unchanged for values the round-trip genuinely cannot evaluate: a cyclic -or bigint-bearing payload value is still reported as changed and still carried, per key. diff --git a/.changeset/seed-read-drops-dead-org-rung.md b/.changeset/seed-read-drops-dead-org-rung.md deleted file mode 100644 index 7c249b06fe..0000000000 --- a/.changeset/seed-read-drops-dead-org-rung.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/runtime": patch ---- - -The package-publish seed read-back no longer runs a two-attempt org-then-env ladder whose rungs resolve the same row. - -`applyPublishedSeeds` — the route-level seed apply behind `POST /packages/:id/publish-drafts`, which runs for protocols that do not self-apply seeds inside `publishPackageDrafts` — read each just-published `seed` body twice when the session had an active organization: once naming the organization, then once env-wide. The comment above it said the first attempt tried the active org and the second fell back, "and resolving the wrong scope here is what silently produced `0 rows loaded`". - -That was true when it was written and is not true now. `seed` declares `allowOrgOverride: false`, and `getMetaItem` resolves `organizationIdForMetaRead(request.type, request.organizationId)` once at its top and spends that binding — never the raw argument — on every read beneath it. The predicate answers `undefined` for every non-overridable type, so both rungs asked the engine the same predicates and served the same answer. Measured rather than reasoned: against the shipping protocol over one store, the two requests produce byte-identical engine reads and byte-identical answers on both the hit and the miss branch, and neutering the second rung reddens nothing on a pinned publish-then-read path (a `view` control confirms the same comparison does separate the two rungs for an org-overridable type). - -The read is now a single call naming no organization, and the comment states that the scope is decided by the registry flag and the gate inside `getMetaItem` rather than by this call site — matching the sentence the `app` flip in the same file already carries. - -One observable changes, and only on the failure branch: `getMetaItem` answers a wrapper rather than a falsy value for a name it cannot resolve, so the second rung was in practice reached only when the read *threw* — where it repeated the identical failing read and appended the same sentence to the client-facing `seedApplied.errors[]` twice. A failed read-back is now reported once. Nothing about which row a publish resolves, or whether its rows load, moves. diff --git a/.changeset/serve-unlinked-database-file-watch.md b/.changeset/serve-unlinked-database-file-watch.md deleted file mode 100644 index 7a85c645f2..0000000000 --- a/.changeset/serve-unlinked-database-file-watch.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@objectstack/cli": patch ---- - -`os serve` now says so when the SQLite file it is serving is no longer the file at its configured path. - -Deleting the data directory under a running server — `rm -rf .objectstack/data`, which is what a `demo:reset` script does and what a fresh-database repro starts with — unlinks the inode without touching the process. SQLite keeps reading and writing the now-invisible file, health keeps answering `200`, and a later boot creates a brand-new database at the same path. From that moment every filesystem inspection of that path describes a *different* database than the running server answers from, and nothing anywhere says so: a row edited there has no observable effect on the live server, and a user who authenticates against the live server is not in that file. Both readings are true, both look like a broken write path, and one investigation that reported them as evidence cost a full P0 cycle. - -A boot that serves an on-disk SQLite file now records that file's identity once the boot is complete and re-checks it on a 30-second interval. When the file is gone, or the path holds a different file, it reports **once** at `error` — naming the path, the consequence (every external observation of this deployment is now false, and it will keep looking healthy) and the fix (restart the server so it opens the file that is at that path now). - -It refuses nothing and retries nothing: the running server is still correct, merely invisible, and breaking a working dev loop to fix a reporting gap would trade a bad hour for a worse one. Nothing is added to any payload, endpoint or state file. Silence from the check is not a claim that the file is intact — every uncertainty in it resolves toward staying quiet, because a false report would send an operator to restart a server whose database is fine. diff --git a/.changeset/service-analytics-text-operator-non-text-column.md b/.changeset/service-analytics-text-operator-non-text-column.md deleted file mode 100644 index 90bd3fe9b1..0000000000 --- a/.changeset/service-analytics-text-operator-non-text-column.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@objectstack/service-analytics": minor ---- - -The three SQL compilers in this package — the RLS read-scope lowering (`compileScopedFilterToSql`), `NativeSQLStrategy`'s own `where` and the `ObjectQLStrategy` SQL echo — compile a text operator over a column whose declared type stores no text to the contract's declared answer. - -`compileScopedFilterToSql(filter, alias, options?)` takes a new optional `nonTextColumn(field)` predicate; when it answers `true`, a positive text operator compiles to `1 = 0` and `$notContains` to `1 = 1` instead of a `LIKE` that coerces on SQLite (`5` renders `'5.0'`) and is refused at query time on Postgres (SQLSTATE 42883 — a 500 on a read scope the platform accepted). The service answers the predicate from the field metadata hook it already holds (`sourceFieldMeta`), exposed to strategies as `DatasetScopedStrategyContext.declaredFieldType`, and the two strategies pass it for the read scope and for the query's own text filters, so a query and its RLS scope answer one cell one way and the echo prints the statement that ran (`FILTER_TEXT_CASES`' `score` rows, maintainer ruling 2026-09-05). A host that wires no field metadata keeps the `LIKE` it always got, and every comparand refusal still runs ahead of the constant. diff --git a/.changeset/service-storage-test-tsc-program.md b/.changeset/service-storage-test-tsc-program.md deleted file mode 100644 index 0879cbebd1..0000000000 --- a/.changeset/service-storage-test-tsc-program.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -"@objectstack/service-storage": patch ---- - -fix(service-storage): put the test layer in front of tsc, and repair what it was hiding (#15050) - -`packages/services/service-storage` had **no `typecheck` script at all** — its -scripts were `build` and `test` — so no tsc program anywhere read this -package's test layer, and its errors were carried instead as a 51-error DEBT -entry in `scripts/check-type-check-coverage.mjs`. Gives it the #14062 / -#14181 "checked test zone" shape: a sibling `tsconfig.test.json` (module -semantics only — `esnext` / `bundler` / `lib: ES2022` — matching how vitest -actually executes these files; strictness inherited and untouched) plus a -`tsconfig.scripts.json` for `scripts/i18n-extract.config.ts` (the ninth -instance of #11351, previously excluded from that ledger only because this -package had no `typecheck` script to hang it on), both named by a new -`typecheck` script. - -Measured before repair: 51 errors under BUILD semantics (`tsc --noEmit -p -tsconfig.json`, which already includes the tests — matching the DEBT entry's -recorded number exactly), 10 under the split. Unlike `service-cluster` -(#14181), this package's BUILD reading was *not* already clean, so both -programs needed genuine repair, not just the test-only split: 23 `TS2835` -(relative imports missing their `.js` extension, required under BUILD's -NodeNext resolution) were fixed by *adding* the extension — which resolves -correctly under both NodeNext and the split's bundler mode — and clearing -that also cleared all 15 `TS7006` "implicitly any" as a downstream cascade -from the same unresolved imports (the shape `@objectstack/core` reported at -98 → 4). The remaining 3 `TS2550` (`Array.prototype.at` needing `lib` -es2022) are rewritten to indexed access rather than widening the shared -BUILD `tsconfig.json`. The 8 code-tier errors (`TS2339` × 4 — a test -helper's object-spread dropped its `Record` index -signature, fixed with an explicit return-shape annotation; `TS2347` × 4 — a -fake `ctx: any`'s `getService(...)` calls converted to `getService(...) -as T`, the pattern one call site in the same file had already adopted for -exactly this reason) are genuine test-file fixes. Both readings now agree at -0/0 — the same result `service-cluster` reported, reached by a longer road. - -The package's DEBT entry (51 errors) is **deleted**, not lowered — the -graduation this ratchet's invariant requires. No `test-typecheck-debt.json` -is added: residue is 0, so none is owed (#5286, maintainer-only to open). -`check:type-source-resolution` went red from onboarding the two new -programs (the documented onboarding-limb case): a registry entry is added -rather than `paths`, measured both ways — `paths` takes this package's test -layer from 0 errors to 306, all in other packages' source. - -No runtime code changes: `src/**` excluding tests is byte-identical, so no -shipped behaviour moves. The `patch` level reflects the published -`package.json` gaining `typecheck` / `check:test-typecheck` scripts and a -`tsx` devDependency. diff --git a/.changeset/session-unbacked-org-claim-dropped.md b/.changeset/session-unbacked-org-claim-dropped.md deleted file mode 100644 index 43502e0f68..0000000000 --- a/.changeset/session-unbacked-org-claim-dropped.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -'@objectstack/core': patch ---- - -A session whose active organization is no longer one the user belongs to now resolves with no active organization instead of that one's data. - -Under a wall-enforcing tenancy posture (`isolated` / `group`), `resolveAuthzContext` took a browser session's stored `activeOrganizationId` as the request tenant without ever comparing it to the user's current memberships — the framework's only such comparison was gated on an API-key principal. A session whose owner had been removed from an organization therefore kept reading that organization's rows and writing into it until the session expired on its own (7 days by default), including when the removal went through the product's own offboarding path. - -That claim is now vetted: if it is not in the caller's `accessible_org_ids`, it is dropped and the context resolves with no active organization at all, which the tenant wall already fails closed on (reads resolve to nothing; a tenant-scoped write is refused by ADR-0123 D2). The principal is **not** refused — a session is a person who may hold memberships elsewhere, so they stay signed in and can switch to an organization they are actually in. The API-key arm is unchanged: a key is its organization binding and is still refused outright. The wire is unchanged; the drop is reported to the operator as a single server-side `warn`. diff --git a/.changeset/session-user-language-retired.md b/.changeset/session-user-language-retired.md deleted file mode 100644 index 36d9459e7d..0000000000 --- a/.changeset/session-user-language-retired.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -feat(spec): retire `SessionUser.language` — the session contract's never-produced "preferred language" (#14788, ADR-0049) - - - -**BREAKING** key removal on a published session type, landing after the -v17.0.0 cut (the lockstep launch-window convention ships it as `minor`; the -prescription is registered under protocol major 18 — `api/SessionUser:language` -in `RETIRED_KEYS_BY_MAJOR[18]` plus the D3 semantic entry -`session-user-language-retired` — where `os migrate meta` users will look). - -`SessionUserSchema.language` (`api/auth.zod.ts`) was declared -`z.string().default('en')` and described as "Preferred language", and had no -producer and no consumer anywhere: no session endpoint ever wrote it, no client -ever read it (objectui measured at its pinned sha: zero readers; the only -in-repo mentions were the schema's own unit test). A reader trusting the -published contract got a constant that was not the user's language — while the -user's real preference had just landed as the first-class column -`sys_user.locale` (#13881), which the session type could not see. Three -spellings of one concept on the published surface, none of them right. The -maintainer ruled option D (2026-09-03): retire the dead key under ADR-0049 -enforce-or-remove and make `GET /auth/me/localization` the ONE read face for -the signed-in user's language. No replacement field joins the session contract -until a session endpoint really produces one — no dual-spelling window. - -FROM → TO: - -- `SessionUser.language` / `SessionUserParsed.language` → *(removed)*. Read - the signed-in user's language from `GET /auth/me/localization` → `locale`, - which now resolves the user's own `sys_user.locale` when set → the request's - `Accept-Language` → the deployment default (`@objectstack/plugin-hono-server` - in the same release). - -One-line fix: delete the key. A producer still writing it fails `tsc` -(`never` input type) and fails to parse with this prescription; a reader still -keying on it now reads `undefined` instead of a permanent `'en'`, and should -read `locale` off `/auth/me/localization` instead. - -The retirement kit: - -- **`retiredKey()` tombstone** (the schema is a non-strict `z.object`, so a bare - delete would have stripped the key silently — ADR-0104): writing `language` - is a `tsc` error and a parse error carrying the prescription, on - `SessionUserSchema` and through both envelopes that embed it - (`SessionResponse.data.user`, `UserProfileResponse.data`). -- **ADR-0087 registration**: `api/SessionUser:language` under major 18 plus - the D3 semantic entry `session-user-language-retired`. A RESPONSE surface — - the server mints a `SessionUser`, nobody authors or persists one — so there - is no source for a D2 conversion to rewrite (the - `api/AuthFeaturesConfig:passkeys` disposition). -- **generated baselines**: `authorable-surface/api.json` carries the - `[RETIRED]` row; `authorable-defaults/api.json` drops the `= "en"` default; - `spec-changes.json`, the upgrade guide and `content/docs/references/api/auth.mdx` - regenerated. -- **pins** in `api/auth.test.ts`: the prescription on parse, absence (no default - minted) on a clean parse, both envelopes refusing the key, and a - `packages/spec/src`-scoped scan for any reader of `.language` off a - `SessionUser`. -- zero in-tree producers or readers, so no in-repo source changes ride along - beyond the endpoint change shipped with it. diff --git a/.changeset/settings-door-value-domain-shared-predicate.md b/.changeset/settings-door-value-domain-shared-predicate.md deleted file mode 100644 index 9ce175ffdb..0000000000 --- a/.changeset/settings-door-value-domain-shared-predicate.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -"@objectstack/service-settings": minor ---- - -fix(service-settings): the settings door answers from the ONE shared value-domain predicate, and refuses a non-member with `value_domain` (#15162) - - - -**BREAKING** for a client that branches on the refusal code. Landing inside -the launch window, so it ships as `minor` (the lockstep convention forbids -`major`); the banner is the carrier, not the bump. - -The services half of the maintainer's ruling of 2026-09-02: **one closed -vocabulary and one membership predicate shared by settings specifiers and -object fields**. The spec half declared them in `@objectstack/spec/shared`; -this package had been carrying a second copy of all three definitions since -`Specifier.valueDomain` shipped. The copies are deleted and the door now asks -`isValueDomainMember` — the call the record write path will make when the -engine half of the same ruling lands (PR #15316, still open). - -**The wire change**, measured on `PUT /api/settings/localization` with -`{"timezone": "Mars/Olympus"}`, base `a56baa2bd` vs this branch: - -| | before | after | -|:--|:--|:--| -| `fields[0].code` | `invalid_value` | `value_domain` | -| `fields[0].message` | `Default timezone must be a valid IANA time zone identifier (e.g. 'Europe/Zurich'). Received 'Mars/Olympus'.` | `Default timezone must be a valid IANA time zone identifier, e.g. Europe/Zurich (got "Mars/Olympus")` | - -Everything else is byte-identical: HTTP 400, the envelope code -`SETTINGS_VALIDATION`, `field`, `label`, `constraint: { valueDomain: … }` and -the echoed `value`. A client that reads `constraint.valueDomain` — the -machine-readable half ADR-0114 asks it to read — is unaffected. A client that -branches on `code === 'invalid_value'` for a domain breach must move to -`value_domain`. - -Why the code moved: ADR-0114's rule is that the code is the **constraint's own -name**, the way `max_length` names the bound it breached. This branch took -`invalid_value` — the catalog's slot for "rejected for a reason no other -member names" — only while no member named a standard-domain breach. The -field-level card's spec half added one, so the slot no longer applies. The -message now renders the published catalog template -`value_domain_` in `en` — the catalog the record write path will render -from once PR #15316 lands, so the two doors under one ruling will describe one -domain in one set of words instead of each composing its own sentence. For an `encrypted` specifier the offending value is still never -echoed: the template's value placeholder takes the same mask the REST boundary -uses (`fields[0].value` stays absent, as before). - -**No value changes verdict.** The accept sets were measured, not assumed, on -the repo's Node 22 baseline (v22.22.2): - -- `iso_3166_alpha2` — the two 249-code lists diffed mechanically before either - was deleted: identical, including order; symmetric difference 0. -- `iso_4217_currency` — this one changes DEFINITION: a run-time - `Intl.supportedValuesOf('currency')` probe becomes the key set of the - checked-in CLDR snapshot `CURRENCY_FRACTION_DIGITS`. 162 codes vs 162, - symmetric difference 0 in both directions (`CHF` in both, `XYZ` in neither). - The behaviour that changes is that the verdict no longer varies with the - host's ICU build — the direction the shared module argues for. A door-level - test now re-measures it: every code the run-time probe admits must still be - admitted. -- `iana_time_zone` — the identical `Intl.DateTimeFormat` probe on both sides, - unmoved. - -A ratchet pin (`value-domains.shared-predicate.pin.test.ts`) reddens if any -non-test source in this package re-acquires a membership table, an `Intl` -enumeration probe, or a second caller of the predicate. diff --git a/.changeset/sharing-rule-evaluation-result-grants-refused.md b/.changeset/sharing-rule-evaluation-result-grants-refused.md deleted file mode 100644 index a700304e5d..0000000000 --- a/.changeset/sharing-rule-evaluation-result-grants-refused.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -'@objectstack/spec': minor ---- - -feat(spec): `SharingRuleEvaluationResult` declares `grantsRefused?: number` — the optional seventh key the sharing-rule evaluate route already answers (#14969) - -`minor`, derived: a new key on a published contract interface is additive public -API (semver "backwards-compatible functionality"), and not `major` because the -key is **optional** — every existing `ISharingRuleService` implementer, in-tree -and out, keeps compiling unchanged, and every consumer typed against the six -counts keeps reading them. - -`POST /api/v1/sharing/rules/:idOrName/evaluate` (ledgered `sdk`, -`shares.rules.evaluate`) passes the service's return value through unfiltered, -and `@objectstack/plugin-sharing` has counted refused grants on its own subtype -since #14754 — so the wire carried `grantsRefused` while the declared client -type (`client.shares.rules.evaluate`, typed `Promise`) -could not name it without a cast. The client gains the key through its spec -import with no edit of its own. - -What the key means, and what its absence means: it counts the grants the -engine **refused** during the pass (`ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` on -an organization-less insert into a tenant-scoped `sys_record_share`); the pass -continues past a refusal, so `grantsRefused > 0` is not a failed pass. The key -is **absent — not `0`** — from any implementation that does not count -refusals. A consumer branching on it must read "unset" as "this implementation -does not report refusals", never as "no grant was refused"; only a present `0` -says the latter. Do not `?? 0` it. - -Optional in the spec composes with the plugin-local narrowing: an -implementation that counts refusals may require the key on its own subtype -(`SharingRuleReconcilePassResult extends SharingRuleEvaluationResult`), a legal -covariant narrowing that still satisfies `ISharingRuleService`. diff --git a/.changeset/signup-existing-address-explicit-refusal.md b/.changeset/signup-existing-address-explicit-refusal.md deleted file mode 100644 index f642783331..0000000000 --- a/.changeset/signup-existing-address-explicit-refusal.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -"@objectstack/plugin-auth": minor -"@objectstack/spec": patch ---- - -`POST /sign-up/email` for an address that already has a `sys_user` row is refused explicitly, instead of answering 200 for a row that is never written (#15587) - -**This is a wire-behaviour change on one lane**: a call that answers `200 {"token":null,"user":{…}}` today answers `422 USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL` after this change. Nothing is newly admitted — the response that changes is one that reported a creation that never happened. - -### What was measured - -Under audience posture `email_domain` (domain allowlisted, `selfRegistrationPermissionSet` resolvable), a sign-up for an address that already carried a `sys_user` row answered **200 with a freshly minted user id** and persisted nothing: no new `sys_user`, no `sys_account`, and the next sign-in a `401` with nothing anywhere explaining it. The same call on the same population under the `invite_only` default was refused honestly with `422 USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL`. An operator, a provisioning script or the console reading the status code concludes the account exists — and this sits directly on the recovery path a locked-out deployment walks, where widening the posture to let a seeded person register is exactly the remedy an operator is pointed at. - -### The mechanism - -better-auth's sign-up route computes `shouldReturnGenericDuplicateResponse = requireEmailVerification || autoSignIn === false` and, when it is on, answers a duplicate with a synthetic in-memory user instead of throwing. **No insert is attempted and nothing is swallowed**: the vendor's `findUserByEmail` short-circuits ahead of `createUser`, which is why no row and no credential appear. - -The posture is not itself the cause — it is only what arms the shield: a posture that permits self-registration **forces** `requireEmailVerification` on. Holding the posture constant at the `invite_only` default and moving only that flag reproduces the divergence exactly, which also means the defect was never confined to the widened postures: `emailAndPassword.autoSignIn: false` arms the same shield under any posture. - -### The fix - -The uniqueness refusal is raised on the `/sign-up/email` before-hook, the same seam and the same reason the audience-posture refusal is already raised there, and built from better-auth's own `BASE_ERROR_CODES` entry so both lanes answer byte-identically. - -**Order is load-bearing: it runs only for a caller the posture already admitted.** Asking uniqueness first would hand an uninvited stranger an account-existence oracle under the `invite_only` default (422 for a real address versus 403 for an unknown one). After the gate, `invite_only` is untouched — a stranger still gets `SELF_REGISTRATION_CLOSED` and learns nothing. - -**Operators of `open` / `email_domain` should know what the honest refusal costs:** on those postures a caller the audience gate admits can now distinguish an address that has an account from one that does not, where the synthetic 200 previously hid it. That is the disclosure the `invite_only` lane has always made to an invitation holder, and the platform's answer for a widened posture is now the same fact rather than a false receipt. - -`USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL` is registered in the ADR-0112 error-code ledger under `@objectstack/plugin-auth`: the platform now **emits** it rather than only passing it through, and an emitted-but-unregistered code is the silent fourth state that ledger exists to prevent. diff --git a/.changeset/single-kernel-tenancy-posture-provider.md b/.changeset/single-kernel-tenancy-posture-provider.md deleted file mode 100644 index 684f6b19c1..0000000000 --- a/.changeset/single-kernel-tenancy-posture-provider.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -"@objectstack/rest": patch -"@objectstack/core": patch ---- - -fix(rest,core): an organization-less or ex-member API key on a walled single-kernel deployment now answers 401 where it answered 200 - -Under a wall-enforcing tenancy posture (`isolated`), an API key stamped with an -organization its owner is no longer a member of **read and wrote that -organization's rows** on the wiring the open core actually builds. Not a silent -empty set — a GET that returned the other organization's records, and a POST -that landed a row read back from the store carrying that organization's id and -the ex-member as its creator. An organization-less key on the same deployment -read `200` with an empty set, which is the silent failure the wall exists to -replace. - -The cause was a seam, not a predicate. `RestServer.computeExecCtx` derived the -effective tenancy posture from a per-request kernel, and on the single-kernel -wiring there is no per-request kernel — so the posture was `undefined` on every -request, and both posture-conditional API-key refusals are gated on it: -`organization_required` in `api-key.ts` and `organization_membership_ended` in -`resolve-authz-context.ts`. Neither ever ran. The Layer 0 wall itself was -active the whole time; it compares against the caller's active organization, -and an API key's tenant is `sys_api_key.active_organization_id` copied verbatim -— the holder's own stored claim. Enforcing the wall is what let the ex-member -through, because the one fact that would expose the ended membership was not an -input to the layer that could act on it. - -The single-kernel branch now derives the posture from a provider `rest-api-plugin` -wires to the lone local kernel's `tenancy` service, in the same shape as the -auth-service provider beside it. A host that registers no `tenancy` service is -unchanged and still admits: there is no wall on such a deployment, so there is -nothing for an organization-less key to be walled out of. A `tenancy` service -that was registered and **failed to build** is an outage and answers `503`, not -an admission — a posture that could not be read is not a posture that is absent. - -Refusals are now also said out loud on the server side, at `warn`, where each -one is decided: the key's row id (never the credential or its hash), the -principal, the organization and the reason. **The wire is unchanged** — both -refusals still answer the generic `401 UNAUTHENTICATED` with no reason in the -body, so a holder of someone else's key learns nothing a plain 401 does not -already tell them. The operator, who previously had a key that was neither -revoked nor expired and a 401 that said nothing, now has a line to find. - -Behaviour that does not move: a current member's key on the same route still -returns its rows and still writes; a request with no credential still answers -401; and an unknown, revoked or expired key is not a refusal at all, so a key -scanner produces no log volume. diff --git a/.changeset/spec-notification-event-migration-id.md b/.changeset/spec-notification-event-migration-id.md deleted file mode 100644 index 786e4da2a7..0000000000 --- a/.changeset/spec-notification-event-migration-id.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -`@objectstack/spec/system` now names the ADR-0030 notification cut-over, so "has this deployment run it?" has a place to be answered. - -`sys_migration` is the ledger a deployment writes to record that a data migration ran against its own database, and consumers read it instead of the platform version. Its well-known ids were `adr-0104-file-references` and `adr-0104-value-shapes` — the two ADR-0104 scans, both driven by an `os migrate` command that records the row. `migrateSysNotificationToEvent` (`@objectstack/metadata/migrations`) had none. It is destructive and one-way, operators are handed the call verbatim in `docs/handoff/adr-0030-notification-convergence.md`, and it recorded nothing when it ran: a deployment that performed the cut-over and one that never did are indistinguishable from the ledger. A row can only be keyed by an id, so without one the question had nowhere to be answered even in principle. - -Added: `NOTIFICATION_EVENT_MIGRATION_ID = 'adr-0030-notification-event'`, exported from `@objectstack/spec/system`. Purely additive — no existing export, schema or predicate changes, and nothing reads the new id yet. - -Deliberately NOT decided here, and the constant's docblock says so rather than leaving its silence to be read as an answer: what a `sys_migration` row under this id means. The two ADR-0104 ids get their `last_run_at` / `applied_at` / `verified_at` / `blocking` semantics from a command that scans, self-checks and only then records; this migration has no command and no self-check, and reports `migrated` / `already_done` / `not_applicable` / `error` to its caller instead. Which of those columns one of its runs may claim, whether anything may gate on the row, and whether a datastore created after the cut-over belongs in `CREATION_ATTESTED_MIGRATION_IDS`, are contract questions on this surface and are left open. diff --git a/.changeset/stranded-run-status-stamp.md b/.changeset/stranded-run-status-stamp.md deleted file mode 100644 index 167063169e..0000000000 --- a/.changeset/stranded-run-status-stamp.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -"@objectstack/service-automation": minor ---- - -feat(automation): a resume that consumed the pause and then failed downstream answers `status: 'stranded'` (#13937) - -The services half of the #13937 shape-4 ruling (maintainer 2026-09-01): -`resumeInternal`'s consumption order is kept — the suspension is consumed -before downstream nodes run, which is what buys exactly-once across a crash — -and the state that order leaves behind when a downstream node throws now -carries the platform-level name #14384 put on the contract. - -`AutomationEngine.resume()` (and every engine continuation that reaches the -same catch arm) returns `{ success: false, status: 'stranded', … }` where it -returned no `status` at all. Stamped on that one exit only: the pause a -durable decision was waiting on is gone, the run is recorded `failed`, and it -can be re-armed only by the explicit operator verb -`restoreConsumedSuspension` (#13909 slice 2, already published) — never by -`resume` (which answers `RUN_NOT_FOUND`) and never automatically. Distinct -from `'failed'` on purpose: that one says the run ran and was rejected; this -one says a recorded continuation stopped mid-flight and an operator has -something to repair. The result's verdict and the restore verb are held to -agree by test: a stranded result is exactly a restorable run. - -Not changed: the run's RECORDED status (the run log, `getRun`, `listRuns`, the -durable `sys_automation_run` history row) stays `failed` — that vocabulary is -`ExecutionStatus` in `@objectstack/spec`, which the ruling did not widen; the -durable discriminator for the condition remains the snapshot the terminal row -carries. No resume semantics move for any pausing node type; shapes 2 and 3 -of the decision stay excluded. - -Also in this change, under the same ruling's exactly-once guarantee, two -repairs to how `restoreConsumedSuspension` finds a stranded run's snapshot: - -- The durable run-history row of a stranded run now records the PAUSE node in - `node_id`. It recorded the node that threw — the run's last step — and the - object store read that column back as the snapshot's node, so a restore - from the row (after a restart, or on another replica) re-armed the run at - the failed node and the next resume skipped it while reporting the run - completed. The throwing node stays in the row's step log and `error`. - Visible on the Runs surface: `sys_automation_run`'s row title and highlight - set are built from `node_id` (`titleFormat '{flow_name} · {node_id}'`), so a - stranded run's row now names the PAUSED node — the one an operator can - re-arm — where it named the node that threw; ordinary completed / failed - rows are unchanged. The `node_id` and `variables_json` field descriptions - carry this carve-out, the way `node_type`'s already did. -- The verb reads the durable row and its own per-process journal as two - witnesses of one strand instead of trusting either alone. The hot copy is - preferred when both describe the same pause (it is the verbatim object the - failure was journalled from). A row that carries no snapshot is read as - "the run moved on" only when this process's own history write landed — - the replica that stranded a run used to keep a hot copy that could re-arm - the run after another replica had restored, resumed and finished it, and - the next resume re-ran every node after the pause. A snapshot the object - store could not persist (over its 256 KiB row budget) is now recorded in - the row as dropped, with the pause it belonged to, so the replica holding - the hot copy still restores and any other replica is refused with a reason - that names the budget and the remedy. - -In-memory and store-less deployments observe no behaviour difference. On the -object store, same-replica restores re-arm the pause node on every path, and -restores from the row alone do too; restores across replicas of a run that -finished elsewhere are refused. diff --git a/.changeset/structural-condition-shape-refused.md b/.changeset/structural-condition-shape-refused.md deleted file mode 100644 index a357b8759b..0000000000 --- a/.changeset/structural-condition-shape-refused.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@objectstack/spec": minor -"@objectstack/service-automation": minor -"@objectstack/lint": minor ---- - -A flow condition that is neither CEL text nor an expression is now refused at build time, instead of being read as an empty condition and answering a silent `false`. - -`evaluateCondition` derives its source as `typeof expression === 'string' ? expression : (expression?.source ?? '')`. For a value that is neither — a number, a boolean, an array — the read yields `undefined`, the `??` supplies `''`, and the empty-source arm returns **`false`**: the "an unauthored branch must not open" rule, applied to a value that was very much authored. Measured: a `decision` node carrying `config: { condition: 42 }` **registered clean** and executed `success: true` with nothing said at any layer; `{ source: 1 }` did not even get that far and threw a bare `TypeError: exprStr.trim is not a function` out of the validator. `config.condition` is also the key a **start node's trigger gate** is read from, so the same value could gate a whole flow shut forever with no signal to the author. - -- The new `structuralConditionRefusal` / `STRUCTURAL_CONDITION_SHAPE_REFUSAL` in `@objectstack/spec/automation` are the single shared notion of why, read by both validators so build time and author time cannot disagree about the shape. `registerFlow` throws, naming the node or edge and attributing the finding; `objectstack validate` reports the same refusal as a located `error`. - -**This is deliberately NOT the `predicate`-slot rule, and the difference is measured.** A ledger `predicate` slot (`decision.conditions[].expression`, a screen field's `visibleWhen`) is declared `z.string()`, so `PREDICATE_SLOT_STRING_REFUSAL` refuses every non-string including an envelope. Neither structural slot is declared that way: `FlowEdgeSchema.condition` is `ExpressionInputSchema`, whose string arm **transforms into** `{ dialect: 'cel', source }` — so after `FlowSchema.parse` every authored edge condition *is* an envelope — and `FlowNodeSchema.config` is an open `z.record` that passes an envelope written at `config.condition` through verbatim, where `evaluateCondition` evaluates it correctly. Both shapes stay accepted here; an envelope with no `dialect`, and an `ast`-carrying one (`ExpressionSchema`'s own `source`-or-`ast` rule), stay accepted too. - -**Strings are untouched, deliberately.** A whitespace-only condition still means "not authored" and still answers `false` on both sides — consistent behaviour, ruled correct, not a defect. What a non-empty string *says* is still `validateExpression('predicate', …)`'s verdict, brace trap and all. Only the shape moved. - -An app that authored a number, a boolean, an array or a source-less object in a node or edge `condition` now fails to register with a message naming the site; the fix is to write the condition as bare CEL text (`record.rating >= 4`) or as an expression envelope. diff --git a/.changeset/studio-object-field-ref-refusal.md b/.changeset/studio-object-field-ref-refusal.md deleted file mode 100644 index bbed1c08b0..0000000000 --- a/.changeset/studio-object-field-ref-refusal.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -"@objectstack/lint": minor -"@objectstack/metadata-protocol": minor ---- - -A publish now refuses an object whose `highlightFields` names a field that does not exist on it — the same gate that refuses a code-authored stack. - -`list-view-field-unknown` inspects `view.columns`, and Studio's app builder mints no `view` items at all, so the reference-integrity family had nothing to inspect on the only artifacts the click path authors. What it authors is the **object**, and an object-level field-name list was covered by nothing that could refuse: measured on `origin/main`, `runtimeAuthoringRulesFor('object')` dispatched seven rules with no reference-integrity rule among them, while the object-level existence check that did exist (`semantic-role-field-unknown`) is `warning`, advisory-tier and CLI-only. So `os validate` exited 0 on a dangling reference and the runtime publish door — the only door a Studio, REST `/meta` or MCP author has — said nothing at all. - -The reproduction is the natural click order, not a contrived one: click-create a field (Studio mints it as `field_10`), add it to `highlightFields`, then give it a label — the API name auto-derives to `health_score` and `highlightFields` keeps `field_10`. Anyone who names a field after placing it produces this. - -- **New rule `object-field-ref-unknown` (`error`)**, in `@objectstack/lint`, over the object-level field-name **lists** that no rule owned: `highlightFields` (ADR-0085) and `publicSharing.redactFields`. It resolves through the same `object-graph` seam as the rest of the family, so the three shared skips hold — an object outside the stack, an object with no readable field map (ADR-0015 `external`), and a registry-injected system column resolved **per object** (`highlightFields: ['owner_id']` is a live pointer on an owned object and a real miss under `ownership: 'none'`). -- **It runs on the runtime publish door.** The reference-integrity suite entry's `runtimeTypes` gains `object`, and the suite's per-member declaration keeps the crossing narrow: this is the only member that judges an object snapshot; every other member keeps `['flow', 'view']` or the frozen `['flow']` default. -- **`validateSemanticRoles` keeps the provenance question** at the same position (`semantic-role-field-unprovisioned`, still `warning`) and no longer restates existence — one finding per path, at one tier. -- **`probes.checked` gained an `objects` counter.** Its absence was the tell: a receipt reading `{seeds: 0, views: 0, widgets: 0}` was accurate while the objects the package published were probed by nothing. - -## Migration - -**A publish that used to succeed can now be refused (HTTP 422, `INVALID_METADATA`).** The receipt names the rule id `object-field-ref-unknown` and the offending path, name-keyed on the wire — for example `objects.proj_task.highlightFields[1]` — plus the string that was written and the fields the object actually has. - -To fix a dangling reference, do one of: - -- rewrite the entry to the field's current API name (after a Studio label edit the derived name is the one to use — `field_10` becomes `health_score`); or -- drop the entry from the list. - -`os validate` / `os build` / `os lint` report the same finding at `error`, so a stack can be repaired before it reaches a publish. If an object legitimately points at a platform-injected system column, no change is needed — the rule resolves those per object and stays silent where the platform really provisions them. diff --git a/.changeset/summary-backfill-recompute-undefined-on-empty.md b/.changeset/summary-backfill-recompute-undefined-on-empty.md deleted file mode 100644 index 4ab5fa0ed8..0000000000 --- a/.changeset/summary-backfill-recompute-undefined-on-empty.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -"@objectstack/objectql": minor -"@objectstack/cli": minor ---- - -feat(objectql,cli): `backfillSummaryNulls` accepts `recomputeUndefinedOnEmpty` — a caller who KNOWS a `min`/`max`/`avg` roll-up column was just declared can have it filled; `os migrate summary-nulls --recompute-undefined-on-empty object.field` surfaces it (#15064) - -A roll-up value has three producers — the insert-time seed, the child-write -recompute, and the one-off backfill — and **declaring a summary field on an -object that already has rows reaches none of them**. For `count`/`sum` the -backfill repairs that as a side effect (every `NULL` is a hole to it). For -`min`/`max`/`avg` it could not: `summaryNullIsBackfillable` decides on the -function alone, so "never computed" and "no child rows" were indistinguishable, -the column stayed `NULL` on every pre-existing parent, and the report said -`filled: 0` — a false all-clear that a timed flow built on the column then -turned into "matches nothing" (the customer case behind cloud#1908). - -**What changes** — maintainer ruling on #15064, option A: the caller who holds -the fact gets a way to say it; the predicate and the default run do not move. - -- `SummaryBackfillOptions.recomputeUndefinedOnEmpty?: string[]` — `object.field` - roll-ups the caller knows were never computed. A named `min`/`max`/`avg` is - walked like a `count`: every `NULL` parent is recomputed through the same - `aggregateSummaryValue` the engine writes. A parent whose aggregate is the - empty-set reading (`null` — no child rows) already holds the engine's own - value, so it is neither counted as a hole nor written; the scoped run is - therefore idempotent in the same "re-run until it reports zero" sense. - Naming a `count`/`sum` is accepted and changes nothing, so a publish path can - pass every column it just declared without knowing the empty-set list. -- A name that resolves to no roll-up owned by an object the run walks — a typo, - a plain field, or an object `objects` left out — is **refused before any row - is read**, dry run or apply, with an ADR-0112 envelope (`code: - 'INVALID_FIELD'`, `status: 400` — the code the projection and write axes - that name a field already answer, while sorting keeps `INVALID_SORT`; - `field` names the first unresolved entry, `fields` all of them). A silent - no-op there would be the same false all-clear this option exists to end. -- `SummaryBackfillReport.recomputedUndefinedOnEmpty: string[]` — the complement - of `skippedUndefinedOnEmpty`, same `object.field (fn)` spelling; `[]` on an - unscoped run. `SummaryBackfillFieldOutcome.fn` widens from `'count' | 'sum'` - to every roll-up function, since a named `max` now appears in `fields`. -- `os migrate summary-nulls --recompute-undefined-on-empty object.field` - (repeatable) passes the scope through; the confirmation prompt names the - columns; `formatSummaryBackfillReport` lists them under "Recomputed on - request" and explains a `NULL` that remains. - -**What does not change:** without the option the walk, the writes, every -counter and the human-readable report are byte-for-byte what they were (pinned -against output captured on `main` before this change); `min`/`max`/`avg` stay -out of scope and keep being reported under `skippedUndefinedOnEmpty`; the -predicate `summaryNullIsBackfillable` is untouched, so `os migrate -summary-nulls` keeps its meaning on every deployment. The only visible delta on -an unscoped run is the one additive report key, `recomputedUndefinedOnEmpty: []`. - -`minor` for both packages: an optional parameter on a published exported -function, a new report key, and a new CLI flag are each a purely additive -widening of a published surface, which takes at least `minor` (bump-level rule, -2026-09-04); the `fix`-shaped motivation does not lower it. diff --git a/.changeset/sys-email-error-description-widen.md b/.changeset/sys-email-error-description-widen.md deleted file mode 100644 index e27ce70c96..0000000000 --- a/.changeset/sys-email-error-description-widen.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -"@objectstack/platform-objects": patch ---- - -fix(platform-objects): `sys_email.error` field help now covers pre-delivery rejections, not only transport failures - -`sys_email.error` was declared as *"Transport error message when status=failed"*. -Since `EmailService.recordRejectedMessage` landed, the same column also carries -the reason a message was rejected by `normalizeMessage` **before** it reached a -transport (an unsendable `from`, no recipient, no subject, no body) — those rows -are written with `status: 'failed'` too, prefixed `rejected before delivery: `. - -Nothing was misleading in the *data*: the row prefixes its own reason, so an -operator reading a failed row is never sent chasing an SMTP host for a message -that never reached one. What was stale was the field's declared `description`, -which Studio surfaces as the field's help text — it named only the transport -case, narrower than what the column has held since that change landed. - -The description now reads: *"Why the message failed — a transport error, or the -validation that rejected it before delivery."* It stays true under both row -shapes and deliberately does not name the row's own `rejected before delivery:` -prefix, so it will not go stale again if that prefix's wording changes. diff --git a/.changeset/today-offset-one-calendar.md b/.changeset/today-offset-one-calendar.md deleted file mode 100644 index 60babce8b2..0000000000 --- a/.changeset/today-offset-one-calendar.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@objectstack/service-automation": patch ---- - -Flow templates: `{TODAY() + n}` and `{TODAY() - n}` now do their day arithmetic on the same calendar they render on (UTC), so the resolved date no longer lands a day off across a DST transition. - -The offset branch of the template resolver shifted the day on the **local** calendar (`getDate` / `setDate`) and then rendered the result on the **UTC** one (`toISOString`). `setDate` preserves wall-clock time, so a local day shift moves the underlying instant by exactly n x 24 hours only while every local day in the window is 24 hours long. Across a spring-forward the window is 23 hours and across a fall-back 25, and when that one hour of slack crosses a UTC midnight the rendered date comes out a day early (spring-forward) or a day late (fall-back). - -The window is narrow — roughly one hour per DST-observing zone, twice a year — but the values written through it persist: a quote expiration, a follow-up date, a close date. Measured across 34 zones at every 30 minutes of 2026 for offsets `+1` and `-1` (1,191,360 instant-offset pairs), the old spelling disagreed with the UTC day in 190 of them, spread over 24 DST-observing zones; the new spelling disagrees in none. - -The same branch serves `{NOW() + n}`, which likewise now moves the instant by exactly n x 24 hours instead of preserving a wall-clock time across the transition. - -Nothing else moves. The bare `{TODAY()}` and `{NOW()}` forms never entered this branch and are byte-for-byte unchanged — they already resolved on UTC, and the offset forms now agree with them. This is not a timezone feature: these tokens remain timezone-unaware by design, and whether they should be is a separate question. diff --git a/.changeset/truthful-stranded-decision-envelope.md b/.changeset/truthful-stranded-decision-envelope.md deleted file mode 100644 index 1f8a3facda..0000000000 --- a/.changeset/truthful-stranded-decision-envelope.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -"@objectstack/types": minor -"@objectstack/plugin-approvals": minor -"@objectstack/rest": minor ---- - -An approval decision that lands while its flow run strands now says so in fields, not only in prose. - -`POST /api/v1/approvals/requests/{id}/reject` — and its sibling decision doors — could produce three coexisting outcomes from one call: the caller read HTTP 500, the request row **was** in its terminal status and had left the pending inbox, and the workflow run was stranded. A caller reading 500 has one honest inference available — "the rejection did not happen" — and it was the wrong one, so scripts and operators retried or escalated against a decision that was already durable. The only carrier of the truth was English prose in `error`, so finding the affected run meant regexing a run id out of a sentence, and nothing said whether that run could be repaired at all. - -The 500 stays. A recorded decision whose flow never advances is still a failure and is still reported as one; the door does not become atomic and no decision is ever rolled back. What changed is that it stops discarding what the engine already said: - -- **The `RESUME_FAILED` body gains four fields**, additively — `finalized` (always `true`: the decision stands), `decision`, `runId`, and `repairable`. Existing consumers see the same `code`, the same `error` and the same status. -- **`repairable` carries the engine's own discriminator** — `AutomationResult.status === 'stranded'`, the state stamped on exactly the exit that journals a repair snapshot. `false` is the answer for every other failure, including a lost run: absence of the signal is not repairability, and a repair verb that would refuse is worse than no promise. -- **`serviceResume` carries `status`** through to the door. It previously read only `success` / `code` / `error`, and the stranded exit reports a `status` and no `code` at all — so the platform's own repairability signal died one line before the envelope was built. - -`@objectstack/types` gains `strandedDecisionFailure` / `strandedDecisionDetails` and the `StrandedDecisionDetails` type — the constructor and its recogniser in one module, so the producing service and the REST door cannot drift. A `RESUME_FAILED` raised without that carrier answers exactly the body it always did; the door never synthesises the envelope. diff --git a/.changeset/try-catch-error-value-code-key.md b/.changeset/try-catch-error-value-code-key.md deleted file mode 100644 index 0fbbeed2da..0000000000 --- a/.changeset/try-catch-error-value-code-key.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@objectstack/spec": minor ---- - -feat(spec): `TryCatchErrorValueSchema` declares the `code` key the `try_catch` engine binds (#14954) - -`TryCatchErrorValue` — the ONE shape the catch region's author, the engine and the run log share for the value a `try_catch` binds to `errorVariable` (default `$error`) — gains an optional `code: string`: the platform-classified error code (ADR-0112) the failing node's own result carried, e.g. `create_record`'s `DUPLICATE_RECORD`. The engine has bound it since `@objectstack/service-automation`'s #14419 change; the schema was a plain `z.object` that did not declare it, so a round-trip through the declared shape silently STRIPPED the key the engine had put there, and the generated reference page documented four keys where the runtime binds five. The `errorVariable` description on `TryCatchConfig` names `code` too, so the authorable surface documents branching on `$error.code`. - -Typed as an open `string`, deliberately not `StandardErrorCode` and not the ledger union: ADR-0112 D3/D4 with the #9106 amendment make the code vocabulary `StandardErrorCode` ∪ registered ledger codes ∪ tenant-authored codes, and `NodeExecutor` is third-party-registrable, so a closed type would be false the moment anyone registers an executor that throws its own code. The closed-at-every-door rule governs `ApiErrorSchema.code` at an HTTP door; this value is bound in-process and never crosses one. - -Additive and optional: every value that parsed before parses byte-identically, and a binding without a classified code still carries no `code` key — absent means "no classified code", never "nothing failed". Semver: a new optional key on a published schema widens the accept set and the exported `TryCatchErrorValue` type without retiring or renaming anything ⇒ `minor`; no ADR-0087 entry is owed because there is nothing an upgrader must migrate. diff --git a/.changeset/undefined-comparand-prescription-position-safe.md b/.changeset/undefined-comparand-prescription-position-safe.md deleted file mode 100644 index 25e5aaf86b..0000000000 --- a/.changeset/undefined-comparand-prescription-position-safe.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@objectstack/spec": patch ---- - -fix(spec): the `undefined` comparand refusal prescribes the null predicate by its ruled spellings (#14426) - -`parseFilterAST`'s comparand-type door refuses an `undefined` comparand at every -position. Its prescription read "Write null for the null predicate, or omit the -key" — position-agnostic advice that, followed at `{ $gt: undefined }`, produced -`{ $gt: null }`, which the 2026-09-01 ruling refuses one door over (and, at an -`$in` / `$nin` / `$between` member, produced the list shapes refused on -2026-08-31). Two loud refusals to reach one right answer. - -The sentence now names the null predicate by its complete spellings — -`{"$eq": null}` / `{"$ne": null}` — or omit the key, so following it never lands -in a refusal at any position the sentence is emitted at. No accept/refuse -behaviour changes: same envelope (`INVALID_FILTER` / 400), same path, same -accepted-set and NOT-applied sentences. diff --git a/.changeset/verify-reads-package-owned-collections.md b/.changeset/verify-reads-package-owned-collections.md deleted file mode 100644 index 66c861ab06..0000000000 --- a/.changeset/verify-reads-package-owned-collections.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -"@objectstack/verify": patch ---- - -fix(verify): `os verify` no longer reports a green run over a multi-package app it measured nothing about - -Every reader in this package took the artifact's **flattened** top level and -nothing else. A multi-package app whose definitions live under `packages[]` — -the shape ADR-0130 D4's option B emits — therefore reached `deriveCrudCases` -with no objects and no datasources, and reached `rlsProbePermissionSet` and -`declaredPositionNames` with no objects and no positions. Nothing threw. The run -derived zero CRUD round-trip cases, built an empty RLS probe permission set, -minted no persona for any declared position, and printed `✓ verify passed`. - -That is the most expensive place in the platform for a false green: `verify`'s -entire job is to be the thing that notices. A missing collection is at least -missing — zero coverage dressed as a passing run is not. - -The four reads now resolve through `resolveArtifactPackageOrder` -(`@objectstack/core`, ADR-0130 D4+D5), **flattened top level first**: - -- `deriveCrudCases` — the objects it derives cases for, and the datasource-by- - name map behind ADR-0015's double write gate. Both, because objects alone - would leave a write-opted-in federated object judged against an empty - datasource map and reported read-only, i.e. skipped by a verifier that says it - covered it. -- `declaredPositionNames` — one RLS persona per declared position. -- `rlsProbePermissionSet` — the object grants and the owner-scoped narrowing - that are what make an RLS run a probe rather than a report about the object - gate. - -The top-level read still answers first and is returned untouched, so an app on -today's additive artifact gets a bit-identical answer, and a stack that declares -an empty collection (`objects: []` is truthy) still gets an empty one. Only a -top level that does not carry the key at all consults `packages[]`. A malformed -`packages` array now surfaces `resolveArtifactPackageOrder`'s ADR-0112 refusal -instead of reading as "this app declares nothing". diff --git a/.changeset/widget-measures-missing-every-family.md b/.changeset/widget-measures-missing-every-family.md deleted file mode 100644 index 97d5c88d9d..0000000000 --- a/.changeset/widget-measures-missing-every-family.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -'@objectstack/lint': minor ---- - -`widget-measures-missing` — the empty-measure selection is reported on every widget family, not just charts - -`chart-measures-missing` (#15462) reported the authoring placeholder only for the chart -family, but the return that produces it is type-independent. At the `@object-ui` revision -this repo pins (`.objectui-sha` = `a472b0716`), `packages/plugin-dashboard/src/DatasetWidget.tsx:683` -reads `if (values.length === 0)` and returns *"Pick measures (values) for this dataset -widget."* ABOVE `isMetric` (`:423`, over `METRIC_TYPES` at `:343`), `isTable` (`:424`) and -the chart branch alike. So a `metric`, `kpi`, `gauge`, `solid-gauge`, `bullet`, `table` or -`pivot` widget that selects no measures renders the same placeholder — the KPI number or -the table the author declared is not drawn at all — and nothing reported it: -`table-count-only` requires `values.length > 0` before it looks, and the rules that iterate -`dimensions[]`/`values[]` are silent on an empty array by construction. - -- **New id `widget-measures-missing`** — a NON-chart declared widget type selects no - measures. Warning tier, suppressible per widget with - `suppressWarnings: ['widget-measures-missing']`, exactly as the chart-family id is. The - message states the consequence its family actually has (the single KPI number is not - drawn / no table is rendered) and the hint names the dataset's declared measures. -- **`chart-measures-missing` is unchanged** — same id, same chart-family population, same - message and same suppression. The condition split rather than widened because "chart" - stops naming it once the population is every family, while the old id is reachable from - the package barrel (a public-surface contract) and may already be written into a board's - `suppressWarnings`. -- `chart-dimensions-missing` stays chart-family only: a dimensionless `metric` or `table` - is what those families are for. - -The two never double-report one widget, in the pin's own order: the measures check runs -before the dimensions one, and `table-count-only` already skips an empty selection. diff --git a/content/docs/deployment/self-hosting.mdx b/content/docs/deployment/self-hosting.mdx index 34985fe037..f7961b1b7a 100644 --- a/content/docs/deployment/self-hosting.mdx +++ b/content/docs/deployment/self-hosting.mdx @@ -74,7 +74,7 @@ docker run -p 8080:8080 \ -e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \ -e OS_AUTH_SECRET \ -e OS_SECRET_KEY \ - ghcr.io/objectstack-ai/objectstack:17.3.0 + ghcr.io/objectstack-ai/objectstack:17.4.0 ``` (`OS_ARTIFACT_PATH` also accepts an `https://` URL, so the artifact can come @@ -92,7 +92,7 @@ docker run -p 8080:8080 \ -e OS_ARTIFACT_URL="https://releases.example.com/hotcrm-2.2.2.json#sha256=<64 hex chars>" \ -e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \ -e OS_AUTH_SECRET -e OS_SECRET_KEY \ - ghcr.io/objectstack-ai/objectstack:17.3.0 + ghcr.io/objectstack-ai/objectstack:17.4.0 ``` Both schemes work: `https://…` is fetched at boot, `file:///…` is read directly @@ -143,7 +143,7 @@ COPY . . RUN npx os build # → dist/objectstack.json # ── Runtime: the official ObjectStack runtime image ────────────────── -FROM ghcr.io/objectstack-ai/objectstack:17.3.0 +FROM ghcr.io/objectstack-ai/objectstack:17.4.0 COPY --from=build --chown=node:node /app/dist/objectstack.json /srv/app/objectstack.json ``` @@ -161,7 +161,7 @@ image)? The official image is nothing more than: ```dockerfile title="Dockerfile (self-built runtime, equivalent)" FROM node:22-slim -RUN npm install -g @objectstack/cli@17.3.0 +RUN npm install -g @objectstack/cli@17.4.0 WORKDIR /srv/app RUN chown node:node /srv/app diff --git a/content/docs/upgrading.mdx b/content/docs/upgrading.mdx index 45ffe164a3..5bd97108de 100644 --- a/content/docs/upgrading.mdx +++ b/content/docs/upgrading.mdx @@ -51,7 +51,7 @@ The official image is `ghcr.io/objectstack-ai/objectstack`, and its tags mirror ```bash # docker-compose.yml, or your orchestrator's manifest -image: ghcr.io/objectstack-ai/objectstack:17.3.0 +image: ghcr.io/objectstack-ai/objectstack:17.4.0 ``` On a host running the artifact directly under systemd, the same move is a file diff --git a/docker/README.md b/docker/README.md index 662511ffb6..75bd9cf7dc 100644 --- a/docker/README.md +++ b/docker/README.md @@ -29,7 +29,7 @@ Multi-arch: `linux/amd64` + `linux/arm64`. [Self-Hosted Deployment](https://objectstack.ai/docs/deployment/self-hosting)): ```dockerfile -FROM ghcr.io/objectstack-ai/objectstack:17.3.0 +FROM ghcr.io/objectstack-ai/objectstack:17.4.0 COPY --chown=node:node dist/objectstack.json /srv/app/objectstack.json ``` @@ -40,7 +40,7 @@ docker run -p 8080:8080 \ -v "$PWD/dist/objectstack.json:/srv/app/objectstack.json:ro" \ -e OS_DATABASE_URL="postgres://user:pass@db-host:5432/myapp" \ -e OS_AUTH_SECRET -e OS_SECRET_KEY \ - ghcr.io/objectstack-ai/objectstack:17.3.0 + ghcr.io/objectstack-ai/objectstack:17.4.0 ``` `OS_ARTIFACT_PATH` also accepts an `https://` URL, so the artifact can come @@ -72,7 +72,7 @@ for a `file:…` path — one box only, wrong for multi-node) and MongoDB (`libsql://…` / Turso). Add one by extending the image: ```dockerfile -FROM ghcr.io/objectstack-ai/objectstack:17.3.0 +FROM ghcr.io/objectstack-ai/objectstack:17.4.0 USER root RUN npm install -g tedious USER node @@ -100,5 +100,5 @@ reverse-proxy / multi-node guidance: ## Local build of this image ```bash -docker build -t objectstack:dev --build-arg OS_CLI_VERSION=17.3.0 docker/ +docker build -t objectstack:dev --build-arg OS_CLI_VERSION=17.4.0 docker/ ``` diff --git a/examples/app-crm/CHANGELOG.md b/examples/app-crm/CHANGELOG.md index 0e73abb995..d8bfdd3a15 100644 --- a/examples/app-crm/CHANGELOG.md +++ b/examples/app-crm/CHANGELOG.md @@ -1,5 +1,46 @@ # @objectstack/example-crm +## 4.0.96 + +### Patch Changes + +- Updated dependencies [98191d2] +- Updated dependencies [ca326b5] +- Updated dependencies [f1a1028] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [8a12067] +- Updated dependencies [ee32e1c] +- Updated dependencies [8744de9] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/runtime@17.4.0 + - @objectstack/spec@17.4.0 + ## 4.0.95 ### Patch Changes diff --git a/examples/app-crm/package.json b/examples/app-crm/package.json index 52e8f9f7e7..c7113ba71e 100644 --- a/examples/app-crm/package.json +++ b/examples/app-crm/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-crm", - "version": "4.0.95", + "version": "4.0.96", "description": "Minimal CRM example \u2014 a smoke-test workspace that exercises the metadata loading pipeline (objects \u2192 views \u2192 app \u2192 dashboard \u2192 hook \u2192 flow \u2192 seed). For a full-featured enterprise CRM see https://github.com/objectstack-ai/hotcrm.", "license": "Apache-2.0", "private": true, diff --git a/examples/app-multi-package/CHANGELOG.md b/examples/app-multi-package/CHANGELOG.md index 6c1bf2e69f..96ff5f7c88 100644 --- a/examples/app-multi-package/CHANGELOG.md +++ b/examples/app-multi-package/CHANGELOG.md @@ -1,5 +1,40 @@ # @objectstack/example-multi-package +## 0.0.3 + +### Patch Changes + +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/spec@17.4.0 + ## 0.0.2 ### Patch Changes diff --git a/examples/app-multi-package/package.json b/examples/app-multi-package/package.json index d42021fed6..4955290209 100644 --- a/examples/app-multi-package/package.json +++ b/examples/app-multi-package/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-multi-package", - "version": "0.0.2", + "version": "0.0.3", "description": "One release artifact carrying TWO packages that share a namespace (ADR-0130 D4) — the producer-side fixture for `packages[]`", "license": "Apache-2.0", "private": true, diff --git a/examples/app-showcase/CHANGELOG.md b/examples/app-showcase/CHANGELOG.md index 7810529083..db9e7723d8 100644 --- a/examples/app-showcase/CHANGELOG.md +++ b/examples/app-showcase/CHANGELOG.md @@ -1,5 +1,58 @@ # @objectstack/example-showcase +## 0.3.18 + +### Patch Changes + +- Updated dependencies [54bb2f1] +- Updated dependencies [98191d2] +- Updated dependencies [ca326b5] +- Updated dependencies [f1a1028] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [a646120] +- Updated dependencies [2200f8e] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [6b66ec7] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [61821e5] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [8a12067] +- Updated dependencies [ee32e1c] +- Updated dependencies [8744de9] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/driver-sql@17.4.0 + - @objectstack/runtime@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/cloud-connection@17.4.0 + - @objectstack/connector-mcp@17.4.0 + - @objectstack/connector-openapi@17.4.0 + - @objectstack/connector-rest@17.4.0 + - @objectstack/connector-slack@17.4.0 + - @objectstack/service-datasource@17.4.0 + ## 0.3.17 ### Patch Changes diff --git a/examples/app-showcase/package.json b/examples/app-showcase/package.json index 7b3ddb7abd..2b8d667cc7 100644 --- a/examples/app-showcase/package.json +++ b/examples/app-showcase/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-showcase", - "version": "0.3.17", + "version": "0.3.18", "description": "Kitchen-sink showcase workspace — exercises every metadata type, every view type, every chart type, and the major end-to-end capability chains (security, automation, analytics). Built for demonstration, debugging, and coverage-driven verification.", "license": "Apache-2.0", "private": true, diff --git a/examples/app-todo/CHANGELOG.md b/examples/app-todo/CHANGELOG.md index 708ff61b8a..f735357cef 100644 --- a/examples/app-todo/CHANGELOG.md +++ b/examples/app-todo/CHANGELOG.md @@ -1,5 +1,63 @@ # @objectstack/example-todo +## 4.0.96 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [98191d2] +- Updated dependencies [ca326b5] +- Updated dependencies [f1a1028] +- Updated dependencies [8f404a5] +- Updated dependencies [a56baa2] +- Updated dependencies [3e3ecb0] +- Updated dependencies [4b3955e] +- Updated dependencies [e944fdb] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [65846bc] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [17f8604] +- Updated dependencies [3bd9b34] +- Updated dependencies [d0ee598] +- Updated dependencies [26144c2] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [8a12067] +- Updated dependencies [ee32e1c] +- Updated dependencies [8744de9] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [ec0a6e7] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/objectql@17.4.0 + - @objectstack/runtime@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/metadata@17.4.0 + - @objectstack/client@17.4.0 + - @objectstack/mcp@17.4.0 + - @objectstack/driver-sqlite-wasm@17.4.0 + - @objectstack/knowledge-memory@17.4.0 + - @objectstack/service-knowledge@17.4.0 + ## 4.0.95 ### Patch Changes diff --git a/examples/app-todo/package.json b/examples/app-todo/package.json index 0e20505d1f..7986269b5d 100644 --- a/examples/app-todo/package.json +++ b/examples/app-todo/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-todo", - "version": "4.0.95", + "version": "4.0.96", "description": "Example Todo App using ObjectStack Protocol", "license": "Apache-2.0", "private": true, diff --git a/examples/embed-objectql/CHANGELOG.md b/examples/embed-objectql/CHANGELOG.md index eaf309f262..e2ec7b70dd 100644 --- a/examples/embed-objectql/CHANGELOG.md +++ b/examples/embed-objectql/CHANGELOG.md @@ -1,5 +1,52 @@ # @objectstack/example-embed-objectql +## 0.0.36 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [a56baa2] +- Updated dependencies [3e3ecb0] +- Updated dependencies [4b3955e] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [2003259] +- Updated dependencies [a646120] +- Updated dependencies [65846bc] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [3bd9b34] +- Updated dependencies [d0ee598] +- Updated dependencies [26144c2] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [ec0a6e7] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/objectql@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/driver-memory@17.4.0 + ## 0.0.35 ### Patch Changes diff --git a/examples/embed-objectql/package.json b/examples/embed-objectql/package.json index 476208f119..8df297d12a 100644 --- a/examples/embed-objectql/package.json +++ b/examples/embed-objectql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/example-embed-objectql", - "version": "0.0.35", + "version": "0.0.36", "private": true, "description": "Embed the ObjectQL engine as a plain library via @objectstack/objectql/core — no kernel, no plugins, no metadata protocol (ADR-0076).", "type": "module", diff --git a/packages/adapters/hono/CHANGELOG.md b/packages/adapters/hono/CHANGELOG.md index a4f64569a0..5e84abb431 100644 --- a/packages/adapters/hono/CHANGELOG.md +++ b/packages/adapters/hono/CHANGELOG.md @@ -1,5 +1,23 @@ # @objectstack/hono +## 17.4.0 + +### Patch Changes + +- Updated dependencies [98191d2] +- Updated dependencies [f1a1028] +- Updated dependencies [2c753fe] +- Updated dependencies [fa85759] +- Updated dependencies [5f7fa1d] +- Updated dependencies [088f761] +- Updated dependencies [8a12067] +- Updated dependencies [ee32e1c] +- Updated dependencies [8744de9] +- Updated dependencies [3d3f60e] + - @objectstack/runtime@17.4.0 + - @objectstack/plugin-hono-server@17.4.0 + - @objectstack/types@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/adapters/hono/package.json b/packages/adapters/hono/package.json index a24d20a546..e5be4d6309 100644 --- a/packages/adapters/hono/package.json +++ b/packages/adapters/hono/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/hono", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/apps/account/CHANGELOG.md b/packages/apps/account/CHANGELOG.md index cf15cb8e37..6c30acffab 100644 --- a/packages/apps/account/CHANGELOG.md +++ b/packages/apps/account/CHANGELOG.md @@ -1,5 +1,42 @@ # @objectstack/account +## 17.4.0 + +### Patch Changes + +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [2bb0614] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/spec@17.4.0 + - @objectstack/platform-objects@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/apps/account/package.json b/packages/apps/account/package.json index f84317e009..51fc18f55a 100644 --- a/packages/apps/account/package.json +++ b/packages/apps/account/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/account", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "ObjectStack Account — the end-user account/self-service console app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", "main": "dist/index.js", diff --git a/packages/apps/setup/CHANGELOG.md b/packages/apps/setup/CHANGELOG.md index 11deed6d49..a04cc38ef0 100644 --- a/packages/apps/setup/CHANGELOG.md +++ b/packages/apps/setup/CHANGELOG.md @@ -1,5 +1,42 @@ # @objectstack/setup +## 17.4.0 + +### Patch Changes + +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [2bb0614] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/spec@17.4.0 + - @objectstack/platform-objects@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/apps/setup/package.json b/packages/apps/setup/package.json index 8a664845d6..a9cec1b067 100644 --- a/packages/apps/setup/package.json +++ b/packages/apps/setup/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/setup", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "ObjectStack Setup — the platform administration app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", "main": "dist/index.js", diff --git a/packages/apps/studio/CHANGELOG.md b/packages/apps/studio/CHANGELOG.md index bab2e5390d..d147aac3df 100644 --- a/packages/apps/studio/CHANGELOG.md +++ b/packages/apps/studio/CHANGELOG.md @@ -1,5 +1,42 @@ # @objectstack/studio +## 17.4.0 + +### Patch Changes + +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [2bb0614] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/spec@17.4.0 + - @objectstack/platform-objects@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/apps/studio/package.json b/packages/apps/studio/package.json index 09315e787f..dce1771982 100644 --- a/packages/apps/studio/package.json +++ b/packages/apps/studio/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/studio", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "ObjectStack Studio — the metadata builder app, packaged as its own ObjectStack app package (ADR-0048: one app per package).", "main": "dist/index.js", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 2b1ff39a66..ca51076a42 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,359 @@ # @objectstack/cli +## 17.4.0 + +### Minor Changes + +- 95d5cbb: Ratify `./hook-body` as a public subpath export — `extractHookBody`, `HookBodyExtractionError`, `HookBodyRefusalKind` and `ExtractedBody` were reachable as a deep `dist/utils/extract-hook-body.js` import until #13123 sealed the surface, and an app's hook-body fidelity harness (hotcrm's `test/helpers/action-sandbox.ts`) consumes them to run the SAME body-only lowering `os build` ships through the real QuickJS runner, so a test executes what production executes rather than a lookalike. The #13123 body names exactly this remedy for an out-of-repo consumer — ratify the subpath as public surface rather than read `dist/` paths — and 17.3.0 applied it to `./console` for cloud's `objectos-runtime`; this applies it to the second consumer (#15325). `@objectstack/cli/hook-body` is a dedicated entry that re-exports those four names and nothing else; the deep `dist/` path stays sealed. Also admits `./package.json`, so the ordinary tooling idiom of reading a dependency's own manifest resolves again. + + `minor`, not `patch`: a new subpath on a published package's `exports` map is a purely additive widening of its public surface — a new accepted key — which takes at least `minor` under the maintainer's 2026-09-04 rule (decision batch #35, on #15294) in the Check Changeset step's "WHICH LEVEL" prose; the commit type never lowers it. +- cf6b671: `os create` now emits a project that installs outside this monorepo. + + Every project the command scaffolded declared its `@objectstack/*` dependencies + with pnpm's `workspace:*` protocol, extended a `tsconfig.json` two directories + above itself, and was written into this repository's own `packages/plugins/` or + `examples/` by default — so a developer following the documented command got a + project `pnpm install` refuses. The default emission is now standalone: + + - `@objectstack/*` dependencies are published semver ranges pinned to the + version of the CLI that generated them; + - the emitted `tsconfig.json` is self-contained and extends nothing; + - the project is written to `./` in the current directory (or `--dir`); + - a `pnpm-workspace.yaml` carries the build approvals a fresh `pnpm install` + needs on pnpm 11. + + The `plugin` template also emits `init` where it used to emit `initialize`. + `initialize` is not part of the `Plugin` contract, so the scaffold did not + type-check under its own `strict` config (TS7006 on the untyped `context` + parameter) and `kernel.use()` refused the plugin at load with + `Plugin init function is required` — a defect the kernel protocol docs + previously carried a warning about instead of a fix. + + The previous monorepo-internal placement is still available for ObjectStack + platform work as the explicit `--in-repo` flag, which keeps the `workspace:*` + specs and writes into `packages/plugins/` or `examples/`. +- ec0a6e7: feat(objectql,cli): `backfillSummaryNulls` accepts `recomputeUndefinedOnEmpty` — a caller who KNOWS a `min`/`max`/`avg` roll-up column was just declared can have it filled; `os migrate summary-nulls --recompute-undefined-on-empty object.field` surfaces it (#15064) + + A roll-up value has three producers — the insert-time seed, the child-write + recompute, and the one-off backfill — and **declaring a summary field on an + object that already has rows reaches none of them**. For `count`/`sum` the + backfill repairs that as a side effect (every `NULL` is a hole to it). For + `min`/`max`/`avg` it could not: `summaryNullIsBackfillable` decides on the + function alone, so "never computed" and "no child rows" were indistinguishable, + the column stayed `NULL` on every pre-existing parent, and the report said + `filled: 0` — a false all-clear that a timed flow built on the column then + turned into "matches nothing" (the customer case behind cloud#1908). + + **What changes** — maintainer ruling on #15064, option A: the caller who holds + the fact gets a way to say it; the predicate and the default run do not move. + + - `SummaryBackfillOptions.recomputeUndefinedOnEmpty?: string[]` — `object.field` + roll-ups the caller knows were never computed. A named `min`/`max`/`avg` is + walked like a `count`: every `NULL` parent is recomputed through the same + `aggregateSummaryValue` the engine writes. A parent whose aggregate is the + empty-set reading (`null` — no child rows) already holds the engine's own + value, so it is neither counted as a hole nor written; the scoped run is + therefore idempotent in the same "re-run until it reports zero" sense. + Naming a `count`/`sum` is accepted and changes nothing, so a publish path can + pass every column it just declared without knowing the empty-set list. + - A name that resolves to no roll-up owned by an object the run walks — a typo, + a plain field, or an object `objects` left out — is **refused before any row + is read**, dry run or apply, with an ADR-0112 envelope (`code: + 'INVALID_FIELD'`, `status: 400` — the code the projection and write axes + that name a field already answer, while sorting keeps `INVALID_SORT`; + `field` names the first unresolved entry, `fields` all of them). A silent + no-op there would be the same false all-clear this option exists to end. + - `SummaryBackfillReport.recomputedUndefinedOnEmpty: string[]` — the complement + of `skippedUndefinedOnEmpty`, same `object.field (fn)` spelling; `[]` on an + unscoped run. `SummaryBackfillFieldOutcome.fn` widens from `'count' | 'sum'` + to every roll-up function, since a named `max` now appears in `fields`. + - `os migrate summary-nulls --recompute-undefined-on-empty object.field` + (repeatable) passes the scope through; the confirmation prompt names the + columns; `formatSummaryBackfillReport` lists them under "Recomputed on + request" and explains a `NULL` that remains. + + **What does not change:** without the option the walk, the writes, every + counter and the human-readable report are byte-for-byte what they were (pinned + against output captured on `main` before this change); `min`/`max`/`avg` stay + out of scope and keep being reported under `skippedUndefinedOnEmpty`; the + predicate `summaryNullIsBackfillable` is untouched, so `os migrate + summary-nulls` keeps its meaning on every deployment. The only visible delta on + an unscoped run is the one additive report key, `recomputedUndefinedOnEmpty: []`. + + `minor` for both packages: an optional parameter on a published exported + function, a new report key, and a new CLI flag are each a purely additive + widening of a published surface, which takes at least `minor` (bump-level rule, + 2026-09-04); the `fix`-shaped motivation does not lower it. + +### Patch Changes + +- c0c07ef: `os package publish --help` no longer points its local-dev example at a directory this repo does not have. + + The last line of the command's `EXAMPLES` block read: + + ``` + $ OS_CLOUD_URL=http://localhost:4000 os package publish # local dev (apps/cloud) + ``` + + `apps/cloud` was deleted from this repository — the reference cloud host now lives in `objectstack-ai/cloud` — so the parenthetical sent a reader to a path that is not in the tree they cloned. This is help text, not a source comment: it is printed verbatim to anyone who runs the command. + + The parenthetical is dropped rather than re-pointed at the other repo. The example is about `OS_CLOUD_URL` overriding the control-plane URL, which the `--server` flag already documents in the same output; which directory happens to serve `localhost:4000` was never part of what the example teaches, and a `--help` reader is not looking for a file in a monorepo. `# local dev` alone carries it, and it now matches how the CLI reference docs have long published the same example. + + No behaviour changes: `examples` is a static help string, and no flag, argument, default or exit code moves. +- ee79099: `os validate|info|diff|lint|compile|build|verify|migrate meta|i18n check|i18n extract --json` no longer print human text on stdout when the config file is missing. + + `resolveConfigPath()` emitted both of its refusals — the explicit-path miss and the auto-detect miss — through `printError` and `console.log`, **both of which write to stdout**, and then called `process.exit(1)` directly. Ten published `--json` faces reach that helper, so a missing config file answered them with exit 1, an unparseable stdout and an **empty stderr**: 206 bytes of prose on the one stream `--json` reserves for the machine. And because the exit was called rather than thrown, every command's catch-all `--json` error exit — all of which sit downstream of a throw — never ran. + + The diagnostic now goes to stderr, where the rest of this CLI's diagnostics already go. Nothing else moves: + + - **the exit code is still 1**, so a consumer branching on exit status sees no change at all; + - **the wording is unchanged**, hints included, so a human reading a terminal sees the same three lines; + - **nothing is accepted or rejected differently** — no config that loaded before fails now, and none that failed now loads. + + ⚠️ **No error payload is invented on this path.** What a `--json` consumer should *receive* when the config file is missing is an envelope question that touches ten published faces at once, and it is deliberately left open here — this change settles only that the machine's channel no longer carries prose. `--json` on this path emits nothing on stdout; a consumer must still read the exit status, exactly as it must today. + + A new pin (`config-miss-stdout-purity.e2e.test.ts`) drives all ten faces on both branches of the helper. The existing purity pin could not: it discovers its family as the commands that call `bootSchemaStack`, and these fail before any kernel boots. +- 8644d1d: `os generate migration` gives a table's own `id` column the shape the platform actually creates. + + Both migration generators hardcoded the primary key as a UUID — `"id" UUID PRIMARY KEY DEFAULT gen_random_uuid()` in the SQL format, `table.uuid('id').primary().defaultTo(db.fn.uuid())` in the TypeScript one (the default format). The platform's SQL driver emits `table.string('id').primary()`, which is knex's `varchar(255)`. A platform id is a string, not a uuid, so on Postgres the generated table refused the platform's very first insert with `22P02 invalid input syntax for type uuid`. + + The quieter half is the `DEFAULT`, and it is why this was worth correcting rather than working around. The driver emits no database-side default at all — its insert path always supplies the id itself — so `gen_random_uuid()` never fired for a platform write, only for an out-of-band one, handing that row a 36-character uuid this platform's id generator would never mint. One table would then hold two incompatible id shapes, with nothing said. + + Both generators now emit the driver's own answer: `"id" VARCHAR(255) PRIMARY KEY` and `table.string('id').primary()`. The correction also closes a contradiction inside the generator file, whose prose already stated that a reference column takes the width of the target's `id` column *because* the driver emits `table.string('id').primary()` — a few hundred lines above the two lines that emitted `uuid`. + + `generate-builtin-id-column.pin.test.ts` reads the width from the driver's own `DEFAULT_STRING_VARCHAR_CHARS` rather than transcribing `255`, so the generators cannot drift away from the driver again without a named failure. +- 095df7f: `os lint` and `os i18n extract` no longer count one translation key twice. + + A translation key is derived from *where a string is addressed*, not from *which declaration was being read* when the walk reached it — and two declarations can address one bundle slot. `collectExpectedEntries` emitted one entry per declaration, so a key reachable twice became two expected entries. Two families were measured, with different causes: + + - **Two carriers, one action.** The normalized config attaches an object's actions to `obj.actions` *and* to the top-level `actions` list — the same object reference, not a copy — so both action branches emitted `objects.OBJECT._actions.ACTION.*`. This is the family the coverage report shows: 70 of 691 baselined units across `app-todo` (40), `app-showcase` (29) and `app-crm` (1). + - **Two declarations, one form field.** `deleteBehavior` is declared twice in each of the `field` and `object` metadata forms, gated on `visibleWhen` (`lookup` vs `master_detail`); both render into one key. Config-independent — it duplicated six entries on every config, including an empty one. + + Neither is an authoring mistake, and neither is fixable where it originates: both are two correct declarations of one displayed string. So the walker now collapses entries that address the same path, keeping the first emission. + + What that corrects, in both directions: + + - **`os lint`'s i18n findings.** The same missing key was reported twice, byte-identically. `pnpm check:i18n-coverage` ratchets the finding *count* while its report calls the number "untranslated declared strings", so translating one key moved the ratchet by two and the frozen debt was ~11% larger than the work it described. The three coverage baselines are regenerated in this change and fall by exactly 70 (691 to 621): `app-crm` 102 to 101, `app-showcase` 443 to 414, `app-todo` 146 to 106. The ratchet's direction, monotonicity and failure text are unchanged — only the population it counts. + - **`os i18n extract`'s reported counts.** `totalExpected` and the per-locale `counts` counted emissions while the skeleton itself had already collapsed the duplicates on the way in, so extract over-reported what it wrote — 1632 claimed against 1531 keys written on `app-showcase`, 894 against 870 on `app-todo`, 930 against 925 on `app-crm`. Those numbers now match the skeleton. + + No generated bundle changes: every duplicate pair measured carries a byte-identical record, so de-duplication removes copies and never a demand. All nine `translations/*.generated.ts` packages stay in sync. +- 54e2369: `os lint --eval` no longer scores a failed generation as a perfect one: a generator that throws now counts 0 toward `meanScore` instead of 100. + + The harness has always handled a throwing `--generator` by substituting an empty stack and scoring that. An empty stack is **100 / grade `A` / `valid: true`** — it has nothing wrong with it because it has nothing in it. So a live eval in which every single generation failed reported the best possible headline number: + + ``` + os lint --eval --json --generator ./throws.mjs + exit 1 · ok: false · passed: 0 · failed: 5 · meanScore: 100 + every case: score 100 · grade A · valid true · generationError "model unavailable" + ``` + + `meanScore` is the first number a human scanning that report reads, and it read perfect precisely when the model under test produced nothing. + + **What was NOT wrong: `passed`.** It carries its own guard (`!generationError && …`), so the failed cases were reported as failed and `ok` was `false` throughout. A reader who cross-read `ok`/`passed` was safe; a reader who checked the mean and moved on got exactly the wrong impression. That is the whole defect, and nothing about `passed`, `ok`, `total`, `failed` or the exit code changes here. + + The repair is the verdict the sibling failure path already used. A generator that *returns* a value nobody can walk was already scored `0 / F / valid: false`, with the reason written into the module: a stack that cannot be walked is not an empty stack, and `valid: true` for one that was never parsed is simply false. A stack that was never produced is not an empty stack either — so both now answer the same: + + ```json + { "id": "invoice_with_line_items", + "generationError": "model unavailable", + "passed": false, + "score": { "score": 0, "grade": "F", "valid": false } } + ``` + + and the run above now reports `meanScore: 0`. + + `meanScore`'s denominator is unchanged and is now stated in the payload's own documentation: the mean is over every case **attempted**, so a failed case contributes its 0 and is counted. The alternative — averaging only over cases that could be scored — is a different metric that would report the quality of the generations that arrived while staying silent about how many never did; a `meanScore` that switched denominators without saying so would be a worse defect than the one being fixed. + + No key is added to or removed from the `--json` payload, and nothing a generator can return is newly accepted or rejected: an off-shape stack is still a **scored** case whose schema errors are why it fails, never a generation error. +- 17ec4b1: `os lint --eval --json` reports an unscorable generated stack as a failed case instead of crashing with no JSON at all. + + The eval harness promised totality in writing — *"Never throws — generation failures become failed cases"* — and the promise was false as written. Its `try` wrapped only the call to your `--generator` module; the `scoreMetadata(stack)` call that follows sat outside it. So a generator that **threw** became a failed case, exactly as documented, while a generator that **returned** a value nobody could walk took the whole process down: + + ``` + os lint --eval --json --generator ./g.mjs + exit 1 · stdout 0 bytes · stderr " Error: poison getter" + ``` + + A caller that asked for `--json` got the framework's human error text on stderr and no document at all to parse. Eval mode dispatches above the project-lint `try`, so the catch-all JSON exit that mode has could never see it either. + + Scoring a stack means walking it, and there are two walks: the normalizer spreads the stack's top level, and the schema parse walks everything below it. A throw from **either** now becomes that case's `generationError` — the same per-case channel a throwing generator already used — so the report exit that was always there emits its JSON, names the cause, and still exits non-zero: + + ```json + { "id": "invoice_with_line_items", + "generationError": "Failed to score the generated stack: poison getter", + "passed": false, + "score": { "score": 0, "grade": "F", "valid": false } } + ``` + + Nothing new appears on the `--json` face: no new key, no new payload shape. The failing exit was already reachable for a throwing generator; it is now reachable for a poisonous one too. + + The failed case is scored `0 / F / valid: false` rather than as an empty stack. An empty stack scores 100 / A / valid, and stamping that on a stack nobody could parse would have put a clean-looking verdict next to a failure — the crash replaced by a quiet wrong answer. + + Unchanged: offline mode, and every off-shape stack a generator can return. Bad metadata is still **scored**, with its schema errors as the reason it fails — it is not rerouted into the failure channel. +- 5023630: The published `os` binary no longer freezes in the kernel when whatever is reading its output stops draining. + + Node puts the CLI's stderr on the non-blocking write path when it opens the pipe, so a write to a reader that has stopped is buffered rather than parking the thread. libuv clears that flag again in the pre-exec of every child spawned with **inherited** stdio — and inheriting is `dup2`, so the flag lives on an open file description the spawner shares. Clearing it for the child clears it for the CLI too. + + Measured on the built binary, `os dev --verbose` with its output piped to a reader that stopped draining: `os dev` spawns `os serve --dev` with inherited stdio at 2.8 s, that child spawns the esbuild service with inherited stderr at 5.2 s, and fd 2 stays blocking for the rest of the run. 3.1 s after the reader stopped, the main thread sat in `write(2)` (`wchan=sock_alloc_send_pskb`), 4 of 4 runs — parked 28.9 s, **ignoring SIGINT while parked**, and released only when the consumer resumed. Not a crash and not a timeout: alive, idle, unresponsive, with an empty log. Anything that pipes `os dev` and reads it slowly — a CI log collector, a backgrounded runner, a supervisor that stops draining while it does work — could park the CLI this way. + + `bin/run.js` now installs `keepStderrNonBlocking()` before oclif can write a byte. The guard re-asserts `O_NONBLOCK` immediately ahead of each write, which is what the measurement requires: the clearing that persisted was made by a **grandchild** the CLI does not spawn and cannot see, so a one-shot at startup would be undone silently and no change to the CLI's own spawn sites would have prevented it. + + The guard itself is not new — it shipped in no published install. It lived at `packages/cli/bin/stderr-nonblocking.mjs`, and `files` names only `dist`, `README.md` and `CHANGELOG.md`; npm packs a `bin` **target** regardless of `files`, which is why `bin/run.js` reached every install and the module beside it reached none. It now compiles from `src/utils/stderr-nonblocking.ts` into `dist/`, under the whitelist that was already there. + + Nothing about which arguments the CLI accepts, what it prints, or what it exits with changes. The refusal of `setBlocking(true)` in `src/utils/format.ts` stands and is untouched — this is its inverse, and what keeps its premise true. +- 0c6c55e: `os serve` now says so when the SQLite file it is serving is no longer the file at its configured path. + + Deleting the data directory under a running server — `rm -rf .objectstack/data`, which is what a `demo:reset` script does and what a fresh-database repro starts with — unlinks the inode without touching the process. SQLite keeps reading and writing the now-invisible file, health keeps answering `200`, and a later boot creates a brand-new database at the same path. From that moment every filesystem inspection of that path describes a *different* database than the running server answers from, and nothing anywhere says so: a row edited there has no observable effect on the live server, and a user who authenticates against the live server is not in that file. Both readings are true, both look like a broken write path, and one investigation that reported them as evidence cost a full P0 cycle. + + A boot that serves an on-disk SQLite file now records that file's identity once the boot is complete and re-checks it on a 30-second interval. When the file is gone, or the path holds a different file, it reports **once** at `error` — naming the path, the consequence (every external observation of this deployment is now false, and it will keep looking healthy) and the fix (restart the server so it opens the file that is at that path now). + + It refuses nothing and retries nothing: the running server is still correct, merely invisible, and breaking a working dev loop to fix a reporting gap would trade a bad hour for a worse one. Nothing is added to any payload, endpoint or state file. Silence from the check is not a claim that the file is intact — every uncertainty in it resolves toward staying quiet, because a false report would send an operator to restart a server whose database is fine. +- Updated dependencies [2ed6be6] +- Updated dependencies [54bb2f1] +- Updated dependencies [98191d2] +- Updated dependencies [ca326b5] +- Updated dependencies [f1a1028] +- Updated dependencies [8f404a5] +- Updated dependencies [954cb0b] +- Updated dependencies [a56baa2] +- Updated dependencies [65846bc] +- Updated dependencies [3e3ecb0] +- Updated dependencies [8e500f2] +- Updated dependencies [4b3955e] +- Updated dependencies [347b777] +- Updated dependencies [36a16d0] +- Updated dependencies [c01b3a6] +- Updated dependencies [a51eb86] +- Updated dependencies [e944fdb] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [236f2df] +- Updated dependencies [d30ccb9] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [4bc9821] +- Updated dependencies [2003259] +- Updated dependencies [a646120] +- Updated dependencies [a06faeb] +- Updated dependencies [a646120] +- Updated dependencies [2200f8e] +- Updated dependencies [a646120] +- Updated dependencies [2200f8e] +- Updated dependencies [65846bc] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [4f85e4d] +- Updated dependencies [fa85759] +- Updated dependencies [5f7fa1d] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [a84e1ce] +- Updated dependencies [65846bc] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [6b66ec7] +- Updated dependencies [222dc0f] +- Updated dependencies [7dafaae] +- Updated dependencies [52b59d6] +- Updated dependencies [f502898] +- Updated dependencies [7bf96cf] +- Updated dependencies [17f8604] +- Updated dependencies [3bd9b34] +- Updated dependencies [b4b37e5] +- Updated dependencies [ba426b0] +- Updated dependencies [d0ee598] +- Updated dependencies [61821e5] +- Updated dependencies [26144c2] +- Updated dependencies [9e9f03a] +- Updated dependencies [5eb24f8] +- Updated dependencies [c64e65f] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [e13ede8] +- Updated dependencies [89758ac] +- Updated dependencies [f5cc78b] +- Updated dependencies [8a12067] +- Updated dependencies [ee32e1c] +- Updated dependencies [8744de9] +- Updated dependencies [a646120] +- Updated dependencies [ebb5550] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [6b8c677] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [5964124] +- Updated dependencies [9408b7f] +- Updated dependencies [615fac3] +- Updated dependencies [ec0a6e7] +- Updated dependencies [2bb0614] +- Updated dependencies [6c439f2] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] +- Updated dependencies [c550baf] +- Updated dependencies [cd55558] + - @objectstack/core@17.4.0 + - @objectstack/objectql@17.4.0 + - @objectstack/metadata-protocol@17.4.0 + - @objectstack/service-analytics@17.4.0 + - @objectstack/driver-sql@17.4.0 + - @objectstack/runtime@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/service-automation@17.4.0 + - @objectstack/lint@17.4.0 + - @objectstack/metadata@17.4.0 + - @objectstack/plugin-auth@17.4.0 + - @objectstack/client@17.4.0 + - @objectstack/console@17.4.0 + - @objectstack/platform-objects@17.4.0 + - @objectstack/rest@17.4.0 + - @objectstack/driver-memory@17.4.0 + - @objectstack/driver-mongodb@17.4.0 + - @objectstack/driver-turso@17.4.0 + - @objectstack/trigger-record-change@17.4.0 + - @objectstack/plugin-hono-server@17.4.0 + - @objectstack/types@17.4.0 + - @objectstack/cloud-connection@17.4.0 + - @objectstack/mcp@17.4.0 + - @objectstack/plugin-security@17.4.0 + - @objectstack/service-storage@17.4.0 + - @objectstack/service-settings@17.4.0 + - @objectstack/plugin-approvals@17.4.0 + - @objectstack/verify@17.4.0 + - @objectstack/driver-sqlite-wasm@17.4.0 + - @objectstack/plugin-audit@17.4.0 + - @objectstack/plugin-email@17.4.0 + - @objectstack/plugin-pinyin-search@17.4.0 + - @objectstack/plugin-reports@17.4.0 + - @objectstack/plugin-sharing@17.4.0 + - @objectstack/plugin-webhooks@17.4.0 + - @objectstack/service-cache@17.4.0 + - @objectstack/service-datasource@17.4.0 + - @objectstack/service-job@17.4.0 + - @objectstack/service-messaging@17.4.0 + - @objectstack/service-package@17.4.0 + - @objectstack/service-queue@17.4.0 + - @objectstack/service-realtime@17.4.0 + - @objectstack/service-sms@17.4.0 + - @objectstack/trigger-api@17.4.0 + - @objectstack/trigger-schedule@17.4.0 + - @objectstack/account@17.4.0 + - @objectstack/setup@17.4.0 + - @objectstack/formula@17.4.0 + - @objectstack/metadata-core@17.4.0 + - @objectstack/observability@17.4.0 + - create-objectstack@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/cli/package.json b/packages/cli/package.json index 5d0ba78b18..7ed99878d6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cli", - "version": "17.3.0", + "version": "17.4.0", "description": "Command Line Interface for ObjectStack Protocol", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/client-react/CHANGELOG.md b/packages/client-react/CHANGELOG.md index cc1006fc28..7820f630ee 100644 --- a/packages/client-react/CHANGELOG.md +++ b/packages/client-react/CHANGELOG.md @@ -1,5 +1,48 @@ # @objectstack/client-react +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [e944fdb] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/client@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/client-react/package.json b/packages/client-react/package.json index af773183e3..a79b601ad8 100644 --- a/packages/client-react/package.json +++ b/packages/client-react/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client-react", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "React hooks for ObjectStack Client SDK", "main": "dist/index.js", diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md index aaffdbb72c..1c4818cebc 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -1,5 +1,110 @@ # @objectstack/client +## 17.4.0 + +### Minor Changes + +- e944fdb: fix(client)!: the `oauth.*` family declares the wire shapes better-auth actually sends — four published `Promise< any >` returns narrowed (#14312) + + **BREAKING** for a typed caller, and it breaks nothing that ever worked at runtime. No request bytes, no URL and no response handling change: this is a declaration catching up with what the routes have always answered. It ships as `minor` under the lockstep launch-window convention (`scripts/check-changeset-no-major.mjs`) — the version number is not the migration signal here, this entry is. + + + + Card 1 of 3 of the #12104 family, under the maintainer's 2026-08-31 ruling: the wire contract is the only source of truth, and better-auth's own `Date`-typed fields are the pre-serialization SERVER shape, not the wire fact. + + ## What changed + + Four methods ended `return res.json()` with no return annotation, so `lib.dom`'s `Response.json(): Promise< any >` was their published type. Each now declares the shape its route serves, and its `exported-any-returns.json` entry is deleted in the same change: + + | method | resolved to (before) | resolves to (now) | + |:--|:--|:--| + | `client.oauth.applications.register(req)` | `any` | `OAuthApplicationRegistration` | + | `client.oauth.applications.get(id)` | `any` | `OAuthApplication` | + | `client.oauth.applications.getPublic(id)` | `any` | `OAuthApplicationPublic` | + | `client.oauth.consent(req)` | `any` | `OAuthConsentResult` | + + `OAuthApplication`, `OAuthApplicationRegistration`, `OAuthApplicationPublic` and `OAuthConsentResult` are newly exported from `@objectstack/client`. These four routes are served BARE by better-auth (`auth-route-ledger.ts` records them `source: 'better-auth'`) — there is no `{ success, data }` envelope to unwrap, and none is introduced. + + ## The exact reads that stop compiling + + Everything below compiled before only because `any` is assignable to, and indexable by, everything. + + ```ts + const app = await client.oauth.applications.get('c_1'); + app.data; // was fine; now TS2339 — these routes carry NO envelope + app.anythingAtAll; // was fine; now TS2339 + + const pub = await client.oauth.applications.getPublic('c_1'); + pub.client_secret; // now TS2339 — the public projection hand-picks 7 columns + pub.grant_types; // now TS2339 — same reason + pub.disabled; // now TS2339 — same reason + + const decision = await client.oauth.consent({ accept: true }); + decision.client_id; // now TS2339 — consent answers `{ redirect, url }` + + // Timestamps are RFC 7591 NUMBERS (Unix epoch seconds), so a caller that + // guessed `Date` or ISO `string` now fails: + new Date(app.client_id_issued_at!).toISOString(); // TS2769: number is not a Date arg + app.client_id_issued_at!.slice(0, 10); // TS2339: not a string + new Date(app.client_id_issued_at! * 1000); // the correct rewrite + ``` + + A caller that only read `client_id`, `client_secret`, `redirect_uris` or `url` needs no change. + + ## Timestamps: `number`, not `Date` and not ISO-8601 + + The ruling ordered every `Date`-typed field declared as an ISO `string` and forbade both a `Date` declaration and a runtime revival layer. **This family has no `Date` field to convert.** RFC 7591 carries `client_id_issued_at` and `client_secret_expires_at` as Unix-epoch SECONDS, and the provider converts its stored `Date` to a number before serialising, so the wire sends neither a `Date` nor an ISO string. Both are declared `number`, and a type-level pin holds them there. The ruling's prohibitions are satisfied: nothing declares a `Date`, and no revival layer exists. + + ## Two places better-auth's own types were the wrong answer + + Read off the wire against a real server, not off the vendor's `.d.ts`: + + - `getPublic` is declared `OAuthClient` — the full row — but its handler hand-picks seven columns. `OAuthApplicationPublic` is that projection, derived with `Pick` so it cannot drift from its parent. Its `redirect_uris` is always `[]` on this route and carries no information. + - `user_id` and `application_type` are declared nullable by the vendor, but the serialiser folds a null column to `undefined`, so `null` is unreachable and is not declared. + + ## `oauth.applications.delete` is deliberately NOT bound + + The fifth method of the family keeps its `Promise< any >` and its ledger entry. Its route answers HTTP 200 with a zero-byte body, so its `res.json()` rejects with a `SyntaxError` on every successful delete. No annotation can be honest while that call stands, and binding it needs a behaviour change — a decision beyond this card's type-narrowing scope. That the shrink-only ledger still carries exactly this one entry is the mechanism working. + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/client/package.json b/packages/client/package.json index 7495670cd9..de67ac3a0f 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/client", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Official Client SDK for ObjectStack Protocol", "main": "dist/index.js", diff --git a/packages/cloud-connection/CHANGELOG.md b/packages/cloud-connection/CHANGELOG.md index 9065017f42..9bfa5caee3 100644 --- a/packages/cloud-connection/CHANGELOG.md +++ b/packages/cloud-connection/CHANGELOG.md @@ -1,5 +1,60 @@ # @objectstack/cloud-connection +## 17.4.0 + +### Patch Changes + +- 6b66ec7: Fix: the marketplace install-local routes now supply the effective tenancy posture to the shared authorization resolver, so both posture-conditional API-key refusals apply at these doors. + + Under a wall-enforcing posture (`isolated`), an API key stamped with an organization its owner has left is refused, as is a key carrying no organization at all. Previously neither guard ran here, because both are conditional on a posture the caller supplies and this seam supplied none — the key's tenant was its own stored `active_organization_id`, never checked against current membership. + + The posture is read from the kernel's `tenancy` service, so it is the posture in force rather than the one requested through `OS_TENANCY_POSTURE`. A deployment that registers no `tenancy` service is unchanged: there is no wall there, and no posture-conditional refusal applies. A `tenancy` service that is registered and fails to build is an outage and answers 503 rather than admitting the caller. +- Updated dependencies [2ed6be6] +- Updated dependencies [98191d2] +- Updated dependencies [ca326b5] +- Updated dependencies [f1a1028] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [8a12067] +- Updated dependencies [ee32e1c] +- Updated dependencies [8744de9] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/runtime@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/types@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/cloud-connection/package.json b/packages/cloud-connection/package.json index 035dedc3d3..a25b5b7508 100644 --- a/packages/cloud-connection/package.json +++ b/packages/cloud-connection/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/cloud-connection", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Runtime-side client for an ObjectStack cloud control plane — marketplace browse proxy, install-local, device-code binding, org catalog and installed views, and the /api/v1/runtime/config discovery endpoint. Open mechanism (ADR-0008): the hub service, plan policy, and entitlements stay server-side.", "type": "module", diff --git a/packages/connectors/connector-mcp/CHANGELOG.md b/packages/connectors/connector-mcp/CHANGELOG.md index 6285b514ce..d5395c81db 100644 --- a/packages/connectors/connector-mcp/CHANGELOG.md +++ b/packages/connectors/connector-mcp/CHANGELOG.md @@ -1,5 +1,46 @@ # @objectstack/connector-mcp +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/connectors/connector-mcp/package.json b/packages/connectors/connector-mcp/package.json index 0aa6fada47..fa347f1c4d 100644 --- a/packages/connectors/connector-mcp/package.json +++ b/packages/connectors/connector-mcp/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-mcp", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Model Context Protocol (MCP) connector for ObjectStack — a generic adapter that turns any MCP server's tools into a connector's actions on the automation engine's connector registry (ADR-0024).", "main": "dist/index.js", diff --git a/packages/connectors/connector-openapi/CHANGELOG.md b/packages/connectors/connector-openapi/CHANGELOG.md index 2cf5cc8ea3..76b90d4f66 100644 --- a/packages/connectors/connector-openapi/CHANGELOG.md +++ b/packages/connectors/connector-openapi/CHANGELOG.md @@ -1,5 +1,46 @@ # @objectstack/connector-openapi +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/connectors/connector-openapi/package.json b/packages/connectors/connector-openapi/package.json index 2fb927e1a7..6a7b4bb28d 100644 --- a/packages/connectors/connector-openapi/package.json +++ b/packages/connectors/connector-openapi/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-openapi", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "OpenAPI 3.x connector generator for ObjectStack — turns a declarative OpenAPI document into connector actions on the automation engine's registry, with a self-contained static-auth HTTP transport (ADR-0023).", "main": "dist/index.js", diff --git a/packages/connectors/connector-rest/CHANGELOG.md b/packages/connectors/connector-rest/CHANGELOG.md index 6bae321440..55d0d40b5c 100644 --- a/packages/connectors/connector-rest/CHANGELOG.md +++ b/packages/connectors/connector-rest/CHANGELOG.md @@ -1,5 +1,46 @@ # @objectstack/connector-rest +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/connectors/connector-rest/package.json b/packages/connectors/connector-rest/package.json index 579e448dfc..9cc8182acb 100644 --- a/packages/connectors/connector-rest/package.json +++ b/packages/connectors/connector-rest/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-rest", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Generic REST connector for ObjectStack — the reference concrete connector that registers a `request` action on the automation engine's connector registry (ADR-0018 §Addendum).", "main": "dist/index.js", diff --git a/packages/connectors/connector-slack/CHANGELOG.md b/packages/connectors/connector-slack/CHANGELOG.md index e679c05976..0b54ab6bc4 100644 --- a/packages/connectors/connector-slack/CHANGELOG.md +++ b/packages/connectors/connector-slack/CHANGELOG.md @@ -1,5 +1,46 @@ # @objectstack/connector-slack +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/connectors/connector-slack/package.json b/packages/connectors/connector-slack/package.json index 476fa3b7eb..52ac1b1b6d 100644 --- a/packages/connectors/connector-slack/package.json +++ b/packages/connectors/connector-slack/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/connector-slack", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Slack Web API connector for ObjectStack — registers `chat.postMessage` / `chat.update` / `call` actions on the automation engine's connector registry (ADR-0018 §Addendum, ADR-0022).", "main": "dist/index.js", diff --git a/packages/console/CHANGELOG.md b/packages/console/CHANGELOG.md index 91cb8d2cae..555e96cc24 100644 --- a/packages/console/CHANGELOG.md +++ b/packages/console/CHANGELOG.md @@ -1,5 +1,49 @@ # @objectstack/console +## 17.4.0 + +### Minor Changes + +- 236f2df: Console (objectui) refreshed to `a472b07167a3`. Frontend changes in this range: + + Derived from the changesets objectui declared over the range — 15 releasing of 18 changesets added across 29 non-merge commits; omitted: 3 release-nothing changesets, 11 commits carrying no changeset (they ship no package code). + + - **minor** — **BREAKING** — Converge the lookup/user widget metadata on the spec's camelCase — one concept, one spelling (objectui#7155, maintainer ruling A′ of 2026-09-03, director decision batch #19). (objectui `351eb3181`) + - **minor** — **BREAKING** — One authority for `KanbanSchema` / `KanbanColumn` / `KanbanCard`: the bare names now belong to `@object-ui/plugin-kanban` (objectui#6172, closing the cross-package half of objectu… (objectui `2c71482ea`) + - **minor** — Retire `ComponentInput.inputType` — the fifth and last key objectui#5905 named (ADR-0049 enforce-or-remove, maintainer ruling 2026-08-31, option B). (objectui `1ec291c0d`) + - **minor** — `@object-ui/core` publishes `resolveRecordSourceObjectName`, the ONE reader for "which object is this block bound to" (objectui#7627). (objectui `b041b9c0c`) + - **minor** — **Published TS surface narrowed:** `DashboardComponentSchema` no longer declares the dashboard-root `title` member (objectui#7623). (objectui `5d0876c5c`) + - **minor** — **BREAKING** — BREAKING (`@object-ui/components`): the chart primitives — `ChartContainer`, `ChartTooltip`, `ChartTooltipContent`, `ChartLegend`, `ChartLegendContent`, `ChartStyle` and the `Char… (objectui `7bf244bea`) + - **minor** — ListView: fold `data={{ provider: 'object', object }}` onto `objectName`, and read the author's view kind from `specType` / `type` (objectui#7477 — step 6 of #2890, released by th… (objectui `00d2fa682`) + - **minor** — Retire the dashboard-**root** `title` read across all five surfaces (objectui#7509, maintainer ruling 2026-09-04, decision batch #29, option C, under ADR-0049). (objectui `1cca678ba`) + - **minor** — **BREAKING** — Re-home the breakpoint layout vocabulary and delete the two dead responsive implementations (objectui#7580, maintainer ruling 2026-09-04, option A). (objectui `e62c44e7e`) + - **minor** — `@object-ui/types/zod`: the zod const `StylePropsSchema` is renamed to `ClassNameStylePropsSchema` (objectui#5928). **The old name is gone** — there is no deprecated alias and no… (objectui `24e027e93`) + - **patch** — Fix `extractToc` eating the underscores out of a `SCREAMING_SNAKE` heading, so its `#id` links resolve to the heading they name again (objectui#7667). (objectui `a472b0716`) + - **patch** — Remove `src/ui/toast.tsx`, an unreferenced primitive, and the dependency only it imported (objectui `2f61238b9`) + - **patch** — Fix `extractToc` deleting tag-shaped text that lives INSIDE an inline code span, so its `#id` links resolve to the heading they name again (objectui#7658). (objectui `90c6d090d`) + - **patch** — A record-page URL now names the object the clicked rows actually came from, in `ObjectTree` and `ObjectCalendar` (objectui#7638). (objectui `2ce2612df`) + - **patch** — fix(app-shell): the object-field options editor no longer drops `default` and `visibleWhen` on save (objectui `97c3e1972`) + + ⚠️ 4 of these carry a breaking change: 4 by the author's own breaking annotation in the changeset body — objectui declares no `major` inside a launch window (`scripts/check-changeset-no-major.mjs`). Each is marked **BREAKING** in the list above — read them before compiling the release record. + + **In this console build, declared nowhere** — objectui merged 11 commits in this range with no `.changeset/*.md`. The code is inside the pin above and ships here, but nothing upstream declared them, so they appear in no objectui CHANGELOG and in no entry above. Listed by subject rather than counted, because a count cannot tell a dependency bump from a form-behaviour change (objectstack#6174); the upstream gate that would prevent this is objectui#3387. + + - _(no changeset)_ fix(scripts): check-doc-links resolves the #fragment, not just the file (objectui#7644) (#7657) (objectui `f7cf7e8a9`) + - _(no changeset)_ docs(plugin-chatbot): document chatbot-floating's seven declared inputs keys (objectui#7594) (#7656) (objectui `8e501cb97`) + - _(no changeset)_ docs(agents): record the never-approve seat rule beside the governed never-list (#7630) (objectui `2e99852ca`) + - _(no changeset)_ refactor(examples): drop the inert root `title` from six catalog dashboards (#7634) (objectui `46cde8264`) + - _(no changeset)_ docs(check-skill-examples): drop the stale zero-jsonc-fences claim (#7631) (objectui `0b24d7f85`) + - _(no changeset)_ docs(governed-guard): replace the retired sha pin with the ruled approval-record predicate (#7616) (objectui `11edab88f`) + - _(no changeset)_ docs(skills): split multi-document JSON fences, drop the `...` elisions, mark every parsing fence (#7608) (objectui `89d6adf37`) + - _(no changeset)_ fix(scripts): judge spec citations at member granularity, and stop the header teaching a retired filter (objectui#7513) (#7617) (objectui `d28d87bf4`) + - _(no changeset)_ fix(governed-guard): an authorised approval record satisfies the queue leg on any commit (#7606) (objectui `0d8fd7ce3`) + - _(no changeset)_ chore(deps): Bump fumadocs-core from 16.14.4 to 16.15.4 (#7059) (objectui `1bae75bb8`) + - _(no changeset)_ docs(claude-md): collapse the two AGENTS.md excerpts to rule + hook + pointer (#7600) (objectui `c70ebaaeb`) + + + + objectui range: `00d3f09c500c...a472b07167a3` + ## 17.3.0 ### Minor Changes diff --git a/packages/console/package.json b/packages/console/package.json index f0d7d6a926..8e4edce2a2 100644 --- a/packages/console/package.json +++ b/packages/console/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/console", - "version": "17.3.0", + "version": "17.4.0", "description": "Prebuilt Console SPA pinned to this framework release, installed as a dependency of @objectstack/cli. Source of truth: @object-ui/console (https://github.com/objectstack-ai/objectui).", "license": "Apache-2.0", "homepage": "https://github.com/objectstack-ai/objectstack/tree/main/packages/console", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 5fc45598f2..d61dcfa993 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,166 @@ # @objectstack/core +## 17.4.0 + +### Minor Changes + +- 2ed6be6: Advisory validation rules no longer flood the startup log, and no longer count a row twice on a clean first boot. + + A `severity: 'warning'` (or `'info'`) validation rule is advisory: it never blocks a write, and its message is written for a person filling in a form. Evaluated across a seed load it produced one `WARN` line per row, so a clean-database first boot opened with a wall of form hints re-cast as boot diagnostics — and an app could reach "zero warnings" only by bending its data or deleting the rule. + + Two changes, and neither moves what a rule evaluates to: + + - **Aggregated reporting on the seed/boot path.** `SeedLoaderService.load()` now runs inside an advisory aggregation scope, and reports one summary line per rule — the rule, the object, the row count, the rule's own message and example rows — instead of one line per row. Off that path (an ordinary interactive write) nothing changes: the same per-write line is emitted verbatim. The new scope is `runWithAdvisoryAggregation` / `recordAdvisoryHit` in `@objectstack/core`. + - **Advisory rules are counted by row, not by write.** An `update` whose payload touches only platform-injected system columns — the shape `claimSeedOwnership` writes when it hands seeded rows to the first admin, `{ owner_id }` — changes no business field, so it no longer re-evaluates the object's advisory rules. Previously a seeded row rang once on insert and again when the claim scan rewrote `owner_id`, so anyone counting startup warnings over-estimated by the number of claimed objects. + + `error`-severity rules are untouched by both changes: an invariant is still enforced on every write, whoever issued it and however little it moved. Membership of the "system column" set is resolved per object by `resolveInjectedSystemColumns`, so an object that declares `ownership: 'org'` (no `owner_id`) or `systemFields: false` is judged on its own columns rather than a fixed list. + +### Patch Changes + +- 6f94458: fix(core): narrow the operation-private-keys pin's scanner to `.ts`, so it judges exactly the population turbo re-runs it for (#15090) + + `packages/core/src/security/operation-private-keys.pin.test.ts` filtered its + candidate set with `/\.tsx?$/` — `.ts` **and** `.tsx` — while this package's + declared radius in the cross-package declaration table is a `packages/**` + subtree glob ending in `.ts`. So the pin judged a population **strictly wider** + than the one either scoping layer of `check:cross-package-test-inputs` knows + about: Layer A never unions this package into the test shard when a `.tsx` file + changes, and Layer B never moves the `test` task's cache hash for one. A `.tsx` + file under `packages/` declaring its own `OPERATION_PRIVATE_KEY_PREFIX` or + `withoutOperationPrivateKeys` was therefore scanned by the pin and invisible to + CI's scoping — landing on `main` with every PR green and then reddening whichever + unrelated PR next touched a `.ts` file. That is the #7802 shape the declaration + table exists to close, one extension wide. + + Repaired by narrowing the **scanner**, not by widening the **glob** — and that + asymmetry is measured rather than assumed. On `b548e438d`, adding a `.tsx` glob + to this package's roster entry and re-deriving `check:cross-package-test-inputs`' + watch hints flips the dispatch-gates self-test case *"nor a .tsx test file inside + it"* from true to false, with the added glob itself as the covering hint. That + case is a live specimen for "a test class the hint route cannot reach", so the + red is real and re-pointing it is a decision in another lane, not a fixup. + + What the boundary costs, measured on the pin's own surface (tracked **plus** + untracked, ignored paths excluded) at `b548e438d`: **5408** `.ts` files scanned, + 8 of them mentioning a guarded symbol; **8** `.tsx` files excluded, **0** of them + mentioning either symbol. The loss is empty today — and that reading is no longer + transcribed and trusted. A new case re-measures it on every run: it asserts the + excluded `.tsx` population is non-empty (so the boundary is an exclusion and not + an empty tree describing itself), that the filter really drops those files, and + that none of them declares either symbol. Ablation, with the restore proven by + blob hash rather than by exit code: re-widening the scanner reddens it while the + offender assertion stays green — which is precisely the failure mode, since a + wider scanner reads as coverage CI never runs — and planting a `.tsx` + redeclaration reddens it with a message that says the choice is a second-gate + trade, not a one-line widening. + + The correspondence between scanner and glob is now stated at **both** ends: the + pin's header and the declaration table's entry for this package. No published + surface moves — the only source file edited is a test. +- 6e67b86: refactor(core): the authz context's time-zone probe is now the shared value-domain predicate, not a third copy of it + + `resolve-authz-context.ts` carried a module-private `isValidTimeZone` — the + `Intl.DateTimeFormat` probe, re-stated. It was the third copy of one + definition, alongside `@objectstack/spec/shared`'s `isValueDomainMember` and + `service-settings`' own re-statement. `coerceTimeZone` now calls + `isValueDomainMember('iana_time_zone', …)` and the copy is gone. + + **No behavioural change, measured rather than asserted.** The two predicates + were run over a shared 4,058-input corpus — the zones + `Intl.supportedValuesOf('timeZone')` omits (`UTC`, `Asia/Kolkata`, + `Europe/Kyiv`, `Asia/Ho_Chi_Minh`, `US/Eastern`, `GMT`), every member of that + enumeration plus its case- and space-padded variants, refusals, `Etc/` and + offset spellings, legacy aliases, and fuzz — with **zero disagreements**, and + the same zero at the `coerceTimeZone` level. The call site's own + pre-processing (trim, stringify a non-string, refuse blank) is unchanged. + + What this buys is drift resistance, not a fix: core's time-zone acceptance now + sits under the shared pins, so a future "modernisation" to + `Intl.supportedValuesOf('timeZone')` — which would silently narrow what the + authz context accepts, since that enumeration omits this platform's own + default `UTC` — turns a test red instead of shipping. +- d4f9b2a: A session whose active organization is no longer one the user belongs to now resolves with no active organization instead of that one's data. + + Under a wall-enforcing tenancy posture (`isolated` / `group`), `resolveAuthzContext` took a browser session's stored `activeOrganizationId` as the request tenant without ever comparing it to the user's current memberships — the framework's only such comparison was gated on an API-key principal. A session whose owner had been removed from an organization therefore kept reading that organization's rows and writing into it until the session expired on its own (7 days by default), including when the removal went through the product's own offboarding path. + + That claim is now vetted: if it is not in the caller's `accessible_org_ids`, it is dropped and the context resolves with no active organization at all, which the tenant wall already fails closed on (reads resolve to nothing; a tenant-scoped write is refused by ADR-0123 D2). The principal is **not** refused — a session is a person who may hold memberships elsewhere, so they stay signed in and can switch to an organization they are actually in. The API-key arm is unchanged: a key is its organization binding and is still refused outright. The wire is unchanged; the drop is reported to the operator as a single server-side `warn`. +- a727043: fix(rest,core): an organization-less or ex-member API key on a walled single-kernel deployment now answers 401 where it answered 200 + + Under a wall-enforcing tenancy posture (`isolated`), an API key stamped with an + organization its owner is no longer a member of **read and wrote that + organization's rows** on the wiring the open core actually builds. Not a silent + empty set — a GET that returned the other organization's records, and a POST + that landed a row read back from the store carrying that organization's id and + the ex-member as its creator. An organization-less key on the same deployment + read `200` with an empty set, which is the silent failure the wall exists to + replace. + + The cause was a seam, not a predicate. `RestServer.computeExecCtx` derived the + effective tenancy posture from a per-request kernel, and on the single-kernel + wiring there is no per-request kernel — so the posture was `undefined` on every + request, and both posture-conditional API-key refusals are gated on it: + `organization_required` in `api-key.ts` and `organization_membership_ended` in + `resolve-authz-context.ts`. Neither ever ran. The Layer 0 wall itself was + active the whole time; it compares against the caller's active organization, + and an API key's tenant is `sys_api_key.active_organization_id` copied verbatim + — the holder's own stored claim. Enforcing the wall is what let the ex-member + through, because the one fact that would expose the ended membership was not an + input to the layer that could act on it. + + The single-kernel branch now derives the posture from a provider `rest-api-plugin` + wires to the lone local kernel's `tenancy` service, in the same shape as the + auth-service provider beside it. A host that registers no `tenancy` service is + unchanged and still admits: there is no wall on such a deployment, so there is + nothing for an organization-less key to be walled out of. A `tenancy` service + that was registered and **failed to build** is an outage and answers `503`, not + an admission — a posture that could not be read is not a posture that is absent. + + Refusals are now also said out loud on the server side, at `warn`, where each + one is decided: the key's row id (never the credential or its hash), the + principal, the organization and the reason. **The wire is unchanged** — both + refusals still answer the generic `401 UNAUTHENTICATED` with no reason in the + body, so a holder of someone else's key learns nothing a plain 401 does not + already tell them. The operator, who previously had a key that was neither + revoked nor expired and a 401 that said nothing, now has a line to find. + + Behaviour that does not move: a current member's key on the same route still + returns its rows and still writes; a request with no credential still answers + 401; and an unknown, revoked or expired key is not a refusal at all, so a key + scanner produces no log volume. +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/spec@17.4.0 + - @objectstack/types@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index aabb64291d..cd4ab73e4e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/core", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Microkernel Core for ObjectStack", "type": "module", diff --git a/packages/create-objectstack/CHANGELOG.md b/packages/create-objectstack/CHANGELOG.md index e69af5c1aa..f80e3f0890 100644 --- a/packages/create-objectstack/CHANGELOG.md +++ b/packages/create-objectstack/CHANGELOG.md @@ -1,5 +1,7 @@ # create-objectstack +## 17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/create-objectstack/package.json b/packages/create-objectstack/package.json index bb6cdb946e..8b36af71df 100644 --- a/packages/create-objectstack/package.json +++ b/packages/create-objectstack/package.json @@ -1,6 +1,6 @@ { "name": "create-objectstack", - "version": "17.3.0", + "version": "17.4.0", "description": "Create a new ObjectStack project — npx create-objectstack", "bin": { "create-objectstack": "./bin/create-objectstack.js" diff --git a/packages/drivers/driver-memory/CHANGELOG.md b/packages/drivers/driver-memory/CHANGELOG.md index b586243483..6839eabfb4 100644 --- a/packages/drivers/driver-memory/CHANGELOG.md +++ b/packages/drivers/driver-memory/CHANGELOG.md @@ -1,5 +1,68 @@ # @objectstack/driver-memory +## 17.4.0 + +### Minor Changes + +- 2003259: fix(driver-memory): `find()`, `findOne()` and `create()` publish their declared types (#14435) + + **BREAKING** for TypeScript consumers — a published TYPE-surface narrowing, the same shape #13878 landed on `update()` / `upsert()` one door over, shipped as `minor` under the launch-window convention (`major` is refused by `check-changeset-no-major`, so the BREAKING banner and the ADR-0087 disposition are the carriers, not the level). + + `IDataDriver` has always declared `Promise[]>`, `Promise | null>` and `Promise>` on these three doors. The emitted `.d.ts` published `Promise`, `Promise` and `Promise>`: the return types of `find` and `findOne` were INFERRED through the backing store's `any[]` rows (`private db: Record` to `getTable()`), and `create` carried an explicit annotation that itself spelled `Record`. They are now declared as the contract declares them. + + What this asks of a consumer holding a concrete `InMemoryDriver`: a caller that reads fields off a `findOne()` result narrows the `null` arm first — the arm the driver has always been able to answer with (`results[0] || null`) and that no caller was ever asked to handle; and a caller that leaned on `any` to read a member off a `find()` row or a `create()` result now types it, since the rows are `Record`. A consumer whose receiver is typed as `IDataDriver` sees no change at all — that declaration already said this. + + The parameters are deliberately untouched: `create(data: Record)` stays as it is, because narrowing an INPUT would be a second, unrelated break, and method parameters compare bivariantly against the contract's `Record`. No runtime behaviour changes; the store keeps its `any[]` rows, which the card measured to cascade if re-typed. + + + +### Patch Changes + +- a646120: fix(driver-memory): the reference matcher's `$notContains` arm answers the predicate, not a type test, for a stored non-string value + + `match()` used to answer `{ n: { $notContains: '5' } }` with NO for `{ n: 5 }` — the arm read `typeof value !== 'string' || value.includes(target)`, so a number failed `$contains` (correct) AND its negation (wrong: for the very reason a number cannot contain the substring, it does not contain it). This package's own live mingo path admitted the row, so one filter answered two ways depending on which face was asked; on this face the failure mode was silently dropped rows. + + The arm now answers what `FILTER_TEXT_CASES`' new `score` rows declare on every face (maintainer ruling 2026-09-05 on the contract card): a stored value that is not a string never satisfies a positive text operator and always satisfies `$notContains`. The no-value cells keep their #13166 answer; nothing else in the matcher moved. +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/types@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/drivers/driver-memory/package.json b/packages/drivers/driver-memory/package.json index 1b7754973f..15ebe23139 100644 --- a/packages/drivers/driver-memory/package.json +++ b/packages/drivers/driver-memory/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-memory", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "In-Memory Driver for ObjectStack (Reference Implementation)", "main": "dist/index.js", diff --git a/packages/drivers/driver-mongodb/CHANGELOG.md b/packages/drivers/driver-mongodb/CHANGELOG.md index e36b1910a5..0188034206 100644 --- a/packages/drivers/driver-mongodb/CHANGELOG.md +++ b/packages/drivers/driver-mongodb/CHANGELOG.md @@ -1,5 +1,102 @@ # @objectstack/driver-mongodb +## 17.4.0 + +### Patch Changes + +- a06faeb: fix(driver-mongodb): put the test layer in front of tsc, so the package's own typecheck reports a PASS and not a NUMBER (#14917) + + `packages/drivers/driver-mongodb`'s `tsconfig.json` excluded `**/*.test.ts`, and + its `typecheck` script is `tsc --noEmit` against that very config. Measured at + `6ed4b811af` with the dependency closure built: that program admits **0** of the + package's 30 `src/**/*.test.ts` files while all **10** of its non-test `src/**` + files ARE there, so `pnpm --filter @objectstack/driver-mongodb typecheck` + exiting 0 was a true sentence carrying no information about any test file. + + The filing's headline — that a compile-time `Equals` / `IsAny` pin here is + "checked by nothing" — is **false**, and the correction on the card is right: a + second program does compile these files. `check-type-check-coverage.mjs`'s + `remeasureProject` drops only the test glob and compares the result against its + `TEST_DEBT` ledger. Confirmed here by ablation rather than argued: a + deliberately false `Equals` pin added to `mongodb-driver.test.ts` takes that + program from 10 errors to 11, above the ledger's recorded 10, which reddens it. + The pins were never phantoms. What was true is narrower, and is what this change + closes: the only program reading this layer was a **debt ratchet** — an + instrument that reports a number and fails when the number moves, not a gate + that reports a pass. + + Gives the package the #5286 sibling shape (`packages/rest`, `runtime`, + `objectql`, `core`): a `tsconfig.test.json` with module semantics only — + `esnext` / `bundler` / `lib: ES2022`, matching how vitest actually executes + these files — strictness inherited and untouched, named by the `typecheck` + script via `check:test-typecheck`. + + Measured: **10** errors under the ratchet's shape (matching its recorded number, + and its recorded composition `TS1309 x7, TS2550 x3`, class for class), and **0** + under the split. All 10 were config-tier in full — 7 `TS1309` (`await` at module + scope in a program NodeNext compiles as CJS, because this package has no `"type": + "module"`) and 3 `TS2550` (`Array.prototype.at` against a `lib` older than + es2022). Neither class says anything about a test, and nothing was exposed + behind them: there was no unresolved-import cascade here to collapse, so there + is no `+n` term. `noUnusedLocals` / `noUnusedParameters` are live for this + package (unlike `driver-turso`, which switches both off) and neither fires. + + The `TEST_DEBT` entry (10 errors) is **deleted**, not lowered — the graduation + this ratchet's invariant requires. No `test-typecheck-debt.json` is added: + residue is 0, so none is owed (#5286, maintainer-only to open). That leaves all + 30 files unledgered, so any error any one of them gains is red on arrival. + + `check:type-source-resolution` went red from onboarding the new program (the + documented onboarding-limb case, #11490): a registry entry is added rather than + `paths`, with its numbers stated in place — 123 tsc programs / 309 pairs before, + 124 / 310 after. The single new pair is `@objectstack/objectql`, a devDependency + that no non-test file in `src/` imports. + + No runtime code changes: not one test file and not one source file is edited, so + no shipped behaviour moves — the suite reports the same 552 passed / 147 skipped + across 30 files as before. The `patch` level reflects the published + `package.json` gaining `typecheck` / `check:test-typecheck` scripts and a `tsx` + devDependency. +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/types@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/drivers/driver-mongodb/package.json b/packages/drivers/driver-mongodb/package.json index ed800a419c..0cff5f31fc 100644 --- a/packages/drivers/driver-mongodb/package.json +++ b/packages/drivers/driver-mongodb/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-mongodb", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "MongoDB Driver for ObjectStack - Native document database driver via official mongodb client", "main": "dist/index.js", diff --git a/packages/drivers/driver-sql/CHANGELOG.md b/packages/drivers/driver-sql/CHANGELOG.md index 6a2866044b..32c804524e 100644 --- a/packages/drivers/driver-sql/CHANGELOG.md +++ b/packages/drivers/driver-sql/CHANGELOG.md @@ -1,5 +1,93 @@ # @objectstack/driver-sql +## 17.4.0 + +### Minor Changes + +- 54bb2f1: The analytics SQL compilers compile the case-sensitive text family per dialect, so a `$contains` policy on SQLite stops admitting rows it excludes (#15684) + + `$contains` / `$notContains` / `$startsWith` / `$endsWith` are case-SENSITIVE on every backend (#4706 Q2 = A). All three of `service-analytics`' SQL compilers emitted `col LIKE ? ESCAPE ?` on every dialect, and SQLite's `LIKE` folds ASCII case unconditionally — the fold cannot be turned off per statement, because `PRAGMA case_sensitive_like` is a connection-global switch. Measured on sql.js over the shared `FILTER_TEXT_ROWS` fixture, `{ name: { $contains: 'acme' } }` answered `['1','2']` — `ACME Corp` **and** `acme corp` — where `FILTER_TEXT_CASES` says `['2']`. + + On two of the three compilers that is a wrong chart. The third is `read-scope-sql.ts`, the ADR-0021 D-C read scope: a scope that **admits** rows the policy's case-sensitive predicate excludes is over-reach, not a loose filter — the same reading that file already applied to its own `LIKE` escaping. The `/analytics/sql` echo was wrong in a third way: it printed `LIKE` while the statement it claims to reproduce ran through a driver that has emitted `GLOB` on the SQLite dialects since #6518. + + What changed: + + - **The construct is chosen per dialect** (`text-match-sql.ts`), arm for arm with `driver-sql`'s own table: `GLOB` on SQLite (case-exact by definition, with its own `*` / `?` / `[` escaped class and no `ESCAPE` clause), `LIKE` over `CAST(… AS BINARY)` on MySQL, and `LIKE` **unchanged** on Postgres, where it is already exactly the ruled semantics. There is no single construct that is case-exact and parses on all three, so the dialect had to become an input rather than a guess. + - **The dialect arrives from the driver that will execute the statement.** New optional `AnalyticsServiceConfig.sqlDialect`, wired by `AnalyticsServicePlugin` from `IDataEngine.getDriverForObject`. `SqlDriver.dialectName` is now public so that answer can be read without a second dialect-resolution table drifting behind the driver's own knex spellings; it is derived and read-only. + - **A host that answers no dialect keeps the `LIKE` it always got** — "cannot answer, do not block". Postgres deployments see byte-identical SQL. + + `$icontains` is untouched: it keeps its own ASCII-only fold on both sides, and collapsing the two families onto one path would hand the case-exact family back the fold the ruling took away from it. `LIKE` escaping is unchanged wherever a `LIKE` is still emitted. +- a646120: A text operator over a column whose declared type stores no text (`Field.number` and its numeric siblings, `Field.boolean`) now compiles to the contract's declared answer on every dialect, instead of a dialect accident. + + Before: `{ score: { $contains: '5' } }` over a numeric column compiled `col GLOB '*5*'` on SQLite and coerced the REAL in its storage class's spelling (`5` as `'5.0'`, so `$endsWith: '0'` matched every row), `col LIKE $1 ESCAPE $2` on Postgres and was refused at query time with SQLSTATE 42883 (`operator does not exist: real ~~ text` — a 500 for a filter the spec accepts), and `CAST(col AS BINARY) LIKE ?` on MySQL. + + Now (`FILTER_TEXT_CASES`' `score` rows, maintainer ruling 2026-09-05): the positive operators (`$contains` / `$startsWith` / `$endsWith` / `$icontains` / `$like` / `$ilike`) compile to `1 = 0` and `$notContains` to `1 = 1` — the same row set as every JS face, decided from the declared type at compile time because the stored value is not visible until run time. Postgres: a 500 becomes a result. The gate reads the `numericFields` / `booleanFields` registries `initObjects` and `registerExternalObject` already fill; a table this driver was never told about keeps the `LIKE` / `GLOB` it always compiled, every comparand refusal still runs first, and the constants compose with the NULL-safe rules (`$notContains` admits a NULL row already) and the `$not` rewrite. Temporal columns are untouched: their stored value IS text on SQLite, so the contract declares nothing for them. + + `driver-sqlite-wasm` and `driver-turso`'s local transport inherit this compiler. +- 2200f8e: feat(driver-sql): `update()` publishes its honest type — the contract's `Record | null`, not `any` (#14438) + + **BREAKING** for TypeScript consumers — a published TYPE-surface narrowing, shipped as `minor` under the launch-window convention (the one PR #14434 used for the same door on `@objectstack/driver-memory`). `SqlDriver.update()` was written out with an explicit `Promise` while it has always answered a missing id with `null` (`formatOutput(...) || null` on the un-rotated path, `null` once every rotation shard has been probed). `IDataDriver.update()` declares `Promise | null>`, and an explicit `any` satisfies that structurally — so the emitted `.d.ts` read `Promise` and no caller holding a `SqlDriver`, or a `SqliteWasmDriver` (which inherits the door unchanged), was ever asked to narrow. It is now declared as the contract declares it, and the protected rotation-path producer `rotatedUpdateById()` carries the same type. A caller that read fields off `update()`'s result through the `any` now narrows the `null` arm first; a caller that leaned on `any` to read undeclared members now types them. No runtime behaviour changes. + + `@objectstack/driver-sqlite-wasm` re-declares no `update` member of its own (measured on its emitted `.d.ts`), so it carries no entry: the narrowing reaches its consumers through this package's `.d.ts`. `@objectstack/driver-turso` overrides the door and carries its own entry. + + + +### Patch Changes + +- 61821e5: A plain unique index over existing duplicate rows no longer kills the boot with the database's raw error, and `os migrate plan` no longer calls that op `safe`. + + Declaring a column unique over a table that already holds duplicates had two very different outcomes depending on one branch in the SQL driver, and only one of them was survivable. + + - **An organization-scoped unique** (the `unique: 'organization'` default, materialised as the NULL-safe `COALESCE(organization_id, '__global__')` composite) kept the boot up: the driver logged at `error` naming the index, the constraint that is not enforced and the remedy, and the ADR-0120 D4 duplicate pre-flight reported the blocked `create_index` as `category: 'destructive'` / `severity: 'error'` with the conflicting key groups and their row counts. + - **A plain unique** — no organization key part at all, reached by an object with `tenancy: { enabled: false }` or by any explicit `unique: 'global'` — took the process down: `initObjects` threw the database's own error, which names the index and the column and no rows and no remedy, nothing reached the durability channel, and `detectManagedDrift` (what `os migrate plan` reports) classified the very same op `category: 'safe'`, `severity: 'warning'`, so `os migrate apply` and dev `autoMigrate: 'safe'` walked straight into the raw failure. + + The plain path now reaches the same posture as the scoped one: + + - **The boot survives and says what is not enforced.** `syncDeclaredIndexes` absorbs a uniqueness violation on a plain unique index the way it already absorbed one on the NULL-safe composite: the failure is logged on the durability channel (`error`) naming the index, the conflicting key groups with their row counts, the constraint that is NOT enforced, and `os migrate plan` as the way out. A non-unique index and any failure that is not a uniqueness violation still surface as before. + - **The duplicate pre-flight covers it.** The ADR-0120 D4 probe no longer skips ops whose NULL-safe column set is empty, so a plain unique `create_index` over dirty data is reported `destructive` / `error` with the same row report instead of `safe`. Nothing new probes it: the existing probe already groups by the bare columns when there is no NULL-safe key part, so both key shapes share one pre-flight rather than a second copy that can drift from the first. + + Consumers of the classification see the op move from the "Safe" group to "Destructive (requires --allow-destructive)" in `os migrate plan` and `os diff`; `os migrate apply` defers it instead of attempting it; the artifact boot gate refuses with a named destructive-drift refusal instead of crashing; and dev `autoMigrate: 'safe'` leaves it alone. Clean data is unaffected — the probe finds nothing and the index is created exactly as before. +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/types@17.4.0 + - @objectstack/observability@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/drivers/driver-sql/package.json b/packages/drivers/driver-sql/package.json index b6c89c50ff..55cd30d82b 100644 --- a/packages/drivers/driver-sql/package.json +++ b/packages/drivers/driver-sql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-sql", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "SQL Driver for ObjectStack - Supports PostgreSQL, MySQL, SQLite via Knex", "main": "dist/index.js", diff --git a/packages/drivers/driver-sqlite-wasm/CHANGELOG.md b/packages/drivers/driver-sqlite-wasm/CHANGELOG.md index 81cbce514d..c9a97b319f 100644 --- a/packages/drivers/driver-sqlite-wasm/CHANGELOG.md +++ b/packages/drivers/driver-sqlite-wasm/CHANGELOG.md @@ -1,5 +1,51 @@ # @objectstack/driver-sqlite-wasm +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [54bb2f1] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [a646120] +- Updated dependencies [2200f8e] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [61821e5] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/driver-sql@17.4.0 + - @objectstack/spec@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/drivers/driver-sqlite-wasm/package.json b/packages/drivers/driver-sqlite-wasm/package.json index a2890926f1..e97049414b 100644 --- a/packages/drivers/driver-sqlite-wasm/package.json +++ b/packages/drivers/driver-sqlite-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-sqlite-wasm", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "WASM SQLite Driver for ObjectStack — runs in browser/WebContainer (StackBlitz) without native bindings", "keywords": [ diff --git a/packages/drivers/driver-turso/CHANGELOG.md b/packages/drivers/driver-turso/CHANGELOG.md index 33c184df82..9999040288 100644 --- a/packages/drivers/driver-turso/CHANGELOG.md +++ b/packages/drivers/driver-turso/CHANGELOG.md @@ -1,5 +1,62 @@ # @objectstack/driver-turso +## 17.4.0 + +### Minor Changes + +- a646120: The remote transport compiles a text operator over a declared numeric or boolean column to the contract's declared answer, in step with the local transport. + + `RemoteTransport.buildWhereSQL` compiles filters independently of `SqlDriver` and keeps no schema, so a text operator over a `Field.number` used to compile `"col" GLOB ?` and coerce the REAL in the storage class's spelling (`5` as `'5.0'`). `TursoDriver` now hands the transport its declared-type rule (`setNonTextColumnResolver`, the same shape as the temporal `setFilterColumnSql` rule), answered from the registries `registerRemoteFieldMetadata` already fills at schema sync — so a positive text operator over such a column compiles to `1 = 0` and `$notContains` to `1 = 1` on BOTH transports (`FILTER_TEXT_CASES`' `score` rows, maintainer ruling 2026-09-05), instead of a dialect accident. A transport nobody handed the rule to compiles exactly as before, and every comparand refusal still runs ahead of the constant. +- 2200f8e: feat(driver-turso): the `update()` override publishes its honest type — `Record | null`, not `any` (#14438) + + **BREAKING** for TypeScript consumers — a published TYPE-surface narrowing, shipped as `minor` under the launch-window convention. `TursoDriver` overrides `update()` rather than inheriting it, and the override was written out with its own explicit `Promise` — so this package's emitted `.d.ts` re-declared the door as `any` on its own and would not have picked up the `@objectstack/driver-sql` narrowing. Both of its branches already answered the contract's type: the local branch forwards to `SqlDriver.update()` (narrowed alongside, #14438) and the remote branch passes `RemoteTransport.update()`'s `Record | null` (#14428) through the generic `formatRemoteRow`. The override now declares what it answers. A caller that read fields off the result through the `any` now narrows the `null` arm first. No runtime behaviour changes. + + + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [54bb2f1] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [a646120] +- Updated dependencies [2200f8e] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [61821e5] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/driver-sql@17.4.0 + - @objectstack/spec@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/drivers/driver-turso/package.json b/packages/drivers/driver-turso/package.json index c8ae28b210..0d751e049e 100644 --- a/packages/drivers/driver-turso/package.json +++ b/packages/drivers/driver-turso/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/driver-turso", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Turso/libSQL Driver for ObjectStack — Edge-first SQLite with embedded replicas", "keywords": [ diff --git a/packages/formula/CHANGELOG.md b/packages/formula/CHANGELOG.md index 400ceab0f1..d22d32af1d 100644 --- a/packages/formula/CHANGELOG.md +++ b/packages/formula/CHANGELOG.md @@ -1,5 +1,40 @@ # @objectstack/formula +## 17.4.0 + +### Patch Changes + +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/spec@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/formula/package.json b/packages/formula/package.json index b51f0efeb9..e4bc189002 100644 --- a/packages/formula/package.json +++ b/packages/formula/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/formula", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "ObjectStack canonical expression engine — CEL (cel-js) + ObjectStack stdlib + dialect registry", "main": "dist/index.js", diff --git a/packages/lint/CHANGELOG.md b/packages/lint/CHANGELOG.md index 055bc3a281..7a8c6d8978 100644 --- a/packages/lint/CHANGELOG.md +++ b/packages/lint/CHANGELOG.md @@ -1,5 +1,379 @@ # @objectstack/lint +## 17.4.0 + +### Minor Changes + +- 954cb0b: feat(service-automation): an `assignment` value may be a CEL envelope — evaluated at run time, validated at `registerFlow`, `objectstack validate` and the runtime publish gate (#15137, the executor half of #14149) + + + + **BREAKING** in the accept-set sense, landing in the launch window as `minor` + (the lockstep convention; the level also follows the 2026-09-04 bump ruling — + this adds `AutomationEngine.evaluateValueEnvelope` to a published surface, and an + additive widening is at least `minor`). No ADR-0087 conversion: no authorable key + is renamed or retired, and the shape this refuses was never a shape any surface + offered. + + The maintainer's 2026-09-02 ruling on #14149 made an assignment value able to be + a CEL **value** expression, so the declared stdlib (`joinNonEmpty`, `map`, `size` + …) is finally reachable from metadata — until now CEL was only ever asked for a + boolean. The spec half landed the contract (PR #15113); this is the half that + makes it do something. + + ```yaml + # before: written into the variable verbatim, and rendered by `notify` as + # {"dialect":"cel","source":"joinNonEmpty(...)"} + # now: evaluated — digest is "Renewal due\nInvoice overdue" + assignments: + digest: { dialect: cel, source: 'joinNonEmpty(rows.map(r, r.subject), "\n")' } + ``` + + - **Evaluated at run time.** The built-in `assignment` executor evaluates a + `value`-role envelope with the expression engine and assigns the result, in the + same CEL scope a flow predicate is evaluated in (one shared scope builder, so a + predicate and a value expression cannot disagree about what `rows` means). A + plain string keeps today's `{token}` interpolation, and every other literal is + still assigned as data. + - **Refused at three doors.** A malformed envelope now stops the flow registering + (`registerFlow` throws, the severity a malformed predicate gets) and surfaces as + a located `error` finding naming the node and the author's own variable — + `config.assignments.digest` — both at `objectstack validate` and at the runtime + publish gate a Studio / REST / MCP flow write goes through + (`validateStackExpressions` is registered `CLI_AND_RUNTIME`, `runtimeTypes: + ['flow']`). Malformed is a composition, not a fixed list: whatever + `AssignmentValueSchema` refuses in the envelope's shape — among them a missing, + empty or non-string `source`, a dialect other than `cel`, a non-object `meta` — + and then CEL that does not parse. All three doors derive that set from the same + two published validators, so none refuses a shape the executor would have run, + and a registered flow never faults for a shape those validators judge malformed. + Two shapes sit outside what either validator can judge — an `ast`-only envelope + and a whitespace-only `source` (it passes `min(1)` and reads as "not authored" + to the validator, while the CEL engine parses it untrimmed) — and those fault + loudly at run time rather than assigning a value. Both are pinned and tracked in + #15430. + - **Only the canonical map.** The ledger declares `assignment.assignments.*` and + nothing else, so the two legacy shapes the executor still normalizes — the + `assignments: [{ variable, value }]` array and the bare `{ : }` + config — keep every meaning they had, envelope-shaped values included. + `AssignmentConfigSchema` is deliberately NOT wired into `parseNodeConfig` for the + array form: refusing it would break flows that register today, and that refusal + is a maintainer ruling rather than a lane's call (#15137 ask 3). + + **What changes silently, and how far it reaches.** A flow that today authors an + envelope-shaped object *as data* in the canonical `assignments` map now evaluates + it — no error on either side, a different value. The discriminator is the spec's + own `isExpressionEnvelopeShaped`: a plain object naming a **string** `dialect`, + in the declared map only. Data that names no `dialect`, names a non-string one, + nests the envelope one level down, or sits in either legacy shape is untouched + and byte-identical. The remaining overlap — a well-formed + `{ dialect: 'cel', source: … }` written as data in the canonical map — is exactly + the spelling the ruling reinterprets; every near-miss the two validators can + judge now refuses loudly at registration instead of changing value in silence. +- 36a16d0: Two new widget-binding rule ids for a chart widget with an empty selection + + `validateWidgetBindings` reported nothing about two dataset-bound chart shapes that the + `@object-ui` revision this repo pins (`.objectui-sha`) visibly degrades. Both are now + warnings, suppressible per widget with `suppressWarnings: ['']`: + + - `chart-measures-missing` — a chart-family widget selects no measures (`values` empty or + absent). `DatasetWidget.tsx:683` returns the authoring placeholder "Pick measures + (values) for this dataset widget." before any query runs, above every family branch, so + no chart is drawn at all. + - `chart-dimensions-missing` — a chart-family widget selects at least one measure but no + dimensions. `DatasetWidget.tsx:423` reads + `const isMetric = METRIC_TYPES.has(widgetType) || dimensions.length === 0;`, so the + widget renders as a single KPI number and the declared chart family is silently ignored. + The hint steers the author to a dimension, or to the `metric`/`kpi` family that matches + what actually renders. + + Warning tier rather than error for both: an empty selection is a work-in-progress state a + build must tolerate, and erroring would gate the `sys_metadata` publish path on a + half-authored widget. Neither shape is folded into `chart-config-missing` — neither is + caused by, nor repairable with, `chartConfig`, which carries presentation only. + + "Chart family" is derived, not hand-listed: every declared `ChartTypeSchema` option that + the pinned renderer routes to its chart branch — the taxonomy minus the renderer's own + `METRIC_TYPES` (`metric`, `kpi`, `gauge`, `solid-gauge`, `bullet`) and its `table`/`pivot` + tabular test. A `metric` tile with no dimensions, such as the shipped `system_overview` + board's own KPI tiles, is therefore not a finding. +- c01b3a6: `chart-field-unknown` drops to `warning` on the three `chartConfig` binding keys the pinned renderer refuses, and says what actually happens + + The rule id covers exactly three positions, and the `@object-ui` revision this repo pins (`.objectui-sha`) refuses all three as bindings, so none of them can produce the data failure the messages described: + + - `chartConfig.xAxis.field` — `axisPresentation` (`@object-ui/core` `src/utils/chart-presentation.ts`) builds the axis presentation **minus** its `field`. The x-axis key is `buildChartSeries`' `xAxisKey`, i.e. the widget's `dimensions[0]`; an authored `field` re-points nothing. + - `chartConfig.yAxis[].field` — the same call, per entry. The entry keeps its slot (the count is what turns on a secondary axis) and its scale and chrome; only the binding is dropped. + - `chartConfig.series[].name` — `mergeAuthoredSeries` pairs an authored entry with the derived series whose `dataKey` it equals, one per entry of `values`. An entry naming no derived series is ignored whole, so the presentation hung on it — the mark, the colour, the stack, the axis side — lands on nothing. + + The renderer pins this by name in `DatasetWidget.chartConfig.test.tsx` ("ignores an authored axis `field` and keeps the derived axis binding", "ignores an authored series and keeps one derived series per measure"). + + So the old message — "the query result will not contain it" — named a query failure that never happens, and `error` blocked a build and a Studio publish for a key that changes nothing at runtime. That is the class `widget-legacy-analytics-shape` reports at `warning` in the same file ("the dashboard renderer ignores them … a silent no-op"), and this id now carries the same tier, the same suppressibility (`suppressWarnings: ['chart-field-unknown']` per widget) and the same kind of sentence. Each message states its own consequence, because the axis positions and the series position are refused for different reasons. + + The finding is **kept**, not deleted: unlike the `chart-config-missing` over-reach this measurement came from, the metadata really is wrong — the author wrote a binding and believes it is in force. + + ## Migration + + **A publish that used to be refused now succeeds.** Ruled 2026-08-15, `validateWidgetBindings` put its whole error set on the `sys_metadata` publish door (Studio / REST `/meta` / MCP) as one "this board cannot render" reference-integrity class. That class was six ids and is now five — `chart-field-unknown` has left it. A dashboard write whose only reference-integrity problem is a refused `chartConfig` binding key is no longer a 422 `INVALID_METADATA`; it publishes, and the finding rides the non-blocking `advisories` channel on the 2xx response instead. The other five (`widget-dataset-unknown`, `widget-dimension-unknown`, `widget-measure-unknown`, `widget-legacy-analytics-unrenderable`, `dashboard-filter-field-unknown`) are unchanged. + + Same direction on the CLI: `os validate` / `os build` / `os lint` report the finding at `warning`, so a stack that used to fail the build over one of these keys now exits 0 with an advisory. If you were relying on the build to stop on it, add the key to your own gate, or fix the binding — the fix has not changed: + + - point `xAxis.field` at a dimension the widget selects (or drop the key — `xAxis` carries presentation only); + - point `yAxis[].field` at a selected measure (or drop it — `yAxis[]` carries presentation only); + - name a selected measure in `series[].name`, remembering that post-cutover (ADR-0021) result rows are keyed by the dataset's measure **name** (`sum_amount`), not the base column (`amount`). + + A deliberately inert key can be silenced per widget with `suppressWarnings: ['chart-field-unknown']`. +- 56fe8c2: A flow predicate authored as a CEL envelope is now refused at build time, instead of running unread by either validator. + + A `predicate`-role expression slot holds **bare CEL text** — `DecisionConditionSchema.expression` is declared `z.string()`, and so is a screen field's `visibleWhen`. An author who instead wrote the `{ dialect, source }` expression *envelope* there reached a shape nothing could see: a flow node's `config` is an open `z.record(z.unknown())` that no Zod schema is parsed against, the unknown-key walk exempts the schemaless node types on purpose (`decision` publishes no descriptor `configSchema`), and the expression ledger's `predicate` arm skipped every non-string as "a type violation for the schema pass to report" — a schema pass that, for those node types, does not exist. `registerFlow` accepted the flow, `objectstack validate` reported nothing, and the evaluator was the only layer that ever read the predicate. + + - `resolveFlowNodeExpressions` now emits a non-string sitting in a `predicate` slot, and the new `predicateSlotRefusal` / `PREDICATE_SLOT_STRING_REFUSAL` say why it is refused — one notion, derived once, read by both validators so build time and author time cannot disagree about the shape. `flow-template` slots keep the old rule: no validator implements that dialect, so a finding there is one nobody could judge. + - `registerFlow` throws, naming the node, the slot and the index, and attributing the finding to the envelope's own `source`. `objectstack validate` reports the same refusal as a located `error`. + + **String predicates are untouched, deliberately.** A whitespace-only string still means "not authored" on both sides, exactly as before; what a non-empty string *says* is still judged by `validateExpression('predicate', …)`, brace trap and all. Only the shape moved. + + An app that authored an envelope in one of these slots now fails to register with a message naming the slot; the fix is to write the predicate as bare CEL text (`record.rating >= 4`). The `{ dialect, source }` envelope remains the `value`-role spelling, on the `assignment` node's `assignments` map. +- b4b37e5: The object publish door now refuses an object whose `searchableFields` entry, or whose built-in list view's `columns` (and every other field-naming position on that list view), names a field the object does not have. + + `#15254` closed this one key over: it crossed the reference-integrity suite onto the object write door for the object's own field-name **lists** (`highlightFields`, `publicSharing.redactFields`). The two members that read the *other* field surfaces an object carries — its ADR-0061 search set and its built-in `listViews` — still declared `runtimeTypes: ['flow', 'view']`, so on the only door a Studio, REST `/meta` or MCP author has they never judged the snapshot that arrived. An object could publish clean with `searchableFields: ['gone_field']` or a list-view column resolving to nothing, and both fail the same silent way downstream: the engine filters a stale search entry out without a word (`resolveSearchFields`), so `$search` scans a narrower set than declared — or, once every entry is stale, the auto-default set the author never chose — and a dangling column renders one field short. + + - **`validateSearchableFields` and `validateListViewFieldRefs` gain `object`** in their suite-member `runtimeTypes`. No new rule and no new finding class: the rule ids (`searchable-field-unknown`, `searchable-field-unsearchable`, `list-view-field-unknown`, `list-view-field-dotted`) and their severities are unchanged — they now reach the door where the author actually is. + - **The crossing carries the #9313 precondition.** Both members resolve only against `stack.objects`, the one collection every per-write snapshot carries, so neither opens a missing-collection false-positive channel; their `views[]` rungs simply find no `stack.views` on an object snapshot. + - **Measured before crossing**, at the door's own snapshot shape and differential, over every shipped object definition in the monorepo: 116 objects (platform-objects 48, showcase 24, plugins 19, services 12, crm 6, metadata-core 5, todo 1, qa 1), 105 built-in list views on 40 objects, 666 list-view field-naming positions and 5 `searchableFields` entries judged — **0 findings for both members, precision 1.0**, against synthetic probes that are refused. + - **`validateSortableFields`, the third sibling, is deliberately not crossed** — it measured equally clean, but that crossing is its own adjudication. + + ## Migration + + **A publish that used to succeed can now be refused (HTTP 422, `INVALID_METADATA`).** The receipt names the rule id and the offending path, name-keyed on the wire — for example `objects.proj_task.searchableFields[1]` or `objects.proj_task.listViews.all.columns[1]` — plus the string that was written and the fields the object actually has. + + To fix a refusal, do one of: + + - rewrite the entry to the field's current API name (after a Studio label edit the derived name is the one to use — `field_10` becomes `health_score`); or + - drop the entry from the declaration; or, for `searchable-field-unsearchable`, target a text-like stored column instead of a virtual or non-scannable one. + + `os validate` / `os build` / `os lint` already reported these findings at the same severity, so a code-authored stack can be repaired before it reaches a publish. Objects that name a platform-injected system column are unaffected — both members resolve those per object and stay silent where the platform really provisions them. +- 9408b7f: A flow condition that is neither CEL text nor an expression is now refused at build time, instead of being read as an empty condition and answering a silent `false`. + + `evaluateCondition` derives its source as `typeof expression === 'string' ? expression : (expression?.source ?? '')`. For a value that is neither — a number, a boolean, an array — the read yields `undefined`, the `??` supplies `''`, and the empty-source arm returns **`false`**: the "an unauthored branch must not open" rule, applied to a value that was very much authored. Measured: a `decision` node carrying `config: { condition: 42 }` **registered clean** and executed `success: true` with nothing said at any layer; `{ source: 1 }` did not even get that far and threw a bare `TypeError: exprStr.trim is not a function` out of the validator. `config.condition` is also the key a **start node's trigger gate** is read from, so the same value could gate a whole flow shut forever with no signal to the author. + + - The new `structuralConditionRefusal` / `STRUCTURAL_CONDITION_SHAPE_REFUSAL` in `@objectstack/spec/automation` are the single shared notion of why, read by both validators so build time and author time cannot disagree about the shape. `registerFlow` throws, naming the node or edge and attributing the finding; `objectstack validate` reports the same refusal as a located `error`. + + **This is deliberately NOT the `predicate`-slot rule, and the difference is measured.** A ledger `predicate` slot (`decision.conditions[].expression`, a screen field's `visibleWhen`) is declared `z.string()`, so `PREDICATE_SLOT_STRING_REFUSAL` refuses every non-string including an envelope. Neither structural slot is declared that way: `FlowEdgeSchema.condition` is `ExpressionInputSchema`, whose string arm **transforms into** `{ dialect: 'cel', source }` — so after `FlowSchema.parse` every authored edge condition *is* an envelope — and `FlowNodeSchema.config` is an open `z.record` that passes an envelope written at `config.condition` through verbatim, where `evaluateCondition` evaluates it correctly. Both shapes stay accepted here; an envelope with no `dialect`, and an `ast`-carrying one (`ExpressionSchema`'s own `source`-or-`ast` rule), stay accepted too. + + **Strings are untouched, deliberately.** A whitespace-only condition still means "not authored" and still answers `false` on both sides — consistent behaviour, ruled correct, not a defect. What a non-empty string *says* is still `validateExpression('predicate', …)`'s verdict, brace trap and all. Only the shape moved. + + An app that authored a number, a boolean, an array or a source-less object in a node or edge `condition` now fails to register with a message naming the site; the fix is to write the condition as bare CEL text (`record.rating >= 4`) or as an expression envelope. +- 615fac3: A publish now refuses an object whose `highlightFields` names a field that does not exist on it — the same gate that refuses a code-authored stack. + + `list-view-field-unknown` inspects `view.columns`, and Studio's app builder mints no `view` items at all, so the reference-integrity family had nothing to inspect on the only artifacts the click path authors. What it authors is the **object**, and an object-level field-name list was covered by nothing that could refuse: measured on `origin/main`, `runtimeAuthoringRulesFor('object')` dispatched seven rules with no reference-integrity rule among them, while the object-level existence check that did exist (`semantic-role-field-unknown`) is `warning`, advisory-tier and CLI-only. So `os validate` exited 0 on a dangling reference and the runtime publish door — the only door a Studio, REST `/meta` or MCP author has — said nothing at all. + + The reproduction is the natural click order, not a contrived one: click-create a field (Studio mints it as `field_10`), add it to `highlightFields`, then give it a label — the API name auto-derives to `health_score` and `highlightFields` keeps `field_10`. Anyone who names a field after placing it produces this. + + - **New rule `object-field-ref-unknown` (`error`)**, in `@objectstack/lint`, over the object-level field-name **lists** that no rule owned: `highlightFields` (ADR-0085) and `publicSharing.redactFields`. It resolves through the same `object-graph` seam as the rest of the family, so the three shared skips hold — an object outside the stack, an object with no readable field map (ADR-0015 `external`), and a registry-injected system column resolved **per object** (`highlightFields: ['owner_id']` is a live pointer on an owned object and a real miss under `ownership: 'none'`). + - **It runs on the runtime publish door.** The reference-integrity suite entry's `runtimeTypes` gains `object`, and the suite's per-member declaration keeps the crossing narrow: this is the only member that judges an object snapshot; every other member keeps `['flow', 'view']` or the frozen `['flow']` default. + - **`validateSemanticRoles` keeps the provenance question** at the same position (`semantic-role-field-unprovisioned`, still `warning`) and no longer restates existence — one finding per path, at one tier. + - **`probes.checked` gained an `objects` counter.** Its absence was the tell: a receipt reading `{seeds: 0, views: 0, widgets: 0}` was accurate while the objects the package published were probed by nothing. + + ## Migration + + **A publish that used to succeed can now be refused (HTTP 422, `INVALID_METADATA`).** The receipt names the rule id `object-field-ref-unknown` and the offending path, name-keyed on the wire — for example `objects.proj_task.highlightFields[1]` — plus the string that was written and the fields the object actually has. + + To fix a dangling reference, do one of: + + - rewrite the entry to the field's current API name (after a Studio label edit the derived name is the one to use — `field_10` becomes `health_score`); or + - drop the entry from the list. + + `os validate` / `os build` / `os lint` report the same finding at `error`, so a stack can be repaired before it reaches a publish. If an object legitimately points at a platform-injected system column, no change is needed — the rule resolves those per object and stays silent where the platform really provisions them. +- cd55558: `widget-measures-missing` — the empty-measure selection is reported on every widget family, not just charts + + `chart-measures-missing` (#15462) reported the authoring placeholder only for the chart + family, but the return that produces it is type-independent. At the `@object-ui` revision + this repo pins (`.objectui-sha` = `a472b0716`), `packages/plugin-dashboard/src/DatasetWidget.tsx:683` + reads `if (values.length === 0)` and returns *"Pick measures (values) for this dataset + widget."* ABOVE `isMetric` (`:423`, over `METRIC_TYPES` at `:343`), `isTable` (`:424`) and + the chart branch alike. So a `metric`, `kpi`, `gauge`, `solid-gauge`, `bullet`, `table` or + `pivot` widget that selects no measures renders the same placeholder — the KPI number or + the table the author declared is not drawn at all — and nothing reported it: + `table-count-only` requires `values.length > 0` before it looks, and the rules that iterate + `dimensions[]`/`values[]` are silent on an empty array by construction. + + - **New id `widget-measures-missing`** — a NON-chart declared widget type selects no + measures. Warning tier, suppressible per widget with + `suppressWarnings: ['widget-measures-missing']`, exactly as the chart-family id is. The + message states the consequence its family actually has (the single KPI number is not + drawn / no table is rendered) and the hint names the dataset's declared measures. + - **`chart-measures-missing` is unchanged** — same id, same chart-family population, same + message and same suppression. The condition split rather than widened because "chart" + stops naming it once the population is every family, while the old id is reachable from + the package barrel (a public-surface contract) and may already be written into a board's + `suppressWarnings`. + - `chart-dimensions-missing` stays chart-family only: a dimensionless `metric` or `table` + is what those families are for. + + The two never double-report one widget, in the pin's own order: the measures check runs + before the dimensions one, and `table-count-only` already skips an empty selection. + +### Patch Changes + +- 347b777: `chart-config-missing` no longer fires on a widget whose binding the renderer derives + + The rule warned on every chart-family widget that declared no `chartConfig`, on the + stated grounds that "the renderer cannot determine which measure to plot, so the series + renders empty". Measured against the `@object-ui` revision this repo pins + (`.objectui-sha`), that consequence is false: `DatasetWidget` derives the x-axis key and + one series per measure from the widget's own `dimensions` / `values` via + `buildChartSeries`, and refuses an authored `ChartAxis.field` / `ChartSeries.name` + outright — `chartConfig` carries presentation only. The renderer pins this by name: + "ignores an authored axis `field` and keeps the derived axis binding", "ignores an + authored series and keeps one derived series per measure", "emits none of the + presentation keys when no chartConfig is declared". + + The false finding was landing on this platform's own shipped metadata — the + `system_overview` dashboard's pie and bar tiles, on the Setup board every customer opens + first — which is the ADR-0072 D1 cost the rule family exists to avoid. + + The rule id is unchanged and keeps one true arm: a `combo` widget with no `chartConfig`, + whose per-series mark is authored as `chartConfig.series[].type` and has no other + channel, so every measure draws with the same default mark and the chart is not a + combination at all. Its message now names that consequence instead of the binding. + An existing `suppressWarnings: ['chart-config-missing']` entry stays valid. +- a51eb86: `chart-measure-unknown` no longer blocks a build over a chart `series[].name` (or a page chart's `yAxis[].field`) that names nothing — those positions are presentation, and the message now says so. + + The rule fired at `error` on every measure position of the three chart surfaces it covers, with one consequence sentence: *"result rows are keyed by MEASURE NAME … so this series comes back empty"*. Read at the `@object-ui` revision this repo pins (`.objectui-sha`), that is true only where the position feeds the dataset query, and the three surfaces do not agree: + + - **Report charts** run the chart's own query out of the two axis strings (`useDatasetRows(dataset, [xAxis], [yAxis], …)` — *"the embedded chart queries only `chart.xAxis` × `chart.yAxis`"*), so `chart.xAxis`/`chart.yAxis` are the binding. `chart.series[]` is *"the author's per-chart override for ONE measure's display name"*, lowered through `mergeAuthoredSeries`, where *"an authored entry naming a measure that is NOT in the dataset selection is **ignored** — membership belongs to the dataset"*. + - **List-view charts** have no presentation position at all: `ListChartConfigSchema` is a strict object of `chartType`/`dataset`/`dimensions`/`values`, and `values[]` is handed to the chart as the dataset measures. + - **Dataset-bound page chart components** query `{ dimensions, measures: values }` and then replace the authored series wholesale with one derived entry per selected measure, so `properties.series[].name` reaches the renderer not at all and `properties.yAxis[].field` re-points nothing. + + **Behaviour change users see:** the three presentation positions — report `chart.series[].name`, page-component `properties.series[].name` and `properties.yAxis[].field` — drop from `error` to `warning`. A build or a metadata publish that used to be refused because of one of them now succeeds, with the finding on the advisory channel. The finding is KEPT, not deleted: the metadata really is wrong — the author wrote a key and believes it is in force. Every query position (report `chart.yAxis`, and `values[]` on all three surfaces) keeps `error` and its existing message verbatim. + + Two smaller corrections ride along, both from the same read: + + - The page surface's `yAxis[].field` refs are no longer concatenated into the `series[]` limb before the measure walk, so an axis position no longer takes the series message. Reading both shapes on that surface stays deliberate; giving them one sentence was not. + - `chart-axis-not-selected` (a declared measure outside the selection) took the same one-size consequence, *"the query does not return it, so the series plots nothing"*. It keeps that wording at a query position and states the real one at a presentation position, where no series is derived for the name in the first place. + + Note that none of these three surfaces declares `suppressWarnings` — it is a dashboard-widget key — so the new advisories cannot be individually silenced; the hint says so instead of pointing at a key that does not exist. +- 7dafaae: No authoring rule throws on a non-record entry of any stack collection. + + A collection is authored either as a list or as a name-keyed map, so every rule that reads one coerces `unknown` into an array of records first. That coercion had been hand-copied into 39 modules, and 23 of the copies spelled the array branch as an unchecked cast — every member was asserted to be a record. A YAML list item left empty deserialises to `null`, so a single stray `-` under `flows:`, `pages:`, `dashboards:`, `datasets:`, `apps:`, `permissions:`, `capabilities:`, `data:`, `hooks:`, `views:`, `actions:`, `translations:` (or a per-object `fields:` / `actions:` / `views:`) reached a property read on `null` and threw a stack trace out of `os lint` / `os validate` instead of reporting a finding. The rules are pure `(stack) => Finding[]` running on the raw path, so nothing upstream had judged the entry's shape. + + Twenty-two of those readers now read through the shared, guarded `recordsOf`, which drops a non-record member of the array shape whole and keeps the author's key on the map shape. Nothing else about what the rules judge changes: a valid entry standing beside a junk one is still read, and still draws exactly the findings it drew before. + + The remaining copies are pinned by a new source-text test in the package, so the predicate cannot be pasted back in: it asserts that `recordsOf` is the only collection coercion, that every module still holding a private one is named in a dated ledger that is exact in both directions, and that no coercion outside a dated single-file allowance casts its array branch unchecked. +- 52b59d6: fix(lint): every `stack.objects` reader skips a non-record entry, so no authoring rule throws on the publish door + + A `null` member of `stack.objects` — what an empty YAML list item + deserialises to, and what a partial editor write leaves behind — crashed + 13 of the 42 `AUTHORING_RULES` with + `TypeError: Cannot read properties of null (reading 'name')`. The + authoring rules are pure `(stack) => Finding[]` (ADR-0019) and run on the + RAW `lint` path as well as the parsed one, so nothing upstream had judged + the entry's shape. At the runtime publish gate they are called inside the + gate rather than behind a try/catch of their own, so the throw was an + exception on a WRITE path, not a skipped finding; on the CLI, `os lint` / + `os validate` / `os compile` died on the first one instead of reporting + the stack. + + The repair before this one guarded ONE seam — the object-graph index every + field-path rule opens with. The crash stood at fourteen more readers of + the same collection, each a hand-copied `asArray` whose array branch was + an unchecked `v as AnyRec[]`. Copies are why: the defensive spelling was + already present in about a dozen siblings and absent in the rest, so + fixing one left the others answering the old way. + + So the copies are gone. `recordsOf` — the guarded reader, exported from + `object-graph.ts` and package-private — is now the one coercion from a + collection authored as an array OR as a name-keyed map into the records it + holds, and fifteen files call it: + + - `validate-expressions.ts`, `validate-list-view-mode.ts`, + `validate-widget-bindings.ts`, `filter-walk.ts`, + `validate-object-references.ts`, `validate-record-title.ts`, + `validate-form-layout.ts`, `lint-autonumber-formats.ts`, + `lint-view-refs.ts`, `validate-org-axis-red-lines.ts`, + `validate-sharing-rule-enforceability.ts` — the eleven sites that threw. + - `validate-searchable-fields.ts`'s `indexObjectSearchTargets` and + `validate-page-field-bindings.ts`'s `indexObjectFields` — two shared + indexers inside the reference-integrity suite, each in front of two + rules and both hidden behind whichever suite member threw first. + - `object-field-groups.ts`'s `indexObjectFieldGroups`, which the + re-measure surfaced only once the eleven above stopped throwing. + - `validate-security-posture.ts`, the one that never threw: an `[]` + member passed its `typeof v === 'object'` read and drew a second + `security-owd-unset` at `object "(object 0)"` — an `error` about an + entry no author wrote. + + The verdict is a SKIP, not a finding, matching the seam it extends: a junk + `objects` member is a SHAPE defect and belongs to the schema, every rule + already re-answers the question in its own per-object guard, and reporting + it at the reader would emit one finding per member for one bad entry. On + the name-keyed map shape a member whose VALUE is unreadable keeps its key + (`{ name }`) — the author named it, only its body is illegible. + + No rule tier, id, message or accept-set changes. A valid object standing + beside a junk one is judged exactly as it is judged alone; only a path + index moves, and only for the rules that index `objects` raw, where + `objects[1]` is the honest position. +- ba426b0: A junk entry in `stack.objects` no longer crashes the reference-integrity rules, and a probe rule that throws is reported instead of read as "nothing wrong". + + `indexObjectGraph` is the first statement of every rule that resolves a field path, and it read each `stack.objects` member without checking it was a record — so a `null` entry (an empty YAML list item, a partial editor write) threw `TypeError: Cannot read properties of null (reading 'name')` before any rule's own per-object guard could run. Because these rules also run inside the runtime publish gate, that was an exception on a write path rather than a missed finding. The seam now drops non-record entries — silently, matching every sibling collection reader in the package — and the valid objects beside them are judged exactly as before. + + On the publish receipt, `runBuildProbes`' object plane wrapped its rule call in a catch that produced an empty finding list, so a crashed rule was indistinguishable from a clean object while `checked.objects` had already counted it. A rule that throws now surfaces as a `runtime`-layer `object_field_ref_rule_failed` error carrying the thrown message, so an unverified object never reads as a verified one. Probes still never fail the publish they verify. +- 89758ac: `chart-axis-not-selected` resolves a report chart against its own `chart.yAxis`, not `report.values` (#15734) + + **Behaviour change — one false finding removed on the report surface.** A report chart whose `chart.yAxis` names a declared measure that `report.values` does not select no longer raises a `chart-axis-not-selected` warning. Nothing else about the rule moves, and no other surface moves at all. + + The warning stated a query consequence the renderer refutes. Read at the `@object-ui` revision this repo pins (`.objectui-sha`), `plugin-report/src/DatasetReportRenderer.tsx` does not query `report.values` for the chart at all — it runs the chart's own, narrower query out of the two axis strings: + + ``` + const state = useDatasetRows( + dataset, + plan.kind === 'series' && xAxis ? [xAxis] : [], + wantsQuery && yAxis ? [yAxis] : [], + ``` + + and says so in that file's own words at the `scopeOrder` docblock: *"the embedded chart queries only `chart.xAxis` × `chart.yAxis`"*. So the measure the warning said "the query does not return" is exactly the one the query asks for, and the chart plots it. `report.values` is the selection of the TABLE beneath the chart. + + Both limbs follow from that one measurement: + + - **No not-selected check at the report `chart.yAxis`.** That position IS the chart's query, so it cannot fail to select itself. `chart-measure-unknown` there is untouched: an UNDECLARED measure is still no column at all, and still an `error`. + - **`chart.series[].name` resolves against the singleton `{ chart.yAxis }`.** The entry is a display-name override paired with a DERIVED series, and the chart derives exactly one (`buildChartSeries(…, [xAxis], [yAxis], …)`). An entry naming `chart.yAxis` now lands however the table is selected, and one naming any other declared measure is still reported — including a measure `report.values` does select, which it could not reach before. + + The list-view and page-component surfaces are unchanged, and carry firing controls that say so: on both, `values` IS the measure set the query asks for (`ObjectView` hands it to the chart; `ObjectChart` queries `{ dimensions: schema.dimensions, measures: schema.values }`), so the existing resolution is the right one there. + + The per-position tier and consequence wording is untouched — only the SET the report surface resolves against moves. +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/spec@17.4.0 + - @objectstack/formula@17.4.0 + - @objectstack/sdui-parser@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/lint/package.json b/packages/lint/package.json index 8d3e50e83a..5ebd2c873b 100644 --- a/packages/lint/package.json +++ b/packages/lint/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/lint", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Static, build-time validation for an ObjectStack metadata graph — dashboard widget bindings, CEL/predicate expressions, and more. Pure (stack) => Issue[] functions shared by the CLI's `os validate` and any other consumer (e.g. AI authoring). Depends on @objectstack/spec; never on a runtime.", "type": "module", diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md index 52b443d228..a16668f4d0 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -1,5 +1,61 @@ # @objectstack/plugin-mcp-server +## 17.4.0 + +### Patch Changes + +- 17f8604: The MCP stdio transport now vets an API key's organization against the deployment's tenancy posture, instead of trusting the key's own stored claim. + + `resolveStdioExecutionContext` — the whole of this transport's authorization, since every caller on it is an API key by construction and there is no session path — built its own header map and called `resolveAuthzContext` with no `tenancyPosture`. Both posture-conditional API-key refusals are gated on the caller supplying one (`organization_required` at admission, `organization_membership_ended` after grants), so a door that supplied none ran neither: the key's `sys_api_key.active_organization_id`, never re-checked against current membership, became the request's tenant. Under a wall-enforcing posture a key stamped with an organization its owner had left read and wrote that organization's rows through this door. + + The posture is now derived in the plugin's `start()`, where the kernel is reachable, and threaded into the resolver. What changes for a deployment: + + - Under `isolated` or `group`, a stdio transport configured with a key whose owner is no longer a member of the organization the key names refuses to start, and a key already live is refused on its next call. Under `isolated`, an organization-less key is refused the same way. Both refusals are logged server-side naming the key, principal, organization and reason; nothing about them reaches the caller. + - A kernel that registers no `tenancy` service is unaffected: no organization wall exists there, so no posture-conditional refusal is made. That is the supported composition, not a degraded one. + - A `tenancy` service that is registered and **fails to build** now raises `SERVICE_UNAVAILABLE` (503) rather than reading as "no posture". A posture that could not be read is not a posture that is absent, and admitting on one is the permissive-on-failure shape this repair exists to avoid. + + The posture is re-read per call, on the same schedule as the identity beside it (ADR-0101 D1), so a wall that comes up or a membership that ends mid-session takes effect on the next call rather than at the next restart. +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/types@17.4.0 + - @objectstack/formula@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/mcp/package.json b/packages/mcp/package.json index e0823a503d..cefcb02b24 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/mcp", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "ObjectStack as an MCP server — exposes your app's objects (and AI tools) over the Model Context Protocol (stdio + Streamable HTTP)", "type": "module", diff --git a/packages/metadata-core/CHANGELOG.md b/packages/metadata-core/CHANGELOG.md index 62d731785b..070a32f576 100644 --- a/packages/metadata-core/CHANGELOG.md +++ b/packages/metadata-core/CHANGELOG.md @@ -1,5 +1,40 @@ # @objectstack/metadata-core +## 17.4.0 + +### Patch Changes + +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/spec@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/metadata-core/package.json b/packages/metadata-core/package.json index bb81c19da6..c669e6d0c1 100644 --- a/packages/metadata-core/package.json +++ b/packages/metadata-core/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-core", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Metadata Repository contracts: types, canonicalization, errors, interface (ADR-0008).", "type": "module", diff --git a/packages/metadata-fs/CHANGELOG.md b/packages/metadata-fs/CHANGELOG.md index 5e51f281c0..5e1b66759f 100644 --- a/packages/metadata-fs/CHANGELOG.md +++ b/packages/metadata-fs/CHANGELOG.md @@ -1,5 +1,11 @@ # @objectstack/metadata-fs +## 17.4.0 + +### Patch Changes + +- @objectstack/metadata-core@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/metadata-fs/package.json b/packages/metadata-fs/package.json index 3c28f016cb..bda6ba4bb5 100644 --- a/packages/metadata-fs/package.json +++ b/packages/metadata-fs/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-fs", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "FileSystemRepository: Node-only Repository implementation backed by JSON files and a JSONL change log (ADR-0008).", "type": "module", diff --git a/packages/metadata-protocol/CHANGELOG.md b/packages/metadata-protocol/CHANGELOG.md index 74d3a950ed..dc2588ae3b 100644 --- a/packages/metadata-protocol/CHANGELOG.md +++ b/packages/metadata-protocol/CHANGELOG.md @@ -1,5 +1,170 @@ # @objectstack/metadata-protocol +## 17.4.0 + +### Minor Changes + +- 2ed6be6: Advisory validation rules no longer flood the startup log, and no longer count a row twice on a clean first boot. + + A `severity: 'warning'` (or `'info'`) validation rule is advisory: it never blocks a write, and its message is written for a person filling in a form. Evaluated across a seed load it produced one `WARN` line per row, so a clean-database first boot opened with a wall of form hints re-cast as boot diagnostics — and an app could reach "zero warnings" only by bending its data or deleting the rule. + + Two changes, and neither moves what a rule evaluates to: + + - **Aggregated reporting on the seed/boot path.** `SeedLoaderService.load()` now runs inside an advisory aggregation scope, and reports one summary line per rule — the rule, the object, the row count, the rule's own message and example rows — instead of one line per row. Off that path (an ordinary interactive write) nothing changes: the same per-write line is emitted verbatim. The new scope is `runWithAdvisoryAggregation` / `recordAdvisoryHit` in `@objectstack/core`. + - **Advisory rules are counted by row, not by write.** An `update` whose payload touches only platform-injected system columns — the shape `claimSeedOwnership` writes when it hands seeded rows to the first admin, `{ owner_id }` — changes no business field, so it no longer re-evaluates the object's advisory rules. Previously a seeded row rang once on insert and again when the claim scan rewrote `owner_id`, so anyone counting startup warnings over-estimated by the number of claimed objects. + + `error`-severity rules are untouched by both changes: an invariant is still enforced on every write, whoever issued it and however little it moved. Membership of the "system column" set is resolved per object by `resolveInjectedSystemColumns`, so an object that declares `ownership: 'org'` (no `owner_id`) or `systemFields: false` is judged on its own columns rather than a fixed list. +- 65846bc: 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) + + + + **BREAKING** on the per-row report of `POST /api/v1/data/:object/batch` (and + the multi-object `POST /api/v1/batch`, which rides the same protocol): a row + refused by the engine's `DuplicateRecordError` envelope now reports + `errors[].code: 'UNIQUE_VIOLATION'` where it reported `'DUPLICATE_RECORD'`. + Shipped as `minor` under the repo's launch-window convention for breaking + changes. Maintainer ruling 2026-09-03 on #14723 (verbatim 「同意,然后执行契约 + 复审」), adopting option A: one wire spelling for a unique-constraint refusal on + every route. + + **Why.** `toRowApiError` put a thrown REGISTERED code on the row verbatim, and + `DUPLICATE_RECORD` is registered, so a `DuplicateRecordError` row said + `DUPLICATE_RECORD` while the whole-request failure on the very same route (the + bulk door's classification in `@objectstack/rest`) answered `UNIQUE_VIOLATION` + — the standard-catalog member `content/docs/protocol/kernel/http-protocol.mdx` + documents for the 409 constraint-violation body. Since the bulk doors were + restored to `UNIQUE_VIOLATION`, the two spellings of one condition sat side by + side in one route's responses, which ADR-0112's one-name-per-concept and the + error-code ledger's own header both forbid. The duplication is removed, not + declared: no ledger waiver is added. + + **What changes.** The row derivation recognises the engine's envelope by the + same two-part gate the whole-request arm uses — the registered code AND the + class name `DuplicateRecordError`, never message text — and reports + `UNIQUE_VIOLATION`. Everything else on the row is unchanged: `httpStatus: 409`, + the platform sentence (no driver text, no bound value — the driver's error + stays on `cause` and never reaches the row), and the sibling `NOT_ATTEMPTED` / + `ROLLED_BACK` rows. + + **What does NOT change.** The engine's thrown identity: `DuplicateRecordError.code` + is still `DUPLICATE_RECORD` for an in-process caller of `engine.insert` / + `engine.update` (a hook, a flow node), and the objectql pins on `insert` / + `insertMany` hold. The single-record `/data` door, which has answered + `UNIQUE_VIOLATION` throughout, does not move. A producer that merely THROWS the + registered `DUPLICATE_RECORD` from its own body without being the engine's + class keeps its own code on the row, exactly as it does at the door. + + **Consumer note.** A batch client that branched on a row's `code` reading + `DUPLICATE_RECORD` reads `UNIQUE_VIOLATION` there now — the same value it + already handles for the whole-request 409 on that route and on the + single-record door. Measured in-repo and in the sibling repos (hotcrm, objectui, + non-test sources): zero consumers branch on either spelling of a row code. +- b4b37e5: The object publish door now refuses an object whose `searchableFields` entry, or whose built-in list view's `columns` (and every other field-naming position on that list view), names a field the object does not have. + + `#15254` closed this one key over: it crossed the reference-integrity suite onto the object write door for the object's own field-name **lists** (`highlightFields`, `publicSharing.redactFields`). The two members that read the *other* field surfaces an object carries — its ADR-0061 search set and its built-in `listViews` — still declared `runtimeTypes: ['flow', 'view']`, so on the only door a Studio, REST `/meta` or MCP author has they never judged the snapshot that arrived. An object could publish clean with `searchableFields: ['gone_field']` or a list-view column resolving to nothing, and both fail the same silent way downstream: the engine filters a stale search entry out without a word (`resolveSearchFields`), so `$search` scans a narrower set than declared — or, once every entry is stale, the auto-default set the author never chose — and a dangling column renders one field short. + + - **`validateSearchableFields` and `validateListViewFieldRefs` gain `object`** in their suite-member `runtimeTypes`. No new rule and no new finding class: the rule ids (`searchable-field-unknown`, `searchable-field-unsearchable`, `list-view-field-unknown`, `list-view-field-dotted`) and their severities are unchanged — they now reach the door where the author actually is. + - **The crossing carries the #9313 precondition.** Both members resolve only against `stack.objects`, the one collection every per-write snapshot carries, so neither opens a missing-collection false-positive channel; their `views[]` rungs simply find no `stack.views` on an object snapshot. + - **Measured before crossing**, at the door's own snapshot shape and differential, over every shipped object definition in the monorepo: 116 objects (platform-objects 48, showcase 24, plugins 19, services 12, crm 6, metadata-core 5, todo 1, qa 1), 105 built-in list views on 40 objects, 666 list-view field-naming positions and 5 `searchableFields` entries judged — **0 findings for both members, precision 1.0**, against synthetic probes that are refused. + - **`validateSortableFields`, the third sibling, is deliberately not crossed** — it measured equally clean, but that crossing is its own adjudication. + + ## Migration + + **A publish that used to succeed can now be refused (HTTP 422, `INVALID_METADATA`).** The receipt names the rule id and the offending path, name-keyed on the wire — for example `objects.proj_task.searchableFields[1]` or `objects.proj_task.listViews.all.columns[1]` — plus the string that was written and the fields the object actually has. + + To fix a refusal, do one of: + + - rewrite the entry to the field's current API name (after a Studio label edit the derived name is the one to use — `field_10` becomes `health_score`); or + - drop the entry from the declaration; or, for `searchable-field-unsearchable`, target a text-like stored column instead of a virtual or non-scannable one. + + `os validate` / `os build` / `os lint` already reported these findings at the same severity, so a code-authored stack can be repaired before it reaches a publish. Objects that name a platform-injected system column are unaffected — both members resolve those per object and stay silent where the platform really provisions them. +- 615fac3: A publish now refuses an object whose `highlightFields` names a field that does not exist on it — the same gate that refuses a code-authored stack. + + `list-view-field-unknown` inspects `view.columns`, and Studio's app builder mints no `view` items at all, so the reference-integrity family had nothing to inspect on the only artifacts the click path authors. What it authors is the **object**, and an object-level field-name list was covered by nothing that could refuse: measured on `origin/main`, `runtimeAuthoringRulesFor('object')` dispatched seven rules with no reference-integrity rule among them, while the object-level existence check that did exist (`semantic-role-field-unknown`) is `warning`, advisory-tier and CLI-only. So `os validate` exited 0 on a dangling reference and the runtime publish door — the only door a Studio, REST `/meta` or MCP author has — said nothing at all. + + The reproduction is the natural click order, not a contrived one: click-create a field (Studio mints it as `field_10`), add it to `highlightFields`, then give it a label — the API name auto-derives to `health_score` and `highlightFields` keeps `field_10`. Anyone who names a field after placing it produces this. + + - **New rule `object-field-ref-unknown` (`error`)**, in `@objectstack/lint`, over the object-level field-name **lists** that no rule owned: `highlightFields` (ADR-0085) and `publicSharing.redactFields`. It resolves through the same `object-graph` seam as the rest of the family, so the three shared skips hold — an object outside the stack, an object with no readable field map (ADR-0015 `external`), and a registry-injected system column resolved **per object** (`highlightFields: ['owner_id']` is a live pointer on an owned object and a real miss under `ownership: 'none'`). + - **It runs on the runtime publish door.** The reference-integrity suite entry's `runtimeTypes` gains `object`, and the suite's per-member declaration keeps the crossing narrow: this is the only member that judges an object snapshot; every other member keeps `['flow', 'view']` or the frozen `['flow']` default. + - **`validateSemanticRoles` keeps the provenance question** at the same position (`semantic-role-field-unprovisioned`, still `warning`) and no longer restates existence — one finding per path, at one tier. + - **`probes.checked` gained an `objects` counter.** Its absence was the tell: a receipt reading `{seeds: 0, views: 0, widgets: 0}` was accurate while the objects the package published were probed by nothing. + + ## Migration + + **A publish that used to succeed can now be refused (HTTP 422, `INVALID_METADATA`).** The receipt names the rule id `object-field-ref-unknown` and the offending path, name-keyed on the wire — for example `objects.proj_task.highlightFields[1]` — plus the string that was written and the fields the object actually has. + + To fix a dangling reference, do one of: + + - rewrite the entry to the field's current API name (after a Studio label edit the derived name is the one to use — `field_10` becomes `health_score`); or + - drop the entry from the list. + + `os validate` / `os build` / `os lint` report the same finding at `error`, so a stack can be repaired before it reaches a publish. If an object legitimately points at a platform-injected system column, no change is needed — the rule resolves those per object and stays silent where the platform really provisions them. + +### Patch Changes + +- ba426b0: A junk entry in `stack.objects` no longer crashes the reference-integrity rules, and a probe rule that throws is reported instead of read as "nothing wrong". + + `indexObjectGraph` is the first statement of every rule that resolves a field path, and it read each `stack.objects` member without checking it was a record — so a `null` entry (an empty YAML list item, a partial editor write) threw `TypeError: Cannot read properties of null (reading 'name')` before any rule's own per-object guard could run. Because these rules also run inside the runtime publish gate, that was an exception on a write path rather than a missed finding. The seam now drops non-record entries — silently, matching every sibling collection reader in the package — and the valid objects beside them are judged exactly as before. + + On the publish receipt, `runBuildProbes`' object plane wrapped its rule call in a catch that produced an empty finding list, so a crashed rule was indistinguishable from a clean object while `checked.objects` had already counted it. A rule that throws now surfaces as a `runtime`-layer `object_field_ref_rule_failed` error carrying the thrown message, so an unverified object never reads as a verified one. Probes still never fail the publish they verify. +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [954cb0b] +- Updated dependencies [a56baa2] +- Updated dependencies [3e3ecb0] +- Updated dependencies [347b777] +- Updated dependencies [36a16d0] +- Updated dependencies [c01b3a6] +- Updated dependencies [a51eb86] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [7dafaae] +- Updated dependencies [52b59d6] +- Updated dependencies [f502898] +- Updated dependencies [3bd9b34] +- Updated dependencies [b4b37e5] +- Updated dependencies [ba426b0] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [89758ac] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [615fac3] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] +- Updated dependencies [cd55558] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/lint@17.4.0 + - @objectstack/metadata@17.4.0 + - @objectstack/types@17.4.0 + - @objectstack/formula@17.4.0 + - @objectstack/metadata-core@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/metadata-protocol/package.json b/packages/metadata-protocol/package.json index cd18ee5fa9..1839dc65c7 100644 --- a/packages/metadata-protocol/package.json +++ b/packages/metadata-protocol/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata-protocol", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "ObjectStack metadata management protocol: sys_metadata CRUD, draft/publish, locks, package ownership, diagnostics (ADR-0076).", "type": "module", diff --git a/packages/metadata/CHANGELOG.md b/packages/metadata/CHANGELOG.md index 61eb06aedd..e67f390196 100644 --- a/packages/metadata/CHANGELOG.md +++ b/packages/metadata/CHANGELOG.md @@ -1,5 +1,167 @@ # @objectstack/metadata +## 17.4.0 + +### Minor Changes + +- a56baa2: feat(metadata,objectql): a keyed plural read on `MetadataManager`, `listNames` fault parity, and an action audit that answers from the same identity and sources as the router + + Two plural reads of one metadata plane could disagree with a by-name read of + that same plane, and the ADR-0110 D5 action-governance audit stood on the + disagreement — reporting `registered handler with NO declaration … REFUSED at + dispatch` about a route the router was resolving and dispatching in the same + boot. + + **`MetadataManager.listNames` gains the per-loader `try`/`catch` that + `loadMany` and `list()` have carried since #5108.** One loader fault used to + produce two different facts depending only on which plural read a caller + reached for: `loadMany` swallowed it and answered short, `listNames` threw. It + now degrades the same way, through the same `reportLoaderReadFailure` / + `reportLoaderReadRecovered` helpers — one outage, one line, one vocabulary. + Callers that relied on `listNames` throwing to detect an outage should read + `listDiagnosed()`, which reports `degraded` explicitly. + + **New: `MetadataManager.loadManyKeyed(type, options?)`** — `loadMany` read under + the identity the STORE holds each item by, returning `{ name, data }` pairs. It + delegates to a loader's own `loadManyKeyed` where one is offered (on + `DatabaseLoader` that shares `loadMany`'s single query, so it costs nothing + extra) and otherwise falls back to that loader's `list()` + per-name `load()`. + ⛔ **`loadMany`'s published return shape does not change**, and no existing + consumer is touched: the key travels *beside* the body, never inside it, so a + body that deliberately carries no `name` stays byte-identical to what was + stored (#14205). + + **The action-governance audit now mirrors the router on both halves of the D5 + bijection.** The declaration half enumerates the plane keyed + (`loadStandaloneActionsKeyed`), so a row whose body does not name itself — a + `sys_metadata` row keyed by its `name` column, or a `FilesystemLoader` file + whose identity is its path — is a declaration to the audit exactly as it is to + the router; the handler half also probes the plane BY NAME + (`lookupMetadataAction`, `loadDiagnosed`/`load`, injected like the existing + registry rung), so a loader fault a plural read swallows can no longer turn a + dispatchable handler into an accusation. Both probes stay conservative in one + direction only: a source that throws leaves the handler on the list. + + Additive on every published signature. `runActionGovernanceInventory` and + `collectEngineActionDeclarations` gain optional parameters and keep their old + ones working unchanged; declaration rows gain an optional `storeKey` (the new + exported `ActionDeclarationRow`). + + **Population change, reported:** `unboundDeclarations` now sees declarations + whose identity is the store key. Its BEFORE was **0, structurally rather than + by sampling** — a nameless row was dropped before reconciliation ran, so it + could never be reported however many a plane held. Its one deliberate + subtraction: a row with neither an own `name` nor a store key is no longer + reported as `actionName: undefined`, which read as a parse failure in the + warning rather than as a finding. + + Known boundary, stated in the audit's docblock rather than left to be + rediscovered: a boot-time audit runs outside any request scope, so if a + composition ever registered `metadata` as `SCOPED` the audit could not reach + that instance at all — before any read method runs. No shipped composition does + (`packages/metadata/src/plugin.ts` registers a static instance), and reaching a + request-scoped service from a boot-time audit is a separate change. +- 3bd9b34: feat(metadata): `deriveViewContainerObject` gets a leaf `/view-container` subpath, so objectql's lean ADR-0076 entry stops loading the manager, chokidar, glob and js-yaml for a six-line pure function + + `packages/objectql/src/engine.ts` reached `deriveViewContainerObject` through + `@objectstack/metadata`'s ROOT entry. `core.ts` — the ADR-0076 lean entry — + re-exports `engine.ts`, so `@objectstack/objectql/core`'s module-init closure + inherited the whole root entry: `MetadataPlugin` -> `NodeMetadataManager` -> + `chokidar`, plus `glob`, `js-yaml` and `readdirp`. + + The same file already carried the answer 79 lines above, at its + `@objectstack/metadata/errors` import: that leaf subpath exists "precisely so a + cross-package consumer gets the predicate without the manager, the loaders or + the YAML/filesystem machinery behind the root entry". This is that pattern, + taken a second time. + + **Measured on the built artifacts, not asserted** — every module Node actually + evaluates when `@objectstack/objectql/core` is loaded in a fresh process, + recorded through a `module.registerHooks` load hook (ESM and CJS) plus + `require.cache`, byte sizes from `statSync`: + + | `@objectstack/objectql/core` | modules | bytes | + |:---|---:|---:| + | before (ESM `dist/core.mjs`) | 190 | 12,348,424 | + | after (ESM `dist/core.mjs`) | 185 | 11,849,808 | + | **delta** | **-5** | **-498,616 (-486.9 KiB)** | + | before (CJS `dist/core.js`) | 188 | 12,654,238 | + | after (CJS `dist/core.js`) | 183 | 12,141,034 | + | **delta** | **-5** | **-513,204 (-501.2 KiB)** | + + Six modules stop loading — `packages/metadata/dist/index.js` (237,747 B), + `js-yaml` (114,610 B), `glob` (82,749 B), `chokidar` (2 files, 54,220 B) and + `readdirp` (9,836 B) — and one 469-byte module takes their place. Marginal + module-init time for that root entry, measured on a warm lean closure, was + ~22 ms (median of 7; 20.4-27.5 ms) out of ~630 ms. + + ⚠️ The figure the finding was argued on — "~3.6 KB to ~450 KB" — is right about + the delta and wrong about the baseline: the lean entry's closure was already + ~11.5 MiB before this import existed, dominated by `@objectstack/spec` + (9,587,914 B) and `zod` (567,918 B), neither of which the metadata root entry + contributes. What the root import cost was ~487 KiB *on top of* that, not a + closure of 450 KB. + + The derivation itself moves to `packages/metadata/src/view-container.ts`, a + module with **no imports at all**, and `view-container-expansion.ts` imports + and re-exports it, so `index.ts`'s root export and `plugin.ts` keep their + spelling and the symbol stays on the root entry — this subpath is an additional + door, not a relocation. A re-export shim onto `view-container-expansion.ts` was + tried first and rejected on measurement: esbuild tree-shakes the unused + `expandRuntimeViewContainer` but keeps its two `@objectstack/spec` import + statements, so that shim's own closure was 84 modules / 3,035 KiB. The real + leaf's is 1 module / 469 B. + + `expandRuntimeViewContainer` is deliberately not exported from the new subpath: + `metadata-manager.ts` is its only caller, the root entry does not export it + either, and it is the half that carries the spec machinery. + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [2bb0614] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/platform-objects@17.4.0 + - @objectstack/types@17.4.0 + - @objectstack/metadata-core@17.4.0 + - @objectstack/metadata-fs@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/metadata/package.json b/packages/metadata/package.json index 7bdfc79e7a..2fbbe01d0e 100644 --- a/packages/metadata/package.json +++ b/packages/metadata/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/metadata", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Metadata loading, saving, and persistence for ObjectStack", "type": "module", diff --git a/packages/objectql/CHANGELOG.md b/packages/objectql/CHANGELOG.md index 96d90aa757..9f4d56a36e 100644 --- a/packages/objectql/CHANGELOG.md +++ b/packages/objectql/CHANGELOG.md @@ -1,5 +1,409 @@ # @objectstack/objectql +## 17.4.0 + +### Minor Changes + +- 2ed6be6: Advisory validation rules no longer flood the startup log, and no longer count a row twice on a clean first boot. + + A `severity: 'warning'` (or `'info'`) validation rule is advisory: it never blocks a write, and its message is written for a person filling in a form. Evaluated across a seed load it produced one `WARN` line per row, so a clean-database first boot opened with a wall of form hints re-cast as boot diagnostics — and an app could reach "zero warnings" only by bending its data or deleting the rule. + + Two changes, and neither moves what a rule evaluates to: + + - **Aggregated reporting on the seed/boot path.** `SeedLoaderService.load()` now runs inside an advisory aggregation scope, and reports one summary line per rule — the rule, the object, the row count, the rule's own message and example rows — instead of one line per row. Off that path (an ordinary interactive write) nothing changes: the same per-write line is emitted verbatim. The new scope is `runWithAdvisoryAggregation` / `recordAdvisoryHit` in `@objectstack/core`. + - **Advisory rules are counted by row, not by write.** An `update` whose payload touches only platform-injected system columns — the shape `claimSeedOwnership` writes when it hands seeded rows to the first admin, `{ owner_id }` — changes no business field, so it no longer re-evaluates the object's advisory rules. Previously a seeded row rang once on insert and again when the claim scan rewrote `owner_id`, so anyone counting startup warnings over-estimated by the number of claimed objects. + + `error`-severity rules are untouched by both changes: an invariant is still enforced on every write, whoever issued it and however little it moved. Membership of the "system column" set is resolved per object by `resolveInjectedSystemColumns`, so an object that declares `ownership: 'org'` (no `owner_id`) or `systemFields: false` is judged on its own columns rather than a fixed list. +- a56baa2: feat(metadata,objectql): a keyed plural read on `MetadataManager`, `listNames` fault parity, and an action audit that answers from the same identity and sources as the router + + Two plural reads of one metadata plane could disagree with a by-name read of + that same plane, and the ADR-0110 D5 action-governance audit stood on the + disagreement — reporting `registered handler with NO declaration … REFUSED at + dispatch` about a route the router was resolving and dispatching in the same + boot. + + **`MetadataManager.listNames` gains the per-loader `try`/`catch` that + `loadMany` and `list()` have carried since #5108.** One loader fault used to + produce two different facts depending only on which plural read a caller + reached for: `loadMany` swallowed it and answered short, `listNames` threw. It + now degrades the same way, through the same `reportLoaderReadFailure` / + `reportLoaderReadRecovered` helpers — one outage, one line, one vocabulary. + Callers that relied on `listNames` throwing to detect an outage should read + `listDiagnosed()`, which reports `degraded` explicitly. + + **New: `MetadataManager.loadManyKeyed(type, options?)`** — `loadMany` read under + the identity the STORE holds each item by, returning `{ name, data }` pairs. It + delegates to a loader's own `loadManyKeyed` where one is offered (on + `DatabaseLoader` that shares `loadMany`'s single query, so it costs nothing + extra) and otherwise falls back to that loader's `list()` + per-name `load()`. + ⛔ **`loadMany`'s published return shape does not change**, and no existing + consumer is touched: the key travels *beside* the body, never inside it, so a + body that deliberately carries no `name` stays byte-identical to what was + stored (#14205). + + **The action-governance audit now mirrors the router on both halves of the D5 + bijection.** The declaration half enumerates the plane keyed + (`loadStandaloneActionsKeyed`), so a row whose body does not name itself — a + `sys_metadata` row keyed by its `name` column, or a `FilesystemLoader` file + whose identity is its path — is a declaration to the audit exactly as it is to + the router; the handler half also probes the plane BY NAME + (`lookupMetadataAction`, `loadDiagnosed`/`load`, injected like the existing + registry rung), so a loader fault a plural read swallows can no longer turn a + dispatchable handler into an accusation. Both probes stay conservative in one + direction only: a source that throws leaves the handler on the list. + + Additive on every published signature. `runActionGovernanceInventory` and + `collectEngineActionDeclarations` gain optional parameters and keep their old + ones working unchanged; declaration rows gain an optional `storeKey` (the new + exported `ActionDeclarationRow`). + + **Population change, reported:** `unboundDeclarations` now sees declarations + whose identity is the store key. Its BEFORE was **0, structurally rather than + by sampling** — a nameless row was dropped before reconciliation ran, so it + could never be reported however many a plane held. Its one deliberate + subtraction: a row with neither an own `name` nor a store key is no longer + reported as `actionName: undefined`, which read as a parse failure in the + warning rather than as a finding. + + Known boundary, stated in the audit's docblock rather than left to be + rediscovered: a boot-time audit runs outside any request scope, so if a + composition ever registered `metadata` as `SCOPED` the audit could not reach + that instance at all — before any read method runs. No shipped composition does + (`packages/metadata/src/plugin.ts` registers a static instance), and reaching a + request-scoped service from a boot-time audit is a separate change. +- fa125f3: feat(objectql,spec): `Field.valueDomain` binds at the write seam — a non-member is refused with `value_domain` (maintainer ruling 2026-09-02 on #14168, engine half) + + **BREAKING** accept-set narrowing on the ObjectQL record write path, shipped as + `minor` under the repo's launch-window convention for breaking changes. + + The key is **already published, and published unenforced**. The version-packages + cut `8a1bad8b8` (2026-09-04 10:20Z) consumed the spec half's changeset + `field-value-domain-slot.md` and released `@objectstack/spec@17.3.0`, which + declares `Field.valueDomain`, parses it, and refuses it on any type other than + `text` — and never reads it when a record is written. The 17.3.0 liveness ledger + states the gap in its own words: "a non-member WRITTEN to a `text` field + declaring a domain is accepted today". That write is accepted on 17.3.0 and is + refused from this release on. + + **Refused shape**, precisely: a record write that supplies a value for a `text` + field whose definition declares `valueDomain`, where the WRITTEN value is not a + member of the named standard. It fails with the field error code `value_domain`, + carrying `constraint: { valueDomain }` and a message that names the standard in + all four platform locales. Nothing else narrows — a field that declares no + `valueDomain` is untouched, and so is every other field type, because the schema + accepts the key on `text` alone and the validator judges exactly that set. + + **Remedy: write a member of the declared standard.** `iana_time_zone` admits + `UTC` and refuses `Mars/Olympus`; `iso_4217_currency` admits `CHF` and refuses + `chf`; `iso_3166_alpha2` admits `CH` and refuses `ZZ`. Dropping the + `valueDomain` declaration from the field lifts the refusal entirely, for an + author who declared a domain they did not mean. + + **No stored row is touched, and none becomes invalid.** This is the `min` / + `max` / `maxLength` transition-gate class: a value stored before the domain was + declared — or before this release — is never re-read, and it survives an edit of + another field on the same record. An absent or empty value follows the field's + `required` handling, not this check. + + + + - The membership test is the spec's shared `isValueDomainMember` — the same + predicate, over the same closed vocabulary, that a settings specifier's + `valueDomain` uses. A time zone accepted in Settings is the time zone + accepted in a field. + - The two authoring forms (`fieldForm`, `objectForm`) gain a `valueDomain` + control, shown on exactly the types the schema accepts the key on. The + object-form control's choices are derived from the vocabulary, not re-typed. +- d0ee598: fix(objectql): the boot loop refuses a view container whose `name` disagrees with the object it binds to, instead of silently rewriting the author's field (#14666) + + **BREAKING** accept-set narrowing on the ObjectQL boot loop's SOURCE registrar + (`registerMetadataCollections`), shipped as `minor` under the repo's + launch-window convention for breaking changes. Ruled on #14666 (2026-09-03, + direction 2). + + An aggregated `defineView` container is keyed by the OBJECT it binds to, not + by its own row identity, and `ViewSchema` declares an optional `name` whose + own description says that for an object-scoped container it *is* the object + name. Nothing enforced that. A container written as + `{ name: 'lead_views', object: 'crm_lead', list: { ... } }` therefore reached + the two SOURCE registrars and got opposite answers: this boot loop overwrote + `name` with the derived key `crm_lead` and registered it, discarding the + author's field with no diagnostic, while the artifact/HMR loader + (`MetadataPlugin._parseAndRegisterArtifact`) refused the whole artifact load + through `assertMetadataRegisterContract` (#7378 row 1, `VALIDATION_ERROR` / + 400). Same document, and whether it loaded at all depended on how the package + was loaded. + + The boot loop now **refuses loudly**, with the same `VALIDATION_ERROR` / 400 + envelope the artifact door raises, naming the container's own `name`, the + object key it derived, and both remedies: drop `name`, or set it to that + derived key. #7378 row 1 already ruled that resolving such a disagreement + silently, in either direction, files the item under a key the caller never + wrote, so the two registrars converge on the refusal rather than on the + rewrite; the artifact door is unchanged. + + **Refused shape**, precisely: an aggregated view container in a stack `views:` + collection that carries a non-empty top-level `name` AND derives a different + object key from its own `object` (or, failing that, `list.data.object` / + `form.data.object`). + + Scope, which the ruling names as this change's main risk. A container with no + `name` is untouched, and still registers under its derived key. So is a + container whose `name` already equals that key, and one that declares no + binding anywhere else, since the derivation then falls back to that same + `name` and cannot disagree with itself. No other metadata kind changes + behaviour: the refusal is gated inside the `views` branch of the generic + registration loop. Standalone ViewItems and flattened overlays travelling in + the assembled `viewItems:` channel are untouched, because a container cannot + reach that channel at all. Every one of these has a control test. + + +- ec0a6e7: feat(objectql,cli): `backfillSummaryNulls` accepts `recomputeUndefinedOnEmpty` — a caller who KNOWS a `min`/`max`/`avg` roll-up column was just declared can have it filled; `os migrate summary-nulls --recompute-undefined-on-empty object.field` surfaces it (#15064) + + A roll-up value has three producers — the insert-time seed, the child-write + recompute, and the one-off backfill — and **declaring a summary field on an + object that already has rows reaches none of them**. For `count`/`sum` the + backfill repairs that as a side effect (every `NULL` is a hole to it). For + `min`/`max`/`avg` it could not: `summaryNullIsBackfillable` decides on the + function alone, so "never computed" and "no child rows" were indistinguishable, + the column stayed `NULL` on every pre-existing parent, and the report said + `filled: 0` — a false all-clear that a timed flow built on the column then + turned into "matches nothing" (the customer case behind cloud#1908). + + **What changes** — maintainer ruling on #15064, option A: the caller who holds + the fact gets a way to say it; the predicate and the default run do not move. + + - `SummaryBackfillOptions.recomputeUndefinedOnEmpty?: string[]` — `object.field` + roll-ups the caller knows were never computed. A named `min`/`max`/`avg` is + walked like a `count`: every `NULL` parent is recomputed through the same + `aggregateSummaryValue` the engine writes. A parent whose aggregate is the + empty-set reading (`null` — no child rows) already holds the engine's own + value, so it is neither counted as a hole nor written; the scoped run is + therefore idempotent in the same "re-run until it reports zero" sense. + Naming a `count`/`sum` is accepted and changes nothing, so a publish path can + pass every column it just declared without knowing the empty-set list. + - A name that resolves to no roll-up owned by an object the run walks — a typo, + a plain field, or an object `objects` left out — is **refused before any row + is read**, dry run or apply, with an ADR-0112 envelope (`code: + 'INVALID_FIELD'`, `status: 400` — the code the projection and write axes + that name a field already answer, while sorting keeps `INVALID_SORT`; + `field` names the first unresolved entry, `fields` all of them). A silent + no-op there would be the same false all-clear this option exists to end. + - `SummaryBackfillReport.recomputedUndefinedOnEmpty: string[]` — the complement + of `skippedUndefinedOnEmpty`, same `object.field (fn)` spelling; `[]` on an + unscoped run. `SummaryBackfillFieldOutcome.fn` widens from `'count' | 'sum'` + to every roll-up function, since a named `max` now appears in `fields`. + - `os migrate summary-nulls --recompute-undefined-on-empty object.field` + (repeatable) passes the scope through; the confirmation prompt names the + columns; `formatSummaryBackfillReport` lists them under "Recomputed on + request" and explains a `NULL` that remains. + + **What does not change:** without the option the walk, the writes, every + counter and the human-readable report are byte-for-byte what they were (pinned + against output captured on `main` before this change); `min`/`max`/`avg` stay + out of scope and keep being reported under `skippedUndefinedOnEmpty`; the + predicate `summaryNullIsBackfillable` is untouched, so `os migrate + summary-nulls` keeps its meaning on every deployment. The only visible delta on + an unscoped run is the one additive report key, `recomputedUndefinedOnEmpty: []`. + + `minor` for both packages: an optional parameter on a published exported + function, a new report key, and a new CLI flag are each a purely additive + widening of a published surface, which takes at least `minor` (bump-level rule, + 2026-09-04); the `fix`-shaped motivation does not lower it. + +### Patch Changes + +- 4b3955e: fix(objectql): a published `BulkDataEvent` now names the ONE organization the tenant wall named for the batch + + `BulkDataEventSchema.organizationId` (`@objectstack/spec/api`, declared by the + contract half) is one organization for a whole predicate write, or absent. The + only bulk producer — `publishBulkDataEvent`, behind the `multi: true` branches + of `update()` / `delete()` — never set it, so every `data.records.updated` / + `data.records.deleted` event read "not asserted" and a tenant-scoped consumer + could deliver nothing per organization on the bulk path. This is the bulk half + of the cross-tenant webhook fan-out leak; the single-record half (`DataEvent`) + landed separately. + + The producer now stamps the key from what it already holds — no second query + on the publish path: under `isolated` the caller's active organization (the + Layer 0 wall's equality term), under `group` the caller's membership set when + it names exactly one organization. It is OMITTED — never the caller's active + organization standing in — on a `single`-posture deployment, on an `isSystem` + context (no wall composed), on a multi-membership `group` sweep, when no + enforcement layer injected a posture (the `OS_TENANCY_POSTURE` env fallback is + deliberately not consulted), when the caller may have crossed the wall as a + `PLATFORM_ADMIN` or carries no resolved posture rung, and on an object the wall + does not key on. `absent` here means "the producer did not assert one + organization for the batch", deliberately NOT the `DataEvent` reading + "belongs to no organization". + + Which objects "the wall does not key on", stated exactly rather than claimed as + a mirror: plugin-security's Layer 0 composes no wall when its `tenancyDisabled` + input is true or the object carries no `organization_id`, and it folds THREE + clauses into `tenancyDisabled` — `tenancy.enabled === false`, + `systemFields.tenant === false`, and the deployment's `platformGlobalObjects` + carve-out. The producer reads the registry's binding of that predicate + (`carriesTenantScopeColumn`: the first two clauses plus the column clause) and + answers absent on a federated (`external`) object; a custom + `tenancy.tenantField` is therefore not an exit by itself — the object is walled + iff it carries `organization_id`, and the key follows the wall. The third + clause is deployment-declared and not readable by the engine: a + deployment-exempted object under an armed wall is still stamped with the + caller's organization by this producer alone, and that population's exact + answer is decided by the seam ruled on in #15706. + + `patch`, not `minor`: the act adds no member to this package's published + surface. `carriesTenantScopeColumn` is exported at module level inside + `registry.ts` only — `@objectstack/objectql`'s entries (`.`, `./core`) re-export + named members and never `export *`, so `dist/index.d.ts`, `dist/core.d.ts` and + both entries' runtime export lists are unchanged (measured on the built `dist`, + with a firing control) — and the emitted event's member was declared, typed + and paid for at `minor` by the spec half. Producer conformance to an existing + optional member under `fix(` changes no public surface of this package. +- 65846bc: fix(objectql): `DuplicateRecordError.developerMessage` names the wire spelling a client branches on (#14723) + + The envelope's `developerMessage` — the remedy sentence addressed to the + application author — told its reader to "branch on `code === 'DUPLICATE_RECORD'`", + which is the engine's THROWN identity and holds only for an in-process caller of + `engine.insert` / `engine.update`. Every REST route reports the same refusal as + `UNIQUE_VIOLATION`, and since #14723 the per-row reports of the batch and import + surfaces do too, so the sentence was a platform contradicting itself on the one + line an author is most likely to copy. It now says both halves: over the HTTP + API branch on `code === 'UNIQUE_VIOLATION'` on every route, whole-request and + per-row alike; inside the engine the thrown class carries `DUPLICATE_RECORD`. + The class's own docblock says the same. Nothing else about the envelope moves: + `code`, `status`, `cause`, `field`, `object` and the user-facing `message` are + byte-identical, and every pin on the engine's thrown code holds. +- 3bd9b34: feat(metadata): `deriveViewContainerObject` gets a leaf `/view-container` subpath, so objectql's lean ADR-0076 entry stops loading the manager, chokidar, glob and js-yaml for a six-line pure function + + `packages/objectql/src/engine.ts` reached `deriveViewContainerObject` through + `@objectstack/metadata`'s ROOT entry. `core.ts` — the ADR-0076 lean entry — + re-exports `engine.ts`, so `@objectstack/objectql/core`'s module-init closure + inherited the whole root entry: `MetadataPlugin` -> `NodeMetadataManager` -> + `chokidar`, plus `glob`, `js-yaml` and `readdirp`. + + The same file already carried the answer 79 lines above, at its + `@objectstack/metadata/errors` import: that leaf subpath exists "precisely so a + cross-package consumer gets the predicate without the manager, the loaders or + the YAML/filesystem machinery behind the root entry". This is that pattern, + taken a second time. + + **Measured on the built artifacts, not asserted** — every module Node actually + evaluates when `@objectstack/objectql/core` is loaded in a fresh process, + recorded through a `module.registerHooks` load hook (ESM and CJS) plus + `require.cache`, byte sizes from `statSync`: + + | `@objectstack/objectql/core` | modules | bytes | + |:---|---:|---:| + | before (ESM `dist/core.mjs`) | 190 | 12,348,424 | + | after (ESM `dist/core.mjs`) | 185 | 11,849,808 | + | **delta** | **-5** | **-498,616 (-486.9 KiB)** | + | before (CJS `dist/core.js`) | 188 | 12,654,238 | + | after (CJS `dist/core.js`) | 183 | 12,141,034 | + | **delta** | **-5** | **-513,204 (-501.2 KiB)** | + + Six modules stop loading — `packages/metadata/dist/index.js` (237,747 B), + `js-yaml` (114,610 B), `glob` (82,749 B), `chokidar` (2 files, 54,220 B) and + `readdirp` (9,836 B) — and one 469-byte module takes their place. Marginal + module-init time for that root entry, measured on a warm lean closure, was + ~22 ms (median of 7; 20.4-27.5 ms) out of ~630 ms. + + ⚠️ The figure the finding was argued on — "~3.6 KB to ~450 KB" — is right about + the delta and wrong about the baseline: the lean entry's closure was already + ~11.5 MiB before this import existed, dominated by `@objectstack/spec` + (9,587,914 B) and `zod` (567,918 B), neither of which the metadata root entry + contributes. What the root import cost was ~487 KiB *on top of* that, not a + closure of 450 KB. + + The derivation itself moves to `packages/metadata/src/view-container.ts`, a + module with **no imports at all**, and `view-container-expansion.ts` imports + and re-exports it, so `index.ts`'s root export and `plugin.ts` keep their + spelling and the symbol stays on the root entry — this subpath is an additional + door, not a relocation. A re-export shim onto `view-container-expansion.ts` was + tried first and rejected on measurement: esbuild tree-shakes the unused + `expandRuntimeViewContainer` but keeps its two `@objectstack/spec` import + statements, so that shim's own closure was 84 modules / 3,035 KiB. The real + leaf's is 1 module / 469 B. + + `expandRuntimeViewContainer` is deliberately not exported from the new subpath: + `metadata-manager.ts` is its only caller, the root entry does not export it + either, and it is the half that carries the spec machinery. +- 26144c2: The platform-object tenancy census is derived and gated instead of hand-written in a comment. Documentation only — no runtime behaviour changes. + + `PLATFORM_OBJECT_TENANCY`'s header explained why the reclassification needs a ledger rather than a schema read, and backed the argument with three hand-written digits and a parenthetical attributing them. Nothing re-derived any of it, so it was true only until the population moved and failed silently when it did — in both of the directions a prose count can. + + The parenthetical mis-attributed the exclusion: it named `sys_sso_provider`'s `tenancy.enabled: false` as an addition to the `managedBy: 'better-auth'` set that object was already in, and left `sys_api_key`'s identical opt-out unnamed. The arithmetic stayed right, which is why no reader and no gate caught it — a wrong reason producing a right total is the shape that survives longest. The digits then went stale when an object opted out of the tenant column through a third mechanism the parenthetical's taxonomy had no slot for (`systemFields: { tenant: false }`), while the gated page next door was updated in the same commit. + + The digits and the parenthetical are deleted rather than corrected. The header now points at `scripts/platform-object-tenancy-census.json` and states the PREDICATE it was missing: an object is inside the machinery when `resolveTenantFieldName` answers non-null on the **registered** schema — after `applySystemFields` has injected the tenant column, because the injected column is what the engine sees, not what the author typed. Counting `managedBy` as if the resolver read it is the mistake that produced the wrong reason. + + The artefact is derived by `scripts/platform-object-tenancy-census.mjs`, which loads `resolveTenantFieldName` and `resolveInjectedSystemColumns` from source and executes them rather than re-spelling what they decide, and is held to the tree by `scripts/check-platform-object-tenancy-census.mjs`. It records per object the declaration on that object's own schema that puts it outside the reach; declarations are not mutually exclusive and an object carrying two keeps both. An excluded object with no declared mechanism is an error, not a default: the generator refuses to commit the row and the gate reds, so a new exclusion mechanism is adjudicated rather than absorbed into an existing total. +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [a56baa2] +- Updated dependencies [65846bc] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [3bd9b34] +- Updated dependencies [b4b37e5] +- Updated dependencies [ba426b0] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [615fac3] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/metadata-protocol@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/metadata@17.4.0 + - @objectstack/types@17.4.0 + - @objectstack/formula@17.4.0 + - @objectstack/metadata-core@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/objectql/package.json b/packages/objectql/package.json index c3d6b44252..d80cba45bf 100644 --- a/packages/objectql/package.json +++ b/packages/objectql/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/objectql", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Isomorphic ObjectQL Engine for ObjectStack", "main": "dist/index.js", diff --git a/packages/observability/CHANGELOG.md b/packages/observability/CHANGELOG.md index abc6646646..6ae89e383a 100644 --- a/packages/observability/CHANGELOG.md +++ b/packages/observability/CHANGELOG.md @@ -1,5 +1,40 @@ # @objectstack/observability +## 17.4.0 + +### Patch Changes + +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/spec@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/observability/package.json b/packages/observability/package.json index 9a7b7963c7..e9dde2649c 100644 --- a/packages/observability/package.json +++ b/packages/observability/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/observability", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Observability contracts and exporters for ObjectStack — MetricsRegistry, ErrorReporter, Logger plus noop/console/OTLP-HTTP exporters. Deployment-target neutral; runtime and services depend on this so the same instrumentation works on Cloudflare Workers, Node, and self-hosted Kubernetes.", "type": "module", diff --git a/packages/platform-objects/CHANGELOG.md b/packages/platform-objects/CHANGELOG.md index 5c6a352b30..49d81959c2 100644 --- a/packages/platform-objects/CHANGELOG.md +++ b/packages/platform-objects/CHANGELOG.md @@ -1,5 +1,94 @@ # @objectstack/platform-objects +## 17.4.0 + +### Patch Changes + +- 85a2459: fix(spec): the dashboard `gap` field no longer describes itself to app authors in Tailwind vocabulary + + `ui/dashboard`'s `gap` key told app authors its value in the vocabulary of a CSS + library they never chose and cannot act on. **Two** independent producer strings + carried that wording, and they feed two independent customer-facing surfaces: + + - `dashboardForm`'s `helpText` — `Grid gap (Tailwind units)` — rendered verbatim in + the Studio property panel, which is spec-driven and feeds this form straight into + the generic form renderer. + - `DashboardSchema.gap`'s `.describe()` — `Grid gap in Tailwind spacing units` — + rendered as this field's row in the published reference page + `content/docs/references/ui/dashboard.mdx`. The reference corpus renders + `.describe()`, never `helpText`. + + Both now read **Space between widgets, in steps of 0.25rem (4 = 1rem)**: what the + author decides, plus the magnitude, stated in a CSS unit instead of a framework's + scale. The magnitude had to survive the rewrite rather than be dropped with the + framework name — the number is a spacing step, so `4` means `1rem` and not `4px`, + and an author who lost that would come away knowing less than before. + + The step size is stated as measured rather than inferred: the dashboard renderer + sets the grid gap as an inline style computed from this key, so every accepted + value is linear and one step is exactly `0.25rem`. "Tailwind units" was doubly + wrong — it named an implementation dependency, and it named one the consumer of + this key does not have. + + **No schema change.** `gap` stays `z.number().int().min(0).optional()` and accepts + exactly what it accepted before; nothing is added to or removed from any public + surface. `columns` is deliberately untouched on both of its producer lines — + `12` is an author-visible fact about the grid being laid out, not a framework + detail — and this is one field's two strings, not a sweep for framework words. + + The `en` metadata-forms translation bundle is a mechanical copy of the form source, + so it is regenerated to match. Translated locales are not touched: regeneration + fills gaps only and never overwrites an existing leaf. +- 2bb0614: fix(platform-objects): `sys_email.error` field help now covers pre-delivery rejections, not only transport failures + + `sys_email.error` was declared as *"Transport error message when status=failed"*. + Since `EmailService.recordRejectedMessage` landed, the same column also carries + the reason a message was rejected by `normalizeMessage` **before** it reached a + transport (an unsendable `from`, no recipient, no subject, no body) — those rows + are written with `status: 'failed'` too, prefixed `rejected before delivery: `. + + Nothing was misleading in the *data*: the row prefixes its own reason, so an + operator reading a failed row is never sent chasing an SMTP host for a message + that never reached one. What was stale was the field's declared `description`, + which Studio surfaces as the field's help text — it named only the transport + case, narrower than what the column has held since that change landed. + + The description now reads: *"Why the message failed — a transport error, or the + validation that rejected it before delivery."* It stays true under both row + shapes and deliberately does not name the row's own `rejected before delivery:` + prefix, so it will not go stale again if that prefix's wording changes. +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/spec@17.4.0 + - @objectstack/metadata-core@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/platform-objects/package.json b/packages/platform-objects/package.json index 57442849b7..ac1b26ac7f 100644 --- a/packages/platform-objects/package.json +++ b/packages/platform-objects/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/platform-objects", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Core platform object schemas for ObjectStack — identity, security, audit, tenant, and metadata objects", "main": "dist/index.js", diff --git a/packages/plugins/embedder-openai/CHANGELOG.md b/packages/plugins/embedder-openai/CHANGELOG.md index 882de324f1..4fb64d8e53 100644 --- a/packages/plugins/embedder-openai/CHANGELOG.md +++ b/packages/plugins/embedder-openai/CHANGELOG.md @@ -1,5 +1,40 @@ # @objectstack/embedder-openai +## 17.4.0 + +### Patch Changes + +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/spec@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/plugins/embedder-openai/package.json b/packages/plugins/embedder-openai/package.json index 2ea074f9a1..3306dde12b 100644 --- a/packages/plugins/embedder-openai/package.json +++ b/packages/plugins/embedder-openai/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/embedder-openai", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "OpenAI-compatible embedder for ObjectStack — works against OpenAI, 阿里通义 DashScope, 智谱 BigModel, 硅基流动 SiliconFlow, 火山引擎 Doubao, MiniMax, Ollama, and any drop-in OpenAI-shape endpoint.", "main": "dist/index.js", diff --git a/packages/plugins/knowledge-memory/CHANGELOG.md b/packages/plugins/knowledge-memory/CHANGELOG.md index d17acf6db1..f9b113a505 100644 --- a/packages/plugins/knowledge-memory/CHANGELOG.md +++ b/packages/plugins/knowledge-memory/CHANGELOG.md @@ -1,5 +1,47 @@ # @objectstack/knowledge-memory +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/service-knowledge@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/plugins/knowledge-memory/package.json b/packages/plugins/knowledge-memory/package.json index 27234013b7..22f1320ef0 100644 --- a/packages/plugins/knowledge-memory/package.json +++ b/packages/plugins/knowledge-memory/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/knowledge-memory", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "In-memory knowledge adapter for ObjectStack (dev / test reference implementation).", "main": "dist/index.js", diff --git a/packages/plugins/knowledge-ragflow/CHANGELOG.md b/packages/plugins/knowledge-ragflow/CHANGELOG.md index 60dfc5ff23..1b7aea771f 100644 --- a/packages/plugins/knowledge-ragflow/CHANGELOG.md +++ b/packages/plugins/knowledge-ragflow/CHANGELOG.md @@ -1,5 +1,47 @@ # @objectstack/knowledge-ragflow +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/service-knowledge@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/plugins/knowledge-ragflow/package.json b/packages/plugins/knowledge-ragflow/package.json index 028ca63c07..00a2ff6996 100644 --- a/packages/plugins/knowledge-ragflow/package.json +++ b/packages/plugins/knowledge-ragflow/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/knowledge-ragflow", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "RAGFlow knowledge adapter for ObjectStack — production-grade RAG via the Apache 2.0 RAGFlow REST API.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-approvals/CHANGELOG.md b/packages/plugins/plugin-approvals/CHANGELOG.md index e274790c75..f2c8f83aa7 100644 --- a/packages/plugins/plugin-approvals/CHANGELOG.md +++ b/packages/plugins/plugin-approvals/CHANGELOG.md @@ -1,5 +1,67 @@ # @objectstack/plugin-approvals +## 17.4.0 + +### Minor Changes + +- 3d3f60e: An approval decision that lands while its flow run strands now says so in fields, not only in prose. + + `POST /api/v1/approvals/requests/{id}/reject` — and its sibling decision doors — could produce three coexisting outcomes from one call: the caller read HTTP 500, the request row **was** in its terminal status and had left the pending inbox, and the workflow run was stranded. A caller reading 500 has one honest inference available — "the rejection did not happen" — and it was the wrong one, so scripts and operators retried or escalated against a decision that was already durable. The only carrier of the truth was English prose in `error`, so finding the affected run meant regexing a run id out of a sentence, and nothing said whether that run could be repaired at all. + + The 500 stays. A recorded decision whose flow never advances is still a failure and is still reported as one; the door does not become atomic and no decision is ever rolled back. What changed is that it stops discarding what the engine already said: + + - **The `RESUME_FAILED` body gains four fields**, additively — `finalized` (always `true`: the decision stands), `decision`, `runId`, and `repairable`. Existing consumers see the same `code`, the same `error` and the same status. + - **`repairable` carries the engine's own discriminator** — `AutomationResult.status === 'stranded'`, the state stamped on exactly the exit that journals a repair snapshot. `false` is the answer for every other failure, including a lost run: absence of the signal is not repairability, and a repair verb that would refuse is worse than no promise. + - **`serviceResume` carries `status`** through to the door. It previously read only `success` / `code` / `error`, and the stranded exit reports a `status` and no `code` at all — so the platform's own repairability signal died one line before the envelope was built. + + `@objectstack/types` gains `strandedDecisionFailure` / `strandedDecisionDetails` and the `StrandedDecisionDetails` type — the constructor and its recogniser in one module, so the producing service and the REST door cannot drift. A `RESUME_FAILED` raised without that carrier answers exactly the body it always did; the door never synthesises the envelope. + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [2bb0614] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/platform-objects@17.4.0 + - @objectstack/types@17.4.0 + - @objectstack/formula@17.4.0 + - @objectstack/metadata-core@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-approvals/package.json b/packages/plugins/plugin-approvals/package.json index 4af6799ad2..b5da20d215 100644 --- a/packages/plugins/plugin-approvals/package.json +++ b/packages/plugins/plugin-approvals/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-approvals", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Multi-step approval engine for ObjectStack — sys_approval_process + sys_approval_request + sys_approval_action + IApprovalService.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-audit/CHANGELOG.md b/packages/plugins/plugin-audit/CHANGELOG.md index 1482cae17b..2bc56d3fe5 100644 --- a/packages/plugins/plugin-audit/CHANGELOG.md +++ b/packages/plugins/plugin-audit/CHANGELOG.md @@ -1,5 +1,57 @@ # @objectstack/plugin-audit +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [a56baa2] +- Updated dependencies [3e3ecb0] +- Updated dependencies [4b3955e] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [65846bc] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [3bd9b34] +- Updated dependencies [d0ee598] +- Updated dependencies [26144c2] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [ec0a6e7] +- Updated dependencies [2bb0614] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/objectql@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/platform-objects@17.4.0 + - @objectstack/metadata-core@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-audit/package.json b/packages/plugins/plugin-audit/package.json index 6a369ad2f3..ed2780c46b 100644 --- a/packages/plugins/plugin-audit/package.json +++ b/packages/plugins/plugin-audit/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-audit", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Audit Plugin for ObjectStack — System audit log object and audit trail", "main": "dist/index.js", diff --git a/packages/plugins/plugin-auth/CHANGELOG.md b/packages/plugins/plugin-auth/CHANGELOG.md index 016275eb38..fcd6a70a76 100644 --- a/packages/plugins/plugin-auth/CHANGELOG.md +++ b/packages/plugins/plugin-auth/CHANGELOG.md @@ -1,5 +1,106 @@ # Changelog +## 17.4.0 + +### Minor Changes + +- aedbaef: `POST /sign-up/email` for an address that already has a `sys_user` row is refused explicitly, instead of answering 200 for a row that is never written (#15587) + + **This is a wire-behaviour change on one lane**: a call that answers `200 {"token":null,"user":{…}}` today answers `422 USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL` after this change. Nothing is newly admitted — the response that changes is one that reported a creation that never happened. + + ### What was measured + + Under audience posture `email_domain` (domain allowlisted, `selfRegistrationPermissionSet` resolvable), a sign-up for an address that already carried a `sys_user` row answered **200 with a freshly minted user id** and persisted nothing: no new `sys_user`, no `sys_account`, and the next sign-in a `401` with nothing anywhere explaining it. The same call on the same population under the `invite_only` default was refused honestly with `422 USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL`. An operator, a provisioning script or the console reading the status code concludes the account exists — and this sits directly on the recovery path a locked-out deployment walks, where widening the posture to let a seeded person register is exactly the remedy an operator is pointed at. + + ### The mechanism + + better-auth's sign-up route computes `shouldReturnGenericDuplicateResponse = requireEmailVerification || autoSignIn === false` and, when it is on, answers a duplicate with a synthetic in-memory user instead of throwing. **No insert is attempted and nothing is swallowed**: the vendor's `findUserByEmail` short-circuits ahead of `createUser`, which is why no row and no credential appear. + + The posture is not itself the cause — it is only what arms the shield: a posture that permits self-registration **forces** `requireEmailVerification` on. Holding the posture constant at the `invite_only` default and moving only that flag reproduces the divergence exactly, which also means the defect was never confined to the widened postures: `emailAndPassword.autoSignIn: false` arms the same shield under any posture. + + ### The fix + + The uniqueness refusal is raised on the `/sign-up/email` before-hook, the same seam and the same reason the audience-posture refusal is already raised there, and built from better-auth's own `BASE_ERROR_CODES` entry so both lanes answer byte-identically. + + **Order is load-bearing: it runs only for a caller the posture already admitted.** Asking uniqueness first would hand an uninvited stranger an account-existence oracle under the `invite_only` default (422 for a real address versus 403 for an unknown one). After the gate, `invite_only` is untouched — a stranger still gets `SELF_REGISTRATION_CLOSED` and learns nothing. + + **Operators of `open` / `email_domain` should know what the honest refusal costs:** on those postures a caller the audience gate admits can now distinguish an address that has an account from one that does not, where the synthetic 200 previously hid it. That is the disclosure the `invite_only` lane has always made to an invitation holder, and the platform's answer for a widened posture is now the same fact rather than a false receipt. + + `USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL` is registered in the ADR-0112 error-code ledger under `@objectstack/plugin-auth`: the platform now **emits** it rather than only passing it through, and an emitted-but-unregistered code is the silent fourth state that ledger exists to prevent. + +### Patch Changes + +- 8e500f2: The `no_sign_in_account_at_boot` report now names a remedy that works — and warns off the one that silences the report itself. + + That boot line fires on the deployment nobody can sign in to: human `sys_user` rows, zero `sys_account` rows. It ended with two remedies, and measured on the exact population it fires on, neither did what its sentence said: + + - **"Open the audience posture so an existing person can register their own login"** produced no login, and for an existing person it never can: self-registration is a user-creation path, so it cannot attach a login to an address that already carries a `sys_user` row, whatever the posture. Widening only ever admits a *new* address — and then every posture other than `invite_only` forces `requireEmailVerification` on, so that login is refused `EMAIL_NOT_VERIFIED` at its first sign-in, and a locked-out self-hosted install is usually the shape with no mail transport wired. + - **"Write a `sys_account` credential row directly against the store"** was worse than useless. The `password` column carries a secret in the platform's own hash format, so a plaintext one authenticates nothing — and the probe behind this report asks only whether *any* `sys_account` row exists, so writing one turns the report off. The operator's first attempt at the named remedy turned the loud dead end back into the silent one the report was written to end. + + The line now names the path that was measured to work: write one pending `sys_invitation` row directly against the store — a lowercase address the directory does not already hold, `status` `pending`, a future `expires_at`, `inviter_id` of any existing `sys_user` — then register through the ordinary sign-up endpoint. The invitation carve-out admits that one creation under every posture, so no door needs widening. It is an admission verdict and not a verification bypass, though, so the line scopes what follows from that: only under the default `invite_only` posture is the recovery mail-transport-free, and it tells the operator to close a widened posture back to `invite_only` before the invited person registers — otherwise the invited login is created, refused `EMAIL_NOT_VERIFIED` at first sign-in, and has silenced this report on the way past. On the `single` tenancy posture that account holder is then promoted to platform admin. The other two are still named, as the two things that look like remedies and are not, because an operator who is going to hand-write a credential row anyway needs to know it blinds the probe. + + **Message text only — no admission semantics move.** Nothing widens, nothing narrows, no accept set changes, and the probe is untouched: this changes what an operator *reads*, not what the platform *admits*. The long form of the same three facts is on the self-hosting deployment page. +- 9e9f03a: A self-registration grant is refused, not silently redirected, when a permission-set row is malformed — and the fourteen dead `{ records }` / `{ data }` normalizer limbs behind that code are gone. + + `plugin-auth` carried fourteen array-or-envelope normalizer blocks of the shape `Array.isArray(x) ? x : x.records ?? []` (thirteen on a `records` limb, one on a `data` limb, four of them written as a guard clause rather than a ternary). All fourteen read the same concrete engine — the `ObjectQL` instance the kernel registers as the `objectql` / `data` service — which answers a bare array on every path, populated or empty. The envelope limb was unreachable code that read as a contract, so the next author writing a defensive normalizer here believed an envelope was possible. The limbs are removed, and the three local engine ports that declared `Promise` (`BootProbeEngine`, `DevAdminSeedProbeEngine`, `PhoneSmsTemplateEngine`) now declare the array they always returned. + + The user-visible change is in `settleSelfRegistrationGrant`, which carried the opposite defect. Its candidate filter dropped any permission-set row whose `id` was missing or blank, silently, before choosing which row to grant: + + - When the malformed row was the only one, the operator was told `no active sys_permission_set row named 'X' resolves` — false, since an active row named exactly that was present. That report is the only signal this path emits, and nothing retries it. + - When the malformed row was the **organization-scoped** one and a global row also carried the declared name, dropping it let the `organization_id == null` arm match instead, and the self-registrant was granted the **global** permission set their organization never declared — with a success log and no other trace. + + `active !== false` remains a selection predicate: a deactivated set still reports the ordinary "does not resolve". A malformed row is no longer a selection at all — the grant is refused and the report names the malformed row, so the ambiguity is surfaced instead of resolved by accident. A well-formed family grants exactly as before. + + **Upgrade note — one family now gets a refusal where it previously got a grant.** If a deployment's `sys_permission_set` already contains a row that is active and carries the declared name but whose `id` is missing or blank, self-registration grants against that name now stop and report, including the case where the malformed row is one nobody was relying on: a malformed **global** row sitting alongside a well-formed **organization-scoped** row used to be dropped silently, letting the org row be granted, and is now refused. This is deliberate — the old behaviour could not tell that family apart from the one where the silent drop granted the *wrong* set — and it is fully reversible without a code change: repair or delete the malformed row and the grant proceeds exactly as before. The refusal is loud and names the row, so it is visible rather than something to discover later; nothing is written while it stands. +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [4bc9821] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [a84e1ce] +- Updated dependencies [65846bc] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [e13ede8] +- Updated dependencies [f5cc78b] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [2bb0614] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/platform-objects@17.4.0 + - @objectstack/rest@17.4.0 + - @objectstack/types@17.4.0 + - @objectstack/service-messaging@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-auth/package.json b/packages/plugins/plugin-auth/package.json index b1b11c0ccc..15874d1fba 100644 --- a/packages/plugins/plugin-auth/package.json +++ b/packages/plugins/plugin-auth/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-auth", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Authentication & Identity Plugin for ObjectStack", "main": "dist/index.js", diff --git a/packages/plugins/plugin-dev/CHANGELOG.md b/packages/plugins/plugin-dev/CHANGELOG.md index 696df98cbb..224d87a052 100644 --- a/packages/plugins/plugin-dev/CHANGELOG.md +++ b/packages/plugins/plugin-dev/CHANGELOG.md @@ -1,5 +1,133 @@ # @objectstack/plugin-dev +## 17.4.0 + +### Patch Changes + +- 88a35c2: fix(plugin-dev): the i18n auto-detect resolves `translations` from `packages[]`, not only the flattened top level (#15232) + + `DevPlugin.init`'s 3b block read `options.stack.translations` and nothing else. + For a multi-package app under the ADR-0130 D4 option-B shape — where + `packages[]` carries each definition exactly once and the flattened top-level + copy is gone — that read returns `undefined`, the detection concludes "this app + declared no copy", and the boot continues. Nothing throws and nothing logs. + + What the developer gets instead is the wrong strings. `I18nServicePlugin` + (`@objectstack/service-i18n`) is never registered, so the `i18n` slot keeps the + core in-memory fallback: `os dev` serves message KEYS, or last release's copy, + for an app that declared real translations. It reads as "the translations are + broken", not as "a collection went missing", which is why it is a reader fix + rather than a footnote. + + The detection now reads the flattened top level FIRST and then each package + body, in the order `resolveArtifactPackageOrder` (`@objectstack/core`, + ADR-0130 D4+D5) registers them: + + - **Every artifact the platform emits today answers bit-identically.** The + flattened level still answers first and short-circuits, so the `packages[]` + pass can only supply a declaration the top level did not have. This is the + reader half of the ruled order (readers first, emitter last, the artifact + additive throughout), so it lands with no change to what any command emits. + - **The caller's original expression is preserved, not re-expressed.** + `Array.isArray(t) && t.length > 0` still decides the top level, per package + body as well — re-expressing a gate as a resolved-and-counted traversal is + what silently changes the verdict for a stack that declares the key empty. + - **⛔ `stack.packages` is not iterated directly.** + `resolveArtifactPackageOrder` is the platform's one traversal and also the + GATE that parses each entry, so a second traversal would disagree with the + load path about which artifacts are loadable. An artifact with no `packages` + key is left entirely on the old path — the key's absence is checked before + the call, because D4's second branch would otherwise hand the caller's own + object back and read the same `translations` twice. + - **A malformed `packages` is refused, not skipped.** A non-array `packages`, + an entry inlined instead of wrapped under `manifest:`, or a duplicate package + id raises the same ADR-0112 envelope (`code` + `status: 422`) that + `ObjectQL.registerApp` raises for the same object later in the same boot. + + The decision — detection plus the locales it derives — is now one exported + function, `devI18nPluginOptions`, so the #15004 option-B acceptance pin + measures it by CALLING it rather than re-implementing the read. `DevPlugin` + keeps the dynamic import and its degradation: those are about the optional + package being installed, which is a different question from what the stack + declares. +- Updated dependencies [2ed6be6] +- Updated dependencies [98191d2] +- Updated dependencies [ca326b5] +- Updated dependencies [f1a1028] +- Updated dependencies [8f404a5] +- Updated dependencies [a56baa2] +- Updated dependencies [3e3ecb0] +- Updated dependencies [8e500f2] +- Updated dependencies [4b3955e] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [4bc9821] +- Updated dependencies [2003259] +- Updated dependencies [a646120] +- Updated dependencies [65846bc] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [fa85759] +- Updated dependencies [5f7fa1d] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [a84e1ce] +- Updated dependencies [a84e1ce] +- Updated dependencies [65846bc] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [3bd9b34] +- Updated dependencies [d0ee598] +- Updated dependencies [26144c2] +- Updated dependencies [9e9f03a] +- Updated dependencies [5eb24f8] +- Updated dependencies [c64e65f] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [e13ede8] +- Updated dependencies [f5cc78b] +- Updated dependencies [8a12067] +- Updated dependencies [ee32e1c] +- Updated dependencies [8744de9] +- Updated dependencies [ebb5550] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [ec0a6e7] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/objectql@17.4.0 + - @objectstack/runtime@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/plugin-auth@17.4.0 + - @objectstack/rest@17.4.0 + - @objectstack/driver-memory@17.4.0 + - @objectstack/plugin-hono-server@17.4.0 + - @objectstack/types@17.4.0 + - @objectstack/service-i18n@17.4.0 + - @objectstack/plugin-security@17.4.0 + - @objectstack/service-storage@17.4.0 + - @objectstack/service-realtime@17.4.0 + - @objectstack/account@17.4.0 + - @objectstack/setup@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/plugins/plugin-dev/package.json b/packages/plugins/plugin-dev/package.json index 8e0dc430da..5a0bfb18e7 100644 --- a/packages/plugins/plugin-dev/package.json +++ b/packages/plugins/plugin-dev/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-dev", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Development Assembly Plugin for ObjectStack — wires the real platform stack for zero-config local development", "main": "dist/index.js", diff --git a/packages/plugins/plugin-email/CHANGELOG.md b/packages/plugins/plugin-email/CHANGELOG.md index 2befc51292..230f5773b4 100644 --- a/packages/plugins/plugin-email/CHANGELOG.md +++ b/packages/plugins/plugin-email/CHANGELOG.md @@ -1,5 +1,49 @@ # @objectstack/plugin-email +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [2bb0614] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/platform-objects@17.4.0 + - @objectstack/formula@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-email/package.json b/packages/plugins/plugin-email/package.json index e7eda7ae2e..33487a3b7f 100644 --- a/packages/plugins/plugin-email/package.json +++ b/packages/plugins/plugin-email/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-email", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Email service plugin for ObjectStack — IEmailService + transport-pluggable outbound delivery with sys_email persistence.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-hono-server/CHANGELOG.md b/packages/plugins/plugin-hono-server/CHANGELOG.md index d21c0b02fb..30a815e57f 100644 --- a/packages/plugins/plugin-hono-server/CHANGELOG.md +++ b/packages/plugins/plugin-hono-server/CHANGELOG.md @@ -1,5 +1,83 @@ # @objectstack/plugin-hono-server +## 17.4.0 + +### Minor Changes + +- 5f7fa1d: feat(hono-server): `GET /auth/me/localization` → `locale` is now the signed-in user's language — `sys_user.locale` when set, then the request's `Accept-Language`, then the deployment default (#14788) + + Maintainer ruling 2026-09-03 (option D on #14788): this endpoint is the ONE + read face for "what language is this user", now that `sys_user.locale` is a + user-stated preference (#13881 / #14787) and the never-produced + `SessionUser.language` is retired from the session contract + (`@objectstack/spec`, same release). + + What changed, for an authenticated caller: + + - `locale` resolves **the user's own `sys_user.locale`** first — read under a + system context by the caller's own id and accepted only when it passes the + column's OWN `locale_bcp47_shape` rule as the registry declares it (the + endpoint evaluates that rule; it carries no second locale parser). A + malformed, blank or unverifiable value falls through, it is never served. + - then **the request's `Accept-Language`** preference (`preferredLocaleFromHeader`, + the same parse REST and the runtime dispatcher feed `execCtx.locale` from); + - then **the deployment default** (`resolveLocalizationContext` — the + `localization.locale` settings cascade, floor `en-US`). + + Before, the resolver behind this endpoint assembled no localization at all, so + `locale` was `null` for every authenticated caller; it is now always a string + for an authenticated caller. The response shape is unchanged + (`{ authenticated, currency, locale, timezone }`), `currency` / `timezone` + are untouched, and the unauthenticated answer (`{ authenticated: false }`) is + unchanged. `resolveSignedInUserLocale` is exported for hosts that compose the + current-user endpoints directly. + +### Patch Changes + +- fa85759: `GET /auth/me/localization` answers the deployment's resolved `currency` and `timezone` instead of `null` + + The handler read both off the request `ExecutionContext`, citing ADR-0053, but the resolver serving this surface is a hand-rolled envelope that never carried them — so every authenticated caller was answered `currency: null, timezone: null` whatever the `localization` settings said, and the console's regional-formatting seed was fed nulls. All three values now come from one reading of the same `resolveLocalizationContext` cascade the dispatcher's shared assembler uses. `locale` resolution is unchanged. `timezone` now always answers (cascade floor `UTC`); `currency` still answers `null` when the deployment configures none — that value has no floor. +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/types@17.4.0 + - @objectstack/observability@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-hono-server/package.json b/packages/plugins/plugin-hono-server/package.json index 8fe69ada78..ac5d841ae4 100644 --- a/packages/plugins/plugin-hono-server/package.json +++ b/packages/plugins/plugin-hono-server/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-hono-server", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Standard Hono Server Adapter for ObjectStack Runtime", "main": "dist/index.js", diff --git a/packages/plugins/plugin-pinyin-search/CHANGELOG.md b/packages/plugins/plugin-pinyin-search/CHANGELOG.md index 240f65cbe7..e35f4765ed 100644 --- a/packages/plugins/plugin-pinyin-search/CHANGELOG.md +++ b/packages/plugins/plugin-pinyin-search/CHANGELOG.md @@ -1,5 +1,28 @@ # @objectstack/plugin-pinyin-search +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [a56baa2] +- Updated dependencies [4b3955e] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [65846bc] +- Updated dependencies [fa125f3] +- Updated dependencies [088f761] +- Updated dependencies [3bd9b34] +- Updated dependencies [d0ee598] +- Updated dependencies [26144c2] +- Updated dependencies [d4f9b2a] +- Updated dependencies [a727043] +- Updated dependencies [ec0a6e7] +- Updated dependencies [3d3f60e] + - @objectstack/core@17.4.0 + - @objectstack/objectql@17.4.0 + - @objectstack/types@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/plugins/plugin-pinyin-search/package.json b/packages/plugins/plugin-pinyin-search/package.json index 5a55d01c01..bb144ca23e 100644 --- a/packages/plugins/plugin-pinyin-search/package.json +++ b/packages/plugins/plugin-pinyin-search/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-pinyin-search", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Pinyin search recall for ObjectStack — populates the hidden `__search` companion column (full pinyin + initials of the display/name field) so `$search` hits CJK names typed as pinyin. Locale-gated via OS_SEARCH_PINYIN_ENABLED (#2486).", "main": "dist/index.js", diff --git a/packages/plugins/plugin-reports/CHANGELOG.md b/packages/plugins/plugin-reports/CHANGELOG.md index a245ee4bc6..f3268262c0 100644 --- a/packages/plugins/plugin-reports/CHANGELOG.md +++ b/packages/plugins/plugin-reports/CHANGELOG.md @@ -1,5 +1,48 @@ # @objectstack/plugin-reports +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [2bb0614] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/platform-objects@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/plugins/plugin-reports/package.json b/packages/plugins/plugin-reports/package.json index 606871f828..48803a90aa 100644 --- a/packages/plugins/plugin-reports/package.json +++ b/packages/plugins/plugin-reports/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-reports", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Saved reports + scheduled email digests for ObjectStack — sys_saved_report + sys_report_schedule + IReportService.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-security/CHANGELOG.md b/packages/plugins/plugin-security/CHANGELOG.md index a21770d6ce..d1f8bb691e 100644 --- a/packages/plugins/plugin-security/CHANGELOG.md +++ b/packages/plugins/plugin-security/CHANGELOG.md @@ -1,5 +1,106 @@ # @objectstack/plugin-security +## 17.4.0 + +### Patch Changes + +- c64e65f: fix(plugin-security): the app default permission set resolves from the first level that NAMES one (#15298) + + `declaredPermissionSets` carried a docblock stating a short-circuit its code did + not have: + + > The `packages[]` pass only supplies a set where the top level had none — which + > is precisely the option-B artifact. + + The code pushed the flattened top level and then **every** package body + unconditionally, so on today's additive artifact (flattened level *and* + `packages[]` both present) every permission set was collected twice. Nothing + observable came of it — the sole caller is private and takes the first + `isDefault` set, which the flattened copy still supplied — so this corrects a + false written contract on a security-path reader, not a live defect. That + distinction is the point: the sentence was load-bearing, because it was the + stated reason the reader half was revertible on its own and safe to land before + the emitter half (#14512), and the next reader would have believed the mechanism + was there. + + ⚠️ Release-notes note: this supersedes one sentence of the #15226 entry in this same + unreleased batch — "The resolution now reads the flattened top level FIRST and then each + package body". That described #15226 accurately when it landed; after this change the + `packages[]` pass runs only where the top level named no default. The earlier entry is + left as written rather than retro-edited, so whoever compiles the notes collapses the two + deliberately instead of reading a contradiction. + + The reader now walks the discipline the docblock claims — start from the + expression this program replaced, `appDefaultPermissionSetName(config.permissions)`, + and consult `packages[]` only where it came back `undefined`. + + - **The condition is the resolved NAME, never the `permissions` container.** + Branching on the container re-creates the silent loss the reader program + exists to remove, one shape further along: a flattened level that carries + permission sets but marks none of them `isDefault` is legal today and + hand-authorable in any `objectstack.config.ts`, and a container-shaped + condition (`Array.isArray(flattened)`, with or without `&& length > 0`) shorts + it past the whole `packages[]` pass and answers `undefined` — nothing thrown, + nothing logged, every member of the app back down to the platform floor alone. + Reading the answer also retires the `[]`-is-truthy trap rather than patching + around it. + - **The package order is resolved BEFORE the top level is consulted.** + `resolveArtifactPackageOrder` refuses a malformed `packages` — not an array, + an entry inlined instead of wrapped under `manifest:`, a duplicate package id + — with an ADR-0112 envelope this reader does not catch, and that refusal must + not become conditional on whether the flattened level happened to name a + default first. An artifact is either loadable or refused; which level answered + is not part of that question. + - **No emitted artifact changes its answer.** Measured, not argued: 26 shapes — + the composed additive artifact, its option-B derivative, the collection-zoo + fixtures behind the #15004 acceptance pin, every config the unit suite drives, + the three malformed-`packages` refusals, and the hand-authored mixed shapes — + return byte-identical results before and after, with `@objectstack/plugin-security` + rebuilt and the change proven present in `dist/` on each leg. +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [2bb0614] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/platform-objects@17.4.0 + - @objectstack/types@17.4.0 + - @objectstack/formula@17.4.0 + - @objectstack/metadata-core@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-security/package.json b/packages/plugins/plugin-security/package.json index 5539cdb0b9..792e46f5ee 100644 --- a/packages/plugins/plugin-security/package.json +++ b/packages/plugins/plugin-security/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-security", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Security Plugin for ObjectStack — RBAC, RLS, and Field-Level Security Runtime", "main": "dist/index.js", diff --git a/packages/plugins/plugin-sharing/CHANGELOG.md b/packages/plugins/plugin-sharing/CHANGELOG.md index b1ffd9d3cd..6f5f4a1f1a 100644 --- a/packages/plugins/plugin-sharing/CHANGELOG.md +++ b/packages/plugins/plugin-sharing/CHANGELOG.md @@ -1,5 +1,61 @@ # @objectstack/plugin-sharing +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [a56baa2] +- Updated dependencies [3e3ecb0] +- Updated dependencies [4b3955e] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [65846bc] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [3bd9b34] +- Updated dependencies [d0ee598] +- Updated dependencies [26144c2] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [ec0a6e7] +- Updated dependencies [2bb0614] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/objectql@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/platform-objects@17.4.0 + - @objectstack/types@17.4.0 + - @objectstack/formula@17.4.0 + - @objectstack/metadata-core@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/plugins/plugin-sharing/package.json b/packages/plugins/plugin-sharing/package.json index f6bc396e68..995a4da41a 100644 --- a/packages/plugins/plugin-sharing/package.json +++ b/packages/plugins/plugin-sharing/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-sharing", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Record-level sharing for ObjectStack — sys_record_share + middleware that enforces sharingModel + ISharingService.", "main": "dist/index.js", diff --git a/packages/plugins/plugin-webhooks/CHANGELOG.md b/packages/plugins/plugin-webhooks/CHANGELOG.md index 6c08a73fec..09f9acb87a 100644 --- a/packages/plugins/plugin-webhooks/CHANGELOG.md +++ b/packages/plugins/plugin-webhooks/CHANGELOG.md @@ -1,5 +1,49 @@ # @objectstack/plugin-webhooks +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [2bb0614] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/platform-objects@17.4.0 + - @objectstack/service-messaging@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/plugins/plugin-webhooks/package.json b/packages/plugins/plugin-webhooks/package.json index b450749bee..81f65bc654 100644 --- a/packages/plugins/plugin-webhooks/package.json +++ b/packages/plugins/plugin-webhooks/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/plugin-webhooks", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Persistent, cluster-aware webhook dispatcher. Durable outbox + per-partition cluster.lock for exactly-once-ish delivery across nodes. See content/docs/concepts/webhook-delivery.mdx.", "type": "module", diff --git a/packages/qa/dogfood/CHANGELOG.md b/packages/qa/dogfood/CHANGELOG.md index 7c015328e0..54eeb55654 100644 --- a/packages/qa/dogfood/CHANGELOG.md +++ b/packages/qa/dogfood/CHANGELOG.md @@ -1,5 +1,81 @@ # @objectstack/dogfood +## 0.0.44 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [54bb2f1] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [a56baa2] +- Updated dependencies [3e3ecb0] +- Updated dependencies [8e500f2] +- Updated dependencies [4b3955e] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [65846bc] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [17f8604] +- Updated dependencies [3bd9b34] +- Updated dependencies [d0ee598] +- Updated dependencies [26144c2] +- Updated dependencies [9e9f03a] +- Updated dependencies [5eb24f8] +- Updated dependencies [c64e65f] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [a646120] +- Updated dependencies [ebb5550] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [ec0a6e7] +- Updated dependencies [2bb0614] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] +- Updated dependencies [c550baf] + - @objectstack/objectql@17.4.0 + - @objectstack/service-analytics@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/metadata@17.4.0 + - @objectstack/plugin-auth@17.4.0 + - @objectstack/platform-objects@17.4.0 + - @objectstack/types@17.4.0 + - @objectstack/mcp@17.4.0 + - @objectstack/plugin-security@17.4.0 + - @objectstack/service-storage@17.4.0 + - @objectstack/verify@17.4.0 + - @objectstack/example-showcase@0.3.18 + - @objectstack/connector-mcp@17.4.0 + - @objectstack/connector-openapi@17.4.0 + - @objectstack/connector-rest@17.4.0 + - @objectstack/plugin-audit@17.4.0 + - @objectstack/plugin-email@17.4.0 + - @objectstack/plugin-sharing@17.4.0 + - @objectstack/plugin-webhooks@17.4.0 + - @objectstack/service-messaging@17.4.0 + - @objectstack/example-crm@4.0.96 + - @objectstack/example-multi-package@0.0.3 + - @objectstack/metadata-core@17.4.0 + ## 0.0.43 ### Patch Changes diff --git a/packages/qa/dogfood/package.json b/packages/qa/dogfood/package.json index f4175a4288..0093702bd7 100644 --- a/packages/qa/dogfood/package.json +++ b/packages/qa/dogfood/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/dogfood", - "version": "0.0.43", + "version": "0.0.44", "private": true, "license": "Apache-2.0", "description": "Dogfood regression gate — hand-written golden tests that boot real example apps through @objectstack/verify's in-process HTTP stack, pinning historical runtime regressions (#2018 timezone bucketing, #1994 cross-owner RLS, #2004 field fidelity) that static checks miss.", diff --git a/packages/qa/downstream-contract/CHANGELOG.md b/packages/qa/downstream-contract/CHANGELOG.md index e2c941d381..50cbee9f4f 100644 --- a/packages/qa/downstream-contract/CHANGELOG.md +++ b/packages/qa/downstream-contract/CHANGELOG.md @@ -1,5 +1,40 @@ # @objectstack/downstream-contract +## 0.0.42 + +### Patch Changes + +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/spec@17.4.0 + ## 0.0.41 ### Patch Changes diff --git a/packages/qa/downstream-contract/package.json b/packages/qa/downstream-contract/package.json index cab016a168..8fc05f97b7 100644 --- a/packages/qa/downstream-contract/package.json +++ b/packages/qa/downstream-contract/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/downstream-contract", - "version": "0.0.41", + "version": "0.0.42", "description": "Frozen third-party consumer fixture — a backward-compatibility gate for @objectstack/spec. Authored the way an external project on a published release authors metadata; if a spec change breaks it, that change is breaking (#2035).", "license": "Apache-2.0", "private": true, diff --git a/packages/qa/http-conformance/CHANGELOG.md b/packages/qa/http-conformance/CHANGELOG.md index 66f2c4fb83..d2d4b43393 100644 --- a/packages/qa/http-conformance/CHANGELOG.md +++ b/packages/qa/http-conformance/CHANGELOG.md @@ -1,5 +1,16 @@ # @objectstack/http-conformance +## 0.1.4 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [d4f9b2a] +- Updated dependencies [a727043] + - @objectstack/core@17.4.0 + ## 0.1.3 ### Patch Changes diff --git a/packages/qa/http-conformance/package.json b/packages/qa/http-conformance/package.json index 6646106b27..882e78982f 100644 --- a/packages/qa/http-conformance/package.json +++ b/packages/qa/http-conformance/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/http-conformance", - "version": "0.1.3", + "version": "0.1.4", "private": true, "license": "Apache-2.0", "description": "HTTP transport-port conformance gate (ADR-0076 D11/OQ#10, #2462) — a zero-dependency node:http reference implementation of IHttpServer plus a cross-adapter suite that boots the dispatcher bridge and REST generator on it AND on plugin-hono-server, pinning that the port stays free of framework-isms. Not published; validation instrument, not a product server.", diff --git a/packages/rest/CHANGELOG.md b/packages/rest/CHANGELOG.md index 15a7f00c9c..3e483b30d0 100644 --- a/packages/rest/CHANGELOG.md +++ b/packages/rest/CHANGELOG.md @@ -1,5 +1,240 @@ # @objectstack/rest +## 17.4.0 + +### Minor Changes + +- 65846bc: 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) + + + + **BREAKING** on the per-row results of the import runner + (`POST /api/v1/data/:object/import` and the import job): a row refused by the + engine's `DuplicateRecordError` envelope now reports `code: 'UNIQUE_VIOLATION'` + where it reported `'DUPLICATE_RECORD'`. Shipped as `minor` under the repo's + launch-window convention for breaking changes. Maintainer ruling 2026-09-03 on + #14723 (verbatim 「同意,然后执行契约复审」), adopting option A: one wire + spelling for a unique-constraint refusal on every route. + + **Why.** `toFailedResult` relayed the thrown error's own `code`, and the engine's + envelope carries the registered `DUPLICATE_RECORD` — while the whole-request + failure on the same import route answered `UNIQUE_VIOLATION` through + `mapDataError`. Two spellings of one condition on one route, which ADR-0112's + one-name-per-concept and the error-code ledger's header both forbid. The + duplication is removed, not declared: no ledger waiver is added. + + **What changes.** The import row derivation applies the whole-request arm's own + predicate — the registered code AND the class name `DuplicateRecordError`, + exported from `error-response.ts` as `isEngineDuplicateRecordEnvelope` and now + shared by the arm and the row report — and reports `UNIQUE_VIOLATION`. A + field-level finding still takes precedence (the envelope carries none), the + row's sentence is unchanged (the platform sentence, sanitised as before; no + driver text), and a producer that merely throws the registered + `DUPLICATE_RECORD` without being the engine's class keeps its own code. + + **What does NOT change.** The whole-request doors (single-record, bulk, import, + metadata, UI) already answered `UNIQUE_VIOLATION` and keep doing so; the arm's + logic is untouched beyond reading the shared predicate. The engine's thrown + identity stays `DUPLICATE_RECORD` in-process. This package's `error-response.ts` + docblock that disclosed the fork under the #14541 contract review now states + the converged rule. + + **Consumer note.** An import client that branched on a row's `code` reading + `DUPLICATE_RECORD` reads `UNIQUE_VIOLATION` there now — the same value it + already handles for the whole-request 409. Measured in-repo and in the sibling + repos (hotcrm, objectui, non-test sources): zero consumers branch on either + spelling of a row code. +- f5cc78b: fix(rest): the generic declared-status passthrough names its object on both error doors (#14725) + + **Response-body change on the published bulk / metadata / UI doors: one optional + key is added, `object`.** Nothing is removed, no status moves, and no `code` + value changes spelling. + + #14541 made the two REST error doors agree for every refusal a *bespoke* arm + classifies. They still disagreed for every refusal that reached the *generic* + declared-status passthrough, because the two copies of that one passthrough + differed by exactly one key: `classifyDataError`'s copy ends + `...(object ? { object } : {})` and `resolveErrorResponse`'s 4xx arm had no such + limb. Measured on `main` @ `a12b15e394` — one error object, both doors: + + | door | before | + |---|---| + | `mapDataError(err, 'duly_note')` (single-record `/data`) | `409 {"error":"…","code":"DUPLICATE_RECORD","object":"duly_note"}` | + | `sendThrownError(res, err, 'duly_note')` (bulk / metadata / UI) | `409 {"error":"…","code":"DUPLICATE_RECORD"}` | + + One refusal, two bodies, decided by which route caught it — the #14541 shape one + arm over. The bulk door now answers the first row too. + + It closes the same card's second residue with it. `recordNotFoundError` + (`@objectstack/core`) declares `code`, `status = 404` **and** `object`, so that + declared status carries a record-level not-found past the `RECORD_NOT_FOUND` arm + into this same generic passthrough on every route reporting through + `handleRouteError` / `sendThrownError`, while the single-record `/data` door + reached the generic arm in `classifyDataError` and shipped the name. Both doors + now agree for that producer in every combination of declared status and + door-supplied object. + + **Who sees the new key.** The name comes from the door's `object` *argument*, + never from `error.object`, so only a route that supplies one is widened. Of 35 + route call sites of this door, **9** pass an argument that can be a non-empty + object name — `POST /data/:object/batch`, `/createMany`, `/updateMany`, + `/deleteMany`, `POST /data/:object/:id/clone`, `POST /data/:object/import`, + `POST /data/:object/import/jobs`, `GET /data/:object/export`, and + `GET /ui/view/:object/:type`. The other 26 (21 passing nothing, 5 passing the + literal `''`) answer byte-identical bodies. `classifiedRefusalAnswer` — the + entry point the analytics dataset face and the record-share family re-dress — + calls this door with no `object` argument at all, so those envelopes' key sets + do not move. + + **What deliberately does not change.** The declared-**5xx** arm gains nothing: + its sibling `declaredServerFaultAnswer` names no object either, so the two doors + already agreed in that band and adding the limb there would *create* a + divergence, on top of putting a caller-supplied name into a body whose whole + rule is that a declared server fault says nothing beyond status and code. The + `RECORD_NOT_FOUND` arm's message-**text** limb + (`/^Record \S+ not found in \S+/i`) is not lifted above the passthrough either — + that boundary is #14541's, and it is now pinned behaviourally and positionally + rather than described. + + Consumer note: a client that key-counts or exact-matches an error body from a + bulk, import, export, clone or UI-view route will see `object` alongside `error` + and `code` where the equivalent single-record `/data` response has carried it all + along. A client that reads named fields is unaffected. +- 3d3f60e: An approval decision that lands while its flow run strands now says so in fields, not only in prose. + + `POST /api/v1/approvals/requests/{id}/reject` — and its sibling decision doors — could produce three coexisting outcomes from one call: the caller read HTTP 500, the request row **was** in its terminal status and had left the pending inbox, and the workflow run was stranded. A caller reading 500 has one honest inference available — "the rejection did not happen" — and it was the wrong one, so scripts and operators retried or escalated against a decision that was already durable. The only carrier of the truth was English prose in `error`, so finding the affected run meant regexing a run id out of a sentence, and nothing said whether that run could be repaired at all. + + The 500 stays. A recorded decision whose flow never advances is still a failure and is still reported as one; the door does not become atomic and no decision is ever rolled back. What changed is that it stops discarding what the engine already said: + + - **The `RESUME_FAILED` body gains four fields**, additively — `finalized` (always `true`: the decision stands), `decision`, `runId`, and `repairable`. Existing consumers see the same `code`, the same `error` and the same status. + - **`repairable` carries the engine's own discriminator** — `AutomationResult.status === 'stranded'`, the state stamped on exactly the exit that journals a repair snapshot. `false` is the answer for every other failure, including a lost run: absence of the signal is not repairability, and a repair verb that would refuse is worse than no promise. + - **`serviceResume` carries `status`** through to the door. It previously read only `success` / `code` / `error`, and the stranded exit reports a `status` and no `code` at all — so the platform's own repairability signal died one line before the envelope was built. + + `@objectstack/types` gains `strandedDecisionFailure` / `strandedDecisionDetails` and the `StrandedDecisionDetails` type — the constructor and its recogniser in one module, so the producing service and the REST door cannot drift. A `RESUME_FAILED` raised without that carrier answers exactly the body it always did; the door never synthesises the envelope. + +### Patch Changes + +- 4bc9821: An organization-scoped caller's own items now appear in the untyped metadata diagnostics sweep. + + `GET /api/v1/meta/diagnostics` has two arms. The `?type=` arm has stated the caller's organization since #13753; the untyped whole-registry sweep passed none, so the Studio governance summary reported clean tiles over a partition it never read — undercounting relative to the per-type drill-down screen you reach by clicking into it. A summary whose whole job is surfacing problems, and which structurally cannot see a class of them while its own drill-down can, issues a false all-clear. The untyped arm now forwards the caller's organization, so items that organization authored on the five `allowOrgOverride: true` types (`view`, `dashboard`, `report`, `translation`, `email_template`) are counted in `stats`, `total` and `scannedItems`. + + The organization is passed RAW, deliberately, and that is the whole of the change — no new parameter, response field, status code or contract surface. There is no single type to fold on for a whole-registry sweep, and folding on any one of them would suppress the organization for every type at once; instead `getMetaDiagnostics` reads each swept type through `getMetaItems`, which applies the `allowOrgOverride` read gate to its own request type, so every type is scoped on its own registry flag. A non-overridable type (`object`, `flow`, `app`, …) is still read environment-wide and no pre-#6190 organization-scoped row is resurrected into the report. An anonymous or organization-less caller reads exactly what it read before, and the `stats` / `total` / `scannedTypes` arithmetic is unchanged in shape. +- a84e1ce: fix(rest): metadata label lookup honours the stack's declared `i18n.fallbackLocale` / `defaultLocale` instead of falling through to the `en` bundle (#14882) + + On a workspace whose labels are authored in `zh-CN` (`defaultLocale: 'zh-CN'`, + `fallbackLocale: 'zh-CN'`) and which ships only a courtesy `en` translation bundle, + `GET /api/v1/meta/object/:name`, the `/meta/:type` list, `GET /api/v1/meta` and the + public-form schema served the ENGLISH bundle labels to a `zh-CN` request (`Entry Sheet` + for an authored `填报单`, `KPI Assessment` for `KPI 考核管理`). The document translators walk + `requested locale → fallback chain → authored label` and default the chain to a literal + `['en']`; every REST seam passed none, so the declared fallback never reached the chain + and `en` was consulted before the authored label. + + Every metadata translation seam now passes `fallbackChain: [i18n.getFallbackLocale()]` — + the locale the i18n service's own `t()` falls back to, which `I18nServicePlugin` receives + from the stack config as `fallbackLocale || defaultLocale || 'en'`. For the workspace + above a `zh-CN` request now resolves `zh-CN → zh-CN → authored label` (the authored + Chinese labels), an `en` request still gets the `en` bundle, and a `zh-CN` bundle, when one + is shipped, still wins over the authored label. + + Feature-detected: an i18n service that does not declare a fallback (the method is + optional on `II18nService`; the core in-memory fallback has none) gets no chain and the + resolver's own default applies exactly as before. A stack declaring `defaultLocale: 'zh-CN'` + with `fallbackLocale: 'en'` is likewise unchanged — the declared `en` is honoured as it + reads. +- e13ede8: The admin "Used by" panel no longer clears a delete when the caller's own organization is using the item. + + `GET /api/v1/meta/:type/:name/references` backs that panel, whose empty case reads "Nothing in the metadata graph points at this item. Safe to delete." — advice given to an operator about to delete something. The door supplied no organization, so the reference sweep read the environment partition only: an organization-scoped `view` (or `dashboard`, `report`, `translation`, `email_template`) pointing straight at the object being deleted was invisible, and the panel issued a false clearance. It now passes the caller's organization, and those references are returned. + + The organization is passed RAW, deliberately, and that is the whole of the change — no new parameter, response field or contract surface. `req.params.type` is the reference TARGET, while the sweep spends the organization on the SOURCES it reads per type; `getMetaItems` applies the `allowOrgOverride` read gate to its own request type, so each source is scoped on its own registry flag. A non-overridable source (`object`, `flow`, `app`, …) is still read environment-wide and no pre-#6190 organization-scoped row is resurrected into a delete clearance. An anonymous or organization-less caller reads exactly what it read before, and no status code or response shape moves. +- a727043: fix(rest,core): an organization-less or ex-member API key on a walled single-kernel deployment now answers 401 where it answered 200 + + Under a wall-enforcing tenancy posture (`isolated`), an API key stamped with an + organization its owner is no longer a member of **read and wrote that + organization's rows** on the wiring the open core actually builds. Not a silent + empty set — a GET that returned the other organization's records, and a POST + that landed a row read back from the store carrying that organization's id and + the ex-member as its creator. An organization-less key on the same deployment + read `200` with an empty set, which is the silent failure the wall exists to + replace. + + The cause was a seam, not a predicate. `RestServer.computeExecCtx` derived the + effective tenancy posture from a per-request kernel, and on the single-kernel + wiring there is no per-request kernel — so the posture was `undefined` on every + request, and both posture-conditional API-key refusals are gated on it: + `organization_required` in `api-key.ts` and `organization_membership_ended` in + `resolve-authz-context.ts`. Neither ever ran. The Layer 0 wall itself was + active the whole time; it compares against the caller's active organization, + and an API key's tenant is `sys_api_key.active_organization_id` copied verbatim + — the holder's own stored claim. Enforcing the wall is what let the ex-member + through, because the one fact that would expose the ended membership was not an + input to the layer that could act on it. + + The single-kernel branch now derives the posture from a provider `rest-api-plugin` + wires to the lone local kernel's `tenancy` service, in the same shape as the + auth-service provider beside it. A host that registers no `tenancy` service is + unchanged and still admits: there is no wall on such a deployment, so there is + nothing for an organization-less key to be walled out of. A `tenancy` service + that was registered and **failed to build** is an outage and answers `503`, not + an admission — a posture that could not be read is not a posture that is absent. + + Refusals are now also said out loud on the server side, at `warn`, where each + one is decided: the key's row id (never the credential or its hash), the + principal, the organization and the reason. **The wire is unchanged** — both + refusals still answer the generic `401 UNAUTHENTICATED` with no reason in the + body, so a holder of someone else's key learns nothing a plain 401 does not + already tell them. The operator, who previously had a key that was neither + revoked nor expired and a 401 that said nothing, now has a line to find. + + Behaviour that does not move: a current member's key on the same route still + returns its rows and still writes; a request with no credential still answers + 401; and an unknown, revoked or expired key is not a refusal at all, so a key + scanner produces no log volume. +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [2bb0614] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/platform-objects@17.4.0 + - @objectstack/types@17.4.0 + - @objectstack/service-package@17.4.0 + - @objectstack/metadata-core@17.4.0 + - @objectstack/observability@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/rest/package.json b/packages/rest/package.json index 8c70d43272..8b3e1c8524 100644 --- a/packages/rest/package.json +++ b/packages/rest/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/rest", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "ObjectStack REST API Server - automatic REST endpoint generation from protocol", "type": "module", diff --git a/packages/runtime/CHANGELOG.md b/packages/runtime/CHANGELOG.md index 6804fde7eb..50f22ec48d 100644 --- a/packages/runtime/CHANGELOG.md +++ b/packages/runtime/CHANGELOG.md @@ -1,5 +1,297 @@ # @objectstack/runtime +## 17.4.0 + +### Minor Changes + +- 2c753fe: feat(runtime): a flow action's run context now carries `recordLoadDenied` (#15168) + + The previous release declared `AutomationContext.recordLoadDenied?: true` and + said so plainly: **declared, not yet populated on the flow face.** The + script/body face of both action doors emitted the signal, but + `dispatchFlowAction` handed `automation.execute` a context without it, so a + `runAs: 'system'` flow that guarded on the documented key was inert — never + `true`, never wrong, and indistinguishable from a flow whose caller could read + the row. + + **This release populates it, on both doors in one stroke** — REST + `POST /api/v1/actions/...` and the MCP `run_action` bridge: + + ```js + // a runAs:'system' flow, guarding before it acts on the subject row + if (context.recordLoadDenied === true) { /* the invoker cannot read this row */ } + ``` + + - **The exact producer shape, unchanged.** The one shared producer + (`loadActionSubjectRecord` → `actionRecordLoadSignal`) already returns + `{ recordLoadDenied?: true }`, and the flow door now spreads it as a + **sibling of `record`** — never a key on the record, and **absent**, never + `false`, when nothing was refused. So a flow reads it exactly as a handler + does, `recordLoadDenied === true`. + - **Both doors, structurally.** `dispatchFlowAction`'s wiring now takes the + load OUTCOME (`subject`) instead of a bare `record`, and derives both the + record and the signal from it. A caller can no longer forward the row while + dropping the verdict that says the caller could not read it — the omission is + a compile error rather than a guard silently inert one door over, which is + the defect the handler-face signal was filed for. + - **Purely additive.** Nothing is refused that was not refused before, no + existing key changes value, and the `recordId` stamp is deliberately kept: + `record.id` still arrives exactly as it did, which is why the flag — and not + `record.id` — is the authorization predicate. Whether the automation engine + *acts* on the key (a flow-level refusal, a step condition) is a separate + decision and is deliberately not part of this change. + - **`@objectstack/spec` (docs only).** The contract's "not yet populated on the + flow face" sentence is retired; no type changes. +- 8a12067: feat(runtime): the platform action route executes the declarative row-level `operation: 'update'` action (#14092) + + The spec half (#15077) made `operation: 'update'` + `patch` parse; nothing performed the + write, so an authored update action reached the action route with no handler and collected + the registry's loud not-registered answer. It now performs the write. + + `POST /api/v1/actions///` — and the MCP `run_action` bridge, through + the same shared executor — performs exactly ONE data-plane update of the current record: + + - **As the caller.** The write carries the caller's own `ExecutionContext`, never the + `isSystem`-elevated context a `type: 'script'` BODY runs under. There is no author body here + to trust, so the data plane's own gate is the only gate — the object's permissions, its hooks + and its validations fire exactly as for a user edit, and their refusals reach the caller with + their own `code` and `status`. This consumes the `runAs: 'user'` direction ruled on #14010; no + `runAs` key is added. + - **A caller who cannot read the row is refused before anything is written** (404 + `RECORD_NOT_FOUND`, the platform's one existence-non-disclosing envelope), by consuming the + caller-scope load's verdict rather than re-deriving it from the stamped `record.id` — the + #14143 class: a swallowed load must never become an implicit grant. + - **The write is `{ ...patch, ...collectedParams }`** — static values under the dialog's, so a + param of the same name wins. Nothing else from the action is merged, and the ADR-0104 D2 param + contract still bounds what the wire can add. + - **No current record ⇒ a located refusal**, never a silent no-op: no `recordId` on the route or + in the body, an action addressed at the object-less key, or an empty write bag each answer 400 + naming the action and the fix. + - **`undoable: true`** returns `undo: { type, objectName, recordId, undoData, redoData }` — the + prior values of exactly the fields written, `null` for a field the row did not carry, so the + existing Undo readers can restore. The three remaining `UndoableOperation` keys (`id`, + `timestamp`, `description`) stay the client's. + - `visible` is deliberately unread here: it is a per-record renderer predicate, and the + authorization is the point above. + + `operation` is read BEFORE `type` at every reader, so the HTTP door and the MCP bridge agree: + `isHeadlessInvokableAction` now accepts a declarative update (it has neither `target` nor `body` + by construction), `headlessActionTypeError` hands it no client-side-type prescription, and + `summarizeAction` reports `operation` and `requiresRecord: true`. + + Unchanged: a handler-less `type: 'script'` action WITHOUT `operation` still gets today's + not-registered 404 — the script path is not widened. + +### Patch Changes + +- 98191d2: fix(runtime): a flat-manifest bundle no longer collects every seed dataset twice + + `AppPlugin.start()` collects seed data from two locations — the top-level + `data` field, then the legacy `manifest.data` for backward compatibility. The + legacy read resolves its base as `this.bundle.manifest || this.bundle`, so on a + FLAT bundle — manifest fields written directly on the bundle rather than nested + under `manifest:`, a shape `AppPlugin` supports by design and this repo's own + tests construct — it re-read the very array the top-level read had just + contributed. Every dataset landed in the collection twice. + + `mergeSeedDatasets` is a plain `push` with no de-duplication, so both copies + reached the shared `seed-datasets` registry, the inline boot seed, and every + later per-org replay. For an `upsert` dataset with an `externalId` the second + pass is idempotent and the cost is doubled work; for a `mode: 'insert'` dataset + it is the dataset APPLIED TWICE per boot — measured here as two `insert` calls + for one record. + + The legacy read now carries the same reference guard its sibling collector has + always carried: `loadTranslations()` performs the identical two-location read + and skips the legacy half when `manifest.translations` IS the array the top + level already contributed. That asymmetry between the two collectors was the + whole defect, so the repair is the sibling's guard rather than a third spelling + of the same idea. + + ⛔ Not a removal of the legacy read: a bundle whose `manifest.data` is a + genuinely different array from its top-level `data` still contributes both, and + a bundle that nests its manifest is unaffected either way. Nothing is added to + or removed from any published surface. +- f1a1028: fix(runtime): a multi-package artifact's collections are read from `packages[]`, not only from the flattened top level + + A release artifact composed with `manifest: 'preserve'` carries every + definition twice — flattened at its top level, and again under + `packages[]` (ADR-0130 D4). Only two readers had ever learned the second + half: `ObjectQLPlugin`'s manifest service and the metadata artifact door. + Every other reader said `artifact.` and nothing else, so an + artifact that carried a collection under `packages[]` alone reached them + EMPTY — and nothing threw. The app booted clean having lost its + declarative actions, its scheduled jobs, its seed data, its object routing + or its default permission set. + + `resolveArtifactCollections` — new, and PACKAGE-PRIVATE to + `@objectstack/runtime` — is now the one way this package reads a top-level + collection out of an artifact in either shape. It takes the artifact's own + top-level value first and whole, then adds from each package body — in + `resolveArtifactPackageOrder`'s dependency order — the items the top level + did not already claim. A bundle that carries no `packages[]` is returned + unchanged, by identity: every single-package artifact and every + `defineStack()` config reads exactly as before. Nothing is added to any + package's published surface: `@objectstack/core` is untouched by this + change, and the new module is not named by + `packages/runtime/src/index.ts`. + + Where one collection key is spelled two ways inside one artifact — + `functions` is `z.union([z.record(…), z.array(…)])`, so two packages can + each be schema-valid and disagree — the read is REFUSED with an ADR-0112 + envelope (`MIXED_ARTIFACT_COLLECTION_SHAPE`, 422) rather than one spelling + being skipped. `composeStacks` already refuses the same mix at compose + time for the same reason. + + Taught to use it, in `@objectstack/runtime`: + + - `AppPlugin` — declared datasources and their auto-connect, the + `datasourceMapping` object routing, the objects handed to the connection + service and to the hot-reload seeder, scheduled jobs, seed datasets, + translation bundles, and the ADR-0057 security collections + (`positions` / `permissions` / `capabilities` / `sharingRules`). A job + handler's `ctx.bundle` is now the resolved view too, so + `ctx.bundle.objects` answers on a multi-package artifact. + - `collectBundleActions`, `collectBundleHooks` and + `collectBundleFunctionEntries` — including the object-EMBEDDED actions + that ride on `objects[]` and disappeared with it. + - `mergeRuntimeModule` — the declaration half. The sibling ESM module + re-supplies every callable regardless of shape, so `functions` was not + absent: a function declared `effect: 'writes'` simply came back as a bare + callable and defaulted to `'pure'`. It registered, it ran, and its writes + were counted as none. + - `createStandaloneStack`'s surfaced `requires` / `objects` / + `permissions` / `positions`, which drive the CLI's tier resolution, its + engine and storage-driver auto-registration, and the ADR-0056 D7 default + permission set. + - `resolve-project-database`'s project-database tier, which opens the + artifact itself and runs before any stack exists (`os dev`, `os start`, + `os db clean`). Without this a multi-package project silently fell + through to the unified default database instead of the datasource it + declared. + + Nothing about what the platform EMITS changes: `composeStacks` and the + artifact format are untouched, and the flattened top level is still + written. This is the reader half of the option-B program (#14512). +- ee32e1c: fix(runtime): a sandboxed hook body no longer launders an untouched `readonly` field onto the row + + A `beforeUpdate`/`beforeInsert` body running in the sandbox made the engine believe it had + written payload keys it never named, and a `readonly` field the caller supplied then survived + the readonly strip and landed. Measured end to end: with `locked_at` declared + `{ type: 'datetime', readonly: true }` and seeded to `2020-01-01`, a caller sending + `locked_at: new Date('2099-12-31…')` alongside a body whose whole source is + `ctx.input.touched_by = 'hook'` stored the caller's 2099 value — while the same object's + readonly `text` field was correctly stripped in the same request. + + The cause was a comparison of unlike things. The write-back decides whether a body wrote + *through* an object-valued key by comparing the host payload value against the VM's exit dump, + and the dump has been through `JSON.stringify`/`JSON.parse` while the host value has not. A + `Date` therefore never compared equal to its own ISO projection, took the documented + "cannot prove equal ⇒ carry it back" path, and was re-asserted onto the proxy that records + which keys a hook wrote. The class was every object-valued value a JSON round-trip cannot + prove equal — an object carrying an `undefined` member included, a `Date` being only its most + reachable member. + + The entry value is now normalised through the same round-trip the VM saw before it is + compared. The same change ends a fidelity loss on non-readonly fields: an untouched key is no + longer carried at all, so a host `Date` is no longer replaced by an ISO string on its way to + the driver. + + Fail-open behaviour is unchanged for values the round-trip genuinely cannot evaluate: a cyclic + or bigint-bearing payload value is still reported as changed and still carried, per key. +- 8744de9: The package-publish seed read-back no longer runs a two-attempt org-then-env ladder whose rungs resolve the same row. + + `applyPublishedSeeds` — the route-level seed apply behind `POST /packages/:id/publish-drafts`, which runs for protocols that do not self-apply seeds inside `publishPackageDrafts` — read each just-published `seed` body twice when the session had an active organization: once naming the organization, then once env-wide. The comment above it said the first attempt tried the active org and the second fell back, "and resolving the wrong scope here is what silently produced `0 rows loaded`". + + That was true when it was written and is not true now. `seed` declares `allowOrgOverride: false`, and `getMetaItem` resolves `organizationIdForMetaRead(request.type, request.organizationId)` once at its top and spends that binding — never the raw argument — on every read beneath it. The predicate answers `undefined` for every non-overridable type, so both rungs asked the engine the same predicates and served the same answer. Measured rather than reasoned: against the shipping protocol over one store, the two requests produce byte-identical engine reads and byte-identical answers on both the hit and the miss branch, and neutering the second rung reddens nothing on a pinned publish-then-read path (a `view` control confirms the same comparison does separate the two rungs for an org-overridable type). + + The read is now a single call naming no organization, and the comment states that the scope is decided by the registry flag and the gate inside `getMetaItem` rather than by this call site — matching the sentence the `app` flip in the same file already carries. + + One observable changes, and only on the failure branch: `getMetaItem` answers a wrapper rather than a falsy value for a name it cannot resolve, so the second rung was in practice reached only when the read *threw* — where it repeated the identical failing read and appended the same sentence to the client-facing `seedApplied.errors[]` twice. A failed read-back is now reported once. Nothing about which row a publish resolves, or whether its rows load, moves. +- Updated dependencies [2ed6be6] +- Updated dependencies [54bb2f1] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [a56baa2] +- Updated dependencies [65846bc] +- Updated dependencies [3e3ecb0] +- Updated dependencies [8e500f2] +- Updated dependencies [4b3955e] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [4bc9821] +- Updated dependencies [2003259] +- Updated dependencies [a646120] +- Updated dependencies [a646120] +- Updated dependencies [2200f8e] +- Updated dependencies [a646120] +- Updated dependencies [2200f8e] +- Updated dependencies [65846bc] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [a84e1ce] +- Updated dependencies [a84e1ce] +- Updated dependencies [65846bc] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [3bd9b34] +- Updated dependencies [b4b37e5] +- Updated dependencies [ba426b0] +- Updated dependencies [d0ee598] +- Updated dependencies [61821e5] +- Updated dependencies [26144c2] +- Updated dependencies [9e9f03a] +- Updated dependencies [5eb24f8] +- Updated dependencies [c64e65f] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [e13ede8] +- Updated dependencies [f5cc78b] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [615fac3] +- Updated dependencies [ec0a6e7] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/objectql@17.4.0 + - @objectstack/metadata-protocol@17.4.0 + - @objectstack/driver-sql@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/metadata@17.4.0 + - @objectstack/plugin-auth@17.4.0 + - @objectstack/rest@17.4.0 + - @objectstack/driver-memory@17.4.0 + - @objectstack/driver-turso@17.4.0 + - @objectstack/types@17.4.0 + - @objectstack/service-i18n@17.4.0 + - @objectstack/plugin-security@17.4.0 + - @objectstack/driver-sqlite-wasm@17.4.0 + - @objectstack/service-cluster@17.4.0 + - @objectstack/service-datasource@17.4.0 + - @objectstack/formula@17.4.0 + - @objectstack/metadata-core@17.4.0 + - @objectstack/observability@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/runtime/package.json b/packages/runtime/package.json index b34067c451..4907c97aee 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/runtime", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "ObjectStack Core Runtime & Query Engine", "type": "module", diff --git a/packages/sdui-parser/CHANGELOG.md b/packages/sdui-parser/CHANGELOG.md index f9f2b0755c..8b049eb7eb 100644 --- a/packages/sdui-parser/CHANGELOG.md +++ b/packages/sdui-parser/CHANGELOG.md @@ -1,5 +1,7 @@ # @objectstack/sdui-parser +## 17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/sdui-parser/package.json b/packages/sdui-parser/package.json index 734ec9c716..06edc5cf09 100644 --- a/packages/sdui-parser/package.json +++ b/packages/sdui-parser/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/sdui-parser", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "ObjectStack constrained JSX-source → SDUI SchemaNode tree compiler (parse, never execute). Isomorphic, zero React. ADR-0080.", "main": "dist/index.js", diff --git a/packages/services/service-analytics/CHANGELOG.md b/packages/services/service-analytics/CHANGELOG.md index 6f4d7ca671..4394b6f7ae 100644 --- a/packages/services/service-analytics/CHANGELOG.md +++ b/packages/services/service-analytics/CHANGELOG.md @@ -1,5 +1,68 @@ # Changelog — @objectstack/service-analytics +## 17.4.0 + +### Minor Changes + +- 54bb2f1: The analytics SQL compilers compile the case-sensitive text family per dialect, so a `$contains` policy on SQLite stops admitting rows it excludes (#15684) + + `$contains` / `$notContains` / `$startsWith` / `$endsWith` are case-SENSITIVE on every backend (#4706 Q2 = A). All three of `service-analytics`' SQL compilers emitted `col LIKE ? ESCAPE ?` on every dialect, and SQLite's `LIKE` folds ASCII case unconditionally — the fold cannot be turned off per statement, because `PRAGMA case_sensitive_like` is a connection-global switch. Measured on sql.js over the shared `FILTER_TEXT_ROWS` fixture, `{ name: { $contains: 'acme' } }` answered `['1','2']` — `ACME Corp` **and** `acme corp` — where `FILTER_TEXT_CASES` says `['2']`. + + On two of the three compilers that is a wrong chart. The third is `read-scope-sql.ts`, the ADR-0021 D-C read scope: a scope that **admits** rows the policy's case-sensitive predicate excludes is over-reach, not a loose filter — the same reading that file already applied to its own `LIKE` escaping. The `/analytics/sql` echo was wrong in a third way: it printed `LIKE` while the statement it claims to reproduce ran through a driver that has emitted `GLOB` on the SQLite dialects since #6518. + + What changed: + + - **The construct is chosen per dialect** (`text-match-sql.ts`), arm for arm with `driver-sql`'s own table: `GLOB` on SQLite (case-exact by definition, with its own `*` / `?` / `[` escaped class and no `ESCAPE` clause), `LIKE` over `CAST(… AS BINARY)` on MySQL, and `LIKE` **unchanged** on Postgres, where it is already exactly the ruled semantics. There is no single construct that is case-exact and parses on all three, so the dialect had to become an input rather than a guess. + - **The dialect arrives from the driver that will execute the statement.** New optional `AnalyticsServiceConfig.sqlDialect`, wired by `AnalyticsServicePlugin` from `IDataEngine.getDriverForObject`. `SqlDriver.dialectName` is now public so that answer can be read without a second dialect-resolution table drifting behind the driver's own knex spellings; it is derived and read-only. + - **A host that answers no dialect keeps the `LIKE` it always got** — "cannot answer, do not block". Postgres deployments see byte-identical SQL. + + `$icontains` is untouched: it keeps its own ASCII-only fold on both sides, and collapsing the two families onto one path would hand the case-exact family back the fold the ruling took away from it. `LIKE` escaping is unchanged wherever a `LIKE` is still emitted. +- a646120: The three SQL compilers in this package — the RLS read-scope lowering (`compileScopedFilterToSql`), `NativeSQLStrategy`'s own `where` and the `ObjectQLStrategy` SQL echo — compile a text operator over a column whose declared type stores no text to the contract's declared answer. + + `compileScopedFilterToSql(filter, alias, options?)` takes a new optional `nonTextColumn(field)` predicate; when it answers `true`, a positive text operator compiles to `1 = 0` and `$notContains` to `1 = 1` instead of a `LIKE` that coerces on SQLite (`5` renders `'5.0'`) and is refused at query time on Postgres (SQLSTATE 42883 — a 500 on a read scope the platform accepted). The service answers the predicate from the field metadata hook it already holds (`sourceFieldMeta`), exposed to strategies as `DatasetScopedStrategyContext.declaredFieldType`, and the two strategies pass it for the read scope and for the query's own text filters, so a query and its RLS scope answer one cell one way and the echo prints the statement that ran (`FILTER_TEXT_CASES`' `score` rows, maintainer ruling 2026-09-05). A host that wires no field metadata keeps the `LIKE` it always got, and every comparand refusal still runs ahead of the constant. + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/types@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/services/service-analytics/package.json b/packages/services/service-analytics/package.json index 1f5cab0c7d..9423f7b794 100644 --- a/packages/services/service-analytics/package.json +++ b/packages/services/service-analytics/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-analytics", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Analytics Service for ObjectStack — implements IAnalyticsService with multi-driver strategy pattern (NativeSQL, ObjectQL, InMemory)", "type": "module", diff --git a/packages/services/service-automation/CHANGELOG.md b/packages/services/service-automation/CHANGELOG.md index 34ee3022a9..2589a65a72 100644 --- a/packages/services/service-automation/CHANGELOG.md +++ b/packages/services/service-automation/CHANGELOG.md @@ -1,5 +1,229 @@ # @objectstack/service-automation +## 17.4.0 + +### Minor Changes + +- 954cb0b: feat(service-automation): an `assignment` value may be a CEL envelope — evaluated at run time, validated at `registerFlow`, `objectstack validate` and the runtime publish gate (#15137, the executor half of #14149) + + + + **BREAKING** in the accept-set sense, landing in the launch window as `minor` + (the lockstep convention; the level also follows the 2026-09-04 bump ruling — + this adds `AutomationEngine.evaluateValueEnvelope` to a published surface, and an + additive widening is at least `minor`). No ADR-0087 conversion: no authorable key + is renamed or retired, and the shape this refuses was never a shape any surface + offered. + + The maintainer's 2026-09-02 ruling on #14149 made an assignment value able to be + a CEL **value** expression, so the declared stdlib (`joinNonEmpty`, `map`, `size` + …) is finally reachable from metadata — until now CEL was only ever asked for a + boolean. The spec half landed the contract (PR #15113); this is the half that + makes it do something. + + ```yaml + # before: written into the variable verbatim, and rendered by `notify` as + # {"dialect":"cel","source":"joinNonEmpty(...)"} + # now: evaluated — digest is "Renewal due\nInvoice overdue" + assignments: + digest: { dialect: cel, source: 'joinNonEmpty(rows.map(r, r.subject), "\n")' } + ``` + + - **Evaluated at run time.** The built-in `assignment` executor evaluates a + `value`-role envelope with the expression engine and assigns the result, in the + same CEL scope a flow predicate is evaluated in (one shared scope builder, so a + predicate and a value expression cannot disagree about what `rows` means). A + plain string keeps today's `{token}` interpolation, and every other literal is + still assigned as data. + - **Refused at three doors.** A malformed envelope now stops the flow registering + (`registerFlow` throws, the severity a malformed predicate gets) and surfaces as + a located `error` finding naming the node and the author's own variable — + `config.assignments.digest` — both at `objectstack validate` and at the runtime + publish gate a Studio / REST / MCP flow write goes through + (`validateStackExpressions` is registered `CLI_AND_RUNTIME`, `runtimeTypes: + ['flow']`). Malformed is a composition, not a fixed list: whatever + `AssignmentValueSchema` refuses in the envelope's shape — among them a missing, + empty or non-string `source`, a dialect other than `cel`, a non-object `meta` — + and then CEL that does not parse. All three doors derive that set from the same + two published validators, so none refuses a shape the executor would have run, + and a registered flow never faults for a shape those validators judge malformed. + Two shapes sit outside what either validator can judge — an `ast`-only envelope + and a whitespace-only `source` (it passes `min(1)` and reads as "not authored" + to the validator, while the CEL engine parses it untrimmed) — and those fault + loudly at run time rather than assigning a value. Both are pinned and tracked in + #15430. + - **Only the canonical map.** The ledger declares `assignment.assignments.*` and + nothing else, so the two legacy shapes the executor still normalizes — the + `assignments: [{ variable, value }]` array and the bare `{ : }` + config — keep every meaning they had, envelope-shaped values included. + `AssignmentConfigSchema` is deliberately NOT wired into `parseNodeConfig` for the + array form: refusing it would break flows that register today, and that refusal + is a maintainer ruling rather than a lane's call (#15137 ask 3). + + **What changes silently, and how far it reaches.** A flow that today authors an + envelope-shaped object *as data* in the canonical `assignments` map now evaluates + it — no error on either side, a different value. The discriminator is the spec's + own `isExpressionEnvelopeShaped`: a plain object naming a **string** `dialect`, + in the declared map only. Data that names no `dialect`, names a non-string one, + nests the envelope one level down, or sits in either legacy shape is untouched + and byte-identical. The remaining overlap — a well-formed + `{ dialect: 'cel', source: … }` written as data in the canonical map — is exactly + the spelling the ruling reinterprets; every near-miss the two validators can + judge now refuses loudly at registration instead of changing value in silence. +- d30ccb9: A contained per-iteration failure is now visible at run level, attributed to its iteration, and bound to its row. + + `loop { body: [ try_catch { try, catch } ] }` is the containment spelling for a per-iteration failure that must not end the sweep (there is deliberately no `loop.config.onIterationError` key). Containment already worked — the failure was caught, the loop went on and the run completed — but nothing said what it had contained: a sweep that lost two rows out of five reported `status=completed selected=5 acted=9 skipped=0` and was indistinguishable from one that lost none. The failure was in the step log and in `nodes[].failures`; no run-level number carried it, the failing step named no row, and `$error` bound no row identity. + + Four changes populate the contract `@objectstack/spec` already declares: + + - **`FlowRunSummary.failed`** — `summarizeRun` now folds `failed = Σ nodes[].failures` over the per-node array it publishes, so the run-level count can never disagree with the breakdown it summarizes. It counts every node execution that failed, contained or fatal; on a run that completed, all of them were contained. + - **`failed=N` on the run summary line** — `formatRunSummaryLine` prints the token whenever the count is present, `failed=0` included. That is the opposite of the `unmeasured` rule beside it and deliberate: `unmeasured` qualifies `acted`, while `failed` answers a question a completed run's line otherwise cannot be asked at all. Read `failed=0` precisely: **no node execution of this run failed**. It is the node fold and only that, so a `subflow` child's own contained failures stay on the child's summary rather than rolling up the way `acted` does — see #15617, where the declaration's two paragraphs are being reconciled. + - **Iteration through `try_catch`** — a step that ran in a `try` or `catch` region inside a loop body now carries the enclosing loop's `iteration`, with `regionKind` still `try` / `catch`. The step says which region ran it *and* which row it ran for. `parallel` branch tagging is unchanged. + - **`$error` binds the row** — the value bound to `errorVariable` (default `$error`) is the declared `TryCatchErrorValue`: `nodeId` and `message` as before, plus `iteration` and the loop's current `item` when the failure happened inside a loop body. A `subflow` / `map` child run has its own variable scope and therefore binds neither, so a parent's row identity never leaks into a child's `$error`. + + **`failed` absent means "not tracked", never `0`.** Runs recorded before this change keep it absent — no migration and no default, the same convention `unmeasured` carries. Defaulting it to zero would tell an operator "nothing failed" about a run nobody measured. Absent, the summary line prints no `failed=` token at all; present-and-zero prints `failed=0`. The count rides in the persisted `summary_json`, including on a summary compacted past the size cap, where the per-node `failures` it folds are exactly what gets dropped. +- 56fe8c2: A flow predicate authored as a CEL envelope is now refused at build time, instead of running unread by either validator. + + A `predicate`-role expression slot holds **bare CEL text** — `DecisionConditionSchema.expression` is declared `z.string()`, and so is a screen field's `visibleWhen`. An author who instead wrote the `{ dialect, source }` expression *envelope* there reached a shape nothing could see: a flow node's `config` is an open `z.record(z.unknown())` that no Zod schema is parsed against, the unknown-key walk exempts the schemaless node types on purpose (`decision` publishes no descriptor `configSchema`), and the expression ledger's `predicate` arm skipped every non-string as "a type violation for the schema pass to report" — a schema pass that, for those node types, does not exist. `registerFlow` accepted the flow, `objectstack validate` reported nothing, and the evaluator was the only layer that ever read the predicate. + + - `resolveFlowNodeExpressions` now emits a non-string sitting in a `predicate` slot, and the new `predicateSlotRefusal` / `PREDICATE_SLOT_STRING_REFUSAL` say why it is refused — one notion, derived once, read by both validators so build time and author time cannot disagree about the shape. `flow-template` slots keep the old rule: no validator implements that dialect, so a finding there is one nobody could judge. + - `registerFlow` throws, naming the node, the slot and the index, and attributing the finding to the envelope's own `source`. `objectstack validate` reports the same refusal as a located `error`. + + **String predicates are untouched, deliberately.** A whitespace-only string still means "not authored" on both sides, exactly as before; what a non-empty string *says* is still judged by `validateExpression('predicate', …)`, brace trap and all. Only the shape moved. + + An app that authored an envelope in one of these slots now fails to register with a message naming the slot; the fix is to write the predicate as bare CEL text (`record.rating >= 4`). The `{ dialect, source }` envelope remains the `value`-role spelling, on the `assignment` node's `assignments` map. +- 5964124: feat(automation): a resume that consumed the pause and then failed downstream answers `status: 'stranded'` (#13937) + + The services half of the #13937 shape-4 ruling (maintainer 2026-09-01): + `resumeInternal`'s consumption order is kept — the suspension is consumed + before downstream nodes run, which is what buys exactly-once across a crash — + and the state that order leaves behind when a downstream node throws now + carries the platform-level name #14384 put on the contract. + + `AutomationEngine.resume()` (and every engine continuation that reaches the + same catch arm) returns `{ success: false, status: 'stranded', … }` where it + returned no `status` at all. Stamped on that one exit only: the pause a + durable decision was waiting on is gone, the run is recorded `failed`, and it + can be re-armed only by the explicit operator verb + `restoreConsumedSuspension` (#13909 slice 2, already published) — never by + `resume` (which answers `RUN_NOT_FOUND`) and never automatically. Distinct + from `'failed'` on purpose: that one says the run ran and was rejected; this + one says a recorded continuation stopped mid-flight and an operator has + something to repair. The result's verdict and the restore verb are held to + agree by test: a stranded result is exactly a restorable run. + + Not changed: the run's RECORDED status (the run log, `getRun`, `listRuns`, the + durable `sys_automation_run` history row) stays `failed` — that vocabulary is + `ExecutionStatus` in `@objectstack/spec`, which the ruling did not widen; the + durable discriminator for the condition remains the snapshot the terminal row + carries. No resume semantics move for any pausing node type; shapes 2 and 3 + of the decision stay excluded. + + Also in this change, under the same ruling's exactly-once guarantee, two + repairs to how `restoreConsumedSuspension` finds a stranded run's snapshot: + + - The durable run-history row of a stranded run now records the PAUSE node in + `node_id`. It recorded the node that threw — the run's last step — and the + object store read that column back as the snapshot's node, so a restore + from the row (after a restart, or on another replica) re-armed the run at + the failed node and the next resume skipped it while reporting the run + completed. The throwing node stays in the row's step log and `error`. + Visible on the Runs surface: `sys_automation_run`'s row title and highlight + set are built from `node_id` (`titleFormat '{flow_name} · {node_id}'`), so a + stranded run's row now names the PAUSED node — the one an operator can + re-arm — where it named the node that threw; ordinary completed / failed + rows are unchanged. The `node_id` and `variables_json` field descriptions + carry this carve-out, the way `node_type`'s already did. + - The verb reads the durable row and its own per-process journal as two + witnesses of one strand instead of trusting either alone. The hot copy is + preferred when both describe the same pause (it is the verbatim object the + failure was journalled from). A row that carries no snapshot is read as + "the run moved on" only when this process's own history write landed — + the replica that stranded a run used to keep a hot copy that could re-arm + the run after another replica had restored, resumed and finished it, and + the next resume re-ran every node after the pause. A snapshot the object + store could not persist (over its 256 KiB row budget) is now recorded in + the row as dropped, with the pause it belonged to, so the replica holding + the hot copy still restores and any other replica is refused with a reason + that names the budget and the remedy. + + In-memory and store-less deployments observe no behaviour difference. On the + object store, same-replica restores re-arm the pause node on every path, and + restores from the row alone do too; restores across replicas of a run that + finished elsewhere are refused. +- 9408b7f: A flow condition that is neither CEL text nor an expression is now refused at build time, instead of being read as an empty condition and answering a silent `false`. + + `evaluateCondition` derives its source as `typeof expression === 'string' ? expression : (expression?.source ?? '')`. For a value that is neither — a number, a boolean, an array — the read yields `undefined`, the `??` supplies `''`, and the empty-source arm returns **`false`**: the "an unauthored branch must not open" rule, applied to a value that was very much authored. Measured: a `decision` node carrying `config: { condition: 42 }` **registered clean** and executed `success: true` with nothing said at any layer; `{ source: 1 }` did not even get that far and threw a bare `TypeError: exprStr.trim is not a function` out of the validator. `config.condition` is also the key a **start node's trigger gate** is read from, so the same value could gate a whole flow shut forever with no signal to the author. + + - The new `structuralConditionRefusal` / `STRUCTURAL_CONDITION_SHAPE_REFUSAL` in `@objectstack/spec/automation` are the single shared notion of why, read by both validators so build time and author time cannot disagree about the shape. `registerFlow` throws, naming the node or edge and attributing the finding; `objectstack validate` reports the same refusal as a located `error`. + + **This is deliberately NOT the `predicate`-slot rule, and the difference is measured.** A ledger `predicate` slot (`decision.conditions[].expression`, a screen field's `visibleWhen`) is declared `z.string()`, so `PREDICATE_SLOT_STRING_REFUSAL` refuses every non-string including an envelope. Neither structural slot is declared that way: `FlowEdgeSchema.condition` is `ExpressionInputSchema`, whose string arm **transforms into** `{ dialect: 'cel', source }` — so after `FlowSchema.parse` every authored edge condition *is* an envelope — and `FlowNodeSchema.config` is an open `z.record` that passes an envelope written at `config.condition` through verbatim, where `evaluateCondition` evaluates it correctly. Both shapes stay accepted here; an envelope with no `dialect`, and an `ast`-carrying one (`ExpressionSchema`'s own `source`-or-`ast` rule), stay accepted too. + + **Strings are untouched, deliberately.** A whitespace-only condition still means "not authored" and still answers `false` on both sides — consistent behaviour, ruled correct, not a defect. What a non-empty string *says* is still `validateExpression('predicate', …)`'s verdict, brace trap and all. Only the shape moved. + + An app that authored a number, a boolean, an array or a source-less object in a node or edge `condition` now fails to register with a message naming the site; the fix is to write the condition as bare CEL text (`record.rating >= 4`) or as an expression envelope. + +### Patch Changes + +- 7bf96cf: A `map` node inside a `loop` body now runs its collection on every iteration, not just the first. + + `map` tracks its progress through the collection in the flow variable `.$mapState`, and wrote it into the flow's **shared** variable scope without ever removing it. A `loop` body region runs in that same scope by construction — that is what makes the iterator variable and the body's mutations visible to the rest of the flow — so the state written by iteration 1 was still there when iteration 2 entered the map. It read back `started === collection.length`, correctly concluded there was nothing left to start, and returned. + + The result was silent partial work reported as success: measured on the engine, **5 iterations x 2 items produced 2 child runs instead of 10**, the map step reported `success` on all five iterations, and the run finished `completed`. Nothing threw and nothing was caught, so `FlowRunSummary.failed` — the run-level counter that exists to expose contained failures — reported `failed = 0` over it. An operator reading that counter was told the run was clean while it had done a fifth of its work. + + The fix is a lifetime correction, not a new key: `$mapState` is now removed once the collection is exhausted, so its lifetime is one execution of the collection rather than the enclosing scope's. + + **The durable-pause path is deliberately unchanged.** A `map` whose per-item subflow pauses still writes its progress before suspending, and still reads it back when the engine re-enters the node — that write is the mechanism resume depends on, because a resume rebuilds the variable scope from the snapshot taken at the suspend and so can never see any later write. Only the node's terminal path clears the key. A `map` resumed mid-collection continues where it left off, exactly as before, and no item is re-run. +- 6c439f2: Flow templates: `{TODAY() + n}` and `{TODAY() - n}` now do their day arithmetic on the same calendar they render on (UTC), so the resolved date no longer lands a day off across a DST transition. + + The offset branch of the template resolver shifted the day on the **local** calendar (`getDate` / `setDate`) and then rendered the result on the **UTC** one (`toISOString`). `setDate` preserves wall-clock time, so a local day shift moves the underlying instant by exactly n x 24 hours only while every local day in the window is 24 hours long. Across a spring-forward the window is 23 hours and across a fall-back 25, and when that one hour of slack crosses a UTC midnight the rendered date comes out a day early (spring-forward) or a day late (fall-back). + + The window is narrow — roughly one hour per DST-observing zone, twice a year — but the values written through it persist: a quote expiration, a follow-up date, a close date. Measured across 34 zones at every 30 minutes of 2026 for offsets `+1` and `-1` (1,191,360 instant-offset pairs), the old spelling disagreed with the UTC day in 190 of them, spread over 24 DST-observing zones; the new spelling disagrees in none. + + The same branch serves `{NOW() + n}`, which likewise now moves the instant by exactly n x 24 hours instead of preserving a wall-clock time across the transition. + + Nothing else moves. The bare `{TODAY()}` and `{NOW()}` forms never entered this branch and are byte-for-byte unchanged — they already resolved on UTC, and the offset forms now agree with them. This is not a timezone feature: these tokens remain timezone-unaware by design, and whether they should be is a separate question. +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [2bb0614] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/platform-objects@17.4.0 + - @objectstack/formula@17.4.0 + - @objectstack/metadata-core@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/services/service-automation/package.json b/packages/services/service-automation/package.json index dbca46d152..6800e28dba 100644 --- a/packages/services/service-automation/package.json +++ b/packages/services/service-automation/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-automation", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Automation Service for ObjectStack — implements IAutomationService with plugin-based DAG flow execution engine", "type": "module", diff --git a/packages/services/service-cache/CHANGELOG.md b/packages/services/service-cache/CHANGELOG.md index a296879af8..17c19632ab 100644 --- a/packages/services/service-cache/CHANGELOG.md +++ b/packages/services/service-cache/CHANGELOG.md @@ -1,5 +1,47 @@ # @objectstack/service-cache +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/observability@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/services/service-cache/package.json b/packages/services/service-cache/package.json index 24975c034d..f92c47d252 100644 --- a/packages/services/service-cache/package.json +++ b/packages/services/service-cache/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cache", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Cache Service for ObjectStack — implements ICacheService with in-memory and Redis adapters", "type": "module", diff --git a/packages/services/service-cluster-redis/CHANGELOG.md b/packages/services/service-cluster-redis/CHANGELOG.md index 11f8eb3ae6..65a0c5f5f9 100644 --- a/packages/services/service-cluster-redis/CHANGELOG.md +++ b/packages/services/service-cluster-redis/CHANGELOG.md @@ -1,5 +1,41 @@ # @objectstack/service-cluster-redis +## 17.4.0 + +### Patch Changes + +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/spec@17.4.0 + - @objectstack/service-cluster@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/services/service-cluster-redis/package.json b/packages/services/service-cluster-redis/package.json index e906029708..eb03afe5bc 100644 --- a/packages/services/service-cluster-redis/package.json +++ b/packages/services/service-cluster-redis/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cluster-redis", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Redis cluster driver for ObjectStack — implements IPubSub/ILock/IKV/ICounter against Redis using ioredis.", "type": "module", diff --git a/packages/services/service-cluster/CHANGELOG.md b/packages/services/service-cluster/CHANGELOG.md index 15e0461343..4c341dc225 100644 --- a/packages/services/service-cluster/CHANGELOG.md +++ b/packages/services/service-cluster/CHANGELOG.md @@ -1,5 +1,46 @@ # @objectstack/service-cluster +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/services/service-cluster/package.json b/packages/services/service-cluster/package.json index b9494cc0a4..befd8fb6df 100644 --- a/packages/services/service-cluster/package.json +++ b/packages/services/service-cluster/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-cluster", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Cluster Service for ObjectStack — pluggable PubSub/Lock/KV/Counter primitives. Memory driver included; postgres/redis drivers ship separately.", "type": "module", diff --git a/packages/services/service-datasource/CHANGELOG.md b/packages/services/service-datasource/CHANGELOG.md index 7332dd4768..aa9f2f214f 100644 --- a/packages/services/service-datasource/CHANGELOG.md +++ b/packages/services/service-datasource/CHANGELOG.md @@ -1,5 +1,63 @@ # @objectstack/service-external-datasource +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [54bb2f1] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [2003259] +- Updated dependencies [a646120] +- Updated dependencies [a06faeb] +- Updated dependencies [a646120] +- Updated dependencies [2200f8e] +- Updated dependencies [a646120] +- Updated dependencies [2200f8e] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [61821e5] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/driver-sql@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/driver-memory@17.4.0 + - @objectstack/driver-mongodb@17.4.0 + - @objectstack/driver-turso@17.4.0 + - @objectstack/types@17.4.0 + - @objectstack/driver-sqlite-wasm@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/services/service-datasource/package.json b/packages/services/service-datasource/package.json index 9ca907fce8..4194f355e4 100644 --- a/packages/services/service-datasource/package.json +++ b/packages/services/service-datasource/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-datasource", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "The datasource service (ADR-0015): external-table federation (introspect/draft/import/validate) + runtime UI datasource lifecycle (list/test/create/update/remove + REST routes). Open-source mechanism; the tier line falls on which ICryptoProvider / driver factory a host injects.", "type": "module", diff --git a/packages/services/service-i18n/CHANGELOG.md b/packages/services/service-i18n/CHANGELOG.md index 76f67d400f..1be5cb058f 100644 --- a/packages/services/service-i18n/CHANGELOG.md +++ b/packages/services/service-i18n/CHANGELOG.md @@ -1,5 +1,59 @@ # @objectstack/service-i18n +## 17.4.0 + +### Minor Changes + +- a84e1ce: feat(service-i18n): `FileI18nAdapter.getFallbackLocale()` reports the `fallbackLocale` the adapter was constructed with (#14882) + + Implements the new optional `II18nService.getFallbackLocale()`. `I18nServicePlugin` + already receives `fallbackLocale || defaultLocale || 'en'` from the stack's `i18n` + config on both boot paths (`os serve`, the dev plugin); this makes that declaration + readable, so the REST metadata reads pass the document translators the same fallback + locale `t()` itself consults. Returns `undefined` when no `fallbackLocale` was given. + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/types@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/services/service-i18n/package.json b/packages/services/service-i18n/package.json index 870371fc27..c118fb2f22 100644 --- a/packages/services/service-i18n/package.json +++ b/packages/services/service-i18n/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-i18n", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "I18n Service for ObjectStack — implements II18nService with file-based locale loading", "type": "module", diff --git a/packages/services/service-job/CHANGELOG.md b/packages/services/service-job/CHANGELOG.md index 857d07754e..f132075997 100644 --- a/packages/services/service-job/CHANGELOG.md +++ b/packages/services/service-job/CHANGELOG.md @@ -1,5 +1,48 @@ # @objectstack/service-job +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [2bb0614] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/platform-objects@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/services/service-job/package.json b/packages/services/service-job/package.json index 5aa0972418..440dc2ab4e 100644 --- a/packages/services/service-job/package.json +++ b/packages/services/service-job/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-job", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Job Service for ObjectStack — implements IJobService with setInterval and cron scheduling", "type": "module", diff --git a/packages/services/service-knowledge/CHANGELOG.md b/packages/services/service-knowledge/CHANGELOG.md index 0356f7dbd9..be5762be51 100644 --- a/packages/services/service-knowledge/CHANGELOG.md +++ b/packages/services/service-knowledge/CHANGELOG.md @@ -1,5 +1,46 @@ # @objectstack/service-knowledge +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/services/service-knowledge/package.json b/packages/services/service-knowledge/package.json index 2c946d4056..0d00598724 100644 --- a/packages/services/service-knowledge/package.json +++ b/packages/services/service-knowledge/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-knowledge", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Knowledge Service for ObjectStack — orchestrator implementing IKnowledgeService over pluggable IKnowledgeAdapter backends (RAGFlow, LlamaIndex, Dify, in-memory).", "type": "module", diff --git a/packages/services/service-messaging/CHANGELOG.md b/packages/services/service-messaging/CHANGELOG.md index 0836d1f2d7..0c5273c7ae 100644 --- a/packages/services/service-messaging/CHANGELOG.md +++ b/packages/services/service-messaging/CHANGELOG.md @@ -1,5 +1,51 @@ # @objectstack/service-messaging +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [2bb0614] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/platform-objects@17.4.0 + - @objectstack/types@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/services/service-messaging/package.json b/packages/services/service-messaging/package.json index 6cb7ae7101..928d1655d0 100644 --- a/packages/services/service-messaging/package.json +++ b/packages/services/service-messaging/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-messaging", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Messaging Service for ObjectStack — outbound notification dispatch (ADR-0012). Ships the MessagingChannel registry, emit() fan-out, and the always-on inbox channel; other channels (email/webhook/push/IM) plug in.", "type": "module", diff --git a/packages/services/service-package/CHANGELOG.md b/packages/services/service-package/CHANGELOG.md index 356f030132..6ac1d45969 100644 --- a/packages/services/service-package/CHANGELOG.md +++ b/packages/services/service-package/CHANGELOG.md @@ -1,5 +1,47 @@ # @objectstack/service-package +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/metadata-core@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/services/service-package/package.json b/packages/services/service-package/package.json index 62734b900e..b64894c8da 100644 --- a/packages/services/service-package/package.json +++ b/packages/services/service-package/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-package", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Package management service for ObjectStack — publish, install, and manage packages", "type": "module", diff --git a/packages/services/service-queue/CHANGELOG.md b/packages/services/service-queue/CHANGELOG.md index 4eb39e12fe..b365127998 100644 --- a/packages/services/service-queue/CHANGELOG.md +++ b/packages/services/service-queue/CHANGELOG.md @@ -1,5 +1,48 @@ # @objectstack/service-queue +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [2bb0614] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/platform-objects@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/services/service-queue/package.json b/packages/services/service-queue/package.json index 7915c9cca5..56061c80e0 100644 --- a/packages/services/service-queue/package.json +++ b/packages/services/service-queue/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-queue", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Queue Service for ObjectStack — implements IQueueService with in-memory and durable DB-backed (sys_job_queue) adapters", "type": "module", diff --git a/packages/services/service-realtime/CHANGELOG.md b/packages/services/service-realtime/CHANGELOG.md index 225ff38c88..5ba4efa154 100644 --- a/packages/services/service-realtime/CHANGELOG.md +++ b/packages/services/service-realtime/CHANGELOG.md @@ -1,5 +1,48 @@ # @objectstack/service-realtime +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [2bb0614] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/platform-objects@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/services/service-realtime/package.json b/packages/services/service-realtime/package.json index d1f8de464e..c9af30dc07 100644 --- a/packages/services/service-realtime/package.json +++ b/packages/services/service-realtime/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-realtime", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Realtime Service for ObjectStack — implements IRealtimeService with WebSocket and in-memory pub/sub", "type": "module", diff --git a/packages/services/service-settings/CHANGELOG.md b/packages/services/service-settings/CHANGELOG.md index df51666809..46bb69b2fe 100644 --- a/packages/services/service-settings/CHANGELOG.md +++ b/packages/services/service-settings/CHANGELOG.md @@ -1,5 +1,116 @@ # @objectstack/service-settings +## 17.4.0 + +### Minor Changes + +- 6b8c677: fix(service-settings): the settings door answers from the ONE shared value-domain predicate, and refuses a non-member with `value_domain` (#15162) + + + + **BREAKING** for a client that branches on the refusal code. Landing inside + the launch window, so it ships as `minor` (the lockstep convention forbids + `major`); the banner is the carrier, not the bump. + + The services half of the maintainer's ruling of 2026-09-02: **one closed + vocabulary and one membership predicate shared by settings specifiers and + object fields**. The spec half declared them in `@objectstack/spec/shared`; + this package had been carrying a second copy of all three definitions since + `Specifier.valueDomain` shipped. The copies are deleted and the door now asks + `isValueDomainMember` — the call the record write path will make when the + engine half of the same ruling lands (PR #15316, still open). + + **The wire change**, measured on `PUT /api/settings/localization` with + `{"timezone": "Mars/Olympus"}`, base `a56baa2bd` vs this branch: + + | | before | after | + |:--|:--|:--| + | `fields[0].code` | `invalid_value` | `value_domain` | + | `fields[0].message` | `Default timezone must be a valid IANA time zone identifier (e.g. 'Europe/Zurich'). Received 'Mars/Olympus'.` | `Default timezone must be a valid IANA time zone identifier, e.g. Europe/Zurich (got "Mars/Olympus")` | + + Everything else is byte-identical: HTTP 400, the envelope code + `SETTINGS_VALIDATION`, `field`, `label`, `constraint: { valueDomain: … }` and + the echoed `value`. A client that reads `constraint.valueDomain` — the + machine-readable half ADR-0114 asks it to read — is unaffected. A client that + branches on `code === 'invalid_value'` for a domain breach must move to + `value_domain`. + + Why the code moved: ADR-0114's rule is that the code is the **constraint's own + name**, the way `max_length` names the bound it breached. This branch took + `invalid_value` — the catalog's slot for "rejected for a reason no other + member names" — only while no member named a standard-domain breach. The + field-level card's spec half added one, so the slot no longer applies. The + message now renders the published catalog template + `value_domain_` in `en` — the catalog the record write path will render + from once PR #15316 lands, so the two doors under one ruling will describe one + domain in one set of words instead of each composing its own sentence. For an `encrypted` specifier the offending value is still never + echoed: the template's value placeholder takes the same mask the REST boundary + uses (`fields[0].value` stays absent, as before). + + **No value changes verdict.** The accept sets were measured, not assumed, on + the repo's Node 22 baseline (v22.22.2): + + - `iso_3166_alpha2` — the two 249-code lists diffed mechanically before either + was deleted: identical, including order; symmetric difference 0. + - `iso_4217_currency` — this one changes DEFINITION: a run-time + `Intl.supportedValuesOf('currency')` probe becomes the key set of the + checked-in CLDR snapshot `CURRENCY_FRACTION_DIGITS`. 162 codes vs 162, + symmetric difference 0 in both directions (`CHF` in both, `XYZ` in neither). + The behaviour that changes is that the verdict no longer varies with the + host's ICU build — the direction the shared module argues for. A door-level + test now re-measures it: every code the run-time probe admits must still be + admitted. + - `iana_time_zone` — the identical `Intl.DateTimeFormat` probe on both sides, + unmoved. + + A ratchet pin (`value-domains.shared-predicate.pin.test.ts`) reddens if any + non-test source in this package re-acquires a membership table, an `Intl` + enumeration probe, or a second caller of the predicate. + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [2bb0614] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/platform-objects@17.4.0 + - @objectstack/types@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/services/service-settings/package.json b/packages/services/service-settings/package.json index 072020d7d3..e0b0225e62 100644 --- a/packages/services/service-settings/package.json +++ b/packages/services/service-settings/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-settings", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Settings service for ObjectStack — manifest registry + K/V resolver (OS_* env > Tenant > User > Default) + REST routes. See ADR-0007.", "type": "module", diff --git a/packages/services/service-sms/CHANGELOG.md b/packages/services/service-sms/CHANGELOG.md index 84e7dd7d4e..0a799e4b00 100644 --- a/packages/services/service-sms/CHANGELOG.md +++ b/packages/services/service-sms/CHANGELOG.md @@ -1,5 +1,49 @@ # @objectstack/service-sms +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [8e500f2] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [9e9f03a] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/plugin-auth@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/services/service-sms/package.json b/packages/services/service-sms/package.json index afaea4ba6d..b432166a24 100644 --- a/packages/services/service-sms/package.json +++ b/packages/services/service-sms/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-sms", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "SMS service for ObjectStack — ISmsService + transport-pluggable outbound delivery (Aliyun / Twilio / log).", "main": "dist/index.js", diff --git a/packages/services/service-storage/CHANGELOG.md b/packages/services/service-storage/CHANGELOG.md index c0b48485bb..e08157f7c0 100644 --- a/packages/services/service-storage/CHANGELOG.md +++ b/packages/services/service-storage/CHANGELOG.md @@ -1,5 +1,98 @@ # @objectstack/service-storage +## 17.4.0 + +### Patch Changes + +- ebb5550: fix(service-storage): put the test layer in front of tsc, and repair what it was hiding (#15050) + + `packages/services/service-storage` had **no `typecheck` script at all** — its + scripts were `build` and `test` — so no tsc program anywhere read this + package's test layer, and its errors were carried instead as a 51-error DEBT + entry in `scripts/check-type-check-coverage.mjs`. Gives it the #14062 / + #14181 "checked test zone" shape: a sibling `tsconfig.test.json` (module + semantics only — `esnext` / `bundler` / `lib: ES2022` — matching how vitest + actually executes these files; strictness inherited and untouched) plus a + `tsconfig.scripts.json` for `scripts/i18n-extract.config.ts` (the ninth + instance of #11351, previously excluded from that ledger only because this + package had no `typecheck` script to hang it on), both named by a new + `typecheck` script. + + Measured before repair: 51 errors under BUILD semantics (`tsc --noEmit -p + tsconfig.json`, which already includes the tests — matching the DEBT entry's + recorded number exactly), 10 under the split. Unlike `service-cluster` + (#14181), this package's BUILD reading was *not* already clean, so both + programs needed genuine repair, not just the test-only split: 23 `TS2835` + (relative imports missing their `.js` extension, required under BUILD's + NodeNext resolution) were fixed by *adding* the extension — which resolves + correctly under both NodeNext and the split's bundler mode — and clearing + that also cleared all 15 `TS7006` "implicitly any" as a downstream cascade + from the same unresolved imports (the shape `@objectstack/core` reported at + 98 → 4). The remaining 3 `TS2550` (`Array.prototype.at` needing `lib` + es2022) are rewritten to indexed access rather than widening the shared + BUILD `tsconfig.json`. The 8 code-tier errors (`TS2339` × 4 — a test + helper's object-spread dropped its `Record` index + signature, fixed with an explicit return-shape annotation; `TS2347` × 4 — a + fake `ctx: any`'s `getService(...)` calls converted to `getService(...) + as T`, the pattern one call site in the same file had already adopted for + exactly this reason) are genuine test-file fixes. Both readings now agree at + 0/0 — the same result `service-cluster` reported, reached by a longer road. + + The package's DEBT entry (51 errors) is **deleted**, not lowered — the + graduation this ratchet's invariant requires. No `test-typecheck-debt.json` + is added: residue is 0, so none is owed (#5286, maintainer-only to open). + `check:type-source-resolution` went red from onboarding the two new + programs (the documented onboarding-limb case): a registry entry is added + rather than `paths`, measured both ways — `paths` takes this package's test + layer from 0 errors to 306, all in other packages' source. + + No runtime code changes: `src/**` excluding tests is byte-identical, so no + shipped behaviour moves. The `patch` level reflects the published + `package.json` gaining `typecheck` / `check:test-typecheck` scripts and a + `tsx` devDependency. +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [2bb0614] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/platform-objects@17.4.0 + - @objectstack/types@17.4.0 + - @objectstack/observability@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/services/service-storage/package.json b/packages/services/service-storage/package.json index a72054e07c..3020f00a8e 100644 --- a/packages/services/service-storage/package.json +++ b/packages/services/service-storage/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/service-storage", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Storage Service for ObjectStack — implements IStorageService with local filesystem and S3 adapter skeleton", "type": "module", diff --git a/packages/spec/CHANGELOG.md b/packages/spec/CHANGELOG.md index 25e75bfc4b..43b4bc1eb7 100644 --- a/packages/spec/CHANGELOG.md +++ b/packages/spec/CHANGELOG.md @@ -1,5 +1,958 @@ # @objectstack/spec +## 17.4.0 + +### Minor Changes + +- 8f404a5: feat(spec)!: `plugins` / `devPlugins` are artifact envelope keys — excluded from the assembled package body and refused inside `packages[]` (#15219) + + + + **BREAKING** accept-set narrowing on `AssembledPackageBodySchema` — the body + under `packages[i].manifest` of a release artifact (ADR-0130 D4): a body that + carries `plugins` or `devPlugins` is now **refused** at the manifest's strict + close (`unrecognized_keys`, naming the key), where it used to parse. Shipped as + `minor` under the repo's launch-window convention for breaking changes; the + hand-migration prescription is registered under protocol major 18. Maintainer + ruling 2026-09-04 on #15219 (director decision batch #32, verbatim 「同意」): + option A for both keys. + + `plugins` and `devPlugins` were members of the assembled-body key set by the + same derivation every other collection uses (`COMPOSE_KEY_DISPOSITIONS` gives + both `concat`). They are the only members whose values are **runtime assembly + instructions** rather than serialisable metadata: `plugins` holds what a host + hands to `kernel.use()` — live plugin instances, manifests or package names — + and `devPlugins` is the `os dev` load list. Inside an artifact a package body + is inert JSON, so a plugin under `packages[i].manifest` could never be + constructed by a loader; every reader reads the top level. The classification + is corrected rather than special-cased: an artifact carries metadata, a host + assembles plugins. + + **What changes** (`packages/spec/src/stack.zod.ts`): + + - `plugins` / `devPlugins` are **envelope keys** — top level only, never inside + `packages[]`. `ASSEMBLED_PACKAGE_BODY_ENVELOPE_KEYS` (`packages`, `plugins`, + `devPlugins`) is declared once and feeds both the `AssembledPackageBodyKey` + derivation and `assembledPackageBodyShape()`. + - Both keys stay `concat`: a live stack still concatenates its plugins to the + top level under `composeStacks`, and `manifest: 'preserve'` no longer folds + them into any package body. + - The two declarations on the stack schema are unchanged. + + **What does NOT change:** `os serve` / `os migrate` / `os dev` keep reading the + top level (now correct by construction); no CLI, core or runtime code moves. + + ## FROM → TO + + ```ts + // before — a package body inside an artifact could carry plugins nobody could load + { packages: [{ manifest: { id: 'com.example.crm', /* … */ plugins: [{ name: 'plugin.x' }] } }] } + + // after — plugins live on the artifact envelope only; the body above is refused: + // packages.0.manifest: unrecognized_keys ['plugins'] + { plugins: [new CrmPlugin()], packages: [{ manifest: { id: 'com.example.crm', /* … */ } }] } + ``` + + **Migration.** Declare `plugins` / `devPlugins` at the stack top level and + delete them from every `packages[i].manifest`. An existing multi-package + artifact that carries `packages[i].manifest.plugins` (if `os build` ever wrote + one — not directly measured) is refused on load after this change and must be + rebuilt from source; a hand-written `packages[]` entry drops the keys. Stacks + that only ever declared the two keys at the top level parse byte-identically. +- 3e3ecb0: The model-facing solution-blueprint mirror can no longer generate an identifier the applier rejects. + + `SolutionBlueprintSchema` (what `apply_blueprint` validates against) and `SolutionBlueprintStrictSchema` (the OpenAI-strict structured-output contract the design model generates against) are two declarations of one shape. Their KEYS were pinned by an existing parity test; their VALUES had never been. Every identifier in the lenient schema carried `.regex(/^[a-z_][a-z0-9_]*$/)` and not one identifier in the strict mirror carried it — 20 leaves apart, measured. + + The consequence was a build whose approval did nothing. Asked for a CRM, the design model emitted a `company_size` select whose option values came straight off the labels — `1_49` for 「1-49人」. Generating that was legal. Applying it was not: on the turn the user clicked 「确认,开始搭建」 the deterministic confirm replay handed that exact blueprint to `apply_blueprint`, which refused it wholesale (`objects.0.fields.2.options.0.value: Invalid string: must match pattern /^[a-z_][a-z0-9_]*$/`) and staged nothing. The app appeared only because the model noticed the error card and retried with a repaired blueprint the user had never seen. + + Every identifier leaf in the strict mirror now carries the same `SNAKE_CASE` constraint the lenient schema enforces — object / field / view / dashboard / widget / app / nav names, `reference`, `nameField`, `columns`, `groupBy`, `measure`, roll-up `object` / `field` / `relationshipField`, condition `field`, and select option `value`. The constraint is emitted into the JSON Schema the model is given (`pattern`), so an out-of-pattern identifier is refused at generation instead of after approval. Option `value` additionally spells out the case that produced the incident: it may never start with a digit, so 「1-49人」 is authored as `size_1_49` — the `label` keeps the human wording untouched, and only the stored value is an identifier. + + A new `strict mirror ↔ lenient schema — VALUE parity` test walks both schemas leaf by leaf and fails on any future divergence, the value-side twin of the key-parity gate that already guards this pair. + + Refs cloud#1967. +- 13c48c2: feat(spec): retire `connector.errorMapping` — eleven authorable keys nothing ever read, one of them spelled like the live `userMessage` channel (#14676, ADR-0049) + + + + **BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep + launch-window convention ships it as `minor`; the migration prescription is + registered under protocol major 18, where `os migrate meta` users will look). + Triage ruling 2026-09-02 on the census card: ADR-0049 enforce-or-remove decides + it — declared-but-unenforced authorable surface with zero measured pull for a + reader comes off. + + `ConnectorSchema.errorMapping` carried `ErrorMappingConfig` (`rules`, + `defaultCategory`, `unmappedBehavior`, `logUnmapped`) and its + `ErrorMappingRule[]` (`sourceCode`, `sourceMessage`, `targetCode`, + `targetCategory`, `severity`, `retryable`, `userMessage`) — eleven keys on the + published authorable surface that **nothing read**: measured on `origin/main`, + the only reference outside the declaring file and its unit test was a + type-identity pin. No provider, dispatcher or materializer ever mapped an + external error through the rules, so `unmappedBehavior` configured nothing and + a rule's `userMessage` was never shown to anyone. That spelling is what made + this worse than ordinary dead surface: it is the name of the **live** + API-error channel (`ApiError.userMessage`, the user-facing refusal text a + thrown HTTP error declares), so an author who had read that documentation and + wrote a connector rule reasonably believed they were marking a refusal for an + end user — and the failure was silent in both directions (it validated, it + published, no message was ever shown). Removal resolves the collision by + deletion; the live channel is untouched. + + **What is refused:** authoring `errorMapping` on a connector, with any value. + `ConnectorSchema` is a non-strict `z.object`, so the key is a `retiredKey()` + tombstone rather than a bare deletion (a deletion would have stripped it in + silence): authoring it is a `tsc` error (`never`) and a parse error carrying + the prescription, on the base schema and — through + `DeclarativeConnectorEntrySchema`, which `superRefine`s the same shape — on + `stack.connectors[]` and the `PUT /api/v1/meta/connector/:name` door. + + **What leaves the public surface:** `ErrorMappingConfigSchema` / + `ErrorMappingConfig` / `ErrorMappingConfigParsed`, `ErrorMappingRuleSchema` / + `ErrorMappingRule`, and `ConnectorErrorCategorySchema` / `ConnectorErrorCategory` + (the enum's only consumers were the two removed shapes; an exported value + schema with no consumer reads as a capability). `api/ErrorCategory` — the + HTTP-response vocabulary — is unaffected. + + **What stays, byte-identical:** every other connector key (`health`, `retry`, + `webhooks`, `fieldMappings`, `syncConfig`, `actions`, `triggers`, `provider`, + `providerConfig`, `auth`, …) with its default and its readers. + + ## FROM → TO + + ```ts + // before — parsed green; nothing ever read the block, no message was ever shown + defineStack({ + connectors: [{ + name: 'payments_api', + label: 'Payments API', + type: 'api', + errorMapping: { + rules: [{ + sourceCode: 429, + targetCode: 'RATE_LIMITED', + targetCategory: 'rate_limit', + severity: 'medium', + retryable: true, + userMessage: 'The payment provider is busy; try again shortly.', + }], + unmappedBehavior: 'generic_error', + }, + }], + }); + + // after — delete the key; there is no replacement because no error-mapping + // engine exists: a connector's failures reach callers as the provider's own + // errors (ADR-0097). A user-facing refusal text is the API error envelope's + // `userMessage`, declared by the code that throws — not connector metadata. + defineStack({ + connectors: [{ name: 'payments_api', label: 'Payments API', type: 'api' }], + }); + ``` + + One-line fix: delete the `errorMapping` block; `os migrate meta --from 17` + lists the mechanical edits for existing sources. + + The retirement kit: + + - `retiredKey()` tombstone on `ConnectorSchema.errorMapping` + (`packages/spec/src/integration/connector.zod.ts`; the section comment + records what the shape was), inherited by `DeclarativeConnectorEntrySchema` + - ADR-0087 registration: `integration/Connector:errorMapping` and + `integration/DeclarativeConnectorEntry:errorMapping` in + `RETIRED_KEYS_BY_MAJOR[18]`; `integration/ErrorMappingConfig`, + `integration/ErrorMappingRule`, `integration/ConnectorErrorCategory` in + `RETIRED_DEFS_BY_MAJOR[18]`; the D2 conversion + `connector-error-mapping-removed` (protocol 18) wired into the step-18 chain + — a pure lossless strip of the block from every `connectors[]` entry, one + notice per connector (the eleven nested keys leave with the block) + - no liveness-ledger row: `connector` is not an enrolled ledger type, so + there is no row to keep or drop + - pin tests (`connector.test.ts`): refusal pins asserting the issue path, + code and prescription on the base schema, the declarative entry, and the + `stack.connectors[]` authoring path; the tsc `never` channel; a + no-materialize pin; the conversion's strip and notice; zero holders of the + seven retired names on every public entry; the ADR-0087 registration + - generated baselines/docs follow the schema (`authorable-surface/`, + `authorable-defaults/`, `api-surface/`, `json-schema.manifest/`, + `declaration-map/`, `export-origins/`, spec-changes, upgrade guide, + reference docs) + - zero authored occurrences in this repo's examples, skills and docs, and + zero hits in objectui at `0d8fd7c`, so no in-repo source changes ride along +- e89fa92: `IDataDriver` now declares `aggregate?` — the one engine-reached driver verb that had no signature to match against. + + The engine has always dispatched native aggregation by presence (`typeof driver.aggregate === 'function'`) and called `driver.aggregate(object, query, options)`, but the interface never spelled the member, so a custom driver's `aggregate` was checked in neither direction: swapped arguments or a non-row result compiled clean and surfaced only after the engine's `having` filter silently matched nothing. The member is declared optional, matching the presence test — a driver without native aggregation omits it and stays conformant, served by the `find()` + in-memory fallback. + + Additive: every in-repo driver already satisfies the declared signature (`(object: string, query: DriverQuery, options?: DriverOptions) => Promise[]>`); a wider parameter union or a looser return type stays assignable. What is newly refused is a wrong argument order or a non-array result. No `DriverCapabilities` bit is added — presence remains the capability test, as `data/driver.zod.ts` rules. +- 56fe8c2: A flow predicate authored as a CEL envelope is now refused at build time, instead of running unread by either validator. + + A `predicate`-role expression slot holds **bare CEL text** — `DecisionConditionSchema.expression` is declared `z.string()`, and so is a screen field's `visibleWhen`. An author who instead wrote the `{ dialect, source }` expression *envelope* there reached a shape nothing could see: a flow node's `config` is an open `z.record(z.unknown())` that no Zod schema is parsed against, the unknown-key walk exempts the schemaless node types on purpose (`decision` publishes no descriptor `configSchema`), and the expression ledger's `predicate` arm skipped every non-string as "a type violation for the schema pass to report" — a schema pass that, for those node types, does not exist. `registerFlow` accepted the flow, `objectstack validate` reported nothing, and the evaluator was the only layer that ever read the predicate. + + - `resolveFlowNodeExpressions` now emits a non-string sitting in a `predicate` slot, and the new `predicateSlotRefusal` / `PREDICATE_SLOT_STRING_REFUSAL` say why it is refused — one notion, derived once, read by both validators so build time and author time cannot disagree about the shape. `flow-template` slots keep the old rule: no validator implements that dialect, so a finding there is one nobody could judge. + - `registerFlow` throws, naming the node, the slot and the index, and attributing the finding to the envelope's own `source`. `objectstack validate` reports the same refusal as a located `error`. + + **String predicates are untouched, deliberately.** A whitespace-only string still means "not authored" on both sides, exactly as before; what a non-empty string *says* is still judged by `validateExpression('predicate', …)`, brace trap and all. Only the shape moved. + + An app that authored an envelope in one of these slots now fails to register with a message naming the slot; the fix is to write the predicate as bare CEL text (`record.rating >= 4`). The `{ dialect, source }` envelope remains the `value`-role spelling, on the `assignment` node's `assignments` map. +- ef3a138: feat(spec)!: an evaluated expression slot requires a non-blank `source` — `EvaluatedExpressionSchema`, composed by the `assignment` value envelope (#15430) + + + + **BREAKING** in the accept-set sense, landing in the launch window as `minor` + (the lockstep convention): on the schemas that type an EVALUATED expression + slot — today the `assignment` node's value envelope, + `AssignmentExpressionValueSchema` — an envelope with no `source` the engine can + evaluate is now **refused at authoring**, where it used to parse, register, + pass `objectstack validate`, and then fault at run time. + + Two spellings of one seam, refused by ONE rule with one message at `source` + (`EVALUATED_EXPRESSION_SOURCE_REQUIRED`): + + ```yaml + assignments: + digest: { dialect: cel, ast: { kind: const } } # `ast` only — no engine evaluates it + greeting: { dialect: cel, source: ' ' } # blank after trimming — parses to EOF + ``` + + > An expression in an evaluated slot needs a non-blank `source`: the expression + > engine evaluates `source` (the canonical persisted form of phase M9.1) and + > cannot evaluate `ast` alone, so an envelope carrying only `ast`, or a `source` + > that is blank after trimming, would validate and register and then fault at + > run time. Write `{ dialect: 'cel', source: '…' }`. + + - **`ExpressionSchema` is NOT narrowed.** It is the persistence contract — + `source` OR `ast` — and its docblock declares that `ast` becomes required in + build output at phase M9.2. The new export `EvaluatedExpressionSchema` (and + its type `EvaluatedExpression`) is a sibling: the same envelope with `source` + required and non-blank, spelled once and composed by every evaluated slot, so + when AST-only evaluation lands the flip is one edit there rather than a + per-slot unwinding. The rule is worded as "an evaluated slot requires whatever + the engine can actually evaluate"; what that is today is `source`. + - **The notion of blank is the engine's own** — `.trim()`, which + `cel-engine.ts`'s helpers already apply — not a third one beside the shape + rule's `min(1)` and `validateExpression`'s trim. + - **Three doors agree.** `registerFlow` refuses the flow, `objectstack validate` + and the runtime publish gate report a located `error` at the author's own + variable (`config.assignments..source`), and the executor's own shape + pass refuses the same set — all through the spec schema, so none of them + grew a rule of its own. + + **What an author does with a refused envelope.** An assignment value that + carried only `ast` has no evaluable form under M9.1: author its `source`. A + whitespace-only `source` was never an expression: delete the entry, or write + the expression. Every envelope with a non-blank `source` is unchanged, and + nothing is renamed, retired or rewritten — the refusal itself carries the + prescription. + + Not touched here: the `predicate` half of the same seam — `evaluateCondition`'s + silent `false` on an envelope without a `source` — is a behaviour change on a + live path with its own card, and the edge-condition schema that carries that + envelope is narrowed in a follow-up once the in-flight change to + `automation/flow.zod.ts` lands. +- fa125f3: feat(objectql,spec): `Field.valueDomain` binds at the write seam — a non-member is refused with `value_domain` (maintainer ruling 2026-09-02 on #14168, engine half) + + **BREAKING** accept-set narrowing on the ObjectQL record write path, shipped as + `minor` under the repo's launch-window convention for breaking changes. + + The key is **already published, and published unenforced**. The version-packages + cut `8a1bad8b8` (2026-09-04 10:20Z) consumed the spec half's changeset + `field-value-domain-slot.md` and released `@objectstack/spec@17.3.0`, which + declares `Field.valueDomain`, parses it, and refuses it on any type other than + `text` — and never reads it when a record is written. The 17.3.0 liveness ledger + states the gap in its own words: "a non-member WRITTEN to a `text` field + declaring a domain is accepted today". That write is accepted on 17.3.0 and is + refused from this release on. + + **Refused shape**, precisely: a record write that supplies a value for a `text` + field whose definition declares `valueDomain`, where the WRITTEN value is not a + member of the named standard. It fails with the field error code `value_domain`, + carrying `constraint: { valueDomain }` and a message that names the standard in + all four platform locales. Nothing else narrows — a field that declares no + `valueDomain` is untouched, and so is every other field type, because the schema + accepts the key on `text` alone and the validator judges exactly that set. + + **Remedy: write a member of the declared standard.** `iana_time_zone` admits + `UTC` and refuses `Mars/Olympus`; `iso_4217_currency` admits `CHF` and refuses + `chf`; `iso_3166_alpha2` admits `CH` and refuses `ZZ`. Dropping the + `valueDomain` declaration from the field lifts the refusal entirely, for an + author who declared a domain they did not mean. + + **No stored row is touched, and none becomes invalid.** This is the `min` / + `max` / `maxLength` transition-gate class: a value stored before the domain was + declared — or before this release — is never re-read, and it survives an edit of + another field on the same record. An absent or empty value follows the field's + `required` handling, not this check. + + + + - The membership test is the spec's shared `isValueDomainMember` — the same + predicate, over the same closed vocabulary, that a settings specifier's + `valueDomain` uses. A time zone accepted in Settings is the time zone + accepted in a field. + - The two authoring forms (`fieldForm`, `objectForm`) gain a `valueDomain` + control, shown on exactly the types the schema accepts the key on. The + object-form control's choices are derived from the vocabulary, not re-typed. +- a646120: `FILTER_TEXT_CASES` declares what a text operator answers over a stored value that is NOT a string, and the fixture gains its first non-string column. + + Measured before this row existed, one filter over one numeric column answered four ways across the platform: `driver-memory`'s reference matcher said NO to `$contains` and to `$notContains` for the same row; its live mingo path, `formula`, objectql's `having`, `driver-mongodb` and the analytics face type-gated (`$contains` NO, `$notContains` YES); the SQLite family coerced the number to text in its storage class's spelling (REAL renders `5` as `'5.0'`); and live Postgres refused at query time with SQLSTATE 42883 — a 500. + + The maintainer ruled the cell on 2026-09-05 (option A, type-gate): a stored value that is not a string never satisfies a positive text operator (`$contains` / `$startsWith` / `$endsWith` / `$icontains` / `$like` / `$ilike`) and satisfies `$notContains` — complementarity holds, on every face. Coercion was refused on the measurement; a declared-type door that refuses the filter before any backend runs is deferred to its own decision card, not rejected. + + - `FilterTextRow` is now `{ id, name, score }` — `score` is a NUMBER on every row (a `0` among them), chosen so a coercing backend answers a visibly non-empty set and a truthiness guard drops a row. + - Five new evaluated rows over `score`: the four positive operators the table can carry answer `[]`, `$notContains` answers all nine. (`$like` / `$ilike` follow the same rule and are pinned on the faces that answer them — the table is a driver's enrolment and `driver-mongodb` refuses those two.) + - `NON_TEXT_STORED_VALUE_TYPES` (`field-value.zod.ts`) — the numeric and boolean value classes, i.e. the declared field types whose stored value is never text — is the list the SQL faces classify a column by at compile time, since they cannot read the value. Temporal types are deliberately absent: their stored form is a dialect question (ADR-0053) the row does not decide. + + Every suite that materialises the fixture adds the column (SQL `initObjects` DDL included). +- 6f1ce7d: feat(spec)!: a text operator over a field whose DECLARED type can never store a string is refused at the engine's field-aware door — the contract rows (#15661) + + + + **BREAKING** accept-set narrowing, declared here and enforced at the engine door: a text operator (`$contains` / `$notContains` / `$startsWith` / `$endsWith` / `$icontains` / `$like` / `$ilike`) over a field whose DECLARED type is numeric, boolean, temporal (`date` / `datetime` / `time`) or structured JSON is refused before any driver runs — `INVALID_FILTER` / 400, naming the field and its declared type — instead of answering `[]` or a dialect accident. Shipped as `minor` under the repo's launch-window convention for breaking changes. Maintainer ruling 2026-09-05 on #15661 (director decision batch #43, verbatim 「同意」): option C-deny. + + The refused set is the union of six EXISTING classes in `field-value.zod.ts`, by reference — `NUMERIC_VALUE_TYPES` ∪ `BOOLEAN_VALUE_TYPES` ∪ `CALENDAR_DATE_TYPES` ∪ `INSTANT_TYPES` ∪ `CLOCK_TIME_TYPES` ∪ `STRUCTURED_JSON_TYPES` — so no new vocabulary is minted and a member added to one of those sets later is refused without a change here. String-valued classes pass: `STRING_VALUE_TYPES`, `autonumber`, the option-code classes (single and multi — `tags` included), the record-id classes, and the file classes. `formula` is judged as the field type its declared `returnType` names (`text` passes; `number` / `boolean` / `date` are refused) and is deferred — not judged — when `returnType` is absent. A dotted path into a structured-JSON field stays unjudged, as `filter-dotted-head` already declares. + + New on `@objectstack/spec/data` (`filter-text-operator-declared-type.ts`): `TEXT_FILTER_OPERATORS` (pinned equal to `StringOperatorSchema`'s keys), `TEXT_OPERATOR_DOOR_REFUSED_TYPES` / `TEXT_OPERATOR_DOOR_PASSING_TYPES`, `FORMULA_RETURN_TYPE_AS_FIELD_TYPE`, the pure verdict `textOperatorDoorVerdict`, the class table `TEXT_OPERATOR_DOOR_TYPE_CLASSES` (every `FieldType` member exactly once — pinned as a census), the fixture object `TEXT_OPERATOR_DOOR_FIXTURE`, and the derived case table `TEXT_OPERATOR_DOOR_CASES` the engine suite consumes. + + The door itself lands in `@objectstack/objectql` under its own engine-lane card (beside the `INVALID_FIELD` unknown-field door, judged against the object's real field map, before any driver dispatch); this changeset is the contract half. Beneath the door nothing moves: a direct driver call — and every evaluator no door fronts — keeps answering `FILTER_TEXT_CASES`' stored-value row (#14079), and the SQL faces' compile-time type-gate set `NON_TEXT_STORED_VALUE_TYPES` stays numeric + boolean, deliberately narrower than the door's set. + + What an author sees after the door lands: a condition such as `{ amount: { $contains: '5' } }` over a `number` field, which used to answer an empty list with no signal, is refused with a message naming `amount`, `number` and `$contains`. The condition was a mistake in every measured occurrence (a substring over a number can never match); drop it, or aim it at the text field that was meant. +- 52804cd: feat(spec)!: `FlowSchema` refuses a flow whose `edges[]` declares the same id twice (#14964) + + + + **BREAKING** accept-set narrowing on `FlowSchema` — a flow whose `edges[]` + carries two edges with the same `id` is now **refused at parse time** — by + `FlowSchema.parse` / `safeParse`, `defineFlow`, and every door that validates a + flow through the schema (`objectstack validate`, the runtime publish gate, a + stack's `flows[]`) — where it used to parse on green. Shipped as `minor` under + the repo's launch-window convention for breaking changes. Maintainer ruling + 2026-09-05 on #14964 (director decision batch #40, verbatim 「同意」): option + A — an `error`, not a `warning`; no opt-out, no transition window. + + Every reader of an edge id assumes the ids in a flow are unique — a designer, + a BPMN export, a flow diff, any traversal that dedupes by id — and nothing + enforced it. A real duplicate (`id: 'e20'` on two edges of one flow) shipped + through two releases of green CI in a downstream app and was inert only + because the engine keys out-edges by `source`, never by `id`: the collision is + invisible until something keys on ids, and then silently wrong rather than + loudly broken. The id space is hand-authored, so the next author picking a + "free" id from the sequence had no way to know it was taken. + + **What changes** (`packages/spec/src/automation/flow.zod.ts`): a `superRefine` + on the flow's `edges[]`. Each later occurrence of an already-declared id raises + one `custom` issue, anchored at `edges[N].id` of the *later* edge and naming + both positions, so the formatted error points at the edge to renumber: + + ```text + ✗ edges.7.id: Duplicate edge id `e20` — `edges[7]` reuses the id already declared by `edges[3]`; every edge id in a flow must be unique. Renumber one of them: … + ``` + + **What does NOT change:** `edges[].id` keeps its name, type and describe; the + node vocabulary, the edge `type` enum and every other refusal are untouched; + a flow with unique edge ids (or no edges) parses exactly as before. Node ids + are not covered by this change. + + The shape that is refused, and what the author does about it — a two-edge + excerpt, the later edge renumbered: + + ```ts + // before — parsed on green, both edges keyed 'e20' + edges: [ + { id: 'e20', source: 'qualify', target: 'convert' }, + { id: 'e20', source: 'convert', target: 'end' }, + ] + + // after — refused at parse (edges.1.id: Duplicate edge id `e20` …); renumber the later one: + edges: [ + { id: 'e20', source: 'qualify', target: 'convert' }, + { id: 'e21', source: 'convert', target: 'end' }, + ] + ``` + + **Remedy.** Renumber the later edge to an id no other edge in that flow + carries; nothing else in the flow needs to move. The census over this + repository found no flow to migrate, so this is a release note, not a + migration: no shipped example, fixture or seed in `packages/**` or + `examples/**` declares a duplicate edge id, and the pinned objectui tree + carries none in its authored flows. The one known downstream instance was + renumbered before this change (hotcrm PR #1571). +- a84e1ce: feat(spec): `II18nService.getFallbackLocale()` — the declared fallback locale is readable, so the metadata-document translators can be handed the chain the deployment declared (#14882) + + `ResolveOptions.fallbackChain` on the `@objectstack/spec/system` label + resolvers (`translateMetadataDocument`, `translateObject`, `translateApp`, + `resolveViewLabel`, …) is the ordered list of locales consulted after the + requested one and BEFORE the authored label. Nothing on `II18nService` + exposed the deployment's declared fallback (`i18n.fallbackLocale`, else + `defaultLocale`), so no serving layer could thread it, and every caller fell + to the resolver's literal `['en']` default. A `zh-CN` workspace that shipped a + courtesy `en` bundle therefore served English bundle text to a `zh-CN` + request ahead of its own authored Chinese labels. + + - New optional contract member `II18nService.getFallbackLocale?(): string | undefined` + — the locale the service's own `t()` consults second. `undefined` (or the + method absent) means nothing was declared, and a serving layer must then + leave the resolver's default in place rather than invent a chain. + - The `fallbackChain` documentation now states who supplies it (the serving + layer, from `getFallbackLocale()`) and that the `['en']` default applies + only when a caller declares no chain at all. The resolver's behaviour for + a caller that passes nothing is unchanged. + + Additive: no existing implementation or caller changes shape. +- bf1054a: feat(spec): retire the fourteen inert deadline keys of the incident-response, training and change-management schemas (#14477, ADR-0049) + + + + **BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep + launch-window convention ships it as `minor`; the migration prescriptions are + registered under protocol major 18, where `os migrate meta` users will look). + Maintainer ruling 2026-09-02 on the census card (ruled A: retire per family): + ADR-0049 enforce-or-remove decides it — declared-but-unenforced deadline + surface with zero measured readers comes off. + + Fourteen hour/minute/day-shaped deadline, SLA and duration key sites — twelve + distinct names, because `durationMinutes` and `estimatedMinutes` each occur at + two sites — sat on the exported incident-response, training and + change-management schemas and in the generated reference docs, and **nothing + read them**: the schemas are exported from `@objectstack/spec/system`, mounted + by no stack key, registered as no metadata type, absent from the 2026-06 + liveness ledgers, and the reader census over every package outside + `packages/spec` (tests and changelogs excluded) and over objectui at the + pinned sha returned zero hits for every key. An author could write + `triageDeadlineHours: 4`, `validityDays: 365` or `regulatorDeadlineHours: 72` + and reasonably expect the platform to escalate, expire or notify — it never + did, and it never said so. Six of the keys carried defaults (30 minutes, + 1 hour, 2555 days; 365, 30 and 14 days) that were materialized into every + parsed document without ever being consulted. A compliance-shaped deadline + that fails silently is the worst form of the shape ADR-0049 names. + + **What is refused:** authoring any of the keys below, with any value, on the + base schema and through every carrier that nests it (`Incident.responsePhases[]`, + `IncidentResponsePolicy.notificationMatrix`, `TrainingPlan.courses[]`, + `ChangeRequest.impact` / `.rollbackPlan` / `.implementation`). None of the + schemas is `.strict()`, so each key is a `retiredKey()` tombstone rather than a + bare deletion (a deletion would have stripped it in silence): authoring it is a + `tsc` error (`never`) and a parse error carrying the prescription + (`invalid_type` at the path of the key). + + | schema | retired keys | + |:--|:--| + | `IncidentResponsePhase` | `targetHours` | + | `IncidentNotificationRule` | `withinMinutes`, `regulatorDeadlineHours` | + | `IncidentNotificationMatrix` | `escalationTimeoutMinutes` (default 30) | + | `IncidentResponsePolicy` | `triageDeadlineHours` (default 1), `retentionDays` (default 2555) | + | `TrainingCourse` | `durationMinutes`, `validityDays` | + | `TrainingPlan` | `recertificationIntervalDays` (default 365), `gracePeriodDays` (default 30), `reminderDaysBefore` (default 14) | + | `ChangeImpact` | `downtime.durationMinutes` | + | `RollbackPlan` | `steps[].estimatedMinutes` | + | `ChangeRequest` | `implementation.steps[].estimatedMinutes` | + + **What stays, byte-identical:** every other key of the three families with its + default and its (absent) readers, and every export — no def leaves the public + surface. Parsed documents no longer carry the six former defaults. + + **Held, not touched:** the `ESignatureConfig` pair (`expirationDays`, + `reminderDays` in `data/document.zod.ts`) — the ruling left that branch open + pending the e-signature roadmap answer; it stays on the card. + + ## FROM → TO + + ```ts + // before — parsed green; no engine ever read a single one of these numbers + const policy: IncidentResponsePolicy = { + notificationMatrix: { + rules: [{ severity: 'critical', channels: ['pagerduty'], recipients: ['security_team'], + withinMinutes: 15, notifyRegulators: true, regulatorDeadlineHours: 72 }], + escalationTimeoutMinutes: 45, + }, + defaultResponseTeam: 'security_team', + triageDeadlineHours: 2, + retentionDays: 3650, + }; + const course: TrainingCourse = { + id: 'COURSE-SEC-001', title: 'Security Fundamentals', description: '…', + category: 'security_awareness', targetRoles: ['all_employees'], + durationMinutes: 60, validityDays: 365, + }; + const rollback: RollbackPlan = { + description: 'Restore from backup', + steps: [{ order: 1, description: 'Restore backup', estimatedMinutes: 15 }], + }; + + // after — delete the keys; there is no replacement because no incident-response, + // training-management or change-management engine exists to keep a deadline. + // Record retention is the object-level `lifecycle` block (ADR-0057), declared on + // the object that stores the records. + const policy: IncidentResponsePolicy = { + notificationMatrix: { + rules: [{ severity: 'critical', channels: ['pagerduty'], recipients: ['security_team'], + notifyRegulators: true }], + }, + defaultResponseTeam: 'security_team', + }; + const course: TrainingCourse = { + id: 'COURSE-SEC-001', title: 'Security Fundamentals', description: '…', + category: 'security_awareness', targetRoles: ['all_employees'], + }; + const rollback: RollbackPlan = { + description: 'Restore from backup', + steps: [{ order: 1, description: 'Restore backup' }], + }; + ``` + + One-line fix: delete the key wherever it is authored. There is no + `os migrate meta` edit list for these keys — none of the schemas is a stack + collection member, so the conversion chain has no seam to walk (the + `MetadataPluginConfig.additionalTypes` precedent); the tombstone prescription + and the protocol-18 upgrade guide are the channels. + + The retirement kit: + + - `retiredKey()` tombstones at all fourteen sites (`packages/spec/src/system/ + incident-response.zod.ts`, `training.zod.ts`, `change-management.zod.ts`; + each file's section comment records what the shape was and why no D2 + conversion exists) + - ADR-0087 registration: fourteen `RETIRED_KEYS_BY_MAJOR[18]` entries (the + three nested change-management sites spelled `ChangeImpact:downtime.durationMinutes`, + `RollbackPlan:steps.estimatedMinutes`, `ChangeRequest:implementation.steps.estimatedMinutes`) + and three D3 semantic entries, one per family + - no liveness-ledger row: none of the three families is an enrolled ledger + type, so there is no row to keep or drop + - pin tests (`deadline-keys-retirement.test.ts`): a refusal pin per site + asserting the issue path, code and prescription on the base schema and + through the nesting carriers; the tsc `never` channel; no-materialize pins + for the six former defaults; the ADR-0087 registration; and a tree-scoped + absence pin over every authored source in the repo + - generated baselines and docs follow the schema: `authorable-surface/` gains + eleven `[RETIRED]` rows, `authorable-defaults/` loses six rows, the three + system reference pages are regenerated, and the gitignored `json-schema/` + output is re-emitted on the next build + - `json-schema.manifest/` is unchanged, and correctly so: it ratchets def + *names*, and retiring keys removes no def from the published surface + - `spec-changes.json` and the protocol upgrade guide are unchanged too: both + project the migration chain at the current protocol major (17), so these + protocol-18 registrations reach them at the 18 cut + - zero authored occurrences in this repo's examples, skills and hand-written + docs, and zero hits in objectui at the pinned sha, so no in-repo source + changes ride along beyond the three families' own unit tests +- 222dc0f: feat(spec): `IJobService.replay` gains an optional third argument, `options?: JobReplayOptions`, carrying `force: true` (#14766 — the contract half of the #14501 A+a2 ruling) + + Additive: the argument is optional, an existing two-argument `replay(name, data?)` implementation keeps compiling and behaving as before, and omitting it is the pre-#14766 call exactly. `JobReplayOptions` is exported from `@objectstack/spec` (`contracts`), with one member, `force?: boolean`. + + **What the contract now declares** (`packages/spec/src/contracts/job-service.ts`, the `replay` TSDoc), for a scheduled (cron) flow whose tick window takes a `(flow, tick-window)` dispatch claim in `sys_flow_dispatch`: + + - `replay(name, data)` on a window whose claim is **absent or failed** re-runs the window — unchanged behaviour, and every job that never takes a claim is this row; + - `replay(name, data)` on a window whose claim **succeeded** is **refused loudly**: the promise rejects with an ADR-0112 envelope — `code: 'RESOURCE_CONFLICT'` (the standard-catalog member HTTP 409 derives; no new extension code) and `status: 409` — whose message names the window asked for and the claim that refused it. Never a silent no-op; + - `replay(name, data, { force: true })` sends anyway; the duplicate is the operator's, taken knowingly. + + **Declared here, enforced by #14501.** This release changes the contract text and the signature only. The refusal semantics are implemented by the services half (#14501: the `(flow, tick-window)` claim through `sys_flow_dispatch`, and `DbJobAdapter.replay` reading it); until that lands, shipped adapters still accept the third argument and ignore it, re-running the window as before. A third-party `IJobService` implementation that already declares `replay` needs no change to keep compiling; one that wants the once-only guarantee implements the table above. +- f502898: feat(spec): list-view grouping is server-side — the group header query and the per-group row page compile from the view (#14556) + + Maintainer ruling A on objectui#7189 (2026-09-02): grouping on a list view is + server-side. The set of groups and every number in a group header — the count + and any per-group aggregation — are properties of the query, not of the fetched + page; rows inside a group are paged. Grouping one fetched window (the interim + behaviour) rendered two headers (86, 14) or five (31/31/30/7/1) for the same + 186 rows in five units depending on row order, and left the rows past the + first window unreachable. + + The contract reuses the query shapes the platform already has — no new query + shape, no new engine verb, no new envelope: + + 1. **The group keys and every header number are ONE aggregate query** + (`EngineAggregateOptions`, executed by `IDataEngine.aggregate`): `groupBy` + is `grouping.fields[].field` in nesting order (a multi-level grouping is a + multi-column `groupBy`), `aggregations` is a `count` node (the group's total + row count, alias `count`) plus the view's declared column summaries mapped + onto `AggregationFunction` — the one aggregation vocabulary datasets already + use — and `where` is the view's composed filter. + 2. **The rows inside a group are the existing paged `find`** + (`EngineQueryOptions`) with the group's key predicate AND-ed into the view + filter, `limit` / `offset` per group. + + New on the `ui` entry, `view-grouping-query.ts`: + + - `compileListViewGroupQuery(view, { where?, depth? })` → the header query; + `compileListViewGroupRowsQuery(view, groupKey, { where?, limit?, offset?, orderBy?, fields? })` + → the row page; `listViewGroupKeyPredicate` (the empty group is spelled with + the `$null` predicate — the spelling the view filter dialect's `is_empty` + lowers to). + - `COLUMN_SUMMARY_AGGREGATION` — the `ColumnSummary` → aggregation table, + exhaustive by type: `count` → a fieldless `count` (`COUNT(*)`), + `count_unique` → `count_distinct`, `sum` / `avg` / `min` / `max` → the same + name, `none` → nothing; `count_filled` / `count_empty` / `percent_filled` / + `percent_empty` map by derivation — one `{ function: 'count', field }` node + (`COUNT(field)`, the non-null count, header column `count_`), from + which `deriveColumnSummary(row, summary, field)` computes all four on the + header row (`count_filled` = `count_`, `count_empty` = `count − + count_`, `percent_filled` = `count_ / count`, 0 when the count + is 0, `percent_empty` = `1 − percent_filled`). Server-side "empty" is `null` + on every face; the footer's client-side reading of `''` / `[]` as empty is + the renderer's to converge. A future member with no counterpart is refused + loudly at compile time (`ListViewGroupQueryError`, `NOT_IMPLEMENTED` / 501, + the summary's path — `UNMAPPED_COLUMN_SUMMARIES`, empty today); a value that + is no member at all is `INVALID_QUERY` / 400. + - Result-column naming on a header row: each grouped field under its own name + (raw stored value, `null` for the empty group; group keys are scalar), `count`, + and each summary under `_` (`columnSummaryAlias`). + + `GroupingConfigSchema` / `GroupingFieldSchema` / `ColumnSummarySchema` now say + this in their docs, with the shape's recorded limits (a date grouping field + groups per distinct stored instant; header cardinality is unbounded). Nothing + changes in what parses: no key is added, removed or re-shaped. `minor` because + a new exported helper and a declared contract semantics ship; not breaking — + the page-scoped behaviour was never declared. Both queries ride the existing + `POST /data/:object/query` door (`protocol.findData` → `engine.aggregate`, + answering `{ object, records, total, hasMore }`); the grid consuming the header + rows is objectui#7189. +- 414c1fc: feat(spec)!: `ComponentPropsMap['element:record_picker'].filter` converges onto the `ViewFilterRule` array form — the last record-form `filter` in the map (#14406, objectui#6206 Option B) + + + + **BREAKING** accept-set change on one props-map entry, shipped as `minor` under + the repo's launch-window convention for breaking changes; the migration + prescription is registered under protocol major 18. + + One filter orthography platform-wide (maintainer batch adjudication 2026-08-25, + verbatim 「同意」, Option B): after `element:number` converged (#12039 Key 2), + `element:record_picker`'s `filter` was the one `filter` input in + `ComponentPropsMap` still declared as the MongoDB-style record + (`FilterConditionSchema`) while the three array-declared siblings + (`record:related_list`, its nested Add-affordance picker, `element:number`) + declared `z.array(ViewFilterRuleSchema)` — the four `object-*` doors declare + `filter` as `z.unknown()`, #15449 — so the filter a list view stores and + renders was refused by the picker beside it. The entry now declares the same + array form those siblings do, and the `FilterConditionSchema` import that existed for this + one site leaves the file with it. + + Sequenced measurement-first, as that convergence had to be: the `record_picker` + read path was measured at the objectui pin before the declaration moved. The + renderer hands `filter` to `query.$filter` and calls `adapter.find()`, whose + `convertQueryParams` lowers a rule array through `translateFilterArray` into + filter AST tuples — the door every list view's stored rule array already takes + — and nothing on that path parses `properties` against the installed spec. + + **Migration** (`element-record-picker-filter-rule-array` — listed by + `os migrate meta --from 17` once the protocol major is 18): a record-form `filter: { status: 'active' }` becomes + `filter: [{ field: 'status', operator: 'equals', value: 'active' }]`; an operator + object `{ amount: { $gt: 100 } }` becomes + `[{ field: 'amount', operator: 'greater_than', value: 100 }]`; several keys + become several rules (they AND). The record form is refused at `filter` + (`invalid_type`, expected array). The binding-level `dataSource.filter` on the + same node is a different key and is unchanged by this release. + + `ElementRecordPickerPropsParsed` is declared (ADR-0122): the entry's parsed + state now differs from its authored state on `filter` (`operator` normalizes on + parse), so the bare alias is no longer isomorphic. +- 5f7fa1d: feat(spec): retire `SessionUser.language` — the session contract's never-produced "preferred language" (#14788, ADR-0049) + + + + **BREAKING** key removal on a published session type, landing after the + v17.0.0 cut (the lockstep launch-window convention ships it as `minor`; the + prescription is registered under protocol major 18 — `api/SessionUser:language` + in `RETIRED_KEYS_BY_MAJOR[18]` plus the D3 semantic entry + `session-user-language-retired` — where `os migrate meta` users will look). + + `SessionUserSchema.language` (`api/auth.zod.ts`) was declared + `z.string().default('en')` and described as "Preferred language", and had no + producer and no consumer anywhere: no session endpoint ever wrote it, no client + ever read it (objectui measured at its pinned sha: zero readers; the only + in-repo mentions were the schema's own unit test). A reader trusting the + published contract got a constant that was not the user's language — while the + user's real preference had just landed as the first-class column + `sys_user.locale` (#13881), which the session type could not see. Three + spellings of one concept on the published surface, none of them right. The + maintainer ruled option D (2026-09-03): retire the dead key under ADR-0049 + enforce-or-remove and make `GET /auth/me/localization` the ONE read face for + the signed-in user's language. No replacement field joins the session contract + until a session endpoint really produces one — no dual-spelling window. + + FROM → TO: + + - `SessionUser.language` / `SessionUserParsed.language` → *(removed)*. Read + the signed-in user's language from `GET /auth/me/localization` → `locale`, + which now resolves the user's own `sys_user.locale` when set → the request's + `Accept-Language` → the deployment default (`@objectstack/plugin-hono-server` + in the same release). + + One-line fix: delete the key. A producer still writing it fails `tsc` + (`never` input type) and fails to parse with this prescription; a reader still + keying on it now reads `undefined` instead of a permanent `'en'`, and should + read `locale` off `/auth/me/localization` instead. + + The retirement kit: + + - **`retiredKey()` tombstone** (the schema is a non-strict `z.object`, so a bare + delete would have stripped the key silently — ADR-0104): writing `language` + is a `tsc` error and a parse error carrying the prescription, on + `SessionUserSchema` and through both envelopes that embed it + (`SessionResponse.data.user`, `UserProfileResponse.data`). + - **ADR-0087 registration**: `api/SessionUser:language` under major 18 plus + the D3 semantic entry `session-user-language-retired`. A RESPONSE surface — + the server mints a `SessionUser`, nobody authors or persists one — so there + is no source for a D2 conversion to rewrite (the + `api/AuthFeaturesConfig:passkeys` disposition). + - **generated baselines**: `authorable-surface/api.json` carries the + `[RETIRED]` row; `authorable-defaults/api.json` drops the `= "en"` default; + `spec-changes.json`, the upgrade guide and `content/docs/references/api/auth.mdx` + regenerated. + - **pins** in `api/auth.test.ts`: the prescription on parse, absence (no default + minted) on a clean parse, both envelopes refusing the key, and a + `packages/spec/src`-scoped scan for any reader of `.language` off a + `SessionUser`. + - zero in-tree producers or readers, so no in-repo source changes ride along + beyond the endpoint change shipped with it. +- 87f0ccc: feat(spec): `SharingRuleEvaluationResult` declares `grantsRefused?: number` — the optional seventh key the sharing-rule evaluate route already answers (#14969) + + `minor`, derived: a new key on a published contract interface is additive public + API (semver "backwards-compatible functionality"), and not `major` because the + key is **optional** — every existing `ISharingRuleService` implementer, in-tree + and out, keeps compiling unchanged, and every consumer typed against the six + counts keeps reading them. + + `POST /api/v1/sharing/rules/:idOrName/evaluate` (ledgered `sdk`, + `shares.rules.evaluate`) passes the service's return value through unfiltered, + and `@objectstack/plugin-sharing` has counted refused grants on its own subtype + since #14754 — so the wire carried `grantsRefused` while the declared client + type (`client.shares.rules.evaluate`, typed `Promise`) + could not name it without a cast. The client gains the key through its spec + import with no edit of its own. + + What the key means, and what its absence means: it counts the grants the + engine **refused** during the pass (`ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` on + an organization-less insert into a tenant-scoped `sys_record_share`); the pass + continues past a refusal, so `grantsRefused > 0` is not a failed pass. The key + is **absent — not `0`** — from any implementation that does not count + refusals. A consumer branching on it must read "unset" as "this implementation + does not report refusals", never as "no grant was refused"; only a present `0` + says the latter. Do not `?? 0` it. + + Optional in the spec composes with the plugin-local narrowing: an + implementation that counts refusals may require the key on its own subtype + (`SharingRuleReconcilePassResult extends SharingRuleEvaluationResult`), a legal + covariant narrowing that still satisfies `ISharingRuleService`. +- c2a336c: `@objectstack/spec/system` now names the ADR-0030 notification cut-over, so "has this deployment run it?" has a place to be answered. + + `sys_migration` is the ledger a deployment writes to record that a data migration ran against its own database, and consumers read it instead of the platform version. Its well-known ids were `adr-0104-file-references` and `adr-0104-value-shapes` — the two ADR-0104 scans, both driven by an `os migrate` command that records the row. `migrateSysNotificationToEvent` (`@objectstack/metadata/migrations`) had none. It is destructive and one-way, operators are handed the call verbatim in `docs/handoff/adr-0030-notification-convergence.md`, and it recorded nothing when it ran: a deployment that performed the cut-over and one that never did are indistinguishable from the ledger. A row can only be keyed by an id, so without one the question had nowhere to be answered even in principle. + + Added: `NOTIFICATION_EVENT_MIGRATION_ID = 'adr-0030-notification-event'`, exported from `@objectstack/spec/system`. Purely additive — no existing export, schema or predicate changes, and nothing reads the new id yet. + + Deliberately NOT decided here, and the constant's docblock says so rather than leaving its silence to be read as an answer: what a `sys_migration` row under this id means. The two ADR-0104 ids get their `last_run_at` / `applied_at` / `verified_at` / `blocking` semantics from a command that scans, self-checks and only then records; this migration has no command and no self-check, and reports `migrated` / `already_done` / `not_applicable` / `error` to its caller instead. Which of those columns one of its runs may claim, whether anything may gate on the row, and whether a datastore created after the cut-over belongs in `CREATION_ATTESTED_MIGRATION_IDS`, are contract questions on this surface and are left open. +- 9408b7f: A flow condition that is neither CEL text nor an expression is now refused at build time, instead of being read as an empty condition and answering a silent `false`. + + `evaluateCondition` derives its source as `typeof expression === 'string' ? expression : (expression?.source ?? '')`. For a value that is neither — a number, a boolean, an array — the read yields `undefined`, the `??` supplies `''`, and the empty-source arm returns **`false`**: the "an unauthored branch must not open" rule, applied to a value that was very much authored. Measured: a `decision` node carrying `config: { condition: 42 }` **registered clean** and executed `success: true` with nothing said at any layer; `{ source: 1 }` did not even get that far and threw a bare `TypeError: exprStr.trim is not a function` out of the validator. `config.condition` is also the key a **start node's trigger gate** is read from, so the same value could gate a whole flow shut forever with no signal to the author. + + - The new `structuralConditionRefusal` / `STRUCTURAL_CONDITION_SHAPE_REFUSAL` in `@objectstack/spec/automation` are the single shared notion of why, read by both validators so build time and author time cannot disagree about the shape. `registerFlow` throws, naming the node or edge and attributing the finding; `objectstack validate` reports the same refusal as a located `error`. + + **This is deliberately NOT the `predicate`-slot rule, and the difference is measured.** A ledger `predicate` slot (`decision.conditions[].expression`, a screen field's `visibleWhen`) is declared `z.string()`, so `PREDICATE_SLOT_STRING_REFUSAL` refuses every non-string including an envelope. Neither structural slot is declared that way: `FlowEdgeSchema.condition` is `ExpressionInputSchema`, whose string arm **transforms into** `{ dialect: 'cel', source }` — so after `FlowSchema.parse` every authored edge condition *is* an envelope — and `FlowNodeSchema.config` is an open `z.record` that passes an envelope written at `config.condition` through verbatim, where `evaluateCondition` evaluates it correctly. Both shapes stay accepted here; an envelope with no `dialect`, and an `ast`-carrying one (`ExpressionSchema`'s own `source`-or-`ast` rule), stay accepted too. + + **Strings are untouched, deliberately.** A whitespace-only condition still means "not authored" and still answers `false` on both sides — consistent behaviour, ruled correct, not a defect. What a non-empty string *says* is still `validateExpression('predicate', …)`'s verdict, brace trap and all. Only the shape moved. + + An app that authored a number, a boolean, an array or a source-less object in a node or edge `condition` now fails to register with a message naming the site; the fix is to write the condition as bare CEL text (`record.rating >= 4`) or as an expression envelope. +- 581d8f8: feat(spec): `TryCatchErrorValueSchema` declares the `code` key the `try_catch` engine binds (#14954) + + `TryCatchErrorValue` — the ONE shape the catch region's author, the engine and the run log share for the value a `try_catch` binds to `errorVariable` (default `$error`) — gains an optional `code: string`: the platform-classified error code (ADR-0112) the failing node's own result carried, e.g. `create_record`'s `DUPLICATE_RECORD`. The engine has bound it since `@objectstack/service-automation`'s #14419 change; the schema was a plain `z.object` that did not declare it, so a round-trip through the declared shape silently STRIPPED the key the engine had put there, and the generated reference page documented four keys where the runtime binds five. The `errorVariable` description on `TryCatchConfig` names `code` too, so the authorable surface documents branching on `$error.code`. + + Typed as an open `string`, deliberately not `StandardErrorCode` and not the ledger union: ADR-0112 D3/D4 with the #9106 amendment make the code vocabulary `StandardErrorCode` ∪ registered ledger codes ∪ tenant-authored codes, and `NodeExecutor` is third-party-registrable, so a closed type would be false the moment anyone registers an executor that throws its own code. The closed-at-every-door rule governs `ApiErrorSchema.code` at an HTTP door; this value is bound in-process and never crosses one. + + Additive and optional: every value that parsed before parses byte-identically, and a binding without a classified code still carries no `code` key — absent means "no classified code", never "nothing failed". Semver: a new optional key on a published schema widens the accept set and the exported `TryCatchErrorValue` type without retiring or renaming anything ⇒ `minor`; no ADR-0087 entry is owed because there is nothing an upgrader must migrate. + +### Patch Changes + +- ca326b5: `IApprovalService.recall`'s contract prose names every actor who may recall, and scopes each one by status (#14670) + + **Documentation only — no key, no accepted value, no runtime behaviour moves.** The implementation has been correct since #12775; only the contract's description of it was stale. + + The docstring said *"Only the submitter (or a system context) may recall"*, then widened to `returned` requests in a second paragraph. Both halves were wrong, in opposite directions: + + - **The list was not exhaustive.** A #3424 override actor — a platform or tenant admin holding no approver slot — may recall a `pending` request. That is the in-product recovery path for an approval routed to an unstaffed position, and this same file already documented it 387 lines above the sentence denying it: the docblock on `ApprovalRequestRow.viewer.can_override` spells the override's levers as `(approve / reject / reassign / recall it)`. One file, two contradicting sentences about the same verb. + - **The ADR-0044 widening read as though it applied to that whole list.** It does not. The override and system arms are ANDed with `status === 'pending'` where they are computed, so neither reaches a `returned` request; an override actor is refused there exactly as any other non-submitter (#12775, maintainer ruling 2026-09-02). Abandoning a revision window is the submitter's alone. + + The rewrite makes **status** the axis instead of appending a caveat, so the second defect cannot come back on a re-read: each status carries its own admitted set, and the `returned` bullet says outright that the submitter is alone in it. + + `ApprovalRecallInput.actorId` carried the same stale sentence (*"Must be the request's submitter (or a system context)"*) and is corrected with it. Fixing only the method docstring would have left the contradiction alive on the very input type the corrected method takes. + + The two sibling docstrings sharing that phrasing are **correct and unchanged**: `ApprovalSendBackInput.actorId` and `ApprovalResubmitInput.actorId`. `isOverrideActor` is called from exactly five places in `plugin-approvals` — `decideNode`, `reassign`, `recall`, `attachViewers` and `visibleRequestIds` — and neither `sendBack` nor `resubmit` is among them, so no override actor reaches either. + + The published prose already described the corrected rule (`content/docs/automation/approvals.mdx`: an admin "may act on any `pending` request — approve, reject, reassign it to a real approver, or recall it"). This docstring was the one surface that had not kept up. +- b548e43: `colorField` now documents what it means: a field to DERIVE a colour from, not a field holding one. + + `TimelineConfigSchema`, `CalendarConfigSchema` and `GanttConfigSchema` each declare a `colorField`, and all three `.describe()` strings said only that the field "determines"/"drives" the colour — `'Field to determine item color'`, `'Field whose value determines the event color'`, `'Field that drives the bar color'`. Read literally, that invites pointing the key at a field whose stored value *is* a colour, which is the one case the renderers need the least: the common author intent is `colorField: 'status'`, a select field whose options already carry the colours. + + The renderers resolve it as a derivation ladder (objectui#7243, shared as `createFieldColorResolver` in `@object-ui/core`): + + 1. the option `color` the field declares for the record's stored value; + 2. else the value itself, when it already is a colour literal (hex 3/6/8-digit, `rgb(...)`, `hsl(...)`); + 3. else each renderer's own last rung — the gantt derives a semantic colour token, the calendar hashes onto its theme-aware palette, the timeline draws its default marker. + + The three strings now say that, each naming its own last rung. **Nothing in the accept set moves**: all three keys stay `z.string().optional()`, and a config pointing `colorField` at a plain hex field is still exactly as valid as before — that is rung 2. This is prose on a declared key, so the only regenerated follower is `content/docs/references/ui/view.mdx`. +- 85a2459: fix(spec): the dashboard `gap` field no longer describes itself to app authors in Tailwind vocabulary + + `ui/dashboard`'s `gap` key told app authors its value in the vocabulary of a CSS + library they never chose and cannot act on. **Two** independent producer strings + carried that wording, and they feed two independent customer-facing surfaces: + + - `dashboardForm`'s `helpText` — `Grid gap (Tailwind units)` — rendered verbatim in + the Studio property panel, which is spec-driven and feeds this form straight into + the generic form renderer. + - `DashboardSchema.gap`'s `.describe()` — `Grid gap in Tailwind spacing units` — + rendered as this field's row in the published reference page + `content/docs/references/ui/dashboard.mdx`. The reference corpus renders + `.describe()`, never `helpText`. + + Both now read **Space between widgets, in steps of 0.25rem (4 = 1rem)**: what the + author decides, plus the magnitude, stated in a CSS unit instead of a framework's + scale. The magnitude had to survive the rewrite rather than be dropped with the + framework name — the number is a spacing step, so `4` means `1rem` and not `4px`, + and an author who lost that would come away knowing less than before. + + The step size is stated as measured rather than inferred: the dashboard renderer + sets the grid gap as an inline style computed from this key, so every accepted + value is linear and one step is exactly `0.25rem`. "Tailwind units" was doubly + wrong — it named an implementation dependency, and it named one the consumer of + this key does not have. + + **No schema change.** `gap` stays `z.number().int().min(0).optional()` and accepts + exactly what it accepted before; nothing is added to or removed from any public + surface. `columns` is deliberately untouched on both of its producer lines — + `12` is an author-visible fact about the grid being laid out, not a framework + detail — and this is one field's two strings, not a sweep for framework words. + + The `en` metadata-forms translation bundle is a mechanical copy of the form source, + so it is regenerated to match. Translated locales are not touched: regeneration + fills gaps only and never overwrites an existing leaf. +- 2c753fe: feat(runtime): a flow action's run context now carries `recordLoadDenied` (#15168) + + The previous release declared `AutomationContext.recordLoadDenied?: true` and + said so plainly: **declared, not yet populated on the flow face.** The + script/body face of both action doors emitted the signal, but + `dispatchFlowAction` handed `automation.execute` a context without it, so a + `runAs: 'system'` flow that guarded on the documented key was inert — never + `true`, never wrong, and indistinguishable from a flow whose caller could read + the row. + + **This release populates it, on both doors in one stroke** — REST + `POST /api/v1/actions/...` and the MCP `run_action` bridge: + + ```js + // a runAs:'system' flow, guarding before it acts on the subject row + if (context.recordLoadDenied === true) { /* the invoker cannot read this row */ } + ``` + + - **The exact producer shape, unchanged.** The one shared producer + (`loadActionSubjectRecord` → `actionRecordLoadSignal`) already returns + `{ recordLoadDenied?: true }`, and the flow door now spreads it as a + **sibling of `record`** — never a key on the record, and **absent**, never + `false`, when nothing was refused. So a flow reads it exactly as a handler + does, `recordLoadDenied === true`. + - **Both doors, structurally.** `dispatchFlowAction`'s wiring now takes the + load OUTCOME (`subject`) instead of a bare `record`, and derives both the + record and the signal from it. A caller can no longer forward the row while + dropping the verdict that says the caller could not read it — the omission is + a compile error rather than a guard silently inert one door over, which is + the defect the handler-face signal was filed for. + - **Purely additive.** Nothing is refused that was not refused before, no + existing key changes value, and the `recordId` stamp is deliberately kept: + `record.id` still arrives exactly as it did, which is why the flag — and not + `record.id` — is the authorization predicate. Whether the automation engine + *acts* on the key (a flow-level refusal, a step condition) is a separate + decision and is deliberately not part of this change. + - **`@objectstack/spec` (docs only).** The contract's "not yet populated on the + flow face" sentence is retired; no type changes. +- d8d2776: The tenant-scope and owning-business-unit system columns now render a localised display name on the `/meta` read exits, as the other platform-injected columns already did. + + `translateObject` carries a built-in label table for the columns the platform injects onto every eligible object, applied while a column still carries its injected English default, so a `zh-CN` / `ja-JP` / `es-ES` request never sees the English label on a custom object that ships no translation entries of its own. The table covered `owner_id`, `created_at`, `created_by`, `updated_at` and `updated_by` but not the two remaining injected columns, `organization_id` (`Organization`) and `owning_business_unit_id` (`Owning Business Unit`), so those two leaked English on every locale. Both rows are added, with the wording the platform bundles already use for the same columns on platform objects. The identity-stable column definitions are untouched, no new authorable key is introduced, and a label a tenant or author customised is still never overridden. +- 5eb24f8: The `PluginSchema` describe strings for `staticPath`, `slug` and `default` now name `ui`, the plugin type the enum actually accepts. + + `PluginSchema.type` is `z.enum(['standard', ...CORE_PLUGIN_TYPES])`, and `CORE_PLUGIN_TYPES` spells the frontend member `ui`. The three describe strings beside it still named `ui-plugin` — a value the same schema refuses two lines above. They are not merely stale: they read as instructions ("Required for `type="ui-plugin"`"), so an author or an agent following the field's own documentation writes a value that is then rejected, with the correct spelling nowhere in the sentence that sent them there. + + The strings now read `(Required for type="ui")`, `(Required for type="ui")` and `(Only one "ui" plugin can be default)`. Because these describes compile into the published JSON Schema and into the generated reference page, the correction reaches every consumer that reads field documentation out of the spec rather than out of the source file — the generated `content/docs/references/kernel/plugin.mdx` table now agrees with the `type` row printed directly above it, which previously listed `'ui'` among the accepted members while the three rows underneath told the reader to write `ui-plugin`. + + No accept/reject behaviour moves: `type: 'ui-plugin'` is refused before and after, `type: 'ui'` is accepted before and after, and no key is added, renamed or removed. The closed-set pin tests that name `ui-plugin` as a non-member are deliberately unchanged — they are the reason this correction is provable. +- 0db2947: Reference pages no longer print `@example` and `@category` tag lines as literal text. + + A module docblock is JSDoc, so its header carries block tags, and the reference-docs + renderer emitted a tag written on a prose line verbatim — 18 such lines reached 14 + customer-facing pages, as `@example Basic field mapping` above a code fence and + `@category Security` at the foot of four `system/` pages. `#13796` removed `@module` + from the page and left these two open, because a blanket `^@\w+` line filter would + have taken reader prose off the page and orphaned the fences below it. + + The verdict is per tag, and the axis is the payload rather than the spelling: + + - **`@example CAPTION` is REWRITTEN** into that caption, in bold, above the block it + captions — the shape `@see` already had (`See also: …`). 12 lines across 10 pages. + Bold rather than a heading because heading renumbering has already run by then, so + an emitted heading would carry a level chosen blind of the page, add entries to the + pages' tables of contents, and put a caption in reach of `check:docs-single-h1`. + - **A bare `@example` is DROPPED.** With no payload it is the `@module` case exactly, + and the fence beneath it is visibly an example without a line announcing one. 2 + lines (`studio/plugin`, `studio/object-designer`), both sitting against the + `check:skill-examples` opt-in marker that was already dropped there. + - **`@category VALUE` is DROPPED.** 4 lines, all reading `Security`, on four pages that + already sit under a `system/` section saying as much — and nothing in the repo reads + the tag: no typedoc or api-extractor (neither is used here), no search index, no + gate. Routing it into page frontmatter instead would publish a field with no + consumer. The tag stays in the source, where it is a legitimate JSDoc tag; only the + rendered page drops it. + + No schema behavior changes. The pins assert on the rendered fragment rather than on the + emitted `.mdx`, because `check:docs` compares the artifact against the source and + reproduced all 18 tag lines faithfully. +- aedbaef: `POST /sign-up/email` for an address that already has a `sys_user` row is refused explicitly, instead of answering 200 for a row that is never written (#15587) + + **This is a wire-behaviour change on one lane**: a call that answers `200 {"token":null,"user":{…}}` today answers `422 USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL` after this change. Nothing is newly admitted — the response that changes is one that reported a creation that never happened. + + ### What was measured + + Under audience posture `email_domain` (domain allowlisted, `selfRegistrationPermissionSet` resolvable), a sign-up for an address that already carried a `sys_user` row answered **200 with a freshly minted user id** and persisted nothing: no new `sys_user`, no `sys_account`, and the next sign-in a `401` with nothing anywhere explaining it. The same call on the same population under the `invite_only` default was refused honestly with `422 USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL`. An operator, a provisioning script or the console reading the status code concludes the account exists — and this sits directly on the recovery path a locked-out deployment walks, where widening the posture to let a seeded person register is exactly the remedy an operator is pointed at. + + ### The mechanism + + better-auth's sign-up route computes `shouldReturnGenericDuplicateResponse = requireEmailVerification || autoSignIn === false` and, when it is on, answers a duplicate with a synthetic in-memory user instead of throwing. **No insert is attempted and nothing is swallowed**: the vendor's `findUserByEmail` short-circuits ahead of `createUser`, which is why no row and no credential appear. + + The posture is not itself the cause — it is only what arms the shield: a posture that permits self-registration **forces** `requireEmailVerification` on. Holding the posture constant at the `invite_only` default and moving only that flag reproduces the divergence exactly, which also means the defect was never confined to the widened postures: `emailAndPassword.autoSignIn: false` arms the same shield under any posture. + + ### The fix + + The uniqueness refusal is raised on the `/sign-up/email` before-hook, the same seam and the same reason the audience-posture refusal is already raised there, and built from better-auth's own `BASE_ERROR_CODES` entry so both lanes answer byte-identically. + + **Order is load-bearing: it runs only for a caller the posture already admitted.** Asking uniqueness first would hand an uninvited stranger an account-existence oracle under the `invite_only` default (422 for a real address versus 403 for an unknown one). After the gate, `invite_only` is untouched — a stranger still gets `SELF_REGISTRATION_CLOSED` and learns nothing. + + **Operators of `open` / `email_domain` should know what the honest refusal costs:** on those postures a caller the audience gate admits can now distinguish an address that has an account from one that does not, where the synthetic 200 previously hid it. That is the disclosure the `invite_only` lane has always made to an invitation holder, and the platform's answer for a widened posture is now the same fact rather than a false receipt. + + `USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL` is registered in the ADR-0112 error-code ledger under `@objectstack/plugin-auth`: the platform now **emits** it rather than only passing it through, and an emitted-but-unregistered code is the silent fourth state that ledger exists to prevent. +- 40a44b9: fix(spec): the `undefined` comparand refusal prescribes the null predicate by its ruled spellings (#14426) + + `parseFilterAST`'s comparand-type door refuses an `undefined` comparand at every + position. Its prescription read "Write null for the null predicate, or omit the + key" — position-agnostic advice that, followed at `{ $gt: undefined }`, produced + `{ $gt: null }`, which the 2026-09-01 ruling refuses one door over (and, at an + `$in` / `$nin` / `$between` member, produced the list shapes refused on + 2026-08-31). Two loud refusals to reach one right answer. + + The sentence now names the null predicate by its complete spellings — + `{"$eq": null}` / `{"$ne": null}` — or omit the key, so following it never lands + in a refusal at any position the sentence is emitted at. No accept/refuse + behaviour changes: same envelope (`INVALID_FILTER` / 400), same path, same + accepted-set and NOT-applied sentences. + ## 17.3.0 ### Minor Changes diff --git a/packages/spec/package.json b/packages/spec/package.json index c610d33150..d9cda605f9 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/spec", - "version": "17.3.0", + "version": "17.4.0", "description": "ObjectStack Protocol & Specification - TypeScript Interfaces, JSON Schemas, and Convention Configurations", "license": "Apache-2.0", "main": "dist/index.js", diff --git a/packages/triggers/trigger-api/CHANGELOG.md b/packages/triggers/trigger-api/CHANGELOG.md index 6c3234d589..52b03164ca 100644 --- a/packages/triggers/trigger-api/CHANGELOG.md +++ b/packages/triggers/trigger-api/CHANGELOG.md @@ -1,5 +1,46 @@ # @objectstack/trigger-api +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/triggers/trigger-api/package.json b/packages/triggers/trigger-api/package.json index 96de46f7b9..ff6bb62d24 100644 --- a/packages/triggers/trigger-api/package.json +++ b/packages/triggers/trigger-api/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-api", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Inbound HTTP/webhook flow trigger for ObjectStack — per-flow HMAC-verified endpoints with queue-backed ingestion (ADR-0041)", "main": "dist/index.js", diff --git a/packages/triggers/trigger-record-change/CHANGELOG.md b/packages/triggers/trigger-record-change/CHANGELOG.md index 8cf2a4a4d2..4ec7a9bcfd 100644 --- a/packages/triggers/trigger-record-change/CHANGELOG.md +++ b/packages/triggers/trigger-record-change/CHANGELOG.md @@ -1,5 +1,97 @@ # @objectstack/plugin-trigger-record-change +## 17.4.0 + +### Patch Changes + +- 4f85e4d: fix(trigger-record-change)!: the record handed to a record-change flow no longer aliases the write's payload (#14744) + + + + **BREAKING** for a flow whose `script` node mutates a NESTED value of the + triggering record IN PLACE: that mutation no longer affects the write the flow + was triggered by. Shipped as `patch` — this change moves no public surface (no + exported symbol, no accepted key or value), and under the maintainer's + 2026-09-04 rule (decision batch #35, on #15294) a `fix(` that changes no public + surface stays `patch`, with breaking-ness carried by this banner and the + ADR-0087 disposition rather than by the level. Maintainer ruling 2026-09-04 on + #14744 (decision batch #38, verbatim 「同意」), adopting option A. + + **Why.** `buildContext` builds the flow's `record` as a shallow overlay of the + pre-image, the mutation payload and the after-row. The top-level object was + new, so a flow ASSIGNING a top-level key reached nothing — but every nested + value in it was the engine's own object, shared by reference. One of those is + `ctx.input.data`, and on a `multi: true` update ADR-0058 Addendum II D3 hands + every per-row context that same payload object, which is the SET clause of the + single `updateMany`. A registered function doing `record.tags.push(...)` + therefore wrote the SET clause without assigning any key: every dispatch's + contribution landed on EVERY matched row, including values derived from another + row's pre-image, and #14099's key-set refusal could not see it because no key + was assigned. Measured end to end on the memory driver and on + `@objectstack/driver-sql` (#15356). + + **What changes.** Both flow-facing roots — `record` (and the `params` alias of + it) and `previous` — are decoupled from the engine's state before the flow + runs. Arrays, plain objects, `Date`, `RegExp`, `Map` and `Set` are copied; + primitives, functions and other class instances are shared, which is the + documented and pinned boundary. A flow still mutates its roots freely and still + observes its own writes for the rest of the run; those writes simply reach + nothing outside it. `previous` is decoupled in the same stroke because it is the + engine's single pre-image object and the same hook context reaches every other + flow bound to the same write. + + **What does NOT change.** The engine's write shape. ADR-0058 Addendum II D3 + stands untouched: one payload still serves N rows and every per-row context is + still handed that one object. #14099's key-set refusal is untouched and is not + widened — a hook that assigns the same key with per-row values still passes it, + and divergent key sets are still refused whole. Flow metadata with no registered + function reached nothing before this change and reaches nothing after it: + assignment nodes write the run's variable map, and `update_record` issues its own + by-id write. Lookup expansion (`config.expand`) still grafts onto the record the + flow holds. + + **Consumer note.** A flow that relied on an in-place nested mutation to persist + — which on a by-id write did persist, and on a `multi: true` write corrupted + every other matched row — writes the record with the `update_record` node + instead. That node is the supported per-row write and is unaffected by this + change. +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/triggers/trigger-record-change/package.json b/packages/triggers/trigger-record-change/package.json index 74ea8b3a37..9b07f7b873 100644 --- a/packages/triggers/trigger-record-change/package.json +++ b/packages/triggers/trigger-record-change/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-record-change", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Record-change flow trigger for ObjectStack — auto-launches flows on object insert/update/delete via ObjectQL lifecycle hooks (ADR-0018)", "main": "dist/index.js", diff --git a/packages/triggers/trigger-schedule/CHANGELOG.md b/packages/triggers/trigger-schedule/CHANGELOG.md index 771882421b..adfbeaf4f4 100644 --- a/packages/triggers/trigger-schedule/CHANGELOG.md +++ b/packages/triggers/trigger-schedule/CHANGELOG.md @@ -1,5 +1,46 @@ # @objectstack/plugin-trigger-schedule +## 17.4.0 + +### Patch Changes + +- Updated dependencies [2ed6be6] +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/spec@17.4.0 + ## 17.3.0 ### Patch Changes diff --git a/packages/triggers/trigger-schedule/package.json b/packages/triggers/trigger-schedule/package.json index ebfe7200d7..bf966bcd7e 100644 --- a/packages/triggers/trigger-schedule/package.json +++ b/packages/triggers/trigger-schedule/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/trigger-schedule", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Schedule flow trigger for ObjectStack — auto-launches flows on a cron/interval/once schedule via the IJobService (ADR-0018)", "main": "dist/index.js", diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index d32b82b11f..716f743691 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,63 @@ # @objectstack/types +## 17.4.0 + +### Minor Changes + +- 3d3f60e: An approval decision that lands while its flow run strands now says so in fields, not only in prose. + + `POST /api/v1/approvals/requests/{id}/reject` — and its sibling decision doors — could produce three coexisting outcomes from one call: the caller read HTTP 500, the request row **was** in its terminal status and had left the pending inbox, and the workflow run was stranded. A caller reading 500 has one honest inference available — "the rejection did not happen" — and it was the wrong one, so scripts and operators retried or escalated against a decision that was already durable. The only carrier of the truth was English prose in `error`, so finding the affected run meant regexing a run id out of a sentence, and nothing said whether that run could be repaired at all. + + The 500 stays. A recorded decision whose flow never advances is still a failure and is still reported as one; the door does not become atomic and no decision is ever rolled back. What changed is that it stops discarding what the engine already said: + + - **The `RESUME_FAILED` body gains four fields**, additively — `finalized` (always `true`: the decision stands), `decision`, `runId`, and `repairable`. Existing consumers see the same `code`, the same `error` and the same status. + - **`repairable` carries the engine's own discriminator** — `AutomationResult.status === 'stranded'`, the state stamped on exactly the exit that journals a repair snapshot. `false` is the answer for every other failure, including a lost run: absence of the signal is not repairability, and a repair verb that would refuse is worse than no promise. + - **`serviceResume` carries `status`** through to the door. It previously read only `success` / `code` / `error`, and the stranded exit reports a `status` and no `code` at all — so the platform's own repairability signal died one line before the envelope was built. + + `@objectstack/types` gains `strandedDecisionFailure` / `strandedDecisionDetails` and the `StrandedDecisionDetails` type — the constructor and its recogniser in one module, so the producing service and the REST door cannot drift. A `RESUME_FAILED` raised without that carrier answers exactly the body it always did; the door never synthesises the envelope. + +### Patch Changes + +- 088f761: `createHostImporter` now loads the `import` build of an ALIASED dual-published package, instead of silently keeping its `require` build. + + An alias declaration — `{"dependencies": {"foo": "npm:bar@1"}}` — installs a package whose manifest is named `bar` under the key `foo`. On the path where CommonJS resolution SUCCEEDS, the importer re-decides only the CONDITION (it asks the package which entry an `import()` gets, so the caller's ESM chain and this load share one instance). That re-decision recognised the package root by walking up from the resolved entry until it found a manifest named after the DECLARATION KEY — `foo` — while an aliased install's manifest is named `bar`. The walk therefore never matched, the re-decision produced nothing, and the load fell back to whatever the CommonJS resolver had answered: the `require` condition. + + For an aliased dual publish that left the process holding two live copies of one package — the CommonJS build behind the host importer, the `import` build in the caller's own chain — which is exactly the split the condition re-decision exists to remove: a plugin registry, a singleton kernel, a module-level cache, one copy each. + + The expectation now comes from the host's own declaration (`npm:name@range`, aliased `workspace:name@range`), the same reading the ESM-only fallback finder has used since it learned about aliases. Nothing about the check's strictness moves: an alias naming one package still does not license a directory holding another, and a non-aliased declaration is still verified against its key. Declarations that name a LOCATION rather than a package (`link:`, `file:`) carry no name to expect, so they keep today's behaviour unchanged. + + Measured population for the behaviour change: zero aliased declarations exist across this workspace's 875 dependency declarations, and 867 of 867 installed declarations already match their key — no ordinary, non-aliased install reaches this path. +- Updated dependencies [ca326b5] +- Updated dependencies [8f404a5] +- Updated dependencies [3e3ecb0] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [a84e1ce] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [5eb24f8] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [5f7fa1d] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [c2a336c] +- Updated dependencies [9408b7f] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/spec@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/types/package.json b/packages/types/package.json index daccf968b9..24f0c8694b 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/types", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Shared interfaces describing the ObjectStack Runtime environment", "main": "dist/index.js", diff --git a/packages/verify/CHANGELOG.md b/packages/verify/CHANGELOG.md index a1cc9fe274..179ddce2b4 100644 --- a/packages/verify/CHANGELOG.md +++ b/packages/verify/CHANGELOG.md @@ -1,5 +1,125 @@ # @objectstack/verify +## 17.4.0 + +### Patch Changes + +- c550baf: fix(verify): `os verify` no longer reports a green run over a multi-package app it measured nothing about + + Every reader in this package took the artifact's **flattened** top level and + nothing else. A multi-package app whose definitions live under `packages[]` — + the shape ADR-0130 D4's option B emits — therefore reached `deriveCrudCases` + with no objects and no datasources, and reached `rlsProbePermissionSet` and + `declaredPositionNames` with no objects and no positions. Nothing threw. The run + derived zero CRUD round-trip cases, built an empty RLS probe permission set, + minted no persona for any declared position, and printed `✓ verify passed`. + + That is the most expensive place in the platform for a false green: `verify`'s + entire job is to be the thing that notices. A missing collection is at least + missing — zero coverage dressed as a passing run is not. + + The four reads now resolve through `resolveArtifactPackageOrder` + (`@objectstack/core`, ADR-0130 D4+D5), **flattened top level first**: + + - `deriveCrudCases` — the objects it derives cases for, and the datasource-by- + name map behind ADR-0015's double write gate. Both, because objects alone + would leave a write-opted-in federated object judged against an empty + datasource map and reported read-only, i.e. skipped by a verifier that says it + covered it. + - `declaredPositionNames` — one RLS persona per declared position. + - `rlsProbePermissionSet` — the object grants and the owner-scoped narrowing + that are what make an RLS run a probe rather than a report about the object + gate. + + The top-level read still answers first and is returned untouched, so an app on + today's additive artifact gets a bit-identical answer, and a stack that declares + an empty collection (`objects: []` is truthy) still gets an empty one. Only a + top level that does not carry the key at all consults `packages[]`. A malformed + `packages` array now surfaces `resolveArtifactPackageOrder`'s ADR-0112 refusal + instead of reading as "this app declares nothing". +- Updated dependencies [2ed6be6] +- Updated dependencies [54bb2f1] +- Updated dependencies [98191d2] +- Updated dependencies [ca326b5] +- Updated dependencies [f1a1028] +- Updated dependencies [8f404a5] +- Updated dependencies [954cb0b] +- Updated dependencies [a56baa2] +- Updated dependencies [3e3ecb0] +- Updated dependencies [8e500f2] +- Updated dependencies [4b3955e] +- Updated dependencies [b548e43] +- Updated dependencies [13c48c2] +- Updated dependencies [d30ccb9] +- Updated dependencies [6f94458] +- Updated dependencies [6e67b86] +- Updated dependencies [85a2459] +- Updated dependencies [e89fa92] +- Updated dependencies [56fe8c2] +- Updated dependencies [4bc9821] +- Updated dependencies [65846bc] +- Updated dependencies [ef3a138] +- Updated dependencies [fa125f3] +- Updated dependencies [a646120] +- Updated dependencies [6f1ce7d] +- Updated dependencies [2c753fe] +- Updated dependencies [52804cd] +- Updated dependencies [fa85759] +- Updated dependencies [5f7fa1d] +- Updated dependencies [088f761] +- Updated dependencies [a84e1ce] +- Updated dependencies [a84e1ce] +- Updated dependencies [65846bc] +- Updated dependencies [bf1054a] +- Updated dependencies [d8d2776] +- Updated dependencies [222dc0f] +- Updated dependencies [f502898] +- Updated dependencies [7bf96cf] +- Updated dependencies [3bd9b34] +- Updated dependencies [d0ee598] +- Updated dependencies [26144c2] +- Updated dependencies [9e9f03a] +- Updated dependencies [5eb24f8] +- Updated dependencies [c64e65f] +- Updated dependencies [414c1fc] +- Updated dependencies [0db2947] +- Updated dependencies [e13ede8] +- Updated dependencies [f5cc78b] +- Updated dependencies [8a12067] +- Updated dependencies [ee32e1c] +- Updated dependencies [8744de9] +- Updated dependencies [a646120] +- Updated dependencies [d4f9b2a] +- Updated dependencies [5f7fa1d] +- Updated dependencies [6b8c677] +- Updated dependencies [87f0ccc] +- Updated dependencies [aedbaef] +- Updated dependencies [a727043] +- Updated dependencies [c2a336c] +- Updated dependencies [5964124] +- Updated dependencies [9408b7f] +- Updated dependencies [ec0a6e7] +- Updated dependencies [2bb0614] +- Updated dependencies [6c439f2] +- Updated dependencies [3d3f60e] +- Updated dependencies [581d8f8] +- Updated dependencies [40a44b9] + - @objectstack/core@17.4.0 + - @objectstack/objectql@17.4.0 + - @objectstack/service-analytics@17.4.0 + - @objectstack/runtime@17.4.0 + - @objectstack/spec@17.4.0 + - @objectstack/service-automation@17.4.0 + - @objectstack/plugin-auth@17.4.0 + - @objectstack/platform-objects@17.4.0 + - @objectstack/rest@17.4.0 + - @objectstack/plugin-hono-server@17.4.0 + - @objectstack/types@17.4.0 + - @objectstack/plugin-security@17.4.0 + - @objectstack/service-settings@17.4.0 + - @objectstack/plugin-sharing@17.4.0 + - @objectstack/service-datasource@17.4.0 + ## 17.3.0 ### Minor Changes diff --git a/packages/verify/package.json b/packages/verify/package.json index 3ebe106911..b696b0e112 100644 --- a/packages/verify/package.json +++ b/packages/verify/package.json @@ -1,6 +1,6 @@ { "name": "@objectstack/verify", - "version": "17.3.0", + "version": "17.4.0", "license": "Apache-2.0", "description": "Boot any ObjectStack app in-process and verify it through the real HTTP stack — auto-derived CRUD round-trip fidelity plus the cross-owner RLS invariant. Catches runtime regressions that static checks miss.", "type": "module",