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
33 changes: 33 additions & 0 deletions .changeset/liveness-live-elsewhere-verdict.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
"@objectstack/spec": patch
---

feat(spec): fifth liveness-ledger verdict `live-elsewhere` — dead here, enforced in a sibling repo, with gate-executable criteria (#13483)

The liveness ledger had no verdict for a key that is dead locally but genuinely
enforced in a sibling repo. The measured case is `manifest.runtime`: zero
load-side dispatch in objectstack+objectui, yet the cloud marketplace publish
gate hard-rejects (HTTP 422) an unverified publisher requesting the `node` tier
(#12400, cloud @15f55df). `dead` read alone licenses deleting the marketplace's
trust-gate input — deletion the maintainer explicitly ruled out (#11330) — and
`live` is refused by the gate, whose repo-local evidence must resolve against
this checkout. The stopgap was a qualifying sentence in the row's `note`; prose
is the weakest protection the ledger knows.

`live-elsewhere` says the split as data, and because `check-liveness.mts`
deliberately forces a decision on unknown statuses, it lands with criteria the
gate executes (`scripts/liveness/elsewhere.mts`, exit 1 on each): the
`evidence` must attribute at least one path to a foreign realm (the enforcer
pointer, foreign commit pinned in prose), `evidenceScope` must be
`"cross-repo"`, `verifiedAt` is REQUIRED (nothing local can watch a foreign
enforcer rot, so an undated elsewhere-claim would be unfalsifiable forever),
and the attestation expires: past 180 days the gate fails demanding a
re-reading of the foreign enforcer — never a re-stamp. If no seat with access
re-attests, the red build is the escalation back to the maintainer.

The status joins the evidence-scan population (any local path such an entry
also cites is held to the full existence/line/anchor/key-mention standard),
`STATUS_COLUMNS`, and the generated `state-counts.md` (new `elsewhere` column).
`manifest.runtime` migrates as the first row, attested at the #12400 reading
(2026-08-29). The ledger README documents the verdict and its re-attestation
discipline.
63 changes: 58 additions & 5 deletions packages/spec/liveness/README.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions packages/spec/liveness/manifest.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -250,10 +250,11 @@
}
},
"runtime": {
"status": "dead",
"verifiedAt": "2026-08-23",
"status": "live-elsewhere",
"verifiedAt": "2026-08-29",
"evidenceScope": "cross-repo",
"note": "READ TWICE HERE, ENFORCED AT THE CLOUD PUBLISH GATE, NOT ENFORCED AT LOAD — three different facts, so read this row before assuming either zero references or zero enforcement. The only reads of `manifest.runtime` in objectstack are two CLI progress lines that echo the value: packages/cli/src/commands/plugin/build.ts:127 and packages/cli/src/commands/plugin/publish.ts:94, both `runtime: ${manifest.runtime ?? 'unset'}`. Nothing dispatches on the tier: there is no `runtime === 'sandbox'` branch anywhere, and the QuickJS runner (packages/runtime/src/sandbox/) is the hook/action SCRIPT-BODY sandbox, reached from body-runner factories, never from a plugin's declared tier. objectui: zero reads. So the declared capability — \"in-process full PluginContext vs QuickJS-WASM capability-gated vs out-of-process\" (ADR-0025 §3.6) — is not delivered here, and echoing a string into a build log is not isolation. WHY THIS WAS SECURITY-SHAPED RATHER THAN COSMETIC, AND WHAT WAS DONE: the tombstone this ledger records one key above used to tell every upgrading author, verbatim, \"If you were relying on [loading.sandboxing] for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced\" — a redirection at a tier this repo does not enforce, i.e. ADR-0049 false compliance with a shipped migration message attached. ⚠️ SCOPE LIMIT NOW DISCHARGED: the cloud leg was measured by #12400 and it CHANGED the picture — the cloud control plane really does consume this key at the marketplace publish gate (`plugin-permission-audit.ts`: an unverified publisher requesting the `node` tier is hard-rejected with HTTP 422 and forced to manual review, reading of 2026-08-29). So the honest statement is split: ENFORCED at publish (cloud), NOT ENFORCED at load (here). Maintainer ruling 2026-08-30 (#11330) took option B — correct the text, do not retire the key — and explicitly ruled OUT retirement, because deleting the key would tear out the marketplace's trust-gate input. That correction has landed: the tombstone and both `runtime` describes now state the publish-gate/load-side split, and so does the ADR-0087 D3 entry `plugin-manifest-loading-retired`. ⚠️ READ `dead` NARROWLY HERE: it is the objectstack+objectui verdict about LOAD-SIDE dispatch (still true — the only local reads are the two CLI echoes above), and it is NOT a licence to delete, because the key has a real cross-repo consumer. Adding load-side enforcement is the open half, tracked as a v18 direction, not as a removal."
"evidence": "cloud: packages/service-cloud/src/plugin-permission-audit.ts#auditPluginPermissions @15f55df + packages/service-cloud/src/routes/package-publish.ts:530 — the marketplace publish gate: an unverified publisher requesting the node tier is hard-rejected with HTTP 422 and forced to manual review (#12400 reading of 2026-08-29). Load-side dispatch here is dead by measurement; the note carries that half.",
"note": "ENFORCED AT THE CLOUD PUBLISH GATE, NOT ENFORCED AT LOAD — and since #13483 the split IS the verdict: `live-elsewhere` says dead here by measurement, genuinely enforced in a sibling repo, and unlike the `dead` this row carried until then (with this note's qualifying sentence as the only guard — prose, which no check reads), it cannot be read as a licence to delete and the gate holds its criteria (foreign pointer, cross-repo scope, dated attestation with a 180d expiry — scripts/liveness/elsewhere.mts). LOCAL HALF, measured 2026-08-23 against b9e9227e3 with per-key controls: the only reads of `manifest.runtime` in objectstack are two CLI progress lines that echo the value — packages/cli/src/commands/plugin/build.ts:127 and packages/cli/src/commands/plugin/publish.ts:94, both `runtime: ${manifest.runtime ?? 'unset'}`. Nothing dispatches on the tier: there is no `runtime === 'sandbox'` branch anywhere, and the QuickJS runner (packages/runtime/src/sandbox/) is the hook/action SCRIPT-BODY sandbox, reached from body-runner factories, never from a plugin's declared tier. objectui: zero reads. So the declared capability — \"in-process full PluginContext vs QuickJS-WASM capability-gated vs out-of-process\" (ADR-0025 §3.6) — is not delivered at load, and echoing a string into a build log is not isolation. FOREIGN HALF (the evidence pointer): #12400 measured cloud origin/main @15f55df on 2026-08-29, controls held (15 manifest-property reads findable there) — `auditPluginPermissions` treats `runtime === 'node'` from an unverified publisher as a hard block, the caller turns it into HTTP 422 with forced manual review, and the tier is persisted to the version row (plugin-artifact.ts:179, package-publish.ts:531). `verifiedAt` on this row is THAT reading's date; re-attestation means someone with cloud access re-reads the enforcer and re-pins the commit — never a bare re-stamp (access is seat-dependent: `add_repo` on cloud is denied from some seats, including the #13483 seat on 2026-09-01, while the #10812 and #12400 readings prove cloud-capable seats exist). WHY THIS WAS SECURITY-SHAPED RATHER THAN COSMETIC: the tombstone this ledger records one key above used to tell every upgrading author, verbatim, \"If you were relying on [loading.sandboxing] for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced\" — a redirection at a tier this repo does not enforce at load, i.e. ADR-0049 false compliance with a shipped migration message attached. Maintainer ruling 2026-08-30 (#11330) took option B — correct the text, do not retire the key — and explicitly ruled OUT retirement, because deleting the key would tear out the marketplace's trust-gate input; the tombstone, both `runtime` describes and the ADR-0087 D3 entry `plugin-manifest-loading-retired` state the publish-gate/load-side split. Adding load-side enforcement is the open half, tracked as a v18 direction, not as a removal — if it lands, the verdict here flips to `live` with local evidence."
},
"packaging": {
"status": "live",
Expand Down
68 changes: 34 additions & 34 deletions packages/spec/liveness/state-counts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,37 +25,37 @@ protection envelope, which the gate auto-classifies `live` on every type that
spreads `MetadataProtectionFields`. See the README's counting-method section
for both corollaries.

| Type | live | exp | dead | planned | classified |
|---|---|---|---|---|---|
| `object` | 51 | 0 | 0 | 1 | 52 |
| `field` | 89 | 0 | 1 | 2 | 92 |
| `flow` | 34 | 0 | 6 | 0 | 40 |
| `action` | 41 | 0 | 3 | 2 | 46 |
| `hook` | 18 | 0 | 2 | 0 | 20 |
| `permission` | 36 | 0 | 6 | 0 | 42 |
| `position` | 12 | 0 | 0 | 0 | 12 |
| `agent` | 21 | 4 | 1 | 0 | 26 |
| `tool` | 13 | 1 | 0 | 0 | 14 |
| `skill` | 16 | 0 | 1 | 0 | 17 |
| `dataset` | 27 | 0 | 0 | 0 | 27 |
| `page` | 23 | 0 | 0 | 1 | 24 |
| `view` | 79 | 0 | 9 | 0 | 88 |
| `report` | 21 | 0 | 0 | 0 | 21 |
| `dashboard` | 34 | 0 | 7 | 0 | 41 |
| `webhook` | 19 | 0 | 0 | 0 | 19 |
| `query` | 16 | 0 | 5 | 0 | 21 |
| `datasource` | 30 | 0 | 0 | 0 | 30 |
| `app` | 47 | 0 | 9 | 0 | 56 |
| `book` | 20 | 0 | 1 | 0 | 21 |
| `doc` | 15 | 0 | 0 | 0 | 15 |
| `email_template` | 21 | 0 | 0 | 0 | 21 |
| `job` | 15 | 0 | 0 | 0 | 15 |
| `mapping` | 14 | 0 | 0 | 0 | 14 |
| `seed` | 12 | 0 | 0 | 0 | 12 |
| `translation` | 19 | 0 | 0 | 2 | 21 |
| `validation` | 15 | 0 | 3 | 0 | 18 |
| `api` | 25 | 0 | 0 | 2 | 27 |
| `capability` | 12 | 0 | 0 | 0 | 12 |
| `qa` | 4 | 0 | 5 | 0 | 9 |
| `manifest` | 22 | 0 | 16 | 0 | 38 |
| **total** | **821** | **5** | **75** | **10** | **911** |
| Type | live | exp | elsewhere | dead | planned | classified |
|---|---|---|---|---|---|---|
| `object` | 51 | 0 | 0 | 0 | 1 | 52 |
| `field` | 89 | 0 | 0 | 1 | 2 | 92 |
| `flow` | 34 | 0 | 0 | 6 | 0 | 40 |
| `action` | 41 | 0 | 0 | 3 | 2 | 46 |
| `hook` | 18 | 0 | 0 | 2 | 0 | 20 |
| `permission` | 36 | 0 | 0 | 6 | 0 | 42 |
| `position` | 12 | 0 | 0 | 0 | 0 | 12 |
| `agent` | 21 | 4 | 0 | 1 | 0 | 26 |
| `tool` | 13 | 1 | 0 | 0 | 0 | 14 |
| `skill` | 16 | 0 | 0 | 1 | 0 | 17 |
| `dataset` | 27 | 0 | 0 | 0 | 0 | 27 |
| `page` | 23 | 0 | 0 | 0 | 1 | 24 |
| `view` | 79 | 0 | 0 | 9 | 0 | 88 |
| `report` | 21 | 0 | 0 | 0 | 0 | 21 |
| `dashboard` | 34 | 0 | 0 | 7 | 0 | 41 |
| `webhook` | 19 | 0 | 0 | 0 | 0 | 19 |
| `query` | 16 | 0 | 0 | 5 | 0 | 21 |
| `datasource` | 30 | 0 | 0 | 0 | 0 | 30 |
| `app` | 47 | 0 | 0 | 9 | 0 | 56 |
| `book` | 20 | 0 | 0 | 1 | 0 | 21 |
| `doc` | 15 | 0 | 0 | 0 | 0 | 15 |
| `email_template` | 21 | 0 | 0 | 0 | 0 | 21 |
| `job` | 15 | 0 | 0 | 0 | 0 | 15 |
| `mapping` | 14 | 0 | 0 | 0 | 0 | 14 |
| `seed` | 12 | 0 | 0 | 0 | 0 | 12 |
| `translation` | 19 | 0 | 0 | 0 | 2 | 21 |
| `validation` | 15 | 0 | 0 | 3 | 0 | 18 |
| `api` | 25 | 0 | 0 | 0 | 2 | 27 |
| `capability` | 12 | 0 | 0 | 0 | 0 | 12 |
| `qa` | 4 | 0 | 0 | 5 | 0 | 9 |
| `manifest` | 22 | 0 | 1 | 15 | 0 | 38 |
| **total** | **821** | **5** | **1** | **74** | **10** | **911** |
4 changes: 2 additions & 2 deletions packages/spec/scripts/liveness/build-state-counts.mts
Original file line numberDiff line numberDiff line change
Expand Up@@ -113,7 +113,7 @@ const rows = foldStateCounts(Object.keys(types), Object.fromEntries(
// The header's rule is that a RED gate is not fatal here — the gate is red
// precisely when this artifact needs rewriting. This failure is the exception,
// and it is the same exception the unparseable report above already carves out:
// there is nothing to rewrite. The fold that produced `rows` reads four status
// there is nothing to rewrite. The fold that produced `rows` reads the published status
// names and drops everything else, so writing now would publish an understated
// `classified` — and the gate's freshness leg would then compare those bytes
// against a re-render of the SAME understated fold and call it current. A stale
Expand All@@ -134,7 +134,7 @@ if (totalErrors.length) {
const rendered = renderStateCounts(rows);
writeFileSync(join(ledgerRoot, STATE_COUNTS_FILE), rendered);

const total = rows.reduce((a, r) => a + r.live + r.experimental + r.dead + r.planned, 0);
const total = rows.reduce((a, r) => a + r.live + r.experimental + r['live-elsewhere'] + r.dead + r.planned, 0);
console.log(`✓ wrote ${STATE_COUNTS_PATH}`);
console.log(` ${rows.length} governed type(s), ${total} classified propert(ies).`);

Expand Down
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
33 changes: 33 additions & 0 deletions .changeset/liveness-live-elsewhere-verdict.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
"@objectstack/spec": patch
---

feat(spec): fifth liveness-ledger verdict `live-elsewhere` — dead here, enforced in a sibling repo, with gate-executable criteria (#13483)

The liveness ledger had no verdict for a key that is dead locally but genuinely
enforced in a sibling repo. The measured case is `manifest.runtime`: zero
load-side dispatch in objectstack+objectui, yet the cloud marketplace publish
gate hard-rejects (HTTP 422) an unverified publisher requesting the `node` tier
(#12400, cloud @15f55df). `dead` read alone licenses deleting the marketplace's
trust-gate input — deletion the maintainer explicitly ruled out (#11330) — and
`live` is refused by the gate, whose repo-local evidence must resolve against
this checkout. The stopgap was a qualifying sentence in the row's `note`; prose
is the weakest protection the ledger knows.

`live-elsewhere` says the split as data, and because `check-liveness.mts`
deliberately forces a decision on unknown statuses, it lands with criteria the
gate executes (`scripts/liveness/elsewhere.mts`, exit 1 on each): the
`evidence` must attribute at least one path to a foreign realm (the enforcer
pointer, foreign commit pinned in prose), `evidenceScope` must be
`"cross-repo"`, `verifiedAt` is REQUIRED (nothing local can watch a foreign
enforcer rot, so an undated elsewhere-claim would be unfalsifiable forever),
and the attestation expires: past 180 days the gate fails demanding a
re-reading of the foreign enforcer — never a re-stamp. If no seat with access
re-attests, the red build is the escalation back to the maintainer.

The status joins the evidence-scan population (any local path such an entry
also cites is held to the full existence/line/anchor/key-mention standard),
`STATUS_COLUMNS`, and the generated `state-counts.md` (new `elsewhere` column).
`manifest.runtime` migrates as the first row, attested at the #12400 reading
(2026-08-29). The ledger README documents the verdict and its re-attestation
discipline.
63 changes: 58 additions & 5 deletions packages/spec/liveness/README.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions packages/spec/liveness/manifest.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -250,10 +250,11 @@
}
},
"runtime": {
"status": "dead",
"verifiedAt": "2026-08-23",
"status": "live-elsewhere",
"verifiedAt": "2026-08-29",
"evidenceScope": "cross-repo",
"note": "READ TWICE HERE, ENFORCED AT THE CLOUD PUBLISH GATE, NOT ENFORCED AT LOAD — three different facts, so read this row before assuming either zero references or zero enforcement. The only reads of `manifest.runtime` in objectstack are two CLI progress lines that echo the value: packages/cli/src/commands/plugin/build.ts:127 and packages/cli/src/commands/plugin/publish.ts:94, both `runtime: ${manifest.runtime ?? 'unset'}`. Nothing dispatches on the tier: there is no `runtime === 'sandbox'` branch anywhere, and the QuickJS runner (packages/runtime/src/sandbox/) is the hook/action SCRIPT-BODY sandbox, reached from body-runner factories, never from a plugin's declared tier. objectui: zero reads. So the declared capability — \"in-process full PluginContext vs QuickJS-WASM capability-gated vs out-of-process\" (ADR-0025 §3.6) — is not delivered here, and echoing a string into a build log is not isolation. WHY THIS WAS SECURITY-SHAPED RATHER THAN COSMETIC, AND WHAT WAS DONE: the tombstone this ledger records one key above used to tell every upgrading author, verbatim, \"If you were relying on [loading.sandboxing] for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced\" — a redirection at a tier this repo does not enforce, i.e. ADR-0049 false compliance with a shipped migration message attached. ⚠️ SCOPE LIMIT NOW DISCHARGED: the cloud leg was measured by #12400 and it CHANGED the picture — the cloud control plane really does consume this key at the marketplace publish gate (`plugin-permission-audit.ts`: an unverified publisher requesting the `node` tier is hard-rejected with HTTP 422 and forced to manual review, reading of 2026-08-29). So the honest statement is split: ENFORCED at publish (cloud), NOT ENFORCED at load (here). Maintainer ruling 2026-08-30 (#11330) took option B — correct the text, do not retire the key — and explicitly ruled OUT retirement, because deleting the key would tear out the marketplace's trust-gate input. That correction has landed: the tombstone and both `runtime` describes now state the publish-gate/load-side split, and so does the ADR-0087 D3 entry `plugin-manifest-loading-retired`. ⚠️ READ `dead` NARROWLY HERE: it is the objectstack+objectui verdict about LOAD-SIDE dispatch (still true — the only local reads are the two CLI echoes above), and it is NOT a licence to delete, because the key has a real cross-repo consumer. Adding load-side enforcement is the open half, tracked as a v18 direction, not as a removal."
"evidence": "cloud: packages/service-cloud/src/plugin-permission-audit.ts#auditPluginPermissions @15f55df + packages/service-cloud/src/routes/package-publish.ts:530 — the marketplace publish gate: an unverified publisher requesting the node tier is hard-rejected with HTTP 422 and forced to manual review (#12400 reading of 2026-08-29). Load-side dispatch here is dead by measurement; the note carries that half.",
"note": "ENFORCED AT THE CLOUD PUBLISH GATE, NOT ENFORCED AT LOAD — and since #13483 the split IS the verdict: `live-elsewhere` says dead here by measurement, genuinely enforced in a sibling repo, and unlike the `dead` this row carried until then (with this note's qualifying sentence as the only guard — prose, which no check reads), it cannot be read as a licence to delete and the gate holds its criteria (foreign pointer, cross-repo scope, dated attestation with a 180d expiry — scripts/liveness/elsewhere.mts). LOCAL HALF, measured 2026-08-23 against b9e9227e3 with per-key controls: the only reads of `manifest.runtime` in objectstack are two CLI progress lines that echo the value — packages/cli/src/commands/plugin/build.ts:127 and packages/cli/src/commands/plugin/publish.ts:94, both `runtime: ${manifest.runtime ?? 'unset'}`. Nothing dispatches on the tier: there is no `runtime === 'sandbox'` branch anywhere, and the QuickJS runner (packages/runtime/src/sandbox/) is the hook/action SCRIPT-BODY sandbox, reached from body-runner factories, never from a plugin's declared tier. objectui: zero reads. So the declared capability — \"in-process full PluginContext vs QuickJS-WASM capability-gated vs out-of-process\" (ADR-0025 §3.6) — is not delivered at load, and echoing a string into a build log is not isolation. FOREIGN HALF (the evidence pointer): #12400 measured cloud origin/main @15f55df on 2026-08-29, controls held (15 manifest-property reads findable there) — `auditPluginPermissions` treats `runtime === 'node'` from an unverified publisher as a hard block, the caller turns it into HTTP 422 with forced manual review, and the tier is persisted to the version row (plugin-artifact.ts:179, package-publish.ts:531). `verifiedAt` on this row is THAT reading's date; re-attestation means someone with cloud access re-reads the enforcer and re-pins the commit — never a bare re-stamp (access is seat-dependent: `add_repo` on cloud is denied from some seats, including the #13483 seat on 2026-09-01, while the #10812 and #12400 readings prove cloud-capable seats exist). WHY THIS WAS SECURITY-SHAPED RATHER THAN COSMETIC: the tombstone this ledger records one key above used to tell every upgrading author, verbatim, \"If you were relying on [loading.sandboxing] for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced\" — a redirection at a tier this repo does not enforce at load, i.e. ADR-0049 false compliance with a shipped migration message attached. Maintainer ruling 2026-08-30 (#11330) took option B — correct the text, do not retire the key — and explicitly ruled OUT retirement, because deleting the key would tear out the marketplace's trust-gate input; the tombstone, both `runtime` describes and the ADR-0087 D3 entry `plugin-manifest-loading-retired` state the publish-gate/load-side split. Adding load-side enforcement is the open half, tracked as a v18 direction, not as a removal — if it lands, the verdict here flips to `live` with local evidence."
},
"packaging": {
"status": "live",
Expand Down
68 changes: 34 additions & 34 deletions packages/spec/liveness/state-counts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,37 +25,37 @@ protection envelope, which the gate auto-classifies `live` on every type that
spreads `MetadataProtectionFields`. See the README's counting-method section
for both corollaries.

| Type | live | exp | dead | planned | classified |
|---|---|---|---|---|---|
| `object` | 51 | 0 | 0 | 1 | 52 |
| `field` | 89 | 0 | 1 | 2 | 92 |
| `flow` | 34 | 0 | 6 | 0 | 40 |
| `action` | 41 | 0 | 3 | 2 | 46 |
| `hook` | 18 | 0 | 2 | 0 | 20 |
| `permission` | 36 | 0 | 6 | 0 | 42 |
| `position` | 12 | 0 | 0 | 0 | 12 |
| `agent` | 21 | 4 | 1 | 0 | 26 |
| `tool` | 13 | 1 | 0 | 0 | 14 |
| `skill` | 16 | 0 | 1 | 0 | 17 |
| `dataset` | 27 | 0 | 0 | 0 | 27 |
| `page` | 23 | 0 | 0 | 1 | 24 |
| `view` | 79 | 0 | 9 | 0 | 88 |
| `report` | 21 | 0 | 0 | 0 | 21 |
| `dashboard` | 34 | 0 | 7 | 0 | 41 |
| `webhook` | 19 | 0 | 0 | 0 | 19 |
| `query` | 16 | 0 | 5 | 0 | 21 |
| `datasource` | 30 | 0 | 0 | 0 | 30 |
| `app` | 47 | 0 | 9 | 0 | 56 |
| `book` | 20 | 0 | 1 | 0 | 21 |
| `doc` | 15 | 0 | 0 | 0 | 15 |
| `email_template` | 21 | 0 | 0 | 0 | 21 |
| `job` | 15 | 0 | 0 | 0 | 15 |
| `mapping` | 14 | 0 | 0 | 0 | 14 |
| `seed` | 12 | 0 | 0 | 0 | 12 |
| `translation` | 19 | 0 | 0 | 2 | 21 |
| `validation` | 15 | 0 | 3 | 0 | 18 |
| `api` | 25 | 0 | 0 | 2 | 27 |
| `capability` | 12 | 0 | 0 | 0 | 12 |
| `qa` | 4 | 0 | 5 | 0 | 9 |
| `manifest` | 22 | 0 | 16 | 0 | 38 |
| **total** | **821** | **5** | **75** | **10** | **911** |
| Type | live | exp | elsewhere | dead | planned | classified |
|---|---|---|---|---|---|---|
| `object` | 51 | 0 | 0 | 0 | 1 | 52 |
| `field` | 89 | 0 | 0 | 1 | 2 | 92 |
| `flow` | 34 | 0 | 0 | 6 | 0 | 40 |
| `action` | 41 | 0 | 0 | 3 | 2 | 46 |
| `hook` | 18 | 0 | 0 | 2 | 0 | 20 |
| `permission` | 36 | 0 | 0 | 6 | 0 | 42 |
| `position` | 12 | 0 | 0 | 0 | 0 | 12 |
| `agent` | 21 | 4 | 0 | 1 | 0 | 26 |
| `tool` | 13 | 1 | 0 | 0 | 0 | 14 |
| `skill` | 16 | 0 | 0 | 1 | 0 | 17 |
| `dataset` | 27 | 0 | 0 | 0 | 0 | 27 |
| `page` | 23 | 0 | 0 | 0 | 1 | 24 |
| `view` | 79 | 0 | 0 | 9 | 0 | 88 |
| `report` | 21 | 0 | 0 | 0 | 0 | 21 |
| `dashboard` | 34 | 0 | 0 | 7 | 0 | 41 |
| `webhook` | 19 | 0 | 0 | 0 | 0 | 19 |
| `query` | 16 | 0 | 0 | 5 | 0 | 21 |
| `datasource` | 30 | 0 | 0 | 0 | 0 | 30 |
| `app` | 47 | 0 | 0 | 9 | 0 | 56 |
| `book` | 20 | 0 | 0 | 1 | 0 | 21 |
| `doc` | 15 | 0 | 0 | 0 | 0 | 15 |
| `email_template` | 21 | 0 | 0 | 0 | 0 | 21 |
| `job` | 15 | 0 | 0 | 0 | 0 | 15 |
| `mapping` | 14 | 0 | 0 | 0 | 0 | 14 |
| `seed` | 12 | 0 | 0 | 0 | 0 | 12 |
| `translation` | 19 | 0 | 0 | 0 | 2 | 21 |
| `validation` | 15 | 0 | 0 | 3 | 0 | 18 |
| `api` | 25 | 0 | 0 | 0 | 2 | 27 |
| `capability` | 12 | 0 | 0 | 0 | 0 | 12 |
| `qa` | 4 | 0 | 0 | 5 | 0 | 9 |
| `manifest` | 22 | 0 | 1 | 15 | 0 | 38 |
| **total** | **821** | **5** | **1** | **74** | **10** | **911** |
4 changes: 2 additions & 2 deletions packages/spec/scripts/liveness/build-state-counts.mts
Original file line numberDiff line numberDiff line change
Expand Up@@ -113,7 +113,7 @@ const rows = foldStateCounts(Object.keys(types), Object.fromEntries(
// The header's rule is that a RED gate is not fatal here — the gate is red
// precisely when this artifact needs rewriting. This failure is the exception,
// and it is the same exception the unparseable report above already carves out:
// there is nothing to rewrite. The fold that produced `rows` reads four status
// there is nothing to rewrite. The fold that produced `rows` reads the published status
// names and drops everything else, so writing now would publish an understated
// `classified` — and the gate's freshness leg would then compare those bytes
// against a re-render of the SAME understated fold and call it current. A stale
Expand All@@ -134,7 +134,7 @@ if (totalErrors.length) {
const rendered = renderStateCounts(rows);
writeFileSync(join(ledgerRoot, STATE_COUNTS_FILE), rendered);

const total = rows.reduce((a, r) => a + r.live + r.experimental + r.dead + r.planned, 0);
const total = rows.reduce((a, r) => a + r.live + r.experimental + r['live-elsewhere'] + r.dead + r.planned, 0);
console.log(`✓ wrote ${STATE_COUNTS_PATH}`);
console.log(` ${rows.length} governed type(s), ${total} classified propert(ies).`);

Expand Down
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
33 changes: 33 additions & 0 deletions .changeset/liveness-live-elsewhere-verdict.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
"@objectstack/spec": patch
---

feat(spec): fifth liveness-ledger verdict `live-elsewhere` — dead here, enforced in a sibling repo, with gate-executable criteria (#13483)

The liveness ledger had no verdict for a key that is dead locally but genuinely
enforced in a sibling repo. The measured case is `manifest.runtime`: zero
load-side dispatch in objectstack+objectui, yet the cloud marketplace publish
gate hard-rejects (HTTP 422) an unverified publisher requesting the `node` tier
(#12400, cloud @15f55df). `dead` read alone licenses deleting the marketplace's
trust-gate input — deletion the maintainer explicitly ruled out (#11330) — and
`live` is refused by the gate, whose repo-local evidence must resolve against
this checkout. The stopgap was a qualifying sentence in the row's `note`; prose
is the weakest protection the ledger knows.

`live-elsewhere` says the split as data, and because `check-liveness.mts`
deliberately forces a decision on unknown statuses, it lands with criteria the
gate executes (`scripts/liveness/elsewhere.mts`, exit 1 on each): the
`evidence` must attribute at least one path to a foreign realm (the enforcer
pointer, foreign commit pinned in prose), `evidenceScope` must be
`"cross-repo"`, `verifiedAt` is REQUIRED (nothing local can watch a foreign
enforcer rot, so an undated elsewhere-claim would be unfalsifiable forever),
and the attestation expires: past 180 days the gate fails demanding a
re-reading of the foreign enforcer — never a re-stamp. If no seat with access
re-attests, the red build is the escalation back to the maintainer.

The status joins the evidence-scan population (any local path such an entry
also cites is held to the full existence/line/anchor/key-mention standard),
`STATUS_COLUMNS`, and the generated `state-counts.md` (new `elsewhere` column).
`manifest.runtime` migrates as the first row, attested at the #12400 reading
(2026-08-29). The ledger README documents the verdict and its re-attestation
discipline.
63 changes: 58 additions & 5 deletions packages/spec/liveness/README.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions packages/spec/liveness/manifest.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -250,10 +250,11 @@
}
},
"runtime": {
"status": "dead",
"verifiedAt": "2026-08-23",
"status": "live-elsewhere",
"verifiedAt": "2026-08-29",
"evidenceScope": "cross-repo",
"note": "READ TWICE HERE, ENFORCED AT THE CLOUD PUBLISH GATE, NOT ENFORCED AT LOAD — three different facts, so read this row before assuming either zero references or zero enforcement. The only reads of `manifest.runtime` in objectstack are two CLI progress lines that echo the value: packages/cli/src/commands/plugin/build.ts:127 and packages/cli/src/commands/plugin/publish.ts:94, both `runtime: ${manifest.runtime ?? 'unset'}`. Nothing dispatches on the tier: there is no `runtime === 'sandbox'` branch anywhere, and the QuickJS runner (packages/runtime/src/sandbox/) is the hook/action SCRIPT-BODY sandbox, reached from body-runner factories, never from a plugin's declared tier. objectui: zero reads. So the declared capability — \"in-process full PluginContext vs QuickJS-WASM capability-gated vs out-of-process\" (ADR-0025 §3.6) — is not delivered here, and echoing a string into a build log is not isolation. WHY THIS WAS SECURITY-SHAPED RATHER THAN COSMETIC, AND WHAT WAS DONE: the tombstone this ledger records one key above used to tell every upgrading author, verbatim, \"If you were relying on [loading.sandboxing] for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced\" — a redirection at a tier this repo does not enforce, i.e. ADR-0049 false compliance with a shipped migration message attached. ⚠️ SCOPE LIMIT NOW DISCHARGED: the cloud leg was measured by #12400 and it CHANGED the picture — the cloud control plane really does consume this key at the marketplace publish gate (`plugin-permission-audit.ts`: an unverified publisher requesting the `node` tier is hard-rejected with HTTP 422 and forced to manual review, reading of 2026-08-29). So the honest statement is split: ENFORCED at publish (cloud), NOT ENFORCED at load (here). Maintainer ruling 2026-08-30 (#11330) took option B — correct the text, do not retire the key — and explicitly ruled OUT retirement, because deleting the key would tear out the marketplace's trust-gate input. That correction has landed: the tombstone and both `runtime` describes now state the publish-gate/load-side split, and so does the ADR-0087 D3 entry `plugin-manifest-loading-retired`. ⚠️ READ `dead` NARROWLY HERE: it is the objectstack+objectui verdict about LOAD-SIDE dispatch (still true — the only local reads are the two CLI echoes above), and it is NOT a licence to delete, because the key has a real cross-repo consumer. Adding load-side enforcement is the open half, tracked as a v18 direction, not as a removal."
"evidence": "cloud: packages/service-cloud/src/plugin-permission-audit.ts#auditPluginPermissions @15f55df + packages/service-cloud/src/routes/package-publish.ts:530 — the marketplace publish gate: an unverified publisher requesting the node tier is hard-rejected with HTTP 422 and forced to manual review (#12400 reading of 2026-08-29). Load-side dispatch here is dead by measurement; the note carries that half.",
"note": "ENFORCED AT THE CLOUD PUBLISH GATE, NOT ENFORCED AT LOAD — and since #13483 the split IS the verdict: `live-elsewhere` says dead here by measurement, genuinely enforced in a sibling repo, and unlike the `dead` this row carried until then (with this note's qualifying sentence as the only guard — prose, which no check reads), it cannot be read as a licence to delete and the gate holds its criteria (foreign pointer, cross-repo scope, dated attestation with a 180d expiry — scripts/liveness/elsewhere.mts). LOCAL HALF, measured 2026-08-23 against b9e9227e3 with per-key controls: the only reads of `manifest.runtime` in objectstack are two CLI progress lines that echo the value — packages/cli/src/commands/plugin/build.ts:127 and packages/cli/src/commands/plugin/publish.ts:94, both `runtime: ${manifest.runtime ?? 'unset'}`. Nothing dispatches on the tier: there is no `runtime === 'sandbox'` branch anywhere, and the QuickJS runner (packages/runtime/src/sandbox/) is the hook/action SCRIPT-BODY sandbox, reached from body-runner factories, never from a plugin's declared tier. objectui: zero reads. So the declared capability — \"in-process full PluginContext vs QuickJS-WASM capability-gated vs out-of-process\" (ADR-0025 §3.6) — is not delivered at load, and echoing a string into a build log is not isolation. FOREIGN HALF (the evidence pointer): #12400 measured cloud origin/main @15f55df on 2026-08-29, controls held (15 manifest-property reads findable there) — `auditPluginPermissions` treats `runtime === 'node'` from an unverified publisher as a hard block, the caller turns it into HTTP 422 with forced manual review, and the tier is persisted to the version row (plugin-artifact.ts:179, package-publish.ts:531). `verifiedAt` on this row is THAT reading's date; re-attestation means someone with cloud access re-reads the enforcer and re-pins the commit — never a bare re-stamp (access is seat-dependent: `add_repo` on cloud is denied from some seats, including the #13483 seat on 2026-09-01, while the #10812 and #12400 readings prove cloud-capable seats exist). WHY THIS WAS SECURITY-SHAPED RATHER THAN COSMETIC: the tombstone this ledger records one key above used to tell every upgrading author, verbatim, \"If you were relying on [loading.sandboxing] for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced\" — a redirection at a tier this repo does not enforce at load, i.e. ADR-0049 false compliance with a shipped migration message attached. Maintainer ruling 2026-08-30 (#11330) took option B — correct the text, do not retire the key — and explicitly ruled OUT retirement, because deleting the key would tear out the marketplace's trust-gate input; the tombstone, both `runtime` describes and the ADR-0087 D3 entry `plugin-manifest-loading-retired` state the publish-gate/load-side split. Adding load-side enforcement is the open half, tracked as a v18 direction, not as a removal — if it lands, the verdict here flips to `live` with local evidence."
},
"packaging": {
"status": "live",
Expand Down
68 changes: 34 additions & 34 deletions packages/spec/liveness/state-counts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,37 +25,37 @@ protection envelope, which the gate auto-classifies `live` on every type that
spreads `MetadataProtectionFields`. See the README's counting-method section
for both corollaries.

| Type | live | exp | dead | planned | classified |
|---|---|---|---|---|---|
| `object` | 51 | 0 | 0 | 1 | 52 |
| `field` | 89 | 0 | 1 | 2 | 92 |
| `flow` | 34 | 0 | 6 | 0 | 40 |
| `action` | 41 | 0 | 3 | 2 | 46 |
| `hook` | 18 | 0 | 2 | 0 | 20 |
| `permission` | 36 | 0 | 6 | 0 | 42 |
| `position` | 12 | 0 | 0 | 0 | 12 |
| `agent` | 21 | 4 | 1 | 0 | 26 |
| `tool` | 13 | 1 | 0 | 0 | 14 |
| `skill` | 16 | 0 | 1 | 0 | 17 |
| `dataset` | 27 | 0 | 0 | 0 | 27 |
| `page` | 23 | 0 | 0 | 1 | 24 |
| `view` | 79 | 0 | 9 | 0 | 88 |
| `report` | 21 | 0 | 0 | 0 | 21 |
| `dashboard` | 34 | 0 | 7 | 0 | 41 |
| `webhook` | 19 | 0 | 0 | 0 | 19 |
| `query` | 16 | 0 | 5 | 0 | 21 |
| `datasource` | 30 | 0 | 0 | 0 | 30 |
| `app` | 47 | 0 | 9 | 0 | 56 |
| `book` | 20 | 0 | 1 | 0 | 21 |
| `doc` | 15 | 0 | 0 | 0 | 15 |
| `email_template` | 21 | 0 | 0 | 0 | 21 |
| `job` | 15 | 0 | 0 | 0 | 15 |
| `mapping` | 14 | 0 | 0 | 0 | 14 |
| `seed` | 12 | 0 | 0 | 0 | 12 |
| `translation` | 19 | 0 | 0 | 2 | 21 |
| `validation` | 15 | 0 | 3 | 0 | 18 |
| `api` | 25 | 0 | 0 | 2 | 27 |
| `capability` | 12 | 0 | 0 | 0 | 12 |
| `qa` | 4 | 0 | 5 | 0 | 9 |
| `manifest` | 22 | 0 | 16 | 0 | 38 |
| **total** | **821** | **5** | **75** | **10** | **911** |
| Type | live | exp | elsewhere | dead | planned | classified |
|---|---|---|---|---|---|---|
| `object` | 51 | 0 | 0 | 0 | 1 | 52 |
| `field` | 89 | 0 | 0 | 1 | 2 | 92 |
| `flow` | 34 | 0 | 0 | 6 | 0 | 40 |
| `action` | 41 | 0 | 0 | 3 | 2 | 46 |
| `hook` | 18 | 0 | 0 | 2 | 0 | 20 |
| `permission` | 36 | 0 | 0 | 6 | 0 | 42 |
| `position` | 12 | 0 | 0 | 0 | 0 | 12 |
| `agent` | 21 | 4 | 0 | 1 | 0 | 26 |
| `tool` | 13 | 1 | 0 | 0 | 0 | 14 |
| `skill` | 16 | 0 | 0 | 1 | 0 | 17 |
| `dataset` | 27 | 0 | 0 | 0 | 0 | 27 |
| `page` | 23 | 0 | 0 | 0 | 1 | 24 |
| `view` | 79 | 0 | 0 | 9 | 0 | 88 |
| `report` | 21 | 0 | 0 | 0 | 0 | 21 |
| `dashboard` | 34 | 0 | 0 | 7 | 0 | 41 |
| `webhook` | 19 | 0 | 0 | 0 | 0 | 19 |
| `query` | 16 | 0 | 0 | 5 | 0 | 21 |
| `datasource` | 30 | 0 | 0 | 0 | 0 | 30 |
| `app` | 47 | 0 | 0 | 9 | 0 | 56 |
| `book` | 20 | 0 | 0 | 1 | 0 | 21 |
| `doc` | 15 | 0 | 0 | 0 | 0 | 15 |
| `email_template` | 21 | 0 | 0 | 0 | 0 | 21 |
| `job` | 15 | 0 | 0 | 0 | 0 | 15 |
| `mapping` | 14 | 0 | 0 | 0 | 0 | 14 |
| `seed` | 12 | 0 | 0 | 0 | 0 | 12 |
| `translation` | 19 | 0 | 0 | 0 | 2 | 21 |
| `validation` | 15 | 0 | 0 | 3 | 0 | 18 |
| `api` | 25 | 0 | 0 | 0 | 2 | 27 |
| `capability` | 12 | 0 | 0 | 0 | 0 | 12 |
| `qa` | 4 | 0 | 0 | 5 | 0 | 9 |
| `manifest` | 22 | 0 | 1 | 15 | 0 | 38 |
| **total** | **821** | **5** | **1** | **74** | **10** | **911** |
4 changes: 2 additions & 2 deletions packages/spec/scripts/liveness/build-state-counts.mts
Original file line numberDiff line numberDiff line change
Expand Up@@ -113,7 +113,7 @@ const rows = foldStateCounts(Object.keys(types), Object.fromEntries(
// The header's rule is that a RED gate is not fatal here — the gate is red
// precisely when this artifact needs rewriting. This failure is the exception,
// and it is the same exception the unparseable report above already carves out:
// there is nothing to rewrite. The fold that produced `rows` reads four status
// there is nothing to rewrite. The fold that produced `rows` reads the published status
// names and drops everything else, so writing now would publish an understated
// `classified` — and the gate's freshness leg would then compare those bytes
// against a re-render of the SAME understated fold and call it current. A stale
Expand All@@ -134,7 +134,7 @@ if (totalErrors.length) {
const rendered = renderStateCounts(rows);
writeFileSync(join(ledgerRoot, STATE_COUNTS_FILE), rendered);

const total = rows.reduce((a, r) => a + r.live + r.experimental + r.dead + r.planned, 0);
const total = rows.reduce((a, r) => a + r.live + r.experimental + r['live-elsewhere'] + r.dead + r.planned, 0);
console.log(`✓ wrote ${STATE_COUNTS_PATH}`);
console.log(` ${rows.length} governed type(s), ${total} classified propert(ies).`);

Expand Down
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
33 changes: 33 additions & 0 deletions .changeset/liveness-live-elsewhere-verdict.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
"@objectstack/spec": patch
---

feat(spec): fifth liveness-ledger verdict `live-elsewhere` — dead here, enforced in a sibling repo, with gate-executable criteria (#13483)

The liveness ledger had no verdict for a key that is dead locally but genuinely
enforced in a sibling repo. The measured case is `manifest.runtime`: zero
load-side dispatch in objectstack+objectui, yet the cloud marketplace publish
gate hard-rejects (HTTP 422) an unverified publisher requesting the `node` tier
(#12400, cloud @15f55df). `dead` read alone licenses deleting the marketplace's
trust-gate input — deletion the maintainer explicitly ruled out (#11330) — and
`live` is refused by the gate, whose repo-local evidence must resolve against
this checkout. The stopgap was a qualifying sentence in the row's `note`; prose
is the weakest protection the ledger knows.

`live-elsewhere` says the split as data, and because `check-liveness.mts`
deliberately forces a decision on unknown statuses, it lands with criteria the
gate executes (`scripts/liveness/elsewhere.mts`, exit 1 on each): the
`evidence` must attribute at least one path to a foreign realm (the enforcer
pointer, foreign commit pinned in prose), `evidenceScope` must be
`"cross-repo"`, `verifiedAt` is REQUIRED (nothing local can watch a foreign
enforcer rot, so an undated elsewhere-claim would be unfalsifiable forever),
and the attestation expires: past 180 days the gate fails demanding a
re-reading of the foreign enforcer — never a re-stamp. If no seat with access
re-attests, the red build is the escalation back to the maintainer.

The status joins the evidence-scan population (any local path such an entry
also cites is held to the full existence/line/anchor/key-mention standard),
`STATUS_COLUMNS`, and the generated `state-counts.md` (new `elsewhere` column).
`manifest.runtime` migrates as the first row, attested at the #12400 reading
(2026-08-29). The ledger README documents the verdict and its re-attestation
discipline.
63 changes: 58 additions & 5 deletions packages/spec/liveness/README.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions packages/spec/liveness/manifest.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -250,10 +250,11 @@
}
},
"runtime": {
"status": "dead",
"verifiedAt": "2026-08-23",
"status": "live-elsewhere",
"verifiedAt": "2026-08-29",
"evidenceScope": "cross-repo",
"note": "READ TWICE HERE, ENFORCED AT THE CLOUD PUBLISH GATE, NOT ENFORCED AT LOAD — three different facts, so read this row before assuming either zero references or zero enforcement. The only reads of `manifest.runtime` in objectstack are two CLI progress lines that echo the value: packages/cli/src/commands/plugin/build.ts:127 and packages/cli/src/commands/plugin/publish.ts:94, both `runtime: ${manifest.runtime ?? 'unset'}`. Nothing dispatches on the tier: there is no `runtime === 'sandbox'` branch anywhere, and the QuickJS runner (packages/runtime/src/sandbox/) is the hook/action SCRIPT-BODY sandbox, reached from body-runner factories, never from a plugin's declared tier. objectui: zero reads. So the declared capability — \"in-process full PluginContext vs QuickJS-WASM capability-gated vs out-of-process\" (ADR-0025 §3.6) — is not delivered here, and echoing a string into a build log is not isolation. WHY THIS WAS SECURITY-SHAPED RATHER THAN COSMETIC, AND WHAT WAS DONE: the tombstone this ledger records one key above used to tell every upgrading author, verbatim, \"If you were relying on [loading.sandboxing] for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced\" — a redirection at a tier this repo does not enforce, i.e. ADR-0049 false compliance with a shipped migration message attached. ⚠️ SCOPE LIMIT NOW DISCHARGED: the cloud leg was measured by #12400 and it CHANGED the picture — the cloud control plane really does consume this key at the marketplace publish gate (`plugin-permission-audit.ts`: an unverified publisher requesting the `node` tier is hard-rejected with HTTP 422 and forced to manual review, reading of 2026-08-29). So the honest statement is split: ENFORCED at publish (cloud), NOT ENFORCED at load (here). Maintainer ruling 2026-08-30 (#11330) took option B — correct the text, do not retire the key — and explicitly ruled OUT retirement, because deleting the key would tear out the marketplace's trust-gate input. That correction has landed: the tombstone and both `runtime` describes now state the publish-gate/load-side split, and so does the ADR-0087 D3 entry `plugin-manifest-loading-retired`. ⚠️ READ `dead` NARROWLY HERE: it is the objectstack+objectui verdict about LOAD-SIDE dispatch (still true — the only local reads are the two CLI echoes above), and it is NOT a licence to delete, because the key has a real cross-repo consumer. Adding load-side enforcement is the open half, tracked as a v18 direction, not as a removal."
"evidence": "cloud: packages/service-cloud/src/plugin-permission-audit.ts#auditPluginPermissions @15f55df + packages/service-cloud/src/routes/package-publish.ts:530 — the marketplace publish gate: an unverified publisher requesting the node tier is hard-rejected with HTTP 422 and forced to manual review (#12400 reading of 2026-08-29). Load-side dispatch here is dead by measurement; the note carries that half.",
"note": "ENFORCED AT THE CLOUD PUBLISH GATE, NOT ENFORCED AT LOAD — and since #13483 the split IS the verdict: `live-elsewhere` says dead here by measurement, genuinely enforced in a sibling repo, and unlike the `dead` this row carried until then (with this note's qualifying sentence as the only guard — prose, which no check reads), it cannot be read as a licence to delete and the gate holds its criteria (foreign pointer, cross-repo scope, dated attestation with a 180d expiry — scripts/liveness/elsewhere.mts). LOCAL HALF, measured 2026-08-23 against b9e9227e3 with per-key controls: the only reads of `manifest.runtime` in objectstack are two CLI progress lines that echo the value — packages/cli/src/commands/plugin/build.ts:127 and packages/cli/src/commands/plugin/publish.ts:94, both `runtime: ${manifest.runtime ?? 'unset'}`. Nothing dispatches on the tier: there is no `runtime === 'sandbox'` branch anywhere, and the QuickJS runner (packages/runtime/src/sandbox/) is the hook/action SCRIPT-BODY sandbox, reached from body-runner factories, never from a plugin's declared tier. objectui: zero reads. So the declared capability — \"in-process full PluginContext vs QuickJS-WASM capability-gated vs out-of-process\" (ADR-0025 §3.6) — is not delivered at load, and echoing a string into a build log is not isolation. FOREIGN HALF (the evidence pointer): #12400 measured cloud origin/main @15f55df on 2026-08-29, controls held (15 manifest-property reads findable there) — `auditPluginPermissions` treats `runtime === 'node'` from an unverified publisher as a hard block, the caller turns it into HTTP 422 with forced manual review, and the tier is persisted to the version row (plugin-artifact.ts:179, package-publish.ts:531). `verifiedAt` on this row is THAT reading's date; re-attestation means someone with cloud access re-reads the enforcer and re-pins the commit — never a bare re-stamp (access is seat-dependent: `add_repo` on cloud is denied from some seats, including the #13483 seat on 2026-09-01, while the #10812 and #12400 readings prove cloud-capable seats exist). WHY THIS WAS SECURITY-SHAPED RATHER THAN COSMETIC: the tombstone this ledger records one key above used to tell every upgrading author, verbatim, \"If you were relying on [loading.sandboxing] for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced\" — a redirection at a tier this repo does not enforce at load, i.e. ADR-0049 false compliance with a shipped migration message attached. Maintainer ruling 2026-08-30 (#11330) took option B — correct the text, do not retire the key — and explicitly ruled OUT retirement, because deleting the key would tear out the marketplace's trust-gate input; the tombstone, both `runtime` describes and the ADR-0087 D3 entry `plugin-manifest-loading-retired` state the publish-gate/load-side split. Adding load-side enforcement is the open half, tracked as a v18 direction, not as a removal — if it lands, the verdict here flips to `live` with local evidence."
},
"packaging": {
"status": "live",
Expand Down
68 changes: 34 additions & 34 deletions packages/spec/liveness/state-counts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,37 +25,37 @@ protection envelope, which the gate auto-classifies `live` on every type that
spreads `MetadataProtectionFields`. See the README's counting-method section
for both corollaries.

| Type | live | exp | dead | planned | classified |
|---|---|---|---|---|---|
| `object` | 51 | 0 | 0 | 1 | 52 |
| `field` | 89 | 0 | 1 | 2 | 92 |
| `flow` | 34 | 0 | 6 | 0 | 40 |
| `action` | 41 | 0 | 3 | 2 | 46 |
| `hook` | 18 | 0 | 2 | 0 | 20 |
| `permission` | 36 | 0 | 6 | 0 | 42 |
| `position` | 12 | 0 | 0 | 0 | 12 |
| `agent` | 21 | 4 | 1 | 0 | 26 |
| `tool` | 13 | 1 | 0 | 0 | 14 |
| `skill` | 16 | 0 | 1 | 0 | 17 |
| `dataset` | 27 | 0 | 0 | 0 | 27 |
| `page` | 23 | 0 | 0 | 1 | 24 |
| `view` | 79 | 0 | 9 | 0 | 88 |
| `report` | 21 | 0 | 0 | 0 | 21 |
| `dashboard` | 34 | 0 | 7 | 0 | 41 |
| `webhook` | 19 | 0 | 0 | 0 | 19 |
| `query` | 16 | 0 | 5 | 0 | 21 |
| `datasource` | 30 | 0 | 0 | 0 | 30 |
| `app` | 47 | 0 | 9 | 0 | 56 |
| `book` | 20 | 0 | 1 | 0 | 21 |
| `doc` | 15 | 0 | 0 | 0 | 15 |
| `email_template` | 21 | 0 | 0 | 0 | 21 |
| `job` | 15 | 0 | 0 | 0 | 15 |
| `mapping` | 14 | 0 | 0 | 0 | 14 |
| `seed` | 12 | 0 | 0 | 0 | 12 |
| `translation` | 19 | 0 | 0 | 2 | 21 |
| `validation` | 15 | 0 | 3 | 0 | 18 |
| `api` | 25 | 0 | 0 | 2 | 27 |
| `capability` | 12 | 0 | 0 | 0 | 12 |
| `qa` | 4 | 0 | 5 | 0 | 9 |
| `manifest` | 22 | 0 | 16 | 0 | 38 |
| **total** | **821** | **5** | **75** | **10** | **911** |
| Type | live | exp | elsewhere | dead | planned | classified |
|---|---|---|---|---|---|---|
| `object` | 51 | 0 | 0 | 0 | 1 | 52 |
| `field` | 89 | 0 | 0 | 1 | 2 | 92 |
| `flow` | 34 | 0 | 0 | 6 | 0 | 40 |
| `action` | 41 | 0 | 0 | 3 | 2 | 46 |
| `hook` | 18 | 0 | 0 | 2 | 0 | 20 |
| `permission` | 36 | 0 | 0 | 6 | 0 | 42 |
| `position` | 12 | 0 | 0 | 0 | 0 | 12 |
| `agent` | 21 | 4 | 0 | 1 | 0 | 26 |
| `tool` | 13 | 1 | 0 | 0 | 0 | 14 |
| `skill` | 16 | 0 | 0 | 1 | 0 | 17 |
| `dataset` | 27 | 0 | 0 | 0 | 0 | 27 |
| `page` | 23 | 0 | 0 | 0 | 1 | 24 |
| `view` | 79 | 0 | 0 | 9 | 0 | 88 |
| `report` | 21 | 0 | 0 | 0 | 0 | 21 |
| `dashboard` | 34 | 0 | 0 | 7 | 0 | 41 |
| `webhook` | 19 | 0 | 0 | 0 | 0 | 19 |
| `query` | 16 | 0 | 0 | 5 | 0 | 21 |
| `datasource` | 30 | 0 | 0 | 0 | 0 | 30 |
| `app` | 47 | 0 | 0 | 9 | 0 | 56 |
| `book` | 20 | 0 | 0 | 1 | 0 | 21 |
| `doc` | 15 | 0 | 0 | 0 | 0 | 15 |
| `email_template` | 21 | 0 | 0 | 0 | 0 | 21 |
| `job` | 15 | 0 | 0 | 0 | 0 | 15 |
| `mapping` | 14 | 0 | 0 | 0 | 0 | 14 |
| `seed` | 12 | 0 | 0 | 0 | 0 | 12 |
| `translation` | 19 | 0 | 0 | 0 | 2 | 21 |
| `validation` | 15 | 0 | 0 | 3 | 0 | 18 |
| `api` | 25 | 0 | 0 | 0 | 2 | 27 |
| `capability` | 12 | 0 | 0 | 0 | 0 | 12 |
| `qa` | 4 | 0 | 0 | 5 | 0 | 9 |
| `manifest` | 22 | 0 | 1 | 15 | 0 | 38 |
| **total** | **821** | **5** | **1** | **74** | **10** | **911** |
4 changes: 2 additions & 2 deletions packages/spec/scripts/liveness/build-state-counts.mts
Original file line numberDiff line numberDiff line change
Expand Up@@ -113,7 +113,7 @@ const rows = foldStateCounts(Object.keys(types), Object.fromEntries(
// The header's rule is that a RED gate is not fatal here — the gate is red
// precisely when this artifact needs rewriting. This failure is the exception,
// and it is the same exception the unparseable report above already carves out:
// there is nothing to rewrite. The fold that produced `rows` reads four status
// there is nothing to rewrite. The fold that produced `rows` reads the published status
// names and drops everything else, so writing now would publish an understated
// `classified` — and the gate's freshness leg would then compare those bytes
// against a re-render of the SAME understated fold and call it current. A stale
Expand All@@ -134,7 +134,7 @@ if (totalErrors.length) {
const rendered = renderStateCounts(rows);
writeFileSync(join(ledgerRoot, STATE_COUNTS_FILE), rendered);

const total = rows.reduce((a, r) => a + r.live + r.experimental + r.dead + r.planned, 0);
const total = rows.reduce((a, r) => a + r.live + r.experimental + r['live-elsewhere'] + r.dead + r.planned, 0);
console.log(`✓ wrote ${STATE_COUNTS_PATH}`);
console.log(` ${rows.length} governed type(s), ${total} classified propert(ies).`);

Expand Down
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
33 changes: 33 additions & 0 deletions .changeset/liveness-live-elsewhere-verdict.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
"@objectstack/spec": patch
---

feat(spec): fifth liveness-ledger verdict `live-elsewhere` — dead here, enforced in a sibling repo, with gate-executable criteria (#13483)

The liveness ledger had no verdict for a key that is dead locally but genuinely
enforced in a sibling repo. The measured case is `manifest.runtime`: zero
load-side dispatch in objectstack+objectui, yet the cloud marketplace publish
gate hard-rejects (HTTP 422) an unverified publisher requesting the `node` tier
(#12400, cloud @15f55df). `dead` read alone licenses deleting the marketplace's
trust-gate input — deletion the maintainer explicitly ruled out (#11330) — and
`live` is refused by the gate, whose repo-local evidence must resolve against
this checkout. The stopgap was a qualifying sentence in the row's `note`; prose
is the weakest protection the ledger knows.

`live-elsewhere` says the split as data, and because `check-liveness.mts`
deliberately forces a decision on unknown statuses, it lands with criteria the
gate executes (`scripts/liveness/elsewhere.mts`, exit 1 on each): the
`evidence` must attribute at least one path to a foreign realm (the enforcer
pointer, foreign commit pinned in prose), `evidenceScope` must be
`"cross-repo"`, `verifiedAt` is REQUIRED (nothing local can watch a foreign
enforcer rot, so an undated elsewhere-claim would be unfalsifiable forever),
and the attestation expires: past 180 days the gate fails demanding a
re-reading of the foreign enforcer — never a re-stamp. If no seat with access
re-attests, the red build is the escalation back to the maintainer.

The status joins the evidence-scan population (any local path such an entry
also cites is held to the full existence/line/anchor/key-mention standard),
`STATUS_COLUMNS`, and the generated `state-counts.md` (new `elsewhere` column).
`manifest.runtime` migrates as the first row, attested at the #12400 reading
(2026-08-29). The ledger README documents the verdict and its re-attestation
discipline.
63 changes: 58 additions & 5 deletions packages/spec/liveness/README.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions packages/spec/liveness/manifest.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -250,10 +250,11 @@
}
},
"runtime": {
"status": "dead",
"verifiedAt": "2026-08-23",
"status": "live-elsewhere",
"verifiedAt": "2026-08-29",
"evidenceScope": "cross-repo",
"note": "READ TWICE HERE, ENFORCED AT THE CLOUD PUBLISH GATE, NOT ENFORCED AT LOAD — three different facts, so read this row before assuming either zero references or zero enforcement. The only reads of `manifest.runtime` in objectstack are two CLI progress lines that echo the value: packages/cli/src/commands/plugin/build.ts:127 and packages/cli/src/commands/plugin/publish.ts:94, both `runtime: ${manifest.runtime ?? 'unset'}`. Nothing dispatches on the tier: there is no `runtime === 'sandbox'` branch anywhere, and the QuickJS runner (packages/runtime/src/sandbox/) is the hook/action SCRIPT-BODY sandbox, reached from body-runner factories, never from a plugin's declared tier. objectui: zero reads. So the declared capability — \"in-process full PluginContext vs QuickJS-WASM capability-gated vs out-of-process\" (ADR-0025 §3.6) — is not delivered here, and echoing a string into a build log is not isolation. WHY THIS WAS SECURITY-SHAPED RATHER THAN COSMETIC, AND WHAT WAS DONE: the tombstone this ledger records one key above used to tell every upgrading author, verbatim, \"If you were relying on [loading.sandboxing] for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced\" — a redirection at a tier this repo does not enforce, i.e. ADR-0049 false compliance with a shipped migration message attached. ⚠️ SCOPE LIMIT NOW DISCHARGED: the cloud leg was measured by #12400 and it CHANGED the picture — the cloud control plane really does consume this key at the marketplace publish gate (`plugin-permission-audit.ts`: an unverified publisher requesting the `node` tier is hard-rejected with HTTP 422 and forced to manual review, reading of 2026-08-29). So the honest statement is split: ENFORCED at publish (cloud), NOT ENFORCED at load (here). Maintainer ruling 2026-08-30 (#11330) took option B — correct the text, do not retire the key — and explicitly ruled OUT retirement, because deleting the key would tear out the marketplace's trust-gate input. That correction has landed: the tombstone and both `runtime` describes now state the publish-gate/load-side split, and so does the ADR-0087 D3 entry `plugin-manifest-loading-retired`. ⚠️ READ `dead` NARROWLY HERE: it is the objectstack+objectui verdict about LOAD-SIDE dispatch (still true — the only local reads are the two CLI echoes above), and it is NOT a licence to delete, because the key has a real cross-repo consumer. Adding load-side enforcement is the open half, tracked as a v18 direction, not as a removal."
"evidence": "cloud: packages/service-cloud/src/plugin-permission-audit.ts#auditPluginPermissions @15f55df + packages/service-cloud/src/routes/package-publish.ts:530 — the marketplace publish gate: an unverified publisher requesting the node tier is hard-rejected with HTTP 422 and forced to manual review (#12400 reading of 2026-08-29). Load-side dispatch here is dead by measurement; the note carries that half.",
"note": "ENFORCED AT THE CLOUD PUBLISH GATE, NOT ENFORCED AT LOAD — and since #13483 the split IS the verdict: `live-elsewhere` says dead here by measurement, genuinely enforced in a sibling repo, and unlike the `dead` this row carried until then (with this note's qualifying sentence as the only guard — prose, which no check reads), it cannot be read as a licence to delete and the gate holds its criteria (foreign pointer, cross-repo scope, dated attestation with a 180d expiry — scripts/liveness/elsewhere.mts). LOCAL HALF, measured 2026-08-23 against b9e9227e3 with per-key controls: the only reads of `manifest.runtime` in objectstack are two CLI progress lines that echo the value — packages/cli/src/commands/plugin/build.ts:127 and packages/cli/src/commands/plugin/publish.ts:94, both `runtime: ${manifest.runtime ?? 'unset'}`. Nothing dispatches on the tier: there is no `runtime === 'sandbox'` branch anywhere, and the QuickJS runner (packages/runtime/src/sandbox/) is the hook/action SCRIPT-BODY sandbox, reached from body-runner factories, never from a plugin's declared tier. objectui: zero reads. So the declared capability — \"in-process full PluginContext vs QuickJS-WASM capability-gated vs out-of-process\" (ADR-0025 §3.6) — is not delivered at load, and echoing a string into a build log is not isolation. FOREIGN HALF (the evidence pointer): #12400 measured cloud origin/main @15f55df on 2026-08-29, controls held (15 manifest-property reads findable there) — `auditPluginPermissions` treats `runtime === 'node'` from an unverified publisher as a hard block, the caller turns it into HTTP 422 with forced manual review, and the tier is persisted to the version row (plugin-artifact.ts:179, package-publish.ts:531). `verifiedAt` on this row is THAT reading's date; re-attestation means someone with cloud access re-reads the enforcer and re-pins the commit — never a bare re-stamp (access is seat-dependent: `add_repo` on cloud is denied from some seats, including the #13483 seat on 2026-09-01, while the #10812 and #12400 readings prove cloud-capable seats exist). WHY THIS WAS SECURITY-SHAPED RATHER THAN COSMETIC: the tombstone this ledger records one key above used to tell every upgrading author, verbatim, \"If you were relying on [loading.sandboxing] for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced\" — a redirection at a tier this repo does not enforce at load, i.e. ADR-0049 false compliance with a shipped migration message attached. Maintainer ruling 2026-08-30 (#11330) took option B — correct the text, do not retire the key — and explicitly ruled OUT retirement, because deleting the key would tear out the marketplace's trust-gate input; the tombstone, both `runtime` describes and the ADR-0087 D3 entry `plugin-manifest-loading-retired` state the publish-gate/load-side split. Adding load-side enforcement is the open half, tracked as a v18 direction, not as a removal — if it lands, the verdict here flips to `live` with local evidence."
},
"packaging": {
"status": "live",
Expand Down
68 changes: 34 additions & 34 deletions packages/spec/liveness/state-counts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,37 +25,37 @@ protection envelope, which the gate auto-classifies `live` on every type that
spreads `MetadataProtectionFields`. See the README's counting-method section
for both corollaries.

| Type | live | exp | dead | planned | classified |
|---|---|---|---|---|---|
| `object` | 51 | 0 | 0 | 1 | 52 |
| `field` | 89 | 0 | 1 | 2 | 92 |
| `flow` | 34 | 0 | 6 | 0 | 40 |
| `action` | 41 | 0 | 3 | 2 | 46 |
| `hook` | 18 | 0 | 2 | 0 | 20 |
| `permission` | 36 | 0 | 6 | 0 | 42 |
| `position` | 12 | 0 | 0 | 0 | 12 |
| `agent` | 21 | 4 | 1 | 0 | 26 |
| `tool` | 13 | 1 | 0 | 0 | 14 |
| `skill` | 16 | 0 | 1 | 0 | 17 |
| `dataset` | 27 | 0 | 0 | 0 | 27 |
| `page` | 23 | 0 | 0 | 1 | 24 |
| `view` | 79 | 0 | 9 | 0 | 88 |
| `report` | 21 | 0 | 0 | 0 | 21 |
| `dashboard` | 34 | 0 | 7 | 0 | 41 |
| `webhook` | 19 | 0 | 0 | 0 | 19 |
| `query` | 16 | 0 | 5 | 0 | 21 |
| `datasource` | 30 | 0 | 0 | 0 | 30 |
| `app` | 47 | 0 | 9 | 0 | 56 |
| `book` | 20 | 0 | 1 | 0 | 21 |
| `doc` | 15 | 0 | 0 | 0 | 15 |
| `email_template` | 21 | 0 | 0 | 0 | 21 |
| `job` | 15 | 0 | 0 | 0 | 15 |
| `mapping` | 14 | 0 | 0 | 0 | 14 |
| `seed` | 12 | 0 | 0 | 0 | 12 |
| `translation` | 19 | 0 | 0 | 2 | 21 |
| `validation` | 15 | 0 | 3 | 0 | 18 |
| `api` | 25 | 0 | 0 | 2 | 27 |
| `capability` | 12 | 0 | 0 | 0 | 12 |
| `qa` | 4 | 0 | 5 | 0 | 9 |
| `manifest` | 22 | 0 | 16 | 0 | 38 |
| **total** | **821** | **5** | **75** | **10** | **911** |
| Type | live | exp | elsewhere | dead | planned | classified |
|---|---|---|---|---|---|---|
| `object` | 51 | 0 | 0 | 0 | 1 | 52 |
| `field` | 89 | 0 | 0 | 1 | 2 | 92 |
| `flow` | 34 | 0 | 0 | 6 | 0 | 40 |
| `action` | 41 | 0 | 0 | 3 | 2 | 46 |
| `hook` | 18 | 0 | 0 | 2 | 0 | 20 |
| `permission` | 36 | 0 | 0 | 6 | 0 | 42 |
| `position` | 12 | 0 | 0 | 0 | 0 | 12 |
| `agent` | 21 | 4 | 0 | 1 | 0 | 26 |
| `tool` | 13 | 1 | 0 | 0 | 0 | 14 |
| `skill` | 16 | 0 | 0 | 1 | 0 | 17 |
| `dataset` | 27 | 0 | 0 | 0 | 0 | 27 |
| `page` | 23 | 0 | 0 | 0 | 1 | 24 |
| `view` | 79 | 0 | 0 | 9 | 0 | 88 |
| `report` | 21 | 0 | 0 | 0 | 0 | 21 |
| `dashboard` | 34 | 0 | 0 | 7 | 0 | 41 |
| `webhook` | 19 | 0 | 0 | 0 | 0 | 19 |
| `query` | 16 | 0 | 0 | 5 | 0 | 21 |
| `datasource` | 30 | 0 | 0 | 0 | 0 | 30 |
| `app` | 47 | 0 | 0 | 9 | 0 | 56 |
| `book` | 20 | 0 | 0 | 1 | 0 | 21 |
| `doc` | 15 | 0 | 0 | 0 | 0 | 15 |
| `email_template` | 21 | 0 | 0 | 0 | 0 | 21 |
| `job` | 15 | 0 | 0 | 0 | 0 | 15 |
| `mapping` | 14 | 0 | 0 | 0 | 0 | 14 |
| `seed` | 12 | 0 | 0 | 0 | 0 | 12 |
| `translation` | 19 | 0 | 0 | 0 | 2 | 21 |
| `validation` | 15 | 0 | 0 | 3 | 0 | 18 |
| `api` | 25 | 0 | 0 | 0 | 2 | 27 |
| `capability` | 12 | 0 | 0 | 0 | 0 | 12 |
| `qa` | 4 | 0 | 0 | 5 | 0 | 9 |
| `manifest` | 22 | 0 | 1 | 15 | 0 | 38 |
| **total** | **821** | **5** | **1** | **74** | **10** | **911** |
4 changes: 2 additions & 2 deletions packages/spec/scripts/liveness/build-state-counts.mts
Original file line numberDiff line numberDiff line change
Expand Up@@ -113,7 +113,7 @@ const rows = foldStateCounts(Object.keys(types), Object.fromEntries(
// The header's rule is that a RED gate is not fatal here — the gate is red
// precisely when this artifact needs rewriting. This failure is the exception,
// and it is the same exception the unparseable report above already carves out:
// there is nothing to rewrite. The fold that produced `rows` reads four status
// there is nothing to rewrite. The fold that produced `rows` reads the published status
// names and drops everything else, so writing now would publish an understated
// `classified` — and the gate's freshness leg would then compare those bytes
// against a re-render of the SAME understated fold and call it current. A stale
Expand All@@ -134,7 +134,7 @@ if (totalErrors.length) {
const rendered = renderStateCounts(rows);
writeFileSync(join(ledgerRoot, STATE_COUNTS_FILE), rendered);

const total = rows.reduce((a, r) => a + r.live + r.experimental + r.dead + r.planned, 0);
const total = rows.reduce((a, r) => a + r.live + r.experimental + r['live-elsewhere'] + r.dead + r.planned, 0);
console.log(`✓ wrote ${STATE_COUNTS_PATH}`);
console.log(` ${rows.length} governed type(s), ${total} classified propert(ies).`);

Expand Down
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
33 changes: 33 additions & 0 deletions .changeset/liveness-live-elsewhere-verdict.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
"@objectstack/spec": patch
---

feat(spec): fifth liveness-ledger verdict `live-elsewhere` — dead here, enforced in a sibling repo, with gate-executable criteria (#13483)

The liveness ledger had no verdict for a key that is dead locally but genuinely
enforced in a sibling repo. The measured case is `manifest.runtime`: zero
load-side dispatch in objectstack+objectui, yet the cloud marketplace publish
gate hard-rejects (HTTP 422) an unverified publisher requesting the `node` tier
(#12400, cloud @15f55df). `dead` read alone licenses deleting the marketplace's
trust-gate input — deletion the maintainer explicitly ruled out (#11330) — and
`live` is refused by the gate, whose repo-local evidence must resolve against
this checkout. The stopgap was a qualifying sentence in the row's `note`; prose
is the weakest protection the ledger knows.

`live-elsewhere` says the split as data, and because `check-liveness.mts`
deliberately forces a decision on unknown statuses, it lands with criteria the
gate executes (`scripts/liveness/elsewhere.mts`, exit 1 on each): the
`evidence` must attribute at least one path to a foreign realm (the enforcer
pointer, foreign commit pinned in prose), `evidenceScope` must be
`"cross-repo"`, `verifiedAt` is REQUIRED (nothing local can watch a foreign
enforcer rot, so an undated elsewhere-claim would be unfalsifiable forever),
and the attestation expires: past 180 days the gate fails demanding a
re-reading of the foreign enforcer — never a re-stamp. If no seat with access
re-attests, the red build is the escalation back to the maintainer.

The status joins the evidence-scan population (any local path such an entry
also cites is held to the full existence/line/anchor/key-mention standard),
`STATUS_COLUMNS`, and the generated `state-counts.md` (new `elsewhere` column).
`manifest.runtime` migrates as the first row, attested at the #12400 reading
(2026-08-29). The ledger README documents the verdict and its re-attestation
discipline.
63 changes: 58 additions & 5 deletions packages/spec/liveness/README.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions packages/spec/liveness/manifest.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -250,10 +250,11 @@
}
},
"runtime": {
"status": "dead",
"verifiedAt": "2026-08-23",
"status": "live-elsewhere",
"verifiedAt": "2026-08-29",
"evidenceScope": "cross-repo",
"note": "READ TWICE HERE, ENFORCED AT THE CLOUD PUBLISH GATE, NOT ENFORCED AT LOAD — three different facts, so read this row before assuming either zero references or zero enforcement. The only reads of `manifest.runtime` in objectstack are two CLI progress lines that echo the value: packages/cli/src/commands/plugin/build.ts:127 and packages/cli/src/commands/plugin/publish.ts:94, both `runtime: ${manifest.runtime ?? 'unset'}`. Nothing dispatches on the tier: there is no `runtime === 'sandbox'` branch anywhere, and the QuickJS runner (packages/runtime/src/sandbox/) is the hook/action SCRIPT-BODY sandbox, reached from body-runner factories, never from a plugin's declared tier. objectui: zero reads. So the declared capability — \"in-process full PluginContext vs QuickJS-WASM capability-gated vs out-of-process\" (ADR-0025 §3.6) — is not delivered here, and echoing a string into a build log is not isolation. WHY THIS WAS SECURITY-SHAPED RATHER THAN COSMETIC, AND WHAT WAS DONE: the tombstone this ledger records one key above used to tell every upgrading author, verbatim, \"If you were relying on [loading.sandboxing] for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced\" — a redirection at a tier this repo does not enforce, i.e. ADR-0049 false compliance with a shipped migration message attached. ⚠️ SCOPE LIMIT NOW DISCHARGED: the cloud leg was measured by #12400 and it CHANGED the picture — the cloud control plane really does consume this key at the marketplace publish gate (`plugin-permission-audit.ts`: an unverified publisher requesting the `node` tier is hard-rejected with HTTP 422 and forced to manual review, reading of 2026-08-29). So the honest statement is split: ENFORCED at publish (cloud), NOT ENFORCED at load (here). Maintainer ruling 2026-08-30 (#11330) took option B — correct the text, do not retire the key — and explicitly ruled OUT retirement, because deleting the key would tear out the marketplace's trust-gate input. That correction has landed: the tombstone and both `runtime` describes now state the publish-gate/load-side split, and so does the ADR-0087 D3 entry `plugin-manifest-loading-retired`. ⚠️ READ `dead` NARROWLY HERE: it is the objectstack+objectui verdict about LOAD-SIDE dispatch (still true — the only local reads are the two CLI echoes above), and it is NOT a licence to delete, because the key has a real cross-repo consumer. Adding load-side enforcement is the open half, tracked as a v18 direction, not as a removal."
"evidence": "cloud: packages/service-cloud/src/plugin-permission-audit.ts#auditPluginPermissions @15f55df + packages/service-cloud/src/routes/package-publish.ts:530 — the marketplace publish gate: an unverified publisher requesting the node tier is hard-rejected with HTTP 422 and forced to manual review (#12400 reading of 2026-08-29). Load-side dispatch here is dead by measurement; the note carries that half.",
"note": "ENFORCED AT THE CLOUD PUBLISH GATE, NOT ENFORCED AT LOAD — and since #13483 the split IS the verdict: `live-elsewhere` says dead here by measurement, genuinely enforced in a sibling repo, and unlike the `dead` this row carried until then (with this note's qualifying sentence as the only guard — prose, which no check reads), it cannot be read as a licence to delete and the gate holds its criteria (foreign pointer, cross-repo scope, dated attestation with a 180d expiry — scripts/liveness/elsewhere.mts). LOCAL HALF, measured 2026-08-23 against b9e9227e3 with per-key controls: the only reads of `manifest.runtime` in objectstack are two CLI progress lines that echo the value — packages/cli/src/commands/plugin/build.ts:127 and packages/cli/src/commands/plugin/publish.ts:94, both `runtime: ${manifest.runtime ?? 'unset'}`. Nothing dispatches on the tier: there is no `runtime === 'sandbox'` branch anywhere, and the QuickJS runner (packages/runtime/src/sandbox/) is the hook/action SCRIPT-BODY sandbox, reached from body-runner factories, never from a plugin's declared tier. objectui: zero reads. So the declared capability — \"in-process full PluginContext vs QuickJS-WASM capability-gated vs out-of-process\" (ADR-0025 §3.6) — is not delivered at load, and echoing a string into a build log is not isolation. FOREIGN HALF (the evidence pointer): #12400 measured cloud origin/main @15f55df on 2026-08-29, controls held (15 manifest-property reads findable there) — `auditPluginPermissions` treats `runtime === 'node'` from an unverified publisher as a hard block, the caller turns it into HTTP 422 with forced manual review, and the tier is persisted to the version row (plugin-artifact.ts:179, package-publish.ts:531). `verifiedAt` on this row is THAT reading's date; re-attestation means someone with cloud access re-reads the enforcer and re-pins the commit — never a bare re-stamp (access is seat-dependent: `add_repo` on cloud is denied from some seats, including the #13483 seat on 2026-09-01, while the #10812 and #12400 readings prove cloud-capable seats exist). WHY THIS WAS SECURITY-SHAPED RATHER THAN COSMETIC: the tombstone this ledger records one key above used to tell every upgrading author, verbatim, \"If you were relying on [loading.sandboxing] for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced\" — a redirection at a tier this repo does not enforce at load, i.e. ADR-0049 false compliance with a shipped migration message attached. Maintainer ruling 2026-08-30 (#11330) took option B — correct the text, do not retire the key — and explicitly ruled OUT retirement, because deleting the key would tear out the marketplace's trust-gate input; the tombstone, both `runtime` describes and the ADR-0087 D3 entry `plugin-manifest-loading-retired` state the publish-gate/load-side split. Adding load-side enforcement is the open half, tracked as a v18 direction, not as a removal — if it lands, the verdict here flips to `live` with local evidence."
},
"packaging": {
"status": "live",
Expand Down
68 changes: 34 additions & 34 deletions packages/spec/liveness/state-counts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,37 +25,37 @@ protection envelope, which the gate auto-classifies `live` on every type that
spreads `MetadataProtectionFields`. See the README's counting-method section
for both corollaries.

| Type | live | exp | dead | planned | classified |
|---|---|---|---|---|---|
| `object` | 51 | 0 | 0 | 1 | 52 |
| `field` | 89 | 0 | 1 | 2 | 92 |
| `flow` | 34 | 0 | 6 | 0 | 40 |
| `action` | 41 | 0 | 3 | 2 | 46 |
| `hook` | 18 | 0 | 2 | 0 | 20 |
| `permission` | 36 | 0 | 6 | 0 | 42 |
| `position` | 12 | 0 | 0 | 0 | 12 |
| `agent` | 21 | 4 | 1 | 0 | 26 |
| `tool` | 13 | 1 | 0 | 0 | 14 |
| `skill` | 16 | 0 | 1 | 0 | 17 |
| `dataset` | 27 | 0 | 0 | 0 | 27 |
| `page` | 23 | 0 | 0 | 1 | 24 |
| `view` | 79 | 0 | 9 | 0 | 88 |
| `report` | 21 | 0 | 0 | 0 | 21 |
| `dashboard` | 34 | 0 | 7 | 0 | 41 |
| `webhook` | 19 | 0 | 0 | 0 | 19 |
| `query` | 16 | 0 | 5 | 0 | 21 |
| `datasource` | 30 | 0 | 0 | 0 | 30 |
| `app` | 47 | 0 | 9 | 0 | 56 |
| `book` | 20 | 0 | 1 | 0 | 21 |
| `doc` | 15 | 0 | 0 | 0 | 15 |
| `email_template` | 21 | 0 | 0 | 0 | 21 |
| `job` | 15 | 0 | 0 | 0 | 15 |
| `mapping` | 14 | 0 | 0 | 0 | 14 |
| `seed` | 12 | 0 | 0 | 0 | 12 |
| `translation` | 19 | 0 | 0 | 2 | 21 |
| `validation` | 15 | 0 | 3 | 0 | 18 |
| `api` | 25 | 0 | 0 | 2 | 27 |
| `capability` | 12 | 0 | 0 | 0 | 12 |
| `qa` | 4 | 0 | 5 | 0 | 9 |
| `manifest` | 22 | 0 | 16 | 0 | 38 |
| **total** | **821** | **5** | **75** | **10** | **911** |
| Type | live | exp | elsewhere | dead | planned | classified |
|---|---|---|---|---|---|---|
| `object` | 51 | 0 | 0 | 0 | 1 | 52 |
| `field` | 89 | 0 | 0 | 1 | 2 | 92 |
| `flow` | 34 | 0 | 0 | 6 | 0 | 40 |
| `action` | 41 | 0 | 0 | 3 | 2 | 46 |
| `hook` | 18 | 0 | 0 | 2 | 0 | 20 |
| `permission` | 36 | 0 | 0 | 6 | 0 | 42 |
| `position` | 12 | 0 | 0 | 0 | 0 | 12 |
| `agent` | 21 | 4 | 0 | 1 | 0 | 26 |
| `tool` | 13 | 1 | 0 | 0 | 0 | 14 |
| `skill` | 16 | 0 | 0 | 1 | 0 | 17 |
| `dataset` | 27 | 0 | 0 | 0 | 0 | 27 |
| `page` | 23 | 0 | 0 | 0 | 1 | 24 |
| `view` | 79 | 0 | 0 | 9 | 0 | 88 |
| `report` | 21 | 0 | 0 | 0 | 0 | 21 |
| `dashboard` | 34 | 0 | 0 | 7 | 0 | 41 |
| `webhook` | 19 | 0 | 0 | 0 | 0 | 19 |
| `query` | 16 | 0 | 0 | 5 | 0 | 21 |
| `datasource` | 30 | 0 | 0 | 0 | 0 | 30 |
| `app` | 47 | 0 | 0 | 9 | 0 | 56 |
| `book` | 20 | 0 | 0 | 1 | 0 | 21 |
| `doc` | 15 | 0 | 0 | 0 | 0 | 15 |
| `email_template` | 21 | 0 | 0 | 0 | 0 | 21 |
| `job` | 15 | 0 | 0 | 0 | 0 | 15 |
| `mapping` | 14 | 0 | 0 | 0 | 0 | 14 |
| `seed` | 12 | 0 | 0 | 0 | 0 | 12 |
| `translation` | 19 | 0 | 0 | 0 | 2 | 21 |
| `validation` | 15 | 0 | 0 | 3 | 0 | 18 |
| `api` | 25 | 0 | 0 | 0 | 2 | 27 |
| `capability` | 12 | 0 | 0 | 0 | 0 | 12 |
| `qa` | 4 | 0 | 0 | 5 | 0 | 9 |
| `manifest` | 22 | 0 | 1 | 15 | 0 | 38 |
| **total** | **821** | **5** | **1** | **74** | **10** | **911** |
4 changes: 2 additions & 2 deletions packages/spec/scripts/liveness/build-state-counts.mts
Original file line numberDiff line numberDiff line change
Expand Up@@ -113,7 +113,7 @@ const rows = foldStateCounts(Object.keys(types), Object.fromEntries(
// The header's rule is that a RED gate is not fatal here — the gate is red
// precisely when this artifact needs rewriting. This failure is the exception,
// and it is the same exception the unparseable report above already carves out:
// there is nothing to rewrite. The fold that produced `rows` reads four status
// there is nothing to rewrite. The fold that produced `rows` reads the published status
// names and drops everything else, so writing now would publish an understated
// `classified` — and the gate's freshness leg would then compare those bytes
// against a re-render of the SAME understated fold and call it current. A stale
Expand All@@ -134,7 +134,7 @@ if (totalErrors.length) {
const rendered = renderStateCounts(rows);
writeFileSync(join(ledgerRoot, STATE_COUNTS_FILE), rendered);

const total = rows.reduce((a, r) => a + r.live + r.experimental + r.dead + r.planned, 0);
const total = rows.reduce((a, r) => a + r.live + r.experimental + r['live-elsewhere'] + r.dead + r.planned, 0);
console.log(`✓ wrote ${STATE_COUNTS_PATH}`);
console.log(` ${rows.length} governed type(s), ${total} classified propert(ies).`);

Expand Down
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
33 changes: 33 additions & 0 deletions .changeset/liveness-live-elsewhere-verdict.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
"@objectstack/spec": patch
---

feat(spec): fifth liveness-ledger verdict `live-elsewhere` — dead here, enforced in a sibling repo, with gate-executable criteria (#13483)

The liveness ledger had no verdict for a key that is dead locally but genuinely
enforced in a sibling repo. The measured case is `manifest.runtime`: zero
load-side dispatch in objectstack+objectui, yet the cloud marketplace publish
gate hard-rejects (HTTP 422) an unverified publisher requesting the `node` tier
(#12400, cloud @15f55df). `dead` read alone licenses deleting the marketplace's
trust-gate input — deletion the maintainer explicitly ruled out (#11330) — and
`live` is refused by the gate, whose repo-local evidence must resolve against
this checkout. The stopgap was a qualifying sentence in the row's `note`; prose
is the weakest protection the ledger knows.

`live-elsewhere` says the split as data, and because `check-liveness.mts`
deliberately forces a decision on unknown statuses, it lands with criteria the
gate executes (`scripts/liveness/elsewhere.mts`, exit 1 on each): the
`evidence` must attribute at least one path to a foreign realm (the enforcer
pointer, foreign commit pinned in prose), `evidenceScope` must be
`"cross-repo"`, `verifiedAt` is REQUIRED (nothing local can watch a foreign
enforcer rot, so an undated elsewhere-claim would be unfalsifiable forever),
and the attestation expires: past 180 days the gate fails demanding a
re-reading of the foreign enforcer — never a re-stamp. If no seat with access
re-attests, the red build is the escalation back to the maintainer.

The status joins the evidence-scan population (any local path such an entry
also cites is held to the full existence/line/anchor/key-mention standard),
`STATUS_COLUMNS`, and the generated `state-counts.md` (new `elsewhere` column).
`manifest.runtime` migrates as the first row, attested at the #12400 reading
(2026-08-29). The ledger README documents the verdict and its re-attestation
discipline.
63 changes: 58 additions & 5 deletions packages/spec/liveness/README.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions packages/spec/liveness/manifest.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -250,10 +250,11 @@
}
},
"runtime": {
"status": "dead",
"verifiedAt": "2026-08-23",
"status": "live-elsewhere",
"verifiedAt": "2026-08-29",
"evidenceScope": "cross-repo",
"note": "READ TWICE HERE, ENFORCED AT THE CLOUD PUBLISH GATE, NOT ENFORCED AT LOAD — three different facts, so read this row before assuming either zero references or zero enforcement. The only reads of `manifest.runtime` in objectstack are two CLI progress lines that echo the value: packages/cli/src/commands/plugin/build.ts:127 and packages/cli/src/commands/plugin/publish.ts:94, both `runtime: ${manifest.runtime ?? 'unset'}`. Nothing dispatches on the tier: there is no `runtime === 'sandbox'` branch anywhere, and the QuickJS runner (packages/runtime/src/sandbox/) is the hook/action SCRIPT-BODY sandbox, reached from body-runner factories, never from a plugin's declared tier. objectui: zero reads. So the declared capability — \"in-process full PluginContext vs QuickJS-WASM capability-gated vs out-of-process\" (ADR-0025 §3.6) — is not delivered here, and echoing a string into a build log is not isolation. WHY THIS WAS SECURITY-SHAPED RATHER THAN COSMETIC, AND WHAT WAS DONE: the tombstone this ledger records one key above used to tell every upgrading author, verbatim, \"If you were relying on [loading.sandboxing] for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced\" — a redirection at a tier this repo does not enforce, i.e. ADR-0049 false compliance with a shipped migration message attached. ⚠️ SCOPE LIMIT NOW DISCHARGED: the cloud leg was measured by #12400 and it CHANGED the picture — the cloud control plane really does consume this key at the marketplace publish gate (`plugin-permission-audit.ts`: an unverified publisher requesting the `node` tier is hard-rejected with HTTP 422 and forced to manual review, reading of 2026-08-29). So the honest statement is split: ENFORCED at publish (cloud), NOT ENFORCED at load (here). Maintainer ruling 2026-08-30 (#11330) took option B — correct the text, do not retire the key — and explicitly ruled OUT retirement, because deleting the key would tear out the marketplace's trust-gate input. That correction has landed: the tombstone and both `runtime` describes now state the publish-gate/load-side split, and so does the ADR-0087 D3 entry `plugin-manifest-loading-retired`. ⚠️ READ `dead` NARROWLY HERE: it is the objectstack+objectui verdict about LOAD-SIDE dispatch (still true — the only local reads are the two CLI echoes above), and it is NOT a licence to delete, because the key has a real cross-repo consumer. Adding load-side enforcement is the open half, tracked as a v18 direction, not as a removal."
"evidence": "cloud: packages/service-cloud/src/plugin-permission-audit.ts#auditPluginPermissions @15f55df + packages/service-cloud/src/routes/package-publish.ts:530 — the marketplace publish gate: an unverified publisher requesting the node tier is hard-rejected with HTTP 422 and forced to manual review (#12400 reading of 2026-08-29). Load-side dispatch here is dead by measurement; the note carries that half.",
"note": "ENFORCED AT THE CLOUD PUBLISH GATE, NOT ENFORCED AT LOAD — and since #13483 the split IS the verdict: `live-elsewhere` says dead here by measurement, genuinely enforced in a sibling repo, and unlike the `dead` this row carried until then (with this note's qualifying sentence as the only guard — prose, which no check reads), it cannot be read as a licence to delete and the gate holds its criteria (foreign pointer, cross-repo scope, dated attestation with a 180d expiry — scripts/liveness/elsewhere.mts). LOCAL HALF, measured 2026-08-23 against b9e9227e3 with per-key controls: the only reads of `manifest.runtime` in objectstack are two CLI progress lines that echo the value — packages/cli/src/commands/plugin/build.ts:127 and packages/cli/src/commands/plugin/publish.ts:94, both `runtime: ${manifest.runtime ?? 'unset'}`. Nothing dispatches on the tier: there is no `runtime === 'sandbox'` branch anywhere, and the QuickJS runner (packages/runtime/src/sandbox/) is the hook/action SCRIPT-BODY sandbox, reached from body-runner factories, never from a plugin's declared tier. objectui: zero reads. So the declared capability — \"in-process full PluginContext vs QuickJS-WASM capability-gated vs out-of-process\" (ADR-0025 §3.6) — is not delivered at load, and echoing a string into a build log is not isolation. FOREIGN HALF (the evidence pointer): #12400 measured cloud origin/main @15f55df on 2026-08-29, controls held (15 manifest-property reads findable there) — `auditPluginPermissions` treats `runtime === 'node'` from an unverified publisher as a hard block, the caller turns it into HTTP 422 with forced manual review, and the tier is persisted to the version row (plugin-artifact.ts:179, package-publish.ts:531). `verifiedAt` on this row is THAT reading's date; re-attestation means someone with cloud access re-reads the enforcer and re-pins the commit — never a bare re-stamp (access is seat-dependent: `add_repo` on cloud is denied from some seats, including the #13483 seat on 2026-09-01, while the #10812 and #12400 readings prove cloud-capable seats exist). WHY THIS WAS SECURITY-SHAPED RATHER THAN COSMETIC: the tombstone this ledger records one key above used to tell every upgrading author, verbatim, \"If you were relying on [loading.sandboxing] for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced\" — a redirection at a tier this repo does not enforce at load, i.e. ADR-0049 false compliance with a shipped migration message attached. Maintainer ruling 2026-08-30 (#11330) took option B — correct the text, do not retire the key — and explicitly ruled OUT retirement, because deleting the key would tear out the marketplace's trust-gate input; the tombstone, both `runtime` describes and the ADR-0087 D3 entry `plugin-manifest-loading-retired` state the publish-gate/load-side split. Adding load-side enforcement is the open half, tracked as a v18 direction, not as a removal — if it lands, the verdict here flips to `live` with local evidence."
},
"packaging": {
"status": "live",
Expand Down
68 changes: 34 additions & 34 deletions packages/spec/liveness/state-counts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,37 +25,37 @@ protection envelope, which the gate auto-classifies `live` on every type that
spreads `MetadataProtectionFields`. See the README's counting-method section
for both corollaries.

| Type | live | exp | dead | planned | classified |
|---|---|---|---|---|---|
| `object` | 51 | 0 | 0 | 1 | 52 |
| `field` | 89 | 0 | 1 | 2 | 92 |
| `flow` | 34 | 0 | 6 | 0 | 40 |
| `action` | 41 | 0 | 3 | 2 | 46 |
| `hook` | 18 | 0 | 2 | 0 | 20 |
| `permission` | 36 | 0 | 6 | 0 | 42 |
| `position` | 12 | 0 | 0 | 0 | 12 |
| `agent` | 21 | 4 | 1 | 0 | 26 |
| `tool` | 13 | 1 | 0 | 0 | 14 |
| `skill` | 16 | 0 | 1 | 0 | 17 |
| `dataset` | 27 | 0 | 0 | 0 | 27 |
| `page` | 23 | 0 | 0 | 1 | 24 |
| `view` | 79 | 0 | 9 | 0 | 88 |
| `report` | 21 | 0 | 0 | 0 | 21 |
| `dashboard` | 34 | 0 | 7 | 0 | 41 |
| `webhook` | 19 | 0 | 0 | 0 | 19 |
| `query` | 16 | 0 | 5 | 0 | 21 |
| `datasource` | 30 | 0 | 0 | 0 | 30 |
| `app` | 47 | 0 | 9 | 0 | 56 |
| `book` | 20 | 0 | 1 | 0 | 21 |
| `doc` | 15 | 0 | 0 | 0 | 15 |
| `email_template` | 21 | 0 | 0 | 0 | 21 |
| `job` | 15 | 0 | 0 | 0 | 15 |
| `mapping` | 14 | 0 | 0 | 0 | 14 |
| `seed` | 12 | 0 | 0 | 0 | 12 |
| `translation` | 19 | 0 | 0 | 2 | 21 |
| `validation` | 15 | 0 | 3 | 0 | 18 |
| `api` | 25 | 0 | 0 | 2 | 27 |
| `capability` | 12 | 0 | 0 | 0 | 12 |
| `qa` | 4 | 0 | 5 | 0 | 9 |
| `manifest` | 22 | 0 | 16 | 0 | 38 |
| **total** | **821** | **5** | **75** | **10** | **911** |
| Type | live | exp | elsewhere | dead | planned | classified |
|---|---|---|---|---|---|---|
| `object` | 51 | 0 | 0 | 0 | 1 | 52 |
| `field` | 89 | 0 | 0 | 1 | 2 | 92 |
| `flow` | 34 | 0 | 0 | 6 | 0 | 40 |
| `action` | 41 | 0 | 0 | 3 | 2 | 46 |
| `hook` | 18 | 0 | 0 | 2 | 0 | 20 |
| `permission` | 36 | 0 | 0 | 6 | 0 | 42 |
| `position` | 12 | 0 | 0 | 0 | 0 | 12 |
| `agent` | 21 | 4 | 0 | 1 | 0 | 26 |
| `tool` | 13 | 1 | 0 | 0 | 0 | 14 |
| `skill` | 16 | 0 | 0 | 1 | 0 | 17 |
| `dataset` | 27 | 0 | 0 | 0 | 0 | 27 |
| `page` | 23 | 0 | 0 | 0 | 1 | 24 |
| `view` | 79 | 0 | 0 | 9 | 0 | 88 |
| `report` | 21 | 0 | 0 | 0 | 0 | 21 |
| `dashboard` | 34 | 0 | 0 | 7 | 0 | 41 |
| `webhook` | 19 | 0 | 0 | 0 | 0 | 19 |
| `query` | 16 | 0 | 0 | 5 | 0 | 21 |
| `datasource` | 30 | 0 | 0 | 0 | 0 | 30 |
| `app` | 47 | 0 | 0 | 9 | 0 | 56 |
| `book` | 20 | 0 | 0 | 1 | 0 | 21 |
| `doc` | 15 | 0 | 0 | 0 | 0 | 15 |
| `email_template` | 21 | 0 | 0 | 0 | 0 | 21 |
| `job` | 15 | 0 | 0 | 0 | 0 | 15 |
| `mapping` | 14 | 0 | 0 | 0 | 0 | 14 |
| `seed` | 12 | 0 | 0 | 0 | 0 | 12 |
| `translation` | 19 | 0 | 0 | 0 | 2 | 21 |
| `validation` | 15 | 0 | 0 | 3 | 0 | 18 |
| `api` | 25 | 0 | 0 | 0 | 2 | 27 |
| `capability` | 12 | 0 | 0 | 0 | 0 | 12 |
| `qa` | 4 | 0 | 0 | 5 | 0 | 9 |
| `manifest` | 22 | 0 | 1 | 15 | 0 | 38 |
| **total** | **821** | **5** | **1** | **74** | **10** | **911** |
4 changes: 2 additions & 2 deletions packages/spec/scripts/liveness/build-state-counts.mts
Original file line numberDiff line numberDiff line change
Expand Up@@ -113,7 +113,7 @@ const rows = foldStateCounts(Object.keys(types), Object.fromEntries(
// The header's rule is that a RED gate is not fatal here — the gate is red
// precisely when this artifact needs rewriting. This failure is the exception,
// and it is the same exception the unparseable report above already carves out:
// there is nothing to rewrite. The fold that produced `rows` reads four status
// there is nothing to rewrite. The fold that produced `rows` reads the published status
// names and drops everything else, so writing now would publish an understated
// `classified` — and the gate's freshness leg would then compare those bytes
// against a re-render of the SAME understated fold and call it current. A stale
Expand All@@ -134,7 +134,7 @@ if (totalErrors.length) {
const rendered = renderStateCounts(rows);
writeFileSync(join(ledgerRoot, STATE_COUNTS_FILE), rendered);

const total = rows.reduce((a, r) => a + r.live + r.experimental + r.dead + r.planned, 0);
const total = rows.reduce((a, r) => a + r.live + r.experimental + r['live-elsewhere'] + r.dead + r.planned, 0);
console.log(`✓ wrote ${STATE_COUNTS_PATH}`);
console.log(` ${rows.length} governed type(s), ${total} classified propert(ies).`);

Expand Down
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
33 changes: 33 additions & 0 deletions .changeset/liveness-live-elsewhere-verdict.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
"@objectstack/spec": patch
---

feat(spec): fifth liveness-ledger verdict `live-elsewhere` — dead here, enforced in a sibling repo, with gate-executable criteria (#13483)

The liveness ledger had no verdict for a key that is dead locally but genuinely
enforced in a sibling repo. The measured case is `manifest.runtime`: zero
load-side dispatch in objectstack+objectui, yet the cloud marketplace publish
gate hard-rejects (HTTP 422) an unverified publisher requesting the `node` tier
(#12400, cloud @15f55df). `dead` read alone licenses deleting the marketplace's
trust-gate input — deletion the maintainer explicitly ruled out (#11330) — and
`live` is refused by the gate, whose repo-local evidence must resolve against
this checkout. The stopgap was a qualifying sentence in the row's `note`; prose
is the weakest protection the ledger knows.

`live-elsewhere` says the split as data, and because `check-liveness.mts`
deliberately forces a decision on unknown statuses, it lands with criteria the
gate executes (`scripts/liveness/elsewhere.mts`, exit 1 on each): the
`evidence` must attribute at least one path to a foreign realm (the enforcer
pointer, foreign commit pinned in prose), `evidenceScope` must be
`"cross-repo"`, `verifiedAt` is REQUIRED (nothing local can watch a foreign
enforcer rot, so an undated elsewhere-claim would be unfalsifiable forever),
and the attestation expires: past 180 days the gate fails demanding a
re-reading of the foreign enforcer — never a re-stamp. If no seat with access
re-attests, the red build is the escalation back to the maintainer.

The status joins the evidence-scan population (any local path such an entry
also cites is held to the full existence/line/anchor/key-mention standard),
`STATUS_COLUMNS`, and the generated `state-counts.md` (new `elsewhere` column).
`manifest.runtime` migrates as the first row, attested at the #12400 reading
(2026-08-29). The ledger README documents the verdict and its re-attestation
discipline.
63 changes: 58 additions & 5 deletions packages/spec/liveness/README.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions packages/spec/liveness/manifest.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -250,10 +250,11 @@
}
},
"runtime": {
"status": "dead",
"verifiedAt": "2026-08-23",
"status": "live-elsewhere",
"verifiedAt": "2026-08-29",
"evidenceScope": "cross-repo",
"note": "READ TWICE HERE, ENFORCED AT THE CLOUD PUBLISH GATE, NOT ENFORCED AT LOAD — three different facts, so read this row before assuming either zero references or zero enforcement. The only reads of `manifest.runtime` in objectstack are two CLI progress lines that echo the value: packages/cli/src/commands/plugin/build.ts:127 and packages/cli/src/commands/plugin/publish.ts:94, both `runtime: ${manifest.runtime ?? 'unset'}`. Nothing dispatches on the tier: there is no `runtime === 'sandbox'` branch anywhere, and the QuickJS runner (packages/runtime/src/sandbox/) is the hook/action SCRIPT-BODY sandbox, reached from body-runner factories, never from a plugin's declared tier. objectui: zero reads. So the declared capability — \"in-process full PluginContext vs QuickJS-WASM capability-gated vs out-of-process\" (ADR-0025 §3.6) — is not delivered here, and echoing a string into a build log is not isolation. WHY THIS WAS SECURITY-SHAPED RATHER THAN COSMETIC, AND WHAT WAS DONE: the tombstone this ledger records one key above used to tell every upgrading author, verbatim, \"If you were relying on [loading.sandboxing] for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced\" — a redirection at a tier this repo does not enforce, i.e. ADR-0049 false compliance with a shipped migration message attached. ⚠️ SCOPE LIMIT NOW DISCHARGED: the cloud leg was measured by #12400 and it CHANGED the picture — the cloud control plane really does consume this key at the marketplace publish gate (`plugin-permission-audit.ts`: an unverified publisher requesting the `node` tier is hard-rejected with HTTP 422 and forced to manual review, reading of 2026-08-29). So the honest statement is split: ENFORCED at publish (cloud), NOT ENFORCED at load (here). Maintainer ruling 2026-08-30 (#11330) took option B — correct the text, do not retire the key — and explicitly ruled OUT retirement, because deleting the key would tear out the marketplace's trust-gate input. That correction has landed: the tombstone and both `runtime` describes now state the publish-gate/load-side split, and so does the ADR-0087 D3 entry `plugin-manifest-loading-retired`. ⚠️ READ `dead` NARROWLY HERE: it is the objectstack+objectui verdict about LOAD-SIDE dispatch (still true — the only local reads are the two CLI echoes above), and it is NOT a licence to delete, because the key has a real cross-repo consumer. Adding load-side enforcement is the open half, tracked as a v18 direction, not as a removal."
"evidence": "cloud: packages/service-cloud/src/plugin-permission-audit.ts#auditPluginPermissions @15f55df + packages/service-cloud/src/routes/package-publish.ts:530 — the marketplace publish gate: an unverified publisher requesting the node tier is hard-rejected with HTTP 422 and forced to manual review (#12400 reading of 2026-08-29). Load-side dispatch here is dead by measurement; the note carries that half.",
"note": "ENFORCED AT THE CLOUD PUBLISH GATE, NOT ENFORCED AT LOAD — and since #13483 the split IS the verdict: `live-elsewhere` says dead here by measurement, genuinely enforced in a sibling repo, and unlike the `dead` this row carried until then (with this note's qualifying sentence as the only guard — prose, which no check reads), it cannot be read as a licence to delete and the gate holds its criteria (foreign pointer, cross-repo scope, dated attestation with a 180d expiry — scripts/liveness/elsewhere.mts). LOCAL HALF, measured 2026-08-23 against b9e9227e3 with per-key controls: the only reads of `manifest.runtime` in objectstack are two CLI progress lines that echo the value — packages/cli/src/commands/plugin/build.ts:127 and packages/cli/src/commands/plugin/publish.ts:94, both `runtime: ${manifest.runtime ?? 'unset'}`. Nothing dispatches on the tier: there is no `runtime === 'sandbox'` branch anywhere, and the QuickJS runner (packages/runtime/src/sandbox/) is the hook/action SCRIPT-BODY sandbox, reached from body-runner factories, never from a plugin's declared tier. objectui: zero reads. So the declared capability — \"in-process full PluginContext vs QuickJS-WASM capability-gated vs out-of-process\" (ADR-0025 §3.6) — is not delivered at load, and echoing a string into a build log is not isolation. FOREIGN HALF (the evidence pointer): #12400 measured cloud origin/main @15f55df on 2026-08-29, controls held (15 manifest-property reads findable there) — `auditPluginPermissions` treats `runtime === 'node'` from an unverified publisher as a hard block, the caller turns it into HTTP 422 with forced manual review, and the tier is persisted to the version row (plugin-artifact.ts:179, package-publish.ts:531). `verifiedAt` on this row is THAT reading's date; re-attestation means someone with cloud access re-reads the enforcer and re-pins the commit — never a bare re-stamp (access is seat-dependent: `add_repo` on cloud is denied from some seats, including the #13483 seat on 2026-09-01, while the #10812 and #12400 readings prove cloud-capable seats exist). WHY THIS WAS SECURITY-SHAPED RATHER THAN COSMETIC: the tombstone this ledger records one key above used to tell every upgrading author, verbatim, \"If you were relying on [loading.sandboxing] for isolation, you had none — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced\" — a redirection at a tier this repo does not enforce at load, i.e. ADR-0049 false compliance with a shipped migration message attached. Maintainer ruling 2026-08-30 (#11330) took option B — correct the text, do not retire the key — and explicitly ruled OUT retirement, because deleting the key would tear out the marketplace's trust-gate input; the tombstone, both `runtime` describes and the ADR-0087 D3 entry `plugin-manifest-loading-retired` state the publish-gate/load-side split. Adding load-side enforcement is the open half, tracked as a v18 direction, not as a removal — if it lands, the verdict here flips to `live` with local evidence."
},
"packaging": {
"status": "live",
Expand Down
68 changes: 34 additions & 34 deletions packages/spec/liveness/state-counts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,37 +25,37 @@ protection envelope, which the gate auto-classifies `live` on every type that
spreads `MetadataProtectionFields`. See the README's counting-method section
for both corollaries.

| Type | live | exp | dead | planned | classified |
|---|---|---|---|---|---|
| `object` | 51 | 0 | 0 | 1 | 52 |
| `field` | 89 | 0 | 1 | 2 | 92 |
| `flow` | 34 | 0 | 6 | 0 | 40 |
| `action` | 41 | 0 | 3 | 2 | 46 |
| `hook` | 18 | 0 | 2 | 0 | 20 |
| `permission` | 36 | 0 | 6 | 0 | 42 |
| `position` | 12 | 0 | 0 | 0 | 12 |
| `agent` | 21 | 4 | 1 | 0 | 26 |
| `tool` | 13 | 1 | 0 | 0 | 14 |
| `skill` | 16 | 0 | 1 | 0 | 17 |
| `dataset` | 27 | 0 | 0 | 0 | 27 |
| `page` | 23 | 0 | 0 | 1 | 24 |
| `view` | 79 | 0 | 9 | 0 | 88 |
| `report` | 21 | 0 | 0 | 0 | 21 |
| `dashboard` | 34 | 0 | 7 | 0 | 41 |
| `webhook` | 19 | 0 | 0 | 0 | 19 |
| `query` | 16 | 0 | 5 | 0 | 21 |
| `datasource` | 30 | 0 | 0 | 0 | 30 |
| `app` | 47 | 0 | 9 | 0 | 56 |
| `book` | 20 | 0 | 1 | 0 | 21 |
| `doc` | 15 | 0 | 0 | 0 | 15 |
| `email_template` | 21 | 0 | 0 | 0 | 21 |
| `job` | 15 | 0 | 0 | 0 | 15 |
| `mapping` | 14 | 0 | 0 | 0 | 14 |
| `seed` | 12 | 0 | 0 | 0 | 12 |
| `translation` | 19 | 0 | 0 | 2 | 21 |
| `validation` | 15 | 0 | 3 | 0 | 18 |
| `api` | 25 | 0 | 0 | 2 | 27 |
| `capability` | 12 | 0 | 0 | 0 | 12 |
| `qa` | 4 | 0 | 5 | 0 | 9 |
| `manifest` | 22 | 0 | 16 | 0 | 38 |
| **total** | **821** | **5** | **75** | **10** | **911** |
| Type | live | exp | elsewhere | dead | planned | classified |
|---|---|---|---|---|---|---|
| `object` | 51 | 0 | 0 | 0 | 1 | 52 |
| `field` | 89 | 0 | 0 | 1 | 2 | 92 |
| `flow` | 34 | 0 | 0 | 6 | 0 | 40 |
| `action` | 41 | 0 | 0 | 3 | 2 | 46 |
| `hook` | 18 | 0 | 0 | 2 | 0 | 20 |
| `permission` | 36 | 0 | 0 | 6 | 0 | 42 |
| `position` | 12 | 0 | 0 | 0 | 0 | 12 |
| `agent` | 21 | 4 | 0 | 1 | 0 | 26 |
| `tool` | 13 | 1 | 0 | 0 | 0 | 14 |
| `skill` | 16 | 0 | 0 | 1 | 0 | 17 |
| `dataset` | 27 | 0 | 0 | 0 | 0 | 27 |
| `page` | 23 | 0 | 0 | 0 | 1 | 24 |
| `view` | 79 | 0 | 0 | 9 | 0 | 88 |
| `report` | 21 | 0 | 0 | 0 | 0 | 21 |
| `dashboard` | 34 | 0 | 0 | 7 | 0 | 41 |
| `webhook` | 19 | 0 | 0 | 0 | 0 | 19 |
| `query` | 16 | 0 | 0 | 5 | 0 | 21 |
| `datasource` | 30 | 0 | 0 | 0 | 0 | 30 |
| `app` | 47 | 0 | 0 | 9 | 0 | 56 |
| `book` | 20 | 0 | 0 | 1 | 0 | 21 |
| `doc` | 15 | 0 | 0 | 0 | 0 | 15 |
| `email_template` | 21 | 0 | 0 | 0 | 0 | 21 |
| `job` | 15 | 0 | 0 | 0 | 0 | 15 |
| `mapping` | 14 | 0 | 0 | 0 | 0 | 14 |
| `seed` | 12 | 0 | 0 | 0 | 0 | 12 |
| `translation` | 19 | 0 | 0 | 0 | 2 | 21 |
| `validation` | 15 | 0 | 0 | 3 | 0 | 18 |
| `api` | 25 | 0 | 0 | 0 | 2 | 27 |
| `capability` | 12 | 0 | 0 | 0 | 0 | 12 |
| `qa` | 4 | 0 | 0 | 5 | 0 | 9 |
| `manifest` | 22 | 0 | 1 | 15 | 0 | 38 |
| **total** | **821** | **5** | **1** | **74** | **10** | **911** |
4 changes: 2 additions & 2 deletions packages/spec/scripts/liveness/build-state-counts.mts
Original file line numberDiff line numberDiff line change
Expand Up@@ -113,7 +113,7 @@ const rows = foldStateCounts(Object.keys(types), Object.fromEntries(
// The header's rule is that a RED gate is not fatal here — the gate is red
// precisely when this artifact needs rewriting. This failure is the exception,
// and it is the same exception the unparseable report above already carves out:
// there is nothing to rewrite. The fold that produced `rows` reads four status
// there is nothing to rewrite. The fold that produced `rows` reads the published status
// names and drops everything else, so writing now would publish an understated
// `classified` — and the gate's freshness leg would then compare those bytes
// against a re-render of the SAME understated fold and call it current. A stale
Expand All@@ -134,7 +134,7 @@ if (totalErrors.length) {
const rendered = renderStateCounts(rows);
writeFileSync(join(ledgerRoot, STATE_COUNTS_FILE), rendered);

const total = rows.reduce((a, r) => a + r.live + r.experimental + r.dead + r.planned, 0);
const total = rows.reduce((a, r) => a + r.live + r.experimental + r['live-elsewhere'] + r.dead + r.planned, 0);
console.log(`✓ wrote ${STATE_COUNTS_PATH}`);
console.log(` ${rows.length} governed type(s), ${total} classified propert(ies).`);

Expand Down
Loading
Loading