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