diff --git a/.changeset/fix-explain-ownership-values.md b/.changeset/fix-explain-ownership-values.md new file mode 100644 index 0000000000..4d7d48f0d1 --- /dev/null +++ b/.changeset/fix-explain-ownership-values.md @@ -0,0 +1,21 @@ +--- +"@objectstack/cli": patch +--- + +fix(cli): `os explain object` documented `ownership` with the wrong allowed values (#3244) + +The schema catalog described the object `ownership` field as the package +*contribution* kind (`"own" | "extend"`, the `ObjectOwnershipEnum` set via +`registerObject`). But `ObjectSchema.ownership` is the **record-ownership +model** — `z.enum(['user', 'org', 'none'])` — a distinct concept the spec +explicitly warns not to conflate despite the shared word. + +`os explain object` now prints: + + ownership 'user' | 'org' | 'none' Record-ownership model: user (default, + injects a reassignable owner_id) | org | none (no per-record owner). + Distinct from the package own/extend contribution kind. + +A regression test (`packages/cli/test/commands.test.ts`) pins the documented +values to the record-ownership enum so the two concepts can't drift back +together. Found during the #1880 docs implementation-accuracy audit. diff --git a/packages/cli/src/commands/explain.ts b/packages/cli/src/commands/explain.ts index 59d3d2bf12..2834d39923 100644 --- a/packages/cli/src/commands/explain.ts +++ b/packages/cli/src/commands/explain.ts @@ -22,7 +22,7 @@ interface SchemaInfo { docsPath: string; } -const SCHEMAS: Record = { +export const SCHEMAS: Record = { object: { name: 'Object', description: 'Defines a data entity in the ObjectStack data model. Objects contain fields, enable capabilities, and form the foundation of the metadata-driven platform.', @@ -34,7 +34,7 @@ const SCHEMAS: Record = { { name: 'label', type: 'string', description: 'Human-readable display name' }, { name: 'pluralLabel', type: 'string', description: 'Plural display name' }, { name: 'description', type: 'string', description: 'Documentation for the object' }, - { name: 'ownership', type: '"own" | "extend"', description: 'Whether this object is owned or extended' }, + { name: 'ownership', type: "'user' | 'org' | 'none'", description: 'Record-ownership model: user (default, injects a reassignable owner_id) | org | none (no per-record owner). Distinct from the package own/extend contribution kind.' }, { name: 'enable', type: 'ObjectCapabilities', description: 'Feature flags (trackHistory, apiEnabled, etc.)' }, { name: 'icon', type: 'string', description: 'Icon identifier for UI display' }, ], diff --git a/packages/cli/test/commands.test.ts b/packages/cli/test/commands.test.ts index c80429bfee..c694c159a1 100644 --- a/packages/cli/test/commands.test.ts +++ b/packages/cli/test/commands.test.ts @@ -11,7 +11,7 @@ import Info from '../src/commands/info'; import Generate from '../src/commands/generate'; import Lint from '../src/commands/lint'; import Diff from '../src/commands/diff'; -import Explain from '../src/commands/explain'; +import Explain, { SCHEMAS } from '../src/commands/explain'; describe('CLI Commands (oclif)', () => { it('should have compile command', () => { @@ -67,3 +67,22 @@ describe('CLI Commands (oclif)', () => { expect(Explain.description).toContain('explanation'); }); }); + +describe('os explain — schema catalog accuracy', () => { + // Regression guard for #3244: `os explain object` used to document the + // `ownership` field as the package-contribution kind (`"own" | "extend"`), + // which is a DISTINCT concept (`ObjectOwnershipEnum`, set via registerObject). + // The real `ObjectSchema.ownership` field is the record-ownership model — + // `z.enum(['user','org','none'])` — see packages/spec/src/data/object.zod.ts. + it('documents object.ownership as the record-ownership model, not the own/extend contribution kind (#3244)', () => { + const ownership = SCHEMAS.object.optional.find((f) => f.name === 'ownership'); + expect(ownership, 'object schema should document an `ownership` field').toBeDefined(); + + // The type string must enumerate exactly the record-ownership enum values. + const tokens = (ownership!.type.match(/'[^']+'|"[^"]+"/g) ?? []).map((t) => t.slice(1, -1)); + expect(new Set(tokens)).toEqual(new Set(['user', 'org', 'none'])); + + // …and must never regress back to the contribution-kind values. + expect(ownership!.type).not.toBe('"own" | "extend"'); + }); +});