From 20526f3dc7308fb0aeb0bc14481dc4747e771aa4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 06:00:18 +0000 Subject: [PATCH 1/2] fix(cli): os init and os generate object declare the scaffolded object with ObjectSchema.create Both doors that write a *.object.ts now emit the one authorised shape (ruling 5644350230, decision batch #122 item 1) instead of a Data.ServiceObject-annotated literal: a value import of ObjectSchema from @objectstack/spec/data, const X = ObjectSchema.create({ ... }), and the unchanged default export the barrels re-export. Repoints the pins that asserted the refused spelling, adds a parity pin over both emitters, updates the cli docs page, and carries the @objectstack/cli changeset with the one mechanical user rewrite. Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP Co-authored-by: Claude --- .changeset/19722-scaffold-object-factory.md | 25 +++ content/docs/deployment/cli.mdx | 11 +- packages/cli/src/commands/generate.ts | 24 ++- packages/cli/src/commands/init.ts | 12 +- .../cli/src/utils/emitted-source-parses.ts | 2 +- .../cli/test/generate-emission-parses.test.ts | 6 +- ...erate-refuses-name-outside-charset.test.ts | 2 +- .../generate-refuses-unparseable-name.test.ts | 4 +- .../test/scaffold-emission-typechecks.test.ts | 8 + .../scaffold-object-declaration-shape.test.ts | 204 ++++++++++++++++++ 10 files changed, 278 insertions(+), 20 deletions(-) create mode 100644 .changeset/19722-scaffold-object-factory.md create mode 100644 packages/cli/test/scaffold-object-declaration-shape.test.ts diff --git a/.changeset/19722-scaffold-object-factory.md b/.changeset/19722-scaffold-object-factory.md new file mode 100644 index 00000000000..e56892e7aee --- /dev/null +++ b/.changeset/19722-scaffold-object-factory.md @@ -0,0 +1,25 @@ +--- +"@objectstack/cli": patch +--- + +`os init` (the `app` and `plugin` templates) and `os generate object` now declare the object they scaffold with `ObjectSchema.create({ … })` — the one authorised shape for a `*.object.ts` — instead of a `Data.ServiceObject`-annotated object literal (#19722). + +The factory parses the declaration against `ObjectSchema` when the file is evaluated, so a mistake surfaces in the file where it was written; the typed literal deferred every check to a build the author might never run. `create-objectstack`'s starter, the data-modeling docs ("Every object definition follows this pattern") and every object file in this repository already used the factory — the two CLI doors were the outliers, and they now write the same shape as each other and as everything else. + +- **What a new scaffold contains**: `import { ObjectSchema } from '@objectstack/spec/data';` (a value import — the factory runs), `const myAppItem = ObjectSchema.create({ … });`, and the unchanged `export default myAppItem;`. The barrel lines both commands write (`export { default as … }`) are unchanged, as are the object's fields, its `sharingModel` and the comment explaining it. +- **Projects you already scaffolded keep working.** Nothing reads the old file differently at runtime, and nothing here renames or rewrites a file you have. +- **Converting an existing file is one mechanical rewrite** — wrap the literal in `ObjectSchema.create( … )`, drop the annotation, and import the factory: + + ```ts + // before + import * as Data from '@objectstack/spec/data'; + const myAppItem: Data.ServiceObject = { name: 'my_app_item', /* … */ }; + export default myAppItem; + + // after + import { ObjectSchema } from '@objectstack/spec/data'; + const myAppItem = ObjectSchema.create({ name: 'my_app_item', /* … */ }); + export default myAppItem; + ``` + + If the converted file now throws when it loads, the factory has found something the literal was carrying unchecked — an unknown top-level key, for example — and the message names it. diff --git a/content/docs/deployment/cli.mdx b/content/docs/deployment/cli.mdx index d1a971d0fdb..19d56e2f954 100644 --- a/content/docs/deployment/cli.mdx +++ b/content/docs/deployment/cli.mdx @@ -141,6 +141,15 @@ os init my-app --no-install # Skip dependency installation | `plugin` | **Metadata package**: declarative objects, built by `objectstack compile`, `private` — *not* the kernel code plugin `os create plugin` emits | | `empty` | Minimal project with just `objectstack.config.ts` | +The `app` and `plugin` templates declare their starter object with +`ObjectSchema.create({ … })` — the one authorised shape for a `*.object.ts`, and the +same one `os generate object` writes. The factory validates the declaration against +the object protocol when the file is evaluated, so a mistake surfaces in the file +where it was written. A project scaffolded by an earlier release carries a +`Data.ServiceObject`-annotated object literal instead; converting it is one mechanical +rewrite — wrap the literal in `ObjectSchema.create( … )`, drop the annotation, and +import `ObjectSchema` from `@objectstack/spec/data`. + #### `os dev` Starts development mode. Three usage shapes: @@ -1438,7 +1447,7 @@ third-party extension primitive, authored as `src/skills/.skill.ts` with - `--dry-run` — Preview without writing files **What it does:** -1. Creates a typed TypeScript file using `Data.ServiceObject`, `UI.View`, `Automation.Flow`, etc. +1. Creates the TypeScript file — an `object` declared with `ObjectSchema.create({ … })`, the same shape the `os init` templates write; the other types as typed literals (`UI.View`, `Automation.Flow`, etc.) 2. Creates or updates the barrel `index.ts` in the target directory 3. Shows a hint to run `objectstack validate` diff --git a/packages/cli/src/commands/generate.ts b/packages/cli/src/commands/generate.ts index be774af8162..03b8006959d 100644 --- a/packages/cli/src/commands/generate.ts +++ b/packages/cli/src/commands/generate.ts @@ -90,13 +90,25 @@ const GENERATORS: Record `import * as Data from '@objectstack/spec/data'; + generate: (name: string) => `import { ObjectSchema } from '@objectstack/spec/data'; /** * ${toTitleCase(name)} Object */ -const ${toCamelCase(name)}: Data.ServiceObject = { +const ${toCamelCase(name)} = ObjectSchema.create({ name: '${toSnakeCase(name)}', label: '${toTitleCase(name)}', pluralLabel: '${toTitleCase(name)}s', @@ -119,7 +131,7 @@ const ${toCamelCase(name)}: Data.ServiceObject = { // authored decision rather than an accident. The other values, and how to // widen access safely: https://objectstack.ai/docs/permissions/sharing-rules sharingModel: 'private', -}; +}); export default ${toCamelCase(name)}; `, @@ -961,9 +973,9 @@ async function runMetadataGeneration(type: string, name: string, flags: { dir?: // // This command ran no name validation at all, so a name that is legal as a // NAME but not as an IDENTIFIER was interpolated straight into a binding - // position and written out under `exit 0` — `const foo.bar: - // Data.ServiceObject = {`, plus a matching barrel line: two files that are - // not TypeScript, from a command that reported success. + // position and written out under `exit 0` — `const foo.bar = + // ObjectSchema.create({` in today's emission, plus a matching barrel line: + // two files that are not TypeScript, from a command that reported success. // // The criterion is PARSEABILITY, not a charset. `findEmissionParseFailures` // asks the compiler about the bytes above and about nothing else, which is diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 427c8ab6ed2..d0a70a8f718 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -646,9 +646,9 @@ export default defineStack({ srcFiles: { 'src/objects/index.ts': (_name, namespace) => `export { default as ${toCamelCase(namespace)}Item } from './${namespace}_item.object'; `, - 'src/objects/__name___item.object.ts': (_name, namespace) => `import * as Data from '@objectstack/spec/data'; + 'src/objects/__name___item.object.ts': (_name, namespace) => `import { ObjectSchema } from '@objectstack/spec/data'; -const ${toCamelCase(namespace)}Item: Data.ServiceObject = { +const ${toCamelCase(namespace)}Item = ObjectSchema.create({ name: '${namespace}_item', label: '${toTitleCase(namespace)} Item', fields: { @@ -679,7 +679,7 @@ const ${toCamelCase(namespace)}Item: Data.ServiceObject = { // authored decision rather than an accident. The other values, and how to // widen access safely: https://objectstack.ai/docs/permissions/sharing-rules sharingModel: 'private', -}; +}); export default ${toCamelCase(namespace)}Item; `, @@ -741,9 +741,9 @@ export default defineStack({ srcFiles: { 'src/objects/index.ts': (_name, namespace) => `export { default as ${toCamelCase(namespace)}Item } from './${namespace}_item.object'; `, - 'src/objects/__name___item.object.ts': (_name, namespace) => `import * as Data from '@objectstack/spec/data'; + 'src/objects/__name___item.object.ts': (_name, namespace) => `import { ObjectSchema } from '@objectstack/spec/data'; -const ${toCamelCase(namespace)}Item: Data.ServiceObject = { +const ${toCamelCase(namespace)}Item = ObjectSchema.create({ name: '${namespace}_item', label: '${toTitleCase(namespace)} Item', fields: { @@ -760,7 +760,7 @@ const ${toCamelCase(namespace)}Item: Data.ServiceObject = { // authored decision rather than an accident. The other values, and how to // widen access safely: https://objectstack.ai/docs/permissions/sharing-rules sharingModel: 'private', -}; +}); export default ${toCamelCase(namespace)}Item; `, diff --git a/packages/cli/src/utils/emitted-source-parses.ts b/packages/cli/src/utils/emitted-source-parses.ts index a47cf374d67..2da64e4478b 100644 --- a/packages/cli/src/utils/emitted-source-parses.ts +++ b/packages/cli/src/utils/emitted-source-parses.ts @@ -11,7 +11,7 @@ * straight into a binding position: * * os generate object foo.bar exit 0 - * src/objects/foo.bar.object.ts -> const foo.bar: Data.ServiceObject = { + * src/objects/foo.bar.object.ts -> const foo.bar = ObjectSchema.create({ * src/objects/index.ts -> export { default as foo.bar } from './foo.bar.object'; * * Two files, neither of them TypeScript, and a command that reported success. diff --git a/packages/cli/test/generate-emission-parses.test.ts b/packages/cli/test/generate-emission-parses.test.ts index 8dd89873afa..6bfa0ec65eb 100644 --- a/packages/cli/test/generate-emission-parses.test.ts +++ b/packages/cli/test/generate-emission-parses.test.ts @@ -11,7 +11,7 @@ * the name went into a binding position untouched: * * os generate object foo.bar exit 0 - * src/objects/foo.bar.object.ts -> const foo.bar: Data.ServiceObject = { + * src/objects/foo.bar.object.ts -> const foo.bar = ObjectSchema.create({ * src/objects/index.ts -> export { default as foo.bar } from './foo.bar.object'; * * One name, TWO broken files, and a command that reported success. The blast @@ -145,7 +145,7 @@ describe('[#16541] CANARY — the card`s measured name is refused, at both emiss const object = ROSTER.find((t) => t.type === 'object'); if (!object) throw new Error('the `object` generator is gone'); const { scaffold } = emissionsFor('object', object.generate, 'foo.bar'); - expect(scaffold.source).toContain('const foo.bar: Data.ServiceObject = {'); + expect(scaffold.source).toContain('const foo.bar = ObjectSchema.create({'); const failures = await findEmissionParseFailures([scaffold]); expect(failures).toHaveLength(1); expect(failures[0].diagnostics.length).toBeGreaterThan(0); @@ -164,7 +164,7 @@ describe('[#16541] DISCRIMINATOR — the verdict comes from the compiler, not fr const object = ROSTER.find((t) => t.type === 'object'); if (!object) throw new Error('the `object` generator is gone'); const { scaffold } = emissionsFor('object', object.generate, 'class'); - expect(scaffold.source).toContain('const class:'); + expect(scaffold.source).toContain('const class ='); expect(await findEmissionParseFailures([scaffold])).not.toEqual([]); }); diff --git a/packages/cli/test/generate-refuses-name-outside-charset.test.ts b/packages/cli/test/generate-refuses-name-outside-charset.test.ts index 033b04ae8c2..6c06d6dd7dc 100644 --- a/packages/cli/test/generate-refuses-name-outside-charset.test.ts +++ b/packages/cli/test/generate-refuses-name-outside-charset.test.ts @@ -37,7 +37,7 @@ * ACCEPTED it (its emission parses clean, asserted here against the very * instrument the command runs). Delete the gate and this name generates. * - `class` — ADMITTED by the gate (every character is in the charset), and - * REFUSED by the parse check for `object`, because `const class:` is not a + * REFUSED by the parse check for `object`, because `const class =` is not a * declaration. Delete the parse check and this name generates. * * ## Why a child process diff --git a/packages/cli/test/generate-refuses-unparseable-name.test.ts b/packages/cli/test/generate-refuses-unparseable-name.test.ts index 788cd4a4b5d..47c276d9a4d 100644 --- a/packages/cli/test/generate-refuses-unparseable-name.test.ts +++ b/packages/cli/test/generate-refuses-unparseable-name.test.ts @@ -42,7 +42,7 @@ * * So the parse check is measured through `class`, added here for that purpose. * It is inside the charset (every character is a lowercase letter), the gate - * admits it, and `const class:` is still not a declaration — so it is the name + * admits it, and `const class =` is still not a declaration — so it is the name * that proves this command consults the compiler before it writes, and that * the layer in front did not swallow the layer behind. ⛔ Nothing was deleted * to make room for it: every `foo.bar` assertion that is still about the @@ -252,7 +252,7 @@ describe('[#16541] CONTROL — an ordinary name is untouched by this change', () it('writes a scaffold that parses, still binding `orderLine`', () => { const scaffold = readFileSync(join(controlDir, 'src', 'objects', 'order_line.object.ts'), 'utf8'); expect(parseErrors(scaffold)).toEqual([]); - expect(scaffold).toContain('const orderLine: Data.ServiceObject = {'); + expect(scaffold).toContain('const orderLine = ObjectSchema.create({'); }); it('writes a barrel that parses, still re-exporting `orderLine`', () => { diff --git a/packages/cli/test/scaffold-emission-typechecks.test.ts b/packages/cli/test/scaffold-emission-typechecks.test.ts index 9a55fc77fef..0d4d4657511 100644 --- a/packages/cli/test/scaffold-emission-typechecks.test.ts +++ b/packages/cli/test/scaffold-emission-typechecks.test.ts @@ -28,6 +28,14 @@ * the hand-written docs already used (`concepts/metadata-driven.mdx`, * `getting-started/quick-reference.mdx`). Nothing was added to the spec. * + * The object scaffolds have since moved off the annotation altogether: they + * declare through `ObjectSchema.create({ … })`, the one authorised shape for a + * `*.object.ts` (ruling 5644350230), whose return type is derived from the + * declaration it validates. This pin is unchanged by that — the other + * generators still annotate with namespace members (`UI.View`, + * `Automation.Flow`, …), and the factory's VALUE import from + * `@objectstack/spec/data` is exactly what this sandbox has to resolve. + * * ## ⭐ Why every existing scaffold pin was green through it * * This package already had two scaffold sweeps, and NEITHER could see this diff --git a/packages/cli/test/scaffold-object-declaration-shape.test.ts b/packages/cli/test/scaffold-object-declaration-shape.test.ts new file mode 100644 index 00000000000..b50a5e5c57c --- /dev/null +++ b/packages/cli/test/scaffold-object-declaration-shape.test.ts @@ -0,0 +1,204 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * PIN — every door this package opens onto a `*.object.ts` writes the ONE + * authorised declaration shape, and they all write the SAME one. + * + * ## The ruling + * + * Ruling 5644350230 (director seat, decision batch #122 item 1) made + * `ObjectSchema.create({ … })` the one authorised shape for a `*.object.ts`. + * The factory parses the declaration against `ObjectSchema` when the file is + * evaluated, so a mistake surfaces where it was written; a + * `Data.ServiceObject`-annotated literal defers every check to a build the + * author may never run. `os init` (both object-bearing `TEMPLATES`) and + * `os generate object` were the outliers: `create-objectstack`'s starter, + * `schema-design.mdx` ("Every object definition follows this pattern") and the + * in-repo object files already stood on the factory. + * + * ## Why parity is a pin and not a comment + * + * `generate.ts`'s object generator declares that it emits what the `os init` + * templates emit, so "the two doors an author can arrive through agree". That + * sentence held the `sharingModel` VALUE for years while both doors kept a + * declaration SHAPE nobody compared. A shape moved at one door and not the + * other is the exact drift this file turns red on. + * + * ## What is read, and with what + * + * The TypeScript parser, over the exact bytes each emitter returns — the same + * instrument `emitted-source-parses.ts` uses, and for the same reason: a regex + * over the source is a second opinion about the grammar. The file's shape is + * reduced to a signature: + * + * - how `ObjectSchema` is imported from `@objectstack/spec/data` — a VALUE + * import is load-bearing, because `import type` is erased at compile time + * and the emitted module would throw on its first evaluation; + * - each top-level declaration: whether it carries a type annotation, and + * what its initializer is; + * - what the file default-exports. Both barrels (`os init`'s + * `src/objects/index.ts` and the line `os generate` appends) re-export + * `default`, so the default export must stay the declared binding. + * + * ## The controls + * + * A reader that cannot tell the two shapes apart would pass every emitter. + * So the pre-ruling annotated literal is read by the same function and must + * be refused, and a type-only factory import must be refused too — each + * proves one half of the judgement can go red. + */ + +import { describe, expect, it } from 'vitest'; +import { ts } from 'ts-morph'; +import { TEMPLATES, sanitizeNamespace } from '../src/commands/init.js'; +import { GENERATOR_SCAFFOLD_TARGETS } from '../src/commands/generate.js'; + +const SPEC_DATA = '@objectstack/spec/data'; + +/** The declaration shape of one emitted `*.object.ts`, binding names abstracted. */ +interface DeclarationShape { + /** `ObjectSchema` from `@objectstack/spec/data`: a value import, a type-only one, or none. */ + factoryImport: 'value' | 'type-only' | 'absent'; + /** Every top-level `const`/`let`/`var` declarator, in source order. */ + declarations: Array<{ binding: string; annotated: boolean; initializer: string }>; + /** What `export default` names: `binding` when it is the declared binding, else the node kind. */ + defaultExport: string | null; +} + +function readShape(source: string): DeclarationShape { + const file = ts.createSourceFile('emitted.object.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); + let factoryImport: DeclarationShape['factoryImport'] = 'absent'; + const declarations: DeclarationShape['declarations'] = []; + let defaultExport: string | null = null; + let defaultName: string | null = null; + + for (const stmt of file.statements) { + if (ts.isImportDeclaration(stmt)) { + const spec = ts.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : ''; + const bindings = stmt.importClause?.namedBindings; + if (spec !== SPEC_DATA || !bindings || !ts.isNamedImports(bindings)) continue; + const named = bindings.elements.find((e) => (e.propertyName ?? e.name).text === 'ObjectSchema'); + if (!named) continue; + factoryImport = stmt.importClause!.isTypeOnly || named.isTypeOnly ? 'type-only' : 'value'; + } else if (ts.isVariableStatement(stmt)) { + for (const decl of stmt.declarationList.declarations) { + const init = decl.initializer; + let initializer = init ? ts.SyntaxKind[init.kind] : 'none'; + if (init && ts.isCallExpression(init)) { + const callee = init.expression.getText(file).replace(/\s+/g, ''); + const arg = init.arguments.length === 1 && ts.isObjectLiteralExpression(init.arguments[0]); + initializer = `${callee}(${arg ? '{…}' : '?'})`; + } + declarations.push({ binding: decl.name.getText(file), annotated: decl.type !== undefined, initializer }); + } + } else if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) { + defaultName = ts.isIdentifier(stmt.expression) ? stmt.expression.text : null; + defaultExport = defaultName ?? ts.SyntaxKind[stmt.expression.kind]; + } + } + if (defaultName !== null && declarations.some((d) => d.binding === defaultName)) defaultExport = 'binding'; + return { factoryImport, declarations, defaultExport }; +} + +/** Every way `shape` departs from the authorised declaration. Empty = conforming. */ +function shapeFindings(shape: DeclarationShape): string[] { + const findings: string[] = []; + if (shape.factoryImport !== 'value') { + findings.push(`\`ObjectSchema\` is not value-imported from '${SPEC_DATA}' (${shape.factoryImport})`); + } + if (shape.declarations.length !== 1) { + findings.push(`expected exactly one top-level declaration, found ${shape.declarations.length}`); + } + for (const d of shape.declarations) { + if (d.initializer !== 'ObjectSchema.create({…})') { + findings.push(`\`${d.binding}\` is initialised by ${d.initializer}, not ObjectSchema.create({…})`); + } + if (d.annotated) findings.push(`\`${d.binding}\` carries a type annotation`); + } + if (shape.defaultExport !== 'binding') { + findings.push(`the default export is ${shape.defaultExport ?? 'absent'}, not the declared binding`); + } + return findings; +} + +/** The shape with binding names dropped, so two doors compare on shape alone. */ +function signature(shape: DeclarationShape): string { + return JSON.stringify({ + factoryImport: shape.factoryImport, + declarations: shape.declarations.map(({ annotated, initializer }) => ({ annotated, initializer })), + defaultExport: shape.defaultExport, + }); +} + +// ── The roster: derived, both halves ───────────────────────────────────── + +const PROJECT_NAME = 'my-app'; +const namespace = sanitizeNamespace(PROJECT_NAME); + +const INIT_DOORS = Object.entries(TEMPLATES).flatMap(([key, template]) => + Object.entries(template.srcFiles ?? {}) + .filter(([file]) => file.endsWith('.object.ts')) + .map(([file, render]) => ({ door: `os init -t ${key} → ${file}`, source: render(PROJECT_NAME, namespace) })), +); + +const GENERATE_DOORS = GENERATOR_SCAFFOLD_TARGETS + .filter((g) => g.type === 'object') + .map((g) => ({ door: `os generate ${g.type} order_line`, source: g.generate('order_line') })); + +const DOORS = [...INIT_DOORS, ...GENERATE_DOORS]; + +describe('the roster is derived and covers both commands', () => { + it('reads every `os init` object template and the `os generate object` emitter', () => { + // `app` and `plugin` each emit one object file today; a template added + // tomorrow joins the roster by being in `TEMPLATES`, not by being listed. + expect(INIT_DOORS.length).toBeGreaterThanOrEqual(2); + expect(GENERATE_DOORS).toHaveLength(1); + }); +}); + +describe('every door writes `ObjectSchema.create({ … })`', () => { + it.each(DOORS)('$door', ({ source }) => { + expect(shapeFindings(readShape(source))).toEqual([]); + }); +}); + +describe('`os init` and `os generate object` write the SAME declaration shape', () => { + it('one signature across every door', () => { + const signatures = new Map(DOORS.map((d) => [d.door, signature(readShape(d.source))])); + expect(new Set(signatures.values()).size, JSON.stringify(Object.fromEntries(signatures), null, 2)).toBe(1); + }); +}); + +describe('controls — the reader can refuse each half', () => { + it('refuses the pre-ruling `Data.ServiceObject`-annotated literal', () => { + const legacy = [ + "import * as Data from '@objectstack/spec/data';", + '', + 'const myAppItem: Data.ServiceObject = {', + " name: 'my_app_item',", + ' fields: {},', + '};', + '', + 'export default myAppItem;', + '', + ].join('\n'); + const findings = shapeFindings(readShape(legacy)); + expect(findings).toContain('`ObjectSchema` is not value-imported from \'@objectstack/spec/data\' (absent)'); + expect(findings).toContain('`myAppItem` is initialised by ObjectLiteralExpression, not ObjectSchema.create({…})'); + expect(findings).toContain('`myAppItem` carries a type annotation'); + }); + + it('refuses a type-only factory import, which is erased before the module runs', () => { + const typeOnly = [ + "import type { ObjectSchema } from '@objectstack/spec/data';", + '', + "const myAppItem = ObjectSchema.create({ name: 'my_app_item', fields: {} });", + '', + 'export default myAppItem;', + '', + ].join('\n'); + expect(shapeFindings(readShape(typeOnly))).toEqual([ + '`ObjectSchema` is not value-imported from \'@objectstack/spec/data\' (type-only)', + ]); + }); +}); From 468000c4d33c114821ce729f204f2286965f4673 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 07:39:25 +0000 Subject: [PATCH 2/2] docs(cli): os generate "What it does" names defineSkill for the skill type The item-1 sentence claimed every non-object type is written as a typed literal; `os g skill` writes `defineSkill({ ... })`. The clause now names the skill exception and lists the five typed-literal types exactly. Claude-Session: https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP Co-authored-by: Claude --- content/docs/deployment/cli.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/docs/deployment/cli.mdx b/content/docs/deployment/cli.mdx index 19d56e2f954..8e83dfbd294 100644 --- a/content/docs/deployment/cli.mdx +++ b/content/docs/deployment/cli.mdx @@ -1447,7 +1447,7 @@ third-party extension primitive, authored as `src/skills/.skill.ts` with - `--dry-run` — Preview without writing files **What it does:** -1. Creates the TypeScript file — an `object` declared with `ObjectSchema.create({ … })`, the same shape the `os init` templates write; the other types as typed literals (`UI.View`, `Automation.Flow`, etc.) +1. Creates the TypeScript file — an `object` declared with `ObjectSchema.create({ … })`, the same shape the `os init` templates write; a `skill` declared with `defineSkill({ … })`; the other types as typed literals (`UI.View`, `UI.Action`, `Automation.Flow`, `UI.Dashboard`, `UI.App`) 2. Creates or updates the barrel `index.ts` in the target directory 3. Shows a hint to run `objectstack validate`