Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .changeset/liveness-rest-server-config-sub-objects.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
---
"@objectstack/spec": patch
---

chore(spec): govern the four `RestServerConfig` sub-objects in the liveness ledger (#14369)

The `liveness/` ledgers ship inside this package's npm tarball (they are named in
`files`), so this is a published-data change even though no runtime behaviour
moves, no schema key changes spelling, and `packages/spec/src/api/rest-server.zod.ts`
is not edited at all.

Four new ledger files — `crud_endpoints.json`, `metadata_endpoints.json`,
`batch_endpoints.json`, `route_generation.json` — classify all 32 authorable
properties of `CrudEndpointsConfigSchema`, `MetadataEndpointsConfigSchema`,
`BatchEndpointsConfigSchema` and `RouteGenerationConfigSchema`, the four
`RestServerConfig` sub-objects a host writes when it constructs the REST server.
They are enrolled through the gate's `SPEC_ONLY_SCHEMAS` override, the route
`query` / `qa` / `manifest` already take: server configuration is neither a
metadata item nor a request body nor a manifest, so no registry has ever held it
and no ratchet rooted in one could ask who reads it.

Seventeen properties are `live` with a symbol-anchored consumer and a producer
pointer at the normalizer that threads the authored value into `this.config`.
Fifteen are `dead` — the ten keys the census filed with this card measured, with
the two container keys (`crud.patterns`, `routes.overrides`) expanded into a row
per member. `routes` is dead entire: `excludeObjects: ['sys_log']` excludes
nothing and `nameTransform: 'plural'` still mounts every route under the raw
object name. `metadata.endpoints.schema` and `batch.operations.upsertMany` are
switches for routes that were never built — no path ending in `/schema` is
mounted anywhere in `packages/rest/src`, and the protocol has no `upsertManyData`
counterpart to its three sibling batch methods.

What this records, and what it deliberately does not. #11984 made
`RestServer.normalizeConfig` PARSE and CONSUME these four sub-objects instead of
casting them, so an out-of-enum or out-of-range value is now refused at
construction. That settles accept/reject and nothing else: executing a declared
contract does not give a key a consumer. No key is removed, enforced, deprecated
or re-described here. The enforce-or-remove call per dead key (ADR-0049) is a
follow-up on the human floor — the enforce route is a feature per key, and
`routes.excludeObjects` is advertised in `RestServerConfigSchema`'s own
`@example`, which makes its removal a capability retirement rather than a cleanup.

Rooted on the four sub-schemas rather than on `RestServerConfigSchema` itself,
which is measurement rather than taste: the ledger walk drills exactly ONE level,
so with the whole config as the root the sub-objects would BE the drilled level
and `metadata.endpoints.schema` / `batch.operations.upsertMany` would have no row
of their own — their container's blanket `live` (three of four members gate a real
route mount) silently covering a dead key, which is the #4956 shape in the file
written to end it. `RestApiConfigSchema` (the fifth sub-object, `api`) is not
enrolled: its consumption seam is still validate-only and is the subject of its
own card, so a census of it would record a half that is about to move.

<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is removed, renamed or re-described: this change adds ledger rows and a gate enrolment, and every key it classifies keeps the exact spelling, type, default and describe() it had. There is no source for `objectstack migrate meta` to rewrite, because no author's config becomes invalid or becomes valid as a result. -->
6 changes: 5 additions & 1 deletion packages/spec/liveness/README.md

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions packages/spec/liveness/batch_endpoints.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
{
"type": "batch_endpoints",
"_note": "BatchEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#BatchEndpointsConfigSchema, the `batch` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `maxBatchSize`, `enableBatchEndpoint` and three of the four `operations.*` switches are read; `operations.upsertMany` and `defaultAtomic` are not. This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness.",
"props": {
"maxBatchSize": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`const maxBatch = batch.maxBatchSize ?? 200` — the cap every batch request is measured against)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Live and load-bearing since #11984 gave it a real parse: before that a configured `0` was the live cap, because `0` is not nullish."
},
"enableBatchEndpoint": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (batch.enableBatchEndpoint && this.protocol.batchData)` gates the generic POST /data/:object/batch mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount, and the second conjunct is a runtime capability rather than a second authored input, so no producer beyond the config threading is needed."
},
"operations": {
"children": {
"createMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.createMany && this.protocol.createManyData)` gates the POST /data/:object/createMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"updateMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.updateMany && this.protocol.updateManyData)` gates the POST /data/:object/updateMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"deleteMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.deleteMany && this.protocol.deleteManyData)` gates the POST /data/:object/deleteMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"upsertMany": {
"status": "dead",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"note": "0 read sites at 2514d49f3. Its three siblings each gate a route mount; this one gates nothing, because there is no upsertMany route to gate — the switch was declared for a batch operation that was never built (`this.protocol` has createManyData / updateManyData / deleteManyData and no upsert counterpart). `operations.upsertMany: false` therefore disables nothing and `true` enables nothing."
}
}
},
"defaultAtomic": {
"status": "dead",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"note": "0 read sites at 2514d49f3. Repo-wide there are no reads outside packages/spec and rest-server.ts either. The key is normalized into `this.config.batch.defaultAtomic` and no batch handler consults it, so `batch.defaultAtomic: false` changes no batch's transaction mode. A switch whose describe() promises a transaction default while the transaction default is decided elsewhere is the false-compliance shape this ledger exists to surface, which is why it is recorded rather than left to a reader's grep."
}
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .changeset/liveness-rest-server-config-sub-objects.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
---
"@objectstack/spec": patch
---

chore(spec): govern the four `RestServerConfig` sub-objects in the liveness ledger (#14369)

The `liveness/` ledgers ship inside this package's npm tarball (they are named in
`files`), so this is a published-data change even though no runtime behaviour
moves, no schema key changes spelling, and `packages/spec/src/api/rest-server.zod.ts`
is not edited at all.

Four new ledger files — `crud_endpoints.json`, `metadata_endpoints.json`,
`batch_endpoints.json`, `route_generation.json` — classify all 32 authorable
properties of `CrudEndpointsConfigSchema`, `MetadataEndpointsConfigSchema`,
`BatchEndpointsConfigSchema` and `RouteGenerationConfigSchema`, the four
`RestServerConfig` sub-objects a host writes when it constructs the REST server.
They are enrolled through the gate's `SPEC_ONLY_SCHEMAS` override, the route
`query` / `qa` / `manifest` already take: server configuration is neither a
metadata item nor a request body nor a manifest, so no registry has ever held it
and no ratchet rooted in one could ask who reads it.

Seventeen properties are `live` with a symbol-anchored consumer and a producer
pointer at the normalizer that threads the authored value into `this.config`.
Fifteen are `dead` — the ten keys the census filed with this card measured, with
the two container keys (`crud.patterns`, `routes.overrides`) expanded into a row
per member. `routes` is dead entire: `excludeObjects: ['sys_log']` excludes
nothing and `nameTransform: 'plural'` still mounts every route under the raw
object name. `metadata.endpoints.schema` and `batch.operations.upsertMany` are
switches for routes that were never built — no path ending in `/schema` is
mounted anywhere in `packages/rest/src`, and the protocol has no `upsertManyData`
counterpart to its three sibling batch methods.

What this records, and what it deliberately does not. #11984 made
`RestServer.normalizeConfig` PARSE and CONSUME these four sub-objects instead of
casting them, so an out-of-enum or out-of-range value is now refused at
construction. That settles accept/reject and nothing else: executing a declared
contract does not give a key a consumer. No key is removed, enforced, deprecated
or re-described here. The enforce-or-remove call per dead key (ADR-0049) is a
follow-up on the human floor — the enforce route is a feature per key, and
`routes.excludeObjects` is advertised in `RestServerConfigSchema`'s own
`@example`, which makes its removal a capability retirement rather than a cleanup.

Rooted on the four sub-schemas rather than on `RestServerConfigSchema` itself,
which is measurement rather than taste: the ledger walk drills exactly ONE level,
so with the whole config as the root the sub-objects would BE the drilled level
and `metadata.endpoints.schema` / `batch.operations.upsertMany` would have no row
of their own — their container's blanket `live` (three of four members gate a real
route mount) silently covering a dead key, which is the #4956 shape in the file
written to end it. `RestApiConfigSchema` (the fifth sub-object, `api`) is not
enrolled: its consumption seam is still validate-only and is the subject of its
own card, so a census of it would record a half that is about to move.

<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is removed, renamed or re-described: this change adds ledger rows and a gate enrolment, and every key it classifies keeps the exact spelling, type, default and describe() it had. There is no source for `objectstack migrate meta` to rewrite, because no author's config becomes invalid or becomes valid as a result. -->
6 changes: 5 additions & 1 deletion packages/spec/liveness/README.md

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions packages/spec/liveness/batch_endpoints.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
{
"type": "batch_endpoints",
"_note": "BatchEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#BatchEndpointsConfigSchema, the `batch` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `maxBatchSize`, `enableBatchEndpoint` and three of the four `operations.*` switches are read; `operations.upsertMany` and `defaultAtomic` are not. This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness.",
"props": {
"maxBatchSize": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`const maxBatch = batch.maxBatchSize ?? 200` — the cap every batch request is measured against)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Live and load-bearing since #11984 gave it a real parse: before that a configured `0` was the live cap, because `0` is not nullish."
},
"enableBatchEndpoint": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (batch.enableBatchEndpoint && this.protocol.batchData)` gates the generic POST /data/:object/batch mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount, and the second conjunct is a runtime capability rather than a second authored input, so no producer beyond the config threading is needed."
},
"operations": {
"children": {
"createMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.createMany && this.protocol.createManyData)` gates the POST /data/:object/createMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"updateMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.updateMany && this.protocol.updateManyData)` gates the POST /data/:object/updateMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"deleteMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.deleteMany && this.protocol.deleteManyData)` gates the POST /data/:object/deleteMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"upsertMany": {
"status": "dead",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"note": "0 read sites at 2514d49f3. Its three siblings each gate a route mount; this one gates nothing, because there is no upsertMany route to gate — the switch was declared for a batch operation that was never built (`this.protocol` has createManyData / updateManyData / deleteManyData and no upsert counterpart). `operations.upsertMany: false` therefore disables nothing and `true` enables nothing."
}
}
},
"defaultAtomic": {
"status": "dead",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"note": "0 read sites at 2514d49f3. Repo-wide there are no reads outside packages/spec and rest-server.ts either. The key is normalized into `this.config.batch.defaultAtomic` and no batch handler consults it, so `batch.defaultAtomic: false` changes no batch's transaction mode. A switch whose describe() promises a transaction default while the transaction default is decided elsewhere is the false-compliance shape this ledger exists to surface, which is why it is recorded rather than left to a reader's grep."
}
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .changeset/liveness-rest-server-config-sub-objects.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
---
"@objectstack/spec": patch
---

chore(spec): govern the four `RestServerConfig` sub-objects in the liveness ledger (#14369)

The `liveness/` ledgers ship inside this package's npm tarball (they are named in
`files`), so this is a published-data change even though no runtime behaviour
moves, no schema key changes spelling, and `packages/spec/src/api/rest-server.zod.ts`
is not edited at all.

Four new ledger files — `crud_endpoints.json`, `metadata_endpoints.json`,
`batch_endpoints.json`, `route_generation.json` — classify all 32 authorable
properties of `CrudEndpointsConfigSchema`, `MetadataEndpointsConfigSchema`,
`BatchEndpointsConfigSchema` and `RouteGenerationConfigSchema`, the four
`RestServerConfig` sub-objects a host writes when it constructs the REST server.
They are enrolled through the gate's `SPEC_ONLY_SCHEMAS` override, the route
`query` / `qa` / `manifest` already take: server configuration is neither a
metadata item nor a request body nor a manifest, so no registry has ever held it
and no ratchet rooted in one could ask who reads it.

Seventeen properties are `live` with a symbol-anchored consumer and a producer
pointer at the normalizer that threads the authored value into `this.config`.
Fifteen are `dead` — the ten keys the census filed with this card measured, with
the two container keys (`crud.patterns`, `routes.overrides`) expanded into a row
per member. `routes` is dead entire: `excludeObjects: ['sys_log']` excludes
nothing and `nameTransform: 'plural'` still mounts every route under the raw
object name. `metadata.endpoints.schema` and `batch.operations.upsertMany` are
switches for routes that were never built — no path ending in `/schema` is
mounted anywhere in `packages/rest/src`, and the protocol has no `upsertManyData`
counterpart to its three sibling batch methods.

What this records, and what it deliberately does not. #11984 made
`RestServer.normalizeConfig` PARSE and CONSUME these four sub-objects instead of
casting them, so an out-of-enum or out-of-range value is now refused at
construction. That settles accept/reject and nothing else: executing a declared
contract does not give a key a consumer. No key is removed, enforced, deprecated
or re-described here. The enforce-or-remove call per dead key (ADR-0049) is a
follow-up on the human floor — the enforce route is a feature per key, and
`routes.excludeObjects` is advertised in `RestServerConfigSchema`'s own
`@example`, which makes its removal a capability retirement rather than a cleanup.

Rooted on the four sub-schemas rather than on `RestServerConfigSchema` itself,
which is measurement rather than taste: the ledger walk drills exactly ONE level,
so with the whole config as the root the sub-objects would BE the drilled level
and `metadata.endpoints.schema` / `batch.operations.upsertMany` would have no row
of their own — their container's blanket `live` (three of four members gate a real
route mount) silently covering a dead key, which is the #4956 shape in the file
written to end it. `RestApiConfigSchema` (the fifth sub-object, `api`) is not
enrolled: its consumption seam is still validate-only and is the subject of its
own card, so a census of it would record a half that is about to move.

<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is removed, renamed or re-described: this change adds ledger rows and a gate enrolment, and every key it classifies keeps the exact spelling, type, default and describe() it had. There is no source for `objectstack migrate meta` to rewrite, because no author's config becomes invalid or becomes valid as a result. -->
6 changes: 5 additions & 1 deletion packages/spec/liveness/README.md

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions packages/spec/liveness/batch_endpoints.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
{
"type": "batch_endpoints",
"_note": "BatchEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#BatchEndpointsConfigSchema, the `batch` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `maxBatchSize`, `enableBatchEndpoint` and three of the four `operations.*` switches are read; `operations.upsertMany` and `defaultAtomic` are not. This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness.",
"props": {
"maxBatchSize": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`const maxBatch = batch.maxBatchSize ?? 200` — the cap every batch request is measured against)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Live and load-bearing since #11984 gave it a real parse: before that a configured `0` was the live cap, because `0` is not nullish."
},
"enableBatchEndpoint": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (batch.enableBatchEndpoint && this.protocol.batchData)` gates the generic POST /data/:object/batch mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount, and the second conjunct is a runtime capability rather than a second authored input, so no producer beyond the config threading is needed."
},
"operations": {
"children": {
"createMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.createMany && this.protocol.createManyData)` gates the POST /data/:object/createMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"updateMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.updateMany && this.protocol.updateManyData)` gates the POST /data/:object/updateMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"deleteMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.deleteMany && this.protocol.deleteManyData)` gates the POST /data/:object/deleteMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"upsertMany": {
"status": "dead",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"note": "0 read sites at 2514d49f3. Its three siblings each gate a route mount; this one gates nothing, because there is no upsertMany route to gate — the switch was declared for a batch operation that was never built (`this.protocol` has createManyData / updateManyData / deleteManyData and no upsert counterpart). `operations.upsertMany: false` therefore disables nothing and `true` enables nothing."
}
}
},
"defaultAtomic": {
"status": "dead",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"note": "0 read sites at 2514d49f3. Repo-wide there are no reads outside packages/spec and rest-server.ts either. The key is normalized into `this.config.batch.defaultAtomic` and no batch handler consults it, so `batch.defaultAtomic: false` changes no batch's transaction mode. A switch whose describe() promises a transaction default while the transaction default is decided elsewhere is the false-compliance shape this ledger exists to surface, which is why it is recorded rather than left to a reader's grep."
}
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .changeset/liveness-rest-server-config-sub-objects.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
---
"@objectstack/spec": patch
---

chore(spec): govern the four `RestServerConfig` sub-objects in the liveness ledger (#14369)

The `liveness/` ledgers ship inside this package's npm tarball (they are named in
`files`), so this is a published-data change even though no runtime behaviour
moves, no schema key changes spelling, and `packages/spec/src/api/rest-server.zod.ts`
is not edited at all.

Four new ledger files — `crud_endpoints.json`, `metadata_endpoints.json`,
`batch_endpoints.json`, `route_generation.json` — classify all 32 authorable
properties of `CrudEndpointsConfigSchema`, `MetadataEndpointsConfigSchema`,
`BatchEndpointsConfigSchema` and `RouteGenerationConfigSchema`, the four
`RestServerConfig` sub-objects a host writes when it constructs the REST server.
They are enrolled through the gate's `SPEC_ONLY_SCHEMAS` override, the route
`query` / `qa` / `manifest` already take: server configuration is neither a
metadata item nor a request body nor a manifest, so no registry has ever held it
and no ratchet rooted in one could ask who reads it.

Seventeen properties are `live` with a symbol-anchored consumer and a producer
pointer at the normalizer that threads the authored value into `this.config`.
Fifteen are `dead` — the ten keys the census filed with this card measured, with
the two container keys (`crud.patterns`, `routes.overrides`) expanded into a row
per member. `routes` is dead entire: `excludeObjects: ['sys_log']` excludes
nothing and `nameTransform: 'plural'` still mounts every route under the raw
object name. `metadata.endpoints.schema` and `batch.operations.upsertMany` are
switches for routes that were never built — no path ending in `/schema` is
mounted anywhere in `packages/rest/src`, and the protocol has no `upsertManyData`
counterpart to its three sibling batch methods.

What this records, and what it deliberately does not. #11984 made
`RestServer.normalizeConfig` PARSE and CONSUME these four sub-objects instead of
casting them, so an out-of-enum or out-of-range value is now refused at
construction. That settles accept/reject and nothing else: executing a declared
contract does not give a key a consumer. No key is removed, enforced, deprecated
or re-described here. The enforce-or-remove call per dead key (ADR-0049) is a
follow-up on the human floor — the enforce route is a feature per key, and
`routes.excludeObjects` is advertised in `RestServerConfigSchema`'s own
`@example`, which makes its removal a capability retirement rather than a cleanup.

Rooted on the four sub-schemas rather than on `RestServerConfigSchema` itself,
which is measurement rather than taste: the ledger walk drills exactly ONE level,
so with the whole config as the root the sub-objects would BE the drilled level
and `metadata.endpoints.schema` / `batch.operations.upsertMany` would have no row
of their own — their container's blanket `live` (three of four members gate a real
route mount) silently covering a dead key, which is the #4956 shape in the file
written to end it. `RestApiConfigSchema` (the fifth sub-object, `api`) is not
enrolled: its consumption seam is still validate-only and is the subject of its
own card, so a census of it would record a half that is about to move.

<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is removed, renamed or re-described: this change adds ledger rows and a gate enrolment, and every key it classifies keeps the exact spelling, type, default and describe() it had. There is no source for `objectstack migrate meta` to rewrite, because no author's config becomes invalid or becomes valid as a result. -->
6 changes: 5 additions & 1 deletion packages/spec/liveness/README.md

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions packages/spec/liveness/batch_endpoints.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
{
"type": "batch_endpoints",
"_note": "BatchEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#BatchEndpointsConfigSchema, the `batch` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `maxBatchSize`, `enableBatchEndpoint` and three of the four `operations.*` switches are read; `operations.upsertMany` and `defaultAtomic` are not. This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness.",
"props": {
"maxBatchSize": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`const maxBatch = batch.maxBatchSize ?? 200` — the cap every batch request is measured against)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Live and load-bearing since #11984 gave it a real parse: before that a configured `0` was the live cap, because `0` is not nullish."
},
"enableBatchEndpoint": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (batch.enableBatchEndpoint && this.protocol.batchData)` gates the generic POST /data/:object/batch mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount, and the second conjunct is a runtime capability rather than a second authored input, so no producer beyond the config threading is needed."
},
"operations": {
"children": {
"createMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.createMany && this.protocol.createManyData)` gates the POST /data/:object/createMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"updateMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.updateMany && this.protocol.updateManyData)` gates the POST /data/:object/updateMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"deleteMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.deleteMany && this.protocol.deleteManyData)` gates the POST /data/:object/deleteMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"upsertMany": {
"status": "dead",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"note": "0 read sites at 2514d49f3. Its three siblings each gate a route mount; this one gates nothing, because there is no upsertMany route to gate — the switch was declared for a batch operation that was never built (`this.protocol` has createManyData / updateManyData / deleteManyData and no upsert counterpart). `operations.upsertMany: false` therefore disables nothing and `true` enables nothing."
}
}
},
"defaultAtomic": {
"status": "dead",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"note": "0 read sites at 2514d49f3. Repo-wide there are no reads outside packages/spec and rest-server.ts either. The key is normalized into `this.config.batch.defaultAtomic` and no batch handler consults it, so `batch.defaultAtomic: false` changes no batch's transaction mode. A switch whose describe() promises a transaction default while the transaction default is decided elsewhere is the false-compliance shape this ledger exists to surface, which is why it is recorded rather than left to a reader's grep."
}
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .changeset/liveness-rest-server-config-sub-objects.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
---
"@objectstack/spec": patch
---

chore(spec): govern the four `RestServerConfig` sub-objects in the liveness ledger (#14369)

The `liveness/` ledgers ship inside this package's npm tarball (they are named in
`files`), so this is a published-data change even though no runtime behaviour
moves, no schema key changes spelling, and `packages/spec/src/api/rest-server.zod.ts`
is not edited at all.

Four new ledger files — `crud_endpoints.json`, `metadata_endpoints.json`,
`batch_endpoints.json`, `route_generation.json` — classify all 32 authorable
properties of `CrudEndpointsConfigSchema`, `MetadataEndpointsConfigSchema`,
`BatchEndpointsConfigSchema` and `RouteGenerationConfigSchema`, the four
`RestServerConfig` sub-objects a host writes when it constructs the REST server.
They are enrolled through the gate's `SPEC_ONLY_SCHEMAS` override, the route
`query` / `qa` / `manifest` already take: server configuration is neither a
metadata item nor a request body nor a manifest, so no registry has ever held it
and no ratchet rooted in one could ask who reads it.

Seventeen properties are `live` with a symbol-anchored consumer and a producer
pointer at the normalizer that threads the authored value into `this.config`.
Fifteen are `dead` — the ten keys the census filed with this card measured, with
the two container keys (`crud.patterns`, `routes.overrides`) expanded into a row
per member. `routes` is dead entire: `excludeObjects: ['sys_log']` excludes
nothing and `nameTransform: 'plural'` still mounts every route under the raw
object name. `metadata.endpoints.schema` and `batch.operations.upsertMany` are
switches for routes that were never built — no path ending in `/schema` is
mounted anywhere in `packages/rest/src`, and the protocol has no `upsertManyData`
counterpart to its three sibling batch methods.

What this records, and what it deliberately does not. #11984 made
`RestServer.normalizeConfig` PARSE and CONSUME these four sub-objects instead of
casting them, so an out-of-enum or out-of-range value is now refused at
construction. That settles accept/reject and nothing else: executing a declared
contract does not give a key a consumer. No key is removed, enforced, deprecated
or re-described here. The enforce-or-remove call per dead key (ADR-0049) is a
follow-up on the human floor — the enforce route is a feature per key, and
`routes.excludeObjects` is advertised in `RestServerConfigSchema`'s own
`@example`, which makes its removal a capability retirement rather than a cleanup.

Rooted on the four sub-schemas rather than on `RestServerConfigSchema` itself,
which is measurement rather than taste: the ledger walk drills exactly ONE level,
so with the whole config as the root the sub-objects would BE the drilled level
and `metadata.endpoints.schema` / `batch.operations.upsertMany` would have no row
of their own — their container's blanket `live` (three of four members gate a real
route mount) silently covering a dead key, which is the #4956 shape in the file
written to end it. `RestApiConfigSchema` (the fifth sub-object, `api`) is not
enrolled: its consumption seam is still validate-only and is the subject of its
own card, so a census of it would record a half that is about to move.

<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is removed, renamed or re-described: this change adds ledger rows and a gate enrolment, and every key it classifies keeps the exact spelling, type, default and describe() it had. There is no source for `objectstack migrate meta` to rewrite, because no author's config becomes invalid or becomes valid as a result. -->
6 changes: 5 additions & 1 deletion packages/spec/liveness/README.md

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions packages/spec/liveness/batch_endpoints.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
{
"type": "batch_endpoints",
"_note": "BatchEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#BatchEndpointsConfigSchema, the `batch` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `maxBatchSize`, `enableBatchEndpoint` and three of the four `operations.*` switches are read; `operations.upsertMany` and `defaultAtomic` are not. This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness.",
"props": {
"maxBatchSize": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`const maxBatch = batch.maxBatchSize ?? 200` — the cap every batch request is measured against)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Live and load-bearing since #11984 gave it a real parse: before that a configured `0` was the live cap, because `0` is not nullish."
},
"enableBatchEndpoint": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (batch.enableBatchEndpoint && this.protocol.batchData)` gates the generic POST /data/:object/batch mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount, and the second conjunct is a runtime capability rather than a second authored input, so no producer beyond the config threading is needed."
},
"operations": {
"children": {
"createMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.createMany && this.protocol.createManyData)` gates the POST /data/:object/createMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"updateMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.updateMany && this.protocol.updateManyData)` gates the POST /data/:object/updateMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"deleteMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.deleteMany && this.protocol.deleteManyData)` gates the POST /data/:object/deleteMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"upsertMany": {
"status": "dead",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"note": "0 read sites at 2514d49f3. Its three siblings each gate a route mount; this one gates nothing, because there is no upsertMany route to gate — the switch was declared for a batch operation that was never built (`this.protocol` has createManyData / updateManyData / deleteManyData and no upsert counterpart). `operations.upsertMany: false` therefore disables nothing and `true` enables nothing."
}
}
},
"defaultAtomic": {
"status": "dead",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"note": "0 read sites at 2514d49f3. Repo-wide there are no reads outside packages/spec and rest-server.ts either. The key is normalized into `this.config.batch.defaultAtomic` and no batch handler consults it, so `batch.defaultAtomic: false` changes no batch's transaction mode. A switch whose describe() promises a transaction default while the transaction default is decided elsewhere is the false-compliance shape this ledger exists to surface, which is why it is recorded rather than left to a reader's grep."
}
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .changeset/liveness-rest-server-config-sub-objects.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
---
"@objectstack/spec": patch
---

chore(spec): govern the four `RestServerConfig` sub-objects in the liveness ledger (#14369)

The `liveness/` ledgers ship inside this package's npm tarball (they are named in
`files`), so this is a published-data change even though no runtime behaviour
moves, no schema key changes spelling, and `packages/spec/src/api/rest-server.zod.ts`
is not edited at all.

Four new ledger files — `crud_endpoints.json`, `metadata_endpoints.json`,
`batch_endpoints.json`, `route_generation.json` — classify all 32 authorable
properties of `CrudEndpointsConfigSchema`, `MetadataEndpointsConfigSchema`,
`BatchEndpointsConfigSchema` and `RouteGenerationConfigSchema`, the four
`RestServerConfig` sub-objects a host writes when it constructs the REST server.
They are enrolled through the gate's `SPEC_ONLY_SCHEMAS` override, the route
`query` / `qa` / `manifest` already take: server configuration is neither a
metadata item nor a request body nor a manifest, so no registry has ever held it
and no ratchet rooted in one could ask who reads it.

Seventeen properties are `live` with a symbol-anchored consumer and a producer
pointer at the normalizer that threads the authored value into `this.config`.
Fifteen are `dead` — the ten keys the census filed with this card measured, with
the two container keys (`crud.patterns`, `routes.overrides`) expanded into a row
per member. `routes` is dead entire: `excludeObjects: ['sys_log']` excludes
nothing and `nameTransform: 'plural'` still mounts every route under the raw
object name. `metadata.endpoints.schema` and `batch.operations.upsertMany` are
switches for routes that were never built — no path ending in `/schema` is
mounted anywhere in `packages/rest/src`, and the protocol has no `upsertManyData`
counterpart to its three sibling batch methods.

What this records, and what it deliberately does not. #11984 made
`RestServer.normalizeConfig` PARSE and CONSUME these four sub-objects instead of
casting them, so an out-of-enum or out-of-range value is now refused at
construction. That settles accept/reject and nothing else: executing a declared
contract does not give a key a consumer. No key is removed, enforced, deprecated
or re-described here. The enforce-or-remove call per dead key (ADR-0049) is a
follow-up on the human floor — the enforce route is a feature per key, and
`routes.excludeObjects` is advertised in `RestServerConfigSchema`'s own
`@example`, which makes its removal a capability retirement rather than a cleanup.

Rooted on the four sub-schemas rather than on `RestServerConfigSchema` itself,
which is measurement rather than taste: the ledger walk drills exactly ONE level,
so with the whole config as the root the sub-objects would BE the drilled level
and `metadata.endpoints.schema` / `batch.operations.upsertMany` would have no row
of their own — their container's blanket `live` (three of four members gate a real
route mount) silently covering a dead key, which is the #4956 shape in the file
written to end it. `RestApiConfigSchema` (the fifth sub-object, `api`) is not
enrolled: its consumption seam is still validate-only and is the subject of its
own card, so a census of it would record a half that is about to move.

<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is removed, renamed or re-described: this change adds ledger rows and a gate enrolment, and every key it classifies keeps the exact spelling, type, default and describe() it had. There is no source for `objectstack migrate meta` to rewrite, because no author's config becomes invalid or becomes valid as a result. -->
6 changes: 5 additions & 1 deletion packages/spec/liveness/README.md

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions packages/spec/liveness/batch_endpoints.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
{
"type": "batch_endpoints",
"_note": "BatchEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#BatchEndpointsConfigSchema, the `batch` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `maxBatchSize`, `enableBatchEndpoint` and three of the four `operations.*` switches are read; `operations.upsertMany` and `defaultAtomic` are not. This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness.",
"props": {
"maxBatchSize": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`const maxBatch = batch.maxBatchSize ?? 200` — the cap every batch request is measured against)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Live and load-bearing since #11984 gave it a real parse: before that a configured `0` was the live cap, because `0` is not nullish."
},
"enableBatchEndpoint": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (batch.enableBatchEndpoint && this.protocol.batchData)` gates the generic POST /data/:object/batch mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount, and the second conjunct is a runtime capability rather than a second authored input, so no producer beyond the config threading is needed."
},
"operations": {
"children": {
"createMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.createMany && this.protocol.createManyData)` gates the POST /data/:object/createMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"updateMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.updateMany && this.protocol.updateManyData)` gates the POST /data/:object/updateMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"deleteMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.deleteMany && this.protocol.deleteManyData)` gates the POST /data/:object/deleteMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"upsertMany": {
"status": "dead",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"note": "0 read sites at 2514d49f3. Its three siblings each gate a route mount; this one gates nothing, because there is no upsertMany route to gate — the switch was declared for a batch operation that was never built (`this.protocol` has createManyData / updateManyData / deleteManyData and no upsert counterpart). `operations.upsertMany: false` therefore disables nothing and `true` enables nothing."
}
}
},
"defaultAtomic": {
"status": "dead",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"note": "0 read sites at 2514d49f3. Repo-wide there are no reads outside packages/spec and rest-server.ts either. The key is normalized into `this.config.batch.defaultAtomic` and no batch handler consults it, so `batch.defaultAtomic: false` changes no batch's transaction mode. A switch whose describe() promises a transaction default while the transaction default is decided elsewhere is the false-compliance shape this ledger exists to surface, which is why it is recorded rather than left to a reader's grep."
}
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .changeset/liveness-rest-server-config-sub-objects.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
---
"@objectstack/spec": patch
---

chore(spec): govern the four `RestServerConfig` sub-objects in the liveness ledger (#14369)

The `liveness/` ledgers ship inside this package's npm tarball (they are named in
`files`), so this is a published-data change even though no runtime behaviour
moves, no schema key changes spelling, and `packages/spec/src/api/rest-server.zod.ts`
is not edited at all.

Four new ledger files — `crud_endpoints.json`, `metadata_endpoints.json`,
`batch_endpoints.json`, `route_generation.json` — classify all 32 authorable
properties of `CrudEndpointsConfigSchema`, `MetadataEndpointsConfigSchema`,
`BatchEndpointsConfigSchema` and `RouteGenerationConfigSchema`, the four
`RestServerConfig` sub-objects a host writes when it constructs the REST server.
They are enrolled through the gate's `SPEC_ONLY_SCHEMAS` override, the route
`query` / `qa` / `manifest` already take: server configuration is neither a
metadata item nor a request body nor a manifest, so no registry has ever held it
and no ratchet rooted in one could ask who reads it.

Seventeen properties are `live` with a symbol-anchored consumer and a producer
pointer at the normalizer that threads the authored value into `this.config`.
Fifteen are `dead` — the ten keys the census filed with this card measured, with
the two container keys (`crud.patterns`, `routes.overrides`) expanded into a row
per member. `routes` is dead entire: `excludeObjects: ['sys_log']` excludes
nothing and `nameTransform: 'plural'` still mounts every route under the raw
object name. `metadata.endpoints.schema` and `batch.operations.upsertMany` are
switches for routes that were never built — no path ending in `/schema` is
mounted anywhere in `packages/rest/src`, and the protocol has no `upsertManyData`
counterpart to its three sibling batch methods.

What this records, and what it deliberately does not. #11984 made
`RestServer.normalizeConfig` PARSE and CONSUME these four sub-objects instead of
casting them, so an out-of-enum or out-of-range value is now refused at
construction. That settles accept/reject and nothing else: executing a declared
contract does not give a key a consumer. No key is removed, enforced, deprecated
or re-described here. The enforce-or-remove call per dead key (ADR-0049) is a
follow-up on the human floor — the enforce route is a feature per key, and
`routes.excludeObjects` is advertised in `RestServerConfigSchema`'s own
`@example`, which makes its removal a capability retirement rather than a cleanup.

Rooted on the four sub-schemas rather than on `RestServerConfigSchema` itself,
which is measurement rather than taste: the ledger walk drills exactly ONE level,
so with the whole config as the root the sub-objects would BE the drilled level
and `metadata.endpoints.schema` / `batch.operations.upsertMany` would have no row
of their own — their container's blanket `live` (three of four members gate a real
route mount) silently covering a dead key, which is the #4956 shape in the file
written to end it. `RestApiConfigSchema` (the fifth sub-object, `api`) is not
enrolled: its consumption seam is still validate-only and is the subject of its
own card, so a census of it would record a half that is about to move.

<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is removed, renamed or re-described: this change adds ledger rows and a gate enrolment, and every key it classifies keeps the exact spelling, type, default and describe() it had. There is no source for `objectstack migrate meta` to rewrite, because no author's config becomes invalid or becomes valid as a result. -->
6 changes: 5 additions & 1 deletion packages/spec/liveness/README.md

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions packages/spec/liveness/batch_endpoints.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
{
"type": "batch_endpoints",
"_note": "BatchEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#BatchEndpointsConfigSchema, the `batch` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `maxBatchSize`, `enableBatchEndpoint` and three of the four `operations.*` switches are read; `operations.upsertMany` and `defaultAtomic` are not. This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness.",
"props": {
"maxBatchSize": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`const maxBatch = batch.maxBatchSize ?? 200` — the cap every batch request is measured against)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Live and load-bearing since #11984 gave it a real parse: before that a configured `0` was the live cap, because `0` is not nullish."
},
"enableBatchEndpoint": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (batch.enableBatchEndpoint && this.protocol.batchData)` gates the generic POST /data/:object/batch mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount, and the second conjunct is a runtime capability rather than a second authored input, so no producer beyond the config threading is needed."
},
"operations": {
"children": {
"createMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.createMany && this.protocol.createManyData)` gates the POST /data/:object/createMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"updateMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.updateMany && this.protocol.updateManyData)` gates the POST /data/:object/updateMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"deleteMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.deleteMany && this.protocol.deleteManyData)` gates the POST /data/:object/deleteMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"upsertMany": {
"status": "dead",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"note": "0 read sites at 2514d49f3. Its three siblings each gate a route mount; this one gates nothing, because there is no upsertMany route to gate — the switch was declared for a batch operation that was never built (`this.protocol` has createManyData / updateManyData / deleteManyData and no upsert counterpart). `operations.upsertMany: false` therefore disables nothing and `true` enables nothing."
}
}
},
"defaultAtomic": {
"status": "dead",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"note": "0 read sites at 2514d49f3. Repo-wide there are no reads outside packages/spec and rest-server.ts either. The key is normalized into `this.config.batch.defaultAtomic` and no batch handler consults it, so `batch.defaultAtomic: false` changes no batch's transaction mode. A switch whose describe() promises a transaction default while the transaction default is decided elsewhere is the false-compliance shape this ledger exists to surface, which is why it is recorded rather than left to a reader's grep."
}
}
}
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .changeset/liveness-rest-server-config-sub-objects.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
---
"@objectstack/spec": patch
---

chore(spec): govern the four `RestServerConfig` sub-objects in the liveness ledger (#14369)

The `liveness/` ledgers ship inside this package's npm tarball (they are named in
`files`), so this is a published-data change even though no runtime behaviour
moves, no schema key changes spelling, and `packages/spec/src/api/rest-server.zod.ts`
is not edited at all.

Four new ledger files — `crud_endpoints.json`, `metadata_endpoints.json`,
`batch_endpoints.json`, `route_generation.json` — classify all 32 authorable
properties of `CrudEndpointsConfigSchema`, `MetadataEndpointsConfigSchema`,
`BatchEndpointsConfigSchema` and `RouteGenerationConfigSchema`, the four
`RestServerConfig` sub-objects a host writes when it constructs the REST server.
They are enrolled through the gate's `SPEC_ONLY_SCHEMAS` override, the route
`query` / `qa` / `manifest` already take: server configuration is neither a
metadata item nor a request body nor a manifest, so no registry has ever held it
and no ratchet rooted in one could ask who reads it.

Seventeen properties are `live` with a symbol-anchored consumer and a producer
pointer at the normalizer that threads the authored value into `this.config`.
Fifteen are `dead` — the ten keys the census filed with this card measured, with
the two container keys (`crud.patterns`, `routes.overrides`) expanded into a row
per member. `routes` is dead entire: `excludeObjects: ['sys_log']` excludes
nothing and `nameTransform: 'plural'` still mounts every route under the raw
object name. `metadata.endpoints.schema` and `batch.operations.upsertMany` are
switches for routes that were never built — no path ending in `/schema` is
mounted anywhere in `packages/rest/src`, and the protocol has no `upsertManyData`
counterpart to its three sibling batch methods.

What this records, and what it deliberately does not. #11984 made
`RestServer.normalizeConfig` PARSE and CONSUME these four sub-objects instead of
casting them, so an out-of-enum or out-of-range value is now refused at
construction. That settles accept/reject and nothing else: executing a declared
contract does not give a key a consumer. No key is removed, enforced, deprecated
or re-described here. The enforce-or-remove call per dead key (ADR-0049) is a
follow-up on the human floor — the enforce route is a feature per key, and
`routes.excludeObjects` is advertised in `RestServerConfigSchema`'s own
`@example`, which makes its removal a capability retirement rather than a cleanup.

Rooted on the four sub-schemas rather than on `RestServerConfigSchema` itself,
which is measurement rather than taste: the ledger walk drills exactly ONE level,
so with the whole config as the root the sub-objects would BE the drilled level
and `metadata.endpoints.schema` / `batch.operations.upsertMany` would have no row
of their own — their container's blanket `live` (three of four members gate a real
route mount) silently covering a dead key, which is the #4956 shape in the file
written to end it. `RestApiConfigSchema` (the fifth sub-object, `api`) is not
enrolled: its consumption seam is still validate-only and is the subject of its
own card, so a census of it would record a half that is about to move.

<!-- adr-0087: not-required (no-migration-prescription) Nothing authorable is removed, renamed or re-described: this change adds ledger rows and a gate enrolment, and every key it classifies keeps the exact spelling, type, default and describe() it had. There is no source for `objectstack migrate meta` to rewrite, because no author's config becomes invalid or becomes valid as a result. -->
6 changes: 5 additions & 1 deletion packages/spec/liveness/README.md

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions packages/spec/liveness/batch_endpoints.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
{
"type": "batch_endpoints",
"_note": "BatchEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#BatchEndpointsConfigSchema, the `batch` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `maxBatchSize`, `enableBatchEndpoint` and three of the four `operations.*` switches are read; `operations.upsertMany` and `defaultAtomic` are not. This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness.",
"props": {
"maxBatchSize": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`const maxBatch = batch.maxBatchSize ?? 200` — the cap every batch request is measured against)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Live and load-bearing since #11984 gave it a real parse: before that a configured `0` was the live cap, because `0` is not nullish."
},
"enableBatchEndpoint": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (batch.enableBatchEndpoint && this.protocol.batchData)` gates the generic POST /data/:object/batch mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount, and the second conjunct is a runtime capability rather than a second authored input, so no producer beyond the config threading is needed."
},
"operations": {
"children": {
"createMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.createMany && this.protocol.createManyData)` gates the POST /data/:object/createMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"updateMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.updateMany && this.protocol.updateManyData)` gates the POST /data/:object/updateMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"deleteMany": {
"status": "live",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.deleteMany && this.protocol.deleteManyData)` gates the POST /data/:object/deleteMany mount)",
"producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)",
"note": "Gates a mount."
},
"upsertMany": {
"status": "dead",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"note": "0 read sites at 2514d49f3. Its three siblings each gate a route mount; this one gates nothing, because there is no upsertMany route to gate — the switch was declared for a batch operation that was never built (`this.protocol` has createManyData / updateManyData / deleteManyData and no upsert counterpart). `operations.upsertMany: false` therefore disables nothing and `true` enables nothing."
}
}
},
"defaultAtomic": {
"status": "dead",
"verifiedAt": "2026-09-02",
"evidenceScope": "in-repo",
"note": "0 read sites at 2514d49f3. Repo-wide there are no reads outside packages/spec and rest-server.ts either. The key is normalized into `this.config.batch.defaultAtomic` and no batch handler consults it, so `batch.defaultAtomic: false` changes no batch's transaction mode. A switch whose describe() promises a transaction default while the transaction default is decided elsewhere is the false-compliance shape this ledger exists to surface, which is why it is recorded rather than left to a reader's grep."
}
}
}
Loading
Loading