@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.
- package/agent-context/skills/metaobjects-audit/references/capability-checklist.md +1 -1
- package/agent-context/skills/metaobjects-authoring/SKILL.md +25 -7
- package/agent-context/skills/metaobjects-codegen/references/csharp.md +10 -0
- package/agent-context/skills/metaobjects-codegen/references/python.md +10 -0
- package/agent-context/skills/metaobjects-codegen/references/typescript.md +11 -0
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +86 -4
- package/dist/collection.js.map +1 -1
- package/package.json +2 -2
- package/src/collection.ts +89 -4
|
@@ -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`
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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", "@
|
|
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
|
|
832
|
-
into an `isArray` field, with optional `@distinct` / `@orderBy`).
|
|
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):
|
package/dist/collection.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/collection.js
CHANGED
|
@@ -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 {
|
|
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}
|
|
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));
|
package/dist/collection.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection.js","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"AAAA,mDAAmD;AACnD,EAAE;AACF,6DAA6D;AAC7D,EAAE;AACF,oEAAoE;AACpE,uEAAuE;AACvE,yEAAyE;AACzE,mEAAmE;AACnE,0EAA0E;AAC1E,qEAAqE;AACrE,4EAA4E;AAC5E,yEAAyE;AACzE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
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.
|
|
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.
|
|
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 {
|
|
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}
|
|
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
|
}
|