@hawkeyexl/manni 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@hawkeyexl/manni",
6
- version: "0.3.0",
6
+ version: "1.0.0",
7
7
  description: "One command line for documentation that is meant to be checked. `manni meta` validates the presence and format of document metadata (Markdown frontmatter and more) against JSON Schema, built for CI. Formerly docmeta.",
8
8
  type: "module",
9
9
  bin: {
@@ -117,6 +117,276 @@ var package_default = {
117
117
  }
118
118
  };
119
119
 
120
+ // src/shared/collections.ts
121
+ import picomatch2 from "picomatch";
122
+
123
+ // src/shared/globs.ts
124
+ import picomatch from "picomatch";
125
+ var matcherCache = /* @__PURE__ */ new Map();
126
+ function matchesFileGlob(relPath, glob) {
127
+ const key = JSON.stringify(glob);
128
+ let m = matcherCache.get(key);
129
+ if (!m) {
130
+ m = picomatch(glob, { dot: true });
131
+ matcherCache.set(key, m);
132
+ }
133
+ return m(relPath.replace(/\\/g, "/"));
134
+ }
135
+
136
+ // src/shared/collections.ts
137
+ var COLLECTION_KEYS = [
138
+ "name",
139
+ "paths",
140
+ "exclude",
141
+ "url",
142
+ "externalMetadata"
143
+ ];
144
+ var EXTERNAL_METADATA_KEYS = ["file", "keys", "tokenEnv", "join"];
145
+ var SCHEMA_KEY = "$schema";
146
+ var STDIN = "<stdin>";
147
+ function isUrlRef(ref) {
148
+ return /^https?:\/\//i.test(ref);
149
+ }
150
+ function externalMetadataUrlProblem(url) {
151
+ let parsed2;
152
+ try {
153
+ parsed2 = new URL(url);
154
+ } catch {
155
+ return "is not a valid URL";
156
+ }
157
+ if (parsed2.username !== "" || parsed2.password !== "") {
158
+ return 'carries a credential in the URL; put the token in an environment variable and name it with "tokenEnv"';
159
+ }
160
+ if (parsed2.protocol === "https:") return null;
161
+ if (parsed2.protocol === "http:" && isLoopback(parsed2.hostname)) return null;
162
+ if (parsed2.protocol === "http:") {
163
+ return "is plain http://; a bearer token over plaintext is a leak, and a public manifest is served over https too";
164
+ }
165
+ return `has the unsupported scheme "${parsed2.protocol}"`;
166
+ }
167
+ function isLoopback(host) {
168
+ return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host === "::1" || host.startsWith("127.");
169
+ }
170
+ function rejectUnknownKeys(raw, allowed, where, source, toError) {
171
+ for (const key of Object.keys(raw)) {
172
+ if (!allowed.includes(key)) {
173
+ throw toError(
174
+ `${source}: ${where} has unknown key "${key}". Supported keys: ${allowed.join(", ")}.`
175
+ );
176
+ }
177
+ }
178
+ }
179
+ function isMapping(value) {
180
+ return typeof value === "object" && value !== null && !Array.isArray(value);
181
+ }
182
+ function isNonEmptyString(value) {
183
+ return typeof value === "string" && value.trim() !== "";
184
+ }
185
+ function parseCollections(raw, source, toError) {
186
+ if (!Array.isArray(raw)) {
187
+ throw toError(`${source}: "collections" must be a list.`);
188
+ }
189
+ if (raw.length === 0) {
190
+ throw toError(
191
+ `${source}: "collections" must name at least one collection; remove the key if there are none.`
192
+ );
193
+ }
194
+ const taken = /* @__PURE__ */ new Map();
195
+ return raw.map((entry, i) => {
196
+ const where = `collections[${i}]`;
197
+ if (!isMapping(entry)) {
198
+ throw toError(`${source}: ${where} must be a mapping.`);
199
+ }
200
+ rejectUnknownKeys(entry, COLLECTION_KEYS, where, source, toError);
201
+ if (!isNonEmptyString(entry.name)) {
202
+ throw toError(`${source}: ${where}.name must be a non-empty string.`);
203
+ }
204
+ const name2 = entry.name;
205
+ const folded = name2.toLowerCase();
206
+ if (folded === "docs") {
207
+ throw toError(
208
+ `${source}: ${where}.name "${name2}" collides with the docs table every query reads. Pick another name, such as "site" or "pages".`
209
+ );
210
+ }
211
+ if (folded.startsWith("sqlite_")) {
212
+ throw toError(
213
+ `${source}: ${where}.name "${name2}" starts with "sqlite_", which SQLite reserves for its own objects. Pick another name.`
214
+ );
215
+ }
216
+ const owner = taken.get(folded);
217
+ if (owner !== void 0) {
218
+ throw toError(
219
+ `${source}: ${where}.name "${name2}" is already taken by collections[${owner}]; names are compared case-insensitively because they become SQL views.`
220
+ );
221
+ }
222
+ taken.set(folded, i);
223
+ if (!Array.isArray(entry.paths) || entry.paths.length === 0 || entry.paths.some((p) => !isNonEmptyString(p))) {
224
+ throw toError(
225
+ `${source}: ${where}.paths must be a non-empty list of files, directories or globs.`
226
+ );
227
+ }
228
+ const paths = entry.paths;
229
+ let exclude = [];
230
+ if (entry.exclude !== void 0) {
231
+ if (!Array.isArray(entry.exclude) || entry.exclude.some((g) => !isNonEmptyString(g))) {
232
+ throw toError(`${source}: ${where}.exclude must be a list of globs.`);
233
+ }
234
+ exclude = entry.exclude;
235
+ }
236
+ let url;
237
+ if (entry.url !== void 0) {
238
+ if (typeof entry.url !== "string" || !isHttpUrl(entry.url)) {
239
+ throw toError(`${source}: ${where}.url must be an http(s) URL.`);
240
+ }
241
+ url = entry.url;
242
+ }
243
+ let externalMetadata = [];
244
+ if (entry.externalMetadata !== void 0) {
245
+ if (!Array.isArray(entry.externalMetadata)) {
246
+ throw toError(`${source}: ${where}.externalMetadata must be a list.`);
247
+ }
248
+ externalMetadata = parseExternalMetadata(
249
+ entry.externalMetadata,
250
+ where,
251
+ source,
252
+ toError
253
+ );
254
+ }
255
+ return {
256
+ name: name2,
257
+ paths,
258
+ exclude,
259
+ externalMetadata,
260
+ ...url === void 0 ? {} : { url }
261
+ };
262
+ });
263
+ }
264
+ function isHttpUrl(value) {
265
+ let parsed2;
266
+ try {
267
+ parsed2 = new URL(value);
268
+ } catch {
269
+ return false;
270
+ }
271
+ return parsed2.protocol === "http:" || parsed2.protocol === "https:";
272
+ }
273
+ function parseExternalMetadata(raw, collectionWhere, source, toError) {
274
+ const owners = /* @__PURE__ */ new Map();
275
+ return raw.map((entry, i) => {
276
+ const where = `${collectionWhere}.externalMetadata[${i}]`;
277
+ if (!isMapping(entry)) {
278
+ throw toError(`${source}: ${where} must be a mapping.`);
279
+ }
280
+ rejectUnknownKeys(entry, EXTERNAL_METADATA_KEYS, where, source, toError);
281
+ if (!isNonEmptyString(entry.file)) {
282
+ throw toError(
283
+ `${source}: ${where}.file must be a non-empty string naming the manifest, relative to the config file.`
284
+ );
285
+ }
286
+ if (!Array.isArray(entry.keys) || entry.keys.length === 0 || entry.keys.some((k) => !isNonEmptyString(k))) {
287
+ throw toError(
288
+ `${source}: ${where}.keys must be a non-empty list of key names.`
289
+ );
290
+ }
291
+ const isUrl = isUrlRef(entry.file);
292
+ if (isUrl) {
293
+ const problem = externalMetadataUrlProblem(entry.file);
294
+ if (problem !== null) {
295
+ throw toError(`${source}: ${where}.file ${problem}.`);
296
+ }
297
+ }
298
+ if (entry.tokenEnv !== void 0) {
299
+ if (!isNonEmptyString(entry.tokenEnv)) {
300
+ throw toError(
301
+ `${source}: ${where}.tokenEnv must be the name of an environment variable.`
302
+ );
303
+ }
304
+ if (!isUrl) {
305
+ throw toError(
306
+ `${source}: ${where}.tokenEnv is set, but "file" is a path, so no request is made and the token would go nowhere. Remove it, or make "file" a URL.`
307
+ );
308
+ }
309
+ }
310
+ const keys = entry.keys;
311
+ if (entry.join !== void 0) {
312
+ if (!isNonEmptyString(entry.join)) {
313
+ throw toError(
314
+ `${source}: ${where}.join must be "path" or the name of a top-level frontmatter field.`
315
+ );
316
+ }
317
+ if (entry.join === SCHEMA_KEY) {
318
+ throw toError(`${source}: ${where}.join may not be "${SCHEMA_KEY}".`);
319
+ }
320
+ if (keys.includes(entry.join)) {
321
+ throw toError(
322
+ `${source}: ${where}.join names "${entry.join}", which the same entry owns \u2014 the value that selects an entry cannot come from the entry.`
323
+ );
324
+ }
325
+ }
326
+ const seen = /* @__PURE__ */ new Set();
327
+ for (const key of keys) {
328
+ if (key === SCHEMA_KEY) {
329
+ throw toError(
330
+ `${source}: ${where}.keys may not include "${SCHEMA_KEY}" \u2014 a manifest never chooses the schema a document is judged by; use "overrides".`
331
+ );
332
+ }
333
+ if (seen.has(key)) {
334
+ throw toError(`${source}: ${where}.keys lists "${key}" twice.`);
335
+ }
336
+ seen.add(key);
337
+ const owner = owners.get(key);
338
+ if (owner !== void 0) {
339
+ throw toError(
340
+ `${source}: ${where}.keys claims "${key}", which externalMetadata[${owner}] already owns \u2014 a key has exactly one manifest in a collection.`
341
+ );
342
+ }
343
+ owners.set(key, i);
344
+ }
345
+ return {
346
+ file: entry.file,
347
+ keys,
348
+ ...typeof entry.tokenEnv === "string" ? { tokenEnv: entry.tokenEnv } : {},
349
+ ...typeof entry.join === "string" ? { join: entry.join } : {}
350
+ };
351
+ });
352
+ }
353
+ function selectCollections(collections, names, source, toError) {
354
+ if (names === void 0 || names.length === 0) return [...collections];
355
+ const wanted = new Set(names);
356
+ for (const name2 of wanted) {
357
+ if (!collections.some((c) => c.name === name2)) {
358
+ const configured = collections.length === 0 ? "(none)" : collections.map((c) => c.name).join(", ");
359
+ const nearMiss = collections.find(
360
+ (c) => c.name.toLowerCase() === name2.toLowerCase()
361
+ );
362
+ const hint = nearMiss === void 0 ? "" : ` Names are case-sensitive; did you mean "${nearMiss.name}"?`;
363
+ throw toError(
364
+ `no collection named "${name2}" in ${source}. Configured: ${configured}.${hint}`
365
+ );
366
+ }
367
+ }
368
+ return collections.filter((c) => wanted.has(c.name));
369
+ }
370
+ function isMember(collection, relPath) {
371
+ if (relPath === STDIN) return false;
372
+ if (relPath.startsWith("../")) return false;
373
+ const matched = collection.paths.some((entry) => {
374
+ const normalized = normalizeEntry(entry);
375
+ if (normalized === "") return false;
376
+ if (isGlob(normalized)) return matchesFileGlob(relPath, normalized);
377
+ return relPath === normalized || relPath.startsWith(`${normalized}/`);
378
+ });
379
+ if (!matched) return false;
380
+ return !collection.exclude.some((glob) => matchesFileGlob(relPath, glob));
381
+ }
382
+ function normalizeEntry(entry) {
383
+ const posix2 = entry.replace(/\\/g, "/").replace(/^\.\//, "");
384
+ return posix2.endsWith("/") ? posix2.slice(0, -1) : posix2;
385
+ }
386
+ function isGlob(entry) {
387
+ return picomatch2.scan(entry).isGlob;
388
+ }
389
+
120
390
  // src/shared/errors.ts
121
391
  var ToolError = class extends Error {
122
392
  /** clig.dev: 2 is operational/usage; 1 is reserved for findings. */
@@ -2617,9 +2887,9 @@ function assertPublishableBuiltinId(id) {
2617
2887
  `Built-in id "${id}" is not publishable: the "check" first segment is reserved for corpus check identities (check:<name>, proposal 0026).`
2618
2888
  );
2619
2889
  }
2620
- if (first === "sidecar") {
2890
+ if (first === "external") {
2621
2891
  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).`
2892
+ `Built-in id "${id}" is not publishable: the "external" first segment is reserved for external-metadata finding identities (external:owned, external:duplicate, proposal 0037).`
2623
2893
  );
2624
2894
  }
2625
2895
  }
@@ -3004,7 +3274,6 @@ async function loadSchema(ref, options = {}) {
3004
3274
 
3005
3275
  // src/meta/core/resolve-schema.ts
3006
3276
  import { isAbsolute, relative, resolve } from "node:path";
3007
- import picomatch from "picomatch";
3008
3277
  var DEFAULT_SCHEMAS = Object.freeze([
3009
3278
  "google:okf:0.1",
3010
3279
  "passo-uno:seven-action:1.0"
@@ -3044,21 +3313,15 @@ function rebaseConfigSchemaRefs(config, configDir, cwd) {
3044
3313
  } : {}
3045
3314
  };
3046
3315
  }
3047
- var matcherCache = /* @__PURE__ */ new Map();
3048
3316
  function overrideGlobs(files) {
3317
+ if (files === void 0) return [];
3049
3318
  return typeof files === "string" ? [files] : files;
3050
3319
  }
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);
3320
+ function overrideMatches(override, filePath, memberOf2) {
3321
+ if (override.collection !== void 0) {
3322
+ return memberOf2.includes(override.collection);
3060
3323
  }
3061
- return m(filePath.replace(/\\/g, "/"));
3324
+ return override.files !== void 0 && matchesFileGlob(filePath, override.files);
3062
3325
  }
3063
3326
  function coerceFileSchema(value) {
3064
3327
  if (value === void 0 || value === null) return void 0;
@@ -3136,8 +3399,9 @@ function resolveSchemaSetWithSource(params) {
3136
3399
  }
3137
3400
  }
3138
3401
  if (config?.overrides) {
3402
+ const memberOf2 = params.memberOf ?? [];
3139
3403
  for (const [i, ov] of config.overrides.entries()) {
3140
- if (matches(ov.files, filePath) && ov.schemas.length > 0) {
3404
+ if (overrideMatches(ov, filePath, memberOf2) && ov.schemas.length > 0) {
3141
3405
  return { schemas: dedupe(ov.schemas), source: "override", overrideIndex: i };
3142
3406
  }
3143
3407
  }
@@ -3153,55 +3417,37 @@ function resolveSchemaSetWithSource(params) {
3153
3417
  function resolveSchemaSet(params) {
3154
3418
  return resolveSchemaSetWithSource(params).schemas;
3155
3419
  }
3156
- function resolveElements(filePath, config) {
3420
+ function resolveElements(filePath, config, memberOf2 = []) {
3157
3421
  const out = [];
3158
3422
  if (config?.elements) out.push(...config.elements);
3159
3423
  for (const ov of config?.overrides ?? []) {
3160
- if (ov.elements && matches(ov.files, filePath)) out.push(...ov.elements);
3424
+ if (ov.elements && overrideMatches(ov, filePath, memberOf2)) {
3425
+ out.push(...ov.elements);
3426
+ }
3161
3427
  }
3162
3428
  return dedupe(out);
3163
3429
  }
3164
3430
 
3165
- // src/meta/core/sidecar-fetch.ts
3166
- var SIDECAR_FETCH_TIMEOUT_MS = 1e4;
3167
- var SIDECAR_FETCH_MAX_BYTES = 5 * 1024 * 1024;
3431
+ // src/meta/core/external-metadata-fetch.ts
3432
+ var EXTERNAL_METADATA_FETCH_TIMEOUT_MS = 1e4;
3433
+ var EXTERNAL_METADATA_FETCH_MAX_BYTES = 5 * 1024 * 1024;
3168
3434
  var MAX_REDIRECTS = 5;
3169
3435
  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 = {}) {
3436
+ async function fetchExternalMetadata(url, opts = {}) {
3191
3437
  if (opts.offline) {
3192
3438
  throw new DocmetaError(
3193
- `Sidecar manifest ${url} is remote and the run is offline. Vendor it to a path, or drop --offline.`
3439
+ `Manifest ${url} is remote and the run is offline. Vendor it to a path, or drop --offline.`
3194
3440
  );
3195
3441
  }
3196
- const timeoutMs = opts.timeoutMs ?? SIDECAR_FETCH_TIMEOUT_MS;
3197
- const maxBytes = opts.maxBytes ?? SIDECAR_FETCH_MAX_BYTES;
3442
+ const timeoutMs = opts.timeoutMs ?? EXTERNAL_METADATA_FETCH_TIMEOUT_MS;
3443
+ const maxBytes = opts.maxBytes ?? EXTERNAL_METADATA_FETCH_MAX_BYTES;
3198
3444
  const env = opts.env ?? process.env;
3199
3445
  let token;
3200
3446
  if (opts.tokenEnv !== void 0) {
3201
3447
  token = env[opts.tokenEnv];
3202
3448
  if (token === void 0 || token === "") {
3203
3449
  throw new DocmetaError(
3204
- `Sidecar manifest ${url}: the environment variable ${opts.tokenEnv} named by "tokenEnv" is not set.`
3450
+ `Manifest ${url}: the environment variable ${opts.tokenEnv} named by "tokenEnv" is not set.`
3205
3451
  );
3206
3452
  }
3207
3453
  }
@@ -3212,13 +3458,13 @@ async function fetchSidecar(url, opts = {}) {
3212
3458
  }
3213
3459
  if (res instanceof Error) {
3214
3460
  throw new DocmetaError(
3215
- `Sidecar manifest ${url} could not be fetched: ${res.message}`
3461
+ `Manifest ${url} could not be fetched: ${res.message}`
3216
3462
  );
3217
3463
  }
3218
3464
  if (!res.ok) {
3219
3465
  const hint = res.status === 404 && token === void 0 ? ' (a private file answers 404 without a token; set "tokenEnv")' : res.status === 404 ? " (a private file answers 404 without a valid token)" : "";
3220
3466
  throw new DocmetaError(
3221
- `Sidecar manifest ${url} could not be fetched: HTTP ${res.status}${hint}.`
3467
+ `Manifest ${url} could not be fetched: HTTP ${res.status}${hint}.`
3222
3468
  );
3223
3469
  }
3224
3470
  return readCapped(url, res, maxBytes);
@@ -3265,7 +3511,7 @@ async function readCapped(url, res, maxBytes) {
3265
3511
  if (total > maxBytes) {
3266
3512
  await reader.cancel();
3267
3513
  throw new DocmetaError(
3268
- `Sidecar manifest ${url} is too large: the response exceeds the ${maxBytes}-byte limit.`
3514
+ `Manifest ${url} is too large: the response exceeds the ${maxBytes}-byte limit.`
3269
3515
  );
3270
3516
  }
3271
3517
  chunks.push(value);
@@ -5458,7 +5704,7 @@ function listFormats() {
5458
5704
  }));
5459
5705
  }
5460
5706
 
5461
- // src/meta/core/sidecars.ts
5707
+ // src/meta/core/external-metadata.ts
5462
5708
  import { readFile as readFile3 } from "node:fs/promises";
5463
5709
  import { isAbsolute as isAbsolute2, relative as relative4, resolve as resolve5, sep as sep2 } from "node:path";
5464
5710
  import {
@@ -5473,7 +5719,7 @@ import {
5473
5719
  import { stat as stat3 } from "node:fs/promises";
5474
5720
  import { extname, relative as relative3, resolve as resolve4 } from "node:path";
5475
5721
  import fg from "fast-glob";
5476
- import picomatch2 from "picomatch";
5722
+ import picomatch3 from "picomatch";
5477
5723
 
5478
5724
  // src/meta/core/gitignore.ts
5479
5725
  import { spawn } from "node:child_process";
@@ -5648,7 +5894,7 @@ async function resolveTargetSet(opts) {
5648
5894
  for (const f of found2) if (keepByExt(f)) walked.add(f);
5649
5895
  continue;
5650
5896
  }
5651
- if (!picomatch2.scan(posixInput).isGlob) {
5897
+ if (!picomatch3.scan(posixInput).isGlob) {
5652
5898
  missing.push(posixInput);
5653
5899
  continue;
5654
5900
  }
@@ -5708,44 +5954,50 @@ function assertNonEmpty(p) {
5708
5954
  );
5709
5955
  }
5710
5956
 
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";
5957
+ // src/meta/core/external-metadata.ts
5958
+ var EXTERNAL_OWNED_SCHEMA = "external:owned";
5959
+ var EXTERNAL_DUPLICATE_SCHEMA = "external:duplicate";
5960
+ var EXTERNAL_KEYWORD = "external";
5715
5961
  var PATH_JOIN = "path";
5716
5962
  var posix = (p) => p.split(sep2).join("/");
5717
5963
  function reportedPath(abs, base) {
5718
5964
  const rel = relative4(base, abs);
5719
5965
  return rel === "" ? "." : posix(rel);
5720
5966
  }
5721
- function sidecarJoin(sidecar) {
5722
- return sidecar.join ?? PATH_JOIN;
5967
+ function externalMetadataJoin(manifest) {
5968
+ return manifest.join ?? PATH_JOIN;
5723
5969
  }
5724
- async function loadSidecars(config, opts) {
5725
- const configured = config?.sidecars;
5726
- if (!configured || configured.length === 0) return null;
5970
+ async function loadExternalMetadata(collections, opts) {
5971
+ const configured = collections.flatMap(
5972
+ (c) => c.externalMetadata.map((m) => ({ collection: c.name, manifest: m }))
5973
+ );
5974
+ if (configured.length === 0) return null;
5727
5975
  const owners = /* @__PURE__ */ new Map();
5728
5976
  const byPath = /* @__PURE__ */ new Map();
5729
5977
  const byField = /* @__PURE__ */ new Map();
5730
5978
  const entries = [];
5731
- for (const sidecar of configured) {
5979
+ for (const { collection, manifest } of configured) {
5732
5980
  let text;
5733
5981
  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 } : {},
5982
+ if (classifyRef(manifest.file).kind === "url") {
5983
+ file = manifest.file;
5984
+ text = await fetchExternalMetadata(manifest.file, {
5985
+ ...manifest.tokenEnv !== void 0 ? { tokenEnv: manifest.tokenEnv } : {},
5738
5986
  ...opts.offline !== void 0 ? { offline: opts.offline } : {},
5739
5987
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
5740
5988
  ...opts.env !== void 0 ? { env: opts.env } : {}
5741
5989
  });
5742
5990
  } else {
5743
- const abs = isAbsolute2(sidecar.file) ? sidecar.file : resolve5(opts.configDir, sidecar.file);
5991
+ const abs = isAbsolute2(manifest.file) ? manifest.file : resolve5(opts.configDir, manifest.file);
5744
5992
  file = reportedPath(abs, opts.base);
5745
5993
  text = await readManifest(abs, file);
5746
5994
  }
5747
- for (const key of sidecar.keys) owners.set(key, file);
5748
- const join7 = sidecarJoin(sidecar);
5995
+ for (const key of manifest.keys) {
5996
+ const list = owners.get(key);
5997
+ if (list) list.push({ collection, file });
5998
+ else owners.set(key, [{ collection, file }]);
5999
+ }
6000
+ const join7 = externalMetadataJoin(manifest);
5749
6001
  let target;
5750
6002
  if (join7 === PATH_JOIN) {
5751
6003
  target = byPath;
@@ -5757,7 +6009,16 @@ async function loadSidecars(config, opts) {
5757
6009
  byField.set(join7, target);
5758
6010
  }
5759
6011
  }
5760
- parseManifest(sidecar, text, file, opts.configDir, join7, target, entries);
6012
+ parseManifest(
6013
+ manifest,
6014
+ collection,
6015
+ text,
6016
+ file,
6017
+ opts.configDir,
6018
+ join7,
6019
+ target,
6020
+ entries
6021
+ );
5761
6022
  }
5762
6023
  return { owners, byPath, byField, entries };
5763
6024
  }
@@ -5767,27 +6028,27 @@ async function readManifest(abs, file) {
5767
6028
  } catch (err) {
5768
6029
  const reason = err instanceof Error ? err.message : String(err);
5769
6030
  throw new DocmetaError(
5770
- `Sidecar manifest ${file} could not be read: ${reason}`
6031
+ `Manifest ${file} could not be read: ${reason}`
5771
6032
  );
5772
6033
  }
5773
6034
  }
5774
- function parseManifest(sidecar, text, file, configDir, join7, target, entries) {
6035
+ function parseManifest(manifest, collection, text, file, configDir, join7, target, entries) {
5775
6036
  const lc = new LineCounter2();
5776
6037
  const doc = parseDocument3(text, { lineCounter: lc, uniqueKeys: false });
5777
6038
  const problem = doc.errors[0];
5778
6039
  if (problem) {
5779
6040
  throw new DocmetaError(
5780
- `Sidecar manifest ${file} is not valid YAML: ${problem.message}`
6041
+ `Manifest ${file} is not valid YAML: ${problem.message}`
5781
6042
  );
5782
6043
  }
5783
6044
  const root = doc.contents;
5784
6045
  if (root === null || isScalar2(root) && root.value === null) return;
5785
6046
  if (!isMap3(root)) {
5786
6047
  throw new DocmetaError(
5787
- `Sidecar manifest ${file}: the manifest must be a mapping from document ${join7 === PATH_JOIN ? "path" : `"${join7}"`} to owned keys.`
6048
+ `Manifest ${file}: the manifest must be a mapping from document ${join7 === PATH_JOIN ? "path" : `"${join7}"`} to owned keys.`
5788
6049
  );
5789
6050
  }
5790
- const owned = new Set(sidecar.keys);
6051
+ const owned = new Set(manifest.keys);
5791
6052
  const lineAt = (node) => {
5792
6053
  const range = isNode(node) ? node.range : void 0;
5793
6054
  return range ? lc.linePos(range[0]).line : void 0;
@@ -5800,13 +6061,13 @@ function parseManifest(sidecar, text, file, configDir, join7, target, entries) {
5800
6061
  if (seen.has(spelled)) {
5801
6062
  const first = seen.get(spelled);
5802
6063
  throw new DocmetaError(
5803
- `Sidecar manifest ${where}: "${spelled}" is named twice (first at line ${first ?? "?"}). Merge the two entries into one.`
6064
+ `Manifest ${where}: "${spelled}" is named twice (first at line ${first ?? "?"}). Merge the two entries into one.`
5804
6065
  );
5805
6066
  }
5806
6067
  seen.set(spelled, entryLine);
5807
6068
  if (!isMap3(pair.value)) {
5808
6069
  throw new DocmetaError(
5809
- `Sidecar manifest ${where}: "${spelled}" must be a mapping of owned keys to values.`
6070
+ `Manifest ${where}: "${spelled}" must be a mapping of owned keys to values.`
5810
6071
  );
5811
6072
  }
5812
6073
  const indexKey = join7 === PATH_JOIN ? resolve5(configDir, spelled) : spelled;
@@ -5815,20 +6076,26 @@ function parseManifest(sidecar, text, file, configDir, join7, target, entries) {
5815
6076
  const key = isScalar2(kv.key) ? String(kv.key.value) : String(kv.key);
5816
6077
  if (key === FILE_SCHEMA_KEY) {
5817
6078
  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.`
6079
+ `Manifest ${where}: "${spelled}" sets "${FILE_SCHEMA_KEY}" \u2014 a manifest never chooses the schema a document is judged by; use "overrides" in the config.`
5819
6080
  );
5820
6081
  }
5821
6082
  if (!owned.has(key)) {
5822
6083
  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.`
6084
+ `Manifest ${where}: "${spelled}" sets "${key}", which this manifest does not own. Add it to the manifest's "keys", or remove it from the entry.`
5824
6085
  );
5825
6086
  }
5826
6087
  const value = kv.value === null ? null : kv.value.toJS(doc, { maxAliasCount: 100 });
5827
6088
  const line = lineAt(kv.key);
5828
- values.set(key, { value, file, ...line === void 0 ? {} : { line } });
6089
+ values.set(key, {
6090
+ value,
6091
+ collection,
6092
+ file,
6093
+ ...line === void 0 ? {} : { line }
6094
+ });
5829
6095
  }
5830
6096
  target.set(indexKey, values);
5831
6097
  entries.push({
6098
+ collection,
5832
6099
  join: join7,
5833
6100
  ...join7 === PATH_JOIN ? { abs: indexKey } : {},
5834
6101
  spelled,
@@ -5843,13 +6110,19 @@ function joinValue(raw) {
5843
6110
  if (typeof raw === "boolean") return String(raw);
5844
6111
  return void 0;
5845
6112
  }
5846
- function mergeSidecars(label, extracted, index, base) {
6113
+ function mergeExternalMetadata(label, extracted, index, memberOf2, base) {
5847
6114
  const none = () => void 0;
5848
- if (!index) return { extracted, collisions: [], joins: [], locate: none };
6115
+ if (!index || memberOf2.length === 0) {
6116
+ return { extracted, collisions: [], joins: [], locate: none };
6117
+ }
6118
+ const mine = (collection) => memberOf2.includes(collection);
5849
6119
  const collisions = [];
5850
6120
  for (const key of Object.keys(extracted.data)) {
5851
- const file = index.owners.get(key);
5852
- if (file !== void 0) collisions.push({ key, file });
6121
+ for (const owner of index.owners.get(key) ?? []) {
6122
+ if (mine(owner.collection)) {
6123
+ collisions.push({ key, file: owner.file, collection: owner.collection });
6124
+ }
6125
+ }
5853
6126
  }
5854
6127
  const sources = [];
5855
6128
  const joins = [];
@@ -5863,8 +6136,23 @@ function mergeSidecars(label, extracted, index, base) {
5863
6136
  const hit = byValue.get(value);
5864
6137
  if (!hit) continue;
5865
6138
  sources.push(hit);
5866
- const first = hit.values().next().value;
5867
- joins.push({ field, value, file: first?.file ?? "manifest" });
6139
+ const first = [...hit.values()].find((sv) => mine(sv.collection));
6140
+ if (first) {
6141
+ joins.push({ field, value, file: first.file, collection: first.collection });
6142
+ }
6143
+ }
6144
+ for (const supplied of sources) {
6145
+ for (const key of supplied.keys()) {
6146
+ const owners = (index.owners.get(key) ?? []).filter(
6147
+ (o) => mine(o.collection)
6148
+ );
6149
+ const [a, b] = owners;
6150
+ if (a && b) {
6151
+ throw new DocmetaError(
6152
+ `${label}: "${key}" is owned by manifests in two of its collections, ${a.collection} (${a.file}) and ${b.collection} (${b.file}); a key has one manifest per file. Narrow one collection's paths or exclude.`
6153
+ );
6154
+ }
6155
+ }
5868
6156
  }
5869
6157
  if (sources.length === 0) {
5870
6158
  return { extracted, collisions, joins, locate: none };
@@ -5873,6 +6161,7 @@ function mergeSidecars(label, extracted, index, base) {
5873
6161
  const merged = /* @__PURE__ */ new Map();
5874
6162
  for (const supplied of sources) {
5875
6163
  for (const [key, sv] of supplied) {
6164
+ if (!mine(sv.collection)) continue;
5876
6165
  if (key in extracted.data) continue;
5877
6166
  data[key] = sv.value;
5878
6167
  merged.set(key, sv);
@@ -5893,20 +6182,23 @@ function topLevelKey(pointer) {
5893
6182
  const seg = pointer.slice(1).split("/")[0] ?? "";
5894
6183
  return seg.replace(/~1/g, "/").replace(/~0/g, "~");
5895
6184
  }
5896
- function sidecarPointer(key) {
6185
+ function externalMetadataPointer(key) {
5897
6186
  return `/${escapePointerSegment(key)}`;
5898
6187
  }
5899
- function orphanEntries(index, loaded, base) {
6188
+ function orphanEntries(index, loaded, base, collections) {
5900
6189
  if (!index) return [];
5901
6190
  const have = new Set(loaded.map((l) => resolve5(base, l)));
5902
6191
  return index.entries.filter(
5903
- (e) => e.join === PATH_JOIN && e.abs !== void 0 && !have.has(e.abs)
6192
+ (e) => inCollections(e, collections) && e.join === PATH_JOIN && e.abs !== void 0 && !have.has(e.abs)
5904
6193
  );
5905
6194
  }
5906
- function orphanJoins(index, matched) {
6195
+ function inCollections(entry, collections) {
6196
+ return collections === void 0 || collections.includes(entry.collection);
6197
+ }
6198
+ function orphanJoins(index, matched, collections) {
5907
6199
  if (!index) return [];
5908
6200
  return index.entries.filter(
5909
- (e) => e.join !== PATH_JOIN && !(matched.get(e.join)?.has(e.spelled) ?? false)
6201
+ (e) => inCollections(e, collections) && e.join !== PATH_JOIN && !(matched.get(e.join)?.has(e.spelled) ?? false)
5910
6202
  );
5911
6203
  }
5912
6204
  function orphanError(orphans) {
@@ -5916,7 +6208,7 @@ function orphanError(orphans) {
5916
6208
  const more = orphans.length > 1 ? ` (${orphans.length - 1} more)` : "";
5917
6209
  const what = first.join === PATH_JOIN ? `names "${first.spelled}", which this run did not load` : `names ${first.join} "${first.spelled}", which no loaded document carries`;
5918
6210
  return new DocmetaError(
5919
- `Sidecar manifest ${where} ${what}${more}. Fix the entry, or remove it.`
6211
+ `Manifest ${where} ${what}${more}. Fix the entry, or remove it.`
5920
6212
  );
5921
6213
  }
5922
6214
 
@@ -5969,13 +6261,22 @@ function parseMapping(text, source, opts) {
5969
6261
  }
5970
6262
  return raw;
5971
6263
  }
6264
+ var COLLECTIONS_KEY = "collections";
5972
6265
  function slice(document, opts) {
5973
6266
  const { doc } = document;
5974
6267
  if (doc !== null && Object.hasOwn(doc, opts.section)) {
5975
6268
  return { value: doc[opts.section] ?? null, wrapped: true };
5976
6269
  }
6270
+ if (doc !== null && Object.hasOwn(doc, COLLECTIONS_KEY)) {
6271
+ return { value: null, wrapped: true };
6272
+ }
5977
6273
  return null;
5978
6274
  }
6275
+ function collectionsOf(document, source, opts) {
6276
+ const { doc } = document;
6277
+ if (doc === null || !Object.hasOwn(doc, COLLECTIONS_KEY)) return [];
6278
+ return parseCollections(doc[COLLECTIONS_KEY], source, opts.toError);
6279
+ }
5979
6280
  function relativeSource(cwd, path) {
5980
6281
  return relative5(cwd, path).replace(/\\/g, "/");
5981
6282
  }
@@ -5999,7 +6300,15 @@ async function findConfigFile(cwd, opts) {
5999
6300
  `"${name2}" is the pre-rename name of the family config file. Rename it to "${FAMILY_CONFIG_NAMES[0] ?? "manni.config.yaml"}".`
6000
6301
  );
6001
6302
  }
6002
- return { path, dir, source, text: document.text, kind, ...found };
6303
+ return {
6304
+ path,
6305
+ dir,
6306
+ source,
6307
+ text: document.text,
6308
+ kind,
6309
+ collections: collectionsOf(document, source, opts),
6310
+ ...found
6311
+ };
6003
6312
  }
6004
6313
  }
6005
6314
  for (const name2 of opts.legacyNames) {
@@ -6008,7 +6317,7 @@ async function findConfigFile(cwd, opts) {
6008
6317
  const document = await readDocument(path, source, opts);
6009
6318
  if (document === null) continue;
6010
6319
  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"}".`
6320
+ `"${name2}" is a deprecated config file name and will stop being read in a future major version. Move its keys under \`${opts.section}:\` in "${FAMILY_CONFIG_NAMES[0] ?? "manni.config.yaml"}", and its paths, exclude and sidecars keys to a top-level collections: list, where sidecars becomes externalMetadata.`
6012
6321
  );
6013
6322
  return {
6014
6323
  path,
@@ -6017,7 +6326,8 @@ async function findConfigFile(cwd, opts) {
6017
6326
  text: document.text,
6018
6327
  value: document.doc,
6019
6328
  wrapped: false,
6020
- kind: "legacy"
6329
+ kind: "legacy",
6330
+ collections: []
6021
6331
  };
6022
6332
  }
6023
6333
  }
@@ -6039,6 +6349,7 @@ async function readConfigFile(explicitPath, cwd, opts) {
6039
6349
  source: explicitPath,
6040
6350
  text: document.text,
6041
6351
  kind: "explicit",
6352
+ collections: collectionsOf(document, explicitPath, opts),
6042
6353
  ...found
6043
6354
  };
6044
6355
  }
@@ -6059,9 +6370,8 @@ function asElementPaths(value, where, source) {
6059
6370
  }
6060
6371
  return list;
6061
6372
  }
6062
- var OVERRIDE_KEYS = ["name", "files", "schemas", "elements"];
6373
+ var OVERRIDE_KEYS = ["collection", "files", "schemas", "elements"];
6063
6374
  var CHECK_KEYS = ["name", "query"];
6064
- var SIDECAR_KEYS = ["file", "keys", "tokenEnv", "join"];
6065
6375
  var CHECK_NAME = /^[a-z0-9][a-z0-9._-]*$/;
6066
6376
  var RESERVED_CHECK_NAMES = /* @__PURE__ */ new Set(["query"]);
6067
6377
  var FILL_KEYS = [
@@ -6073,12 +6383,9 @@ var FILL_KEYS = [
6073
6383
  "concurrency"
6074
6384
  ];
6075
6385
  var CONFIG_KEYS = [
6076
- "paths",
6077
- "exclude",
6078
6386
  "schemas",
6079
6387
  "overrides",
6080
6388
  "checks",
6081
- "sidecars",
6082
6389
  "baseline",
6083
6390
  "allowEmpty",
6084
6391
  "respectGitignore",
@@ -6088,7 +6395,7 @@ var CONFIG_KEYS = [
6088
6395
  "fill",
6089
6396
  "elements"
6090
6397
  ];
6091
- function rejectUnknownKeys(raw, allowed, where, source) {
6398
+ function rejectUnknownKeys2(raw, allowed, where, source) {
6092
6399
  for (const key of Object.keys(raw)) {
6093
6400
  if (!allowed.includes(key)) {
6094
6401
  throw new DocmetaError(
@@ -6158,7 +6465,7 @@ function parseSchemaEntry(entry, where, source) {
6158
6465
  );
6159
6466
  }
6160
6467
  const raw = entry;
6161
- rejectUnknownKeys(raw, SCHEMA_ENTRY_KEYS, where, source);
6468
+ rejectUnknownKeys2(raw, SCHEMA_ENTRY_KEYS, where, source);
6162
6469
  if (typeof raw.ref !== "string" || raw.ref.trim() === "") {
6163
6470
  throw new DocmetaError(
6164
6471
  `${source}: ${where}.ref must be a non-empty schema reference string.`
@@ -6201,6 +6508,7 @@ function parseConfig(text, source) {
6201
6508
  return parseConfigValue(raw, source);
6202
6509
  }
6203
6510
  function parseConfigValue(raw, source, section) {
6511
+ assertNoMovedKeys(raw, source);
6204
6512
  if (section === void 0) return parseConfigDocument(raw, source);
6205
6513
  try {
6206
6514
  return parseConfigDocument(raw, source);
@@ -6222,92 +6530,49 @@ function withSection(message, source, section) {
6222
6530
  if (/^[a-zA-Z]+(\[|\.| —)/.test(rest)) return `${head}${section}.${rest}`;
6223
6531
  return message;
6224
6532
  }
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() === "") {
6533
+ var CONFIG_REF = "https://hawkeyexl.github.io/manni/meta/reference/configuration/";
6534
+ var MOVED_KEYS = [
6535
+ [
6536
+ "paths",
6537
+ `Document sets are declared once for every tool, under a top-level collections: list. See ${CONFIG_REF}#collections`
6538
+ ],
6539
+ [
6540
+ "exclude",
6541
+ `Document sets are declared once for every tool, under a top-level collections: list. See ${CONFIG_REF}#collections`
6542
+ ],
6543
+ [
6544
+ "sidecars",
6545
+ `It is externalMetadata on a collection, under the top-level collections: list. See ${CONFIG_REF}#external-metadata`
6546
+ ]
6547
+ ];
6548
+ function assertNoMovedKeys(raw, source) {
6549
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return;
6550
+ const obj = raw;
6551
+ for (const [key, remedy] of MOVED_KEYS) {
6552
+ if (Object.hasOwn(obj, key)) {
6237
6553
  throw new DocmetaError(
6238
- `${source}: sidecars[${i}].file must be a non-empty string naming the manifest, relative to the config file.`
6554
+ `${source}: "${key}" is no longer a meta key. ${remedy}`
6239
6555
  );
6240
6556
  }
6241
- if (!Array.isArray(e.keys) || e.keys.length === 0 || e.keys.some((k) => typeof k !== "string" || k.trim() === "")) {
6557
+ }
6558
+ if (!Array.isArray(obj.overrides)) return;
6559
+ for (const [i, entry] of obj.overrides.entries()) {
6560
+ if (typeof entry === "object" && entry !== null && !Array.isArray(entry) && Object.hasOwn(entry, "name")) {
6242
6561
  throw new DocmetaError(
6243
- `${source}: sidecars[${i}].keys must be a non-empty list of key names.`
6562
+ `${source}: overrides[${i}] no longer carries "name". Define a collection with that name and point the override at it with collection:.`
6244
6563
  );
6245
6564
  }
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
- });
6565
+ }
6566
+ }
6567
+ function assertOverrideCollections(config, collections, source) {
6568
+ const names = new Set(collections.map((c) => c.name));
6569
+ for (const [i, ov] of (config.overrides ?? []).entries()) {
6570
+ if (ov.collection === void 0 || names.has(ov.collection)) continue;
6571
+ const defined = collections.length === 0 ? "(none)" : collections.map((c) => c.name).join(", ");
6572
+ throw new DocmetaError(
6573
+ `${source}: overrides[${i}].collection names "${ov.collection}", which collections: does not define. Defined: ${defined}.`
6574
+ );
6575
+ }
6311
6576
  }
6312
6577
  function parseConfigDocument(raw, source) {
6313
6578
  if (raw == null) return {};
@@ -6315,25 +6580,37 @@ function parseConfigDocument(raw, source) {
6315
6580
  throw new DocmetaError(`${source}: top level must be a mapping.`);
6316
6581
  }
6317
6582
  const obj = raw;
6318
- rejectUnknownKeys(obj, CONFIG_KEYS, "the top level", source);
6583
+ rejectUnknownKeys2(obj, CONFIG_KEYS, "the top level", source);
6319
6584
  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
6585
  if (obj.schemas !== void 0)
6324
6586
  config.schemas = asSchemaList(obj.schemas, "schemas", source);
6325
6587
  if (obj.overrides !== void 0) {
6326
6588
  if (!Array.isArray(obj.overrides)) {
6327
6589
  throw new DocmetaError(`${source}: "overrides" must be a list.`);
6328
6590
  }
6329
- const seenNames = /* @__PURE__ */ new Set();
6330
6591
  config.overrides = obj.overrides.map((entry, i) => {
6331
6592
  if (typeof entry !== "object" || entry === null) {
6332
6593
  throw new DocmetaError(`${source}: overrides[${i}] must be a mapping.`);
6333
6594
  }
6334
6595
  const e = entry;
6335
- rejectUnknownKeys(e, OVERRIDE_KEYS, `overrides[${i}]`, source);
6336
- const files = asFileGlobs(e.files, `overrides[${i}].files`, source);
6596
+ rejectUnknownKeys2(e, OVERRIDE_KEYS, `overrides[${i}]`, source);
6597
+ const hasFiles = e.files !== void 0;
6598
+ const hasCollection = e.collection !== void 0;
6599
+ if (hasFiles === hasCollection) {
6600
+ throw new DocmetaError(
6601
+ `${source}: overrides[${i}] must carry exactly one of "files" or "collection".`
6602
+ );
6603
+ }
6604
+ const files = hasFiles ? asFileGlobs(e.files, `overrides[${i}].files`, source) : void 0;
6605
+ let collection;
6606
+ if (hasCollection) {
6607
+ if (typeof e.collection !== "string" || e.collection.trim() === "") {
6608
+ throw new DocmetaError(
6609
+ `${source}: overrides[${i}].collection must be a non-empty string naming a collection.`
6610
+ );
6611
+ }
6612
+ collection = e.collection;
6613
+ }
6337
6614
  const schemas = e.schemas === void 0 ? [] : asStringList(e.schemas, `overrides[${i}].schemas`, source);
6338
6615
  const elements = e.elements === void 0 ? void 0 : asElementPaths(e.elements, `overrides[${i}].elements`, source);
6339
6616
  if (schemas.length === 0 && (elements === void 0 || elements.length === 0)) {
@@ -6341,50 +6618,17 @@ function parseConfigDocument(raw, source) {
6341
6618
  `${source}: overrides[${i}] sets neither "schemas" nor "elements", so it has no effect.`
6342
6619
  );
6343
6620
  }
6344
- let name2;
6345
- if (e.name !== void 0) {
6346
- if (typeof e.name !== "string" || e.name.trim() === "") {
6347
- throw new DocmetaError(
6348
- `${source}: overrides[${i}].name must be a non-empty string \u2014 it becomes the collection's view name.`
6349
- );
6350
- }
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;
6373
- }
6374
6621
  return {
6375
- files,
6622
+ ...files !== void 0 ? { files } : {},
6623
+ ...collection !== void 0 ? { collection } : {},
6376
6624
  schemas,
6377
- ...elements ? { elements } : {},
6378
- ...name2 !== void 0 ? { name: name2 } : {}
6625
+ ...elements ? { elements } : {}
6379
6626
  };
6380
6627
  });
6381
6628
  }
6382
6629
  if (obj.checks !== void 0) {
6383
6630
  config.checks = parseChecks(obj.checks, source);
6384
6631
  }
6385
- if (obj.sidecars !== void 0) {
6386
- config.sidecars = parseSidecars(obj.sidecars, source);
6387
- }
6388
6632
  if (obj.elements !== void 0) {
6389
6633
  config.elements = asElementPaths(obj.elements, "elements", source);
6390
6634
  }
@@ -6435,7 +6679,7 @@ function parseChecks(value, source) {
6435
6679
  );
6436
6680
  }
6437
6681
  const e = entry;
6438
- rejectUnknownKeys(e, CHECK_KEYS, `checks[${i}]`, source);
6682
+ rejectUnknownKeys2(e, CHECK_KEYS, `checks[${i}]`, source);
6439
6683
  if (typeof e.name !== "string" || e.name === "") {
6440
6684
  throw new DocmetaError(
6441
6685
  `${source}: checks[${i}].name must be a non-empty string.`
@@ -6470,7 +6714,7 @@ function parseSchemaCache(value, source) {
6470
6714
  throw new DocmetaError(`${source}: "schemaCache" must be a mapping.`);
6471
6715
  }
6472
6716
  const raw = value;
6473
- rejectUnknownKeys(raw, SCHEMA_CACHE_KEYS, '"schemaCache"', source);
6717
+ rejectUnknownKeys2(raw, SCHEMA_CACHE_KEYS, '"schemaCache"', source);
6474
6718
  const schemaCache = {};
6475
6719
  const ttl = raw.ttlHours;
6476
6720
  if (ttl !== void 0) {
@@ -6490,7 +6734,7 @@ function parseSchemaTrust(value, source) {
6490
6734
  );
6491
6735
  }
6492
6736
  const raw = value;
6493
- rejectUnknownKeys(raw, SCHEMA_TRUST_KEYS, '"schemaTrust"', source);
6737
+ rejectUnknownKeys2(raw, SCHEMA_TRUST_KEYS, '"schemaTrust"', source);
6494
6738
  const schemaTrust = {};
6495
6739
  const mode = raw.documentRefs;
6496
6740
  if (mode !== void 0) {
@@ -6517,7 +6761,7 @@ function parseFill(value, source) {
6517
6761
  throw new DocmetaError(`${source}: "fill" must be a mapping.`);
6518
6762
  }
6519
6763
  const raw = value;
6520
- rejectUnknownKeys(raw, FILL_KEYS, '"fill"', source);
6764
+ rejectUnknownKeys2(raw, FILL_KEYS, '"fill"', source);
6521
6765
  const fill = {};
6522
6766
  const asString = (key) => {
6523
6767
  const v = raw[key];
@@ -6560,29 +6804,312 @@ async function loadConfig(explicitPath, cwd = process.cwd()) {
6560
6804
  const file = explicitPath ? await readConfigFile(explicitPath, cwd, CONFIG_FILE) : await findConfigFile(cwd, CONFIG_FILE);
6561
6805
  if (file === null) return null;
6562
6806
  const section = file.wrapped ? META_SECTION : void 0;
6807
+ const config = parseConfigValue(file.value, file.source, section);
6808
+ assertOverrideCollections(config, file.collections, file.source);
6563
6809
  return {
6564
- config: parseConfigValue(file.value, file.source, section),
6810
+ config,
6565
6811
  path: file.path,
6566
6812
  dir: file.dir,
6813
+ source: file.source,
6814
+ collections: file.collections,
6567
6815
  ...section !== void 0 ? { section } : {}
6568
6816
  };
6569
6817
  }
6818
+ var STDIN_TOKEN_LITERAL = "-";
6570
6819
  async function resolveRunConfig(opts) {
6571
6820
  const cwd = opts.cwd ?? process.cwd();
6821
+ const wanted = opts.collections ?? [];
6822
+ if (wanted.length > 0) {
6823
+ const typed = opts.inputs.filter((i) => i !== STDIN_TOKEN_LITERAL);
6824
+ if (typed.length > 0) {
6825
+ throw new DocmetaError(
6826
+ "--collection selects a configured collection; it cannot be combined with paths."
6827
+ );
6828
+ }
6829
+ }
6572
6830
  const loaded = opts.noConfig ? null : await loadConfig(opts.configPath, cwd);
6831
+ if (wanted.length > 0 && loaded === null) {
6832
+ throw new DocmetaError("--collection needs a config file to select from.");
6833
+ }
6573
6834
  if (loaded) opts.onConfigLoaded?.({ path: loaded.path, dir: loaded.dir });
6574
6835
  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;
6836
+ const collections = loaded ? selectCollections(
6837
+ loaded.collections,
6838
+ opts.collections,
6839
+ loaded.source,
6840
+ (message) => new DocmetaError(message)
6841
+ ) : [];
6842
+ const fromCollections = opts.inputs.length === 0;
6843
+ const inputs = fromCollections ? collections.flatMap((c) => c.paths) : opts.inputs;
6844
+ const base = fromCollections && inputs.length > 0 && loaded ? loaded.dir : cwd;
6578
6845
  return {
6579
6846
  config,
6580
6847
  inputs,
6581
6848
  base,
6582
- ...loaded ? { configDir: loaded.dir, configPath: loaded.path } : {}
6849
+ collections,
6850
+ declaredCollections: loaded ? [...loaded.collections] : [],
6851
+ fromCollections,
6852
+ ...loaded ? {
6853
+ configDir: loaded.dir,
6854
+ configPath: loaded.path,
6855
+ ...loaded.section === void 0 ? {} : { configSection: loaded.section }
6856
+ } : {}
6583
6857
  };
6584
6858
  }
6585
6859
 
6860
+ // src/meta/core/collections.ts
6861
+ import { isAbsolute as isAbsolute3, relative as relative6, resolve as resolvePath2, sep as sep3 } from "node:path";
6862
+
6863
+ // src/meta/core/projection.ts
6864
+ var SYSTEM_COLUMNS = ["_path", "_format", "_present", "_data"];
6865
+ var RESERVED = new Set(SYSTEM_COLUMNS);
6866
+ function quoteIdent(name2) {
6867
+ return `"${name2.replaceAll('"', '""')}"`;
6868
+ }
6869
+ function bindValue(value) {
6870
+ if (value === void 0 || value === null) return null;
6871
+ switch (typeof value) {
6872
+ case "boolean":
6873
+ return value ? 1 : 0;
6874
+ case "number":
6875
+ return Number.isFinite(value) ? value : null;
6876
+ case "bigint":
6877
+ case "string":
6878
+ return value;
6879
+ default:
6880
+ return JSON.stringify(value);
6881
+ }
6882
+ }
6883
+ function corpusDataColumns(entries, extraKeys = []) {
6884
+ const keys = /* @__PURE__ */ new Set();
6885
+ for (const { extracted } of entries) {
6886
+ for (const key of Object.keys(extracted.data)) {
6887
+ if (key !== "" && !RESERVED.has(key)) keys.add(key);
6888
+ }
6889
+ }
6890
+ for (const key of extraKeys) {
6891
+ if (key !== "" && !RESERVED.has(key)) keys.add(key);
6892
+ }
6893
+ return [...keys].sort();
6894
+ }
6895
+ function createDocsTable(db, entries, dataColumns) {
6896
+ db.exec(
6897
+ `CREATE TABLE docs ("_path" TEXT PRIMARY KEY, "_format" TEXT, "_present" INTEGER, "_data" TEXT${dataColumns.map((c) => `, ${quoteIdent(c)}`).join("")})`
6898
+ );
6899
+ const insert = db.prepare(
6900
+ `INSERT INTO docs VALUES (${["?", "?", "?", "?", ...dataColumns.map(() => "?")].join(", ")})`
6901
+ );
6902
+ db.exec("BEGIN");
6903
+ for (const { label, extracted } of entries) {
6904
+ insert.run(
6905
+ label,
6906
+ extracted.format,
6907
+ extracted.present ? 1 : 0,
6908
+ JSON.stringify(extracted.data),
6909
+ ...dataColumns.map((c) => bindValue(extracted.data[c]))
6910
+ );
6911
+ }
6912
+ db.exec("COMMIT");
6913
+ }
6914
+ function registerLineFor(db, entries) {
6915
+ const byLabel = new Map(entries.map((e) => [e.label, e.extracted]));
6916
+ db.function("lineFor", { deterministic: true }, (path, key) => {
6917
+ if (typeof path !== "string" || typeof key !== "string") return null;
6918
+ return byLabel.get(path)?.lineFor(key) ?? null;
6919
+ });
6920
+ }
6921
+ function assertSingleStatement(sql) {
6922
+ const cut = topLevelSemicolon(sql);
6923
+ if (cut === -1) return;
6924
+ if (!isTrivia(sql.slice(cut + 1))) {
6925
+ throw new DocmetaError(
6926
+ "Run a single SQL statement per query; text after the first `;` would be silently ignored."
6927
+ );
6928
+ }
6929
+ }
6930
+ function startsStringOrIdent(sql, i) {
6931
+ const ch = sql[i];
6932
+ return ch === "'" || ch === '"' || ch === "`" || ch === "[";
6933
+ }
6934
+ function startsComment(sql, i) {
6935
+ const ch = sql[i];
6936
+ return ch === "-" && sql[i + 1] === "-" || ch === "/" && sql[i + 1] === "*";
6937
+ }
6938
+ function skipStringOrIdent(sql, i) {
6939
+ const quote = sql[i];
6940
+ if (quote === "[") {
6941
+ let j2 = i + 1;
6942
+ while (j2 < sql.length && sql[j2] !== "]") j2++;
6943
+ return j2 + 1;
6944
+ }
6945
+ let j = i + 1;
6946
+ while (j < sql.length) {
6947
+ if (sql[j] === quote) {
6948
+ if (sql[j + 1] === quote) {
6949
+ j += 2;
6950
+ continue;
6951
+ }
6952
+ return j + 1;
6953
+ }
6954
+ j++;
6955
+ }
6956
+ return j;
6957
+ }
6958
+ function skipComment(sql, i) {
6959
+ if (sql[i] === "-") {
6960
+ let j = i + 2;
6961
+ while (j < sql.length && sql[j] !== "\n") j++;
6962
+ return j < sql.length ? j + 1 : j;
6963
+ }
6964
+ const end = sql.indexOf("*/", i + 2);
6965
+ return end === -1 ? sql.length : end + 2;
6966
+ }
6967
+ function topLevelSemicolon(sql) {
6968
+ let i = 0;
6969
+ while (i < sql.length) {
6970
+ if (startsStringOrIdent(sql, i)) {
6971
+ i = skipStringOrIdent(sql, i);
6972
+ } else if (startsComment(sql, i)) {
6973
+ i = skipComment(sql, i);
6974
+ } else if (sql[i] === ";") {
6975
+ return i;
6976
+ } else {
6977
+ i++;
6978
+ }
6979
+ }
6980
+ return -1;
6981
+ }
6982
+ var PARAM_NAME_HEAD = /^[A-Za-z_][A-Za-z0-9_]*/;
6983
+ function isParamName(name2) {
6984
+ return PARAM_NAME_HEAD.exec(name2)?.[0] === name2;
6985
+ }
6986
+ function collectNamedParameters(sql) {
6987
+ const tokens = [];
6988
+ const seen = /* @__PURE__ */ new Set();
6989
+ const prefixOf = /* @__PURE__ */ new Map();
6990
+ let i = 0;
6991
+ while (i < sql.length) {
6992
+ const ch = sql[i];
6993
+ if (startsStringOrIdent(sql, i)) {
6994
+ i = skipStringOrIdent(sql, i);
6995
+ } else if (startsComment(sql, i)) {
6996
+ i = skipComment(sql, i);
6997
+ } else if (ch === "$" || ch === ":" || ch === "@") {
6998
+ if (ch === ":" && sql[i + 1] === ":") {
6999
+ i += 2;
7000
+ continue;
7001
+ }
7002
+ const m = PARAM_NAME_HEAD.exec(sql.slice(i + 1));
7003
+ if (!m) {
7004
+ i++;
7005
+ continue;
7006
+ }
7007
+ const token = ch + m[0];
7008
+ const priorPrefix = prefixOf.get(m[0]);
7009
+ if (priorPrefix !== void 0 && priorPrefix !== ch) {
7010
+ throw new DocmetaError(
7011
+ `The statement references both ${priorPrefix}${m[0]} and ${token} \u2014 one name under two prefixes, which the engine rejects as conflicting. Use one spelling throughout.`
7012
+ );
7013
+ }
7014
+ prefixOf.set(m[0], ch);
7015
+ if (!seen.has(token)) {
7016
+ seen.add(token);
7017
+ tokens.push(token);
7018
+ }
7019
+ i += 1 + m[0].length;
7020
+ } else {
7021
+ i++;
7022
+ }
7023
+ }
7024
+ return tokens;
7025
+ }
7026
+ function stripLeadingTrivia(sql) {
7027
+ let i = 0;
7028
+ for (; ; ) {
7029
+ while (i < sql.length && /\s/.test(sql[i] ?? "")) i++;
7030
+ if (startsComment(sql, i)) {
7031
+ i = skipComment(sql, i);
7032
+ continue;
7033
+ }
7034
+ return sql.slice(i);
7035
+ }
7036
+ }
7037
+ function isTrivia(text) {
7038
+ let i = 0;
7039
+ while (i < text.length) {
7040
+ const ch = text[i];
7041
+ if (ch === void 0 || /\s/.test(ch) || ch === ";") {
7042
+ i++;
7043
+ continue;
7044
+ }
7045
+ if (startsComment(text, i)) {
7046
+ i = skipComment(text, i);
7047
+ continue;
7048
+ }
7049
+ return false;
7050
+ }
7051
+ return true;
7052
+ }
7053
+ var sqliteModule;
7054
+ function loadSqlite() {
7055
+ sqliteModule ??= (async () => {
7056
+ const original = process.emitWarning.bind(process);
7057
+ const filtered = (warning, ...rest) => {
7058
+ const text = typeof warning === "string" ? warning : warning.message;
7059
+ if (text.includes("SQLite is an experimental feature")) return;
7060
+ Reflect.apply(original, process, [warning, ...rest]);
7061
+ };
7062
+ process.emitWarning = filtered;
7063
+ try {
7064
+ return await import("node:sqlite");
7065
+ } finally {
7066
+ process.emitWarning = original;
7067
+ }
7068
+ })();
7069
+ return sqliteModule;
7070
+ }
7071
+
7072
+ // src/meta/core/collections.ts
7073
+ function memberOf(collections, configDir, base, label) {
7074
+ if (collections.length === 0) return [];
7075
+ if (label === STDIN_LABEL) return [];
7076
+ const rel = relative6(configDir, resolvePath2(base, label)).split(sep3).join("/");
7077
+ if (isAbsolute3(rel)) return [];
7078
+ return collections.filter((c) => isMember(c, rel)).map((c) => c.name);
7079
+ }
7080
+ function retainMembers(files, membersFor) {
7081
+ return files.filter((label) => membersFor(label).length > 0);
7082
+ }
7083
+ function collectionNames(collections) {
7084
+ return (collections ?? []).map((c) => c.name);
7085
+ }
7086
+ function collectCollections(entries, params) {
7087
+ const declared = params.collections ?? [];
7088
+ if (declared.length === 0) return [];
7089
+ const base = params.base ?? ".";
7090
+ const configDir = params.configDir ?? base;
7091
+ const members = /* @__PURE__ */ new Map();
7092
+ for (const entry of entries) {
7093
+ for (const name2 of memberOf(declared, configDir, base, entry.label)) {
7094
+ const list = members.get(name2);
7095
+ if (list) list.push(entry.label);
7096
+ else members.set(name2, [entry.label]);
7097
+ }
7098
+ }
7099
+ return declared.map((c) => ({ name: c.name, members: members.get(c.name) ?? [] }));
7100
+ }
7101
+ function quoteSqlString(value) {
7102
+ return `'${value.replaceAll("'", "''")}'`;
7103
+ }
7104
+ function createCollectionViews(db, collections) {
7105
+ for (const c of collections) {
7106
+ const where = c.members.length === 0 ? "0" : `_path IN (${c.members.map(quoteSqlString).join(", ")})`;
7107
+ db.exec(
7108
+ `CREATE VIEW ${quoteIdent(c.name)} AS SELECT * FROM docs WHERE ${where}`
7109
+ );
7110
+ }
7111
+ }
7112
+
6586
7113
  // src/meta/core/validator.ts
6587
7114
  import * as AjvDraft07Ns from "ajv";
6588
7115
  import * as Ajv2019Ns from "ajv/dist/2019.js";
@@ -6813,7 +7340,7 @@ import { resolve as resolve9, extname as extname2 } from "node:path";
6813
7340
  // src/meta/core/baseline.ts
6814
7341
  import { createHash as createHash3 } from "node:crypto";
6815
7342
  import { readFile as readFile5 } from "node:fs/promises";
6816
- import { relative as relative6, resolve as resolve8 } from "node:path";
7343
+ import { relative as relative7, resolve as resolve8 } from "node:path";
6817
7344
  var DEFAULT_BASELINE_PATH = ".manni-baseline.json";
6818
7345
  var LEGACY_BASELINE_PATH = ".docmeta-baseline.json";
6819
7346
  var BASELINE_VERSION = 1;
@@ -6821,13 +7348,13 @@ var FINGERPRINT_RE = /^[0-9a-f]{16}$/;
6821
7348
  function canonicalFilePath(file, ctx) {
6822
7349
  if (!ctx) return file;
6823
7350
  const from = ctx.runBase ?? ctx.base;
6824
- return relative6(ctx.base, resolve8(from, file)).replace(/\\/g, "/");
7351
+ return relative7(ctx.base, resolve8(from, file)).replace(/\\/g, "/");
6825
7352
  }
6826
7353
  function canonicalSchemaRef(ref, ctx) {
6827
7354
  if (!ctx) return ref;
6828
7355
  if (classifyRef(ref).kind !== "file") return ref;
6829
7356
  const abs = resolve8(ctx.cwd, ref);
6830
- return relative6(ctx.base, abs).replace(/\\/g, "/");
7357
+ return relative7(ctx.base, abs).replace(/\\/g, "/");
6831
7358
  }
6832
7359
  function fingerprint(e, ctx) {
6833
7360
  const parts = [
@@ -6851,408 +7378,134 @@ function serializeBaseline(baseline) {
6851
7378
  },
6852
7379
  null,
6853
7380
  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;
7381
+ )}
7382
+ `;
7102
7383
  }
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;
7384
+ function bad(source, detail) {
7385
+ throw new DocmetaError(`Baseline "${source}": ${detail}`);
7106
7386
  }
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
- }
7387
+ function parseBaseline(text, source) {
7388
+ let raw;
7389
+ try {
7390
+ raw = JSON.parse(stripBom(text));
7391
+ } catch (err) {
7392
+ bad(source, `invalid JSON: ${err.message}`);
7144
7393
  }
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);
7394
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
7395
+ bad(source, "top level must be an object.");
7156
7396
  }
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;
7397
+ const obj = raw;
7398
+ if (obj.version !== BASELINE_VERSION) {
7399
+ bad(
7400
+ source,
7401
+ `unsupported version ${JSON.stringify(obj.version)} (this manni writes version ${BASELINE_VERSION}). Re-record it with \`manni meta validate --write-baseline\`.`
7402
+ );
7403
+ }
7404
+ const generatedWith = typeof obj.generatedWith === "string" ? obj.generatedWith : "";
7405
+ const rawEntries = obj.entries;
7406
+ if (typeof rawEntries !== "object" || rawEntries === null || Array.isArray(rawEntries)) {
7407
+ bad(source, '"entries" must be an object mapping file paths to fingerprints.');
7408
+ }
7409
+ const entries = /* @__PURE__ */ Object.create(null);
7410
+ for (const [file, value] of Object.entries(
7411
+ rawEntries
7412
+ )) {
7413
+ if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
7414
+ bad(source, `entries["${file}"] must be a list of fingerprint strings.`);
7165
7415
  }
7166
- if (startsComment(text, i)) {
7167
- i = skipComment(text, i);
7168
- continue;
7416
+ const malformed = value.find((v) => !FINGERPRINT_RE.test(v));
7417
+ if (malformed !== void 0) {
7418
+ bad(
7419
+ source,
7420
+ `entries["${file}"] contains ${JSON.stringify(malformed)}, which is not a fingerprint (16 lowercase hex characters).`
7421
+ );
7169
7422
  }
7170
- return false;
7423
+ entries[file] = value;
7171
7424
  }
7172
- return true;
7425
+ return { version: BASELINE_VERSION, generatedWith, entries };
7173
7426
  }
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;
7427
+ async function readBaseline(absPath, source) {
7428
+ let text;
7429
+ try {
7430
+ text = await readFile5(absPath, "utf8");
7431
+ } catch (err) {
7432
+ if (err.code === "ENOENT") return null;
7433
+ throw new DocmetaError(
7434
+ `Baseline "${source}" could not be read: ${err.message}`
7435
+ );
7436
+ }
7437
+ return parseBaseline(text, source);
7191
7438
  }
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
- );
7439
+ async function writeBaselineFile(absPath, baseline, source) {
7440
+ try {
7441
+ await writeFileAtomic(absPath, serializeBaseline(baseline));
7442
+ } catch (err) {
7443
+ throw new DocmetaError(
7444
+ `Baseline "${source}" could not be written: ${err.message}`
7445
+ );
7446
+ }
7198
7447
  }
7199
- function collectionNames(config) {
7200
- return namedOverrides(config).map((n) => n.name);
7448
+ function buildBaseline(results, generatedWith, ctx) {
7449
+ const entries = /* @__PURE__ */ Object.create(null);
7450
+ for (const r of results) {
7451
+ if (r.errors.length === 0) continue;
7452
+ const prints = [...new Set(r.errors.map((e) => fingerprint(e, ctx)))].sort();
7453
+ entries[canonicalFilePath(r.file, ctx)] = prints;
7454
+ }
7455
+ return { version: BASELINE_VERSION, generatedWith, entries };
7201
7456
  }
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
- );
7457
+ function applyBaseline(results, baseline, ctx) {
7458
+ let recorded = 0;
7459
+ let suppressed = 0;
7460
+ let stale = 0;
7461
+ const applied = results.map((r) => {
7462
+ const known = baseline.entries[canonicalFilePath(r.file, ctx)];
7463
+ if (!known) return r;
7464
+ const recordedHere = new Set(known);
7465
+ recorded += recordedHere.size;
7466
+ const seen = /* @__PURE__ */ new Set();
7467
+ const fresh = [];
7468
+ for (const e of r.errors) {
7469
+ const print = fingerprint(e, ctx);
7470
+ if (recordedHere.has(print)) {
7471
+ seen.add(print);
7472
+ suppressed += 1;
7473
+ } else {
7474
+ fresh.push(e);
7238
7475
  }
7239
7476
  }
7240
- }
7241
- return named.map(({ name: name2, index }) => ({
7242
- name: name2,
7243
- members: members.get(index) ?? []
7244
- }));
7477
+ stale += recordedHere.size - seen.size;
7478
+ const baselined = r.errors.length - fresh.length;
7479
+ return {
7480
+ ...r,
7481
+ ok: fresh.length === 0,
7482
+ errors: fresh,
7483
+ ...baselined > 0 ? { baselined } : {}
7484
+ };
7485
+ });
7486
+ return { results: applied, recorded, suppressed, stale };
7245
7487
  }
7246
- function quoteSqlString(value) {
7247
- return `'${value.replaceAll("'", "''")}'`;
7488
+ function diffBaselines(previous, next) {
7489
+ const flatten = (b) => {
7490
+ const out = /* @__PURE__ */ new Set();
7491
+ if (!b) return out;
7492
+ for (const [file, prints] of Object.entries(b.entries)) {
7493
+ for (const p of prints) out.add(`${file}\0${p}`);
7494
+ }
7495
+ return out;
7496
+ };
7497
+ const before = flatten(previous);
7498
+ const after = flatten(next);
7499
+ let added = 0;
7500
+ let removed = 0;
7501
+ for (const k of after) if (!before.has(k)) added += 1;
7502
+ for (const k of before) if (!after.has(k)) removed += 1;
7503
+ return { added, removed };
7248
7504
  }
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
- }
7505
+ function countFingerprints(baseline) {
7506
+ let n = 0;
7507
+ for (const prints of Object.values(baseline.entries)) n += prints.length;
7508
+ return n;
7256
7509
  }
7257
7510
 
7258
7511
  // src/meta/core/checks.ts
@@ -7416,17 +7669,27 @@ function resolveBaselineRequest(opts, configured, configDir, cwd) {
7416
7669
  }
7417
7670
  async function runValidate(opts) {
7418
7671
  const cwd = opts.cwd ?? process.cwd();
7419
- const { config, inputs, base, configDir } = await resolveRunConfig({
7672
+ const {
7673
+ config,
7674
+ inputs,
7675
+ base,
7676
+ configDir,
7677
+ collections,
7678
+ declaredCollections,
7679
+ fromCollections
7680
+ } = await resolveRunConfig({
7420
7681
  cwd,
7421
7682
  configPath: opts.configPath,
7422
7683
  noConfig: opts.noConfig,
7423
7684
  inputs: opts.inputs,
7685
+ ...opts.collections !== void 0 ? { collections: opts.collections } : {},
7424
7686
  onConfigLoaded: opts.onConfigLoaded
7425
7687
  });
7688
+ const membersFor = (label) => memberOf(collections, configDir ?? cwd, base, label);
7426
7689
  const usingStdin = inputs.includes(STDIN_TOKEN);
7427
7690
  if (inputs.length === 0) {
7428
7691
  throw new DocmetaError(
7429
- "No files to validate. Pass paths/globs, or add `paths:` under `meta:` in manni.config.yaml."
7692
+ "No files to validate. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml."
7430
7693
  );
7431
7694
  }
7432
7695
  const forcedExtractor = opts.as ? extractorByName(opts.as) : void 0;
@@ -7438,8 +7701,8 @@ async function runValidate(opts) {
7438
7701
  const exts = opts.exts ?? (forcedExtractor ? forcedExtractor.extensions : void 0);
7439
7702
  const fileInputs = inputs.filter((i) => i !== STDIN_TOKEN);
7440
7703
  const allowEmpty = opts.allowEmpty ?? config?.allowEmpty;
7441
- const exclude = [...config?.exclude ?? [], ...opts.exclude ?? []];
7442
- const { files, gitignoreSkipped } = await resolveTargetSet({
7704
+ const exclude = opts.exclude ?? [];
7705
+ const { files: walked, gitignoreSkipped } = await resolveTargetSet({
7443
7706
  inputs: fileInputs,
7444
7707
  exts,
7445
7708
  exclude,
@@ -7451,6 +7714,7 @@ async function runValidate(opts) {
7451
7714
  onNotice: opts.onNotice
7452
7715
  })
7453
7716
  });
7717
+ const files = fromCollections ? retainMembers(walked, membersFor) : walked;
7454
7718
  assertNonEmpty({
7455
7719
  files,
7456
7720
  inputs: fileInputs,
@@ -7462,7 +7726,7 @@ async function runValidate(opts) {
7462
7726
  action: "validated"
7463
7727
  });
7464
7728
  const trustRoot = schemaTrustRoot(cwd, configDir);
7465
- const sidecars = await loadSidecars(config, {
7729
+ const externalMetadata = await loadExternalMetadata(collections, {
7466
7730
  configDir: configDir ?? cwd,
7467
7731
  base,
7468
7732
  offline: opts.offline ?? config?.offline ?? false
@@ -7483,16 +7747,18 @@ async function runValidate(opts) {
7483
7747
  );
7484
7748
  const results = [];
7485
7749
  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
7750
+ const scopedToCollections = (opts.collections?.length ?? 0) > 0;
7751
+ const scopedByFlags = opts.inputs.length > 0 || // redundant with inputs.length (stdin is an input) — kept as belt-and-suspenders
7487
7752
  usingStdin || opts.as !== void 0 || opts.exts !== void 0 || opts.exclude !== void 0 && opts.exclude.length > 0 || opts.respectGitignore !== void 0 || (opts.cliSchemas?.length ?? 0) > 0;
7753
+ const scoped = scopedByFlags || scopedToCollections;
7488
7754
  const configuredChecks = config?.checks ?? [];
7489
- if (!scoped) {
7490
- const orphans = orphanEntries(sidecars, files, base);
7755
+ if (!scopedByFlags) {
7756
+ const covered = scopedToCollections ? collections.map((c) => c.name) : void 0;
7757
+ const orphans = orphanEntries(externalMetadata, files, base, covered);
7491
7758
  if (orphans.length > 0) throw orphanError(orphans);
7492
7759
  }
7493
7760
  const checksWillRun = configuredChecks.length > 0 && opts.checks !== false && !scoped;
7494
7761
  const checkEntries = [];
7495
- const checkResolved = /* @__PURE__ */ new Map();
7496
7762
  const processOne = async (label, content, extension) => {
7497
7763
  const extractor = forcedExtractor ?? extractorForExtension(extension);
7498
7764
  if (!extractor) {
@@ -7500,10 +7766,11 @@ async function runValidate(opts) {
7500
7766
  `Unsupported file type "${extension}" for "${label}". Supported: ${supportedExtensions().join(", ")}. Use --as to override.`
7501
7767
  );
7502
7768
  }
7769
+ const members = membersFor(label);
7503
7770
  let extracted;
7504
7771
  try {
7505
7772
  extracted = extractor.extract(content, label, {
7506
- elements: resolveElements(label, config)
7773
+ elements: resolveElements(label, config, members)
7507
7774
  });
7508
7775
  } catch (err) {
7509
7776
  if (err instanceof DocmetaError) throw err;
@@ -7512,7 +7779,7 @@ async function runValidate(opts) {
7512
7779
  );
7513
7780
  return;
7514
7781
  }
7515
- const merged = mergeSidecars(label, extracted, sidecars, base);
7782
+ const merged = mergeExternalMetadata(label, extracted, externalMetadata, members, base);
7516
7783
  extracted = merged.extracted;
7517
7784
  for (const j of merged.joins) {
7518
7785
  const byValue = joinHits.get(j.field) ?? /* @__PURE__ */ new Map();
@@ -7530,6 +7797,7 @@ async function runValidate(opts) {
7530
7797
  fileSchema: extracted.data[FILE_SCHEMA_KEY],
7531
7798
  cliSchemas: opts.cliSchemas,
7532
7799
  config,
7800
+ memberOf: members,
7533
7801
  // A document's own `$schema` is measured from the run's directory, the
7534
7802
  // same base `loadSchema` will read it from, and contained to the
7535
7803
  // repository the run is standing in.
@@ -7543,7 +7811,6 @@ async function runValidate(opts) {
7543
7811
  );
7544
7812
  return;
7545
7813
  }
7546
- if (checksWillRun) checkResolved.set(label, resolved);
7547
7814
  const schemaSet = resolved.schemas;
7548
7815
  let errors;
7549
7816
  try {
@@ -7563,14 +7830,14 @@ async function runValidate(opts) {
7563
7830
  );
7564
7831
  return;
7565
7832
  }
7566
- for (const { key, file } of merged.collisions) {
7833
+ for (const { key, file, collection } of merged.collisions) {
7567
7834
  const line = extracted.lineFor(key);
7568
7835
  errors.push({
7569
- schema: SIDECAR_OWNED_SCHEMA,
7570
- keyword: SIDECAR_KEYWORD,
7836
+ schema: EXTERNAL_OWNED_SCHEMA,
7837
+ keyword: EXTERNAL_KEYWORD,
7571
7838
  subject: key,
7572
- instancePath: sidecarPointer(key),
7573
- message: `"${key}" is owned by sidecar ${file}; remove it from the document`,
7839
+ instancePath: externalMetadataPointer(key),
7840
+ message: `"${key}" is owned by manifest ${file} (collection ${collection}); remove it from the document`,
7574
7841
  ...line != null ? { line } : {}
7575
7842
  });
7576
7843
  }
@@ -7595,7 +7862,7 @@ async function runValidate(opts) {
7595
7862
  const content = await readFile6(resolve9(base, file), "utf8");
7596
7863
  await processOne(file, content, extname2(file));
7597
7864
  }
7598
- if (sidecars) {
7865
+ if (externalMetadata) {
7599
7866
  const byLabel = new Map(results.map((r) => [r.file, r]));
7600
7867
  const matched = /* @__PURE__ */ new Map();
7601
7868
  for (const [field, byValue] of joinHits) {
@@ -7607,10 +7874,10 @@ async function runValidate(opts) {
7607
7874
  const result = byLabel.get(hit.label);
7608
7875
  if (!result) continue;
7609
7876
  result.errors.push({
7610
- schema: SIDECAR_DUPLICATE_SCHEMA,
7611
- keyword: SIDECAR_KEYWORD,
7877
+ schema: EXTERNAL_DUPLICATE_SCHEMA,
7878
+ keyword: EXTERNAL_KEYWORD,
7612
7879
  subject: value,
7613
- instancePath: sidecarPointer(field),
7880
+ instancePath: externalMetadataPointer(field),
7614
7881
  message: `${hits.length} documents carry ${field} "${value}"; ${hit.file} cannot tell them apart (${others})`,
7615
7882
  ...hit.line != null ? { line: hit.line } : {}
7616
7883
  });
@@ -7618,26 +7885,26 @@ async function runValidate(opts) {
7618
7885
  }
7619
7886
  }
7620
7887
  }
7621
- if (!scoped) {
7622
- const orphans = orphanJoins(sidecars, matched);
7888
+ if (!scopedByFlags) {
7889
+ const covered = scopedToCollections ? collections.map((c) => c.name) : void 0;
7890
+ const orphans = orphanJoins(externalMetadata, matched, covered);
7623
7891
  if (orphans.length > 0) throw orphanError(orphans);
7624
7892
  }
7625
7893
  }
7626
7894
  if (configuredChecks.length > 0 && opts.checks !== false) {
7627
7895
  if (scoped) {
7628
- opts.onNotice?.("corpus checks skipped: run is scoped");
7896
+ opts.onNotice?.(
7897
+ scopedToCollections ? `corpus checks skipped: run is scoped to collections ${collections.map((c) => c.name).join(", ")}` : "corpus checks skipped: run is scoped"
7898
+ );
7629
7899
  } else {
7630
7900
  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 } : {}
7901
+ // Every declared collection is a view (0041 rule 7), holding the
7902
+ // members this run loaded — decided by the same path arithmetic
7903
+ // `membersFor` uses, so a check and a `manni meta query` over the same
7904
+ // corpus see the same collections.
7905
+ collections: declaredCollections,
7906
+ ...configDir !== void 0 ? { configDir } : {},
7907
+ base
7641
7908
  });
7642
7909
  const byFile = new Map(results.map((r) => [r.file, r]));
7643
7910
  for (const [file, errs] of findings) {
@@ -7719,17 +7986,18 @@ async function runGet(opts) {
7719
7986
  if (opts.fields.length === 0) {
7720
7987
  throw new DocmetaError("Specify at least one field to get.");
7721
7988
  }
7722
- const { config, inputs, base, configDir } = await resolveRunConfig({
7989
+ const { config, inputs, base, configDir, collections, fromCollections } = await resolveRunConfig({
7723
7990
  cwd,
7724
7991
  configPath: opts.configPath,
7725
7992
  noConfig: opts.noConfig,
7726
7993
  inputs: opts.inputs,
7994
+ ...opts.collections !== void 0 ? { collections: opts.collections } : {},
7727
7995
  onConfigLoaded: opts.onConfigLoaded
7728
7996
  });
7729
7997
  const usingStdin = inputs.includes(STDIN_TOKEN);
7730
7998
  if (inputs.length === 0) {
7731
7999
  throw new DocmetaError(
7732
- "No files to read. Pass paths/globs, or add `paths:` under `meta:` in manni.config.yaml."
8000
+ "No files to read. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml."
7733
8001
  );
7734
8002
  }
7735
8003
  const forced = opts.as ? extractorByName(opts.as) : void 0;
@@ -7741,8 +8009,9 @@ async function runGet(opts) {
7741
8009
  const exts = opts.exts ?? (forced ? forced.extensions : void 0);
7742
8010
  const fileInputs = inputs.filter((i) => i !== STDIN_TOKEN);
7743
8011
  const allowEmpty = opts.allowEmpty ?? config?.allowEmpty;
7744
- const exclude = [...config?.exclude ?? [], ...opts.exclude ?? []];
7745
- const { files, gitignoreSkipped } = await resolveTargetSet({
8012
+ const exclude = opts.exclude ?? [];
8013
+ const membersFor = (label) => memberOf(collections, configDir ?? cwd, base, label);
8014
+ const { files: walked, gitignoreSkipped } = await resolveTargetSet({
7746
8015
  inputs: fileInputs,
7747
8016
  exts,
7748
8017
  exclude,
@@ -7754,6 +8023,7 @@ async function runGet(opts) {
7754
8023
  onNotice: opts.onNotice
7755
8024
  })
7756
8025
  });
8026
+ const files = fromCollections ? retainMembers(walked, membersFor) : walked;
7757
8027
  assertNonEmpty({
7758
8028
  files,
7759
8029
  inputs: fileInputs,
@@ -7765,7 +8035,7 @@ async function runGet(opts) {
7765
8035
  action: "read"
7766
8036
  });
7767
8037
  const out = [];
7768
- const sidecars = await loadSidecars(config, {
8038
+ const externalMetadata = await loadExternalMetadata(collections, {
7769
8039
  configDir: configDir ?? cwd,
7770
8040
  base,
7771
8041
  offline: opts.offline ?? config?.offline ?? false
@@ -7777,14 +8047,16 @@ async function runGet(opts) {
7777
8047
  `Unsupported file type "${extension}" for "${label}". Supported: ${supportedExtensions().join(", ")}. Use --as to override.`
7778
8048
  );
7779
8049
  }
8050
+ const members = membersFor(label);
7780
8051
  let extracted;
7781
8052
  try {
7782
- extracted = mergeSidecars(
8053
+ extracted = mergeExternalMetadata(
7783
8054
  label,
7784
8055
  extractor.extract(content, label, {
7785
- elements: resolveElements(label, config)
8056
+ elements: resolveElements(label, config, members)
7786
8057
  }),
7787
- sidecars,
8058
+ externalMetadata,
8059
+ members,
7788
8060
  base
7789
8061
  ).extracted;
7790
8062
  } catch (err) {
@@ -7844,7 +8116,7 @@ function parseJsonPointer(pointer) {
7844
8116
  // src/meta/commands/schemas.ts
7845
8117
  import { existsSync as existsSync3 } from "node:fs";
7846
8118
  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";
8119
+ import { dirname as dirname5, extname as extname4, isAbsolute as isAbsolute4, join as join5, relative as relative8, resolve as resolve11, sep as sep4 } from "node:path";
7848
8120
  import { parseDocument as parseDocument4, parse as parseYaml3, stringify } from "yaml";
7849
8121
  function getSchemasInfo() {
7850
8122
  return { builtins: listBuiltins(), formats: listFormats() };
@@ -7852,7 +8124,7 @@ function getSchemasInfo() {
7852
8124
  var DEFAULT_VENDOR_DIR = "./schema";
7853
8125
  var DEFAULT_CONFIG_NAME = "manni.config.yaml";
7854
8126
  function posixRelative(from, to) {
7855
- return relative7(from, to).split(sep3).join("/");
8127
+ return relative8(from, to).split(sep4).join("/");
7856
8128
  }
7857
8129
  function vendorFileName(url) {
7858
8130
  let parsed2;
@@ -7944,7 +8216,7 @@ async function runVendorSchema(opts) {
7944
8216
  const absFile = join5(absDir, vendorFileName(url));
7945
8217
  await assertNotIgnored(absFile, absDir, cwd, opts.onNotice);
7946
8218
  const fromConfig = posixRelative(configDir, absFile);
7947
- const ref = isAbsolute3(fromConfig) || fromConfig === "" ? absFile : fromConfig.startsWith(".") ? fromConfig : `./${fromConfig}`;
8219
+ const ref = isAbsolute4(fromConfig) || fromConfig === "" ? absFile : fromConfig.startsWith(".") ? fromConfig : `./${fromConfig}`;
7948
8220
  const { bytes } = await fetchSchemaBytes(url, {
7949
8221
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
7950
8222
  ...opts.maxBytes !== void 0 ? { maxBytes: opts.maxBytes } : {}
@@ -8088,22 +8360,23 @@ async function runInferSchema(opts) {
8088
8360
  `--min-coverage must be a percentage between 0 and 100, got ${minCoverage}.`
8089
8361
  );
8090
8362
  }
8091
- const { config, inputs, base, configDir } = await resolveRunConfig({
8363
+ const { config, inputs, base, configDir, collections, fromCollections } = await resolveRunConfig({
8092
8364
  cwd,
8093
8365
  configPath: opts.configPath,
8094
8366
  noConfig: opts.noConfig,
8095
8367
  inputs: opts.inputs,
8368
+ ...opts.collections !== void 0 ? { collections: opts.collections } : {},
8096
8369
  onConfigLoaded: opts.onConfigLoaded
8097
8370
  });
8098
8371
  const usingStdin = inputs.includes(STDIN_TOKEN);
8099
- const sidecars = await loadSidecars(config, {
8372
+ const externalMetadata = await loadExternalMetadata(collections, {
8100
8373
  configDir: configDir ?? cwd,
8101
8374
  base,
8102
8375
  offline: opts.offline ?? config?.offline ?? false
8103
8376
  });
8104
8377
  if (inputs.length === 0) {
8105
8378
  throw new DocmetaError(
8106
- "No files to scan. Pass paths/globs, or add `paths:` under `meta:` in manni.config.yaml."
8379
+ "No files to scan. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml."
8107
8380
  );
8108
8381
  }
8109
8382
  const forced = opts.as ? extractorByName(opts.as) : void 0;
@@ -8136,8 +8409,9 @@ async function runInferSchema(opts) {
8136
8409
  const exts = opts.exts ?? (forced ? forced.extensions : void 0);
8137
8410
  const fileInputs = inputs.filter((i) => i !== STDIN_TOKEN);
8138
8411
  const allowEmpty = opts.allowEmpty ?? config?.allowEmpty;
8139
- const exclude = [...config?.exclude ?? [], ...opts.exclude ?? []];
8140
- const { files, gitignoreSkipped } = await resolveTargetSet({
8412
+ const exclude = opts.exclude ?? [];
8413
+ const membersFor = (label) => memberOf(collections, configDir ?? cwd, base, label);
8414
+ const { files: walked, gitignoreSkipped } = await resolveTargetSet({
8141
8415
  inputs: fileInputs,
8142
8416
  exts,
8143
8417
  exclude,
@@ -8149,6 +8423,7 @@ async function runInferSchema(opts) {
8149
8423
  onNotice: opts.onNotice
8150
8424
  })
8151
8425
  });
8426
+ const files = fromCollections ? retainMembers(walked, membersFor) : walked;
8152
8427
  assertNonEmpty({
8153
8428
  files,
8154
8429
  inputs: fileInputs,
@@ -8171,14 +8446,16 @@ async function runInferSchema(opts) {
8171
8446
  );
8172
8447
  }
8173
8448
  filesScanned += 1;
8449
+ const members = membersFor(label);
8174
8450
  let extracted;
8175
8451
  try {
8176
- extracted = mergeSidecars(
8452
+ extracted = mergeExternalMetadata(
8177
8453
  label,
8178
8454
  extractor.extract(content, label, {
8179
- elements: resolveElements(label, config)
8455
+ elements: resolveElements(label, config, members)
8180
8456
  }),
8181
- sidecars,
8457
+ externalMetadata,
8458
+ members,
8182
8459
  base
8183
8460
  ).extracted;
8184
8461
  } catch (err) {
@@ -8264,7 +8541,7 @@ async function runInferSchema(opts) {
8264
8541
  import { randomBytes } from "node:crypto";
8265
8542
  import { existsSync as existsSync4 } from "node:fs";
8266
8543
  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";
8544
+ import { dirname as dirname6, isAbsolute as isAbsolute5, resolve as resolve12, extname as extname5, sep as sep5 } from "node:path";
8268
8545
  import { parseDocument as parseDocument5, isMap as isMap4, isSeq as isSeq2, isScalar as isScalar3 } from "yaml";
8269
8546
  async function runQuery(opts) {
8270
8547
  const cwd = opts.cwd ?? process.cwd();
@@ -8297,19 +8574,29 @@ async function runQuery(opts) {
8297
8574
  );
8298
8575
  }
8299
8576
  }
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
- );
8577
+ const {
8578
+ config,
8579
+ inputs,
8580
+ base,
8581
+ configDir,
8582
+ configPath,
8583
+ configSection,
8584
+ collections,
8585
+ declaredCollections,
8586
+ fromCollections
8587
+ } = await resolveRunConfig({
8588
+ cwd,
8589
+ configPath: opts.configPath,
8590
+ noConfig: opts.noConfig,
8591
+ inputs: opts.inputs,
8592
+ ...opts.collections !== void 0 ? { collections: opts.collections } : {},
8593
+ onConfigLoaded: opts.onConfigLoaded
8594
+ });
8595
+ const membersFor = (label) => memberOf(collections, configDir ?? cwd, base, label);
8309
8596
  const usingStdin = inputs.includes(STDIN_TOKEN);
8310
8597
  if (inputs.length === 0) {
8311
8598
  throw new DocmetaError(
8312
- "No files to read. Pass paths/globs, or add `paths:` under `meta:` in manni.config.yaml."
8599
+ "No files to read. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml."
8313
8600
  );
8314
8601
  }
8315
8602
  const forced = opts.as ? extractorByName(opts.as) : void 0;
@@ -8321,8 +8608,8 @@ async function runQuery(opts) {
8321
8608
  const exts = opts.exts ?? (forced ? forced.extensions : void 0);
8322
8609
  const fileInputs = inputs.filter((i) => i !== STDIN_TOKEN);
8323
8610
  const allowEmpty = opts.allowEmpty ?? config?.allowEmpty;
8324
- const exclude = [...config?.exclude ?? [], ...opts.exclude ?? []];
8325
- const { files, gitignoreSkipped } = await resolveTargetSet({
8611
+ const exclude = opts.exclude ?? [];
8612
+ const { files: walked, gitignoreSkipped } = await resolveTargetSet({
8326
8613
  inputs: fileInputs,
8327
8614
  exts,
8328
8615
  exclude,
@@ -8334,6 +8621,7 @@ async function runQuery(opts) {
8334
8621
  onNotice: opts.onNotice
8335
8622
  })
8336
8623
  });
8624
+ const files = fromCollections ? retainMembers(walked, membersFor) : walked;
8337
8625
  assertNonEmpty({
8338
8626
  files,
8339
8627
  inputs: fileInputs,
@@ -8345,7 +8633,7 @@ async function runQuery(opts) {
8345
8633
  action: "queried"
8346
8634
  });
8347
8635
  const entries = [];
8348
- const sidecars = await loadSidecars(config, {
8636
+ const externalMetadata = await loadExternalMetadata(collections, {
8349
8637
  configDir: configDir ?? cwd,
8350
8638
  base,
8351
8639
  offline: opts.offline ?? config?.offline ?? false
@@ -8357,12 +8645,14 @@ async function runQuery(opts) {
8357
8645
  `Unsupported file type "${extension}" for "${label}". Supported: ${supportedExtensions().join(", ")}. Use --as to override.`
8358
8646
  );
8359
8647
  }
8360
- const extracted = mergeSidecars(
8648
+ const members = membersFor(label);
8649
+ const extracted = mergeExternalMetadata(
8361
8650
  label,
8362
8651
  extractor.extract(content, label, {
8363
- elements: resolveElements(label, config)
8652
+ elements: resolveElements(label, config, members)
8364
8653
  }),
8365
- sidecars,
8654
+ externalMetadata,
8655
+ members,
8366
8656
  base
8367
8657
  ).extracted;
8368
8658
  entries.push({ label, extracted, extractor });
@@ -8396,8 +8686,12 @@ async function runQuery(opts) {
8396
8686
  cwd,
8397
8687
  configDir,
8398
8688
  configPath,
8689
+ configSection,
8399
8690
  trustRoot: schemaTrustRoot(cwd, configDir),
8400
- sidecars,
8691
+ externalMetadata,
8692
+ collections,
8693
+ declaredCollections,
8694
+ memberships: membersFor,
8401
8695
  onNotice: opts.onNotice
8402
8696
  });
8403
8697
  return { ...run, frame: { cwd, base: configDir ?? cwd, runBase: base } };
@@ -8485,10 +8779,9 @@ async function runSql(sql, entries, ctx) {
8485
8779
  createCollectionViews(
8486
8780
  db,
8487
8781
  collectCollections(entries, {
8488
- config: ctx.config,
8489
- fileBase: ctx.cwd,
8490
- trustRoot: ctx.trustRoot,
8491
- ...ctx.onNotice ? { onNotice: ctx.onNotice } : {}
8782
+ collections: ctx.declaredCollections,
8783
+ ...ctx.configDir !== void 0 ? { configDir: ctx.configDir } : {},
8784
+ base: ctx.base
8492
8785
  })
8493
8786
  );
8494
8787
  viewsBuilt = true;
@@ -8504,7 +8797,9 @@ async function runSql(sql, entries, ctx) {
8504
8797
  );
8505
8798
  }
8506
8799
  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()));
8800
+ const observesCatalog = lower.includes("sqlite_master") || lower.includes("sqlite_schema") || lower.includes("pragma") || /^(create|drop|alter)\b/i.test(head) || collectionNames(ctx.declaredCollections).some(
8801
+ (n) => lower.includes(n.toLowerCase())
8802
+ );
8508
8803
  if (observesCatalog) buildViews();
8509
8804
  const sentinel = `manni:null:${randomBytes(16).toString("hex")}`;
8510
8805
  db.function("explicit_null", () => sentinel);
@@ -8518,7 +8813,7 @@ async function runSql(sql, entries, ctx) {
8518
8813
  } catch (err) {
8519
8814
  const message = err.message;
8520
8815
  const missing = missingTableName(message);
8521
- if (missing !== void 0 && !viewsBuilt && namesConfiguredCollection(missing, ctx.config)) {
8816
+ if (missing !== void 0 && !viewsBuilt && namesConfiguredCollection(missing, ctx.declaredCollections)) {
8522
8817
  throw new MissingCollectionView();
8523
8818
  }
8524
8819
  refuseSqlError(err);
@@ -8592,9 +8887,9 @@ function missingTableName(message) {
8592
8887
  const at = message.indexOf(prefix);
8593
8888
  return at === -1 ? void 0 : message.slice(at + prefix.length);
8594
8889
  }
8595
- function namesConfiguredCollection(missing, config) {
8890
+ function namesConfiguredCollection(missing, collections) {
8596
8891
  const candidates = [missing, missing.replace(/^(main|temp)\./i, "")];
8597
- return collectionNames(config).some(
8892
+ return collectionNames(collections).some(
8598
8893
  (n) => candidates.some((c) => c.toLowerCase() === n.toLowerCase())
8599
8894
  );
8600
8895
  }
@@ -8913,7 +9208,7 @@ function displayPath(ctx, abs) {
8913
9208
  }
8914
9209
  function assertSchemaWriteWithin(ctx, abs) {
8915
9210
  const within = posixRelative(ctx.trustRoot.dir, abs);
8916
- if (within !== "" && !within.startsWith("..") && !isAbsolute4(within)) return;
9211
+ if (within !== "" && !within.startsWith("..") && !isAbsolute5(within)) return;
8917
9212
  throw new DocmetaError(
8918
9213
  `The resolved schema "${abs}" lives outside ${ctx.trustRoot.dir}; DDL edits schemas inside the repository. Evolve it where it lives, or vendor a copy in.`
8919
9214
  );
@@ -9030,6 +9325,7 @@ async function planSchemaMutation(ops, entries, ctx) {
9030
9325
  filePath: e.label,
9031
9326
  fileSchema: e.extracted.data[FILE_SCHEMA_KEY],
9032
9327
  config: ctx.config,
9328
+ memberOf: ctx.memberships(e.label),
9033
9329
  fileBase: ctx.cwd,
9034
9330
  trustRoot: ctx.trustRoot,
9035
9331
  onNotice: ctx.onNotice
@@ -9056,7 +9352,11 @@ async function planSchemaMutation(ops, entries, ctx) {
9056
9352
  const overrides = ctx.config?.overrides ?? [];
9057
9353
  const named = [...groupIndexes].sort((a, b) => a - b).flatMap((i) => {
9058
9354
  const o = overrides[i];
9059
- return o?.name !== void 0 ? [`${o.name} (${overrideGlobs(o.files).join(", ")})`] : [];
9355
+ if (o?.collection === void 0) return [];
9356
+ const declared = ctx.collections.find((c) => c.name === o.collection);
9357
+ return [
9358
+ `${o.collection} (${overrideGlobs(declared?.paths).join(", ")})`
9359
+ ];
9060
9360
  });
9061
9361
  const last = named[named.length - 1];
9062
9362
  const remedy = named.length >= 2 && last !== void 0 ? `The run spans ${[named.slice(0, -1).join(", "), last].join(" and ")}; re-run over one group's files, or pass -s <schema> to name the contract directly.` : "Scope the run to one override group, or pass -s <schema> to name the contract directly.";
@@ -9321,7 +9621,7 @@ async function planConfigEdit(ctx, edit, runRefs) {
9321
9621
  }
9322
9622
  return void 0;
9323
9623
  };
9324
- const rebaseRaw = (raw) => classifyRef(raw).kind === "file" && !isAbsolute4(raw) ? resolve12(configDir, raw) : raw;
9624
+ const rebaseRaw = (raw) => classifyRef(raw).kind === "file" && !isAbsolute5(raw) ? resolve12(configDir, raw) : raw;
9325
9625
  const wantSet = runRefs.map(rebaseRaw).sort().join("\n");
9326
9626
  const seqResolvesToRunSet = (node) => {
9327
9627
  if (!isSeq2(node)) return false;
@@ -9347,9 +9647,11 @@ async function planConfigEdit(ctx, edit, runRefs) {
9347
9647
  }
9348
9648
  return n;
9349
9649
  };
9350
- const topSeq = doc.get("schemas", true);
9650
+ const root = ctx.configSection === void 0 ? doc : doc.get(ctx.configSection, true);
9651
+ const keyOf2 = (key) => root === doc ? doc.get(key, true) : isMap4(root) ? root.get(key, true) : void 0;
9652
+ const topSeq = keyOf2("schemas");
9351
9653
  let repointCount = repoint(topSeq);
9352
- const overrides = doc.get("overrides", true);
9654
+ const overrides = keyOf2("overrides");
9353
9655
  if (isSeq2(overrides)) {
9354
9656
  for (const entry of overrides.items) {
9355
9657
  if (isMap4(entry)) repointCount += repoint(entry.get("schemas", true));
@@ -9364,7 +9666,7 @@ async function planConfigEdit(ctx, edit, runRefs) {
9364
9666
  if (!isMap4(item)) continue;
9365
9667
  const raw = rawRefOf(item);
9366
9668
  if (raw === void 0 || classifyRef(raw).kind !== "file") continue;
9367
- const abs = isAbsolute4(raw) ? raw : resolve12(configDir, raw);
9669
+ const abs = isAbsolute5(raw) ? raw : resolve12(configDir, raw);
9368
9670
  if (foldPath(abs) !== wantPath) continue;
9369
9671
  pinFrom = item.get("integrity");
9370
9672
  item.set("integrity", edit.integrity.value);
@@ -9611,28 +9913,28 @@ function buildChanges(diff, entries, sentinel, ctx, renameHints = [], booleanAdd
9611
9913
  }
9612
9914
  changes.push({ file: from, renamed: to, written: false });
9613
9915
  }
9614
- refuseSidecarWrites(changes, entries, ctx);
9916
+ refuseExternalMetadataWrites(changes, entries, ctx);
9615
9917
  return changes;
9616
9918
  }
9617
9919
  function validateNewPath(p, base) {
9618
9920
  const refuse = (why) => {
9619
9921
  throw new DocmetaError(`"${p}" is not a usable path: ${why}.`);
9620
9922
  };
9621
- if (isAbsolute4(p)) refuse("it is absolute");
9923
+ if (isAbsolute5(p)) refuse("it is absolute");
9622
9924
  if (p.split(/[\\/]/).some((segment) => segment === "..")) {
9623
9925
  refuse("it traverses upward");
9624
9926
  }
9625
9927
  const abs = resolve12(base, p);
9626
9928
  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");
9929
+ if (!abs.startsWith(resolve12(base) + sep5)) refuse("it escapes the corpus");
9628
9930
  }
9629
- function refuseSidecarWrites(changes, entries, ctx) {
9630
- const index = ctx.sidecars;
9931
+ function refuseExternalMetadataWrites(changes, entries, ctx) {
9932
+ const index = ctx.externalMetadata;
9631
9933
  if (!index) return;
9632
- const owned = (key) => index.owners.get(key);
9934
+ const owned = (key) => index.owners.get(key)?.[0]?.file;
9633
9935
  const refuse = (file, key, owner) => {
9634
9936
  throw new DocmetaError(
9635
- `"${file}": "${key}" is owned by sidecar ${owner}; edit the sidecar file instead.`
9937
+ `"${file}": "${key}" is owned by manifest ${owner}; edit the manifest instead.`
9636
9938
  );
9637
9939
  };
9638
9940
  const data = new Map(entries.map((e) => [e.label, e.extracted.data]));
@@ -9657,7 +9959,7 @@ function refuseSidecarWrites(changes, entries, ctx) {
9657
9959
  if (entry !== void 0 && entry.size > 0) {
9658
9960
  const manifest = [...entry.values()][0]?.file ?? "manifest";
9659
9961
  throw new DocmetaError(
9660
- `"${c.file}": sidecar ${manifest} names it; rename the manifest entry first.`
9962
+ `"${c.file}": manifest ${manifest} names it; rename the manifest entry first.`
9661
9963
  );
9662
9964
  }
9663
9965
  continue;
@@ -9674,7 +9976,7 @@ function refuseSidecarWrites(changes, entries, ctx) {
9674
9976
  if (entry !== void 0) {
9675
9977
  const manifest = entry.values().next().value?.file ?? "manifest";
9676
9978
  throw new DocmetaError(
9677
- `"${c.file}": "${key}" is the field sidecar ${manifest} joins on, and this document has an entry; change the manifest first.`
9979
+ `"${c.file}": "${key}" is the field manifest ${manifest} joins on, and this document has an entry; change the manifest first.`
9678
9980
  );
9679
9981
  }
9680
9982
  }
@@ -9747,12 +10049,14 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
9747
10049
  const entry = byLabel.get(label);
9748
10050
  if (!entry) throw new DocmetaError(`No loaded entry for "${label}".`);
9749
10051
  const content = await readFile9(path, "utf8");
9750
- const current = mergeSidecars(
10052
+ const members = ctx.memberships(label);
10053
+ const current = mergeExternalMetadata(
9751
10054
  label,
9752
10055
  entry.extractor.extract(content, label, {
9753
- elements: resolveElements(label, ctx.config)
10056
+ elements: resolveElements(label, ctx.config, members)
9754
10057
  }),
9755
- ctx.sidecars,
10058
+ ctx.externalMetadata,
10059
+ members,
9756
10060
  ctx.base
9757
10061
  ).extracted;
9758
10062
  if (ops.cleared) {
@@ -9788,12 +10092,13 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
9788
10092
  deletions: ops.deletions
9789
10093
  });
9790
10094
  if (ops.deletions.length > 0) {
9791
- const check = mergeSidecars(
10095
+ const check = mergeExternalMetadata(
9792
10096
  label,
9793
10097
  entry.extractor.extract(applied, label, {
9794
- elements: resolveElements(label, ctx.config)
10098
+ elements: resolveElements(label, ctx.config, members)
9795
10099
  }),
9796
- ctx.sidecars,
10100
+ ctx.externalMetadata,
10101
+ members,
9797
10102
  ctx.base
9798
10103
  ).extracted;
9799
10104
  for (const key of ops.deletions) {
@@ -10091,11 +10396,12 @@ function addUsage(total, next) {
10091
10396
  }
10092
10397
  async function runFill(opts) {
10093
10398
  const cwd = opts.cwd ?? process.cwd();
10094
- const { config, inputs, base, configDir } = await resolveRunConfig({
10399
+ const { config, inputs, base, configDir, collections, fromCollections } = await resolveRunConfig({
10095
10400
  cwd,
10096
10401
  configPath: opts.configPath,
10097
10402
  noConfig: opts.noConfig,
10098
10403
  inputs: opts.inputs,
10404
+ ...opts.collections !== void 0 ? { collections: opts.collections } : {},
10099
10405
  onConfigLoaded: opts.onConfigLoaded
10100
10406
  });
10101
10407
  const schemaOptions = schemaLoadOptions({
@@ -10110,7 +10416,7 @@ async function runFill(opts) {
10110
10416
  const usingStdin = inputs.includes(STDIN_TOKEN);
10111
10417
  if (inputs.length === 0) {
10112
10418
  throw new DocmetaError(
10113
- "No files to fill. Pass paths/globs, or add `paths:` under `meta:` in manni.config.yaml."
10419
+ "No files to fill. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml."
10114
10420
  );
10115
10421
  }
10116
10422
  const forcedExtractor = opts.as ? extractorByName(opts.as) : void 0;
@@ -10159,8 +10465,9 @@ async function runFill(opts) {
10159
10465
  const fileInputs = inputs.filter((i) => i !== STDIN_TOKEN);
10160
10466
  const allowEmpty = opts.allowEmpty ?? config?.allowEmpty;
10161
10467
  const fillExts = opts.exts ?? forcedExtractor?.extensions;
10162
- const fillExclude = [...config?.exclude ?? [], ...opts.exclude ?? []];
10163
- const { files, gitignoreSkipped } = await resolveTargetSet({
10468
+ const fillExclude = opts.exclude ?? [];
10469
+ const membersFor = (label) => memberOf(collections, configDir ?? cwd, base, label);
10470
+ const { files: walked, gitignoreSkipped } = await resolveTargetSet({
10164
10471
  inputs: fileInputs,
10165
10472
  exts: fillExts,
10166
10473
  exclude: fillExclude,
@@ -10172,6 +10479,7 @@ async function runFill(opts) {
10172
10479
  onNotice: opts.onNotice
10173
10480
  })
10174
10481
  });
10482
+ const files = fromCollections ? retainMembers(walked, membersFor) : walked;
10175
10483
  assertNonEmpty({
10176
10484
  files,
10177
10485
  inputs: fileInputs,
@@ -10221,7 +10529,7 @@ async function runFill(opts) {
10221
10529
  let turnsUsed = 0;
10222
10530
  let inFlight = 0;
10223
10531
  const turnsExhausted = () => maxTurns != null && turnsUsed + inFlight >= maxTurns;
10224
- const sidecars = await loadSidecars(config, {
10532
+ const externalMetadata = await loadExternalMetadata(collections, {
10225
10533
  configDir: configDir ?? cwd,
10226
10534
  base,
10227
10535
  offline: opts.offline ?? config?.offline ?? false
@@ -10242,14 +10550,16 @@ async function runFill(opts) {
10242
10550
  `The "${extractor.name}" format is read-only; manni meta fill cannot write metadata back to it.`
10243
10551
  );
10244
10552
  }
10245
- const elements = resolveElements(label, config);
10553
+ const members = membersFor(label);
10554
+ const elements = resolveElements(label, config, members);
10246
10555
  let extracted;
10247
10556
  let merged;
10248
10557
  try {
10249
- merged = mergeSidecars(
10558
+ merged = mergeExternalMetadata(
10250
10559
  label,
10251
10560
  extractor.extract(content, label, { elements }),
10252
- sidecars,
10561
+ externalMetadata,
10562
+ members,
10253
10563
  base
10254
10564
  );
10255
10565
  extracted = merged.extracted;
@@ -10263,6 +10573,7 @@ async function runFill(opts) {
10263
10573
  fileSchema: extracted.data[FILE_SCHEMA_KEY],
10264
10574
  cliSchemas: opts.cliSchemas,
10265
10575
  config,
10576
+ memberOf: members,
10266
10577
  // Same trust boundary as `validate`: `fill` writes metadata back into
10267
10578
  // the document, so a schema a document chose for itself decides what
10268
10579
  // gets written — if anything, a stronger reason to guard it.
@@ -10299,16 +10610,16 @@ async function runFill(opts) {
10299
10610
  existingErrors,
10300
10611
  only
10301
10612
  );
10302
- const ownedCandidates = sidecars ? candidates.flatMap((c) => {
10303
- const file = sidecars.owners.get(c.key);
10304
- return file === void 0 ? [] : [{ key: c.key, file }];
10613
+ const ownedCandidates = externalMetadata ? candidates.flatMap((c) => {
10614
+ const owner = externalMetadata.owners.get(c.key)?.[0];
10615
+ return owner === void 0 ? [] : [{ key: c.key, file: owner.file }];
10305
10616
  }) : [];
10306
10617
  const firstOwned = ownedCandidates[0];
10307
10618
  if (firstOwned) {
10308
10619
  return errorResult(
10309
10620
  label,
10310
10621
  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.`,
10622
+ `"${firstOwned.key}" is owned by manifest ${firstOwned.file}; manni meta fill cannot write a manifest key. Add it to the manifest instead.`,
10312
10623
  schemaSet
10313
10624
  );
10314
10625
  }
@@ -10838,19 +11149,19 @@ async function mapConcurrent(items, limit, fn) {
10838
11149
  }
10839
11150
 
10840
11151
  // src/meta/reporters/sarif.ts
10841
- import { isAbsolute as isAbsolute5 } from "node:path";
11152
+ import { isAbsolute as isAbsolute6 } from "node:path";
10842
11153
 
10843
11154
  // src/meta/reporters/rule-id.ts
10844
11155
  var SYNTHETIC_SCHEMA = "(parse)";
10845
11156
  var PARSE_ERROR_RULE = "manni/parse-error";
10846
11157
  var SCHEMA_ERROR_RULE = "manni/schema-error";
10847
- var SIDECAR_OWNED_RULE = "sidecar:owned/sidecar";
10848
- var SIDECAR_DUPLICATE_RULE = "sidecar:duplicate/sidecar";
11158
+ var EXTERNAL_OWNED_RULE = "external:owned/external";
11159
+ var EXTERNAL_DUPLICATE_RULE = "external:duplicate/external";
10849
11160
  var RESERVED_RULES = {
10850
11161
  [PARSE_ERROR_RULE]: "The document's metadata block could not be parsed.",
10851
11162
  [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."
11163
+ [EXTERNAL_OWNED_RULE]: "The document carries a key a manifest owns; the manifest is the only place that key may be set.",
11164
+ [EXTERNAL_DUPLICATE_RULE]: "Two documents carry the same value of a manifest's join field, so one manifest entry matched both."
10854
11165
  };
10855
11166
  function ruleIdFor(e, frame) {
10856
11167
  if (e.schema === SYNTHETIC_SCHEMA) {
@@ -10878,7 +11189,7 @@ function uriFrame(frame) {
10878
11189
  }
10879
11190
  function artifactUri(file, frame) {
10880
11191
  const uri = canonicalFilePath(file, frame);
10881
- if (uri === "" || uri.startsWith("../") || isAbsolute5(uri)) return null;
11192
+ if (uri === "" || uri.startsWith("../") || isAbsolute6(uri)) return null;
10882
11193
  return uri;
10883
11194
  }
10884
11195
  function renderSarif(results, opts = {}) {
@@ -11418,6 +11729,10 @@ export {
11418
11729
  ToolError,
11419
11730
  programName,
11420
11731
  setProgramName,
11732
+ externalMetadataUrlProblem,
11733
+ parseCollections,
11734
+ selectCollections,
11735
+ isMember,
11421
11736
  findConfigFile,
11422
11737
  readConfigFile,
11423
11738
  escapeWorkflowCommandMessage,
@@ -11443,32 +11758,32 @@ export {
11443
11758
  collectSchemaPins,
11444
11759
  rebaseConfigSchemaRefs,
11445
11760
  resolveSchemaSet,
11446
- SIDECAR_FETCH_TIMEOUT_MS,
11447
- SIDECAR_FETCH_MAX_BYTES,
11448
- sidecarUrlProblem,
11449
- fetchSidecar,
11761
+ EXTERNAL_METADATA_FETCH_TIMEOUT_MS,
11762
+ EXTERNAL_METADATA_FETCH_MAX_BYTES,
11763
+ fetchExternalMetadata,
11450
11764
  locateFrontmatter,
11451
11765
  frontmatterInnerText,
11452
11766
  extractFrontmatter,
11453
11767
  applyFrontmatter,
11454
11768
  extractorForExtension,
11455
11769
  supportedExtensions,
11456
- SIDECAR_OWNED_SCHEMA,
11457
- SIDECAR_DUPLICATE_SCHEMA,
11458
- SIDECAR_KEYWORD,
11770
+ EXTERNAL_OWNED_SCHEMA,
11771
+ EXTERNAL_DUPLICATE_SCHEMA,
11772
+ EXTERNAL_KEYWORD,
11459
11773
  PATH_JOIN,
11460
- sidecarJoin,
11461
- loadSidecars,
11462
- mergeSidecars,
11463
- sidecarPointer,
11774
+ externalMetadataJoin,
11775
+ loadExternalMetadata,
11776
+ mergeExternalMetadata,
11777
+ externalMetadataPointer,
11464
11778
  orphanEntries,
11465
11779
  orphanJoins,
11466
11780
  orphanError,
11467
11781
  parseConfig,
11468
11782
  loadConfig,
11469
11783
  resolveRunConfig,
11470
- Validator,
11471
11784
  isParamName,
11785
+ memberOf,
11786
+ Validator,
11472
11787
  rowsToFindings,
11473
11788
  runValidate,
11474
11789
  runGet,
@@ -11504,4 +11819,4 @@ export {
11504
11819
  renderQuery,
11505
11820
  renderQueryCsv
11506
11821
  };
11507
- //# sourceMappingURL=chunk-7M2YOXVG.js.map
11822
+ //# sourceMappingURL=chunk-L7HN3AWA.js.map