@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.
- package/dist/{chunk-BKJOQHWN.js → chunk-HECVSJOR.js} +36 -2
- package/dist/chunk-HECVSJOR.js.map +1 -0
- package/dist/{chunk-7M2YOXVG.js → chunk-L7HN3AWA.js} +1070 -755
- package/dist/chunk-L7HN3AWA.js.map +1 -0
- package/dist/cli.js +71 -11
- package/dist/cli.js.map +1 -1
- package/dist/docmeta.js +2 -2
- package/dist/index.d.ts +275 -115
- package/dist/index.js +31 -23
- package/package.json +1 -1
- package/dist/chunk-7M2YOXVG.js.map +0 -1
- package/dist/chunk-BKJOQHWN.js.map +0 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "@hawkeyexl/manni",
|
|
6
|
-
version: "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 === "
|
|
2890
|
+
if (first === "external") {
|
|
2621
2891
|
throw new Error(
|
|
2622
|
-
`Built-in id "${id}" is not publishable: the "
|
|
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
|
|
3052
|
-
|
|
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
|
|
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 (
|
|
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 &&
|
|
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/
|
|
3166
|
-
var
|
|
3167
|
-
var
|
|
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
|
|
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
|
-
`
|
|
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 ??
|
|
3197
|
-
const maxBytes = opts.maxBytes ??
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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/
|
|
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
|
|
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 (!
|
|
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/
|
|
5712
|
-
var
|
|
5713
|
-
var
|
|
5714
|
-
var
|
|
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
|
|
5722
|
-
return
|
|
5967
|
+
function externalMetadataJoin(manifest) {
|
|
5968
|
+
return manifest.join ?? PATH_JOIN;
|
|
5723
5969
|
}
|
|
5724
|
-
async function
|
|
5725
|
-
const configured =
|
|
5726
|
-
|
|
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
|
|
5979
|
+
for (const { collection, manifest } of configured) {
|
|
5732
5980
|
let text;
|
|
5733
5981
|
let file;
|
|
5734
|
-
if (classifyRef(
|
|
5735
|
-
file =
|
|
5736
|
-
text = await
|
|
5737
|
-
...
|
|
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(
|
|
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
|
|
5748
|
-
|
|
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(
|
|
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
|
-
`
|
|
6031
|
+
`Manifest ${file} could not be read: ${reason}`
|
|
5771
6032
|
);
|
|
5772
6033
|
}
|
|
5773
6034
|
}
|
|
5774
|
-
function parseManifest(
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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(
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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, {
|
|
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
|
|
6113
|
+
function mergeExternalMetadata(label, extracted, index, memberOf2, base) {
|
|
5847
6114
|
const none = () => void 0;
|
|
5848
|
-
if (!index
|
|
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
|
|
5852
|
-
|
|
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().
|
|
5867
|
-
|
|
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
|
|
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
|
|
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
|
-
`
|
|
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 {
|
|
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 = ["
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
}
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
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}:
|
|
6554
|
+
`${source}: "${key}" is no longer a meta key. ${remedy}`
|
|
6239
6555
|
);
|
|
6240
6556
|
}
|
|
6241
|
-
|
|
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}:
|
|
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
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6336
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
6576
|
-
|
|
6577
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
7104
|
-
|
|
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
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
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
|
-
|
|
7167
|
-
|
|
7168
|
-
|
|
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
|
-
|
|
7423
|
+
entries[file] = value;
|
|
7171
7424
|
}
|
|
7172
|
-
return
|
|
7425
|
+
return { version: BASELINE_VERSION, generatedWith, entries };
|
|
7173
7426
|
}
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
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
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
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
|
|
7200
|
-
|
|
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
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
7207
|
-
|
|
7208
|
-
if (
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7219
|
-
|
|
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
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
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
|
|
7247
|
-
|
|
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
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
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 {
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 (!
|
|
7490
|
-
const
|
|
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 =
|
|
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:
|
|
7570
|
-
keyword:
|
|
7836
|
+
schema: EXTERNAL_OWNED_SCHEMA,
|
|
7837
|
+
keyword: EXTERNAL_KEYWORD,
|
|
7571
7838
|
subject: key,
|
|
7572
|
-
instancePath:
|
|
7573
|
-
message: `"${key}" is owned by
|
|
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 (
|
|
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:
|
|
7611
|
-
keyword:
|
|
7877
|
+
schema: EXTERNAL_DUPLICATE_SCHEMA,
|
|
7878
|
+
keyword: EXTERNAL_KEYWORD,
|
|
7612
7879
|
subject: value,
|
|
7613
|
-
instancePath:
|
|
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 (!
|
|
7622
|
-
const
|
|
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?.(
|
|
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
|
-
//
|
|
7632
|
-
//
|
|
7633
|
-
//
|
|
7634
|
-
//
|
|
7635
|
-
|
|
7636
|
-
...
|
|
7637
|
-
|
|
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
|
|
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 =
|
|
7745
|
-
const
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
8140
|
-
const
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 {
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
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.
|
|
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.
|
|
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,
|
|
8890
|
+
function namesConfiguredCollection(missing, collections) {
|
|
8596
8891
|
const candidates = [missing, missing.replace(/^(main|temp)\./i, "")];
|
|
8597
|
-
return collectionNames(
|
|
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("..") && !
|
|
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
|
-
|
|
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" && !
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
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) +
|
|
9929
|
+
if (!abs.startsWith(resolve12(base) + sep5)) refuse("it escapes the corpus");
|
|
9628
9930
|
}
|
|
9629
|
-
function
|
|
9630
|
-
const index = ctx.
|
|
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
|
|
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}":
|
|
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
|
|
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
|
|
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.
|
|
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 =
|
|
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.
|
|
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
|
|
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 =
|
|
10163
|
-
const
|
|
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
|
|
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
|
|
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 =
|
|
10558
|
+
merged = mergeExternalMetadata(
|
|
10250
10559
|
label,
|
|
10251
10560
|
extractor.extract(content, label, { elements }),
|
|
10252
|
-
|
|
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 =
|
|
10303
|
-
const
|
|
10304
|
-
return
|
|
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
|
|
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
|
|
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
|
|
10848
|
-
var
|
|
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
|
-
[
|
|
10853
|
-
[
|
|
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("../") ||
|
|
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
|
-
|
|
11447
|
-
|
|
11448
|
-
|
|
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
|
-
|
|
11457
|
-
|
|
11458
|
-
|
|
11770
|
+
EXTERNAL_OWNED_SCHEMA,
|
|
11771
|
+
EXTERNAL_DUPLICATE_SCHEMA,
|
|
11772
|
+
EXTERNAL_KEYWORD,
|
|
11459
11773
|
PATH_JOIN,
|
|
11460
|
-
|
|
11461
|
-
|
|
11462
|
-
|
|
11463
|
-
|
|
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-
|
|
11822
|
+
//# sourceMappingURL=chunk-L7HN3AWA.js.map
|