@hawkeyexl/manni 0.2.0 → 1.0.0

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.
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@hawkeyexl/manni",
6
- version: "0.2.0",
6
+ version: "1.0.0",
7
7
  description: "One command line for documentation that is meant to be checked. `manni meta` validates the presence and format of document metadata (Markdown frontmatter and more) against JSON Schema, built for CI. Formerly docmeta.",
8
8
  type: "module",
9
9
  bin: {
@@ -117,6 +117,276 @@ var package_default = {
117
117
  }
118
118
  };
119
119
 
120
+ // src/shared/collections.ts
121
+ import picomatch2 from "picomatch";
122
+
123
+ // src/shared/globs.ts
124
+ import picomatch from "picomatch";
125
+ var matcherCache = /* @__PURE__ */ new Map();
126
+ function matchesFileGlob(relPath, glob) {
127
+ const key = JSON.stringify(glob);
128
+ let m = matcherCache.get(key);
129
+ if (!m) {
130
+ m = picomatch(glob, { dot: true });
131
+ matcherCache.set(key, m);
132
+ }
133
+ return m(relPath.replace(/\\/g, "/"));
134
+ }
135
+
136
+ // src/shared/collections.ts
137
+ var COLLECTION_KEYS = [
138
+ "name",
139
+ "paths",
140
+ "exclude",
141
+ "url",
142
+ "externalMetadata"
143
+ ];
144
+ var EXTERNAL_METADATA_KEYS = ["file", "keys", "tokenEnv", "join"];
145
+ var SCHEMA_KEY = "$schema";
146
+ var STDIN = "<stdin>";
147
+ function isUrlRef(ref) {
148
+ return /^https?:\/\//i.test(ref);
149
+ }
150
+ function externalMetadataUrlProblem(url) {
151
+ let parsed2;
152
+ try {
153
+ parsed2 = new URL(url);
154
+ } catch {
155
+ return "is not a valid URL";
156
+ }
157
+ if (parsed2.username !== "" || parsed2.password !== "") {
158
+ return 'carries a credential in the URL; put the token in an environment variable and name it with "tokenEnv"';
159
+ }
160
+ if (parsed2.protocol === "https:") return null;
161
+ if (parsed2.protocol === "http:" && isLoopback(parsed2.hostname)) return null;
162
+ if (parsed2.protocol === "http:") {
163
+ return "is plain http://; a bearer token over plaintext is a leak, and a public manifest is served over https too";
164
+ }
165
+ return `has the unsupported scheme "${parsed2.protocol}"`;
166
+ }
167
+ function isLoopback(host) {
168
+ return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1" || host.startsWith("127.");
169
+ }
170
+ function rejectUnknownKeys(raw, allowed, where, source, toError) {
171
+ for (const key of Object.keys(raw)) {
172
+ if (!allowed.includes(key)) {
173
+ throw toError(
174
+ `${source}: ${where} has unknown key "${key}". Supported keys: ${allowed.join(", ")}.`
175
+ );
176
+ }
177
+ }
178
+ }
179
+ function isMapping(value) {
180
+ return typeof value === "object" && value !== null && !Array.isArray(value);
181
+ }
182
+ function isNonEmptyString(value) {
183
+ return typeof value === "string" && value.trim() !== "";
184
+ }
185
+ function parseCollections(raw, source, toError) {
186
+ if (!Array.isArray(raw)) {
187
+ throw toError(`${source}: "collections" must be a list.`);
188
+ }
189
+ if (raw.length === 0) {
190
+ throw toError(
191
+ `${source}: "collections" must name at least one collection; remove the key if there are none.`
192
+ );
193
+ }
194
+ const taken = /* @__PURE__ */ new Map();
195
+ return raw.map((entry, i) => {
196
+ const where = `collections[${i}]`;
197
+ if (!isMapping(entry)) {
198
+ throw toError(`${source}: ${where} must be a mapping.`);
199
+ }
200
+ rejectUnknownKeys(entry, COLLECTION_KEYS, where, source, toError);
201
+ if (!isNonEmptyString(entry.name)) {
202
+ throw toError(`${source}: ${where}.name must be a non-empty string.`);
203
+ }
204
+ const name2 = entry.name;
205
+ const folded = name2.toLowerCase();
206
+ if (folded === "docs") {
207
+ throw toError(
208
+ `${source}: ${where}.name "${name2}" collides with the docs table every query reads. Pick another name, such as "site" or "pages".`
209
+ );
210
+ }
211
+ if (folded.startsWith("sqlite_")) {
212
+ throw toError(
213
+ `${source}: ${where}.name "${name2}" starts with "sqlite_", which SQLite reserves for its own objects. Pick another name.`
214
+ );
215
+ }
216
+ const owner = taken.get(folded);
217
+ if (owner !== void 0) {
218
+ throw toError(
219
+ `${source}: ${where}.name "${name2}" is already taken by collections[${owner}]; names are compared case-insensitively because they become SQL views.`
220
+ );
221
+ }
222
+ taken.set(folded, i);
223
+ if (!Array.isArray(entry.paths) || entry.paths.length === 0 || entry.paths.some((p) => !isNonEmptyString(p))) {
224
+ throw toError(
225
+ `${source}: ${where}.paths must be a non-empty list of files, directories or globs.`
226
+ );
227
+ }
228
+ const paths = entry.paths;
229
+ let exclude = [];
230
+ if (entry.exclude !== void 0) {
231
+ if (!Array.isArray(entry.exclude) || entry.exclude.some((g) => !isNonEmptyString(g))) {
232
+ throw toError(`${source}: ${where}.exclude must be a list of globs.`);
233
+ }
234
+ exclude = entry.exclude;
235
+ }
236
+ let url;
237
+ if (entry.url !== void 0) {
238
+ if (typeof entry.url !== "string" || !isHttpUrl(entry.url)) {
239
+ throw toError(`${source}: ${where}.url must be an http(s) URL.`);
240
+ }
241
+ url = entry.url;
242
+ }
243
+ let externalMetadata = [];
244
+ if (entry.externalMetadata !== void 0) {
245
+ if (!Array.isArray(entry.externalMetadata)) {
246
+ throw toError(`${source}: ${where}.externalMetadata must be a list.`);
247
+ }
248
+ externalMetadata = parseExternalMetadata(
249
+ entry.externalMetadata,
250
+ where,
251
+ source,
252
+ toError
253
+ );
254
+ }
255
+ return {
256
+ name: name2,
257
+ paths,
258
+ exclude,
259
+ externalMetadata,
260
+ ...url === void 0 ? {} : { url }
261
+ };
262
+ });
263
+ }
264
+ function isHttpUrl(value) {
265
+ let parsed2;
266
+ try {
267
+ parsed2 = new URL(value);
268
+ } catch {
269
+ return false;
270
+ }
271
+ return parsed2.protocol === "http:" || parsed2.protocol === "https:";
272
+ }
273
+ function parseExternalMetadata(raw, collectionWhere, source, toError) {
274
+ const owners = /* @__PURE__ */ new Map();
275
+ return raw.map((entry, i) => {
276
+ const where = `${collectionWhere}.externalMetadata[${i}]`;
277
+ if (!isMapping(entry)) {
278
+ throw toError(`${source}: ${where} must be a mapping.`);
279
+ }
280
+ rejectUnknownKeys(entry, EXTERNAL_METADATA_KEYS, where, source, toError);
281
+ if (!isNonEmptyString(entry.file)) {
282
+ throw toError(
283
+ `${source}: ${where}.file must be a non-empty string naming the manifest, relative to the config file.`
284
+ );
285
+ }
286
+ if (!Array.isArray(entry.keys) || entry.keys.length === 0 || entry.keys.some((k) => !isNonEmptyString(k))) {
287
+ throw toError(
288
+ `${source}: ${where}.keys must be a non-empty list of key names.`
289
+ );
290
+ }
291
+ const isUrl = isUrlRef(entry.file);
292
+ if (isUrl) {
293
+ const problem = externalMetadataUrlProblem(entry.file);
294
+ if (problem !== null) {
295
+ throw toError(`${source}: ${where}.file ${problem}.`);
296
+ }
297
+ }
298
+ if (entry.tokenEnv !== void 0) {
299
+ if (!isNonEmptyString(entry.tokenEnv)) {
300
+ throw toError(
301
+ `${source}: ${where}.tokenEnv must be the name of an environment variable.`
302
+ );
303
+ }
304
+ if (!isUrl) {
305
+ throw toError(
306
+ `${source}: ${where}.tokenEnv is set, but "file" is a path, so no request is made and the token would go nowhere. Remove it, or make "file" a URL.`
307
+ );
308
+ }
309
+ }
310
+ const keys = entry.keys;
311
+ if (entry.join !== void 0) {
312
+ if (!isNonEmptyString(entry.join)) {
313
+ throw toError(
314
+ `${source}: ${where}.join must be "path" or the name of a top-level frontmatter field.`
315
+ );
316
+ }
317
+ if (entry.join === SCHEMA_KEY) {
318
+ throw toError(`${source}: ${where}.join may not be "${SCHEMA_KEY}".`);
319
+ }
320
+ if (keys.includes(entry.join)) {
321
+ throw toError(
322
+ `${source}: ${where}.join names "${entry.join}", which the same entry owns \u2014 the value that selects an entry cannot come from the entry.`
323
+ );
324
+ }
325
+ }
326
+ const seen = /* @__PURE__ */ new Set();
327
+ for (const key of keys) {
328
+ if (key === SCHEMA_KEY) {
329
+ throw toError(
330
+ `${source}: ${where}.keys may not include "${SCHEMA_KEY}" \u2014 a manifest never chooses the schema a document is judged by; use "overrides".`
331
+ );
332
+ }
333
+ if (seen.has(key)) {
334
+ throw toError(`${source}: ${where}.keys lists "${key}" twice.`);
335
+ }
336
+ seen.add(key);
337
+ const owner = owners.get(key);
338
+ if (owner !== void 0) {
339
+ throw toError(
340
+ `${source}: ${where}.keys claims "${key}", which externalMetadata[${owner}] already owns \u2014 a key has exactly one manifest in a collection.`
341
+ );
342
+ }
343
+ owners.set(key, i);
344
+ }
345
+ return {
346
+ file: entry.file,
347
+ keys,
348
+ ...typeof entry.tokenEnv === "string" ? { tokenEnv: entry.tokenEnv } : {},
349
+ ...typeof entry.join === "string" ? { join: entry.join } : {}
350
+ };
351
+ });
352
+ }
353
+ function selectCollections(collections, names, source, toError) {
354
+ if (names === void 0 || names.length === 0) return [...collections];
355
+ const wanted = new Set(names);
356
+ for (const name2 of wanted) {
357
+ if (!collections.some((c) => c.name === name2)) {
358
+ const configured = collections.length === 0 ? "(none)" : collections.map((c) => c.name).join(", ");
359
+ const nearMiss = collections.find(
360
+ (c) => c.name.toLowerCase() === name2.toLowerCase()
361
+ );
362
+ const hint = nearMiss === void 0 ? "" : ` Names are case-sensitive; did you mean "${nearMiss.name}"?`;
363
+ throw toError(
364
+ `no collection named "${name2}" in ${source}. Configured: ${configured}.${hint}`
365
+ );
366
+ }
367
+ }
368
+ return collections.filter((c) => wanted.has(c.name));
369
+ }
370
+ function isMember(collection, relPath) {
371
+ if (relPath === STDIN) return false;
372
+ if (relPath.startsWith("../")) return false;
373
+ const matched = collection.paths.some((entry) => {
374
+ const normalized = normalizeEntry(entry);
375
+ if (normalized === "") return false;
376
+ if (isGlob(normalized)) return matchesFileGlob(relPath, normalized);
377
+ return relPath === normalized || relPath.startsWith(`${normalized}/`);
378
+ });
379
+ if (!matched) return false;
380
+ return !collection.exclude.some((glob) => matchesFileGlob(relPath, glob));
381
+ }
382
+ function normalizeEntry(entry) {
383
+ const posix2 = entry.replace(/\\/g, "/").replace(/^\.\//, "");
384
+ return posix2.endsWith("/") ? posix2.slice(0, -1) : posix2;
385
+ }
386
+ function isGlob(entry) {
387
+ return picomatch2.scan(entry).isGlob;
388
+ }
389
+
120
390
  // src/shared/errors.ts
121
391
  var ToolError = class extends Error {
122
392
  /** clig.dev: 2 is operational/usage; 1 is reserved for findings. */
@@ -154,8 +424,8 @@ var RENAME_ATTEMPTS = 3;
154
424
  function errorCode(err) {
155
425
  return typeof err === "object" && err !== null && "code" in err ? String(err.code) : void 0;
156
426
  }
157
- var wait = (ms) => new Promise((resolve12) => {
158
- setTimeout(resolve12, ms);
427
+ var wait = (ms) => new Promise((resolve14) => {
428
+ setTimeout(resolve14, ms);
159
429
  });
160
430
  async function writeFileAtomic(path, contents) {
161
431
  const tmp = join(
@@ -2611,11 +2881,17 @@ var BUILTINS = /* @__PURE__ */ new Map([
2611
2881
  ["anthropic:claude-subagent:2.1", __default23]
2612
2882
  ]);
2613
2883
  function assertPublishableBuiltinId(id) {
2614
- if (id.split(":")[0] === "check") {
2884
+ const first = id.split(":")[0];
2885
+ if (first === "check") {
2615
2886
  throw new Error(
2616
2887
  `Built-in id "${id}" is not publishable: the "check" first segment is reserved for corpus check identities (check:<name>, proposal 0026).`
2617
2888
  );
2618
2889
  }
2890
+ if (first === "external") {
2891
+ throw new Error(
2892
+ `Built-in id "${id}" is not publishable: the "external" first segment is reserved for external-metadata finding identities (external:owned, external:duplicate, proposal 0037).`
2893
+ );
2894
+ }
2619
2895
  }
2620
2896
  for (const id of BUILTINS.keys()) assertPublishableBuiltinId(id);
2621
2897
  function listBuiltins() {
@@ -2743,8 +3019,8 @@ var SCHEMA_KEYS = [
2743
3019
  "multipleOf"
2744
3020
  ];
2745
3021
  var RETRY_DELAY_MS = 500;
2746
- var wait2 = (ms) => new Promise((resolve12) => {
2747
- setTimeout(resolve12, ms);
3022
+ var wait2 = (ms) => new Promise((resolve14) => {
3023
+ setTimeout(resolve14, ms);
2748
3024
  });
2749
3025
  var RetryableFetchError = class extends DocmetaError {
2750
3026
  };
@@ -2996,114 +3272,361 @@ async function loadSchema(ref, options = {}) {
2996
3272
  }
2997
3273
  }
2998
3274
 
2999
- // src/meta/extractors/frontmatter.ts
3000
- import { parseDocument, LineCounter, isMap, isSeq, isScalar } from "yaml";
3001
- import { parse as parseToml } from "smol-toml";
3002
-
3003
- // src/meta/extractors/date-value.ts
3004
- function isoDateValue(value) {
3005
- if (!(value instanceof Date)) return value;
3006
- return Number.isNaN(value.getTime()) ? String(value) : value.toISOString();
3275
+ // src/meta/core/resolve-schema.ts
3276
+ import { isAbsolute, relative, resolve } from "node:path";
3277
+ var DEFAULT_SCHEMAS = Object.freeze([
3278
+ "google:okf:0.1",
3279
+ "passo-uno:seven-action:1.0"
3280
+ ]);
3281
+ var FILE_SCHEMA_KEY = "$schema";
3282
+ function schemaEntryRef(entry) {
3283
+ return typeof entry === "string" ? entry : entry.ref;
3007
3284
  }
3008
-
3009
- // src/meta/extractors/pointer.ts
3010
- function escapePointerSegment(key) {
3011
- return key.replace(/~/g, "~0").replace(/\//g, "~1");
3285
+ function collectSchemaPins(config) {
3286
+ const pins = /* @__PURE__ */ new Map();
3287
+ for (const entry of config?.schemas ?? []) {
3288
+ if (typeof entry === "string") continue;
3289
+ if (entry.source === void 0 && entry.integrity === void 0) continue;
3290
+ pins.set(entry.ref, {
3291
+ ...entry.source !== void 0 ? { source: entry.source } : {},
3292
+ ...entry.integrity !== void 0 ? { integrity: entry.integrity } : {}
3293
+ });
3294
+ }
3295
+ return pins;
3012
3296
  }
3013
- function positionForFactory(map) {
3014
- return (pointer) => {
3015
- const start = pointer !== "" && !pointer.startsWith("/") ? `/${escapePointerSegment(pointer)}` : pointer;
3016
- if (map.has(start)) return map.get(start);
3017
- let p = start;
3018
- while (p.length > 0) {
3019
- const idx = p.lastIndexOf("/");
3020
- if (idx < 0) break;
3021
- p = p.slice(0, idx);
3022
- if (map.has(p)) return map.get(p);
3023
- }
3024
- return map.get("");
3297
+ function rebaseConfigSchemaRefs(config, configDir, cwd) {
3298
+ if (resolve(configDir) === resolve(cwd)) return config;
3299
+ const rebase = (ref) => classifyRef(ref).kind === "file" && !isAbsolute(ref) ? resolve(configDir, ref) : ref;
3300
+ const rebaseEntry = (entry) => typeof entry === "string" ? rebase(entry) : {
3301
+ ...entry,
3302
+ ref: rebase(entry.ref),
3303
+ ...entry.source !== void 0 ? { source: rebase(entry.source) } : {}
3304
+ };
3305
+ return {
3306
+ ...config,
3307
+ ...config.schemas ? { schemas: config.schemas.map(rebaseEntry) } : {},
3308
+ ...config.overrides ? {
3309
+ overrides: config.overrides.map((o) => ({
3310
+ ...o,
3311
+ schemas: o.schemas.map(rebase)
3312
+ }))
3313
+ } : {}
3025
3314
  };
3026
3315
  }
3027
-
3028
- // src/meta/extractors/frontmatter.ts
3029
- var OPEN_FENCE = /^(?:---|\+\+\+|;;;)\r?\n/;
3030
- var FENCES = [
3031
- { open: /^---\r?\n/, flavor: "yaml", isClose: (l) => l === "---" || l === "..." },
3032
- { open: /^\+\+\+\r?\n/, flavor: "toml", isClose: (l) => l === "+++" },
3033
- { open: /^;;;\r?\n/, flavor: "json", isClose: (l) => l === ";;;" }
3034
- ];
3035
- function stripBom2(content) {
3036
- return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
3037
- }
3038
- function hasFrontmatterFence(content) {
3039
- return OPEN_FENCE.test(stripBom2(content));
3316
+ function overrideGlobs(files) {
3317
+ if (files === void 0) return [];
3318
+ return typeof files === "string" ? [files] : files;
3040
3319
  }
3041
- function locateFrontmatter(content) {
3042
- const bom = content.charCodeAt(0) === 65279 ? 1 : 0;
3043
- const body = bom === 1 ? content.slice(1) : content;
3044
- const fence = FENCES.find((f) => f.open.test(body));
3045
- if (!fence) return null;
3046
- const openNl = body.indexOf("\n");
3047
- const eol = openNl > 0 && body.charCodeAt(openNl - 1) === 13 ? "\r\n" : "\n";
3048
- const innerStart = openNl + 1;
3049
- let cursor = innerStart;
3050
- while (cursor <= body.length) {
3051
- const nl = body.indexOf("\n", cursor);
3052
- const hasNl = nl !== -1;
3053
- const lineEnd = hasNl ? nl : body.length;
3054
- const hadCr = lineEnd > cursor && body.charCodeAt(lineEnd - 1) === 13;
3055
- const line = body.slice(cursor, hadCr ? lineEnd - 1 : lineEnd);
3056
- if (fence.isClose(line)) {
3057
- return {
3058
- flavor: fence.flavor,
3059
- openStart: bom,
3060
- innerStart: innerStart + bom,
3061
- innerEnd: cursor + bom,
3062
- closeEnd: (hasNl ? lineEnd + 1 : lineEnd) + bom,
3063
- eol,
3064
- firstContentLine: 2
3065
- };
3066
- }
3067
- if (!hasNl) break;
3068
- cursor = lineEnd + 1;
3320
+ function overrideMatches(override, filePath, memberOf2) {
3321
+ if (override.collection !== void 0) {
3322
+ return memberOf2.includes(override.collection);
3069
3323
  }
3070
- return null;
3324
+ return override.files !== void 0 && matchesFileGlob(filePath, override.files);
3071
3325
  }
3072
- function frontmatterInnerText(content, loc) {
3073
- return content.slice(loc.innerStart, loc.innerEnd).replace(/\r\n/g, "\n").replace(/\n$/, "");
3326
+ function coerceFileSchema(value) {
3327
+ if (value === void 0 || value === null) return void 0;
3328
+ if (typeof value === "string") return [value];
3329
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
3330
+ return value;
3331
+ }
3332
+ throw new Error(
3333
+ `Invalid "${FILE_SCHEMA_KEY}": must be a string or a list of strings.`
3334
+ );
3074
3335
  }
3075
- function buildLineMap(doc, lc, prefixLines) {
3076
- const map = /* @__PURE__ */ new Map();
3077
- map.set("", 1);
3078
- const lineAt = (offset) => offset == null ? void 0 : lc.linePos(offset).line + prefixLines;
3079
- const walk = (node, pointer) => {
3080
- if (isMap(node)) {
3081
- for (const pair of node.items) {
3082
- const key = isScalar(pair.key) ? String(pair.key.value) : String(pair.key);
3083
- const ptr = `${pointer}/${escapePointerSegment(key)}`;
3084
- const line = lineAt(
3085
- pair.key?.range?.[0]
3086
- );
3087
- if (line != null) map.set(ptr, line);
3088
- if (pair.value) walk(pair.value, ptr);
3089
- }
3090
- } else if (isSeq(node)) {
3091
- node.items.forEach((item, i) => {
3092
- const ptr = `${pointer}/${i}`;
3093
- const line = lineAt(
3094
- item?.range?.[0]
3095
- );
3096
- if (line != null) map.set(ptr, line);
3097
- walk(item, ptr);
3098
- });
3099
- }
3100
- };
3101
- if (doc.contents) walk(doc.contents, "");
3102
- return map;
3336
+ function dedupe(refs) {
3337
+ return [...new Set(refs)];
3103
3338
  }
3104
- var TOML_KEY = /^\s*(?:([A-Za-z0-9_-]+)|"([^"\\]*)"|'([^']*)')\s*=/;
3105
- var TOML_TABLE = /^\s*\[\[?\s*([A-Za-z0-9_-]+)/;
3106
- function buildTomlLineMap(raw, prefixLines) {
3339
+ function hostAllowed(url, hosts) {
3340
+ const hostname = url.hostname.toLowerCase();
3341
+ const host = url.host.toLowerCase();
3342
+ return hosts.some((h) => {
3343
+ const want = h.trim().toLowerCase();
3344
+ return want === hostname || want === host;
3345
+ });
3346
+ }
3347
+ function assertDocumentRefAllowed(ref, mode, params) {
3348
+ const { kind } = classifyRef(ref);
3349
+ if (kind === "builtin" || isPublishedBuiltinUrl(ref)) return;
3350
+ if (kind === "url") {
3351
+ if (mode === "local") {
3352
+ throw new DocmetaError(
3353
+ `Refusing the "${FILE_SCHEMA_KEY}" URL "${ref}": schemaTrust.documentRefs is "local", so a document may name a built-in id or a schema file inside the repository, but not a URL. Vendor it with \`manni meta schemas vendor ${ref}\` and reference the local copy, or put the URL in \`schemas:\` where an operator controls it.`
3354
+ );
3355
+ }
3356
+ const hosts = params.config?.schemaTrust?.hosts;
3357
+ if (!hosts || hosts.length === 0) return;
3358
+ let url;
3359
+ try {
3360
+ url = new URL(ref);
3361
+ } catch {
3362
+ throw new DocmetaError(
3363
+ `Refusing the "${FILE_SCHEMA_KEY}" URL "${ref}": it cannot be parsed as a URL, so it cannot be checked against schemaTrust.hosts.`
3364
+ );
3365
+ }
3366
+ if (!hostAllowed(url, hosts)) {
3367
+ throw new DocmetaError(
3368
+ `Refusing the "${FILE_SCHEMA_KEY}" URL "${ref}": host "${url.host}" is not in schemaTrust.hosts (${hosts.join(", ")}). Add the host there, or reference a schema the config already names.`
3369
+ );
3370
+ }
3371
+ return;
3372
+ }
3373
+ const root = params.trustRoot;
3374
+ if (!root) return;
3375
+ const abs = resolve(params.fileBase ?? process.cwd(), ref);
3376
+ const within = relative(root.dir, abs);
3377
+ if (within !== "" && !within.startsWith("..") && !isAbsolute(within)) return;
3378
+ const boundary = root.source === "git" ? `the repository root (${root.dir})` : root.source === "config" ? `${root.dir} \u2014 no git repository was found, so the config file's own directory is the boundary` : `${root.dir} \u2014 no git repository and no config file were found, so the directory the command was run from is the boundary`;
3379
+ const scope = root.source === "git" ? "the repository" : "the project";
3380
+ throw new DocmetaError(
3381
+ `Refusing the "${FILE_SCHEMA_KEY}" path "${ref}": it resolves outside ${boundary}. A document may only name a schema inside ${scope}; a path in \`schemas:\` or on \`--schema\` is not restricted this way.`
3382
+ );
3383
+ }
3384
+ function resolveSchemaSetWithSource(params) {
3385
+ const { filePath, fileSchema, cliSchemas, config } = params;
3386
+ if (cliSchemas && cliSchemas.length > 0) {
3387
+ return { schemas: dedupe(cliSchemas), source: "cli" };
3388
+ }
3389
+ const fromFile = coerceFileSchema(fileSchema);
3390
+ if (fromFile && fromFile.length > 0) {
3391
+ const mode = config?.schemaTrust?.documentRefs ?? "any";
3392
+ if (mode === "none") {
3393
+ params.onNotice?.(
3394
+ `${filePath}: "${FILE_SCHEMA_KEY}" is ignored (${fromFile.join(", ")}) \u2014 schemaTrust.documentRefs is "none", so the schema set comes from the config instead. Remove the key from the document, or set schemaTrust.documentRefs to "local" or "any" to honor it.`
3395
+ );
3396
+ } else {
3397
+ for (const ref of fromFile) assertDocumentRefAllowed(ref, mode, params);
3398
+ return { schemas: dedupe(fromFile), source: "document" };
3399
+ }
3400
+ }
3401
+ if (config?.overrides) {
3402
+ const memberOf2 = params.memberOf ?? [];
3403
+ for (const [i, ov] of config.overrides.entries()) {
3404
+ if (overrideMatches(ov, filePath, memberOf2) && ov.schemas.length > 0) {
3405
+ return { schemas: dedupe(ov.schemas), source: "override", overrideIndex: i };
3406
+ }
3407
+ }
3408
+ }
3409
+ if (config?.schemas && config.schemas.length > 0) {
3410
+ return {
3411
+ schemas: dedupe(config.schemas.map(schemaEntryRef)),
3412
+ source: "config"
3413
+ };
3414
+ }
3415
+ return { schemas: [...DEFAULT_SCHEMAS], source: "default" };
3416
+ }
3417
+ function resolveSchemaSet(params) {
3418
+ return resolveSchemaSetWithSource(params).schemas;
3419
+ }
3420
+ function resolveElements(filePath, config, memberOf2 = []) {
3421
+ const out = [];
3422
+ if (config?.elements) out.push(...config.elements);
3423
+ for (const ov of config?.overrides ?? []) {
3424
+ if (ov.elements && overrideMatches(ov, filePath, memberOf2)) {
3425
+ out.push(...ov.elements);
3426
+ }
3427
+ }
3428
+ return dedupe(out);
3429
+ }
3430
+
3431
+ // src/meta/core/external-metadata-fetch.ts
3432
+ var EXTERNAL_METADATA_FETCH_TIMEOUT_MS = 1e4;
3433
+ var EXTERNAL_METADATA_FETCH_MAX_BYTES = 5 * 1024 * 1024;
3434
+ var MAX_REDIRECTS = 5;
3435
+ var RETRY_DELAY_MS2 = 500;
3436
+ async function fetchExternalMetadata(url, opts = {}) {
3437
+ if (opts.offline) {
3438
+ throw new DocmetaError(
3439
+ `Manifest ${url} is remote and the run is offline. Vendor it to a path, or drop --offline.`
3440
+ );
3441
+ }
3442
+ const timeoutMs = opts.timeoutMs ?? EXTERNAL_METADATA_FETCH_TIMEOUT_MS;
3443
+ const maxBytes = opts.maxBytes ?? EXTERNAL_METADATA_FETCH_MAX_BYTES;
3444
+ const env = opts.env ?? process.env;
3445
+ let token;
3446
+ if (opts.tokenEnv !== void 0) {
3447
+ token = env[opts.tokenEnv];
3448
+ if (token === void 0 || token === "") {
3449
+ throw new DocmetaError(
3450
+ `Manifest ${url}: the environment variable ${opts.tokenEnv} named by "tokenEnv" is not set.`
3451
+ );
3452
+ }
3453
+ }
3454
+ let res = await requestWithRedirects(url, token, timeoutMs);
3455
+ if (res instanceof Error || res.status >= 500) {
3456
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS2));
3457
+ res = await requestWithRedirects(url, token, timeoutMs);
3458
+ }
3459
+ if (res instanceof Error) {
3460
+ throw new DocmetaError(
3461
+ `Manifest ${url} could not be fetched: ${res.message}`
3462
+ );
3463
+ }
3464
+ if (!res.ok) {
3465
+ const hint = res.status === 404 && token === void 0 ? ' (a private file answers 404 without a token; set "tokenEnv")' : res.status === 404 ? " (a private file answers 404 without a valid token)" : "";
3466
+ throw new DocmetaError(
3467
+ `Manifest ${url} could not be fetched: HTTP ${res.status}${hint}.`
3468
+ );
3469
+ }
3470
+ return readCapped(url, res, maxBytes);
3471
+ }
3472
+ async function requestWithRedirects(url, token, timeoutMs) {
3473
+ const origin = new URL(url).origin;
3474
+ let current = url;
3475
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
3476
+ const sameOrigin = new URL(current).origin === origin;
3477
+ const headers = { accept: "text/plain, application/x-yaml, */*" };
3478
+ if (token !== void 0 && sameOrigin) headers.authorization = `Bearer ${token}`;
3479
+ let res;
3480
+ try {
3481
+ res = await fetch(current, {
3482
+ headers,
3483
+ redirect: "manual",
3484
+ signal: AbortSignal.timeout(timeoutMs)
3485
+ });
3486
+ } catch (err) {
3487
+ const isAbort2 = err instanceof Error && err.name === "TimeoutError";
3488
+ return isAbort2 ? new Error(`timed out after ${timeoutMs}ms`) : new Error(err instanceof Error ? err.message : String(err));
3489
+ }
3490
+ if (res.status >= 300 && res.status < 400) {
3491
+ const location = res.headers.get("location");
3492
+ if (location === null) return res;
3493
+ await res.body?.cancel();
3494
+ current = new URL(location, current).toString();
3495
+ continue;
3496
+ }
3497
+ return res;
3498
+ }
3499
+ return new Error(`more than ${MAX_REDIRECTS} redirects`);
3500
+ }
3501
+ async function readCapped(url, res, maxBytes) {
3502
+ const body = res.body;
3503
+ if (!body) return "";
3504
+ const reader = body.getReader();
3505
+ const chunks = [];
3506
+ let total = 0;
3507
+ for (; ; ) {
3508
+ const { done, value } = await reader.read();
3509
+ if (done || value === void 0) break;
3510
+ total += value.byteLength;
3511
+ if (total > maxBytes) {
3512
+ await reader.cancel();
3513
+ throw new DocmetaError(
3514
+ `Manifest ${url} is too large: the response exceeds the ${maxBytes}-byte limit.`
3515
+ );
3516
+ }
3517
+ chunks.push(value);
3518
+ }
3519
+ return Buffer.concat(chunks).toString("utf8");
3520
+ }
3521
+
3522
+ // src/meta/extractors/frontmatter.ts
3523
+ import { parseDocument, LineCounter, isMap, isSeq, isScalar } from "yaml";
3524
+ import { parse as parseToml } from "smol-toml";
3525
+
3526
+ // src/meta/extractors/date-value.ts
3527
+ function isoDateValue(value) {
3528
+ if (!(value instanceof Date)) return value;
3529
+ return Number.isNaN(value.getTime()) ? String(value) : value.toISOString();
3530
+ }
3531
+
3532
+ // src/meta/extractors/pointer.ts
3533
+ function escapePointerSegment(key) {
3534
+ return key.replace(/~/g, "~0").replace(/\//g, "~1");
3535
+ }
3536
+ function positionForFactory(map) {
3537
+ return (pointer) => {
3538
+ const start = pointer !== "" && !pointer.startsWith("/") ? `/${escapePointerSegment(pointer)}` : pointer;
3539
+ if (map.has(start)) return map.get(start);
3540
+ let p = start;
3541
+ while (p.length > 0) {
3542
+ const idx = p.lastIndexOf("/");
3543
+ if (idx < 0) break;
3544
+ p = p.slice(0, idx);
3545
+ if (map.has(p)) return map.get(p);
3546
+ }
3547
+ return map.get("");
3548
+ };
3549
+ }
3550
+
3551
+ // src/meta/extractors/frontmatter.ts
3552
+ var OPEN_FENCE = /^(?:---|\+\+\+|;;;)\r?\n/;
3553
+ var FENCES = [
3554
+ { open: /^---\r?\n/, flavor: "yaml", isClose: (l) => l === "---" || l === "..." },
3555
+ { open: /^\+\+\+\r?\n/, flavor: "toml", isClose: (l) => l === "+++" },
3556
+ { open: /^;;;\r?\n/, flavor: "json", isClose: (l) => l === ";;;" }
3557
+ ];
3558
+ function stripBom2(content) {
3559
+ return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
3560
+ }
3561
+ function hasFrontmatterFence(content) {
3562
+ return OPEN_FENCE.test(stripBom2(content));
3563
+ }
3564
+ function locateFrontmatter(content) {
3565
+ const bom = content.charCodeAt(0) === 65279 ? 1 : 0;
3566
+ const body = bom === 1 ? content.slice(1) : content;
3567
+ const fence = FENCES.find((f) => f.open.test(body));
3568
+ if (!fence) return null;
3569
+ const openNl = body.indexOf("\n");
3570
+ const eol = openNl > 0 && body.charCodeAt(openNl - 1) === 13 ? "\r\n" : "\n";
3571
+ const innerStart = openNl + 1;
3572
+ let cursor = innerStart;
3573
+ while (cursor <= body.length) {
3574
+ const nl = body.indexOf("\n", cursor);
3575
+ const hasNl = nl !== -1;
3576
+ const lineEnd = hasNl ? nl : body.length;
3577
+ const hadCr = lineEnd > cursor && body.charCodeAt(lineEnd - 1) === 13;
3578
+ const line = body.slice(cursor, hadCr ? lineEnd - 1 : lineEnd);
3579
+ if (fence.isClose(line)) {
3580
+ return {
3581
+ flavor: fence.flavor,
3582
+ openStart: bom,
3583
+ innerStart: innerStart + bom,
3584
+ innerEnd: cursor + bom,
3585
+ closeEnd: (hasNl ? lineEnd + 1 : lineEnd) + bom,
3586
+ eol,
3587
+ firstContentLine: 2
3588
+ };
3589
+ }
3590
+ if (!hasNl) break;
3591
+ cursor = lineEnd + 1;
3592
+ }
3593
+ return null;
3594
+ }
3595
+ function frontmatterInnerText(content, loc) {
3596
+ return content.slice(loc.innerStart, loc.innerEnd).replace(/\r\n/g, "\n").replace(/\n$/, "");
3597
+ }
3598
+ function buildLineMap(doc, lc, prefixLines) {
3599
+ const map = /* @__PURE__ */ new Map();
3600
+ map.set("", 1);
3601
+ const lineAt = (offset) => offset == null ? void 0 : lc.linePos(offset).line + prefixLines;
3602
+ const walk = (node, pointer) => {
3603
+ if (isMap(node)) {
3604
+ for (const pair of node.items) {
3605
+ const key = isScalar(pair.key) ? String(pair.key.value) : String(pair.key);
3606
+ const ptr = `${pointer}/${escapePointerSegment(key)}`;
3607
+ const line = lineAt(
3608
+ pair.key?.range?.[0]
3609
+ );
3610
+ if (line != null) map.set(ptr, line);
3611
+ if (pair.value) walk(pair.value, ptr);
3612
+ }
3613
+ } else if (isSeq(node)) {
3614
+ node.items.forEach((item, i) => {
3615
+ const ptr = `${pointer}/${i}`;
3616
+ const line = lineAt(
3617
+ item?.range?.[0]
3618
+ );
3619
+ if (line != null) map.set(ptr, line);
3620
+ walk(item, ptr);
3621
+ });
3622
+ }
3623
+ };
3624
+ if (doc.contents) walk(doc.contents, "");
3625
+ return map;
3626
+ }
3627
+ var TOML_KEY = /^\s*(?:([A-Za-z0-9_-]+)|"([^"\\]*)"|'([^']*)')\s*=/;
3628
+ var TOML_TABLE = /^\s*\[\[?\s*([A-Za-z0-9_-]+)/;
3629
+ function buildTomlLineMap(raw, prefixLines) {
3107
3630
  const map = /* @__PURE__ */ new Map();
3108
3631
  map.set("", 1);
3109
3632
  const record = (key, i) => {
@@ -3755,8 +4278,8 @@ function ditaShape(content, root, filePath) {
3755
4278
  const lower = filePath?.toLowerCase() ?? "";
3756
4279
  const byExtension2 = (lower.endsWith(".dita") || lower.endsWith(".ditamap")) && DITA_ROOTS.has(name2);
3757
4280
  if (!byDoctype && !byClass && !byArch && !byExtension2) return void 0;
3758
- const isMap4 = /^\s*[-+]\s+map\//.test(cls) || MAP_ROOTS.has(name2) || lower.endsWith(".ditamap");
3759
- return isMap4 ? { kind: "map", container: "topicmeta", preamble: /* @__PURE__ */ new Set(["title"]) } : {
4281
+ const isMap5 = /^\s*[-+]\s+map\//.test(cls) || MAP_ROOTS.has(name2) || lower.endsWith(".ditamap");
4282
+ return isMap5 ? { kind: "map", container: "topicmeta", preamble: /* @__PURE__ */ new Set(["title"]) } : {
3760
4283
  kind: "topic",
3761
4284
  container: "metadata",
3762
4285
  preamble: /* @__PURE__ */ new Set([
@@ -5181,174 +5704,26 @@ function listFormats() {
5181
5704
  }));
5182
5705
  }
5183
5706
 
5184
- // src/meta/core/resolve-schema.ts
5185
- import { isAbsolute, relative, resolve } from "node:path";
5186
- import picomatch from "picomatch";
5187
- var DEFAULT_SCHEMAS = Object.freeze([
5188
- "google:okf:0.1",
5189
- "passo-uno:seven-action:1.0"
5190
- ]);
5191
- var FILE_SCHEMA_KEY = "$schema";
5192
- function schemaEntryRef(entry) {
5193
- return typeof entry === "string" ? entry : entry.ref;
5194
- }
5195
- function collectSchemaPins(config) {
5196
- const pins = /* @__PURE__ */ new Map();
5197
- for (const entry of config?.schemas ?? []) {
5198
- if (typeof entry === "string") continue;
5199
- if (entry.source === void 0 && entry.integrity === void 0) continue;
5200
- pins.set(entry.ref, {
5201
- ...entry.source !== void 0 ? { source: entry.source } : {},
5202
- ...entry.integrity !== void 0 ? { integrity: entry.integrity } : {}
5203
- });
5204
- }
5205
- return pins;
5206
- }
5207
- function rebaseConfigSchemaRefs(config, configDir, cwd) {
5208
- if (resolve(configDir) === resolve(cwd)) return config;
5209
- const rebase = (ref) => classifyRef(ref).kind === "file" && !isAbsolute(ref) ? resolve(configDir, ref) : ref;
5210
- const rebaseEntry = (entry) => typeof entry === "string" ? rebase(entry) : {
5211
- ...entry,
5212
- ref: rebase(entry.ref),
5213
- ...entry.source !== void 0 ? { source: rebase(entry.source) } : {}
5214
- };
5215
- return {
5216
- ...config,
5217
- ...config.schemas ? { schemas: config.schemas.map(rebaseEntry) } : {},
5218
- ...config.overrides ? {
5219
- overrides: config.overrides.map((o) => ({
5220
- ...o,
5221
- schemas: o.schemas.map(rebase)
5222
- }))
5223
- } : {}
5224
- };
5225
- }
5226
- var matcherCache = /* @__PURE__ */ new Map();
5227
- function overrideGlobs(files) {
5228
- return typeof files === "string" ? [files] : files;
5229
- }
5230
- function matchesFileGlob(glob, filePath) {
5231
- return matches(glob, filePath);
5232
- }
5233
- function matches(glob, filePath) {
5234
- const key = JSON.stringify(glob);
5235
- let m = matcherCache.get(key);
5236
- if (!m) {
5237
- m = picomatch(glob, { dot: true });
5238
- matcherCache.set(key, m);
5239
- }
5240
- return m(filePath.replace(/\\/g, "/"));
5241
- }
5242
- function coerceFileSchema(value) {
5243
- if (value === void 0 || value === null) return void 0;
5244
- if (typeof value === "string") return [value];
5245
- if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
5246
- return value;
5247
- }
5248
- throw new Error(
5249
- `Invalid "${FILE_SCHEMA_KEY}": must be a string or a list of strings.`
5250
- );
5251
- }
5252
- function dedupe(refs) {
5253
- return [...new Set(refs)];
5254
- }
5255
- function hostAllowed(url, hosts) {
5256
- const hostname = url.hostname.toLowerCase();
5257
- const host = url.host.toLowerCase();
5258
- return hosts.some((h) => {
5259
- const want = h.trim().toLowerCase();
5260
- return want === hostname || want === host;
5261
- });
5262
- }
5263
- function assertDocumentRefAllowed(ref, mode, params) {
5264
- const { kind } = classifyRef(ref);
5265
- if (kind === "builtin" || isPublishedBuiltinUrl(ref)) return;
5266
- if (kind === "url") {
5267
- if (mode === "local") {
5268
- throw new DocmetaError(
5269
- `Refusing the "${FILE_SCHEMA_KEY}" URL "${ref}": schemaTrust.documentRefs is "local", so a document may name a built-in id or a schema file inside the repository, but not a URL. Vendor it with \`manni meta schemas vendor ${ref}\` and reference the local copy, or put the URL in \`schemas:\` where an operator controls it.`
5270
- );
5271
- }
5272
- const hosts = params.config?.schemaTrust?.hosts;
5273
- if (!hosts || hosts.length === 0) return;
5274
- let url;
5275
- try {
5276
- url = new URL(ref);
5277
- } catch {
5278
- throw new DocmetaError(
5279
- `Refusing the "${FILE_SCHEMA_KEY}" URL "${ref}": it cannot be parsed as a URL, so it cannot be checked against schemaTrust.hosts.`
5280
- );
5281
- }
5282
- if (!hostAllowed(url, hosts)) {
5283
- throw new DocmetaError(
5284
- `Refusing the "${FILE_SCHEMA_KEY}" URL "${ref}": host "${url.host}" is not in schemaTrust.hosts (${hosts.join(", ")}). Add the host there, or reference a schema the config already names.`
5285
- );
5286
- }
5287
- return;
5288
- }
5289
- const root = params.trustRoot;
5290
- if (!root) return;
5291
- const abs = resolve(params.fileBase ?? process.cwd(), ref);
5292
- const within = relative(root.dir, abs);
5293
- if (within !== "" && !within.startsWith("..") && !isAbsolute(within)) return;
5294
- const boundary = root.source === "git" ? `the repository root (${root.dir})` : root.source === "config" ? `${root.dir} \u2014 no git repository was found, so the config file's own directory is the boundary` : `${root.dir} \u2014 no git repository and no config file were found, so the directory the command was run from is the boundary`;
5295
- const scope = root.source === "git" ? "the repository" : "the project";
5296
- throw new DocmetaError(
5297
- `Refusing the "${FILE_SCHEMA_KEY}" path "${ref}": it resolves outside ${boundary}. A document may only name a schema inside ${scope}; a path in \`schemas:\` or on \`--schema\` is not restricted this way.`
5298
- );
5299
- }
5300
- function resolveSchemaSetWithSource(params) {
5301
- const { filePath, fileSchema, cliSchemas, config } = params;
5302
- if (cliSchemas && cliSchemas.length > 0) {
5303
- return { schemas: dedupe(cliSchemas), source: "cli" };
5304
- }
5305
- const fromFile = coerceFileSchema(fileSchema);
5306
- if (fromFile && fromFile.length > 0) {
5307
- const mode = config?.schemaTrust?.documentRefs ?? "any";
5308
- if (mode === "none") {
5309
- params.onNotice?.(
5310
- `${filePath}: "${FILE_SCHEMA_KEY}" is ignored (${fromFile.join(", ")}) \u2014 schemaTrust.documentRefs is "none", so the schema set comes from the config instead. Remove the key from the document, or set schemaTrust.documentRefs to "local" or "any" to honor it.`
5311
- );
5312
- } else {
5313
- for (const ref of fromFile) assertDocumentRefAllowed(ref, mode, params);
5314
- return { schemas: dedupe(fromFile), source: "document" };
5315
- }
5316
- }
5317
- if (config?.overrides) {
5318
- for (const [i, ov] of config.overrides.entries()) {
5319
- if (matches(ov.files, filePath) && ov.schemas.length > 0) {
5320
- return { schemas: dedupe(ov.schemas), source: "override", overrideIndex: i };
5321
- }
5322
- }
5323
- }
5324
- if (config?.schemas && config.schemas.length > 0) {
5325
- return {
5326
- schemas: dedupe(config.schemas.map(schemaEntryRef)),
5327
- source: "config"
5328
- };
5329
- }
5330
- return { schemas: [...DEFAULT_SCHEMAS], source: "default" };
5331
- }
5332
- function resolveSchemaSet(params) {
5333
- return resolveSchemaSetWithSource(params).schemas;
5334
- }
5335
- function resolveElements(filePath, config) {
5336
- const out = [];
5337
- if (config?.elements) out.push(...config.elements);
5338
- for (const ov of config?.overrides ?? []) {
5339
- if (ov.elements && matches(ov.files, filePath)) out.push(...ov.elements);
5340
- }
5341
- return dedupe(out);
5342
- }
5707
+ // src/meta/core/external-metadata.ts
5708
+ import { readFile as readFile3 } from "node:fs/promises";
5709
+ import { isAbsolute as isAbsolute2, relative as relative4, resolve as resolve5, sep as sep2 } from "node:path";
5710
+ import {
5711
+ LineCounter as LineCounter2,
5712
+ isMap as isMap3,
5713
+ isNode,
5714
+ isScalar as isScalar2,
5715
+ parseDocument as parseDocument3
5716
+ } from "yaml";
5343
5717
 
5344
- // src/meta/core/config.ts
5345
- import { resolve as resolve4 } from "node:path";
5346
- import { parse as parseYaml2 } from "yaml";
5718
+ // src/meta/core/load-files.ts
5719
+ import { stat as stat3 } from "node:fs/promises";
5720
+ import { extname, relative as relative3, resolve as resolve4 } from "node:path";
5721
+ import fg from "fast-glob";
5722
+ import picomatch3 from "picomatch";
5347
5723
 
5348
- // src/shared/config-file.ts
5349
- import { readFile as readFile3 } from "node:fs/promises";
5350
- import { dirname as dirname3, join as join4, relative as relative2, resolve as resolve3 } from "node:path";
5351
- import { parse as parseYaml } from "yaml";
5724
+ // src/meta/core/gitignore.ts
5725
+ import { spawn } from "node:child_process";
5726
+ import { dirname as dirname3, relative as relative2, resolve as resolve3, sep } from "node:path";
5352
5727
 
5353
5728
  // src/shared/git-root.ts
5354
5729
  import { existsSync } from "node:fs";
@@ -5377,64 +5752,542 @@ function searchPath(cwd) {
5377
5752
  }
5378
5753
  }
5379
5754
 
5380
- // src/shared/warn.ts
5381
- var said = /* @__PURE__ */ new Set();
5382
- function warn(message) {
5383
- if (said.has(message)) return;
5384
- said.add(message);
5385
- process.stderr.write(`${programName()}: ${message}
5386
- `);
5755
+ // src/meta/core/gitignore.ts
5756
+ var GITIGNORE_UNAVAILABLE = ".gitignore filtering was requested but git could not answer here (no repository, or no git on PATH); no files were skipped.";
5757
+ var unavailable = () => ({
5758
+ ignored: /* @__PURE__ */ new Set(),
5759
+ available: false
5760
+ });
5761
+ async function gitIgnored(paths, cwd) {
5762
+ if (paths.length === 0) {
5763
+ return { ignored: /* @__PURE__ */ new Set(), available: true };
5764
+ }
5765
+ const rootOf = /* @__PURE__ */ new Map();
5766
+ const byRoot = /* @__PURE__ */ new Map();
5767
+ let inSomeRepo = false;
5768
+ for (const spelled of paths) {
5769
+ const abs = resolve3(cwd, spelled);
5770
+ const dir = dirname3(abs);
5771
+ let root = rootOf.get(dir);
5772
+ if (root === void 0) {
5773
+ root = findGitRoot(dir);
5774
+ rootOf.set(dir, root);
5775
+ }
5776
+ if (root === null) continue;
5777
+ inSomeRepo = true;
5778
+ const rel = relative2(root, abs).split(sep).join("/");
5779
+ const bucket = byRoot.get(root);
5780
+ if (bucket) bucket.push({ rel, spelled });
5781
+ else byRoot.set(root, [{ rel, spelled }]);
5782
+ }
5783
+ if (!inSomeRepo) return unavailable();
5784
+ const ignored = /* @__PURE__ */ new Set();
5785
+ let answered = 0;
5786
+ for (const [root, bucket] of byRoot) {
5787
+ const answer = await checkIgnore(
5788
+ bucket.map((b) => b.rel),
5789
+ root
5790
+ );
5791
+ if (!answer.available) continue;
5792
+ answered += 1;
5793
+ for (const b of bucket) {
5794
+ if (answer.ignored.has(b.rel)) ignored.add(b.spelled);
5795
+ }
5796
+ }
5797
+ return answered > 0 ? { ignored, available: true } : unavailable();
5798
+ }
5799
+ function checkIgnore(paths, cwd) {
5800
+ return new Promise((settle) => {
5801
+ let done = false;
5802
+ const finish = (answer) => {
5803
+ if (done) return;
5804
+ done = true;
5805
+ settle(answer);
5806
+ };
5807
+ let child;
5808
+ try {
5809
+ child = spawn("git", ["check-ignore", "--stdin", "-z"], {
5810
+ cwd,
5811
+ windowsHide: true
5812
+ });
5813
+ } catch {
5814
+ finish(unavailable());
5815
+ return;
5816
+ }
5817
+ let out = "";
5818
+ child.stdout.setEncoding("utf8");
5819
+ child.stdout.on("data", (chunk) => {
5820
+ out += chunk;
5821
+ });
5822
+ child.stderr.resume();
5823
+ child.on("error", () => {
5824
+ finish(unavailable());
5825
+ });
5826
+ child.stdin.on("error", () => {
5827
+ });
5828
+ child.on("close", (code) => {
5829
+ if (code !== 0 && code !== 1) {
5830
+ finish(unavailable());
5831
+ return;
5832
+ }
5833
+ finish({
5834
+ ignored: new Set(out.split("\0").filter((p) => p !== "")),
5835
+ available: true
5836
+ });
5837
+ });
5838
+ child.stdin.end(paths.join("\0"));
5839
+ });
5387
5840
  }
5388
5841
 
5389
- // src/shared/config-file.ts
5390
- var FAMILY_CONFIG_NAMES = [
5391
- "manni.config.yaml",
5392
- "manni.config.yml"
5393
- ];
5394
- var MOOSE_CONFIG_NAMES = [
5395
- "moose.config.yaml",
5396
- "moose.config.yml"
5397
- ];
5398
- async function readDocument(path, source, opts) {
5399
- let text;
5842
+ // src/meta/core/load-files.ts
5843
+ var DEFAULT_IGNORE = ["**/node_modules/**", "**/.git/**"];
5844
+ var STDIN_TOKEN = "-";
5845
+ var STDIN_LABEL = "<stdin>";
5846
+ function toPosix(p) {
5847
+ return p.replace(/\\/g, "/");
5848
+ }
5849
+ async function statOrNull(p) {
5400
5850
  try {
5401
- text = await readFile3(path, "utf8");
5851
+ return await stat3(p);
5402
5852
  } catch {
5403
5853
  return null;
5404
5854
  }
5405
- return { text, doc: parseMapping(text, source, opts) };
5406
- }
5407
- function parseMapping(text, source, opts) {
5408
- let raw;
5409
- try {
5410
- raw = parseYaml(text);
5411
- } catch (err) {
5412
- throw opts.toError(`${source}: invalid YAML: ${err.message}`);
5413
- }
5414
- if (raw == null) return null;
5415
- if (typeof raw !== "object" || Array.isArray(raw)) {
5416
- throw opts.toError(`${source}: top level must be a mapping.`);
5417
- }
5418
- return raw;
5419
- }
5420
- function slice(document, opts) {
5421
- const { doc } = document;
5422
- if (doc !== null && Object.hasOwn(doc, opts.section)) {
5423
- return { value: doc[opts.section] ?? null, wrapped: true };
5424
- }
5425
- return null;
5426
5855
  }
5427
- function relativeSource(cwd, path) {
5428
- return relative2(cwd, path).replace(/\\/g, "/");
5856
+ function gitignoreOptions(opts) {
5857
+ const asked = opts.flag === true || opts.flag === void 0 && opts.configured === true;
5858
+ const notice = opts.onNotice;
5859
+ return {
5860
+ respectGitignore: opts.flag ?? opts.configured ?? true,
5861
+ ...asked && notice ? {
5862
+ onGitignoreUnavailable: () => {
5863
+ notice(GITIGNORE_UNAVAILABLE);
5864
+ }
5865
+ } : {}
5866
+ };
5429
5867
  }
5430
- async function findConfigFile(cwd, opts) {
5431
- const start = resolve3(cwd);
5432
- for (const dir of searchPath(start)) {
5433
- const family = [
5434
- { names: FAMILY_CONFIG_NAMES, kind: "manni" },
5435
- { names: MOOSE_CONFIG_NAMES, kind: "moose" }
5436
- ];
5437
- for (const { names, kind } of family) {
5868
+ async function resolveTargetSet(opts) {
5869
+ const cwd = opts.cwd ?? process.cwd();
5870
+ const exts = (opts.exts ?? supportedExtensions()).map(
5871
+ (e) => e.toLowerCase().startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`
5872
+ );
5873
+ const ignore = [...DEFAULT_IGNORE, ...opts.exclude ?? []];
5874
+ const named = /* @__PURE__ */ new Set();
5875
+ const walked = /* @__PURE__ */ new Set();
5876
+ const keepByExt = (file) => exts.includes(extname(file).toLowerCase());
5877
+ const missing = [];
5878
+ for (const input of opts.inputs) {
5879
+ if (input === STDIN_TOKEN) continue;
5880
+ const posixInput = toPosix(input);
5881
+ const abs = resolve4(cwd, posixInput);
5882
+ const st = await statOrNull(abs);
5883
+ if (st?.isFile()) {
5884
+ named.add(toPosix(relative3(cwd, abs)));
5885
+ continue;
5886
+ }
5887
+ if (st?.isDirectory()) {
5888
+ const found2 = await fg(`${posixInput}/**/*`, {
5889
+ cwd,
5890
+ ignore,
5891
+ onlyFiles: true,
5892
+ dot: false
5893
+ });
5894
+ for (const f of found2) if (keepByExt(f)) walked.add(f);
5895
+ continue;
5896
+ }
5897
+ if (!picomatch3.scan(posixInput).isGlob) {
5898
+ missing.push(posixInput);
5899
+ continue;
5900
+ }
5901
+ const found = await fg(posixInput, {
5902
+ cwd,
5903
+ ignore,
5904
+ onlyFiles: true,
5905
+ dot: false
5906
+ });
5907
+ for (const f of found) if (keepByExt(f)) walked.add(f);
5908
+ }
5909
+ if (missing.length > 0 && !opts.allowEmpty) {
5910
+ const names = missing.map((m) => `"${m}"`).join(", ");
5911
+ throw new DocmetaError(
5912
+ missing.length === 1 ? `File not found: ${names}.` : `Files not found: ${names}.`
5913
+ );
5914
+ }
5915
+ let gitignoreSkipped = 0;
5916
+ if (opts.respectGitignore !== false && walked.size > 0) {
5917
+ const candidates = [...walked].filter((f) => !named.has(f));
5918
+ const { ignored, available } = await gitIgnored(candidates, cwd);
5919
+ if (available) {
5920
+ for (const f of ignored) {
5921
+ if (walked.delete(f)) gitignoreSkipped += 1;
5922
+ }
5923
+ } else {
5924
+ opts.onGitignoreUnavailable?.();
5925
+ }
5926
+ }
5927
+ return {
5928
+ files: [.../* @__PURE__ */ new Set([...named, ...walked])].sort(),
5929
+ gitignoreSkipped
5930
+ };
5931
+ }
5932
+ function assertNonEmpty(p) {
5933
+ if (p.allowEmpty || p.usingStdin || p.files.length > 0) return;
5934
+ const tried = p.inputs.map((i) => `"${i}"`).join(", ");
5935
+ const notes = [];
5936
+ if (p.exts && p.exts.length > 0) {
5937
+ notes.push(`extensions: ${p.exts.join(", ")}`);
5938
+ }
5939
+ if (p.exclude && p.exclude.length > 0) {
5940
+ notes.push(`excludes: ${p.exclude.map((e) => `"${e}"`).join(", ")}`);
5941
+ }
5942
+ if (p.gitignoreSkipped && p.gitignoreSkipped > 0) {
5943
+ notes.push(
5944
+ `.gitignore skipped ${p.gitignoreSkipped} (pass --no-gitignore to check them)`
5945
+ );
5946
+ }
5947
+ throw new DocmetaError(
5948
+ [
5949
+ `No files matched. Patterns tried: ${tried}.`,
5950
+ ...notes.length > 0 ? [`Filters applied \u2014 ${notes.join("; ")}.`] : [],
5951
+ `Nothing was ${p.action}, so this is an error rather than a pass.`,
5952
+ `Pass --allow-empty (or set allowEmpty: true) if matching nothing is expected.`
5953
+ ].join("\n")
5954
+ );
5955
+ }
5956
+
5957
+ // src/meta/core/external-metadata.ts
5958
+ var EXTERNAL_OWNED_SCHEMA = "external:owned";
5959
+ var EXTERNAL_DUPLICATE_SCHEMA = "external:duplicate";
5960
+ var EXTERNAL_KEYWORD = "external";
5961
+ var PATH_JOIN = "path";
5962
+ var posix = (p) => p.split(sep2).join("/");
5963
+ function reportedPath(abs, base) {
5964
+ const rel = relative4(base, abs);
5965
+ return rel === "" ? "." : posix(rel);
5966
+ }
5967
+ function externalMetadataJoin(manifest) {
5968
+ return manifest.join ?? PATH_JOIN;
5969
+ }
5970
+ async function loadExternalMetadata(collections, opts) {
5971
+ const configured = collections.flatMap(
5972
+ (c) => c.externalMetadata.map((m) => ({ collection: c.name, manifest: m }))
5973
+ );
5974
+ if (configured.length === 0) return null;
5975
+ const owners = /* @__PURE__ */ new Map();
5976
+ const byPath = /* @__PURE__ */ new Map();
5977
+ const byField = /* @__PURE__ */ new Map();
5978
+ const entries = [];
5979
+ for (const { collection, manifest } of configured) {
5980
+ let text;
5981
+ let file;
5982
+ if (classifyRef(manifest.file).kind === "url") {
5983
+ file = manifest.file;
5984
+ text = await fetchExternalMetadata(manifest.file, {
5985
+ ...manifest.tokenEnv !== void 0 ? { tokenEnv: manifest.tokenEnv } : {},
5986
+ ...opts.offline !== void 0 ? { offline: opts.offline } : {},
5987
+ ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
5988
+ ...opts.env !== void 0 ? { env: opts.env } : {}
5989
+ });
5990
+ } else {
5991
+ const abs = isAbsolute2(manifest.file) ? manifest.file : resolve5(opts.configDir, manifest.file);
5992
+ file = reportedPath(abs, opts.base);
5993
+ text = await readManifest(abs, file);
5994
+ }
5995
+ for (const key of manifest.keys) {
5996
+ const list = owners.get(key);
5997
+ if (list) list.push({ collection, file });
5998
+ else owners.set(key, [{ collection, file }]);
5999
+ }
6000
+ const join7 = externalMetadataJoin(manifest);
6001
+ let target;
6002
+ if (join7 === PATH_JOIN) {
6003
+ target = byPath;
6004
+ } else {
6005
+ const existing = byField.get(join7);
6006
+ if (existing) target = existing;
6007
+ else {
6008
+ target = /* @__PURE__ */ new Map();
6009
+ byField.set(join7, target);
6010
+ }
6011
+ }
6012
+ parseManifest(
6013
+ manifest,
6014
+ collection,
6015
+ text,
6016
+ file,
6017
+ opts.configDir,
6018
+ join7,
6019
+ target,
6020
+ entries
6021
+ );
6022
+ }
6023
+ return { owners, byPath, byField, entries };
6024
+ }
6025
+ async function readManifest(abs, file) {
6026
+ try {
6027
+ return await readFile3(abs, "utf8");
6028
+ } catch (err) {
6029
+ const reason = err instanceof Error ? err.message : String(err);
6030
+ throw new DocmetaError(
6031
+ `Manifest ${file} could not be read: ${reason}`
6032
+ );
6033
+ }
6034
+ }
6035
+ function parseManifest(manifest, collection, text, file, configDir, join7, target, entries) {
6036
+ const lc = new LineCounter2();
6037
+ const doc = parseDocument3(text, { lineCounter: lc, uniqueKeys: false });
6038
+ const problem = doc.errors[0];
6039
+ if (problem) {
6040
+ throw new DocmetaError(
6041
+ `Manifest ${file} is not valid YAML: ${problem.message}`
6042
+ );
6043
+ }
6044
+ const root = doc.contents;
6045
+ if (root === null || isScalar2(root) && root.value === null) return;
6046
+ if (!isMap3(root)) {
6047
+ throw new DocmetaError(
6048
+ `Manifest ${file}: the manifest must be a mapping from document ${join7 === PATH_JOIN ? "path" : `"${join7}"`} to owned keys.`
6049
+ );
6050
+ }
6051
+ const owned = new Set(manifest.keys);
6052
+ const lineAt = (node) => {
6053
+ const range = isNode(node) ? node.range : void 0;
6054
+ return range ? lc.linePos(range[0]).line : void 0;
6055
+ };
6056
+ const seen = /* @__PURE__ */ new Map();
6057
+ for (const pair of root.items) {
6058
+ const spelled = isScalar2(pair.key) ? String(pair.key.value) : String(pair.key);
6059
+ const entryLine = lineAt(pair.key);
6060
+ const where = entryLine === void 0 ? file : `${file}:${entryLine}`;
6061
+ if (seen.has(spelled)) {
6062
+ const first = seen.get(spelled);
6063
+ throw new DocmetaError(
6064
+ `Manifest ${where}: "${spelled}" is named twice (first at line ${first ?? "?"}). Merge the two entries into one.`
6065
+ );
6066
+ }
6067
+ seen.set(spelled, entryLine);
6068
+ if (!isMap3(pair.value)) {
6069
+ throw new DocmetaError(
6070
+ `Manifest ${where}: "${spelled}" must be a mapping of owned keys to values.`
6071
+ );
6072
+ }
6073
+ const indexKey = join7 === PATH_JOIN ? resolve5(configDir, spelled) : spelled;
6074
+ const values = target.get(indexKey) ?? /* @__PURE__ */ new Map();
6075
+ for (const kv of pair.value.items) {
6076
+ const key = isScalar2(kv.key) ? String(kv.key.value) : String(kv.key);
6077
+ if (key === FILE_SCHEMA_KEY) {
6078
+ throw new DocmetaError(
6079
+ `Manifest ${where}: "${spelled}" sets "${FILE_SCHEMA_KEY}" \u2014 a manifest never chooses the schema a document is judged by; use "overrides" in the config.`
6080
+ );
6081
+ }
6082
+ if (!owned.has(key)) {
6083
+ throw new DocmetaError(
6084
+ `Manifest ${where}: "${spelled}" sets "${key}", which this manifest does not own. Add it to the manifest's "keys", or remove it from the entry.`
6085
+ );
6086
+ }
6087
+ const value = kv.value === null ? null : kv.value.toJS(doc, { maxAliasCount: 100 });
6088
+ const line = lineAt(kv.key);
6089
+ values.set(key, {
6090
+ value,
6091
+ collection,
6092
+ file,
6093
+ ...line === void 0 ? {} : { line }
6094
+ });
6095
+ }
6096
+ target.set(indexKey, values);
6097
+ entries.push({
6098
+ collection,
6099
+ join: join7,
6100
+ ...join7 === PATH_JOIN ? { abs: indexKey } : {},
6101
+ spelled,
6102
+ file,
6103
+ ...entryLine === void 0 ? {} : { line: entryLine }
6104
+ });
6105
+ }
6106
+ }
6107
+ function joinValue(raw) {
6108
+ if (typeof raw === "string") return raw;
6109
+ if (typeof raw === "number" || typeof raw === "bigint") return String(raw);
6110
+ if (typeof raw === "boolean") return String(raw);
6111
+ return void 0;
6112
+ }
6113
+ function mergeExternalMetadata(label, extracted, index, memberOf2, base) {
6114
+ const none = () => void 0;
6115
+ if (!index || memberOf2.length === 0) {
6116
+ return { extracted, collisions: [], joins: [], locate: none };
6117
+ }
6118
+ const mine = (collection) => memberOf2.includes(collection);
6119
+ const collisions = [];
6120
+ for (const key of Object.keys(extracted.data)) {
6121
+ for (const owner of index.owners.get(key) ?? []) {
6122
+ if (mine(owner.collection)) {
6123
+ collisions.push({ key, file: owner.file, collection: owner.collection });
6124
+ }
6125
+ }
6126
+ }
6127
+ const sources = [];
6128
+ const joins = [];
6129
+ if (label !== STDIN_LABEL) {
6130
+ const byPath = index.byPath.get(resolve5(base, label));
6131
+ if (byPath) sources.push(byPath);
6132
+ }
6133
+ for (const [field, byValue] of index.byField) {
6134
+ const value = joinValue(extracted.data[field]);
6135
+ if (value === void 0) continue;
6136
+ const hit = byValue.get(value);
6137
+ if (!hit) continue;
6138
+ sources.push(hit);
6139
+ const first = [...hit.values()].find((sv) => mine(sv.collection));
6140
+ if (first) {
6141
+ joins.push({ field, value, file: first.file, collection: first.collection });
6142
+ }
6143
+ }
6144
+ for (const supplied of sources) {
6145
+ for (const key of supplied.keys()) {
6146
+ const owners = (index.owners.get(key) ?? []).filter(
6147
+ (o) => mine(o.collection)
6148
+ );
6149
+ const [a, b] = owners;
6150
+ if (a && b) {
6151
+ throw new DocmetaError(
6152
+ `${label}: "${key}" is owned by manifests in two of its collections, ${a.collection} (${a.file}) and ${b.collection} (${b.file}); a key has one manifest per file. Narrow one collection's paths or exclude.`
6153
+ );
6154
+ }
6155
+ }
6156
+ }
6157
+ if (sources.length === 0) {
6158
+ return { extracted, collisions, joins, locate: none };
6159
+ }
6160
+ const data = { ...extracted.data };
6161
+ const merged = /* @__PURE__ */ new Map();
6162
+ for (const supplied of sources) {
6163
+ for (const [key, sv] of supplied) {
6164
+ if (!mine(sv.collection)) continue;
6165
+ if (key in extracted.data) continue;
6166
+ data[key] = sv.value;
6167
+ merged.set(key, sv);
6168
+ }
6169
+ }
6170
+ const locate = (pointer) => {
6171
+ const top = topLevelKey(pointer);
6172
+ if (top === void 0) return void 0;
6173
+ const sv = merged.get(top);
6174
+ if (!sv) return void 0;
6175
+ return { file: sv.file, ...sv.line === void 0 ? {} : { line: sv.line } };
6176
+ };
6177
+ return { extracted: { ...extracted, data }, collisions, joins, locate };
6178
+ }
6179
+ function topLevelKey(pointer) {
6180
+ if (pointer === "") return void 0;
6181
+ if (!pointer.startsWith("/")) return pointer;
6182
+ const seg = pointer.slice(1).split("/")[0] ?? "";
6183
+ return seg.replace(/~1/g, "/").replace(/~0/g, "~");
6184
+ }
6185
+ function externalMetadataPointer(key) {
6186
+ return `/${escapePointerSegment(key)}`;
6187
+ }
6188
+ function orphanEntries(index, loaded, base, collections) {
6189
+ if (!index) return [];
6190
+ const have = new Set(loaded.map((l) => resolve5(base, l)));
6191
+ return index.entries.filter(
6192
+ (e) => inCollections(e, collections) && e.join === PATH_JOIN && e.abs !== void 0 && !have.has(e.abs)
6193
+ );
6194
+ }
6195
+ function inCollections(entry, collections) {
6196
+ return collections === void 0 || collections.includes(entry.collection);
6197
+ }
6198
+ function orphanJoins(index, matched, collections) {
6199
+ if (!index) return [];
6200
+ return index.entries.filter(
6201
+ (e) => inCollections(e, collections) && e.join !== PATH_JOIN && !(matched.get(e.join)?.has(e.spelled) ?? false)
6202
+ );
6203
+ }
6204
+ function orphanError(orphans) {
6205
+ const first = orphans[0];
6206
+ if (!first) throw new Error("orphanError called with no orphans");
6207
+ const where = first.line === void 0 ? first.file : `${first.file}:${first.line}`;
6208
+ const more = orphans.length > 1 ? ` (${orphans.length - 1} more)` : "";
6209
+ const what = first.join === PATH_JOIN ? `names "${first.spelled}", which this run did not load` : `names ${first.join} "${first.spelled}", which no loaded document carries`;
6210
+ return new DocmetaError(
6211
+ `Manifest ${where} ${what}${more}. Fix the entry, or remove it.`
6212
+ );
6213
+ }
6214
+
6215
+ // src/meta/core/config.ts
6216
+ import { resolve as resolve7 } from "node:path";
6217
+ import { parse as parseYaml2 } from "yaml";
6218
+
6219
+ // src/shared/config-file.ts
6220
+ import { readFile as readFile4 } from "node:fs/promises";
6221
+ import { dirname as dirname4, join as join4, relative as relative5, resolve as resolve6 } from "node:path";
6222
+ import { parse as parseYaml } from "yaml";
6223
+
6224
+ // src/shared/warn.ts
6225
+ var said = /* @__PURE__ */ new Set();
6226
+ function warn(message) {
6227
+ if (said.has(message)) return;
6228
+ said.add(message);
6229
+ process.stderr.write(`${programName()}: ${message}
6230
+ `);
6231
+ }
6232
+
6233
+ // src/shared/config-file.ts
6234
+ var FAMILY_CONFIG_NAMES = [
6235
+ "manni.config.yaml",
6236
+ "manni.config.yml"
6237
+ ];
6238
+ var MOOSE_CONFIG_NAMES = [
6239
+ "moose.config.yaml",
6240
+ "moose.config.yml"
6241
+ ];
6242
+ async function readDocument(path, source, opts) {
6243
+ let text;
6244
+ try {
6245
+ text = await readFile4(path, "utf8");
6246
+ } catch {
6247
+ return null;
6248
+ }
6249
+ return { text, doc: parseMapping(text, source, opts) };
6250
+ }
6251
+ function parseMapping(text, source, opts) {
6252
+ let raw;
6253
+ try {
6254
+ raw = parseYaml(text);
6255
+ } catch (err) {
6256
+ throw opts.toError(`${source}: invalid YAML: ${err.message}`);
6257
+ }
6258
+ if (raw == null) return null;
6259
+ if (typeof raw !== "object" || Array.isArray(raw)) {
6260
+ throw opts.toError(`${source}: top level must be a mapping.`);
6261
+ }
6262
+ return raw;
6263
+ }
6264
+ var COLLECTIONS_KEY = "collections";
6265
+ function slice(document, opts) {
6266
+ const { doc } = document;
6267
+ if (doc !== null && Object.hasOwn(doc, opts.section)) {
6268
+ return { value: doc[opts.section] ?? null, wrapped: true };
6269
+ }
6270
+ if (doc !== null && Object.hasOwn(doc, COLLECTIONS_KEY)) {
6271
+ return { value: null, wrapped: true };
6272
+ }
6273
+ return null;
6274
+ }
6275
+ function collectionsOf(document, source, opts) {
6276
+ const { doc } = document;
6277
+ if (doc === null || !Object.hasOwn(doc, COLLECTIONS_KEY)) return [];
6278
+ return parseCollections(doc[COLLECTIONS_KEY], source, opts.toError);
6279
+ }
6280
+ function relativeSource(cwd, path) {
6281
+ return relative5(cwd, path).replace(/\\/g, "/");
6282
+ }
6283
+ async function findConfigFile(cwd, opts) {
6284
+ const start = resolve6(cwd);
6285
+ for (const dir of searchPath(start)) {
6286
+ const family = [
6287
+ { names: FAMILY_CONFIG_NAMES, kind: "manni" },
6288
+ { names: MOOSE_CONFIG_NAMES, kind: "moose" }
6289
+ ];
6290
+ for (const { names, kind } of family) {
5438
6291
  for (const name2 of names) {
5439
6292
  const path = join4(dir, name2);
5440
6293
  const source = relativeSource(start, path);
@@ -5447,7 +6300,15 @@ async function findConfigFile(cwd, opts) {
5447
6300
  `"${name2}" is the pre-rename name of the family config file. Rename it to "${FAMILY_CONFIG_NAMES[0] ?? "manni.config.yaml"}".`
5448
6301
  );
5449
6302
  }
5450
- return { path, dir, source, text: document.text, kind, ...found };
6303
+ return {
6304
+ path,
6305
+ dir,
6306
+ source,
6307
+ text: document.text,
6308
+ kind,
6309
+ collections: collectionsOf(document, source, opts),
6310
+ ...found
6311
+ };
5451
6312
  }
5452
6313
  }
5453
6314
  for (const name2 of opts.legacyNames) {
@@ -5456,7 +6317,7 @@ async function findConfigFile(cwd, opts) {
5456
6317
  const document = await readDocument(path, source, opts);
5457
6318
  if (document === null) continue;
5458
6319
  warn(
5459
- `"${name2}" is a deprecated config file name and will stop being read in a future major version. Move its keys under \`${opts.section}:\` in "${FAMILY_CONFIG_NAMES[0] ?? "manni.config.yaml"}".`
6320
+ `"${name2}" is a deprecated config file name and will stop being read in a future major version. Move its keys under \`${opts.section}:\` in "${FAMILY_CONFIG_NAMES[0] ?? "manni.config.yaml"}", and its paths, exclude and sidecars keys to a top-level collections: list, where sidecars becomes externalMetadata.`
5460
6321
  );
5461
6322
  return {
5462
6323
  path,
@@ -5465,14 +6326,15 @@ async function findConfigFile(cwd, opts) {
5465
6326
  text: document.text,
5466
6327
  value: document.doc,
5467
6328
  wrapped: false,
5468
- kind: "legacy"
6329
+ kind: "legacy",
6330
+ collections: []
5469
6331
  };
5470
6332
  }
5471
6333
  }
5472
6334
  return null;
5473
6335
  }
5474
6336
  async function readConfigFile(explicitPath, cwd, opts) {
5475
- const path = resolve3(cwd, explicitPath);
6337
+ const path = resolve6(cwd, explicitPath);
5476
6338
  const document = await readDocument(path, explicitPath, opts);
5477
6339
  if (document === null) {
5478
6340
  throw opts.toError(`Config file not found: "${explicitPath}".`);
@@ -5483,10 +6345,11 @@ async function readConfigFile(explicitPath, cwd, opts) {
5483
6345
  };
5484
6346
  return {
5485
6347
  path,
5486
- dir: dirname3(path),
6348
+ dir: dirname4(path),
5487
6349
  source: explicitPath,
5488
6350
  text: document.text,
5489
6351
  kind: "explicit",
6352
+ collections: collectionsOf(document, explicitPath, opts),
5490
6353
  ...found
5491
6354
  };
5492
6355
  }
@@ -5507,7 +6370,7 @@ function asElementPaths(value, where, source) {
5507
6370
  }
5508
6371
  return list;
5509
6372
  }
5510
- var OVERRIDE_KEYS = ["name", "files", "schemas", "elements"];
6373
+ var OVERRIDE_KEYS = ["collection", "files", "schemas", "elements"];
5511
6374
  var CHECK_KEYS = ["name", "query"];
5512
6375
  var CHECK_NAME = /^[a-z0-9][a-z0-9._-]*$/;
5513
6376
  var RESERVED_CHECK_NAMES = /* @__PURE__ */ new Set(["query"]);
@@ -5520,8 +6383,6 @@ var FILL_KEYS = [
5520
6383
  "concurrency"
5521
6384
  ];
5522
6385
  var CONFIG_KEYS = [
5523
- "paths",
5524
- "exclude",
5525
6386
  "schemas",
5526
6387
  "overrides",
5527
6388
  "checks",
@@ -5534,7 +6395,7 @@ var CONFIG_KEYS = [
5534
6395
  "fill",
5535
6396
  "elements"
5536
6397
  ];
5537
- function rejectUnknownKeys(raw, allowed, where, source) {
6398
+ function rejectUnknownKeys2(raw, allowed, where, source) {
5538
6399
  for (const key of Object.keys(raw)) {
5539
6400
  if (!allowed.includes(key)) {
5540
6401
  throw new DocmetaError(
@@ -5604,7 +6465,7 @@ function parseSchemaEntry(entry, where, source) {
5604
6465
  );
5605
6466
  }
5606
6467
  const raw = entry;
5607
- rejectUnknownKeys(raw, SCHEMA_ENTRY_KEYS, where, source);
6468
+ rejectUnknownKeys2(raw, SCHEMA_ENTRY_KEYS, where, source);
5608
6469
  if (typeof raw.ref !== "string" || raw.ref.trim() === "") {
5609
6470
  throw new DocmetaError(
5610
6471
  `${source}: ${where}.ref must be a non-empty schema reference string.`
@@ -5647,6 +6508,7 @@ function parseConfig(text, source) {
5647
6508
  return parseConfigValue(raw, source);
5648
6509
  }
5649
6510
  function parseConfigValue(raw, source, section) {
6511
+ assertNoMovedKeys(raw, source);
5650
6512
  if (section === void 0) return parseConfigDocument(raw, source);
5651
6513
  try {
5652
6514
  return parseConfigDocument(raw, source);
@@ -5668,73 +6530,99 @@ function withSection(message, source, section) {
5668
6530
  if (/^[a-zA-Z]+(\[|\.| —)/.test(rest)) return `${head}${section}.${rest}`;
5669
6531
  return message;
5670
6532
  }
6533
+ var CONFIG_REF = "https://hawkeyexl.github.io/manni/meta/reference/configuration/";
6534
+ var MOVED_KEYS = [
6535
+ [
6536
+ "paths",
6537
+ `Document sets are declared once for every tool, under a top-level collections: list. See ${CONFIG_REF}#collections`
6538
+ ],
6539
+ [
6540
+ "exclude",
6541
+ `Document sets are declared once for every tool, under a top-level collections: list. See ${CONFIG_REF}#collections`
6542
+ ],
6543
+ [
6544
+ "sidecars",
6545
+ `It is externalMetadata on a collection, under the top-level collections: list. See ${CONFIG_REF}#external-metadata`
6546
+ ]
6547
+ ];
6548
+ function assertNoMovedKeys(raw, source) {
6549
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return;
6550
+ const obj = raw;
6551
+ for (const [key, remedy] of MOVED_KEYS) {
6552
+ if (Object.hasOwn(obj, key)) {
6553
+ throw new DocmetaError(
6554
+ `${source}: "${key}" is no longer a meta key. ${remedy}`
6555
+ );
6556
+ }
6557
+ }
6558
+ if (!Array.isArray(obj.overrides)) return;
6559
+ for (const [i, entry] of obj.overrides.entries()) {
6560
+ if (typeof entry === "object" && entry !== null && !Array.isArray(entry) && Object.hasOwn(entry, "name")) {
6561
+ throw new DocmetaError(
6562
+ `${source}: overrides[${i}] no longer carries "name". Define a collection with that name and point the override at it with collection:.`
6563
+ );
6564
+ }
6565
+ }
6566
+ }
6567
+ function assertOverrideCollections(config, collections, source) {
6568
+ const names = new Set(collections.map((c) => c.name));
6569
+ for (const [i, ov] of (config.overrides ?? []).entries()) {
6570
+ if (ov.collection === void 0 || names.has(ov.collection)) continue;
6571
+ const defined = collections.length === 0 ? "(none)" : collections.map((c) => c.name).join(", ");
6572
+ throw new DocmetaError(
6573
+ `${source}: overrides[${i}].collection names "${ov.collection}", which collections: does not define. Defined: ${defined}.`
6574
+ );
6575
+ }
6576
+ }
5671
6577
  function parseConfigDocument(raw, source) {
5672
6578
  if (raw == null) return {};
5673
6579
  if (typeof raw !== "object" || Array.isArray(raw)) {
5674
6580
  throw new DocmetaError(`${source}: top level must be a mapping.`);
5675
6581
  }
5676
6582
  const obj = raw;
5677
- rejectUnknownKeys(obj, CONFIG_KEYS, "the top level", source);
6583
+ rejectUnknownKeys2(obj, CONFIG_KEYS, "the top level", source);
5678
6584
  const config = {};
5679
- if (obj.paths !== void 0) config.paths = asStringList(obj.paths, "paths", source);
5680
- if (obj.exclude !== void 0)
5681
- config.exclude = asStringList(obj.exclude, "exclude", source);
5682
6585
  if (obj.schemas !== void 0)
5683
6586
  config.schemas = asSchemaList(obj.schemas, "schemas", source);
5684
6587
  if (obj.overrides !== void 0) {
5685
6588
  if (!Array.isArray(obj.overrides)) {
5686
6589
  throw new DocmetaError(`${source}: "overrides" must be a list.`);
5687
6590
  }
5688
- const seenNames = /* @__PURE__ */ new Set();
5689
6591
  config.overrides = obj.overrides.map((entry, i) => {
5690
6592
  if (typeof entry !== "object" || entry === null) {
5691
6593
  throw new DocmetaError(`${source}: overrides[${i}] must be a mapping.`);
5692
6594
  }
5693
6595
  const e = entry;
5694
- rejectUnknownKeys(e, OVERRIDE_KEYS, `overrides[${i}]`, source);
5695
- const files = asFileGlobs(e.files, `overrides[${i}].files`, source);
5696
- const schemas = e.schemas === void 0 ? [] : asStringList(e.schemas, `overrides[${i}].schemas`, source);
5697
- const elements = e.elements === void 0 ? void 0 : asElementPaths(e.elements, `overrides[${i}].elements`, source);
5698
- if (schemas.length === 0 && (elements === void 0 || elements.length === 0)) {
6596
+ rejectUnknownKeys2(e, OVERRIDE_KEYS, `overrides[${i}]`, source);
6597
+ const hasFiles = e.files !== void 0;
6598
+ const hasCollection = e.collection !== void 0;
6599
+ if (hasFiles === hasCollection) {
5699
6600
  throw new DocmetaError(
5700
- `${source}: overrides[${i}] sets neither "schemas" nor "elements", so it has no effect.`
6601
+ `${source}: overrides[${i}] must carry exactly one of "files" or "collection".`
5701
6602
  );
5702
6603
  }
5703
- let name2;
5704
- if (e.name !== void 0) {
5705
- if (typeof e.name !== "string" || e.name.trim() === "") {
6604
+ const files = hasFiles ? asFileGlobs(e.files, `overrides[${i}].files`, source) : void 0;
6605
+ let collection;
6606
+ if (hasCollection) {
6607
+ if (typeof e.collection !== "string" || e.collection.trim() === "") {
5706
6608
  throw new DocmetaError(
5707
- `${source}: overrides[${i}].name must be a non-empty string \u2014 it becomes the collection's view name.`
6609
+ `${source}: overrides[${i}].collection must be a non-empty string naming a collection.`
5708
6610
  );
5709
6611
  }
5710
- if (e.name.toLowerCase() === "docs") {
5711
- throw new DocmetaError(
5712
- `${source}: overrides[${i}].name "${e.name}" collides with the docs table every query reads. Pick another name.`
5713
- );
5714
- }
5715
- if (e.name.toLowerCase().startsWith("sqlite_")) {
5716
- throw new DocmetaError(
5717
- `${source}: overrides[${i}].name "${e.name}" starts with "sqlite_", which SQLite reserves for its own objects. Pick another name.`
5718
- );
5719
- }
5720
- if (schemas.length === 0) {
5721
- throw new DocmetaError(
5722
- `${source}: overrides[${i}] names a collection ("${e.name}") but sets no "schemas", so it can never win schema resolution and its view would always be empty. Add "schemas", or drop the name.`
5723
- );
5724
- }
5725
- if (seenNames.has(e.name.toLowerCase())) {
5726
- throw new DocmetaError(
5727
- `${source}: overrides[${i}] reuses the name "${e.name}"; collection names must be unique.`
5728
- );
5729
- }
5730
- seenNames.add(e.name.toLowerCase());
5731
- name2 = e.name;
6612
+ collection = e.collection;
6613
+ }
6614
+ const schemas = e.schemas === void 0 ? [] : asStringList(e.schemas, `overrides[${i}].schemas`, source);
6615
+ const elements = e.elements === void 0 ? void 0 : asElementPaths(e.elements, `overrides[${i}].elements`, source);
6616
+ if (schemas.length === 0 && (elements === void 0 || elements.length === 0)) {
6617
+ throw new DocmetaError(
6618
+ `${source}: overrides[${i}] sets neither "schemas" nor "elements", so it has no effect.`
6619
+ );
5732
6620
  }
5733
6621
  return {
5734
- files,
6622
+ ...files !== void 0 ? { files } : {},
6623
+ ...collection !== void 0 ? { collection } : {},
5735
6624
  schemas,
5736
- ...elements ? { elements } : {},
5737
- ...name2 !== void 0 ? { name: name2 } : {}
6625
+ ...elements ? { elements } : {}
5738
6626
  };
5739
6627
  });
5740
6628
  }
@@ -5791,7 +6679,7 @@ function parseChecks(value, source) {
5791
6679
  );
5792
6680
  }
5793
6681
  const e = entry;
5794
- rejectUnknownKeys(e, CHECK_KEYS, `checks[${i}]`, source);
6682
+ rejectUnknownKeys2(e, CHECK_KEYS, `checks[${i}]`, source);
5795
6683
  if (typeof e.name !== "string" || e.name === "") {
5796
6684
  throw new DocmetaError(
5797
6685
  `${source}: checks[${i}].name must be a non-empty string.`
@@ -5826,7 +6714,7 @@ function parseSchemaCache(value, source) {
5826
6714
  throw new DocmetaError(`${source}: "schemaCache" must be a mapping.`);
5827
6715
  }
5828
6716
  const raw = value;
5829
- rejectUnknownKeys(raw, SCHEMA_CACHE_KEYS, '"schemaCache"', source);
6717
+ rejectUnknownKeys2(raw, SCHEMA_CACHE_KEYS, '"schemaCache"', source);
5830
6718
  const schemaCache = {};
5831
6719
  const ttl = raw.ttlHours;
5832
6720
  if (ttl !== void 0) {
@@ -5846,7 +6734,7 @@ function parseSchemaTrust(value, source) {
5846
6734
  );
5847
6735
  }
5848
6736
  const raw = value;
5849
- rejectUnknownKeys(raw, SCHEMA_TRUST_KEYS, '"schemaTrust"', source);
6737
+ rejectUnknownKeys2(raw, SCHEMA_TRUST_KEYS, '"schemaTrust"', source);
5850
6738
  const schemaTrust = {};
5851
6739
  const mode = raw.documentRefs;
5852
6740
  if (mode !== void 0) {
@@ -5873,7 +6761,7 @@ function parseFill(value, source) {
5873
6761
  throw new DocmetaError(`${source}: "fill" must be a mapping.`);
5874
6762
  }
5875
6763
  const raw = value;
5876
- rejectUnknownKeys(raw, FILL_KEYS, '"fill"', source);
6764
+ rejectUnknownKeys2(raw, FILL_KEYS, '"fill"', source);
5877
6765
  const fill = {};
5878
6766
  const asString = (key) => {
5879
6767
  const v = raw[key];
@@ -5909,865 +6797,715 @@ function parseFill(value, source) {
5909
6797
  function schemaTrustRoot(cwd, configDir) {
5910
6798
  const git = findGitRoot(cwd);
5911
6799
  if (git !== null) return { dir: git, source: "git" };
5912
- if (configDir !== void 0) return { dir: resolve4(configDir), source: "config" };
5913
- return { dir: resolve4(cwd), source: "cwd" };
6800
+ if (configDir !== void 0) return { dir: resolve7(configDir), source: "config" };
6801
+ return { dir: resolve7(cwd), source: "cwd" };
5914
6802
  }
5915
6803
  async function loadConfig(explicitPath, cwd = process.cwd()) {
5916
6804
  const file = explicitPath ? await readConfigFile(explicitPath, cwd, CONFIG_FILE) : await findConfigFile(cwd, CONFIG_FILE);
5917
6805
  if (file === null) return null;
5918
6806
  const section = file.wrapped ? META_SECTION : void 0;
6807
+ const config = parseConfigValue(file.value, file.source, section);
6808
+ assertOverrideCollections(config, file.collections, file.source);
5919
6809
  return {
5920
- config: parseConfigValue(file.value, file.source, section),
6810
+ config,
5921
6811
  path: file.path,
5922
6812
  dir: file.dir,
6813
+ source: file.source,
6814
+ collections: file.collections,
5923
6815
  ...section !== void 0 ? { section } : {}
5924
6816
  };
5925
6817
  }
6818
+ var STDIN_TOKEN_LITERAL = "-";
5926
6819
  async function resolveRunConfig(opts) {
5927
6820
  const cwd = opts.cwd ?? process.cwd();
6821
+ const wanted = opts.collections ?? [];
6822
+ if (wanted.length > 0) {
6823
+ const typed = opts.inputs.filter((i) => i !== STDIN_TOKEN_LITERAL);
6824
+ if (typed.length > 0) {
6825
+ throw new DocmetaError(
6826
+ "--collection selects a configured collection; it cannot be combined with paths."
6827
+ );
6828
+ }
6829
+ }
5928
6830
  const loaded = opts.noConfig ? null : await loadConfig(opts.configPath, cwd);
6831
+ if (wanted.length > 0 && loaded === null) {
6832
+ throw new DocmetaError("--collection needs a config file to select from.");
6833
+ }
5929
6834
  if (loaded) opts.onConfigLoaded?.({ path: loaded.path, dir: loaded.dir });
5930
6835
  const config = loaded ? rebaseConfigSchemaRefs(loaded.config, loaded.dir, cwd) : null;
5931
- const fromConfig = opts.inputs.length === 0;
5932
- const inputs = fromConfig ? config?.paths ?? [] : opts.inputs;
5933
- const base = fromConfig && inputs.length > 0 && loaded ? loaded.dir : cwd;
6836
+ const collections = loaded ? selectCollections(
6837
+ loaded.collections,
6838
+ opts.collections,
6839
+ loaded.source,
6840
+ (message) => new DocmetaError(message)
6841
+ ) : [];
6842
+ const fromCollections = opts.inputs.length === 0;
6843
+ const inputs = fromCollections ? collections.flatMap((c) => c.paths) : opts.inputs;
6844
+ const base = fromCollections && inputs.length > 0 && loaded ? loaded.dir : cwd;
5934
6845
  return {
5935
6846
  config,
5936
6847
  inputs,
5937
6848
  base,
5938
- ...loaded ? { configDir: loaded.dir, configPath: loaded.path } : {}
6849
+ collections,
6850
+ declaredCollections: loaded ? [...loaded.collections] : [],
6851
+ fromCollections,
6852
+ ...loaded ? {
6853
+ configDir: loaded.dir,
6854
+ configPath: loaded.path,
6855
+ ...loaded.section === void 0 ? {} : { configSection: loaded.section }
6856
+ } : {}
5939
6857
  };
5940
6858
  }
5941
6859
 
5942
- // src/meta/core/validator.ts
5943
- import * as AjvDraft07Ns from "ajv";
5944
- import * as Ajv2019Ns from "ajv/dist/2019.js";
5945
- import * as Ajv2020Ns from "ajv/dist/2020.js";
5946
- import * as AjvDraft04Ns from "ajv-draft-04";
5947
- import * as addFormatsNs from "ajv-formats";
5948
- import { createRequire } from "node:module";
5949
- var draft06MetaSchema = createRequire(import.meta.url)(
5950
- "ajv/dist/refs/json-schema-draft-06.json"
5951
- );
5952
- var AjvDraft07 = AjvDraft07Ns.default;
5953
- var Ajv2019 = Ajv2019Ns.default;
5954
- var Ajv2020 = Ajv2020Ns.default;
5955
- var AjvDraft04 = AjvDraft04Ns.default;
5956
- var addFormats = addFormatsNs.default;
5957
- function dialectOf(schema) {
5958
- const meta = typeof schema.$schema === "string" ? schema.$schema : "";
5959
- if (meta.includes("2019-09")) return "2019";
5960
- if (meta.includes("draft-07") || meta.includes("draft/7") || meta.includes("draft-06") || meta.includes("draft/6")) {
5961
- return "draft7";
6860
+ // src/meta/core/collections.ts
6861
+ import { isAbsolute as isAbsolute3, relative as relative6, resolve as resolvePath2, sep as sep3 } from "node:path";
6862
+
6863
+ // src/meta/core/projection.ts
6864
+ var SYSTEM_COLUMNS = ["_path", "_format", "_present", "_data"];
6865
+ var RESERVED = new Set(SYSTEM_COLUMNS);
6866
+ function quoteIdent(name2) {
6867
+ return `"${name2.replaceAll('"', '""')}"`;
6868
+ }
6869
+ function bindValue(value) {
6870
+ if (value === void 0 || value === null) return null;
6871
+ switch (typeof value) {
6872
+ case "boolean":
6873
+ return value ? 1 : 0;
6874
+ case "number":
6875
+ return Number.isFinite(value) ? value : null;
6876
+ case "bigint":
6877
+ case "string":
6878
+ return value;
6879
+ default:
6880
+ return JSON.stringify(value);
5962
6881
  }
5963
- if (meta.includes("draft-04") || meta.includes("draft/4")) return "draft4";
5964
- return "2020";
5965
6882
  }
5966
- function buildAjv(dialect) {
5967
- const opts = { allErrors: true, strict: false };
5968
- const ajv = dialect === "2019" ? new Ajv2019(opts) : dialect === "draft7" ? new AjvDraft07(opts) : dialect === "draft4" ? new AjvDraft04(opts) : new Ajv2020(opts);
5969
- addFormats(ajv);
5970
- if (dialect === "draft7") ajv.addMetaSchema(draft06MetaSchema);
5971
- registerBuiltins(ajv, dialect);
5972
- return ajv;
6883
+ function corpusDataColumns(entries, extraKeys = []) {
6884
+ const keys = /* @__PURE__ */ new Set();
6885
+ for (const { extracted } of entries) {
6886
+ for (const key of Object.keys(extracted.data)) {
6887
+ if (key !== "" && !RESERVED.has(key)) keys.add(key);
6888
+ }
6889
+ }
6890
+ for (const key of extraKeys) {
6891
+ if (key !== "" && !RESERVED.has(key)) keys.add(key);
6892
+ }
6893
+ return [...keys].sort();
5973
6894
  }
5974
- function registerBuiltins(ajv, dialect) {
5975
- for (const { id, url, legacyUrl, schema } of publishedBuiltins()) {
5976
- ajv.addSchema(schema, url, void 0, false);
5977
- ajv.addSchema(schema, legacyUrl, void 0, false);
5978
- if (dialect === "draft4") ajv.addSchema(schema, id, void 0, false);
6895
+ function createDocsTable(db, entries, dataColumns) {
6896
+ db.exec(
6897
+ `CREATE TABLE docs ("_path" TEXT PRIMARY KEY, "_format" TEXT, "_present" INTEGER, "_data" TEXT${dataColumns.map((c) => `, ${quoteIdent(c)}`).join("")})`
6898
+ );
6899
+ const insert = db.prepare(
6900
+ `INSERT INTO docs VALUES (${["?", "?", "?", "?", ...dataColumns.map(() => "?")].join(", ")})`
6901
+ );
6902
+ db.exec("BEGIN");
6903
+ for (const { label, extracted } of entries) {
6904
+ insert.run(
6905
+ label,
6906
+ extracted.format,
6907
+ extracted.present ? 1 : 0,
6908
+ JSON.stringify(extracted.data),
6909
+ ...dataColumns.map((c) => bindValue(extracted.data[c]))
6910
+ );
5979
6911
  }
6912
+ db.exec("COMMIT");
5980
6913
  }
5981
- function compileWithFormats(schema) {
5982
- return buildAjv("2020").compile(schema);
6914
+ function registerLineFor(db, entries) {
6915
+ const byLabel = new Map(entries.map((e) => [e.label, e.extracted]));
6916
+ db.function("lineFor", { deterministic: true }, (path, key) => {
6917
+ if (typeof path !== "string" || typeof key !== "string") return null;
6918
+ return byLabel.get(path)?.lineFor(key) ?? null;
6919
+ });
5983
6920
  }
5984
- var formatRegistry;
5985
- function formatsAjv() {
5986
- if (!formatRegistry) {
5987
- formatRegistry = new Ajv2020({ strict: false });
5988
- addFormats(formatRegistry);
6921
+ function assertSingleStatement(sql) {
6922
+ const cut = topLevelSemicolon(sql);
6923
+ if (cut === -1) return;
6924
+ if (!isTrivia(sql.slice(cut + 1))) {
6925
+ throw new DocmetaError(
6926
+ "Run a single SQL statement per query; text after the first `;` would be silently ignored."
6927
+ );
5989
6928
  }
5990
- return formatRegistry;
5991
6929
  }
5992
- var stringFormatsMemo;
5993
- function stringFormatNames() {
5994
- if (!stringFormatsMemo) {
5995
- const names = /* @__PURE__ */ new Set();
5996
- const registered = formatsAjv().formats;
5997
- for (const [name2, def] of Object.entries(registered)) {
5998
- const enforcesStrings = def instanceof RegExp || typeof def === "function" || typeof def === "object" && def !== null && (def.type === void 0 || def.type === "string");
5999
- if (enforcesStrings) names.add(name2);
6000
- }
6001
- stringFormatsMemo = names;
6002
- }
6003
- return stringFormatsMemo;
6930
+ function startsStringOrIdent(sql, i) {
6931
+ const ch = sql[i];
6932
+ return ch === "'" || ch === '"' || ch === "`" || ch === "[";
6004
6933
  }
6005
- var formatCheckers = /* @__PURE__ */ new Map();
6006
- function validatesFormat(name2, value) {
6007
- let fn = formatCheckers.get(name2);
6008
- if (!fn) {
6009
- fn = formatsAjv().compile({ type: "string", format: name2 });
6010
- formatCheckers.set(name2, fn);
6011
- }
6012
- return fn(value);
6934
+ function startsComment(sql, i) {
6935
+ const ch = sql[i];
6936
+ return ch === "-" && sql[i + 1] === "-" || ch === "/" && sql[i + 1] === "*";
6013
6937
  }
6014
- var Validator = class {
6015
- /**
6016
- * How this validator's schemas are loaded: the disk cache location, its TTL,
6017
- * and `--offline`. Held per instance rather than read from module state, so
6018
- * two differently-configured validations in one process each get their own
6019
- * settings.
6020
- *
6021
- * That is not full isolation, and the difference matters to a library
6022
- * caller: `schema-registry` keeps a process-wide memo of fetched schemas, so
6023
- * one validator's successful fetch is visible to another. `offline` is
6024
- * excluded from that sharing on purpose — an offline validator will not be
6025
- * served something this process pulled over the network — but the memo is
6026
- * still shared, so a URL fetched once is not re-fetched per instance.
6027
- */
6028
- constructor(schemaOptions = {}) {
6029
- this.schemaOptions = schemaOptions;
6938
+ function skipStringOrIdent(sql, i) {
6939
+ const quote = sql[i];
6940
+ if (quote === "[") {
6941
+ let j2 = i + 1;
6942
+ while (j2 < sql.length && sql[j2] !== "]") j2++;
6943
+ return j2 + 1;
6030
6944
  }
6031
- schemaOptions;
6032
- ajvByDialect = /* @__PURE__ */ new Map();
6033
- /**
6034
- * Keyed on the in-flight *promise*, not the resolved validator. Caching the
6035
- * result made this a check-then-act race: `fill` walks files through a worker
6036
- * pool, so every worker missed the cache while the first `loadSchema` was
6037
- * still pending and they all then compiled the same schema into the one
6038
- * shared per-dialect Ajv. Ajv registers a schema's `$id` on the first compile
6039
- * and rejects the second with "schema with key or id ... already exists",
6040
- * which took down any multi-file run against an $id-bearing schema. Storing
6041
- * the promise before the first await lets the losers await the one compile.
6042
- */
6043
- cache = /* @__PURE__ */ new Map();
6044
- ajvFor(dialect) {
6045
- let ajv = this.ajvByDialect.get(dialect);
6046
- if (!ajv) {
6047
- ajv = buildAjv(dialect);
6048
- this.ajvByDialect.set(dialect, ajv);
6945
+ let j = i + 1;
6946
+ while (j < sql.length) {
6947
+ if (sql[j] === quote) {
6948
+ if (sql[j + 1] === quote) {
6949
+ j += 2;
6950
+ continue;
6951
+ }
6952
+ return j + 1;
6049
6953
  }
6050
- return ajv;
6954
+ j++;
6051
6955
  }
6052
- /**
6053
- * Synchronous by design: the `cache.set` has to happen in the same tick as
6054
- * the miss, or a second caller can slip in before the entry exists.
6055
- */
6056
- compile(ref) {
6057
- const cached = this.cache.get(ref);
6058
- if (cached) return cached;
6059
- const pending = this.compileUncached(ref);
6060
- this.cache.set(ref, pending);
6061
- pending.catch(() => {
6062
- if (this.cache.get(ref) === pending) this.cache.delete(ref);
6063
- });
6064
- return pending;
6956
+ return j;
6957
+ }
6958
+ function skipComment(sql, i) {
6959
+ if (sql[i] === "-") {
6960
+ let j = i + 2;
6961
+ while (j < sql.length && sql[j] !== "\n") j++;
6962
+ return j < sql.length ? j + 1 : j;
6065
6963
  }
6066
- async compileUncached(ref) {
6067
- const schema = await loadSchema(ref, this.schemaOptions);
6068
- try {
6069
- const ajv = this.ajvFor(dialectOf(schema));
6070
- const id = typeof schema["$id"] === "string" ? schema["$id"] : void 0;
6071
- const registered = id != null ? ajv.getSchema(id) : void 0;
6072
- if (registered) {
6073
- if (registered.schema === schema) return registered;
6074
- const { $id: _taken, ...anonymous } = schema;
6075
- return ajv.compile(anonymous);
6076
- }
6077
- return ajv.compile(schema);
6078
- } catch (err) {
6079
- throw new DocmetaError(
6080
- `Schema "${ref}" failed to compile: ${err.message}`
6081
- );
6964
+ const end = sql.indexOf("*/", i + 2);
6965
+ return end === -1 ? sql.length : end + 2;
6966
+ }
6967
+ function topLevelSemicolon(sql) {
6968
+ let i = 0;
6969
+ while (i < sql.length) {
6970
+ if (startsStringOrIdent(sql, i)) {
6971
+ i = skipStringOrIdent(sql, i);
6972
+ } else if (startsComment(sql, i)) {
6973
+ i = skipComment(sql, i);
6974
+ } else if (sql[i] === ";") {
6975
+ return i;
6976
+ } else {
6977
+ i++;
6082
6978
  }
6083
6979
  }
6084
- /**
6085
- * Validate `data` against every schema in `refs`. Returns all violations,
6086
- * each tagged with the schema that produced it and a source line via
6087
- * `lineFor`.
6088
- *
6089
- * `colFor` is optional and additive: this signature is public, so a fourth
6090
- * *required* parameter — or a widened third one — would be a consumer break.
6091
- * Callers with an extractor that supplies no column pass nothing and get the
6092
- * previous behavior exactly.
6093
- */
6094
- async validate(data, refs, lineFor, colFor) {
6095
- const { [FILE_SCHEMA_KEY]: _omit, ...subject } = data;
6096
- void _omit;
6097
- const errors = [];
6098
- for (const ref of refs) {
6099
- const fn = await this.compile(ref);
6100
- const ok = fn(subject);
6101
- if (ok) continue;
6102
- for (const e of fn.errors ?? []) {
6103
- errors.push(toFieldError(ref, e, lineFor, colFor));
6980
+ return -1;
6981
+ }
6982
+ var PARAM_NAME_HEAD = /^[A-Za-z_][A-Za-z0-9_]*/;
6983
+ function isParamName(name2) {
6984
+ return PARAM_NAME_HEAD.exec(name2)?.[0] === name2;
6985
+ }
6986
+ function collectNamedParameters(sql) {
6987
+ const tokens = [];
6988
+ const seen = /* @__PURE__ */ new Set();
6989
+ const prefixOf = /* @__PURE__ */ new Map();
6990
+ let i = 0;
6991
+ while (i < sql.length) {
6992
+ const ch = sql[i];
6993
+ if (startsStringOrIdent(sql, i)) {
6994
+ i = skipStringOrIdent(sql, i);
6995
+ } else if (startsComment(sql, i)) {
6996
+ i = skipComment(sql, i);
6997
+ } else if (ch === "$" || ch === ":" || ch === "@") {
6998
+ if (ch === ":" && sql[i + 1] === ":") {
6999
+ i += 2;
7000
+ continue;
7001
+ }
7002
+ const m = PARAM_NAME_HEAD.exec(sql.slice(i + 1));
7003
+ if (!m) {
7004
+ i++;
7005
+ continue;
7006
+ }
7007
+ const token = ch + m[0];
7008
+ const priorPrefix = prefixOf.get(m[0]);
7009
+ if (priorPrefix !== void 0 && priorPrefix !== ch) {
7010
+ throw new DocmetaError(
7011
+ `The statement references both ${priorPrefix}${m[0]} and ${token} \u2014 one name under two prefixes, which the engine rejects as conflicting. Use one spelling throughout.`
7012
+ );
7013
+ }
7014
+ prefixOf.set(m[0], ch);
7015
+ if (!seen.has(token)) {
7016
+ seen.add(token);
7017
+ tokens.push(token);
6104
7018
  }
7019
+ i += 1 + m[0].length;
7020
+ } else {
7021
+ i++;
6105
7022
  }
6106
- return errors;
6107
- }
6108
- };
6109
- function subjectOf(e) {
6110
- switch (e.keyword) {
6111
- case "required":
6112
- return e.params.missingProperty;
6113
- case "additionalProperties":
6114
- return e.params.additionalProperty;
6115
- case "format":
6116
- return e.params.format;
6117
- case "type":
6118
- return e.params.type;
6119
- default:
6120
- return void 0;
6121
7023
  }
7024
+ return tokens;
6122
7025
  }
6123
- function toFieldError(schema, e, lineFor, colFor) {
6124
- const defined = e;
6125
- const instancePath = e.instancePath;
6126
- const subject = subjectOf(defined);
6127
- let message = e.message ?? "is invalid";
6128
- let wantsColumn = true;
6129
- if (defined.keyword === "required") {
6130
- message = `must have required property '${defined.params.missingProperty}'`;
6131
- wantsColumn = false;
6132
- } else if (defined.keyword === "additionalProperties") {
6133
- message = `must NOT have additional property '${defined.params.additionalProperty}'`;
6134
- wantsColumn = false;
7026
+ function stripLeadingTrivia(sql) {
7027
+ let i = 0;
7028
+ for (; ; ) {
7029
+ while (i < sql.length && /\s/.test(sql[i] ?? "")) i++;
7030
+ if (startsComment(sql, i)) {
7031
+ i = skipComment(sql, i);
7032
+ continue;
7033
+ }
7034
+ return sql.slice(i);
6135
7035
  }
6136
- const line = lineFor(instancePath);
6137
- const col = wantsColumn ? colFor?.(instancePath) : void 0;
6138
- return {
6139
- schema,
6140
- instancePath,
6141
- message,
6142
- keyword: e.keyword,
6143
- ...subject != null ? { subject } : {},
6144
- ...line != null ? { line } : {},
6145
- ...col != null ? { col } : {}
6146
- };
6147
7036
  }
6148
-
6149
- // src/meta/commands/validate.ts
6150
- import { existsSync as existsSync2 } from "node:fs";
6151
- import { readFile as readFile5 } from "node:fs/promises";
6152
- import { resolve as resolve7, extname as extname2 } from "node:path";
6153
-
6154
- // src/meta/core/baseline.ts
6155
- import { createHash as createHash3 } from "node:crypto";
6156
- import { readFile as readFile4 } from "node:fs/promises";
6157
- import { relative as relative3, resolve as resolve5 } from "node:path";
6158
- var DEFAULT_BASELINE_PATH = ".manni-baseline.json";
6159
- var LEGACY_BASELINE_PATH = ".docmeta-baseline.json";
6160
- var BASELINE_VERSION = 1;
6161
- var FINGERPRINT_RE = /^[0-9a-f]{16}$/;
6162
- function canonicalFilePath(file, ctx) {
6163
- if (!ctx) return file;
6164
- const from = ctx.runBase ?? ctx.base;
6165
- return relative3(ctx.base, resolve5(from, file)).replace(/\\/g, "/");
7037
+ function isTrivia(text) {
7038
+ let i = 0;
7039
+ while (i < text.length) {
7040
+ const ch = text[i];
7041
+ if (ch === void 0 || /\s/.test(ch) || ch === ";") {
7042
+ i++;
7043
+ continue;
7044
+ }
7045
+ if (startsComment(text, i)) {
7046
+ i = skipComment(text, i);
7047
+ continue;
7048
+ }
7049
+ return false;
7050
+ }
7051
+ return true;
6166
7052
  }
6167
- function canonicalSchemaRef(ref, ctx) {
6168
- if (!ctx) return ref;
6169
- if (classifyRef(ref).kind !== "file") return ref;
6170
- const abs = resolve5(ctx.cwd, ref);
6171
- return relative3(ctx.base, abs).replace(/\\/g, "/");
7053
+ var sqliteModule;
7054
+ function loadSqlite() {
7055
+ sqliteModule ??= (async () => {
7056
+ const original = process.emitWarning.bind(process);
7057
+ const filtered = (warning, ...rest) => {
7058
+ const text = typeof warning === "string" ? warning : warning.message;
7059
+ if (text.includes("SQLite is an experimental feature")) return;
7060
+ Reflect.apply(original, process, [warning, ...rest]);
7061
+ };
7062
+ process.emitWarning = filtered;
7063
+ try {
7064
+ return await import("node:sqlite");
7065
+ } finally {
7066
+ process.emitWarning = original;
7067
+ }
7068
+ })();
7069
+ return sqliteModule;
6172
7070
  }
6173
- function fingerprint(e, ctx) {
6174
- const parts = [
6175
- canonicalSchemaRef(e.schema, ctx),
6176
- e.instancePath,
6177
- e.keyword,
6178
- e.subject ?? ""
6179
- ];
6180
- return createHash3("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
7071
+
7072
+ // src/meta/core/collections.ts
7073
+ function memberOf(collections, configDir, base, label) {
7074
+ if (collections.length === 0) return [];
7075
+ if (label === STDIN_LABEL) return [];
7076
+ const rel = relative6(configDir, resolvePath2(base, label)).split(sep3).join("/");
7077
+ if (isAbsolute3(rel)) return [];
7078
+ return collections.filter((c) => isMember(c, rel)).map((c) => c.name);
6181
7079
  }
6182
- function serializeBaseline(baseline) {
6183
- const entries = /* @__PURE__ */ Object.create(null);
6184
- for (const file of Object.keys(baseline.entries).sort()) {
6185
- entries[file] = [...baseline.entries[file] ?? []].sort();
6186
- }
6187
- return `${JSON.stringify(
6188
- {
6189
- version: baseline.version,
6190
- generatedWith: baseline.generatedWith,
6191
- entries
6192
- },
6193
- null,
6194
- 2
6195
- )}
6196
- `;
7080
+ function retainMembers(files, membersFor) {
7081
+ return files.filter((label) => membersFor(label).length > 0);
6197
7082
  }
6198
- function bad(source, detail) {
6199
- throw new DocmetaError(`Baseline "${source}": ${detail}`);
7083
+ function collectionNames(collections) {
7084
+ return (collections ?? []).map((c) => c.name);
6200
7085
  }
6201
- function parseBaseline(text, source) {
6202
- let raw;
6203
- try {
6204
- raw = JSON.parse(stripBom(text));
6205
- } catch (err) {
6206
- bad(source, `invalid JSON: ${err.message}`);
6207
- }
6208
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
6209
- bad(source, "top level must be an object.");
6210
- }
6211
- const obj = raw;
6212
- if (obj.version !== BASELINE_VERSION) {
6213
- bad(
6214
- source,
6215
- `unsupported version ${JSON.stringify(obj.version)} (this manni writes version ${BASELINE_VERSION}). Re-record it with \`manni meta validate --write-baseline\`.`
6216
- );
6217
- }
6218
- const generatedWith = typeof obj.generatedWith === "string" ? obj.generatedWith : "";
6219
- const rawEntries = obj.entries;
6220
- if (typeof rawEntries !== "object" || rawEntries === null || Array.isArray(rawEntries)) {
6221
- bad(source, '"entries" must be an object mapping file paths to fingerprints.');
6222
- }
6223
- const entries = /* @__PURE__ */ Object.create(null);
6224
- for (const [file, value] of Object.entries(
6225
- rawEntries
6226
- )) {
6227
- if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
6228
- bad(source, `entries["${file}"] must be a list of fingerprint strings.`);
6229
- }
6230
- const malformed = value.find((v) => !FINGERPRINT_RE.test(v));
6231
- if (malformed !== void 0) {
6232
- bad(
6233
- source,
6234
- `entries["${file}"] contains ${JSON.stringify(malformed)}, which is not a fingerprint (16 lowercase hex characters).`
6235
- );
7086
+ function collectCollections(entries, params) {
7087
+ const declared = params.collections ?? [];
7088
+ if (declared.length === 0) return [];
7089
+ const base = params.base ?? ".";
7090
+ const configDir = params.configDir ?? base;
7091
+ const members = /* @__PURE__ */ new Map();
7092
+ for (const entry of entries) {
7093
+ for (const name2 of memberOf(declared, configDir, base, entry.label)) {
7094
+ const list = members.get(name2);
7095
+ if (list) list.push(entry.label);
7096
+ else members.set(name2, [entry.label]);
6236
7097
  }
6237
- entries[file] = value;
6238
7098
  }
6239
- return { version: BASELINE_VERSION, generatedWith, entries };
7099
+ return declared.map((c) => ({ name: c.name, members: members.get(c.name) ?? [] }));
6240
7100
  }
6241
- async function readBaseline(absPath, source) {
6242
- let text;
6243
- try {
6244
- text = await readFile4(absPath, "utf8");
6245
- } catch (err) {
6246
- if (err.code === "ENOENT") return null;
6247
- throw new DocmetaError(
6248
- `Baseline "${source}" could not be read: ${err.message}`
6249
- );
6250
- }
6251
- return parseBaseline(text, source);
7101
+ function quoteSqlString(value) {
7102
+ return `'${value.replaceAll("'", "''")}'`;
6252
7103
  }
6253
- async function writeBaselineFile(absPath, baseline, source) {
6254
- try {
6255
- await writeFileAtomic(absPath, serializeBaseline(baseline));
6256
- } catch (err) {
6257
- throw new DocmetaError(
6258
- `Baseline "${source}" could not be written: ${err.message}`
7104
+ function createCollectionViews(db, collections) {
7105
+ for (const c of collections) {
7106
+ const where = c.members.length === 0 ? "0" : `_path IN (${c.members.map(quoteSqlString).join(", ")})`;
7107
+ db.exec(
7108
+ `CREATE VIEW ${quoteIdent(c.name)} AS SELECT * FROM docs WHERE ${where}`
6259
7109
  );
6260
7110
  }
6261
7111
  }
6262
- function buildBaseline(results, generatedWith, ctx) {
6263
- const entries = /* @__PURE__ */ Object.create(null);
6264
- for (const r of results) {
6265
- if (r.errors.length === 0) continue;
6266
- const prints = [...new Set(r.errors.map((e) => fingerprint(e, ctx)))].sort();
6267
- entries[canonicalFilePath(r.file, ctx)] = prints;
6268
- }
6269
- return { version: BASELINE_VERSION, generatedWith, entries };
6270
- }
6271
- function applyBaseline(results, baseline, ctx) {
6272
- let recorded = 0;
6273
- let suppressed = 0;
6274
- let stale = 0;
6275
- const applied = results.map((r) => {
6276
- const known = baseline.entries[canonicalFilePath(r.file, ctx)];
6277
- if (!known) return r;
6278
- const recordedHere = new Set(known);
6279
- recorded += recordedHere.size;
6280
- const seen = /* @__PURE__ */ new Set();
6281
- const fresh = [];
6282
- for (const e of r.errors) {
6283
- const print = fingerprint(e, ctx);
6284
- if (recordedHere.has(print)) {
6285
- seen.add(print);
6286
- suppressed += 1;
6287
- } else {
6288
- fresh.push(e);
6289
- }
6290
- }
6291
- stale += recordedHere.size - seen.size;
6292
- const baselined = r.errors.length - fresh.length;
6293
- return {
6294
- ...r,
6295
- ok: fresh.length === 0,
6296
- errors: fresh,
6297
- ...baselined > 0 ? { baselined } : {}
6298
- };
6299
- });
6300
- return { results: applied, recorded, suppressed, stale };
6301
- }
6302
- function diffBaselines(previous, next) {
6303
- const flatten = (b) => {
6304
- const out = /* @__PURE__ */ new Set();
6305
- if (!b) return out;
6306
- for (const [file, prints] of Object.entries(b.entries)) {
6307
- for (const p of prints) out.add(`${file}\0${p}`);
6308
- }
6309
- return out;
6310
- };
6311
- const before = flatten(previous);
6312
- const after = flatten(next);
6313
- let added = 0;
6314
- let removed = 0;
6315
- for (const k of after) if (!before.has(k)) added += 1;
6316
- for (const k of before) if (!after.has(k)) removed += 1;
6317
- return { added, removed };
6318
- }
6319
- function countFingerprints(baseline) {
6320
- let n = 0;
6321
- for (const prints of Object.values(baseline.entries)) n += prints.length;
6322
- return n;
6323
- }
6324
7112
 
6325
- // src/meta/core/load-files.ts
6326
- import { stat as stat3 } from "node:fs/promises";
6327
- import { extname, relative as relative4, resolve as resolve6 } from "node:path";
6328
- import fg from "fast-glob";
6329
- import picomatch2 from "picomatch";
6330
-
6331
- // src/meta/core/gitignore.ts
6332
- import { spawn } from "node:child_process";
6333
- var GITIGNORE_UNAVAILABLE = ".gitignore filtering was requested but git could not answer here (no repository, or no git on PATH); no files were skipped.";
6334
- var unavailable = () => ({
6335
- ignored: /* @__PURE__ */ new Set(),
6336
- available: false
6337
- });
6338
- function gitIgnored(paths, cwd) {
6339
- if (paths.length === 0) {
6340
- return Promise.resolve({ ignored: /* @__PURE__ */ new Set(), available: true });
7113
+ // src/meta/core/validator.ts
7114
+ import * as AjvDraft07Ns from "ajv";
7115
+ import * as Ajv2019Ns from "ajv/dist/2019.js";
7116
+ import * as Ajv2020Ns from "ajv/dist/2020.js";
7117
+ import * as AjvDraft04Ns from "ajv-draft-04";
7118
+ import * as addFormatsNs from "ajv-formats";
7119
+ import { createRequire } from "node:module";
7120
+ var draft06MetaSchema = createRequire(import.meta.url)(
7121
+ "ajv/dist/refs/json-schema-draft-06.json"
7122
+ );
7123
+ var AjvDraft07 = AjvDraft07Ns.default;
7124
+ var Ajv2019 = Ajv2019Ns.default;
7125
+ var Ajv2020 = Ajv2020Ns.default;
7126
+ var AjvDraft04 = AjvDraft04Ns.default;
7127
+ var addFormats = addFormatsNs.default;
7128
+ function dialectOf(schema) {
7129
+ const meta = typeof schema.$schema === "string" ? schema.$schema : "";
7130
+ if (meta.includes("2019-09")) return "2019";
7131
+ if (meta.includes("draft-07") || meta.includes("draft/7") || meta.includes("draft-06") || meta.includes("draft/6")) {
7132
+ return "draft7";
6341
7133
  }
6342
- return new Promise((settle) => {
6343
- let done = false;
6344
- const finish = (answer) => {
6345
- if (done) return;
6346
- done = true;
6347
- settle(answer);
6348
- };
6349
- let child;
6350
- try {
6351
- child = spawn("git", ["check-ignore", "--stdin", "-z"], {
6352
- cwd,
6353
- windowsHide: true
6354
- });
6355
- } catch {
6356
- finish(unavailable());
6357
- return;
6358
- }
6359
- let out = "";
6360
- child.stdout.setEncoding("utf8");
6361
- child.stdout.on("data", (chunk) => {
6362
- out += chunk;
6363
- });
6364
- child.stderr.resume();
6365
- child.on("error", () => {
6366
- finish(unavailable());
6367
- });
6368
- child.stdin.on("error", () => {
6369
- });
6370
- child.on("close", (code) => {
6371
- if (code !== 0 && code !== 1) {
6372
- finish(unavailable());
6373
- return;
6374
- }
6375
- finish({
6376
- ignored: new Set(out.split("\0").filter((p) => p !== "")),
6377
- available: true
6378
- });
6379
- });
6380
- child.stdin.end(paths.join("\0"));
6381
- });
7134
+ if (meta.includes("draft-04") || meta.includes("draft/4")) return "draft4";
7135
+ return "2020";
6382
7136
  }
6383
-
6384
- // src/meta/core/load-files.ts
6385
- var DEFAULT_IGNORE = ["**/node_modules/**", "**/.git/**"];
6386
- var STDIN_TOKEN = "-";
6387
- var STDIN_LABEL = "<stdin>";
6388
- function toPosix(p) {
6389
- return p.replace(/\\/g, "/");
7137
+ function buildAjv(dialect) {
7138
+ const opts = { allErrors: true, strict: false };
7139
+ const ajv = dialect === "2019" ? new Ajv2019(opts) : dialect === "draft7" ? new AjvDraft07(opts) : dialect === "draft4" ? new AjvDraft04(opts) : new Ajv2020(opts);
7140
+ addFormats(ajv);
7141
+ if (dialect === "draft7") ajv.addMetaSchema(draft06MetaSchema);
7142
+ registerBuiltins(ajv, dialect);
7143
+ return ajv;
6390
7144
  }
6391
- async function statOrNull(p) {
6392
- try {
6393
- return await stat3(p);
6394
- } catch {
6395
- return null;
7145
+ function registerBuiltins(ajv, dialect) {
7146
+ for (const { id, url, legacyUrl, schema } of publishedBuiltins()) {
7147
+ ajv.addSchema(schema, url, void 0, false);
7148
+ ajv.addSchema(schema, legacyUrl, void 0, false);
7149
+ if (dialect === "draft4") ajv.addSchema(schema, id, void 0, false);
6396
7150
  }
6397
7151
  }
6398
- function gitignoreOptions(opts) {
6399
- const asked = opts.flag === true || opts.flag === void 0 && opts.configured === true;
6400
- const notice = opts.onNotice;
6401
- return {
6402
- respectGitignore: opts.flag ?? opts.configured ?? true,
6403
- ...asked && notice ? {
6404
- onGitignoreUnavailable: () => {
6405
- notice(GITIGNORE_UNAVAILABLE);
6406
- }
6407
- } : {}
6408
- };
7152
+ function compileWithFormats(schema) {
7153
+ return buildAjv("2020").compile(schema);
6409
7154
  }
6410
- async function resolveTargetSet(opts) {
6411
- const cwd = opts.cwd ?? process.cwd();
6412
- const exts = (opts.exts ?? supportedExtensions()).map(
6413
- (e) => e.toLowerCase().startsWith(".") ? e.toLowerCase() : `.${e.toLowerCase()}`
6414
- );
6415
- const ignore = [...DEFAULT_IGNORE, ...opts.exclude ?? []];
6416
- const named = /* @__PURE__ */ new Set();
6417
- const walked = /* @__PURE__ */ new Set();
6418
- const keepByExt = (file) => exts.includes(extname(file).toLowerCase());
6419
- const missing = [];
6420
- for (const input of opts.inputs) {
6421
- if (input === STDIN_TOKEN) continue;
6422
- const posixInput = toPosix(input);
6423
- const abs = resolve6(cwd, posixInput);
6424
- const st = await statOrNull(abs);
6425
- if (st?.isFile()) {
6426
- named.add(toPosix(relative4(cwd, abs)));
6427
- continue;
6428
- }
6429
- if (st?.isDirectory()) {
6430
- const found2 = await fg(`${posixInput}/**/*`, {
6431
- cwd,
6432
- ignore,
6433
- onlyFiles: true,
6434
- dot: false
6435
- });
6436
- for (const f of found2) if (keepByExt(f)) walked.add(f);
6437
- continue;
6438
- }
6439
- if (!picomatch2.scan(posixInput).isGlob) {
6440
- missing.push(posixInput);
6441
- continue;
6442
- }
6443
- const found = await fg(posixInput, {
6444
- cwd,
6445
- ignore,
6446
- onlyFiles: true,
6447
- dot: false
6448
- });
6449
- for (const f of found) if (keepByExt(f)) walked.add(f);
6450
- }
6451
- if (missing.length > 0 && !opts.allowEmpty) {
6452
- const names = missing.map((m) => `"${m}"`).join(", ");
6453
- throw new DocmetaError(
6454
- missing.length === 1 ? `File not found: ${names}.` : `Files not found: ${names}.`
6455
- );
7155
+ var formatRegistry;
7156
+ function formatsAjv() {
7157
+ if (!formatRegistry) {
7158
+ formatRegistry = new Ajv2020({ strict: false });
7159
+ addFormats(formatRegistry);
6456
7160
  }
6457
- let gitignoreSkipped = 0;
6458
- if (opts.respectGitignore !== false && walked.size > 0) {
6459
- const candidates = [...walked].filter((f) => !named.has(f));
6460
- const { ignored, available } = await gitIgnored(candidates, cwd);
6461
- if (available) {
6462
- for (const f of ignored) {
6463
- if (walked.delete(f)) gitignoreSkipped += 1;
6464
- }
6465
- } else {
6466
- opts.onGitignoreUnavailable?.();
7161
+ return formatRegistry;
7162
+ }
7163
+ var stringFormatsMemo;
7164
+ function stringFormatNames() {
7165
+ if (!stringFormatsMemo) {
7166
+ const names = /* @__PURE__ */ new Set();
7167
+ const registered = formatsAjv().formats;
7168
+ for (const [name2, def] of Object.entries(registered)) {
7169
+ const enforcesStrings = def instanceof RegExp || typeof def === "function" || typeof def === "object" && def !== null && (def.type === void 0 || def.type === "string");
7170
+ if (enforcesStrings) names.add(name2);
6467
7171
  }
7172
+ stringFormatsMemo = names;
6468
7173
  }
6469
- return {
6470
- files: [.../* @__PURE__ */ new Set([...named, ...walked])].sort(),
6471
- gitignoreSkipped
6472
- };
7174
+ return stringFormatsMemo;
6473
7175
  }
6474
- function assertNonEmpty(p) {
6475
- if (p.allowEmpty || p.usingStdin || p.files.length > 0) return;
6476
- const tried = p.inputs.map((i) => `"${i}"`).join(", ");
6477
- const notes = [];
6478
- if (p.exts && p.exts.length > 0) {
6479
- notes.push(`extensions: ${p.exts.join(", ")}`);
7176
+ var formatCheckers = /* @__PURE__ */ new Map();
7177
+ function validatesFormat(name2, value) {
7178
+ let fn = formatCheckers.get(name2);
7179
+ if (!fn) {
7180
+ fn = formatsAjv().compile({ type: "string", format: name2 });
7181
+ formatCheckers.set(name2, fn);
6480
7182
  }
6481
- if (p.exclude && p.exclude.length > 0) {
6482
- notes.push(`excludes: ${p.exclude.map((e) => `"${e}"`).join(", ")}`);
7183
+ return fn(value);
7184
+ }
7185
+ var Validator = class {
7186
+ /**
7187
+ * How this validator's schemas are loaded: the disk cache location, its TTL,
7188
+ * and `--offline`. Held per instance rather than read from module state, so
7189
+ * two differently-configured validations in one process each get their own
7190
+ * settings.
7191
+ *
7192
+ * That is not full isolation, and the difference matters to a library
7193
+ * caller: `schema-registry` keeps a process-wide memo of fetched schemas, so
7194
+ * one validator's successful fetch is visible to another. `offline` is
7195
+ * excluded from that sharing on purpose — an offline validator will not be
7196
+ * served something this process pulled over the network — but the memo is
7197
+ * still shared, so a URL fetched once is not re-fetched per instance.
7198
+ */
7199
+ constructor(schemaOptions = {}) {
7200
+ this.schemaOptions = schemaOptions;
6483
7201
  }
6484
- if (p.gitignoreSkipped && p.gitignoreSkipped > 0) {
6485
- notes.push(
6486
- `.gitignore skipped ${p.gitignoreSkipped} (pass --no-gitignore to check them)`
6487
- );
7202
+ schemaOptions;
7203
+ ajvByDialect = /* @__PURE__ */ new Map();
7204
+ /**
7205
+ * Keyed on the in-flight *promise*, not the resolved validator. Caching the
7206
+ * result made this a check-then-act race: `fill` walks files through a worker
7207
+ * pool, so every worker missed the cache while the first `loadSchema` was
7208
+ * still pending and they all then compiled the same schema into the one
7209
+ * shared per-dialect Ajv. Ajv registers a schema's `$id` on the first compile
7210
+ * and rejects the second with "schema with key or id ... already exists",
7211
+ * which took down any multi-file run against an $id-bearing schema. Storing
7212
+ * the promise before the first await lets the losers await the one compile.
7213
+ */
7214
+ cache = /* @__PURE__ */ new Map();
7215
+ ajvFor(dialect) {
7216
+ let ajv = this.ajvByDialect.get(dialect);
7217
+ if (!ajv) {
7218
+ ajv = buildAjv(dialect);
7219
+ this.ajvByDialect.set(dialect, ajv);
7220
+ }
7221
+ return ajv;
6488
7222
  }
6489
- throw new DocmetaError(
6490
- [
6491
- `No files matched. Patterns tried: ${tried}.`,
6492
- ...notes.length > 0 ? [`Filters applied \u2014 ${notes.join("; ")}.`] : [],
6493
- `Nothing was ${p.action}, so this is an error rather than a pass.`,
6494
- `Pass --allow-empty (or set allowEmpty: true) if matching nothing is expected.`
6495
- ].join("\n")
6496
- );
6497
- }
6498
-
6499
- // src/meta/core/projection.ts
6500
- var SYSTEM_COLUMNS = ["_path", "_format", "_present", "_data"];
6501
- var RESERVED = new Set(SYSTEM_COLUMNS);
6502
- function quoteIdent(name2) {
6503
- return `"${name2.replaceAll('"', '""')}"`;
6504
- }
6505
- function bindValue(value) {
6506
- if (value === void 0 || value === null) return null;
6507
- switch (typeof value) {
6508
- case "boolean":
6509
- return value ? 1 : 0;
6510
- case "number":
6511
- return Number.isFinite(value) ? value : null;
6512
- case "bigint":
6513
- case "string":
6514
- return value;
6515
- default:
6516
- return JSON.stringify(value);
7223
+ /**
7224
+ * Synchronous by design: the `cache.set` has to happen in the same tick as
7225
+ * the miss, or a second caller can slip in before the entry exists.
7226
+ */
7227
+ compile(ref) {
7228
+ const cached = this.cache.get(ref);
7229
+ if (cached) return cached;
7230
+ const pending = this.compileUncached(ref);
7231
+ this.cache.set(ref, pending);
7232
+ pending.catch(() => {
7233
+ if (this.cache.get(ref) === pending) this.cache.delete(ref);
7234
+ });
7235
+ return pending;
6517
7236
  }
6518
- }
6519
- function corpusDataColumns(entries, extraKeys = []) {
6520
- const keys = /* @__PURE__ */ new Set();
6521
- for (const { extracted } of entries) {
6522
- for (const key of Object.keys(extracted.data)) {
6523
- if (key !== "" && !RESERVED.has(key)) keys.add(key);
7237
+ async compileUncached(ref) {
7238
+ const schema = await loadSchema(ref, this.schemaOptions);
7239
+ try {
7240
+ const ajv = this.ajvFor(dialectOf(schema));
7241
+ const id = typeof schema["$id"] === "string" ? schema["$id"] : void 0;
7242
+ const registered = id != null ? ajv.getSchema(id) : void 0;
7243
+ if (registered) {
7244
+ if (registered.schema === schema) return registered;
7245
+ const { $id: _taken, ...anonymous } = schema;
7246
+ return ajv.compile(anonymous);
7247
+ }
7248
+ return ajv.compile(schema);
7249
+ } catch (err) {
7250
+ throw new DocmetaError(
7251
+ `Schema "${ref}" failed to compile: ${err.message}`
7252
+ );
7253
+ }
7254
+ }
7255
+ /**
7256
+ * Validate `data` against every schema in `refs`. Returns all violations,
7257
+ * each tagged with the schema that produced it and a source line via
7258
+ * `lineFor`.
7259
+ *
7260
+ * `colFor` is optional and additive: this signature is public, so a fourth
7261
+ * *required* parameter — or a widened third one — would be a consumer break.
7262
+ * Callers with an extractor that supplies no column pass nothing and get the
7263
+ * previous behavior exactly.
7264
+ */
7265
+ async validate(data, refs, lineFor, colFor, locate) {
7266
+ const { [FILE_SCHEMA_KEY]: _omit, ...subject } = data;
7267
+ void _omit;
7268
+ const errors = [];
7269
+ for (const ref of refs) {
7270
+ const fn = await this.compile(ref);
7271
+ const ok = fn(subject);
7272
+ if (ok) continue;
7273
+ for (const e of fn.errors ?? []) {
7274
+ errors.push(toFieldError(ref, e, lineFor, colFor, locate));
7275
+ }
6524
7276
  }
7277
+ return errors;
6525
7278
  }
6526
- for (const key of extraKeys) {
6527
- if (key !== "" && !RESERVED.has(key)) keys.add(key);
7279
+ };
7280
+ function subjectOf(e) {
7281
+ switch (e.keyword) {
7282
+ case "required":
7283
+ return e.params.missingProperty;
7284
+ case "additionalProperties":
7285
+ return e.params.additionalProperty;
7286
+ case "format":
7287
+ return e.params.format;
7288
+ case "type":
7289
+ return e.params.type;
7290
+ default:
7291
+ return void 0;
6528
7292
  }
6529
- return [...keys].sort();
6530
7293
  }
6531
- function createDocsTable(db, entries, dataColumns) {
6532
- db.exec(
6533
- `CREATE TABLE docs ("_path" TEXT PRIMARY KEY, "_format" TEXT, "_present" INTEGER, "_data" TEXT${dataColumns.map((c) => `, ${quoteIdent(c)}`).join("")})`
6534
- );
6535
- const insert = db.prepare(
6536
- `INSERT INTO docs VALUES (${["?", "?", "?", "?", ...dataColumns.map(() => "?")].join(", ")})`
6537
- );
6538
- db.exec("BEGIN");
6539
- for (const { label, extracted } of entries) {
6540
- insert.run(
6541
- label,
6542
- extracted.format,
6543
- extracted.present ? 1 : 0,
6544
- JSON.stringify(extracted.data),
6545
- ...dataColumns.map((c) => bindValue(extracted.data[c]))
6546
- );
7294
+ function toFieldError(schema, e, lineFor, colFor, locate) {
7295
+ const defined = e;
7296
+ const instancePath = e.instancePath;
7297
+ const subject = subjectOf(defined);
7298
+ let message = e.message ?? "is invalid";
7299
+ let wantsColumn = true;
7300
+ if (defined.keyword === "required") {
7301
+ message = `must have required property '${defined.params.missingProperty}'`;
7302
+ wantsColumn = false;
7303
+ } else if (defined.keyword === "additionalProperties") {
7304
+ message = `must NOT have additional property '${defined.params.additionalProperty}'`;
7305
+ wantsColumn = false;
6547
7306
  }
6548
- db.exec("COMMIT");
6549
- }
6550
- function registerLineFor(db, entries) {
6551
- const byLabel = new Map(entries.map((e) => [e.label, e.extracted]));
6552
- db.function("lineFor", { deterministic: true }, (path, key) => {
6553
- if (typeof path !== "string" || typeof key !== "string") return null;
6554
- return byLabel.get(path)?.lineFor(key) ?? null;
6555
- });
6556
- }
6557
- function assertSingleStatement(sql) {
6558
- const cut = topLevelSemicolon(sql);
6559
- if (cut === -1) return;
6560
- if (!isTrivia(sql.slice(cut + 1))) {
6561
- throw new DocmetaError(
6562
- "Run a single SQL statement per query; text after the first `;` would be silently ignored."
6563
- );
7307
+ const located = locate?.(instancePath) ?? (defined.keyword === "additionalProperties" ? locate?.(
7308
+ `${instancePath}/${escapePointerSegment(defined.params.additionalProperty)}`
7309
+ ) : void 0);
7310
+ if (located) {
7311
+ return {
7312
+ schema,
7313
+ instancePath,
7314
+ message,
7315
+ keyword: e.keyword,
7316
+ ...subject != null ? { subject } : {},
7317
+ file: located.file,
7318
+ ...located.line != null ? { line: located.line } : {},
7319
+ ...wantsColumn && located.col != null ? { col: located.col } : {}
7320
+ };
6564
7321
  }
7322
+ const line = lineFor(instancePath);
7323
+ const col = wantsColumn ? colFor?.(instancePath) : void 0;
7324
+ return {
7325
+ schema,
7326
+ instancePath,
7327
+ message,
7328
+ keyword: e.keyword,
7329
+ ...subject != null ? { subject } : {},
7330
+ ...line != null ? { line } : {},
7331
+ ...col != null ? { col } : {}
7332
+ };
6565
7333
  }
6566
- function startsStringOrIdent(sql, i) {
6567
- const ch = sql[i];
6568
- return ch === "'" || ch === '"' || ch === "`" || ch === "[";
6569
- }
6570
- function startsComment(sql, i) {
6571
- const ch = sql[i];
6572
- return ch === "-" && sql[i + 1] === "-" || ch === "/" && sql[i + 1] === "*";
7334
+
7335
+ // src/meta/commands/validate.ts
7336
+ import { existsSync as existsSync2 } from "node:fs";
7337
+ import { readFile as readFile6 } from "node:fs/promises";
7338
+ import { resolve as resolve9, extname as extname2 } from "node:path";
7339
+
7340
+ // src/meta/core/baseline.ts
7341
+ import { createHash as createHash3 } from "node:crypto";
7342
+ import { readFile as readFile5 } from "node:fs/promises";
7343
+ import { relative as relative7, resolve as resolve8 } from "node:path";
7344
+ var DEFAULT_BASELINE_PATH = ".manni-baseline.json";
7345
+ var LEGACY_BASELINE_PATH = ".docmeta-baseline.json";
7346
+ var BASELINE_VERSION = 1;
7347
+ var FINGERPRINT_RE = /^[0-9a-f]{16}$/;
7348
+ function canonicalFilePath(file, ctx) {
7349
+ if (!ctx) return file;
7350
+ const from = ctx.runBase ?? ctx.base;
7351
+ return relative7(ctx.base, resolve8(from, file)).replace(/\\/g, "/");
6573
7352
  }
6574
- function skipStringOrIdent(sql, i) {
6575
- const quote = sql[i];
6576
- if (quote === "[") {
6577
- let j2 = i + 1;
6578
- while (j2 < sql.length && sql[j2] !== "]") j2++;
6579
- return j2 + 1;
6580
- }
6581
- let j = i + 1;
6582
- while (j < sql.length) {
6583
- if (sql[j] === quote) {
6584
- if (sql[j + 1] === quote) {
6585
- j += 2;
6586
- continue;
6587
- }
6588
- return j + 1;
6589
- }
6590
- j++;
6591
- }
6592
- return j;
7353
+ function canonicalSchemaRef(ref, ctx) {
7354
+ if (!ctx) return ref;
7355
+ if (classifyRef(ref).kind !== "file") return ref;
7356
+ const abs = resolve8(ctx.cwd, ref);
7357
+ return relative7(ctx.base, abs).replace(/\\/g, "/");
6593
7358
  }
6594
- function skipComment(sql, i) {
6595
- if (sql[i] === "-") {
6596
- let j = i + 2;
6597
- while (j < sql.length && sql[j] !== "\n") j++;
6598
- return j < sql.length ? j + 1 : j;
6599
- }
6600
- const end = sql.indexOf("*/", i + 2);
6601
- return end === -1 ? sql.length : end + 2;
7359
+ function fingerprint(e, ctx) {
7360
+ const parts = [
7361
+ canonicalSchemaRef(e.schema, ctx),
7362
+ e.instancePath,
7363
+ e.keyword,
7364
+ e.subject ?? ""
7365
+ ];
7366
+ return createHash3("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
6602
7367
  }
6603
- function topLevelSemicolon(sql) {
6604
- let i = 0;
6605
- while (i < sql.length) {
6606
- if (startsStringOrIdent(sql, i)) {
6607
- i = skipStringOrIdent(sql, i);
6608
- } else if (startsComment(sql, i)) {
6609
- i = skipComment(sql, i);
6610
- } else if (sql[i] === ";") {
6611
- return i;
6612
- } else {
6613
- i++;
6614
- }
7368
+ function serializeBaseline(baseline) {
7369
+ const entries = /* @__PURE__ */ Object.create(null);
7370
+ for (const file of Object.keys(baseline.entries).sort()) {
7371
+ entries[file] = [...baseline.entries[file] ?? []].sort();
6615
7372
  }
6616
- return -1;
7373
+ return `${JSON.stringify(
7374
+ {
7375
+ version: baseline.version,
7376
+ generatedWith: baseline.generatedWith,
7377
+ entries
7378
+ },
7379
+ null,
7380
+ 2
7381
+ )}
7382
+ `;
6617
7383
  }
6618
- var PARAM_NAME_HEAD = /^[A-Za-z_][A-Za-z0-9_]*/;
6619
- function isParamName(name2) {
6620
- return PARAM_NAME_HEAD.exec(name2)?.[0] === name2;
7384
+ function bad(source, detail) {
7385
+ throw new DocmetaError(`Baseline "${source}": ${detail}`);
6621
7386
  }
6622
- function collectNamedParameters(sql) {
6623
- const tokens = [];
6624
- const seen = /* @__PURE__ */ new Set();
6625
- const prefixOf = /* @__PURE__ */ new Map();
6626
- let i = 0;
6627
- while (i < sql.length) {
6628
- const ch = sql[i];
6629
- if (startsStringOrIdent(sql, i)) {
6630
- i = skipStringOrIdent(sql, i);
6631
- } else if (startsComment(sql, i)) {
6632
- i = skipComment(sql, i);
6633
- } else if (ch === "$" || ch === ":" || ch === "@") {
6634
- if (ch === ":" && sql[i + 1] === ":") {
6635
- i += 2;
6636
- continue;
6637
- }
6638
- const m = PARAM_NAME_HEAD.exec(sql.slice(i + 1));
6639
- if (!m) {
6640
- i++;
6641
- continue;
6642
- }
6643
- const token = ch + m[0];
6644
- const priorPrefix = prefixOf.get(m[0]);
6645
- if (priorPrefix !== void 0 && priorPrefix !== ch) {
6646
- throw new DocmetaError(
6647
- `The statement references both ${priorPrefix}${m[0]} and ${token} \u2014 one name under two prefixes, which the engine rejects as conflicting. Use one spelling throughout.`
6648
- );
6649
- }
6650
- prefixOf.set(m[0], ch);
6651
- if (!seen.has(token)) {
6652
- seen.add(token);
6653
- tokens.push(token);
6654
- }
6655
- i += 1 + m[0].length;
6656
- } else {
6657
- i++;
6658
- }
7387
+ function parseBaseline(text, source) {
7388
+ let raw;
7389
+ try {
7390
+ raw = JSON.parse(stripBom(text));
7391
+ } catch (err) {
7392
+ bad(source, `invalid JSON: ${err.message}`);
6659
7393
  }
6660
- return tokens;
6661
- }
6662
- function stripLeadingTrivia(sql) {
6663
- let i = 0;
6664
- for (; ; ) {
6665
- while (i < sql.length && /\s/.test(sql[i] ?? "")) i++;
6666
- if (startsComment(sql, i)) {
6667
- i = skipComment(sql, i);
6668
- continue;
6669
- }
6670
- return sql.slice(i);
7394
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
7395
+ bad(source, "top level must be an object.");
6671
7396
  }
6672
- }
6673
- function isTrivia(text) {
6674
- let i = 0;
6675
- while (i < text.length) {
6676
- const ch = text[i];
6677
- if (ch === void 0 || /\s/.test(ch) || ch === ";") {
6678
- i++;
6679
- continue;
7397
+ const obj = raw;
7398
+ if (obj.version !== BASELINE_VERSION) {
7399
+ bad(
7400
+ source,
7401
+ `unsupported version ${JSON.stringify(obj.version)} (this manni writes version ${BASELINE_VERSION}). Re-record it with \`manni meta validate --write-baseline\`.`
7402
+ );
7403
+ }
7404
+ const generatedWith = typeof obj.generatedWith === "string" ? obj.generatedWith : "";
7405
+ const rawEntries = obj.entries;
7406
+ if (typeof rawEntries !== "object" || rawEntries === null || Array.isArray(rawEntries)) {
7407
+ bad(source, '"entries" must be an object mapping file paths to fingerprints.');
7408
+ }
7409
+ const entries = /* @__PURE__ */ Object.create(null);
7410
+ for (const [file, value] of Object.entries(
7411
+ rawEntries
7412
+ )) {
7413
+ if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
7414
+ bad(source, `entries["${file}"] must be a list of fingerprint strings.`);
6680
7415
  }
6681
- if (startsComment(text, i)) {
6682
- i = skipComment(text, i);
6683
- continue;
7416
+ const malformed = value.find((v) => !FINGERPRINT_RE.test(v));
7417
+ if (malformed !== void 0) {
7418
+ bad(
7419
+ source,
7420
+ `entries["${file}"] contains ${JSON.stringify(malformed)}, which is not a fingerprint (16 lowercase hex characters).`
7421
+ );
6684
7422
  }
6685
- return false;
7423
+ entries[file] = value;
6686
7424
  }
6687
- return true;
7425
+ return { version: BASELINE_VERSION, generatedWith, entries };
6688
7426
  }
6689
- var sqliteModule;
6690
- function loadSqlite() {
6691
- sqliteModule ??= (async () => {
6692
- const original = process.emitWarning.bind(process);
6693
- const filtered = (warning, ...rest) => {
6694
- const text = typeof warning === "string" ? warning : warning.message;
6695
- if (text.includes("SQLite is an experimental feature")) return;
6696
- Reflect.apply(original, process, [warning, ...rest]);
6697
- };
6698
- process.emitWarning = filtered;
6699
- try {
6700
- return await import("node:sqlite");
6701
- } finally {
6702
- process.emitWarning = original;
6703
- }
6704
- })();
6705
- return sqliteModule;
7427
+ async function readBaseline(absPath, source) {
7428
+ let text;
7429
+ try {
7430
+ text = await readFile5(absPath, "utf8");
7431
+ } catch (err) {
7432
+ if (err.code === "ENOENT") return null;
7433
+ throw new DocmetaError(
7434
+ `Baseline "${source}" could not be read: ${err.message}`
7435
+ );
7436
+ }
7437
+ return parseBaseline(text, source);
6706
7438
  }
6707
-
6708
- // src/meta/core/collections.ts
6709
- function namedOverrides(config) {
6710
- return (config?.overrides ?? []).flatMap(
6711
- (o, i) => o.name !== void 0 ? [{ name: o.name, files: o.files, index: i }] : []
6712
- );
7439
+ async function writeBaselineFile(absPath, baseline, source) {
7440
+ try {
7441
+ await writeFileAtomic(absPath, serializeBaseline(baseline));
7442
+ } catch (err) {
7443
+ throw new DocmetaError(
7444
+ `Baseline "${source}" could not be written: ${err.message}`
7445
+ );
7446
+ }
6713
7447
  }
6714
- function collectionNames(config) {
6715
- return namedOverrides(config).map((n) => n.name);
7448
+ function buildBaseline(results, generatedWith, ctx) {
7449
+ const entries = /* @__PURE__ */ Object.create(null);
7450
+ for (const r of results) {
7451
+ if (r.errors.length === 0) continue;
7452
+ const prints = [...new Set(r.errors.map((e) => fingerprint(e, ctx)))].sort();
7453
+ entries[canonicalFilePath(r.file, ctx)] = prints;
7454
+ }
7455
+ return { version: BASELINE_VERSION, generatedWith, entries };
6716
7456
  }
6717
- function collectCollections(entries, params) {
6718
- const named = namedOverrides(params.config);
6719
- if (named.length === 0) return [];
6720
- const members = /* @__PURE__ */ new Map();
6721
- for (const entry of entries) {
6722
- let resolved;
6723
- if (params.resolved) {
6724
- const pre = params.resolved.get(entry.label);
6725
- if (!pre) continue;
6726
- resolved = pre;
6727
- } else {
6728
- try {
6729
- resolved = resolveSchemaSetWithSource({
6730
- filePath: entry.label,
6731
- fileSchema: entry.extracted.data[FILE_SCHEMA_KEY],
6732
- ...params.cliSchemas ? { cliSchemas: params.cliSchemas } : {},
6733
- config: params.config,
6734
- ...params.fileBase !== void 0 ? { fileBase: params.fileBase } : {},
6735
- ...params.trustRoot ? { trustRoot: params.trustRoot } : {}
6736
- });
6737
- } catch {
6738
- continue;
6739
- }
6740
- }
6741
- if (resolved.source === "override" && resolved.overrideIndex !== void 0) {
6742
- const list = members.get(resolved.overrideIndex);
6743
- if (list) list.push(entry.label);
6744
- else members.set(resolved.overrideIndex, [entry.label]);
6745
- continue;
6746
- }
6747
- if (resolved.source === "document") {
6748
- const excluded = named.find((g) => matchesFileGlob(g.files, entry.label));
6749
- if (excluded) {
6750
- params.onNotice?.(
6751
- `${entry.label}: not in the "${excluded.name}" collection \u2014 its own "${FILE_SCHEMA_KEY}" won schema resolution over the override (${overrideGlobs(excluded.files).join(", ")}). Membership follows the schema a file is validated as; set schemaTrust.documentRefs to "none" to let the override decide.`
6752
- );
7457
+ function applyBaseline(results, baseline, ctx) {
7458
+ let recorded = 0;
7459
+ let suppressed = 0;
7460
+ let stale = 0;
7461
+ const applied = results.map((r) => {
7462
+ const known = baseline.entries[canonicalFilePath(r.file, ctx)];
7463
+ if (!known) return r;
7464
+ const recordedHere = new Set(known);
7465
+ recorded += recordedHere.size;
7466
+ const seen = /* @__PURE__ */ new Set();
7467
+ const fresh = [];
7468
+ for (const e of r.errors) {
7469
+ const print = fingerprint(e, ctx);
7470
+ if (recordedHere.has(print)) {
7471
+ seen.add(print);
7472
+ suppressed += 1;
7473
+ } else {
7474
+ fresh.push(e);
6753
7475
  }
6754
7476
  }
6755
- }
6756
- return named.map(({ name: name2, index }) => ({
6757
- name: name2,
6758
- members: members.get(index) ?? []
6759
- }));
7477
+ stale += recordedHere.size - seen.size;
7478
+ const baselined = r.errors.length - fresh.length;
7479
+ return {
7480
+ ...r,
7481
+ ok: fresh.length === 0,
7482
+ errors: fresh,
7483
+ ...baselined > 0 ? { baselined } : {}
7484
+ };
7485
+ });
7486
+ return { results: applied, recorded, suppressed, stale };
6760
7487
  }
6761
- function quoteSqlString(value) {
6762
- return `'${value.replaceAll("'", "''")}'`;
7488
+ function diffBaselines(previous, next) {
7489
+ const flatten = (b) => {
7490
+ const out = /* @__PURE__ */ new Set();
7491
+ if (!b) return out;
7492
+ for (const [file, prints] of Object.entries(b.entries)) {
7493
+ for (const p of prints) out.add(`${file}\0${p}`);
7494
+ }
7495
+ return out;
7496
+ };
7497
+ const before = flatten(previous);
7498
+ const after = flatten(next);
7499
+ let added = 0;
7500
+ let removed = 0;
7501
+ for (const k of after) if (!before.has(k)) added += 1;
7502
+ for (const k of before) if (!after.has(k)) removed += 1;
7503
+ return { added, removed };
6763
7504
  }
6764
- function createCollectionViews(db, collections) {
6765
- for (const c of collections) {
6766
- const where = c.members.length === 0 ? "0" : `_path IN (${c.members.map(quoteSqlString).join(", ")})`;
6767
- db.exec(
6768
- `CREATE VIEW ${quoteIdent(c.name)} AS SELECT * FROM docs WHERE ${where}`
6769
- );
6770
- }
7505
+ function countFingerprints(baseline) {
7506
+ let n = 0;
7507
+ for (const prints of Object.values(baseline.entries)) n += prints.length;
7508
+ return n;
6771
7509
  }
6772
7510
 
6773
7511
  // src/meta/core/checks.ts
@@ -6900,16 +7638,16 @@ function parseErrorResult(file, format, message, keyword) {
6900
7638
  }
6901
7639
  function resolveBaselineRequest(opts, configured, configDir, cwd) {
6902
7640
  const named = (label) => ({
6903
- absPath: resolve7(cwd, label),
7641
+ absPath: resolve9(cwd, label),
6904
7642
  label
6905
7643
  });
6906
7644
  const implied = () => {
6907
7645
  const base = configDir ?? cwd;
6908
7646
  if (configured !== void 0) {
6909
- return { absPath: resolve7(base, configured), label: configured };
7647
+ return { absPath: resolve9(base, configured), label: configured };
6910
7648
  }
6911
- const current = resolve7(base, DEFAULT_BASELINE_PATH);
6912
- const legacy = resolve7(base, LEGACY_BASELINE_PATH);
7649
+ const current = resolve9(base, DEFAULT_BASELINE_PATH);
7650
+ const legacy = resolve9(base, LEGACY_BASELINE_PATH);
6913
7651
  if (!existsSync2(current) && existsSync2(legacy)) {
6914
7652
  warn(
6915
7653
  `"${LEGACY_BASELINE_PATH}" is the pre-rename baseline file name. Rename it to "${DEFAULT_BASELINE_PATH}".`
@@ -6931,17 +7669,27 @@ function resolveBaselineRequest(opts, configured, configDir, cwd) {
6931
7669
  }
6932
7670
  async function runValidate(opts) {
6933
7671
  const cwd = opts.cwd ?? process.cwd();
6934
- const { config, inputs, base, configDir } = await resolveRunConfig({
7672
+ const {
7673
+ config,
7674
+ inputs,
7675
+ base,
7676
+ configDir,
7677
+ collections,
7678
+ declaredCollections,
7679
+ fromCollections
7680
+ } = await resolveRunConfig({
6935
7681
  cwd,
6936
7682
  configPath: opts.configPath,
6937
7683
  noConfig: opts.noConfig,
6938
7684
  inputs: opts.inputs,
7685
+ ...opts.collections !== void 0 ? { collections: opts.collections } : {},
6939
7686
  onConfigLoaded: opts.onConfigLoaded
6940
7687
  });
7688
+ const membersFor = (label) => memberOf(collections, configDir ?? cwd, base, label);
6941
7689
  const usingStdin = inputs.includes(STDIN_TOKEN);
6942
7690
  if (inputs.length === 0) {
6943
7691
  throw new DocmetaError(
6944
- "No files to validate. Pass paths/globs, or add `paths:` under `meta:` in manni.config.yaml."
7692
+ "No files to validate. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml."
6945
7693
  );
6946
7694
  }
6947
7695
  const forcedExtractor = opts.as ? extractorByName(opts.as) : void 0;
@@ -6953,8 +7701,8 @@ async function runValidate(opts) {
6953
7701
  const exts = opts.exts ?? (forcedExtractor ? forcedExtractor.extensions : void 0);
6954
7702
  const fileInputs = inputs.filter((i) => i !== STDIN_TOKEN);
6955
7703
  const allowEmpty = opts.allowEmpty ?? config?.allowEmpty;
6956
- const exclude = [...config?.exclude ?? [], ...opts.exclude ?? []];
6957
- const { files, gitignoreSkipped } = await resolveTargetSet({
7704
+ const exclude = opts.exclude ?? [];
7705
+ const { files: walked, gitignoreSkipped } = await resolveTargetSet({
6958
7706
  inputs: fileInputs,
6959
7707
  exts,
6960
7708
  exclude,
@@ -6966,6 +7714,7 @@ async function runValidate(opts) {
6966
7714
  onNotice: opts.onNotice
6967
7715
  })
6968
7716
  });
7717
+ const files = fromCollections ? retainMembers(walked, membersFor) : walked;
6969
7718
  assertNonEmpty({
6970
7719
  files,
6971
7720
  inputs: fileInputs,
@@ -6977,6 +7726,11 @@ async function runValidate(opts) {
6977
7726
  action: "validated"
6978
7727
  });
6979
7728
  const trustRoot = schemaTrustRoot(cwd, configDir);
7729
+ const externalMetadata = await loadExternalMetadata(collections, {
7730
+ configDir: configDir ?? cwd,
7731
+ base,
7732
+ offline: opts.offline ?? config?.offline ?? false
7733
+ });
6980
7734
  const validator = new Validator(
6981
7735
  schemaLoadOptions({
6982
7736
  // The config's directory when a config governs the run, so one project
@@ -6992,12 +7746,19 @@ async function runValidate(opts) {
6992
7746
  })
6993
7747
  );
6994
7748
  const results = [];
6995
- const scoped = opts.inputs.length > 0 || // redundant with inputs.length (stdin is an input) — kept as belt-and-suspenders
7749
+ const joinHits = /* @__PURE__ */ new Map();
7750
+ const scopedToCollections = (opts.collections?.length ?? 0) > 0;
7751
+ const scopedByFlags = opts.inputs.length > 0 || // redundant with inputs.length (stdin is an input) — kept as belt-and-suspenders
6996
7752
  usingStdin || opts.as !== void 0 || opts.exts !== void 0 || opts.exclude !== void 0 && opts.exclude.length > 0 || opts.respectGitignore !== void 0 || (opts.cliSchemas?.length ?? 0) > 0;
7753
+ const scoped = scopedByFlags || scopedToCollections;
6997
7754
  const configuredChecks = config?.checks ?? [];
7755
+ if (!scopedByFlags) {
7756
+ const covered = scopedToCollections ? collections.map((c) => c.name) : void 0;
7757
+ const orphans = orphanEntries(externalMetadata, files, base, covered);
7758
+ if (orphans.length > 0) throw orphanError(orphans);
7759
+ }
6998
7760
  const checksWillRun = configuredChecks.length > 0 && opts.checks !== false && !scoped;
6999
7761
  const checkEntries = [];
7000
- const checkResolved = /* @__PURE__ */ new Map();
7001
7762
  const processOne = async (label, content, extension) => {
7002
7763
  const extractor = forcedExtractor ?? extractorForExtension(extension);
7003
7764
  if (!extractor) {
@@ -7005,10 +7766,11 @@ async function runValidate(opts) {
7005
7766
  `Unsupported file type "${extension}" for "${label}". Supported: ${supportedExtensions().join(", ")}. Use --as to override.`
7006
7767
  );
7007
7768
  }
7769
+ const members = membersFor(label);
7008
7770
  let extracted;
7009
7771
  try {
7010
7772
  extracted = extractor.extract(content, label, {
7011
- elements: resolveElements(label, config)
7773
+ elements: resolveElements(label, config, members)
7012
7774
  });
7013
7775
  } catch (err) {
7014
7776
  if (err instanceof DocmetaError) throw err;
@@ -7017,6 +7779,16 @@ async function runValidate(opts) {
7017
7779
  );
7018
7780
  return;
7019
7781
  }
7782
+ const merged = mergeExternalMetadata(label, extracted, externalMetadata, members, base);
7783
+ extracted = merged.extracted;
7784
+ for (const j of merged.joins) {
7785
+ const byValue = joinHits.get(j.field) ?? /* @__PURE__ */ new Map();
7786
+ const line = extracted.lineFor(j.field);
7787
+ const hits = byValue.get(j.value) ?? [];
7788
+ hits.push({ label, file: j.file, ...line != null ? { line } : {} });
7789
+ byValue.set(j.value, hits);
7790
+ joinHits.set(j.field, byValue);
7791
+ }
7020
7792
  if (checksWillRun) checkEntries.push({ label, extracted });
7021
7793
  let resolved;
7022
7794
  try {
@@ -7025,6 +7797,7 @@ async function runValidate(opts) {
7025
7797
  fileSchema: extracted.data[FILE_SCHEMA_KEY],
7026
7798
  cliSchemas: opts.cliSchemas,
7027
7799
  config,
7800
+ memberOf: members,
7028
7801
  // A document's own `$schema` is measured from the run's directory, the
7029
7802
  // same base `loadSchema` will read it from, and contained to the
7030
7803
  // repository the run is standing in.
@@ -7038,7 +7811,6 @@ async function runValidate(opts) {
7038
7811
  );
7039
7812
  return;
7040
7813
  }
7041
- if (checksWillRun) checkResolved.set(label, resolved);
7042
7814
  const schemaSet = resolved.schemas;
7043
7815
  let errors;
7044
7816
  try {
@@ -7046,7 +7818,8 @@ async function runValidate(opts) {
7046
7818
  extracted.data,
7047
7819
  schemaSet,
7048
7820
  extracted.lineFor,
7049
- extracted.colFor
7821
+ extracted.colFor,
7822
+ merged.locate
7050
7823
  );
7051
7824
  } catch (err) {
7052
7825
  if (!(err instanceof DocmetaError) || resolved.source !== "document") {
@@ -7057,6 +7830,17 @@ async function runValidate(opts) {
7057
7830
  );
7058
7831
  return;
7059
7832
  }
7833
+ for (const { key, file, collection } of merged.collisions) {
7834
+ const line = extracted.lineFor(key);
7835
+ errors.push({
7836
+ schema: EXTERNAL_OWNED_SCHEMA,
7837
+ keyword: EXTERNAL_KEYWORD,
7838
+ subject: key,
7839
+ instancePath: externalMetadataPointer(key),
7840
+ message: `"${key}" is owned by manifest ${file} (collection ${collection}); remove it from the document`,
7841
+ ...line != null ? { line } : {}
7842
+ });
7843
+ }
7060
7844
  results.push({
7061
7845
  file: label,
7062
7846
  format: extractor.name,
@@ -7075,24 +7859,52 @@ async function runValidate(opts) {
7075
7859
  await processOne(STDIN_LABEL, content, forcedExtractor.extensions[0] ?? "");
7076
7860
  }
7077
7861
  for (const file of files) {
7078
- const content = await readFile5(resolve7(base, file), "utf8");
7862
+ const content = await readFile6(resolve9(base, file), "utf8");
7079
7863
  await processOne(file, content, extname2(file));
7080
7864
  }
7865
+ if (externalMetadata) {
7866
+ const byLabel = new Map(results.map((r) => [r.file, r]));
7867
+ const matched = /* @__PURE__ */ new Map();
7868
+ for (const [field, byValue] of joinHits) {
7869
+ matched.set(field, new Set(byValue.keys()));
7870
+ for (const [value, hits] of byValue) {
7871
+ if (hits.length < 2) continue;
7872
+ for (const hit of hits) {
7873
+ const others = hits.filter((h) => h.label !== hit.label).map((h) => h.label).join(", ");
7874
+ const result = byLabel.get(hit.label);
7875
+ if (!result) continue;
7876
+ result.errors.push({
7877
+ schema: EXTERNAL_DUPLICATE_SCHEMA,
7878
+ keyword: EXTERNAL_KEYWORD,
7879
+ subject: value,
7880
+ instancePath: externalMetadataPointer(field),
7881
+ message: `${hits.length} documents carry ${field} "${value}"; ${hit.file} cannot tell them apart (${others})`,
7882
+ ...hit.line != null ? { line: hit.line } : {}
7883
+ });
7884
+ result.ok = false;
7885
+ }
7886
+ }
7887
+ }
7888
+ if (!scopedByFlags) {
7889
+ const covered = scopedToCollections ? collections.map((c) => c.name) : void 0;
7890
+ const orphans = orphanJoins(externalMetadata, matched, covered);
7891
+ if (orphans.length > 0) throw orphanError(orphans);
7892
+ }
7893
+ }
7081
7894
  if (configuredChecks.length > 0 && opts.checks !== false) {
7082
7895
  if (scoped) {
7083
- opts.onNotice?.("corpus checks skipped: run is scoped");
7896
+ opts.onNotice?.(
7897
+ scopedToCollections ? `corpus checks skipped: run is scoped to collections ${collections.map((c) => c.name).join(", ")}` : "corpus checks skipped: run is scoped"
7898
+ );
7084
7899
  } else {
7085
7900
  const findings = await runChecks(configuredChecks, checkEntries, {
7086
- // The same resolution inputs the per-file loop used, so a check's
7087
- // collection views (0027) hold exactly the files each override group
7088
- // was validated as — and `resolved` hands over that loop's outcomes,
7089
- // so membership is read from them instead of resolving twice.
7090
- config,
7091
- ...opts.cliSchemas ? { cliSchemas: opts.cliSchemas } : {},
7092
- fileBase: cwd,
7093
- trustRoot,
7094
- resolved: checkResolved,
7095
- ...opts.onNotice ? { onNotice: opts.onNotice } : {}
7901
+ // Every declared collection is a view (0041 rule 7), holding the
7902
+ // members this run loaded — decided by the same path arithmetic
7903
+ // `membersFor` uses, so a check and a `manni meta query` over the same
7904
+ // corpus see the same collections.
7905
+ collections: declaredCollections,
7906
+ ...configDir !== void 0 ? { configDir } : {},
7907
+ base
7096
7908
  });
7097
7909
  const byFile = new Map(results.map((r) => [r.file, r]));
7098
7910
  for (const [file, errs] of findings) {
@@ -7167,24 +7979,25 @@ async function settleBaseline(results, request, ctx) {
7167
7979
  }
7168
7980
 
7169
7981
  // src/meta/commands/get.ts
7170
- import { readFile as readFile6 } from "node:fs/promises";
7171
- import { resolve as resolve8, extname as extname3 } from "node:path";
7982
+ import { readFile as readFile7 } from "node:fs/promises";
7983
+ import { resolve as resolve10, extname as extname3 } from "node:path";
7172
7984
  async function runGet(opts) {
7173
7985
  const cwd = opts.cwd ?? process.cwd();
7174
7986
  if (opts.fields.length === 0) {
7175
7987
  throw new DocmetaError("Specify at least one field to get.");
7176
7988
  }
7177
- const { config, inputs, base } = await resolveRunConfig({
7989
+ const { config, inputs, base, configDir, collections, fromCollections } = await resolveRunConfig({
7178
7990
  cwd,
7179
7991
  configPath: opts.configPath,
7180
7992
  noConfig: opts.noConfig,
7181
7993
  inputs: opts.inputs,
7994
+ ...opts.collections !== void 0 ? { collections: opts.collections } : {},
7182
7995
  onConfigLoaded: opts.onConfigLoaded
7183
7996
  });
7184
7997
  const usingStdin = inputs.includes(STDIN_TOKEN);
7185
7998
  if (inputs.length === 0) {
7186
7999
  throw new DocmetaError(
7187
- "No files to read. Pass paths/globs, or add `paths:` under `meta:` in manni.config.yaml."
8000
+ "No files to read. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml."
7188
8001
  );
7189
8002
  }
7190
8003
  const forced = opts.as ? extractorByName(opts.as) : void 0;
@@ -7196,8 +8009,9 @@ async function runGet(opts) {
7196
8009
  const exts = opts.exts ?? (forced ? forced.extensions : void 0);
7197
8010
  const fileInputs = inputs.filter((i) => i !== STDIN_TOKEN);
7198
8011
  const allowEmpty = opts.allowEmpty ?? config?.allowEmpty;
7199
- const exclude = [...config?.exclude ?? [], ...opts.exclude ?? []];
7200
- const { files, gitignoreSkipped } = await resolveTargetSet({
8012
+ const exclude = opts.exclude ?? [];
8013
+ const membersFor = (label) => memberOf(collections, configDir ?? cwd, base, label);
8014
+ const { files: walked, gitignoreSkipped } = await resolveTargetSet({
7201
8015
  inputs: fileInputs,
7202
8016
  exts,
7203
8017
  exclude,
@@ -7209,6 +8023,7 @@ async function runGet(opts) {
7209
8023
  onNotice: opts.onNotice
7210
8024
  })
7211
8025
  });
8026
+ const files = fromCollections ? retainMembers(walked, membersFor) : walked;
7212
8027
  assertNonEmpty({
7213
8028
  files,
7214
8029
  inputs: fileInputs,
@@ -7220,6 +8035,11 @@ async function runGet(opts) {
7220
8035
  action: "read"
7221
8036
  });
7222
8037
  const out = [];
8038
+ const externalMetadata = await loadExternalMetadata(collections, {
8039
+ configDir: configDir ?? cwd,
8040
+ base,
8041
+ offline: opts.offline ?? config?.offline ?? false
8042
+ });
7223
8043
  const readOne = (label, content, extension) => {
7224
8044
  const extractor = forced ?? extractorForExtension(extension);
7225
8045
  if (!extractor) {
@@ -7227,11 +8047,18 @@ async function runGet(opts) {
7227
8047
  `Unsupported file type "${extension}" for "${label}". Supported: ${supportedExtensions().join(", ")}. Use --as to override.`
7228
8048
  );
7229
8049
  }
8050
+ const members = membersFor(label);
7230
8051
  let extracted;
7231
8052
  try {
7232
- extracted = extractor.extract(content, label, {
7233
- elements: resolveElements(label, config)
7234
- });
8053
+ extracted = mergeExternalMetadata(
8054
+ label,
8055
+ extractor.extract(content, label, {
8056
+ elements: resolveElements(label, config, members)
8057
+ }),
8058
+ externalMetadata,
8059
+ members,
8060
+ base
8061
+ ).extracted;
7235
8062
  } catch (err) {
7236
8063
  if (err instanceof DocmetaError) throw err;
7237
8064
  const reason = err instanceof Error ? err.message : String(err);
@@ -7251,7 +8078,7 @@ async function runGet(opts) {
7251
8078
  readOne("<stdin>", opts.stdinContent ?? "", forced.extensions[0] ?? "");
7252
8079
  }
7253
8080
  for (const file of files) {
7254
- const content = await readFile6(resolve8(base, file), "utf8");
8081
+ const content = await readFile7(resolve10(base, file), "utf8");
7255
8082
  readOne(file, content, extname3(file));
7256
8083
  }
7257
8084
  return out;
@@ -7288,16 +8115,16 @@ function parseJsonPointer(pointer) {
7288
8115
 
7289
8116
  // src/meta/commands/schemas.ts
7290
8117
  import { existsSync as existsSync3 } from "node:fs";
7291
- import { mkdir as mkdir2, readFile as readFile7 } from "node:fs/promises";
7292
- import { dirname as dirname4, extname as extname4, isAbsolute as isAbsolute2, join as join5, relative as relative5, resolve as resolve9, sep } from "node:path";
7293
- import { parseDocument as parseDocument3, parse as parseYaml3, stringify } from "yaml";
8118
+ import { mkdir as mkdir2, readFile as readFile8 } from "node:fs/promises";
8119
+ import { dirname as dirname5, extname as extname4, isAbsolute as isAbsolute4, join as join5, relative as relative8, resolve as resolve11, sep as sep4 } from "node:path";
8120
+ import { parseDocument as parseDocument4, parse as parseYaml3, stringify } from "yaml";
7294
8121
  function getSchemasInfo() {
7295
8122
  return { builtins: listBuiltins(), formats: listFormats() };
7296
8123
  }
7297
8124
  var DEFAULT_VENDOR_DIR = "./schema";
7298
8125
  var DEFAULT_CONFIG_NAME = "manni.config.yaml";
7299
8126
  function posixRelative(from, to) {
7300
- return relative5(from, to).split(sep).join("/");
8127
+ return relative8(from, to).split(sep4).join("/");
7301
8128
  }
7302
8129
  function vendorFileName(url) {
7303
8130
  let parsed2;
@@ -7362,7 +8189,7 @@ function renderConfig(existing, entries, section) {
7362
8189
  if (existing === null) {
7363
8190
  return stringify({ [META_SECTION]: { schemas: entries } });
7364
8191
  }
7365
- const doc = parseDocument3(existing);
8192
+ const doc = parseDocument4(existing);
7366
8193
  doc.setIn(section === void 0 ? ["schemas"] : [section, "schemas"], entries);
7367
8194
  return doc.toString();
7368
8195
  }
@@ -7383,13 +8210,13 @@ async function runVendorSchema(opts) {
7383
8210
  );
7384
8211
  }
7385
8212
  const loaded = await loadConfig(opts.configPath, cwd);
7386
- const configPath = loaded?.path ?? resolve9(cwd, DEFAULT_CONFIG_NAME);
8213
+ const configPath = loaded?.path ?? resolve11(cwd, DEFAULT_CONFIG_NAME);
7387
8214
  const configDir = loaded?.dir ?? cwd;
7388
- const absDir = resolve9(cwd, opts.dir ?? DEFAULT_VENDOR_DIR);
8215
+ const absDir = resolve11(cwd, opts.dir ?? DEFAULT_VENDOR_DIR);
7389
8216
  const absFile = join5(absDir, vendorFileName(url));
7390
8217
  await assertNotIgnored(absFile, absDir, cwd, opts.onNotice);
7391
8218
  const fromConfig = posixRelative(configDir, absFile);
7392
- const ref = isAbsolute2(fromConfig) || fromConfig === "" ? absFile : fromConfig.startsWith(".") ? fromConfig : `./${fromConfig}`;
8219
+ const ref = isAbsolute4(fromConfig) || fromConfig === "" ? absFile : fromConfig.startsWith(".") ? fromConfig : `./${fromConfig}`;
7393
8220
  const { bytes } = await fetchSchemaBytes(url, {
7394
8221
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
7395
8222
  ...opts.maxBytes !== void 0 ? { maxBytes: opts.maxBytes } : {}
@@ -7404,7 +8231,7 @@ async function runVendorSchema(opts) {
7404
8231
  }
7405
8232
  let unchanged = false;
7406
8233
  if (existsSync3(absFile)) {
7407
- const current = await readFile7(absFile);
8234
+ const current = await readFile8(absFile);
7408
8235
  unchanged = bytes.equals(current);
7409
8236
  if (!unchanged && !folded.replaced) {
7410
8237
  throw new DocmetaError(
@@ -7414,7 +8241,7 @@ async function runVendorSchema(opts) {
7414
8241
  }
7415
8242
  await mkdir2(absDir, { recursive: true });
7416
8243
  await writeFileAtomic(absFile, bytes);
7417
- const existing = loaded ? await readFile7(configPath, "utf8") : null;
8244
+ const existing = loaded ? await readFile8(configPath, "utf8") : null;
7418
8245
  const section = loaded ? loaded.section : META_SECTION;
7419
8246
  const text = renderConfig(existing, folded.entries, section);
7420
8247
  reparseConfig(text, posixRelative(cwd, configPath) || DEFAULT_CONFIG_NAME, section);
@@ -7533,17 +8360,23 @@ async function runInferSchema(opts) {
7533
8360
  `--min-coverage must be a percentage between 0 and 100, got ${minCoverage}.`
7534
8361
  );
7535
8362
  }
7536
- const { config, inputs, base } = await resolveRunConfig({
8363
+ const { config, inputs, base, configDir, collections, fromCollections } = await resolveRunConfig({
7537
8364
  cwd,
7538
8365
  configPath: opts.configPath,
7539
8366
  noConfig: opts.noConfig,
7540
8367
  inputs: opts.inputs,
8368
+ ...opts.collections !== void 0 ? { collections: opts.collections } : {},
7541
8369
  onConfigLoaded: opts.onConfigLoaded
7542
8370
  });
7543
8371
  const usingStdin = inputs.includes(STDIN_TOKEN);
8372
+ const externalMetadata = await loadExternalMetadata(collections, {
8373
+ configDir: configDir ?? cwd,
8374
+ base,
8375
+ offline: opts.offline ?? config?.offline ?? false
8376
+ });
7544
8377
  if (inputs.length === 0) {
7545
8378
  throw new DocmetaError(
7546
- "No files to scan. Pass paths/globs, or add `paths:` under `meta:` in manni.config.yaml."
8379
+ "No files to scan. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml."
7547
8380
  );
7548
8381
  }
7549
8382
  const forced = opts.as ? extractorByName(opts.as) : void 0;
@@ -7559,10 +8392,10 @@ async function runInferSchema(opts) {
7559
8392
  }
7560
8393
  let absOut;
7561
8394
  if (opts.out !== void 0) {
7562
- absOut = resolve9(cwd, opts.out);
8395
+ absOut = resolve11(cwd, opts.out);
7563
8396
  await assertNotIgnored(
7564
8397
  absOut,
7565
- dirname4(absOut),
8398
+ dirname5(absOut),
7566
8399
  cwd,
7567
8400
  opts.onNotice,
7568
8401
  DRAFT_IGNORE_TEXT
@@ -7576,8 +8409,9 @@ async function runInferSchema(opts) {
7576
8409
  const exts = opts.exts ?? (forced ? forced.extensions : void 0);
7577
8410
  const fileInputs = inputs.filter((i) => i !== STDIN_TOKEN);
7578
8411
  const allowEmpty = opts.allowEmpty ?? config?.allowEmpty;
7579
- const exclude = [...config?.exclude ?? [], ...opts.exclude ?? []];
7580
- const { files, gitignoreSkipped } = await resolveTargetSet({
8412
+ const exclude = opts.exclude ?? [];
8413
+ const membersFor = (label) => memberOf(collections, configDir ?? cwd, base, label);
8414
+ const { files: walked, gitignoreSkipped } = await resolveTargetSet({
7581
8415
  inputs: fileInputs,
7582
8416
  exts,
7583
8417
  exclude,
@@ -7589,6 +8423,7 @@ async function runInferSchema(opts) {
7589
8423
  onNotice: opts.onNotice
7590
8424
  })
7591
8425
  });
8426
+ const files = fromCollections ? retainMembers(walked, membersFor) : walked;
7592
8427
  assertNonEmpty({
7593
8428
  files,
7594
8429
  inputs: fileInputs,
@@ -7611,11 +8446,18 @@ async function runInferSchema(opts) {
7611
8446
  );
7612
8447
  }
7613
8448
  filesScanned += 1;
8449
+ const members = membersFor(label);
7614
8450
  let extracted;
7615
8451
  try {
7616
- extracted = extractor.extract(content, label, {
7617
- elements: resolveElements(label, config)
7618
- });
8452
+ extracted = mergeExternalMetadata(
8453
+ label,
8454
+ extractor.extract(content, label, {
8455
+ elements: resolveElements(label, config, members)
8456
+ }),
8457
+ externalMetadata,
8458
+ members,
8459
+ base
8460
+ ).extracted;
7619
8461
  } catch (err) {
7620
8462
  unreadable.push({
7621
8463
  file: label,
@@ -7637,7 +8479,7 @@ async function runInferSchema(opts) {
7637
8479
  scanOne(STDIN_LABEL, opts.stdinContent ?? "", forced.extensions[0] ?? "");
7638
8480
  }
7639
8481
  for (const file of files) {
7640
- const content = await readFile7(resolve9(base, file), "utf8");
8482
+ const content = await readFile8(resolve11(base, file), "utf8");
7641
8483
  scanOne(file, content, extname4(file));
7642
8484
  }
7643
8485
  for (const u of unreadable) {
@@ -7679,7 +8521,7 @@ async function runInferSchema(opts) {
7679
8521
  };
7680
8522
  const keys = kept.map(({ sawEmptyString: _drop, ...rest }) => rest);
7681
8523
  if (absOut !== void 0) {
7682
- await mkdir2(dirname4(absOut), { recursive: true });
8524
+ await mkdir2(dirname5(absOut), { recursive: true });
7683
8525
  await writeFileAtomic(absOut, `${JSON.stringify(draft, null, 2)}
7684
8526
  `);
7685
8527
  }
@@ -7698,9 +8540,9 @@ async function runInferSchema(opts) {
7698
8540
  // src/meta/commands/query.ts
7699
8541
  import { randomBytes } from "node:crypto";
7700
8542
  import { existsSync as existsSync4 } from "node:fs";
7701
- import { mkdir as mkdir3, open, readFile as readFile8, rename as rename2, rm as rm2 } from "node:fs/promises";
7702
- import { dirname as dirname5, isAbsolute as isAbsolute3, resolve as resolve10, extname as extname5, sep as sep2 } from "node:path";
7703
- import { parseDocument as parseDocument4, isMap as isMap3, isSeq as isSeq2, isScalar as isScalar2 } from "yaml";
8543
+ import { mkdir as mkdir3, open, readFile as readFile9, rename as rename2, rm as rm2 } from "node:fs/promises";
8544
+ import { dirname as dirname6, isAbsolute as isAbsolute5, resolve as resolve12, extname as extname5, sep as sep5 } from "node:path";
8545
+ import { parseDocument as parseDocument5, isMap as isMap4, isSeq as isSeq2, isScalar as isScalar3 } from "yaml";
7704
8546
  async function runQuery(opts) {
7705
8547
  const cwd = opts.cwd ?? process.cwd();
7706
8548
  const sql = opts.sql.trim();
@@ -7732,19 +8574,29 @@ async function runQuery(opts) {
7732
8574
  );
7733
8575
  }
7734
8576
  }
7735
- const { config, inputs, base, configDir, configPath } = await resolveRunConfig(
7736
- {
7737
- cwd,
7738
- configPath: opts.configPath,
7739
- noConfig: opts.noConfig,
7740
- inputs: opts.inputs,
7741
- onConfigLoaded: opts.onConfigLoaded
7742
- }
7743
- );
8577
+ const {
8578
+ config,
8579
+ inputs,
8580
+ base,
8581
+ configDir,
8582
+ configPath,
8583
+ configSection,
8584
+ collections,
8585
+ declaredCollections,
8586
+ fromCollections
8587
+ } = await resolveRunConfig({
8588
+ cwd,
8589
+ configPath: opts.configPath,
8590
+ noConfig: opts.noConfig,
8591
+ inputs: opts.inputs,
8592
+ ...opts.collections !== void 0 ? { collections: opts.collections } : {},
8593
+ onConfigLoaded: opts.onConfigLoaded
8594
+ });
8595
+ const membersFor = (label) => memberOf(collections, configDir ?? cwd, base, label);
7744
8596
  const usingStdin = inputs.includes(STDIN_TOKEN);
7745
8597
  if (inputs.length === 0) {
7746
8598
  throw new DocmetaError(
7747
- "No files to read. Pass paths/globs, or add `paths:` under `meta:` in manni.config.yaml."
8599
+ "No files to read. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml."
7748
8600
  );
7749
8601
  }
7750
8602
  const forced = opts.as ? extractorByName(opts.as) : void 0;
@@ -7756,8 +8608,8 @@ async function runQuery(opts) {
7756
8608
  const exts = opts.exts ?? (forced ? forced.extensions : void 0);
7757
8609
  const fileInputs = inputs.filter((i) => i !== STDIN_TOKEN);
7758
8610
  const allowEmpty = opts.allowEmpty ?? config?.allowEmpty;
7759
- const exclude = [...config?.exclude ?? [], ...opts.exclude ?? []];
7760
- const { files, gitignoreSkipped } = await resolveTargetSet({
8611
+ const exclude = opts.exclude ?? [];
8612
+ const { files: walked, gitignoreSkipped } = await resolveTargetSet({
7761
8613
  inputs: fileInputs,
7762
8614
  exts,
7763
8615
  exclude,
@@ -7769,6 +8621,7 @@ async function runQuery(opts) {
7769
8621
  onNotice: opts.onNotice
7770
8622
  })
7771
8623
  });
8624
+ const files = fromCollections ? retainMembers(walked, membersFor) : walked;
7772
8625
  assertNonEmpty({
7773
8626
  files,
7774
8627
  inputs: fileInputs,
@@ -7780,6 +8633,11 @@ async function runQuery(opts) {
7780
8633
  action: "queried"
7781
8634
  });
7782
8635
  const entries = [];
8636
+ const externalMetadata = await loadExternalMetadata(collections, {
8637
+ configDir: configDir ?? cwd,
8638
+ base,
8639
+ offline: opts.offline ?? config?.offline ?? false
8640
+ });
7783
8641
  const readOne = (label, content, extension) => {
7784
8642
  const extractor = forced ?? extractorForExtension(extension);
7785
8643
  if (!extractor) {
@@ -7787,9 +8645,16 @@ async function runQuery(opts) {
7787
8645
  `Unsupported file type "${extension}" for "${label}". Supported: ${supportedExtensions().join(", ")}. Use --as to override.`
7788
8646
  );
7789
8647
  }
7790
- const extracted = extractor.extract(content, label, {
7791
- elements: resolveElements(label, config)
7792
- });
8648
+ const members = membersFor(label);
8649
+ const extracted = mergeExternalMetadata(
8650
+ label,
8651
+ extractor.extract(content, label, {
8652
+ elements: resolveElements(label, config, members)
8653
+ }),
8654
+ externalMetadata,
8655
+ members,
8656
+ base
8657
+ ).extracted;
7793
8658
  entries.push({ label, extracted, extractor });
7794
8659
  };
7795
8660
  if (usingStdin) {
@@ -7807,10 +8672,10 @@ async function runQuery(opts) {
7807
8672
  readOne(STDIN_LABEL, opts.stdinContent ?? "", ext);
7808
8673
  }
7809
8674
  for (const file of files) {
7810
- const content = await readFile8(resolve10(base, file), "utf8");
8675
+ const content = await readFile9(resolve12(base, file), "utf8");
7811
8676
  readOne(file, content, extname5(file));
7812
8677
  }
7813
- const db = opts.db === void 0 ? void 0 : { resolved: resolve10(cwd, opts.db), display: opts.db };
8678
+ const db = opts.db === void 0 ? void 0 : { resolved: resolve12(cwd, opts.db), display: opts.db };
7814
8679
  const run = await runSql(sql, entries, {
7815
8680
  target: db,
7816
8681
  params: boundParams,
@@ -7821,7 +8686,12 @@ async function runQuery(opts) {
7821
8686
  cwd,
7822
8687
  configDir,
7823
8688
  configPath,
8689
+ configSection,
7824
8690
  trustRoot: schemaTrustRoot(cwd, configDir),
8691
+ externalMetadata,
8692
+ collections,
8693
+ declaredCollections,
8694
+ memberships: membersFor,
7825
8695
  onNotice: opts.onNotice
7826
8696
  });
7827
8697
  return { ...run, frame: { cwd, base: configDir ?? cwd, runBase: base } };
@@ -7885,7 +8755,7 @@ async function runSql(sql, entries, ctx) {
7885
8755
  );
7886
8756
  const { DatabaseSync } = await loadSqlite();
7887
8757
  if (target) {
7888
- await mkdir3(dirname5(target.resolved), { recursive: true });
8758
+ await mkdir3(dirname6(target.resolved), { recursive: true });
7889
8759
  await prepareDbTarget(target.resolved, target.display);
7890
8760
  }
7891
8761
  let db;
@@ -7909,10 +8779,9 @@ async function runSql(sql, entries, ctx) {
7909
8779
  createCollectionViews(
7910
8780
  db,
7911
8781
  collectCollections(entries, {
7912
- config: ctx.config,
7913
- fileBase: ctx.cwd,
7914
- trustRoot: ctx.trustRoot,
7915
- ...ctx.onNotice ? { onNotice: ctx.onNotice } : {}
8782
+ collections: ctx.declaredCollections,
8783
+ ...ctx.configDir !== void 0 ? { configDir: ctx.configDir } : {},
8784
+ base: ctx.base
7916
8785
  })
7917
8786
  );
7918
8787
  viewsBuilt = true;
@@ -7928,7 +8797,9 @@ async function runSql(sql, entries, ctx) {
7928
8797
  );
7929
8798
  }
7930
8799
  const lower = sql.toLowerCase();
7931
- const observesCatalog = lower.includes("sqlite_master") || lower.includes("sqlite_schema") || lower.includes("pragma") || /^(create|drop|alter)\b/i.test(head) || collectionNames(ctx.config).some((n) => lower.includes(n.toLowerCase()));
8800
+ const observesCatalog = lower.includes("sqlite_master") || lower.includes("sqlite_schema") || lower.includes("pragma") || /^(create|drop|alter)\b/i.test(head) || collectionNames(ctx.declaredCollections).some(
8801
+ (n) => lower.includes(n.toLowerCase())
8802
+ );
7932
8803
  if (observesCatalog) buildViews();
7933
8804
  const sentinel = `manni:null:${randomBytes(16).toString("hex")}`;
7934
8805
  db.function("explicit_null", () => sentinel);
@@ -7942,7 +8813,7 @@ async function runSql(sql, entries, ctx) {
7942
8813
  } catch (err) {
7943
8814
  const message = err.message;
7944
8815
  const missing = missingTableName(message);
7945
- if (missing !== void 0 && !viewsBuilt && namesConfiguredCollection(missing, ctx.config)) {
8816
+ if (missing !== void 0 && !viewsBuilt && namesConfiguredCollection(missing, ctx.declaredCollections)) {
7946
8817
  throw new MissingCollectionView();
7947
8818
  }
7948
8819
  refuseSqlError(err);
@@ -8016,9 +8887,9 @@ function missingTableName(message) {
8016
8887
  const at = message.indexOf(prefix);
8017
8888
  return at === -1 ? void 0 : message.slice(at + prefix.length);
8018
8889
  }
8019
- function namesConfiguredCollection(missing, config) {
8890
+ function namesConfiguredCollection(missing, collections) {
8020
8891
  const candidates = [missing, missing.replace(/^(main|temp)\./i, "")];
8021
- return collectionNames(config).some(
8892
+ return collectionNames(collections).some(
8022
8893
  (n) => candidates.some((c) => c.toLowerCase() === n.toLowerCase())
8023
8894
  );
8024
8895
  }
@@ -8337,20 +9208,20 @@ function displayPath(ctx, abs) {
8337
9208
  }
8338
9209
  function assertSchemaWriteWithin(ctx, abs) {
8339
9210
  const within = posixRelative(ctx.trustRoot.dir, abs);
8340
- if (within !== "" && !within.startsWith("..") && !isAbsolute3(within)) return;
9211
+ if (within !== "" && !within.startsWith("..") && !isAbsolute5(within)) return;
8341
9212
  throw new DocmetaError(
8342
9213
  `The resolved schema "${abs}" lives outside ${ctx.trustRoot.dir}; DDL edits schemas inside the repository. Evolve it where it lives, or vendor a copy in.`
8343
9214
  );
8344
9215
  }
8345
9216
  function foldPath(p) {
8346
- const abs = resolve10(p);
9217
+ const abs = resolve12(p);
8347
9218
  return process.platform === "win32" ? abs.toLowerCase() : abs;
8348
9219
  }
8349
9220
  function filePinsByPath(ctx) {
8350
9221
  const byPath = /* @__PURE__ */ new Map();
8351
9222
  for (const [ref, pin] of collectSchemaPins(ctx.config)) {
8352
9223
  if (classifyRef(ref).kind !== "file") continue;
8353
- byPath.set(foldPath(resolve10(ctx.cwd, ref)), pin);
9224
+ byPath.set(foldPath(resolve12(ctx.cwd, ref)), pin);
8354
9225
  }
8355
9226
  return byPath;
8356
9227
  }
@@ -8378,13 +9249,13 @@ async function loadSetMembers(refs, ctx) {
8378
9249
  });
8379
9250
  continue;
8380
9251
  }
8381
- const abs = resolve10(ctx.cwd, ref);
9252
+ const abs = resolve12(ctx.cwd, ref);
8382
9253
  const canon = foldPath(abs);
8383
9254
  if (seenFiles.has(canon)) continue;
8384
9255
  seenFiles.add(canon);
8385
9256
  let text;
8386
9257
  try {
8387
- text = await readFile8(abs, "utf8");
9258
+ text = await readFile9(abs, "utf8");
8388
9259
  } catch {
8389
9260
  throw new DocmetaError(
8390
9261
  `Schema file not found: "${ref}" (looked at ${abs}).`
@@ -8454,6 +9325,7 @@ async function planSchemaMutation(ops, entries, ctx) {
8454
9325
  filePath: e.label,
8455
9326
  fileSchema: e.extracted.data[FILE_SCHEMA_KEY],
8456
9327
  config: ctx.config,
9328
+ memberOf: ctx.memberships(e.label),
8457
9329
  fileBase: ctx.cwd,
8458
9330
  trustRoot: ctx.trustRoot,
8459
9331
  onNotice: ctx.onNotice
@@ -8480,7 +9352,11 @@ async function planSchemaMutation(ops, entries, ctx) {
8480
9352
  const overrides = ctx.config?.overrides ?? [];
8481
9353
  const named = [...groupIndexes].sort((a, b) => a - b).flatMap((i) => {
8482
9354
  const o = overrides[i];
8483
- return o?.name !== void 0 ? [`${o.name} (${overrideGlobs(o.files).join(", ")})`] : [];
9355
+ if (o?.collection === void 0) return [];
9356
+ const declared = ctx.collections.find((c) => c.name === o.collection);
9357
+ return [
9358
+ `${o.collection} (${overrideGlobs(declared?.paths).join(", ")})`
9359
+ ];
8484
9360
  });
8485
9361
  const last = named[named.length - 1];
8486
9362
  const remedy = named.length >= 2 && last !== void 0 ? `The run spans ${[named.slice(0, -1).join(", "), last].join(" and ")}; re-run over one group's files, or pass -s <schema> to name the contract directly.` : "Scope the run to one override group, or pass -s <schema> to name the contract directly.";
@@ -8578,7 +9454,7 @@ async function planSchemaMutation(ops, entries, ctx) {
8578
9454
  const [, name2, ver] = target.builtinId.split(":");
8579
9455
  const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, "-");
8580
9456
  const forkName = `${safe(name2 ?? "schema")}-${safe(ver ?? "0")}.local.json`;
8581
- schemaAbs = resolve10(ctx.configDir ?? ctx.cwd, "schemas", forkName);
9457
+ schemaAbs = resolve12(ctx.configDir ?? ctx.cwd, "schemas", forkName);
8582
9458
  assertSchemaWriteWithin(ctx, schemaAbs);
8583
9459
  if (existsSync4(schemaAbs)) {
8584
9460
  throw new DocmetaError(
@@ -8587,7 +9463,7 @@ async function planSchemaMutation(ops, entries, ctx) {
8587
9463
  }
8588
9464
  await assertNotIgnored(
8589
9465
  schemaAbs,
8590
- dirname5(schemaAbs),
9466
+ dirname6(schemaAbs),
8591
9467
  ctx.cwd,
8592
9468
  ctx.onNotice,
8593
9469
  FORK_IGNORE_TEXT
@@ -8734,18 +9610,18 @@ async function planConfigEdit(ctx, edit, runRefs) {
8734
9610
  if (configPath === void 0 || configDir === void 0) {
8735
9611
  return { changes: [], repointed: false };
8736
9612
  }
8737
- const text = await readFile8(configPath, "utf8");
8738
- const doc = parseDocument4(stripBom(text));
9613
+ const text = await readFile9(configPath, "utf8");
9614
+ const doc = parseDocument5(stripBom(text));
8739
9615
  const display = displayPath(ctx, configPath);
8740
9616
  const rawRefOf = (item) => {
8741
- if (isScalar2(item) && typeof item.value === "string") return item.value;
8742
- if (isMap3(item)) {
9617
+ if (isScalar3(item) && typeof item.value === "string") return item.value;
9618
+ if (isMap4(item)) {
8743
9619
  const ref = item.get("ref");
8744
9620
  if (typeof ref === "string") return ref;
8745
9621
  }
8746
9622
  return void 0;
8747
9623
  };
8748
- const rebaseRaw = (raw) => classifyRef(raw).kind === "file" && !isAbsolute3(raw) ? resolve10(configDir, raw) : raw;
9624
+ const rebaseRaw = (raw) => classifyRef(raw).kind === "file" && !isAbsolute5(raw) ? resolve12(configDir, raw) : raw;
8749
9625
  const wantSet = runRefs.map(rebaseRaw).sort().join("\n");
8750
9626
  const seqResolvesToRunSet = (node) => {
8751
9627
  if (!isSeq2(node)) return false;
@@ -8761,22 +9637,24 @@ async function planConfigEdit(ctx, edit, runRefs) {
8761
9637
  for (const item of node.items) {
8762
9638
  const raw = rawRefOf(item);
8763
9639
  if (raw === void 0 || !wants(raw)) continue;
8764
- if (isScalar2(item)) {
9640
+ if (isScalar3(item)) {
8765
9641
  item.value = spec.newRef;
8766
9642
  n += 1;
8767
- } else if (isMap3(item)) {
9643
+ } else if (isMap4(item)) {
8768
9644
  item.set("ref", spec.newRef);
8769
9645
  n += 1;
8770
9646
  }
8771
9647
  }
8772
9648
  return n;
8773
9649
  };
8774
- const topSeq = doc.get("schemas", true);
9650
+ const root = ctx.configSection === void 0 ? doc : doc.get(ctx.configSection, true);
9651
+ const keyOf2 = (key) => root === doc ? doc.get(key, true) : isMap4(root) ? root.get(key, true) : void 0;
9652
+ const topSeq = keyOf2("schemas");
8775
9653
  let repointCount = repoint(topSeq);
8776
- const overrides = doc.get("overrides", true);
9654
+ const overrides = keyOf2("overrides");
8777
9655
  if (isSeq2(overrides)) {
8778
9656
  for (const entry of overrides.items) {
8779
- if (isMap3(entry)) repointCount += repoint(entry.get("schemas", true));
9657
+ if (isMap4(entry)) repointCount += repoint(entry.get("schemas", true));
8780
9658
  }
8781
9659
  }
8782
9660
  const repointed = repointCount > 0;
@@ -8785,10 +9663,10 @@ async function planConfigEdit(ctx, edit, runRefs) {
8785
9663
  if (edit.integrity && isSeq2(topSeq)) {
8786
9664
  const wantPath = foldPath(edit.integrity.path);
8787
9665
  for (const item of topSeq.items) {
8788
- if (!isMap3(item)) continue;
9666
+ if (!isMap4(item)) continue;
8789
9667
  const raw = rawRefOf(item);
8790
9668
  if (raw === void 0 || classifyRef(raw).kind !== "file") continue;
8791
- const abs = isAbsolute3(raw) ? raw : resolve10(configDir, raw);
9669
+ const abs = isAbsolute5(raw) ? raw : resolve12(configDir, raw);
8792
9670
  if (foldPath(abs) !== wantPath) continue;
8793
9671
  pinFrom = item.get("integrity");
8794
9672
  item.set("integrity", edit.integrity.value);
@@ -9009,7 +9887,7 @@ function buildChanges(diff, entries, sentinel, ctx, renameHints = [], booleanAdd
9009
9887
  `"${file}": no writable format for that extension.`
9010
9888
  );
9011
9889
  }
9012
- if (existsSync4(resolve10(ctx.base, file))) {
9890
+ if (existsSync4(resolve12(ctx.base, file))) {
9013
9891
  throw new DocmetaError(
9014
9892
  `"${file}" already exists; INSERT creates new files only.`
9015
9893
  );
@@ -9028,26 +9906,81 @@ function buildChanges(diff, entries, sentinel, ctx, renameHints = [], booleanAdd
9028
9906
  `"${from}" -> "${to}": a rename may not change the extension.`
9029
9907
  );
9030
9908
  }
9031
- if (existsSync4(resolve10(ctx.base, to))) {
9909
+ if (existsSync4(resolve12(ctx.base, to))) {
9032
9910
  throw new DocmetaError(
9033
9911
  `"${to}" already exists; the rename would overwrite it.`
9034
9912
  );
9035
9913
  }
9036
9914
  changes.push({ file: from, renamed: to, written: false });
9037
9915
  }
9916
+ refuseExternalMetadataWrites(changes, entries, ctx);
9038
9917
  return changes;
9039
9918
  }
9040
9919
  function validateNewPath(p, base) {
9041
9920
  const refuse = (why) => {
9042
9921
  throw new DocmetaError(`"${p}" is not a usable path: ${why}.`);
9043
9922
  };
9044
- if (isAbsolute3(p)) refuse("it is absolute");
9923
+ if (isAbsolute5(p)) refuse("it is absolute");
9045
9924
  if (p.split(/[\\/]/).some((segment) => segment === "..")) {
9046
9925
  refuse("it traverses upward");
9047
9926
  }
9048
- const abs = resolve10(base, p);
9049
- if (abs === resolve10(base)) refuse("it points at the corpus root, not a file");
9050
- if (!abs.startsWith(resolve10(base) + sep2)) refuse("it escapes the corpus");
9927
+ const abs = resolve12(base, p);
9928
+ if (abs === resolve12(base)) refuse("it points at the corpus root, not a file");
9929
+ if (!abs.startsWith(resolve12(base) + sep5)) refuse("it escapes the corpus");
9930
+ }
9931
+ function refuseExternalMetadataWrites(changes, entries, ctx) {
9932
+ const index = ctx.externalMetadata;
9933
+ if (!index) return;
9934
+ const owned = (key) => index.owners.get(key)?.[0]?.file;
9935
+ const refuse = (file, key, owner) => {
9936
+ throw new DocmetaError(
9937
+ `"${file}": "${key}" is owned by manifest ${owner}; edit the manifest instead.`
9938
+ );
9939
+ };
9940
+ const data = new Map(entries.map((e) => [e.label, e.extracted.data]));
9941
+ for (const c of changes) {
9942
+ if ("schema" in c || "config" in c) continue;
9943
+ if ("cleared" in c) {
9944
+ for (const key of Object.keys(data.get(c.file) ?? {})) {
9945
+ const owner = owned(key);
9946
+ if (owner !== void 0) refuse(c.file, key, owner);
9947
+ }
9948
+ continue;
9949
+ }
9950
+ if ("created" in c) {
9951
+ for (const key of Object.keys(c.to)) {
9952
+ const owner = owned(key);
9953
+ if (owner !== void 0) refuse(c.file, key, owner);
9954
+ }
9955
+ continue;
9956
+ }
9957
+ if ("renamed" in c) {
9958
+ const entry = index.byPath.get(resolve12(ctx.base, c.file));
9959
+ if (entry !== void 0 && entry.size > 0) {
9960
+ const manifest = [...entry.values()][0]?.file ?? "manifest";
9961
+ throw new DocmetaError(
9962
+ `"${c.file}": manifest ${manifest} names it; rename the manifest entry first.`
9963
+ );
9964
+ }
9965
+ continue;
9966
+ }
9967
+ const keys = "renamedFrom" in c ? [c.key, c.renamedFrom] : [c.key];
9968
+ for (const key of keys) {
9969
+ const owner = owned(key);
9970
+ if (owner !== void 0) refuse(c.file, key, owner);
9971
+ const byValue = index.byField.get(key);
9972
+ if (byValue === void 0) continue;
9973
+ const raw = data.get(c.file)?.[key];
9974
+ const value = typeof raw === "string" ? raw : typeof raw === "number" || typeof raw === "boolean" ? String(raw) : void 0;
9975
+ const entry = value === void 0 ? void 0 : byValue.get(value);
9976
+ if (entry !== void 0) {
9977
+ const manifest = entry.values().next().value?.file ?? "manifest";
9978
+ throw new DocmetaError(
9979
+ `"${c.file}": "${key}" is the field manifest ${manifest} joins on, and this document has an entry; change the manifest first.`
9980
+ );
9981
+ }
9982
+ }
9983
+ }
9051
9984
  }
9052
9985
  async function applyChanges(changes, entries, ctx, schemaPlan) {
9053
9986
  const schemaWrites = schemaPlan?.writes ?? [];
@@ -9075,14 +10008,14 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
9075
10008
  const pendingWrites = [];
9076
10009
  const pendingRenames = [];
9077
10010
  for (const [label, ops] of grouped) {
9078
- const path = resolve10(ctx.base, label);
10011
+ const path = resolve12(ctx.base, label);
9079
10012
  if (ops.renamedTo !== void 0) {
9080
10013
  if (!existsSync4(path)) {
9081
10014
  throw new DocmetaError(
9082
10015
  `"${label}" is gone from disk since it was read; re-run the query.`
9083
10016
  );
9084
10017
  }
9085
- const toDisk = resolve10(ctx.base, ops.renamedTo);
10018
+ const toDisk = resolve12(ctx.base, ops.renamedTo);
9086
10019
  if (existsSync4(toDisk)) {
9087
10020
  throw new DocmetaError(
9088
10021
  `"${ops.renamedTo}" appeared on disk since the plan; re-run the query.`
@@ -9115,10 +10048,17 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
9115
10048
  }
9116
10049
  const entry = byLabel.get(label);
9117
10050
  if (!entry) throw new DocmetaError(`No loaded entry for "${label}".`);
9118
- const content = await readFile8(path, "utf8");
9119
- const current = entry.extractor.extract(content, label, {
9120
- elements: resolveElements(label, ctx.config)
9121
- });
10051
+ const content = await readFile9(path, "utf8");
10052
+ const members = ctx.memberships(label);
10053
+ const current = mergeExternalMetadata(
10054
+ label,
10055
+ entry.extractor.extract(content, label, {
10056
+ elements: resolveElements(label, ctx.config, members)
10057
+ }),
10058
+ ctx.externalMetadata,
10059
+ members,
10060
+ ctx.base
10061
+ ).extracted;
9122
10062
  if (ops.cleared) {
9123
10063
  if (!deepEqual(current.data, entry.extracted.data)) {
9124
10064
  throw new DocmetaError(
@@ -9152,9 +10092,15 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
9152
10092
  deletions: ops.deletions
9153
10093
  });
9154
10094
  if (ops.deletions.length > 0) {
9155
- const check = entry.extractor.extract(applied, label, {
9156
- elements: resolveElements(label, ctx.config)
9157
- });
10095
+ const check = mergeExternalMetadata(
10096
+ label,
10097
+ entry.extractor.extract(applied, label, {
10098
+ elements: resolveElements(label, ctx.config, members)
10099
+ }),
10100
+ ctx.externalMetadata,
10101
+ members,
10102
+ ctx.base
10103
+ ).extracted;
9158
10104
  for (const key of ops.deletions) {
9159
10105
  if (check.data[key] !== void 0) {
9160
10106
  throw new DocmetaError(
@@ -9177,7 +10123,7 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
9177
10123
  }
9178
10124
  let now;
9179
10125
  try {
9180
- now = await readFile8(w.path, "utf8");
10126
+ now = await readFile9(w.path, "utf8");
9181
10127
  } catch {
9182
10128
  now = void 0;
9183
10129
  }
@@ -9188,10 +10134,10 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
9188
10134
  }
9189
10135
  }
9190
10136
  const dirs = /* @__PURE__ */ new Set();
9191
- for (const w of schemaWrites) dirs.add(dirname5(w.path));
9192
- for (const r of pendingRenames) dirs.add(dirname5(r.to));
10137
+ for (const w of schemaWrites) dirs.add(dirname6(w.path));
10138
+ for (const r of pendingRenames) dirs.add(dirname6(r.to));
9193
10139
  for (const p of pendingWrites) {
9194
- if (p.ensureDir) dirs.add(dirname5(p.path));
10140
+ if (p.ensureDir) dirs.add(dirname6(p.path));
9195
10141
  }
9196
10142
  for (const d of dirs) await mkdir3(d, { recursive: true });
9197
10143
  const writeSchema = async () => {
@@ -9306,8 +10252,8 @@ function skipExpression(sql, from) {
9306
10252
  }
9307
10253
 
9308
10254
  // src/meta/commands/fill.ts
9309
- import { readFile as readFile9 } from "node:fs/promises";
9310
- import { resolve as resolve11, extname as extname6, join as join6 } from "node:path";
10255
+ import { readFile as readFile10 } from "node:fs/promises";
10256
+ import { resolve as resolve13, extname as extname6, join as join6 } from "node:path";
9311
10257
  import {
9312
10258
  completeValidatedJSON,
9313
10259
  makeProvider,
@@ -9450,11 +10396,12 @@ function addUsage(total, next) {
9450
10396
  }
9451
10397
  async function runFill(opts) {
9452
10398
  const cwd = opts.cwd ?? process.cwd();
9453
- const { config, inputs, base, configDir } = await resolveRunConfig({
10399
+ const { config, inputs, base, configDir, collections, fromCollections } = await resolveRunConfig({
9454
10400
  cwd,
9455
10401
  configPath: opts.configPath,
9456
10402
  noConfig: opts.noConfig,
9457
10403
  inputs: opts.inputs,
10404
+ ...opts.collections !== void 0 ? { collections: opts.collections } : {},
9458
10405
  onConfigLoaded: opts.onConfigLoaded
9459
10406
  });
9460
10407
  const schemaOptions = schemaLoadOptions({
@@ -9469,7 +10416,7 @@ async function runFill(opts) {
9469
10416
  const usingStdin = inputs.includes(STDIN_TOKEN);
9470
10417
  if (inputs.length === 0) {
9471
10418
  throw new DocmetaError(
9472
- "No files to fill. Pass paths/globs, or add `paths:` under `meta:` in manni.config.yaml."
10419
+ "No files to fill. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml."
9473
10420
  );
9474
10421
  }
9475
10422
  const forcedExtractor = opts.as ? extractorByName(opts.as) : void 0;
@@ -9518,8 +10465,9 @@ async function runFill(opts) {
9518
10465
  const fileInputs = inputs.filter((i) => i !== STDIN_TOKEN);
9519
10466
  const allowEmpty = opts.allowEmpty ?? config?.allowEmpty;
9520
10467
  const fillExts = opts.exts ?? forcedExtractor?.extensions;
9521
- const fillExclude = [...config?.exclude ?? [], ...opts.exclude ?? []];
9522
- const { files, gitignoreSkipped } = await resolveTargetSet({
10468
+ const fillExclude = opts.exclude ?? [];
10469
+ const membersFor = (label) => memberOf(collections, configDir ?? cwd, base, label);
10470
+ const { files: walked, gitignoreSkipped } = await resolveTargetSet({
9523
10471
  inputs: fileInputs,
9524
10472
  exts: fillExts,
9525
10473
  exclude: fillExclude,
@@ -9531,6 +10479,7 @@ async function runFill(opts) {
9531
10479
  onNotice: opts.onNotice
9532
10480
  })
9533
10481
  });
10482
+ const files = fromCollections ? retainMembers(walked, membersFor) : walked;
9534
10483
  assertNonEmpty({
9535
10484
  files,
9536
10485
  inputs: fileInputs,
@@ -9580,6 +10529,11 @@ async function runFill(opts) {
9580
10529
  let turnsUsed = 0;
9581
10530
  let inFlight = 0;
9582
10531
  const turnsExhausted = () => maxTurns != null && turnsUsed + inFlight >= maxTurns;
10532
+ const externalMetadata = await loadExternalMetadata(collections, {
10533
+ configDir: configDir ?? cwd,
10534
+ base,
10535
+ offline: opts.offline ?? config?.offline ?? false
10536
+ });
9583
10537
  const processOne = async (label, content, extension) => {
9584
10538
  const extractor = forcedExtractor ?? extractorForExtension(extension);
9585
10539
  if (!extractor) {
@@ -9596,10 +10550,19 @@ async function runFill(opts) {
9596
10550
  `The "${extractor.name}" format is read-only; manni meta fill cannot write metadata back to it.`
9597
10551
  );
9598
10552
  }
9599
- const elements = resolveElements(label, config);
10553
+ const members = membersFor(label);
10554
+ const elements = resolveElements(label, config, members);
9600
10555
  let extracted;
10556
+ let merged;
9601
10557
  try {
9602
- extracted = extractor.extract(content, label, { elements });
10558
+ merged = mergeExternalMetadata(
10559
+ label,
10560
+ extractor.extract(content, label, { elements }),
10561
+ externalMetadata,
10562
+ members,
10563
+ base
10564
+ );
10565
+ extracted = merged.extracted;
9603
10566
  } catch (err) {
9604
10567
  return errorResult(label, extractor.name, err.message);
9605
10568
  }
@@ -9610,6 +10573,7 @@ async function runFill(opts) {
9610
10573
  fileSchema: extracted.data[FILE_SCHEMA_KEY],
9611
10574
  cliSchemas: opts.cliSchemas,
9612
10575
  config,
10576
+ memberOf: members,
9613
10577
  // Same trust boundary as `validate`: `fill` writes metadata back into
9614
10578
  // the document, so a schema a document chose for itself decides what
9615
10579
  // gets written — if anything, a stronger reason to guard it.
@@ -9631,7 +10595,8 @@ async function runFill(opts) {
9631
10595
  extracted.data,
9632
10596
  schemaSet,
9633
10597
  extracted.lineFor,
9634
- extracted.colFor
10598
+ extracted.colFor,
10599
+ merged.locate
9635
10600
  );
9636
10601
  } catch (err) {
9637
10602
  if (!(err instanceof DocmetaError) || resolved.source !== "document") {
@@ -9645,6 +10610,19 @@ async function runFill(opts) {
9645
10610
  existingErrors,
9646
10611
  only
9647
10612
  );
10613
+ const ownedCandidates = externalMetadata ? candidates.flatMap((c) => {
10614
+ const owner = externalMetadata.owners.get(c.key)?.[0];
10615
+ return owner === void 0 ? [] : [{ key: c.key, file: owner.file }];
10616
+ }) : [];
10617
+ const firstOwned = ownedCandidates[0];
10618
+ if (firstOwned) {
10619
+ return errorResult(
10620
+ label,
10621
+ extractor.name,
10622
+ `"${firstOwned.key}" is owned by manifest ${firstOwned.file}; manni meta fill cannot write a manifest key. Add it to the manifest instead.`,
10623
+ schemaSet
10624
+ );
10625
+ }
9648
10626
  if (candidates.length === 0) {
9649
10627
  return {
9650
10628
  file: label,
@@ -9785,9 +10763,9 @@ async function runFill(opts) {
9785
10763
  const fields = gate(candidates, proposals, threshold);
9786
10764
  const accepted = fields.filter((f) => f.written);
9787
10765
  if (accepted.length > 0) {
9788
- const merged = { ...extracted.data, ...patchOf(accepted) };
10766
+ const merged2 = { ...extracted.data, ...patchOf(accepted) };
9789
10767
  const after = await validator.validate(
9790
- merged,
10768
+ merged2,
9791
10769
  schemaSet,
9792
10770
  extracted.lineFor,
9793
10771
  extracted.colFor
@@ -9831,7 +10809,7 @@ async function runFill(opts) {
9831
10809
  }
9832
10810
  const changed = next !== content;
9833
10811
  if (changed && !dryRun && label !== "<stdin>") {
9834
- await writeFileAtomic(resolve11(base, label), next);
10812
+ await writeFileAtomic(resolve13(base, label), next);
9835
10813
  }
9836
10814
  return {
9837
10815
  file: label,
@@ -9853,7 +10831,7 @@ async function runFill(opts) {
9853
10831
  );
9854
10832
  }
9855
10833
  const fileResults = await mapConcurrent(files, concurrency, async (file) => {
9856
- const content = await readFile9(resolve11(base, file), "utf8");
10834
+ const content = await readFile10(resolve13(base, file), "utf8");
9857
10835
  return processOne(file, content, extname6(file));
9858
10836
  });
9859
10837
  results.push(...fileResults);
@@ -10171,15 +11149,19 @@ async function mapConcurrent(items, limit, fn) {
10171
11149
  }
10172
11150
 
10173
11151
  // src/meta/reporters/sarif.ts
10174
- import { isAbsolute as isAbsolute4 } from "node:path";
11152
+ import { isAbsolute as isAbsolute6 } from "node:path";
10175
11153
 
10176
11154
  // src/meta/reporters/rule-id.ts
10177
11155
  var SYNTHETIC_SCHEMA = "(parse)";
10178
11156
  var PARSE_ERROR_RULE = "manni/parse-error";
10179
11157
  var SCHEMA_ERROR_RULE = "manni/schema-error";
11158
+ var EXTERNAL_OWNED_RULE = "external:owned/external";
11159
+ var EXTERNAL_DUPLICATE_RULE = "external:duplicate/external";
10180
11160
  var RESERVED_RULES = {
10181
11161
  [PARSE_ERROR_RULE]: "The document's metadata block could not be parsed.",
10182
- [SCHEMA_ERROR_RULE]: "No schema set could be resolved for the document."
11162
+ [SCHEMA_ERROR_RULE]: "No schema set could be resolved for the document.",
11163
+ [EXTERNAL_OWNED_RULE]: "The document carries a key a manifest owns; the manifest is the only place that key may be set.",
11164
+ [EXTERNAL_DUPLICATE_RULE]: "Two documents carry the same value of a manifest's join field, so one manifest entry matched both."
10183
11165
  };
10184
11166
  function ruleIdFor(e, frame) {
10185
11167
  if (e.schema === SYNTHETIC_SCHEMA) {
@@ -10207,7 +11189,7 @@ function uriFrame(frame) {
10207
11189
  }
10208
11190
  function artifactUri(file, frame) {
10209
11191
  const uri = canonicalFilePath(file, frame);
10210
- if (uri === "" || uri.startsWith("../") || isAbsolute4(uri)) return null;
11192
+ if (uri === "" || uri.startsWith("../") || isAbsolute6(uri)) return null;
10211
11193
  return uri;
10212
11194
  }
10213
11195
  function renderSarif(results, opts = {}) {
@@ -10221,12 +11203,13 @@ function renderSarif(results, opts = {}) {
10221
11203
  for (const r of results) {
10222
11204
  if (r.errors.length === 0) continue;
10223
11205
  if (r.file === STDIN_LABEL) continue;
10224
- const uri = artifactUri(r.file, pathFrame);
10225
- if (uri === null) {
10226
- dropped += r.errors.length;
10227
- continue;
10228
- }
11206
+ const docUri = artifactUri(r.file, pathFrame);
10229
11207
  for (const e of r.errors) {
11208
+ const uri = e.file != null ? artifactUri(e.file, pathFrame) : docUri;
11209
+ if (uri === null) {
11210
+ dropped += 1;
11211
+ continue;
11212
+ }
10230
11213
  const ruleId = ruleIdFor(e, frame);
10231
11214
  if (!rules.has(ruleId)) {
10232
11215
  rules.set(ruleId, {
@@ -10334,7 +11317,7 @@ function renderJunit(results, opts = {}) {
10334
11317
  }
10335
11318
  lines.push(`${open2}>`);
10336
11319
  for (const e of r.errors) {
10337
- const where = e.line != null ? ` (line ${e.line})` : "";
11320
+ const where = e.file != null ? ` (${e.file}${e.line != null ? `:${e.line}` : ""})` : e.line != null ? ` (line ${e.line})` : "";
10338
11321
  lines.push(
10339
11322
  ` <failure${attr("type", ruleIdFor(e, opts.frame))}${attr(
10340
11323
  "message",
@@ -10443,7 +11426,7 @@ function renderPretty(results, summary, opts = {}) {
10443
11426
  }
10444
11427
  lines.push(`${c.red("\u2717")} ${r.file}${forgiven}`);
10445
11428
  for (const e of r.errors) {
10446
- const loc = e.line != null ? c.dim(` (line ${e.line})`) : "";
11429
+ const loc = e.file != null ? c.dim(` (${e.file}${e.line != null ? `:${e.line}` : ""})`) : e.line != null ? c.dim(` (line ${e.line})`) : "";
10447
11430
  lines.push(
10448
11431
  ` ${c.cyan(fieldLabel(e.instancePath))} ${e.message}${loc} ${c.dim(
10449
11432
  `[${e.schema}]`
@@ -10467,7 +11450,7 @@ function renderGithub(results) {
10467
11450
  const lines = [];
10468
11451
  for (const r of results) {
10469
11452
  for (const e of r.errors) {
10470
- const params = [`file=${escapeWorkflowCommandProperty(r.file)}`];
11453
+ const params = [`file=${escapeWorkflowCommandProperty(e.file ?? r.file)}`];
10471
11454
  if (e.line != null) params.push(`line=${e.line}`);
10472
11455
  if (e.col != null) params.push(`col=${e.col}`);
10473
11456
  const msg = escapeWorkflowCommandMessage(
@@ -10746,6 +11729,10 @@ export {
10746
11729
  ToolError,
10747
11730
  programName,
10748
11731
  setProgramName,
11732
+ externalMetadataUrlProblem,
11733
+ parseCollections,
11734
+ selectCollections,
11735
+ isMember,
10749
11736
  findConfigFile,
10750
11737
  readConfigFile,
10751
11738
  escapeWorkflowCommandMessage,
@@ -10766,22 +11753,37 @@ export {
10766
11753
  fetchSchemaBytes,
10767
11754
  schemaLoadOptions,
10768
11755
  loadSchema,
11756
+ DEFAULT_SCHEMAS,
11757
+ schemaEntryRef,
11758
+ collectSchemaPins,
11759
+ rebaseConfigSchemaRefs,
11760
+ resolveSchemaSet,
11761
+ EXTERNAL_METADATA_FETCH_TIMEOUT_MS,
11762
+ EXTERNAL_METADATA_FETCH_MAX_BYTES,
11763
+ fetchExternalMetadata,
10769
11764
  locateFrontmatter,
10770
11765
  frontmatterInnerText,
10771
11766
  extractFrontmatter,
10772
11767
  applyFrontmatter,
10773
11768
  extractorForExtension,
10774
11769
  supportedExtensions,
10775
- DEFAULT_SCHEMAS,
10776
- schemaEntryRef,
10777
- collectSchemaPins,
10778
- rebaseConfigSchemaRefs,
10779
- resolveSchemaSet,
11770
+ EXTERNAL_OWNED_SCHEMA,
11771
+ EXTERNAL_DUPLICATE_SCHEMA,
11772
+ EXTERNAL_KEYWORD,
11773
+ PATH_JOIN,
11774
+ externalMetadataJoin,
11775
+ loadExternalMetadata,
11776
+ mergeExternalMetadata,
11777
+ externalMetadataPointer,
11778
+ orphanEntries,
11779
+ orphanJoins,
11780
+ orphanError,
10780
11781
  parseConfig,
10781
11782
  loadConfig,
10782
11783
  resolveRunConfig,
10783
- Validator,
10784
11784
  isParamName,
11785
+ memberOf,
11786
+ Validator,
10785
11787
  rowsToFindings,
10786
11788
  runValidate,
10787
11789
  runGet,
@@ -10817,4 +11819,4 @@ export {
10817
11819
  renderQuery,
10818
11820
  renderQueryCsv
10819
11821
  };
10820
- //# sourceMappingURL=chunk-2OALUJV2.js.map
11822
+ //# sourceMappingURL=chunk-L7HN3AWA.js.map