From d2004592695d139ff2e6b2b5d639a5dde09f0663 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 11:13:05 +0000 Subject: [PATCH 1/6] feat(spec)!: HookSchema refuses a hook body that targets a stored-metadata table, with the runtime's prescription The refused set is the runtime bind's: a hook carrying a body whose object names sys_metadata or sys_metadata_history (string or any list member), judged by isStoredMetadataBodyObject. A code handler and the wildcard stay outside it. The JSON-stage hook derives through safeExtend so the check survives the handler narrowing. Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude --- packages/spec/src/data/hook.zod.ts | 78 +++++++++++++++++++++++++++++- packages/spec/src/stack.zod.ts | 8 ++- 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/packages/spec/src/data/hook.zod.ts b/packages/spec/src/data/hook.zod.ts index 7666b7c4a26..75b528786f9 100644 --- a/packages/spec/src/data/hook.zod.ts +++ b/packages/spec/src/data/hook.zod.ts @@ -6,6 +6,11 @@ import { lazySchema } from '../shared/lazy-schema'; import { retiredKey } from '../shared/retired-key'; import { strictObject } from '../shared/strict-object'; import { MetadataProtectionFields } from '../kernel/metadata-protection.zod'; +// [#21565] The stored-metadata family's ONE membership predicate — the function +// the runtime's bind refusal and every stored-body read exit judge by. ⛔ Never +// restate its table list here: a second list is the drift the kernel module's +// header refuses. No cycle: nothing that module reaches imports this file. +import { isStoredMetadataBodyObject } from '../kernel/metadata-type-redaction'; import { HookBodySchema } from './hook-body.zod'; // Type-only, and it must stay that way: `contracts/` already imports `data/` // (`contracts/data-engine.ts`), so a VALUE import here would close a runtime @@ -64,6 +69,73 @@ const hookTargetError = + "`object: 'account'` or `object: ['account', 'contact']` — or, if firing on " + "every object really is the intent, write the wildcard explicitly: `object: '*'`."; +/* + * ── A hook body is never bound to a table of stored metadata ──────────────── + * + * [#21565] `sys_metadata` and `sys_metadata_history` — the family + * `isStoredMetadataBodyObject` answers for — have ONE writer for an + * app-authored body: the metadata protocol, where a change is validated and its + * provenance recorded (#21520, ruling A). The runtime enforces that at its + * binding point: `hookBodyRunnerFactory` (`packages/runtime/src/sandbox/ + * body-runner.ts`) throws for a hook whose `body` targets a family table, so + * the hook is never registered and never runs. Authoring accepted the very same + * hook — the metadata save door answered 200 for it — and the author learned + * otherwise only from a server log. + * + * So the parse refuses it as well, with the runtime's own prescription, at + * every door that parses a hook: the metadata save door, `defineStack` / + * `os validate`, compile, and an artifact's parse (the JSON-stage hook in + * `stack.zod.ts` keeps this check through `safeExtend`). The refused set is + * EXACTLY the bind's — read from `storedMetadataBodyHookBindingRefusal` in + * `packages/runtime/src/stored-metadata-body-boundary.ts`, never widened or + * narrowed here: + * + * - a hook carrying a `body`, in any form {@link HookBodySchema} admits — the + * runtime refuses before it looks at the body's language; + * - whose `object` NAMES a family table: the string itself, or any member of + * the list, and one family member refuses the whole hook, as at bind; + * - judged by the same predicate, by exact name. + * + * Outside it, exactly as at bind: a hook with no `body` (a code `handler` — the + * platform's own hooks are code, and ruling A leaves platform code out), and the + * wildcard `'*'`, which names no family table: it binds, and the runtime never + * runs its body for a family table's event. + * + * The prescription repeats the runtime's sentence word for word: the runtime + * keeps it in a module-private constant `packages/spec` cannot import, and no + * shared constant exists. + */ +const STORED_METADATA_BODY_PRESCRIPTION = + 'Change metadata through the metadata API (`PUT /api/v1/meta/:type/:name`, the metadata protocol), ' + + 'where it is validated and its provenance is recorded. Elevation (`runAs`, a system context) does not ' + + 'change this.'; + +/** + * The object-level check that refuses a hook `body` bound to a stored-metadata + * table — one issue per family table the target names, at `object` for a + * string target and at `object[i]` for a list member. See the block above for + * the refused set and why it is the runtime's. + */ +function refuseBodyOnStoredMetadataTarget( + hook: { object: string | string[]; body?: unknown }, + ctx: z.RefinementCtx, +): void { + if (hook.body === undefined) return; + const listed = Array.isArray(hook.object); + const targets = listed ? (hook.object as string[]) : [hook.object as string]; + targets.forEach((name, index) => { + if (!isStoredMetadataBodyObject(name)) return; + ctx.addIssue({ + code: 'custom', + path: listed ? ['object', index] : ['object'], + message: + `\`object\` names '${name}', a table of stored metadata, and this hook carries a \`body\`: an ` + + 'app-authored hook body may not be bound to a stored-metadata table, so the runtime refuses to ' + + `register the hook and it never runs. ${STORED_METADATA_BODY_PRESCRIPTION}`, + }); + }); +} + /** * Hook Lifecycle Events * Defines the interception points in the ObjectQL execution pipeline. @@ -397,7 +469,11 @@ export const HookSchema = lazySchema(() => strictObject( // REJECTED here — the same live 422 that `permission` hit on the ADR-0094 // overlay path before Tier-A declared them (#4001 findings log, entries 2/8). ...MetadataProtectionFields, -})); + // [#21565] The one object-level check: it pairs `object` with `body`, which + // no field-level refine can see together. A schema DERIVED from this one by + // overriding a key must use `.safeExtend()` (zod refuses `.extend()` over a + // refined object), which is what keeps the check on the derived schema too. +}).superRefine(refuseBodyOnStoredMetadataTarget)); /** * Hook Runtime Context diff --git a/packages/spec/src/stack.zod.ts b/packages/spec/src/stack.zod.ts index aa6368d1f13..0029bf064db 100644 --- a/packages/spec/src/stack.zod.ts +++ b/packages/spec/src/stack.zod.ts @@ -1361,9 +1361,15 @@ export type AssembledPackageBodyParsed = z.infer Date: Sat, 3 Oct 2026 11:16:24 +0000 Subject: [PATCH 2/6] test(spec): pin the stored-metadata target refusal; register its D3 entry and changeset Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude --- ...ook-body-stored-metadata-target-refused.md | 34 +++ .../hook-body-stored-metadata-target.test.ts | 198 ++++++++++++++++++ ...ook-body-stored-metadata-target-refused.ts | 47 +++++ packages/spec/src/migrations/registry.ts | 59 ++++++ 4 files changed, 338 insertions(+) create mode 100644 .changeset/21565-hook-body-stored-metadata-target-refused.md create mode 100644 packages/spec/src/data/hook-body-stored-metadata-target.test.ts create mode 100644 packages/spec/src/migrations/entries/semantic/18.hook-body-stored-metadata-target-refused.ts diff --git a/.changeset/21565-hook-body-stored-metadata-target-refused.md b/.changeset/21565-hook-body-stored-metadata-target-refused.md new file mode 100644 index 00000000000..fd85c3c507c --- /dev/null +++ b/.changeset/21565-hook-body-stored-metadata-target-refused.md @@ -0,0 +1,34 @@ +--- +'@objectstack/spec': minor +--- + +A hook whose `body` targets a table of stored metadata, `sys_metadata` or `sys_metadata_history`, is refused at parse, with the runtime's prescription: change metadata through the metadata API. + +Clause-②: yes (narrowing) + + + +**BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings. + +**Why.** An app-authored body may not touch the two stored-metadata tables: for a body, the metadata protocol is their only writer, where a change is validated and its provenance is recorded. The runtime already enforces that where a body hook becomes a handler: such a hook is refused at registration and never runs. But `HookSchema` still accepted it, so the metadata save door answered 200 for a hook that would never fire, and the author learned otherwise only from a server log. + +**What is refused.** A hook carrying a `body`, in any form, whose `object` names `sys_metadata` or `sys_metadata_history`, as the string or as any member of the list. One such member refuses the whole hook, as the runtime does. The issue's `code` is `custom`, at `object` (or `object.N` for a list member), and its message names the table and ends with the runtime's prescription. The membership test is the kernel's own `isStoredMetadataBodyObject`, the predicate the runtime judges by. That covers `HookSchema`, `defineHook()`, `defineStack` (`STACK_SCHEMA_INVALID`, 422, at `hooks.N.object`), `os validate`, which runs the same stack parse, an artifact's parse, and the metadata save door (`422 INVALID_METADATA`). + +**What stays accepted, byte for byte.** A hook with no `body` on those tables (a code `handler`, which is how the platform writes its own hooks), a wildcard (`object: '*'`) hook with a `body` (it names neither table: the runtime binds it and never runs its body for those tables' events), and every hook on any other object. + +## FROM → TO + +| you wrote | write instead | +|:--|:--| +| a hook with a `body` and `object: 'sys_metadata'` or `object: 'sys_metadata_history'` | change metadata through the metadata API (`PUT /api/v1/meta/:type/:name`) instead, and delete the hook | +| a hook with a `body` whose `object` list includes either table | drop those tables from the list; change metadata through the metadata API instead | +| a hook with a `body` on `'*'` or on any other object | unchanged | + +**The one-line fix: delete the hook, or remove `sys_metadata` and `sys_metadata_history` from its `object`, and make the change through the metadata API.** The runtime never ran such a hook, so removing it changes nothing an app does. + +**Who is affected, measured.** No authored hook targets either table in this repository's `packages/**` and `examples/**` at `44072fc2b9` (317 hook-shaped declarations, 24 of them outside tests; the only hits are the runtime's own tests of its registration refusal) or in hotcrm at `94668373f2` (44 declarations, 40 outside tests, no hit). Deployed metadata was not measured. A stored hook row of this shape still loads, now with a `[metadata_spec_invalid]` warning and a `_diagnostics` badge, and is still never bound. + +### The kit + +- **The refusal.** An object-level check attached to `HookSchema` with `.superRefine(...)`. A schema derived from `HookSchema` by overriding a key must use `.safeExtend()`, which keeps the check; zod refuses `.extend()` over a refined object. The artifact-stage hook in `@objectstack/spec` now derives that way. +- **The ledger.** The D3 semantic entry `hook-body-stored-metadata-target-refused` (protocol 18). No key is removed, so there is no tombstone, and there is no D2 conversion: a refused hook carries no intent a rewrite could keep. diff --git a/packages/spec/src/data/hook-body-stored-metadata-target.test.ts b/packages/spec/src/data/hook-body-stored-metadata-target.test.ts new file mode 100644 index 00000000000..c242c93230e --- /dev/null +++ b/packages/spec/src/data/hook-body-stored-metadata-target.test.ts @@ -0,0 +1,198 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#21565] `HookSchema` refuses a hook `body` bound to a table of stored + * metadata — the authoring half of #21520's ruling A, whose runtime half + * (`hookBodyRunnerFactory`) refuses the same hook at registration. + * + * The refused set must be EXACTLY the runtime bind's, read from + * `storedMetadataBodyHookBindingRefusal` (`packages/runtime/src/ + * stored-metadata-body-boundary.ts`): a hook carrying a `body` (any form) + * whose `object` names a family table, as the string or as any list member, + * judged by `isStoredMetadataBodyObject`. Not wider — a code `handler` and the + * wildcard `'*'` parse — and not narrower — one family member in a list + * refuses the hook, whatever else the list names. + * + * Accepted fixtures are compared against their expected parse output by + * serialized bytes, so the check is proven to add no transformation. + */ + +import { describe, expect, it } from 'vitest'; +import { isStoredMetadataBodyObject, STORED_METADATA_BODY_OBJECTS } from '../kernel/metadata-type-redaction'; +import { getMetadataTypeSchema } from '../kernel/metadata-type-schemas'; +import { MIGRATIONS_BY_MAJOR, RETIRED_KEYS_BY_MAJOR } from '../migrations/registry'; +import { ArtifactStagePackageBodySchema, defineStack } from '../stack.zod'; +import { defineHook, HookSchema } from './hook.zod'; + +const ENTRY_ID = 'hook-body-stored-metadata-target-refused'; +const PRESCRIPTION_OPENING = 'Change metadata through the metadata API (`PUT /api/v1/meta/:type/:name`, the metadata protocol)'; +const FAMILY = [...STORED_METADATA_BODY_OBJECTS]; + +const JS_BODY = { language: 'js', source: 'ctx.input.status = "seen";' } as const; +const EXPRESSION_BODY = { language: 'expression', source: 'true' } as const; + +/** The defaults `HookSchema` fills in — the whole of what a parse adds to an accepted hook. */ +const DEFAULTS = { priority: 100, async: false, onError: 'abort', runAs: 'inherit' } as const; + +const hook = (object: string | string[], extra: Record = { body: JS_BODY }) => ({ + name: 'stamp_status', + object, + events: ['beforeInsert'], + ...extra, +}); + +interface IssueSig { code: string; path: string; message: string } + +function refusalOf(input: unknown): IssueSig[] { + const r = HookSchema.safeParse(input); + expect(r.success, 'expected the parse to refuse this hook').toBe(false); + return r.success ? [] : r.error.issues.map((i) => ({ code: i.code, path: i.path.join('.'), message: i.message })); +} + +/** An accepted hook parses to exactly its input plus the schema defaults, byte for byte. */ +function expectAcceptedUnchanged(input: Record, expected: Record): void { + const r = HookSchema.safeParse(input); + expect(r.success, JSON.stringify(r.error?.issues ?? [])).toBe(true); + if (r.success) expect(JSON.stringify(r.data)).toBe(JSON.stringify(expected)); +} + +describe('HookSchema — a hook body bound to a stored-metadata table is refused at parse', () => { + it.each(FAMILY)('refuses a body hook whose string target is `%s`, at `object`, with the prescription', (table) => { + const issues = refusalOf(hook(table)); + expect(issues.map(({ code, path }) => ({ code, path }))).toEqual([{ code: 'custom', path: 'object' }]); + const [{ message }] = issues; + expect(message.startsWith(`\`object\` names '${table}', a table of stored metadata, and this hook carries a \`body\``)).toBe(true); + expect(message).toContain(PRESCRIPTION_OPENING); + }); + + it('refuses every body form, not only sandboxed JS: the runtime refuses before it reads the language', () => { + for (const body of [JS_BODY, EXPRESSION_BODY]) { + expect(refusalOf(hook('sys_metadata', { body })).map((i) => i.path), body.language).toEqual(['object']); + } + }); + + it('a list target is refused at the family member, and one member refuses the whole hook', () => { + expect(refusalOf(hook(['crm_account', 'sys_metadata_history'])).map((i) => i.path)).toEqual(['object.1']); + // Each family member it names is named, in list order. + expect(refusalOf(hook(['sys_metadata', 'crm_account', 'sys_metadata_history'])).map((i) => i.path)) + .toEqual(['object.0', 'object.2']); + }); + + it('a list naming the wildcard AND a family table is refused: the family table is named', () => { + expect(refusalOf(hook(['*', 'sys_metadata'])).map((i) => i.path)).toEqual(['object.1']); + }); + + it('defineHook refuses the same hook', () => { + expect(() => defineHook(hook('sys_metadata') as never)).toThrow(); + }); + + it('the registered `hook` type schema — what the metadata save door validates against — refuses it too', () => { + const schema = getMetadataTypeSchema('hook') as unknown as typeof HookSchema; + expect(schema).toBeDefined(); + const r = schema.safeParse(hook('sys_metadata')); + expect(r.success).toBe(false); + expect(r.success ? [] : r.error.issues.map((i) => i.path.join('.'))).toEqual(['object']); + }); +}); + +describe('HookSchema — what stays accepted, byte for byte (the runtime binds these)', () => { + it.each(FAMILY)('CONTROL: a code `handler` hook on `%s` parses unchanged — platform hooks are code', (table) => { + const input = hook(table, { handler: 'stamp_status' }); + expectAcceptedUnchanged(input, { ...input, ...DEFAULTS }); + }); + + it('CONTROL: a wildcard body hook parses unchanged — it names no family table', () => { + const input = hook('*'); + expectAcceptedUnchanged(input, { ...input, body: { ...JS_BODY, capabilities: [] }, ...DEFAULTS }); + }); + + it('CONTROL: a body hook on an ordinary object parses unchanged, string and list forms', () => { + for (const object of ['crm_account', ['crm_account', 'crm_contact']]) { + const input = hook(object); + expectAcceptedUnchanged(input, { ...input, body: { ...JS_BODY, capabilities: [] }, ...DEFAULTS }); + } + }); + + it('the refused set is the predicate\'s, by exact name — never a second list', () => { + const targets: Array = [ + ...FAMILY, + 'SYS_METADATA', + 'sys_metadata_draft', + 'sys_meta', + 'metadata', + '*', + 'crm_account', + ['crm_account', 'sys_metadata'], + ['crm_account', 'crm_contact'], + ]; + for (const object of targets) { + const names = Array.isArray(object) ? object : [object]; + const expectedRefused = names.some((n) => isStoredMetadataBodyObject(n)); + expect(HookSchema.safeParse(hook(object)).success, JSON.stringify(object)).toBe(!expectedRefused); + // Without a body the predicate is never asked: every target parses. + expect(HookSchema.safeParse(hook(object, { handler: 'stamp_status' })).success, JSON.stringify(object)).toBe(true); + } + }); +}); + +describe('every door that parses a hook refuses it', () => { + const stackWith = (hooks: unknown[]) => ({ + manifest: { id: 'com.example.hooks', name: 'hooks', version: '1.0.0', type: 'app', namespace: 'hks' }, + objects: [{ name: 'hks_note', label: 'Note', fields: { title: { type: 'text', label: 'Title' } } }], + hooks, + }); + + it('defineStack wraps the refusal in its ADR-0112 envelope, at `hooks.N.object`', () => { + let refusal: { code?: unknown; status?: unknown; issues?: Array<{ path: unknown[]; code: string }> } | undefined; + try { + defineStack(stackWith([hook('hks_note'), hook('sys_metadata')]) as never); + } catch (e) { + refusal = e as typeof refusal; + } + expect(refusal, 'defineStack must refuse the family-target body hook').toBeDefined(); + expect({ code: refusal!.code, status: refusal!.status }).toEqual({ code: 'STACK_SCHEMA_INVALID', status: 422 }); + expect(refusal!.issues!.map((i) => ({ path: i.path.join('.'), code: i.code }))).toEqual([ + { path: 'hooks.1.object', code: 'custom' }, + ]); + }); + + it('CONTROL: defineStack accepts the ordinary body hook alone', () => { + expect(() => defineStack(stackWith([hook('hks_note')]) as never)).not.toThrow(); + }); + + it('an artifact\'s parse refuses it: the JSON-stage hook keeps the check through `safeExtend`', () => { + const body = { id: 'com.example.hooks', name: 'hooks', version: '1.0.0', type: 'app' }; + const refused = ArtifactStagePackageBodySchema.safeParse({ ...body, hooks: [hook('sys_metadata')] }); + expect(refused.success).toBe(false); + expect(refused.success ? [] : refused.error.issues.map((i) => i.path.join('.'))).toEqual(['hooks.0.object']); + + // CONTROL: an ordinary body hook, and a lowered string handler on a family table, parse. + for (const h of [hook('crm_account'), hook('sys_metadata', { handler: 'stamp_status' })]) { + const r = ArtifactStagePackageBodySchema.safeParse({ ...body, hooks: [h] }); + expect(r.success, JSON.stringify(r.error?.issues ?? [])).toBe(true); + } + // CONTROL: `safeExtend` kept the artifact stage's own narrowing — an inline callable is still refused. + const callable = ArtifactStagePackageBodySchema.safeParse({ ...body, hooks: [hook('crm_account', { handler: () => {} })] }); + expect(callable.success).toBe(false); + }); +}); + +describe('the ADR-0087 ledger', () => { + it('registers one D3 entry at protocol 18, with no D2 conversion', () => { + const entries = MIGRATIONS_BY_MAJOR[18]!.semantic.filter((e) => e.id === ENTRY_ID); + expect(entries, 'the narrowing needs its own D3 entry').toHaveLength(1); + const [entry] = entries; + expect(entry!.conversionIds ?? []).toEqual([]); + expect(entry!.replacement).toContain('Change metadata through the metadata API'); + expect(entry!.acceptanceCriteria.length).toBeGreaterThan(0); + }); + + it('registers no tombstone: `object` and `body` stay in the walked shape', () => { + const all = Object.values(RETIRED_KEYS_BY_MAJOR).flat(); + expect(all.filter((k) => k.startsWith('data/Hook:object') || k.startsWith('data/Hook:body'))).toEqual([]); + // CONTROL: the flattened table is the real one — it carries a known step-18 tombstone. + expect(all).toContain('api/RestApiEndpoint:timeout'); + const shape = (HookSchema as unknown as { shape: Record }).shape; + expect(Object.keys(shape)).toEqual(expect.arrayContaining(['object', 'body'])); + }); +}); diff --git a/packages/spec/src/migrations/entries/semantic/18.hook-body-stored-metadata-target-refused.ts b/packages/spec/src/migrations/entries/semantic/18.hook-body-stored-metadata-target-refused.ts new file mode 100644 index 00000000000..2cc886444ee --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.hook-body-stored-metadata-target-refused.ts @@ -0,0 +1,47 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +// #21565 — the D3 entry for `HookSchema`'s refusal of a hook body bound to a +// stored-metadata table: the authoring half of #21520's ruling A (record +// 5965059068), whose runtime half refuses the same hook at bind. It narrows the +// hook's accept set; no key is removed, so there is no tombstone and no +// RETIRED_KEYS_BY_MAJOR row. There is no D2 conversion either: a refused hook +// carries no intent the chain could rewrite into one the runtime runs. +export const entry: SemanticMigration = { + id: 'hook-body-stored-metadata-target-refused', + // No backticks and no pipes in `surface` — build-upgrade-guide.ts renders it + // inside a code span and a table cell. + surface: + 'hook.object naming sys_metadata or sys_metadata_history, as the string or as any member of the ' + + 'list, on a hook that carries a body', + replacement: + 'Change metadata through the metadata API (`PUT /api/v1/meta/:type/:name`, the metadata protocol), ' + + 'where it is validated and its provenance is recorded. Delete the hook, or point its `object` at ' + + 'the tables the logic really concerns. Elevation (`runAs`, a system context) does not change this.', + reason: + '`HookSchema` accepted a hook whose `body` targets `sys_metadata` or `sys_metadata_history`, the ' + + 'tables that hold stored metadata. The maintainer ruled (2026-10-03) that an app-authored body may ' + + 'not touch those tables: for a body, the metadata protocol is their only writer, where a change is ' + + 'validated and its provenance recorded. The runtime enforces that where a body hook becomes a ' + + 'handler, refusing such a hook at registration so that it never runs, but every authoring door ' + + 'still accepted it: the metadata save door answered 200, and the author learned otherwise only ' + + 'from a server log. The parse now refuses it too, with the runtime\'s own prescription, so ' + + '`objectstack validate`, `defineStack`, compile, an artifact\'s parse and the metadata save door ' + + '(a 422) each name the target at `object`, or at the list member. The refused set is exactly the ' + + 'runtime\'s: a hook carrying a `body`, in any form, whose `object` names a stored-metadata table, ' + + 'as the string or as any member of the list, and one such member refuses the whole hook. A hook ' + + 'with no `body` (a code `handler`, which is how the platform writes its own hooks) and the ' + + 'wildcard `\'*\'` are outside it, as they are at registration: a wildcard names no stored-metadata ' + + 'table, so it binds, and the runtime never runs its body for those tables\' events. No authored ' + + 'hook targeting either table was measured in this repository, its examples or hotcrm. There is no ' + + 'mechanical rewrite: retargeting the hook, dropping its body or deleting it each changes what the ' + + 'author wrote, and the runtime already never ran it. A stored hook row of this shape still loads, ' + + 'now with a `[metadata_spec_invalid]` warning and a `_diagnostics` badge, and is still never bound.', + acceptanceCriteria: + '`objectstack validate` reports no issue at a hook\'s `object` path: no hook that carries a `body` ' + + 'names `sys_metadata` or `sys_metadata_history` in its `object`, as the string or in the list. ' + + 'Every change those hooks made to metadata is made through the metadata API instead. Saving each ' + + 'formerly affected hook through the metadata API succeeds instead of answering a 422 that names ' + + '`object`, and boot logs no binding refusal naming one of those tables for a hook.', +}; diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index f0f2613e59f..c6cdd6378f7 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -5642,6 +5642,22 @@ const STEP18_RATIONALE: readonly RationaleFragment[] = [ + 'dropped it would accept it at load, the grace window ruling B refused. Its D3 record is ' + 'the semantic entry `inline-grid-column-identity-only-currency-scale-refused`.', }, + { + id: 'hook-body-stored-metadata-target-refused', + order: 67, + text: + 'It also refuses, at parse, a hook whose `body` targets a table of stored metadata, ' + + '`sys_metadata` or `sys_metadata_history` (maintainer ruling 2026-10-03, letter A: an ' + + 'app-authored body may not touch those tables, whose only writer for a body is the metadata ' + + 'protocol). The runtime already refused such a hook where a body becomes a handler, so it ' + + 'never ran, while the metadata save door answered 200 for it. `HookSchema` now refuses the ' + + 'same set at `object`, or at the list member, with the runtime\'s prescription to change ' + + 'metadata through the metadata API, judged by the one predicate the runtime uses: a hook ' + + 'with a `body` in any form whose target names either table. A code `handler` and the ' + + 'wildcard `\'*\'` stay outside it, as they are at registration. No key is removed, so there is ' + + 'no tombstone, and no D2 conversion exists: a refused hook carries no intent a rewrite could ' + + 'keep. Its D3 record is the semantic entry `hook-body-stored-metadata-target-refused`.', + }, { id: 'list-view-page-mount-retired', order: 27, @@ -13079,6 +13095,49 @@ const step18: MigrationStep = { + 'carries `scale`. Each column `name` names a field of the subform\'s `childObject`, and the ' + 'master-detail grid renders a value — not a blank cell — in each column for a row that has one.', }, + // #21565 — the D3 entry for `HookSchema`'s refusal of a hook body bound to a + // stored-metadata table: the authoring half of #21520's ruling A (record + // 5965059068), whose runtime half refuses the same hook at bind. It narrows the + // hook's accept set; no key is removed, so there is no tombstone and no + // RETIRED_KEYS_BY_MAJOR row. There is no D2 conversion either: a refused hook + // carries no intent the chain could rewrite into one the runtime runs. + { + id: 'hook-body-stored-metadata-target-refused', + // No backticks and no pipes in `surface` — build-upgrade-guide.ts renders it + // inside a code span and a table cell. + surface: + 'hook.object naming sys_metadata or sys_metadata_history, as the string or as any member of the ' + + 'list, on a hook that carries a body', + replacement: + 'Change metadata through the metadata API (`PUT /api/v1/meta/:type/:name`, the metadata protocol), ' + + 'where it is validated and its provenance is recorded. Delete the hook, or point its `object` at ' + + 'the tables the logic really concerns. Elevation (`runAs`, a system context) does not change this.', + reason: + '`HookSchema` accepted a hook whose `body` targets `sys_metadata` or `sys_metadata_history`, the ' + + 'tables that hold stored metadata. The maintainer ruled (2026-10-03) that an app-authored body may ' + + 'not touch those tables: for a body, the metadata protocol is their only writer, where a change is ' + + 'validated and its provenance recorded. The runtime enforces that where a body hook becomes a ' + + 'handler, refusing such a hook at registration so that it never runs, but every authoring door ' + + 'still accepted it: the metadata save door answered 200, and the author learned otherwise only ' + + 'from a server log. The parse now refuses it too, with the runtime\'s own prescription, so ' + + '`objectstack validate`, `defineStack`, compile, an artifact\'s parse and the metadata save door ' + + '(a 422) each name the target at `object`, or at the list member. The refused set is exactly the ' + + 'runtime\'s: a hook carrying a `body`, in any form, whose `object` names a stored-metadata table, ' + + 'as the string or as any member of the list, and one such member refuses the whole hook. A hook ' + + 'with no `body` (a code `handler`, which is how the platform writes its own hooks) and the ' + + 'wildcard `\'*\'` are outside it, as they are at registration: a wildcard names no stored-metadata ' + + 'table, so it binds, and the runtime never runs its body for those tables\' events. No authored ' + + 'hook targeting either table was measured in this repository, its examples or hotcrm. There is no ' + + 'mechanical rewrite: retargeting the hook, dropping its body or deleting it each changes what the ' + + 'author wrote, and the runtime already never ran it. A stored hook row of this shape still loads, ' + + 'now with a `[metadata_spec_invalid]` warning and a `_diagnostics` badge, and is still never bound.', + acceptanceCriteria: + '`objectstack validate` reports no issue at a hook\'s `object` path: no hook that carries a `body` ' + + 'names `sys_metadata` or `sys_metadata_history` in its `object`, as the string or in the list. ' + + 'Every change those hooks made to metadata is made through the metadata API instead. Saving each ' + + 'formerly affected hook through the metadata API succeeds instead of answering a 422 that names ' + + '`object`, and boot logs no binding refusal naming one of those tables for a hook.', + }, { id: 'hook-register-undispatched-lifecycle-event-refused', surface: From 719464e66ae53e364d1c964fdbea2f35dea356f2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 11:32:31 +0000 Subject: [PATCH 3/6] test(metadata-protocol): the hook save door refuses a body bound to a stored-metadata table Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude --- ...nvalid-metadata-422-face-inventory.test.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts b/packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts index 7dac9eb3799..909498c5dfd 100644 --- a/packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts +++ b/packages/metadata-protocol/src/protocol.invalid-metadata-422-face-inventory.test.ts @@ -428,3 +428,67 @@ describe('[#20161] a `joined` report\'s `chart` is refused at the metadata door' expect([...rows.values()].map((r) => r.type)).toEqual(['report']); }); }); + +// ═══════════════════════════════════════════════════════════════════════════ +// 6. #21565 — the `hook` door refuses a body bound to a stored-metadata table +// ═══════════════════════════════════════════════════════════════════════════ +// +// An app-authored hook body may not be bound to `sys_metadata` or +// `sys_metadata_history`: the runtime refuses such a hook where a body becomes +// a handler (`hookBodyRunnerFactory`), so it never runs. The save door used to +// answer 200 for it. `HookSchema` now refuses it at parse, so THIS gate — the +// one `PUT /api/v1/meta/hook/:name` reaches, built here exactly as that route +// builds it for the administrator (`writeFace: 'meta-envelope'`, the actor +// named) — refuses it with the ADR-0112 envelope, the issue located at +// `object`, and the runtime's prescription. Rides this file's pinned engine +// double, as sections 4 and 5 do. ⛔ No check of its own lives in +// `protocol.ts`: the refusal is the registered type schema's. + +async function saveHookAsAdministrator(protocol: any, item: Record): Promise { + try { + return await protocol.saveMetaItem({ + type: 'hook', + name: item.name, + item, + writeFace: 'meta-envelope', + actor: 'usr_admin', + }); + } catch (e: any) { + return e; + } +} + +describe('[#21565] a hook body bound to a stored-metadata table is refused at the metadata door', () => { + const body = { language: 'js', source: "ctx.input.status = 'seen';" }; + const hookOn = (object: string | string[]) => ({ + name: 'stamp_status', + object, + events: ['beforeInsert'], + body, + }); + + it.each([ + ['sys_metadata', 'object'], + ['sys_metadata_history', 'object'], + [['hks_note', 'sys_metadata'], 'object.1'], + ] as const)('`object: %j` — 422 INVALID_METADATA at `%s`, with the prescription, nothing stored', async (object, path) => { + const { protocol, rows } = makeProtocol(); + const err = await saveHookAsAdministrator(protocol, hookOn(object as string | string[])); + + expect(err).toBeInstanceOf(Error); + expect({ code: err.code, status: err.status }).toEqual({ code: 'INVALID_METADATA', status: 422 }); + const issues = err.issues as Array<{ code?: string; path?: string; message: string }>; + expect(issues.map((i) => [i.code, i.path])).toEqual([['custom', path]]); + expect(issues[0]!.message).toContain('a table of stored metadata'); + expect(issues[0]!.message).toContain('Change metadata through the metadata API'); + expect(rows.size).toBe(0); + }); + + it('CONTROL — the same body hook on an ordinary object saves as before', async () => { + const { protocol, rows } = makeProtocol(); + const result = await saveHookAsAdministrator(protocol, hookOn('hks_note')); + + expect(result instanceof Error ? `${result.message} ${JSON.stringify((result as any).issues ?? [])}` : 'stored').toBe('stored'); + expect([...rows.values()].map((r) => [r.type, r.name])).toEqual([['hook', 'stamp_status']]); + }); +}); From 152e36aacc16941b6081c25bbbe9ffe03e2780d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 11:41:08 +0000 Subject: [PATCH 4/6] chore(spec): record the hook check's five dropped-refinement sites The object-level check has no JSON Schema form, so the published Hook file and the four installed-package files embedding it are wider than the parse at the hook node. Hand-edited, as the ledger requires. Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude --- packages/spec/dropped-refinements.baseline.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/spec/dropped-refinements.baseline.json b/packages/spec/dropped-refinements.baseline.json index 8b331234f2f..412d577d6fa 100644 --- a/packages/spec/dropped-refinements.baseline.json +++ b/packages/spec/dropped-refinements.baseline.json @@ -3,7 +3,7 @@ "measured": { "zod": "4.4.3", "publishedSchemasWithDroppedRefinements": 217, - "droppedRefinementSites": 653, + "droppedRefinementSites": 658, "refinementSitesThatDidProject": 369, "refinementSitesWithNoJsonFormToCompare": 0 }, @@ -80,6 +80,7 @@ "manifest.flows.element", "manifest.flows.element.errorHandling", "manifest.flows.element.nodes.element.in.waitEventConfig", + "manifest.hooks.element", "manifest.hooks.element.object", "manifest.jobs.element.schedule.options[0].timezone", "manifest.navigationContributions.element.items.element.lazy.options[0]", @@ -188,6 +189,7 @@ "data.options[1].manifest.flows.element", "data.options[1].manifest.flows.element.errorHandling", "data.options[1].manifest.flows.element.nodes.element.in.waitEventConfig", + "data.options[1].manifest.hooks.element", "data.options[1].manifest.hooks.element.object", "data.options[1].manifest.jobs.element.schedule.options[0].timezone", "data.options[1].manifest.objectExtensions.element", @@ -281,6 +283,7 @@ "options[1].manifest.flows.element", "options[1].manifest.flows.element.errorHandling", "options[1].manifest.flows.element.nodes.element.in.waitEventConfig", + "options[1].manifest.hooks.element", "options[1].manifest.hooks.element.object", "options[1].manifest.jobs.element.schedule.options[0].timezone", "options[1].manifest.objectExtensions.element", @@ -334,6 +337,7 @@ "data.packages.element.options[1].manifest.flows.element", "data.packages.element.options[1].manifest.flows.element.errorHandling", "data.packages.element.options[1].manifest.flows.element.nodes.element.in.waitEventConfig", + "data.packages.element.options[1].manifest.hooks.element", "data.packages.element.options[1].manifest.hooks.element.object", "data.packages.element.options[1].manifest.jobs.element.schedule.options[0].timezone", "data.packages.element.options[1].manifest.objectExtensions.element", @@ -721,6 +725,7 @@ }, "data/Hook": { "sites": [ + "", "object" ] }, From 3dd7c76ef068c0864cc94d387e51b10a747fba2e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 12:13:14 +0000 Subject: [PATCH 5/6] refactor(spec): declare the stored-metadata family set in an import-free kernel leaf hook.zod.ts needs only the predicate. Importing it from metadata-type-redaction.ts pulled that module's closure (the credential derivation and the conversion chain) into the hook schema's source graph, which the generated skills/objectstack-data reference index reads: seven unrelated schema pointers. The leaf is re-exported from the redaction module, so every importer and the kernel entry keep the same objects. Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude --- .../hook-body-stored-metadata-target.test.ts | 8 +++ packages/spec/src/data/hook.zod.ts | 10 ++-- .../src/kernel/metadata-type-redaction.ts | 38 ++++--------- .../kernel/stored-metadata-body-objects.ts | 57 +++++++++++++++++++ 4 files changed, 83 insertions(+), 30 deletions(-) create mode 100644 packages/spec/src/kernel/stored-metadata-body-objects.ts diff --git a/packages/spec/src/data/hook-body-stored-metadata-target.test.ts b/packages/spec/src/data/hook-body-stored-metadata-target.test.ts index c242c93230e..f5e503d7c5e 100644 --- a/packages/spec/src/data/hook-body-stored-metadata-target.test.ts +++ b/packages/spec/src/data/hook-body-stored-metadata-target.test.ts @@ -19,6 +19,7 @@ import { describe, expect, it } from 'vitest'; import { isStoredMetadataBodyObject, STORED_METADATA_BODY_OBJECTS } from '../kernel/metadata-type-redaction'; +import * as leaf from '../kernel/stored-metadata-body-objects'; import { getMetadataTypeSchema } from '../kernel/metadata-type-schemas'; import { MIGRATIONS_BY_MAJOR, RETIRED_KEYS_BY_MAJOR } from '../migrations/registry'; import { ArtifactStagePackageBodySchema, defineStack } from '../stack.zod'; @@ -113,6 +114,13 @@ describe('HookSchema — what stays accepted, byte for byte (the runtime binds t } }); + it('ONE definition: the kernel module re-exports the leaf\'s set and predicate as the very same objects', () => { + // `hook.zod.ts` imports the leaf; the runtime imports `@objectstack/spec/kernel`, + // which re-exports `metadata-type-redaction.ts`, which re-exports the leaf. + expect(isStoredMetadataBodyObject).toBe(leaf.isStoredMetadataBodyObject); + expect(STORED_METADATA_BODY_OBJECTS).toBe(leaf.STORED_METADATA_BODY_OBJECTS); + }); + it('the refused set is the predicate\'s, by exact name — never a second list', () => { const targets: Array = [ ...FAMILY, diff --git a/packages/spec/src/data/hook.zod.ts b/packages/spec/src/data/hook.zod.ts index 75b528786f9..6cc51014eab 100644 --- a/packages/spec/src/data/hook.zod.ts +++ b/packages/spec/src/data/hook.zod.ts @@ -7,10 +7,12 @@ import { retiredKey } from '../shared/retired-key'; import { strictObject } from '../shared/strict-object'; import { MetadataProtectionFields } from '../kernel/metadata-protection.zod'; // [#21565] The stored-metadata family's ONE membership predicate — the function -// the runtime's bind refusal and every stored-body read exit judge by. ⛔ Never -// restate its table list here: a second list is the drift the kernel module's -// header refuses. No cycle: nothing that module reaches imports this file. -import { isStoredMetadataBodyObject } from '../kernel/metadata-type-redaction'; +// object the runtime's bind refusal and every stored-body read exit judge by +// (`@objectstack/spec/kernel` re-exports it from this leaf). ⛔ Never restate +// its table list here. Imported from the import-free leaf, not from +// `metadata-type-redaction.ts`, whose closure (the credential derivation and the +// conversion chain) has no business in this schema's import graph. +import { isStoredMetadataBodyObject } from '../kernel/stored-metadata-body-objects'; import { HookBodySchema } from './hook-body.zod'; // Type-only, and it must stay that way: `contracts/` already imports `data/` // (`contracts/data-engine.ts`), so a VALUE import here would close a runtime diff --git a/packages/spec/src/kernel/metadata-type-redaction.ts b/packages/spec/src/kernel/metadata-type-redaction.ts index 2559454e90b..e3b21fa6c7c 100644 --- a/packages/spec/src/kernel/metadata-type-redaction.ts +++ b/packages/spec/src/kernel/metadata-type-redaction.ts @@ -48,6 +48,8 @@ import { redactDatasourceConfig } from '../data/datasource-credential-redaction'; import { PLURAL_TO_SINGULAR } from '../shared/metadata-collection.zod'; +// [#21565] The family set and its predicate: declared in a leaf, re-exported below. +import { STORED_METADATA_BODY_OBJECTS, isStoredMetadataBodyObject } from './stored-metadata-body-objects'; /** What a {@link MetadataTypeRedactor} returns: the servable item, and what was withheld. */ export interface MetadataRedactionResult { @@ -152,28 +154,17 @@ export function listMetadataTypeRedactorTypes(): string[] { // serialized metadata body", and the one redaction of that body (#21120) // --------------------------------------------------------------------------- -/** - * [#21120] The object (table) names whose `metadata` column stores one - * serialized metadata BODY, of the type the same row's `type` column names — - * the table every `/meta` read exit rehydrates from (`sys_metadata`) and its - * version snapshots (`sys_metadata_history`). - * - * This is the family boundary for the stored-metadata-body security invariant: - * every surface that can SERVE, COPY or EVALUATE one of these rows' body is a - * credential read exit, and either projects the body through the ONE redactor - * below or refuses. It lives HERE — not in `@objectstack/metadata-protocol` — - * for the same reason {@link getMetadataTypeRedactor} does: the surfaces that - * must consult it are service packages (`@objectstack/service-analytics`), - * plugins (`@objectstack/plugin-audit`) and the engine - * (`@objectstack/objectql`), and **none of them depends on - * `@objectstack/metadata-protocol`**, while all of them already import - * `@objectstack/spec/kernel`. A copy per surface is exactly the - * two-definitions-drift this module's header refuses for the registry. +/* + * [#21120] The family set — `sys_metadata` / `sys_metadata_history`, the tables + * whose `metadata` column stores one serialized metadata BODY — and its + * membership predicate are declared in the leaf module + * `./stored-metadata-body-objects` and re-exported here unchanged, so every + * importer of this module and of `@objectstack/spec/kernel` receives the very + * same objects. [#21565] They moved so that `data/hook.zod.ts` can judge a hook + * target by the predicate without importing this module's closure; the leaf's + * header says why. ⛔ Never restate the list here. */ -export const STORED_METADATA_BODY_OBJECTS: ReadonlySet = new Set([ - 'sys_metadata', - 'sys_metadata_history', -]); +export { STORED_METADATA_BODY_OBJECTS, isStoredMetadataBodyObject }; /** The column holding the serialized body, on every {@link STORED_METADATA_BODY_OBJECTS} member. */ export const STORED_METADATA_BODY_COLUMN = 'metadata'; @@ -181,11 +172,6 @@ export const STORED_METADATA_BODY_COLUMN = 'metadata'; /** The column naming the body's metadata type — what selects its redactor. */ export const STORED_METADATA_TYPE_COLUMN = 'type'; -/** Whether `object`'s rows carry a stored metadata body a read exit must project. */ -export function isStoredMetadataBodyObject(object: string): boolean { - return STORED_METADATA_BODY_OBJECTS.has(object); -} - /** * Redact one stored metadata body VALUE (the `metadata` column), choosing the * redactor by `type` through {@link getMetadataTypeRedactor} — the single diff --git a/packages/spec/src/kernel/stored-metadata-body-objects.ts b/packages/spec/src/kernel/stored-metadata-body-objects.ts new file mode 100644 index 00000000000..cab087362f5 --- /dev/null +++ b/packages/spec/src/kernel/stored-metadata-body-objects.ts @@ -0,0 +1,57 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The stored-metadata family: the object (table) names whose rows carry one + * serialized metadata BODY, and the one membership predicate every consumer + * judges by (#21120). + * + * ## Why a leaf module (#21565) + * + * It imports nothing, on purpose. The set and its predicate were declared in + * `metadata-type-redaction.ts`, which also carries the body redactor and so + * imports the datasource credential derivation and, through + * `shared/metadata-collection.zod.ts`, the conversion chain. `data/hook.zod.ts` + * needs the predicate to refuse a hook body bound to a family table, and + * importing it from there pulled that whole closure into the hook schema's + * import graph. The bundler tree-shakes it away, but every source-graph walker + * reads it, the generated `skills/objectstack-data` reference index among + * them. Declared here, the predicate reaches a schema without that closure. + * + * `metadata-type-redaction.ts` re-exports both names, so every importer, and + * the `@objectstack/spec/kernel` export, still receives these very objects. + * ⛔ Never declare a second list of these tables anywhere: one definition is + * the point. + */ + +/** + * [#21120] The object (table) names whose `metadata` column stores one + * serialized metadata BODY, of the type the same row's `type` column names — + * the table every `/meta` read exit rehydrates from (`sys_metadata`) and its + * version snapshots (`sys_metadata_history`). + * + * This is the family boundary for the stored-metadata-body security invariant: + * every surface that can SERVE, COPY or EVALUATE one of these rows' body is a + * credential read exit, and either projects the body through the ONE redactor + * (`redactStoredMetadataBody`, `metadata-type-redaction.ts`) or refuses. It + * lives in `@objectstack/spec/kernel` — not in `@objectstack/metadata-protocol` + * — because the surfaces that must consult it are service packages + * (`@objectstack/service-analytics`), plugins (`@objectstack/plugin-audit`) and + * the engine (`@objectstack/objectql`), and **none of them depends on + * `@objectstack/metadata-protocol`**, while all of them already import + * `@objectstack/spec/kernel`. A copy per surface is exactly the + * two-definitions drift the redaction module's header refuses for its + * registry. + * + * [#21520] It is also the family an app-authored body may not touch: the + * runtime refuses to bind a hook body to one of these tables, and refuses a + * body's write to them; [#21565] `HookSchema` refuses the same hook at parse. + */ +export const STORED_METADATA_BODY_OBJECTS: ReadonlySet = new Set([ + 'sys_metadata', + 'sys_metadata_history', +]); + +/** Whether `object`'s rows carry a stored metadata body a read exit must project. */ +export function isStoredMetadataBodyObject(object: string): boolean { + return STORED_METADATA_BODY_OBJECTS.has(object); +} From 8c605b97851b7ac77ac2d1d881e04ac05dc33c3e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 12:33:06 +0000 Subject: [PATCH 6/6] chore(spec): regenerate export-origins for the stored-metadata kernel leaf gen:export-origins output after the family set and its predicate moved to src/kernel/stored-metadata-body-objects.ts; the kernel entry re-exports the same two declarations from their new origin. Confirmed by a fresh build: check:export-origins green, and a generator re-run rewrote 0 shards. Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude --- packages/spec/export-origins/kernel.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/spec/export-origins/kernel.json b/packages/spec/export-origins/kernel.json index 4655f8ac773..a0e8901802e 100644 --- a/packages/spec/export-origins/kernel.json +++ b/packages/spec/export-origins/kernel.json @@ -372,7 +372,7 @@ "SEED_WRITE_EXECUTION_CONTEXT": "src/kernel/execution-context.zod.ts#SEED_WRITE_EXECUTION_CONTEXT (const)", "SEMVER_2_0_0_VERSION_PATTERN": "src/kernel/version-grammar.ts#SEMVER_2_0_0_VERSION_PATTERN (const)", "STORED_METADATA_BODY_COLUMN": "src/kernel/metadata-type-redaction.ts#STORED_METADATA_BODY_COLUMN (const)", - "STORED_METADATA_BODY_OBJECTS": "src/kernel/metadata-type-redaction.ts#STORED_METADATA_BODY_OBJECTS (const)", + "STORED_METADATA_BODY_OBJECTS": "src/kernel/stored-metadata-body-objects.ts#STORED_METADATA_BODY_OBJECTS (const)", "STORED_METADATA_TYPE_COLUMN": "src/kernel/metadata-type-redaction.ts#STORED_METADATA_TYPE_COLUMN (const)", "SandboxConfig": "src/kernel/plugin-security-advanced.zod.ts#SandboxConfig (type)", "SandboxConfigParsed": "src/kernel/plugin-security-advanced.zod.ts#SandboxConfigParsed (type)", @@ -462,7 +462,7 @@ "getMetadataTypeSchema": "src/kernel/metadata-type-schemas.ts#getMetadataTypeSchema (function)", "isConsumerInstallable": "src/kernel/plugin.zod.ts#isConsumerInstallable (function)", "isKnownPlatformCapability": "src/kernel/platform-capabilities.ts#isKnownPlatformCapability (function)", - "isStoredMetadataBodyObject": "src/kernel/metadata-type-redaction.ts#isStoredMetadataBodyObject (function)", + "isStoredMetadataBodyObject": "src/kernel/stored-metadata-body-objects.ts#isStoredMetadataBodyObject (function)", "lintUnknownAuthoringKeys": "src/kernel/metadata-authoring-lint.ts#lintUnknownAuthoringKeys (function)", "lintUnknownKeysAgainstSchema": "src/kernel/metadata-authoring-lint.ts#lintUnknownKeysAgainstSchema (function)", "lintUnknownStackKeys": "src/kernel/metadata-authoring-lint.ts#lintUnknownStackKeys (function)",