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
10 changes: 10 additions & 0 deletions .changeset/15206-boot-report-carry-overs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/metadata-protocol': patch
---

The legacy-organization boot report reports each of its reads on its own, and a stored row under a code-defined datasource name is named once at boot

Clause-②: no

- `[metadata_org_scoped_unserved]` reads `sys_metadata` and counts the legacy rows of `sys_metadata_commit`, `sys_metadata_history` and `sys_metadata_audit` as four separate terms. A read that fails for any reason but a missing table now costs only its own term: the line names it NOT MEASURED, with the reason, and still reports the other terms. Before, one failing read silenced the whole line.
- `[metadata_sealed_overlay_unserved]` no longer lists an environment row stored under the name of a datasource the host defines in code. The datasource restore's boot warning already names that row, with its own remedy, so the row was named twice. No read serves the row either way, and nothing is deleted or rewritten.
4 changes: 3 additions & 1 deletion .changeset/15206-reads-environment-only.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
'@objectstack/metadata-protocol': minor
'@objectstack/metadata-core': minor
Expand All @@ -23,7 +23,7 @@
- `overlayScope` on the layered read is `'env'` or `null`; `'org'` is gone.
- `listDrafts` lists the environment's drafts only, and its rows carry no `organizationId`.
- `listCommits` lists the environment's commits only, for every caller: a legacy organization commit is not shown, so an operator on the packages door no longer sees every organization's legacy commits, and a rollback plan never contains one.
- [Triage ruling Q1 → C] An environment row that overlays an item a managed package ships, on a type sealed against overlays (any type whose registry entry admits no overlay — a managed flow, action, hook, object or datasource, for example — written through the `OS_METADATA_WRITABLE` hatch before the seal), is not served: the package's definition is. An `object` such row is not loaded at boot either, so the engine keeps the packaged schema. A stored fork of a code-declared permission set keeps its own ruling (detection reading and Discard Overlay) and is still served.
- [Triage ruling Q1 → C] An environment row that overlays an item a managed package ships, on a type sealed against overlays (any type whose registry entry admits no overlay — a managed flow, action, hook, object or datasource, for example — written through the `OS_METADATA_WRITABLE` hatch before the seal), is not served: the package's definition is. An `object` such row is not loaded at boot either, so the engine keeps the packaged schema. A stored fork of a code-declared permission set keeps its own ruling instead: the reads do not decline it, a cold boot refuses a deployment that holds one (ADR-0048), and the offline `os migrate security-catalog-overlays` removes it.
- `@objectstack/objectql` binds the environment's authored hook and action rows only; a legacy organization-scoped hook or action row is no longer bound.
- Boot names both populations, once each: `[metadata_org_scoped_unserved]` lists every legacy organization row per type (active and draft), plus the legacy rows of `sys_metadata_commit`, `sys_metadata_history` and `sys_metadata_audit`, and names the v18 migration ceremony (`os migrate`, ADR-0131 D10) that carries them; `[metadata_sealed_overlay_unserved]` lists the sealed overlays per type with the two remedies. Nothing is deleted or rewritten.
- The anonymous public-form doors keep reading the Default Organization's legacy `view` layer, fail-closed, until that ceremony (triage ruling Q3 A) — through the protocol-internal `legacyFormOrganizationId` key, which no spec request declares and which the protocol honours for `view` alone.
Expand All @@ -36,9 +36,11 @@
| `organizationId` on a `GetMetaItems` / `GetMetaItem` / `GetMetaItemLayered` / `AuditMetaItem` / `HistoryMetaItem` / `GetMetaItemCached` request (`@objectstack/spec`) | drop it: the read is the environment's. The key is stripped at a spec parse (the schemas are not strict) and no longer type-checks |
| `organizationId` on a `listDrafts` / `listCommits` / `diffMetaItem` / `getMetaDiagnostics` / `findReferencesToMeta` request (`@objectstack/metadata-protocol`) | drop it |
| `ListDraftsResponse` draft `organizationId` | gone: every listed draft is the environment's |
| `SysMetadataRepository.listDrafts` row `organizationId` (`@objectstack/metadata-protocol`) | gone: every listed row is an environment draft, whichever repository asks |
| `overlayScope: 'org'` (`GetMetaItemLayeredResponse`) | only `'env'` or `null` |
| `declaresOrgOverride`, `organizationIdForMetaRead` (`@objectstack/metadata-core`) | removed — no read takes an organization. Read `allowOrgOverride` off `DEFAULT_METADATA_TYPE_REGISTRY` where a type's overlay channel is the question |
| `MetadataAuthoringGateContext.organizationId` (`@objectstack/metadata-protocol`) | removed — no write is organization-scoped, and no gate read it |
| `organizationId` on `projectPermissionMutation`'s `evt` (`@objectstack/plugin-security`) | drop it: the projection reads the environment → code layered read, whatever organization the event names |
| a legacy organization-scoped `sys_metadata` row a deployment relied on | re-save the item in Studio to make it environment-wide now, or wait for the v18 migration ceremony, which promotes it |
| an environment overlay of a managed item on a sealed type (flow, action, hook, object, datasource …) | re-express the change as a new item under a new name (a linkage-free clone), then delete the stored row; or delete the row |

Expand Down
2 changes: 1 addition & 1 deletion content/docs/concepts/metadata-lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ A permission set and a position also have a switch, outside the refusal: `POST /

The switches are operator-only where one install serves several organizations. The refusal names the same path whichever layer answers it: `403 NOT_OVERRIDABLE` for a write that names no base, and `403 ITEM_LOCKED` for one that names the read-only package. Which writes are refused, and with which code, is unchanged.

An environment row stored over a managed item of a type sealed against overlays (a flow, action, hook, object or datasource, for example — any type whose registry entry admits no overlay) before the seal — written through the `OS_METADATA_WRITABLE` hatch — is **not served** (ADR-0131 D6): every read, and boot, takes the package's definition, and the row stays at rest, untouched. Boot names each such row, per type (`[metadata_sealed_overlay_unserved]`), with the two remedies: re-express the change as a new item under a new name, or delete the stored row. A stored fork of a code-declared permission set is the one exception: the deployment does not boot while it holds one (ADR-0048), and `os migrate security-catalog-overlays` lists and, with `--apply`, deletes it.
An environment row stored over a managed item of a type sealed against overlays (a flow, action, hook, object or datasource, for example — any type whose registry entry admits no overlay) before the seal — written through the `OS_METADATA_WRITABLE` hatch — is **not served** (ADR-0131 D6): every read, and boot, takes the package's definition, and the row stays at rest, untouched. Boot names each such row, per type (`[metadata_sealed_overlay_unserved]`), with the two remedies: re-express the change as a new item under a new name, or delete the stored row. A row under the name of a datasource the host defines in code is named once, by the datasource restore's boot warning, with that warning's remedy. A stored fork of a code-declared permission set is the one exception: the deployment does not boot while it holds one (ADR-0048), and `os migrate security-catalog-overlays` lists and, with `--apply`, deletes it.

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,38 @@ describe('[#6190, ADR-0131 D6] cold boot names every legacy organization-scoped
expect(call).toBeGreaterThan(1);
expect(result).toMatchObject({ loaded: 1, errors: 0, storeUnavailable: false });
expect(warns.filter((w) => w.includes('DB hydration skipped'))).toEqual([]);
expect(warns.filter((w) => w.includes(AUDIT))).toEqual([]);
// [#15206] …and the failed read is said, not swallowed: its term is
// NOT MEASURED, with the reason.
const audit = warns.filter((w) => w.includes(AUDIT));
expect(audit).toHaveLength(1);
expect(audit[0]).toContain('NOT MEASURED');
expect(audit[0]).toContain('sys_metadata (probe exploded)');
});

it('[#15206] a read that fails costs its own term only: the other terms are reported as measured', async () => {
const { engine } = makeEngine([
row({ type: 'flow', name: 'org_sweep', organization_id: 'org_a' }),
], {
ledgers: {
sys_metadata_commit: [{ organization_id: 'org_a' }],
sys_metadata_audit: [{ organization_id: 'org_b' }],
},
});
const inner = engine.count;
engine.count = async (t: string, q: any) => {
if (t === 'sys_metadata_history') throw new Error('history count refused');
return inner(t, q);
};

const { result, warns } = await bootAndCapture(engine);

expect(result).toMatchObject({ errors: 0, storeUnavailable: false });
const line = warns.find((w) => w.includes(AUDIT));
expect(line, `no ${AUDIT} line in: ${JSON.stringify(warns)}`).toBeDefined();
expect(line).toContain('flow×1 (org_sweep@org_a)');
expect(line).toContain('sys_metadata_commit×1, sys_metadata_audit×1');
expect(line).toContain('NOT MEASURED');
expect(line).toContain('sys_metadata_history (history count refused)');
expect(line).not.toContain('sys_metadata_history×');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,29 @@ describe('§3 the boot report names both populations, per type, with their remed
const { warns } = await boot({ sys_metadata: [metaRow('view', 'env_grid', null, { label: 'env' })] });
expect(warns.filter((w) => w.includes('_unserved]'))).toEqual([]);
});

it('[#15206] a row under a datasource name the host defines in code is left to the datasource restore\'s warning; a package-declared one is still listed', async () => {
const warns: string[] = [];
vi.spyOn(console, 'warn').mockImplementation((...a: unknown[]) => { warns.push(a.map(String).join(' ')); });
const { protocol } = harness({
sys_metadata: [
metaRow('datasource', 'host_ds', null, { label: 'stored' }),
metaRow('datasource', 'pkg_ds', null, { label: 'stored' }),
],
});
protocol.getServicesRegistry = () => new Map([['code-datasource-names', new Set(['host_ds'])]]);
protocol.engine.registry.getAllPackages = () => [{ manifest: { datasources: [{ name: 'pkg_ds' }] } }];
// Both rows are still declined at read; only the boot line's membership moves.
expect(protocol.declinesStoredRow('datasource', 'host_ds')).toBe(true);
expect(protocol.declinesStoredRow('datasource', 'pkg_ds')).toBe(true);

await protocol.loadMetaFromDb();

const sealed = warns.filter((w) => w.includes('[metadata_sealed_overlay_unserved]'));
expect(sealed).toHaveLength(1);
expect(sealed[0]).toContain('datasource×1 (pkg_ds)');
expect(sealed[0]).not.toContain('host_ds');
});
});

describe('§4 [triage ruling Q3 A] the anonymous form doors\' legacy layer is the one read left that reaches a legacy organization', () => {
Expand Down
Loading
Loading