From cbb0c27f6731b0ffc822526ab2ebbe1dd872e740 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 14:56:56 +0000 Subject: [PATCH 01/11] feat(spec): the override declaration, the .custom pairing helpers, the overridable-type list, the __c reservation and the display whitelist ADR-0146 D2, D3, D6, D9, D10: one Zod field set overrides { package, basedOn } spread on the thirteen overridable types; the pair's id, namespace and dependency-range helpers; the closed classification of every registry type; the __c suffix and the base-field display-property whitelist. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- packages/spec/src/automation/flow.zod.ts | 3 + .../spec/src/data/field-customization.test.ts | 102 +++++ packages/spec/src/data/field-customization.ts | 84 ++++ packages/spec/src/data/index.ts | 3 + packages/spec/src/data/mapping.zod.ts | 3 + .../src/kernel/customization-package.test.ts | 226 +++++++++++ .../spec/src/kernel/customization-package.ts | 366 ++++++++++++++++++ packages/spec/src/kernel/index.ts | 5 + .../src/kernel/override-declaration.test.ts | 205 ++++++++++ .../src/kernel/override-declaration.zod.ts | 133 +++++++ packages/spec/src/system/book.zod.ts | 3 + packages/spec/src/system/doc.zod.ts | 3 + .../spec/src/system/email-template.zod.ts | 3 + packages/spec/src/system/translation.zod.ts | 3 + packages/spec/src/ui/action.zod.ts | 3 + packages/spec/src/ui/app.zod.ts | 3 + packages/spec/src/ui/dashboard.zod.ts | 3 + packages/spec/src/ui/dataset.zod.ts | 3 + packages/spec/src/ui/page.zod.ts | 3 + packages/spec/src/ui/report.zod.ts | 3 + packages/spec/src/ui/view.zod.ts | 5 + 21 files changed, 1165 insertions(+) create mode 100644 packages/spec/src/data/field-customization.test.ts create mode 100644 packages/spec/src/data/field-customization.ts create mode 100644 packages/spec/src/kernel/customization-package.test.ts create mode 100644 packages/spec/src/kernel/customization-package.ts create mode 100644 packages/spec/src/kernel/override-declaration.test.ts create mode 100644 packages/spec/src/kernel/override-declaration.zod.ts diff --git a/packages/spec/src/automation/flow.zod.ts b/packages/spec/src/automation/flow.zod.ts index 086bb631224..d34b8bd97de 100644 --- a/packages/spec/src/automation/flow.zod.ts +++ b/packages/spec/src/automation/flow.zod.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import { ProtectionSchema } from '../shared/protection.zod'; import { MetadataProtectionFields } from '../kernel/metadata-protection.zod'; +import { OverrideDeclarationFields } from '../kernel/override-declaration.zod'; import { EvaluatedExpressionInputSchema } from '../shared/expression.zod'; /** @@ -1361,6 +1362,8 @@ export const FlowSchema = lazySchema(() => strictObject( // ADR-0010 — runtime protection envelope (internal — set by loader). ...MetadataProtectionFields, + // ADR-0146 D2 — the override declaration (authored; travels with the item). + ...OverrideDeclarationFields, }).superRefine((flow, ctx) => { // Two hand-authored id spaces, one rule each, one shape — a later occurrence diff --git a/packages/spec/src/data/field-customization.test.ts b/packages/spec/src/data/field-customization.test.ts new file mode 100644 index 00000000000..4ebf66415c5 --- /dev/null +++ b/packages/spec/src/data/field-customization.test.ts @@ -0,0 +1,102 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * ADR-0146 D10 — the `__c` reservation and the base-field display-property + * whitelist, measured against `FieldSchema`. + */ + +import { describe, expect, it } from 'vitest'; +import type { z } from 'zod'; +import { BASE_FIELD_DISPLAY_WHITELIST, EXTENSION_FIELD_SUFFIX, isExtensionFieldName } from './field-customization'; +import { FieldSchema, SelectOptionSchema } from './field.zod'; + +/** The object shape behind a (possibly optional) schema. */ +function shapeOf(schema: unknown): Record { + const s = schema as { shape?: Record }; + if (!s.shape) throw new Error('not an object schema'); + return s.shape; +} + +/** + * The properties D10 and D6 name as the vendor's, plus every `FieldSchema` key + * whose change alters DDL, a stored value, a validation verdict or a + * permission — the four things a whitelisted property must leave alone. + */ +const STRUCTURAL_FIELD_KEYS = [ + // D10 / D6, named + 'type', 'required', 'unique', 'reference', 'defaultValue', 'expression', 'options', + // DDL / storage + 'name', 'storage', 'multiple', 'maxLength', 'precision', 'scale', 'picklist', 'referenceVia', + 'deleteBehavior', 'referenceDeletedWhen', 'autonumberFormat', 'externalId', 'valueDomain', 'returnType', + // validation verdicts + 'minLength', 'min', 'max', 'requiredWhen', 'accept', 'maxSize', + // permission + 'requiredPermissions', 'readonly', 'readonlyWhen', 'maskingRule', 'hidden', 'internal', 'system', +] as const; +const STRUCTURAL_OPTION_KEYS = ['value'] as const; + +describe('the `__c` reservation', () => { + it('the suffix is `__c` (maintainer: 「扩展字段用 __c 后缀,只有 .custom 包能用。」)', () => { + expect(EXTENSION_FIELD_SUFFIX).toBe('__c'); + }); + + it.each([ + ['region__c', true], + ['annual_budget__c', true], + ['region', false], + ['region_c', false], + ['region__c_2', false], + ['__c', false], + ['', false], + [undefined, false], + ])('isExtensionFieldName(%j) → %s', (name, verdict) => { + expect(isExtensionFieldName(name)).toBe(verdict); + }); + + it('an extension field name is a valid field name — the reservation needs no grammar change', () => { + expect(shapeOf(FieldSchema).name!.safeParse('region__c').success).toBe(true); + }); +}); + +describe('the base-field display-property whitelist', () => { + const fieldShape = shapeOf(FieldSchema); + const optionShape = shapeOf(SelectOptionSchema); + + it('is the class ADR-0146 D10 names: label, description, help text, placeholder, option labels', () => { + expect([...BASE_FIELD_DISPLAY_WHITELIST.field]).toEqual(['label', 'description', 'inlineHelpText', 'placeholder']); + expect([...BASE_FIELD_DISPLAY_WHITELIST.option]).toEqual(['label']); + }); + + it('is closed: frozen at both levels', () => { + expect(Object.isFrozen(BASE_FIELD_DISPLAY_WHITELIST)).toBe(true); + expect(Object.isFrozen(BASE_FIELD_DISPLAY_WHITELIST.field)).toBe(true); + expect(Object.isFrozen(BASE_FIELD_DISPLAY_WHITELIST.option)).toBe(true); + }); + + it.each(BASE_FIELD_DISPLAY_WHITELIST.field.map((k) => [k]))('field member `%s` exists on FieldSchema and is text', (key) => { + expect(Object.keys(fieldShape)).toContain(key); + expect(fieldShape[key]!.safeParse('Display text').success).toBe(true); + }); + + it.each(BASE_FIELD_DISPLAY_WHITELIST.option.map((k) => [k]))('option member `%s` exists on an option of FieldSchema.options and is text', (key) => { + expect(Object.keys(optionShape)).toContain(key); + expect(optionShape[key]!.safeParse('Display text').success).toBe(true); + }); + + it('the option schema measured is the one FieldSchema.options holds', () => { + const parsed = FieldSchema.parse({ type: 'select', options: [{ label: 'Open', value: 'open' }] }); + expect(parsed.options?.[0]).toEqual(expect.objectContaining(SelectOptionSchema.parse({ label: 'Open', value: 'open' }))); + }); + + it('every structural key the test names is a real FieldSchema key (the list cannot rot into a no-op)', () => { + for (const key of STRUCTURAL_FIELD_KEYS) expect(Object.keys(fieldShape), key).toContain(key); + for (const key of STRUCTURAL_OPTION_KEYS) expect(Object.keys(optionShape), key).toContain(key); + }); + + it('no member is structural', () => { + const structural = new Set(STRUCTURAL_FIELD_KEYS); + expect(BASE_FIELD_DISPLAY_WHITELIST.field.filter((k) => structural.has(k))).toEqual([]); + const structuralOption = new Set(STRUCTURAL_OPTION_KEYS); + expect(BASE_FIELD_DISPLAY_WHITELIST.option.filter((k) => structuralOption.has(k))).toEqual([]); + }); +}); diff --git a/packages/spec/src/data/field-customization.ts b/packages/spec/src/data/field-customization.ts new file mode 100644 index 00000000000..52cb532b888 --- /dev/null +++ b/packages/spec/src/data/field-customization.ts @@ -0,0 +1,84 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * # Customer extensions of a base object's fields (ADR-0146 D10) + * + * Objects are extended, never replaced. A customer's extension of a base + * object lives in the base's paired `.custom` package, under two rules + * this module spells once: + * + * 1. **Extension fields end in `__c`, and only a `.custom` package may declare + * one** (maintainer, verbatim: 「扩展字段用 __c 后缀,只有 .custom 包能用。」). + * {@link EXTENSION_FIELD_SUFFIX} and {@link isExtensionFieldName} are the + * reservation; refusing the suffix outside `.custom` (at publish, at + * `os validate`, at the install door) is the enforcement card's, not this + * module's. + * 2. **A base field changes only within a display-property whitelist** (Q7). + * {@link BASE_FIELD_DISPLAY_WHITELIST} is that whitelist: the closed constant + * the write gate reads, never a judgement it makes. + * + * Constants and one pure predicate, imports nothing: the shape that carries a + * per-field patch on an object extension, and the merge that applies it, are + * not declared here. + */ + +/** + * The suffix that reserves a field name to the customer's layer: a customer's + * field on `crm_account` is `region__c`, so a vendor's next version can never + * take that name (ADR-0146 D10). A field one vendor package adds to another + * vendor's object keeps the plain spelling; the suffix names the customer's + * layer and nothing else. + */ +export const EXTENSION_FIELD_SUFFIX = '__c'; + +/** + * Whether `name` is spelled as a customer extension field: it ends in + * {@link EXTENSION_FIELD_SUFFIX} after a non-empty stem (`region__c` yes, + * `region` no, a bare `__c` no). Judges the suffix only; whether the name is a + * valid field name at all is `FieldSchema`'s question. + */ +export function isExtensionFieldName(name: unknown): boolean { + return typeof name === 'string' + && name.length > EXTENSION_FIELD_SUFFIX.length + && name.endsWith(EXTENSION_FIELD_SUFFIX); +} + +/** + * The base-field display-property whitelist (ADR-0146 D10, Q7; "Decided here" + * item 4): the properties of a base field a customer's extension may patch. + * + * A property is admitted by D10's test, never by convenience: changing it + * alters **no DDL, no stored value, no validation verdict and no permission**. + * Each member below is a `FieldSchema` key (or, under `option`, a key of one + * entry of a field's `options`) measured against that test: + * + * | member | `FieldSchema` key | why it passes | + * |:--|:--|:--| + * | label | `label` | display text; no column, value, check or grant reads it | + * | description | `description` | tooltip text | + * | help text | `inlineHelpText` | help text shown beside the input | + * | placeholder | `placeholder` | placeholder text of an empty input | + * | option label | `options[].label` | display text of an option, keyed by its stored `value`, which stays the vendor's | + * + * Everything else on a base field — `type`, `required`, `unique`, `reference`, + * `defaultValue`, formulas (`expression`), `options` membership and each + * option's `value` — stays the vendor's (ADR-0146 D6) and is refused on the + * extension with a prescription naming this whitelist. + * + * Deliberately the class D10 names and nothing more. Other display-only keys + * may pass the same test (`rows`, `group`, `widget`, an option's `color`); each + * enters by a recorded decision, because widening a published whitelist is + * reversible and narrowing it is not. `field-customization.test.ts` checks that + * every member exists on `FieldSchema` and that none is structural. + */ +export const BASE_FIELD_DISPLAY_WHITELIST = Object.freeze({ + /** Keys of the base field itself. */ + field: Object.freeze(['label', 'description', 'inlineHelpText', 'placeholder'] as const), + /** Keys of one entry of the base field's `options`, the entry addressed by its stored `value`. */ + option: Object.freeze(['label'] as const), +}); + +/** A base field key the extension may patch. */ +export type BaseFieldDisplayProperty = (typeof BASE_FIELD_DISPLAY_WHITELIST.field)[number]; +/** A key of one base field option the extension may patch. */ +export type BaseFieldOptionDisplayProperty = (typeof BASE_FIELD_DISPLAY_WHITELIST.option)[number]; diff --git a/packages/spec/src/data/index.ts b/packages/spec/src/data/index.ts index 281e4c9288b..72c2d50dc88 100644 --- a/packages/spec/src/data/index.ts +++ b/packages/spec/src/data/index.ts @@ -180,6 +180,9 @@ export * from './api-derivation'; // so the two can never drift apart (#7521). export * from './managed-api-affordance'; export * from './field.zod'; +// [ADR-0146 D10] The customer extension rules on a base object's fields: the +// `__c` reservation and the base-field display-property whitelist. +export * from './field-customization'; // [ADR-0144 Open item 3] The CLDR fraction digits per currency — the checked-in // CLDR snapshot whose key set is the `iso_4217_currency` value domain. Its // digit counts are CLDR's, which differ from ISO 4217's minor units for some diff --git a/packages/spec/src/data/mapping.zod.ts b/packages/spec/src/data/mapping.zod.ts index 6e2ff1171da..c141571237a 100644 --- a/packages/spec/src/data/mapping.zod.ts +++ b/packages/spec/src/data/mapping.zod.ts @@ -4,6 +4,7 @@ import { z } from 'zod'; import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod'; import { strictObject } from '../shared/strict-object'; import { MetadataProtectionFields } from '../kernel/metadata-protection.zod'; +import { OverrideDeclarationFields } from '../kernel/override-declaration.zod'; // `QuerySchema` left with `extractQuery` in 17.0.0 (#4509) — a mapping no // longer carries a query of its own. @@ -442,6 +443,8 @@ export const MappingSchema = lazySchema(() => strictObject({ // they were dropped on every parse: protection metadata lost on round-trip, // and a hard 422 the day this shape closed — which is today. ...MetadataProtectionFields, + // ADR-0146 D2 — the override declaration (authored; travels with the item). + ...OverrideDeclarationFields, })); export type Mapping = z.input; diff --git a/packages/spec/src/kernel/customization-package.test.ts b/packages/spec/src/kernel/customization-package.test.ts new file mode 100644 index 00000000000..dcbde8f42be --- /dev/null +++ b/packages/spec/src/kernel/customization-package.test.ts @@ -0,0 +1,226 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * ADR-0146 D2, D3, D6, D9, D10 — the paired `.custom` package's identity + * rules and the closed overridable-type list, as table tests. + */ + +import { describe, expect, it } from 'vitest'; +import { + CUSTOMIZATION_NAMESPACE_SUFFIX, + MAX_CUSTOMIZATION_BASE_NAMESPACE_LENGTH, + OVERRIDABLE_METADATA_TYPES, + OVERRIDE_TYPE_CLASSIFICATION, + customizationBasePackageId, + customizationDependencyRange, + customizationNamespace, + customizationPackageId, + getOverrideTypeClassification, + isCustomizationPackage, + isCustomizationPairOf, + isOverridableMetadataType, +} from './customization-package'; +import { CUSTOMIZATION_PACKAGE_SUFFIX } from './override-declaration.zod'; +import { ManifestSchema, MANIFEST_ID_PATTERN } from './manifest.zod'; +import { DEFAULT_METADATA_TYPE_REGISTRY, type MetadataType } from './metadata-plugin.zod'; +import { deriveNamespaceFromPackageId } from './namespace-prefix'; + +describe('the pair\'s package id', () => { + it('the suffix is `.custom` (maintainer: 「同意 后缀改成 .custom」)', () => { + expect(CUSTOMIZATION_PACKAGE_SUFFIX).toBe('.custom'); + }); + + it.each([ + ['hotcrm', 'hotcrm.custom'], + ['com.acme.crm', 'com.acme.crm.custom'], + ['com.example.showcase', 'com.example.showcase.custom'], + ['com.example.help-desk', 'com.example.help-desk.custom'], + ])('customizationPackageId(%s) → %s, a valid manifest id', (base, pair) => { + expect(customizationPackageId(base)).toEqual({ ok: true, value: pair }); + expect(MANIFEST_ID_PATTERN.test(pair)).toBe(true); + }); + + it.each([ + ['', 'base_id_missing'], + [undefined, 'base_id_missing'], + [42, 'base_id_missing'], + ['hotcrm.custom', 'base_is_customization_package'], + ['com.example.showcase.custom', 'base_is_customization_package'], + ['Com.Acme', 'base_id_invalid'], + ['@acme/crm', 'base_id_invalid'], + ['com..acme', 'base_id_invalid'], + ])('customizationPackageId(%j) is refused: %s', (base, reason) => { + expect(customizationPackageId(base)).toEqual({ ok: false, reason }); + }); + + it.each([ + ['hotcrm.custom', 'hotcrm'], + ['com.example.showcase.custom', 'com.example.showcase'], + ['com.acme.crm', null], + ['custom', null], + ['.custom', null], + ['a.custom.custom', null], + ['Com.Acme.custom', null], + [undefined, null], + ])('customizationBasePackageId(%j) → %j', (id, base) => { + expect(customizationBasePackageId(id)).toBe(base); + expect(isCustomizationPackage(id)).toBe(base !== null); + }); + + it('base-of is the exact inverse of pair-of on every valid base', () => { + for (const base of ['hotcrm', 'com.acme.crm', 'com.example.showcase', 'org.example.help-desk']) { + const pair = customizationPackageId(base); + expect(pair.ok).toBe(true); + if (pair.ok) expect(customizationBasePackageId(pair.value)).toBe(base); + } + }); + + it.each([ + ['com.acme.crm.custom', 'com.acme.crm', true], + ['hotcrm.custom', 'hotcrm', true], + // A dependent package is not the pair, whatever it depends on (ruling A). + ['com.partner.crm-addon', 'com.acme.crm', false], + // The pair of a DIFFERENT base. + ['com.acme.crm.custom', 'com.acme.crmx', false], + ['com.acme.crmx.custom', 'com.acme.crm', false], + // The base itself is not its own pair. + ['com.acme.crm', 'com.acme.crm', false], + // A pair is never paired again. + ['com.acme.crm.custom.custom', 'com.acme.crm.custom', false], + ])('isCustomizationPairOf(%s, %s) → %s', (pair, base, verdict) => { + expect(isCustomizationPairOf(pair, base)).toBe(verdict); + }); +}); + +describe('the pair\'s namespace — `_custom`, never derived', () => { + it('the derived namespace of a pair id is the wrong one (the reason the rule exists)', () => { + expect(deriveNamespaceFromPackageId('hotcrm.custom')).toBe('custom'); + expect(customizationNamespace('crm')).toEqual({ ok: true, value: 'crm_custom' }); + }); + + it('13 is the grammar\'s own limit: 20 characters less the 7 of `_custom`', () => { + const grammar = ManifestSchema.shape.namespace; + const at = 'a'.repeat(MAX_CUSTOMIZATION_BASE_NAMESPACE_LENGTH) + CUSTOMIZATION_NAMESPACE_SUFFIX; + const past = 'a'.repeat(MAX_CUSTOMIZATION_BASE_NAMESPACE_LENGTH + 1) + CUSTOMIZATION_NAMESPACE_SUFFIX; + expect(MAX_CUSTOMIZATION_BASE_NAMESPACE_LENGTH).toBe(13); + expect(grammar.safeParse(at).success).toBe(true); + expect(grammar.safeParse(past).success).toBe(false); + }); + + it.each([ + ['crm', 'crm_custom'], + ['feature_flags', 'feature_flags_custom'], // 13 characters — the longest base in the repository today + ['abcdefghijklm', 'abcdefghijklm_custom'], + ])('customizationNamespace(%s) → %s', (base, pair) => { + expect(customizationNamespace(base)).toEqual({ ok: true, value: pair }); + expect(ManifestSchema.shape.namespace.safeParse(pair).success).toBe(true); + }); + + it.each([ + ['abcdefghijklmn', 'base_namespace_too_long'], // 14 characters + ['a_twenty_char_namesp', 'base_namespace_too_long'], + ['', 'base_namespace_missing'], + [undefined, 'base_namespace_missing'], + ['Crm', 'base_namespace_invalid'], + ['1crm', 'base_namespace_invalid'], + ])('customizationNamespace(%j) is refused: %s — no truncation, no hashing', (base, reason) => { + expect(customizationNamespace(base)).toEqual({ ok: false, reason }); + }); +}); + +describe('the pair\'s dependency range — `^` of the base version', () => { + it.each([ + ['3.2.1', '^3'], + ['1.0.0', '^1'], + ['0.4.0', '^0'], + ['10.0.0', '^10'], + ['2.0.0-beta.1', '^2'], + ['1.4.0+build.7', '^1'], + ['1.0.0-rc.1+exp.sha.5114f85', '^1'], + ])('customizationDependencyRange(%s) → %s', (version, range) => { + expect(customizationDependencyRange(version)).toEqual({ ok: true, value: range }); + }); + + it.each([['v1.0.0'], ['1.0'], ['latest'], [''], ['01.1.1'], [undefined], [3]])( + 'customizationDependencyRange(%j) is refused: base_version_not_semver', + (version) => { + expect(customizationDependencyRange(version)).toEqual({ ok: false, reason: 'base_version_not_semver' }); + }, + ); +}); + +describe('the closed overridable-type list, pinned to the registry', () => { + const registryTypes = DEFAULT_METADATA_TYPE_REGISTRY.map((e) => e.type).sort(); + const classified = Object.keys(OVERRIDE_TYPE_CLASSIFICATION).sort(); + + it('classifies every registry type — a type added to the registry without a row here fails', () => { + expect(registryTypes.filter((t) => !classified.includes(t))).toEqual([]); + }); + + it('classifies nothing the registry does not hold', () => { + expect(classified.filter((t) => !registryTypes.includes(t))).toEqual([]); + }); + + it('every runtime-creatable type has a classification (the registry\'s `allowRuntimeCreate` closure)', () => { + const runtimeCreatable = DEFAULT_METADATA_TYPE_REGISTRY.filter((e) => e.allowRuntimeCreate).map((e) => e.type); + expect(runtimeCreatable.length).toBeGreaterThan(0); + for (const type of runtimeCreatable) expect(getOverrideTypeClassification(type), type).toBeDefined(); + }); + + it('every overridable type is runtime-creatable (D2: "Studio or the metadata API can author")', () => { + const byType = new Map(DEFAULT_METADATA_TYPE_REGISTRY.map((e) => [e.type, e])); + for (const type of OVERRIDABLE_METADATA_TYPES) expect(byType.get(type)?.allowRuntimeCreate, type).toBe(true); + }); + + it('a `not_runtime_creatable` row is one the registry really closes', () => { + const byType = new Map(DEFAULT_METADATA_TYPE_REGISTRY.map((e) => [e.type, e])); + for (const [type, row] of Object.entries(OVERRIDE_TYPE_CLASSIFICATION)) { + if (row.basis === 'not_runtime_creatable') expect(byType.get(type)?.allowRuntimeCreate, type).toBe(false); + } + }); + + it('D6: permission sets, positions and capabilities are excluded whatever the registry says', () => { + for (const type of ['permission', 'position', 'capability'] as const) { + expect(OVERRIDE_TYPE_CLASSIFICATION[type]).toEqual({ disposition: 'excluded', basis: 'adr_0146_d6_security_catalog' }); + expect(isOverridableMetadataType(type)).toBe(false); + } + }); + + it('D10: `object` is extended, never replaced', () => { + expect(OVERRIDE_TYPE_CLASSIFICATION.object.disposition).toBe('extend_only'); + expect(isOverridableMetadataType('object')).toBe(false); + }); + + it('D2: the code-only types stay outside', () => { + for (const type of ['field', 'hook', 'job', 'api', 'agent'] as const) { + expect(OVERRIDE_TYPE_CLASSIFICATION[type].basis).toBe('adr_0126_code_only'); + expect(isOverridableMetadataType(type)).toBe(false); + } + }); + + it('D9: flow carries its four conditions and action its ledger switch plus conditions 1–3, as data', () => { + expect(OVERRIDE_TYPE_CLASSIFICATION.flow.conditions).toEqual([ + 'served_over_code_package', 'rebound_in_one_reload', 'ledger_identity_inherited', 'version_pinning', + ]); + expect(OVERRIDE_TYPE_CLASSIFICATION.action.conditions).toEqual([ + 'action_ledger_switch', 'served_over_code_package', 'rebound_in_one_reload', 'ledger_identity_inherited', + ]); + const conditioned = Object.entries(OVERRIDE_TYPE_CLASSIFICATION) + .filter(([, row]) => 'conditions' in row).map(([type]) => type).sort(); + expect(conditioned).toEqual(['action', 'flow']); + }); + + it('the types ADR-0146 does not decide are excluded (today\'s posture) and marked for the maintainer', () => { + const undecided = (Object.keys(OVERRIDE_TYPE_CLASSIFICATION) as MetadataType[]) + .filter((t) => OVERRIDE_TYPE_CLASSIFICATION[t].basis === 'undecided').sort(); + expect(undecided).toEqual(['datasource', 'seed', 'skill', 'tool']); + for (const type of undecided) expect(OVERRIDE_TYPE_CLASSIFICATION[type].disposition).toBe('excluded'); + }); + + it('isOverridableMetadataType answers false for a name that is no metadata type', () => { + expect(isOverridableMetadataType('view')).toBe(true); + expect(isOverridableMetadataType('not_a_type')).toBe(false); + expect(isOverridableMetadataType('toString')).toBe(false); + expect(getOverrideTypeClassification(undefined)).toBeUndefined(); + }); +}); diff --git a/packages/spec/src/kernel/customization-package.ts b/packages/spec/src/kernel/customization-package.ts new file mode 100644 index 00000000000..48ecf6b8020 --- /dev/null +++ b/packages/spec/src/kernel/customization-package.ts @@ -0,0 +1,366 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { MANIFEST_ID_PATTERN, ManifestSchema } from './manifest.zod'; +import type { MetadataType } from './metadata-plugin.zod'; +import { CUSTOMIZATION_PACKAGE_SUFFIX } from './override-declaration.zod'; +import { SEMVER_2_0_0_VERSION_PATTERN } from './version-grammar'; + +/** + * # The paired customization package (ADR-0146 D2, D3, D6) + * + * Every installed base gets one paired writable package, `.custom`, that + * holds the customer's changes to it. This module is the ONE spelling of the + * pair's identity rules — the id, the namespace, the dependency range — and of + * the closed list of metadata types a declared override applies to. The doors + * of ADR-0146's Rollout read it: the ensure door creates the pair from + * {@link customizationPackageId}, {@link customizationNamespace} and + * {@link customizationDependencyRange}; the write gate asks + * {@link isCustomizationPairOf} and {@link OVERRIDE_TYPE_CLASSIFICATION}; the + * uninstall door asks {@link customizationBasePackageId}. ⛔ None of them + * re-derives any of it: a second spelling is a declaration the first one cannot + * be checked against. + * + * Pure functions over strings, no I/O, no registry read: whether a pair is + * INSTALLED is the door's question, never this module's. + * + * Every derivation answers a discriminated result, never a throw and never a + * guess: `{ ok: true, value }`, or `{ ok: false, reason }` naming why the input + * has no answer, so a door refuses with a sentence instead of inventing a + * value (no truncation, no hashing). + */ + +// ───────────────────────────────────────────────────────────────────── +// Results +// ───────────────────────────────────────────────────────────────────── + +/** A derivation that produced its value. */ +export interface CustomizationDerived { + ok: true; + value: string; +} + +/** + * Why a base has no paired package id: + * - `base_id_missing` — the base id is empty or not a string; + * - `base_is_customization_package` — the base is itself a `.custom` package, + * and a pair is never paired again; + * - `base_id_invalid` — `.custom` would not be a valid package id + * (`MANIFEST_ID_PATTERN`: lowercase dot-separated segments). + */ +export type CustomizationPackageIdRefusal = + | 'base_id_missing' + | 'base_is_customization_package' + | 'base_id_invalid'; + +/** + * Why a base namespace has no pair namespace: + * - `base_namespace_missing` — empty or not a string; + * - `base_namespace_too_long` — longer than + * {@link MAX_CUSTOMIZATION_BASE_NAMESPACE_LENGTH}, so `_custom` + * exceeds the manifest namespace grammar's 20 characters; + * - `base_namespace_invalid` — `_custom` fails the manifest namespace + * grammar for any other reason (an uppercase letter, a leading digit). + */ +export type CustomizationNamespaceRefusal = + | 'base_namespace_missing' + | 'base_namespace_too_long' + | 'base_namespace_invalid'; + +/** Why a base version yields no dependency range: it is not a SemVer 2.0.0 version. */ +export type CustomizationDependencyRangeRefusal = 'base_version_not_semver'; + +/** A derivation that has no value, and the named reason. */ +export interface CustomizationRefused { + ok: false; + reason: R; +} + +export type CustomizationDerivation = CustomizationDerived | CustomizationRefused; + +// ───────────────────────────────────────────────────────────────────── +// The pair's package id (ADR-0146 D3) +// ───────────────────────────────────────────────────────────────────── + +/** + * The paired customization package id of a base: `.custom`. + * + * | base | answer | + * |:--|:--| + * | `hotcrm` | `hotcrm.custom` | + * | `com.example.showcase` | `com.example.showcase.custom` | + * | `hotcrm.custom` | refused — `base_is_customization_package` | + * | `Com.Acme`, `@acme/crm` | refused — `base_id_invalid` | + */ +export function customizationPackageId( + baseId: unknown, +): CustomizationDerivation { + if (typeof baseId !== 'string' || baseId.length === 0) return { ok: false, reason: 'base_id_missing' }; + if (baseId.endsWith(CUSTOMIZATION_PACKAGE_SUFFIX)) return { ok: false, reason: 'base_is_customization_package' }; + const pair = `${baseId}${CUSTOMIZATION_PACKAGE_SUFFIX}`; + if (!MANIFEST_ID_PATTERN.test(pair)) return { ok: false, reason: 'base_id_invalid' }; + return { ok: true, value: pair }; +} + +/** + * The base of a paired customization package id, or `null` when `id` is not + * one: `com.example.showcase.custom` → `com.example.showcase`. Exactly the + * inverse of {@link customizationPackageId}: an id is a pair iff some base + * derives it, so `a.custom.custom` (whose would-be base is itself a pair) and + * the bare `.custom` have no base. + */ +export function customizationBasePackageId(id: unknown): string | null { + if (typeof id !== 'string' || !id.endsWith(CUSTOMIZATION_PACKAGE_SUFFIX)) return null; + const base = id.slice(0, -CUSTOMIZATION_PACKAGE_SUFFIX.length); + const derived = customizationPackageId(base); + return derived.ok && derived.value === id ? base : null; +} + +/** Whether `id` is a paired customization package id (some base derives it). */ +export function isCustomizationPackage(id: unknown): boolean { + return customizationBasePackageId(id) !== null; +} + +/** + * Whether `pairId` is THE paired customization package of `baseId` — the + * predicate the write gate reads for an override declaration: only the paired + * `.custom` declares an override of ``'s items (the ruling recorded + * on ADR-0146's amendment card, ruling A). + */ +export function isCustomizationPairOf(pairId: unknown, baseId: unknown): boolean { + const derived = customizationPackageId(baseId); + return derived.ok && derived.value === pairId; +} + +// ───────────────────────────────────────────────────────────────────── +// The pair's namespace (ADR-0146 D3, Q4) +// ───────────────────────────────────────────────────────────────────── + +/** The suffix the pair's namespace adds to the base's: `crm` → `crm_custom`. */ +export const CUSTOMIZATION_NAMESPACE_SUFFIX = '_custom'; + +/** + * The longest base namespace that has a pair namespace: the manifest namespace + * grammar admits 20 characters, and {@link CUSTOMIZATION_NAMESPACE_SUFFIX} + * takes 7. `customization-package.test.ts` pins this number against the + * grammar itself (`ManifestSchema.shape.namespace`), so the two cannot drift. + */ +export const MAX_CUSTOMIZATION_BASE_NAMESPACE_LENGTH = 13; + +/** + * The pair's namespace: `_custom`, set explicitly by the ensure door + * and never derived from the pair's id (ADR-0146 D3: + * `deriveNamespaceFromPackageId('hotcrm.custom')` answers `custom`, which is + * never written). + * + * A base namespace longer than {@link MAX_CUSTOMIZATION_BASE_NAMESPACE_LENGTH} + * gets NO pair namespace — `base_namespace_too_long`, and the ensure door + * refuses that base with a sentence. ⛔ No truncation and no hashing: either is + * a second spelling of the pair's namespace that the declaration cannot be + * checked against. (ADR-0146 is silent on this edge; it was decided by the + * spec card and listed for the maintainer.) + * + * The answer is judged against the manifest's own namespace grammar + * (`ManifestSchema.shape.namespace`), so a value this returns is always one a + * manifest accepts. + */ +export function customizationNamespace( + baseNamespace: unknown, +): CustomizationDerivation { + if (typeof baseNamespace !== 'string' || baseNamespace.length === 0) { + return { ok: false, reason: 'base_namespace_missing' }; + } + if (baseNamespace.length > MAX_CUSTOMIZATION_BASE_NAMESPACE_LENGTH) { + return { ok: false, reason: 'base_namespace_too_long' }; + } + const pair = `${baseNamespace}${CUSTOMIZATION_NAMESPACE_SUFFIX}`; + if (!ManifestSchema.shape.namespace.safeParse(pair).success) { + return { ok: false, reason: 'base_namespace_invalid' }; + } + return { ok: true, value: pair }; +} + +// ───────────────────────────────────────────────────────────────────── +// The pair's dependency range (ADR-0146 D3, "Decided here" item 1) +// ───────────────────────────────────────────────────────────────────── + +/** + * The range the pair declares on its base in `manifest.dependencies`: + * `^` of the base's `manifest.version` at creation (ADR-0146 D3 and + * "Decided here" item 1) — it admits the base's later minor and patch versions + * at a re-install and refuses a different major. + * + * | base version | answer | + * |:--|:--| + * | `3.2.1` | `^3` | + * | `0.4.0` | `^0` (every `0.x`) | + * | `2.0.0-beta.1`, `1.4.0+build.7` | `^2`, `^1` — the major of the numeric core | + * | `v1.0.0`, `1.0`, `latest` | refused — `base_version_not_semver` | + * + * A prerelease or build-metadata base answers the major of its numeric core, + * the ADR's words read literally. Note what that range then admits: the + * ADR-0141 D2 install check reads an installed version without a prerelease + * tag only, so a pair whose base is installed at a prerelease is re-installable + * once the base is at a release of that major. + */ +export function customizationDependencyRange( + baseVersion: unknown, +): CustomizationDerivation { + if (typeof baseVersion !== 'string') return { ok: false, reason: 'base_version_not_semver' }; + const match = SEMVER_2_0_0_VERSION_PATTERN.exec(baseVersion); + if (!match) return { ok: false, reason: 'base_version_not_semver' }; + return { ok: true, value: `^${match[1]}` }; +} + +// ───────────────────────────────────────────────────────────────────── +// The overridable-type list (ADR-0146 D2, D6, D9, D10) +// ───────────────────────────────────────────────────────────────────── + +/** + * What a declared override may do to a type's base item: + * - `overridable` — a `.custom` item of the base item's name, carrying + * `overrides`, replaces it whole (subject to `conditions`); + * - `extend_only` — the base item is extended, never replaced (`object`, + * ADR-0146 D10); + * - `excluded` — no override channel. + */ +export type OverrideDisposition = 'overridable' | 'extend_only' | 'excluded'; + +/** + * A condition ADR-0146 D9 sets before an override door for a behavioural type + * opens. Carried as data so the write gate reads it and never re-derives it: + * - `served_over_code_package` — D9.1: the declared item is armed over the + * loader's body for the name; + * - `rebound_in_one_reload` — D9.2: the trigger is moved from the base's + * definition to the override's in one reload step; + * - `ledger_identity_inherited` — D9.3: one `sys_metadata_activation` row + * switches whichever definition is armed for the name; + * - `version_pinning` — D9.4: a suspended run resumes on the version it started + * on (flow only); + * - `action_ledger_switch` — D9's `action` paragraph: the action ledger switch + * lands first (outside ADR-0146's epic). + */ +export type OverrideCondition = + | 'served_over_code_package' + | 'rebound_in_one_reload' + | 'ledger_identity_inherited' + | 'version_pinning' + | 'action_ledger_switch'; + +/** + * Where a classification comes from: + * - `adr_0146_d2_listed` — named in D2's "Which types" list; + * - `adr_0146_d6_security_catalog` — D6: one name per deployment (permission + * sets, positions, capabilities); + * - `adr_0146_d10_extended` — D10: objects are extended, never replaced; + * - `adr_0126_code_only` — D2 and D6: the types ADR-0126 §3 places outside the + * regimes as code gain no override channel; + * - `not_package_shipped` — D2's first sentence fails: packages never ship the + * type as a standalone document; + * - `not_runtime_creatable` — D2's first sentence fails: neither Studio nor the + * metadata API can author the type (`allowRuntimeCreate: false`); + * - `undecided` — ADR-0146 does not decide the type; classified `excluded` + * (today's posture, a reversible default) until the maintainer rules. + */ +export type OverrideClassificationBasis = + | 'adr_0146_d2_listed' + | 'adr_0146_d6_security_catalog' + | 'adr_0146_d10_extended' + | 'adr_0126_code_only' + | 'not_package_shipped' + | 'not_runtime_creatable' + | 'undecided'; + +/** One type's override classification. */ +export interface OverrideTypeClassification { + readonly disposition: OverrideDisposition; + readonly basis: OverrideClassificationBasis; + /** D9 conditions an override door for this type waits for (behavioural types only). */ + readonly conditions?: readonly OverrideCondition[]; +} + +const listed: OverrideTypeClassification = { disposition: 'overridable', basis: 'adr_0146_d2_listed' }; +const securityCatalog: OverrideTypeClassification = { disposition: 'excluded', basis: 'adr_0146_d6_security_catalog' }; +const codeOnly: OverrideTypeClassification = { disposition: 'excluded', basis: 'adr_0126_code_only' }; +const notRuntimeCreatable: OverrideTypeClassification = { disposition: 'excluded', basis: 'not_runtime_creatable' }; +const undecided: OverrideTypeClassification = { disposition: 'excluded', basis: 'undecided' }; + +/** + * The CLOSED override classification of every metadata type — the constant the + * write gate reads, never a judgement it makes (ADR-0146 D2: "the closed list + * is a constant the write gate reads"). + * + * Total over `MetadataType` at compile time and pinned to + * `DEFAULT_METADATA_TYPE_REGISTRY` by `customization-package.test.ts`, which + * fails when a registry type is added without a row here, when a row names no + * registry type, and when an `overridable` row is not runtime-creatable. + * + * Four runtime-creatable types ADR-0146 does not decide are `undecided` + * (classified `excluded`, today's posture) and listed for the maintainer: + * `seed`, `datasource`, `tool`, `skill`. + */ +export const OVERRIDE_TYPE_CLASSIFICATION = { + // ── D2's list ────────────────────────────────────────────────────── + view: listed, + dashboard: listed, + report: listed, + translation: listed, + email_template: listed, + page: listed, + app: listed, + dataset: listed, + doc: listed, + book: listed, + mapping: listed, + flow: { + disposition: 'overridable', + basis: 'adr_0146_d2_listed', + conditions: ['served_over_code_package', 'rebound_in_one_reload', 'ledger_identity_inherited', 'version_pinning'], + }, + action: { + disposition: 'overridable', + basis: 'adr_0146_d2_listed', + conditions: ['action_ledger_switch', 'served_over_code_package', 'rebound_in_one_reload', 'ledger_identity_inherited'], + }, + // ── D10: extended, never replaced ────────────────────────────────── + object: { disposition: 'extend_only', basis: 'adr_0146_d10_extended' }, + // ── D6: the security catalog's one-name-per-deployment types ────── + permission: securityCatalog, + position: securityCatalog, + capability: securityCatalog, + // ── D2 / D6: code-only types stay outside ────────────────────────── + field: codeOnly, + hook: codeOnly, + job: codeOnly, + api: codeOnly, + agent: codeOnly, + // ── D2's first sentence: not a package-shipped, runtime-authorable document + external_catalog: { disposition: 'excluded', basis: 'not_package_shipped' }, + picklist: notRuntimeCreatable, + sharing_rule: notRuntimeCreatable, + queue: notRuntimeCreatable, + assignment_rule: notRuntimeCreatable, + // ── Not decided by ADR-0146 — listed for the maintainer ──────────── + seed: undecided, + datasource: undecided, + tool: undecided, + skill: undecided, +} as const satisfies Record; + +/** The metadata types a declared override applies to (`disposition: 'overridable'`). */ +export const OVERRIDABLE_METADATA_TYPES: readonly MetadataType[] = Object.freeze( + (Object.keys(OVERRIDE_TYPE_CLASSIFICATION) as MetadataType[]).filter( + (type) => OVERRIDE_TYPE_CLASSIFICATION[type].disposition === 'overridable', + ), +); + +/** The classification of `type`, or `undefined` for a name that is no metadata type. */ +export function getOverrideTypeClassification(type: unknown): OverrideTypeClassification | undefined { + return typeof type === 'string' && Object.prototype.hasOwnProperty.call(OVERRIDE_TYPE_CLASSIFICATION, type) + ? OVERRIDE_TYPE_CLASSIFICATION[type as MetadataType] + : undefined; +} + +/** Whether a declared override applies to `type` (its disposition is `overridable`). */ +export function isOverridableMetadataType(type: unknown): boolean { + return getOverrideTypeClassification(type)?.disposition === 'overridable'; +} diff --git a/packages/spec/src/kernel/index.ts b/packages/spec/src/kernel/index.ts index baf1437875c..07d2f09a19f 100644 --- a/packages/spec/src/kernel/index.ts +++ b/packages/spec/src/kernel/index.ts @@ -36,6 +36,11 @@ export * from './platform-capabilities'; export * from './metadata-loader.zod'; export * from './metadata-plugin.zod'; export * from './metadata-protection.zod'; +// [ADR-0146 D2, D3] The override declaration every overridable type spreads, +// and the paired `.custom` package's identity rules and the closed +// overridable-type list the doors read. +export * from './override-declaration.zod'; +export * from './customization-package'; // The read path's OWN annotations (`_diagnostics`, `_draft`) — the underscore // keys that, unlike the protection envelope above, must never survive back into // a persisted body or a strict re-parse (#4326, cloud#971). diff --git a/packages/spec/src/kernel/override-declaration.test.ts b/packages/spec/src/kernel/override-declaration.test.ts new file mode 100644 index 00000000000..4cb42f9935b --- /dev/null +++ b/packages/spec/src/kernel/override-declaration.test.ts @@ -0,0 +1,205 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * ADR-0146 D2 — the override declaration `overrides: { package, basedOn }`. + * + * Three facts, each measured rather than assumed: + * + * 1. the shape accepts the declaration and refuses every malformed one; + * 2. every type the closed list classifies `overridable` carries the key + * through the parse the metadata door's save runs (the stored body), and a + * type it excludes refuses it by name; + * 3. the key survives `defineStack()` and the package export's view channel. + * (The export's own top-level strip is pinned in `@objectstack/runtime`, + * `domains/packages-export-override-declaration.test.ts`.) + */ + +import { describe, expect, it } from 'vitest'; +import { OverrideDeclarationSchema, OverrideDeclarationFields, CUSTOMIZATION_PACKAGE_SUFFIX } from './override-declaration.zod'; +import { OVERRIDE_TYPE_CLASSIFICATION, OVERRIDABLE_METADATA_TYPES } from './customization-package'; +import { getMetadataTypeSchema } from './metadata-type-schemas'; +import { getMetadataCreateSeed } from './metadata-create-seeds'; +import { MetadataProtectionFields } from './metadata-protection.zod'; +import { AssembledViewArtifactSchema } from '../ui/assembled-views.zod'; +import { defineStack } from '../stack.zod'; +import type { MetadataType } from './metadata-plugin.zod'; + +const DECLARATION = { package: 'com.acme.crm', basedOn: '3.2.0' } as const; + +/** The issue paths and codes of a failed parse, for a located assertion. */ +function issuesOf(result: { success: boolean; error?: { issues: Array<{ code: string; path: PropertyKey[]; keys?: string[] }> } }) { + return (result.error?.issues ?? []).map((i) => ({ code: i.code, path: i.path.join('.'), keys: i.keys })); +} + +describe('OverrideDeclarationSchema — the shape', () => { + it('accepts { package, basedOn }', () => { + expect(OverrideDeclarationSchema.parse(DECLARATION)).toEqual(DECLARATION); + }); + + it('accepts a dotted base id and a prerelease basedOn — basedOn is recorded, never judged', () => { + const d = { package: 'com.example.showcase', basedOn: 'anything a person wrote' }; + expect(OverrideDeclarationSchema.safeParse(d).success).toBe(true); + }); + + it.each([ + ['no package', { basedOn: '3.2.0' }, 'package'], + ['no basedOn', { package: 'com.acme.crm' }, 'basedOn'], + ['an empty package', { package: '', basedOn: '3.2.0' }, 'package'], + ['an empty basedOn', { package: 'com.acme.crm', basedOn: '' }, 'basedOn'], + ['a non-string basedOn', { package: 'com.acme.crm', basedOn: 3 }, 'basedOn'], + ])('refuses %s, at that key', (_label, body, key) => { + const r = OverrideDeclarationSchema.safeParse(body); + expect(r.success).toBe(false); + expect(issuesOf(r).map((i) => i.path)).toContain(key); + }); + + it('refuses an unknown key by name — a misspelt key is never dropped', () => { + const r = OverrideDeclarationSchema.safeParse({ ...DECLARATION, basedon: '3.2.0' }); + expect(r.success).toBe(false); + expect(issuesOf(r)).toContainEqual(expect.objectContaining({ code: 'unrecognized_keys', keys: ['basedon'] })); + }); + + it(`refuses a \`${CUSTOMIZATION_PACKAGE_SUFFIX}\` package — the declaration names the BASE, never a pair`, () => { + const r = OverrideDeclarationSchema.safeParse({ package: `com.acme.crm${CUSTOMIZATION_PACKAGE_SUFFIX}`, basedOn: '3.2.0' }); + expect(r.success).toBe(false); + expect(issuesOf(r)).toContainEqual(expect.objectContaining({ code: 'custom', path: 'package' })); + }); + + it('describes basedOn as recorded and never judged (ADR-0146 D2)', () => { + const described = OverrideDeclarationSchema.shape.basedOn.description ?? ''; + expect(described).toMatch(/Recorded, never judged/); + expect(OverrideDeclarationFields.overrides.description ?? '').toMatch(/recorded and never judged/); + }); + + it('is NOT part of the ADR-0010 envelope the registration and clone paths strip', () => { + // `Object.keys(MetadataProtectionFields)` is the set `artifact-collections.ts` + // and `flow-clone.ts` drop as loader-stamped state. The declaration is authored. + expect(Object.keys(MetadataProtectionFields)).not.toContain('overrides'); + }); +}); + +// ───────────────────────────────────────────────────────────────────── +// The stored body: the parse the metadata door's save runs +// ───────────────────────────────────────────────────────────────────── + +/** Minimal valid bodies for the overridable types with no create seed. */ +const EXTRA_FIXTURES: Partial>> = { + report: { name: 'sales_by_stage', label: 'Sales by Stage', type: 'summary', dataset: 'sales', rows: ['stage'], values: ['revenue'] }, + app: { name: 'crm', label: 'CRM', navigation: [] }, + doc: { name: 'crm_lead_guide', content: '# Lead Guide' }, + book: { name: 'crm_guide', groups: [{ key: 'start', label: 'Start', include: 'crm_*' }] }, +}; + +function fixtureFor(type: MetadataType): Record { + const seed = getMetadataCreateSeed(type) as Record | undefined; + const body = seed ?? EXTRA_FIXTURES[type]; + if (!body) throw new Error(`no fixture for overridable type '${type}'`); + return structuredClone(body); +} + +/** The flattened list overlay — the package-less overlay row's shape ADR-0146 D11 migrates. */ +const LIST_OVERLAY = { + type: 'grid', + data: { provider: 'object', object: 'crm_account' }, + columns: ['name'], + name: 'crm_account.default', + viewKind: 'list', + object: 'crm_account', +}; + +describe('the stored body — every overridable type carries `overrides` through its metadata schema', () => { + it('the overridable list is the thirteen types ADR-0146 D2 names', () => { + expect([...OVERRIDABLE_METADATA_TYPES].sort()).toEqual([ + 'action', 'app', 'book', 'dashboard', 'dataset', 'doc', 'email_template', + 'flow', 'mapping', 'page', 'report', 'translation', 'view', + ]); + }); + + it.each(OVERRIDABLE_METADATA_TYPES.map((t) => [t]))('%s keeps `overrides` on parse', (type) => { + const schema = getMetadataTypeSchema(type)!; + const body = { ...fixtureFor(type as MetadataType), overrides: { ...DECLARATION } }; + const r = schema.safeParse(body); + expect(issuesOf(r)).toEqual([]); + expect((r.data as Record).overrides).toEqual(DECLARATION); + }); + + it.each(OVERRIDABLE_METADATA_TYPES.map((t) => [t]))('%s refuses a malformed declaration, located under `overrides`', (type) => { + const schema = getMetadataTypeSchema(type)!; + const r = schema.safeParse({ ...fixtureFor(type as MetadataType), overrides: { package: 'com.acme.crm' } }); + expect(r.success).toBe(false); + expect(JSON.stringify(r.error?.issues)).toContain('basedOn'); + }); + + it('a flattened view overlay (the migrated package-less row) keeps `overrides` too', () => { + const r = getMetadataTypeSchema('view')!.safeParse({ ...LIST_OVERLAY, overrides: { ...DECLARATION } }); + expect(issuesOf(r)).toEqual([]); + expect((r.data as Record).overrides).toEqual(DECLARATION); + }); + + // The types the closed list excludes and that have a create seed: the key is + // not declared on them, so a body carrying it is refused by name — never + // silently stored as an override no door honours. + const excludedWithSeed = (Object.keys(OVERRIDE_TYPE_CLASSIFICATION) as MetadataType[]) + .filter((t) => OVERRIDE_TYPE_CLASSIFICATION[t].disposition !== 'overridable' && getMetadataCreateSeed(t) !== undefined); + + it('measures at least the D6 security type, the extend-only type and a code-only type among them', () => { + expect(excludedWithSeed).toEqual(expect.arrayContaining(['permission', 'object', 'hook'])); + }); + + it.each(excludedWithSeed.map((t) => [t]))('%s (not overridable) refuses `overrides` as an unknown key', (type) => { + const schema = getMetadataTypeSchema(type)!; + const r = schema.safeParse({ ...(getMetadataCreateSeed(type) as object), overrides: { ...DECLARATION } }); + expect(r.success).toBe(false); + expect(issuesOf(r)).toContainEqual(expect.objectContaining({ code: 'unrecognized_keys', keys: ['overrides'] })); + }); +}); + +// ───────────────────────────────────────────────────────────────────── +// defineStack() and the export's view channel +// ───────────────────────────────────────────────────────────────────── + +describe('`overrides` survives defineStack()', () => { + // Strict (the default): every collection is parsed by its item schema and + // the cross-reference checks run, so the key is judged, not skipped. + const OBJ = 'crm_custom_ticket'; + const o = () => ({ ...DECLARATION }); + const stack = defineStack({ + manifest: { id: 'com.acme.crm.custom', name: 'crm_custom', version: '1.0.0', type: 'app', namespace: 'crm_custom' }, + objects: [{ name: OBJ, label: 'Ticket', fields: { subject: { type: 'text', label: 'Subject' } } }], + apps: [{ ...EXTRA_FIXTURES.app!, overrides: o() }], + pages: [{ ...fixtureFor('page'), object: OBJ, overrides: o() }], + dashboards: [{ ...fixtureFor('dashboard'), overrides: o() }], + datasets: [{ ...fixtureFor('dataset'), name: 'tickets', object: OBJ, overrides: o() }], + reports: [{ name: 'tickets_by_subject', label: 'Tickets', type: 'summary', dataset: 'tickets', rows: ['subject'], values: ['count'], overrides: o() }], + actions: [{ ...fixtureFor('action'), overrides: o() }], + flows: [{ ...fixtureFor('flow'), overrides: o() }], + emailTemplates: [{ ...fixtureFor('email_template'), overrides: o() }], + docs: [{ ...EXTRA_FIXTURES.doc!, overrides: o() }], + books: [{ ...EXTRA_FIXTURES.book!, overrides: o() }], + mappings: [{ ...fixtureFor('mapping'), targetObject: OBJ, fieldMapping: [{ source: 'Subject', target: 'subject' }], overrides: o() }], + } as never) as unknown as Record>>; + + it.each([ + 'apps', 'pages', 'dashboards', 'reports', 'datasets', 'actions', 'flows', 'emailTemplates', 'docs', 'books', 'mappings', + ])('stack.%s[0].overrides is the declaration as written', (collection) => { + expect(stack[collection]?.[0]?.overrides).toEqual(DECLARATION); + }); + + it('a malformed declaration is refused by the strict build, not carried', () => { + expect(() => defineStack({ + manifest: { id: 'com.acme.crm.custom', name: 'crm_custom', version: '1.0.0', type: 'app', namespace: 'crm_custom' }, + flows: [{ ...fixtureFor('flow'), overrides: { package: 'com.acme.crm' } }], + } as never)).toThrow(/basedOn/); + }); +}); + +describe('`overrides` survives the export\'s view channel (`viewItems:`)', () => { + it.each([ + ['a standalone ViewItem', () => fixtureFor('view')], + ['a flattened list overlay', () => structuredClone(LIST_OVERLAY)], + ])('%s keeps `overrides` through AssembledViewArtifactSchema', (_label, body) => { + const r = AssembledViewArtifactSchema.safeParse({ ...body(), overrides: { ...DECLARATION } }); + expect(issuesOf(r)).toEqual([]); + expect((r.data as Record).overrides).toEqual(DECLARATION); + }); +}); diff --git a/packages/spec/src/kernel/override-declaration.zod.ts b/packages/spec/src/kernel/override-declaration.zod.ts new file mode 100644 index 00000000000..4285ddf5058 --- /dev/null +++ b/packages/spec/src/kernel/override-declaration.zod.ts @@ -0,0 +1,133 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { z } from 'zod'; +import { strictObject } from '../shared/strict-object'; + +/** + * # The override declaration (ADR-0146 D2) + * + * A customization of an installed base package (HotCRM) is an item of the + * base's paired writable package `.custom` (ADR-0146 D3). An item of that + * package that bears a base item's name and carries + * + * ```ts + * overrides: { package: '', basedOn: '' } + * ``` + * + * replaces the base's item WHOLE for every read that serves the base's name + * (ADR-0146 D8). The declaration lives in the item body, so it travels with the + * item through export and git: what a developer writes into `hotcrm.custom` in a + * repository and what an administrator does in Studio produce the same + * document. Only the paired `.custom` package declares an override (the + * ruling recorded on #28390, ruling A); the pairing helpers in + * `customization-package.ts` are what a door asks. + * + * ## Why a leaf module + * + * It imports nothing but zod and `strictObject`, on purpose. Every overridable + * type's schema spreads {@link OverrideDeclarationFields}, and some of those + * schemas are imported by `manifest.zod.ts` (`ui/app.zod.ts`); importing the + * manifest or the registry from here would close a module cycle that the eager + * schema build (`OS_EAGER_SCHEMAS=1`) evaluates in temporal-dead-zone order. + * The helpers that need the manifest grammar live in `customization-package.ts`, + * which no schema imports. + * + * ## What this module does NOT do + * + * It declares the shape. Admitting a declared override, refusing an undeclared + * same-named item, refusing a declaration from a package that is not the pair, + * and serving the declared document are the doors' (ADR-0146 Rollout, P1: + * the write gate, the effective read, the ensure door). Which types may carry + * the declaration is the closed list `OVERRIDE_TYPE_CLASSIFICATION`, never a + * judgement a door makes. + */ + +/** + * The suffix that names a base package's paired customization package: + * `hotcrm` → `hotcrm.custom`, `com.example.showcase` → + * `com.example.showcase.custom` (ADR-0146 D3; maintainer, verbatim: + * 「同意 后缀改成 .custom」). + * + * Declared here, in the leaf the schemas import, so the declaration's own + * refusal ("a pair is never a base") and the pairing helpers in + * `customization-package.ts` read one spelling. ⛔ Never spell `'.custom'` + * anywhere else: import this constant or ask a helper. + */ +export const CUSTOMIZATION_PACKAGE_SUFFIX = '.custom'; + +/** + * The override declaration's body: which base package's item this item + * replaces, and which version of that item the author started from. + * + * Closed: an unknown key is refused by name, because a misspelt `basedon` + * dropped in silence would leave a declaration that records nothing. + */ +export const OverrideDeclarationSchema = strictObject({ + surface: 'this override declaration', + history: + 'New with ADR-0146: the shape was closed from its first release, so a misspelt key is ' + + 'refused instead of dropped.', + aliases: { packageId: 'package', base: 'package', basePackage: 'package', version: 'basedOn', baseVersion: 'basedOn' }, +}, { + package: z.string() + .min(1) + .refine((id) => !id.endsWith(CUSTOMIZATION_PACKAGE_SUFFIX), { + message: + `\`overrides.package\` names the BASE package whose item this item replaces, never a \`${CUSTOMIZATION_PACKAGE_SUFFIX}\` package: ` + + `the declaring item lives in the base's paired \`${CUSTOMIZATION_PACKAGE_SUFFIX}\` package and names the base it overrides ` + + `(write \`com.acme.crm\`, not \`com.acme.crm${CUSTOMIZATION_PACKAGE_SUFFIX}\`).`, + }) + .describe( + 'The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). ' + + 'Only an item of the base\'s paired ".custom" package declares an override.', + ), + basedOn: z.string() + .min(1) + .describe( + 'Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). ' + + 'No door compares it with the installed base, no upgrade diff is produced from it, and nothing is ' + + 'refused on it; it tells a person re-basing the override where it started (ADR-0146 D2).', + ), +}).describe( + 'Override declaration (ADR-0146 D2): this item, of the base\'s paired ".custom" package and bearing a ' + + 'base item\'s name, replaces that base item whole for every read that serves the name.', +); + +export type OverrideDeclaration = z.input; +/** Post-parse shape of {@link OverrideDeclaration}. */ +export type OverrideDeclarationParsed = z.infer; + +/** + * Raw shape spliced into the schema of every type whose + * `OVERRIDE_TYPE_CLASSIFICATION` disposition is `'overridable'`, in the style of + * `MetadataProtectionFields`. + * + * ⛔ Not spread into `MetadataProtectionFields`, and deliberately so: that + * envelope is spread into types ADR-0146 excludes (`permission`, `position`, + * `capability`, the code-only types, `object`), and it is the set the + * registration and clone paths strip as loader-stamped state + * (`Object.keys(MetadataProtectionFields)`). The declaration is AUTHORED — it + * must survive registration and export — so it is its own spread, on the + * overridable types only. + * + * @example + * ```ts + * // An item of `com.acme.crm.custom`: + * { + * name: 'crm_account_welcome', + * subject: 'Welcome aboard', + * // …the whole document… + * overrides: { package: 'com.acme.crm', basedOn: '3.2.0' }, + * } + * ``` + */ +export const OverrideDeclarationFields = { + /** + * Declares that this item replaces the base package's item of the same name + * (ADR-0146 D2). `basedOn` is recorded and never judged. + */ + overrides: OverrideDeclarationSchema.optional().describe( + 'Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of ".custom" replaces the ' + + 'base package\'s same-named item whole. `basedOn` is recorded and never judged.', + ), +} as const; diff --git a/packages/spec/src/system/book.zod.ts b/packages/spec/src/system/book.zod.ts index 6bb1240e6c6..3a93b150ba2 100644 --- a/packages/spec/src/system/book.zod.ts +++ b/packages/spec/src/system/book.zod.ts @@ -5,6 +5,7 @@ import { lazySchema } from '../shared/lazy-schema'; import { strictObject } from '../shared/strict-object'; import { retiredKey } from '../shared/retired-key'; import { MetadataProtectionFields } from '../kernel/metadata-protection.zod'; +import { OverrideDeclarationFields } from '../kernel/override-declaration.zod'; /** * Package Documentation Navigation — the `book` element (ADR-0046 §6). @@ -172,6 +173,8 @@ export const BookSchema = lazySchema(() => // were dropped on every parse — protection metadata lost on round-trip, and // a hard 422 waiting for the day this shape is closed. ...MetadataProtectionFields, + // ADR-0146 D2 — the override declaration (authored; travels with the item). + ...OverrideDeclarationFields, }), ); diff --git a/packages/spec/src/system/doc.zod.ts b/packages/spec/src/system/doc.zod.ts index 4a77eb10fcf..d1756c46a16 100644 --- a/packages/spec/src/system/doc.zod.ts +++ b/packages/spec/src/system/doc.zod.ts @@ -27,6 +27,7 @@ import { z } from 'zod'; import { lazySchema } from '../shared/lazy-schema'; import { strictObject } from '../shared/strict-object'; import { MetadataProtectionFields } from '../kernel/metadata-protection.zod'; +import { OverrideDeclarationFields } from '../kernel/override-declaration.zod'; export const DocSchema = lazySchema(() => strictObject({ surface: 'this doc', @@ -151,6 +152,8 @@ export const DocSchema = lazySchema(() => strictObject({ // on every parse — the inverse drift that made `permission` 422 when it went // strict (#4001 findings log, entry 2). ...MetadataProtectionFields, + // ADR-0146 D2 — the override declaration (authored; travels with the item). + ...OverrideDeclarationFields, })); export type Doc = z.input; export type DocTranslation = NonNullable[string]; diff --git a/packages/spec/src/system/email-template.zod.ts b/packages/spec/src/system/email-template.zod.ts index d42b4f01e41..600e2b27285 100644 --- a/packages/spec/src/system/email-template.zod.ts +++ b/packages/spec/src/system/email-template.zod.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import { ProtectionSchema } from '../shared/protection.zod'; import { MetadataProtectionFields } from '../kernel/metadata-protection.zod'; +import { OverrideDeclarationFields } from '../kernel/override-declaration.zod'; import { lazySchema } from '../shared/lazy-schema'; import { retiredKey } from '../shared/retired-key'; import { strictObject } from '../shared/strict-object'; @@ -264,6 +265,8 @@ export const EmailTemplateDefinitionSchema = lazySchema(() => strictObject({ // ADR-0010 — runtime protection envelope (internal — set by loader). ...MetadataProtectionFields, + // ADR-0146 D2 — the override declaration (authored; travels with the item). + ...OverrideDeclarationFields, })); diff --git a/packages/spec/src/system/translation.zod.ts b/packages/spec/src/system/translation.zod.ts index dcc458e0584..babf8e8747e 100644 --- a/packages/spec/src/system/translation.zod.ts +++ b/packages/spec/src/system/translation.zod.ts @@ -10,6 +10,7 @@ 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'; +import { OverrideDeclarationFields } from '../kernel/override-declaration.zod'; import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod'; import { textSlotTemplateRefusal } from '../automation/flow-text-slot-template'; @@ -2472,6 +2473,8 @@ export const TranslationItemSchema = lazySchema(() => strictObject({ // dropped on every parse — and `authored-translation-sync` has to strip them // by hand on the read side precisely because the schema could not hold them. ...MetadataProtectionFields, + // ADR-0146 D2 — the override declaration (authored; travels with the item). + ...OverrideDeclarationFields, }).describe('One locale of translations — the `translation` metadata type')); /** A single `translation` metadata item. */ diff --git a/packages/spec/src/ui/action.zod.ts b/packages/spec/src/ui/action.zod.ts index 375cb7369ce..0c40490159d 100644 --- a/packages/spec/src/ui/action.zod.ts +++ b/packages/spec/src/ui/action.zod.ts @@ -46,6 +46,7 @@ import { MEMBERSHIP_REACH_NAMES, lowerRequiresMembershipReach } from '../identit import { strictUnknownKeyError } from '../shared/suggestions.zod'; import { strictObject } from '../shared/strict-object'; import { MetadataProtectionFields } from '../kernel/metadata-protection.zod'; +import { OverrideDeclarationFields } from '../kernel/override-declaration.zod'; import { lazySchema } from '../shared/lazy-schema'; import { aiJsonSchemaSlot } from '../shared/ai-json-schema-slot'; import { ACTION_TARGET_ALIASES } from './action-target-aliases'; @@ -1999,6 +2000,8 @@ const actionObject = () => strictObject({ // parse. This is the LAST name on the undeclared-envelope debt list the // structural walk opened with eight of. ...MetadataProtectionFields, + // ADR-0146 D2 — the override declaration (authored; travels with the item). + ...OverrideDeclarationFields, }); /** diff --git a/packages/spec/src/ui/app.zod.ts b/packages/spec/src/ui/app.zod.ts index 657505d6a5a..63738a9d5aa 100644 --- a/packages/spec/src/ui/app.zod.ts +++ b/packages/spec/src/ui/app.zod.ts @@ -8,6 +8,7 @@ import { ElementApiDataSourceSchema } from './page.zod'; import { strictObject, type StrictObjectOptions } from '../shared/strict-object'; import { lazySchema } from '../shared/lazy-schema'; import { MetadataProtectionFields } from '../kernel/metadata-protection.zod'; +import { OverrideDeclarationFields } from '../kernel/override-declaration.zod'; import { ProtectionSchema } from '../shared/protection.zod'; import { requiredOneOf } from '../shared/refinement-projection'; @@ -1902,6 +1903,8 @@ export const AppSchema = lazySchema(() => strictObject( // ADR-0010 — runtime protection envelope (internal — set by loader). ...MetadataProtectionFields, + // ADR-0146 D2 — the override declaration (authored; travels with the item). + ...OverrideDeclarationFields, })); /** diff --git a/packages/spec/src/ui/dashboard.zod.ts b/packages/spec/src/ui/dashboard.zod.ts index b9873204205..71b2b6bc6bd 100644 --- a/packages/spec/src/ui/dashboard.zod.ts +++ b/packages/spec/src/ui/dashboard.zod.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import { ProtectionSchema } from '../shared/protection.zod'; import { MetadataProtectionFields } from '../kernel/metadata-protection.zod'; +import { OverrideDeclarationFields } from '../kernel/override-declaration.zod'; import { strictObject } from '../shared/strict-object'; import type { KeySetGuidance } from '../shared/suggestions.zod'; import { FilterConditionSchema } from '../data/filter.zod'; @@ -1795,6 +1796,8 @@ export const DashboardSchema = lazySchema(() => strictObject({ // ADR-0010 — runtime protection envelope (internal — set by loader). ...MetadataProtectionFields, + // ADR-0146 D2 — the override declaration (authored; travels with the item). + ...OverrideDeclarationFields, })); diff --git a/packages/spec/src/ui/dataset.zod.ts b/packages/spec/src/ui/dataset.zod.ts index 1271b7ec733..c34139dae35 100644 --- a/packages/spec/src/ui/dataset.zod.ts +++ b/packages/spec/src/ui/dataset.zod.ts @@ -5,6 +5,7 @@ import { lazySchema } from '../shared/lazy-schema'; import { strictObject } from '../shared/strict-object'; import { ProtectionSchema } from '../shared/protection.zod'; import { MetadataProtectionFields } from '../kernel/metadata-protection.zod'; +import { OverrideDeclarationFields } from '../kernel/override-declaration.zod'; import { analyticsCarrierFilter } from './analytics-carrier-filter'; import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod'; import { I18nLabelSchema } from './i18n.zod'; @@ -681,6 +682,8 @@ export const DatasetSchema = lazySchema(() => strictObject({ // ADR-0010 — runtime protection envelope (internal — set by loader). ...MetadataProtectionFields, + // ADR-0146 D2 — the override declaration (authored; travels with the item). + ...OverrideDeclarationFields, }).superRefine((ds, ctx) => { // Measure names must be unique (presentations reference them by name). const measureNames = new Set(); diff --git a/packages/spec/src/ui/page.zod.ts b/packages/spec/src/ui/page.zod.ts index 12d1dfbfa36..551a753cfa2 100644 --- a/packages/spec/src/ui/page.zod.ts +++ b/packages/spec/src/ui/page.zod.ts @@ -22,6 +22,7 @@ import { lazySchema } from '../shared/lazy-schema'; import { strictObject } from '../shared/strict-object'; import { ruleArrayFilterError } from './filter-rule-array'; import { MetadataProtectionFields } from '../kernel/metadata-protection.zod'; +import { OverrideDeclarationFields } from '../kernel/override-declaration.zod'; import { PAGE_TYPE_DEFAULT, misplacedDefaultPageRefusal } from './default-record-page'; /** @@ -2169,6 +2170,8 @@ export const PageSchema = lazySchema(() => strictObject({ // parse — protection metadata lost on round-trip, and a hard 422 the day this // shape closed. ...MetadataProtectionFields, + // ADR-0146 D2 — the override declaration (authored; travels with the item). + ...OverrideDeclarationFields, }) // ADR-0080/0081 + ADR-0078 (completeness): an html/react page with no // `source` is silently inert — fail loudly at author time, never render empty. diff --git a/packages/spec/src/ui/report.zod.ts b/packages/spec/src/ui/report.zod.ts index d992db12d03..dfe1bd03268 100644 --- a/packages/spec/src/ui/report.zod.ts +++ b/packages/spec/src/ui/report.zod.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; import { ProtectionSchema } from '../shared/protection.zod'; import { MetadataProtectionFields } from '../kernel/metadata-protection.zod'; +import { OverrideDeclarationFields } from '../kernel/override-declaration.zod'; import { analyticsCarrierFilter } from './analytics-carrier-filter'; import { ChartConfigSchema } from './chart.zod'; import { DATE_RANGE_DEFAULT_RANGES } from './dashboard.zod'; @@ -697,6 +698,8 @@ export const ReportSchema = lazySchema(() => strictObject({ // ADR-0010 — runtime protection envelope (internal — set by loader). ...MetadataProtectionFields, + // ADR-0146 D2 — the override declaration (authored; travels with the item). + ...OverrideDeclarationFields, }).superRefine((r, ctx) => { // ADR-0021 single-form: a report is dataset-bound. A `joined` report carries diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 05c15dc948a..08e91294228 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -51,6 +51,7 @@ import { z } from 'zod'; import { ProtectionSchema } from '../shared/protection.zod'; import { MetadataProtectionFields } from '../kernel/metadata-protection.zod'; +import { OverrideDeclarationFields } from '../kernel/override-declaration.zod'; import { closedObject, strictObject, strictObjectError, type StrictObjectOptions } from '../shared/strict-object'; import { SnakeCaseIdentifierSchema, SystemIdentifierSchema, QUALIFIED_ITEM_NAME_PATTERN } from '../shared/identifiers.zod'; import { EvaluatedExpressionInputSchema } from '../shared/expression.zod'; @@ -5546,6 +5547,8 @@ function viewItemBaseShape() { ), // ADR-0010 — runtime protection envelope (internal — set by loader). ...MetadataProtectionFields, + // ADR-0146 D2 — the override declaration (authored; travels with the item). + ...OverrideDeclarationFields, }; } @@ -6195,6 +6198,8 @@ function flattenedViewOverlayFields(kind: K) { hidden: retiredKey(VIEW_ITEM_HIDDEN_RETIRED), protection: ProtectionSchema.optional(), ...MetadataProtectionFields, + // ADR-0146 D2 — the override declaration (authored; travels with the item). + ...OverrideDeclarationFields, // Structural guards — a flattened overlay is neither a record nor a container. config: z.undefined().optional(), list: z.undefined().optional(), From 67180a5a1c2b57613ba5262d47742dc72ab0fb5a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 14:59:53 +0000 Subject: [PATCH 02/11] chore(spec): liveness rows for overrides, the export pin and the changeset Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- .changeset/28404-override-declaration.md | 15 +++ ...ckages-export-override-declaration.test.ts | 110 ++++++++++++++++++ packages/spec/liveness/action.json | 17 +++ packages/spec/liveness/app.json | 17 +++ packages/spec/liveness/book.json | 17 +++ packages/spec/liveness/dashboard.json | 17 +++ packages/spec/liveness/dataset.json | 17 +++ packages/spec/liveness/doc.json | 17 +++ packages/spec/liveness/email_template.json | 17 +++ packages/spec/liveness/flow.json | 17 +++ packages/spec/liveness/mapping.json | 17 +++ packages/spec/liveness/page.json | 17 +++ packages/spec/liveness/report.json | 17 +++ packages/spec/liveness/state-counts/action.md | 2 +- packages/spec/liveness/state-counts/app.md | 2 +- packages/spec/liveness/state-counts/book.md | 2 +- .../spec/liveness/state-counts/dashboard.md | 2 +- .../spec/liveness/state-counts/dataset.md | 2 +- packages/spec/liveness/state-counts/doc.md | 2 +- .../liveness/state-counts/email_template.md | 2 +- packages/spec/liveness/state-counts/flow.md | 2 +- .../spec/liveness/state-counts/mapping.md | 2 +- packages/spec/liveness/state-counts/page.md | 2 +- packages/spec/liveness/state-counts/report.md | 2 +- .../spec/liveness/state-counts/translation.md | 2 +- packages/spec/liveness/translation.json | 17 +++ 26 files changed, 341 insertions(+), 12 deletions(-) create mode 100644 .changeset/28404-override-declaration.md create mode 100644 packages/runtime/src/domains/packages-export-override-declaration.test.ts diff --git a/.changeset/28404-override-declaration.md b/.changeset/28404-override-declaration.md new file mode 100644 index 00000000000..f0899fef5b9 --- /dev/null +++ b/.changeset/28404-override-declaration.md @@ -0,0 +1,15 @@ +--- +'@objectstack/spec': minor +--- + +feat(spec): one override declaration for code and Studio — `overrides: { package, basedOn }`, the `.custom` pairing helpers, the closed overridable-type list, the `__c` reservation and the base-field display whitelist (ADR-0146) + +Clause-②: yes (widening) +Metadata-protocol: added + +A customer's change to an installed package lives in its paired `.custom` package (ADR-0146). This release declares the shared definitions every later door reads, so a developer's `hotcrm.custom` in a repository and an administrator's Studio customization write the same thing: + +- **The override declaration.** `view`, `dashboard`, `report`, `translation`, `email_template`, `page`, `app`, `dataset`, `doc`, `book`, `mapping`, `flow` and `action` items accept an optional `overrides: { package, basedOn }`: this item replaces the base package's same-named item whole. `package` names the base (never a `.custom` id); `basedOn` is recorded and never judged. The key survives `defineStack()`, the metadata save and the package export. No door acts on it yet: the write gate and the effective read are separate changes. +- **The pair's identity** (`@objectstack/spec/kernel`): `CUSTOMIZATION_PACKAGE_SUFFIX` (`.custom`), `customizationPackageId`, `customizationBasePackageId`, `isCustomizationPackage`, `isCustomizationPairOf`, `customizationNamespace` (`_custom`; a base namespace longer than 13 characters gets none, with the named reason `base_namespace_too_long`, never a truncation) and `customizationDependencyRange` (`^` of the base version). Each derivation answers `{ ok: true, value }` or `{ ok: false, reason }`. +- **The overridable-type list.** `OVERRIDE_TYPE_CLASSIFICATION` classifies every metadata type as `overridable`, `extend_only` (`object`) or `excluded` (`permission`, `position`, `capability`, the code-only types and others), with `flow`'s and `action`'s preconditions carried as data. `OVERRIDABLE_METADATA_TYPES` and `isOverridableMetadataType` read it. `seed`, `datasource`, `tool` and `skill` are `excluded` pending a ruling. +- **Customer extension fields** (`@objectstack/spec/data`): `EXTENSION_FIELD_SUFFIX` (`__c`), `isExtensionFieldName`, and `BASE_FIELD_DISPLAY_WHITELIST`: the base-field properties an extension may change (`label`, `description`, `inlineHelpText`, `placeholder`, and an option's `label`). Enforcing both is a separate change. diff --git a/packages/runtime/src/domains/packages-export-override-declaration.test.ts b/packages/runtime/src/domains/packages-export-override-declaration.test.ts new file mode 100644 index 00000000000..70eb4c95fb0 --- /dev/null +++ b/packages/runtime/src/domains/packages-export-override-declaration.test.ts @@ -0,0 +1,110 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * ADR-0146 D8 item 3 — "Export carries the declaration": `GET + * /packages/.custom/export` carries each overriding item with its + * `overrides` block, and the exported body re-parses through the item's own + * schema with the block intact, so the package travels to another environment + * still saying what it overrides. + * + * The export strips provenance at the top level of each item (every + * `_`-prefixed key and a fixed bookkeeping set). This file pins that the + * declaration, an AUTHORED key, is not in that strip: measured through the + * real door over a protocol double, never inferred from the key's spelling. + * + * The driver is the sibling package-door tests' (`packages-capability-gate.test.ts`): + * `HttpDispatcher` over a fake kernel whose services are doubles. + */ + +import { describe, it, expect, vi } from 'vitest'; +import { FlowSchema } from '@objectstack/spec/automation'; +import { AssembledViewArtifactSchema } from '@objectstack/spec/ui'; +import { customizationPackageId } from '@objectstack/spec/kernel'; +import { HttpDispatcher } from '../http-dispatcher.js'; + +const BASE = 'com.acme.crm'; +const PAIR = (() => { + const derived = customizationPackageId(BASE); + if (!derived.ok) throw new Error('fixture base must have a pair'); + return derived.value; +})(); +const DECLARATION = { package: BASE, basedOn: '3.2.0' }; + +/** What the protocol serves for the pair: one overriding flow and one overriding view item. */ +const SERVED: Record>> = { + flow: [{ + name: 'crm_lead_assign', + label: 'Assign leads', + type: 'autolaunched', + nodes: [], + edges: [], + overrides: DECLARATION, + _packageId: PAIR, + _provenance: 'org', + }], + view: [{ + name: 'crm_account.all', + object: 'crm_account', + viewKind: 'list', + label: 'All accounts', + config: { type: 'grid', columns: ['name'], data: { provider: 'object', object: 'crm_account' } }, + overrides: DECLARATION, + _packageId: PAIR, + }], +}; + +function makeDoor() { + const getMetaItems = vi.fn(async (req: { type: string; packageId: string }) => ({ + items: req.packageId === PAIR ? structuredClone(SERVED[req.type] ?? []) : [], + })); + const registry = { + getAllPackages: vi.fn().mockReturnValue([]), + getPackage: vi.fn((id: string) => (id === PAIR ? { id, manifest: { id, name: 'crm_custom', version: '1.0.0' } } : undefined)), + }; + const kernel: any = { + context: { + getService: (name: string) => + name === 'objectql' ? { registry } : name === 'protocol' ? { getMetaItems } : null, + }, + }; + return { dispatcher: new HttpDispatcher(kernel), getMetaItems }; +} + +const reader = (): any => ({ + request: {}, + environmentId: 'platform', + executionContext: { userId: 'u_admin', isSystem: false, systemPermissions: ['studio.access'] }, +}); + +async function exportPair(): Promise> { + const { dispatcher } = makeDoor(); + const r = await dispatcher.handlePackages(`/${encodeURIComponent(PAIR)}/export`, 'GET', undefined, {}, reader()); + expect(r.response?.status).toBe(200); + return r.response?.body?.data; +} + +describe('GET /packages/.custom/export carries the override declaration (ADR-0146 D8.3)', () => { + it('an overriding flow is exported with its `overrides` block, and its provenance stripped', async () => { + const manifest = await exportPair(); + const flow = manifest.flows?.[0]; + expect(flow?.overrides).toEqual(DECLARATION); + expect(flow).not.toHaveProperty('_packageId'); + expect(flow).not.toHaveProperty('_provenance'); + }); + + it('the exported flow re-parses through FlowSchema with the declaration intact', async () => { + const manifest = await exportPair(); + const reparsed = FlowSchema.safeParse(manifest.flows?.[0]); + expect(reparsed.success, JSON.stringify(reparsed.error?.issues)).toBe(true); + expect((reparsed.data as { overrides?: unknown }).overrides).toEqual(DECLARATION); + }); + + it('an overriding view item travels in `viewItems:` with its block, and re-parses there', async () => { + const manifest = await exportPair(); + const item = manifest.viewItems?.[0]; + expect(item?.overrides).toEqual(DECLARATION); + const reparsed = AssembledViewArtifactSchema.safeParse(item); + expect(reparsed.success, JSON.stringify(reparsed.error?.issues)).toBe(true); + expect((reparsed.data as { overrides?: unknown }).overrides).toEqual(DECLARATION); + }); +}); diff --git a/packages/spec/liveness/action.json b/packages/spec/liveness/action.json index 719b74620df..b4120f3b1cd 100644 --- a/packages/spec/liveness/action.json +++ b/packages/spec/liveness/action.json @@ -252,6 +252,23 @@ "status": "dead", "verifiedAt": "2026-09-28", "note": "REMOVED 2026-09-28 (#20323, ADR-0049 enforce-or-remove) — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-18 conversion action-aria-removed. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); write the accessible name as the action's required `label`, and put a region name in the placing node's `aria` block (`page.components[].aria` or the list view `aria`). The `live` grade this row replaces rested on an uncited note — 「PARTIAL — honored by a few objectui renderers, not the core action buttons/menus」 — with no reader behind it: re-measured at the `.objectui-sha` pin f8a9d0fb05, none of the action surfaces (action:button/icon/menu/group/bar, the grid row and bulk action menus, record:quick_actions, the declared-actions bar) reads an action's `aria`; the only `schema.aria` readers there are the placing nodes' own blocks (the record:* page components, the list view, element:button's props). Measured at objectui @f8a9d0fb05 (moved out of the packages/spec/src/ui/action.zod.ts tombstone comment, #23434): none of the surfaces that render an action — `action:button`, `action:icon`, `action:menu`, `action:group`, `action:bar`, the grid's row and bulk action menus, `record:quick_actions`, the declared-actions bar — reads an action's `aria`; the only `schema.aria` readers are the placing nodes' own blocks." + }, + "overrides": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "PLANNED RECORD (ADR-0146 D2, the spec card N-08): the override declaration `overrides: { package, basedOn }`, one field set (`OverrideDeclarationFields`, packages/spec/src/kernel/override-declaration.zod.ts) spread on the thirteen types `OVERRIDE_TYPE_CLASSIFICATION` classifies `overridable`. CARRIED today, measured: it survives the metadata save's schema parse, `defineStack()` and `GET /packages/:id/export` (packages/spec/src/kernel/override-declaration.test.ts; packages/runtime/src/domains/packages-export-override-declaration.test.ts). ACTED ON by nothing yet: the write gate that admits a declared override from the paired `.custom` and refuses an undeclared same-named item, and the effective read that serves the declared document over the base's, are later cards of ADR-0146's Rollout (P1). Flip to `live` on the commit that lands the reader, citing it.", + "children": { + "package": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row): the base package id the declaring item replaces an item of. The pair predicate the write gate will read is `isCustomizationPairOf` (packages/spec/src/kernel/customization-package.ts). The schema refuses a `.custom` id here at parse." + }, + "basedOn": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row). RECORDED, NEVER JUDGED by contract (ADR-0146 D2, 'Decided here' item 2): no door compares it with the installed base, produces an upgrade diff from it, or refuses on it. Its whole reading is a person's (Studio's override chip, the export); it stays `planned` until that reader lands, and must never gain a gate." + } + } } } } diff --git a/packages/spec/liveness/app.json b/packages/spec/liveness/app.json index e5758b3eb9f..63143a740ce 100644 --- a/packages/spec/liveness/app.json +++ b/packages/spec/liveness/app.json @@ -331,6 +331,23 @@ "verifiedAt": "2026-09-27", "evidence": "packages/rest/src/meta-item-read-gate.ts#filterAppForUserWithReason (`if (item._unpublished === true && !sysPerms.has('studio.access') && !sysPerms.has('setup.access'))` — the ADR-0045 publish gate, which judges THIS key and deliberately not `hidden`); packages/runtime/src/domains/packages.ts#handlePackagesRequest (POST /packages/:id/publish-drafts flips it to `false` — never deletes it, because ADR-0045 §3 keeps publish and unpublish symmetric)", "note": "MACHINE-MANAGED publish gate (ADR-0045 §3, amended 2026-08-09 / #4829) — never authored: written by the AI additive-materialization path (cloud) and cleared by POST /packages/:id/publish-drafts. SERVER-enforced: filterAppForUser withholds an unpublished app from every metadata response except a builder's (studio/setup access), for direct-URL preview. Declared on AppSchema rather than omitted because the write path validates against that schema (saveMetaItem → 422; Registry.validate('app') → AppSchema.parse), so the flip itself would be unwritable otherwise. Stored pre-amendment rows carrying `hidden: true` are rewritten here by the ADR-0087 conversion `app-hidden-to-unpublished`. 2026-08-28: RE-ANCHORED (commit 8f10a79f7) and REPOINTED, both legs — `rest-server.ts:2669` had rotted onto a docblock about a diagnostic view being neither translated nor cached, ~480 lines below the gate, and `packages.ts:245` onto the domain-wide anonymous-deny floor, ~270 lines above the flip. Re-closed by hand against 8cb96ec41." + }, + "overrides": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "PLANNED RECORD (ADR-0146 D2, the spec card N-08): the override declaration `overrides: { package, basedOn }`, one field set (`OverrideDeclarationFields`, packages/spec/src/kernel/override-declaration.zod.ts) spread on the thirteen types `OVERRIDE_TYPE_CLASSIFICATION` classifies `overridable`. CARRIED today, measured: it survives the metadata save's schema parse, `defineStack()` and `GET /packages/:id/export` (packages/spec/src/kernel/override-declaration.test.ts; packages/runtime/src/domains/packages-export-override-declaration.test.ts). ACTED ON by nothing yet: the write gate that admits a declared override from the paired `.custom` and refuses an undeclared same-named item, and the effective read that serves the declared document over the base's, are later cards of ADR-0146's Rollout (P1). Flip to `live` on the commit that lands the reader, citing it.", + "children": { + "package": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row): the base package id the declaring item replaces an item of. The pair predicate the write gate will read is `isCustomizationPairOf` (packages/spec/src/kernel/customization-package.ts). The schema refuses a `.custom` id here at parse." + }, + "basedOn": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row). RECORDED, NEVER JUDGED by contract (ADR-0146 D2, 'Decided here' item 2): no door compares it with the installed base, produces an upgrade diff from it, or refuses on it. Its whole reading is a person's (Studio's override chip, the export); it stays `planned` until that reader lands, and must never gain a gate." + } + } } } } diff --git a/packages/spec/liveness/book.json b/packages/spec/liveness/book.json index bc74e1eebac..1cab88c77c8 100644 --- a/packages/spec/liveness/book.json +++ b/packages/spec/liveness/book.json @@ -90,6 +90,23 @@ } }, "note": "Drilled because `translations` diverges (dead) from its six live siblings." + }, + "overrides": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "PLANNED RECORD (ADR-0146 D2, the spec card N-08): the override declaration `overrides: { package, basedOn }`, one field set (`OverrideDeclarationFields`, packages/spec/src/kernel/override-declaration.zod.ts) spread on the thirteen types `OVERRIDE_TYPE_CLASSIFICATION` classifies `overridable`. CARRIED today, measured: it survives the metadata save's schema parse, `defineStack()` and `GET /packages/:id/export` (packages/spec/src/kernel/override-declaration.test.ts; packages/runtime/src/domains/packages-export-override-declaration.test.ts). ACTED ON by nothing yet: the write gate that admits a declared override from the paired `.custom` and refuses an undeclared same-named item, and the effective read that serves the declared document over the base's, are later cards of ADR-0146's Rollout (P1). Flip to `live` on the commit that lands the reader, citing it.", + "children": { + "package": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row): the base package id the declaring item replaces an item of. The pair predicate the write gate will read is `isCustomizationPairOf` (packages/spec/src/kernel/customization-package.ts). The schema refuses a `.custom` id here at parse." + }, + "basedOn": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row). RECORDED, NEVER JUDGED by contract (ADR-0146 D2, 'Decided here' item 2): no door compares it with the installed base, produces an upgrade diff from it, or refuses on it. Its whole reading is a person's (Studio's override chip, the export); it stays `planned` until that reader lands, and must never gain a gate." + } + } } } } diff --git a/packages/spec/liveness/dashboard.json b/packages/spec/liveness/dashboard.json index 04a30a188a1..ff8a42c4c60 100644 --- a/packages/spec/liveness/dashboard.json +++ b/packages/spec/liveness/dashboard.json @@ -253,6 +253,23 @@ "evidenceScope": "in-repo", "evidence": "packages/rest/src/meta-item-read-gate.ts#holdsRequiredPermissions is the ONE predicate the `/meta` read gate asks of every carrier of the key (an app, its navigation entries, a list view, a dashboard): the caller must hold every capability named, and an absent or empty list is no gate. The `dashboard` arm of packages/rest/src/meta-item-read-gate.ts#createMetaItemReadGate refuses a dashboard whose capabilities the caller does not all hold, 403 PERMISSION_DENIED (the app's whole refusal), on every door and before the ADR-0057 D10 widget gate — so a dashboard's direct URL is refused by the server — and packages/rest/src/meta-item-read-gate.ts#createMetaListReadGate leaves it out of `GET /meta/dashboard`. Pinned per audience (holder served, non-holder not served, no key served to all) on every `/meta/:type/:name` door by packages/rest/src/meta-alternate-door-read-gates.test.ts, and across both transports by packages/runtime/src/domains/meta-item-read-gate-parity.test.ts and packages/runtime/src/domains/meta-list-read-gate-parity.test.ts", "note": "planned → live 2026-10-10 (#22639, the rest half of the ruling of record 6095014058, letter A): the `/meta` read gate enforces the key on the list read and the by-name read of `dashboard`. The prior PLANNED note (#22611) is superseded and its flip instruction discharged in this same edit: `authorWarn` / `authorHint` dropped, and the describe's not-enforced clause deleted (packages/spec/src/ui/audience-required-permissions.ts). Boundaries: the refusal is WHOLE and reaches an author as anyone (the app's whole refusal, ruling 5856774816); a caller with no signed-in user is not gated (the app arm's rule — an anonymous caller is refused before any `/meta` read of this type). What the console shows a refused direct URL is objectui's half, filed when this one is accepted. The lint resolves the capability names (packages/lint/src/validate-capability-references.ts), since a misspelled one now hides the dashboard from everyone." + }, + "overrides": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "PLANNED RECORD (ADR-0146 D2, the spec card N-08): the override declaration `overrides: { package, basedOn }`, one field set (`OverrideDeclarationFields`, packages/spec/src/kernel/override-declaration.zod.ts) spread on the thirteen types `OVERRIDE_TYPE_CLASSIFICATION` classifies `overridable`. CARRIED today, measured: it survives the metadata save's schema parse, `defineStack()` and `GET /packages/:id/export` (packages/spec/src/kernel/override-declaration.test.ts; packages/runtime/src/domains/packages-export-override-declaration.test.ts). ACTED ON by nothing yet: the write gate that admits a declared override from the paired `.custom` and refuses an undeclared same-named item, and the effective read that serves the declared document over the base's, are later cards of ADR-0146's Rollout (P1). Flip to `live` on the commit that lands the reader, citing it.", + "children": { + "package": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row): the base package id the declaring item replaces an item of. The pair predicate the write gate will read is `isCustomizationPairOf` (packages/spec/src/kernel/customization-package.ts). The schema refuses a `.custom` id here at parse." + }, + "basedOn": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row). RECORDED, NEVER JUDGED by contract (ADR-0146 D2, 'Decided here' item 2): no door compares it with the installed base, produces an upgrade diff from it, or refuses on it. Its whole reading is a person's (Studio's override chip, the export); it stays `planned` until that reader lands, and must never gain a gate." + } + } } } } diff --git a/packages/spec/liveness/dataset.json b/packages/spec/liveness/dataset.json index 257564b90ba..febd6afc08c 100644 --- a/packages/spec/liveness/dataset.json +++ b/packages/spec/liveness/dataset.json @@ -161,6 +161,23 @@ } } } + }, + "overrides": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "PLANNED RECORD (ADR-0146 D2, the spec card N-08): the override declaration `overrides: { package, basedOn }`, one field set (`OverrideDeclarationFields`, packages/spec/src/kernel/override-declaration.zod.ts) spread on the thirteen types `OVERRIDE_TYPE_CLASSIFICATION` classifies `overridable`. CARRIED today, measured: it survives the metadata save's schema parse, `defineStack()` and `GET /packages/:id/export` (packages/spec/src/kernel/override-declaration.test.ts; packages/runtime/src/domains/packages-export-override-declaration.test.ts). ACTED ON by nothing yet: the write gate that admits a declared override from the paired `.custom` and refuses an undeclared same-named item, and the effective read that serves the declared document over the base's, are later cards of ADR-0146's Rollout (P1). Flip to `live` on the commit that lands the reader, citing it.", + "children": { + "package": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row): the base package id the declaring item replaces an item of. The pair predicate the write gate will read is `isCustomizationPairOf` (packages/spec/src/kernel/customization-package.ts). The schema refuses a `.custom` id here at parse." + }, + "basedOn": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row). RECORDED, NEVER JUDGED by contract (ADR-0146 D2, 'Decided here' item 2): no door compares it with the installed base, produces an upgrade diff from it, or refuses on it. Its whole reading is a person's (Studio's override chip, the export); it stays `planned` until that reader lands, and must never gain a gate." + } + } } } } diff --git a/packages/spec/liveness/doc.json b/packages/spec/liveness/doc.json index 6a3cfd2f57c..6c1365f7f64 100644 --- a/packages/spec/liveness/doc.json +++ b/packages/spec/liveness/doc.json @@ -49,6 +49,23 @@ "verifiedAt": "2026-09-27", "evidence": "packages/spec/src/system/book.zod.ts#matchesInclude (`Array.isArray(doc.tags) && doc.tags.includes(include.tag)` — the tag branch of `include`); packages/spec/src/system/book.zod.ts#resolveBookTree (the caller that decides membership from it); packages/rest/src/meta-item-read-gate.ts#createMetaBookTreeAnswer (`tags: d.tags` in the tree corpus the route hands `audience.readableTree(book, docs)` — the transport half); packages/rest/src/meta-item-read-gate.ts#resolveBookTree (`resolveBookTree(book, docs, book._packageId, books)` — where that corpus reaches the resolver)", "note": "DECLARED 2026-08-02 (#4509, ADR-0049) — the enforce half of enforce-or-remove. The consumer, the transport and the resolver-side interface all predated this key: matchesInclude compared `doc.tags` against a group's `include: { tag }`, the book route already forwarded `tags: d.tags`, and ResolverDoc declared `tags?: string[]` marked '(P3d; absent today)'. What was missing was one line HERE — DocSchema is strict, so authoring `tags:` was a parse error, every doc reached the resolver with tags undefined, and the `{ tag }` include variant could never match. Removing the variant was the alternative and was rejected: a union member has no clean tombstone (retiredKey covers object keys), so authors would have gotten a bare union error, and it would have discarded working matcher code. Live on arrival — the branch it feeds is reachable the moment a doc carries a tag. 2026-08-28: RE-ANCHORED (commit 8cb96ec41) and REPOINTED — `book.zod.ts:213` is `description?: string;` in the `ResolvedEntry` interface, and the parenthetical claiming it was \"matchesInclude tag branch\" named the right function ~61 lines away; the \"reached from :255 and :281\" hops were prose the ledger never bounded. `rest-server.ts:3218` had rotted onto `this.registerSecurityEndpoints(bp)` — a route-registration call, ~1,700 lines from the corpus that forwards the tags. Re-closed by hand against 93ea19bca." + }, + "overrides": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "PLANNED RECORD (ADR-0146 D2, the spec card N-08): the override declaration `overrides: { package, basedOn }`, one field set (`OverrideDeclarationFields`, packages/spec/src/kernel/override-declaration.zod.ts) spread on the thirteen types `OVERRIDE_TYPE_CLASSIFICATION` classifies `overridable`. CARRIED today, measured: it survives the metadata save's schema parse, `defineStack()` and `GET /packages/:id/export` (packages/spec/src/kernel/override-declaration.test.ts; packages/runtime/src/domains/packages-export-override-declaration.test.ts). ACTED ON by nothing yet: the write gate that admits a declared override from the paired `.custom` and refuses an undeclared same-named item, and the effective read that serves the declared document over the base's, are later cards of ADR-0146's Rollout (P1). Flip to `live` on the commit that lands the reader, citing it.", + "children": { + "package": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row): the base package id the declaring item replaces an item of. The pair predicate the write gate will read is `isCustomizationPairOf` (packages/spec/src/kernel/customization-package.ts). The schema refuses a `.custom` id here at parse." + }, + "basedOn": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row). RECORDED, NEVER JUDGED by contract (ADR-0146 D2, 'Decided here' item 2): no door compares it with the installed base, produces an upgrade diff from it, or refuses on it. Its whole reading is a person's (Studio's override chip, the export); it stays `planned` until that reader lands, and must never gain a gate." + } + } } } } diff --git a/packages/spec/liveness/email_template.json b/packages/spec/liveness/email_template.json index 3aace7ee6e8..102cec516e0 100644 --- a/packages/spec/liveness/email_template.json +++ b/packages/spec/liveness/email_template.json @@ -89,6 +89,23 @@ "evidence": "ui/packages/app-shell/src/views/metadata-admin/ResourceListPage.tsx#defaultColumns:1293 (the Studio metadata list's default `description` column, which line 573 falls back to when a type registers no `listColumns`, and whose cell line 902 reads as `row.item[c.key]`); ui/packages/app-shell/src/views/metadata-admin/default-schemas.ts#registerDefaultMetadataSchemas:331 (`email_template` is registered with a `defaultSchema` and a `fieldOrder` and no `listColumns` of its own)", "note": "Docs-shaped: read by humans in Studio, never by the send path. Until 2026-10-11 it was materialized into `sys_email_template.description`; that projection retired with the rows (ADR-0131 D6/D10), and the Studio list read — measured at the `.objectui-sha` pin 4997995022f8 — is the consumer now (#7131).", "evidenceScope": "in-repo" + }, + "overrides": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "PLANNED RECORD (ADR-0146 D2, the spec card N-08): the override declaration `overrides: { package, basedOn }`, one field set (`OverrideDeclarationFields`, packages/spec/src/kernel/override-declaration.zod.ts) spread on the thirteen types `OVERRIDE_TYPE_CLASSIFICATION` classifies `overridable`. CARRIED today, measured: it survives the metadata save's schema parse, `defineStack()` and `GET /packages/:id/export` (packages/spec/src/kernel/override-declaration.test.ts; packages/runtime/src/domains/packages-export-override-declaration.test.ts). ACTED ON by nothing yet: the write gate that admits a declared override from the paired `.custom` and refuses an undeclared same-named item, and the effective read that serves the declared document over the base's, are later cards of ADR-0146's Rollout (P1). Flip to `live` on the commit that lands the reader, citing it.", + "children": { + "package": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row): the base package id the declaring item replaces an item of. The pair predicate the write gate will read is `isCustomizationPairOf` (packages/spec/src/kernel/customization-package.ts). The schema refuses a `.custom` id here at parse." + }, + "basedOn": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row). RECORDED, NEVER JUDGED by contract (ADR-0146 D2, 'Decided here' item 2): no door compares it with the installed base, produces an upgrade diff from it, or refuses on it. Its whole reading is a person's (Studio's override chip, the export); it stays `planned` until that reader lands, and must never gain a gate." + } + } } } } diff --git a/packages/spec/liveness/flow.json b/packages/spec/liveness/flow.json index 1588c75aaa0..0ec231b2a9e 100644 --- a/packages/spec/liveness/flow.json +++ b/packages/spec/liveness/flow.json @@ -148,6 +148,23 @@ "evidence": "packages/services/service-automation/src/engine.ts" } } + }, + "overrides": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "PLANNED RECORD (ADR-0146 D2, the spec card N-08): the override declaration `overrides: { package, basedOn }`, one field set (`OverrideDeclarationFields`, packages/spec/src/kernel/override-declaration.zod.ts) spread on the thirteen types `OVERRIDE_TYPE_CLASSIFICATION` classifies `overridable`. CARRIED today, measured: it survives the metadata save's schema parse, `defineStack()` and `GET /packages/:id/export` (packages/spec/src/kernel/override-declaration.test.ts; packages/runtime/src/domains/packages-export-override-declaration.test.ts). ACTED ON by nothing yet: the write gate that admits a declared override from the paired `.custom` and refuses an undeclared same-named item, and the effective read that serves the declared document over the base's, are later cards of ADR-0146's Rollout (P1). Flip to `live` on the commit that lands the reader, citing it.", + "children": { + "package": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row): the base package id the declaring item replaces an item of. The pair predicate the write gate will read is `isCustomizationPairOf` (packages/spec/src/kernel/customization-package.ts). The schema refuses a `.custom` id here at parse." + }, + "basedOn": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row). RECORDED, NEVER JUDGED by contract (ADR-0146 D2, 'Decided here' item 2): no door compares it with the installed base, produces an upgrade diff from it, or refuses on it. Its whole reading is a person's (Studio's override chip, the export); it stays `planned` until that reader lands, and must never gain a gate." + } + } } } } diff --git a/packages/spec/liveness/mapping.json b/packages/spec/liveness/mapping.json index bd4c342d96b..151a3752ef1 100644 --- a/packages/spec/liveness/mapping.json +++ b/packages/spec/liveness/mapping.json @@ -95,6 +95,23 @@ } } } + }, + "overrides": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "PLANNED RECORD (ADR-0146 D2, the spec card N-08): the override declaration `overrides: { package, basedOn }`, one field set (`OverrideDeclarationFields`, packages/spec/src/kernel/override-declaration.zod.ts) spread on the thirteen types `OVERRIDE_TYPE_CLASSIFICATION` classifies `overridable`. CARRIED today, measured: it survives the metadata save's schema parse, `defineStack()` and `GET /packages/:id/export` (packages/spec/src/kernel/override-declaration.test.ts; packages/runtime/src/domains/packages-export-override-declaration.test.ts). ACTED ON by nothing yet: the write gate that admits a declared override from the paired `.custom` and refuses an undeclared same-named item, and the effective read that serves the declared document over the base's, are later cards of ADR-0146's Rollout (P1). Flip to `live` on the commit that lands the reader, citing it.", + "children": { + "package": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row): the base package id the declaring item replaces an item of. The pair predicate the write gate will read is `isCustomizationPairOf` (packages/spec/src/kernel/customization-package.ts). The schema refuses a `.custom` id here at parse." + }, + "basedOn": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row). RECORDED, NEVER JUDGED by contract (ADR-0146 D2, 'Decided here' item 2): no door compares it with the installed base, produces an upgrade diff from it, or refuses on it. Its whole reading is a person's (Studio's override chip, the export); it stays `planned` until that reader lands, and must never gain a gate." + } + } } } } diff --git a/packages/spec/liveness/page.json b/packages/spec/liveness/page.json index 127601affc5..33fc213648f 100644 --- a/packages/spec/liveness/page.json +++ b/packages/spec/liveness/page.json @@ -241,6 +241,23 @@ "slots": { "status": "live", "note": "slotted-page override map (header/actions/alerts/highlights/details/discussion; the tabs slot is a tombstone) — objectui plugin-detail; Preview render in objectui#1948. Verified in the Studio Design slot editor during the dogfood." + }, + "overrides": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "PLANNED RECORD (ADR-0146 D2, the spec card N-08): the override declaration `overrides: { package, basedOn }`, one field set (`OverrideDeclarationFields`, packages/spec/src/kernel/override-declaration.zod.ts) spread on the thirteen types `OVERRIDE_TYPE_CLASSIFICATION` classifies `overridable`. CARRIED today, measured: it survives the metadata save's schema parse, `defineStack()` and `GET /packages/:id/export` (packages/spec/src/kernel/override-declaration.test.ts; packages/runtime/src/domains/packages-export-override-declaration.test.ts). ACTED ON by nothing yet: the write gate that admits a declared override from the paired `.custom` and refuses an undeclared same-named item, and the effective read that serves the declared document over the base's, are later cards of ADR-0146's Rollout (P1). Flip to `live` on the commit that lands the reader, citing it.", + "children": { + "package": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row): the base package id the declaring item replaces an item of. The pair predicate the write gate will read is `isCustomizationPairOf` (packages/spec/src/kernel/customization-package.ts). The schema refuses a `.custom` id here at parse." + }, + "basedOn": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row). RECORDED, NEVER JUDGED by contract (ADR-0146 D2, 'Decided here' item 2): no door compares it with the installed base, produces an upgrade diff from it, or refuses on it. Its whole reading is a person's (Studio's override chip, the export); it stays `planned` until that reader lands, and must never gain a gate." + } + } } } } diff --git a/packages/spec/liveness/report.json b/packages/spec/liveness/report.json index 3a391b9c913..b39d4e0da7f 100644 --- a/packages/spec/liveness/report.json +++ b/packages/spec/liveness/report.json @@ -126,6 +126,23 @@ "status": "live", "verifiedAt": "2026-09-27", "note": "objectui: joined-report sub-reports, each dataset-bound — DatasetReportRenderer joined branch (:1462-1524 at the `.objectui-sha` pin f8a9d0fb) reads a block's dataset/rows/columns/values/runtimeFilter/order/type/label/description/name and draws it as a table. A block has NO chart: `chart` was removed from JoinedReportBlockSchema in #20161 because no renderer ever read it (`block.chart`: zero hits over objectui packages/ at the pin, against a lit `block.runtimeFilter` control). A block with no `dataset` (packages/spec/src/ui/report.zod.ts joinedBlockDatasetRequired, #21702; read point moved out of spec source, #23434): at objectui @ab1879721595 packages/plugin-report/src/DatasetReportRenderer.tsx#DatasetReportRenderer the joined branch hands each block's dataset to its table as `String(block.dataset ?? '')` and the table's query hook goes idle on an empty name; a report whose blocks all lack one fails objectui @ab1879721595 packages/plugin-report/src/DatasetReportRenderer.tsx#isDatasetReport and falls through to the pre-9.0 presentation bridge, which queries nothing." + }, + "overrides": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "PLANNED RECORD (ADR-0146 D2, the spec card N-08): the override declaration `overrides: { package, basedOn }`, one field set (`OverrideDeclarationFields`, packages/spec/src/kernel/override-declaration.zod.ts) spread on the thirteen types `OVERRIDE_TYPE_CLASSIFICATION` classifies `overridable`. CARRIED today, measured: it survives the metadata save's schema parse, `defineStack()` and `GET /packages/:id/export` (packages/spec/src/kernel/override-declaration.test.ts; packages/runtime/src/domains/packages-export-override-declaration.test.ts). ACTED ON by nothing yet: the write gate that admits a declared override from the paired `.custom` and refuses an undeclared same-named item, and the effective read that serves the declared document over the base's, are later cards of ADR-0146's Rollout (P1). Flip to `live` on the commit that lands the reader, citing it.", + "children": { + "package": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row): the base package id the declaring item replaces an item of. The pair predicate the write gate will read is `isCustomizationPairOf` (packages/spec/src/kernel/customization-package.ts). The schema refuses a `.custom` id here at parse." + }, + "basedOn": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row). RECORDED, NEVER JUDGED by contract (ADR-0146 D2, 'Decided here' item 2): no door compares it with the installed base, produces an upgrade diff from it, or refuses on it. Its whole reading is a person's (Studio's override chip, the export); it stays `planned` until that reader lands, and must never gain a gate." + } + } } } } diff --git a/packages/spec/liveness/state-counts/action.md b/packages/spec/liveness/state-counts/action.md index d44ea8eb3e9..e54afd341ee 100644 --- a/packages/spec/liveness/state-counts/action.md +++ b/packages/spec/liveness/state-counts/action.md @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them. | Type | live | exp | elsewhere | dead | planned | classified | |---|---|---|---|---|---|---| -| `action` | 50 | 0 | 0 | 1 | 0 | 51 | +| `action` | 50 | 0 | 0 | 1 | 2 | 53 | diff --git a/packages/spec/liveness/state-counts/app.md b/packages/spec/liveness/state-counts/app.md index 80ea528a4fb..0e3986f5688 100644 --- a/packages/spec/liveness/state-counts/app.md +++ b/packages/spec/liveness/state-counts/app.md @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them. | Type | live | exp | elsewhere | dead | planned | classified | |---|---|---|---|---|---|---| -| `app` | 51 | 0 | 0 | 0 | 4 | 55 | +| `app` | 51 | 0 | 0 | 0 | 6 | 57 | diff --git a/packages/spec/liveness/state-counts/book.md b/packages/spec/liveness/state-counts/book.md index ec8aac4381d..5daef479ace 100644 --- a/packages/spec/liveness/state-counts/book.md +++ b/packages/spec/liveness/state-counts/book.md @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them. | Type | live | exp | elsewhere | dead | planned | classified | |---|---|---|---|---|---|---| -| `book` | 20 | 0 | 0 | 1 | 0 | 21 | +| `book` | 20 | 0 | 0 | 1 | 2 | 23 | diff --git a/packages/spec/liveness/state-counts/dashboard.md b/packages/spec/liveness/state-counts/dashboard.md index fa8b7e19b60..7591516931c 100644 --- a/packages/spec/liveness/state-counts/dashboard.md +++ b/packages/spec/liveness/state-counts/dashboard.md @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them. | Type | live | exp | elsewhere | dead | planned | classified | |---|---|---|---|---|---|---| -| `dashboard` | 43 | 0 | 0 | 6 | 0 | 49 | +| `dashboard` | 43 | 0 | 0 | 6 | 2 | 51 | diff --git a/packages/spec/liveness/state-counts/dataset.md b/packages/spec/liveness/state-counts/dataset.md index ffaf19bda9b..605c97366fa 100644 --- a/packages/spec/liveness/state-counts/dataset.md +++ b/packages/spec/liveness/state-counts/dataset.md @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them. | Type | live | exp | elsewhere | dead | planned | classified | |---|---|---|---|---|---|---| -| `dataset` | 32 | 0 | 0 | 0 | 0 | 32 | +| `dataset` | 32 | 0 | 0 | 0 | 2 | 34 | diff --git a/packages/spec/liveness/state-counts/doc.md b/packages/spec/liveness/state-counts/doc.md index 314a315a461..827b54dba27 100644 --- a/packages/spec/liveness/state-counts/doc.md +++ b/packages/spec/liveness/state-counts/doc.md @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them. | Type | live | exp | elsewhere | dead | planned | classified | |---|---|---|---|---|---|---| -| `doc` | 15 | 0 | 0 | 0 | 0 | 15 | +| `doc` | 15 | 0 | 0 | 0 | 2 | 17 | diff --git a/packages/spec/liveness/state-counts/email_template.md b/packages/spec/liveness/state-counts/email_template.md index df1fc9eb239..3d9d0a6cf8f 100644 --- a/packages/spec/liveness/state-counts/email_template.md +++ b/packages/spec/liveness/state-counts/email_template.md @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them. | Type | live | exp | elsewhere | dead | planned | classified | |---|---|---|---|---|---|---| -| `email_template` | 19 | 0 | 0 | 2 | 0 | 21 | +| `email_template` | 19 | 0 | 0 | 2 | 2 | 23 | diff --git a/packages/spec/liveness/state-counts/flow.md b/packages/spec/liveness/state-counts/flow.md index b72830085ef..52cafd01c7c 100644 --- a/packages/spec/liveness/state-counts/flow.md +++ b/packages/spec/liveness/state-counts/flow.md @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them. | Type | live | exp | elsewhere | dead | planned | classified | |---|---|---|---|---|---|---| -| `flow` | 36 | 0 | 0 | 0 | 0 | 36 | +| `flow` | 36 | 0 | 0 | 0 | 2 | 38 | diff --git a/packages/spec/liveness/state-counts/mapping.md b/packages/spec/liveness/state-counts/mapping.md index 9543eb8a502..eed9ad72705 100644 --- a/packages/spec/liveness/state-counts/mapping.md +++ b/packages/spec/liveness/state-counts/mapping.md @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them. | Type | live | exp | elsewhere | dead | planned | classified | |---|---|---|---|---|---|---| -| `mapping` | 20 | 0 | 0 | 0 | 0 | 20 | +| `mapping` | 20 | 0 | 0 | 0 | 2 | 22 | diff --git a/packages/spec/liveness/state-counts/page.md b/packages/spec/liveness/state-counts/page.md index d66b358c1b2..2bbef13717d 100644 --- a/packages/spec/liveness/state-counts/page.md +++ b/packages/spec/liveness/state-counts/page.md @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them. | Type | live | exp | elsewhere | dead | planned | classified | |---|---|---|---|---|---|---| -| `page` | 34 | 0 | 0 | 1 | 7 | 42 | +| `page` | 34 | 0 | 0 | 1 | 9 | 44 | diff --git a/packages/spec/liveness/state-counts/report.md b/packages/spec/liveness/state-counts/report.md index c7292b2f903..f45e8716554 100644 --- a/packages/spec/liveness/state-counts/report.md +++ b/packages/spec/liveness/state-counts/report.md @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them. | Type | live | exp | elsewhere | dead | planned | classified | |---|---|---|---|---|---|---| -| `report` | 39 | 0 | 0 | 1 | 1 | 41 | +| `report` | 39 | 0 | 0 | 1 | 3 | 43 | diff --git a/packages/spec/liveness/state-counts/translation.md b/packages/spec/liveness/state-counts/translation.md index 368254b51fd..4e45674bce4 100644 --- a/packages/spec/liveness/state-counts/translation.md +++ b/packages/spec/liveness/state-counts/translation.md @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them. | Type | live | exp | elsewhere | dead | planned | classified | |---|---|---|---|---|---|---| -| `translation` | 42 | 0 | 0 | 0 | 0 | 42 | +| `translation` | 42 | 0 | 0 | 0 | 2 | 44 | diff --git a/packages/spec/liveness/translation.json b/packages/spec/liveness/translation.json index 7f1fa28f5ae..7a6c82fc4fd 100644 --- a/packages/spec/liveness/translation.json +++ b/packages/spec/liveness/translation.json @@ -335,6 +335,23 @@ "verifiedAt": "2026-08-01", "evidence": "ui/apps/console/src/pages/settings/useSettingsLabel.ts (:104 at objectui @940ba24)", "note": "cross-namespace Settings chrome strings (source badges by resolution layer); the client scans every namespace carrying a settingsCommon block." + }, + "overrides": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "PLANNED RECORD (ADR-0146 D2, the spec card N-08): the override declaration `overrides: { package, basedOn }`, one field set (`OverrideDeclarationFields`, packages/spec/src/kernel/override-declaration.zod.ts) spread on the thirteen types `OVERRIDE_TYPE_CLASSIFICATION` classifies `overridable`. CARRIED today, measured: it survives the metadata save's schema parse, `defineStack()` and `GET /packages/:id/export` (packages/spec/src/kernel/override-declaration.test.ts; packages/runtime/src/domains/packages-export-override-declaration.test.ts). ACTED ON by nothing yet: the write gate that admits a declared override from the paired `.custom` and refuses an undeclared same-named item, and the effective read that serves the declared document over the base's, are later cards of ADR-0146's Rollout (P1). Flip to `live` on the commit that lands the reader, citing it.", + "children": { + "package": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row): the base package id the declaring item replaces an item of. The pair predicate the write gate will read is `isCustomizationPairOf` (packages/spec/src/kernel/customization-package.ts). The schema refuses a `.custom` id here at parse." + }, + "basedOn": { + "status": "planned", + "verifiedAt": "2026-10-31", + "note": "Planned with its container `overrides` (see that row). RECORDED, NEVER JUDGED by contract (ADR-0146 D2, 'Decided here' item 2): no door compares it with the installed base, produces an upgrade diff from it, or refuses on it. Its whole reading is a person's (Studio's override chip, the export); it stays `planned` until that reader lands, and must never gain a gate." + } + } } } } From 5c2f55ddc41d698f4df301f165e934150485f695 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 15:09:27 +0000 Subject: [PATCH 03/11] chore(spec): regenerate the report liveness count after the merge Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- packages/spec/liveness/state-counts/report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/spec/liveness/state-counts/report.md b/packages/spec/liveness/state-counts/report.md index f45e8716554..c4b052c5696 100644 --- a/packages/spec/liveness/state-counts/report.md +++ b/packages/spec/liveness/state-counts/report.md @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them. | Type | live | exp | elsewhere | dead | planned | classified | |---|---|---|---|---|---|---| -| `report` | 39 | 0 | 0 | 1 | 3 | 43 | +| `report` | 40 | 0 | 0 | 1 | 2 | 43 | From 2b51218a791a18f6b2fe2fb2a2ad0061472789df Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 15:11:52 +0000 Subject: [PATCH 04/11] fix(spec): refuse a .custom overrides.package with a published pattern, not a refinement Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- .../spec/src/kernel/override-declaration.test.ts | 16 +++++++++++++++- .../spec/src/kernel/override-declaration.zod.ts | 11 ++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/spec/src/kernel/override-declaration.test.ts b/packages/spec/src/kernel/override-declaration.test.ts index 4cb42f9935b..ccd3210637d 100644 --- a/packages/spec/src/kernel/override-declaration.test.ts +++ b/packages/spec/src/kernel/override-declaration.test.ts @@ -15,6 +15,7 @@ */ import { describe, expect, it } from 'vitest'; +import { z } from 'zod'; import { OverrideDeclarationSchema, OverrideDeclarationFields, CUSTOMIZATION_PACKAGE_SUFFIX } from './override-declaration.zod'; import { OVERRIDE_TYPE_CLASSIFICATION, OVERRIDABLE_METADATA_TYPES } from './customization-package'; import { getMetadataTypeSchema } from './metadata-type-schemas'; @@ -62,7 +63,20 @@ describe('OverrideDeclarationSchema — the shape', () => { it(`refuses a \`${CUSTOMIZATION_PACKAGE_SUFFIX}\` package — the declaration names the BASE, never a pair`, () => { const r = OverrideDeclarationSchema.safeParse({ package: `com.acme.crm${CUSTOMIZATION_PACKAGE_SUFFIX}`, basedOn: '3.2.0' }); expect(r.success).toBe(false); - expect(issuesOf(r)).toContainEqual(expect.objectContaining({ code: 'custom', path: 'package' })); + expect(issuesOf(r)).toContainEqual(expect.objectContaining({ code: 'invalid_format', path: 'package' })); + }); + + it('accepts an id that merely contains the suffix\'s letters', () => { + for (const id of ['com.acme.customx', 'com.acme.custom-crm', 'custom']) { + expect(OverrideDeclarationSchema.safeParse({ package: id, basedOn: '1.0.0' }).success, id).toBe(true); + } + }); + + it('publishes the refusal into the JSON Schema as a pattern (no dropped refinement)', () => { + const json = z.toJSONSchema(OverrideDeclarationSchema) as { properties: { package: { pattern?: string } } }; + const pattern = new RegExp(json.properties.package.pattern ?? ''); + expect(pattern.test('com.acme.crm')).toBe(true); + expect(pattern.test(`com.acme.crm${CUSTOMIZATION_PACKAGE_SUFFIX}`)).toBe(false); }); it('describes basedOn as recorded and never judged (ADR-0146 D2)', () => { diff --git a/packages/spec/src/kernel/override-declaration.zod.ts b/packages/spec/src/kernel/override-declaration.zod.ts index 4285ddf5058..27c44a58eda 100644 --- a/packages/spec/src/kernel/override-declaration.zod.ts +++ b/packages/spec/src/kernel/override-declaration.zod.ts @@ -55,6 +55,12 @@ import { strictObject } from '../shared/strict-object'; */ export const CUSTOMIZATION_PACKAGE_SUFFIX = '.custom'; +/** {@link CUSTOMIZATION_PACKAGE_SUFFIX}, escaped for a regular expression. */ +const ESCAPED_CUSTOMIZATION_PACKAGE_SUFFIX = CUSTOMIZATION_PACKAGE_SUFFIX.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +/** Any non-empty string that does not end in {@link CUSTOMIZATION_PACKAGE_SUFFIX}. */ +const NOT_A_CUSTOMIZATION_PACKAGE_ID = new RegExp(`^(?![\\s\\S]*${ESCAPED_CUSTOMIZATION_PACKAGE_SUFFIX}$)[\\s\\S]+$`); + /** * The override declaration's body: which base package's item this item * replaces, and which version of that item the author started from. @@ -71,7 +77,10 @@ export const OverrideDeclarationSchema = strictObject({ }, { package: z.string() .min(1) - .refine((id) => !id.endsWith(CUSTOMIZATION_PACKAGE_SUFFIX), { + // A pattern, not a `.refine()`: a pattern publishes into the JSON Schema + // (`json-schema/**`), so a document the file accepts is one the parse + // accepts. Built from the constant, so the suffix has one spelling. + .regex(NOT_A_CUSTOMIZATION_PACKAGE_ID, { message: `\`overrides.package\` names the BASE package whose item this item replaces, never a \`${CUSTOMIZATION_PACKAGE_SUFFIX}\` package: ` + `the declaring item lives in the base's paired \`${CUSTOMIZATION_PACKAGE_SUFFIX}\` package and names the base it overrides ` From 104d9e80eefdf202c1d1b6c9388b4438504ce692 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 16:29:15 +0000 Subject: [PATCH 05/11] chore(spec): regenerate artefacts for the override declaration; type the new tests Regenerated skill references, api-surface, export-origins, declaration-map, reference docs, the strictness-ledger counts and the authorable surface for the override declaration and the pairing helpers. The two new test files now pass the test typecheck without a debt entry. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- .../docs/references/api/automation-api.mdx | 12 +++ content/docs/references/api/metadata.mdx | 1 + content/docs/references/automation/flow.mdx | 9 +++ content/docs/references/data/mapping.mdx | 8 ++ content/docs/references/data/object.mdx | 1 + content/docs/references/index.mdx | 9 ++- content/docs/references/kernel/index.mdx | 3 +- content/docs/references/kernel/meta.json | 3 +- .../references/kernel/metadata-plugin.mdx | 1 + .../kernel/override-declaration.mdx | 75 +++++++++++++++++++ content/docs/references/system/book.mdx | 8 ++ content/docs/references/system/doc.mdx | 8 ++ .../docs/references/system/email-template.mdx | 8 ++ .../docs/references/system/translation.mdx | 8 ++ content/docs/references/ui/action.mdx | 8 ++ content/docs/references/ui/app.mdx | 8 ++ content/docs/references/ui/dashboard.mdx | 8 ++ content/docs/references/ui/dataset.mdx | 8 ++ content/docs/references/ui/page.mdx | 8 ++ content/docs/references/ui/report.mdx | 8 ++ content/docs/references/ui/view.mdx | 32 ++++++++ .../kernel.md | 2 +- packages/spec/api-surface/data.json | 5 ++ packages/spec/api-surface/kernel.json | 27 +++++++ packages/spec/authorable-surface/api.json | 1 + .../spec/authorable-surface/automation.json | 1 + packages/spec/authorable-surface/data.json | 1 + packages/spec/authorable-surface/kernel.json | 2 + packages/spec/authorable-surface/system.json | 4 + packages/spec/authorable-surface/ui.json | 6 ++ packages/spec/declaration-map/kernel.json | 2 + packages/spec/export-origins/data.json | 5 ++ packages/spec/export-origins/kernel.json | 27 +++++++ .../spec/json-schema.manifest/kernel.json | 1 + .../src/kernel/customization-package.test.ts | 4 +- .../src/kernel/override-declaration.test.ts | 2 +- skills/objectstack-ai/references/_index.md | 1 + .../references/_index.md | 1 + skills/objectstack-data/references/_index.md | 1 + skills/objectstack-ui/references/_index.md | 1 + 40 files changed, 318 insertions(+), 10 deletions(-) create mode 100644 content/docs/references/kernel/override-declaration.mdx diff --git a/content/docs/references/api/automation-api.mdx b/content/docs/references/api/automation-api.mdx index 47b869c1952..ce42af6f00d 100644 --- a/content/docs/references/api/automation-api.mdx +++ b/content/docs/references/api/automation-api.mdx @@ -129,6 +129,7 @@ const result = AutomationApiErrorCode.parse(data); | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | ### Nested Shape: `CreateFlowRequest.variables[number]` @@ -186,6 +187,13 @@ const result = AutomationApiErrorCode.parse(data); | **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | | **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | +### Nested Shape: `CreateFlowRequest.overrides` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | +| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | + --- @@ -249,6 +257,7 @@ const result = AutomationApiErrorCode.parse(data); | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | --- @@ -789,6 +798,7 @@ const result = AutomationApiErrorCode.parse(data); | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | --- @@ -1098,6 +1108,7 @@ const result = AutomationApiErrorCode.parse(data); | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | --- @@ -1162,6 +1173,7 @@ const result = AutomationApiErrorCode.parse(data); | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | --- diff --git a/content/docs/references/api/metadata.mdx b/content/docs/references/api/metadata.mdx index de4f82ee7ac..47874fadcfd 100644 --- a/content/docs/references/api/metadata.mdx +++ b/content/docs/references/api/metadata.mdx @@ -105,6 +105,7 @@ const result = AppDefinitionResponseSchema.parse(data); | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | --- diff --git a/content/docs/references/automation/flow.mdx b/content/docs/references/automation/flow.mdx index 16278f56b12..75cc8557d5b 100644 --- a/content/docs/references/automation/flow.mdx +++ b/content/docs/references/automation/flow.mdx @@ -50,6 +50,7 @@ const result = FlowSchema.parse(data); | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | ### Nested Shape: `Flow.variables[number]` @@ -107,6 +108,13 @@ const result = FlowSchema.parse(data); | **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | | **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | +### Nested Shape: `Flow.overrides` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | +| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | + --- @@ -264,6 +272,7 @@ const result = FlowSchema.parse(data); | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | --- diff --git a/content/docs/references/data/mapping.mdx b/content/docs/references/data/mapping.mdx index bad43bad443..0b9f9e14fbb 100644 --- a/content/docs/references/data/mapping.mdx +++ b/content/docs/references/data/mapping.mdx @@ -57,6 +57,7 @@ const result = ImportFieldMappingSchema.parse(data); | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | ### Nested Shape: `Mapping.fieldMapping[number]` @@ -77,6 +78,13 @@ const result = ImportFieldMappingSchema.parse(data); | **recordsPath** | `string` | optional | Dot path, within the action's result, to the array of records (e.g. `body.results`); omitted ⇒ the result's `body` is the array | | **watermark** | `{ field: string; param: string }` | optional | Timestamp-incremental pull; omitted ⇒ every pull reads the full set. Either way a pull makes ONE action call and reads ONE response: the connector's paging is not followed, so a paged endpoint yields its first page only — and an incremental pull over a newest-first paged endpoint moves its starting point past the pages it never read | +### Nested Shape: `Mapping.overrides` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | +| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | + --- diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index ddc9e850495..bcb48a01324 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -525,6 +525,7 @@ const result = ApiMethod.parse(data); | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | ### Nested Shape: `Object.protection` diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 2215a7e8e78..fe657cada28 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,7 +1,7 @@ --- title: Protocol reference — every schema by module navTitle: Protocol Reference -description: Every schema published by @objectstack/spec — 1571 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1572 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -26,7 +26,7 @@ counts are sums of the rows they head. Regenerate with | [Data Protocol](/docs/references/data) | 31 | 174 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | | [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. | | [Integration Protocol](/docs/references/integration) | 1 | 22 | The single connector protocol (ADR-0097) — catalog descriptors and provider-bound instances. | -| [Kernel Protocol](/docs/references/kernel) | 29 | 151 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. | +| [Kernel Protocol](/docs/references/kernel) | 30 | 152 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. | | [Marketplace Protocol](/docs/references/marketplace) | 4 | 30 | The package & marketplace format — package identity and versions, listing, publish, review, search, install, template manifests. | | [QA Protocol](/docs/references/qa) | 1 | 8 | Declarative test suites — scenarios, steps, actions and assertions. | | [Security Protocol](/docs/references/security) | 6 | 37 | Permission sets, row-level security, sharing rules, tenancy posture. | @@ -34,7 +34,7 @@ counts are sums of the rows they head. Regenerate with | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 37 | 287 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | | [UI Protocol](/docs/references/ui) | 16 | 172 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **204** | **1571** | 14 protocol modules | +| **Total** | **205** | **1572** | 14 protocol modules | --- @@ -204,7 +204,7 @@ The single connector protocol (ADR-0097) — catalog descriptors and provider-bo ## Kernel Protocol -**Source:** `packages/spec/src/kernel/` · **Import:** `@objectstack/spec/kernel` · **29 pages, 151 schemas** +**Source:** `packages/spec/src/kernel/` · **Import:** `@objectstack/spec/kernel` · **30 pages, 152 schemas** Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. @@ -225,6 +225,7 @@ Plugin lifecycle and manifests, capabilities and security, metadata loading, ser | [`metadata-loader.zod.ts`](/docs/references/kernel/metadata-loader) | `MetadataFallbackStrategy`, `MetadataManagerConfig` | | [`metadata-plugin.zod.ts`](/docs/references/kernel/metadata-plugin) | `MetadataBulkResult`, `MetadataDependency`, `MetadataPluginConfig`, `MetadataPluginManifest`, `MetadataQuery`, `MetadataQueryResult`, `MetadataType`, `MetadataTypeRegistryEntry`, `MetadataValidationResult` | | [`metadata-protection.zod.ts`](/docs/references/kernel/metadata-protection) | `MetadataLock`, `MetadataLockSource`, `MetadataProvenance` | +| [`override-declaration.zod.ts`](/docs/references/kernel/override-declaration) | `OverrideDeclaration` | | [`package-artifact.zod.ts`](/docs/references/kernel/package-artifact) | `ArtifactChecksum`, `ArtifactFileEntry`, `ArtifactSignature`, `MetadataCategoryEnum`, `PackageArtifact` | | [`package-registry.zod.ts`](/docs/references/kernel/package-registry) | `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `GetPackageRequest`, `GetPackageResponse`, `InstallPackageRequest`, `InstallPackageResponse`, `InstalledPackage`, `InstalledPackageSource`, `ListPackagesRequest`, `ListPackagesResponse`, `NamespaceConflictError`, `NamespaceRegistryEntry`, `PackageStatusEnum`, `UninstallPackageRequest`, `UninstallPackageResponse` | | [`plugin.zod.ts`](/docs/references/kernel/plugin) | `Plugin` | diff --git a/content/docs/references/kernel/index.mdx b/content/docs/references/kernel/index.mdx index 4c1c3e77c5f..89bb5be364f 100644 --- a/content/docs/references/kernel/index.mdx +++ b/content/docs/references/kernel/index.mdx @@ -1,7 +1,7 @@ --- title: Kernel Protocol — complete schema reference navTitle: Kernel Protocol -description: "The ObjectStack Kernel Protocol in 29 reference pages: every schema in @objectstack/spec with its properties, types, defaults and a TypeScript example." +description: "The ObjectStack Kernel Protocol in 30 reference pages: every schema in @objectstack/spec with its properties, types, defaults and a TypeScript example." --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -24,6 +24,7 @@ This section contains all protocol schemas for the kernel layer of ObjectStack. + diff --git a/content/docs/references/kernel/meta.json b/content/docs/references/kernel/meta.json index 5574501de14..ce258d0d460 100644 --- a/content/docs/references/kernel/meta.json +++ b/content/docs/references/kernel/meta.json @@ -34,6 +34,7 @@ "events-dlq", "events-handlers", "events-integrations", - "events-queue" + "events-queue", + "override-declaration" ] } \ No newline at end of file diff --git a/content/docs/references/kernel/metadata-plugin.mdx b/content/docs/references/kernel/metadata-plugin.mdx index 027fa3d685d..eb22aa63203 100644 --- a/content/docs/references/kernel/metadata-plugin.mdx +++ b/content/docs/references/kernel/metadata-plugin.mdx @@ -368,6 +368,7 @@ const result = MetadataBulkResultSchema.parse(data); | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | --- diff --git a/content/docs/references/kernel/override-declaration.mdx b/content/docs/references/kernel/override-declaration.mdx new file mode 100644 index 00000000000..20eb6368a1f --- /dev/null +++ b/content/docs/references/kernel/override-declaration.mdx @@ -0,0 +1,75 @@ +--- +title: Override Declaration — Kernel Protocol +navTitle: Override Declaration +description: "A customization of an installed base package (HotCRM) is an item of the base's paired writable package .custom." +--- + +{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} + +## The override declaration (ADR-0146 D2) + +A customization of an installed base package (HotCRM) is an item of the +base's paired writable package `.custom` (ADR-0146 D3). An item of that +package that bears a base item's name and carries + +```ts +overrides: { package: '', basedOn: '' } +``` + +replaces the base's item WHOLE for every read that serves the base's name +(ADR-0146 D8). The declaration lives in the item body, so it travels with the +item through export and git: what a developer writes into `hotcrm.custom` in a +repository and what an administrator does in Studio produce the same +document. Only the paired `.custom` package declares an override (the +ruling recorded on #28390, ruling A); the pairing helpers in +`customization-package.ts` are what a door asks. + +### Why a leaf module + +It imports nothing but zod and `strictObject`, on purpose. Every overridable +type's schema spreads `OverrideDeclarationFields`, and some of those +schemas are imported by `manifest.zod.ts` (`ui/app.zod.ts`); importing the +manifest or the registry from here would close a module cycle that the eager +schema build (`OS_EAGER_SCHEMAS=1`) evaluates in temporal-dead-zone order. +The helpers that need the manifest grammar live in `customization-package.ts`, +which no schema imports. + +### What this module does NOT do + +It declares the shape. Admitting a declared override, refusing an undeclared +same-named item, refusing a declaration from a package that is not the pair, +and serving the declared document are the doors' (ADR-0146 Rollout, P1: +the write gate, the effective read, the ensure door). Which types may carry +the declaration is the closed list `OVERRIDE_TYPE_CLASSIFICATION`, never a +judgement a door makes. + + +**Source:** `packages/spec/src/kernel/override-declaration.zod.ts` + + +## TypeScript Usage + +```typescript +import { OverrideDeclarationSchema } from '@objectstack/spec/kernel'; +import type { OverrideDeclaration } from '@objectstack/spec/kernel'; + +// Validate data +const result = OverrideDeclarationSchema.parse(data); +``` + +--- + +## OverrideDeclaration + +Override declaration (ADR-0146 D2): this item, of the base's paired "``.custom" package and bearing a base item's name, replaces that base item whole for every read that serves the name. + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | +| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | + + +--- + diff --git a/content/docs/references/system/book.mdx b/content/docs/references/system/book.mdx index 0cab1ce8a51..7aa67e34db8 100644 --- a/content/docs/references/system/book.mdx +++ b/content/docs/references/system/book.mdx @@ -61,6 +61,7 @@ const result = BookSchema.parse(data); | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | ### Nested Shape: `Book.groups[number]` @@ -74,6 +75,13 @@ const result = BookSchema.parse(data); | **package** | `string` | optional | Scope the rule to a package id (default: the book package; cross-package via ADR-0048) | | **pages** | `(string \| { doc?: string; href?: string; label?: string; badge?: string; … })[]` | optional | OPTIONAL explicit override — hand-pin a curated order; wins over `include` | +### Nested Shape: `Book.overrides` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | +| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | + --- diff --git a/content/docs/references/system/doc.mdx b/content/docs/references/system/doc.mdx index 5410e3ae27d..d3d7e7e7472 100644 --- a/content/docs/references/system/doc.mdx +++ b/content/docs/references/system/doc.mdx @@ -64,6 +64,14 @@ const result = DocSchema.parse(data); | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | + +### Nested Shape: `Doc.overrides` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | +| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | --- diff --git a/content/docs/references/system/email-template.mdx b/content/docs/references/system/email-template.mdx index 9ee7f1983c2..72ff3bb403f 100644 --- a/content/docs/references/system/email-template.mdx +++ b/content/docs/references/system/email-template.mdx @@ -66,6 +66,7 @@ const result = EmailTemplateDefinitionSchema.parse(data); | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | ### Nested Shape: `EmailTemplateDefinition.variables[number]` @@ -84,6 +85,13 @@ const result = EmailTemplateDefinitionSchema.parse(data); | **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | | **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | +### Nested Shape: `EmailTemplateDefinition.overrides` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | +| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | + --- diff --git a/content/docs/references/system/translation.mdx b/content/docs/references/system/translation.mdx index cd6a46c704e..46f9d8922bd 100644 --- a/content/docs/references/system/translation.mdx +++ b/content/docs/references/system/translation.mdx @@ -810,6 +810,7 @@ One locale of translations — the `translation` metadata type | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | ### Nested Shape: `TranslationItem.objects[string]` @@ -947,6 +948,13 @@ Translation data for a single object | :--- | :--- | :--- | :--- | | **sourceLabels** | `{ env?: string; global?: string; tenant?: string; user?: string; … }` | optional | Source badge labels by resolution layer | +### Nested Shape: `TranslationItem.overrides` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | +| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | + --- diff --git a/content/docs/references/ui/action.mdx b/content/docs/references/ui/action.mdx index b5ceb4a3023..0e417215ec7 100644 --- a/content/docs/references/ui/action.mdx +++ b/content/docs/references/ui/action.mdx @@ -78,6 +78,7 @@ const result = ActionSchema.parse(data); | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | ### Allowed Values: `Action.requiresFeature` @@ -181,6 +182,13 @@ L2 sandboxed JS body — runs inside an isolated VM with declared capabilities | **navigate** | `string` | ✅ | Route/URL template navigated to after the action succeeds. Interpolates $`{param.*}` (params-dialog values), $`{ctx.*}` (origin/apiBase/user/org/recordId/selection) and $`{result.*}` (the action's server response payload — NEW with this key, e.g. $`{result.id}`; on a type:'flow' action the completed run's answer, whose `output` carries the flow's isOutput variables, e.g. $`{result.output.renewal.id}`). An http:// or https:// URL, a mailto: address, or an in-app path starting with / or ./ (an SPA route hop); a template that opens with ${ is judged after interpolation. Every other scheme and a protocol-relative //host are refused. An in-app path is a console route, written without the console's mount prefix; a record is /apps/APP/OBJECT/record/ID, where APP is the app's route segment (its package id; its name also resolves). No app-relative form is declared and no $`{ctx.*}` member names the current app, so write the app segment. Renderers MUST encodeURIComponent values in query positions. | | **openIn** | `Enum<'self' \| 'newTab'>` | optional (default: `"self"`) | Where to perform the post-success navigation: 'self' (default — in-place SPA navigation, immune to popup blocking) or 'newTab'. Closed enum — no general navigation DSL. | +### Nested Shape: `Action.overrides` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | +| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | + --- diff --git a/content/docs/references/ui/app.mdx b/content/docs/references/ui/app.mdx index 94cd0aa4ba3..025b92dafde 100644 --- a/content/docs/references/ui/app.mdx +++ b/content/docs/references/ui/app.mdx @@ -88,6 +88,7 @@ const result = ActionNavItemSchema.parse(data); | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | ### Nested Shape: `App.branding` @@ -310,6 +311,13 @@ Documentation entry on the app menu (ADR-0046). Targets a `book` and/or a `doc` | **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | | **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | +### Nested Shape: `App.overrides` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | +| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | + --- diff --git a/content/docs/references/ui/dashboard.mdx b/content/docs/references/ui/dashboard.mdx index 22d19cafba6..cbbe8268642 100644 --- a/content/docs/references/ui/dashboard.mdx +++ b/content/docs/references/ui/dashboard.mdx @@ -48,6 +48,7 @@ const result = DashboardSchema.parse(data); | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | ### Nested Shape: `Dashboard.header` @@ -110,6 +111,13 @@ const result = DashboardSchema.parse(data); | **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | | **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | +### Nested Shape: `Dashboard.overrides` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | +| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | + --- diff --git a/content/docs/references/ui/dataset.mdx b/content/docs/references/ui/dataset.mdx index 5a9902358ab..6674b7c8e6b 100644 --- a/content/docs/references/ui/dataset.mdx +++ b/content/docs/references/ui/dataset.mdx @@ -65,6 +65,7 @@ const result = DatasetSchema.parse(data); | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | ### Nested Shape: `Dataset.dimensions[number]` @@ -100,6 +101,13 @@ const result = DatasetSchema.parse(data); | **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | | **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | +### Nested Shape: `Dataset.overrides` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | +| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | + --- diff --git a/content/docs/references/ui/page.mdx b/content/docs/references/ui/page.mdx index 26119a323dd..7199ef33a23 100644 --- a/content/docs/references/ui/page.mdx +++ b/content/docs/references/ui/page.mdx @@ -226,6 +226,7 @@ View filter rule | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | ### Nested Shape: `Page.variables[number]` @@ -306,6 +307,13 @@ View filter rule | **tabs** | `never` | optional | [REMOVED] `page.slots.tabs` was removed in @objectstack/spec 18.3.0 (ADR-0085) — an authored strip replaced the whole synthesized one, so the related lists, approvals, attachments and history the platform composes from the object were dropped unless re-added by hand, and the page stayed tabbed at every width instead of taking the record page's columns. Delete the key. The record page composes its tabs and columns from the object's roles and relationship flags: to change which related lists appear or their order, set `relatedList` / `relatedList: 'primary'` on the child's reference field; to replace the Details body, author `slots.details`; a page that needs its own strip is `kind: 'full'`, drawn as authored. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | | **discussion** | `{ type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string; id?: string; label?: string \| Record; properties?: Record; … } \| { type: Enum<'page:header' \| 'page:footer' \| 'page:sidebar' \| 'page:tabs' \| 'page:accordion' \| …> \| string; id?: string; label?: string \| Record; properties?: Record; … }[]` | optional | | +### Nested Shape: `Page.overrides` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | +| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | + --- diff --git a/content/docs/references/ui/report.mdx b/content/docs/references/ui/report.mdx index 28b6c03d15b..ac886532efb 100644 --- a/content/docs/references/ui/report.mdx +++ b/content/docs/references/ui/report.mdx @@ -78,6 +78,7 @@ const result = JoinedReportBlockSchema.parse(data); | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | ### Nested Shape: `Report.order[number]` @@ -145,6 +146,13 @@ const result = JoinedReportBlockSchema.parse(data); | **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | | **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | +### Nested Shape: `Report.overrides` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | +| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | + --- diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 85408a02d0e..8f70ad1082a 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -1998,6 +1998,7 @@ This schema accepts one of the following structures: | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | ### Nested Shape: `ViewItem[viewKind='list'].config` @@ -2058,6 +2059,13 @@ This schema accepts one of the following structures: | **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | | **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | +### Nested Shape: `ViewItem[viewKind='list'].overrides` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | +| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | + --- #### Option 2 @@ -2084,6 +2092,7 @@ This schema accepts one of the following structures: | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | ### Nested Shape: `ViewItem[viewKind='form'].config` @@ -2121,6 +2130,13 @@ This schema accepts one of the following structures: | **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | | **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | +### Nested Shape: `ViewItem[viewKind='form'].overrides` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | +| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | + --- @@ -2165,6 +2181,7 @@ This schema accepts one of the following structures: | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | | **isPinned** | `boolean` | optional | Console round-trip: the view is pinned in the object's view switcher. Written by the console's pin toggle through the `view` metadata API and read back to draw the pinned group. Stored on the view's row, which has no per-user scope. Not authored. | | **sortOrder** | `integer` | optional | Console round-trip: the view's position among the object's saved views in the switcher, 0-based and counted over saved views only (a code-defined view carries none). Written by the console's drag-reorder and read back to order the tabs. Not authored: `order` is the authored default position. | | **visibility** | `Enum<'private' \| 'team' \| 'organization' \| 'public'>` | optional | Console round-trip: the group the switcher files this view's tab under (private, team, organization or public). Display grouping only, NOT access control: nothing restricts who can list or open the view by this value. No console control sets it; the console carries a stored value forward when it re-saves the row. Not authored. | @@ -2230,6 +2247,13 @@ This schema accepts one of the following structures: | **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | | **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | +### Nested Shape: `ViewItemWire[viewKind='list'].overrides` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | +| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | + ### Nested Shape: `ViewItemWire[viewKind='list'].columnState` | Property | Type | Required | Description | @@ -2263,6 +2287,7 @@ This schema accepts one of the following structures: | **_packageId** | `string` | optional | Owning package machine id. | | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | +| **overrides** | `{ package: string; basedOn: string }` | optional | Override declaration (ADR-0146 D2): `{ package, basedOn }` — this item of "``.custom" replaces the base package's same-named item whole. `basedOn` is recorded and never judged. | | **isPinned** | `boolean` | optional | Console round-trip: the view is pinned in the object's view switcher. Written by the console's pin toggle through the `view` metadata API and read back to draw the pinned group. Stored on the view's row, which has no per-user scope. Not authored. | | **sortOrder** | `integer` | optional | Console round-trip: the view's position among the object's saved views in the switcher, 0-based and counted over saved views only (a code-defined view carries none). Written by the console's drag-reorder and read back to order the tabs. Not authored: `order` is the authored default position. | | **visibility** | `Enum<'private' \| 'team' \| 'organization' \| 'public'>` | optional | Console round-trip: the group the switcher files this view's tab under (private, team, organization or public). Display grouping only, NOT access control: nothing restricts who can list or open the view by this value. No console control sets it; the console carries a stored value forward when it re-saves the row. Not authored. | @@ -2305,6 +2330,13 @@ This schema accepts one of the following structures: | **reason** | `string` | ✅ | User-visible reason shown when the lock blocks an action. | | **docsUrl** | `string` | optional | Optional URL the Studio banner links to for more context. | +### Nested Shape: `ViewItemWire[viewKind='form'].overrides` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | +| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | + ### Nested Shape: `ViewItemWire[viewKind='form'].columnState` | Property | Type | Required | Description | diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/kernel.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/kernel.md index 3625c957d44..c0c068f9904 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts/kernel.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts/kernel.md @@ -19,4 +19,4 @@ hand-patch a number here** — fix the code or the verdict and regenerate. | Dir | Sites | |---|---| -| `kernel/` | 237 | +| `kernel/` | 238 | diff --git a/packages/spec/api-surface/data.json b/packages/spec/api-surface/data.json index 520b7adf3a5..557dfd5167c 100644 --- a/packages/spec/api-surface/data.json +++ b/packages/spec/api-surface/data.json @@ -50,6 +50,7 @@ "AutoPersistenceConfigSchema (const)", "AutonumberFormatSource (interface)", "AutonumberToken (type)", + "BASE_FIELD_DISPLAY_WHITELIST (const)", "BOOLEAN_COMPARAND_DOOR_CASES (const)", "BOOLEAN_COMPARAND_DOOR_FIXTURE (const)", "BOOLEAN_COMPARAND_DOOR_FIXTURE_FIELDS (const)", @@ -66,6 +67,8 @@ "BULK_WRITE_HOOK_DISPATCH_CONTRACT (const)", "BaseEngineOptions (type)", "BaseEngineOptionsSchema (const)", + "BaseFieldDisplayProperty (type)", + "BaseFieldOptionDisplayProperty (type)", "BaseValidationRuleShape (interface)", "BooleanComparandDoorCase (type)", "BooleanComparandDoorDeferredCase (interface)", @@ -237,6 +240,7 @@ "DroppedFieldsEventSchema (const)", "EMPTY_OPERATOR_ARMS (const)", "ENGINE_UPDATE_UPSERT_REMOVED (const)", + "EXTENSION_FIELD_SUFFIX (const)", "EffectiveApiMethods (interface)", "EmptyOperatorArm (type)", "EmptyOperatorExpansion (interface)", @@ -918,6 +922,7 @@ "isDateRangePresetName (function)", "isEmptyFilterValue (function)", "isExpressionEnvelopeDefault (function)", + "isExtensionFieldName (function)", "isFileIdToken (function)", "isFilterAST (function)", "isGlobalUnique (function)", diff --git a/packages/spec/api-surface/kernel.json b/packages/spec/api-surface/kernel.json index fd84a0f13fd..b87389e65dc 100644 --- a/packages/spec/api-surface/kernel.json +++ b/packages/spec/api-surface/kernel.json @@ -20,6 +20,8 @@ "BreakingChangeSchema (const)", "CONSUMER_INSTALLABLE_TYPES (const)", "CORE_PLUGIN_TYPES (const)", + "CUSTOMIZATION_NAMESPACE_SUFFIX (const)", + "CUSTOMIZATION_PACKAGE_SUFFIX (const)", "CapabilityClassification (interface)", "CapabilityConformanceLevel (type)", "CapabilityConformanceLevelSchema (const)", @@ -38,6 +40,12 @@ "CompatibilityMatrixEntryParsed (type)", "CompatibilityMatrixEntrySchema (const)", "CompletenessFinding (interface)", + "CustomizationDependencyRangeRefusal (type)", + "CustomizationDerivation (type)", + "CustomizationDerived (interface)", + "CustomizationNamespaceRefusal (type)", + "CustomizationPackageIdRefusal (type)", + "CustomizationRefused (interface)", "DEFAULT_INSTALL_MODES (const)", "DEFAULT_METADATA_TYPE_REGISTRY (const)", "DeadLetterQueueEntry (type)", @@ -161,6 +169,7 @@ "ListPackagesResponseSchema (const)", "MANIFEST_ID_EXAMPLES (const)", "MANIFEST_ID_PATTERN (const)", + "MAX_CUSTOMIZATION_BASE_NAMESPACE_LENGTH (const)", "METADATA_READ_DECORATIONS (const)", "ManifestPermissions (type)", "ManifestPermissionsSchema (const)", @@ -211,6 +220,8 @@ "NamespaceConflictErrorSchema (const)", "NamespaceRegistryEntry (type)", "NamespaceRegistryEntrySchema (const)", + "OVERRIDABLE_METADATA_TYPES (const)", + "OVERRIDE_TYPE_CLASSIFICATION (const)", "ObjectStackManifest (type)", "ObjectStackManifestParsed (type)", "OclifPluginConfig (type)", @@ -221,6 +232,14 @@ "OpsFilePathSchema (const)", "OpsPluginStructure (type)", "OpsPluginStructureSchema (const)", + "OverrideClassificationBasis (type)", + "OverrideCondition (type)", + "OverrideDeclaration (type)", + "OverrideDeclarationFields (const)", + "OverrideDeclarationParsed (type)", + "OverrideDeclarationSchema (const)", + "OverrideDisposition (type)", + "OverrideTypeClassification (interface)", "PLATFORM_ALWAYS_ON_CAPABILITIES (const)", "PLATFORM_CAPABILITY_PROVIDERS (const)", "PLATFORM_CAPABILITY_TOKENS (const)", @@ -444,6 +463,10 @@ "checkViewCompleteness (function)", "checkWebhookCompleteness (function)", "classifyRequiredCapability (function)", + "customizationBasePackageId (function)", + "customizationDependencyRange (function)", + "customizationNamespace (function)", + "customizationPackageId (function)", "deriveNamespaceFromPackageId (function)", "evaluateLockForDelete (function)", "evaluateLockForWrite (function)", @@ -453,8 +476,12 @@ "getMetadataTypeActions (function)", "getMetadataTypeRedactor (function)", "getMetadataTypeSchema (function)", + "getOverrideTypeClassification (function)", "isConsumerInstallable (function)", + "isCustomizationPackage (function)", + "isCustomizationPairOf (function)", "isKnownPlatformCapability (function)", + "isOverridableMetadataType (function)", "isStoredMetadataBodyObject (function)", "lintUnknownAuthoringKeys (function)", "lintUnknownKeysAgainstSchema (function)", diff --git a/packages/spec/authorable-surface/api.json b/packages/spec/authorable-surface/api.json index 3249d7097b6..7b54a642f5f 100644 --- a/packages/spec/authorable-surface/api.json +++ b/packages/spec/authorable-surface/api.json @@ -365,6 +365,7 @@ "api/CreateFlowRequest:label", "api/CreateFlowRequest:name", "api/CreateFlowRequest:nodes", + "api/CreateFlowRequest:overrides", "api/CreateFlowRequest:protection", "api/CreateFlowRequest:requiredPermissions", "api/CreateFlowRequest:runAs", diff --git a/packages/spec/authorable-surface/automation.json b/packages/spec/authorable-surface/automation.json index 5e75359d33e..9defccb5002 100644 --- a/packages/spec/authorable-surface/automation.json +++ b/packages/spec/authorable-surface/automation.json @@ -184,6 +184,7 @@ "automation/Flow:label", "automation/Flow:name", "automation/Flow:nodes", + "automation/Flow:overrides", "automation/Flow:protection", "automation/Flow:requiredPermissions", "automation/Flow:runAs", diff --git a/packages/spec/authorable-surface/data.json b/packages/spec/authorable-surface/data.json index d04a1e830f1..55e39e4b615 100644 --- a/packages/spec/authorable-surface/data.json +++ b/packages/spec/authorable-surface/data.json @@ -541,6 +541,7 @@ "data/Mapping:label", "data/Mapping:mode", "data/Mapping:name", + "data/Mapping:overrides", "data/Mapping:sourceFormat", "data/Mapping:targetObject", "data/Mapping:upsertKey", diff --git a/packages/spec/authorable-surface/kernel.json b/packages/spec/authorable-surface/kernel.json index 700f4f2c986..75f06818841 100644 --- a/packages/spec/authorable-surface/kernel.json +++ b/packages/spec/authorable-surface/kernel.json @@ -377,6 +377,8 @@ "kernel/OpsPluginStructure:files", "kernel/OpsPluginStructure:metadata", "kernel/OpsPluginStructure:root", + "kernel/OverrideDeclaration:basedOn", + "kernel/OverrideDeclaration:package", "kernel/PackageArtifact:builtAt", "kernel/PackageArtifact:builtWith", "kernel/PackageArtifact:checksums", diff --git a/packages/spec/authorable-surface/system.json b/packages/spec/authorable-surface/system.json index fdc9250b952..3d7d1b68865 100644 --- a/packages/spec/authorable-surface/system.json +++ b/packages/spec/authorable-surface/system.json @@ -149,6 +149,7 @@ "system/Book:label", "system/Book:name", "system/Book:order", + "system/Book:overrides", "system/Book:slug", "system/BookGroup:include", "system/BookGroup:key", @@ -356,6 +357,7 @@ "system/Doc:label", "system/Doc:name", "system/Doc:order", + "system/Doc:overrides", "system/Doc:tags", "system/Doc:translations", "system/EmailAddressConfig:address", @@ -394,6 +396,7 @@ "system/EmailTemplateDefinition:label", "system/EmailTemplateDefinition:locale", "system/EmailTemplateDefinition:name", + "system/EmailTemplateDefinition:overrides", "system/EmailTemplateDefinition:protection", "system/EmailTemplateDefinition:replyTo", "system/EmailTemplateDefinition:subject", @@ -1379,6 +1382,7 @@ "system/TranslationItem:metadataForms", "system/TranslationItem:name", "system/TranslationItem:objects", + "system/TranslationItem:overrides", "system/TranslationItem:pages", "system/TranslationItem:permissions", "system/TranslationItem:picklists", diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index 057a463d806..463d2f418fd 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -35,6 +35,7 @@ "ui/Action:operation", "ui/Action:order", "ui/Action:outcomeMessages", + "ui/Action:overrides", "ui/Action:params", "ui/Action:patch", "ui/Action:pressed", @@ -184,6 +185,7 @@ "ui/App:label", "ui/App:name", "ui/App:navigation", + "ui/App:overrides", "ui/App:protection", "ui/App:requiredPermissions", "ui/AppBranding:accentColor", @@ -316,6 +318,7 @@ "ui/Dashboard:header", "ui/Dashboard:label", "ui/Dashboard:name", + "ui/Dashboard:overrides", "ui/Dashboard:protection", "ui/Dashboard:refreshInterval [RETIRED]", "ui/Dashboard:refreshIntervalSeconds", @@ -392,6 +395,7 @@ "ui/Dataset:measures", "ui/Dataset:name", "ui/Dataset:object", + "ui/Dataset:overrides", "ui/Dataset:protection", "ui/DatasetDimension:dateGranularity", "ui/DatasetDimension:field", @@ -1053,6 +1057,7 @@ "ui/Page:label", "ui/Page:name", "ui/Page:object", + "ui/Page:overrides", "ui/Page:print", "ui/Page:record", "ui/Page:regions", @@ -1253,6 +1258,7 @@ "ui/Report:label", "ui/Report:name", "ui/Report:order", + "ui/Report:overrides", "ui/Report:protection", "ui/Report:rows", "ui/Report:runtimeFilter", diff --git a/packages/spec/declaration-map/kernel.json b/packages/spec/declaration-map/kernel.json index c5e9b481a30..e9bbab2b3d8 100644 --- a/packages/spec/declaration-map/kernel.json +++ b/packages/spec/declaration-map/kernel.json @@ -154,6 +154,8 @@ "OpsFilePathSchema": "kernel/OpsFilePath", "OpsPluginStructure": "kernel/OpsPluginStructure", "OpsPluginStructureSchema": "kernel/OpsPluginStructure", + "OverrideDeclaration": "kernel/OverrideDeclaration", + "OverrideDeclarationSchema": "kernel/OverrideDeclaration", "PackageArtifact": "kernel/PackageArtifact", "PackageArtifactSchema": "kernel/PackageArtifact", "PackageDependencyConflict": "kernel/PackageDependencyConflict", diff --git a/packages/spec/export-origins/data.json b/packages/spec/export-origins/data.json index 4ec83f2a079..b6be907860f 100644 --- a/packages/spec/export-origins/data.json +++ b/packages/spec/export-origins/data.json @@ -47,6 +47,7 @@ "AutoPersistenceConfigSchema": "src/data/driver/memory.zod.ts#AutoPersistenceConfigSchema (const)", "AutonumberFormatSource": "src/data/autonumber-format.ts#AutonumberFormatSource (interface)", "AutonumberToken": "src/data/autonumber-format.ts#AutonumberToken (type)", + "BASE_FIELD_DISPLAY_WHITELIST": "src/data/field-customization.ts#BASE_FIELD_DISPLAY_WHITELIST (const)", "BOOLEAN_COMPARAND_DOOR_CASES": "src/data/filter-boolean-comparand-declared-type.ts#BOOLEAN_COMPARAND_DOOR_CASES (const)", "BOOLEAN_COMPARAND_DOOR_FIXTURE": "src/data/filter-boolean-comparand-declared-type.ts#BOOLEAN_COMPARAND_DOOR_FIXTURE (const)", "BOOLEAN_COMPARAND_DOOR_FIXTURE_FIELDS": "src/data/filter-boolean-comparand-declared-type.ts#BOOLEAN_COMPARAND_DOOR_FIXTURE_FIELDS (const)", @@ -63,6 +64,8 @@ "BULK_WRITE_HOOK_DISPATCH_CONTRACT": "src/data/bulk-write-hook-conformance.ts#BULK_WRITE_HOOK_DISPATCH_CONTRACT (const)", "BaseEngineOptions": "src/data/data-engine.zod.ts#BaseEngineOptions (type)", "BaseEngineOptionsSchema": "src/data/data-engine.zod.ts#BaseEngineOptionsSchema (const)", + "BaseFieldDisplayProperty": "src/data/field-customization.ts#BaseFieldDisplayProperty (type)", + "BaseFieldOptionDisplayProperty": "src/data/field-customization.ts#BaseFieldOptionDisplayProperty (type)", "BaseValidationRuleShape": "src/data/validation.zod.ts#BaseValidationRuleShape (interface)", "BooleanComparandDoorCase": "src/data/filter-boolean-comparand-declared-type.ts#BooleanComparandDoorCase (type)", "BooleanComparandDoorDeferredCase": "src/data/filter-boolean-comparand-declared-type.ts#BooleanComparandDoorDeferredCase (interface)", @@ -232,6 +235,7 @@ "DroppedFieldsEventSchema": "src/data/data-engine.zod.ts#DroppedFieldsEventSchema (const)", "EMPTY_OPERATOR_ARMS": "src/data/filter-empty-operator.ts#EMPTY_OPERATOR_ARMS (const)", "ENGINE_UPDATE_UPSERT_REMOVED": "src/data/data-engine.zod.ts#ENGINE_UPDATE_UPSERT_REMOVED (const)", + "EXTENSION_FIELD_SUFFIX": "src/data/field-customization.ts#EXTENSION_FIELD_SUFFIX (const)", "EffectiveApiMethods": "src/data/api-derivation.ts#EffectiveApiMethods (interface)", "EmptyOperatorArm": "src/data/filter-empty-operator.ts#EmptyOperatorArm (type)", "EmptyOperatorExpansion": "src/data/filter-empty-operator.ts#EmptyOperatorExpansion (interface)", @@ -904,6 +908,7 @@ "isDateRangePresetName": "src/data/date-range-presets.ts#isDateRangePresetName (function)", "isEmptyFilterValue": "src/data/filter-empty-operator.ts#isEmptyFilterValue (function)", "isExpressionEnvelopeDefault": "src/data/default-value-shape.ts#isExpressionEnvelopeDefault (function)", + "isExtensionFieldName": "src/data/field-customization.ts#isExtensionFieldName (function)", "isFileIdToken": "src/data/field-value.zod.ts#isFileIdToken (function)", "isFilterAST": "src/data/filter.zod.ts#isFilterAST (function)", "isGlobalUnique": "src/data/field.zod.ts#isGlobalUnique (function)", diff --git a/packages/spec/export-origins/kernel.json b/packages/spec/export-origins/kernel.json index ce8494045ea..ee4a50a45fa 100644 --- a/packages/spec/export-origins/kernel.json +++ b/packages/spec/export-origins/kernel.json @@ -20,6 +20,8 @@ "BreakingChangeSchema": "src/kernel/plugin-versioning.zod.ts#BreakingChangeSchema (const)", "CONSUMER_INSTALLABLE_TYPES": "src/kernel/plugin.zod.ts#CONSUMER_INSTALLABLE_TYPES (const)", "CORE_PLUGIN_TYPES": "src/kernel/plugin.zod.ts#CORE_PLUGIN_TYPES (const)", + "CUSTOMIZATION_NAMESPACE_SUFFIX": "src/kernel/customization-package.ts#CUSTOMIZATION_NAMESPACE_SUFFIX (const)", + "CUSTOMIZATION_PACKAGE_SUFFIX": "src/kernel/override-declaration.zod.ts#CUSTOMIZATION_PACKAGE_SUFFIX (const)", "CapabilityClassification": "src/kernel/platform-capabilities.ts#CapabilityClassification (interface)", "CapabilityConformanceLevel": "src/kernel/plugin-capability.zod.ts#CapabilityConformanceLevel (type)", "CapabilityConformanceLevelSchema": "src/kernel/plugin-capability.zod.ts#CapabilityConformanceLevelSchema (const)", @@ -38,6 +40,12 @@ "CompatibilityMatrixEntryParsed": "src/kernel/plugin-versioning.zod.ts#CompatibilityMatrixEntryParsed (type)", "CompatibilityMatrixEntrySchema": "src/kernel/plugin-versioning.zod.ts#CompatibilityMatrixEntrySchema (const)", "CompletenessFinding": "src/kernel/functional-completeness.ts#CompletenessFinding (interface)", + "CustomizationDependencyRangeRefusal": "src/kernel/customization-package.ts#CustomizationDependencyRangeRefusal (type)", + "CustomizationDerivation": "src/kernel/customization-package.ts#CustomizationDerivation (type)", + "CustomizationDerived": "src/kernel/customization-package.ts#CustomizationDerived (interface)", + "CustomizationNamespaceRefusal": "src/kernel/customization-package.ts#CustomizationNamespaceRefusal (type)", + "CustomizationPackageIdRefusal": "src/kernel/customization-package.ts#CustomizationPackageIdRefusal (type)", + "CustomizationRefused": "src/kernel/customization-package.ts#CustomizationRefused (interface)", "DEFAULT_INSTALL_MODES": "src/kernel/manifest.zod.ts#DEFAULT_INSTALL_MODES (const)", "DEFAULT_METADATA_TYPE_REGISTRY": "src/kernel/metadata-plugin.zod.ts#DEFAULT_METADATA_TYPE_REGISTRY (const)", "DeadLetterQueueEntry": "src/kernel/events/dlq.zod.ts#DeadLetterQueueEntry (type)", @@ -160,6 +168,7 @@ "ListPackagesResponseSchema": "src/kernel/package-registry.zod.ts#ListPackagesResponseSchema (const)", "MANIFEST_ID_EXAMPLES": "src/kernel/manifest.zod.ts#MANIFEST_ID_EXAMPLES (const)", "MANIFEST_ID_PATTERN": "src/kernel/manifest.zod.ts#MANIFEST_ID_PATTERN (const)", + "MAX_CUSTOMIZATION_BASE_NAMESPACE_LENGTH": "src/kernel/customization-package.ts#MAX_CUSTOMIZATION_BASE_NAMESPACE_LENGTH (const)", "METADATA_READ_DECORATIONS": "src/kernel/metadata-read-decorations.ts#METADATA_READ_DECORATIONS (const)", "ManifestPermissions": "src/kernel/manifest.zod.ts#ManifestPermissions (type)", "ManifestPermissionsSchema": "src/kernel/manifest.zod.ts#ManifestPermissionsSchema (const)", @@ -210,6 +219,8 @@ "NamespaceConflictErrorSchema": "src/kernel/package-registry.zod.ts#NamespaceConflictErrorSchema (const)", "NamespaceRegistryEntry": "src/kernel/package-registry.zod.ts#NamespaceRegistryEntry (type)", "NamespaceRegistryEntrySchema": "src/kernel/package-registry.zod.ts#NamespaceRegistryEntrySchema (const)", + "OVERRIDABLE_METADATA_TYPES": "src/kernel/customization-package.ts#OVERRIDABLE_METADATA_TYPES (const)", + "OVERRIDE_TYPE_CLASSIFICATION": "src/kernel/customization-package.ts#OVERRIDE_TYPE_CLASSIFICATION (const)", "ObjectStackManifest": "src/kernel/manifest.zod.ts#ObjectStackManifest (type)", "ObjectStackManifestParsed": "src/kernel/manifest.zod.ts#ObjectStackManifestParsed (type)", "OclifPluginConfig": "src/kernel/cli-extension.zod.ts#OclifPluginConfig (type)", @@ -220,6 +231,14 @@ "OpsFilePathSchema": "src/kernel/plugin-structure.zod.ts#OpsFilePathSchema (const)", "OpsPluginStructure": "src/kernel/plugin-structure.zod.ts#OpsPluginStructure (type)", "OpsPluginStructureSchema": "src/kernel/plugin-structure.zod.ts#OpsPluginStructureSchema (const)", + "OverrideClassificationBasis": "src/kernel/customization-package.ts#OverrideClassificationBasis (type)", + "OverrideCondition": "src/kernel/customization-package.ts#OverrideCondition (type)", + "OverrideDeclaration": "src/kernel/override-declaration.zod.ts#OverrideDeclaration (type)", + "OverrideDeclarationFields": "src/kernel/override-declaration.zod.ts#OverrideDeclarationFields (const)", + "OverrideDeclarationParsed": "src/kernel/override-declaration.zod.ts#OverrideDeclarationParsed (type)", + "OverrideDeclarationSchema": "src/kernel/override-declaration.zod.ts#OverrideDeclarationSchema (const)", + "OverrideDisposition": "src/kernel/customization-package.ts#OverrideDisposition (type)", + "OverrideTypeClassification": "src/kernel/customization-package.ts#OverrideTypeClassification (interface)", "PLATFORM_ALWAYS_ON_CAPABILITIES": "src/kernel/platform-capabilities.ts#PLATFORM_ALWAYS_ON_CAPABILITIES (const)", "PLATFORM_CAPABILITY_PROVIDERS": "src/kernel/platform-capabilities.ts#PLATFORM_CAPABILITY_PROVIDERS (const)", "PLATFORM_CAPABILITY_TOKENS": "src/kernel/platform-capabilities.ts#PLATFORM_CAPABILITY_TOKENS (const)", @@ -440,6 +459,10 @@ "checkViewCompleteness": "src/kernel/functional-completeness.ts#checkViewCompleteness (function)", "checkWebhookCompleteness": "src/kernel/functional-completeness.ts#checkWebhookCompleteness (function)", "classifyRequiredCapability": "src/kernel/platform-capabilities.ts#classifyRequiredCapability (function)", + "customizationBasePackageId": "src/kernel/customization-package.ts#customizationBasePackageId (function)", + "customizationDependencyRange": "src/kernel/customization-package.ts#customizationDependencyRange (function)", + "customizationNamespace": "src/kernel/customization-package.ts#customizationNamespace (function)", + "customizationPackageId": "src/kernel/customization-package.ts#customizationPackageId (function)", "deriveNamespaceFromPackageId": "src/kernel/namespace-prefix.ts#deriveNamespaceFromPackageId (function)", "evaluateLockForDelete": "src/kernel/metadata-protection.zod.ts#evaluateLockForDelete (function)", "evaluateLockForWrite": "src/kernel/metadata-protection.zod.ts#evaluateLockForWrite (function)", @@ -449,8 +472,12 @@ "getMetadataTypeActions": "src/kernel/metadata-type-schemas.ts#getMetadataTypeActions (function)", "getMetadataTypeRedactor": "src/kernel/metadata-type-redaction.ts#getMetadataTypeRedactor (function)", "getMetadataTypeSchema": "src/kernel/metadata-type-schemas.ts#getMetadataTypeSchema (function)", + "getOverrideTypeClassification": "src/kernel/customization-package.ts#getOverrideTypeClassification (function)", "isConsumerInstallable": "src/kernel/plugin.zod.ts#isConsumerInstallable (function)", + "isCustomizationPackage": "src/kernel/customization-package.ts#isCustomizationPackage (function)", + "isCustomizationPairOf": "src/kernel/customization-package.ts#isCustomizationPairOf (function)", "isKnownPlatformCapability": "src/kernel/platform-capabilities.ts#isKnownPlatformCapability (function)", + "isOverridableMetadataType": "src/kernel/customization-package.ts#isOverridableMetadataType (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)", diff --git a/packages/spec/json-schema.manifest/kernel.json b/packages/spec/json-schema.manifest/kernel.json index 2242bf2347e..89d9240c89d 100644 --- a/packages/spec/json-schema.manifest/kernel.json +++ b/packages/spec/json-schema.manifest/kernel.json @@ -80,6 +80,7 @@ "kernel/OpsDomainModule", "kernel/OpsFilePath", "kernel/OpsPluginStructure", + "kernel/OverrideDeclaration", "kernel/PackageArtifact", "kernel/PackageDependencyConflict", "kernel/PackageDependencyResolutionResult", diff --git a/packages/spec/src/kernel/customization-package.test.ts b/packages/spec/src/kernel/customization-package.test.ts index dcbde8f42be..8b96c68172a 100644 --- a/packages/spec/src/kernel/customization-package.test.ts +++ b/packages/spec/src/kernel/customization-package.test.ts @@ -150,7 +150,7 @@ describe('the pair\'s dependency range — `^` of the base version', () = }); describe('the closed overridable-type list, pinned to the registry', () => { - const registryTypes = DEFAULT_METADATA_TYPE_REGISTRY.map((e) => e.type).sort(); + const registryTypes: string[] = DEFAULT_METADATA_TYPE_REGISTRY.map((e) => e.type).sort(); const classified = Object.keys(OVERRIDE_TYPE_CLASSIFICATION).sort(); it('classifies every registry type — a type added to the registry without a row here fails', () => { @@ -173,7 +173,7 @@ describe('the closed overridable-type list, pinned to the registry', () => { }); it('a `not_runtime_creatable` row is one the registry really closes', () => { - const byType = new Map(DEFAULT_METADATA_TYPE_REGISTRY.map((e) => [e.type, e])); + const byType = new Map(DEFAULT_METADATA_TYPE_REGISTRY.map((e) => [e.type, e])); for (const [type, row] of Object.entries(OVERRIDE_TYPE_CLASSIFICATION)) { if (row.basis === 'not_runtime_creatable') expect(byType.get(type)?.allowRuntimeCreate, type).toBe(false); } diff --git a/packages/spec/src/kernel/override-declaration.test.ts b/packages/spec/src/kernel/override-declaration.test.ts index ccd3210637d..74f29e82b99 100644 --- a/packages/spec/src/kernel/override-declaration.test.ts +++ b/packages/spec/src/kernel/override-declaration.test.ts @@ -73,7 +73,7 @@ describe('OverrideDeclarationSchema — the shape', () => { }); it('publishes the refusal into the JSON Schema as a pattern (no dropped refinement)', () => { - const json = z.toJSONSchema(OverrideDeclarationSchema) as { properties: { package: { pattern?: string } } }; + const json = z.toJSONSchema(OverrideDeclarationSchema) as unknown as { properties: { package: { pattern?: string } } }; const pattern = new RegExp(json.properties.package.pattern ?? ''); expect(pattern.test('com.acme.crm')).toBe(true); expect(pattern.test(`com.acme.crm${CUSTOMIZATION_PACKAGE_SUFFIX}`)).toBe(false); diff --git a/skills/objectstack-ai/references/_index.md b/skills/objectstack-ai/references/_index.md index b62a2339f15..3f51b69e791 100644 --- a/skills/objectstack-ai/references/_index.md +++ b/skills/objectstack-ai/references/_index.md @@ -24,6 +24,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, UniqueScopeSchema - `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification - `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010) +- `node_modules/@objectstack/spec/src/kernel/override-declaration.zod.ts` — The override declaration (ADR-0146 D2) - `node_modules/@objectstack/spec/src/shared/conditional-formatting.zod.ts` — The **single declaration** of a conditional formatting rule — - `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum - `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol diff --git a/skills/objectstack-automation/references/_index.md b/skills/objectstack-automation/references/_index.md index 4a56e7aafaa..80d4ac602c8 100644 --- a/skills/objectstack-automation/references/_index.md +++ b/skills/objectstack-automation/references/_index.md @@ -27,6 +27,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/data/context-tokens.zod.ts` — Context Tokens — the declarative placeholders that resolve against the - `node_modules/@objectstack/spec/src/data/date-macros.zod.ts` — Date Macro Tokens — the declarative placeholders the UI substitutes - `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010) +- `node_modules/@objectstack/spec/src/kernel/override-declaration.zod.ts` — The override declaration (ADR-0146 D2) - `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol - `node_modules/@objectstack/spec/src/shared/identifiers.zod.ts` — Exports: SystemIdentifierSchema, SnakeCaseIdentifierSchema, MetadataItemNameSchema - `node_modules/@objectstack/spec/src/shared/protection.zod.ts` — Package-level metadata protection (ADR-0010 §3.7 — Phase 4.3) diff --git a/skills/objectstack-data/references/_index.md b/skills/objectstack-data/references/_index.md index d036e57f460..8c52375e4f4 100644 --- a/skills/objectstack-data/references/_index.md +++ b/skills/objectstack-data/references/_index.md @@ -42,6 +42,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/identity/eval-user.zod.ts` — EvalUser — the one user-context contract (ADR-0068 D1). - `node_modules/@objectstack/spec/src/kernel/execution-context.zod.ts` — Exports: AuthGateCodeSchema, AuthGateScopeSchema, ExecutionContextSchema - `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010) +- `node_modules/@objectstack/spec/src/kernel/override-declaration.zod.ts` — The override declaration (ADR-0146 D2) - `node_modules/@objectstack/spec/src/security/explain.zod.ts` — [ADR-0090 D6] Access-explanation contract — `explain(principal, object, - `node_modules/@objectstack/spec/src/security/rls.zod.ts` — Row-Level Security (RLS) Protocol - `node_modules/@objectstack/spec/src/shared/conditional-formatting.zod.ts` — The **single declaration** of a conditional formatting rule — diff --git a/skills/objectstack-ui/references/_index.md b/skills/objectstack-ui/references/_index.md index e83ae7f34c1..c37f9ef870c 100644 --- a/skills/objectstack-ui/references/_index.md +++ b/skills/objectstack-ui/references/_index.md @@ -36,6 +36,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/identity/eval-user.zod.ts` — EvalUser — the one user-context contract (ADR-0068 D1). - `node_modules/@objectstack/spec/src/kernel/execution-context.zod.ts` — Exports: AuthGateCodeSchema, AuthGateScopeSchema, ExecutionContextSchema - `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010) +- `node_modules/@objectstack/spec/src/kernel/override-declaration.zod.ts` — The override declaration (ADR-0146 D2) - `node_modules/@objectstack/spec/src/security/explain.zod.ts` — [ADR-0090 D6] Access-explanation contract — `explain(principal, object, - `node_modules/@objectstack/spec/src/security/permission.zod.ts` — Exports: ObjectAccessScopeSchema, ObjectPermissionSchema, EffectiveObjectPermissionSchema, AdminScopeSchema, FieldPermissionSchema - `node_modules/@objectstack/spec/src/security/rls.zod.ts` — Row-Level Security (RLS) Protocol From 395b5e16dfaa4643c0e0b1507b10a2425421e0c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 17:41:44 +0000 Subject: [PATCH 06/11] docs(spec): count the override-declaration module in llms.txt The kernel domain now holds 31 schema modules and the package 213. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- packages/spec/llms.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/spec/llms.txt b/packages/spec/llms.txt index 11edee3ccf2..5af8969ab70 100644 --- a/packages/spec/llms.txt +++ b/packages/spec/llms.txt @@ -77,7 +77,7 @@ const query = { --- -## 3. Schema Inventory by Domain (212 schemas) +## 3. Schema Inventory by Domain (213 schemas) Counted as `*.zod.ts` modules under `packages/spec/src//` — the sources that ship in this tarball (`files` includes `src/**/*.zod.ts`), so every number @@ -86,7 +86,7 @@ here is verifiable from the installed package. | Domain | Count | Key Schemas | |--------|-------|-------------| | system | 37 | Auth, Cache, Demo Staff, Dev Login, Email / SMS Config, Encryption, Fate Supplement (organization ownership), HTTP Server, License, Logging, Metrics | -| kernel | 30 | Plugin, Manifest, Events (6 sub-modules), Feature, Context, Package Registry | +| kernel | 31 | Plugin, Manifest, Events (6 sub-modules), Feature, Context, Package Registry, Override Declaration | | data | 32 | Object, Field, Picklist, Query, Filter, Driver (SQL/NoSQL/Memory/Mongo/Postgres), Cube, Seed | | api | 33 | Endpoint, REST Server, Discovery, OData, Batch, WebSocket, Response Envelope, Package Lifecycle, Package API (assembled stage), Record Page, App Metadata Bundle | | ui | 18 | View, App, Action, Dashboard, Page, Chart, Component, Animation | From e8d402b7e9df38624fb34358436713dc3347cb6b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 18:14:12 +0000 Subject: [PATCH 07/11] test(spec): record the override declaration in the form and flow-slot ledgers The full spec suite named three pins the new key reaches: - the metadata form reconciliation: `overrides` is accepted by eight schemas whose hand-written forms do not offer it. Each gets a root ledger row: the declaration is `planned` and Studio writes it on a customization, so a hand-filled control is not how it is authored. - the flows translation face: `flow.overrides` is a base package id and a version, never drawn for a person running the flow. - the description renderer's count of level-1 module openers: the new module's header now opens at level 2, so the count stands. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- .../src/kernel/override-declaration.zod.ts | 6 +- .../src/system/flows-translation-face.test.ts | 4 ++ .../metadata-form-zod-reconciliation.test.ts | 62 +++++++++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/packages/spec/src/kernel/override-declaration.zod.ts b/packages/spec/src/kernel/override-declaration.zod.ts index 27c44a58eda..948a4dc1062 100644 --- a/packages/spec/src/kernel/override-declaration.zod.ts +++ b/packages/spec/src/kernel/override-declaration.zod.ts @@ -4,7 +4,7 @@ import { z } from 'zod'; import { strictObject } from '../shared/strict-object'; /** - * # The override declaration (ADR-0146 D2) + * ## The override declaration (ADR-0146 D2) * * A customization of an installed base package (HotCRM) is an item of the * base's paired writable package `.custom` (ADR-0146 D3). An item of that @@ -22,7 +22,7 @@ import { strictObject } from '../shared/strict-object'; * ruling recorded on #28390, ruling A); the pairing helpers in * `customization-package.ts` are what a door asks. * - * ## Why a leaf module + * ### Why a leaf module * * It imports nothing but zod and `strictObject`, on purpose. Every overridable * type's schema spreads {@link OverrideDeclarationFields}, and some of those @@ -32,7 +32,7 @@ import { strictObject } from '../shared/strict-object'; * The helpers that need the manifest grammar live in `customization-package.ts`, * which no schema imports. * - * ## What this module does NOT do + * ### What this module does NOT do * * It declares the shape. Admitting a declared override, refusing an undeclared * same-named item, refusing a declaration from a package that is not the pair, diff --git a/packages/spec/src/system/flows-translation-face.test.ts b/packages/spec/src/system/flows-translation-face.test.ts index 246eb0722fe..a712699fa2e 100644 --- a/packages/spec/src/system/flows-translation-face.test.ts +++ b/packages/spec/src/system/flows-translation-face.test.ts @@ -116,6 +116,10 @@ const RUNNER_SLOTS: Readonly> = { 'flow._packageId': noReader(FRAMEWORK_FIELD), 'flow._packageVersion': noReader(FRAMEWORK_FIELD), 'flow._lockDocsUrl': noReader(FRAMEWORK_FIELD), + 'flow.overrides': noReader( + 'the override declaration (ADR-0146 D2): a base package id and the base version the override was taken from, ' + + 'read by the customization doors and never drawn for a person running the flow', + ), // FlowNodeSchema (its `config` is judged per type below) 'node.id': noReader('the node id — the address of `screens.`, `refusals.` and `approvals.`'), diff --git a/packages/spec/src/system/metadata-form-zod-reconciliation.test.ts b/packages/spec/src/system/metadata-form-zod-reconciliation.test.ts index eb971505f84..412bb7c1ca8 100644 --- a/packages/spec/src/system/metadata-form-zod-reconciliation.test.ts +++ b/packages/spec/src/system/metadata-form-zod-reconciliation.test.ts @@ -657,6 +657,68 @@ const LEDGER: ReadonlyArray = [ // the first arm form rather than for this ledger. `scope` has no live writer // of `shared` or `personal`, so there is no platform-written state to give as // the reason; `owner` and `hidden` are no longer writable at all. + + // ADR-0146 D2 — the override declaration lands spec-first: `planned` in the + // liveness ledger, admitted by no write gate and served by no read until the + // ADR-0146 doors land. Studio writes it when an administrator customizes a + // base item into its `.custom` package; a form control an author fills + // by hand would declare an override nothing yet honours. + { + kind: 'omit', + type: 'report', + path: ROOT_PATH, + key: 'overrides', + why: 'declared, not enforced yet — liveness verdict `planned`: the override declaration (ADR-0146 D2) parses, is stored and travels through export, but no door admits or serves it until the write gate and the effective read land. Studio writes it when it saves a customization into `.custom`; a hand-filled control is not how it is authored', + }, + { + kind: 'omit', + type: 'dataset', + path: ROOT_PATH, + key: 'overrides', + why: 'declared, not enforced yet — liveness verdict `planned`: the override declaration (ADR-0146 D2) parses, is stored and travels through export, but no door admits or serves it until the write gate and the effective read land. Studio writes it when it saves a customization into `.custom`; a hand-filled control is not how it is authored', + }, + { + kind: 'omit', + type: 'app', + path: ROOT_PATH, + key: 'overrides', + why: 'declared, not enforced yet — liveness verdict `planned`: the override declaration (ADR-0146 D2) parses, is stored and travels through export, but no door admits or serves it until the write gate and the effective read land. Studio writes it when it saves a customization into `.custom`; a hand-filled control is not how it is authored', + }, + { + kind: 'omit', + type: 'dashboard', + path: ROOT_PATH, + key: 'overrides', + why: 'declared, not enforced yet — liveness verdict `planned`: the override declaration (ADR-0146 D2) parses, is stored and travels through export, but no door admits or serves it until the write gate and the effective read land. Studio writes it when it saves a customization into `.custom`; a hand-filled control is not how it is authored', + }, + { + kind: 'omit', + type: 'action', + path: ROOT_PATH, + key: 'overrides', + why: 'declared, not enforced yet — liveness verdict `planned`: the override declaration (ADR-0146 D2) parses, is stored and travels through export, but no door admits or serves it until the write gate and the effective read land. Studio writes it when it saves a customization into `.custom`; a hand-filled control is not how it is authored', + }, + { + kind: 'omit', + type: 'page', + path: ROOT_PATH, + key: 'overrides', + why: 'declared, not enforced yet — liveness verdict `planned`: the override declaration (ADR-0146 D2) parses, is stored and travels through export, but no door admits or serves it until the write gate and the effective read land. Studio writes it when it saves a customization into `.custom`; a hand-filled control is not how it is authored', + }, + { + kind: 'omit', + type: 'flow', + path: ROOT_PATH, + key: 'overrides', + why: 'declared, not enforced yet — liveness verdict `planned`: the override declaration (ADR-0146 D2) parses, is stored and travels through export, but no door admits or serves it until the write gate and the effective read land. Studio writes it when it saves a customization into `.custom`; a hand-filled control is not how it is authored', + }, + { + kind: 'omit', + type: 'email_template', + path: ROOT_PATH, + key: 'overrides', + why: 'declared, not enforced yet — liveness verdict `planned`: the override declaration (ADR-0146 D2) parses, is stored and travels through export, but no door admits or serves it until the write gate and the effective read land. Studio writes it when it saves a customization into `.custom`; a hand-filled control is not how it is authored', + }, ]; // ──────────────────────────────────────────────────────────────────────────── From 0f9936f445609d5b56502e9ce4627a3a757c4e64 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 18:54:56 +0000 Subject: [PATCH 08/11] chore(spec): regenerate artefacts on the merged tree Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- content/docs/references/index.mdx | 10 +++++----- content/docs/references/ui/report.mdx | 4 ++-- packages/spec/authorable-surface/api.json | 2 ++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index c4e28bec71b..6886564a0e8 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,7 +1,7 @@ --- title: Protocol reference — every schema by module navTitle: Protocol Reference -description: Every schema published by @objectstack/spec — 1574 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1575 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -21,7 +21,7 @@ counts are sums of the rows they head. Regenerate with | Module | Pages | Schemas | Description | | :--- | ---: | ---: | :--- | | [AI Protocol](/docs/references/ai) | 12 | 68 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. | -| [API Protocol](/docs/references/api) | 34 | 452 | REST contracts, endpoints, routing, realtime, batch, discovery. | +| [API Protocol](/docs/references/api) | 34 | 453 | REST contracts, endpoints, routing, realtime, batch, discovery. | | [Automation Protocol](/docs/references/automation) | 15 | 77 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. | | [Data Protocol](/docs/references/data) | 31 | 174 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. | | [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. | @@ -34,7 +34,7 @@ counts are sums of the rows they head. Regenerate with | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 37 | 287 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | | [UI Protocol](/docs/references/ui) | 16 | 174 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **205** | **1574** | 14 protocol modules | +| **Total** | **205** | **1575** | 14 protocol modules | --- @@ -63,7 +63,7 @@ Agents, tools, skills, RAG and knowledge sources, model registry, conversations. ## API Protocol -**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **34 pages, 452 schemas** +**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **34 pages, 453 schemas** REST contracts, endpoints, routing, realtime, batch, discovery. @@ -92,7 +92,7 @@ REST contracts, endpoints, routing, realtime, batch, discovery. | [`package-api-assembled.zod.ts`](/docs/references/api/package-api-assembled) | `AssembledInstalledPackage`, `GetInstalledPackageResponse`, `InstalledPackageAtEitherStage`, `ListInstalledPackagesResponse` | | [`package-lifecycle.zod.ts`](/docs/references/api/package-lifecycle) | `DiscardPackageDraftsResponse`, `DuplicatePackageResponse`, `ImportUnmanagedPackageRequest`, `ImportUnmanagedPackageResponse`, `ListPackageCommitsResponse`, `PackageExportManifest`, `PackagePublishResult`, `ReassignOrphanedMetadataResponse`, `RevertPackageCommitResponse`, `RollbackToPackageCommitResponse` | | [`plugin-rest-api.zod.ts`](/docs/references/api/plugin-rest-api) | `ErrorHandlingConfig`, `OpenApiGenerationConfig`, `RequestValidationConfig`, `ResponseEnvelopeConfig`, `RestApiEndpoint`, `RestApiPluginConfig`, `RestApiRouteCategory`, `RestApiRouteRegistration`, `ValidationMode` | -| [`protocol.zod.ts`](/docs/references/api/protocol) | `AiAgentCapabilities`, `AiAgentChatRequest`, `AiAgentSummary`, `AiAgentsResponse`, `AiChatRequest`, `AiChatResponse`, `AiCompleteRequest`, `AiConversation`, `AiMessage`, `AiModelsResponse`, `AiPendingAction`, `AiPendingActionStatus`, `AiStreamChunk`, `ApproveAiPendingActionResponse`, `AuditMetaItemRequest`, `AuditMetaItemResponse`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CloneDataResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DiffMetaItemResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `FindReferencesToMetaResponse`, `FlowRegistration`, `FlowRegistrationRefusal`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaDiagnosticsResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemLayeredRequest`, `GetMetaItemLayeredResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetPublishedMetaItemResponse`, `GetRecordFieldAccessResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HistoryMetaItemRequest`, `HistoryMetaItemResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListDraftsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `MetadataAuthoringClient`, `MetadataAuthoringSurface`, `MetadataConflictError`, `Notification`, `NotificationPreferences`, `PublishMetaItemRequest`, `PublishMetaItemResponse`, `PublishPackageDraftsResponse`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `RollbackMetaItemResponse`, `RuntimeAuthoringIssue`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SearchAllHit`, `SearchAllPageHit`, `SearchAllResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `ValidateDataIssue`, `ValidateDataRequest`, `ValidateDataResponse` | +| [`protocol.zod.ts`](/docs/references/api/protocol) | `AiAgentCapabilities`, `AiAgentChatRequest`, `AiAgentSummary`, `AiAgentsResponse`, `AiChatRequest`, `AiChatResponse`, `AiCompleteRequest`, `AiConversation`, `AiMessage`, `AiModelsResponse`, `AiPendingAction`, `AiPendingActionStatus`, `AiStreamChunk`, `ApproveAiPendingActionResponse`, `AuditMetaItemRequest`, `AuditMetaItemResponse`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CloneDataResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DiffMetaItemResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `FindReferencesToMetaResponse`, `FlowRegistration`, `FlowRegistrationRefusal`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaDiagnosticsResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemLayeredRequest`, `GetMetaItemLayeredResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetPublishedMetaItemResponse`, `GetRecordFieldAccessResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HistoryMetaItemRequest`, `HistoryMetaItemResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListDraftsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `MetadataAuthoringClient`, `MetadataAuthoringSurface`, `MetadataConflictError`, `Notification`, `NotificationPreferences`, `PublishMetaItemRequest`, `PublishMetaItemResponse`, `PublishPackageDraftsRequest`, `PublishPackageDraftsResponse`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `RollbackMetaItemResponse`, `RuntimeAuthoringIssue`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SearchAllHit`, `SearchAllPageHit`, `SearchAllResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `ValidateDataIssue`, `ValidateDataRequest`, `ValidateDataResponse` | | [`query-adapter.zod.ts`](/docs/references/api/query-adapter) | `ODataQueryAdapter`, `OperatorMapping`, `QueryAdapterConfig`, `QueryAdapterTarget`, `RestQueryAdapter` | | [`realtime.zod.ts`](/docs/references/api/realtime) | `RealtimeConfig`, `RealtimeEvent`, `RealtimeEventType`, `RealtimePresence`, `Subscription`, `SubscriptionEvent`, `TransportProtocol` | | [`realtime-shared.zod.ts`](/docs/references/api/realtime-shared) | `BasePresence`, `PresenceStatus`, `RealtimeRecordAction` | diff --git a/content/docs/references/ui/report.mdx b/content/docs/references/ui/report.mdx index ac886532efb..a738489a46d 100644 --- a/content/docs/references/ui/report.mdx +++ b/content/docs/references/ui/report.mdx @@ -105,7 +105,7 @@ const result = JoinedReportBlockSchema.parse(data); | **annotations** | `{ type?: Enum<'line' \| 'region'>; axis?: Enum<'x' \| 'y'>; value: number \| string; endValue?: number \| string; … }[]` | optional | Reference lines/bands drawn over the plot: `{ type: "line" \| "region", axis: "x" \| "y", value, endValue?, color?, label?, style? }` | | **interaction** | `{ tooltips?: boolean; brush?: boolean }` | optional | Interaction toggles: `{ tooltips?, brush? }` | | **aria** | `never` | optional | [REMOVED] `ChartConfig.aria` — authored as `dashboard.widgets[].chartConfig.aria`, `report.chart.aria` and `report.blocks[].chart.aria` — was removed in @objectstack/spec 17 (ADR-0049 D2). No chart renderer ever applied it: the chart implementation declares no `aria` prop, the presentation lowering names it nowhere, and the react `` block never published it, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied on this same chart config is its sibling `description`, which the chart renderer lowers onto the chart graphic as `role="img"` plus `aria-label`. The shared `AriaProps` shape is NOT gone — `ariaLabel` / `ariaDescribedBy` / `role` stay live in the `aria` block on `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | -| **seriesBy** | `string` | optional | Dataset dimension name that splits the yAxis measure into one series per value of it (bound-dataset dimension, not a raw field); with it set, `series[].name` names a value of this dimension | +| **seriesBy** | `string` | optional | Dataset dimension name that splits the yAxis measure into one series per value of it (bound-dataset dimension, not a raw field); with it set, `series[].name` names a value of this dimension. Refused on a single-series type (pie/donut/funnel/treemap/sankey) and when it names the xAxis dimension | ### Nested Shape: `Report.dateRange` @@ -176,7 +176,7 @@ const result = JoinedReportBlockSchema.parse(data); | **annotations** | `{ type?: Enum<'line' \| 'region'>; axis?: Enum<'x' \| 'y'>; value: number \| string; endValue?: number \| string; … }[]` | optional | Reference lines/bands drawn over the plot: `{ type: "line" \| "region", axis: "x" \| "y", value, endValue?, color?, label?, style? }` | | **interaction** | `{ tooltips?: boolean; brush?: boolean }` | optional | Interaction toggles: `{ tooltips?, brush? }` | | **aria** | `never` | optional | [REMOVED] `ChartConfig.aria` — authored as `dashboard.widgets[].chartConfig.aria`, `report.chart.aria` and `report.blocks[].chart.aria` — was removed in @objectstack/spec 17 (ADR-0049 D2). No chart renderer ever applied it: the chart implementation declares no `aria` prop, the presentation lowering names it nowhere, and the react `` block never published it, so ARIA attributes declared here parsed and then silently did not reach the DOM. Delete the key. The accessible name that IS applied on this same chart config is its sibling `description`, which the chart renderer lowers onto the chart graphic as `role="img"` plus `aria-label`. The shared `AriaProps` shape is NOT gone — `ariaLabel` / `ariaDescribedBy` / `role` stay live in the `aria` block on `page.aria`, `page.components[].aria` and the list view `aria`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. | -| **seriesBy** | `string` | optional | Dataset dimension name that splits the yAxis measure into one series per value of it (bound-dataset dimension, not a raw field); with it set, `series[].name` names a value of this dimension | +| **seriesBy** | `string` | optional | Dataset dimension name that splits the yAxis measure into one series per value of it (bound-dataset dimension, not a raw field); with it set, `series[].name` names a value of this dimension. Refused on a single-series type (pie/donut/funnel/treemap/sankey) and when it names the xAxis dimension | ### Allowed Values: `ReportChart.type` diff --git a/packages/spec/authorable-surface/api.json b/packages/spec/authorable-surface/api.json index 7b54a642f5f..fe5aa937053 100644 --- a/packages/spec/authorable-surface/api.json +++ b/packages/spec/authorable-surface/api.json @@ -1371,6 +1371,8 @@ "api/PublishMetaItemResponse:seq", "api/PublishMetaItemResponse:success", "api/PublishMetaItemResponse:version", + "api/PublishPackageDraftsRequest:backfill", + "api/PublishPackageDraftsRequest:message", "api/PublishPackageDraftsResponse:backfillApplied", "api/PublishPackageDraftsResponse:commitId", "api/PublishPackageDraftsResponse:failed", From e0253ce45c42f6688e112e25a7eed70b16de77c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 19:45:38 +0000 Subject: [PATCH 09/11] chore(spec): regenerate artefacts on the merged tree (app landing) Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- content/docs/references/api/metadata.mdx | 1 + content/docs/references/index.mdx | 10 ++++----- content/docs/references/ui/app.mdx | 26 +++++++++++++++++++--- packages/spec/authorable-surface/ui.json | 3 +++ packages/spec/liveness/state-counts/app.md | 2 +- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/content/docs/references/api/metadata.mdx b/content/docs/references/api/metadata.mdx index 47874fadcfd..b9512e01ff0 100644 --- a/content/docs/references/api/metadata.mdx +++ b/content/docs/references/api/metadata.mdx @@ -95,6 +95,7 @@ const result = AppDefinitionResponseSchema.parse(data); | **navigation** | `({ id: string; label?: string \| Record; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +8 more)[]` | optional | Full navigation tree for the app sidebar | | **areas** | `{ id: string; label: string \| Record; icon?: string; description?: string \| Record; … }[]` | optional | Navigation areas for partitioning navigation by business domain | | **contextSelectors** | `{ id: string; label: string \| Record; icon?: string; optionsSource: object; … }[]` | optional | App-level scope dropdowns whose value is injected into nav items as `{}` template vars | +| **landing** | `{ item: string; visible: string \| object }[]` | optional | [PLANNED — not yet rendered] Per-audience landing: the first entry whose `visible` (CEL) holds for the reader names the navigation entry (`item`, an `id` of this app's own `navigation` or `areas[].navigation`) the app opens on. An `item` that names no entry of this app, or a `group` / `separator` / `action` entry, is refused at parse. No match, or no key: the app opens on its first navigation item by `order`, as before. No console reads this list yet, so until the landing redirect ships every reader still opens on the first navigation item. | | **requiredPermissions** | `string[]` | optional | Permissions required to access this app | | **defaultAgent** | `string` | optional | Platform agent bound to this app's ambient chat ('ask' is the implicit default; 'build' for authoring surfaces) — ADR-0063 §1 | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this app. | diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx index 6886564a0e8..4797af599fb 100644 --- a/content/docs/references/index.mdx +++ b/content/docs/references/index.mdx @@ -1,7 +1,7 @@ --- title: Protocol reference — every schema by module navTitle: Protocol Reference -description: Every schema published by @objectstack/spec — 1575 schemas across 14 protocol modules +description: Every schema published by @objectstack/spec — 1576 schemas across 14 protocol modules --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -33,8 +33,8 @@ counts are sums of the rows they head. Regenerate with | [Shared Protocol](/docs/references/shared) | 10 | 31 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. | | [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. | | [System Protocol](/docs/references/system) | 37 | 287 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. | -| [UI Protocol](/docs/references/ui) | 16 | 174 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | -| **Total** | **205** | **1575** | 14 protocol modules | +| [UI Protocol](/docs/references/ui) | 16 | 175 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. | +| **Total** | **205** | **1576** | 14 protocol modules | --- @@ -372,7 +372,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a ## UI Protocol -**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 174 schemas** +**Source:** `packages/spec/src/ui/` · **Import:** `@objectstack/spec/ui` · **16 pages, 175 schemas** Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. @@ -380,7 +380,7 @@ Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI lay | :--- | :--- | | [`action.zod.ts`](/docs/references/ui/action) | `Action`, `ActionAi`, `ActionLocation`, `ActionParam`, `ActionType`, `InlineAction` | | [`action-params.zod.ts`](/docs/references/ui/action-params) | `ActionSession` | -| [`app.zod.ts`](/docs/references/ui/app) | `ActionNavItem`, `App`, `AppBranding`, `AppContextSelector`, `ComponentNavItem`, `DashboardNavItem`, `DocNavItem`, `GroupNavItem`, `NavigationArea`, `NavigationContribution`, `NavigationItem`, `ObjectNavItem`, `PageNavItem`, `ReportNavItem`, `UrlNavItem` | +| [`app.zod.ts`](/docs/references/ui/app) | `ActionNavItem`, `App`, `AppBranding`, `AppContextSelector`, `AppLandingEntry`, `ComponentNavItem`, `DashboardNavItem`, `DocNavItem`, `GroupNavItem`, `NavigationArea`, `NavigationContribution`, `NavigationItem`, `ObjectNavItem`, `PageNavItem`, `ReportNavItem`, `UrlNavItem` | | [`bulk-action.zod.ts`](/docs/references/ui/bulk-action) | `BulkActionDef`, `BulkActionExecution`, `BulkActionOperation`, `BulkActionParam` | | [`chart.zod.ts`](/docs/references/ui/chart) | `ChartAggregate`, `ChartAggregateFunction`, `ChartAnnotation`, `ChartAxis`, `ChartConfig`, `ChartDrillDown`, `ChartGroupBy`, `ChartInteraction`, `ChartSeries`, `ChartType` | | [`component.zod.ts`](/docs/references/ui/component) | `ActionButtonProps`, `ActionGroupProps`, `ActionIconProps`, `ActionMenuProps`, `ElementButtonProps`, `ElementDefinitionListProps`, `ElementFilterProps`, `ElementFormProps`, `ElementImageProps`, `ElementMetadataViewerProps`, `ElementNumberProps`, `ElementRecordPickerProps`, `ElementRepeaterProps`, `ElementTextInputProps`, `ElementTextProps`, `ObjectCalendarProps`, `ObjectFormProps`, `ObjectGanttProps`, `ObjectGridProps`, `ObjectKanbanProps`, `ObjectMapProps`, `ObjectMasterDetailFormProps`, `ObjectMetricProps`, `ObjectTimelineProps`, `ObjectTreeProps`, `PageAccordionProps`, `PageCardProps`, `PageContainerProps`, `PageHeaderProps`, `PageTabsProps`, `RecordActivityProps`, `RecordAlertAction`, `RecordAlertProps`, `RecordChatterProps`, `RecordDetailsProps`, `RecordHighlightsField`, `RecordHighlightsProps`, `RecordHistoryProps`, `RecordLineItemsProps`, `RecordPathProps`, `RecordQuickActionsProps`, `RecordReferenceRailProps`, `RecordRelatedListProps`, `ReferenceRailEntry` | diff --git a/content/docs/references/ui/app.mdx b/content/docs/references/ui/app.mdx index 025b92dafde..9eb6e1d0ede 100644 --- a/content/docs/references/ui/app.mdx +++ b/content/docs/references/ui/app.mdx @@ -1,7 +1,7 @@ --- title: App schema — UI Protocol property reference navTitle: App -description: "App schemas of the ObjectStack UI Protocol: ActionNavItem, App, AppBranding and 12 more — each property with its type, default and a TypeScript example." +description: "App schemas of the ObjectStack UI Protocol: ActionNavItem, App, AppBranding and 13 more — each property with its type, default and a TypeScript example." --- {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} @@ -13,8 +13,8 @@ description: "App schemas of the ObjectStack UI Protocol: ActionNavItem, App, Ap ## TypeScript Usage ```typescript -import { ActionNavItemSchema, AppSchema, AppBrandingSchema, AppContextSelectorSchema, ComponentNavItemSchema, DashboardNavItemSchema, DocNavItemSchema, GroupNavItemSchema, NavigationAreaSchema, NavigationContributionSchema, NavigationItemSchema, ObjectNavItemSchema, PageNavItemSchema, ReportNavItemSchema, UrlNavItemSchema } from '@objectstack/spec/ui'; -import type { ActionNavItem, App, AppBranding, AppContextSelector, ComponentNavItem, DashboardNavItem, DocNavItem, GroupNavItem, NavigationArea, NavigationContribution, NavigationItem, ObjectNavItem, PageNavItem, ReportNavItem, UrlNavItem } from '@objectstack/spec/ui'; +import { ActionNavItemSchema, AppSchema, AppBrandingSchema, AppContextSelectorSchema, AppLandingEntrySchema, ComponentNavItemSchema, DashboardNavItemSchema, DocNavItemSchema, GroupNavItemSchema, NavigationAreaSchema, NavigationContributionSchema, NavigationItemSchema, ObjectNavItemSchema, PageNavItemSchema, ReportNavItemSchema, UrlNavItemSchema } from '@objectstack/spec/ui'; +import type { ActionNavItem, App, AppBranding, AppContextSelector, AppLandingEntry, ComponentNavItem, DashboardNavItem, DocNavItem, GroupNavItem, NavigationArea, NavigationContribution, NavigationItem, ObjectNavItem, PageNavItem, ReportNavItem, UrlNavItem } from '@objectstack/spec/ui'; // Validate data const result = ActionNavItemSchema.parse(data); @@ -78,6 +78,7 @@ const result = ActionNavItemSchema.parse(data); | **navigation** | `({ id: string; label?: string \| Record; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +8 more)[]` | optional | Full navigation tree for the app sidebar | | **areas** | `{ id: string; label: string \| Record; icon?: string; description?: string \| Record; … }[]` | optional | Navigation areas for partitioning navigation by business domain | | **contextSelectors** | `{ id: string; label: string \| Record; icon?: string; optionsSource: object; … }[]` | optional | App-level scope dropdowns whose value is injected into nav items as `{}` template vars | +| **landing** | `{ item: string; visible: string \| object }[]` | optional | [PLANNED — not yet rendered] Per-audience landing: the first entry whose `visible` (CEL) holds for the reader names the navigation entry (`item`, an `id` of this app's own `navigation` or `areas[].navigation`) the app opens on. An `item` that names no entry of this app, or a `group` / `separator` / `action` entry, is refused at parse. No match, or no key: the app opens on its first navigation item by `order`, as before. No console reads this list yet, so until the landing redirect ships every reader still opens on the first navigation item. | | **requiredPermissions** | `string[]` | optional | Permissions required to access this app | | **defaultAgent** | `string` | optional | Platform agent bound to this app's ambient chat ('ask' is the implicit default; 'build' for authoring surfaces) — ADR-0063 §1 | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this app. | @@ -303,6 +304,13 @@ Documentation entry on the app menu (ADR-0046). Targets a `book` and/or a `doc` | **allValue** | `string` | optional (default: `""`) | Sentinel value meaning "no concrete selection yet" (empty string is almost always right) | | **persist** | `Enum<'query' \| 'session' \| 'none'>` | optional (default: `"query"`) | Persist selection via URL query, sessionStorage, or not at all | +### Nested Shape: `App.landing[number]` + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **item** | `string` | ✅ | Id of the app's own navigation entry this audience lands on (any depth of `navigation` or `areas[].navigation`). Refused at parse when no such entry exists, or when it is a `group`, `separator` or `action` entry, which open nothing. | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | ✅ | Audience predicate (CEL), the dialect of a navigation entry's `visible`, e.g. P`'service_agent' in current_user.positions`. Required. It reads the signed-in reader (`current_user`, `features`); there is no `data` here, and a `visible` that reads `data` is refused at parse. | + ### Nested Shape: `App.protection` | Property | Type | Required | Description | @@ -358,6 +366,18 @@ Documentation entry on the app menu (ADR-0046). Targets a `book` and/or a `doc` | **filter** | `{ key: string; op?: Enum<'eq' \| 'ne' \| 'in' \| 'nin'>; value: string \| string[] }[]` | optional | Predicates (AND) each option row must satisfy | +--- + +## AppLandingEntry + +### Properties + +| Property | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| **item** | `string` | ✅ | Id of the app's own navigation entry this audience lands on (any depth of `navigation` or `areas[].navigation`). Refused at parse when no such entry exists, or when it is a `group`, `separator` or `action` entry, which open nothing. | +| **visible** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | ✅ | Audience predicate (CEL), the dialect of a navigation entry's `visible`, e.g. P`'service_agent' in current_user.positions`. Required. It reads the signed-in reader (`current_user`, `features`); there is no `data` here, and a `visible` that reads `data` is refused at parse. | + + --- ## ComponentNavItem diff --git a/packages/spec/authorable-surface/ui.json b/packages/spec/authorable-surface/ui.json index 4574b3a54b1..e7a62553055 100644 --- a/packages/spec/authorable-surface/ui.json +++ b/packages/spec/authorable-surface/ui.json @@ -183,6 +183,7 @@ "ui/App:icon", "ui/App:isDefault", "ui/App:label", + "ui/App:landing", "ui/App:name", "ui/App:navigation", "ui/App:overrides", @@ -198,6 +199,8 @@ "ui/AppContextSelector:label", "ui/AppContextSelector:optionsSource", "ui/AppContextSelector:persist", + "ui/AppLandingEntry:item", + "ui/AppLandingEntry:visible", "ui/AppearanceConfig:allowedVisualizations", "ui/AppearanceConfig:showDescription", "ui/AriaProps:ariaDescribedBy", diff --git a/packages/spec/liveness/state-counts/app.md b/packages/spec/liveness/state-counts/app.md index 0e3986f5688..40afbeaa9d2 100644 --- a/packages/spec/liveness/state-counts/app.md +++ b/packages/spec/liveness/state-counts/app.md @@ -12,4 +12,4 @@ committed anywhere: `check:liveness` sums the shards when it reads them. | Type | live | exp | elsewhere | dead | planned | classified | |---|---|---|---|---|---|---| -| `app` | 51 | 0 | 0 | 0 | 6 | 57 | +| `app` | 51 | 0 | 0 | 0 | 8 | 59 | From e55a4e7c6bfc8691a20fa79031d88d083e14c2d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 20:44:22 +0000 Subject: [PATCH 10/11] test(metadata-protocol): count `overrides` in the served /meta/types property pins The override declaration adds one top-level property to `action`, `app`, `dashboard`, `dataset`, `flow`, `page` and `report`; the degenerate-derivation suite pins each served count, so each moves by one and the `action` sample names the new key. The excluded types keep their counts. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- ...ocol.meta-types-degenerate-derivation.test.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts b/packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts index 589130330d1..5d85f4546de 100644 --- a/packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts +++ b/packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts @@ -259,10 +259,13 @@ describe('#17501 — /meta/types serves a real schema for `action`, and moves no // joined the accepted set, and the sample names it. // [#27944] 49 → 50: `submitText` (the param dialog's submit button // label) joined the accepted set, and the sample names it. - expect(Object.keys(properties).length + retiredTopLevelCount('action')).toBe(50); + // [#28404] 50 → 51: `overrides` (the ADR-0146 D2 override declaration + // a `.custom` item carries) joined the accepted set, and the + // sample names it. + expect(Object.keys(properties).length + retiredTopLevelCount('action')).toBe(51); // A sample an author would actually address, and the one #17500's // repeater titles need a node to sit on. - for (const key of ['name', 'label', 'objectName', 'type', 'params', 'locations', 'outcomeMessages', 'requiresMembershipReach', 'disabledReason', 'pressed', 'submitText']) { + for (const key of ['name', 'label', 'objectName', 'type', 'params', 'locations', 'outcomeMessages', 'requiresMembershipReach', 'disabledReason', 'pressed', 'submitText', 'overrides']) { expect(Object.keys(properties)).toContain(key); } }); @@ -357,9 +360,14 @@ describe('#17501 — /meta/types serves a real schema for `action`, and moves no // [#27945] `field` 80 → 81: `lookupAutofill`, the fields a pick in a // lookup fills. // [#28022] `app` 22 → 23: `landing`, the per-audience landing list. + // [#28404] `app` 23 → 24, `dashboard` 20 → 21, `dataset` 16 → 17, `flow` + // 22 → 23, `page` 26 → 27, `report` 23 → 24: `overrides`, the ADR-0146 D2 + // override declaration, on each overridable type. The excluded types + // (`agent`, `field`, `hook`, `object`, `position`, `skill`, `tool`) do not + // gain it, and their counts stay. const CARD_PROPERTY_COUNTS: Record = { - agent: 24, app: 23, dashboard: 20, dataset: 16, field: 81, flow: 22, - hook: 23, object: 49, page: 26, position: 13, report: 23, skill: 16, tool: 14, + agent: 24, app: 24, dashboard: 21, dataset: 17, field: 81, flow: 23, + hook: 23, object: 49, page: 27, position: 13, report: 24, skill: 16, tool: 14, }; it.each(Object.entries(CARD_PROPERTY_COUNTS))( From 5d79f48d2f424d5c142928d8bb8d69d412aa9f98 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 20:58:20 +0000 Subject: [PATCH 11/11] feat(spec): record the maintainer's rulings in the override classification and `basedOn` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The maintainer agreed the seven open points on #28404. Two wording changes follow from that ruling: - `OVERRIDE_TYPE_CLASSIFICATION`: `seed`, `datasource`, `tool` and `skill` stay `excluded`, but on a decided basis instead of `undecided`. `adr_0126_one_shot` (seed), `setup_owned_connection` (datasource: ADR-0146 D1 plus ADR-0126 §3 origin-gated) and `behavioural_switch_first` (tool, skill: action's path). The `undecided` basis is gone, and a test pins that no row carries it. - `overrides.basedOn` records the base package's `manifest.version` the override was taken from, not a history version of the item. It is still recorded and never judged. Reference pages regenerated. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V --- .../docs/references/api/automation-api.mdx | 2 +- content/docs/references/automation/flow.mdx | 2 +- content/docs/references/data/mapping.mdx | 2 +- .../kernel/override-declaration.mdx | 4 +- content/docs/references/system/book.mdx | 2 +- content/docs/references/system/doc.mdx | 2 +- .../docs/references/system/email-template.mdx | 2 +- .../docs/references/system/translation.mdx | 2 +- content/docs/references/ui/action.mdx | 2 +- content/docs/references/ui/app.mdx | 2 +- content/docs/references/ui/dashboard.mdx | 2 +- content/docs/references/ui/dataset.mdx | 2 +- content/docs/references/ui/page.mdx | 2 +- content/docs/references/ui/report.mdx | 2 +- content/docs/references/ui/view.mdx | 8 ++-- .../src/kernel/customization-package.test.ts | 16 +++++--- .../spec/src/kernel/customization-package.ts | 40 +++++++++++++------ .../src/kernel/override-declaration.zod.ts | 5 ++- 18 files changed, 60 insertions(+), 39 deletions(-) diff --git a/content/docs/references/api/automation-api.mdx b/content/docs/references/api/automation-api.mdx index ce42af6f00d..e584efbe99d 100644 --- a/content/docs/references/api/automation-api.mdx +++ b/content/docs/references/api/automation-api.mdx @@ -192,7 +192,7 @@ const result = AutomationApiErrorCode.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | -| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | +| **basedOn** | `string` | ✅ | Recorded, never judged: the base package's `manifest.version` this override was taken from (e.g. "3.2.0"), not a history version of the item. No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | --- diff --git a/content/docs/references/automation/flow.mdx b/content/docs/references/automation/flow.mdx index 75cc8557d5b..7cfead5157c 100644 --- a/content/docs/references/automation/flow.mdx +++ b/content/docs/references/automation/flow.mdx @@ -113,7 +113,7 @@ const result = FlowSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | -| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | +| **basedOn** | `string` | ✅ | Recorded, never judged: the base package's `manifest.version` this override was taken from (e.g. "3.2.0"), not a history version of the item. No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | --- diff --git a/content/docs/references/data/mapping.mdx b/content/docs/references/data/mapping.mdx index 0b9f9e14fbb..43d2c250610 100644 --- a/content/docs/references/data/mapping.mdx +++ b/content/docs/references/data/mapping.mdx @@ -83,7 +83,7 @@ const result = ImportFieldMappingSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | -| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | +| **basedOn** | `string` | ✅ | Recorded, never judged: the base package's `manifest.version` this override was taken from (e.g. "3.2.0"), not a history version of the item. No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | --- diff --git a/content/docs/references/kernel/override-declaration.mdx b/content/docs/references/kernel/override-declaration.mdx index 20eb6368a1f..b4141b8d6b5 100644 --- a/content/docs/references/kernel/override-declaration.mdx +++ b/content/docs/references/kernel/override-declaration.mdx @@ -13,7 +13,7 @@ base's paired writable package `.custom` (ADR-0146 D3). An item of that package that bears a base item's name and carries ```ts -overrides: { package: '', basedOn: '' } +overrides: { package: '', basedOn: '' } ``` replaces the base's item WHOLE for every read that serves the base's name @@ -68,7 +68,7 @@ Override declaration (ADR-0146 D2): this item, of the base's paired "``.cu | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | -| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | +| **basedOn** | `string` | ✅ | Recorded, never judged: the base package's `manifest.version` this override was taken from (e.g. "3.2.0"), not a history version of the item. No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | --- diff --git a/content/docs/references/system/book.mdx b/content/docs/references/system/book.mdx index 7aa67e34db8..7717355315d 100644 --- a/content/docs/references/system/book.mdx +++ b/content/docs/references/system/book.mdx @@ -80,7 +80,7 @@ const result = BookSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | -| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | +| **basedOn** | `string` | ✅ | Recorded, never judged: the base package's `manifest.version` this override was taken from (e.g. "3.2.0"), not a history version of the item. No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | --- diff --git a/content/docs/references/system/doc.mdx b/content/docs/references/system/doc.mdx index d3d7e7e7472..d4e125bc65b 100644 --- a/content/docs/references/system/doc.mdx +++ b/content/docs/references/system/doc.mdx @@ -71,7 +71,7 @@ const result = DocSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | -| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | +| **basedOn** | `string` | ✅ | Recorded, never judged: the base package's `manifest.version` this override was taken from (e.g. "3.2.0"), not a history version of the item. No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | --- diff --git a/content/docs/references/system/email-template.mdx b/content/docs/references/system/email-template.mdx index 72ff3bb403f..530e3527807 100644 --- a/content/docs/references/system/email-template.mdx +++ b/content/docs/references/system/email-template.mdx @@ -90,7 +90,7 @@ const result = EmailTemplateDefinitionSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | -| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | +| **basedOn** | `string` | ✅ | Recorded, never judged: the base package's `manifest.version` this override was taken from (e.g. "3.2.0"), not a history version of the item. No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | --- diff --git a/content/docs/references/system/translation.mdx b/content/docs/references/system/translation.mdx index 46f9d8922bd..b18e87f2c45 100644 --- a/content/docs/references/system/translation.mdx +++ b/content/docs/references/system/translation.mdx @@ -953,7 +953,7 @@ Translation data for a single object | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | -| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | +| **basedOn** | `string` | ✅ | Recorded, never judged: the base package's `manifest.version` this override was taken from (e.g. "3.2.0"), not a history version of the item. No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | --- diff --git a/content/docs/references/ui/action.mdx b/content/docs/references/ui/action.mdx index 0e417215ec7..0df121b50a5 100644 --- a/content/docs/references/ui/action.mdx +++ b/content/docs/references/ui/action.mdx @@ -187,7 +187,7 @@ L2 sandboxed JS body — runs inside an isolated VM with declared capabilities | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | -| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | +| **basedOn** | `string` | ✅ | Recorded, never judged: the base package's `manifest.version` this override was taken from (e.g. "3.2.0"), not a history version of the item. No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | --- diff --git a/content/docs/references/ui/app.mdx b/content/docs/references/ui/app.mdx index 9eb6e1d0ede..31eca3fb74a 100644 --- a/content/docs/references/ui/app.mdx +++ b/content/docs/references/ui/app.mdx @@ -324,7 +324,7 @@ Documentation entry on the app menu (ADR-0046). Targets a `book` and/or a `doc` | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | -| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | +| **basedOn** | `string` | ✅ | Recorded, never judged: the base package's `manifest.version` this override was taken from (e.g. "3.2.0"), not a history version of the item. No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | --- diff --git a/content/docs/references/ui/dashboard.mdx b/content/docs/references/ui/dashboard.mdx index cbbe8268642..596513583d6 100644 --- a/content/docs/references/ui/dashboard.mdx +++ b/content/docs/references/ui/dashboard.mdx @@ -116,7 +116,7 @@ const result = DashboardSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | -| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | +| **basedOn** | `string` | ✅ | Recorded, never judged: the base package's `manifest.version` this override was taken from (e.g. "3.2.0"), not a history version of the item. No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | --- diff --git a/content/docs/references/ui/dataset.mdx b/content/docs/references/ui/dataset.mdx index 6674b7c8e6b..3035ae51e23 100644 --- a/content/docs/references/ui/dataset.mdx +++ b/content/docs/references/ui/dataset.mdx @@ -106,7 +106,7 @@ const result = DatasetSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | -| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | +| **basedOn** | `string` | ✅ | Recorded, never judged: the base package's `manifest.version` this override was taken from (e.g. "3.2.0"), not a history version of the item. No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | --- diff --git a/content/docs/references/ui/page.mdx b/content/docs/references/ui/page.mdx index c85dc2a4f80..fd6124f2cb0 100644 --- a/content/docs/references/ui/page.mdx +++ b/content/docs/references/ui/page.mdx @@ -328,7 +328,7 @@ View filter rule | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | -| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | +| **basedOn** | `string` | ✅ | Recorded, never judged: the base package's `manifest.version` this override was taken from (e.g. "3.2.0"), not a history version of the item. No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | --- diff --git a/content/docs/references/ui/report.mdx b/content/docs/references/ui/report.mdx index a738489a46d..35a1bc45c29 100644 --- a/content/docs/references/ui/report.mdx +++ b/content/docs/references/ui/report.mdx @@ -151,7 +151,7 @@ const result = JoinedReportBlockSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | -| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | +| **basedOn** | `string` | ✅ | Recorded, never judged: the base package's `manifest.version` this override was taken from (e.g. "3.2.0"), not a history version of the item. No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | --- diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 8d92c6acbd5..fcb071c42c7 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -2173,7 +2173,7 @@ This schema accepts one of the following structures: | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | -| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | +| **basedOn** | `string` | ✅ | Recorded, never judged: the base package's `manifest.version` this override was taken from (e.g. "3.2.0"), not a history version of the item. No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | --- @@ -2244,7 +2244,7 @@ This schema accepts one of the following structures: | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | -| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | +| **basedOn** | `string` | ✅ | Recorded, never judged: the base package's `manifest.version` this override was taken from (e.g. "3.2.0"), not a history version of the item. No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | --- @@ -2361,7 +2361,7 @@ This schema accepts one of the following structures: | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | -| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | +| **basedOn** | `string` | ✅ | Recorded, never judged: the base package's `manifest.version` this override was taken from (e.g. "3.2.0"), not a history version of the item. No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | ### Nested Shape: `ViewItemWire[viewKind='list'].columnState` @@ -2444,7 +2444,7 @@ This schema accepts one of the following structures: | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **package** | `string` | ✅ | The base package id whose same-named item this item replaces whole (e.g. "com.acme.crm"). Only an item of the base's paired "``.custom" package declares an override. | -| **basedOn** | `string` | ✅ | Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | +| **basedOn** | `string` | ✅ | Recorded, never judged: the base package's `manifest.version` this override was taken from (e.g. "3.2.0"), not a history version of the item. No door compares it with the installed base, no upgrade diff is produced from it, and nothing is refused on it; it tells a person re-basing the override where it started (ADR-0146 D2). | ### Nested Shape: `ViewItemWire[viewKind='form'].columnState` diff --git a/packages/spec/src/kernel/customization-package.test.ts b/packages/spec/src/kernel/customization-package.test.ts index 8b96c68172a..7dc41c83d30 100644 --- a/packages/spec/src/kernel/customization-package.test.ts +++ b/packages/spec/src/kernel/customization-package.test.ts @@ -22,7 +22,7 @@ import { } from './customization-package'; import { CUSTOMIZATION_PACKAGE_SUFFIX } from './override-declaration.zod'; import { ManifestSchema, MANIFEST_ID_PATTERN } from './manifest.zod'; -import { DEFAULT_METADATA_TYPE_REGISTRY, type MetadataType } from './metadata-plugin.zod'; +import { DEFAULT_METADATA_TYPE_REGISTRY } from './metadata-plugin.zod'; import { deriveNamespaceFromPackageId } from './namespace-prefix'; describe('the pair\'s package id', () => { @@ -210,11 +210,15 @@ describe('the closed overridable-type list, pinned to the registry', () => { expect(conditioned).toEqual(['action', 'flow']); }); - it('the types ADR-0146 does not decide are excluded (today\'s posture) and marked for the maintainer', () => { - const undecided = (Object.keys(OVERRIDE_TYPE_CLASSIFICATION) as MetadataType[]) - .filter((t) => OVERRIDE_TYPE_CLASSIFICATION[t].basis === 'undecided').sort(); - expect(undecided).toEqual(['datasource', 'seed', 'skill', 'tool']); - for (const type of undecided) expect(OVERRIDE_TYPE_CLASSIFICATION[type].disposition).toBe('excluded'); + it('the four types ADR-0146 does not name are excluded on the basis the maintainer ruled (#28404)', () => { + expect(OVERRIDE_TYPE_CLASSIFICATION.seed).toEqual({ disposition: 'excluded', basis: 'adr_0126_one_shot' }); + expect(OVERRIDE_TYPE_CLASSIFICATION.datasource).toEqual({ disposition: 'excluded', basis: 'setup_owned_connection' }); + expect(OVERRIDE_TYPE_CLASSIFICATION.tool).toEqual({ disposition: 'excluded', basis: 'behavioural_switch_first' }); + expect(OVERRIDE_TYPE_CLASSIFICATION.skill).toEqual({ disposition: 'excluded', basis: 'behavioural_switch_first' }); + // No row is left undecided: a type added later is classified with a + // decided basis in the same change. + const bases = new Set(Object.values(OVERRIDE_TYPE_CLASSIFICATION).map((row) => row.basis as string)); + expect(bases.has('undecided')).toBe(false); }); it('isOverridableMetadataType answers false for a name that is no metadata type', () => { diff --git a/packages/spec/src/kernel/customization-package.ts b/packages/spec/src/kernel/customization-package.ts index 48ecf6b8020..1961bd29774 100644 --- a/packages/spec/src/kernel/customization-package.ts +++ b/packages/spec/src/kernel/customization-package.ts @@ -258,8 +258,22 @@ export type OverrideCondition = * type as a standalone document; * - `not_runtime_creatable` — D2's first sentence fails: neither Studio nor the * metadata API can author the type (`allowRuntimeCreate: false`); - * - `undecided` — ADR-0146 does not decide the type; classified `excluded` - * (today's posture, a reversible default) until the maintainer rules. + * - `adr_0126_one_shot` — ADR-0126 §3 places the type outside as one-shot: it + * is applied once at install or publish and holds nothing durable to + * override; a customization adds its own item under a new name instead + * (`seed`); + * - `setup_owned_connection` — ADR-0146 D1 gives the type to Setup, not to the + * metadata a package carries, and ADR-0126 §3 makes it origin-gated: one + * physical connection is environment configuration that must not travel in + * an exported `.custom` package (`datasource`); + * - `behavioural_switch_first` — a behavioural type takes `action`'s path + * (ADR-0146 D9): its ledger switch lands first and an override door opens + * later, under D9-style conditions written for it then (`tool`, `skill`; + * ADR-0126 §3 pre-charts both for disable + clone). + * + * The maintainer ruled the last three on #28404 (「同意」, 2026-10-31): no row + * is left undecided, and a type added later is classified with a decided basis + * in the same change. */ export type OverrideClassificationBasis = | 'adr_0146_d2_listed' @@ -268,7 +282,9 @@ export type OverrideClassificationBasis = | 'adr_0126_code_only' | 'not_package_shipped' | 'not_runtime_creatable' - | 'undecided'; + | 'adr_0126_one_shot' + | 'setup_owned_connection' + | 'behavioural_switch_first'; /** One type's override classification. */ export interface OverrideTypeClassification { @@ -282,7 +298,7 @@ const listed: OverrideTypeClassification = { disposition: 'overridable', basis: const securityCatalog: OverrideTypeClassification = { disposition: 'excluded', basis: 'adr_0146_d6_security_catalog' }; const codeOnly: OverrideTypeClassification = { disposition: 'excluded', basis: 'adr_0126_code_only' }; const notRuntimeCreatable: OverrideTypeClassification = { disposition: 'excluded', basis: 'not_runtime_creatable' }; -const undecided: OverrideTypeClassification = { disposition: 'excluded', basis: 'undecided' }; +const behaviouralSwitchFirst: OverrideTypeClassification = { disposition: 'excluded', basis: 'behavioural_switch_first' }; /** * The CLOSED override classification of every metadata type — the constant the @@ -294,9 +310,9 @@ const undecided: OverrideTypeClassification = { disposition: 'excluded', basis: * fails when a registry type is added without a row here, when a row names no * registry type, and when an `overridable` row is not runtime-creatable. * - * Four runtime-creatable types ADR-0146 does not decide are `undecided` - * (classified `excluded`, today's posture) and listed for the maintainer: - * `seed`, `datasource`, `tool`, `skill`. + * Four runtime-creatable types ADR-0146 does not name (`seed`, `datasource`, + * `tool`, `skill`) are `excluded` with the basis the maintainer ruled on + * #28404 (「同意」, 2026-10-31). */ export const OVERRIDE_TYPE_CLASSIFICATION = { // ── D2's list ────────────────────────────────────────────────────── @@ -339,11 +355,11 @@ export const OVERRIDE_TYPE_CLASSIFICATION = { sharing_rule: notRuntimeCreatable, queue: notRuntimeCreatable, assignment_rule: notRuntimeCreatable, - // ── Not decided by ADR-0146 — listed for the maintainer ──────────── - seed: undecided, - datasource: undecided, - tool: undecided, - skill: undecided, + // ── Not named by ADR-0146 — ruled by the maintainer on #28404 ────── + seed: { disposition: 'excluded', basis: 'adr_0126_one_shot' }, + datasource: { disposition: 'excluded', basis: 'setup_owned_connection' }, + tool: behaviouralSwitchFirst, + skill: behaviouralSwitchFirst, } as const satisfies Record; /** The metadata types a declared override applies to (`disposition: 'overridable'`). */ diff --git a/packages/spec/src/kernel/override-declaration.zod.ts b/packages/spec/src/kernel/override-declaration.zod.ts index 948a4dc1062..3ca02facb0f 100644 --- a/packages/spec/src/kernel/override-declaration.zod.ts +++ b/packages/spec/src/kernel/override-declaration.zod.ts @@ -11,7 +11,7 @@ import { strictObject } from '../shared/strict-object'; * package that bears a base item's name and carries * * ```ts - * overrides: { package: '', basedOn: '' } + * overrides: { package: '', basedOn: '' } * ``` * * replaces the base's item WHOLE for every read that serves the base's name @@ -93,7 +93,8 @@ export const OverrideDeclarationSchema = strictObject({ basedOn: z.string() .min(1) .describe( - 'Recorded, never judged: the version of the base item this override was taken from (e.g. "3.2.0"). ' + 'Recorded, never judged: the base package\'s `manifest.version` this override was taken from (e.g. "3.2.0"), ' + + 'not a history version of the item. ' + 'No door compares it with the installed base, no upgrade diff is produced from it, and nothing is ' + 'refused on it; it tells a person re-basing the override where it started (ADR-0146 D2).', ),