From 8f49cdb79e5c0276afc71fea9253771ae1c34f53 Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Tue, 16 Jun 2026 23:13:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(cli):=20liveness=20author-warning=20lint?= =?UTF-8?q?=20=E2=80=94=20close=20the=20spec-liveness=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The liveness ledgers classify every authorable property live/experimental/ dead with evidence, and the CI gate enforces classification completeness — but that knowledge never reached the author (very often an AI) writing the metadata. This feeds it back at build time. New `compile` lint (lint-liveness-properties.ts) reads the ledgers and warns when an authored object/field sets a misleading property — e.g. `object.enable.feeds` (no feed runtime), `object.versioning` (no engine), `field.columnName` (driver ignores it), `field.maxRating`/`vectorConfig` (renderer reads a different key) — with a corrective hint. Advisory only; never fails the build, like the existing flow anti-pattern lint. Signal-over-noise by design: opt-in per ledger entry via a new `authorWarn`/`authorHint` annotation (experimental entries warn by default). Booleans warn only when truthy and only `default(false)` flags are marked, so schema defaults (enable.trash/searchable) never trip it. Coverage grows by annotating more entries, not by touching lint code. - spec: ledger entries gain optional authorWarn/authorHint; `liveness/` now shipped in package `files` so the CLI can read it. Seeded the misleading object capability flags + aspirational blocks and dead field props. - README documents the authorWarn convention + the default-false caveat. Verified end-to-end via a real `objectstack compile` (warnings fired for enable.feeds/versioning/columnName; the lint also caught two genuine pre-existing dead-prop usages in app-showcase). CLI 451 + 9 new lint tests green; liveness gate green. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/liveness-author-warnings.md | 12 ++ packages/cli/src/commands/compile.ts | 17 ++ .../utils/lint-liveness-properties.test.ts | 84 +++++++++ .../cli/src/utils/lint-liveness-properties.ts | 173 ++++++++++++++++++ packages/spec/liveness/README.md | 30 +++ packages/spec/liveness/field.json | 24 ++- packages/spec/liveness/object.json | 47 +++-- packages/spec/package.json | 1 + 8 files changed, 370 insertions(+), 18 deletions(-) create mode 100644 .changeset/liveness-author-warnings.md create mode 100644 packages/cli/src/utils/lint-liveness-properties.test.ts create mode 100644 packages/cli/src/utils/lint-liveness-properties.ts diff --git a/.changeset/liveness-author-warnings.md b/.changeset/liveness-author-warnings.md new file mode 100644 index 0000000000..d5da65370a --- /dev/null +++ b/.changeset/liveness-author-warnings.md @@ -0,0 +1,12 @@ +--- +"@objectstack/cli": minor +"@objectstack/spec": patch +--- + +feat(cli): liveness author-warning lint — close the spec-liveness loop on the author side. + +The liveness ledgers already classify every authorable property live/experimental/dead with evidence, and the CI gate enforces classification *completeness* — but that knowledge never reached the person (very often an AI) writing the metadata. The new `compile` lint (`lint-liveness-properties.ts`) reads the ledgers and emits an advisory **warning** when an authored object/field sets a property that is misleading at runtime — e.g. `object.enable.feeds` (no feed runtime; comments live on sys_comment), `object.versioning` (no versioning engine), `field.columnName` (driver ignores it; column == field key), `field.maxRating`/`vectorConfig` (renderer reads a different key) — each with a corrective hint toward the supported alternative. Never fails the build (advisory only), consistent with the existing flow anti-pattern lint. + +Signal-over-noise by design: warnings are **opt-in per ledger entry** via a new `authorWarn`/`authorHint` annotation (plus `experimental` entries warn by default). Booleans warn only when set truthy, and only `default(false)` flags are marked, so schema defaults (`enable.trash`, `enable.searchable`) never trip it. Coverage grows by annotating more ledger entries, not by changing lint code; today it covers `object` (incl. `enable.*`) and `field`. + +- `@objectstack/spec`: ledger entries gain optional `authorWarn`/`authorHint`; `liveness/` is now shipped in the package `files` so the CLI can read it. Seeded annotations on the misleading object capability flags + aspirational blocks and the misleading dead field props. No schema/runtime change. diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index 051f639391..3a44a236e5 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -11,6 +11,7 @@ import { lowerCallables } from '../utils/lower-callables.js'; import { validateStackExpressions } from '../utils/validate-expressions.js'; import { validateWidgetBindings } from '../utils/validate-widget-bindings.js'; import { lintFlowPatterns } from '../utils/lint-flow-patterns.js'; +import { lintLivenessProperties } from '../utils/lint-liveness-properties.js'; import { collectAndLintDocs } from '../utils/collect-docs.js'; import { buildRuntimeBundle, cleanupOldRuntimeBundles } from '../utils/build-runtime.js'; import { @@ -208,6 +209,22 @@ export default class Compile extends Command { } } + // 3d-bis. Liveness author-warning lint — close the spec-liveness loop on + // the author side: an authored property the ledger marks dead-and- + // misleading (e.g. `object.enable.feeds`, `field.columnName`) or + // experimental is set hopefully but does nothing / isn't enforced at + // runtime. Advisory only; ledger-driven (entries opt in via + // `authorWarn`), so it's high-signal and NEVER fails the build. + const livenessLint = lintLivenessProperties(result.data as Record); + if (livenessLint.length > 0 && !flags.json) { + console.log(''); + for (const fnd of livenessLint) { + printWarning(`${fnd.where}: ${fnd.message}`); + console.log(chalk.dim(` ${fnd.hint}`)); + console.log(chalk.dim(` rule: ${fnd.rule}`)); + } + } + // 3d. Package docs (ADR-0046): compile flat `src/docs/*.md` into // `docs: DocSchema[]` and lint the combined set (flatness, // namespace-prefixed names, MDX/image ban, same-package link diff --git a/packages/cli/src/utils/lint-liveness-properties.test.ts b/packages/cli/src/utils/lint-liveness-properties.test.ts new file mode 100644 index 0000000000..1bcab90638 --- /dev/null +++ b/packages/cli/src/utils/lint-liveness-properties.test.ts @@ -0,0 +1,84 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import { + lintLivenessProperties, + LIVENESS_DEAD_PROPERTY, +} from './lint-liveness-properties.js'; + +/** + * These run against the REAL ledgers shipped by `@objectstack/spec` (the same + * files the gate enforces), so they double as a contract test: if an + * `authorWarn` annotation is removed from `enable.feeds` / `columnName` / etc., + * the matching assertion fails. + */ + +const objStack = (obj: Record) => ({ objects: [{ name: 'widget', ...obj }] }); +const rules = (findings: { rule: string }[]) => findings.map((f) => f.rule); +const paths = (findings: { message: string }[]) => findings.map((f) => f.message); + +describe('lintLivenessProperties', () => { + it('warns on an authored dead capability flag (enable.feeds: true)', () => { + const findings = lintLivenessProperties(objStack({ enable: { feeds: true } })); + expect(findings.length).toBeGreaterThan(0); + const feeds = findings.find((f) => f.message.includes('enable.feeds')); + expect(feeds).toBeDefined(); + expect(feeds!.rule).toBe(LIVENESS_DEAD_PROPERTY); + expect(feeds!.where).toBe("object 'widget'"); + expect(feeds!.hint.length).toBeGreaterThan(0); + }); + + it('does NOT warn on a default-on flag the author left alone (enable.trash: true)', () => { + const findings = lintLivenessProperties(objStack({ enable: { trash: true } })); + expect(paths(findings).some((m) => m.includes('enable.trash'))).toBe(false); + }); + + it('does NOT warn when a dead boolean flag is explicitly false (enable.files: false)', () => { + const findings = lintLivenessProperties(objStack({ enable: { files: false } })); + expect(paths(findings).some((m) => m.includes('enable.files'))).toBe(false); + }); + + it('warns on a present dead object block (versioning)', () => { + const findings = lintLivenessProperties(objStack({ versioning: { enabled: true } })); + expect(paths(findings).some((m) => m.includes('versioning'))).toBe(true); + }); + + it('warns on a misleading dead field prop (columnName)', () => { + const findings = lintLivenessProperties( + objStack({ fields: [{ name: 'code', type: 'text', columnName: 'legacy_code' }] }), + ); + const f = findings.find((x) => x.message.includes('columnName')); + expect(f).toBeDefined(); + expect(f!.where).toBe("object 'widget' · field 'code'"); + }); + + it('warns on a field-level index flag set true, but not when false', () => { + const on = lintLivenessProperties(objStack({ fields: [{ name: 'code', type: 'text', index: true }] })); + expect(paths(on).some((m) => m.includes('`index`'))).toBe(true); + const off = lintLivenessProperties(objStack({ fields: [{ name: 'code', type: 'text', index: false }] })); + expect(paths(off).some((m) => m.includes('`index`'))).toBe(false); + }); + + it('is silent for a clean object with only live properties', () => { + const findings = lintLivenessProperties( + objStack({ + label: 'Widget', + enable: { apiEnabled: true }, + fields: [{ name: 'name', type: 'text', label: 'Name' }], + }), + ); + expect(findings).toEqual([]); + }); + + it('handles objects as a keyed record (not just arrays)', () => { + const findings = lintLivenessProperties({ + objects: { widget: { name: 'widget', enable: { feeds: true } } }, + }); + expect(rules(findings)).toContain(LIVENESS_DEAD_PROPERTY); + }); + + it('returns [] on an empty / shapeless stack', () => { + expect(lintLivenessProperties({})).toEqual([]); + expect(lintLivenessProperties({ objects: [] })).toEqual([]); + }); +}); diff --git a/packages/cli/src/utils/lint-liveness-properties.ts b/packages/cli/src/utils/lint-liveness-properties.ts new file mode 100644 index 0000000000..d078bb018f --- /dev/null +++ b/packages/cli/src/utils/lint-liveness-properties.ts @@ -0,0 +1,173 @@ +// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Build-time lint that closes the spec-liveness loop on the AUTHOR side. + * + * The liveness ledgers (`@objectstack/spec/liveness/.json`) classify every + * authorable metadata property as live / experimental / dead with evidence. The + * CI gate enforces that classification is *complete*, but the ledger's knowledge + * never reached the person (very often an AI) writing the metadata. This lint + * surfaces it: when an authored object/field sets a property the ledger marks as + * dead-and-misleading (or experimental), it emits an advisory WARNING — "you set + * this expecting it to do something; at runtime it does nothing" — with a hint + * toward the supported alternative. It NEVER fails the build. + * + * Signal over noise is the whole point, so the ledger opts in per entry via + * `"authorWarn": true` (+ an optional `"authorHint"`). A property being merely + * `dead` is NOT enough — plenty of dead props are benign display/doc metadata. + * Only entries an author would be *misled* by are marked. Booleans warn only when + * set truthy (so schema defaults like `enable.trash` never trip it); object/ + * string/array props warn when present at all. + */ + +import { createRequire } from 'node:module'; +import { dirname, join } from 'node:path'; +import { existsSync, readFileSync } from 'node:fs'; + +export interface LivenessLintFinding { + where: string; + message: string; + hint: string; + rule: string; +} + +export const LIVENESS_DEAD_PROPERTY = 'liveness-dead-property'; +export const LIVENESS_EXPERIMENTAL_PROPERTY = 'liveness-experimental-property'; + +type AnyRec = Record; + +interface LedgerEntry { + status?: string; + authorWarn?: boolean; + authorHint?: string; + note?: string; + children?: Record; +} + +/** Flattened, warn-only view of a type's ledger: propPath → entry (incl. `a.b` children). */ +type WarnMap = Map; + +function asArray(v: unknown): AnyRec[] { + if (Array.isArray(v)) return v as AnyRec[]; + if (v && typeof v === 'object') return Object.entries(v as AnyRec).map(([name, def]) => ({ name, ...(def as AnyRec) })); + return []; +} + +/** Locate `@objectstack/spec`'s shipped `liveness/` dir (workspace src or published files). */ +function resolveLivenessDir(): string | null { + try { + const require = createRequire(import.meta.url); + const pkgJson = require.resolve('@objectstack/spec/package.json'); + const dir = join(dirname(pkgJson), 'liveness'); + return existsSync(dir) ? dir : null; + } catch { + return null; + } +} + +/** Build the warn-only lookup for one type, flattening one level of `children`. */ +function loadWarnMap(dir: string, type: string): WarnMap { + const map: WarnMap = new Map(); + const file = join(dir, `${type}.json`); + if (!existsSync(file)) return map; + let ledger: { props?: Record }; + try { + ledger = JSON.parse(readFileSync(file, 'utf8')); + } catch { + return map; + } + const props = ledger.props || {}; + for (const [key, entry] of Object.entries(props)) { + if (entry?.children) { + for (const [ck, centry] of Object.entries(entry.children)) { + if (shouldWarn(centry)) map.set(`${key}.${ck}`, centry); + } + } + if (shouldWarn(entry)) map.set(key, entry); + } + return map; +} + +/** An entry warns when explicitly opted in, OR when it's experimental (a declared-but-unenforced guarantee). */ +function shouldWarn(entry: LedgerEntry | undefined): boolean { + if (!entry) return false; + return entry.authorWarn === true || entry.status === 'experimental'; +} + +/** A value that signals authoring intent: booleans only when truthy; everything else when present. */ +function isAuthored(value: unknown): boolean { + if (value === undefined || value === null) return false; + if (typeof value === 'boolean') return value === true; + return true; +} + +function describe(entry: LedgerEntry): { kind: string; rule: string } { + if (entry.status === 'experimental') { + return { kind: 'is experimental — declared but NOT enforced at runtime', rule: LIVENESS_EXPERIMENTAL_PROPERTY }; + } + return { kind: 'has no runtime effect (liveness: dead)', rule: LIVENESS_DEAD_PROPERTY }; +} + +/** Check one metadata item's set properties against its type's warn-map. */ +function checkItem( + type: string, + item: AnyRec, + whereBase: string, + warnMap: WarnMap, + findings: LivenessLintFinding[], +): void { + for (const [path, entry] of warnMap) { + const value = path.includes('.') + ? getNested(item, path) + : item[path]; + if (!isAuthored(value)) continue; + const { kind, rule } = describe(entry); + const hint = entry.authorHint + ?? entry.note + ?? 'Remove it — it is declared in the spec but not consumed at runtime.'; + findings.push({ + where: whereBase, + message: `sets \`${path}\` but this ${type} property ${kind}.`, + hint, + rule, + }); + } +} + +/** Resolve a dotted path one or more levels, treating a missing parent as absent. */ +function getNested(obj: AnyRec, path: string): unknown { + let cur: unknown = obj; + for (const seg of path.split('.')) { + if (cur === null || typeof cur !== 'object') return undefined; + cur = (cur as AnyRec)[seg]; + } + return cur; +} + +/** + * Lint the compiled stack for authored properties the liveness ledger flags as + * misleading. Advisory only — returns findings, never throws. v1 covers the two + * highest-signal surfaces (objects incl. their `enable.*` flags, and their + * fields); the mechanism is ledger-driven, so coverage grows by marking more + * entries `authorWarn` rather than touching this code. + */ +export function lintLivenessProperties(stack: AnyRec): LivenessLintFinding[] { + const dir = resolveLivenessDir(); + if (!dir) return []; + const objectWarn = loadWarnMap(dir, 'object'); + const fieldWarn = loadWarnMap(dir, 'field'); + if (objectWarn.size === 0 && fieldWarn.size === 0) return []; + + const findings: LivenessLintFinding[] = []; + for (const obj of asArray(stack.objects)) { + const objName = typeof obj.name === 'string' ? obj.name : '(unnamed object)'; + if (objectWarn.size > 0) checkItem('object', obj, `object '${objName}'`, objectWarn, findings); + if (fieldWarn.size > 0) { + for (const field of asArray(obj.fields)) { + const fieldName = typeof field.name === 'string' ? field.name : '(unnamed field)'; + checkItem('field', field, `object '${objName}' · field '${fieldName}'`, fieldWarn, findings); + } + } + } + return findings; +} diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index 3ca182ad05..1b09eed2dd 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -35,6 +35,36 @@ Resolution per property: **ledger entry → spec `.describe()` marker → UNCLAS Framework provenance/lock fields (`_lock*`, `_provenance`, `_packageId/Version`, `protection` — ADR-0010) are auto-classified `live`. +## Author warnings — closing the loop (`authorWarn`) + +Classification is also fed back to the *author* at build time. The CLI `compile` +lint (`packages/cli/src/utils/lint-liveness-properties.ts`) reads these ledgers and +emits an advisory **warning** when an authored object/field sets a property that is +misleading — "you set this expecting it to do something; at runtime it does nothing / +isn't enforced" — with a corrective hint. Never fails the build. + +Signal over noise is the whole point, so warnings are **opt-in per entry**: + +| Field | Effect | +|---|---| +| `"authorWarn": true` | warn when this property is authored (in addition, any `experimental` entry warns by default — it's a declared-but-unenforced guarantee). | +| `"authorHint": "…"` | the corrective one-liner shown under the warning (falls back to `note`). | + +Two rules keep it false-positive-free, **both of which the marker author must respect**: + +1. **Only mark genuinely *misleading* dead props** — ones that imply a capability/behavior + that doesn't exist (`enable.feeds`, `field.columnName`, `versioning`). Benign display/doc + metadata that's "dead" (no runtime reader) — `description`, `tags`, `icon` — must NOT be + marked; an author isn't misled by them. +2. **Booleans: only mark `default(false)` flags.** The lint warns on a boolean only when set + `true`, and it can't tell author-set-`true` from a schema default. A `default(true)` flag + (`enable.trash`/`mru`, `enable.searchable`) would then warn on *every* object that has an + `enable` block — so leave those unmarked (see `enable.searchable`'s `_authorWarnSkipped`). + Object/string/array props warn when merely present, so this caveat is boolean-only. + +The lint is ledger-driven: coverage grows by marking more entries `authorWarn`, not by +touching the lint code. Today it covers `object` (incl. `enable.*`) and `field`. + ## Granularity — drill one level A property is classified at the top level by default. A **container** property (object / diff --git a/packages/spec/liveness/field.json b/packages/spec/liveness/field.json index 0eb6632cf7..02158d9b10 100644 --- a/packages/spec/liveness/field.json +++ b/packages/spec/liveness/field.json @@ -149,16 +149,22 @@ }, "referenceFilters": { "status": "dead", - "evidence": "lookup dialog reads lookup_filters (LookupField.tsx:171) — entirely dead as authored (naming drift)" + "evidence": "lookup dialog reads lookup_filters (LookupField.tsx:171) — entirely dead as authored (naming drift)", + "authorWarn": true, + "authorHint": "The lookup dialog reads `lookup_filters`, not `referenceFilters` — as authored this filters nothing. Use the supported lookup-filter key, or remove it to avoid implying a constrained lookup." }, "maxRating": { "status": "dead", - "evidence": "RatingField reads `max` (RatingField.tsx:13) — dead + redundant with max" + "evidence": "RatingField reads `max` (RatingField.tsx:13) — dead + redundant with max", + "authorWarn": true, + "authorHint": "The rating renderer reads `max`, not `maxRating` — set `max` instead; `maxRating` is ignored." }, "columnName": { "status": "dead", "evidence": "resolveColumnName (spec system-names.ts:182) has ZERO call sites; SQL driver hardcodes column = field key", - "note": "DANGEROUS — advertises custom physical columns the driver never honors." + "note": "DANGEROUS — advertises custom physical columns the driver never honors.", + "authorWarn": true, + "authorHint": "The physical column always equals the field key — a custom `columnName` is silently ignored by the driver. Remove it; rename the field key itself if you need a different column." }, "searchable": { "status": "live", @@ -167,7 +173,9 @@ }, "index": { "status": "dead", - "evidence": "field-level — driver reads object indexes[] (sql-driver.ts:1252); field bool unused" + "evidence": "field-level — driver reads object indexes[] (sql-driver.ts:1252); field bool unused", + "authorWarn": true, + "authorHint": "A field-level `index: true` creates no index — the driver builds indexes from the object's `indexes[]` array. Declare the index there instead." }, "externalId": { "status": "live", @@ -181,11 +189,15 @@ }, "vectorConfig": { "status": "dead", - "evidence": "nested config — renderers read flat dimensions; no consumer, no vector-index DDL" + "evidence": "nested config — renderers read flat dimensions; no consumer, no vector-index DDL", + "authorWarn": true, + "authorHint": "This nested block is not read (no vector-index DDL). Set the flat `dimensions` sibling instead." }, "fileAttachmentConfig": { "status": "dead", - "evidence": "nested config — renderers read flat multiple/accept/maxSize (FileField.tsx:16); no size/type/virus enforcement in write path" + "evidence": "nested config — renderers read flat multiple/accept/maxSize (FileField.tsx:16); no size/type/virus enforcement in write path", + "authorWarn": true, + "authorHint": "This nested block is not read — and note no size/type enforcement happens on write. Use the flat `multiple`/`accept`/`maxSize` siblings for the renderer." }, "dependencies": { "status": "dead", diff --git a/packages/spec/liveness/object.json b/packages/spec/liveness/object.json index f41097a61f..13b6bf2e3c 100644 --- a/packages/spec/liveness/object.json +++ b/packages/spec/liveness/object.json @@ -141,23 +141,32 @@ }, "trackHistory": { "status": "dead", - "evidence": "no behavior-changing reader (database-loader.trackHistory is a separate ctor option)" + "evidence": "no behavior-changing reader (database-loader.trackHistory is a separate ctor option)", + "authorWarn": true, + "authorHint": "Per-field history is live via `Field.trackHistory` (plugin-audit, ADR-0052) — set it on the fields you want tracked instead of this object flag." }, "searchable": { "status": "dead", - "evidence": "no behavior-changing reader" + "evidence": "no behavior-changing reader", + "_authorWarnSkipped": "defaults to true in the schema — the lint can't distinguish author-set-true from the default, so warning here would fire on every object with an enable block. Only default-FALSE booleans are safe to authorWarn." }, "files": { "status": "dead", - "evidence": "no behavior-changing reader" + "evidence": "no behavior-changing reader", + "authorWarn": true, + "authorHint": "File attachments are modeled with a `Field.file`/`Field.image` (or the sys_attachment object), not this object flag — it enables nothing on its own." }, "feeds": { "status": "dead", - "evidence": "no behavior-changing reader" + "evidence": "no behavior-changing reader", + "authorWarn": true, + "authorHint": "Comments/collaboration live on the dedicated sys_comment object (plugin-audit), not this flag — it enables no feed at runtime." }, "activities": { "status": "dead", - "evidence": "no behavior-changing reader" + "evidence": "no behavior-changing reader", + "authorWarn": true, + "authorHint": "Tasks/events live on the dedicated sys_activity object (plugin-audit), not this flag — it enables no activity suite at runtime." }, "trash": { "status": "dead", @@ -176,31 +185,45 @@ }, "versioning": { "status": "dead", - "evidence": "aspirational; no runtime" + "evidence": "aspirational; no runtime", + "authorWarn": true, + "authorHint": "No record-versioning engine reads this block — setting it stores nothing and snapshots no history. Remove it (use Field.trackHistory for field-level history)." }, "partitioning": { "status": "dead", - "evidence": "aspirational; no runtime" + "evidence": "aspirational; no runtime", + "authorWarn": true, + "authorHint": "No table-partitioning is applied from this block. Remove it — it has no runtime effect." }, "softDelete": { "status": "dead", - "evidence": "no runtime; duplicates the (also dead) enable.trash" + "evidence": "no runtime; duplicates the (also dead) enable.trash", + "authorWarn": true, + "authorHint": "There is no soft-delete/recycle-bin runtime. Deletes are hard deletes; remove this to avoid implying restore semantics that don't exist." }, "search": { "status": "dead", - "evidence": "SearchConfigSchema — no runtime; duplicates enable.searchable" + "evidence": "SearchConfigSchema — no runtime; duplicates enable.searchable", + "authorWarn": true, + "authorHint": "No search-engine config is consumed. Remove it — records remain queryable via the normal data API regardless." }, "defaultDetailForm": { "status": "dead", - "evidence": "documented fallback chain unimplemented" + "evidence": "documented fallback chain unimplemented", + "authorWarn": true, + "authorHint": "The detail form is chosen by the view layer; this field's documented fallback chain is unimplemented and ignored. Remove it." }, "recordName": { "status": "dead", - "evidence": "superseded by a field of type:'autonumber' + autonumberFormat (engine.ts:757)" + "evidence": "superseded by a field of type:'autonumber' + autonumberFormat (engine.ts:757)", + "authorWarn": true, + "authorHint": "Auto-naming is done by a `Field` of type 'autonumber' (with autonumberFormat). This block is not read — model the name field instead." }, "keyPrefix": { "status": "dead", - "evidence": "no runtime" + "evidence": "no runtime", + "authorWarn": true, + "authorHint": "Record ids are not prefixed from this value (no Salesforce-style key prefix runtime). Remove it — it has no effect." }, "tags": { "status": "dead", diff --git a/packages/spec/package.json b/packages/spec/package.json index e139abd862..670083b639 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -172,6 +172,7 @@ "files": [ "dist", "json-schema", + "liveness", "prompts", "llms.txt", "README.md",