@metaobjectsdev/sdk 0.24.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -122,7 +122,7 @@ classify it (using the classification scheme in `SKILL.md`) and route the cutove
122
122
 
123
123
  ## Index — `index.*` (non-unique retrieval)
124
124
 
125
- - **`index.lookup`** (`@fields` required; physical escapes `@using`/`@expr`/`@where`/`@orders`) —
125
+ - **`index.lookup`** (keys off `@fields` XOR `@expr` — exactly one, never both; physical escapes `@using`/`@where`/`@orders`) —
126
126
  a NON-unique retrieval index (uniqueness is what distinguishes it from `identity.secondary`);
127
127
  hunt hand-created lookup / recency indexes (`CREATE INDEX …`) it models.
128
128
 
@@ -590,17 +590,27 @@ does NOT enforce uniqueness. Choose the right construct by what the constraint I
590
590
  | Unique alternate key (e.g. email, slug) | `identity.secondary` — uniqueness is the type |
591
591
  | Query-performance index, no uniqueness | `index.lookup` |
592
592
 
593
- `@fields` names the indexed columns and is **required** (at least one). The db provider
594
- contributes physical-tuning attrs: `@orders` (per-column sort direction), `@using` (access
595
- method`gin`/`gist`/`hash`; default `btree`), `@expr` (key expression derived from
596
- `@fields`, e.g. for a functional index), and `@where` (partial-index predicate).
593
+ **An index keys off EXACTLY ONE of `@fields` or `@expr`.** `@fields` names the indexed
594
+ columns; `@expr` is a raw key expression used **instead of** `@fields` (a functional index).
595
+ Declaring **neither** or **both** is `ERR_INVALID_INDEX`. This applies to
596
+ `identity.secondary` as well as `index.lookup`: uniqueness lives in the type, so a unique
597
+ index keys itself the same way.
598
+
599
+ The db provider contributes physical-tuning attrs alongside either form: `@orders`
600
+ (per-column sort direction), `@using` (access method — `gin`/`gist`/`hash`; default
601
+ `btree`), and `@where` (partial-index predicate).
597
602
 
598
603
  ```json
599
604
  { "index.lookup": { "name": "byCreatedAt", "@fields": ["createdAt"], "@orders": ["desc"] } }
600
605
  { "index.lookup": { "name": "byStatusCreatedAt", "@fields": ["status", "createdAt"] } }
601
- { "index.lookup": { "name": "byEmailCI", "@fields": ["email"], "@expr": "lower(email)" } }
606
+ { "index.lookup": { "name": "byEmailCI", "@expr": "lower(email)" } }
607
+ { "identity.secondary": { "name": "uniqLowerEmail", "@expr": "lower(email)" } }
602
608
  ```
603
609
 
610
+ > **Do not write `@fields` and `@expr` together.** It reads as "index this column, by this
611
+ > expression", but the expression is the whole key — the `@fields` list was silently
612
+ > discarded. It is now refused rather than half-honoured.
613
+
604
614
  `index.lookup` is a sibling of `identity.*` — declare it as a direct child of an `object.entity`,
605
615
  at the same level as fields and identities.
606
616
 
@@ -828,8 +838,16 @@ amendment 2026-08-06.)
828
838
 
829
839
  **Origin vocabulary (#195).** `origin.aggregate @agg` takes `count`/`sum`/`avg`/`min`/`max`
830
840
  (numeric reduces over `@of`), `any`/`all` (predicate quantifiers over a `@filter`; `@of`
831
- forbidden; empty set → `any=false`, `all=true`), and `collect` (an array rollup of `@of`
832
- into an `isArray` field, with optional `@distinct` / `@orderBy`). Any aggregate may be
841
+ forbidden; empty set → `any=false`, `all=true`), and `collect` (an array rollup
842
+ into an `isArray` field, with optional `@distinct` / `@orderBy`). **`collect` is the one
843
+ `@agg` where `@of` is OPTIONAL (#335):** name a column with `@of` to collect scalars, or
844
+ omit `@of` on a `field.object @objectRef` to collect each related row as that declared
845
+ value object — a **whole-object rollup**, lowered to `jsonb_agg(jsonb_build_object(…))`
846
+ on Postgres. The whole-object form requires an explicit `@via`, refuses `@distinct` (it is
847
+ a no-op whenever the value object carries the primary key), and requires every value-object
848
+ member to match a field on the `@via` **terminal** entity by name, with the same subtype
849
+ and array-ness. The declared value object IS the exposure: a field the entity has and the
850
+ value object omits is not projected. Any aggregate may be
833
851
  row-scoped with `@filter`. `origin.computed` carries a closed structured `@expr` tree (a
834
852
  derived scalar). `origin.first` picks one related row's column (`@of`) along `@via`,
835
853
  ordered by a **required `@orderBy`** (`["field:asc|desc", …]`, with the PK as tie-break) —
@@ -72,6 +72,16 @@ dotnet meta docs metaobjects --out Docs # → Docs/api/csharp (AGENT-API.md +
72
72
  `AGENT-API.md` — the exact imports, signatures, and payload field shapes for the
73
73
  generated code. **Before calling any generated code, read `api/csharp/AGENT-API.md`.**
74
74
 
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
+
75
85
  ## Persistence + routes are the deployed artifact
76
86
 
77
87
  C# generates a *complete* server stack: the entity classes + `AppDbContext` ARE the
@@ -38,6 +38,16 @@ metaobjects docs ./metadata --out ./docs # → ./docs/api/python (AGENT-API.md
38
38
  `AGENT-API.md` — the exact imports, signatures, and payload field shapes for the
39
39
  generated code. **Before calling any generated code, read `api/python/AGENT-API.md`.**
40
40
 
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
+
41
51
  ## Generators
42
52
 
43
53
  Wire generators by their stable name (`--generators <names>`), or run the default set.
@@ -67,6 +67,17 @@ block, and the inferred types.
67
67
  A second file, `.metaobjects/config.json`, holds static project state parseable by
68
68
  non-TS tooling; `meta init` scaffolds both plus the `metaobjects/` source dir.
69
69
 
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
+
70
81
  ## The generators
71
82
 
72
83
  From `@metaobjectsdev/codegen-ts/generators` (server-side, framework-neutral):
@@ -1 +1 @@
1
- {"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"AAkBA,OAAO,EAKL,KAAK,cAAc,EAEpB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,UAAU;IACzB;2EACuE;IACvE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;4BAGwB;IACxB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;IAC5C;;;;;gFAK4E;IAC5E,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAC3C;;gFAE4E;IAC5E,QAAQ,CAAC,cAAc,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,SAAS,CAAC;IAChE;;;;2EAIuE;IACvE,QAAQ,CAAC,oBAAoB,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;CAC9D;AAaD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9B,OAAO,CAAC,UAAU,CAAC,CA0ErB"}
1
+ {"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"AAmBA,OAAO,EAKL,KAAK,cAAc,EAEpB,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,UAAU;IACzB;2EACuE;IACvE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;4BAGwB;IACxB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;IAC5C;;;;;gFAK4E;IAC5E,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAC3C;;gFAE4E;IAC5E,QAAQ,CAAC,cAAc,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,SAAS,CAAC;IAChE;;;;2EAIuE;IACvE,QAAQ,CAAC,oBAAoB,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;CAC9D;AA8ED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,iBAAiB,CACrC,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9B,OAAO,CAAC,UAAU,CAAC,CA6FrB"}
@@ -10,13 +10,79 @@
10
10
  // today (`DEFAULT_SOURCES` in `sources.ts`); a project that declares
11
11
  // `sources` can point anywhere. No other call site may assume the directory
12
12
  // name — this is where that assumption is allowed to live, exactly once.
13
- import { join, resolve } from "node:path";
14
- import { ParseError, codeSource } from "@metaobjectsdev/metadata";
13
+ import { dirname, extname, join, resolve } from "node:path";
14
+ import { readdir, readFile } from "node:fs/promises";
15
+ import { ParseError, codeSource, SUBTYPE_ROOT, TYPE_METADATA } from "@metaobjectsdev/metadata";
15
16
  import { CONFIG_FILE, loadConfig } from "./config.js";
16
17
  import { discoverCollectionRoot, exists, isDir } from "./discovery.js";
17
18
  import { compileScope, matchesScope } from "./scope.js";
18
- import { DEFAULT_METADATA_DIR, DEFAULT_METAOBJECTS_DIR } from "./metadata-files.js";
19
+ import { DEFAULT_METADATA_DIR, DEFAULT_METAOBJECTS_DIR, isMetadataFile } from "./metadata-files.js";
19
20
  import { DEFAULT_SOURCES, orderedPathSpecs, resolveSpecPath, resolveSources, } from "./sources.js";
21
+ /** The canonical-JSON document root key (`metadata.root`) and the sigil-free
22
+ * YAML root mapping key (`metadata:`, at column 0 — ADR-0006), built from the
23
+ * metamodel constants rather than spelled out. */
24
+ const JSON_ROOT_KEY = `${TYPE_METADATA}.${SUBTYPE_ROOT}`;
25
+ const YAML_ROOT_KEY = new RegExp(`^${TYPE_METADATA}\\s*:`, "m");
26
+ /** Bound on the sniff below: one hit is enough to diagnose, and this runs only
27
+ * on a path that has already failed. */
28
+ const MAX_SNIFFED_FILES = 20;
29
+ /**
30
+ * Cheap "is this file a metadata DOCUMENT?" sniff — for a diagnostic, never for
31
+ * loading. It decides whether to change an error MESSAGE; nothing downstream
32
+ * reads its answer, and a false negative costs only the generic message.
33
+ *
34
+ * A file-EXTENSION test cannot answer this: `package.json` and `tsconfig.json`
35
+ * carry a recognized metadata extension ({@link isMetadataFile}), so every JS
36
+ * project root that has no metadata yet would be misdiagnosed as a metadata
37
+ * directory — a confidently wrong hint, which is worse than the generic one it
38
+ * would replace. The document root is `metadata.root` in canonical JSON and the
39
+ * sigil-free `metadata:` key in YAML authoring (ADR-0006), so those are what
40
+ * this looks for.
41
+ */
42
+ async function sniffsAsMetadataDocument(file) {
43
+ const text = await readFile(file, "utf8").catch(() => undefined);
44
+ if (text === undefined)
45
+ return false;
46
+ if (extname(file).toLowerCase() !== ".json") {
47
+ // YAML: the root mapping key, at column 0. Not a parse — this package has
48
+ // no YAML parser, and acquiring one to improve an error message would be a
49
+ // dependency bought with nothing.
50
+ return YAML_ROOT_KEY.test(text);
51
+ }
52
+ try {
53
+ const doc = JSON.parse(text);
54
+ return typeof doc === "object" && doc !== null && JSON_ROOT_KEY in doc;
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
60
+ /**
61
+ * Does `dir` hold metadata but carry no project marker — i.e. is it the
62
+ * METADATA directory, handed to a command whose directory argument is the
63
+ * PROJECT ROOT? (#344)
64
+ *
65
+ * That mistake is predictable rather than careless: the sibling `docs`
66
+ * positionals in the Python and C# ports ARE the metadata directory, and all
67
+ * three are spelled the same way. The generic diagnostic then advises declaring
68
+ * `sources`, which from inside the metadata directory is a dead end.
69
+ *
70
+ * Only the top level is scanned. A metadata directory has metadata files in it;
71
+ * recursing would walk `node_modules/` on the very input this must NOT
72
+ * misdiagnose.
73
+ */
74
+ async function holdsMetadataButIsNoRoot(dir, hasConfig) {
75
+ // A directory carrying the marker IS a project root, by the only definition
76
+ // of one there is (`discovery.ts`). Nothing to diagnose.
77
+ if (hasConfig)
78
+ return false;
79
+ const entries = await readdir(dir).catch(() => []);
80
+ for (const entry of entries.filter(isMetadataFile).slice(0, MAX_SNIFFED_FILES)) {
81
+ if (await sniffsAsMetadataDocument(join(dir, entry)))
82
+ return true;
83
+ }
84
+ return false;
85
+ }
20
86
  /** Narrow the zod-inferred `Config["scope"]` (whose `.optional()` fields are
21
87
  * typed `T | undefined` even when present) down to `Scope`'s
22
88
  * exactOptionalPropertyTypes-safe shape — a key is omitted entirely rather
@@ -98,8 +164,24 @@ export async function resolveCollection(startDir, opts) {
98
164
  // whenever a discovered config declares no `sources`, where nothing has
99
165
  // probed it at all.
100
166
  if (specs === DEFAULT_SOURCES && !(await isDir(join(configDir, DEFAULT_METADATA_DIR)))) {
167
+ // #344 — name the one wrong answer worth naming before falling back to the
168
+ // generic advice. Told "declare sources" while standing in the metadata
169
+ // directory, an author declares the metadata FILES, and the config schema
170
+ // then rejects a bare string (a `sources` entry is `{ "path": … }`) — two
171
+ // dead ends in a row for a caller whose only mistake was passing the
172
+ // directory the sibling ports' `docs` positional wants.
173
+ if (await holdsMetadataButIsNoRoot(configDir, hasConfig)) {
174
+ const parent = dirname(configDir);
175
+ const near = await discoverCollectionRoot(parent);
176
+ const suggested = near.hasConfig ? near.dir : parent;
177
+ throw new ParseError(`${configDir} looks like a metadata directory, not a project root — it holds metadata ` +
178
+ `files but carries no ${DEFAULT_METAOBJECTS_DIR}/${CONFIG_FILE}. A directory argument here is ` +
179
+ `the PROJECT ROOT that CONTAINS your metadata; where the metadata lives is then the root's ` +
180
+ `"sources" (default: the "${DEFAULT_METADATA_DIR}" directory beneath it). Try ${suggested} instead.`, { code: "ERR_COLLECTION_NOT_FOUND", source: codeSource("resolveCollection") });
181
+ }
101
182
  throw new ParseError(`no metadata sources declared in ${configDir} and no default "${DEFAULT_METADATA_DIR}" directory found. ` +
102
- `Declare "sources" in ${DEFAULT_METAOBJECTS_DIR}/config.json, or run 'meta init' to scaffold.`, { code: "ERR_COLLECTION_NOT_FOUND", source: codeSource("resolveCollection") });
183
+ `Declare "sources" in ${DEFAULT_METAOBJECTS_DIR}/${CONFIG_FILE} each entry is an OBJECT, ` +
184
+ `e.g. "sources": [{ "path": "model" }] — or run 'meta init' to scaffold.`, { code: "ERR_COLLECTION_NOT_FOUND", source: codeSource("resolveCollection") });
103
185
  }
104
186
  const sources = await resolveSources(configDir, specs);
105
187
  const scope = compileScope(toScope(scopeSpec));
@@ -1 +1 @@
1
- {"version":3,"file":"collection.js","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"collection.js","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metaobjectsdev/sdk",
3
- "version": "0.24.0",
3
+ "version": "0.24.1",
4
4
  "description": "Workspace helpers and agent-docs utilities for MetaObjects projects.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -56,7 +56,7 @@
56
56
  "access": "public"
57
57
  },
58
58
  "dependencies": {
59
- "@metaobjectsdev/metadata": "0.24.0",
59
+ "@metaobjectsdev/metadata": "0.24.1",
60
60
  "zod": "^3.23.0"
61
61
  },
62
62
  "devDependencies": {
package/src/collection.ts CHANGED
@@ -10,12 +10,13 @@
10
10
  // today (`DEFAULT_SOURCES` in `sources.ts`); a project that declares
11
11
  // `sources` can point anywhere. No other call site may assume the directory
12
12
  // name — this is where that assumption is allowed to live, exactly once.
13
- import { join, resolve } from "node:path";
14
- import { ParseError, codeSource } from "@metaobjectsdev/metadata";
13
+ import { dirname, extname, join, resolve } from "node:path";
14
+ import { readdir, readFile } from "node:fs/promises";
15
+ import { ParseError, codeSource, SUBTYPE_ROOT, TYPE_METADATA } from "@metaobjectsdev/metadata";
15
16
  import { CONFIG_FILE, loadConfig, type Config } from "./config.js";
16
17
  import { discoverCollectionRoot, exists, isDir } from "./discovery.js";
17
18
  import { compileScope, matchesScope, type Scope } from "./scope.js";
18
- import { DEFAULT_METADATA_DIR, DEFAULT_METAOBJECTS_DIR } from "./metadata-files.js";
19
+ import { DEFAULT_METADATA_DIR, DEFAULT_METAOBJECTS_DIR, isMetadataFile } from "./metadata-files.js";
19
20
  import {
20
21
  DEFAULT_SOURCES,
21
22
  orderedPathSpecs,
@@ -68,6 +69,71 @@ export interface Collection {
68
69
  readonly migrateScopePatterns: readonly string[] | undefined;
69
70
  }
70
71
 
72
+ /** The canonical-JSON document root key (`metadata.root`) and the sigil-free
73
+ * YAML root mapping key (`metadata:`, at column 0 — ADR-0006), built from the
74
+ * metamodel constants rather than spelled out. */
75
+ const JSON_ROOT_KEY = `${TYPE_METADATA}.${SUBTYPE_ROOT}`;
76
+ const YAML_ROOT_KEY = new RegExp(`^${TYPE_METADATA}\\s*:`, "m");
77
+
78
+ /** Bound on the sniff below: one hit is enough to diagnose, and this runs only
79
+ * on a path that has already failed. */
80
+ const MAX_SNIFFED_FILES = 20;
81
+
82
+ /**
83
+ * Cheap "is this file a metadata DOCUMENT?" sniff — for a diagnostic, never for
84
+ * loading. It decides whether to change an error MESSAGE; nothing downstream
85
+ * reads its answer, and a false negative costs only the generic message.
86
+ *
87
+ * A file-EXTENSION test cannot answer this: `package.json` and `tsconfig.json`
88
+ * carry a recognized metadata extension ({@link isMetadataFile}), so every JS
89
+ * project root that has no metadata yet would be misdiagnosed as a metadata
90
+ * directory — a confidently wrong hint, which is worse than the generic one it
91
+ * would replace. The document root is `metadata.root` in canonical JSON and the
92
+ * sigil-free `metadata:` key in YAML authoring (ADR-0006), so those are what
93
+ * this looks for.
94
+ */
95
+ async function sniffsAsMetadataDocument(file: string): Promise<boolean> {
96
+ const text = await readFile(file, "utf8").catch(() => undefined);
97
+ if (text === undefined) return false;
98
+ if (extname(file).toLowerCase() !== ".json") {
99
+ // YAML: the root mapping key, at column 0. Not a parse — this package has
100
+ // no YAML parser, and acquiring one to improve an error message would be a
101
+ // dependency bought with nothing.
102
+ return YAML_ROOT_KEY.test(text);
103
+ }
104
+ try {
105
+ const doc: unknown = JSON.parse(text);
106
+ return typeof doc === "object" && doc !== null && JSON_ROOT_KEY in doc;
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Does `dir` hold metadata but carry no project marker — i.e. is it the
114
+ * METADATA directory, handed to a command whose directory argument is the
115
+ * PROJECT ROOT? (#344)
116
+ *
117
+ * That mistake is predictable rather than careless: the sibling `docs`
118
+ * positionals in the Python and C# ports ARE the metadata directory, and all
119
+ * three are spelled the same way. The generic diagnostic then advises declaring
120
+ * `sources`, which from inside the metadata directory is a dead end.
121
+ *
122
+ * Only the top level is scanned. A metadata directory has metadata files in it;
123
+ * recursing would walk `node_modules/` on the very input this must NOT
124
+ * misdiagnose.
125
+ */
126
+ async function holdsMetadataButIsNoRoot(dir: string, hasConfig: boolean): Promise<boolean> {
127
+ // A directory carrying the marker IS a project root, by the only definition
128
+ // of one there is (`discovery.ts`). Nothing to diagnose.
129
+ if (hasConfig) return false;
130
+ const entries = await readdir(dir).catch(() => [] as string[]);
131
+ for (const entry of entries.filter(isMetadataFile).slice(0, MAX_SNIFFED_FILES)) {
132
+ if (await sniffsAsMetadataDocument(join(dir, entry))) return true;
133
+ }
134
+ return false;
135
+ }
136
+
71
137
  /** Narrow the zod-inferred `Config["scope"]` (whose `.optional()` fields are
72
138
  * typed `T | undefined` even when present) down to `Scope`'s
73
139
  * exactOptionalPropertyTypes-safe shape — a key is omitted entirely rather
@@ -155,9 +221,28 @@ export async function resolveCollection(
155
221
  // whenever a discovered config declares no `sources`, where nothing has
156
222
  // probed it at all.
157
223
  if (specs === DEFAULT_SOURCES && !(await isDir(join(configDir, DEFAULT_METADATA_DIR)))) {
224
+ // #344 — name the one wrong answer worth naming before falling back to the
225
+ // generic advice. Told "declare sources" while standing in the metadata
226
+ // directory, an author declares the metadata FILES, and the config schema
227
+ // then rejects a bare string (a `sources` entry is `{ "path": … }`) — two
228
+ // dead ends in a row for a caller whose only mistake was passing the
229
+ // directory the sibling ports' `docs` positional wants.
230
+ if (await holdsMetadataButIsNoRoot(configDir, hasConfig)) {
231
+ const parent = dirname(configDir);
232
+ const near = await discoverCollectionRoot(parent);
233
+ const suggested = near.hasConfig ? near.dir : parent;
234
+ throw new ParseError(
235
+ `${configDir} looks like a metadata directory, not a project root — it holds metadata ` +
236
+ `files but carries no ${DEFAULT_METAOBJECTS_DIR}/${CONFIG_FILE}. A directory argument here is ` +
237
+ `the PROJECT ROOT that CONTAINS your metadata; where the metadata lives is then the root's ` +
238
+ `"sources" (default: the "${DEFAULT_METADATA_DIR}" directory beneath it). Try ${suggested} instead.`,
239
+ { code: "ERR_COLLECTION_NOT_FOUND", source: codeSource("resolveCollection") },
240
+ );
241
+ }
158
242
  throw new ParseError(
159
243
  `no metadata sources declared in ${configDir} and no default "${DEFAULT_METADATA_DIR}" directory found. ` +
160
- `Declare "sources" in ${DEFAULT_METAOBJECTS_DIR}/config.json, or run 'meta init' to scaffold.`,
244
+ `Declare "sources" in ${DEFAULT_METAOBJECTS_DIR}/${CONFIG_FILE} each entry is an OBJECT, ` +
245
+ `e.g. "sources": [{ "path": "model" }] — or run 'meta init' to scaffold.`,
161
246
  { code: "ERR_COLLECTION_NOT_FOUND", source: codeSource("resolveCollection") },
162
247
  );
163
248
  }