Skip to content

Commit bcef4d9

Browse files
dmealingclaude
andcommitted
fix(cli,sdk): a directory argument is the PROJECT ROOT, and the error now says so (#344)
`meta docs metaobjects` has never worked on Node and never will: the Node positional is the project root, while the Python and C# `docs` positionals are the metadata directory — and all three are spelled the same way in help text. The Node one was literally named `<metadata>`. The diagnostic then advised declaring `sources`, which from inside the metadata directory is a second dead end: a `sources` entry is a tagged-union object (`{ "path": … }`), so the bare string an author writes next fails config-schema validation. - `resolveCollection` names the mistake when the directory it resolved holds metadata but carries no `.metaobjects/config.json`, and points at the directory to pass instead. The discriminator is the document ROOT (`metadata.root` / a column-0 `metadata:`), NOT the file extension — `package.json` and `tsconfig.json` carry a recognized metadata extension, so an extension test would misdiagnose every JS project root with no metadata yet, and a confidently wrong hint is worse than the generic one. - The generic message gained the shape it was asking for: `"sources": [{ "path": "model" }]`. - The positional is renamed `[<project-root>]` in both help surfaces, and `DocsFlags.metadata` → `projectRoot` — the internal name is what kept regenerating the misleading word. - The agent-context `metaobjects-codegen` references gained the cross-port note beside their own `docs` examples (the C# one shipped `dotnet meta docs metaobjects`, the exact string that misleads on Node), and the `sources` entry shape — which appeared in no installed skill — is now in the TS reference. TypeScript-only, deliberately: the sibling ports' directory arguments are metadata directories throughout, so the mistake is unreachable there. Their identically worded `ERR_COLLECTION_NOT_FOUND` is untouched, and the cross-port `source-resolution-conformance` corpus pins error CODES, not message text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
1 parent bf6fdb7 commit bcef4d9

14 files changed

Lines changed: 378 additions & 34 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,55 @@ extracting the authored examples from shipped docs, skills and fixtures and load
301301
under a strict loader — is tracked separately; `meta upgrade`'s retirement map is already
302302
the natural source of truth for "what must no longer appear in an example".
303303

304+
### Fixed — `meta docs metaobjects` sent you down a dead end, twice ([#344](https://github.com/metaobjectsdev/metaobjects/issues/344))
305+
306+
**Nothing here is a regression, and the CLI was behaving as designed** — which is the
307+
point. The Node `meta docs` positional is the **project root**; the Python and C# `docs`
308+
positionals are the **metadata directory** (`metaobjects docs ./metadata`, `dotnet meta
309+
docs metaobjects`), and all three are spelled the same way in help text. The Node one was
310+
literally named `<metadata>`. So `meta docs metaobjects --out out` told Node "the project
311+
root is `./metaobjects`", which asks it to find a `metaobjects/metaobjects/`.
312+
313+
It then reported *"no metadata sources declared … Declare `sources` in
314+
`.metaobjects/config.json`, or run `meta init` to scaffold"* — and **following that advice
315+
does not work either.** Declaring `"sources": ["metaobjects/meta.x.yaml"]` fails config-schema
316+
validation, because a `sources` entry is a tagged-union OBJECT (`{ "path": … }`), never a
317+
bare string. Two dead ends in a row for a caller whose only mistake was passing the
318+
directory a sibling port's `docs` wants. Reproduced on this repo's own unmodified
319+
`examples/advanced-modeling`.
320+
321+
**The error now names the mistake.** `resolveCollection` fires the targeted diagnostic when
322+
the directory it resolved holds metadata but carries no `.metaobjects/config.json` — the
323+
only project marker there is — and points at the directory to pass instead:
324+
325+
```
326+
<dir>/metaobjects looks like a metadata directory, not a project root — it holds metadata
327+
files but carries no .metaobjects/config.json. A directory argument here is the PROJECT
328+
ROOT that CONTAINS your metadata … Try <dir> instead.
329+
```
330+
331+
**The discriminator is the document ROOT, not the file extension**, and that distinction is
332+
load-bearing: `package.json` and `tsconfig.json` carry a recognized metadata extension, so
333+
an extension test would misdiagnose every JS project root that has no metadata yet — a
334+
confidently wrong hint, which is worse than the generic one it replaces. A cheap sniff for
335+
`metadata.root` (canonical JSON) / a top-level `metadata:` (sigil-free YAML, ADR-0006)
336+
decides it, on the already-failing path only, and both arms are pinned. The generic message
337+
also gained the shape it was asking for: `"sources": [{ "path": "model" }]`.
338+
339+
**The positional is renamed `[<project-root>]`** in `meta --help` and `meta docs --help`,
340+
along with `DocsFlags.metadata` → `projectRoot` — cosmetic, no behavioural change, but the
341+
explanatory sentence was already there and did not prevent the trap; the internal name is
342+
what kept regenerating the misleading word. The agent-context `metaobjects-codegen`
343+
references gained the cross-port note beside their own `docs` examples (the C# reference
344+
shipped `dotnet meta docs metaobjects`, the exact string that misleads on Node), and the
345+
`sources` entry shape — which appeared in **no** installed skill — is now in the TypeScript
346+
reference.
347+
348+
**TypeScript-only, deliberately.** The sibling ports' directory arguments are metadata
349+
directories throughout, so the mistake is structurally unreachable there; their identically
350+
worded `ERR_COLLECTION_NOT_FOUND` is untouched. The cross-port `source-resolution-conformance`
351+
corpus pins error CODES, not message text, and the code is unchanged.
352+
304353
### Added — every shipped metadata example is now gated against the strict registry ([#337](https://github.com/metaobjectsdev/metaobjects/issues/337))
305354

306355
**The same failure has now landed three times, and an adopter found it every time.** A doc

‎agent-context/skills/metaobjects-codegen/references/csharp.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@ dotnet meta docs metaobjects --out Docs # → Docs/api/csharp (AGENT-API.md +
7272
`AGENT-API.md` — the exact imports, signatures, and payload field shapes for the
7373
generated code. **Before calling any generated code, read `api/csharp/AGENT-API.md`.**
7474

75+
**The two `docs` positionals are NOT the same argument.** This one is the METADATA
76+
directory. The Node `meta docs` positional — used by every stack, since `migrate`,
77+
`verify --db` and the neutral model docs are Node-only — is the PROJECT ROOT that
78+
CONTAINS the metadata. Run `meta docs` with no positional, from the project root:
79+
80+
```bash
81+
dotnet meta docs metaobjects --out Docs # C#: the METADATA dir
82+
meta docs --out Docs # Node: run from the PROJECT ROOT (no positional)
83+
```
84+
7585
## Persistence + routes are the deployed artifact
7686

7787
C# generates a *complete* server stack: the entity classes + `AppDbContext` ARE the

‎agent-context/skills/metaobjects-codegen/references/python.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ metaobjects docs ./metadata --out ./docs # → ./docs/api/python (AGENT-API.md
3838
`AGENT-API.md` — the exact imports, signatures, and payload field shapes for the
3939
generated code. **Before calling any generated code, read `api/python/AGENT-API.md`.**
4040

41+
**The two `docs` positionals are NOT the same argument.** This one is the METADATA
42+
directory. The Node `meta docs` positional — used by every stack, since `migrate`,
43+
`verify --db` and the neutral model docs are Node-only — is the PROJECT ROOT that
44+
CONTAINS the metadata. Run `meta docs` with no positional, from the project root:
45+
46+
```bash
47+
metaobjects docs ./metadata --out ./docs # Python: the METADATA dir
48+
meta docs --out ./docs # Node: run from the PROJECT ROOT (no positional)
49+
```
50+
4151
## Generators
4252

4353
Wire generators by their stable name (`--generators <names>`), or run the default set.

‎agent-context/skills/metaobjects-codegen/references/typescript.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,17 @@ block, and the inferred types.
6767
A second file, `.metaobjects/config.json`, holds static project state parseable by
6868
non-TS tooling; `meta init` scaffolds both plus the `metaobjects/` source dir.
6969

70+
`sources` in that file is where the metadata lives — `metaobjects/` is only its
71+
DEFAULT value, so a project can point it anywhere. **Every entry is an OBJECT, never
72+
a bare string**, and it names a DIRECTORY or a file:
73+
74+
```jsonc
75+
{ "schema_version": 1, "sources": [{ "path": "model" }, { "path": "../shared/metadata" }] }
76+
```
77+
78+
Every command's directory argument (`meta docs <project-root>`, `--cwd`) is the
79+
PROJECT ROOT that CONTAINS the metadata — never the metadata directory itself.
80+
7081
## The generators
7182

7283
From `@metaobjectsdev/codegen-ts/generators` (server-side, framework-neutral):

‎docs/features/metadata-sources.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,30 @@ through to the default and quietly generate from a stale `metaobjects/`.
193193
were declared anywhere up the walk, **and** no default `metaobjects/` directory
194194
exists either.
195195

196+
### Every directory argument is the PROJECT ROOT
197+
198+
`--cwd`, and the `meta docs` positional, name the project root — the directory that
199+
**contains** your metadata. They are never the metadata directory itself; where the
200+
metadata lives is `sources`' answer, and pointing a command at `metaobjects/` asks it
201+
to find a `metaobjects/metaobjects/`.
202+
203+
That is worth stating because the mistake is predictable rather than careless: the
204+
**Python and C# `docs` positionals ARE the metadata directory** (`metaobjects docs
205+
./metadata`, `dotnet meta docs metaobjects`), and each port spells its own the same
206+
way in help text. The Node CLI is the one every stack uses for `migrate`,
207+
`verify --db` and the neutral model docs, so the two conventions meet on one machine.
208+
209+
```bash
210+
meta docs --out ./docs # ✅ from the project root — the normal form
211+
meta docs ./apps/api --out ./docs # ✅ a project root elsewhere (SCOPES the run to it)
212+
meta docs metaobjects --out ./docs # ❌ that is the metadata directory
213+
```
214+
215+
Pointed at a metadata directory, `ERR_COLLECTION_NOT_FOUND` now says so and names the
216+
directory to pass instead
217+
([#344](https://github.com/metaobjectsdev/metaobjects/issues/344)) — it used to advise
218+
declaring `sources`, which from inside the metadata directory leads nowhere.
219+
196220
---
197221

198222
## `scope` — an output filter over fully-qualified names

‎fixtures/agent-context-conformance/python/expected/.claude/skills/metaobjects-codegen/references/python.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ metaobjects docs ./metadata --out ./docs # → ./docs/api/python (AGENT-API.md
3838
`AGENT-API.md` — the exact imports, signatures, and payload field shapes for the
3939
generated code. **Before calling any generated code, read `api/python/AGENT-API.md`.**
4040

41+
**The two `docs` positionals are NOT the same argument.** This one is the METADATA
42+
directory. The Node `meta docs` positional — used by every stack, since `migrate`,
43+
`verify --db` and the neutral model docs are Node-only — is the PROJECT ROOT that
44+
CONTAINS the metadata. Run `meta docs` with no positional, from the project root:
45+
46+
```bash
47+
metaobjects docs ./metadata --out ./docs # Python: the METADATA dir
48+
meta docs --out ./docs # Node: run from the PROJECT ROOT (no positional)
49+
```
50+
4151
## Generators
4252

4353
Wire generators by their stable name (`--generators <names>`), or run the default set.

‎fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-codegen/references/typescript.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,17 @@ block, and the inferred types.
6767
A second file, `.metaobjects/config.json`, holds static project state parseable by
6868
non-TS tooling; `meta init` scaffolds both plus the `metaobjects/` source dir.
6969

70+
`sources` in that file is where the metadata lives — `metaobjects/` is only its
71+
DEFAULT value, so a project can point it anywhere. **Every entry is an OBJECT, never
72+
a bare string**, and it names a DIRECTORY or a file:
73+
74+
```jsonc
75+
{ "schema_version": 1, "sources": [{ "path": "model" }, { "path": "../shared/metadata" }] }
76+
```
77+
78+
Every command's directory argument (`meta docs <project-root>`, `--cwd`) is the
79+
PROJECT ROOT that CONTAINS the metadata — never the metadata directory itself.
80+
7081
## The generators
7182

7283
From `@metaobjectsdev/codegen-ts/generators` (server-side, framework-neutral):

‎fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-codegen/references/typescript.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,17 @@ block, and the inferred types.
6767
A second file, `.metaobjects/config.json`, holds static project state parseable by
6868
non-TS tooling; `meta init` scaffolds both plus the `metaobjects/` source dir.
6969

70+
`sources` in that file is where the metadata lives — `metaobjects/` is only its
71+
DEFAULT value, so a project can point it anywhere. **Every entry is an OBJECT, never
72+
a bare string**, and it names a DIRECTORY or a file:
73+
74+
```jsonc
75+
{ "schema_version": 1, "sources": [{ "path": "model" }, { "path": "../shared/metadata" }] }
76+
```
77+
78+
Every command's directory argument (`meta docs <project-root>`, `--cwd`) is the
79+
PROJECT ROOT that CONTAINS the metadata — never the metadata directory itself.
80+
7081
## The generators
7182

7283
From `@metaobjectsdev/codegen-ts/generators` (server-side, framework-neutral):

‎server/typescript/packages/cli/src/commands/docs.ts‎

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// `meta docs <metadata> --out <dir>` — STANDALONE neutral metadata docs.
1+
// `meta docs [<project-root>] --out <dir>` — STANDALONE neutral metadata docs.
22
//
33
// Emits one neutral page per entity (`<Entity>.md`) and one per
44
// `template.output` (`<Template>.md`) from metadata ALONE — no gen config, no
@@ -39,16 +39,20 @@ import { generateSite, SITE_TEMPLATE_NAMES, SITE_ASSET_NAMES, readSiteFile } fro
3939
type DocsLayout = "flat" | "package";
4040

4141
interface DocsFlags {
42-
/** Project root holding `metaobjects/` (the metadata to document). */
43-
metadata: string;
42+
/** The PROJECT ROOT to resolve metadata from — the directory that CONTAINS
43+
* the metadata, never the metadata directory itself (#344). Named for what
44+
* it is: while it was called `metadata` the help text spelled the positional
45+
* `<metadata>`, which is what the Python and C# `docs` positionals actually
46+
* mean, and pointing this one at `metaobjects/` fails. */
47+
projectRoot: string;
4448
/** Output directory for the rendered pages. */
4549
out: string;
4650
/** Page-placement layout. `flat` (default) writes `<Name>.md` at the out
4751
* root; `package` folds pages under package-path subdirs (collision-proof
4852
* for multi-package models with repeated short names). */
4953
layout: DocsLayout;
5054
/** Optional override for the project root used to resolve adopter
51-
* `templates/` overrides. Defaults to the metadata root. */
55+
* `templates/` overrides. Defaults to the project root. */
5256
templates?: string;
5357
/** Optional directory holding the prompt `.mustache` sources, for a project
5458
* whose templates live outside the conventional `metaobjects/` or `templates/`
@@ -66,12 +70,12 @@ interface DocsFlags {
6670
outProvided: boolean;
6771
/** Whether `--layout` was explicitly passed (same override semantics). */
6872
layoutProvided: boolean;
69-
/** Whether the `<metadata>` positional was explicitly passed. An explicit path
73+
/** Whether the `<project-root>` positional was explicitly passed. An explicit path
7074
* DEFINES the source set (`resolveCollection`'s `explicitDir` pin); the default
7175
* cwd discovers one by walking up. Without this distinction the two are
7276
* indistinguishable at the call site, which is how an explicitly-scoped run came
7377
* to have an ancestor config's sources unioned into it (#327). */
74-
metadataProvided: boolean;
78+
projectRootProvided: boolean;
7579
/** FR-033 S3 — document the METAMODEL ITSELF (the built-in type/subtype/attr
7680
* vocabulary) instead of a user's entities. Needs NO metadata + NO config. */
7781
metamodel: boolean;
@@ -93,7 +97,7 @@ function parseLayout(v: string | undefined, flag: string): DocsLayout {
9397
}
9498

9599
function parseDocsArgs(argv: string[], cwd: string): DocsFlags {
96-
let metadata: string | undefined;
100+
let projectRoot: string | undefined;
97101
let out: string | undefined;
98102
let templates: string | undefined;
99103
let prompts: string | undefined;
@@ -155,8 +159,8 @@ function parseDocsArgs(argv: string[], cwd: string): DocsFlags {
155159
prompts = a.slice("--prompts=".length);
156160
} else if (a.startsWith("-")) {
157161
throw new Error(`unknown flag: ${a}`);
158-
} else if (metadata === undefined) {
159-
metadata = a;
162+
} else if (projectRoot === undefined) {
163+
projectRoot = a;
160164
} else {
161165
throw new Error(`unexpected argument: ${a}`);
162166
}
@@ -172,11 +176,11 @@ function parseDocsArgs(argv: string[], cwd: string): DocsFlags {
172176
if (wantApi) surfaces.push("api");
173177
if (wantRequirements) surfaces.push("requirements");
174178
return {
175-
// `<metadata>` is the project root that contains metaobjects/; default cwd
176-
// (mirrors how migrate/gen treat the working directory as the root).
177-
metadata: metadata ?? cwd,
178-
metadataProvided: metadata !== undefined,
179-
// Default out dir, resolved against the metadata root below. In --metamodel
179+
// `<project-root>` is the project root that CONTAINS the metadata; default
180+
// cwd (mirrors how migrate/gen treat the working directory as the root).
181+
projectRoot: projectRoot ?? cwd,
182+
projectRootProvided: projectRoot !== undefined,
183+
// Default out dir, resolved against the project root below. In --metamodel
180184
// mode the renderer writes under <out>/metamodel/, default ./docs/metamodel.
181185
out: out ?? (wantMetamodel ? "./docs/metamodel" : "./docs"),
182186
// Default flat preserves today's single-package output (+ existing goldens).
@@ -210,7 +214,7 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
210214
return metamodelDocsCommand(cwd, flags.out);
211215
}
212216

213-
const metaRoot = resolvePath(cwd, flags.metadata);
217+
const metaRoot = resolvePath(cwd, flags.projectRoot);
214218
// Absolute prompt-source dir for the site (--prompts), for a project whose
215219
// templates live outside metaobjects/ or templates/ (e.g. data/templates/).
216220
const promptsDir = flags.prompts !== undefined ? resolvePath(cwd, flags.prompts) : undefined;
@@ -238,11 +242,11 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
238242
// everything project-relative — `metaobjects.config.ts` and its providers,
239243
// the `docs.outDir` it names, the adopter `templates/` overrides, the owned
240244
// `codegen/docs-site/` theme — has to come from that same directory. Reading
241-
// the config from the ambient `<metadata>` argument while the metadata came
245+
// the config from the ambient `<project-root>` argument while the metadata came
242246
// from an ancestor renders the ancestor's model with the subdirectory's
243247
// (absent) providers. For a run at the project root the two are the same path.
244248
//
245-
// An EXPLICIT `<metadata>` positional pins the collection rather than seeding a
249+
// An EXPLICIT `<project-root>` positional pins the collection rather than seeding a
246250
// walk (#327). The argument has always meant "document this": before sources were
247251
// resolvable it read `<path>/metaobjects/` and nothing else, and passing it to
248252
// discovery turned it into a starting point, so the nearest ancestor
@@ -255,7 +259,7 @@ export async function docsCommand(args: string[], cwd: string): Promise<number>
255259
try {
256260
collection = await resolveCollection(
257261
metaRoot,
258-
flags.metadataProvided ? { explicitDir: metaRoot } : undefined,
262+
flags.projectRootProvided ? { explicitDir: metaRoot } : undefined,
259263
);
260264
} catch (err) {
261265
log.error(`docs: ${(err as Error).message}`);

0 commit comments

Comments
 (0)