@hawkeyexl/manni 0.3.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@hawkeyexl/manni",
6
- version: "0.3.0",
6
+ version: "1.0.1",
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. */
@@ -126,6 +396,19 @@ var ToolError = class extends Error {
126
396
  this.name = "ToolError";
127
397
  }
128
398
  };
399
+ function errorMessage(err) {
400
+ if (err instanceof Error) return err.message;
401
+ if (typeof err === "string") return err;
402
+ if (typeof err === "object" && err !== null && "message" in err && typeof err.message === "string") {
403
+ return err.message;
404
+ }
405
+ try {
406
+ const json = JSON.stringify(err);
407
+ if (json !== void 0) return json;
408
+ } catch {
409
+ }
410
+ return String(err);
411
+ }
129
412
 
130
413
  // src/meta/types.ts
131
414
  var DocmetaError = class extends ToolError {
@@ -2617,9 +2900,9 @@ function assertPublishableBuiltinId(id) {
2617
2900
  `Built-in id "${id}" is not publishable: the "check" first segment is reserved for corpus check identities (check:<name>, proposal 0026).`
2618
2901
  );
2619
2902
  }
2620
- if (first === "sidecar") {
2903
+ if (first === "external") {
2621
2904
  throw new Error(
2622
- `Built-in id "${id}" is not publishable: the "sidecar" first segment is reserved for sidecar finding identities (sidecar:owned, proposal 0037).`
2905
+ `Built-in id "${id}" is not publishable: the "external" first segment is reserved for external-metadata finding identities (external:owned, external:duplicate, proposal 0037).`
2623
2906
  );
2624
2907
  }
2625
2908
  }
@@ -2843,7 +3126,7 @@ async function requestSchema(ref, timeoutMs, timedOut) {
2843
3126
  } catch (err) {
2844
3127
  if (isAbort(err)) throw timedOut;
2845
3128
  throw new RetryableFetchError(
2846
- `Failed to fetch schema "${ref}": ${err.message}`
3129
+ `Failed to fetch schema "${ref}": ${errorMessage(err)}`
2847
3130
  );
2848
3131
  }
2849
3132
  if (!res.ok) {
@@ -2875,7 +3158,7 @@ async function fetchSchemaBytes(ref, options = {}) {
2875
3158
  if (err instanceof DocmetaError) throw err;
2876
3159
  if (isAbort(err)) throw timedOut;
2877
3160
  throw new DocmetaError(
2878
- `Failed to fetch schema "${ref}": ${err.message}`
3161
+ `Failed to fetch schema "${ref}": ${errorMessage(err)}`
2879
3162
  );
2880
3163
  }
2881
3164
  const raw = stripBom(bytes.toString("utf8"));
@@ -2884,7 +3167,7 @@ async function fetchSchemaBytes(ref, options = {}) {
2884
3167
  parsed2 = JSON.parse(raw);
2885
3168
  } catch (err) {
2886
3169
  throw new DocmetaError(
2887
- `Schema "${ref}" did not return valid JSON: ${err.message}`
3170
+ `Schema "${ref}" did not return valid JSON: ${errorMessage(err)}`
2888
3171
  );
2889
3172
  }
2890
3173
  return { bytes, schema: assertFetchedSchema(ref, parsed2, raw) };
@@ -2997,14 +3280,13 @@ async function loadSchema(ref, options = {}) {
2997
3280
  return JSON.parse(text);
2998
3281
  } catch (err) {
2999
3282
  throw new DocmetaError(
3000
- `Schema file "${ref}" is not valid JSON: ${withoutFileExcerpt(err.message, text)}`
3283
+ `Schema file "${ref}" is not valid JSON: ${withoutFileExcerpt(errorMessage(err), text)}`
3001
3284
  );
3002
3285
  }
3003
3286
  }
3004
3287
 
3005
3288
  // src/meta/core/resolve-schema.ts
3006
3289
  import { isAbsolute, relative, resolve } from "node:path";
3007
- import picomatch from "picomatch";
3008
3290
  var DEFAULT_SCHEMAS = Object.freeze([
3009
3291
  "google:okf:0.1",
3010
3292
  "passo-uno:seven-action:1.0"
@@ -3044,21 +3326,15 @@ function rebaseConfigSchemaRefs(config, configDir, cwd) {
3044
3326
  } : {}
3045
3327
  };
3046
3328
  }
3047
- var matcherCache = /* @__PURE__ */ new Map();
3048
3329
  function overrideGlobs(files) {
3330
+ if (files === void 0) return [];
3049
3331
  return typeof files === "string" ? [files] : files;
3050
3332
  }
3051
- function matchesFileGlob(glob, filePath) {
3052
- return matches(glob, filePath);
3053
- }
3054
- function matches(glob, filePath) {
3055
- const key = JSON.stringify(glob);
3056
- let m = matcherCache.get(key);
3057
- if (!m) {
3058
- m = picomatch(glob, { dot: true });
3059
- matcherCache.set(key, m);
3333
+ function overrideMatches(override, filePath, memberOf2) {
3334
+ if (override.collection !== void 0) {
3335
+ return memberOf2.includes(override.collection);
3060
3336
  }
3061
- return m(filePath.replace(/\\/g, "/"));
3337
+ return override.files !== void 0 && matchesFileGlob(filePath, override.files);
3062
3338
  }
3063
3339
  function coerceFileSchema(value) {
3064
3340
  if (value === void 0 || value === null) return void 0;
@@ -3136,8 +3412,9 @@ function resolveSchemaSetWithSource(params) {
3136
3412
  }
3137
3413
  }
3138
3414
  if (config?.overrides) {
3415
+ const memberOf2 = params.memberOf ?? [];
3139
3416
  for (const [i, ov] of config.overrides.entries()) {
3140
- if (matches(ov.files, filePath) && ov.schemas.length > 0) {
3417
+ if (overrideMatches(ov, filePath, memberOf2) && ov.schemas.length > 0) {
3141
3418
  return { schemas: dedupe(ov.schemas), source: "override", overrideIndex: i };
3142
3419
  }
3143
3420
  }
@@ -3153,55 +3430,37 @@ function resolveSchemaSetWithSource(params) {
3153
3430
  function resolveSchemaSet(params) {
3154
3431
  return resolveSchemaSetWithSource(params).schemas;
3155
3432
  }
3156
- function resolveElements(filePath, config) {
3433
+ function resolveElements(filePath, config, memberOf2 = []) {
3157
3434
  const out = [];
3158
3435
  if (config?.elements) out.push(...config.elements);
3159
3436
  for (const ov of config?.overrides ?? []) {
3160
- if (ov.elements && matches(ov.files, filePath)) out.push(...ov.elements);
3437
+ if (ov.elements && overrideMatches(ov, filePath, memberOf2)) {
3438
+ out.push(...ov.elements);
3439
+ }
3161
3440
  }
3162
3441
  return dedupe(out);
3163
3442
  }
3164
3443
 
3165
- // src/meta/core/sidecar-fetch.ts
3166
- var SIDECAR_FETCH_TIMEOUT_MS = 1e4;
3167
- var SIDECAR_FETCH_MAX_BYTES = 5 * 1024 * 1024;
3444
+ // src/meta/core/external-metadata-fetch.ts
3445
+ var EXTERNAL_METADATA_FETCH_TIMEOUT_MS = 1e4;
3446
+ var EXTERNAL_METADATA_FETCH_MAX_BYTES = 5 * 1024 * 1024;
3168
3447
  var MAX_REDIRECTS = 5;
3169
3448
  var RETRY_DELAY_MS2 = 500;
3170
- function sidecarUrlProblem(url) {
3171
- let parsed2;
3172
- try {
3173
- parsed2 = new URL(url);
3174
- } catch {
3175
- return "is not a valid URL";
3176
- }
3177
- if (parsed2.username !== "" || parsed2.password !== "") {
3178
- return 'carries a credential in the URL; put the token in an environment variable and name it with "tokenEnv"';
3179
- }
3180
- if (parsed2.protocol === "https:") return null;
3181
- if (parsed2.protocol === "http:" && isLoopback(parsed2.hostname)) return null;
3182
- if (parsed2.protocol === "http:") {
3183
- return "is plain http://; a bearer token over plaintext is a leak, and a public manifest is served over https too";
3184
- }
3185
- return `has the unsupported scheme "${parsed2.protocol}"`;
3186
- }
3187
- function isLoopback(host) {
3188
- return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1" || host.startsWith("127.");
3189
- }
3190
- async function fetchSidecar(url, opts = {}) {
3449
+ async function fetchExternalMetadata(url, opts = {}) {
3191
3450
  if (opts.offline) {
3192
3451
  throw new DocmetaError(
3193
- `Sidecar manifest ${url} is remote and the run is offline. Vendor it to a path, or drop --offline.`
3452
+ `Manifest ${url} is remote and the run is offline. Vendor it to a path, or drop --offline.`
3194
3453
  );
3195
3454
  }
3196
- const timeoutMs = opts.timeoutMs ?? SIDECAR_FETCH_TIMEOUT_MS;
3197
- const maxBytes = opts.maxBytes ?? SIDECAR_FETCH_MAX_BYTES;
3455
+ const timeoutMs = opts.timeoutMs ?? EXTERNAL_METADATA_FETCH_TIMEOUT_MS;
3456
+ const maxBytes = opts.maxBytes ?? EXTERNAL_METADATA_FETCH_MAX_BYTES;
3198
3457
  const env = opts.env ?? process.env;
3199
3458
  let token;
3200
3459
  if (opts.tokenEnv !== void 0) {
3201
3460
  token = env[opts.tokenEnv];
3202
3461
  if (token === void 0 || token === "") {
3203
3462
  throw new DocmetaError(
3204
- `Sidecar manifest ${url}: the environment variable ${opts.tokenEnv} named by "tokenEnv" is not set.`
3463
+ `Manifest ${url}: the environment variable ${opts.tokenEnv} named by "tokenEnv" is not set.`
3205
3464
  );
3206
3465
  }
3207
3466
  }
@@ -3212,13 +3471,13 @@ async function fetchSidecar(url, opts = {}) {
3212
3471
  }
3213
3472
  if (res instanceof Error) {
3214
3473
  throw new DocmetaError(
3215
- `Sidecar manifest ${url} could not be fetched: ${res.message}`
3474
+ `Manifest ${url} could not be fetched: ${res.message}`
3216
3475
  );
3217
3476
  }
3218
3477
  if (!res.ok) {
3219
3478
  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)" : "";
3220
3479
  throw new DocmetaError(
3221
- `Sidecar manifest ${url} could not be fetched: HTTP ${res.status}${hint}.`
3480
+ `Manifest ${url} could not be fetched: HTTP ${res.status}${hint}.`
3222
3481
  );
3223
3482
  }
3224
3483
  return readCapped(url, res, maxBytes);
@@ -3265,7 +3524,7 @@ async function readCapped(url, res, maxBytes) {
3265
3524
  if (total > maxBytes) {
3266
3525
  await reader.cancel();
3267
3526
  throw new DocmetaError(
3268
- `Sidecar manifest ${url} is too large: the response exceeds the ${maxBytes}-byte limit.`
3527
+ `Manifest ${url} is too large: the response exceeds the ${maxBytes}-byte limit.`
3269
3528
  );
3270
3529
  }
3271
3530
  chunks.push(value);
@@ -4933,7 +5192,7 @@ function applyXml(original, patch, options = {}) {
4933
5192
  before = readXml(original, options.filePath, { elements: options.elements });
4934
5193
  } catch (err) {
4935
5194
  throw new DocmetaError(
4936
- `Refusing to write metadata: ${err.message}`
5195
+ `Refusing to write metadata: ${errorMessage(err)}`
4937
5196
  );
4938
5197
  }
4939
5198
  const content = before.body;
@@ -5068,7 +5327,7 @@ function verify2(next, before, patch, options) {
5068
5327
  }).data;
5069
5328
  } catch (err) {
5070
5329
  throw new DocmetaError(
5071
- `Refusing to write XML metadata: the rewritten document did not parse (${err.message}).`
5330
+ `Refusing to write XML metadata: the rewritten document did not parse (${errorMessage(err)}).`
5072
5331
  );
5073
5332
  }
5074
5333
  if (!deepEqual(actual, expected)) {
@@ -5458,7 +5717,7 @@ function listFormats() {
5458
5717
  }));
5459
5718
  }
5460
5719
 
5461
- // src/meta/core/sidecars.ts
5720
+ // src/meta/core/external-metadata.ts
5462
5721
  import { readFile as readFile3 } from "node:fs/promises";
5463
5722
  import { isAbsolute as isAbsolute2, relative as relative4, resolve as resolve5, sep as sep2 } from "node:path";
5464
5723
  import {
@@ -5473,7 +5732,7 @@ import {
5473
5732
  import { stat as stat3 } from "node:fs/promises";
5474
5733
  import { extname, relative as relative3, resolve as resolve4 } from "node:path";
5475
5734
  import fg from "fast-glob";
5476
- import picomatch2 from "picomatch";
5735
+ import picomatch3 from "picomatch";
5477
5736
 
5478
5737
  // src/meta/core/gitignore.ts
5479
5738
  import { spawn } from "node:child_process";
@@ -5648,7 +5907,7 @@ async function resolveTargetSet(opts) {
5648
5907
  for (const f of found2) if (keepByExt(f)) walked.add(f);
5649
5908
  continue;
5650
5909
  }
5651
- if (!picomatch2.scan(posixInput).isGlob) {
5910
+ if (!picomatch3.scan(posixInput).isGlob) {
5652
5911
  missing.push(posixInput);
5653
5912
  continue;
5654
5913
  }
@@ -5708,44 +5967,50 @@ function assertNonEmpty(p) {
5708
5967
  );
5709
5968
  }
5710
5969
 
5711
- // src/meta/core/sidecars.ts
5712
- var SIDECAR_OWNED_SCHEMA = "sidecar:owned";
5713
- var SIDECAR_DUPLICATE_SCHEMA = "sidecar:duplicate";
5714
- var SIDECAR_KEYWORD = "sidecar";
5970
+ // src/meta/core/external-metadata.ts
5971
+ var EXTERNAL_OWNED_SCHEMA = "external:owned";
5972
+ var EXTERNAL_DUPLICATE_SCHEMA = "external:duplicate";
5973
+ var EXTERNAL_KEYWORD = "external";
5715
5974
  var PATH_JOIN = "path";
5716
5975
  var posix = (p) => p.split(sep2).join("/");
5717
5976
  function reportedPath(abs, base) {
5718
5977
  const rel = relative4(base, abs);
5719
5978
  return rel === "" ? "." : posix(rel);
5720
5979
  }
5721
- function sidecarJoin(sidecar) {
5722
- return sidecar.join ?? PATH_JOIN;
5980
+ function externalMetadataJoin(manifest) {
5981
+ return manifest.join ?? PATH_JOIN;
5723
5982
  }
5724
- async function loadSidecars(config, opts) {
5725
- const configured = config?.sidecars;
5726
- if (!configured || configured.length === 0) return null;
5983
+ async function loadExternalMetadata(collections, opts) {
5984
+ const configured = collections.flatMap(
5985
+ (c) => c.externalMetadata.map((m) => ({ collection: c.name, manifest: m }))
5986
+ );
5987
+ if (configured.length === 0) return null;
5727
5988
  const owners = /* @__PURE__ */ new Map();
5728
5989
  const byPath = /* @__PURE__ */ new Map();
5729
5990
  const byField = /* @__PURE__ */ new Map();
5730
5991
  const entries = [];
5731
- for (const sidecar of configured) {
5992
+ for (const { collection, manifest } of configured) {
5732
5993
  let text;
5733
5994
  let file;
5734
- if (classifyRef(sidecar.file).kind === "url") {
5735
- file = sidecar.file;
5736
- text = await fetchSidecar(sidecar.file, {
5737
- ...sidecar.tokenEnv !== void 0 ? { tokenEnv: sidecar.tokenEnv } : {},
5995
+ if (classifyRef(manifest.file).kind === "url") {
5996
+ file = manifest.file;
5997
+ text = await fetchExternalMetadata(manifest.file, {
5998
+ ...manifest.tokenEnv !== void 0 ? { tokenEnv: manifest.tokenEnv } : {},
5738
5999
  ...opts.offline !== void 0 ? { offline: opts.offline } : {},
5739
6000
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
5740
6001
  ...opts.env !== void 0 ? { env: opts.env } : {}
5741
6002
  });
5742
6003
  } else {
5743
- const abs = isAbsolute2(sidecar.file) ? sidecar.file : resolve5(opts.configDir, sidecar.file);
6004
+ const abs = isAbsolute2(manifest.file) ? manifest.file : resolve5(opts.configDir, manifest.file);
5744
6005
  file = reportedPath(abs, opts.base);
5745
6006
  text = await readManifest(abs, file);
5746
6007
  }
5747
- for (const key of sidecar.keys) owners.set(key, file);
5748
- const join7 = sidecarJoin(sidecar);
6008
+ for (const key of manifest.keys) {
6009
+ const list = owners.get(key);
6010
+ if (list) list.push({ collection, file });
6011
+ else owners.set(key, [{ collection, file }]);
6012
+ }
6013
+ const join7 = externalMetadataJoin(manifest);
5749
6014
  let target;
5750
6015
  if (join7 === PATH_JOIN) {
5751
6016
  target = byPath;
@@ -5757,7 +6022,16 @@ async function loadSidecars(config, opts) {
5757
6022
  byField.set(join7, target);
5758
6023
  }
5759
6024
  }
5760
- parseManifest(sidecar, text, file, opts.configDir, join7, target, entries);
6025
+ parseManifest(
6026
+ manifest,
6027
+ collection,
6028
+ text,
6029
+ file,
6030
+ opts.configDir,
6031
+ join7,
6032
+ target,
6033
+ entries
6034
+ );
5761
6035
  }
5762
6036
  return { owners, byPath, byField, entries };
5763
6037
  }
@@ -5767,27 +6041,27 @@ async function readManifest(abs, file) {
5767
6041
  } catch (err) {
5768
6042
  const reason = err instanceof Error ? err.message : String(err);
5769
6043
  throw new DocmetaError(
5770
- `Sidecar manifest ${file} could not be read: ${reason}`
6044
+ `Manifest ${file} could not be read: ${reason}`
5771
6045
  );
5772
6046
  }
5773
6047
  }
5774
- function parseManifest(sidecar, text, file, configDir, join7, target, entries) {
6048
+ function parseManifest(manifest, collection, text, file, configDir, join7, target, entries) {
5775
6049
  const lc = new LineCounter2();
5776
6050
  const doc = parseDocument3(text, { lineCounter: lc, uniqueKeys: false });
5777
6051
  const problem = doc.errors[0];
5778
6052
  if (problem) {
5779
6053
  throw new DocmetaError(
5780
- `Sidecar manifest ${file} is not valid YAML: ${problem.message}`
6054
+ `Manifest ${file} is not valid YAML: ${problem.message}`
5781
6055
  );
5782
6056
  }
5783
6057
  const root = doc.contents;
5784
6058
  if (root === null || isScalar2(root) && root.value === null) return;
5785
6059
  if (!isMap3(root)) {
5786
6060
  throw new DocmetaError(
5787
- `Sidecar manifest ${file}: the manifest must be a mapping from document ${join7 === PATH_JOIN ? "path" : `"${join7}"`} to owned keys.`
6061
+ `Manifest ${file}: the manifest must be a mapping from document ${join7 === PATH_JOIN ? "path" : `"${join7}"`} to owned keys.`
5788
6062
  );
5789
6063
  }
5790
- const owned = new Set(sidecar.keys);
6064
+ const owned = new Set(manifest.keys);
5791
6065
  const lineAt = (node) => {
5792
6066
  const range = isNode(node) ? node.range : void 0;
5793
6067
  return range ? lc.linePos(range[0]).line : void 0;
@@ -5800,13 +6074,13 @@ function parseManifest(sidecar, text, file, configDir, join7, target, entries) {
5800
6074
  if (seen.has(spelled)) {
5801
6075
  const first = seen.get(spelled);
5802
6076
  throw new DocmetaError(
5803
- `Sidecar manifest ${where}: "${spelled}" is named twice (first at line ${first ?? "?"}). Merge the two entries into one.`
6077
+ `Manifest ${where}: "${spelled}" is named twice (first at line ${first ?? "?"}). Merge the two entries into one.`
5804
6078
  );
5805
6079
  }
5806
6080
  seen.set(spelled, entryLine);
5807
6081
  if (!isMap3(pair.value)) {
5808
6082
  throw new DocmetaError(
5809
- `Sidecar manifest ${where}: "${spelled}" must be a mapping of owned keys to values.`
6083
+ `Manifest ${where}: "${spelled}" must be a mapping of owned keys to values.`
5810
6084
  );
5811
6085
  }
5812
6086
  const indexKey = join7 === PATH_JOIN ? resolve5(configDir, spelled) : spelled;
@@ -5815,20 +6089,26 @@ function parseManifest(sidecar, text, file, configDir, join7, target, entries) {
5815
6089
  const key = isScalar2(kv.key) ? String(kv.key.value) : String(kv.key);
5816
6090
  if (key === FILE_SCHEMA_KEY) {
5817
6091
  throw new DocmetaError(
5818
- `Sidecar manifest ${where}: "${spelled}" sets "${FILE_SCHEMA_KEY}" \u2014 a sidecar never chooses the schema a document is judged by; use "overrides" in the config.`
6092
+ `Manifest ${where}: "${spelled}" sets "${FILE_SCHEMA_KEY}" \u2014 a manifest never chooses the schema a document is judged by; use "overrides" in the config.`
5819
6093
  );
5820
6094
  }
5821
6095
  if (!owned.has(key)) {
5822
6096
  throw new DocmetaError(
5823
- `Sidecar manifest ${where}: "${spelled}" sets "${key}", which this sidecar does not own. Add it to the sidecar's "keys", or remove it from the entry.`
6097
+ `Manifest ${where}: "${spelled}" sets "${key}", which this manifest does not own. Add it to the manifest's "keys", or remove it from the entry.`
5824
6098
  );
5825
6099
  }
5826
6100
  const value = kv.value === null ? null : kv.value.toJS(doc, { maxAliasCount: 100 });
5827
6101
  const line = lineAt(kv.key);
5828
- values.set(key, { value, file, ...line === void 0 ? {} : { line } });
6102
+ values.set(key, {
6103
+ value,
6104
+ collection,
6105
+ file,
6106
+ ...line === void 0 ? {} : { line }
6107
+ });
5829
6108
  }
5830
6109
  target.set(indexKey, values);
5831
6110
  entries.push({
6111
+ collection,
5832
6112
  join: join7,
5833
6113
  ...join7 === PATH_JOIN ? { abs: indexKey } : {},
5834
6114
  spelled,
@@ -5843,13 +6123,19 @@ function joinValue(raw) {
5843
6123
  if (typeof raw === "boolean") return String(raw);
5844
6124
  return void 0;
5845
6125
  }
5846
- function mergeSidecars(label, extracted, index, base) {
6126
+ function mergeExternalMetadata(label, extracted, index, memberOf2, base) {
5847
6127
  const none = () => void 0;
5848
- if (!index) return { extracted, collisions: [], joins: [], locate: none };
6128
+ if (!index || memberOf2.length === 0) {
6129
+ return { extracted, collisions: [], joins: [], locate: none };
6130
+ }
6131
+ const mine = (collection) => memberOf2.includes(collection);
5849
6132
  const collisions = [];
5850
6133
  for (const key of Object.keys(extracted.data)) {
5851
- const file = index.owners.get(key);
5852
- if (file !== void 0) collisions.push({ key, file });
6134
+ for (const owner of index.owners.get(key) ?? []) {
6135
+ if (mine(owner.collection)) {
6136
+ collisions.push({ key, file: owner.file, collection: owner.collection });
6137
+ }
6138
+ }
5853
6139
  }
5854
6140
  const sources = [];
5855
6141
  const joins = [];
@@ -5863,8 +6149,23 @@ function mergeSidecars(label, extracted, index, base) {
5863
6149
  const hit = byValue.get(value);
5864
6150
  if (!hit) continue;
5865
6151
  sources.push(hit);
5866
- const first = hit.values().next().value;
5867
- joins.push({ field, value, file: first?.file ?? "manifest" });
6152
+ const first = [...hit.values()].find((sv) => mine(sv.collection));
6153
+ if (first) {
6154
+ joins.push({ field, value, file: first.file, collection: first.collection });
6155
+ }
6156
+ }
6157
+ for (const supplied of sources) {
6158
+ for (const key of supplied.keys()) {
6159
+ const owners = (index.owners.get(key) ?? []).filter(
6160
+ (o) => mine(o.collection)
6161
+ );
6162
+ const [a, b] = owners;
6163
+ if (a && b) {
6164
+ throw new DocmetaError(
6165
+ `${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.`
6166
+ );
6167
+ }
6168
+ }
5868
6169
  }
5869
6170
  if (sources.length === 0) {
5870
6171
  return { extracted, collisions, joins, locate: none };
@@ -5873,6 +6174,7 @@ function mergeSidecars(label, extracted, index, base) {
5873
6174
  const merged = /* @__PURE__ */ new Map();
5874
6175
  for (const supplied of sources) {
5875
6176
  for (const [key, sv] of supplied) {
6177
+ if (!mine(sv.collection)) continue;
5876
6178
  if (key in extracted.data) continue;
5877
6179
  data[key] = sv.value;
5878
6180
  merged.set(key, sv);
@@ -5893,20 +6195,23 @@ function topLevelKey(pointer) {
5893
6195
  const seg = pointer.slice(1).split("/")[0] ?? "";
5894
6196
  return seg.replace(/~1/g, "/").replace(/~0/g, "~");
5895
6197
  }
5896
- function sidecarPointer(key) {
6198
+ function externalMetadataPointer(key) {
5897
6199
  return `/${escapePointerSegment(key)}`;
5898
6200
  }
5899
- function orphanEntries(index, loaded, base) {
6201
+ function orphanEntries(index, loaded, base, collections) {
5900
6202
  if (!index) return [];
5901
6203
  const have = new Set(loaded.map((l) => resolve5(base, l)));
5902
6204
  return index.entries.filter(
5903
- (e) => e.join === PATH_JOIN && e.abs !== void 0 && !have.has(e.abs)
6205
+ (e) => inCollections(e, collections) && e.join === PATH_JOIN && e.abs !== void 0 && !have.has(e.abs)
5904
6206
  );
5905
6207
  }
5906
- function orphanJoins(index, matched) {
6208
+ function inCollections(entry, collections) {
6209
+ return collections === void 0 || collections.includes(entry.collection);
6210
+ }
6211
+ function orphanJoins(index, matched, collections) {
5907
6212
  if (!index) return [];
5908
6213
  return index.entries.filter(
5909
- (e) => e.join !== PATH_JOIN && !(matched.get(e.join)?.has(e.spelled) ?? false)
6214
+ (e) => inCollections(e, collections) && e.join !== PATH_JOIN && !(matched.get(e.join)?.has(e.spelled) ?? false)
5910
6215
  );
5911
6216
  }
5912
6217
  function orphanError(orphans) {
@@ -5916,7 +6221,7 @@ function orphanError(orphans) {
5916
6221
  const more = orphans.length > 1 ? ` (${orphans.length - 1} more)` : "";
5917
6222
  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`;
5918
6223
  return new DocmetaError(
5919
- `Sidecar manifest ${where} ${what}${more}. Fix the entry, or remove it.`
6224
+ `Manifest ${where} ${what}${more}. Fix the entry, or remove it.`
5920
6225
  );
5921
6226
  }
5922
6227
 
@@ -5961,7 +6266,7 @@ function parseMapping(text, source, opts) {
5961
6266
  try {
5962
6267
  raw = parseYaml(text);
5963
6268
  } catch (err) {
5964
- throw opts.toError(`${source}: invalid YAML: ${err.message}`);
6269
+ throw opts.toError(`${source}: invalid YAML: ${errorMessage(err)}`);
5965
6270
  }
5966
6271
  if (raw == null) return null;
5967
6272
  if (typeof raw !== "object" || Array.isArray(raw)) {
@@ -5969,13 +6274,22 @@ function parseMapping(text, source, opts) {
5969
6274
  }
5970
6275
  return raw;
5971
6276
  }
6277
+ var COLLECTIONS_KEY = "collections";
5972
6278
  function slice(document, opts) {
5973
6279
  const { doc } = document;
5974
6280
  if (doc !== null && Object.hasOwn(doc, opts.section)) {
5975
6281
  return { value: doc[opts.section] ?? null, wrapped: true };
5976
6282
  }
6283
+ if (doc !== null && Object.hasOwn(doc, COLLECTIONS_KEY)) {
6284
+ return { value: null, wrapped: true };
6285
+ }
5977
6286
  return null;
5978
6287
  }
6288
+ function collectionsOf(document, source, opts) {
6289
+ const { doc } = document;
6290
+ if (doc === null || !Object.hasOwn(doc, COLLECTIONS_KEY)) return [];
6291
+ return parseCollections(doc[COLLECTIONS_KEY], source, opts.toError);
6292
+ }
5979
6293
  function relativeSource(cwd, path) {
5980
6294
  return relative5(cwd, path).replace(/\\/g, "/");
5981
6295
  }
@@ -5999,7 +6313,15 @@ async function findConfigFile(cwd, opts) {
5999
6313
  `"${name2}" is the pre-rename name of the family config file. Rename it to "${FAMILY_CONFIG_NAMES[0] ?? "manni.config.yaml"}".`
6000
6314
  );
6001
6315
  }
6002
- return { path, dir, source, text: document.text, kind, ...found };
6316
+ return {
6317
+ path,
6318
+ dir,
6319
+ source,
6320
+ text: document.text,
6321
+ kind,
6322
+ collections: collectionsOf(document, source, opts),
6323
+ ...found
6324
+ };
6003
6325
  }
6004
6326
  }
6005
6327
  for (const name2 of opts.legacyNames) {
@@ -6008,7 +6330,7 @@ async function findConfigFile(cwd, opts) {
6008
6330
  const document = await readDocument(path, source, opts);
6009
6331
  if (document === null) continue;
6010
6332
  warn(
6011
- `"${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"}".`
6333
+ `"${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.`
6012
6334
  );
6013
6335
  return {
6014
6336
  path,
@@ -6017,7 +6339,8 @@ async function findConfigFile(cwd, opts) {
6017
6339
  text: document.text,
6018
6340
  value: document.doc,
6019
6341
  wrapped: false,
6020
- kind: "legacy"
6342
+ kind: "legacy",
6343
+ collections: []
6021
6344
  };
6022
6345
  }
6023
6346
  }
@@ -6039,6 +6362,7 @@ async function readConfigFile(explicitPath, cwd, opts) {
6039
6362
  source: explicitPath,
6040
6363
  text: document.text,
6041
6364
  kind: "explicit",
6365
+ collections: collectionsOf(document, explicitPath, opts),
6042
6366
  ...found
6043
6367
  };
6044
6368
  }
@@ -6054,14 +6378,13 @@ function asElementPaths(value, where, source) {
6054
6378
  try {
6055
6379
  parseElementPath(path);
6056
6380
  } catch (err) {
6057
- throw new DocmetaError(`${source}: ${where} \u2014 ${err.message}`);
6381
+ throw new DocmetaError(`${source}: ${where} \u2014 ${errorMessage(err)}`);
6058
6382
  }
6059
6383
  }
6060
6384
  return list;
6061
6385
  }
6062
- var OVERRIDE_KEYS = ["name", "files", "schemas", "elements"];
6386
+ var OVERRIDE_KEYS = ["collection", "files", "schemas", "elements"];
6063
6387
  var CHECK_KEYS = ["name", "query"];
6064
- var SIDECAR_KEYS = ["file", "keys", "tokenEnv", "join"];
6065
6388
  var CHECK_NAME = /^[a-z0-9][a-z0-9._-]*$/;
6066
6389
  var RESERVED_CHECK_NAMES = /* @__PURE__ */ new Set(["query"]);
6067
6390
  var FILL_KEYS = [
@@ -6073,12 +6396,9 @@ var FILL_KEYS = [
6073
6396
  "concurrency"
6074
6397
  ];
6075
6398
  var CONFIG_KEYS = [
6076
- "paths",
6077
- "exclude",
6078
6399
  "schemas",
6079
6400
  "overrides",
6080
6401
  "checks",
6081
- "sidecars",
6082
6402
  "baseline",
6083
6403
  "allowEmpty",
6084
6404
  "respectGitignore",
@@ -6088,7 +6408,7 @@ var CONFIG_KEYS = [
6088
6408
  "fill",
6089
6409
  "elements"
6090
6410
  ];
6091
- function rejectUnknownKeys(raw, allowed, where, source) {
6411
+ function rejectUnknownKeys2(raw, allowed, where, source) {
6092
6412
  for (const key of Object.keys(raw)) {
6093
6413
  if (!allowed.includes(key)) {
6094
6414
  throw new DocmetaError(
@@ -6158,7 +6478,7 @@ function parseSchemaEntry(entry, where, source) {
6158
6478
  );
6159
6479
  }
6160
6480
  const raw = entry;
6161
- rejectUnknownKeys(raw, SCHEMA_ENTRY_KEYS, where, source);
6481
+ rejectUnknownKeys2(raw, SCHEMA_ENTRY_KEYS, where, source);
6162
6482
  if (typeof raw.ref !== "string" || raw.ref.trim() === "") {
6163
6483
  throw new DocmetaError(
6164
6484
  `${source}: ${where}.ref must be a non-empty schema reference string.`
@@ -6195,12 +6515,13 @@ function parseConfig(text, source) {
6195
6515
  raw = parseYaml2(text);
6196
6516
  } catch (err) {
6197
6517
  throw new DocmetaError(
6198
- `${source}: invalid YAML: ${err.message}`
6518
+ `${source}: invalid YAML: ${errorMessage(err)}`
6199
6519
  );
6200
6520
  }
6201
6521
  return parseConfigValue(raw, source);
6202
6522
  }
6203
6523
  function parseConfigValue(raw, source, section) {
6524
+ assertNoMovedKeys(raw, source);
6204
6525
  if (section === void 0) return parseConfigDocument(raw, source);
6205
6526
  try {
6206
6527
  return parseConfigDocument(raw, source);
@@ -6222,92 +6543,56 @@ function withSection(message, source, section) {
6222
6543
  if (/^[a-zA-Z]+(\[|\.| —)/.test(rest)) return `${head}${section}.${rest}`;
6223
6544
  return message;
6224
6545
  }
6225
- function parseSidecars(raw, source) {
6226
- if (!Array.isArray(raw)) {
6227
- throw new DocmetaError(`${source}: "sidecars" must be a list.`);
6228
- }
6229
- const owners = /* @__PURE__ */ new Map();
6230
- return raw.map((entry, i) => {
6231
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
6232
- throw new DocmetaError(`${source}: sidecars[${i}] must be a mapping.`);
6233
- }
6234
- const e = entry;
6235
- rejectUnknownKeys(e, SIDECAR_KEYS, `sidecars[${i}]`, source);
6236
- if (typeof e.file !== "string" || e.file.trim() === "") {
6546
+ var CONFIG_REF = "https://hawkeyexl.github.io/manni/meta/reference/configuration/";
6547
+ var MOVED_KEYS = [
6548
+ [
6549
+ "paths",
6550
+ `Document sets are declared once for every tool, under a top-level collections: list. See ${CONFIG_REF}#collections`
6551
+ ],
6552
+ [
6553
+ "exclude",
6554
+ `Document sets are declared once for every tool, under a top-level collections: list. See ${CONFIG_REF}#collections`
6555
+ ],
6556
+ [
6557
+ "sidecars",
6558
+ `It is externalMetadata on a collection, under the top-level collections: list. See ${CONFIG_REF}#external-metadata`
6559
+ ]
6560
+ ];
6561
+ function assertNoMovedKeys(raw, source) {
6562
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return;
6563
+ const obj = raw;
6564
+ for (const [key, remedy] of MOVED_KEYS) {
6565
+ if (Object.hasOwn(obj, key)) {
6237
6566
  throw new DocmetaError(
6238
- `${source}: sidecars[${i}].file must be a non-empty string naming the manifest, relative to the config file.`
6567
+ `${source}: "${key}" is no longer a meta key. ${remedy}`
6239
6568
  );
6240
6569
  }
6241
- if (!Array.isArray(e.keys) || e.keys.length === 0 || e.keys.some((k) => typeof k !== "string" || k.trim() === "")) {
6570
+ }
6571
+ if (!Array.isArray(obj.overrides)) return;
6572
+ for (const [i, entry] of obj.overrides.entries()) {
6573
+ if (typeof entry === "object" && entry !== null && !Array.isArray(entry) && Object.hasOwn(entry, "name")) {
6242
6574
  throw new DocmetaError(
6243
- `${source}: sidecars[${i}].keys must be a non-empty list of key names.`
6575
+ `${source}: overrides[${i}] no longer carries "name". Define a collection with that name and point the override at it with collection:.`
6244
6576
  );
6245
6577
  }
6246
- const isUrl = classifyRef(e.file).kind === "url";
6247
- if (isUrl) {
6248
- const problem = sidecarUrlProblem(e.file);
6249
- if (problem !== null) {
6250
- throw new DocmetaError(`${source}: sidecars[${i}].file ${problem}.`);
6251
- }
6252
- }
6253
- if (e.tokenEnv !== void 0) {
6254
- if (typeof e.tokenEnv !== "string" || e.tokenEnv.trim() === "") {
6255
- throw new DocmetaError(
6256
- `${source}: sidecars[${i}].tokenEnv must be the name of an environment variable.`
6257
- );
6258
- }
6259
- if (!isUrl) {
6260
- throw new DocmetaError(
6261
- `${source}: sidecars[${i}].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.`
6262
- );
6263
- }
6264
- }
6265
- const keys = e.keys;
6266
- if (e.join !== void 0) {
6267
- if (typeof e.join !== "string" || e.join.trim() === "") {
6268
- throw new DocmetaError(
6269
- `${source}: sidecars[${i}].join must be "path" or the name of a top-level frontmatter field.`
6270
- );
6271
- }
6272
- if (e.join === FILE_SCHEMA_KEY) {
6273
- throw new DocmetaError(
6274
- `${source}: sidecars[${i}].join may not be "${FILE_SCHEMA_KEY}".`
6275
- );
6276
- }
6277
- if (keys.includes(e.join)) {
6278
- throw new DocmetaError(
6279
- `${source}: sidecars[${i}].join names "${e.join}", which the same entry owns \u2014 the value that selects an entry cannot come from the entry.`
6280
- );
6281
- }
6282
- }
6283
- const seen = /* @__PURE__ */ new Set();
6284
- for (const key of keys) {
6285
- if (key === FILE_SCHEMA_KEY) {
6286
- throw new DocmetaError(
6287
- `${source}: sidecars[${i}].keys may not include "${FILE_SCHEMA_KEY}" \u2014 a sidecar never chooses the schema a document is judged by; use "overrides".`
6288
- );
6289
- }
6290
- if (seen.has(key)) {
6291
- throw new DocmetaError(
6292
- `${source}: sidecars[${i}].keys lists "${key}" twice.`
6293
- );
6294
- }
6295
- seen.add(key);
6296
- const owner = owners.get(key);
6297
- if (owner !== void 0) {
6298
- throw new DocmetaError(
6299
- `${source}: sidecars[${i}].keys claims "${key}", which sidecars[${owner}] already owns \u2014 a key has exactly one sidecar.`
6300
- );
6301
- }
6302
- owners.set(key, i);
6303
- }
6304
- return {
6305
- file: e.file,
6306
- keys,
6307
- ...typeof e.tokenEnv === "string" ? { tokenEnv: e.tokenEnv } : {},
6308
- ...typeof e.join === "string" ? { join: e.join } : {}
6309
- };
6310
- });
6578
+ }
6579
+ }
6580
+ function inSection(message, source, section) {
6581
+ return section === void 0 ? message : withSection(message, source, section);
6582
+ }
6583
+ function assertOverrideCollections(config, collections, source, section) {
6584
+ const names = new Set(collections.map((c) => c.name));
6585
+ for (const [i, ov] of (config.overrides ?? []).entries()) {
6586
+ if (ov.collection === void 0 || names.has(ov.collection)) continue;
6587
+ const defined = collections.length === 0 ? "(none)" : collections.map((c) => c.name).join(", ");
6588
+ throw new DocmetaError(
6589
+ inSection(
6590
+ `${source}: overrides[${i}].collection names "${ov.collection}", which collections: does not define. Defined: ${defined}.`,
6591
+ source,
6592
+ section
6593
+ )
6594
+ );
6595
+ }
6311
6596
  }
6312
6597
  function parseConfigDocument(raw, source) {
6313
6598
  if (raw == null) return {};
@@ -6315,76 +6600,55 @@ function parseConfigDocument(raw, source) {
6315
6600
  throw new DocmetaError(`${source}: top level must be a mapping.`);
6316
6601
  }
6317
6602
  const obj = raw;
6318
- rejectUnknownKeys(obj, CONFIG_KEYS, "the top level", source);
6603
+ rejectUnknownKeys2(obj, CONFIG_KEYS, "the top level", source);
6319
6604
  const config = {};
6320
- if (obj.paths !== void 0) config.paths = asStringList(obj.paths, "paths", source);
6321
- if (obj.exclude !== void 0)
6322
- config.exclude = asStringList(obj.exclude, "exclude", source);
6323
6605
  if (obj.schemas !== void 0)
6324
6606
  config.schemas = asSchemaList(obj.schemas, "schemas", source);
6325
6607
  if (obj.overrides !== void 0) {
6326
6608
  if (!Array.isArray(obj.overrides)) {
6327
6609
  throw new DocmetaError(`${source}: "overrides" must be a list.`);
6328
6610
  }
6329
- const seenNames = /* @__PURE__ */ new Set();
6330
6611
  config.overrides = obj.overrides.map((entry, i) => {
6331
6612
  if (typeof entry !== "object" || entry === null) {
6332
6613
  throw new DocmetaError(`${source}: overrides[${i}] must be a mapping.`);
6333
6614
  }
6334
6615
  const e = entry;
6335
- rejectUnknownKeys(e, OVERRIDE_KEYS, `overrides[${i}]`, source);
6336
- const files = asFileGlobs(e.files, `overrides[${i}].files`, source);
6337
- const schemas = e.schemas === void 0 ? [] : asStringList(e.schemas, `overrides[${i}].schemas`, source);
6338
- const elements = e.elements === void 0 ? void 0 : asElementPaths(e.elements, `overrides[${i}].elements`, source);
6339
- if (schemas.length === 0 && (elements === void 0 || elements.length === 0)) {
6616
+ rejectUnknownKeys2(e, OVERRIDE_KEYS, `overrides[${i}]`, source);
6617
+ const hasFiles = e.files !== void 0;
6618
+ const hasCollection = e.collection !== void 0;
6619
+ if (hasFiles === hasCollection) {
6340
6620
  throw new DocmetaError(
6341
- `${source}: overrides[${i}] sets neither "schemas" nor "elements", so it has no effect.`
6621
+ `${source}: overrides[${i}] must carry exactly one of "files" or "collection".`
6342
6622
  );
6343
6623
  }
6344
- let name2;
6345
- if (e.name !== void 0) {
6346
- if (typeof e.name !== "string" || e.name.trim() === "") {
6624
+ const files = hasFiles ? asFileGlobs(e.files, `overrides[${i}].files`, source) : void 0;
6625
+ let collection;
6626
+ if (hasCollection) {
6627
+ if (typeof e.collection !== "string" || e.collection.trim() === "") {
6347
6628
  throw new DocmetaError(
6348
- `${source}: overrides[${i}].name must be a non-empty string \u2014 it becomes the collection's view name.`
6629
+ `${source}: overrides[${i}].collection must be a non-empty string naming a collection.`
6349
6630
  );
6350
6631
  }
6351
- if (e.name.toLowerCase() === "docs") {
6352
- throw new DocmetaError(
6353
- `${source}: overrides[${i}].name "${e.name}" collides with the docs table every query reads. Pick another name.`
6354
- );
6355
- }
6356
- if (e.name.toLowerCase().startsWith("sqlite_")) {
6357
- throw new DocmetaError(
6358
- `${source}: overrides[${i}].name "${e.name}" starts with "sqlite_", which SQLite reserves for its own objects. Pick another name.`
6359
- );
6360
- }
6361
- if (schemas.length === 0) {
6362
- throw new DocmetaError(
6363
- `${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.`
6364
- );
6365
- }
6366
- if (seenNames.has(e.name.toLowerCase())) {
6367
- throw new DocmetaError(
6368
- `${source}: overrides[${i}] reuses the name "${e.name}"; collection names must be unique.`
6369
- );
6370
- }
6371
- seenNames.add(e.name.toLowerCase());
6372
- name2 = e.name;
6632
+ collection = e.collection;
6633
+ }
6634
+ const schemas = e.schemas === void 0 ? [] : asStringList(e.schemas, `overrides[${i}].schemas`, source);
6635
+ const elements = e.elements === void 0 ? void 0 : asElementPaths(e.elements, `overrides[${i}].elements`, source);
6636
+ if (schemas.length === 0 && (elements === void 0 || elements.length === 0)) {
6637
+ throw new DocmetaError(
6638
+ `${source}: overrides[${i}] sets neither "schemas" nor "elements", so it has no effect.`
6639
+ );
6373
6640
  }
6374
6641
  return {
6375
- files,
6642
+ ...files !== void 0 ? { files } : {},
6643
+ ...collection !== void 0 ? { collection } : {},
6376
6644
  schemas,
6377
- ...elements ? { elements } : {},
6378
- ...name2 !== void 0 ? { name: name2 } : {}
6645
+ ...elements ? { elements } : {}
6379
6646
  };
6380
6647
  });
6381
6648
  }
6382
6649
  if (obj.checks !== void 0) {
6383
6650
  config.checks = parseChecks(obj.checks, source);
6384
6651
  }
6385
- if (obj.sidecars !== void 0) {
6386
- config.sidecars = parseSidecars(obj.sidecars, source);
6387
- }
6388
6652
  if (obj.elements !== void 0) {
6389
6653
  config.elements = asElementPaths(obj.elements, "elements", source);
6390
6654
  }
@@ -6435,7 +6699,7 @@ function parseChecks(value, source) {
6435
6699
  );
6436
6700
  }
6437
6701
  const e = entry;
6438
- rejectUnknownKeys(e, CHECK_KEYS, `checks[${i}]`, source);
6702
+ rejectUnknownKeys2(e, CHECK_KEYS, `checks[${i}]`, source);
6439
6703
  if (typeof e.name !== "string" || e.name === "") {
6440
6704
  throw new DocmetaError(
6441
6705
  `${source}: checks[${i}].name must be a non-empty string.`
@@ -6470,7 +6734,7 @@ function parseSchemaCache(value, source) {
6470
6734
  throw new DocmetaError(`${source}: "schemaCache" must be a mapping.`);
6471
6735
  }
6472
6736
  const raw = value;
6473
- rejectUnknownKeys(raw, SCHEMA_CACHE_KEYS, '"schemaCache"', source);
6737
+ rejectUnknownKeys2(raw, SCHEMA_CACHE_KEYS, '"schemaCache"', source);
6474
6738
  const schemaCache = {};
6475
6739
  const ttl = raw.ttlHours;
6476
6740
  if (ttl !== void 0) {
@@ -6490,7 +6754,7 @@ function parseSchemaTrust(value, source) {
6490
6754
  );
6491
6755
  }
6492
6756
  const raw = value;
6493
- rejectUnknownKeys(raw, SCHEMA_TRUST_KEYS, '"schemaTrust"', source);
6757
+ rejectUnknownKeys2(raw, SCHEMA_TRUST_KEYS, '"schemaTrust"', source);
6494
6758
  const schemaTrust = {};
6495
6759
  const mode = raw.documentRefs;
6496
6760
  if (mode !== void 0) {
@@ -6517,7 +6781,7 @@ function parseFill(value, source) {
6517
6781
  throw new DocmetaError(`${source}: "fill" must be a mapping.`);
6518
6782
  }
6519
6783
  const raw = value;
6520
- rejectUnknownKeys(raw, FILL_KEYS, '"fill"', source);
6784
+ rejectUnknownKeys2(raw, FILL_KEYS, '"fill"', source);
6521
6785
  const fill = {};
6522
6786
  const asString = (key) => {
6523
6787
  const v = raw[key];
@@ -6560,29 +6824,318 @@ async function loadConfig(explicitPath, cwd = process.cwd()) {
6560
6824
  const file = explicitPath ? await readConfigFile(explicitPath, cwd, CONFIG_FILE) : await findConfigFile(cwd, CONFIG_FILE);
6561
6825
  if (file === null) return null;
6562
6826
  const section = file.wrapped ? META_SECTION : void 0;
6827
+ const config = parseConfigValue(file.value, file.source, section);
6828
+ assertOverrideCollections(config, file.collections, file.source, section);
6563
6829
  return {
6564
- config: parseConfigValue(file.value, file.source, section),
6830
+ config,
6565
6831
  path: file.path,
6566
6832
  dir: file.dir,
6833
+ source: file.source,
6834
+ collections: file.collections,
6567
6835
  ...section !== void 0 ? { section } : {}
6568
6836
  };
6569
6837
  }
6838
+ var STDIN_TOKEN_LITERAL = "-";
6570
6839
  async function resolveRunConfig(opts) {
6571
6840
  const cwd = opts.cwd ?? process.cwd();
6841
+ const wanted = opts.collections ?? [];
6842
+ if (wanted.length > 0) {
6843
+ const typed = opts.inputs.filter((i) => i !== STDIN_TOKEN_LITERAL);
6844
+ if (typed.length > 0) {
6845
+ throw new DocmetaError(
6846
+ "--collection selects a configured collection; it cannot be combined with paths."
6847
+ );
6848
+ }
6849
+ }
6572
6850
  const loaded = opts.noConfig ? null : await loadConfig(opts.configPath, cwd);
6851
+ if (wanted.length > 0 && loaded === null) {
6852
+ throw new DocmetaError("--collection needs a config file to select from.");
6853
+ }
6573
6854
  if (loaded) opts.onConfigLoaded?.({ path: loaded.path, dir: loaded.dir });
6574
6855
  const config = loaded ? rebaseConfigSchemaRefs(loaded.config, loaded.dir, cwd) : null;
6575
- const fromConfig = opts.inputs.length === 0;
6576
- const inputs = fromConfig ? config?.paths ?? [] : opts.inputs;
6577
- const base = fromConfig && inputs.length > 0 && loaded ? loaded.dir : cwd;
6856
+ const collections = loaded ? selectCollections(
6857
+ loaded.collections,
6858
+ opts.collections,
6859
+ loaded.source,
6860
+ (message) => new DocmetaError(message)
6861
+ ) : [];
6862
+ const fromCollections = opts.inputs.length === 0;
6863
+ const inputs = fromCollections ? collections.flatMap((c) => c.paths) : opts.inputs;
6864
+ const base = fromCollections && inputs.length > 0 && loaded ? loaded.dir : cwd;
6578
6865
  return {
6579
6866
  config,
6580
6867
  inputs,
6581
6868
  base,
6582
- ...loaded ? { configDir: loaded.dir, configPath: loaded.path } : {}
6869
+ collections,
6870
+ declaredCollections: loaded ? [...loaded.collections] : [],
6871
+ fromCollections,
6872
+ ...loaded ? {
6873
+ configDir: loaded.dir,
6874
+ configPath: loaded.path,
6875
+ ...loaded.section === void 0 ? {} : { configSection: loaded.section }
6876
+ } : {}
6583
6877
  };
6584
6878
  }
6585
6879
 
6880
+ // src/meta/core/collections.ts
6881
+ import { isAbsolute as isAbsolute3, relative as relative6, resolve as resolvePath2, sep as sep3 } from "node:path";
6882
+
6883
+ // src/meta/core/projection.ts
6884
+ var SYSTEM_COLUMNS = ["_path", "_format", "_present", "_data"];
6885
+ var RESERVED = new Set(SYSTEM_COLUMNS);
6886
+ function quoteIdent(name2) {
6887
+ return `"${name2.replaceAll('"', '""')}"`;
6888
+ }
6889
+ function bindValue(value) {
6890
+ if (value === void 0 || value === null) return null;
6891
+ switch (typeof value) {
6892
+ case "boolean":
6893
+ return value ? 1 : 0;
6894
+ case "number":
6895
+ return Number.isFinite(value) ? value : null;
6896
+ case "bigint":
6897
+ case "string":
6898
+ return value;
6899
+ default:
6900
+ return JSON.stringify(value);
6901
+ }
6902
+ }
6903
+ function corpusDataColumns(entries, extraKeys = []) {
6904
+ const keys = /* @__PURE__ */ new Set();
6905
+ for (const { extracted } of entries) {
6906
+ for (const key of Object.keys(extracted.data)) {
6907
+ if (key !== "" && !RESERVED.has(key)) keys.add(key);
6908
+ }
6909
+ }
6910
+ for (const key of extraKeys) {
6911
+ if (key !== "" && !RESERVED.has(key)) keys.add(key);
6912
+ }
6913
+ return [...keys].sort();
6914
+ }
6915
+ function createDocsTable(db, entries, dataColumns) {
6916
+ db.exec(
6917
+ `CREATE TABLE docs ("_path" TEXT PRIMARY KEY, "_format" TEXT, "_present" INTEGER, "_data" TEXT${dataColumns.map((c) => `, ${quoteIdent(c)}`).join("")})`
6918
+ );
6919
+ const insert = db.prepare(
6920
+ `INSERT INTO docs VALUES (${["?", "?", "?", "?", ...dataColumns.map(() => "?")].join(", ")})`
6921
+ );
6922
+ db.exec("BEGIN");
6923
+ let committed = false;
6924
+ try {
6925
+ for (const { label, extracted } of entries) {
6926
+ insert.run(
6927
+ label,
6928
+ extracted.format,
6929
+ extracted.present ? 1 : 0,
6930
+ JSON.stringify(extracted.data),
6931
+ ...dataColumns.map((c) => bindValue(extracted.data[c]))
6932
+ );
6933
+ }
6934
+ db.exec("COMMIT");
6935
+ committed = true;
6936
+ } finally {
6937
+ if (!committed) db.exec("ROLLBACK");
6938
+ }
6939
+ }
6940
+ function registerLineFor(db, entries) {
6941
+ const byLabel = new Map(entries.map((e) => [e.label, e.extracted]));
6942
+ db.function("lineFor", { deterministic: true }, (path, key) => {
6943
+ if (typeof path !== "string" || typeof key !== "string") return null;
6944
+ return byLabel.get(path)?.lineFor(key) ?? null;
6945
+ });
6946
+ }
6947
+ function assertSingleStatement(sql) {
6948
+ const cut = topLevelSemicolon(sql);
6949
+ if (cut === -1) return;
6950
+ if (!isTrivia(sql.slice(cut + 1))) {
6951
+ throw new DocmetaError(
6952
+ "Run a single SQL statement per query; text after the first `;` would be silently ignored."
6953
+ );
6954
+ }
6955
+ }
6956
+ function startsStringOrIdent(sql, i) {
6957
+ const ch = sql[i];
6958
+ return ch === "'" || ch === '"' || ch === "`" || ch === "[";
6959
+ }
6960
+ function startsComment(sql, i) {
6961
+ const ch = sql[i];
6962
+ return ch === "-" && sql[i + 1] === "-" || ch === "/" && sql[i + 1] === "*";
6963
+ }
6964
+ function skipStringOrIdent(sql, i) {
6965
+ const quote = sql[i];
6966
+ if (quote === "[") {
6967
+ let j2 = i + 1;
6968
+ while (j2 < sql.length && sql[j2] !== "]") j2++;
6969
+ return j2 + 1;
6970
+ }
6971
+ let j = i + 1;
6972
+ while (j < sql.length) {
6973
+ if (sql[j] === quote) {
6974
+ if (sql[j + 1] === quote) {
6975
+ j += 2;
6976
+ continue;
6977
+ }
6978
+ return j + 1;
6979
+ }
6980
+ j++;
6981
+ }
6982
+ return j;
6983
+ }
6984
+ function skipComment(sql, i) {
6985
+ if (sql[i] === "-") {
6986
+ let j = i + 2;
6987
+ while (j < sql.length && sql[j] !== "\n") j++;
6988
+ return j < sql.length ? j + 1 : j;
6989
+ }
6990
+ const end = sql.indexOf("*/", i + 2);
6991
+ return end === -1 ? sql.length : end + 2;
6992
+ }
6993
+ function topLevelSemicolon(sql) {
6994
+ let i = 0;
6995
+ while (i < sql.length) {
6996
+ if (startsStringOrIdent(sql, i)) {
6997
+ i = skipStringOrIdent(sql, i);
6998
+ } else if (startsComment(sql, i)) {
6999
+ i = skipComment(sql, i);
7000
+ } else if (sql[i] === ";") {
7001
+ return i;
7002
+ } else {
7003
+ i++;
7004
+ }
7005
+ }
7006
+ return -1;
7007
+ }
7008
+ var PARAM_NAME_HEAD = /^[A-Za-z_][A-Za-z0-9_]*/;
7009
+ function isParamName(name2) {
7010
+ return PARAM_NAME_HEAD.exec(name2)?.[0] === name2;
7011
+ }
7012
+ function collectNamedParameters(sql) {
7013
+ const tokens = [];
7014
+ const seen = /* @__PURE__ */ new Set();
7015
+ const prefixOf = /* @__PURE__ */ new Map();
7016
+ let i = 0;
7017
+ while (i < sql.length) {
7018
+ const ch = sql[i];
7019
+ if (startsStringOrIdent(sql, i)) {
7020
+ i = skipStringOrIdent(sql, i);
7021
+ } else if (startsComment(sql, i)) {
7022
+ i = skipComment(sql, i);
7023
+ } else if (ch === "$" || ch === ":" || ch === "@") {
7024
+ if (ch === ":" && sql[i + 1] === ":") {
7025
+ i += 2;
7026
+ continue;
7027
+ }
7028
+ const m = PARAM_NAME_HEAD.exec(sql.slice(i + 1));
7029
+ if (!m) {
7030
+ i++;
7031
+ continue;
7032
+ }
7033
+ const token = ch + m[0];
7034
+ const priorPrefix = prefixOf.get(m[0]);
7035
+ if (priorPrefix !== void 0 && priorPrefix !== ch) {
7036
+ throw new DocmetaError(
7037
+ `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.`
7038
+ );
7039
+ }
7040
+ prefixOf.set(m[0], ch);
7041
+ if (!seen.has(token)) {
7042
+ seen.add(token);
7043
+ tokens.push(token);
7044
+ }
7045
+ i += 1 + m[0].length;
7046
+ } else {
7047
+ i++;
7048
+ }
7049
+ }
7050
+ return tokens;
7051
+ }
7052
+ function stripLeadingTrivia(sql) {
7053
+ let i = 0;
7054
+ for (; ; ) {
7055
+ while (i < sql.length && /\s/.test(sql[i] ?? "")) i++;
7056
+ if (startsComment(sql, i)) {
7057
+ i = skipComment(sql, i);
7058
+ continue;
7059
+ }
7060
+ return sql.slice(i);
7061
+ }
7062
+ }
7063
+ function isTrivia(text) {
7064
+ let i = 0;
7065
+ while (i < text.length) {
7066
+ const ch = text[i];
7067
+ if (ch === void 0 || /\s/.test(ch) || ch === ";") {
7068
+ i++;
7069
+ continue;
7070
+ }
7071
+ if (startsComment(text, i)) {
7072
+ i = skipComment(text, i);
7073
+ continue;
7074
+ }
7075
+ return false;
7076
+ }
7077
+ return true;
7078
+ }
7079
+ var sqliteModule;
7080
+ function loadSqlite() {
7081
+ sqliteModule ??= (async () => {
7082
+ const original = process.emitWarning.bind(process);
7083
+ const filtered = (warning, ...rest) => {
7084
+ const text = typeof warning === "string" ? warning : warning.message;
7085
+ if (text.includes("SQLite is an experimental feature")) return;
7086
+ Reflect.apply(original, process, [warning, ...rest]);
7087
+ };
7088
+ process.emitWarning = filtered;
7089
+ try {
7090
+ return await import("node:sqlite");
7091
+ } finally {
7092
+ process.emitWarning = original;
7093
+ }
7094
+ })();
7095
+ return sqliteModule;
7096
+ }
7097
+
7098
+ // src/meta/core/collections.ts
7099
+ function memberOf(collections, configDir, base, label) {
7100
+ if (collections.length === 0) return [];
7101
+ if (label === STDIN_LABEL) return [];
7102
+ const rel = relative6(configDir, resolvePath2(base, label)).split(sep3).join("/");
7103
+ if (isAbsolute3(rel)) return [];
7104
+ return collections.filter((c) => isMember(c, rel)).map((c) => c.name);
7105
+ }
7106
+ function retainMembers(files, membersFor) {
7107
+ return files.filter((label) => membersFor(label).length > 0);
7108
+ }
7109
+ function collectionNames(collections) {
7110
+ return (collections ?? []).map((c) => c.name);
7111
+ }
7112
+ function collectCollections(entries, params) {
7113
+ const declared = params.collections ?? [];
7114
+ if (declared.length === 0) return [];
7115
+ const base = params.base ?? ".";
7116
+ const configDir = params.configDir ?? base;
7117
+ const members = /* @__PURE__ */ new Map();
7118
+ for (const entry of entries) {
7119
+ for (const name2 of memberOf(declared, configDir, base, entry.label)) {
7120
+ const list = members.get(name2);
7121
+ if (list) list.push(entry.label);
7122
+ else members.set(name2, [entry.label]);
7123
+ }
7124
+ }
7125
+ return declared.map((c) => ({ name: c.name, members: members.get(c.name) ?? [] }));
7126
+ }
7127
+ function quoteSqlString(value) {
7128
+ return `'${value.replaceAll("'", "''")}'`;
7129
+ }
7130
+ function createCollectionViews(db, collections) {
7131
+ for (const c of collections) {
7132
+ const where = c.members.length === 0 ? "0" : `_path IN (${c.members.map(quoteSqlString).join(", ")})`;
7133
+ db.exec(
7134
+ `CREATE VIEW ${quoteIdent(c.name)} AS SELECT * FROM docs WHERE ${where}`
7135
+ );
7136
+ }
7137
+ }
7138
+
6586
7139
  // src/meta/core/validator.ts
6587
7140
  import * as AjvDraft07Ns from "ajv";
6588
7141
  import * as Ajv2019Ns from "ajv/dist/2019.js";
@@ -6721,7 +7274,7 @@ var Validator = class {
6721
7274
  return ajv.compile(schema);
6722
7275
  } catch (err) {
6723
7276
  throw new DocmetaError(
6724
- `Schema "${ref}" failed to compile: ${err.message}`
7277
+ `Schema "${ref}" failed to compile: ${errorMessage(err)}`
6725
7278
  );
6726
7279
  }
6727
7280
  }
@@ -6813,7 +7366,7 @@ import { resolve as resolve9, extname as extname2 } from "node:path";
6813
7366
  // src/meta/core/baseline.ts
6814
7367
  import { createHash as createHash3 } from "node:crypto";
6815
7368
  import { readFile as readFile5 } from "node:fs/promises";
6816
- import { relative as relative6, resolve as resolve8 } from "node:path";
7369
+ import { relative as relative7, resolve as resolve8 } from "node:path";
6817
7370
  var DEFAULT_BASELINE_PATH = ".manni-baseline.json";
6818
7371
  var LEGACY_BASELINE_PATH = ".docmeta-baseline.json";
6819
7372
  var BASELINE_VERSION = 1;
@@ -6821,13 +7374,13 @@ var FINGERPRINT_RE = /^[0-9a-f]{16}$/;
6821
7374
  function canonicalFilePath(file, ctx) {
6822
7375
  if (!ctx) return file;
6823
7376
  const from = ctx.runBase ?? ctx.base;
6824
- return relative6(ctx.base, resolve8(from, file)).replace(/\\/g, "/");
7377
+ return relative7(ctx.base, resolve8(from, file)).replace(/\\/g, "/");
6825
7378
  }
6826
7379
  function canonicalSchemaRef(ref, ctx) {
6827
7380
  if (!ctx) return ref;
6828
7381
  if (classifyRef(ref).kind !== "file") return ref;
6829
7382
  const abs = resolve8(ctx.cwd, ref);
6830
- return relative6(ctx.base, abs).replace(/\\/g, "/");
7383
+ return relative7(ctx.base, abs).replace(/\\/g, "/");
6831
7384
  }
6832
7385
  function fingerprint(e, ctx) {
6833
7386
  const parts = [
@@ -6850,409 +7403,135 @@ function serializeBaseline(baseline) {
6850
7403
  entries
6851
7404
  },
6852
7405
  null,
6853
- 2
6854
- )}
6855
- `;
6856
- }
6857
- function bad(source, detail) {
6858
- throw new DocmetaError(`Baseline "${source}": ${detail}`);
6859
- }
6860
- function parseBaseline(text, source) {
6861
- let raw;
6862
- try {
6863
- raw = JSON.parse(stripBom(text));
6864
- } catch (err) {
6865
- bad(source, `invalid JSON: ${err.message}`);
6866
- }
6867
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
6868
- bad(source, "top level must be an object.");
6869
- }
6870
- const obj = raw;
6871
- if (obj.version !== BASELINE_VERSION) {
6872
- bad(
6873
- source,
6874
- `unsupported version ${JSON.stringify(obj.version)} (this manni writes version ${BASELINE_VERSION}). Re-record it with \`manni meta validate --write-baseline\`.`
6875
- );
6876
- }
6877
- const generatedWith = typeof obj.generatedWith === "string" ? obj.generatedWith : "";
6878
- const rawEntries = obj.entries;
6879
- if (typeof rawEntries !== "object" || rawEntries === null || Array.isArray(rawEntries)) {
6880
- bad(source, '"entries" must be an object mapping file paths to fingerprints.');
6881
- }
6882
- const entries = /* @__PURE__ */ Object.create(null);
6883
- for (const [file, value] of Object.entries(
6884
- rawEntries
6885
- )) {
6886
- if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
6887
- bad(source, `entries["${file}"] must be a list of fingerprint strings.`);
6888
- }
6889
- const malformed = value.find((v) => !FINGERPRINT_RE.test(v));
6890
- if (malformed !== void 0) {
6891
- bad(
6892
- source,
6893
- `entries["${file}"] contains ${JSON.stringify(malformed)}, which is not a fingerprint (16 lowercase hex characters).`
6894
- );
6895
- }
6896
- entries[file] = value;
6897
- }
6898
- return { version: BASELINE_VERSION, generatedWith, entries };
6899
- }
6900
- async function readBaseline(absPath, source) {
6901
- let text;
6902
- try {
6903
- text = await readFile5(absPath, "utf8");
6904
- } catch (err) {
6905
- if (err.code === "ENOENT") return null;
6906
- throw new DocmetaError(
6907
- `Baseline "${source}" could not be read: ${err.message}`
6908
- );
6909
- }
6910
- return parseBaseline(text, source);
6911
- }
6912
- async function writeBaselineFile(absPath, baseline, source) {
6913
- try {
6914
- await writeFileAtomic(absPath, serializeBaseline(baseline));
6915
- } catch (err) {
6916
- throw new DocmetaError(
6917
- `Baseline "${source}" could not be written: ${err.message}`
6918
- );
6919
- }
6920
- }
6921
- function buildBaseline(results, generatedWith, ctx) {
6922
- const entries = /* @__PURE__ */ Object.create(null);
6923
- for (const r of results) {
6924
- if (r.errors.length === 0) continue;
6925
- const prints = [...new Set(r.errors.map((e) => fingerprint(e, ctx)))].sort();
6926
- entries[canonicalFilePath(r.file, ctx)] = prints;
6927
- }
6928
- return { version: BASELINE_VERSION, generatedWith, entries };
6929
- }
6930
- function applyBaseline(results, baseline, ctx) {
6931
- let recorded = 0;
6932
- let suppressed = 0;
6933
- let stale = 0;
6934
- const applied = results.map((r) => {
6935
- const known = baseline.entries[canonicalFilePath(r.file, ctx)];
6936
- if (!known) return r;
6937
- const recordedHere = new Set(known);
6938
- recorded += recordedHere.size;
6939
- const seen = /* @__PURE__ */ new Set();
6940
- const fresh = [];
6941
- for (const e of r.errors) {
6942
- const print = fingerprint(e, ctx);
6943
- if (recordedHere.has(print)) {
6944
- seen.add(print);
6945
- suppressed += 1;
6946
- } else {
6947
- fresh.push(e);
6948
- }
6949
- }
6950
- stale += recordedHere.size - seen.size;
6951
- const baselined = r.errors.length - fresh.length;
6952
- return {
6953
- ...r,
6954
- ok: fresh.length === 0,
6955
- errors: fresh,
6956
- ...baselined > 0 ? { baselined } : {}
6957
- };
6958
- });
6959
- return { results: applied, recorded, suppressed, stale };
6960
- }
6961
- function diffBaselines(previous, next) {
6962
- const flatten = (b) => {
6963
- const out = /* @__PURE__ */ new Set();
6964
- if (!b) return out;
6965
- for (const [file, prints] of Object.entries(b.entries)) {
6966
- for (const p of prints) out.add(`${file}\0${p}`);
6967
- }
6968
- return out;
6969
- };
6970
- const before = flatten(previous);
6971
- const after = flatten(next);
6972
- let added = 0;
6973
- let removed = 0;
6974
- for (const k of after) if (!before.has(k)) added += 1;
6975
- for (const k of before) if (!after.has(k)) removed += 1;
6976
- return { added, removed };
6977
- }
6978
- function countFingerprints(baseline) {
6979
- let n = 0;
6980
- for (const prints of Object.values(baseline.entries)) n += prints.length;
6981
- return n;
6982
- }
6983
-
6984
- // src/meta/core/projection.ts
6985
- var SYSTEM_COLUMNS = ["_path", "_format", "_present", "_data"];
6986
- var RESERVED = new Set(SYSTEM_COLUMNS);
6987
- function quoteIdent(name2) {
6988
- return `"${name2.replaceAll('"', '""')}"`;
6989
- }
6990
- function bindValue(value) {
6991
- if (value === void 0 || value === null) return null;
6992
- switch (typeof value) {
6993
- case "boolean":
6994
- return value ? 1 : 0;
6995
- case "number":
6996
- return Number.isFinite(value) ? value : null;
6997
- case "bigint":
6998
- case "string":
6999
- return value;
7000
- default:
7001
- return JSON.stringify(value);
7002
- }
7003
- }
7004
- function corpusDataColumns(entries, extraKeys = []) {
7005
- const keys = /* @__PURE__ */ new Set();
7006
- for (const { extracted } of entries) {
7007
- for (const key of Object.keys(extracted.data)) {
7008
- if (key !== "" && !RESERVED.has(key)) keys.add(key);
7009
- }
7010
- }
7011
- for (const key of extraKeys) {
7012
- if (key !== "" && !RESERVED.has(key)) keys.add(key);
7013
- }
7014
- return [...keys].sort();
7015
- }
7016
- function createDocsTable(db, entries, dataColumns) {
7017
- db.exec(
7018
- `CREATE TABLE docs ("_path" TEXT PRIMARY KEY, "_format" TEXT, "_present" INTEGER, "_data" TEXT${dataColumns.map((c) => `, ${quoteIdent(c)}`).join("")})`
7019
- );
7020
- const insert = db.prepare(
7021
- `INSERT INTO docs VALUES (${["?", "?", "?", "?", ...dataColumns.map(() => "?")].join(", ")})`
7022
- );
7023
- db.exec("BEGIN");
7024
- for (const { label, extracted } of entries) {
7025
- insert.run(
7026
- label,
7027
- extracted.format,
7028
- extracted.present ? 1 : 0,
7029
- JSON.stringify(extracted.data),
7030
- ...dataColumns.map((c) => bindValue(extracted.data[c]))
7031
- );
7032
- }
7033
- db.exec("COMMIT");
7034
- }
7035
- function registerLineFor(db, entries) {
7036
- const byLabel = new Map(entries.map((e) => [e.label, e.extracted]));
7037
- db.function("lineFor", { deterministic: true }, (path, key) => {
7038
- if (typeof path !== "string" || typeof key !== "string") return null;
7039
- return byLabel.get(path)?.lineFor(key) ?? null;
7040
- });
7041
- }
7042
- function assertSingleStatement(sql) {
7043
- const cut = topLevelSemicolon(sql);
7044
- if (cut === -1) return;
7045
- if (!isTrivia(sql.slice(cut + 1))) {
7046
- throw new DocmetaError(
7047
- "Run a single SQL statement per query; text after the first `;` would be silently ignored."
7048
- );
7049
- }
7050
- }
7051
- function startsStringOrIdent(sql, i) {
7052
- const ch = sql[i];
7053
- return ch === "'" || ch === '"' || ch === "`" || ch === "[";
7054
- }
7055
- function startsComment(sql, i) {
7056
- const ch = sql[i];
7057
- return ch === "-" && sql[i + 1] === "-" || ch === "/" && sql[i + 1] === "*";
7058
- }
7059
- function skipStringOrIdent(sql, i) {
7060
- const quote = sql[i];
7061
- if (quote === "[") {
7062
- let j2 = i + 1;
7063
- while (j2 < sql.length && sql[j2] !== "]") j2++;
7064
- return j2 + 1;
7065
- }
7066
- let j = i + 1;
7067
- while (j < sql.length) {
7068
- if (sql[j] === quote) {
7069
- if (sql[j + 1] === quote) {
7070
- j += 2;
7071
- continue;
7072
- }
7073
- return j + 1;
7074
- }
7075
- j++;
7076
- }
7077
- return j;
7078
- }
7079
- function skipComment(sql, i) {
7080
- if (sql[i] === "-") {
7081
- let j = i + 2;
7082
- while (j < sql.length && sql[j] !== "\n") j++;
7083
- return j < sql.length ? j + 1 : j;
7084
- }
7085
- const end = sql.indexOf("*/", i + 2);
7086
- return end === -1 ? sql.length : end + 2;
7087
- }
7088
- function topLevelSemicolon(sql) {
7089
- let i = 0;
7090
- while (i < sql.length) {
7091
- if (startsStringOrIdent(sql, i)) {
7092
- i = skipStringOrIdent(sql, i);
7093
- } else if (startsComment(sql, i)) {
7094
- i = skipComment(sql, i);
7095
- } else if (sql[i] === ";") {
7096
- return i;
7097
- } else {
7098
- i++;
7099
- }
7100
- }
7101
- return -1;
7406
+ 2
7407
+ )}
7408
+ `;
7102
7409
  }
7103
- var PARAM_NAME_HEAD = /^[A-Za-z_][A-Za-z0-9_]*/;
7104
- function isParamName(name2) {
7105
- return PARAM_NAME_HEAD.exec(name2)?.[0] === name2;
7410
+ function bad(source, detail) {
7411
+ throw new DocmetaError(`Baseline "${source}": ${detail}`);
7106
7412
  }
7107
- function collectNamedParameters(sql) {
7108
- const tokens = [];
7109
- const seen = /* @__PURE__ */ new Set();
7110
- const prefixOf = /* @__PURE__ */ new Map();
7111
- let i = 0;
7112
- while (i < sql.length) {
7113
- const ch = sql[i];
7114
- if (startsStringOrIdent(sql, i)) {
7115
- i = skipStringOrIdent(sql, i);
7116
- } else if (startsComment(sql, i)) {
7117
- i = skipComment(sql, i);
7118
- } else if (ch === "$" || ch === ":" || ch === "@") {
7119
- if (ch === ":" && sql[i + 1] === ":") {
7120
- i += 2;
7121
- continue;
7122
- }
7123
- const m = PARAM_NAME_HEAD.exec(sql.slice(i + 1));
7124
- if (!m) {
7125
- i++;
7126
- continue;
7127
- }
7128
- const token = ch + m[0];
7129
- const priorPrefix = prefixOf.get(m[0]);
7130
- if (priorPrefix !== void 0 && priorPrefix !== ch) {
7131
- throw new DocmetaError(
7132
- `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.`
7133
- );
7134
- }
7135
- prefixOf.set(m[0], ch);
7136
- if (!seen.has(token)) {
7137
- seen.add(token);
7138
- tokens.push(token);
7139
- }
7140
- i += 1 + m[0].length;
7141
- } else {
7142
- i++;
7143
- }
7413
+ function parseBaseline(text, source) {
7414
+ let raw;
7415
+ try {
7416
+ raw = JSON.parse(stripBom(text));
7417
+ } catch (err) {
7418
+ bad(source, `invalid JSON: ${errorMessage(err)}`);
7144
7419
  }
7145
- return tokens;
7146
- }
7147
- function stripLeadingTrivia(sql) {
7148
- let i = 0;
7149
- for (; ; ) {
7150
- while (i < sql.length && /\s/.test(sql[i] ?? "")) i++;
7151
- if (startsComment(sql, i)) {
7152
- i = skipComment(sql, i);
7153
- continue;
7154
- }
7155
- return sql.slice(i);
7420
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
7421
+ bad(source, "top level must be an object.");
7156
7422
  }
7157
- }
7158
- function isTrivia(text) {
7159
- let i = 0;
7160
- while (i < text.length) {
7161
- const ch = text[i];
7162
- if (ch === void 0 || /\s/.test(ch) || ch === ";") {
7163
- i++;
7164
- continue;
7423
+ const obj = raw;
7424
+ if (obj.version !== BASELINE_VERSION) {
7425
+ bad(
7426
+ source,
7427
+ `unsupported version ${JSON.stringify(obj.version)} (this manni writes version ${BASELINE_VERSION}). Re-record it with \`manni meta validate --write-baseline\`.`
7428
+ );
7429
+ }
7430
+ const generatedWith = typeof obj.generatedWith === "string" ? obj.generatedWith : "";
7431
+ const rawEntries = obj.entries;
7432
+ if (typeof rawEntries !== "object" || rawEntries === null || Array.isArray(rawEntries)) {
7433
+ bad(source, '"entries" must be an object mapping file paths to fingerprints.');
7434
+ }
7435
+ const entries = /* @__PURE__ */ Object.create(null);
7436
+ for (const [file, value] of Object.entries(
7437
+ rawEntries
7438
+ )) {
7439
+ if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
7440
+ bad(source, `entries["${file}"] must be a list of fingerprint strings.`);
7165
7441
  }
7166
- if (startsComment(text, i)) {
7167
- i = skipComment(text, i);
7168
- continue;
7442
+ const malformed = value.find((v) => !FINGERPRINT_RE.test(v));
7443
+ if (malformed !== void 0) {
7444
+ bad(
7445
+ source,
7446
+ `entries["${file}"] contains ${JSON.stringify(malformed)}, which is not a fingerprint (16 lowercase hex characters).`
7447
+ );
7169
7448
  }
7170
- return false;
7449
+ entries[file] = value;
7171
7450
  }
7172
- return true;
7451
+ return { version: BASELINE_VERSION, generatedWith, entries };
7173
7452
  }
7174
- var sqliteModule;
7175
- function loadSqlite() {
7176
- sqliteModule ??= (async () => {
7177
- const original = process.emitWarning.bind(process);
7178
- const filtered = (warning, ...rest) => {
7179
- const text = typeof warning === "string" ? warning : warning.message;
7180
- if (text.includes("SQLite is an experimental feature")) return;
7181
- Reflect.apply(original, process, [warning, ...rest]);
7182
- };
7183
- process.emitWarning = filtered;
7184
- try {
7185
- return await import("node:sqlite");
7186
- } finally {
7187
- process.emitWarning = original;
7188
- }
7189
- })();
7190
- return sqliteModule;
7453
+ async function readBaseline(absPath, source) {
7454
+ let text;
7455
+ try {
7456
+ text = await readFile5(absPath, "utf8");
7457
+ } catch (err) {
7458
+ if (err.code === "ENOENT") return null;
7459
+ throw new DocmetaError(
7460
+ `Baseline "${source}" could not be read: ${errorMessage(err)}`
7461
+ );
7462
+ }
7463
+ return parseBaseline(text, source);
7191
7464
  }
7192
-
7193
- // src/meta/core/collections.ts
7194
- function namedOverrides(config) {
7195
- return (config?.overrides ?? []).flatMap(
7196
- (o, i) => o.name !== void 0 ? [{ name: o.name, files: o.files, index: i }] : []
7197
- );
7465
+ async function writeBaselineFile(absPath, baseline, source) {
7466
+ try {
7467
+ await writeFileAtomic(absPath, serializeBaseline(baseline));
7468
+ } catch (err) {
7469
+ throw new DocmetaError(
7470
+ `Baseline "${source}" could not be written: ${errorMessage(err)}`
7471
+ );
7472
+ }
7198
7473
  }
7199
- function collectionNames(config) {
7200
- return namedOverrides(config).map((n) => n.name);
7474
+ function buildBaseline(results, generatedWith, ctx) {
7475
+ const entries = /* @__PURE__ */ Object.create(null);
7476
+ for (const r of results) {
7477
+ if (r.errors.length === 0) continue;
7478
+ const prints = [...new Set(r.errors.map((e) => fingerprint(e, ctx)))].sort();
7479
+ entries[canonicalFilePath(r.file, ctx)] = prints;
7480
+ }
7481
+ return { version: BASELINE_VERSION, generatedWith, entries };
7201
7482
  }
7202
- function collectCollections(entries, params) {
7203
- const named = namedOverrides(params.config);
7204
- if (named.length === 0) return [];
7205
- const members = /* @__PURE__ */ new Map();
7206
- for (const entry of entries) {
7207
- let resolved;
7208
- if (params.resolved) {
7209
- const pre = params.resolved.get(entry.label);
7210
- if (!pre) continue;
7211
- resolved = pre;
7212
- } else {
7213
- try {
7214
- resolved = resolveSchemaSetWithSource({
7215
- filePath: entry.label,
7216
- fileSchema: entry.extracted.data[FILE_SCHEMA_KEY],
7217
- ...params.cliSchemas ? { cliSchemas: params.cliSchemas } : {},
7218
- config: params.config,
7219
- ...params.fileBase !== void 0 ? { fileBase: params.fileBase } : {},
7220
- ...params.trustRoot ? { trustRoot: params.trustRoot } : {}
7221
- });
7222
- } catch {
7223
- continue;
7224
- }
7225
- }
7226
- if (resolved.source === "override" && resolved.overrideIndex !== void 0) {
7227
- const list = members.get(resolved.overrideIndex);
7228
- if (list) list.push(entry.label);
7229
- else members.set(resolved.overrideIndex, [entry.label]);
7230
- continue;
7231
- }
7232
- if (resolved.source === "document") {
7233
- const excluded = named.find((g) => matchesFileGlob(g.files, entry.label));
7234
- if (excluded) {
7235
- params.onNotice?.(
7236
- `${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.`
7237
- );
7483
+ function applyBaseline(results, baseline, ctx) {
7484
+ let recorded = 0;
7485
+ let suppressed = 0;
7486
+ let stale = 0;
7487
+ const applied = results.map((r) => {
7488
+ const known = baseline.entries[canonicalFilePath(r.file, ctx)];
7489
+ if (!known) return r;
7490
+ const recordedHere = new Set(known);
7491
+ recorded += recordedHere.size;
7492
+ const seen = /* @__PURE__ */ new Set();
7493
+ const fresh = [];
7494
+ for (const e of r.errors) {
7495
+ const print = fingerprint(e, ctx);
7496
+ if (recordedHere.has(print)) {
7497
+ seen.add(print);
7498
+ suppressed += 1;
7499
+ } else {
7500
+ fresh.push(e);
7238
7501
  }
7239
7502
  }
7240
- }
7241
- return named.map(({ name: name2, index }) => ({
7242
- name: name2,
7243
- members: members.get(index) ?? []
7244
- }));
7503
+ stale += recordedHere.size - seen.size;
7504
+ const baselined = r.errors.length - fresh.length;
7505
+ return {
7506
+ ...r,
7507
+ ok: fresh.length === 0,
7508
+ errors: fresh,
7509
+ ...baselined > 0 ? { baselined } : {}
7510
+ };
7511
+ });
7512
+ return { results: applied, recorded, suppressed, stale };
7245
7513
  }
7246
- function quoteSqlString(value) {
7247
- return `'${value.replaceAll("'", "''")}'`;
7514
+ function diffBaselines(previous, next) {
7515
+ const flatten = (b) => {
7516
+ const out = /* @__PURE__ */ new Set();
7517
+ if (!b) return out;
7518
+ for (const [file, prints] of Object.entries(b.entries)) {
7519
+ for (const p of prints) out.add(`${file}\0${p}`);
7520
+ }
7521
+ return out;
7522
+ };
7523
+ const before = flatten(previous);
7524
+ const after = flatten(next);
7525
+ let added = 0;
7526
+ let removed = 0;
7527
+ for (const k of after) if (!before.has(k)) added += 1;
7528
+ for (const k of before) if (!after.has(k)) removed += 1;
7529
+ return { added, removed };
7248
7530
  }
7249
- function createCollectionViews(db, collections) {
7250
- for (const c of collections) {
7251
- const where = c.members.length === 0 ? "0" : `_path IN (${c.members.map(quoteSqlString).join(", ")})`;
7252
- db.exec(
7253
- `CREATE VIEW ${quoteIdent(c.name)} AS SELECT * FROM docs WHERE ${where}`
7254
- );
7255
- }
7531
+ function countFingerprints(baseline) {
7532
+ let n = 0;
7533
+ for (const prints of Object.values(baseline.entries)) n += prints.length;
7534
+ return n;
7256
7535
  }
7257
7536
 
7258
7537
  // src/meta/core/checks.ts
@@ -7335,7 +7614,7 @@ async function runChecks(checks, entries, ctx = {}) {
7335
7614
  tokens = collectNamedParameters(check.query);
7336
7615
  } catch (err) {
7337
7616
  throw new DocmetaError(
7338
- `check "${check.name}": ${err.message}`
7617
+ `check "${check.name}": ${errorMessage(err)}`
7339
7618
  );
7340
7619
  }
7341
7620
  if (tokens.length > 0) {
@@ -7351,7 +7630,7 @@ async function runChecks(checks, entries, ctx = {}) {
7351
7630
  columns = stmt.columns().map((c) => c.name);
7352
7631
  rows = stmt.all();
7353
7632
  } catch (err) {
7354
- const message = err.message;
7633
+ const message = errorMessage(err);
7355
7634
  throw new DocmetaError(
7356
7635
  message.includes("attempt to write a readonly database") ? `check "${check.name}": ${message} \u2014 checks are read-only by design; a check is a SELECT over the projection. Edit the corpus with \`manni meta query\` instead.` : `check "${check.name}": ${message}`
7357
7636
  );
@@ -7416,17 +7695,27 @@ function resolveBaselineRequest(opts, configured, configDir, cwd) {
7416
7695
  }
7417
7696
  async function runValidate(opts) {
7418
7697
  const cwd = opts.cwd ?? process.cwd();
7419
- const { config, inputs, base, configDir } = await resolveRunConfig({
7698
+ const {
7699
+ config,
7700
+ inputs,
7701
+ base,
7702
+ configDir,
7703
+ collections,
7704
+ declaredCollections,
7705
+ fromCollections
7706
+ } = await resolveRunConfig({
7420
7707
  cwd,
7421
7708
  configPath: opts.configPath,
7422
7709
  noConfig: opts.noConfig,
7423
7710
  inputs: opts.inputs,
7711
+ ...opts.collections !== void 0 ? { collections: opts.collections } : {},
7424
7712
  onConfigLoaded: opts.onConfigLoaded
7425
7713
  });
7714
+ const membersFor = (label) => memberOf(collections, configDir ?? cwd, base, label);
7426
7715
  const usingStdin = inputs.includes(STDIN_TOKEN);
7427
7716
  if (inputs.length === 0) {
7428
7717
  throw new DocmetaError(
7429
- "No files to validate. Pass paths/globs, or add `paths:` under `meta:` in manni.config.yaml."
7718
+ "No files to validate. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml."
7430
7719
  );
7431
7720
  }
7432
7721
  const forcedExtractor = opts.as ? extractorByName(opts.as) : void 0;
@@ -7438,8 +7727,8 @@ async function runValidate(opts) {
7438
7727
  const exts = opts.exts ?? (forcedExtractor ? forcedExtractor.extensions : void 0);
7439
7728
  const fileInputs = inputs.filter((i) => i !== STDIN_TOKEN);
7440
7729
  const allowEmpty = opts.allowEmpty ?? config?.allowEmpty;
7441
- const exclude = [...config?.exclude ?? [], ...opts.exclude ?? []];
7442
- const { files, gitignoreSkipped } = await resolveTargetSet({
7730
+ const exclude = opts.exclude ?? [];
7731
+ const { files: walked, gitignoreSkipped } = await resolveTargetSet({
7443
7732
  inputs: fileInputs,
7444
7733
  exts,
7445
7734
  exclude,
@@ -7451,6 +7740,7 @@ async function runValidate(opts) {
7451
7740
  onNotice: opts.onNotice
7452
7741
  })
7453
7742
  });
7743
+ const files = fromCollections ? retainMembers(walked, membersFor) : walked;
7454
7744
  assertNonEmpty({
7455
7745
  files,
7456
7746
  inputs: fileInputs,
@@ -7462,7 +7752,7 @@ async function runValidate(opts) {
7462
7752
  action: "validated"
7463
7753
  });
7464
7754
  const trustRoot = schemaTrustRoot(cwd, configDir);
7465
- const sidecars = await loadSidecars(config, {
7755
+ const externalMetadata = await loadExternalMetadata(collections, {
7466
7756
  configDir: configDir ?? cwd,
7467
7757
  base,
7468
7758
  offline: opts.offline ?? config?.offline ?? false
@@ -7483,16 +7773,18 @@ async function runValidate(opts) {
7483
7773
  );
7484
7774
  const results = [];
7485
7775
  const joinHits = /* @__PURE__ */ new Map();
7486
- const scoped = opts.inputs.length > 0 || // redundant with inputs.length (stdin is an input) — kept as belt-and-suspenders
7776
+ const scopedToCollections = (opts.collections?.length ?? 0) > 0;
7777
+ const scopedByFlags = opts.inputs.length > 0 || // redundant with inputs.length (stdin is an input) — kept as belt-and-suspenders
7487
7778
  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;
7779
+ const scoped = scopedByFlags || scopedToCollections;
7488
7780
  const configuredChecks = config?.checks ?? [];
7489
- if (!scoped) {
7490
- const orphans = orphanEntries(sidecars, files, base);
7781
+ if (!scopedByFlags) {
7782
+ const covered = scopedToCollections ? collections.map((c) => c.name) : void 0;
7783
+ const orphans = orphanEntries(externalMetadata, files, base, covered);
7491
7784
  if (orphans.length > 0) throw orphanError(orphans);
7492
7785
  }
7493
7786
  const checksWillRun = configuredChecks.length > 0 && opts.checks !== false && !scoped;
7494
7787
  const checkEntries = [];
7495
- const checkResolved = /* @__PURE__ */ new Map();
7496
7788
  const processOne = async (label, content, extension) => {
7497
7789
  const extractor = forcedExtractor ?? extractorForExtension(extension);
7498
7790
  if (!extractor) {
@@ -7500,19 +7792,20 @@ async function runValidate(opts) {
7500
7792
  `Unsupported file type "${extension}" for "${label}". Supported: ${supportedExtensions().join(", ")}. Use --as to override.`
7501
7793
  );
7502
7794
  }
7795
+ const members = membersFor(label);
7503
7796
  let extracted;
7504
7797
  try {
7505
7798
  extracted = extractor.extract(content, label, {
7506
- elements: resolveElements(label, config)
7799
+ elements: resolveElements(label, config, members)
7507
7800
  });
7508
7801
  } catch (err) {
7509
7802
  if (err instanceof DocmetaError) throw err;
7510
7803
  results.push(
7511
- parseErrorResult(label, extractor.name, err.message, "parse")
7804
+ parseErrorResult(label, extractor.name, errorMessage(err), "parse")
7512
7805
  );
7513
7806
  return;
7514
7807
  }
7515
- const merged = mergeSidecars(label, extracted, sidecars, base);
7808
+ const merged = mergeExternalMetadata(label, extracted, externalMetadata, members, base);
7516
7809
  extracted = merged.extracted;
7517
7810
  for (const j of merged.joins) {
7518
7811
  const byValue = joinHits.get(j.field) ?? /* @__PURE__ */ new Map();
@@ -7530,6 +7823,7 @@ async function runValidate(opts) {
7530
7823
  fileSchema: extracted.data[FILE_SCHEMA_KEY],
7531
7824
  cliSchemas: opts.cliSchemas,
7532
7825
  config,
7826
+ memberOf: members,
7533
7827
  // A document's own `$schema` is measured from the run's directory, the
7534
7828
  // same base `loadSchema` will read it from, and contained to the
7535
7829
  // repository the run is standing in.
@@ -7539,11 +7833,10 @@ async function runValidate(opts) {
7539
7833
  });
7540
7834
  } catch (err) {
7541
7835
  results.push(
7542
- parseErrorResult(label, extractor.name, err.message, "schema")
7836
+ parseErrorResult(label, extractor.name, errorMessage(err), "schema")
7543
7837
  );
7544
7838
  return;
7545
7839
  }
7546
- if (checksWillRun) checkResolved.set(label, resolved);
7547
7840
  const schemaSet = resolved.schemas;
7548
7841
  let errors;
7549
7842
  try {
@@ -7563,14 +7856,14 @@ async function runValidate(opts) {
7563
7856
  );
7564
7857
  return;
7565
7858
  }
7566
- for (const { key, file } of merged.collisions) {
7859
+ for (const { key, file, collection } of merged.collisions) {
7567
7860
  const line = extracted.lineFor(key);
7568
7861
  errors.push({
7569
- schema: SIDECAR_OWNED_SCHEMA,
7570
- keyword: SIDECAR_KEYWORD,
7862
+ schema: EXTERNAL_OWNED_SCHEMA,
7863
+ keyword: EXTERNAL_KEYWORD,
7571
7864
  subject: key,
7572
- instancePath: sidecarPointer(key),
7573
- message: `"${key}" is owned by sidecar ${file}; remove it from the document`,
7865
+ instancePath: externalMetadataPointer(key),
7866
+ message: `"${key}" is owned by manifest ${file} (collection ${collection}); remove it from the document`,
7574
7867
  ...line != null ? { line } : {}
7575
7868
  });
7576
7869
  }
@@ -7595,7 +7888,7 @@ async function runValidate(opts) {
7595
7888
  const content = await readFile6(resolve9(base, file), "utf8");
7596
7889
  await processOne(file, content, extname2(file));
7597
7890
  }
7598
- if (sidecars) {
7891
+ if (externalMetadata) {
7599
7892
  const byLabel = new Map(results.map((r) => [r.file, r]));
7600
7893
  const matched = /* @__PURE__ */ new Map();
7601
7894
  for (const [field, byValue] of joinHits) {
@@ -7607,10 +7900,10 @@ async function runValidate(opts) {
7607
7900
  const result = byLabel.get(hit.label);
7608
7901
  if (!result) continue;
7609
7902
  result.errors.push({
7610
- schema: SIDECAR_DUPLICATE_SCHEMA,
7611
- keyword: SIDECAR_KEYWORD,
7903
+ schema: EXTERNAL_DUPLICATE_SCHEMA,
7904
+ keyword: EXTERNAL_KEYWORD,
7612
7905
  subject: value,
7613
- instancePath: sidecarPointer(field),
7906
+ instancePath: externalMetadataPointer(field),
7614
7907
  message: `${hits.length} documents carry ${field} "${value}"; ${hit.file} cannot tell them apart (${others})`,
7615
7908
  ...hit.line != null ? { line: hit.line } : {}
7616
7909
  });
@@ -7618,26 +7911,26 @@ async function runValidate(opts) {
7618
7911
  }
7619
7912
  }
7620
7913
  }
7621
- if (!scoped) {
7622
- const orphans = orphanJoins(sidecars, matched);
7914
+ if (!scopedByFlags) {
7915
+ const covered = scopedToCollections ? collections.map((c) => c.name) : void 0;
7916
+ const orphans = orphanJoins(externalMetadata, matched, covered);
7623
7917
  if (orphans.length > 0) throw orphanError(orphans);
7624
7918
  }
7625
7919
  }
7626
7920
  if (configuredChecks.length > 0 && opts.checks !== false) {
7627
7921
  if (scoped) {
7628
- opts.onNotice?.("corpus checks skipped: run is scoped");
7922
+ opts.onNotice?.(
7923
+ scopedToCollections ? `corpus checks skipped: run is scoped to collections ${collections.map((c) => c.name).join(", ")}` : "corpus checks skipped: run is scoped"
7924
+ );
7629
7925
  } else {
7630
7926
  const findings = await runChecks(configuredChecks, checkEntries, {
7631
- // The same resolution inputs the per-file loop used, so a check's
7632
- // collection views (0027) hold exactly the files each override group
7633
- // was validated as — and `resolved` hands over that loop's outcomes,
7634
- // so membership is read from them instead of resolving twice.
7635
- config,
7636
- ...opts.cliSchemas ? { cliSchemas: opts.cliSchemas } : {},
7637
- fileBase: cwd,
7638
- trustRoot,
7639
- resolved: checkResolved,
7640
- ...opts.onNotice ? { onNotice: opts.onNotice } : {}
7927
+ // Every declared collection is a view (0041 rule 7), holding the
7928
+ // members this run loaded — decided by the same path arithmetic
7929
+ // `membersFor` uses, so a check and a `manni meta query` over the same
7930
+ // corpus see the same collections.
7931
+ collections: declaredCollections,
7932
+ ...configDir !== void 0 ? { configDir } : {},
7933
+ base
7641
7934
  });
7642
7935
  const byFile = new Map(results.map((r) => [r.file, r]));
7643
7936
  for (const [file, errs] of findings) {
@@ -7719,17 +8012,18 @@ async function runGet(opts) {
7719
8012
  if (opts.fields.length === 0) {
7720
8013
  throw new DocmetaError("Specify at least one field to get.");
7721
8014
  }
7722
- const { config, inputs, base, configDir } = await resolveRunConfig({
8015
+ const { config, inputs, base, configDir, collections, fromCollections } = await resolveRunConfig({
7723
8016
  cwd,
7724
8017
  configPath: opts.configPath,
7725
8018
  noConfig: opts.noConfig,
7726
8019
  inputs: opts.inputs,
8020
+ ...opts.collections !== void 0 ? { collections: opts.collections } : {},
7727
8021
  onConfigLoaded: opts.onConfigLoaded
7728
8022
  });
7729
8023
  const usingStdin = inputs.includes(STDIN_TOKEN);
7730
8024
  if (inputs.length === 0) {
7731
8025
  throw new DocmetaError(
7732
- "No files to read. Pass paths/globs, or add `paths:` under `meta:` in manni.config.yaml."
8026
+ "No files to read. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml."
7733
8027
  );
7734
8028
  }
7735
8029
  const forced = opts.as ? extractorByName(opts.as) : void 0;
@@ -7741,8 +8035,9 @@ async function runGet(opts) {
7741
8035
  const exts = opts.exts ?? (forced ? forced.extensions : void 0);
7742
8036
  const fileInputs = inputs.filter((i) => i !== STDIN_TOKEN);
7743
8037
  const allowEmpty = opts.allowEmpty ?? config?.allowEmpty;
7744
- const exclude = [...config?.exclude ?? [], ...opts.exclude ?? []];
7745
- const { files, gitignoreSkipped } = await resolveTargetSet({
8038
+ const exclude = opts.exclude ?? [];
8039
+ const membersFor = (label) => memberOf(collections, configDir ?? cwd, base, label);
8040
+ const { files: walked, gitignoreSkipped } = await resolveTargetSet({
7746
8041
  inputs: fileInputs,
7747
8042
  exts,
7748
8043
  exclude,
@@ -7754,6 +8049,7 @@ async function runGet(opts) {
7754
8049
  onNotice: opts.onNotice
7755
8050
  })
7756
8051
  });
8052
+ const files = fromCollections ? retainMembers(walked, membersFor) : walked;
7757
8053
  assertNonEmpty({
7758
8054
  files,
7759
8055
  inputs: fileInputs,
@@ -7765,7 +8061,7 @@ async function runGet(opts) {
7765
8061
  action: "read"
7766
8062
  });
7767
8063
  const out = [];
7768
- const sidecars = await loadSidecars(config, {
8064
+ const externalMetadata = await loadExternalMetadata(collections, {
7769
8065
  configDir: configDir ?? cwd,
7770
8066
  base,
7771
8067
  offline: opts.offline ?? config?.offline ?? false
@@ -7777,14 +8073,16 @@ async function runGet(opts) {
7777
8073
  `Unsupported file type "${extension}" for "${label}". Supported: ${supportedExtensions().join(", ")}. Use --as to override.`
7778
8074
  );
7779
8075
  }
8076
+ const members = membersFor(label);
7780
8077
  let extracted;
7781
8078
  try {
7782
- extracted = mergeSidecars(
8079
+ extracted = mergeExternalMetadata(
7783
8080
  label,
7784
8081
  extractor.extract(content, label, {
7785
- elements: resolveElements(label, config)
8082
+ elements: resolveElements(label, config, members)
7786
8083
  }),
7787
- sidecars,
8084
+ externalMetadata,
8085
+ members,
7788
8086
  base
7789
8087
  ).extracted;
7790
8088
  } catch (err) {
@@ -7844,7 +8142,7 @@ function parseJsonPointer(pointer) {
7844
8142
  // src/meta/commands/schemas.ts
7845
8143
  import { existsSync as existsSync3 } from "node:fs";
7846
8144
  import { mkdir as mkdir2, readFile as readFile8 } from "node:fs/promises";
7847
- import { dirname as dirname5, extname as extname4, isAbsolute as isAbsolute3, join as join5, relative as relative7, resolve as resolve11, sep as sep3 } from "node:path";
8145
+ 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";
7848
8146
  import { parseDocument as parseDocument4, parse as parseYaml3, stringify } from "yaml";
7849
8147
  function getSchemasInfo() {
7850
8148
  return { builtins: listBuiltins(), formats: listFormats() };
@@ -7852,7 +8150,7 @@ function getSchemasInfo() {
7852
8150
  var DEFAULT_VENDOR_DIR = "./schema";
7853
8151
  var DEFAULT_CONFIG_NAME = "manni.config.yaml";
7854
8152
  function posixRelative(from, to) {
7855
- return relative7(from, to).split(sep3).join("/");
8153
+ return relative8(from, to).split(sep4).join("/");
7856
8154
  }
7857
8155
  function vendorFileName(url) {
7858
8156
  let parsed2;
@@ -7944,7 +8242,7 @@ async function runVendorSchema(opts) {
7944
8242
  const absFile = join5(absDir, vendorFileName(url));
7945
8243
  await assertNotIgnored(absFile, absDir, cwd, opts.onNotice);
7946
8244
  const fromConfig = posixRelative(configDir, absFile);
7947
- const ref = isAbsolute3(fromConfig) || fromConfig === "" ? absFile : fromConfig.startsWith(".") ? fromConfig : `./${fromConfig}`;
8245
+ const ref = isAbsolute4(fromConfig) || fromConfig === "" ? absFile : fromConfig.startsWith(".") ? fromConfig : `./${fromConfig}`;
7948
8246
  const { bytes } = await fetchSchemaBytes(url, {
7949
8247
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
7950
8248
  ...opts.maxBytes !== void 0 ? { maxBytes: opts.maxBytes } : {}
@@ -8088,22 +8386,23 @@ async function runInferSchema(opts) {
8088
8386
  `--min-coverage must be a percentage between 0 and 100, got ${minCoverage}.`
8089
8387
  );
8090
8388
  }
8091
- const { config, inputs, base, configDir } = await resolveRunConfig({
8389
+ const { config, inputs, base, configDir, collections, fromCollections } = await resolveRunConfig({
8092
8390
  cwd,
8093
8391
  configPath: opts.configPath,
8094
8392
  noConfig: opts.noConfig,
8095
8393
  inputs: opts.inputs,
8394
+ ...opts.collections !== void 0 ? { collections: opts.collections } : {},
8096
8395
  onConfigLoaded: opts.onConfigLoaded
8097
8396
  });
8098
8397
  const usingStdin = inputs.includes(STDIN_TOKEN);
8099
- const sidecars = await loadSidecars(config, {
8398
+ const externalMetadata = await loadExternalMetadata(collections, {
8100
8399
  configDir: configDir ?? cwd,
8101
8400
  base,
8102
8401
  offline: opts.offline ?? config?.offline ?? false
8103
8402
  });
8104
8403
  if (inputs.length === 0) {
8105
8404
  throw new DocmetaError(
8106
- "No files to scan. Pass paths/globs, or add `paths:` under `meta:` in manni.config.yaml."
8405
+ "No files to scan. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml."
8107
8406
  );
8108
8407
  }
8109
8408
  const forced = opts.as ? extractorByName(opts.as) : void 0;
@@ -8136,8 +8435,9 @@ async function runInferSchema(opts) {
8136
8435
  const exts = opts.exts ?? (forced ? forced.extensions : void 0);
8137
8436
  const fileInputs = inputs.filter((i) => i !== STDIN_TOKEN);
8138
8437
  const allowEmpty = opts.allowEmpty ?? config?.allowEmpty;
8139
- const exclude = [...config?.exclude ?? [], ...opts.exclude ?? []];
8140
- const { files, gitignoreSkipped } = await resolveTargetSet({
8438
+ const exclude = opts.exclude ?? [];
8439
+ const membersFor = (label) => memberOf(collections, configDir ?? cwd, base, label);
8440
+ const { files: walked, gitignoreSkipped } = await resolveTargetSet({
8141
8441
  inputs: fileInputs,
8142
8442
  exts,
8143
8443
  exclude,
@@ -8149,6 +8449,7 @@ async function runInferSchema(opts) {
8149
8449
  onNotice: opts.onNotice
8150
8450
  })
8151
8451
  });
8452
+ const files = fromCollections ? retainMembers(walked, membersFor) : walked;
8152
8453
  assertNonEmpty({
8153
8454
  files,
8154
8455
  inputs: fileInputs,
@@ -8171,14 +8472,16 @@ async function runInferSchema(opts) {
8171
8472
  );
8172
8473
  }
8173
8474
  filesScanned += 1;
8475
+ const members = membersFor(label);
8174
8476
  let extracted;
8175
8477
  try {
8176
- extracted = mergeSidecars(
8478
+ extracted = mergeExternalMetadata(
8177
8479
  label,
8178
8480
  extractor.extract(content, label, {
8179
- elements: resolveElements(label, config)
8481
+ elements: resolveElements(label, config, members)
8180
8482
  }),
8181
- sidecars,
8483
+ externalMetadata,
8484
+ members,
8182
8485
  base
8183
8486
  ).extracted;
8184
8487
  } catch (err) {
@@ -8264,7 +8567,7 @@ async function runInferSchema(opts) {
8264
8567
  import { randomBytes } from "node:crypto";
8265
8568
  import { existsSync as existsSync4 } from "node:fs";
8266
8569
  import { mkdir as mkdir3, open, readFile as readFile9, rename as rename2, rm as rm2 } from "node:fs/promises";
8267
- import { dirname as dirname6, isAbsolute as isAbsolute4, resolve as resolve12, extname as extname5, sep as sep4 } from "node:path";
8570
+ import { dirname as dirname6, isAbsolute as isAbsolute5, resolve as resolve12, extname as extname5, sep as sep5 } from "node:path";
8268
8571
  import { parseDocument as parseDocument5, isMap as isMap4, isSeq as isSeq2, isScalar as isScalar3 } from "yaml";
8269
8572
  async function runQuery(opts) {
8270
8573
  const cwd = opts.cwd ?? process.cwd();
@@ -8297,19 +8600,29 @@ async function runQuery(opts) {
8297
8600
  );
8298
8601
  }
8299
8602
  }
8300
- const { config, inputs, base, configDir, configPath } = await resolveRunConfig(
8301
- {
8302
- cwd,
8303
- configPath: opts.configPath,
8304
- noConfig: opts.noConfig,
8305
- inputs: opts.inputs,
8306
- onConfigLoaded: opts.onConfigLoaded
8307
- }
8308
- );
8603
+ const {
8604
+ config,
8605
+ inputs,
8606
+ base,
8607
+ configDir,
8608
+ configPath,
8609
+ configSection,
8610
+ collections,
8611
+ declaredCollections,
8612
+ fromCollections
8613
+ } = await resolveRunConfig({
8614
+ cwd,
8615
+ configPath: opts.configPath,
8616
+ noConfig: opts.noConfig,
8617
+ inputs: opts.inputs,
8618
+ ...opts.collections !== void 0 ? { collections: opts.collections } : {},
8619
+ onConfigLoaded: opts.onConfigLoaded
8620
+ });
8621
+ const membersFor = (label) => memberOf(collections, configDir ?? cwd, base, label);
8309
8622
  const usingStdin = inputs.includes(STDIN_TOKEN);
8310
8623
  if (inputs.length === 0) {
8311
8624
  throw new DocmetaError(
8312
- "No files to read. Pass paths/globs, or add `paths:` under `meta:` in manni.config.yaml."
8625
+ "No files to read. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml."
8313
8626
  );
8314
8627
  }
8315
8628
  const forced = opts.as ? extractorByName(opts.as) : void 0;
@@ -8321,8 +8634,8 @@ async function runQuery(opts) {
8321
8634
  const exts = opts.exts ?? (forced ? forced.extensions : void 0);
8322
8635
  const fileInputs = inputs.filter((i) => i !== STDIN_TOKEN);
8323
8636
  const allowEmpty = opts.allowEmpty ?? config?.allowEmpty;
8324
- const exclude = [...config?.exclude ?? [], ...opts.exclude ?? []];
8325
- const { files, gitignoreSkipped } = await resolveTargetSet({
8637
+ const exclude = opts.exclude ?? [];
8638
+ const { files: walked, gitignoreSkipped } = await resolveTargetSet({
8326
8639
  inputs: fileInputs,
8327
8640
  exts,
8328
8641
  exclude,
@@ -8334,6 +8647,7 @@ async function runQuery(opts) {
8334
8647
  onNotice: opts.onNotice
8335
8648
  })
8336
8649
  });
8650
+ const files = fromCollections ? retainMembers(walked, membersFor) : walked;
8337
8651
  assertNonEmpty({
8338
8652
  files,
8339
8653
  inputs: fileInputs,
@@ -8345,7 +8659,7 @@ async function runQuery(opts) {
8345
8659
  action: "queried"
8346
8660
  });
8347
8661
  const entries = [];
8348
- const sidecars = await loadSidecars(config, {
8662
+ const externalMetadata = await loadExternalMetadata(collections, {
8349
8663
  configDir: configDir ?? cwd,
8350
8664
  base,
8351
8665
  offline: opts.offline ?? config?.offline ?? false
@@ -8357,12 +8671,14 @@ async function runQuery(opts) {
8357
8671
  `Unsupported file type "${extension}" for "${label}". Supported: ${supportedExtensions().join(", ")}. Use --as to override.`
8358
8672
  );
8359
8673
  }
8360
- const extracted = mergeSidecars(
8674
+ const members = membersFor(label);
8675
+ const extracted = mergeExternalMetadata(
8361
8676
  label,
8362
8677
  extractor.extract(content, label, {
8363
- elements: resolveElements(label, config)
8678
+ elements: resolveElements(label, config, members)
8364
8679
  }),
8365
- sidecars,
8680
+ externalMetadata,
8681
+ members,
8366
8682
  base
8367
8683
  ).extracted;
8368
8684
  entries.push({ label, extracted, extractor });
@@ -8396,8 +8712,12 @@ async function runQuery(opts) {
8396
8712
  cwd,
8397
8713
  configDir,
8398
8714
  configPath,
8715
+ configSection,
8399
8716
  trustRoot: schemaTrustRoot(cwd, configDir),
8400
- sidecars,
8717
+ externalMetadata,
8718
+ collections,
8719
+ declaredCollections,
8720
+ memberships: membersFor,
8401
8721
  onNotice: opts.onNotice
8402
8722
  });
8403
8723
  return { ...run, frame: { cwd, base: configDir ?? cwd, runBase: base } };
@@ -8440,7 +8760,7 @@ async function prepareDbTarget(resolved, display) {
8440
8760
  var MissingCollectionView = class extends Error {
8441
8761
  };
8442
8762
  function refuseSqlError(err) {
8443
- const message = err.message;
8763
+ const message = errorMessage(err);
8444
8764
  if (message.includes("UNIQUE constraint failed: docs._path")) {
8445
8765
  throw new DocmetaError("That _path already exists in the corpus.");
8446
8766
  }
@@ -8469,7 +8789,7 @@ async function runSql(sql, entries, ctx) {
8469
8789
  db = new DatabaseSync(target ? target.resolved : ":memory:");
8470
8790
  } catch (err) {
8471
8791
  throw new DocmetaError(
8472
- `Cannot open "${target?.display ?? ":memory:"}": ${err.message}`
8792
+ `Cannot open "${target?.display ?? ":memory:"}": ${errorMessage(err)}`
8473
8793
  );
8474
8794
  }
8475
8795
  const dbInfo = target ? {
@@ -8485,10 +8805,9 @@ async function runSql(sql, entries, ctx) {
8485
8805
  createCollectionViews(
8486
8806
  db,
8487
8807
  collectCollections(entries, {
8488
- config: ctx.config,
8489
- fileBase: ctx.cwd,
8490
- trustRoot: ctx.trustRoot,
8491
- ...ctx.onNotice ? { onNotice: ctx.onNotice } : {}
8808
+ collections: ctx.declaredCollections,
8809
+ ...ctx.configDir !== void 0 ? { configDir: ctx.configDir } : {},
8810
+ base: ctx.base
8492
8811
  })
8493
8812
  );
8494
8813
  viewsBuilt = true;
@@ -8504,7 +8823,9 @@ async function runSql(sql, entries, ctx) {
8504
8823
  );
8505
8824
  }
8506
8825
  const lower = sql.toLowerCase();
8507
- 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()));
8826
+ const observesCatalog = lower.includes("sqlite_master") || lower.includes("sqlite_schema") || lower.includes("pragma") || /^(create|drop|alter)\b/i.test(head) || collectionNames(ctx.declaredCollections).some(
8827
+ (n) => lower.includes(n.toLowerCase())
8828
+ );
8508
8829
  if (observesCatalog) buildViews();
8509
8830
  const sentinel = `manni:null:${randomBytes(16).toString("hex")}`;
8510
8831
  db.function("explicit_null", () => sentinel);
@@ -8516,9 +8837,9 @@ async function runSql(sql, entries, ctx) {
8516
8837
  stmt = db.prepare(sql);
8517
8838
  columns = stmt.columns().map((c) => c.name);
8518
8839
  } catch (err) {
8519
- const message = err.message;
8840
+ const message = errorMessage(err);
8520
8841
  const missing = missingTableName(message);
8521
- if (missing !== void 0 && !viewsBuilt && namesConfiguredCollection(missing, ctx.config)) {
8842
+ if (missing !== void 0 && !viewsBuilt && namesConfiguredCollection(missing, ctx.declaredCollections)) {
8522
8843
  throw new MissingCollectionView();
8523
8844
  }
8524
8845
  refuseSqlError(err);
@@ -8592,9 +8913,9 @@ function missingTableName(message) {
8592
8913
  const at = message.indexOf(prefix);
8593
8914
  return at === -1 ? void 0 : message.slice(at + prefix.length);
8594
8915
  }
8595
- function namesConfiguredCollection(missing, config) {
8916
+ function namesConfiguredCollection(missing, collections) {
8596
8917
  const candidates = [missing, missing.replace(/^(main|temp)\./i, "")];
8597
- return collectionNames(config).some(
8918
+ return collectionNames(collections).some(
8598
8919
  (n) => candidates.some((c) => c.toLowerCase() === n.toLowerCase())
8599
8920
  );
8600
8921
  }
@@ -8913,7 +9234,7 @@ function displayPath(ctx, abs) {
8913
9234
  }
8914
9235
  function assertSchemaWriteWithin(ctx, abs) {
8915
9236
  const within = posixRelative(ctx.trustRoot.dir, abs);
8916
- if (within !== "" && !within.startsWith("..") && !isAbsolute4(within)) return;
9237
+ if (within !== "" && !within.startsWith("..") && !isAbsolute5(within)) return;
8917
9238
  throw new DocmetaError(
8918
9239
  `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.`
8919
9240
  );
@@ -9030,12 +9351,13 @@ async function planSchemaMutation(ops, entries, ctx) {
9030
9351
  filePath: e.label,
9031
9352
  fileSchema: e.extracted.data[FILE_SCHEMA_KEY],
9032
9353
  config: ctx.config,
9354
+ memberOf: ctx.memberships(e.label),
9033
9355
  fileBase: ctx.cwd,
9034
9356
  trustRoot: ctx.trustRoot,
9035
9357
  onNotice: ctx.onNotice
9036
9358
  });
9037
9359
  } catch (err) {
9038
- throw new DocmetaError(`"${e.label}": ${err.message}`);
9360
+ throw new DocmetaError(`"${e.label}": ${errorMessage(err)}`);
9039
9361
  }
9040
9362
  sources.add(resolved.source);
9041
9363
  if (resolved.overrideIndex !== void 0) {
@@ -9056,7 +9378,11 @@ async function planSchemaMutation(ops, entries, ctx) {
9056
9378
  const overrides = ctx.config?.overrides ?? [];
9057
9379
  const named = [...groupIndexes].sort((a, b) => a - b).flatMap((i) => {
9058
9380
  const o = overrides[i];
9059
- return o?.name !== void 0 ? [`${o.name} (${overrideGlobs(o.files).join(", ")})`] : [];
9381
+ if (o?.collection === void 0) return [];
9382
+ const declared = ctx.collections.find((c) => c.name === o.collection);
9383
+ return [
9384
+ `${o.collection} (${overrideGlobs(declared?.paths).join(", ")})`
9385
+ ];
9060
9386
  });
9061
9387
  const last = named[named.length - 1];
9062
9388
  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.";
@@ -9321,7 +9647,7 @@ async function planConfigEdit(ctx, edit, runRefs) {
9321
9647
  }
9322
9648
  return void 0;
9323
9649
  };
9324
- const rebaseRaw = (raw) => classifyRef(raw).kind === "file" && !isAbsolute4(raw) ? resolve12(configDir, raw) : raw;
9650
+ const rebaseRaw = (raw) => classifyRef(raw).kind === "file" && !isAbsolute5(raw) ? resolve12(configDir, raw) : raw;
9325
9651
  const wantSet = runRefs.map(rebaseRaw).sort().join("\n");
9326
9652
  const seqResolvesToRunSet = (node) => {
9327
9653
  if (!isSeq2(node)) return false;
@@ -9347,9 +9673,11 @@ async function planConfigEdit(ctx, edit, runRefs) {
9347
9673
  }
9348
9674
  return n;
9349
9675
  };
9350
- const topSeq = doc.get("schemas", true);
9676
+ const root = ctx.configSection === void 0 ? doc : doc.get(ctx.configSection, true);
9677
+ const keyOf2 = (key) => root === doc ? doc.get(key, true) : isMap4(root) ? root.get(key, true) : void 0;
9678
+ const topSeq = keyOf2("schemas");
9351
9679
  let repointCount = repoint(topSeq);
9352
- const overrides = doc.get("overrides", true);
9680
+ const overrides = keyOf2("overrides");
9353
9681
  if (isSeq2(overrides)) {
9354
9682
  for (const entry of overrides.items) {
9355
9683
  if (isMap4(entry)) repointCount += repoint(entry.get("schemas", true));
@@ -9364,7 +9692,7 @@ async function planConfigEdit(ctx, edit, runRefs) {
9364
9692
  if (!isMap4(item)) continue;
9365
9693
  const raw = rawRefOf(item);
9366
9694
  if (raw === void 0 || classifyRef(raw).kind !== "file") continue;
9367
- const abs = isAbsolute4(raw) ? raw : resolve12(configDir, raw);
9695
+ const abs = isAbsolute5(raw) ? raw : resolve12(configDir, raw);
9368
9696
  if (foldPath(abs) !== wantPath) continue;
9369
9697
  pinFrom = item.get("integrity");
9370
9698
  item.set("integrity", edit.integrity.value);
@@ -9611,28 +9939,28 @@ function buildChanges(diff, entries, sentinel, ctx, renameHints = [], booleanAdd
9611
9939
  }
9612
9940
  changes.push({ file: from, renamed: to, written: false });
9613
9941
  }
9614
- refuseSidecarWrites(changes, entries, ctx);
9942
+ refuseExternalMetadataWrites(changes, entries, ctx);
9615
9943
  return changes;
9616
9944
  }
9617
9945
  function validateNewPath(p, base) {
9618
9946
  const refuse = (why) => {
9619
9947
  throw new DocmetaError(`"${p}" is not a usable path: ${why}.`);
9620
9948
  };
9621
- if (isAbsolute4(p)) refuse("it is absolute");
9949
+ if (isAbsolute5(p)) refuse("it is absolute");
9622
9950
  if (p.split(/[\\/]/).some((segment) => segment === "..")) {
9623
9951
  refuse("it traverses upward");
9624
9952
  }
9625
9953
  const abs = resolve12(base, p);
9626
9954
  if (abs === resolve12(base)) refuse("it points at the corpus root, not a file");
9627
- if (!abs.startsWith(resolve12(base) + sep4)) refuse("it escapes the corpus");
9955
+ if (!abs.startsWith(resolve12(base) + sep5)) refuse("it escapes the corpus");
9628
9956
  }
9629
- function refuseSidecarWrites(changes, entries, ctx) {
9630
- const index = ctx.sidecars;
9957
+ function refuseExternalMetadataWrites(changes, entries, ctx) {
9958
+ const index = ctx.externalMetadata;
9631
9959
  if (!index) return;
9632
- const owned = (key) => index.owners.get(key);
9960
+ const owned = (key) => index.owners.get(key)?.[0]?.file;
9633
9961
  const refuse = (file, key, owner) => {
9634
9962
  throw new DocmetaError(
9635
- `"${file}": "${key}" is owned by sidecar ${owner}; edit the sidecar file instead.`
9963
+ `"${file}": "${key}" is owned by manifest ${owner}; edit the manifest instead.`
9636
9964
  );
9637
9965
  };
9638
9966
  const data = new Map(entries.map((e) => [e.label, e.extracted.data]));
@@ -9657,7 +9985,7 @@ function refuseSidecarWrites(changes, entries, ctx) {
9657
9985
  if (entry !== void 0 && entry.size > 0) {
9658
9986
  const manifest = [...entry.values()][0]?.file ?? "manifest";
9659
9987
  throw new DocmetaError(
9660
- `"${c.file}": sidecar ${manifest} names it; rename the manifest entry first.`
9988
+ `"${c.file}": manifest ${manifest} names it; rename the manifest entry first.`
9661
9989
  );
9662
9990
  }
9663
9991
  continue;
@@ -9674,7 +10002,7 @@ function refuseSidecarWrites(changes, entries, ctx) {
9674
10002
  if (entry !== void 0) {
9675
10003
  const manifest = entry.values().next().value?.file ?? "manifest";
9676
10004
  throw new DocmetaError(
9677
- `"${c.file}": "${key}" is the field sidecar ${manifest} joins on, and this document has an entry; change the manifest first.`
10005
+ `"${c.file}": "${key}" is the field manifest ${manifest} joins on, and this document has an entry; change the manifest first.`
9678
10006
  );
9679
10007
  }
9680
10008
  }
@@ -9738,7 +10066,9 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
9738
10066
  path,
9739
10067
  content: extractor.apply("", ops.created, {
9740
10068
  filePath: label,
9741
- elements: resolveElements(label, ctx.config)
10069
+ // The new file's memberships, from its path alone. An INSERT has to
10070
+ // write through the element set a read of that file would use.
10071
+ elements: resolveElements(label, ctx.config, ctx.memberships(label))
9742
10072
  }),
9743
10073
  ensureDir: true
9744
10074
  });
@@ -9747,12 +10077,14 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
9747
10077
  const entry = byLabel.get(label);
9748
10078
  if (!entry) throw new DocmetaError(`No loaded entry for "${label}".`);
9749
10079
  const content = await readFile9(path, "utf8");
9750
- const current = mergeSidecars(
10080
+ const members = ctx.memberships(label);
10081
+ const current = mergeExternalMetadata(
9751
10082
  label,
9752
10083
  entry.extractor.extract(content, label, {
9753
- elements: resolveElements(label, ctx.config)
10084
+ elements: resolveElements(label, ctx.config, members)
9754
10085
  }),
9755
- ctx.sidecars,
10086
+ ctx.externalMetadata,
10087
+ members,
9756
10088
  ctx.base
9757
10089
  ).extracted;
9758
10090
  if (ops.cleared) {
@@ -9784,16 +10116,19 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
9784
10116
  }
9785
10117
  const applied = entry.extractor.apply(content, ops.patch, {
9786
10118
  filePath: label,
9787
- elements: resolveElements(label, ctx.config),
10119
+ // The memberships the read above used. Without them a collection-scoped
10120
+ // element the read found has nowhere to be written.
10121
+ elements: resolveElements(label, ctx.config, members),
9788
10122
  deletions: ops.deletions
9789
10123
  });
9790
10124
  if (ops.deletions.length > 0) {
9791
- const check = mergeSidecars(
10125
+ const check = mergeExternalMetadata(
9792
10126
  label,
9793
10127
  entry.extractor.extract(applied, label, {
9794
- elements: resolveElements(label, ctx.config)
10128
+ elements: resolveElements(label, ctx.config, members)
9795
10129
  }),
9796
- ctx.sidecars,
10130
+ ctx.externalMetadata,
10131
+ members,
9797
10132
  ctx.base
9798
10133
  ).extracted;
9799
10134
  for (const key of ops.deletions) {
@@ -9846,7 +10181,7 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
9846
10181
  await rename2(r.from, r.to);
9847
10182
  } catch (err) {
9848
10183
  throw new DocmetaError(
9849
- `Cannot move "${r.from}" to "${r.to}": ${err.message}`
10184
+ `Cannot move "${r.from}" to "${r.to}": ${errorMessage(err)}`
9850
10185
  );
9851
10186
  }
9852
10187
  }
@@ -10091,11 +10426,12 @@ function addUsage(total, next) {
10091
10426
  }
10092
10427
  async function runFill(opts) {
10093
10428
  const cwd = opts.cwd ?? process.cwd();
10094
- const { config, inputs, base, configDir } = await resolveRunConfig({
10429
+ const { config, inputs, base, configDir, collections, fromCollections } = await resolveRunConfig({
10095
10430
  cwd,
10096
10431
  configPath: opts.configPath,
10097
10432
  noConfig: opts.noConfig,
10098
10433
  inputs: opts.inputs,
10434
+ ...opts.collections !== void 0 ? { collections: opts.collections } : {},
10099
10435
  onConfigLoaded: opts.onConfigLoaded
10100
10436
  });
10101
10437
  const schemaOptions = schemaLoadOptions({
@@ -10110,7 +10446,7 @@ async function runFill(opts) {
10110
10446
  const usingStdin = inputs.includes(STDIN_TOKEN);
10111
10447
  if (inputs.length === 0) {
10112
10448
  throw new DocmetaError(
10113
- "No files to fill. Pass paths/globs, or add `paths:` under `meta:` in manni.config.yaml."
10449
+ "No files to fill. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml."
10114
10450
  );
10115
10451
  }
10116
10452
  const forcedExtractor = opts.as ? extractorByName(opts.as) : void 0;
@@ -10159,8 +10495,9 @@ async function runFill(opts) {
10159
10495
  const fileInputs = inputs.filter((i) => i !== STDIN_TOKEN);
10160
10496
  const allowEmpty = opts.allowEmpty ?? config?.allowEmpty;
10161
10497
  const fillExts = opts.exts ?? forcedExtractor?.extensions;
10162
- const fillExclude = [...config?.exclude ?? [], ...opts.exclude ?? []];
10163
- const { files, gitignoreSkipped } = await resolveTargetSet({
10498
+ const fillExclude = opts.exclude ?? [];
10499
+ const membersFor = (label) => memberOf(collections, configDir ?? cwd, base, label);
10500
+ const { files: walked, gitignoreSkipped } = await resolveTargetSet({
10164
10501
  inputs: fileInputs,
10165
10502
  exts: fillExts,
10166
10503
  exclude: fillExclude,
@@ -10172,6 +10509,7 @@ async function runFill(opts) {
10172
10509
  onNotice: opts.onNotice
10173
10510
  })
10174
10511
  });
10512
+ const files = fromCollections ? retainMembers(walked, membersFor) : walked;
10175
10513
  assertNonEmpty({
10176
10514
  files,
10177
10515
  inputs: fileInputs,
@@ -10206,7 +10544,7 @@ async function runFill(opts) {
10206
10544
  provider = construct();
10207
10545
  } catch (err) {
10208
10546
  throw new DocmetaError(
10209
- err instanceof InferenceError ? err.message : `Could not construct the "${providerName}" provider: ${err.message}`
10547
+ err instanceof InferenceError ? err.message : `Could not construct the "${providerName}" provider: ${errorMessage(err)}`
10210
10548
  );
10211
10549
  }
10212
10550
  }
@@ -10221,7 +10559,7 @@ async function runFill(opts) {
10221
10559
  let turnsUsed = 0;
10222
10560
  let inFlight = 0;
10223
10561
  const turnsExhausted = () => maxTurns != null && turnsUsed + inFlight >= maxTurns;
10224
- const sidecars = await loadSidecars(config, {
10562
+ const externalMetadata = await loadExternalMetadata(collections, {
10225
10563
  configDir: configDir ?? cwd,
10226
10564
  base,
10227
10565
  offline: opts.offline ?? config?.offline ?? false
@@ -10242,19 +10580,21 @@ async function runFill(opts) {
10242
10580
  `The "${extractor.name}" format is read-only; manni meta fill cannot write metadata back to it.`
10243
10581
  );
10244
10582
  }
10245
- const elements = resolveElements(label, config);
10583
+ const members = membersFor(label);
10584
+ const elements = resolveElements(label, config, members);
10246
10585
  let extracted;
10247
10586
  let merged;
10248
10587
  try {
10249
- merged = mergeSidecars(
10588
+ merged = mergeExternalMetadata(
10250
10589
  label,
10251
10590
  extractor.extract(content, label, { elements }),
10252
- sidecars,
10591
+ externalMetadata,
10592
+ members,
10253
10593
  base
10254
10594
  );
10255
10595
  extracted = merged.extracted;
10256
10596
  } catch (err) {
10257
- return errorResult(label, extractor.name, err.message);
10597
+ return errorResult(label, extractor.name, errorMessage(err));
10258
10598
  }
10259
10599
  let resolved;
10260
10600
  try {
@@ -10263,6 +10603,7 @@ async function runFill(opts) {
10263
10603
  fileSchema: extracted.data[FILE_SCHEMA_KEY],
10264
10604
  cliSchemas: opts.cliSchemas,
10265
10605
  config,
10606
+ memberOf: members,
10266
10607
  // Same trust boundary as `validate`: `fill` writes metadata back into
10267
10608
  // the document, so a schema a document chose for itself decides what
10268
10609
  // gets written — if anything, a stronger reason to guard it.
@@ -10271,7 +10612,7 @@ async function runFill(opts) {
10271
10612
  onNotice: opts.onNotice
10272
10613
  });
10273
10614
  } catch (err) {
10274
- return errorResult(label, extractor.name, err.message);
10615
+ return errorResult(label, extractor.name, errorMessage(err));
10275
10616
  }
10276
10617
  const schemaSet = resolved.schemas;
10277
10618
  let schemas;
@@ -10299,16 +10640,16 @@ async function runFill(opts) {
10299
10640
  existingErrors,
10300
10641
  only
10301
10642
  );
10302
- const ownedCandidates = sidecars ? candidates.flatMap((c) => {
10303
- const file = sidecars.owners.get(c.key);
10304
- return file === void 0 ? [] : [{ key: c.key, file }];
10643
+ const ownedCandidates = externalMetadata ? candidates.flatMap((c) => {
10644
+ const owner = externalMetadata.owners.get(c.key)?.[0];
10645
+ return owner === void 0 ? [] : [{ key: c.key, file: owner.file }];
10305
10646
  }) : [];
10306
10647
  const firstOwned = ownedCandidates[0];
10307
10648
  if (firstOwned) {
10308
10649
  return errorResult(
10309
10650
  label,
10310
10651
  extractor.name,
10311
- `"${firstOwned.key}" is owned by sidecar ${firstOwned.file}; manni meta fill cannot write a sidecar key. Add it to the manifest instead.`,
10652
+ `"${firstOwned.key}" is owned by manifest ${firstOwned.file}; manni meta fill cannot write a manifest key. Add it to the manifest instead.`,
10312
10653
  schemaSet
10313
10654
  );
10314
10655
  }
@@ -10325,7 +10666,7 @@ async function runFill(opts) {
10325
10666
  try {
10326
10667
  extractor.apply(content, {}, { filePath: label, elements });
10327
10668
  } catch (err) {
10328
- return errorResult(label, extractor.name, err.message, schemaSet);
10669
+ return errorResult(label, extractor.name, errorMessage(err), schemaSet);
10329
10670
  }
10330
10671
  const cacheKey = buildCacheKey([
10331
10672
  identity.provider,
@@ -10363,7 +10704,7 @@ async function runFill(opts) {
10363
10704
  return errorResult(
10364
10705
  label,
10365
10706
  extractor.name,
10366
- `Could not build a proposal schema from ${schemaSet.join(", ")}: ${err.message}`,
10707
+ `Could not build a proposal schema from ${schemaSet.join(", ")}: ${errorMessage(err)}`,
10367
10708
  schemaSet
10368
10709
  );
10369
10710
  }
@@ -10491,7 +10832,7 @@ async function runFill(opts) {
10491
10832
  return errorResult(
10492
10833
  label,
10493
10834
  extractor.name,
10494
- err.message,
10835
+ errorMessage(err),
10495
10836
  schemaSet,
10496
10837
  fields
10497
10838
  );
@@ -10791,7 +11132,7 @@ async function resolveIdentity(spec) {
10791
11132
  return await resolveProviderIdentityAsync(spec);
10792
11133
  } catch (err) {
10793
11134
  throw new DocmetaError(
10794
- err instanceof InferenceError ? err.message : `Could not resolve provider "${spec.provider}": ${err.message}`
11135
+ err instanceof InferenceError ? err.message : `Could not resolve provider "${spec.provider}": ${errorMessage(err)}`
10795
11136
  );
10796
11137
  }
10797
11138
  }
@@ -10838,19 +11179,19 @@ async function mapConcurrent(items, limit, fn) {
10838
11179
  }
10839
11180
 
10840
11181
  // src/meta/reporters/sarif.ts
10841
- import { isAbsolute as isAbsolute5 } from "node:path";
11182
+ import { isAbsolute as isAbsolute6 } from "node:path";
10842
11183
 
10843
11184
  // src/meta/reporters/rule-id.ts
10844
11185
  var SYNTHETIC_SCHEMA = "(parse)";
10845
11186
  var PARSE_ERROR_RULE = "manni/parse-error";
10846
11187
  var SCHEMA_ERROR_RULE = "manni/schema-error";
10847
- var SIDECAR_OWNED_RULE = "sidecar:owned/sidecar";
10848
- var SIDECAR_DUPLICATE_RULE = "sidecar:duplicate/sidecar";
11188
+ var EXTERNAL_OWNED_RULE = "external:owned/external";
11189
+ var EXTERNAL_DUPLICATE_RULE = "external:duplicate/external";
10849
11190
  var RESERVED_RULES = {
10850
11191
  [PARSE_ERROR_RULE]: "The document's metadata block could not be parsed.",
10851
11192
  [SCHEMA_ERROR_RULE]: "No schema set could be resolved for the document.",
10852
- [SIDECAR_OWNED_RULE]: "The document carries a key a sidecar manifest owns; the manifest is the only place that key may be set.",
10853
- [SIDECAR_DUPLICATE_RULE]: "Two documents carry the same value of a sidecar's join field, so one manifest entry matched both."
11193
+ [EXTERNAL_OWNED_RULE]: "The document carries a key a manifest owns; the manifest is the only place that key may be set.",
11194
+ [EXTERNAL_DUPLICATE_RULE]: "Two documents carry the same value of a manifest's join field, so one manifest entry matched both."
10854
11195
  };
10855
11196
  function ruleIdFor(e, frame) {
10856
11197
  if (e.schema === SYNTHETIC_SCHEMA) {
@@ -10878,7 +11219,7 @@ function uriFrame(frame) {
10878
11219
  }
10879
11220
  function artifactUri(file, frame) {
10880
11221
  const uri = canonicalFilePath(file, frame);
10881
- if (uri === "" || uri.startsWith("../") || isAbsolute5(uri)) return null;
11222
+ if (uri === "" || uri.startsWith("../") || isAbsolute6(uri)) return null;
10882
11223
  return uri;
10883
11224
  }
10884
11225
  function renderSarif(results, opts = {}) {
@@ -11416,8 +11757,13 @@ export {
11416
11757
  palette,
11417
11758
  shouldColor,
11418
11759
  ToolError,
11760
+ errorMessage,
11419
11761
  programName,
11420
11762
  setProgramName,
11763
+ externalMetadataUrlProblem,
11764
+ parseCollections,
11765
+ selectCollections,
11766
+ isMember,
11421
11767
  findConfigFile,
11422
11768
  readConfigFile,
11423
11769
  escapeWorkflowCommandMessage,
@@ -11443,32 +11789,32 @@ export {
11443
11789
  collectSchemaPins,
11444
11790
  rebaseConfigSchemaRefs,
11445
11791
  resolveSchemaSet,
11446
- SIDECAR_FETCH_TIMEOUT_MS,
11447
- SIDECAR_FETCH_MAX_BYTES,
11448
- sidecarUrlProblem,
11449
- fetchSidecar,
11792
+ EXTERNAL_METADATA_FETCH_TIMEOUT_MS,
11793
+ EXTERNAL_METADATA_FETCH_MAX_BYTES,
11794
+ fetchExternalMetadata,
11450
11795
  locateFrontmatter,
11451
11796
  frontmatterInnerText,
11452
11797
  extractFrontmatter,
11453
11798
  applyFrontmatter,
11454
11799
  extractorForExtension,
11455
11800
  supportedExtensions,
11456
- SIDECAR_OWNED_SCHEMA,
11457
- SIDECAR_DUPLICATE_SCHEMA,
11458
- SIDECAR_KEYWORD,
11801
+ EXTERNAL_OWNED_SCHEMA,
11802
+ EXTERNAL_DUPLICATE_SCHEMA,
11803
+ EXTERNAL_KEYWORD,
11459
11804
  PATH_JOIN,
11460
- sidecarJoin,
11461
- loadSidecars,
11462
- mergeSidecars,
11463
- sidecarPointer,
11805
+ externalMetadataJoin,
11806
+ loadExternalMetadata,
11807
+ mergeExternalMetadata,
11808
+ externalMetadataPointer,
11464
11809
  orphanEntries,
11465
11810
  orphanJoins,
11466
11811
  orphanError,
11467
11812
  parseConfig,
11468
11813
  loadConfig,
11469
11814
  resolveRunConfig,
11470
- Validator,
11471
11815
  isParamName,
11816
+ memberOf,
11817
+ Validator,
11472
11818
  rowsToFindings,
11473
11819
  runValidate,
11474
11820
  runGet,
@@ -11504,4 +11850,4 @@ export {
11504
11850
  renderQuery,
11505
11851
  renderQueryCsv
11506
11852
  };
11507
- //# sourceMappingURL=chunk-7M2YOXVG.js.map
11853
+ //# sourceMappingURL=chunk-IRHWILCO.js.map