@hawkeyexl/manni 1.0.0 → 1.1.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-L7HN3AWA.js → chunk-7SE7XDCL.js} +3034 -421
- package/dist/chunk-7SE7XDCL.js.map +1 -0
- package/dist/{chunk-HECVSJOR.js → chunk-WP2BP3ZX.js} +105 -6
- package/dist/chunk-WP2BP3ZX.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/docmeta.js +2 -2
- package/dist/index.d.ts +719 -7
- package/dist/index.js +77 -1
- package/package.json +1 -1
- package/dist/chunk-HECVSJOR.js.map +0 -1
- package/dist/chunk-L7HN3AWA.js.map +0 -1
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "@hawkeyexl/manni",
|
|
6
|
-
version: "1.
|
|
6
|
+
version: "1.1.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: {
|
|
@@ -208,6 +208,11 @@ function parseCollections(raw, source, toError) {
|
|
|
208
208
|
`${source}: ${where}.name "${name2}" collides with the docs table every query reads. Pick another name, such as "site" or "pages".`
|
|
209
209
|
);
|
|
210
210
|
}
|
|
211
|
+
if (folded === "derived" || folded === "_derived_rows" || folded === "resolved") {
|
|
212
|
+
throw toError(
|
|
213
|
+
`${source}: ${where}.name "${name2}" collides with the derived table a query builds beside docs (the derived view, its _derived_rows table, and the resolved view over both). Pick another name.`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
211
216
|
if (folded.startsWith("sqlite_")) {
|
|
212
217
|
throw toError(
|
|
213
218
|
`${source}: ${where}.name "${name2}" starts with "sqlite_", which SQLite reserves for its own objects. Pick another name.`
|
|
@@ -396,6 +401,19 @@ var ToolError = class extends Error {
|
|
|
396
401
|
this.name = "ToolError";
|
|
397
402
|
}
|
|
398
403
|
};
|
|
404
|
+
function errorMessage(err) {
|
|
405
|
+
if (err instanceof Error) return err.message;
|
|
406
|
+
if (typeof err === "string") return err;
|
|
407
|
+
if (typeof err === "object" && err !== null && "message" in err && typeof err.message === "string") {
|
|
408
|
+
return err.message;
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
const json = JSON.stringify(err);
|
|
412
|
+
if (json !== void 0) return json;
|
|
413
|
+
} catch {
|
|
414
|
+
}
|
|
415
|
+
return String(err);
|
|
416
|
+
}
|
|
399
417
|
|
|
400
418
|
// src/meta/types.ts
|
|
401
419
|
var DocmetaError = class extends ToolError {
|
|
@@ -424,8 +442,8 @@ var RENAME_ATTEMPTS = 3;
|
|
|
424
442
|
function errorCode(err) {
|
|
425
443
|
return typeof err === "object" && err !== null && "code" in err ? String(err.code) : void 0;
|
|
426
444
|
}
|
|
427
|
-
var wait = (ms) => new Promise((
|
|
428
|
-
setTimeout(
|
|
445
|
+
var wait = (ms) => new Promise((resolve17) => {
|
|
446
|
+
setTimeout(resolve17, ms);
|
|
429
447
|
});
|
|
430
448
|
async function writeFileAtomic(path, contents) {
|
|
431
449
|
const tmp = join(
|
|
@@ -619,11 +637,11 @@ import { resolve as resolvePath } from "node:path";
|
|
|
619
637
|
import { Buffer as Buffer2 } from "node:buffer";
|
|
620
638
|
|
|
621
639
|
// src/meta/core/json-text.ts
|
|
622
|
-
function stripBom(
|
|
623
|
-
return
|
|
640
|
+
function stripBom(text2) {
|
|
641
|
+
return text2.charCodeAt(0) === 65279 ? text2.slice(1) : text2;
|
|
624
642
|
}
|
|
625
|
-
function detectJsonIndent(
|
|
626
|
-
return /^\{\s*\n([ \t]+)/.exec(
|
|
643
|
+
function detectJsonIndent(text2) {
|
|
644
|
+
return /^\{\s*\n([ \t]+)/.exec(text2)?.[1] ?? " ";
|
|
627
645
|
}
|
|
628
646
|
function toJsonText(value) {
|
|
629
647
|
return JSON.stringify(value);
|
|
@@ -2892,6 +2910,11 @@ function assertPublishableBuiltinId(id) {
|
|
|
2892
2910
|
`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
2911
|
);
|
|
2894
2912
|
}
|
|
2913
|
+
if (first === "derived") {
|
|
2914
|
+
throw new Error(
|
|
2915
|
+
`Built-in id "${id}" is not publishable: the "derived" first segment is reserved for derived finding identities (derived:stale).`
|
|
2916
|
+
);
|
|
2917
|
+
}
|
|
2895
2918
|
}
|
|
2896
2919
|
for (const id of BUILTINS.keys()) assertPublishableBuiltinId(id);
|
|
2897
2920
|
function listBuiltins() {
|
|
@@ -3019,8 +3042,8 @@ var SCHEMA_KEYS = [
|
|
|
3019
3042
|
"multipleOf"
|
|
3020
3043
|
];
|
|
3021
3044
|
var RETRY_DELAY_MS = 500;
|
|
3022
|
-
var wait2 = (ms) => new Promise((
|
|
3023
|
-
setTimeout(
|
|
3045
|
+
var wait2 = (ms) => new Promise((resolve17) => {
|
|
3046
|
+
setTimeout(resolve17, ms);
|
|
3024
3047
|
});
|
|
3025
3048
|
var RetryableFetchError = class extends DocmetaError {
|
|
3026
3049
|
};
|
|
@@ -3029,12 +3052,12 @@ function isAbort(err) {
|
|
|
3029
3052
|
return name2 === "TimeoutError" || name2 === "AbortError";
|
|
3030
3053
|
}
|
|
3031
3054
|
var JSON_PARSE_EXCERPT = /^Unexpected token '[\s\S]', (?:\.\.\.)?"([\s\S]*)"(?:\.\.\.)? is not valid JSON$/;
|
|
3032
|
-
function lineColumn(
|
|
3055
|
+
function lineColumn(text2, index) {
|
|
3033
3056
|
let line = 1;
|
|
3034
3057
|
let column = 1;
|
|
3035
|
-
const stop = Math.min(index,
|
|
3058
|
+
const stop = Math.min(index, text2.length);
|
|
3036
3059
|
for (let i = 0; i < stop; i++) {
|
|
3037
|
-
if (
|
|
3060
|
+
if (text2.charCodeAt(i) === 10) {
|
|
3038
3061
|
line += 1;
|
|
3039
3062
|
column = 1;
|
|
3040
3063
|
} else {
|
|
@@ -3043,12 +3066,12 @@ function lineColumn(text, index) {
|
|
|
3043
3066
|
}
|
|
3044
3067
|
return `line ${line} column ${column}`;
|
|
3045
3068
|
}
|
|
3046
|
-
function withoutFileExcerpt(message,
|
|
3069
|
+
function withoutFileExcerpt(message, text2) {
|
|
3047
3070
|
const match = JSON_PARSE_EXCERPT.exec(message);
|
|
3048
3071
|
if (!match) return message;
|
|
3049
3072
|
const quoted = match[1];
|
|
3050
|
-
const at = quoted === void 0 || quoted === "" ? -1 :
|
|
3051
|
-
return at < 0 ? "unexpected token" : `unexpected token at ${lineColumn(
|
|
3073
|
+
const at = quoted === void 0 || quoted === "" ? -1 : text2.indexOf(quoted);
|
|
3074
|
+
return at < 0 ? "unexpected token" : `unexpected token at ${lineColumn(text2, at)}`;
|
|
3052
3075
|
}
|
|
3053
3076
|
function excerpt(raw, limit = 200) {
|
|
3054
3077
|
const flat = raw.replace(/\s+/g, " ").trim();
|
|
@@ -3113,7 +3136,7 @@ async function requestSchema(ref, timeoutMs, timedOut) {
|
|
|
3113
3136
|
} catch (err) {
|
|
3114
3137
|
if (isAbort(err)) throw timedOut;
|
|
3115
3138
|
throw new RetryableFetchError(
|
|
3116
|
-
`Failed to fetch schema "${ref}": ${err
|
|
3139
|
+
`Failed to fetch schema "${ref}": ${errorMessage(err)}`
|
|
3117
3140
|
);
|
|
3118
3141
|
}
|
|
3119
3142
|
if (!res.ok) {
|
|
@@ -3145,7 +3168,7 @@ async function fetchSchemaBytes(ref, options = {}) {
|
|
|
3145
3168
|
if (err instanceof DocmetaError) throw err;
|
|
3146
3169
|
if (isAbort(err)) throw timedOut;
|
|
3147
3170
|
throw new DocmetaError(
|
|
3148
|
-
`Failed to fetch schema "${ref}": ${err
|
|
3171
|
+
`Failed to fetch schema "${ref}": ${errorMessage(err)}`
|
|
3149
3172
|
);
|
|
3150
3173
|
}
|
|
3151
3174
|
const raw = stripBom(bytes.toString("utf8"));
|
|
@@ -3154,7 +3177,7 @@ async function fetchSchemaBytes(ref, options = {}) {
|
|
|
3154
3177
|
parsed2 = JSON.parse(raw);
|
|
3155
3178
|
} catch (err) {
|
|
3156
3179
|
throw new DocmetaError(
|
|
3157
|
-
`Schema "${ref}" did not return valid JSON: ${err
|
|
3180
|
+
`Schema "${ref}" did not return valid JSON: ${errorMessage(err)}`
|
|
3158
3181
|
);
|
|
3159
3182
|
}
|
|
3160
3183
|
return { bytes, schema: assertFetchedSchema(ref, parsed2, raw) };
|
|
@@ -3262,12 +3285,12 @@ async function loadSchema(ref, options = {}) {
|
|
|
3262
3285
|
if (pin?.integrity !== void 0) {
|
|
3263
3286
|
assertIntegrity(ref, bytes, pin, pin.integrity);
|
|
3264
3287
|
}
|
|
3265
|
-
const
|
|
3288
|
+
const text2 = stripBom(bytes.toString("utf8"));
|
|
3266
3289
|
try {
|
|
3267
|
-
return JSON.parse(
|
|
3290
|
+
return JSON.parse(text2);
|
|
3268
3291
|
} catch (err) {
|
|
3269
3292
|
throw new DocmetaError(
|
|
3270
|
-
`Schema file "${ref}" is not valid JSON: ${withoutFileExcerpt(err
|
|
3293
|
+
`Schema file "${ref}" is not valid JSON: ${withoutFileExcerpt(errorMessage(err), text2)}`
|
|
3271
3294
|
);
|
|
3272
3295
|
}
|
|
3273
3296
|
}
|
|
@@ -3990,9 +4013,9 @@ function mergeToml(inner, patch, deletions = []) {
|
|
|
3990
4013
|
if (insertAt >= lines.length) out.push(...appended);
|
|
3991
4014
|
return out.join("\n");
|
|
3992
4015
|
}
|
|
3993
|
-
function parseJsonBlockText(
|
|
3994
|
-
if (
|
|
3995
|
-
const parsed2 = JSON.parse(
|
|
4016
|
+
function parseJsonBlockText(text2) {
|
|
4017
|
+
if (text2.trim() === "") return {};
|
|
4018
|
+
const parsed2 = JSON.parse(text2);
|
|
3996
4019
|
if (parsed2 == null || typeof parsed2 !== "object" || Array.isArray(parsed2)) {
|
|
3997
4020
|
throw new DocmetaError(
|
|
3998
4021
|
"Cannot rewrite front matter: the root must be an object."
|
|
@@ -4000,14 +4023,14 @@ function parseJsonBlockText(text) {
|
|
|
4000
4023
|
}
|
|
4001
4024
|
return parsed2;
|
|
4002
4025
|
}
|
|
4003
|
-
function parseBlock(flavor,
|
|
4026
|
+
function parseBlock(flavor, text2) {
|
|
4004
4027
|
switch (flavor) {
|
|
4005
4028
|
case "json":
|
|
4006
|
-
return parseJsonBlockText(
|
|
4029
|
+
return parseJsonBlockText(text2);
|
|
4007
4030
|
case "toml":
|
|
4008
|
-
return
|
|
4031
|
+
return text2.trim() === "" ? {} : parseToml2(text2);
|
|
4009
4032
|
case "yaml": {
|
|
4010
|
-
const doc = parseDocument2(
|
|
4033
|
+
const doc = parseDocument2(text2);
|
|
4011
4034
|
if (doc.errors.length > 0) {
|
|
4012
4035
|
throw new DocmetaError(
|
|
4013
4036
|
`Cannot rewrite invalid YAML front matter: ${doc.errors[0]?.message ?? "parse error"}`
|
|
@@ -4147,10 +4170,10 @@ function parseTitle(lines) {
|
|
|
4147
4170
|
if (FIELD.test(first)) return { next: i };
|
|
4148
4171
|
const over = adornment(first);
|
|
4149
4172
|
if (over) {
|
|
4150
|
-
const
|
|
4173
|
+
const text2 = lines[i + 1] ?? "";
|
|
4151
4174
|
const under2 = adornment(lines[i + 2] ?? "");
|
|
4152
|
-
if (
|
|
4153
|
-
return { title:
|
|
4175
|
+
if (text2.trim() !== "" && !FIELD.test(text2) && under2 != null && under2.char === over.char && under2.length === over.length && over.length >= text2.trim().length) {
|
|
4176
|
+
return { title: text2.trim(), line: i + 2, next: i + 3 };
|
|
4154
4177
|
}
|
|
4155
4178
|
return { next: i };
|
|
4156
4179
|
}
|
|
@@ -4516,13 +4539,13 @@ function isMetadataAttribute(name2) {
|
|
|
4516
4539
|
return name2 !== "xmlns" && !name2.startsWith("xmlns:");
|
|
4517
4540
|
}
|
|
4518
4541
|
function elementText(el, allowEmpty = false) {
|
|
4519
|
-
let
|
|
4542
|
+
let text2 = "";
|
|
4520
4543
|
for (let n = el.firstChild; n; n = n.nextSibling) {
|
|
4521
4544
|
if (n.nodeType === 1) return void 0;
|
|
4522
|
-
if (n.nodeType === 3 || n.nodeType === 4)
|
|
4545
|
+
if (n.nodeType === 3 || n.nodeType === 4) text2 += n.nodeValue ?? "";
|
|
4523
4546
|
}
|
|
4524
|
-
if (!allowEmpty &&
|
|
4525
|
-
return
|
|
4547
|
+
if (!allowEmpty && text2.trim() === "") return void 0;
|
|
4548
|
+
return text2.trim();
|
|
4526
4549
|
}
|
|
4527
4550
|
function ditaLiftedElements(container) {
|
|
4528
4551
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -4559,15 +4582,15 @@ function own(table, key) {
|
|
|
4559
4582
|
function liftableChildren(root) {
|
|
4560
4583
|
const out = /* @__PURE__ */ new Map();
|
|
4561
4584
|
for (const child of childElements(root)) {
|
|
4562
|
-
const
|
|
4563
|
-
if (
|
|
4585
|
+
const text2 = elementText(child);
|
|
4586
|
+
if (text2 === void 0) continue;
|
|
4564
4587
|
const key = liftKey(root.nodeName, child.nodeName);
|
|
4565
4588
|
const group = out.get(key);
|
|
4566
4589
|
if (group) {
|
|
4567
4590
|
group.els.push(child);
|
|
4568
|
-
group.values.push(typeValue3(
|
|
4591
|
+
group.values.push(typeValue3(text2));
|
|
4569
4592
|
} else {
|
|
4570
|
-
out.set(key, { els: [child], values: [typeValue3(
|
|
4593
|
+
out.set(key, { els: [child], values: [typeValue3(text2)] });
|
|
4571
4594
|
}
|
|
4572
4595
|
}
|
|
4573
4596
|
return out;
|
|
@@ -4917,7 +4940,7 @@ function ownLift(container, element) {
|
|
|
4917
4940
|
const lifts = DITA_LIFTS[container];
|
|
4918
4941
|
return lifts && Object.prototype.hasOwnProperty.call(lifts, element) ? lifts[element] : void 0;
|
|
4919
4942
|
}
|
|
4920
|
-
function newElementEdits(content, starts, root, shape,
|
|
4943
|
+
function newElementEdits(content, starts, root, shape, items2, emit, escape) {
|
|
4921
4944
|
const eol = content.includes(CRLF) ? CRLF : LF;
|
|
4922
4945
|
const liftRootEl = liftRoot(root, shape);
|
|
4923
4946
|
const tagsFor = (item) => {
|
|
@@ -4931,7 +4954,7 @@ function newElementEdits(content, starts, root, shape, items, emit, escape) {
|
|
|
4931
4954
|
});
|
|
4932
4955
|
};
|
|
4933
4956
|
const byContainer = /* @__PURE__ */ new Map();
|
|
4934
|
-
for (const item of
|
|
4957
|
+
for (const item of items2) {
|
|
4935
4958
|
const group = byContainer.get(item.container);
|
|
4936
4959
|
if (group) group.push(item);
|
|
4937
4960
|
else byContainer.set(item.container, [item]);
|
|
@@ -5179,7 +5202,7 @@ function applyXml(original, patch, options = {}) {
|
|
|
5179
5202
|
before = readXml(original, options.filePath, { elements: options.elements });
|
|
5180
5203
|
} catch (err) {
|
|
5181
5204
|
throw new DocmetaError(
|
|
5182
|
-
`Refusing to write metadata: ${err
|
|
5205
|
+
`Refusing to write metadata: ${errorMessage(err)}`
|
|
5183
5206
|
);
|
|
5184
5207
|
}
|
|
5185
5208
|
const content = before.body;
|
|
@@ -5278,13 +5301,13 @@ function assertNoElementCollision(key, root) {
|
|
|
5278
5301
|
);
|
|
5279
5302
|
}
|
|
5280
5303
|
function emitScalar(key, value) {
|
|
5281
|
-
const
|
|
5282
|
-
if (
|
|
5304
|
+
const text2 = stringifyYaml(value).replace(/\n$/, "");
|
|
5305
|
+
if (text2.includes("\n")) {
|
|
5283
5306
|
throw new DocmetaError(
|
|
5284
5307
|
`Refusing to write "${key}": the value needs more than one line, which an XML attribute cannot hold. Set it manually.`
|
|
5285
5308
|
);
|
|
5286
5309
|
}
|
|
5287
|
-
return
|
|
5310
|
+
return text2;
|
|
5288
5311
|
}
|
|
5289
5312
|
function escapeAttr(value, quote) {
|
|
5290
5313
|
const escaped = value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -5314,7 +5337,7 @@ function verify2(next, before, patch, options) {
|
|
|
5314
5337
|
}).data;
|
|
5315
5338
|
} catch (err) {
|
|
5316
5339
|
throw new DocmetaError(
|
|
5317
|
-
`Refusing to write XML metadata: the rewritten document did not parse (${err
|
|
5340
|
+
`Refusing to write XML metadata: the rewritten document did not parse (${errorMessage(err)}).`
|
|
5318
5341
|
);
|
|
5319
5342
|
}
|
|
5320
5343
|
if (!deepEqual(actual, expected)) {
|
|
@@ -5355,13 +5378,13 @@ var HEAD_NOT_METADATA = /* @__PURE__ */ new Set(["script", "style"]);
|
|
|
5355
5378
|
var HEAD_SINGLETONS = /* @__PURE__ */ new Set(["title", "base"]);
|
|
5356
5379
|
var elementName = (el) => el ? el.tagName.toLowerCase() : "";
|
|
5357
5380
|
function headText(el, allowEmpty = false) {
|
|
5358
|
-
let
|
|
5381
|
+
let text2 = "";
|
|
5359
5382
|
for (const node of el.childNodes) {
|
|
5360
5383
|
if (defaultTreeAdapter.isElementNode(node)) return void 0;
|
|
5361
|
-
if (defaultTreeAdapter.isTextNode(node))
|
|
5384
|
+
if (defaultTreeAdapter.isTextNode(node)) text2 += node.value;
|
|
5362
5385
|
}
|
|
5363
|
-
if (!allowEmpty &&
|
|
5364
|
-
return
|
|
5386
|
+
if (!allowEmpty && text2.trim() === "") return void 0;
|
|
5387
|
+
return text2.trim();
|
|
5365
5388
|
}
|
|
5366
5389
|
function liftableHeadChildren(head) {
|
|
5367
5390
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -5369,15 +5392,15 @@ function liftableHeadChildren(head) {
|
|
|
5369
5392
|
if (!defaultTreeAdapter.isElementNode(node)) continue;
|
|
5370
5393
|
const name2 = node.tagName.toLowerCase();
|
|
5371
5394
|
if (HEAD_NOT_METADATA.has(name2)) continue;
|
|
5372
|
-
const
|
|
5373
|
-
if (
|
|
5395
|
+
const text2 = headText(node);
|
|
5396
|
+
if (text2 === void 0) continue;
|
|
5374
5397
|
const key = liftKey("head", name2);
|
|
5375
5398
|
const group = out.get(key);
|
|
5376
5399
|
if (group) {
|
|
5377
5400
|
group.els.push(node);
|
|
5378
|
-
group.texts.push(
|
|
5401
|
+
group.texts.push(text2);
|
|
5379
5402
|
} else {
|
|
5380
|
-
out.set(key, { els: [node], texts: [
|
|
5403
|
+
out.set(key, { els: [node], texts: [text2] });
|
|
5381
5404
|
}
|
|
5382
5405
|
}
|
|
5383
5406
|
return out;
|
|
@@ -5426,8 +5449,8 @@ function readHtml(source, options) {
|
|
|
5426
5449
|
} else if (node.tagName === "title") {
|
|
5427
5450
|
if (data.title === void 0) {
|
|
5428
5451
|
const first = node.childNodes[0];
|
|
5429
|
-
const
|
|
5430
|
-
setKey("title",
|
|
5452
|
+
const text2 = first && defaultTreeAdapter.isTextNode(first) ? first.value : "";
|
|
5453
|
+
setKey("title", text2, { kind: "title", el: node }, line, location?.startCol);
|
|
5431
5454
|
}
|
|
5432
5455
|
} else if (node.tagName === "meta") {
|
|
5433
5456
|
const key = attrValue(node, "name") ?? attrValue(node, "property");
|
|
@@ -5571,8 +5594,8 @@ function elementTextEdit2(el, key, emitted) {
|
|
|
5571
5594
|
}
|
|
5572
5595
|
function insertEdit2(content, head, at, tags) {
|
|
5573
5596
|
const { eol, indent } = headStyle(content, head, at);
|
|
5574
|
-
const
|
|
5575
|
-
return { start: at, end: at, text };
|
|
5597
|
+
const text2 = tags.map((t) => `${eol}${indent}${t}`).join("");
|
|
5598
|
+
return { start: at, end: at, text: text2 };
|
|
5576
5599
|
}
|
|
5577
5600
|
function headStyle(content, head, at) {
|
|
5578
5601
|
for (const child of head.childNodes) {
|
|
@@ -5608,13 +5631,13 @@ function attrValueSpan2(content, start, end) {
|
|
|
5608
5631
|
return { start: i, end, quote: DQ3, wrap: true };
|
|
5609
5632
|
}
|
|
5610
5633
|
function emitScalar2(key, value) {
|
|
5611
|
-
const
|
|
5612
|
-
if (
|
|
5634
|
+
const text2 = stringifyYaml2(value).replace(/\n$/, "");
|
|
5635
|
+
if (text2.includes("\n")) {
|
|
5613
5636
|
throw new DocmetaError(
|
|
5614
5637
|
`Refusing to write "${key}": the value needs more than one line, which an HTML attribute cannot hold. Set it manually.`
|
|
5615
5638
|
);
|
|
5616
5639
|
}
|
|
5617
|
-
return
|
|
5640
|
+
return text2;
|
|
5618
5641
|
}
|
|
5619
5642
|
function escapeAttr2(value, quote) {
|
|
5620
5643
|
const escaped = value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -5977,11 +6000,11 @@ async function loadExternalMetadata(collections, opts) {
|
|
|
5977
6000
|
const byField = /* @__PURE__ */ new Map();
|
|
5978
6001
|
const entries = [];
|
|
5979
6002
|
for (const { collection, manifest } of configured) {
|
|
5980
|
-
let
|
|
6003
|
+
let text2;
|
|
5981
6004
|
let file;
|
|
5982
6005
|
if (classifyRef(manifest.file).kind === "url") {
|
|
5983
6006
|
file = manifest.file;
|
|
5984
|
-
|
|
6007
|
+
text2 = await fetchExternalMetadata(manifest.file, {
|
|
5985
6008
|
...manifest.tokenEnv !== void 0 ? { tokenEnv: manifest.tokenEnv } : {},
|
|
5986
6009
|
...opts.offline !== void 0 ? { offline: opts.offline } : {},
|
|
5987
6010
|
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
|
|
@@ -5990,32 +6013,32 @@ async function loadExternalMetadata(collections, opts) {
|
|
|
5990
6013
|
} else {
|
|
5991
6014
|
const abs = isAbsolute2(manifest.file) ? manifest.file : resolve5(opts.configDir, manifest.file);
|
|
5992
6015
|
file = reportedPath(abs, opts.base);
|
|
5993
|
-
|
|
6016
|
+
text2 = await readManifest(abs, file);
|
|
5994
6017
|
}
|
|
5995
6018
|
for (const key of manifest.keys) {
|
|
5996
6019
|
const list = owners.get(key);
|
|
5997
6020
|
if (list) list.push({ collection, file });
|
|
5998
6021
|
else owners.set(key, [{ collection, file }]);
|
|
5999
6022
|
}
|
|
6000
|
-
const
|
|
6023
|
+
const join8 = externalMetadataJoin(manifest);
|
|
6001
6024
|
let target;
|
|
6002
|
-
if (
|
|
6025
|
+
if (join8 === PATH_JOIN) {
|
|
6003
6026
|
target = byPath;
|
|
6004
6027
|
} else {
|
|
6005
|
-
const existing = byField.get(
|
|
6028
|
+
const existing = byField.get(join8);
|
|
6006
6029
|
if (existing) target = existing;
|
|
6007
6030
|
else {
|
|
6008
6031
|
target = /* @__PURE__ */ new Map();
|
|
6009
|
-
byField.set(
|
|
6032
|
+
byField.set(join8, target);
|
|
6010
6033
|
}
|
|
6011
6034
|
}
|
|
6012
6035
|
parseManifest(
|
|
6013
6036
|
manifest,
|
|
6014
6037
|
collection,
|
|
6015
|
-
|
|
6038
|
+
text2,
|
|
6016
6039
|
file,
|
|
6017
6040
|
opts.configDir,
|
|
6018
|
-
|
|
6041
|
+
join8,
|
|
6019
6042
|
target,
|
|
6020
6043
|
entries
|
|
6021
6044
|
);
|
|
@@ -6032,9 +6055,9 @@ async function readManifest(abs, file) {
|
|
|
6032
6055
|
);
|
|
6033
6056
|
}
|
|
6034
6057
|
}
|
|
6035
|
-
function parseManifest(manifest, collection,
|
|
6058
|
+
function parseManifest(manifest, collection, text2, file, configDir, join8, target, entries) {
|
|
6036
6059
|
const lc = new LineCounter2();
|
|
6037
|
-
const doc = parseDocument3(
|
|
6060
|
+
const doc = parseDocument3(text2, { lineCounter: lc, uniqueKeys: false });
|
|
6038
6061
|
const problem = doc.errors[0];
|
|
6039
6062
|
if (problem) {
|
|
6040
6063
|
throw new DocmetaError(
|
|
@@ -6045,7 +6068,7 @@ function parseManifest(manifest, collection, text, file, configDir, join7, targe
|
|
|
6045
6068
|
if (root === null || isScalar2(root) && root.value === null) return;
|
|
6046
6069
|
if (!isMap3(root)) {
|
|
6047
6070
|
throw new DocmetaError(
|
|
6048
|
-
`Manifest ${file}: the manifest must be a mapping from document ${
|
|
6071
|
+
`Manifest ${file}: the manifest must be a mapping from document ${join8 === PATH_JOIN ? "path" : `"${join8}"`} to owned keys.`
|
|
6049
6072
|
);
|
|
6050
6073
|
}
|
|
6051
6074
|
const owned = new Set(manifest.keys);
|
|
@@ -6070,7 +6093,7 @@ function parseManifest(manifest, collection, text, file, configDir, join7, targe
|
|
|
6070
6093
|
`Manifest ${where}: "${spelled}" must be a mapping of owned keys to values.`
|
|
6071
6094
|
);
|
|
6072
6095
|
}
|
|
6073
|
-
const indexKey =
|
|
6096
|
+
const indexKey = join8 === PATH_JOIN ? resolve5(configDir, spelled) : spelled;
|
|
6074
6097
|
const values = target.get(indexKey) ?? /* @__PURE__ */ new Map();
|
|
6075
6098
|
for (const kv of pair.value.items) {
|
|
6076
6099
|
const key = isScalar2(kv.key) ? String(kv.key.value) : String(kv.key);
|
|
@@ -6096,8 +6119,8 @@ function parseManifest(manifest, collection, text, file, configDir, join7, targe
|
|
|
6096
6119
|
target.set(indexKey, values);
|
|
6097
6120
|
entries.push({
|
|
6098
6121
|
collection,
|
|
6099
|
-
join:
|
|
6100
|
-
...
|
|
6122
|
+
join: join8,
|
|
6123
|
+
...join8 === PATH_JOIN ? { abs: indexKey } : {},
|
|
6101
6124
|
spelled,
|
|
6102
6125
|
file,
|
|
6103
6126
|
...entryLine === void 0 ? {} : { line: entryLine }
|
|
@@ -6212,6 +6235,97 @@ function orphanError(orphans) {
|
|
|
6212
6235
|
);
|
|
6213
6236
|
}
|
|
6214
6237
|
|
|
6238
|
+
// src/meta/core/derive/types.ts
|
|
6239
|
+
var DERIVABLE_FIELDS = [
|
|
6240
|
+
"created",
|
|
6241
|
+
"last-updated",
|
|
6242
|
+
"authors",
|
|
6243
|
+
"owner",
|
|
6244
|
+
"reviewed-by",
|
|
6245
|
+
"last-reviewed"
|
|
6246
|
+
];
|
|
6247
|
+
function isBuiltinField(x) {
|
|
6248
|
+
return DERIVABLE_FIELDS.includes(x);
|
|
6249
|
+
}
|
|
6250
|
+
var DERIVE_SOURCES = ["git", "codeowners", "github", "gitlab", "command"];
|
|
6251
|
+
function isDeriveSource(x) {
|
|
6252
|
+
return DERIVE_SOURCES.includes(x);
|
|
6253
|
+
}
|
|
6254
|
+
function derivableFields(commands) {
|
|
6255
|
+
return [...DERIVABLE_FIELDS, ...Object.keys(commands ?? {}).sort()];
|
|
6256
|
+
}
|
|
6257
|
+
var DERIVED_STALE_SCHEMA = "derived:stale";
|
|
6258
|
+
var DERIVED_KEYWORD = "derived";
|
|
6259
|
+
function compareDerived(field, asserted, derived) {
|
|
6260
|
+
const base = {
|
|
6261
|
+
field,
|
|
6262
|
+
...asserted !== void 0 ? { asserted } : {}
|
|
6263
|
+
};
|
|
6264
|
+
if (derived == null) {
|
|
6265
|
+
return { ...base, derived: null, status: "unknown", written: false };
|
|
6266
|
+
}
|
|
6267
|
+
const status = asserted === void 0 ? "unset" : deepEqual2(asserted, derived.value) ? "current" : "stale";
|
|
6268
|
+
return {
|
|
6269
|
+
...base,
|
|
6270
|
+
derived: derived.value,
|
|
6271
|
+
source: derived.source,
|
|
6272
|
+
evidence: derived.evidence,
|
|
6273
|
+
status,
|
|
6274
|
+
written: false
|
|
6275
|
+
};
|
|
6276
|
+
}
|
|
6277
|
+
function staleFindings(fields, lineFor) {
|
|
6278
|
+
const findings = [];
|
|
6279
|
+
for (const f of fields) {
|
|
6280
|
+
if (f.status !== "stale" && f.status !== "unset") continue;
|
|
6281
|
+
const instancePath = `/${escapePointerSegment(f.field)}`;
|
|
6282
|
+
const line = lineFor(instancePath);
|
|
6283
|
+
const says = `${f.source ?? "a source"} says ${fmt(f.derived)}`;
|
|
6284
|
+
const trail = f.evidence === void 0 ? "" : ` (${f.evidence})`;
|
|
6285
|
+
const head = f.status === "stale" ? `${f.field} says ${fmt(f.asserted)}` : `${f.field} is not set`;
|
|
6286
|
+
findings.push({
|
|
6287
|
+
schema: DERIVED_STALE_SCHEMA,
|
|
6288
|
+
keyword: DERIVED_KEYWORD,
|
|
6289
|
+
subject: f.field,
|
|
6290
|
+
instancePath,
|
|
6291
|
+
message: `${head}; ${says}${trail} \u2014 run manni meta derive`,
|
|
6292
|
+
...line !== void 0 ? { line } : {}
|
|
6293
|
+
});
|
|
6294
|
+
}
|
|
6295
|
+
return findings;
|
|
6296
|
+
}
|
|
6297
|
+
function fmt(value) {
|
|
6298
|
+
if (typeof value === "string") return value;
|
|
6299
|
+
return toJsonText(value) ?? String(value);
|
|
6300
|
+
}
|
|
6301
|
+
function deepEqual2(a, b) {
|
|
6302
|
+
if (isList(a) || isList(b)) {
|
|
6303
|
+
if (!isList(a) || !isList(b) || a.length !== b.length) return false;
|
|
6304
|
+
const unused = [...b];
|
|
6305
|
+
for (const x of a) {
|
|
6306
|
+
const i = unused.findIndex((y) => deepEqual2(x, y));
|
|
6307
|
+
if (i === -1) return false;
|
|
6308
|
+
unused.splice(i, 1);
|
|
6309
|
+
}
|
|
6310
|
+
return true;
|
|
6311
|
+
}
|
|
6312
|
+
if (isPlainObject(a) || isPlainObject(b)) {
|
|
6313
|
+
if (!isPlainObject(a) || !isPlainObject(b)) return false;
|
|
6314
|
+
const keys = Object.keys(a);
|
|
6315
|
+
if (keys.length !== Object.keys(b).length) return false;
|
|
6316
|
+
return keys.every(
|
|
6317
|
+
(k) => Object.hasOwn(b, k) && deepEqual2(a[k], b[k])
|
|
6318
|
+
);
|
|
6319
|
+
}
|
|
6320
|
+
return toJsonText(a) === toJsonText(b);
|
|
6321
|
+
}
|
|
6322
|
+
function isList(v) {
|
|
6323
|
+
return Array.isArray(v);
|
|
6324
|
+
}
|
|
6325
|
+
function isPlainObject(v) {
|
|
6326
|
+
return typeof v === "object" && v !== null && !isList(v) && !(v instanceof Date);
|
|
6327
|
+
}
|
|
6328
|
+
|
|
6215
6329
|
// src/meta/core/config.ts
|
|
6216
6330
|
import { resolve as resolve7 } from "node:path";
|
|
6217
6331
|
import { parse as parseYaml2 } from "yaml";
|
|
@@ -6240,20 +6354,20 @@ var MOOSE_CONFIG_NAMES = [
|
|
|
6240
6354
|
"moose.config.yml"
|
|
6241
6355
|
];
|
|
6242
6356
|
async function readDocument(path, source, opts) {
|
|
6243
|
-
let
|
|
6357
|
+
let text2;
|
|
6244
6358
|
try {
|
|
6245
|
-
|
|
6359
|
+
text2 = await readFile4(path, "utf8");
|
|
6246
6360
|
} catch {
|
|
6247
6361
|
return null;
|
|
6248
6362
|
}
|
|
6249
|
-
return { text, doc: parseMapping(
|
|
6363
|
+
return { text: text2, doc: parseMapping(text2, source, opts) };
|
|
6250
6364
|
}
|
|
6251
|
-
function parseMapping(
|
|
6365
|
+
function parseMapping(text2, source, opts) {
|
|
6252
6366
|
let raw;
|
|
6253
6367
|
try {
|
|
6254
|
-
raw = parseYaml(
|
|
6368
|
+
raw = parseYaml(text2);
|
|
6255
6369
|
} catch (err) {
|
|
6256
|
-
throw opts.toError(`${source}: invalid YAML: ${err
|
|
6370
|
+
throw opts.toError(`${source}: invalid YAML: ${errorMessage(err)}`);
|
|
6257
6371
|
}
|
|
6258
6372
|
if (raw == null) return null;
|
|
6259
6373
|
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
@@ -6365,15 +6479,36 @@ function asElementPaths(value, where, source) {
|
|
|
6365
6479
|
try {
|
|
6366
6480
|
parseElementPath(path);
|
|
6367
6481
|
} catch (err) {
|
|
6368
|
-
throw new DocmetaError(`${source}: ${where} \u2014 ${err
|
|
6482
|
+
throw new DocmetaError(`${source}: ${where} \u2014 ${errorMessage(err)}`);
|
|
6369
6483
|
}
|
|
6370
6484
|
}
|
|
6371
6485
|
return list;
|
|
6372
6486
|
}
|
|
6373
6487
|
var OVERRIDE_KEYS = ["collection", "files", "schemas", "elements"];
|
|
6374
6488
|
var CHECK_KEYS = ["name", "query"];
|
|
6489
|
+
var DERIVE_KEYS = ["fields", "sources", "codeowners", "commands"];
|
|
6490
|
+
var DERIVE_COMMAND_KEYS = ["run", "timeout"];
|
|
6491
|
+
var BUILTIN_CLAIMED_BY = {
|
|
6492
|
+
created: "git",
|
|
6493
|
+
"last-updated": "git",
|
|
6494
|
+
authors: "git",
|
|
6495
|
+
owner: "codeowners",
|
|
6496
|
+
"reviewed-by": "GitHub or GitLab",
|
|
6497
|
+
"last-reviewed": "GitHub or GitLab"
|
|
6498
|
+
};
|
|
6499
|
+
var DERIVED_TABLE_RESERVED = /* @__PURE__ */ new Set(["_path", "_sources"]);
|
|
6500
|
+
var DERIVABLE_LIST = `Derivable fields: ${DERIVABLE_FIELDS.join(", ")}, or any key with an entry in derive.commands.`;
|
|
6375
6501
|
var CHECK_NAME = /^[a-z0-9][a-z0-9._-]*$/;
|
|
6376
|
-
var RESERVED_CHECK_NAMES = /* @__PURE__ */ new
|
|
6502
|
+
var RESERVED_CHECK_NAMES = /* @__PURE__ */ new Map([
|
|
6503
|
+
[
|
|
6504
|
+
"query",
|
|
6505
|
+
"`manni meta query --check` files its ad-hoc findings as check:query, and a configured check with the same name would share their identity"
|
|
6506
|
+
],
|
|
6507
|
+
[
|
|
6508
|
+
"derive",
|
|
6509
|
+
"`manni meta derive` is a verb of its own, and a check reporting as check:derive would read as its findings"
|
|
6510
|
+
]
|
|
6511
|
+
]);
|
|
6377
6512
|
var FILL_KEYS = [
|
|
6378
6513
|
"provider",
|
|
6379
6514
|
"model",
|
|
@@ -6386,6 +6521,7 @@ var CONFIG_KEYS = [
|
|
|
6386
6521
|
"schemas",
|
|
6387
6522
|
"overrides",
|
|
6388
6523
|
"checks",
|
|
6524
|
+
"derive",
|
|
6389
6525
|
"baseline",
|
|
6390
6526
|
"allowEmpty",
|
|
6391
6527
|
"respectGitignore",
|
|
@@ -6496,13 +6632,13 @@ function parseSchemaEntry(entry, where, source) {
|
|
|
6496
6632
|
}
|
|
6497
6633
|
return parsed2;
|
|
6498
6634
|
}
|
|
6499
|
-
function parseConfig(
|
|
6635
|
+
function parseConfig(text2, source) {
|
|
6500
6636
|
let raw;
|
|
6501
6637
|
try {
|
|
6502
|
-
raw = parseYaml2(
|
|
6638
|
+
raw = parseYaml2(text2);
|
|
6503
6639
|
} catch (err) {
|
|
6504
6640
|
throw new DocmetaError(
|
|
6505
|
-
`${source}: invalid YAML: ${err
|
|
6641
|
+
`${source}: invalid YAML: ${errorMessage(err)}`
|
|
6506
6642
|
);
|
|
6507
6643
|
}
|
|
6508
6644
|
return parseConfigValue(raw, source);
|
|
@@ -6564,15 +6700,170 @@ function assertNoMovedKeys(raw, source) {
|
|
|
6564
6700
|
}
|
|
6565
6701
|
}
|
|
6566
6702
|
}
|
|
6567
|
-
function
|
|
6703
|
+
function inSection(message, source, section) {
|
|
6704
|
+
return section === void 0 ? message : withSection(message, source, section);
|
|
6705
|
+
}
|
|
6706
|
+
function assertOverrideCollections(config, collections, source, section) {
|
|
6568
6707
|
const names = new Set(collections.map((c) => c.name));
|
|
6569
6708
|
for (const [i, ov] of (config.overrides ?? []).entries()) {
|
|
6570
6709
|
if (ov.collection === void 0 || names.has(ov.collection)) continue;
|
|
6571
6710
|
const defined = collections.length === 0 ? "(none)" : collections.map((c) => c.name).join(", ");
|
|
6572
6711
|
throw new DocmetaError(
|
|
6573
|
-
|
|
6712
|
+
inSection(
|
|
6713
|
+
`${source}: overrides[${i}].collection names "${ov.collection}", which collections: does not define. Defined: ${defined}.`,
|
|
6714
|
+
source,
|
|
6715
|
+
section
|
|
6716
|
+
)
|
|
6717
|
+
);
|
|
6718
|
+
}
|
|
6719
|
+
}
|
|
6720
|
+
function manifestOwning(key, collections) {
|
|
6721
|
+
for (const [c, collection] of collections.entries()) {
|
|
6722
|
+
for (const [e, manifest] of collection.externalMetadata.entries()) {
|
|
6723
|
+
if (manifest.keys.includes(key)) {
|
|
6724
|
+
return { collection: c, name: collection.name, entry: e, file: manifest.file };
|
|
6725
|
+
}
|
|
6726
|
+
}
|
|
6727
|
+
}
|
|
6728
|
+
return void 0;
|
|
6729
|
+
}
|
|
6730
|
+
function assertManagedFieldsUnowned(config, collections, source, section) {
|
|
6731
|
+
const refuse = (path, owner) => {
|
|
6732
|
+
const message = `${source}: ${path} is owned by collections[${owner.collection}].externalMetadata[${owner.entry}] (${owner.file}) \u2014 a managed field has one authority, and a manifest key already has one. Drop it from one side.`;
|
|
6733
|
+
throw new DocmetaError(
|
|
6734
|
+
inSection(message, source, section)
|
|
6735
|
+
);
|
|
6736
|
+
};
|
|
6737
|
+
for (const key of Object.keys(config.derive?.commands ?? {})) {
|
|
6738
|
+
const owner = manifestOwning(key, collections);
|
|
6739
|
+
if (owner !== void 0) refuse(`derive.commands.${key}`, owner);
|
|
6740
|
+
}
|
|
6741
|
+
for (const [i, field] of (config.derive?.fields ?? []).entries()) {
|
|
6742
|
+
const owner = manifestOwning(field, collections);
|
|
6743
|
+
if (owner !== void 0) refuse(`derive.fields[${i}] "${field}"`, owner);
|
|
6744
|
+
}
|
|
6745
|
+
}
|
|
6746
|
+
function parseDerive(raw, source) {
|
|
6747
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
6748
|
+
throw new DocmetaError(
|
|
6749
|
+
`${source}: "derive" must be a mapping. Write it as \`derive:\` with \`fields:\` beneath it.`
|
|
6750
|
+
);
|
|
6751
|
+
}
|
|
6752
|
+
const e = raw;
|
|
6753
|
+
rejectUnknownKeys2(e, DERIVE_KEYS, '"derive"', source);
|
|
6754
|
+
if (e.fields === void 0 && e.sources === void 0 && e.codeowners === void 0 && e.commands === void 0) {
|
|
6755
|
+
throw new DocmetaError(
|
|
6756
|
+
`${source}: "derive" sets nothing. Give it \`fields:\` to manage, \`commands:\` to derive from a command, or \`sources:\` or \`codeowners:\` to shape the reads.`
|
|
6757
|
+
);
|
|
6758
|
+
}
|
|
6759
|
+
const commands = e.commands === void 0 ? void 0 : parseDeriveCommands(e.commands, source);
|
|
6760
|
+
if (e.fields !== void 0 && (!Array.isArray(e.fields) || e.fields.length === 0 || e.fields.some((f) => typeof f !== "string"))) {
|
|
6761
|
+
throw new DocmetaError(
|
|
6762
|
+
`${source}: derive.fields must be a non-empty list of field names. ${DERIVABLE_LIST}`
|
|
6763
|
+
);
|
|
6764
|
+
}
|
|
6765
|
+
const fields = [];
|
|
6766
|
+
(e.fields ?? []).forEach((f, i) => {
|
|
6767
|
+
if (!isBuiltinField(f) && !(commands !== void 0 && Object.hasOwn(commands, f))) {
|
|
6768
|
+
throw new DocmetaError(
|
|
6769
|
+
`${source}: derive.fields[${i}] "${f}" is not derivable. ${DERIVABLE_LIST}`
|
|
6770
|
+
);
|
|
6771
|
+
}
|
|
6772
|
+
if (fields.includes(f)) {
|
|
6773
|
+
throw new DocmetaError(`${source}: derive.fields lists "${f}" twice.`);
|
|
6774
|
+
}
|
|
6775
|
+
fields.push(f);
|
|
6776
|
+
});
|
|
6777
|
+
const derive = { fields };
|
|
6778
|
+
if (e.sources !== void 0) {
|
|
6779
|
+
if (!Array.isArray(e.sources) || e.sources.length === 0 || e.sources.some((s) => typeof s !== "string")) {
|
|
6780
|
+
throw new DocmetaError(
|
|
6781
|
+
`${source}: derive.sources must be a non-empty list of source names. Sources: ${DERIVE_SOURCES.join(", ")}.`
|
|
6782
|
+
);
|
|
6783
|
+
}
|
|
6784
|
+
const sources = [];
|
|
6785
|
+
e.sources.forEach((s, i) => {
|
|
6786
|
+
if (!isDeriveSource(s)) {
|
|
6787
|
+
throw new DocmetaError(
|
|
6788
|
+
`${source}: derive.sources[${i}] "${s}" is not a source. Sources: ${DERIVE_SOURCES.join(", ")}.`
|
|
6789
|
+
);
|
|
6790
|
+
}
|
|
6791
|
+
if (sources.includes(s)) {
|
|
6792
|
+
throw new DocmetaError(`${source}: derive.sources lists "${s}" twice.`);
|
|
6793
|
+
}
|
|
6794
|
+
sources.push(s);
|
|
6795
|
+
});
|
|
6796
|
+
derive.sources = sources;
|
|
6797
|
+
}
|
|
6798
|
+
if (e.codeowners !== void 0) {
|
|
6799
|
+
if (typeof e.codeowners !== "string" || e.codeowners.trim() === "") {
|
|
6800
|
+
throw new DocmetaError(
|
|
6801
|
+
`${source}: derive.codeowners must be a non-empty path to a CODEOWNERS file, relative to the config file.`
|
|
6802
|
+
);
|
|
6803
|
+
}
|
|
6804
|
+
derive.codeowners = e.codeowners;
|
|
6805
|
+
}
|
|
6806
|
+
if (commands !== void 0) derive.commands = commands;
|
|
6807
|
+
return derive;
|
|
6808
|
+
}
|
|
6809
|
+
function parseDeriveCommands(raw, source) {
|
|
6810
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
6811
|
+
throw new DocmetaError(
|
|
6812
|
+
`${source}: derive.commands must be a mapping of field name to command.`
|
|
6574
6813
|
);
|
|
6575
6814
|
}
|
|
6815
|
+
const commands = {};
|
|
6816
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
6817
|
+
if (key.trim() === "") {
|
|
6818
|
+
throw new DocmetaError(`${source}: derive.commands has an empty field name.`);
|
|
6819
|
+
}
|
|
6820
|
+
if (key === FILE_SCHEMA_KEY) {
|
|
6821
|
+
throw new DocmetaError(
|
|
6822
|
+
`${source}: derive.commands may not derive "${FILE_SCHEMA_KEY}" \u2014 a command never chooses the schema a document is judged by.`
|
|
6823
|
+
);
|
|
6824
|
+
}
|
|
6825
|
+
if (isBuiltinField(key)) {
|
|
6826
|
+
throw new DocmetaError(
|
|
6827
|
+
`${source}: derive.commands.${key} targets a field ${BUILTIN_CLAIMED_BY[key]} already derives; a command may only derive a field no built-in source claims.`
|
|
6828
|
+
);
|
|
6829
|
+
}
|
|
6830
|
+
if (DERIVED_TABLE_RESERVED.has(key)) {
|
|
6831
|
+
throw new DocmetaError(
|
|
6832
|
+
`${source}: derive.commands.${key} collides with a column of the derived table a query builds (${[...DERIVED_TABLE_RESERVED].join(", ")}). Pick another field name.`
|
|
6833
|
+
);
|
|
6834
|
+
}
|
|
6835
|
+
const where = `derive.commands.${key}`;
|
|
6836
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
6837
|
+
throw new DocmetaError(
|
|
6838
|
+
`${source}: ${where} must be a mapping with \`run:\` beneath it.`
|
|
6839
|
+
);
|
|
6840
|
+
}
|
|
6841
|
+
const c = value;
|
|
6842
|
+
rejectUnknownKeys2(c, DERIVE_COMMAND_KEYS, where, source);
|
|
6843
|
+
if (!Array.isArray(c.run) || c.run.length === 0) {
|
|
6844
|
+
throw new DocmetaError(
|
|
6845
|
+
`${source}: ${where}.run must be a non-empty list of strings, the program first.`
|
|
6846
|
+
);
|
|
6847
|
+
}
|
|
6848
|
+
const run2 = [];
|
|
6849
|
+
c.run.forEach((arg, j) => {
|
|
6850
|
+
if (typeof arg !== "string" || arg.trim() === "") {
|
|
6851
|
+
throw new DocmetaError(`${source}: ${where}.run[${j}] must be a non-empty string.`);
|
|
6852
|
+
}
|
|
6853
|
+
run2.push(arg);
|
|
6854
|
+
});
|
|
6855
|
+
const command = { run: run2 };
|
|
6856
|
+
if (c.timeout !== void 0) {
|
|
6857
|
+
if (typeof c.timeout !== "number" || !Number.isInteger(c.timeout) || c.timeout <= 0) {
|
|
6858
|
+
throw new DocmetaError(
|
|
6859
|
+
`${source}: ${where}.timeout must be a whole number of seconds, greater than zero.`
|
|
6860
|
+
);
|
|
6861
|
+
}
|
|
6862
|
+
command.timeout = c.timeout;
|
|
6863
|
+
}
|
|
6864
|
+
commands[key] = command;
|
|
6865
|
+
}
|
|
6866
|
+
return commands;
|
|
6576
6867
|
}
|
|
6577
6868
|
function parseConfigDocument(raw, source) {
|
|
6578
6869
|
if (raw == null) return {};
|
|
@@ -6629,6 +6920,9 @@ function parseConfigDocument(raw, source) {
|
|
|
6629
6920
|
if (obj.checks !== void 0) {
|
|
6630
6921
|
config.checks = parseChecks(obj.checks, source);
|
|
6631
6922
|
}
|
|
6923
|
+
if (obj.derive !== void 0) {
|
|
6924
|
+
config.derive = parseDerive(obj.derive, source);
|
|
6925
|
+
}
|
|
6632
6926
|
if (obj.elements !== void 0) {
|
|
6633
6927
|
config.elements = asElementPaths(obj.elements, "elements", source);
|
|
6634
6928
|
}
|
|
@@ -6690,9 +6984,10 @@ function parseChecks(value, source) {
|
|
|
6690
6984
|
`${source}: checks[${i}].name "${e.name}" must match [a-z0-9][a-z0-9._-]* and not end in ".json" \u2014 the name is part of each finding's identity (check:<name>).`
|
|
6691
6985
|
);
|
|
6692
6986
|
}
|
|
6693
|
-
|
|
6987
|
+
const reserved = RESERVED_CHECK_NAMES.get(e.name);
|
|
6988
|
+
if (reserved !== void 0) {
|
|
6694
6989
|
throw new DocmetaError(
|
|
6695
|
-
`${source}: checks[${i}].name "${e.name}" is reserved \u2014
|
|
6990
|
+
`${source}: checks[${i}].name "${e.name}" is reserved \u2014 ${reserved}. Pick another name.`
|
|
6696
6991
|
);
|
|
6697
6992
|
}
|
|
6698
6993
|
if (seen.has(e.name)) {
|
|
@@ -6795,8 +7090,8 @@ function parseFill(value, source) {
|
|
|
6795
7090
|
return fill;
|
|
6796
7091
|
}
|
|
6797
7092
|
function schemaTrustRoot(cwd, configDir) {
|
|
6798
|
-
const
|
|
6799
|
-
if (
|
|
7093
|
+
const git2 = findGitRoot(cwd);
|
|
7094
|
+
if (git2 !== null) return { dir: git2, source: "git" };
|
|
6800
7095
|
if (configDir !== void 0) return { dir: resolve7(configDir), source: "config" };
|
|
6801
7096
|
return { dir: resolve7(cwd), source: "cwd" };
|
|
6802
7097
|
}
|
|
@@ -6805,7 +7100,8 @@ async function loadConfig(explicitPath, cwd = process.cwd()) {
|
|
|
6805
7100
|
if (file === null) return null;
|
|
6806
7101
|
const section = file.wrapped ? META_SECTION : void 0;
|
|
6807
7102
|
const config = parseConfigValue(file.value, file.source, section);
|
|
6808
|
-
assertOverrideCollections(config, file.collections, file.source);
|
|
7103
|
+
assertOverrideCollections(config, file.collections, file.source, section);
|
|
7104
|
+
assertManagedFieldsUnowned(config, file.collections, file.source, section);
|
|
6809
7105
|
return {
|
|
6810
7106
|
config,
|
|
6811
7107
|
path: file.path,
|
|
@@ -6900,16 +7196,22 @@ function createDocsTable(db, entries, dataColumns) {
|
|
|
6900
7196
|
`INSERT INTO docs VALUES (${["?", "?", "?", "?", ...dataColumns.map(() => "?")].join(", ")})`
|
|
6901
7197
|
);
|
|
6902
7198
|
db.exec("BEGIN");
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
7199
|
+
let committed = false;
|
|
7200
|
+
try {
|
|
7201
|
+
for (const { label, extracted } of entries) {
|
|
7202
|
+
insert.run(
|
|
7203
|
+
label,
|
|
7204
|
+
extracted.format,
|
|
7205
|
+
extracted.present ? 1 : 0,
|
|
7206
|
+
JSON.stringify(extracted.data),
|
|
7207
|
+
...dataColumns.map((c) => bindValue(extracted.data[c]))
|
|
7208
|
+
);
|
|
7209
|
+
}
|
|
7210
|
+
db.exec("COMMIT");
|
|
7211
|
+
committed = true;
|
|
7212
|
+
} finally {
|
|
7213
|
+
if (!committed) db.exec("ROLLBACK");
|
|
6911
7214
|
}
|
|
6912
|
-
db.exec("COMMIT");
|
|
6913
7215
|
}
|
|
6914
7216
|
function registerLineFor(db, entries) {
|
|
6915
7217
|
const byLabel = new Map(entries.map((e) => [e.label, e.extracted]));
|
|
@@ -6984,7 +7286,7 @@ function isParamName(name2) {
|
|
|
6984
7286
|
return PARAM_NAME_HEAD.exec(name2)?.[0] === name2;
|
|
6985
7287
|
}
|
|
6986
7288
|
function collectNamedParameters(sql) {
|
|
6987
|
-
const
|
|
7289
|
+
const tokens2 = [];
|
|
6988
7290
|
const seen = /* @__PURE__ */ new Set();
|
|
6989
7291
|
const prefixOf = /* @__PURE__ */ new Map();
|
|
6990
7292
|
let i = 0;
|
|
@@ -7014,14 +7316,14 @@ function collectNamedParameters(sql) {
|
|
|
7014
7316
|
prefixOf.set(m[0], ch);
|
|
7015
7317
|
if (!seen.has(token)) {
|
|
7016
7318
|
seen.add(token);
|
|
7017
|
-
|
|
7319
|
+
tokens2.push(token);
|
|
7018
7320
|
}
|
|
7019
7321
|
i += 1 + m[0].length;
|
|
7020
7322
|
} else {
|
|
7021
7323
|
i++;
|
|
7022
7324
|
}
|
|
7023
7325
|
}
|
|
7024
|
-
return
|
|
7326
|
+
return tokens2;
|
|
7025
7327
|
}
|
|
7026
7328
|
function stripLeadingTrivia(sql) {
|
|
7027
7329
|
let i = 0;
|
|
@@ -7034,16 +7336,16 @@ function stripLeadingTrivia(sql) {
|
|
|
7034
7336
|
return sql.slice(i);
|
|
7035
7337
|
}
|
|
7036
7338
|
}
|
|
7037
|
-
function isTrivia(
|
|
7339
|
+
function isTrivia(text2) {
|
|
7038
7340
|
let i = 0;
|
|
7039
|
-
while (i <
|
|
7040
|
-
const ch =
|
|
7341
|
+
while (i < text2.length) {
|
|
7342
|
+
const ch = text2[i];
|
|
7041
7343
|
if (ch === void 0 || /\s/.test(ch) || ch === ";") {
|
|
7042
7344
|
i++;
|
|
7043
7345
|
continue;
|
|
7044
7346
|
}
|
|
7045
|
-
if (startsComment(
|
|
7046
|
-
i = skipComment(
|
|
7347
|
+
if (startsComment(text2, i)) {
|
|
7348
|
+
i = skipComment(text2, i);
|
|
7047
7349
|
continue;
|
|
7048
7350
|
}
|
|
7049
7351
|
return false;
|
|
@@ -7055,8 +7357,8 @@ function loadSqlite() {
|
|
|
7055
7357
|
sqliteModule ??= (async () => {
|
|
7056
7358
|
const original = process.emitWarning.bind(process);
|
|
7057
7359
|
const filtered = (warning, ...rest) => {
|
|
7058
|
-
const
|
|
7059
|
-
if (
|
|
7360
|
+
const text2 = typeof warning === "string" ? warning : warning.message;
|
|
7361
|
+
if (text2.includes("SQLite is an experimental feature")) return;
|
|
7060
7362
|
Reflect.apply(original, process, [warning, ...rest]);
|
|
7061
7363
|
};
|
|
7062
7364
|
process.emitWarning = filtered;
|
|
@@ -7248,7 +7550,7 @@ var Validator = class {
|
|
|
7248
7550
|
return ajv.compile(schema);
|
|
7249
7551
|
} catch (err) {
|
|
7250
7552
|
throw new DocmetaError(
|
|
7251
|
-
`Schema "${ref}" failed to compile: ${err
|
|
7553
|
+
`Schema "${ref}" failed to compile: ${errorMessage(err)}`
|
|
7252
7554
|
);
|
|
7253
7555
|
}
|
|
7254
7556
|
}
|
|
@@ -7332,116 +7634,1748 @@ function toFieldError(schema, e, lineFor, colFor, locate) {
|
|
|
7332
7634
|
};
|
|
7333
7635
|
}
|
|
7334
7636
|
|
|
7335
|
-
// src/meta/
|
|
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
|
|
7637
|
+
// src/meta/core/derive/cache.ts
|
|
7341
7638
|
import { createHash as createHash3 } from "node:crypto";
|
|
7342
|
-
import {
|
|
7343
|
-
import {
|
|
7344
|
-
var
|
|
7345
|
-
var
|
|
7346
|
-
var
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7639
|
+
import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
7640
|
+
import { join as join5 } from "node:path";
|
|
7641
|
+
var REVIEW_CACHE_DIR = ".manni/meta/review-cache";
|
|
7642
|
+
var REVIEW_CACHE_VERSION = 1;
|
|
7643
|
+
var ReviewCache = class {
|
|
7644
|
+
constructor(dir, enabled) {
|
|
7645
|
+
this.dir = dir;
|
|
7646
|
+
this.enabled = enabled;
|
|
7647
|
+
}
|
|
7648
|
+
dir;
|
|
7649
|
+
enabled;
|
|
7650
|
+
entryPath(key) {
|
|
7651
|
+
const digest = createHash3("sha256").update(key).digest("hex");
|
|
7652
|
+
return join5(this.dir, `${digest}.json`);
|
|
7653
|
+
}
|
|
7654
|
+
/** The cached change, or `undefined` for a miss. Never `null`: nulls are not stored. */
|
|
7655
|
+
get(key) {
|
|
7656
|
+
if (!this.enabled) return void 0;
|
|
7657
|
+
let raw;
|
|
7658
|
+
try {
|
|
7659
|
+
raw = JSON.parse(readFileSync(this.entryPath(key), "utf8"));
|
|
7660
|
+
} catch {
|
|
7661
|
+
return void 0;
|
|
7662
|
+
}
|
|
7663
|
+
if (!isRecord(raw)) return void 0;
|
|
7664
|
+
if (raw.version !== REVIEW_CACHE_VERSION || raw.key !== key) return void 0;
|
|
7665
|
+
const change2 = readChange(raw.change);
|
|
7666
|
+
return change2 ?? void 0;
|
|
7667
|
+
}
|
|
7668
|
+
set(key, value) {
|
|
7669
|
+
if (!this.enabled) return;
|
|
7670
|
+
const entry = { version: REVIEW_CACHE_VERSION, key, change: value };
|
|
7671
|
+
const path = this.entryPath(key);
|
|
7672
|
+
const tmp = `${path}.${process.pid}-${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
7673
|
+
try {
|
|
7674
|
+
mkdirSync(this.dir, { recursive: true });
|
|
7675
|
+
writeFileSync(tmp, `${JSON.stringify(entry, null, 2)}
|
|
7676
|
+
`, "utf8");
|
|
7677
|
+
renameSync(tmp, path);
|
|
7678
|
+
} catch {
|
|
7679
|
+
try {
|
|
7680
|
+
rmSync(tmp, { force: true });
|
|
7681
|
+
} catch {
|
|
7682
|
+
}
|
|
7683
|
+
}
|
|
7684
|
+
}
|
|
7685
|
+
};
|
|
7686
|
+
function cachedClient(client, cache, identity) {
|
|
7687
|
+
return {
|
|
7688
|
+
detect: () => client.detect(),
|
|
7689
|
+
status: () => client.status(),
|
|
7690
|
+
async mergedChangeFor(sha) {
|
|
7691
|
+
if (identity === null) return client.mergedChangeFor(sha);
|
|
7692
|
+
const key = `${identity.host}/${identity.project}/${sha}`;
|
|
7693
|
+
const hit = cache.get(key);
|
|
7694
|
+
if (hit !== void 0) return hit;
|
|
7695
|
+
const change2 = await client.mergedChangeFor(sha);
|
|
7696
|
+
if (change2 !== null) cache.set(key, change2);
|
|
7697
|
+
return change2;
|
|
7698
|
+
}
|
|
7699
|
+
};
|
|
7358
7700
|
}
|
|
7359
|
-
function
|
|
7360
|
-
|
|
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);
|
|
7701
|
+
function isRecord(v) {
|
|
7702
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
7367
7703
|
}
|
|
7368
|
-
function
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7704
|
+
function readChange(v) {
|
|
7705
|
+
if (!isRecord(v)) return null;
|
|
7706
|
+
const { id, mergedAt, approvals } = v;
|
|
7707
|
+
if (typeof id !== "number" || typeof mergedAt !== "string") return null;
|
|
7708
|
+
if (!Array.isArray(approvals)) return null;
|
|
7709
|
+
const read = [];
|
|
7710
|
+
for (const a of approvals) {
|
|
7711
|
+
if (!isRecord(a)) return null;
|
|
7712
|
+
if (typeof a.login !== "string" || typeof a.submittedAt !== "string") return null;
|
|
7713
|
+
read.push({ login: a.login, submittedAt: a.submittedAt });
|
|
7372
7714
|
}
|
|
7373
|
-
return
|
|
7374
|
-
{
|
|
7375
|
-
version: baseline.version,
|
|
7376
|
-
generatedWith: baseline.generatedWith,
|
|
7377
|
-
entries
|
|
7378
|
-
},
|
|
7379
|
-
null,
|
|
7380
|
-
2
|
|
7381
|
-
)}
|
|
7382
|
-
`;
|
|
7715
|
+
return { id, mergedAt, approvals: read };
|
|
7383
7716
|
}
|
|
7384
|
-
|
|
7385
|
-
|
|
7717
|
+
|
|
7718
|
+
// src/meta/core/derive/spawn.ts
|
|
7719
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
7720
|
+
var DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
7721
|
+
var MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
7722
|
+
var BinMissing = class extends Error {
|
|
7723
|
+
};
|
|
7724
|
+
function run(bin, args, opts) {
|
|
7725
|
+
return new Promise((settle, reject) => {
|
|
7726
|
+
let done = false;
|
|
7727
|
+
const finish = (r) => {
|
|
7728
|
+
if (done) return;
|
|
7729
|
+
done = true;
|
|
7730
|
+
settle(r);
|
|
7731
|
+
};
|
|
7732
|
+
const fail = (err) => {
|
|
7733
|
+
if (done) return;
|
|
7734
|
+
done = true;
|
|
7735
|
+
reject(err);
|
|
7736
|
+
};
|
|
7737
|
+
let child;
|
|
7738
|
+
try {
|
|
7739
|
+
child = spawn2(bin, [...opts.prefixArgs ?? [], ...args], {
|
|
7740
|
+
cwd: opts.cwd,
|
|
7741
|
+
windowsHide: true
|
|
7742
|
+
});
|
|
7743
|
+
} catch (err) {
|
|
7744
|
+
fail(err instanceof Error ? err : new Error(String(err)));
|
|
7745
|
+
return;
|
|
7746
|
+
}
|
|
7747
|
+
let stdout = "";
|
|
7748
|
+
let stderr = "";
|
|
7749
|
+
let timedOut = false;
|
|
7750
|
+
let tooLarge = false;
|
|
7751
|
+
const stop = (r) => {
|
|
7752
|
+
clearTimeout(timer);
|
|
7753
|
+
child.kill();
|
|
7754
|
+
setTimeout(() => child.kill("SIGKILL"), 1e3).unref();
|
|
7755
|
+
finish({ code: null, ...r });
|
|
7756
|
+
};
|
|
7757
|
+
child.stdout.setEncoding("utf8");
|
|
7758
|
+
child.stdout.on("data", (chunk) => {
|
|
7759
|
+
stdout += chunk;
|
|
7760
|
+
if (stdout.length > MAX_OUTPUT_BYTES) {
|
|
7761
|
+
tooLarge = true;
|
|
7762
|
+
stop({ stdout, stderr, timedOut, tooLarge });
|
|
7763
|
+
}
|
|
7764
|
+
});
|
|
7765
|
+
child.stderr.setEncoding("utf8");
|
|
7766
|
+
child.stderr.on("data", (chunk) => {
|
|
7767
|
+
stderr += chunk;
|
|
7768
|
+
if (stderr.length > MAX_OUTPUT_BYTES) {
|
|
7769
|
+
tooLarge = true;
|
|
7770
|
+
stop({ stdout, stderr, timedOut, tooLarge });
|
|
7771
|
+
}
|
|
7772
|
+
});
|
|
7773
|
+
child.stdin.end();
|
|
7774
|
+
const timer = setTimeout(() => {
|
|
7775
|
+
timedOut = true;
|
|
7776
|
+
stop({ stdout, stderr, timedOut, tooLarge });
|
|
7777
|
+
}, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2);
|
|
7778
|
+
child.on("error", (err) => {
|
|
7779
|
+
clearTimeout(timer);
|
|
7780
|
+
fail(err.code === "ENOENT" ? new BinMissing(err.message) : err);
|
|
7781
|
+
});
|
|
7782
|
+
child.on("close", (code) => {
|
|
7783
|
+
clearTimeout(timer);
|
|
7784
|
+
finish({ code, stdout, stderr, timedOut, tooLarge });
|
|
7785
|
+
});
|
|
7786
|
+
});
|
|
7386
7787
|
}
|
|
7387
|
-
function
|
|
7388
|
-
|
|
7788
|
+
function commandLine(bin, args) {
|
|
7789
|
+
return [bin, ...args].join(" ");
|
|
7790
|
+
}
|
|
7791
|
+
function lastLine(text2) {
|
|
7792
|
+
const lines = text2.split(/\r?\n/).filter((l) => l.trim() !== "");
|
|
7793
|
+
return lines.at(-1) ?? "";
|
|
7794
|
+
}
|
|
7795
|
+
|
|
7796
|
+
// src/meta/core/derive/command.ts
|
|
7797
|
+
var PATH_PLACEHOLDER = "{path}";
|
|
7798
|
+
function isPerFile(command) {
|
|
7799
|
+
return command.run.some((arg) => arg.includes(PATH_PLACEHOLDER));
|
|
7800
|
+
}
|
|
7801
|
+
function argvFor(command, label) {
|
|
7802
|
+
return command.run.map((arg) => arg.split(PATH_PLACEHOLDER).join(label));
|
|
7803
|
+
}
|
|
7804
|
+
function valueOf(stdout) {
|
|
7805
|
+
const text2 = stdout.trim();
|
|
7806
|
+
if (text2 === "") return null;
|
|
7807
|
+
if (!/^[{["]/.test(text2)) return text2;
|
|
7389
7808
|
try {
|
|
7390
|
-
|
|
7391
|
-
} catch
|
|
7392
|
-
|
|
7393
|
-
}
|
|
7394
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
7395
|
-
bad(source, "top level must be an object.");
|
|
7396
|
-
}
|
|
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
|
-
);
|
|
7809
|
+
return JSON.parse(text2);
|
|
7810
|
+
} catch {
|
|
7811
|
+
return text2;
|
|
7403
7812
|
}
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7813
|
+
}
|
|
7814
|
+
async function deriveFromCommands(inputs, commands, opts) {
|
|
7815
|
+
const records = /* @__PURE__ */ new Map();
|
|
7816
|
+
const fields = Object.keys(commands);
|
|
7817
|
+
if (fields.length === 0 || inputs.length === 0) {
|
|
7818
|
+
return { status: { available: true }, records };
|
|
7408
7819
|
}
|
|
7409
|
-
const
|
|
7410
|
-
for (const
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
if (
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
);
|
|
7820
|
+
for (const input of inputs) records.set(input.label, {});
|
|
7821
|
+
for (const field of fields) {
|
|
7822
|
+
const command = commands[field];
|
|
7823
|
+
if (command === void 0) continue;
|
|
7824
|
+
if (isPerFile(command)) {
|
|
7825
|
+
for (const [label, record] of records) {
|
|
7826
|
+
const outcome = await runOne(field, argvFor(command, label), command.timeoutMs, opts);
|
|
7827
|
+
if ("reason" in outcome) return unavailable2(outcome.reason);
|
|
7828
|
+
record[field] = outcome.value;
|
|
7829
|
+
}
|
|
7830
|
+
} else {
|
|
7831
|
+
const outcome = await runOne(field, command.run, command.timeoutMs, opts);
|
|
7832
|
+
if ("reason" in outcome) return unavailable2(outcome.reason);
|
|
7833
|
+
for (const record of records.values()) record[field] = outcome.value;
|
|
7422
7834
|
}
|
|
7423
|
-
entries[file] = value;
|
|
7424
7835
|
}
|
|
7425
|
-
return {
|
|
7836
|
+
return { status: { available: true }, records };
|
|
7426
7837
|
}
|
|
7427
|
-
|
|
7428
|
-
|
|
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);
|
|
7838
|
+
function unavailable2(reason) {
|
|
7839
|
+
return { status: { available: false, reason }, records: /* @__PURE__ */ new Map() };
|
|
7438
7840
|
}
|
|
7439
|
-
async function
|
|
7841
|
+
async function runOne(field, argv, timeoutMs, opts) {
|
|
7842
|
+
const [program, ...args] = argv;
|
|
7843
|
+
const where = `(derive.commands.${field})`;
|
|
7844
|
+
if (program === void 0) {
|
|
7845
|
+
return { reason: `no program to run ${where}` };
|
|
7846
|
+
}
|
|
7847
|
+
const line = argv.join(" ");
|
|
7848
|
+
let result;
|
|
7440
7849
|
try {
|
|
7441
|
-
await
|
|
7850
|
+
result = await run(opts.spawn?.bin ?? program, args, {
|
|
7851
|
+
...opts.spawn,
|
|
7852
|
+
cwd: opts.spawn?.cwd ?? opts.cwd,
|
|
7853
|
+
timeoutMs
|
|
7854
|
+
});
|
|
7442
7855
|
} catch (err) {
|
|
7443
|
-
|
|
7444
|
-
|
|
7856
|
+
if (err instanceof BinMissing) {
|
|
7857
|
+
return { reason: `\`${program}\` is not on PATH ${where}` };
|
|
7858
|
+
}
|
|
7859
|
+
return { reason: `\`${line}\` could not be run: ${errorMessage(err)} ${where}` };
|
|
7860
|
+
}
|
|
7861
|
+
if (result.timedOut) {
|
|
7862
|
+
return { reason: `\`${line}\` timed out after ${String(timeoutMs / 1e3)}s ${where}` };
|
|
7863
|
+
}
|
|
7864
|
+
if (result.tooLarge) {
|
|
7865
|
+
return {
|
|
7866
|
+
reason: `\`${line}\` wrote more than ${String(MAX_OUTPUT_BYTES / (1024 * 1024))} MiB; a command reports one field's value ${where}`
|
|
7867
|
+
};
|
|
7868
|
+
}
|
|
7869
|
+
if (result.code !== 0) {
|
|
7870
|
+
const last = lastLine(result.stderr);
|
|
7871
|
+
const detail = last === "" ? "no output on stderr" : last;
|
|
7872
|
+
const exit = result.code === null ? "killed by a signal" : `exit ${String(result.code)}`;
|
|
7873
|
+
return { reason: `\`${line}\` failed (${exit}): ${detail} ${where}` };
|
|
7874
|
+
}
|
|
7875
|
+
const value = valueOf(result.stdout);
|
|
7876
|
+
return { value: value === null ? null : { value, source: "command", evidence: line } };
|
|
7877
|
+
}
|
|
7878
|
+
|
|
7879
|
+
// src/meta/core/derive/reviews.ts
|
|
7880
|
+
import { resolve as resolvePath3 } from "node:path";
|
|
7881
|
+
function parseOriginUrl(url) {
|
|
7882
|
+
const trimmed = url.trim();
|
|
7883
|
+
if (trimmed === "") return null;
|
|
7884
|
+
if (isLocalPath(trimmed)) return null;
|
|
7885
|
+
const scp = /^(?:[^@/:]+@)?([^/:]+):(?!\/\/)(.+)$/.exec(trimmed);
|
|
7886
|
+
if (scp) {
|
|
7887
|
+
const host = scp[1];
|
|
7888
|
+
const path = scp[2];
|
|
7889
|
+
if (host === void 0 || path === void 0) return null;
|
|
7890
|
+
if (/^[A-Za-z]$/.test(host)) return null;
|
|
7891
|
+
return project(host, path);
|
|
7892
|
+
}
|
|
7893
|
+
let parsed2;
|
|
7894
|
+
try {
|
|
7895
|
+
parsed2 = new URL(trimmed);
|
|
7896
|
+
} catch {
|
|
7897
|
+
return null;
|
|
7898
|
+
}
|
|
7899
|
+
if (!/^(https?|ssh|git):$/.test(parsed2.protocol)) return null;
|
|
7900
|
+
if (parsed2.hostname === "") return null;
|
|
7901
|
+
return project(parsed2.hostname, parsed2.pathname);
|
|
7902
|
+
}
|
|
7903
|
+
function isLocalPath(s) {
|
|
7904
|
+
return s.startsWith("/") || s.startsWith("./") || s.startsWith("../") || s.startsWith("\\\\");
|
|
7905
|
+
}
|
|
7906
|
+
function project(host, path) {
|
|
7907
|
+
const segments = path.replace(/\.git$/, "").split("/").filter((s) => s !== "");
|
|
7908
|
+
if (segments.length < 2) return null;
|
|
7909
|
+
return { host, project: segments.join("/") };
|
|
7910
|
+
}
|
|
7911
|
+
function identityFromOrigin(host, project2, requested) {
|
|
7912
|
+
const name2 = host.toLowerCase();
|
|
7913
|
+
if (name2.includes("github")) return { kind: "github", host, project: project2 };
|
|
7914
|
+
if (name2.includes("gitlab")) return { kind: "gitlab", host, project: project2 };
|
|
7915
|
+
const only = requested !== void 0 && requested.length === 1 ? requested[0] : void 0;
|
|
7916
|
+
return only === void 0 ? null : { kind: only, host, project: project2 };
|
|
7917
|
+
}
|
|
7918
|
+
var UnknownHostClient = class {
|
|
7919
|
+
constructor(host) {
|
|
7920
|
+
this.host = host;
|
|
7921
|
+
}
|
|
7922
|
+
host;
|
|
7923
|
+
detect() {
|
|
7924
|
+
return Promise.resolve(null);
|
|
7925
|
+
}
|
|
7926
|
+
status() {
|
|
7927
|
+
return Promise.resolve({
|
|
7928
|
+
available: false,
|
|
7929
|
+
reason: `the origin remote is ${this.host}, which names neither GitHub nor GitLab; list exactly one of github, gitlab in derive.sources to say which it is`
|
|
7930
|
+
});
|
|
7931
|
+
}
|
|
7932
|
+
mergedChangeFor() {
|
|
7933
|
+
return Promise.resolve(null);
|
|
7934
|
+
}
|
|
7935
|
+
};
|
|
7936
|
+
function httpStatus(stderr) {
|
|
7937
|
+
const m = /\b(?:HTTP\s+(\d{3})|(\d{3})\s+Not\s+Found)\b/i.exec(stderr);
|
|
7938
|
+
const code = m?.[1] ?? m?.[2];
|
|
7939
|
+
return code === void 0 ? null : Number(code);
|
|
7940
|
+
}
|
|
7941
|
+
function isNotFound(r) {
|
|
7942
|
+
const status = httpStatus(r.stderr);
|
|
7943
|
+
return status === 404 || status === 422;
|
|
7944
|
+
}
|
|
7945
|
+
var CliClient = class {
|
|
7946
|
+
constructor(identity, spawnOpts) {
|
|
7947
|
+
this.identity = identity;
|
|
7948
|
+
this.spawnOpts = spawnOpts;
|
|
7949
|
+
}
|
|
7950
|
+
identity;
|
|
7951
|
+
spawnOpts;
|
|
7952
|
+
detect() {
|
|
7953
|
+
return Promise.resolve(this.identity);
|
|
7954
|
+
}
|
|
7955
|
+
async status() {
|
|
7956
|
+
if (this.identity === null) {
|
|
7957
|
+
return { available: false, reason: "no origin remote to tell GitHub from GitLab" };
|
|
7958
|
+
}
|
|
7959
|
+
const { host } = this.identity;
|
|
7960
|
+
let r;
|
|
7961
|
+
try {
|
|
7962
|
+
r = await this.run(["auth", "status", "--hostname", host]);
|
|
7963
|
+
} catch (err) {
|
|
7964
|
+
if (err instanceof BinMissing) {
|
|
7965
|
+
return {
|
|
7966
|
+
available: false,
|
|
7967
|
+
reason: `${this.tool} is not on PATH (origin is ${host}); install ${this.tool} and run ${this.tool} auth login`
|
|
7968
|
+
};
|
|
7969
|
+
}
|
|
7970
|
+
throw err;
|
|
7971
|
+
}
|
|
7972
|
+
if (r.timedOut) throw this.timeout(["auth", "status", "--hostname", host]);
|
|
7973
|
+
if (r.code !== 0) {
|
|
7974
|
+
return {
|
|
7975
|
+
available: false,
|
|
7976
|
+
reason: `${this.tool} is not authenticated for ${host}; run ${this.tool} auth login`
|
|
7977
|
+
};
|
|
7978
|
+
}
|
|
7979
|
+
return { available: true };
|
|
7980
|
+
}
|
|
7981
|
+
async mergedChangeFor(sha) {
|
|
7982
|
+
if (this.identity === null) return null;
|
|
7983
|
+
return this.lookup(this.identity, sha);
|
|
7984
|
+
}
|
|
7985
|
+
get bin() {
|
|
7986
|
+
return this.spawnOpts.bin ?? this.tool;
|
|
7987
|
+
}
|
|
7988
|
+
run(args) {
|
|
7989
|
+
return run(this.bin, args, this.spawnOpts);
|
|
7990
|
+
}
|
|
7991
|
+
timeout(args) {
|
|
7992
|
+
const ms = this.spawnOpts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
7993
|
+
return new DocmetaError(
|
|
7994
|
+
`${commandLine(this.tool, args)} timed out after ${ms}ms`
|
|
7995
|
+
);
|
|
7996
|
+
}
|
|
7997
|
+
/**
|
|
7998
|
+
* Run a call whose stdout is JSON. A not-found answer is `null`; any other
|
|
7999
|
+
* failure throws, naming the command the user can re-run by hand. The
|
|
8000
|
+
* message quotes the tool's own name rather than `bin`, which under test
|
|
8001
|
+
* is `node`.
|
|
8002
|
+
*/
|
|
8003
|
+
async json(args) {
|
|
8004
|
+
let r;
|
|
8005
|
+
try {
|
|
8006
|
+
r = await this.run(args);
|
|
8007
|
+
} catch (err) {
|
|
8008
|
+
if (err instanceof BinMissing) {
|
|
8009
|
+
throw new DocmetaError(
|
|
8010
|
+
`${commandLine(this.tool, args)} failed: ${this.tool} is not on PATH`
|
|
8011
|
+
);
|
|
8012
|
+
}
|
|
8013
|
+
throw err;
|
|
8014
|
+
}
|
|
8015
|
+
const cmd = commandLine(this.tool, args);
|
|
8016
|
+
if (r.timedOut) throw this.timeout(args);
|
|
8017
|
+
if (r.tooLarge) {
|
|
8018
|
+
throw new DocmetaError(`${cmd} wrote more output than manni will read`);
|
|
8019
|
+
}
|
|
8020
|
+
if (r.code !== 0) {
|
|
8021
|
+
if (isNotFound(r)) return null;
|
|
8022
|
+
const detail = lastLine(r.stderr) || lastLine(r.stdout);
|
|
8023
|
+
const exit = r.code === null ? "signal" : `exit ${r.code}`;
|
|
8024
|
+
throw new DocmetaError(
|
|
8025
|
+
`${cmd} failed (${exit})${detail === "" ? "" : `: ${detail}`}`
|
|
8026
|
+
);
|
|
8027
|
+
}
|
|
8028
|
+
try {
|
|
8029
|
+
return JSON.parse(r.stdout);
|
|
8030
|
+
} catch {
|
|
8031
|
+
throw new DocmetaError(`${cmd} did not return JSON`);
|
|
8032
|
+
}
|
|
8033
|
+
}
|
|
8034
|
+
};
|
|
8035
|
+
function isRecord2(v) {
|
|
8036
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
8037
|
+
}
|
|
8038
|
+
function items(v) {
|
|
8039
|
+
return Array.isArray(v) ? v.filter(isRecord2) : [];
|
|
8040
|
+
}
|
|
8041
|
+
function numberOf(v) {
|
|
8042
|
+
return typeof v === "number" && Number.isInteger(v) ? v : null;
|
|
8043
|
+
}
|
|
8044
|
+
function stringOf(v) {
|
|
8045
|
+
return typeof v === "string" && v !== "" ? v : null;
|
|
8046
|
+
}
|
|
8047
|
+
function timestampOf(v) {
|
|
8048
|
+
if (v === void 0) return void 0;
|
|
8049
|
+
return stringOf(v);
|
|
8050
|
+
}
|
|
8051
|
+
function normalizeLogin(login) {
|
|
8052
|
+
return login.replace(/\[bot\]$/, "");
|
|
8053
|
+
}
|
|
8054
|
+
function latestApprovals(reviews) {
|
|
8055
|
+
const latest = /* @__PURE__ */ new Map();
|
|
8056
|
+
for (const r of reviews) {
|
|
8057
|
+
if (r.state !== "APPROVED") continue;
|
|
8058
|
+
const key = normalizeLogin(r.login);
|
|
8059
|
+
const seen = latest.get(key);
|
|
8060
|
+
if (seen === void 0 || r.submittedAt > seen.submittedAt) {
|
|
8061
|
+
latest.set(key, { login: r.login, submittedAt: r.submittedAt });
|
|
8062
|
+
}
|
|
8063
|
+
}
|
|
8064
|
+
return byTime([...latest.values()]);
|
|
8065
|
+
}
|
|
8066
|
+
function byTime(approvals) {
|
|
8067
|
+
return [...approvals].sort(
|
|
8068
|
+
(a, b) => a.submittedAt < b.submittedAt ? -1 : a.submittedAt > b.submittedAt ? 1 : 0
|
|
8069
|
+
);
|
|
8070
|
+
}
|
|
8071
|
+
var GitHubClient = class extends CliClient {
|
|
8072
|
+
tool = "gh";
|
|
8073
|
+
async lookup(identity, sha) {
|
|
8074
|
+
const { host, project: project2 } = identity;
|
|
8075
|
+
const api = (endpoint) => this.json(["api", "--hostname", host, endpoint]);
|
|
8076
|
+
let candidates = items(await api(`repos/${project2}/commits/${sha}/pulls`)).map(
|
|
8077
|
+
(p) => ({ number: numberOf(p.number), mergedAt: timestampOf(p.merged_at) })
|
|
8078
|
+
);
|
|
8079
|
+
if (candidates.length === 0) {
|
|
8080
|
+
const found = await this.json([
|
|
8081
|
+
"pr",
|
|
8082
|
+
"list",
|
|
8083
|
+
"--search",
|
|
8084
|
+
sha,
|
|
8085
|
+
"--state",
|
|
8086
|
+
"merged",
|
|
8087
|
+
"--json",
|
|
8088
|
+
"number,mergedAt",
|
|
8089
|
+
"-R",
|
|
8090
|
+
`${host}/${project2}`
|
|
8091
|
+
]);
|
|
8092
|
+
candidates = items(found).map((p) => ({
|
|
8093
|
+
number: numberOf(p.number),
|
|
8094
|
+
mergedAt: timestampOf(p.mergedAt)
|
|
8095
|
+
}));
|
|
8096
|
+
}
|
|
8097
|
+
for (const c of candidates) {
|
|
8098
|
+
if (c.number === null) continue;
|
|
8099
|
+
let mergedAt = c.mergedAt;
|
|
8100
|
+
if (mergedAt === void 0) {
|
|
8101
|
+
const pr = await api(`repos/${project2}/pulls/${c.number}`);
|
|
8102
|
+
mergedAt = isRecord2(pr) ? stringOf(pr.merged_at) ?? null : null;
|
|
8103
|
+
}
|
|
8104
|
+
if (mergedAt === null) continue;
|
|
8105
|
+
const raw = [];
|
|
8106
|
+
for (let page = 1; ; page += 1) {
|
|
8107
|
+
const batch = items(
|
|
8108
|
+
await api(`repos/${project2}/pulls/${c.number}/reviews?per_page=100&page=${page}`)
|
|
8109
|
+
);
|
|
8110
|
+
raw.push(...batch);
|
|
8111
|
+
if (batch.length < 100) break;
|
|
8112
|
+
}
|
|
8113
|
+
const reviews = raw.map((r) => ({
|
|
8114
|
+
login: isRecord2(r.user) ? stringOf(r.user.login) : null,
|
|
8115
|
+
state: stringOf(r.state),
|
|
8116
|
+
submittedAt: stringOf(r.submitted_at)
|
|
8117
|
+
})).filter(
|
|
8118
|
+
(r) => r.login !== null && r.state !== null && r.submittedAt !== null
|
|
8119
|
+
);
|
|
8120
|
+
return { id: c.number, mergedAt, approvals: latestApprovals(reviews) };
|
|
8121
|
+
}
|
|
8122
|
+
return null;
|
|
8123
|
+
}
|
|
8124
|
+
};
|
|
8125
|
+
var GitLabClient = class extends CliClient {
|
|
8126
|
+
tool = "glab";
|
|
8127
|
+
async lookup(identity, sha) {
|
|
8128
|
+
const { host } = identity;
|
|
8129
|
+
const api = (endpoint) => this.json(["api", "--hostname", host, endpoint]);
|
|
8130
|
+
const merged = items(
|
|
8131
|
+
await api(`projects/:fullpath/repository/commits/${sha}/merge_requests`)
|
|
8132
|
+
).map((mr) => ({ iid: numberOf(mr.iid), mergedAt: stringOf(mr.merged_at) })).find(
|
|
8133
|
+
(mr) => mr.iid !== null && mr.mergedAt !== null
|
|
8134
|
+
);
|
|
8135
|
+
if (merged === void 0) return null;
|
|
8136
|
+
const answer = await api(`projects/:fullpath/merge_requests/${merged.iid}/approvals`);
|
|
8137
|
+
const approvedBy = isRecord2(answer) ? items(answer.approved_by) : [];
|
|
8138
|
+
const approvals = [];
|
|
8139
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8140
|
+
for (const entry of approvedBy) {
|
|
8141
|
+
const login = isRecord2(entry.user) ? stringOf(entry.user.username) : null;
|
|
8142
|
+
if (login === null) continue;
|
|
8143
|
+
const key = normalizeLogin(login);
|
|
8144
|
+
if (seen.has(key)) continue;
|
|
8145
|
+
seen.add(key);
|
|
8146
|
+
approvals.push({ login, submittedAt: merged.mergedAt });
|
|
8147
|
+
}
|
|
8148
|
+
return { id: merged.iid, mergedAt: merged.mergedAt, approvals };
|
|
8149
|
+
}
|
|
8150
|
+
};
|
|
8151
|
+
async function createReviewClient(root, opts) {
|
|
8152
|
+
const spawnOpts = { ...opts.spawn, cwd: opts.spawn?.cwd ?? root };
|
|
8153
|
+
const origin = await originUrl(root);
|
|
8154
|
+
const parsed2 = origin === null ? null : parseOriginUrl(origin);
|
|
8155
|
+
const identity = parsed2 === null ? null : identityFromOrigin(parsed2.host, parsed2.project, opts.requested);
|
|
8156
|
+
if (parsed2 !== null && identity === null) return new UnknownHostClient(parsed2.host);
|
|
8157
|
+
const client = identity?.kind === "gitlab" ? new GitLabClient(identity, spawnOpts) : new GitHubClient(identity, spawnOpts);
|
|
8158
|
+
const cache = new ReviewCache(resolvePath3(root, opts.cacheDir), opts.cache);
|
|
8159
|
+
return cachedClient(client, cache, identity);
|
|
8160
|
+
}
|
|
8161
|
+
async function originUrl(root) {
|
|
8162
|
+
try {
|
|
8163
|
+
const r = await run("git", ["remote", "get-url", "origin"], { cwd: root });
|
|
8164
|
+
if (r.code !== 0 || r.timedOut || r.tooLarge) return null;
|
|
8165
|
+
return stringOf(r.stdout.trim());
|
|
8166
|
+
} catch {
|
|
8167
|
+
return null;
|
|
8168
|
+
}
|
|
8169
|
+
}
|
|
8170
|
+
var NOTHING = { "reviewed-by": null, "last-reviewed": null };
|
|
8171
|
+
async function deriveFromReviews(inputs, client) {
|
|
8172
|
+
const records = /* @__PURE__ */ new Map();
|
|
8173
|
+
const status = await client.status();
|
|
8174
|
+
if (!status.available) return { status, records };
|
|
8175
|
+
const identity = await client.detect();
|
|
8176
|
+
const kind = identity?.kind ?? "github";
|
|
8177
|
+
const bySha = /* @__PURE__ */ new Map();
|
|
8178
|
+
const changeFor = (sha) => {
|
|
8179
|
+
let pending = bySha.get(sha);
|
|
8180
|
+
if (pending === void 0) {
|
|
8181
|
+
pending = client.mergedChangeFor(sha);
|
|
8182
|
+
bySha.set(sha, pending);
|
|
8183
|
+
}
|
|
8184
|
+
return pending;
|
|
8185
|
+
};
|
|
8186
|
+
for (const input of inputs) {
|
|
8187
|
+
if (input.sha === null) {
|
|
8188
|
+
records.set(input.label, NOTHING);
|
|
8189
|
+
continue;
|
|
8190
|
+
}
|
|
8191
|
+
const change2 = await changeFor(input.sha);
|
|
8192
|
+
records.set(input.label, factsFrom(change2, kind));
|
|
8193
|
+
}
|
|
8194
|
+
return { status, records };
|
|
8195
|
+
}
|
|
8196
|
+
function factsFrom(change2, kind) {
|
|
8197
|
+
if (change2 === null || change2.approvals.length === 0) return NOTHING;
|
|
8198
|
+
const ordered = byTime(change2.approvals);
|
|
8199
|
+
const latest = ordered.at(-1);
|
|
8200
|
+
if (latest === void 0) return NOTHING;
|
|
8201
|
+
const evidence = kind === "gitlab" ? `gitlab MR !${change2.id}` : `github PR #${change2.id}`;
|
|
8202
|
+
const dateEvidence = kind === "gitlab" ? `${evidence} (merge date; GitLab records no approval time)` : evidence;
|
|
8203
|
+
return {
|
|
8204
|
+
"reviewed-by": {
|
|
8205
|
+
value: ordered.map((a) => a.login),
|
|
8206
|
+
source: kind,
|
|
8207
|
+
evidence
|
|
8208
|
+
},
|
|
8209
|
+
"last-reviewed": {
|
|
8210
|
+
value: utcDate(latest.submittedAt),
|
|
8211
|
+
source: kind,
|
|
8212
|
+
evidence: dateEvidence
|
|
8213
|
+
}
|
|
8214
|
+
};
|
|
8215
|
+
}
|
|
8216
|
+
function utcDate(iso) {
|
|
8217
|
+
const ms = Date.parse(iso);
|
|
8218
|
+
if (Number.isNaN(ms)) return iso;
|
|
8219
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
8220
|
+
}
|
|
8221
|
+
|
|
8222
|
+
// src/meta/core/derive/index.ts
|
|
8223
|
+
import { dirname as dirname7, resolve as resolve9 } from "node:path";
|
|
8224
|
+
|
|
8225
|
+
// src/meta/core/derive/codeowners.ts
|
|
8226
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
8227
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
8228
|
+
import { dirname as dirname5, relative as relative7, resolve as resolve8, sep as sep4 } from "node:path";
|
|
8229
|
+
import picomatch4 from "picomatch";
|
|
8230
|
+
var CODEOWNERS_LOCATIONS = [
|
|
8231
|
+
".github/CODEOWNERS",
|
|
8232
|
+
"CODEOWNERS",
|
|
8233
|
+
"docs/CODEOWNERS",
|
|
8234
|
+
".gitlab/CODEOWNERS"
|
|
8235
|
+
];
|
|
8236
|
+
function findCodeowners(root, explicit) {
|
|
8237
|
+
if (explicit !== void 0) {
|
|
8238
|
+
const path = resolve8(root, explicit);
|
|
8239
|
+
return existsSync2(path) ? path : null;
|
|
8240
|
+
}
|
|
8241
|
+
for (const loc of CODEOWNERS_LOCATIONS) {
|
|
8242
|
+
const path = resolve8(root, ...loc.split("/"));
|
|
8243
|
+
if (existsSync2(path)) return path;
|
|
8244
|
+
}
|
|
8245
|
+
return null;
|
|
8246
|
+
}
|
|
8247
|
+
var SECTION_HEADER = /^(\^?)\[([^\]]+)\](?:\[(\d+)\])?(.*)$/;
|
|
8248
|
+
var TOKEN = /(?:\\.|[^\s\\])+/g;
|
|
8249
|
+
function parseCodeowners(text2, path, label) {
|
|
8250
|
+
const rules = [];
|
|
8251
|
+
const sections = [];
|
|
8252
|
+
let current;
|
|
8253
|
+
const lines = text2.split(/\r?\n/);
|
|
8254
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
8255
|
+
const raw = lines[i] ?? "";
|
|
8256
|
+
const line = raw.trim();
|
|
8257
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
8258
|
+
if (line.startsWith("[") || line.startsWith("^[")) {
|
|
8259
|
+
const m = SECTION_HEADER.exec(line);
|
|
8260
|
+
if (!m) continue;
|
|
8261
|
+
const [, caret, rawName, approvals, rest] = m;
|
|
8262
|
+
const name2 = (rawName ?? "").trim();
|
|
8263
|
+
if (name2 === "") continue;
|
|
8264
|
+
const owners2 = tokens(rest ?? "");
|
|
8265
|
+
const existing = sections.find(
|
|
8266
|
+
(s) => s.name.toLowerCase() === name2.toLowerCase()
|
|
8267
|
+
);
|
|
8268
|
+
if (existing) {
|
|
8269
|
+
if (owners2.length > 0) existing.defaultOwners = owners2;
|
|
8270
|
+
current = existing;
|
|
8271
|
+
continue;
|
|
8272
|
+
}
|
|
8273
|
+
const section = {
|
|
8274
|
+
name: name2,
|
|
8275
|
+
optional: caret === "^",
|
|
8276
|
+
...approvals !== void 0 ? { approvals: Number(approvals) } : {},
|
|
8277
|
+
defaultOwners: owners2
|
|
8278
|
+
};
|
|
8279
|
+
sections.push(section);
|
|
8280
|
+
current = section;
|
|
8281
|
+
continue;
|
|
8282
|
+
}
|
|
8283
|
+
const [first, ...owners] = tokens(line);
|
|
8284
|
+
if (first === void 0) continue;
|
|
8285
|
+
const pattern = unescapePattern(first);
|
|
8286
|
+
if (pattern === "" || pattern === "/") continue;
|
|
8287
|
+
rules.push({
|
|
8288
|
+
pattern,
|
|
8289
|
+
owners,
|
|
8290
|
+
line: i + 1,
|
|
8291
|
+
...current ? { section: current.name } : {}
|
|
8292
|
+
});
|
|
8293
|
+
}
|
|
8294
|
+
return { path, label, rules, sections };
|
|
8295
|
+
}
|
|
8296
|
+
function tokens(s) {
|
|
8297
|
+
return s.match(TOKEN) ?? [];
|
|
8298
|
+
}
|
|
8299
|
+
function unescapePattern(token) {
|
|
8300
|
+
return token.replace(/\\ /g, " ").replace(/^\\#/, "#");
|
|
8301
|
+
}
|
|
8302
|
+
function ownersFor(file, relPath) {
|
|
8303
|
+
const matchers = matchersFor(file);
|
|
8304
|
+
const defaults = new Map(file.sections.map((s) => [s.name, s.defaultOwners]));
|
|
8305
|
+
const order = [void 0, ...file.sections.map((s) => s.name)];
|
|
8306
|
+
const winners = /* @__PURE__ */ new Map();
|
|
8307
|
+
file.rules.forEach((rule, i) => {
|
|
8308
|
+
if (matchers[i]?.(relPath)) winners.set(rule.section, rule);
|
|
8309
|
+
});
|
|
8310
|
+
if (winners.size === 0) return null;
|
|
8311
|
+
const owners = [];
|
|
8312
|
+
let line = 0;
|
|
8313
|
+
let lineFromOwner = false;
|
|
8314
|
+
for (const section of order) {
|
|
8315
|
+
const rule = winners.get(section);
|
|
8316
|
+
if (!rule) continue;
|
|
8317
|
+
const chosen = rule.owners.length > 0 ? rule.owners : (section === void 0 ? void 0 : defaults.get(section)) ?? [];
|
|
8318
|
+
if (chosen.length > 0) {
|
|
8319
|
+
line = rule.line;
|
|
8320
|
+
lineFromOwner = true;
|
|
8321
|
+
} else if (!lineFromOwner) {
|
|
8322
|
+
line = rule.line;
|
|
8323
|
+
}
|
|
8324
|
+
for (const o of chosen) if (!owners.includes(o)) owners.push(o);
|
|
8325
|
+
}
|
|
8326
|
+
return { owners, line };
|
|
8327
|
+
}
|
|
8328
|
+
var matcherCache2 = /* @__PURE__ */ new WeakMap();
|
|
8329
|
+
function matchersFor(file) {
|
|
8330
|
+
let matchers = matcherCache2.get(file);
|
|
8331
|
+
if (!matchers) {
|
|
8332
|
+
matchers = file.rules.map((r) => picomatch4(toGlobs(r.pattern), PICOMATCH));
|
|
8333
|
+
matcherCache2.set(file, matchers);
|
|
8334
|
+
}
|
|
8335
|
+
return matchers;
|
|
8336
|
+
}
|
|
8337
|
+
var PICOMATCH = { dot: true, windows: false };
|
|
8338
|
+
function toGlobs(pattern) {
|
|
8339
|
+
let p = pattern;
|
|
8340
|
+
const dirOnly = p.endsWith("/");
|
|
8341
|
+
if (dirOnly) p = p.slice(0, -1);
|
|
8342
|
+
let anchored = false;
|
|
8343
|
+
if (p.startsWith("/")) {
|
|
8344
|
+
anchored = true;
|
|
8345
|
+
p = p.slice(1);
|
|
8346
|
+
} else if (p.includes("/") && !p.startsWith("**/")) {
|
|
8347
|
+
anchored = true;
|
|
8348
|
+
}
|
|
8349
|
+
const base = anchored || p.startsWith("**/") ? p : `**/${p}`;
|
|
8350
|
+
if (dirOnly) return [`${base}/**`];
|
|
8351
|
+
const last = p.slice(p.lastIndexOf("/") + 1);
|
|
8352
|
+
return /[*?[]/.test(last) ? [base] : [base, `${base}/**`];
|
|
8353
|
+
}
|
|
8354
|
+
var NOT_FOUND_REASON = `no CODEOWNERS file found (looked for ${CODEOWNERS_LOCATIONS.join(", ")})`;
|
|
8355
|
+
async function deriveFromCodeowners(inputs, opts) {
|
|
8356
|
+
const records = /* @__PURE__ */ new Map();
|
|
8357
|
+
const explicitPath = opts.explicit === void 0 ? void 0 : resolve8(opts.configDir ?? process.cwd(), opts.explicit);
|
|
8358
|
+
if (explicitPath !== void 0 && !existsSync2(explicitPath)) {
|
|
8359
|
+
return {
|
|
8360
|
+
status: { available: false, reason: `CODEOWNERS not found at ${explicitPath}` },
|
|
8361
|
+
records
|
|
8362
|
+
};
|
|
8363
|
+
}
|
|
8364
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
8365
|
+
const rootCache = /* @__PURE__ */ new Map();
|
|
8366
|
+
for (const input of inputs) {
|
|
8367
|
+
const dir = dirname5(input.absPath);
|
|
8368
|
+
let root = rootCache.get(dir);
|
|
8369
|
+
if (root === void 0) {
|
|
8370
|
+
root = opts.rootOf(input.absPath);
|
|
8371
|
+
rootCache.set(dir, root);
|
|
8372
|
+
}
|
|
8373
|
+
if (root === null) continue;
|
|
8374
|
+
const bucket = byRoot.get(root);
|
|
8375
|
+
if (bucket) bucket.push(input);
|
|
8376
|
+
else byRoot.set(root, [input]);
|
|
8377
|
+
}
|
|
8378
|
+
let found = 0;
|
|
8379
|
+
const parsed2 = /* @__PURE__ */ new Map();
|
|
8380
|
+
for (const [root, bucket] of byRoot) {
|
|
8381
|
+
const path = explicitPath ?? findCodeowners(root);
|
|
8382
|
+
if (path === null) {
|
|
8383
|
+
for (const input of bucket) records.set(input.label, null);
|
|
8384
|
+
continue;
|
|
8385
|
+
}
|
|
8386
|
+
found += 1;
|
|
8387
|
+
const label = toPosix2(relative7(opts.configDir ?? root, path));
|
|
8388
|
+
const key = `${path}\0${label}`;
|
|
8389
|
+
let file = parsed2.get(key);
|
|
8390
|
+
if (!file) {
|
|
8391
|
+
file = readFile5(path, "utf8").then(
|
|
8392
|
+
(text2) => parseCodeowners(text2, path, label)
|
|
8393
|
+
);
|
|
8394
|
+
parsed2.set(key, file);
|
|
8395
|
+
}
|
|
8396
|
+
let owners;
|
|
8397
|
+
try {
|
|
8398
|
+
owners = await file;
|
|
8399
|
+
} catch (err) {
|
|
8400
|
+
return {
|
|
8401
|
+
status: {
|
|
8402
|
+
available: false,
|
|
8403
|
+
reason: `CODEOWNERS at ${label} could not be read: ${errorMessage(err)}`
|
|
8404
|
+
},
|
|
8405
|
+
records
|
|
8406
|
+
};
|
|
8407
|
+
}
|
|
8408
|
+
for (const input of bucket) {
|
|
8409
|
+
const rel = toPosix2(relative7(root, input.absPath));
|
|
8410
|
+
const hit = ownersFor(owners, rel);
|
|
8411
|
+
records.set(
|
|
8412
|
+
input.label,
|
|
8413
|
+
hit === null ? null : {
|
|
8414
|
+
value: hit.owners,
|
|
8415
|
+
source: "codeowners",
|
|
8416
|
+
evidence: `${label}:${hit.line}`
|
|
8417
|
+
}
|
|
8418
|
+
);
|
|
8419
|
+
}
|
|
8420
|
+
}
|
|
8421
|
+
if (byRoot.size === 0) {
|
|
8422
|
+
return {
|
|
8423
|
+
status: { available: false, reason: "no git repository contains the documents" },
|
|
8424
|
+
records
|
|
8425
|
+
};
|
|
8426
|
+
}
|
|
8427
|
+
if (found === 0 && explicitPath === void 0) {
|
|
8428
|
+
return { status: { available: true, reason: NOT_FOUND_REASON }, records };
|
|
8429
|
+
}
|
|
8430
|
+
return { status: { available: true }, records };
|
|
8431
|
+
}
|
|
8432
|
+
function toPosix2(p) {
|
|
8433
|
+
return p.split(sep4).join("/");
|
|
8434
|
+
}
|
|
8435
|
+
|
|
8436
|
+
// src/meta/core/derive/git.ts
|
|
8437
|
+
import { constants as bufferConstants } from "node:buffer";
|
|
8438
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
8439
|
+
import { dirname as dirname6, extname as extname2, relative as relative8, sep as sep5 } from "node:path";
|
|
8440
|
+
var DEFAULT_BULK_THRESHOLD = 32;
|
|
8441
|
+
var NULL_SHA = "0".repeat(40);
|
|
8442
|
+
var MAX_GIT_OUTPUT_BYTES = Math.min(
|
|
8443
|
+
512 * 1024 * 1024,
|
|
8444
|
+
bufferConstants.MAX_STRING_LENGTH
|
|
8445
|
+
);
|
|
8446
|
+
var HistoryTooLarge = class extends Error {
|
|
8447
|
+
};
|
|
8448
|
+
var BlobsUnreadable = class extends Error {
|
|
8449
|
+
constructor(detail) {
|
|
8450
|
+
super(detail);
|
|
8451
|
+
this.detail = detail;
|
|
8452
|
+
}
|
|
8453
|
+
detail;
|
|
8454
|
+
};
|
|
8455
|
+
function bodyOf(content, fenced) {
|
|
8456
|
+
if (fenced !== true) return content;
|
|
8457
|
+
const loc = locateFrontmatter(content);
|
|
8458
|
+
if (!loc) return content;
|
|
8459
|
+
return content.slice(0, loc.openStart) + content.slice(loc.closeEnd);
|
|
8460
|
+
}
|
|
8461
|
+
async function deriveFromGit(inputs, opts) {
|
|
8462
|
+
const records = /* @__PURE__ */ new Map();
|
|
8463
|
+
const byRoot = groupByRoot(inputs);
|
|
8464
|
+
if (byRoot.size === 0) {
|
|
8465
|
+
return {
|
|
8466
|
+
status: {
|
|
8467
|
+
available: false,
|
|
8468
|
+
reason: "no git repository contains the documents"
|
|
8469
|
+
},
|
|
8470
|
+
records
|
|
8471
|
+
};
|
|
8472
|
+
}
|
|
8473
|
+
const threshold = opts.bulkThreshold ?? DEFAULT_BULK_THRESHOLD;
|
|
8474
|
+
const maxBytes = opts.maxOutputBytes ?? MAX_GIT_OUTPUT_BYTES;
|
|
8475
|
+
const reasons = [];
|
|
8476
|
+
for (const [root, bucket] of byRoot) {
|
|
8477
|
+
const run2 = (args, stdin) => runGit(args, root, stdin, maxBytes);
|
|
8478
|
+
const shallow = await run2(["rev-parse", "--is-shallow-repository"]);
|
|
8479
|
+
if (shallow.missing) {
|
|
8480
|
+
return {
|
|
8481
|
+
status: { available: false, reason: "git is not on PATH" },
|
|
8482
|
+
records: /* @__PURE__ */ new Map()
|
|
8483
|
+
};
|
|
8484
|
+
}
|
|
8485
|
+
if (shallow.code !== 0) {
|
|
8486
|
+
reasons.push(`git could not read the repository at ${root}`);
|
|
8487
|
+
continue;
|
|
8488
|
+
}
|
|
8489
|
+
if (text(shallow).trim() === "true") {
|
|
8490
|
+
reasons.push(
|
|
8491
|
+
`this checkout is shallow; use actions/checkout with fetch-depth: 0 (${root})`
|
|
8492
|
+
);
|
|
8493
|
+
continue;
|
|
8494
|
+
}
|
|
8495
|
+
let histories;
|
|
8496
|
+
let blobs;
|
|
8497
|
+
try {
|
|
8498
|
+
histories = bucket.length <= threshold ? await perFileHistories(run2, bucket) : await bulkHistories(run2, bucket);
|
|
8499
|
+
if (histories === null) {
|
|
8500
|
+
reasons.push(`git could not read the history at ${root}`);
|
|
8501
|
+
continue;
|
|
8502
|
+
}
|
|
8503
|
+
blobs = await fetchBlobs(run2, neededBlobs(histories));
|
|
8504
|
+
} catch (err) {
|
|
8505
|
+
if (err instanceof BlobsUnreadable) {
|
|
8506
|
+
reasons.push(`git cat-file could not read the history (${root}): ${err.detail}`);
|
|
8507
|
+
continue;
|
|
8508
|
+
}
|
|
8509
|
+
if (!(err instanceof HistoryTooLarge)) throw err;
|
|
8510
|
+
reasons.push(`git history is too large to read in one pass (${root})`);
|
|
8511
|
+
continue;
|
|
8512
|
+
}
|
|
8513
|
+
for (const entry of bucket) {
|
|
8514
|
+
const history = histories.get(entry.rel) ?? [];
|
|
8515
|
+
records.set(
|
|
8516
|
+
entry.input.label,
|
|
8517
|
+
judge(entry.input, root, history, blobs, opts.now)
|
|
8518
|
+
);
|
|
8519
|
+
}
|
|
8520
|
+
}
|
|
8521
|
+
if (reasons.length > 0) {
|
|
8522
|
+
return {
|
|
8523
|
+
status: { available: false, reason: reasons.join("; ") },
|
|
8524
|
+
records: /* @__PURE__ */ new Map()
|
|
8525
|
+
};
|
|
8526
|
+
}
|
|
8527
|
+
return { status: { available: true }, records };
|
|
8528
|
+
}
|
|
8529
|
+
function groupByRoot(inputs) {
|
|
8530
|
+
const rootOf = /* @__PURE__ */ new Map();
|
|
8531
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
8532
|
+
for (const input of inputs) {
|
|
8533
|
+
const dir = dirname6(input.absPath);
|
|
8534
|
+
let root = rootOf.get(dir);
|
|
8535
|
+
if (root === void 0) {
|
|
8536
|
+
root = findGitRoot(dir);
|
|
8537
|
+
rootOf.set(dir, root);
|
|
8538
|
+
}
|
|
8539
|
+
if (root === null) continue;
|
|
8540
|
+
const rel = relative8(root, input.absPath).split(sep5).join("/");
|
|
8541
|
+
const bucket = byRoot.get(root);
|
|
8542
|
+
if (bucket) bucket.push({ input, rel });
|
|
8543
|
+
else byRoot.set(root, [{ input, rel }]);
|
|
8544
|
+
}
|
|
8545
|
+
return byRoot;
|
|
8546
|
+
}
|
|
8547
|
+
var RECORD_FORMAT = "--format=%x1e%H%x00%aI%x00%an%x00%ae%x00%(trailers:key=Co-authored-by,valueonly,unfold,separator=%x1f)%x00%(trailers:key=Reviewed-by,valueonly,unfold,separator=%x1f)";
|
|
8548
|
+
var LOG_ARGS = [
|
|
8549
|
+
"-c",
|
|
8550
|
+
"core.quotePath=false",
|
|
8551
|
+
"log",
|
|
8552
|
+
"--raw",
|
|
8553
|
+
"-M",
|
|
8554
|
+
"--no-abbrev",
|
|
8555
|
+
RECORD_FORMAT
|
|
8556
|
+
];
|
|
8557
|
+
async function perFileHistories(run2, bucket) {
|
|
8558
|
+
const histories = /* @__PURE__ */ new Map();
|
|
8559
|
+
for (const entry of bucket) {
|
|
8560
|
+
const out = await run2([...LOG_ARGS, "--follow", "--", pathspec(entry.rel)]);
|
|
8561
|
+
if (out.tooLarge) throw new HistoryTooLarge();
|
|
8562
|
+
if (out.code !== 0) {
|
|
8563
|
+
if (await isUnbornHead(run2)) return histories;
|
|
8564
|
+
return null;
|
|
8565
|
+
}
|
|
8566
|
+
const attributed = attribute(parseLog(text(out)), [entry.rel]);
|
|
8567
|
+
histories.set(entry.rel, attributed.get(entry.rel) ?? []);
|
|
8568
|
+
}
|
|
8569
|
+
return histories;
|
|
8570
|
+
}
|
|
8571
|
+
async function bulkHistories(run2, bucket) {
|
|
8572
|
+
const out = await run2(LOG_ARGS);
|
|
8573
|
+
if (out.tooLarge) throw new HistoryTooLarge();
|
|
8574
|
+
if (out.code !== 0) {
|
|
8575
|
+
if (await isUnbornHead(run2)) return /* @__PURE__ */ new Map();
|
|
8576
|
+
return null;
|
|
8577
|
+
}
|
|
8578
|
+
return attribute(
|
|
8579
|
+
parseLog(text(out)),
|
|
8580
|
+
bucket.map((e) => e.rel)
|
|
8581
|
+
);
|
|
8582
|
+
}
|
|
8583
|
+
function pathspec(rel) {
|
|
8584
|
+
return /[*?[\]]/.test(rel) ? `:(literal)${rel}` : rel;
|
|
8585
|
+
}
|
|
8586
|
+
async function isUnbornHead(run2) {
|
|
8587
|
+
const out = await run2(["rev-parse", "--verify", "--quiet", "HEAD^{commit}"]);
|
|
8588
|
+
return !out.missing && out.code !== 0;
|
|
8589
|
+
}
|
|
8590
|
+
function parseLog(text2) {
|
|
8591
|
+
const records = [];
|
|
8592
|
+
for (const chunk of text2.split("")) {
|
|
8593
|
+
if (chunk === "") continue;
|
|
8594
|
+
const nl = chunk.indexOf("\n");
|
|
8595
|
+
const header = nl === -1 ? chunk : chunk.slice(0, nl);
|
|
8596
|
+
const fields = header.split("\0");
|
|
8597
|
+
const sha = fields[0];
|
|
8598
|
+
if (sha === void 0 || sha === "") continue;
|
|
8599
|
+
const raw = [];
|
|
8600
|
+
if (nl !== -1) {
|
|
8601
|
+
for (const line of chunk.slice(nl + 1).split("\n")) {
|
|
8602
|
+
const entry = parseRawLine(line);
|
|
8603
|
+
if (entry) raw.push(entry);
|
|
8604
|
+
}
|
|
8605
|
+
}
|
|
8606
|
+
records.push({
|
|
8607
|
+
sha,
|
|
8608
|
+
authorDate: fields[1] ?? "",
|
|
8609
|
+
authorName: fields[2] ?? "",
|
|
8610
|
+
authorEmail: fields[3] ?? "",
|
|
8611
|
+
coAuthors: splitTrailers(fields[4]),
|
|
8612
|
+
reviewedBy: splitTrailers(fields[5]),
|
|
8613
|
+
raw
|
|
8614
|
+
});
|
|
8615
|
+
}
|
|
8616
|
+
return records;
|
|
8617
|
+
}
|
|
8618
|
+
function splitTrailers(field) {
|
|
8619
|
+
if (field === void 0 || field === "") return [];
|
|
8620
|
+
return field.split("").map((s) => s.trim()).filter((s) => s !== "");
|
|
8621
|
+
}
|
|
8622
|
+
function parseRawLine(line) {
|
|
8623
|
+
if (!line.startsWith(":")) return null;
|
|
8624
|
+
const tab = line.indexOf(" ");
|
|
8625
|
+
if (tab === -1) return null;
|
|
8626
|
+
const meta = line.slice(1, tab).split(" ");
|
|
8627
|
+
const oldBlob = meta[2];
|
|
8628
|
+
const newBlob = meta[3];
|
|
8629
|
+
const status = meta[4];
|
|
8630
|
+
if (oldBlob === void 0 || newBlob === void 0 || status === void 0) {
|
|
8631
|
+
return null;
|
|
8632
|
+
}
|
|
8633
|
+
const paths = line.slice(tab + 1).split(" ").map(unquotePath);
|
|
8634
|
+
const first = paths[0];
|
|
8635
|
+
if (first === void 0) return null;
|
|
8636
|
+
const second = paths[1];
|
|
8637
|
+
const renamed = status.startsWith("R") || status.startsWith("C");
|
|
8638
|
+
return {
|
|
8639
|
+
status,
|
|
8640
|
+
oldBlob,
|
|
8641
|
+
newBlob,
|
|
8642
|
+
oldPath: first,
|
|
8643
|
+
newPath: renamed && second !== void 0 ? second : first
|
|
8644
|
+
};
|
|
8645
|
+
}
|
|
8646
|
+
function unquotePath(p) {
|
|
8647
|
+
if (p.length < 2 || !p.startsWith('"') || !p.endsWith('"')) return p;
|
|
8648
|
+
const inner = p.slice(1, -1);
|
|
8649
|
+
const bytes = [];
|
|
8650
|
+
for (let i = 0; i < inner.length; i += 1) {
|
|
8651
|
+
const ch = inner[i];
|
|
8652
|
+
if (ch !== "\\") {
|
|
8653
|
+
bytes.push(...Buffer.from(ch ?? "", "utf8"));
|
|
8654
|
+
continue;
|
|
8655
|
+
}
|
|
8656
|
+
const next = inner[i + 1];
|
|
8657
|
+
if (next === void 0) break;
|
|
8658
|
+
const octal = /^[0-7]{3}/.exec(inner.slice(i + 1));
|
|
8659
|
+
if (octal) {
|
|
8660
|
+
bytes.push(parseInt(octal[0], 8));
|
|
8661
|
+
i += 3;
|
|
8662
|
+
continue;
|
|
8663
|
+
}
|
|
8664
|
+
const simple = {
|
|
8665
|
+
a: 7,
|
|
8666
|
+
b: 8,
|
|
8667
|
+
t: 9,
|
|
8668
|
+
n: 10,
|
|
8669
|
+
v: 11,
|
|
8670
|
+
f: 12,
|
|
8671
|
+
r: 13,
|
|
8672
|
+
'"': 34,
|
|
8673
|
+
"\\": 92
|
|
8674
|
+
};
|
|
8675
|
+
const code = simple[next];
|
|
8676
|
+
if (code === void 0) bytes.push(...Buffer.from(next, "utf8"));
|
|
8677
|
+
else bytes.push(code);
|
|
8678
|
+
i += 1;
|
|
8679
|
+
}
|
|
8680
|
+
return Buffer.from(bytes).toString("utf8");
|
|
8681
|
+
}
|
|
8682
|
+
function attribute(records, wanted) {
|
|
8683
|
+
const histories = /* @__PURE__ */ new Map();
|
|
8684
|
+
const tracked = /* @__PURE__ */ new Map();
|
|
8685
|
+
for (const rel of wanted) {
|
|
8686
|
+
histories.set(rel, []);
|
|
8687
|
+
tracked.set(rel, rel);
|
|
8688
|
+
}
|
|
8689
|
+
for (const record of records) {
|
|
8690
|
+
if (record.raw.length === 0) continue;
|
|
8691
|
+
const byNewPath = /* @__PURE__ */ new Map();
|
|
8692
|
+
for (const entry of record.raw) byNewPath.set(entry.newPath, entry);
|
|
8693
|
+
for (const [rel, current] of tracked) {
|
|
8694
|
+
const entry = byNewPath.get(current);
|
|
8695
|
+
if (!entry) continue;
|
|
8696
|
+
histories.get(rel)?.push({
|
|
8697
|
+
sha: record.sha,
|
|
8698
|
+
authorDate: record.authorDate,
|
|
8699
|
+
authorName: record.authorName,
|
|
8700
|
+
authorEmail: record.authorEmail,
|
|
8701
|
+
coAuthors: record.coAuthors,
|
|
8702
|
+
reviewedBy: record.reviewedBy,
|
|
8703
|
+
oldBlob: entry.oldBlob === NULL_SHA ? null : entry.oldBlob,
|
|
8704
|
+
newBlob: entry.newBlob === NULL_SHA ? null : entry.newBlob,
|
|
8705
|
+
pathAtCommit: entry.newPath
|
|
8706
|
+
});
|
|
8707
|
+
if (entry.oldPath !== entry.newPath) tracked.set(rel, entry.oldPath);
|
|
8708
|
+
}
|
|
8709
|
+
}
|
|
8710
|
+
return histories;
|
|
8711
|
+
}
|
|
8712
|
+
function neededBlobs(histories) {
|
|
8713
|
+
const shas = /* @__PURE__ */ new Set();
|
|
8714
|
+
for (const history of histories.values()) {
|
|
8715
|
+
for (const h of history) {
|
|
8716
|
+
if (h.oldBlob !== null) shas.add(h.oldBlob);
|
|
8717
|
+
if (h.newBlob !== null) shas.add(h.newBlob);
|
|
8718
|
+
}
|
|
8719
|
+
}
|
|
8720
|
+
return shas;
|
|
8721
|
+
}
|
|
8722
|
+
async function fetchBlobs(run2, shas) {
|
|
8723
|
+
const blobs = /* @__PURE__ */ new Map();
|
|
8724
|
+
if (shas.size === 0) return blobs;
|
|
8725
|
+
const out = await run2(["cat-file", "--batch"], `${[...shas].join("\n")}
|
|
8726
|
+
`);
|
|
8727
|
+
if (out.tooLarge) throw new HistoryTooLarge();
|
|
8728
|
+
if (out.code !== 0) throw new BlobsUnreadable(`exit ${String(out.code)}`);
|
|
8729
|
+
const buf = out.raw;
|
|
8730
|
+
let cursor = 0;
|
|
8731
|
+
while (cursor < buf.length) {
|
|
8732
|
+
const nl = buf.indexOf(10, cursor);
|
|
8733
|
+
if (nl === -1) break;
|
|
8734
|
+
const header = buf.toString("utf8", cursor, nl).split(" ");
|
|
8735
|
+
cursor = nl + 1;
|
|
8736
|
+
const sha = header[0];
|
|
8737
|
+
const kind = header[1];
|
|
8738
|
+
const size = Number(header[2]);
|
|
8739
|
+
if (sha === void 0 || kind !== "blob" || !Number.isFinite(size)) {
|
|
8740
|
+
continue;
|
|
8741
|
+
}
|
|
8742
|
+
blobs.set(sha, buf.toString("utf8", cursor, cursor + size));
|
|
8743
|
+
cursor += size + 1;
|
|
8744
|
+
}
|
|
8745
|
+
return blobs;
|
|
8746
|
+
}
|
|
8747
|
+
var git = (value, evidence) => ({
|
|
8748
|
+
value,
|
|
8749
|
+
source: "git",
|
|
8750
|
+
evidence
|
|
8751
|
+
});
|
|
8752
|
+
function judge(input, root, history, blobs, now) {
|
|
8753
|
+
const fenced = input.extracted.fenced;
|
|
8754
|
+
const extractor = extractorFor(input);
|
|
8755
|
+
const blobBody = (sha) => sha === null ? null : bodyOf(blobs.get(sha) ?? "", fenced);
|
|
8756
|
+
const stampsOf = (sha) => {
|
|
8757
|
+
if (sha === null) return {};
|
|
8758
|
+
const content = blobs.get(sha);
|
|
8759
|
+
return content === void 0 ? {} : extractStamps(extractor, content, input.absPath);
|
|
8760
|
+
};
|
|
8761
|
+
const bodyChanging = [...history].reverse().filter((h) => !sameBody(blobBody(h.newBlob), blobBody(h.oldBlob)));
|
|
8762
|
+
const oldest = history[history.length - 1];
|
|
8763
|
+
const newestBody = bodyChanging[bodyChanging.length - 1];
|
|
8764
|
+
const head = history[0];
|
|
8765
|
+
const headBlob = head === void 0 ? null : head.newBlob;
|
|
8766
|
+
const created = oldest === void 0 ? null : createdOf(oldest, stampsOf(oldest.newBlob));
|
|
8767
|
+
const workingChanged = !sameBody(
|
|
8768
|
+
bodyOf(input.content, fenced),
|
|
8769
|
+
headBlob === null ? null : blobBody(headBlob)
|
|
8770
|
+
);
|
|
8771
|
+
let lastUpdated;
|
|
8772
|
+
let lastBodyCommit;
|
|
8773
|
+
if (workingChanged) {
|
|
8774
|
+
lastBodyCommit = null;
|
|
8775
|
+
const working = input.extracted.data["last-updated"];
|
|
8776
|
+
const committed = stampsOf(headBlob)["last-updated"];
|
|
8777
|
+
lastUpdated = working !== void 0 && working !== null && !sameValue(working, committed) ? git(working, "stamped in working tree") : git(localDate(now()), "uncommitted body change");
|
|
8778
|
+
} else if (newestBody === void 0) {
|
|
8779
|
+
lastBodyCommit = null;
|
|
8780
|
+
lastUpdated = null;
|
|
8781
|
+
} else {
|
|
8782
|
+
lastBodyCommit = newestBody.sha;
|
|
8783
|
+
lastUpdated = lastUpdatedOf(
|
|
8784
|
+
newestBody,
|
|
8785
|
+
stampsOf(newestBody.newBlob),
|
|
8786
|
+
stampsOf(newestBody.oldBlob)
|
|
8787
|
+
);
|
|
8788
|
+
}
|
|
8789
|
+
const authors = bodyChanging.length === 0 ? null : authorsOf(bodyChanging);
|
|
8790
|
+
let reviewedBy = null;
|
|
8791
|
+
let lastReviewed = null;
|
|
8792
|
+
if (!workingChanged && newestBody !== void 0 && newestBody.reviewedBy.length > 0) {
|
|
8793
|
+
const evidence = `Reviewed-by trailer in ${short(newestBody.sha)}`;
|
|
8794
|
+
reviewedBy = git(newestBody.reviewedBy.map(nameOf), evidence);
|
|
8795
|
+
lastReviewed = git(datePart(newestBody.authorDate), evidence);
|
|
8796
|
+
}
|
|
8797
|
+
return {
|
|
8798
|
+
created,
|
|
8799
|
+
"last-updated": lastUpdated,
|
|
8800
|
+
authors,
|
|
8801
|
+
"reviewed-by": reviewedBy,
|
|
8802
|
+
"last-reviewed": lastReviewed,
|
|
8803
|
+
lastBodyCommit,
|
|
8804
|
+
root
|
|
8805
|
+
};
|
|
8806
|
+
}
|
|
8807
|
+
function createdOf(oldest, stamps) {
|
|
8808
|
+
const stamp = stamps["created"];
|
|
8809
|
+
if (stamp !== void 0 && stamp !== null) {
|
|
8810
|
+
return git(stamp, `stamped in ${short(oldest.sha)}`);
|
|
8811
|
+
}
|
|
8812
|
+
const date = datePart(oldest.authorDate);
|
|
8813
|
+
return git(date, `added in ${short(oldest.sha)} (${date})`);
|
|
8814
|
+
}
|
|
8815
|
+
function lastUpdatedOf(commit, after, before) {
|
|
8816
|
+
const stamp = after["last-updated"];
|
|
8817
|
+
if (stamp !== void 0 && stamp !== null && !sameValue(stamp, before["last-updated"])) {
|
|
8818
|
+
return git(stamp, `stamped in ${short(commit.sha)}`);
|
|
8819
|
+
}
|
|
8820
|
+
const date = datePart(commit.authorDate);
|
|
8821
|
+
return git(date, `body changed in ${short(commit.sha)} (${date})`);
|
|
8822
|
+
}
|
|
8823
|
+
function authorsOf(bodyChanging) {
|
|
8824
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8825
|
+
const names = [];
|
|
8826
|
+
const add = (name2, email) => {
|
|
8827
|
+
if (name2 === "" || name2.endsWith("[bot]")) return;
|
|
8828
|
+
const key = email === null || email === "" ? `name:${name2.toLowerCase()}` : email.toLowerCase();
|
|
8829
|
+
if (seen.has(key)) return;
|
|
8830
|
+
seen.add(key);
|
|
8831
|
+
names.push(name2);
|
|
8832
|
+
};
|
|
8833
|
+
for (const h of bodyChanging) {
|
|
8834
|
+
add(h.authorName, h.authorEmail);
|
|
8835
|
+
for (const co of h.coAuthors) add(nameOf(co), emailOf(co));
|
|
8836
|
+
}
|
|
8837
|
+
const n = bodyChanging.length;
|
|
8838
|
+
return git(names, `${n} body-changing ${n === 1 ? "commit" : "commits"}`);
|
|
8839
|
+
}
|
|
8840
|
+
function extractorFor(input) {
|
|
8841
|
+
return extractorByName(input.extracted.format) ?? extractorForExtension(extname2(input.absPath));
|
|
8842
|
+
}
|
|
8843
|
+
function extractStamps(extractor, content, filePath) {
|
|
8844
|
+
if (!extractor) return {};
|
|
8845
|
+
try {
|
|
8846
|
+
return extractor.extract(content, filePath).data;
|
|
8847
|
+
} catch {
|
|
8848
|
+
return {};
|
|
8849
|
+
}
|
|
8850
|
+
}
|
|
8851
|
+
function sameBody(a, b) {
|
|
8852
|
+
if (a === null || b === null) return a === b;
|
|
8853
|
+
return a.replace(/\r\n/g, "\n") === b.replace(/\r\n/g, "\n");
|
|
8854
|
+
}
|
|
8855
|
+
function sameValue(a, b) {
|
|
8856
|
+
return toJsonText(a) === toJsonText(b);
|
|
8857
|
+
}
|
|
8858
|
+
var short = (sha) => sha.slice(0, 7);
|
|
8859
|
+
var datePart = (iso) => iso.slice(0, 10);
|
|
8860
|
+
function localDate(d) {
|
|
8861
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
8862
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
8863
|
+
}
|
|
8864
|
+
function nameOf(trailer) {
|
|
8865
|
+
const lt = trailer.lastIndexOf("<");
|
|
8866
|
+
return (lt === -1 ? trailer : trailer.slice(0, lt)).trim();
|
|
8867
|
+
}
|
|
8868
|
+
function emailOf(trailer) {
|
|
8869
|
+
const m = /<([^>]*)>\s*$/.exec(trailer);
|
|
8870
|
+
return m?.[1]?.trim() ?? null;
|
|
8871
|
+
}
|
|
8872
|
+
function text(out) {
|
|
8873
|
+
try {
|
|
8874
|
+
return out.raw.toString("utf8");
|
|
8875
|
+
} catch {
|
|
8876
|
+
throw new HistoryTooLarge();
|
|
8877
|
+
}
|
|
8878
|
+
}
|
|
8879
|
+
function runGit(args, cwd, stdin, maxBytes) {
|
|
8880
|
+
return new Promise((settle, reject) => {
|
|
8881
|
+
let done = false;
|
|
8882
|
+
const finish = (out) => {
|
|
8883
|
+
if (done) return;
|
|
8884
|
+
done = true;
|
|
8885
|
+
settle(out);
|
|
8886
|
+
};
|
|
8887
|
+
const fail = (err) => {
|
|
8888
|
+
if (done) return;
|
|
8889
|
+
done = true;
|
|
8890
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
8891
|
+
};
|
|
8892
|
+
const failed = (missing) => ({
|
|
8893
|
+
code: null,
|
|
8894
|
+
raw: Buffer.alloc(0),
|
|
8895
|
+
missing,
|
|
8896
|
+
tooLarge: false
|
|
8897
|
+
});
|
|
8898
|
+
let child;
|
|
8899
|
+
try {
|
|
8900
|
+
child = spawn3("git", args, { cwd, windowsHide: true });
|
|
8901
|
+
} catch {
|
|
8902
|
+
finish(failed(true));
|
|
8903
|
+
return;
|
|
8904
|
+
}
|
|
8905
|
+
let chunks = [];
|
|
8906
|
+
let total = 0;
|
|
8907
|
+
let tooLarge = false;
|
|
8908
|
+
child.stdout.on("data", (chunk) => {
|
|
8909
|
+
if (tooLarge) return;
|
|
8910
|
+
total += chunk.length;
|
|
8911
|
+
if (total > maxBytes) {
|
|
8912
|
+
tooLarge = true;
|
|
8913
|
+
chunks = [];
|
|
8914
|
+
child.kill();
|
|
8915
|
+
return;
|
|
8916
|
+
}
|
|
8917
|
+
chunks.push(chunk);
|
|
8918
|
+
});
|
|
8919
|
+
child.stderr.resume();
|
|
8920
|
+
child.on("error", (err) => {
|
|
8921
|
+
finish(failed(err.code === "ENOENT"));
|
|
8922
|
+
});
|
|
8923
|
+
child.stdin.on("error", () => {
|
|
8924
|
+
});
|
|
8925
|
+
child.on("close", (code) => {
|
|
8926
|
+
try {
|
|
8927
|
+
const raw = tooLarge ? Buffer.alloc(0) : Buffer.concat(chunks);
|
|
8928
|
+
finish({ code, raw, missing: false, tooLarge });
|
|
8929
|
+
} catch (err) {
|
|
8930
|
+
fail(err);
|
|
8931
|
+
}
|
|
8932
|
+
});
|
|
8933
|
+
if (stdin === void 0) child.stdin.end();
|
|
8934
|
+
else child.stdin.end(stdin);
|
|
8935
|
+
});
|
|
8936
|
+
}
|
|
8937
|
+
|
|
8938
|
+
// src/meta/core/derive/index.ts
|
|
8939
|
+
var FIELD_SOURCES = {
|
|
8940
|
+
created: ["git"],
|
|
8941
|
+
"last-updated": ["git"],
|
|
8942
|
+
authors: ["git"],
|
|
8943
|
+
owner: ["codeowners"],
|
|
8944
|
+
"reviewed-by": ["github", "gitlab", "git"],
|
|
8945
|
+
"last-reviewed": ["github", "gitlab", "git"]
|
|
8946
|
+
};
|
|
8947
|
+
var REVIEW_SOURCES = ["github", "gitlab"];
|
|
8948
|
+
var HOST_NAME = {
|
|
8949
|
+
github: "GitHub",
|
|
8950
|
+
gitlab: "GitLab"
|
|
8951
|
+
};
|
|
8952
|
+
function sourcesFor(field, commands) {
|
|
8953
|
+
if (isBuiltinField(field)) return FIELD_SOURCES[field];
|
|
8954
|
+
if (commands !== void 0 && Object.hasOwn(commands, field)) return ["command"];
|
|
8955
|
+
return [];
|
|
8956
|
+
}
|
|
8957
|
+
function consultedSources(fields, sources, commands) {
|
|
8958
|
+
const needed = /* @__PURE__ */ new Set();
|
|
8959
|
+
for (const field of fields) {
|
|
8960
|
+
for (const s of sourcesFor(field, commands)) needed.add(s);
|
|
8961
|
+
}
|
|
8962
|
+
return sources.filter((s) => needed.has(s));
|
|
8963
|
+
}
|
|
8964
|
+
function assertSourcesAvailable(sources, hint) {
|
|
8965
|
+
for (const name2 of Object.keys(sources)) {
|
|
8966
|
+
const status = sources[name2];
|
|
8967
|
+
if (status === void 0 || status.available) continue;
|
|
8968
|
+
const reason = status.reason ?? "it could not answer";
|
|
8969
|
+
throw new DocmetaError(
|
|
8970
|
+
`${name2} source unavailable: ${reason}${hint === void 0 ? "" : `; ${hint}`}`
|
|
8971
|
+
);
|
|
8972
|
+
}
|
|
8973
|
+
}
|
|
8974
|
+
async function deriveMetadata(inputs, ctx) {
|
|
8975
|
+
const consulted = new Set(consultedSources(ctx.fields, ctx.sources, ctx.commands));
|
|
8976
|
+
const sources = {};
|
|
8977
|
+
let git2 = /* @__PURE__ */ new Map();
|
|
8978
|
+
if (consulted.has("git")) {
|
|
8979
|
+
const result = await deriveFromGit(inputs, { cwd: ctx.cwd, now: ctx.now });
|
|
8980
|
+
sources.git = result.status;
|
|
8981
|
+
git2 = result.records;
|
|
8982
|
+
}
|
|
8983
|
+
const rootCache = /* @__PURE__ */ new Map();
|
|
8984
|
+
const rootOf = (absPath) => {
|
|
8985
|
+
const dir = dirname7(absPath);
|
|
8986
|
+
let root = rootCache.get(dir);
|
|
8987
|
+
if (root === void 0) {
|
|
8988
|
+
root = findGitRoot(dir);
|
|
8989
|
+
rootCache.set(dir, root);
|
|
8990
|
+
}
|
|
8991
|
+
return root;
|
|
8992
|
+
};
|
|
8993
|
+
const rootFor = (input) => git2.get(input.label)?.root ?? rootOf(input.absPath);
|
|
8994
|
+
let owners = /* @__PURE__ */ new Map();
|
|
8995
|
+
if (consulted.has("codeowners")) {
|
|
8996
|
+
const result = await deriveFromCodeowners(inputs, {
|
|
8997
|
+
explicit: ctx.codeowners,
|
|
8998
|
+
configDir: ctx.configDir,
|
|
8999
|
+
rootOf
|
|
9000
|
+
});
|
|
9001
|
+
sources.codeowners = result.status;
|
|
9002
|
+
owners = result.records;
|
|
9003
|
+
}
|
|
9004
|
+
const requested = REVIEW_SOURCES.filter((s) => consulted.has(s));
|
|
9005
|
+
let reviews = /* @__PURE__ */ new Map();
|
|
9006
|
+
if (requested.length > 0) {
|
|
9007
|
+
if (!consulted.has("git")) {
|
|
9008
|
+
for (const s of requested) {
|
|
9009
|
+
sources[s] = {
|
|
9010
|
+
available: false,
|
|
9011
|
+
reason: `the ${s} source needs the git source to find each document's commits; add git to sources`
|
|
9012
|
+
};
|
|
9013
|
+
}
|
|
9014
|
+
} else if (sources.git?.available === false) {
|
|
9015
|
+
for (const s of requested) {
|
|
9016
|
+
sources[s] = {
|
|
9017
|
+
available: false,
|
|
9018
|
+
reason: `the git source could not answer, so the ${s} source cannot locate any commit`
|
|
9019
|
+
};
|
|
9020
|
+
}
|
|
9021
|
+
} else {
|
|
9022
|
+
const result = await deriveReviewsByRoot(inputs, git2, rootFor, ctx, requested);
|
|
9023
|
+
Object.assign(sources, result.status);
|
|
9024
|
+
reviews = result.records;
|
|
9025
|
+
}
|
|
9026
|
+
}
|
|
9027
|
+
let commanded = /* @__PURE__ */ new Map();
|
|
9028
|
+
const doomed = Object.values(sources).some((s) => !s.available);
|
|
9029
|
+
if (consulted.has("command") && ctx.commands !== void 0 && !doomed) {
|
|
9030
|
+
const wanted = new Set(ctx.fields);
|
|
9031
|
+
const commands = Object.fromEntries(
|
|
9032
|
+
Object.entries(ctx.commands).filter(([field]) => wanted.has(field))
|
|
9033
|
+
);
|
|
9034
|
+
if (Object.keys(commands).length > 0) {
|
|
9035
|
+
const result = await deriveFromCommands(inputs, commands, { cwd: ctx.configDir ?? ctx.cwd });
|
|
9036
|
+
sources.command = result.status;
|
|
9037
|
+
commanded = result.records;
|
|
9038
|
+
}
|
|
9039
|
+
}
|
|
9040
|
+
const records = /* @__PURE__ */ new Map();
|
|
9041
|
+
for (const input of inputs) {
|
|
9042
|
+
const fields = {};
|
|
9043
|
+
const g = git2.get(input.label);
|
|
9044
|
+
const r = reviews.get(input.label);
|
|
9045
|
+
const c = commanded.get(input.label);
|
|
9046
|
+
for (const field of ctx.fields) {
|
|
9047
|
+
let value = null;
|
|
9048
|
+
for (const source of sourcesFor(field, ctx.commands)) {
|
|
9049
|
+
if (!consulted.has(source)) continue;
|
|
9050
|
+
const candidate = valueFrom(source, field, g, r, owners.get(input.label), c);
|
|
9051
|
+
if (candidate !== null) {
|
|
9052
|
+
value = candidate;
|
|
9053
|
+
break;
|
|
9054
|
+
}
|
|
9055
|
+
}
|
|
9056
|
+
fields[field] = value;
|
|
9057
|
+
}
|
|
9058
|
+
records.set(input.label, { file: input.label, fields });
|
|
9059
|
+
}
|
|
9060
|
+
return { records, sources };
|
|
9061
|
+
}
|
|
9062
|
+
function valueFrom(source, field, git2, reviews, owner, commanded) {
|
|
9063
|
+
switch (source) {
|
|
9064
|
+
case "git":
|
|
9065
|
+
if (git2 === void 0 || !isBuiltinField(field) || field === "owner") return null;
|
|
9066
|
+
return git2[field];
|
|
9067
|
+
case "command":
|
|
9068
|
+
return commanded?.[field] ?? null;
|
|
9069
|
+
case "codeowners":
|
|
9070
|
+
return field === "owner" ? owner ?? null : null;
|
|
9071
|
+
case "github":
|
|
9072
|
+
case "gitlab": {
|
|
9073
|
+
if (reviews === void 0) return null;
|
|
9074
|
+
if (field !== "reviewed-by" && field !== "last-reviewed") return null;
|
|
9075
|
+
const fact = reviews[field];
|
|
9076
|
+
return fact !== null && fact.source === source ? fact : null;
|
|
9077
|
+
}
|
|
9078
|
+
}
|
|
9079
|
+
}
|
|
9080
|
+
async function deriveReviewsByRoot(inputs, git2, rootFor, ctx, requested) {
|
|
9081
|
+
const byRoot = /* @__PURE__ */ new Map();
|
|
9082
|
+
for (const input of inputs) {
|
|
9083
|
+
const root = rootFor(input);
|
|
9084
|
+
if (root === null) continue;
|
|
9085
|
+
const bucket = byRoot.get(root) ?? [];
|
|
9086
|
+
bucket.push(input);
|
|
9087
|
+
byRoot.set(root, bucket);
|
|
9088
|
+
}
|
|
9089
|
+
const status = {};
|
|
9090
|
+
const records = /* @__PURE__ */ new Map();
|
|
9091
|
+
const unavailable3 = (which, reason) => {
|
|
9092
|
+
for (const s of which) status[s] = { available: false, reason };
|
|
9093
|
+
return { status, records: /* @__PURE__ */ new Map() };
|
|
9094
|
+
};
|
|
9095
|
+
if (byRoot.size === 0) {
|
|
9096
|
+
return unavailable3(requested, "no git repository contains the documents");
|
|
9097
|
+
}
|
|
9098
|
+
const cacheDir = resolve9(ctx.configDir ?? ctx.cwd, REVIEW_CACHE_DIR);
|
|
9099
|
+
for (const [root, bucket] of byRoot) {
|
|
9100
|
+
const client = ctx.reviews ?? await createReviewClient(root, { cwd: ctx.cwd, cache: ctx.cache, cacheDir, requested });
|
|
9101
|
+
const identity = await client.detect();
|
|
9102
|
+
if (identity === null) {
|
|
9103
|
+
const s = await client.status();
|
|
9104
|
+
return unavailable3(requested, s.reason ?? "no origin remote to tell GitHub from GitLab");
|
|
9105
|
+
}
|
|
9106
|
+
const source = identity.kind;
|
|
9107
|
+
if (!requested.includes(source)) {
|
|
9108
|
+
return unavailable3(
|
|
9109
|
+
requested,
|
|
9110
|
+
`the origin remote is ${identity.host}, which is ${HOST_NAME[source]}; add ${source} to sources, or drop reviewed-by and last-reviewed from the managed fields`
|
|
9111
|
+
);
|
|
9112
|
+
}
|
|
9113
|
+
const result = await deriveFromReviews(
|
|
9114
|
+
bucket.map((input) => ({
|
|
9115
|
+
label: input.label,
|
|
9116
|
+
sha: git2.get(input.label)?.lastBodyCommit ?? null
|
|
9117
|
+
})),
|
|
9118
|
+
client
|
|
9119
|
+
);
|
|
9120
|
+
if (!result.status.available) {
|
|
9121
|
+
status[source] = result.status;
|
|
9122
|
+
return { status, records: /* @__PURE__ */ new Map() };
|
|
9123
|
+
}
|
|
9124
|
+
status[source] = result.status;
|
|
9125
|
+
for (const [label, facts] of result.records) records.set(label, facts);
|
|
9126
|
+
}
|
|
9127
|
+
return { status, records };
|
|
9128
|
+
}
|
|
9129
|
+
|
|
9130
|
+
// src/meta/core/derive/config.ts
|
|
9131
|
+
var DEFAULT_COMMAND_TIMEOUT_MS = 6e4;
|
|
9132
|
+
function commandsOf(config) {
|
|
9133
|
+
if (config?.commands === void 0) return void 0;
|
|
9134
|
+
const out = {};
|
|
9135
|
+
for (const [field, c] of Object.entries(config.commands)) {
|
|
9136
|
+
out[field] = {
|
|
9137
|
+
run: c.run,
|
|
9138
|
+
timeoutMs: c.timeout === void 0 ? DEFAULT_COMMAND_TIMEOUT_MS : Math.round(c.timeout * 1e3)
|
|
9139
|
+
};
|
|
9140
|
+
}
|
|
9141
|
+
return out;
|
|
9142
|
+
}
|
|
9143
|
+
|
|
9144
|
+
// src/meta/core/derive/table.ts
|
|
9145
|
+
var DERIVED_VIEW = "derived";
|
|
9146
|
+
var DERIVED_ROWS = "_derived_rows";
|
|
9147
|
+
var RESOLVED_VIEW = "resolved";
|
|
9148
|
+
var ORIGIN_COLUMN = "_origin";
|
|
9149
|
+
var SOURCES_COLUMN = "_sources";
|
|
9150
|
+
function derivedColumns(commands) {
|
|
9151
|
+
return ["_path", ...derivableFields(commands), SOURCES_COLUMN];
|
|
9152
|
+
}
|
|
9153
|
+
function mentionsDerived(sql) {
|
|
9154
|
+
return /\b(?:from|join|update|into|table|view)\s+(?:"derived"|derived\b)/i.test(sql);
|
|
9155
|
+
}
|
|
9156
|
+
function fieldsForSql(sql, fields) {
|
|
9157
|
+
if (sql.includes("*") || /\b_(?:sources|origin)\b/i.test(sql)) return [...fields];
|
|
9158
|
+
return fields.filter(
|
|
9159
|
+
(field) => new RegExp(`(?<![\\w-])${escapeRegExp(field)}(?![\\w-])`, "i").test(sql)
|
|
9160
|
+
);
|
|
9161
|
+
}
|
|
9162
|
+
function escapeRegExp(s) {
|
|
9163
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9164
|
+
}
|
|
9165
|
+
function createDerivedView(db, records, columns) {
|
|
9166
|
+
const quoted = columns.map(quoteIdent);
|
|
9167
|
+
db.exec(
|
|
9168
|
+
`CREATE TABLE ${DERIVED_ROWS} (${quoted.map((c, i) => i === 0 ? `${c} TEXT PRIMARY KEY` : `${c} TEXT`).join(", ")})`
|
|
9169
|
+
);
|
|
9170
|
+
const insert = db.prepare(
|
|
9171
|
+
`INSERT INTO ${DERIVED_ROWS} VALUES (${quoted.map(() => "?").join(", ")})`
|
|
9172
|
+
);
|
|
9173
|
+
const fields = columns.slice(1, -1);
|
|
9174
|
+
db.exec("BEGIN");
|
|
9175
|
+
let committed = false;
|
|
9176
|
+
try {
|
|
9177
|
+
for (const [label, record] of records) {
|
|
9178
|
+
const sources = {};
|
|
9179
|
+
const values = fields.map((field) => {
|
|
9180
|
+
const derived = record.fields[field];
|
|
9181
|
+
if (derived == null) return null;
|
|
9182
|
+
sources[field] = { source: derived.source, evidence: derived.evidence };
|
|
9183
|
+
return bindValue(derived.value);
|
|
9184
|
+
});
|
|
9185
|
+
insert.run(label, ...values, JSON.stringify(sources));
|
|
9186
|
+
}
|
|
9187
|
+
db.exec("COMMIT");
|
|
9188
|
+
committed = true;
|
|
9189
|
+
} finally {
|
|
9190
|
+
if (!committed) db.exec("ROLLBACK");
|
|
9191
|
+
}
|
|
9192
|
+
db.exec(`CREATE VIEW ${DERIVED_VIEW} AS SELECT * FROM ${DERIVED_ROWS}`);
|
|
9193
|
+
}
|
|
9194
|
+
function resolvedColumns(docsColumns, commands) {
|
|
9195
|
+
const columns = ["_path"];
|
|
9196
|
+
const seen = /* @__PURE__ */ new Set(["_path", ORIGIN_COLUMN, SOURCES_COLUMN]);
|
|
9197
|
+
for (const column of docsColumns) {
|
|
9198
|
+
if (seen.has(column) || RESERVED.has(column)) continue;
|
|
9199
|
+
seen.add(column);
|
|
9200
|
+
columns.push(column);
|
|
9201
|
+
}
|
|
9202
|
+
for (const field of derivableFields(commands)) {
|
|
9203
|
+
if (seen.has(field)) continue;
|
|
9204
|
+
seen.add(field);
|
|
9205
|
+
columns.push(field);
|
|
9206
|
+
}
|
|
9207
|
+
columns.push(ORIGIN_COLUMN, SOURCES_COLUMN);
|
|
9208
|
+
return columns;
|
|
9209
|
+
}
|
|
9210
|
+
function mentionsResolved(sql) {
|
|
9211
|
+
return /\b(?:from|join|update|into|table|view)\s+(?:"resolved"|resolved\b)/i.test(sql);
|
|
9212
|
+
}
|
|
9213
|
+
function jsonPath(field) {
|
|
9214
|
+
return `$.${JSON.stringify(field)}`;
|
|
9215
|
+
}
|
|
9216
|
+
function sqlString(value) {
|
|
9217
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
9218
|
+
}
|
|
9219
|
+
function createResolvedView(db, docsColumns, commands) {
|
|
9220
|
+
const columns = resolvedColumns(docsColumns, commands);
|
|
9221
|
+
const fields = columns.slice(1, -2);
|
|
9222
|
+
const inDocs = new Set(docsColumns);
|
|
9223
|
+
const inDerived = new Set(derivableFields(commands));
|
|
9224
|
+
const selects = [`d.${quoteIdent("_path")} AS ${quoteIdent("_path")}`];
|
|
9225
|
+
const origin = [];
|
|
9226
|
+
for (const field of fields) {
|
|
9227
|
+
const path = sqlString(jsonPath(field));
|
|
9228
|
+
const asserted = `json_type(d.${quoteIdent("_data")}, ${path}) IS NOT NULL`;
|
|
9229
|
+
const assertedValue = inDocs.has(field) ? `d.${quoteIdent(field)}` : `d.${quoteIdent("_data")} ->> ${path}`;
|
|
9230
|
+
if (inDerived.has(field)) {
|
|
9231
|
+
const derivedValue = `r.${quoteIdent(field)}`;
|
|
9232
|
+
selects.push(
|
|
9233
|
+
`CASE WHEN ${asserted} THEN ${assertedValue} ELSE ${derivedValue} END AS ${quoteIdent(field)}`
|
|
9234
|
+
);
|
|
9235
|
+
origin.push(
|
|
9236
|
+
`${sqlString(field)}, CASE WHEN ${asserted} THEN 'asserted' WHEN ${derivedValue} IS NOT NULL THEN 'derived' END`
|
|
9237
|
+
);
|
|
9238
|
+
} else {
|
|
9239
|
+
selects.push(`${assertedValue} AS ${quoteIdent(field)}`);
|
|
9240
|
+
origin.push(`${sqlString(field)}, CASE WHEN ${asserted} THEN 'asserted' END`);
|
|
9241
|
+
}
|
|
9242
|
+
}
|
|
9243
|
+
selects.push(
|
|
9244
|
+
`json_patch('{}', json_object(${origin.join(", ")})) AS ${quoteIdent(ORIGIN_COLUMN)}`
|
|
9245
|
+
);
|
|
9246
|
+
selects.push(`r.${quoteIdent(SOURCES_COLUMN)} AS ${quoteIdent(SOURCES_COLUMN)}`);
|
|
9247
|
+
db.exec(
|
|
9248
|
+
`CREATE VIEW ${RESOLVED_VIEW} AS SELECT ${selects.join(", ")} FROM docs d LEFT JOIN ${DERIVED_ROWS} r USING (${quoteIdent("_path")})`
|
|
9249
|
+
);
|
|
9250
|
+
}
|
|
9251
|
+
async function deriveForTable(inputs, ctx, hint, fields) {
|
|
9252
|
+
const derive = ctx.config?.derive;
|
|
9253
|
+
const commands = commandsOf(derive);
|
|
9254
|
+
const result = await deriveMetadata(inputs, {
|
|
9255
|
+
cwd: ctx.cwd,
|
|
9256
|
+
base: ctx.base,
|
|
9257
|
+
...ctx.configDir !== void 0 ? { configDir: ctx.configDir } : {},
|
|
9258
|
+
sources: derive?.sources ?? [...DERIVE_SOURCES],
|
|
9259
|
+
fields,
|
|
9260
|
+
...derive?.codeowners !== void 0 ? { codeowners: derive.codeowners } : {},
|
|
9261
|
+
...commands !== void 0 ? { commands } : {},
|
|
9262
|
+
cache: ctx.cache,
|
|
9263
|
+
now: ctx.now ?? (() => /* @__PURE__ */ new Date())
|
|
9264
|
+
});
|
|
9265
|
+
assertSourcesAvailable(result.sources, hint);
|
|
9266
|
+
return result.records;
|
|
9267
|
+
}
|
|
9268
|
+
|
|
9269
|
+
// src/meta/commands/validate.ts
|
|
9270
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
9271
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
9272
|
+
import { resolve as resolve11, extname as extname3 } from "node:path";
|
|
9273
|
+
|
|
9274
|
+
// src/meta/core/baseline.ts
|
|
9275
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
9276
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
9277
|
+
import { relative as relative9, resolve as resolve10 } from "node:path";
|
|
9278
|
+
var DEFAULT_BASELINE_PATH = ".manni-baseline.json";
|
|
9279
|
+
var LEGACY_BASELINE_PATH = ".docmeta-baseline.json";
|
|
9280
|
+
var BASELINE_VERSION = 1;
|
|
9281
|
+
var FINGERPRINT_RE = /^[0-9a-f]{16}$/;
|
|
9282
|
+
function canonicalFilePath(file, ctx) {
|
|
9283
|
+
if (!ctx) return file;
|
|
9284
|
+
const from = ctx.runBase ?? ctx.base;
|
|
9285
|
+
return relative9(ctx.base, resolve10(from, file)).replace(/\\/g, "/");
|
|
9286
|
+
}
|
|
9287
|
+
function canonicalSchemaRef(ref, ctx) {
|
|
9288
|
+
if (!ctx) return ref;
|
|
9289
|
+
if (classifyRef(ref).kind !== "file") return ref;
|
|
9290
|
+
const abs = resolve10(ctx.cwd, ref);
|
|
9291
|
+
return relative9(ctx.base, abs).replace(/\\/g, "/");
|
|
9292
|
+
}
|
|
9293
|
+
function fingerprint(e, ctx) {
|
|
9294
|
+
const parts = [
|
|
9295
|
+
canonicalSchemaRef(e.schema, ctx),
|
|
9296
|
+
e.instancePath,
|
|
9297
|
+
e.keyword,
|
|
9298
|
+
e.subject ?? ""
|
|
9299
|
+
];
|
|
9300
|
+
return createHash4("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
|
|
9301
|
+
}
|
|
9302
|
+
function serializeBaseline(baseline) {
|
|
9303
|
+
const entries = /* @__PURE__ */ Object.create(null);
|
|
9304
|
+
for (const file of Object.keys(baseline.entries).sort()) {
|
|
9305
|
+
entries[file] = [...baseline.entries[file] ?? []].sort();
|
|
9306
|
+
}
|
|
9307
|
+
return `${JSON.stringify(
|
|
9308
|
+
{
|
|
9309
|
+
version: baseline.version,
|
|
9310
|
+
generatedWith: baseline.generatedWith,
|
|
9311
|
+
entries
|
|
9312
|
+
},
|
|
9313
|
+
null,
|
|
9314
|
+
2
|
|
9315
|
+
)}
|
|
9316
|
+
`;
|
|
9317
|
+
}
|
|
9318
|
+
function bad(source, detail) {
|
|
9319
|
+
throw new DocmetaError(`Baseline "${source}": ${detail}`);
|
|
9320
|
+
}
|
|
9321
|
+
function parseBaseline(text2, source) {
|
|
9322
|
+
let raw;
|
|
9323
|
+
try {
|
|
9324
|
+
raw = JSON.parse(stripBom(text2));
|
|
9325
|
+
} catch (err) {
|
|
9326
|
+
bad(source, `invalid JSON: ${errorMessage(err)}`);
|
|
9327
|
+
}
|
|
9328
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
9329
|
+
bad(source, "top level must be an object.");
|
|
9330
|
+
}
|
|
9331
|
+
const obj = raw;
|
|
9332
|
+
if (obj.version !== BASELINE_VERSION) {
|
|
9333
|
+
bad(
|
|
9334
|
+
source,
|
|
9335
|
+
`unsupported version ${JSON.stringify(obj.version)} (this manni writes version ${BASELINE_VERSION}). Re-record it with \`manni meta validate --write-baseline\`.`
|
|
9336
|
+
);
|
|
9337
|
+
}
|
|
9338
|
+
const generatedWith = typeof obj.generatedWith === "string" ? obj.generatedWith : "";
|
|
9339
|
+
const rawEntries = obj.entries;
|
|
9340
|
+
if (typeof rawEntries !== "object" || rawEntries === null || Array.isArray(rawEntries)) {
|
|
9341
|
+
bad(source, '"entries" must be an object mapping file paths to fingerprints.');
|
|
9342
|
+
}
|
|
9343
|
+
const entries = /* @__PURE__ */ Object.create(null);
|
|
9344
|
+
for (const [file, value] of Object.entries(
|
|
9345
|
+
rawEntries
|
|
9346
|
+
)) {
|
|
9347
|
+
if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
|
|
9348
|
+
bad(source, `entries["${file}"] must be a list of fingerprint strings.`);
|
|
9349
|
+
}
|
|
9350
|
+
const malformed = value.find((v) => !FINGERPRINT_RE.test(v));
|
|
9351
|
+
if (malformed !== void 0) {
|
|
9352
|
+
bad(
|
|
9353
|
+
source,
|
|
9354
|
+
`entries["${file}"] contains ${JSON.stringify(malformed)}, which is not a fingerprint (16 lowercase hex characters).`
|
|
9355
|
+
);
|
|
9356
|
+
}
|
|
9357
|
+
entries[file] = value;
|
|
9358
|
+
}
|
|
9359
|
+
return { version: BASELINE_VERSION, generatedWith, entries };
|
|
9360
|
+
}
|
|
9361
|
+
async function readBaseline(absPath, source) {
|
|
9362
|
+
let text2;
|
|
9363
|
+
try {
|
|
9364
|
+
text2 = await readFile6(absPath, "utf8");
|
|
9365
|
+
} catch (err) {
|
|
9366
|
+
if (err.code === "ENOENT") return null;
|
|
9367
|
+
throw new DocmetaError(
|
|
9368
|
+
`Baseline "${source}" could not be read: ${errorMessage(err)}`
|
|
9369
|
+
);
|
|
9370
|
+
}
|
|
9371
|
+
return parseBaseline(text2, source);
|
|
9372
|
+
}
|
|
9373
|
+
async function writeBaselineFile(absPath, baseline, source) {
|
|
9374
|
+
try {
|
|
9375
|
+
await writeFileAtomic(absPath, serializeBaseline(baseline));
|
|
9376
|
+
} catch (err) {
|
|
9377
|
+
throw new DocmetaError(
|
|
9378
|
+
`Baseline "${source}" could not be written: ${errorMessage(err)}`
|
|
7445
9379
|
);
|
|
7446
9380
|
}
|
|
7447
9381
|
}
|
|
@@ -7560,8 +9494,8 @@ function synthesizeMessage(extras, row) {
|
|
|
7560
9494
|
const pairs = extras.flatMap((c) => {
|
|
7561
9495
|
const value = row[c];
|
|
7562
9496
|
if (value === null || value === void 0) return [];
|
|
7563
|
-
const
|
|
7564
|
-
return [`${c}=${
|
|
9497
|
+
const text2 = typeof value === "string" ? value : typeof value === "number" || typeof value === "bigint" ? String(value) : JSON.stringify(value);
|
|
9498
|
+
return [`${c}=${text2}`];
|
|
7565
9499
|
});
|
|
7566
9500
|
return pairs.length > 0 ? pairs.join(", ") : "check matched";
|
|
7567
9501
|
}
|
|
@@ -7571,9 +9505,26 @@ async function runChecks(checks, entries, ctx = {}) {
|
|
|
7571
9505
|
const { DatabaseSync } = await loadSqlite();
|
|
7572
9506
|
const db = new DatabaseSync(":memory:");
|
|
7573
9507
|
try {
|
|
7574
|
-
|
|
9508
|
+
const dataColumns = corpusDataColumns(entries);
|
|
9509
|
+
createDocsTable(db, entries, dataColumns);
|
|
7575
9510
|
registerLineFor(db, entries);
|
|
7576
9511
|
createCollectionViews(db, collectCollections(entries, ctx));
|
|
9512
|
+
const wantsDerived = checks.filter(
|
|
9513
|
+
(c) => mentionsDerived(c.query) || mentionsResolved(c.query)
|
|
9514
|
+
);
|
|
9515
|
+
if (wantsDerived.length > 0) {
|
|
9516
|
+
if (ctx.derive === void 0) {
|
|
9517
|
+
throw new DocmetaError(
|
|
9518
|
+
`check "${wantsDerived[0]?.name ?? ""}": the SQL names the derived or resolved table, but this run cannot derive. Run it through \`manni meta validate\`, or drop the reference.`
|
|
9519
|
+
);
|
|
9520
|
+
}
|
|
9521
|
+
const commands = ctx.commands;
|
|
9522
|
+
const readable = derivableFields(commands);
|
|
9523
|
+
const fields = /* @__PURE__ */ new Set();
|
|
9524
|
+
for (const c of wantsDerived) for (const f of fieldsForSql(c.query, readable)) fields.add(f);
|
|
9525
|
+
createDerivedView(db, await ctx.derive([...fields]), derivedColumns(commands));
|
|
9526
|
+
createResolvedView(db, dataColumns, commands);
|
|
9527
|
+
}
|
|
7577
9528
|
db.exec("PRAGMA query_only = 1");
|
|
7578
9529
|
const loaded = new Set(entries.map((e) => e.label));
|
|
7579
9530
|
for (const check of checks) {
|
|
@@ -7583,17 +9534,17 @@ async function runChecks(checks, entries, ctx = {}) {
|
|
|
7583
9534
|
`check "${check.name}": ${head.split(/\s/, 1)[0]?.toUpperCase() ?? "that statement"} is refused: it can write outside the docs table.`
|
|
7584
9535
|
);
|
|
7585
9536
|
}
|
|
7586
|
-
let
|
|
9537
|
+
let tokens2;
|
|
7587
9538
|
try {
|
|
7588
|
-
|
|
9539
|
+
tokens2 = collectNamedParameters(check.query);
|
|
7589
9540
|
} catch (err) {
|
|
7590
9541
|
throw new DocmetaError(
|
|
7591
|
-
`check "${check.name}": ${err
|
|
9542
|
+
`check "${check.name}": ${errorMessage(err)}`
|
|
7592
9543
|
);
|
|
7593
9544
|
}
|
|
7594
|
-
if (
|
|
9545
|
+
if (tokens2.length > 0) {
|
|
7595
9546
|
throw new DocmetaError(
|
|
7596
|
-
`check "${check.name}": the SQL references ${
|
|
9547
|
+
`check "${check.name}": the SQL references ${tokens2.join(", ")}, but checks bind no parameters \u2014 an unbound name binds NULL and matches nothing, so the gate would pass on a typo. Inline the value in the SQL.`
|
|
7597
9548
|
);
|
|
7598
9549
|
}
|
|
7599
9550
|
let columns;
|
|
@@ -7604,7 +9555,7 @@ async function runChecks(checks, entries, ctx = {}) {
|
|
|
7604
9555
|
columns = stmt.columns().map((c) => c.name);
|
|
7605
9556
|
rows = stmt.all();
|
|
7606
9557
|
} catch (err) {
|
|
7607
|
-
const message = err
|
|
9558
|
+
const message = errorMessage(err);
|
|
7608
9559
|
throw new DocmetaError(
|
|
7609
9560
|
message.includes("attempt to write a readonly database") ? `check "${check.name}": ${message} \u2014 checks are read-only by design; a check is a SELECT over the projection. Edit the corpus with \`manni meta query\` instead.` : `check "${check.name}": ${message}`
|
|
7610
9561
|
);
|
|
@@ -7638,17 +9589,17 @@ function parseErrorResult(file, format, message, keyword) {
|
|
|
7638
9589
|
}
|
|
7639
9590
|
function resolveBaselineRequest(opts, configured, configDir, cwd) {
|
|
7640
9591
|
const named = (label) => ({
|
|
7641
|
-
absPath:
|
|
9592
|
+
absPath: resolve11(cwd, label),
|
|
7642
9593
|
label
|
|
7643
9594
|
});
|
|
7644
9595
|
const implied = () => {
|
|
7645
9596
|
const base = configDir ?? cwd;
|
|
7646
9597
|
if (configured !== void 0) {
|
|
7647
|
-
return { absPath:
|
|
9598
|
+
return { absPath: resolve11(base, configured), label: configured };
|
|
7648
9599
|
}
|
|
7649
|
-
const current =
|
|
7650
|
-
const legacy =
|
|
7651
|
-
if (!
|
|
9600
|
+
const current = resolve11(base, DEFAULT_BASELINE_PATH);
|
|
9601
|
+
const legacy = resolve11(base, LEGACY_BASELINE_PATH);
|
|
9602
|
+
if (!existsSync3(current) && existsSync3(legacy)) {
|
|
7652
9603
|
warn(
|
|
7653
9604
|
`"${LEGACY_BASELINE_PATH}" is the pre-rename baseline file name. Rename it to "${DEFAULT_BASELINE_PATH}".`
|
|
7654
9605
|
);
|
|
@@ -7667,6 +9618,13 @@ function resolveBaselineRequest(opts, configured, configDir, cwd) {
|
|
|
7667
9618
|
if (configured) return { ...implied(), write: false };
|
|
7668
9619
|
return null;
|
|
7669
9620
|
}
|
|
9621
|
+
function once(fn) {
|
|
9622
|
+
let pending;
|
|
9623
|
+
return () => {
|
|
9624
|
+
pending ??= fn();
|
|
9625
|
+
return pending;
|
|
9626
|
+
};
|
|
9627
|
+
}
|
|
7670
9628
|
async function runValidate(opts) {
|
|
7671
9629
|
const cwd = opts.cwd ?? process.cwd();
|
|
7672
9630
|
const {
|
|
@@ -7759,6 +9717,39 @@ async function runValidate(opts) {
|
|
|
7759
9717
|
}
|
|
7760
9718
|
const checksWillRun = configuredChecks.length > 0 && opts.checks !== false && !scoped;
|
|
7761
9719
|
const checkEntries = [];
|
|
9720
|
+
const deriveConfig = config?.derive;
|
|
9721
|
+
const deriveWillRun = deriveConfig !== void 0 && deriveConfig.fields.length > 0 && opts.derive !== false;
|
|
9722
|
+
const derivedChecks = checksWillRun ? configuredChecks.filter((c) => mentionsDerived(c.query) || mentionsResolved(c.query)) : [];
|
|
9723
|
+
const checksNeedDerived = derivedChecks.length > 0;
|
|
9724
|
+
const deriveInputs = [];
|
|
9725
|
+
const keepDeriveInputs = deriveWillRun || checksNeedDerived;
|
|
9726
|
+
const deriveCommands = commandsOf(deriveConfig);
|
|
9727
|
+
const deriveFields = new Set(deriveWillRun ? deriveConfig.fields : []);
|
|
9728
|
+
const readable = derivableFields(deriveCommands);
|
|
9729
|
+
for (const c of derivedChecks) for (const f of fieldsForSql(c.query, readable)) deriveFields.add(f);
|
|
9730
|
+
const derivedOnce = once(async () => {
|
|
9731
|
+
const derived = await deriveMetadata(deriveInputs, {
|
|
9732
|
+
cwd,
|
|
9733
|
+
base,
|
|
9734
|
+
...configDir !== void 0 ? { configDir } : {},
|
|
9735
|
+
sources: deriveConfig?.sources ?? [...DERIVE_SOURCES],
|
|
9736
|
+
fields: [...deriveFields],
|
|
9737
|
+
...deriveConfig?.codeowners !== void 0 ? { codeowners: deriveConfig.codeowners } : {},
|
|
9738
|
+
...deriveCommands !== void 0 ? { commands: deriveCommands } : {},
|
|
9739
|
+
cache: opts.cache ?? true,
|
|
9740
|
+
now: opts.now ?? (() => /* @__PURE__ */ new Date())
|
|
9741
|
+
});
|
|
9742
|
+
assertSourcesAvailable(
|
|
9743
|
+
derived.sources,
|
|
9744
|
+
deriveWillRun ? "pass --no-derive to skip the comparison, or narrow derive.sources" : "narrow derive.sources in manni.config.yaml"
|
|
9745
|
+
);
|
|
9746
|
+
for (const [name2, status] of Object.entries(derived.sources)) {
|
|
9747
|
+
if (status.available && status.reason !== void 0) {
|
|
9748
|
+
opts.onNotice?.(`${name2}: ${status.reason}`);
|
|
9749
|
+
}
|
|
9750
|
+
}
|
|
9751
|
+
return derived;
|
|
9752
|
+
});
|
|
7762
9753
|
const processOne = async (label, content, extension) => {
|
|
7763
9754
|
const extractor = forcedExtractor ?? extractorForExtension(extension);
|
|
7764
9755
|
if (!extractor) {
|
|
@@ -7775,10 +9766,13 @@ async function runValidate(opts) {
|
|
|
7775
9766
|
} catch (err) {
|
|
7776
9767
|
if (err instanceof DocmetaError) throw err;
|
|
7777
9768
|
results.push(
|
|
7778
|
-
parseErrorResult(label, extractor.name, err
|
|
9769
|
+
parseErrorResult(label, extractor.name, errorMessage(err), "parse")
|
|
7779
9770
|
);
|
|
7780
9771
|
return;
|
|
7781
9772
|
}
|
|
9773
|
+
if (keepDeriveInputs && label !== STDIN_LABEL) {
|
|
9774
|
+
deriveInputs.push({ label, absPath: resolve11(base, label), content, extracted });
|
|
9775
|
+
}
|
|
7782
9776
|
const merged = mergeExternalMetadata(label, extracted, externalMetadata, members, base);
|
|
7783
9777
|
extracted = merged.extracted;
|
|
7784
9778
|
for (const j of merged.joins) {
|
|
@@ -7807,7 +9801,7 @@ async function runValidate(opts) {
|
|
|
7807
9801
|
});
|
|
7808
9802
|
} catch (err) {
|
|
7809
9803
|
results.push(
|
|
7810
|
-
parseErrorResult(label, extractor.name, err
|
|
9804
|
+
parseErrorResult(label, extractor.name, errorMessage(err), "schema")
|
|
7811
9805
|
);
|
|
7812
9806
|
return;
|
|
7813
9807
|
}
|
|
@@ -7859,8 +9853,8 @@ async function runValidate(opts) {
|
|
|
7859
9853
|
await processOne(STDIN_LABEL, content, forcedExtractor.extensions[0] ?? "");
|
|
7860
9854
|
}
|
|
7861
9855
|
for (const file of files) {
|
|
7862
|
-
const content = await
|
|
7863
|
-
await processOne(file, content,
|
|
9856
|
+
const content = await readFile7(resolve11(base, file), "utf8");
|
|
9857
|
+
await processOne(file, content, extname3(file));
|
|
7864
9858
|
}
|
|
7865
9859
|
if (externalMetadata) {
|
|
7866
9860
|
const byLabel = new Map(results.map((r) => [r.file, r]));
|
|
@@ -7904,7 +9898,16 @@ async function runValidate(opts) {
|
|
|
7904
9898
|
// corpus see the same collections.
|
|
7905
9899
|
collections: declaredCollections,
|
|
7906
9900
|
...configDir !== void 0 ? { configDir } : {},
|
|
7907
|
-
base
|
|
9901
|
+
base,
|
|
9902
|
+
// A check that names the `derived` table (0040) gets the same view
|
|
9903
|
+
// `query` builds, from the run's one derivation — which already
|
|
9904
|
+
// covers every field the checks can read, so the list handed back
|
|
9905
|
+
// here is not needed. Built only when asked for, because building
|
|
9906
|
+
// it spawns git.
|
|
9907
|
+
derive: async () => (await derivedOnce()).records,
|
|
9908
|
+
// Its columns include the configured commands' keys (0042), which a
|
|
9909
|
+
// check's context cannot read from a config since 0041.
|
|
9910
|
+
...deriveCommands !== void 0 ? { commands: deriveCommands } : {}
|
|
7908
9911
|
});
|
|
7909
9912
|
const byFile = new Map(results.map((r) => [r.file, r]));
|
|
7910
9913
|
for (const [file, errs] of findings) {
|
|
@@ -7919,6 +9922,24 @@ async function runValidate(opts) {
|
|
|
7919
9922
|
}
|
|
7920
9923
|
}
|
|
7921
9924
|
}
|
|
9925
|
+
if (deriveWillRun && deriveInputs.length > 0) {
|
|
9926
|
+
const derived = await derivedOnce();
|
|
9927
|
+
const byFile = new Map(results.map((r) => [r.file, r]));
|
|
9928
|
+
for (const input of deriveInputs) {
|
|
9929
|
+
const record = derived.records.get(input.label);
|
|
9930
|
+
const result = byFile.get(input.label);
|
|
9931
|
+
if (!record || !result) continue;
|
|
9932
|
+
const findings = staleFindings(
|
|
9933
|
+
deriveConfig.fields.map(
|
|
9934
|
+
(field) => compareDerived(field, input.extracted.data[field], record.fields[field])
|
|
9935
|
+
),
|
|
9936
|
+
input.extracted.lineFor
|
|
9937
|
+
);
|
|
9938
|
+
if (findings.length === 0) continue;
|
|
9939
|
+
result.errors.push(...findings);
|
|
9940
|
+
result.ok = false;
|
|
9941
|
+
}
|
|
9942
|
+
}
|
|
7922
9943
|
const frame = { cwd, base: configDir ?? cwd, runBase: base };
|
|
7923
9944
|
const { results: reported, baseline } = await settleBaseline(
|
|
7924
9945
|
results,
|
|
@@ -7979,8 +10000,8 @@ async function settleBaseline(results, request, ctx) {
|
|
|
7979
10000
|
}
|
|
7980
10001
|
|
|
7981
10002
|
// src/meta/commands/get.ts
|
|
7982
|
-
import { readFile as
|
|
7983
|
-
import { resolve as
|
|
10003
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
10004
|
+
import { resolve as resolve12, extname as extname4 } from "node:path";
|
|
7984
10005
|
async function runGet(opts) {
|
|
7985
10006
|
const cwd = opts.cwd ?? process.cwd();
|
|
7986
10007
|
if (opts.fields.length === 0) {
|
|
@@ -8040,6 +10061,11 @@ async function runGet(opts) {
|
|
|
8040
10061
|
base,
|
|
8041
10062
|
offline: opts.offline ?? config?.offline ?? false
|
|
8042
10063
|
});
|
|
10064
|
+
const commands = commandsOf(config?.derive) ?? {};
|
|
10065
|
+
const deriving = opts.derived !== false;
|
|
10066
|
+
const derivableFields2 = deriving ? opts.fields.filter((f) => isBuiltinField(f) || Object.hasOwn(commands, f)) : [];
|
|
10067
|
+
const deriveInputs = [];
|
|
10068
|
+
const carried = /* @__PURE__ */ new Map();
|
|
8043
10069
|
const readOne = (label, content, extension) => {
|
|
8044
10070
|
const extractor = forced ?? extractorForExtension(extension);
|
|
8045
10071
|
if (!extractor) {
|
|
@@ -8050,15 +10076,18 @@ async function runGet(opts) {
|
|
|
8050
10076
|
const members = membersFor(label);
|
|
8051
10077
|
let extracted;
|
|
8052
10078
|
try {
|
|
8053
|
-
|
|
8054
|
-
label,
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
10079
|
+
const own2 = extractor.extract(content, label, {
|
|
10080
|
+
elements: resolveElements(label, config, members)
|
|
10081
|
+
});
|
|
10082
|
+
if (deriving && label !== STDIN_LABEL) {
|
|
10083
|
+
deriveInputs.push({
|
|
10084
|
+
label,
|
|
10085
|
+
absPath: resolve12(base, label),
|
|
10086
|
+
content,
|
|
10087
|
+
extracted: own2
|
|
10088
|
+
});
|
|
10089
|
+
}
|
|
10090
|
+
extracted = mergeExternalMetadata(label, own2, externalMetadata, members, base).extracted;
|
|
8062
10091
|
} catch (err) {
|
|
8063
10092
|
if (err instanceof DocmetaError) throw err;
|
|
8064
10093
|
const reason = err instanceof Error ? err.message : String(err);
|
|
@@ -8066,7 +10095,12 @@ async function runGet(opts) {
|
|
|
8066
10095
|
return;
|
|
8067
10096
|
}
|
|
8068
10097
|
const values = {};
|
|
8069
|
-
|
|
10098
|
+
const carries = {};
|
|
10099
|
+
for (const f of opts.fields) {
|
|
10100
|
+
values[f] = resolveField(extracted.data, f);
|
|
10101
|
+
carries[f] = carriesField(extracted.data, f);
|
|
10102
|
+
}
|
|
10103
|
+
carried.set(label, carries);
|
|
8070
10104
|
out.push({ file: label, present: extracted.present, values });
|
|
8071
10105
|
};
|
|
8072
10106
|
if (usingStdin) {
|
|
@@ -8075,14 +10109,78 @@ async function runGet(opts) {
|
|
|
8075
10109
|
"Reading from stdin (`-`) requires --as <format> to choose an extractor."
|
|
8076
10110
|
);
|
|
8077
10111
|
}
|
|
8078
|
-
readOne(
|
|
10112
|
+
readOne(STDIN_LABEL, opts.stdinContent ?? "", forced.extensions[0] ?? "");
|
|
8079
10113
|
}
|
|
8080
10114
|
for (const file of files) {
|
|
8081
|
-
const content = await
|
|
8082
|
-
readOne(file, content,
|
|
10115
|
+
const content = await readFile8(resolve12(base, file), "utf8");
|
|
10116
|
+
readOne(file, content, extname4(file));
|
|
10117
|
+
}
|
|
10118
|
+
if (deriving) {
|
|
10119
|
+
await attachResolved(out, deriveInputs, {
|
|
10120
|
+
fields: opts.fields,
|
|
10121
|
+
derivable: derivableFields2,
|
|
10122
|
+
carried,
|
|
10123
|
+
run: {
|
|
10124
|
+
cwd,
|
|
10125
|
+
base,
|
|
10126
|
+
configDir,
|
|
10127
|
+
config,
|
|
10128
|
+
cache: opts.cache ?? true,
|
|
10129
|
+
now: opts.now ?? (() => /* @__PURE__ */ new Date())
|
|
10130
|
+
}
|
|
10131
|
+
});
|
|
8083
10132
|
}
|
|
8084
10133
|
return out;
|
|
8085
10134
|
}
|
|
10135
|
+
async function attachResolved(out, inputs, opts) {
|
|
10136
|
+
const { run: run2 } = opts;
|
|
10137
|
+
const derive = run2.config?.derive;
|
|
10138
|
+
const commands = commandsOf(derive);
|
|
10139
|
+
const records = opts.derivable.length === 0 || inputs.length === 0 ? /* @__PURE__ */ new Map() : await (async () => {
|
|
10140
|
+
const result = await deriveMetadata(inputs, {
|
|
10141
|
+
cwd: run2.cwd,
|
|
10142
|
+
base: run2.base,
|
|
10143
|
+
...run2.configDir !== void 0 ? { configDir: run2.configDir } : {},
|
|
10144
|
+
sources: derive?.sources ?? [...DERIVE_SOURCES],
|
|
10145
|
+
fields: opts.derivable,
|
|
10146
|
+
...derive?.codeowners !== void 0 ? { codeowners: derive.codeowners } : {},
|
|
10147
|
+
...commands !== void 0 ? { commands } : {},
|
|
10148
|
+
cache: run2.cache,
|
|
10149
|
+
now: run2.now
|
|
10150
|
+
});
|
|
10151
|
+
assertSourcesAvailable(
|
|
10152
|
+
result.sources,
|
|
10153
|
+
"pass --no-derived, or narrow derive.sources"
|
|
10154
|
+
);
|
|
10155
|
+
return result.records;
|
|
10156
|
+
})();
|
|
10157
|
+
for (const r of out) {
|
|
10158
|
+
if (r.error !== void 0) continue;
|
|
10159
|
+
const record = records.get(r.file);
|
|
10160
|
+
const derived = {};
|
|
10161
|
+
for (const field of opts.derivable) {
|
|
10162
|
+
derived[field] = record?.fields[field] ?? null;
|
|
10163
|
+
}
|
|
10164
|
+
const carries = opts.carried.get(r.file) ?? {};
|
|
10165
|
+
const resolved = {};
|
|
10166
|
+
const origin = {};
|
|
10167
|
+
for (const field of opts.fields) {
|
|
10168
|
+
const evidence = derived[field];
|
|
10169
|
+
if (carries[field] === true) {
|
|
10170
|
+
resolved[field] = r.values[field];
|
|
10171
|
+
origin[field] = "asserted";
|
|
10172
|
+
} else if (evidence != null) {
|
|
10173
|
+
resolved[field] = evidence.value;
|
|
10174
|
+
origin[field] = "derived";
|
|
10175
|
+
} else {
|
|
10176
|
+
resolved[field] = void 0;
|
|
10177
|
+
}
|
|
10178
|
+
}
|
|
10179
|
+
if (opts.derivable.length > 0) r.derived = derived;
|
|
10180
|
+
r.resolved = resolved;
|
|
10181
|
+
r.origin = origin;
|
|
10182
|
+
}
|
|
10183
|
+
}
|
|
8086
10184
|
function resolveField(data, field) {
|
|
8087
10185
|
const segments = field.startsWith("/") ? parseJsonPointer(field) : field.split(".");
|
|
8088
10186
|
const descended = descend(data, segments);
|
|
@@ -8092,6 +10190,23 @@ function resolveField(data, field) {
|
|
|
8092
10190
|
}
|
|
8093
10191
|
return void 0;
|
|
8094
10192
|
}
|
|
10193
|
+
function carriesField(data, field) {
|
|
10194
|
+
const segments = field.startsWith("/") ? parseJsonPointer(field) : field.split(".");
|
|
10195
|
+
if (carriesPath(data, segments)) return true;
|
|
10196
|
+
return !field.startsWith("/") && field.includes(".") && Object.prototype.hasOwnProperty.call(data, field);
|
|
10197
|
+
}
|
|
10198
|
+
function carriesPath(data, segments) {
|
|
10199
|
+
const last = segments.at(-1);
|
|
10200
|
+
if (last === void 0) return true;
|
|
10201
|
+
const parent = segments.length === 1 ? data : descend(data, segments.slice(0, -1));
|
|
10202
|
+
if (Array.isArray(parent)) {
|
|
10203
|
+
return /^\d+$/.test(last) && Number(last) < parent.length;
|
|
10204
|
+
}
|
|
10205
|
+
if (parent !== null && typeof parent === "object") {
|
|
10206
|
+
return Object.prototype.hasOwnProperty.call(parent, last);
|
|
10207
|
+
}
|
|
10208
|
+
return false;
|
|
10209
|
+
}
|
|
8095
10210
|
function descend(data, segments) {
|
|
8096
10211
|
let current = data;
|
|
8097
10212
|
for (const segment of segments) {
|
|
@@ -8114,9 +10229,9 @@ function parseJsonPointer(pointer) {
|
|
|
8114
10229
|
}
|
|
8115
10230
|
|
|
8116
10231
|
// src/meta/commands/schemas.ts
|
|
8117
|
-
import { existsSync as
|
|
8118
|
-
import { mkdir as mkdir2, readFile as
|
|
8119
|
-
import { dirname as
|
|
10232
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
10233
|
+
import { mkdir as mkdir2, readFile as readFile9 } from "node:fs/promises";
|
|
10234
|
+
import { dirname as dirname8, extname as extname5, isAbsolute as isAbsolute4, join as join6, relative as relative10, resolve as resolve13, sep as sep6 } from "node:path";
|
|
8120
10235
|
import { parseDocument as parseDocument4, parse as parseYaml3, stringify } from "yaml";
|
|
8121
10236
|
function getSchemasInfo() {
|
|
8122
10237
|
return { builtins: listBuiltins(), formats: listFormats() };
|
|
@@ -8124,7 +10239,7 @@ function getSchemasInfo() {
|
|
|
8124
10239
|
var DEFAULT_VENDOR_DIR = "./schema";
|
|
8125
10240
|
var DEFAULT_CONFIG_NAME = "manni.config.yaml";
|
|
8126
10241
|
function posixRelative(from, to) {
|
|
8127
|
-
return
|
|
10242
|
+
return relative10(from, to).split(sep6).join("/");
|
|
8128
10243
|
}
|
|
8129
10244
|
function vendorFileName(url) {
|
|
8130
10245
|
let parsed2;
|
|
@@ -8149,20 +10264,20 @@ function vendorFileName(url) {
|
|
|
8149
10264
|
if (!name2.toLowerCase().endsWith(".json")) name2 += ".json";
|
|
8150
10265
|
return name2;
|
|
8151
10266
|
}
|
|
8152
|
-
async function assertNotIgnored(absFile, absDir, cwd, onNotice,
|
|
10267
|
+
async function assertNotIgnored(absFile, absDir, cwd, onNotice, text2 = VENDOR_IGNORE_TEXT) {
|
|
8153
10268
|
const relFile = posixRelative(cwd, absFile);
|
|
8154
10269
|
const relDir = posixRelative(cwd, absDir);
|
|
8155
10270
|
const candidates = [relFile];
|
|
8156
10271
|
if (relDir !== "" && !relDir.startsWith("..")) candidates.push(relDir);
|
|
8157
10272
|
const answer = await gitIgnored(candidates, cwd);
|
|
8158
10273
|
if (!answer.available) {
|
|
8159
|
-
onNotice?.(
|
|
10274
|
+
onNotice?.(text2.unchecked(relDir || "."));
|
|
8160
10275
|
return;
|
|
8161
10276
|
}
|
|
8162
10277
|
const ignoredDir = answer.ignored.has(relDir);
|
|
8163
10278
|
const ignoredFile = answer.ignored.has(relFile);
|
|
8164
10279
|
if (!ignoredDir && !ignoredFile) return;
|
|
8165
|
-
throw new DocmetaError(
|
|
10280
|
+
throw new DocmetaError(text2.refusal(ignoredDir ? relDir : relFile));
|
|
8166
10281
|
}
|
|
8167
10282
|
var VENDOR_IGNORE_TEXT = {
|
|
8168
10283
|
refusal: (target) => `Refusing to vendor into "${target}": git reports it as ignored. A vendored schema has to be committed \u2014 an ignored copy validates on this machine and is simply missing in CI, where the failure reads as a schema nobody changed. Vendor into a directory your repository tracks (\`--dir\`, default \`${DEFAULT_VENDOR_DIR}\`), or drop the .gitignore rule covering this path.`,
|
|
@@ -8193,12 +10308,12 @@ function renderConfig(existing, entries, section) {
|
|
|
8193
10308
|
doc.setIn(section === void 0 ? ["schemas"] : [section, "schemas"], entries);
|
|
8194
10309
|
return doc.toString();
|
|
8195
10310
|
}
|
|
8196
|
-
function reparseConfig(
|
|
10311
|
+
function reparseConfig(text2, source, section) {
|
|
8197
10312
|
if (section === void 0) {
|
|
8198
|
-
parseConfig(
|
|
10313
|
+
parseConfig(text2, source);
|
|
8199
10314
|
return;
|
|
8200
10315
|
}
|
|
8201
|
-
const doc = parseYaml3(
|
|
10316
|
+
const doc = parseYaml3(text2);
|
|
8202
10317
|
parseConfigValue(doc[section], source, section);
|
|
8203
10318
|
}
|
|
8204
10319
|
async function runVendorSchema(opts) {
|
|
@@ -8210,10 +10325,10 @@ async function runVendorSchema(opts) {
|
|
|
8210
10325
|
);
|
|
8211
10326
|
}
|
|
8212
10327
|
const loaded = await loadConfig(opts.configPath, cwd);
|
|
8213
|
-
const configPath = loaded?.path ??
|
|
10328
|
+
const configPath = loaded?.path ?? resolve13(cwd, DEFAULT_CONFIG_NAME);
|
|
8214
10329
|
const configDir = loaded?.dir ?? cwd;
|
|
8215
|
-
const absDir =
|
|
8216
|
-
const absFile =
|
|
10330
|
+
const absDir = resolve13(cwd, opts.dir ?? DEFAULT_VENDOR_DIR);
|
|
10331
|
+
const absFile = join6(absDir, vendorFileName(url));
|
|
8217
10332
|
await assertNotIgnored(absFile, absDir, cwd, opts.onNotice);
|
|
8218
10333
|
const fromConfig = posixRelative(configDir, absFile);
|
|
8219
10334
|
const ref = isAbsolute4(fromConfig) || fromConfig === "" ? absFile : fromConfig.startsWith(".") ? fromConfig : `./${fromConfig}`;
|
|
@@ -8230,8 +10345,8 @@ async function runVendorSchema(opts) {
|
|
|
8230
10345
|
);
|
|
8231
10346
|
}
|
|
8232
10347
|
let unchanged = false;
|
|
8233
|
-
if (
|
|
8234
|
-
const current = await
|
|
10348
|
+
if (existsSync4(absFile)) {
|
|
10349
|
+
const current = await readFile9(absFile);
|
|
8235
10350
|
unchanged = bytes.equals(current);
|
|
8236
10351
|
if (!unchanged && !folded.replaced) {
|
|
8237
10352
|
throw new DocmetaError(
|
|
@@ -8241,11 +10356,11 @@ async function runVendorSchema(opts) {
|
|
|
8241
10356
|
}
|
|
8242
10357
|
await mkdir2(absDir, { recursive: true });
|
|
8243
10358
|
await writeFileAtomic(absFile, bytes);
|
|
8244
|
-
const existing = loaded ? await
|
|
10359
|
+
const existing = loaded ? await readFile9(configPath, "utf8") : null;
|
|
8245
10360
|
const section = loaded ? loaded.section : META_SECTION;
|
|
8246
|
-
const
|
|
8247
|
-
reparseConfig(
|
|
8248
|
-
await writeFileAtomic(configPath,
|
|
10361
|
+
const text2 = renderConfig(existing, folded.entries, section);
|
|
10362
|
+
reparseConfig(text2, posixRelative(cwd, configPath) || DEFAULT_CONFIG_NAME, section);
|
|
10363
|
+
await writeFileAtomic(configPath, text2);
|
|
8249
10364
|
return {
|
|
8250
10365
|
url,
|
|
8251
10366
|
file: posixRelative(cwd, absFile),
|
|
@@ -8392,15 +10507,15 @@ async function runInferSchema(opts) {
|
|
|
8392
10507
|
}
|
|
8393
10508
|
let absOut;
|
|
8394
10509
|
if (opts.out !== void 0) {
|
|
8395
|
-
absOut =
|
|
10510
|
+
absOut = resolve13(cwd, opts.out);
|
|
8396
10511
|
await assertNotIgnored(
|
|
8397
10512
|
absOut,
|
|
8398
|
-
|
|
10513
|
+
dirname8(absOut),
|
|
8399
10514
|
cwd,
|
|
8400
10515
|
opts.onNotice,
|
|
8401
10516
|
DRAFT_IGNORE_TEXT
|
|
8402
10517
|
);
|
|
8403
|
-
if (
|
|
10518
|
+
if (existsSync4(absOut)) {
|
|
8404
10519
|
throw new DocmetaError(
|
|
8405
10520
|
`"${posixRelative(cwd, absOut) || opts.out}" already exists, and \`schemas infer\` will not overwrite it. The draft is a starting point you then edit, so clobbering it would throw away the edits. Pass a different --out path, or remove that file first.`
|
|
8406
10521
|
);
|
|
@@ -8479,8 +10594,8 @@ async function runInferSchema(opts) {
|
|
|
8479
10594
|
scanOne(STDIN_LABEL, opts.stdinContent ?? "", forced.extensions[0] ?? "");
|
|
8480
10595
|
}
|
|
8481
10596
|
for (const file of files) {
|
|
8482
|
-
const content = await
|
|
8483
|
-
scanOne(file, content,
|
|
10597
|
+
const content = await readFile9(resolve13(base, file), "utf8");
|
|
10598
|
+
scanOne(file, content, extname5(file));
|
|
8484
10599
|
}
|
|
8485
10600
|
for (const u of unreadable) {
|
|
8486
10601
|
opts.onNotice?.(`could not read metadata from "${u.file}": ${u.message}`);
|
|
@@ -8521,7 +10636,7 @@ async function runInferSchema(opts) {
|
|
|
8521
10636
|
};
|
|
8522
10637
|
const keys = kept.map(({ sawEmptyString: _drop, ...rest }) => rest);
|
|
8523
10638
|
if (absOut !== void 0) {
|
|
8524
|
-
await mkdir2(
|
|
10639
|
+
await mkdir2(dirname8(absOut), { recursive: true });
|
|
8525
10640
|
await writeFileAtomic(absOut, `${JSON.stringify(draft, null, 2)}
|
|
8526
10641
|
`);
|
|
8527
10642
|
}
|
|
@@ -8539,9 +10654,9 @@ async function runInferSchema(opts) {
|
|
|
8539
10654
|
|
|
8540
10655
|
// src/meta/commands/query.ts
|
|
8541
10656
|
import { randomBytes } from "node:crypto";
|
|
8542
|
-
import { existsSync as
|
|
8543
|
-
import { mkdir as mkdir3, open, readFile as
|
|
8544
|
-
import { dirname as
|
|
10657
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
10658
|
+
import { mkdir as mkdir3, open, readFile as readFile10, rename as rename2, rm as rm2 } from "node:fs/promises";
|
|
10659
|
+
import { dirname as dirname9, isAbsolute as isAbsolute5, resolve as resolve14, extname as extname6, sep as sep7 } from "node:path";
|
|
8545
10660
|
import { parseDocument as parseDocument5, isMap as isMap4, isSeq as isSeq2, isScalar as isScalar3 } from "yaml";
|
|
8546
10661
|
async function runQuery(opts) {
|
|
8547
10662
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -8646,16 +10761,11 @@ async function runQuery(opts) {
|
|
|
8646
10761
|
);
|
|
8647
10762
|
}
|
|
8648
10763
|
const members = membersFor(label);
|
|
8649
|
-
const
|
|
8650
|
-
label,
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
externalMetadata,
|
|
8655
|
-
members,
|
|
8656
|
-
base
|
|
8657
|
-
).extracted;
|
|
8658
|
-
entries.push({ label, extracted, extractor });
|
|
10764
|
+
const own2 = extractor.extract(content, label, {
|
|
10765
|
+
elements: resolveElements(label, config, members)
|
|
10766
|
+
});
|
|
10767
|
+
const extracted = mergeExternalMetadata(label, own2, externalMetadata, members, base).extracted;
|
|
10768
|
+
entries.push({ label, extracted, extractor, own: own2, content });
|
|
8659
10769
|
};
|
|
8660
10770
|
if (usingStdin) {
|
|
8661
10771
|
if (!forced) {
|
|
@@ -8672,15 +10782,17 @@ async function runQuery(opts) {
|
|
|
8672
10782
|
readOne(STDIN_LABEL, opts.stdinContent ?? "", ext);
|
|
8673
10783
|
}
|
|
8674
10784
|
for (const file of files) {
|
|
8675
|
-
const content = await
|
|
8676
|
-
readOne(file, content,
|
|
10785
|
+
const content = await readFile10(resolve14(base, file), "utf8");
|
|
10786
|
+
readOne(file, content, extname6(file));
|
|
8677
10787
|
}
|
|
8678
|
-
const db = opts.db === void 0 ? void 0 : { resolved:
|
|
8679
|
-
const
|
|
10788
|
+
const db = opts.db === void 0 ? void 0 : { resolved: resolve14(cwd, opts.db), display: opts.db };
|
|
10789
|
+
const run2 = await runSql(sql, entries, {
|
|
8680
10790
|
target: db,
|
|
8681
10791
|
params: boundParams,
|
|
8682
10792
|
...opts.schemas !== void 0 && opts.schemas.length > 0 ? { cliSchemas: opts.schemas } : {},
|
|
8683
10793
|
write: !opts.dryRun,
|
|
10794
|
+
cache: opts.cache ?? true,
|
|
10795
|
+
...opts.now !== void 0 ? { now: opts.now } : {},
|
|
8684
10796
|
base,
|
|
8685
10797
|
config,
|
|
8686
10798
|
cwd,
|
|
@@ -8692,9 +10804,10 @@ async function runQuery(opts) {
|
|
|
8692
10804
|
collections,
|
|
8693
10805
|
declaredCollections,
|
|
8694
10806
|
memberships: membersFor,
|
|
10807
|
+
managed: new Set(config?.derive?.fields ?? []),
|
|
8695
10808
|
onNotice: opts.onNotice
|
|
8696
10809
|
});
|
|
8697
|
-
return { ...
|
|
10810
|
+
return { ...run2, frame: { cwd, base: configDir ?? cwd, runBase: base } };
|
|
8698
10811
|
}
|
|
8699
10812
|
function bindParams(params) {
|
|
8700
10813
|
const out = /* @__PURE__ */ Object.create(null);
|
|
@@ -8733,16 +10846,28 @@ async function prepareDbTarget(resolved, display) {
|
|
|
8733
10846
|
}
|
|
8734
10847
|
var MissingCollectionView = class extends Error {
|
|
8735
10848
|
};
|
|
10849
|
+
var MissingDerivedTable = class extends Error {
|
|
10850
|
+
};
|
|
8736
10851
|
function refuseSqlError(err) {
|
|
8737
|
-
const message = err
|
|
10852
|
+
const message = errorMessage(err);
|
|
8738
10853
|
if (message.includes("UNIQUE constraint failed: docs._path")) {
|
|
8739
10854
|
throw new DocmetaError("That _path already exists in the corpus.");
|
|
8740
10855
|
}
|
|
8741
10856
|
const viewWrite = /cannot modify (.+?) because it is a view/.exec(message);
|
|
8742
10857
|
const viewName = viewWrite?.[1];
|
|
8743
10858
|
if (viewName !== void 0) {
|
|
10859
|
+
if (viewName.toLowerCase() === DERIVED_VIEW) {
|
|
10860
|
+
throw new DocmetaError(
|
|
10861
|
+
`SQL error: ${message}; a collection or the derived table is read-only \u2014 the derived table is recomputed from the evidence on every run. Change the evidence, or stamp it into the documents with manni meta derive.`
|
|
10862
|
+
);
|
|
10863
|
+
}
|
|
10864
|
+
if (viewName.toLowerCase() === RESOLVED_VIEW) {
|
|
10865
|
+
throw new DocmetaError(
|
|
10866
|
+
`SQL error: ${message}; the resolved table is read-only \u2014 it is \`docs\` and the evidence joined, so write to docs, or stamp the evidence with manni meta derive.`
|
|
10867
|
+
);
|
|
10868
|
+
}
|
|
8744
10869
|
throw new DocmetaError(
|
|
8745
|
-
`SQL error: ${message}; a collection is read-only \u2014 write through docs: UPDATE docs \u2026 WHERE _path IN (SELECT _path FROM "${viewName.replaceAll('"', '""')}").`
|
|
10870
|
+
`SQL error: ${message}; a collection or the derived table is read-only \u2014 write through docs: UPDATE docs \u2026 WHERE _path IN (SELECT _path FROM "${viewName.replaceAll('"', '""')}").`
|
|
8746
10871
|
);
|
|
8747
10872
|
}
|
|
8748
10873
|
throw new DocmetaError(`SQL error: ${message}`);
|
|
@@ -8755,7 +10880,7 @@ async function runSql(sql, entries, ctx) {
|
|
|
8755
10880
|
);
|
|
8756
10881
|
const { DatabaseSync } = await loadSqlite();
|
|
8757
10882
|
if (target) {
|
|
8758
|
-
await mkdir3(
|
|
10883
|
+
await mkdir3(dirname9(target.resolved), { recursive: true });
|
|
8759
10884
|
await prepareDbTarget(target.resolved, target.display);
|
|
8760
10885
|
}
|
|
8761
10886
|
let db;
|
|
@@ -8763,7 +10888,7 @@ async function runSql(sql, entries, ctx) {
|
|
|
8763
10888
|
db = new DatabaseSync(target ? target.resolved : ":memory:");
|
|
8764
10889
|
} catch (err) {
|
|
8765
10890
|
throw new DocmetaError(
|
|
8766
|
-
`Cannot open "${target?.display ?? ":memory:"}": ${err
|
|
10891
|
+
`Cannot open "${target?.display ?? ":memory:"}": ${errorMessage(err)}`
|
|
8767
10892
|
);
|
|
8768
10893
|
}
|
|
8769
10894
|
const dbInfo = target ? {
|
|
@@ -8771,6 +10896,7 @@ async function runSql(sql, entries, ctx) {
|
|
|
8771
10896
|
files: entries.length,
|
|
8772
10897
|
columns: SYSTEM_COLUMNS.length + dataColumns.length
|
|
8773
10898
|
} : void 0;
|
|
10899
|
+
const derivedTable = { built: false };
|
|
8774
10900
|
try {
|
|
8775
10901
|
createDocsTable(db, entries, dataColumns);
|
|
8776
10902
|
let viewsBuilt = false;
|
|
@@ -8787,9 +10913,36 @@ async function runSql(sql, entries, ctx) {
|
|
|
8787
10913
|
viewsBuilt = true;
|
|
8788
10914
|
};
|
|
8789
10915
|
if (target) buildViews();
|
|
10916
|
+
const buildDerived = async () => {
|
|
10917
|
+
if (derivedTable.built) return;
|
|
10918
|
+
const inputs = entries.filter((e) => e.label !== STDIN_LABEL).map((e) => ({
|
|
10919
|
+
label: e.label,
|
|
10920
|
+
absPath: resolve14(ctx.base, e.label),
|
|
10921
|
+
content: e.content,
|
|
10922
|
+
extracted: e.own
|
|
10923
|
+
}));
|
|
10924
|
+
const commands = commandsOf(ctx.config?.derive);
|
|
10925
|
+
const records = await deriveForTable(
|
|
10926
|
+
inputs,
|
|
10927
|
+
{
|
|
10928
|
+
cwd: ctx.cwd,
|
|
10929
|
+
base: ctx.base,
|
|
10930
|
+
...ctx.configDir !== void 0 ? { configDir: ctx.configDir } : {},
|
|
10931
|
+
config: ctx.config,
|
|
10932
|
+
cache: ctx.cache,
|
|
10933
|
+
...ctx.now !== void 0 ? { now: ctx.now } : {}
|
|
10934
|
+
},
|
|
10935
|
+
"narrow derive.sources in manni.config.yaml",
|
|
10936
|
+
fieldsForSql(sql, derivableFields(commands))
|
|
10937
|
+
);
|
|
10938
|
+
createDerivedView(db, records, derivedColumns(commands));
|
|
10939
|
+
createResolvedView(db, dataColumns, commands);
|
|
10940
|
+
derivedTable.built = true;
|
|
10941
|
+
};
|
|
8790
10942
|
if (sql === "") {
|
|
8791
10943
|
return { columns: [], rows: [], ...dbInfo ? { db: dbInfo } : {} };
|
|
8792
10944
|
}
|
|
10945
|
+
if (mentionsDerived(sql) || mentionsResolved(sql)) await buildDerived();
|
|
8793
10946
|
const head = stripLeadingTrivia(sql);
|
|
8794
10947
|
if (/^(attach|vacuum)\b/i.test(head)) {
|
|
8795
10948
|
throw new DocmetaError(
|
|
@@ -8811,11 +10964,15 @@ async function runSql(sql, entries, ctx) {
|
|
|
8811
10964
|
stmt = db.prepare(sql);
|
|
8812
10965
|
columns = stmt.columns().map((c) => c.name);
|
|
8813
10966
|
} catch (err) {
|
|
8814
|
-
const message = err
|
|
10967
|
+
const message = errorMessage(err);
|
|
8815
10968
|
const missing = missingTableName(message);
|
|
8816
10969
|
if (missing !== void 0 && !viewsBuilt && namesConfiguredCollection(missing, ctx.declaredCollections)) {
|
|
8817
10970
|
throw new MissingCollectionView();
|
|
8818
10971
|
}
|
|
10972
|
+
const missingBare = missing?.replace(/^(main|temp)\./i, "").toLowerCase();
|
|
10973
|
+
if (!derivedTable.built && (missingBare === DERIVED_VIEW || missingBare === RESOLVED_VIEW)) {
|
|
10974
|
+
throw new MissingDerivedTable();
|
|
10975
|
+
}
|
|
8819
10976
|
refuseSqlError(err);
|
|
8820
10977
|
}
|
|
8821
10978
|
const before = snapshotRows(db);
|
|
@@ -8874,11 +11031,20 @@ async function runSql(sql, entries, ctx) {
|
|
|
8874
11031
|
try {
|
|
8875
11032
|
return await runOnce();
|
|
8876
11033
|
} catch (err) {
|
|
11034
|
+
if (err instanceof MissingDerivedTable) {
|
|
11035
|
+
await buildDerived();
|
|
11036
|
+
return await runOnce();
|
|
11037
|
+
}
|
|
8877
11038
|
if (!(err instanceof MissingCollectionView)) throw err;
|
|
8878
11039
|
buildViews();
|
|
8879
11040
|
return await runOnce();
|
|
8880
11041
|
}
|
|
8881
11042
|
} finally {
|
|
11043
|
+
if (target && derivedTable.built) {
|
|
11044
|
+
db.exec(
|
|
11045
|
+
`DROP VIEW IF EXISTS ${RESOLVED_VIEW}; DROP VIEW IF EXISTS ${DERIVED_VIEW}; DROP TABLE IF EXISTS ${DERIVED_ROWS}`
|
|
11046
|
+
);
|
|
11047
|
+
}
|
|
8882
11048
|
db.close();
|
|
8883
11049
|
}
|
|
8884
11050
|
}
|
|
@@ -9030,21 +11196,21 @@ function appendedColumnDef(before, after) {
|
|
|
9030
11196
|
const comma = /^\s*,\s*/.exec(appended);
|
|
9031
11197
|
return comma ? appended.slice(comma[0].length) : void 0;
|
|
9032
11198
|
}
|
|
9033
|
-
function skipWs(
|
|
11199
|
+
function skipWs(text2, from) {
|
|
9034
11200
|
let i = from;
|
|
9035
|
-
while (i <
|
|
11201
|
+
while (i < text2.length && /\s/.test(text2[i] ?? "")) i++;
|
|
9036
11202
|
return i;
|
|
9037
11203
|
}
|
|
9038
|
-
function indexOfCheck(
|
|
11204
|
+
function indexOfCheck(text2, from) {
|
|
9039
11205
|
let i = from;
|
|
9040
|
-
while (i <
|
|
9041
|
-
const ch =
|
|
9042
|
-
if (startsStringOrIdent(
|
|
9043
|
-
i = skipStringOrIdent(
|
|
9044
|
-
} else if (startsComment(
|
|
9045
|
-
i = skipComment(
|
|
11206
|
+
while (i < text2.length) {
|
|
11207
|
+
const ch = text2[i];
|
|
11208
|
+
if (startsStringOrIdent(text2, i)) {
|
|
11209
|
+
i = skipStringOrIdent(text2, i);
|
|
11210
|
+
} else if (startsComment(text2, i)) {
|
|
11211
|
+
i = skipComment(text2, i);
|
|
9046
11212
|
} else if (ch !== void 0 && /[A-Za-z_]/.test(ch)) {
|
|
9047
|
-
const m = /^[A-Za-z_][A-Za-z0-9_]*/.exec(
|
|
11213
|
+
const m = /^[A-Za-z_][A-Za-z0-9_]*/.exec(text2.slice(i));
|
|
9048
11214
|
const word = m?.[0] ?? "";
|
|
9049
11215
|
if (word.toUpperCase() === "CHECK") return i;
|
|
9050
11216
|
i += Math.max(word.length, 1);
|
|
@@ -9054,17 +11220,17 @@ function indexOfCheck(text, from) {
|
|
|
9054
11220
|
}
|
|
9055
11221
|
return -1;
|
|
9056
11222
|
}
|
|
9057
|
-
function matchingParenEnd(
|
|
11223
|
+
function matchingParenEnd(text2, open2) {
|
|
9058
11224
|
let depth = 0;
|
|
9059
11225
|
let i = open2;
|
|
9060
|
-
while (i <
|
|
9061
|
-
const ch =
|
|
9062
|
-
if (startsStringOrIdent(
|
|
9063
|
-
i = skipStringOrIdent(
|
|
11226
|
+
while (i < text2.length) {
|
|
11227
|
+
const ch = text2[i];
|
|
11228
|
+
if (startsStringOrIdent(text2, i)) {
|
|
11229
|
+
i = skipStringOrIdent(text2, i);
|
|
9064
11230
|
continue;
|
|
9065
11231
|
}
|
|
9066
|
-
if (startsComment(
|
|
9067
|
-
i = skipComment(
|
|
11232
|
+
if (startsComment(text2, i)) {
|
|
11233
|
+
i = skipComment(text2, i);
|
|
9068
11234
|
continue;
|
|
9069
11235
|
}
|
|
9070
11236
|
if (ch === "(") depth++;
|
|
@@ -9084,9 +11250,9 @@ function enumForAddedColumn(catalogBefore, catalogAfter, key, mapped) {
|
|
|
9084
11250
|
};
|
|
9085
11251
|
const def = appendedColumnDef(catalogBefore, catalogAfter);
|
|
9086
11252
|
if (def === void 0) {
|
|
9087
|
-
const count = (
|
|
11253
|
+
const count = (text2) => {
|
|
9088
11254
|
let n = 0;
|
|
9089
|
-
for (let i2 = indexOfCheck(
|
|
11255
|
+
for (let i2 = indexOfCheck(text2, 0); i2 !== -1; i2 = indexOfCheck(text2, i2 + 5)) {
|
|
9090
11256
|
n++;
|
|
9091
11257
|
}
|
|
9092
11258
|
return n;
|
|
@@ -9199,8 +11365,8 @@ var FORK_IGNORE_TEXT = {
|
|
|
9199
11365
|
refusal: (target) => `Refusing to fork into "${target}": git reports it as ignored. The forked schema must be committed \u2014 an ignored copy validates on this machine and is simply missing in CI, where the failure reads as a schema nobody changed. Drop the .gitignore rule covering this path, or evolve a tracked local schema instead.`,
|
|
9200
11366
|
unchecked: (where) => `could not check .gitignore for "${where}" (no repository here, or no git on PATH). The forked schema must be committed \u2014 make sure this path is tracked.`
|
|
9201
11367
|
};
|
|
9202
|
-
function matchEol(reference,
|
|
9203
|
-
return reference.includes("\r\n") ?
|
|
11368
|
+
function matchEol(reference, text2) {
|
|
11369
|
+
return reference.includes("\r\n") ? text2.replace(/\n/g, "\r\n") : text2;
|
|
9204
11370
|
}
|
|
9205
11371
|
function displayPath(ctx, abs) {
|
|
9206
11372
|
const rel = posixRelative(ctx.base, abs);
|
|
@@ -9214,14 +11380,14 @@ function assertSchemaWriteWithin(ctx, abs) {
|
|
|
9214
11380
|
);
|
|
9215
11381
|
}
|
|
9216
11382
|
function foldPath(p) {
|
|
9217
|
-
const abs =
|
|
11383
|
+
const abs = resolve14(p);
|
|
9218
11384
|
return process.platform === "win32" ? abs.toLowerCase() : abs;
|
|
9219
11385
|
}
|
|
9220
11386
|
function filePinsByPath(ctx) {
|
|
9221
11387
|
const byPath = /* @__PURE__ */ new Map();
|
|
9222
11388
|
for (const [ref, pin] of collectSchemaPins(ctx.config)) {
|
|
9223
11389
|
if (classifyRef(ref).kind !== "file") continue;
|
|
9224
|
-
byPath.set(foldPath(
|
|
11390
|
+
byPath.set(foldPath(resolve14(ctx.cwd, ref)), pin);
|
|
9225
11391
|
}
|
|
9226
11392
|
return byPath;
|
|
9227
11393
|
}
|
|
@@ -9249,13 +11415,13 @@ async function loadSetMembers(refs, ctx) {
|
|
|
9249
11415
|
});
|
|
9250
11416
|
continue;
|
|
9251
11417
|
}
|
|
9252
|
-
const abs =
|
|
11418
|
+
const abs = resolve14(ctx.cwd, ref);
|
|
9253
11419
|
const canon = foldPath(abs);
|
|
9254
11420
|
if (seenFiles.has(canon)) continue;
|
|
9255
11421
|
seenFiles.add(canon);
|
|
9256
|
-
let
|
|
11422
|
+
let text2;
|
|
9257
11423
|
try {
|
|
9258
|
-
|
|
11424
|
+
text2 = await readFile10(abs, "utf8");
|
|
9259
11425
|
} catch {
|
|
9260
11426
|
throw new DocmetaError(
|
|
9261
11427
|
`Schema file not found: "${ref}" (looked at ${abs}).`
|
|
@@ -9263,7 +11429,7 @@ async function loadSetMembers(refs, ctx) {
|
|
|
9263
11429
|
}
|
|
9264
11430
|
const pin = pins.get(canon);
|
|
9265
11431
|
if (pin?.integrity !== void 0) {
|
|
9266
|
-
if (integrityOf(Buffer.from(
|
|
11432
|
+
if (integrityOf(Buffer.from(text2, "utf8")) !== pin.integrity) {
|
|
9267
11433
|
throw new DocmetaError(
|
|
9268
11434
|
`Schema "${ref}" does not match its recorded integrity; its contents have moved since it was vendored. Re-vendor it before evolving it.`
|
|
9269
11435
|
);
|
|
@@ -9271,7 +11437,7 @@ async function loadSetMembers(refs, ctx) {
|
|
|
9271
11437
|
}
|
|
9272
11438
|
let parsed2;
|
|
9273
11439
|
try {
|
|
9274
|
-
parsed2 = JSON.parse(stripBom(
|
|
11440
|
+
parsed2 = JSON.parse(stripBom(text2));
|
|
9275
11441
|
} catch {
|
|
9276
11442
|
throw new DocmetaError(
|
|
9277
11443
|
`Schema "${ref}" is not valid JSON, so DDL cannot read it. Fix the file first.`
|
|
@@ -9286,7 +11452,7 @@ async function loadSetMembers(refs, ctx) {
|
|
|
9286
11452
|
ref,
|
|
9287
11453
|
kind: "file",
|
|
9288
11454
|
abs,
|
|
9289
|
-
text,
|
|
11455
|
+
text: text2,
|
|
9290
11456
|
schema: parsed2
|
|
9291
11457
|
});
|
|
9292
11458
|
}
|
|
@@ -9331,7 +11497,7 @@ async function planSchemaMutation(ops, entries, ctx) {
|
|
|
9331
11497
|
onNotice: ctx.onNotice
|
|
9332
11498
|
});
|
|
9333
11499
|
} catch (err) {
|
|
9334
|
-
throw new DocmetaError(`"${e.label}": ${err
|
|
11500
|
+
throw new DocmetaError(`"${e.label}": ${errorMessage(err)}`);
|
|
9335
11501
|
}
|
|
9336
11502
|
sources.add(resolved.source);
|
|
9337
11503
|
if (resolved.overrideIndex !== void 0) {
|
|
@@ -9454,16 +11620,16 @@ async function planSchemaMutation(ops, entries, ctx) {
|
|
|
9454
11620
|
const [, name2, ver] = target.builtinId.split(":");
|
|
9455
11621
|
const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, "-");
|
|
9456
11622
|
const forkName = `${safe(name2 ?? "schema")}-${safe(ver ?? "0")}.local.json`;
|
|
9457
|
-
schemaAbs =
|
|
11623
|
+
schemaAbs = resolve14(ctx.configDir ?? ctx.cwd, "schemas", forkName);
|
|
9458
11624
|
assertSchemaWriteWithin(ctx, schemaAbs);
|
|
9459
|
-
if (
|
|
11625
|
+
if (existsSync5(schemaAbs)) {
|
|
9460
11626
|
throw new DocmetaError(
|
|
9461
11627
|
`"${displayPath(ctx, schemaAbs)}" already exists; refusing to overwrite it with a fork of ${target.builtinId}.`
|
|
9462
11628
|
);
|
|
9463
11629
|
}
|
|
9464
11630
|
await assertNotIgnored(
|
|
9465
11631
|
schemaAbs,
|
|
9466
|
-
|
|
11632
|
+
dirname9(schemaAbs),
|
|
9467
11633
|
ctx.cwd,
|
|
9468
11634
|
ctx.onNotice,
|
|
9469
11635
|
FORK_IGNORE_TEXT
|
|
@@ -9610,8 +11776,8 @@ async function planConfigEdit(ctx, edit, runRefs) {
|
|
|
9610
11776
|
if (configPath === void 0 || configDir === void 0) {
|
|
9611
11777
|
return { changes: [], repointed: false };
|
|
9612
11778
|
}
|
|
9613
|
-
const
|
|
9614
|
-
const doc = parseDocument5(stripBom(
|
|
11779
|
+
const text2 = await readFile10(configPath, "utf8");
|
|
11780
|
+
const doc = parseDocument5(stripBom(text2));
|
|
9615
11781
|
const display = displayPath(ctx, configPath);
|
|
9616
11782
|
const rawRefOf = (item) => {
|
|
9617
11783
|
if (isScalar3(item) && typeof item.value === "string") return item.value;
|
|
@@ -9621,7 +11787,7 @@ async function planConfigEdit(ctx, edit, runRefs) {
|
|
|
9621
11787
|
}
|
|
9622
11788
|
return void 0;
|
|
9623
11789
|
};
|
|
9624
|
-
const rebaseRaw = (raw) => classifyRef(raw).kind === "file" && !isAbsolute5(raw) ?
|
|
11790
|
+
const rebaseRaw = (raw) => classifyRef(raw).kind === "file" && !isAbsolute5(raw) ? resolve14(configDir, raw) : raw;
|
|
9625
11791
|
const wantSet = runRefs.map(rebaseRaw).sort().join("\n");
|
|
9626
11792
|
const seqResolvesToRunSet = (node) => {
|
|
9627
11793
|
if (!isSeq2(node)) return false;
|
|
@@ -9666,7 +11832,7 @@ async function planConfigEdit(ctx, edit, runRefs) {
|
|
|
9666
11832
|
if (!isMap4(item)) continue;
|
|
9667
11833
|
const raw = rawRefOf(item);
|
|
9668
11834
|
if (raw === void 0 || classifyRef(raw).kind !== "file") continue;
|
|
9669
|
-
const abs = isAbsolute5(raw) ? raw :
|
|
11835
|
+
const abs = isAbsolute5(raw) ? raw : resolve14(configDir, raw);
|
|
9670
11836
|
if (foldPath(abs) !== wantPath) continue;
|
|
9671
11837
|
pinFrom = item.get("integrity");
|
|
9672
11838
|
item.set("integrity", edit.integrity.value);
|
|
@@ -9698,8 +11864,8 @@ async function planConfigEdit(ctx, edit, runRefs) {
|
|
|
9698
11864
|
return {
|
|
9699
11865
|
write: {
|
|
9700
11866
|
path: configPath,
|
|
9701
|
-
content: matchEol(
|
|
9702
|
-
expected:
|
|
11867
|
+
content: matchEol(text2, doc.toString({ lineWidth: 0 })),
|
|
11868
|
+
expected: text2
|
|
9703
11869
|
},
|
|
9704
11870
|
changes,
|
|
9705
11871
|
repointed
|
|
@@ -9882,12 +12048,12 @@ function buildChanges(diff, entries, sentinel, ctx, renameHints = [], booleanAdd
|
|
|
9882
12048
|
}
|
|
9883
12049
|
}
|
|
9884
12050
|
validateNewPath(file, ctx.base);
|
|
9885
|
-
if (!extractorForExtension(
|
|
12051
|
+
if (!extractorForExtension(extname6(file))?.apply) {
|
|
9886
12052
|
throw new DocmetaError(
|
|
9887
12053
|
`"${file}": no writable format for that extension.`
|
|
9888
12054
|
);
|
|
9889
12055
|
}
|
|
9890
|
-
if (
|
|
12056
|
+
if (existsSync5(resolve14(ctx.base, file))) {
|
|
9891
12057
|
throw new DocmetaError(
|
|
9892
12058
|
`"${file}" already exists; INSERT creates new files only.`
|
|
9893
12059
|
);
|
|
@@ -9901,12 +12067,12 @@ function buildChanges(diff, entries, sentinel, ctx, renameHints = [], booleanAdd
|
|
|
9901
12067
|
}
|
|
9902
12068
|
for (const { from, to } of diff.renamedFiles) {
|
|
9903
12069
|
validateNewPath(to, ctx.base);
|
|
9904
|
-
if (
|
|
12070
|
+
if (extname6(to).toLowerCase() !== extname6(from).toLowerCase()) {
|
|
9905
12071
|
throw new DocmetaError(
|
|
9906
12072
|
`"${from}" -> "${to}": a rename may not change the extension.`
|
|
9907
12073
|
);
|
|
9908
12074
|
}
|
|
9909
|
-
if (
|
|
12075
|
+
if (existsSync5(resolve14(ctx.base, to))) {
|
|
9910
12076
|
throw new DocmetaError(
|
|
9911
12077
|
`"${to}" already exists; the rename would overwrite it.`
|
|
9912
12078
|
);
|
|
@@ -9914,8 +12080,23 @@ function buildChanges(diff, entries, sentinel, ctx, renameHints = [], booleanAdd
|
|
|
9914
12080
|
changes.push({ file: from, renamed: to, written: false });
|
|
9915
12081
|
}
|
|
9916
12082
|
refuseExternalMetadataWrites(changes, entries, ctx);
|
|
12083
|
+
refuseManagedWrites(changes, entries, ctx);
|
|
9917
12084
|
return changes;
|
|
9918
12085
|
}
|
|
12086
|
+
function refuseManagedWrites(changes, entries, ctx) {
|
|
12087
|
+
if (ctx.managed.size === 0) return;
|
|
12088
|
+
const refuse = (key) => {
|
|
12089
|
+
throw new DocmetaError(
|
|
12090
|
+
`"${key}" is managed by derive; run manni meta derive instead.`
|
|
12091
|
+
);
|
|
12092
|
+
};
|
|
12093
|
+
const data = new Map(entries.map((e) => [e.label, e.extracted.data]));
|
|
12094
|
+
for (const c of changes) {
|
|
12095
|
+
if ("schema" in c || "config" in c || "renamed" in c) continue;
|
|
12096
|
+
const keys = "cleared" in c ? Object.keys(data.get(c.file) ?? {}) : "created" in c ? Object.keys(c.to) : "renamedFrom" in c ? [c.key, c.renamedFrom] : [c.key];
|
|
12097
|
+
for (const key of keys) if (ctx.managed.has(key)) refuse(key);
|
|
12098
|
+
}
|
|
12099
|
+
}
|
|
9919
12100
|
function validateNewPath(p, base) {
|
|
9920
12101
|
const refuse = (why) => {
|
|
9921
12102
|
throw new DocmetaError(`"${p}" is not a usable path: ${why}.`);
|
|
@@ -9924,9 +12105,9 @@ function validateNewPath(p, base) {
|
|
|
9924
12105
|
if (p.split(/[\\/]/).some((segment) => segment === "..")) {
|
|
9925
12106
|
refuse("it traverses upward");
|
|
9926
12107
|
}
|
|
9927
|
-
const abs =
|
|
9928
|
-
if (abs ===
|
|
9929
|
-
if (!abs.startsWith(
|
|
12108
|
+
const abs = resolve14(base, p);
|
|
12109
|
+
if (abs === resolve14(base)) refuse("it points at the corpus root, not a file");
|
|
12110
|
+
if (!abs.startsWith(resolve14(base) + sep7)) refuse("it escapes the corpus");
|
|
9930
12111
|
}
|
|
9931
12112
|
function refuseExternalMetadataWrites(changes, entries, ctx) {
|
|
9932
12113
|
const index = ctx.externalMetadata;
|
|
@@ -9955,7 +12136,7 @@ function refuseExternalMetadataWrites(changes, entries, ctx) {
|
|
|
9955
12136
|
continue;
|
|
9956
12137
|
}
|
|
9957
12138
|
if ("renamed" in c) {
|
|
9958
|
-
const entry = index.byPath.get(
|
|
12139
|
+
const entry = index.byPath.get(resolve14(ctx.base, c.file));
|
|
9959
12140
|
if (entry !== void 0 && entry.size > 0) {
|
|
9960
12141
|
const manifest = [...entry.values()][0]?.file ?? "manifest";
|
|
9961
12142
|
throw new DocmetaError(
|
|
@@ -10008,15 +12189,15 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
|
|
|
10008
12189
|
const pendingWrites = [];
|
|
10009
12190
|
const pendingRenames = [];
|
|
10010
12191
|
for (const [label, ops] of grouped) {
|
|
10011
|
-
const path =
|
|
12192
|
+
const path = resolve14(ctx.base, label);
|
|
10012
12193
|
if (ops.renamedTo !== void 0) {
|
|
10013
|
-
if (!
|
|
12194
|
+
if (!existsSync5(path)) {
|
|
10014
12195
|
throw new DocmetaError(
|
|
10015
12196
|
`"${label}" is gone from disk since it was read; re-run the query.`
|
|
10016
12197
|
);
|
|
10017
12198
|
}
|
|
10018
|
-
const toDisk =
|
|
10019
|
-
if (
|
|
12199
|
+
const toDisk = resolve14(ctx.base, ops.renamedTo);
|
|
12200
|
+
if (existsSync5(toDisk)) {
|
|
10020
12201
|
throw new DocmetaError(
|
|
10021
12202
|
`"${ops.renamedTo}" appeared on disk since the plan; re-run the query.`
|
|
10022
12203
|
);
|
|
@@ -10025,13 +12206,13 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
|
|
|
10025
12206
|
continue;
|
|
10026
12207
|
}
|
|
10027
12208
|
if (ops.created !== void 0) {
|
|
10028
|
-
const extractor = extractorForExtension(
|
|
12209
|
+
const extractor = extractorForExtension(extname6(label));
|
|
10029
12210
|
if (!extractor?.apply) {
|
|
10030
12211
|
throw new DocmetaError(
|
|
10031
12212
|
`"${label}": no writable format for that extension.`
|
|
10032
12213
|
);
|
|
10033
12214
|
}
|
|
10034
|
-
if (
|
|
12215
|
+
if (existsSync5(path)) {
|
|
10035
12216
|
throw new DocmetaError(
|
|
10036
12217
|
`"${label}" appeared on disk since the plan; re-run the query.`
|
|
10037
12218
|
);
|
|
@@ -10040,7 +12221,9 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
|
|
|
10040
12221
|
path,
|
|
10041
12222
|
content: extractor.apply("", ops.created, {
|
|
10042
12223
|
filePath: label,
|
|
10043
|
-
|
|
12224
|
+
// The new file's memberships, from its path alone. An INSERT has to
|
|
12225
|
+
// write through the element set a read of that file would use.
|
|
12226
|
+
elements: resolveElements(label, ctx.config, ctx.memberships(label))
|
|
10044
12227
|
}),
|
|
10045
12228
|
ensureDir: true
|
|
10046
12229
|
});
|
|
@@ -10048,7 +12231,7 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
|
|
|
10048
12231
|
}
|
|
10049
12232
|
const entry = byLabel.get(label);
|
|
10050
12233
|
if (!entry) throw new DocmetaError(`No loaded entry for "${label}".`);
|
|
10051
|
-
const content = await
|
|
12234
|
+
const content = await readFile10(path, "utf8");
|
|
10052
12235
|
const members = ctx.memberships(label);
|
|
10053
12236
|
const current = mergeExternalMetadata(
|
|
10054
12237
|
label,
|
|
@@ -10088,7 +12271,9 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
|
|
|
10088
12271
|
}
|
|
10089
12272
|
const applied = entry.extractor.apply(content, ops.patch, {
|
|
10090
12273
|
filePath: label,
|
|
10091
|
-
|
|
12274
|
+
// The memberships the read above used. Without them a collection-scoped
|
|
12275
|
+
// element the read found has nowhere to be written.
|
|
12276
|
+
elements: resolveElements(label, ctx.config, members),
|
|
10092
12277
|
deletions: ops.deletions
|
|
10093
12278
|
});
|
|
10094
12279
|
if (ops.deletions.length > 0) {
|
|
@@ -10114,7 +12299,7 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
|
|
|
10114
12299
|
for (const w of schemaWrites) {
|
|
10115
12300
|
const display = displayPath(ctx, w.path);
|
|
10116
12301
|
if (w.expected === void 0) {
|
|
10117
|
-
if (
|
|
12302
|
+
if (existsSync5(w.path)) {
|
|
10118
12303
|
throw new DocmetaError(
|
|
10119
12304
|
`"${display}" appeared on disk since the plan; re-run the query.`
|
|
10120
12305
|
);
|
|
@@ -10123,7 +12308,7 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
|
|
|
10123
12308
|
}
|
|
10124
12309
|
let now;
|
|
10125
12310
|
try {
|
|
10126
|
-
now = await
|
|
12311
|
+
now = await readFile10(w.path, "utf8");
|
|
10127
12312
|
} catch {
|
|
10128
12313
|
now = void 0;
|
|
10129
12314
|
}
|
|
@@ -10134,10 +12319,10 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
|
|
|
10134
12319
|
}
|
|
10135
12320
|
}
|
|
10136
12321
|
const dirs = /* @__PURE__ */ new Set();
|
|
10137
|
-
for (const w of schemaWrites) dirs.add(
|
|
10138
|
-
for (const r of pendingRenames) dirs.add(
|
|
12322
|
+
for (const w of schemaWrites) dirs.add(dirname9(w.path));
|
|
12323
|
+
for (const r of pendingRenames) dirs.add(dirname9(r.to));
|
|
10139
12324
|
for (const p of pendingWrites) {
|
|
10140
|
-
if (p.ensureDir) dirs.add(
|
|
12325
|
+
if (p.ensureDir) dirs.add(dirname9(p.path));
|
|
10141
12326
|
}
|
|
10142
12327
|
for (const d of dirs) await mkdir3(d, { recursive: true });
|
|
10143
12328
|
const writeSchema = async () => {
|
|
@@ -10151,7 +12336,7 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
|
|
|
10151
12336
|
await rename2(r.from, r.to);
|
|
10152
12337
|
} catch (err) {
|
|
10153
12338
|
throw new DocmetaError(
|
|
10154
|
-
`Cannot move "${r.from}" to "${r.to}": ${err
|
|
12339
|
+
`Cannot move "${r.from}" to "${r.to}": ${errorMessage(err)}`
|
|
10155
12340
|
);
|
|
10156
12341
|
}
|
|
10157
12342
|
}
|
|
@@ -10252,8 +12437,8 @@ function skipExpression(sql, from) {
|
|
|
10252
12437
|
}
|
|
10253
12438
|
|
|
10254
12439
|
// src/meta/commands/fill.ts
|
|
10255
|
-
import { readFile as
|
|
10256
|
-
import { resolve as
|
|
12440
|
+
import { readFile as readFile11 } from "node:fs/promises";
|
|
12441
|
+
import { resolve as resolve15, extname as extname7, join as join7 } from "node:path";
|
|
10257
12442
|
import {
|
|
10258
12443
|
completeValidatedJSON,
|
|
10259
12444
|
makeProvider,
|
|
@@ -10455,6 +12640,7 @@ async function runFill(opts) {
|
|
|
10455
12640
|
);
|
|
10456
12641
|
const dryRun = Boolean(opts.dryRun);
|
|
10457
12642
|
const only = opts.fields != null ? new Set(opts.fields) : void 0;
|
|
12643
|
+
const managed = new Set(config?.derive?.fields ?? []);
|
|
10458
12644
|
const requestedProvider = opts.provider ?? config?.fill?.provider ?? DEFAULT_PROVIDER;
|
|
10459
12645
|
const providerName = opts.local === true && requestedProvider === "auto" ? "llama-cpp" : requestedProvider;
|
|
10460
12646
|
const model = opts.model ?? config?.fill?.model;
|
|
@@ -10514,13 +12700,13 @@ async function runFill(opts) {
|
|
|
10514
12700
|
provider = construct();
|
|
10515
12701
|
} catch (err) {
|
|
10516
12702
|
throw new DocmetaError(
|
|
10517
|
-
err instanceof InferenceError ? err.message : `Could not construct the "${providerName}" provider: ${err
|
|
12703
|
+
err instanceof InferenceError ? err.message : `Could not construct the "${providerName}" provider: ${errorMessage(err)}`
|
|
10518
12704
|
);
|
|
10519
12705
|
}
|
|
10520
12706
|
}
|
|
10521
12707
|
return provider;
|
|
10522
12708
|
};
|
|
10523
|
-
const cache = opts.cache === false ? void 0 : new JsonCache(
|
|
12709
|
+
const cache = opts.cache === false ? void 0 : new JsonCache(join7(cwd, CACHE_DIR), true, "manni");
|
|
10524
12710
|
const pricing = pricingFor(identity.model);
|
|
10525
12711
|
const validator = new Validator(schemaOptions);
|
|
10526
12712
|
let costUsd = 0;
|
|
@@ -10564,7 +12750,7 @@ async function runFill(opts) {
|
|
|
10564
12750
|
);
|
|
10565
12751
|
extracted = merged.extracted;
|
|
10566
12752
|
} catch (err) {
|
|
10567
|
-
return errorResult(label, extractor.name, err
|
|
12753
|
+
return errorResult(label, extractor.name, errorMessage(err));
|
|
10568
12754
|
}
|
|
10569
12755
|
let resolved;
|
|
10570
12756
|
try {
|
|
@@ -10582,7 +12768,7 @@ async function runFill(opts) {
|
|
|
10582
12768
|
onNotice: opts.onNotice
|
|
10583
12769
|
});
|
|
10584
12770
|
} catch (err) {
|
|
10585
|
-
return errorResult(label, extractor.name, err
|
|
12771
|
+
return errorResult(label, extractor.name, errorMessage(err));
|
|
10586
12772
|
}
|
|
10587
12773
|
const schemaSet = resolved.schemas;
|
|
10588
12774
|
let schemas;
|
|
@@ -10604,12 +12790,21 @@ async function runFill(opts) {
|
|
|
10604
12790
|
}
|
|
10605
12791
|
return errorResult(label, extractor.name, err.message);
|
|
10606
12792
|
}
|
|
10607
|
-
const
|
|
12793
|
+
const proposed = collectCandidates(
|
|
10608
12794
|
schemas,
|
|
10609
12795
|
extracted.data,
|
|
10610
12796
|
existingErrors,
|
|
10611
12797
|
only
|
|
10612
12798
|
);
|
|
12799
|
+
const managedSkips = proposed.filter((c) => managed.has(c.key)).map((c) => ({
|
|
12800
|
+
field: `/${c.key}`,
|
|
12801
|
+
required: c.required,
|
|
12802
|
+
confidence: 0,
|
|
12803
|
+
reasoning: "managed by derive",
|
|
12804
|
+
written: false,
|
|
12805
|
+
skipReason: "managed"
|
|
12806
|
+
}));
|
|
12807
|
+
const candidates = proposed.filter((c) => !managed.has(c.key));
|
|
10613
12808
|
const ownedCandidates = externalMetadata ? candidates.flatMap((c) => {
|
|
10614
12809
|
const owner = externalMetadata.owners.get(c.key)?.[0];
|
|
10615
12810
|
return owner === void 0 ? [] : [{ key: c.key, file: owner.file }];
|
|
@@ -10628,7 +12823,7 @@ async function runFill(opts) {
|
|
|
10628
12823
|
file: label,
|
|
10629
12824
|
format: extractor.name,
|
|
10630
12825
|
schemas: schemaSet,
|
|
10631
|
-
fields:
|
|
12826
|
+
fields: managedSkips,
|
|
10632
12827
|
changed: false,
|
|
10633
12828
|
...opts.includeContent ? { content } : {}
|
|
10634
12829
|
};
|
|
@@ -10636,7 +12831,7 @@ async function runFill(opts) {
|
|
|
10636
12831
|
try {
|
|
10637
12832
|
extractor.apply(content, {}, { filePath: label, elements });
|
|
10638
12833
|
} catch (err) {
|
|
10639
|
-
return errorResult(label, extractor.name, err
|
|
12834
|
+
return errorResult(label, extractor.name, errorMessage(err), schemaSet);
|
|
10640
12835
|
}
|
|
10641
12836
|
const cacheKey = buildCacheKey([
|
|
10642
12837
|
identity.provider,
|
|
@@ -10674,7 +12869,7 @@ async function runFill(opts) {
|
|
|
10674
12869
|
return errorResult(
|
|
10675
12870
|
label,
|
|
10676
12871
|
extractor.name,
|
|
10677
|
-
`Could not build a proposal schema from ${schemaSet.join(", ")}: ${err
|
|
12872
|
+
`Could not build a proposal schema from ${schemaSet.join(", ")}: ${errorMessage(err)}`,
|
|
10678
12873
|
schemaSet
|
|
10679
12874
|
);
|
|
10680
12875
|
}
|
|
@@ -10697,9 +12892,9 @@ async function runFill(opts) {
|
|
|
10697
12892
|
break;
|
|
10698
12893
|
}
|
|
10699
12894
|
inFlight++;
|
|
10700
|
-
let
|
|
12895
|
+
let run2;
|
|
10701
12896
|
try {
|
|
10702
|
-
|
|
12897
|
+
run2 = await completeValidatedJSON({
|
|
10703
12898
|
provider: getProvider(),
|
|
10704
12899
|
system: FILL_SYSTEM_PROMPT,
|
|
10705
12900
|
user: buildUserPrompt({
|
|
@@ -10712,22 +12907,22 @@ async function runFill(opts) {
|
|
|
10712
12907
|
schema: envelope,
|
|
10713
12908
|
validate
|
|
10714
12909
|
});
|
|
10715
|
-
costUsd += costOfUsage(
|
|
12910
|
+
costUsd += costOfUsage(run2.usage, pricing);
|
|
10716
12911
|
turnsUsed++;
|
|
10717
12912
|
} finally {
|
|
10718
12913
|
inFlight--;
|
|
10719
12914
|
}
|
|
10720
|
-
if (
|
|
12915
|
+
if (run2.error != null && looksLikeOverflow(run2.error)) {
|
|
10721
12916
|
overflowed = true;
|
|
10722
|
-
failure =
|
|
12917
|
+
failure = run2.error;
|
|
10723
12918
|
break;
|
|
10724
12919
|
}
|
|
10725
|
-
if (
|
|
10726
|
-
failure =
|
|
12920
|
+
if (run2.error != null || run2.result == null) {
|
|
12921
|
+
failure = run2.error ?? "The model returned no proposal.";
|
|
10727
12922
|
break;
|
|
10728
12923
|
}
|
|
10729
|
-
usageTotal = addUsage(usageTotal,
|
|
10730
|
-
collected.push(
|
|
12924
|
+
usageTotal = addUsage(usageTotal, run2.usage);
|
|
12925
|
+
collected.push(run2.result);
|
|
10731
12926
|
}
|
|
10732
12927
|
if (overflowed && attempt < 2) {
|
|
10733
12928
|
usageTotal = void 0;
|
|
@@ -10760,7 +12955,7 @@ async function runFill(opts) {
|
|
|
10760
12955
|
proposals = mergeProposals(sets);
|
|
10761
12956
|
cache?.set(cacheKey, { proposals, ...usageTotal ? { usage: usageTotal } : {} });
|
|
10762
12957
|
}
|
|
10763
|
-
const fields = gate(candidates, proposals, threshold);
|
|
12958
|
+
const fields = [...managedSkips, ...gate(candidates, proposals, threshold)];
|
|
10764
12959
|
const accepted = fields.filter((f) => f.written);
|
|
10765
12960
|
if (accepted.length > 0) {
|
|
10766
12961
|
const merged2 = { ...extracted.data, ...patchOf(accepted) };
|
|
@@ -10802,14 +12997,14 @@ async function runFill(opts) {
|
|
|
10802
12997
|
return errorResult(
|
|
10803
12998
|
label,
|
|
10804
12999
|
extractor.name,
|
|
10805
|
-
err
|
|
13000
|
+
errorMessage(err),
|
|
10806
13001
|
schemaSet,
|
|
10807
13002
|
fields
|
|
10808
13003
|
);
|
|
10809
13004
|
}
|
|
10810
13005
|
const changed = next !== content;
|
|
10811
13006
|
if (changed && !dryRun && label !== "<stdin>") {
|
|
10812
|
-
await writeFileAtomic(
|
|
13007
|
+
await writeFileAtomic(resolve15(base, label), next);
|
|
10813
13008
|
}
|
|
10814
13009
|
return {
|
|
10815
13010
|
file: label,
|
|
@@ -10831,8 +13026,8 @@ async function runFill(opts) {
|
|
|
10831
13026
|
);
|
|
10832
13027
|
}
|
|
10833
13028
|
const fileResults = await mapConcurrent(files, concurrency, async (file) => {
|
|
10834
|
-
const content = await
|
|
10835
|
-
return processOne(file, content,
|
|
13029
|
+
const content = await readFile11(resolve15(base, file), "utf8");
|
|
13030
|
+
return processOne(file, content, extname7(file));
|
|
10836
13031
|
});
|
|
10837
13032
|
results.push(...fileResults);
|
|
10838
13033
|
return {
|
|
@@ -11015,8 +13210,8 @@ function plannedFor(schemas) {
|
|
|
11015
13210
|
}
|
|
11016
13211
|
function withRenamedRefs(node, renames) {
|
|
11017
13212
|
if (Array.isArray(node)) {
|
|
11018
|
-
const
|
|
11019
|
-
return
|
|
13213
|
+
const items2 = node;
|
|
13214
|
+
return items2.map((item) => withRenamedRefs(item, renames));
|
|
11020
13215
|
}
|
|
11021
13216
|
if (typeof node !== "object" || node === null) return node;
|
|
11022
13217
|
const out = {};
|
|
@@ -11102,7 +13297,7 @@ async function resolveIdentity(spec) {
|
|
|
11102
13297
|
return await resolveProviderIdentityAsync(spec);
|
|
11103
13298
|
} catch (err) {
|
|
11104
13299
|
throw new DocmetaError(
|
|
11105
|
-
err instanceof InferenceError ? err.message : `Could not resolve provider "${spec.provider}": ${err
|
|
13300
|
+
err instanceof InferenceError ? err.message : `Could not resolve provider "${spec.provider}": ${errorMessage(err)}`
|
|
11106
13301
|
);
|
|
11107
13302
|
}
|
|
11108
13303
|
}
|
|
@@ -11130,16 +13325,16 @@ function summarize(results, costUsd, cached) {
|
|
|
11130
13325
|
cached
|
|
11131
13326
|
};
|
|
11132
13327
|
}
|
|
11133
|
-
async function mapConcurrent(
|
|
11134
|
-
const out = new Array(
|
|
13328
|
+
async function mapConcurrent(items2, limit, fn) {
|
|
13329
|
+
const out = new Array(items2.length);
|
|
11135
13330
|
let next = 0;
|
|
11136
13331
|
const workers = Array.from(
|
|
11137
|
-
{ length: Math.max(1, Math.min(limit,
|
|
13332
|
+
{ length: Math.max(1, Math.min(limit, items2.length)) },
|
|
11138
13333
|
async () => {
|
|
11139
13334
|
for (; ; ) {
|
|
11140
13335
|
const i = next++;
|
|
11141
|
-
const item =
|
|
11142
|
-
if (i >=
|
|
13336
|
+
const item = items2[i];
|
|
13337
|
+
if (i >= items2.length || item === void 0) return;
|
|
11143
13338
|
out[i] = await fn(item);
|
|
11144
13339
|
}
|
|
11145
13340
|
}
|
|
@@ -11148,6 +13343,252 @@ async function mapConcurrent(items, limit, fn) {
|
|
|
11148
13343
|
return out;
|
|
11149
13344
|
}
|
|
11150
13345
|
|
|
13346
|
+
// src/meta/commands/derive.ts
|
|
13347
|
+
import { readFile as readFile12 } from "node:fs/promises";
|
|
13348
|
+
import { extname as extname8, resolve as resolve16 } from "node:path";
|
|
13349
|
+
var SOURCE_HINT = "narrow --sources or --fields";
|
|
13350
|
+
async function runDerive(opts) {
|
|
13351
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
13352
|
+
const { config, inputs, base, configDir, collections, declaredCollections, fromCollections } = await resolveRunConfig({
|
|
13353
|
+
cwd,
|
|
13354
|
+
configPath: opts.configPath,
|
|
13355
|
+
noConfig: opts.noConfig,
|
|
13356
|
+
inputs: opts.inputs,
|
|
13357
|
+
...opts.collections !== void 0 ? { collections: opts.collections } : {},
|
|
13358
|
+
onConfigLoaded: opts.onConfigLoaded
|
|
13359
|
+
});
|
|
13360
|
+
if (inputs.includes(STDIN_TOKEN)) {
|
|
13361
|
+
throw new DocmetaError("cannot derive <stdin>: no history behind it");
|
|
13362
|
+
}
|
|
13363
|
+
if (inputs.length === 0) {
|
|
13364
|
+
throw new DocmetaError(
|
|
13365
|
+
"No files to derive. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml."
|
|
13366
|
+
);
|
|
13367
|
+
}
|
|
13368
|
+
const commands = commandsOf(config?.derive);
|
|
13369
|
+
const fields = resolveFields(opts.fields, config?.derive?.fields, commands);
|
|
13370
|
+
for (const field of fields) {
|
|
13371
|
+
const owner = manifestOwning(field, declaredCollections);
|
|
13372
|
+
if (owner !== void 0) {
|
|
13373
|
+
throw new DocmetaError(
|
|
13374
|
+
`"${field}" is owned by the manifest ${owner.file} on collection ${owner.name}; a managed field has one authority, and a manifest key already has one.`
|
|
13375
|
+
);
|
|
13376
|
+
}
|
|
13377
|
+
}
|
|
13378
|
+
const sources = resolveSources(opts.sources, config?.derive?.sources);
|
|
13379
|
+
const check = Boolean(opts.check);
|
|
13380
|
+
const dryRun = Boolean(opts.dryRun) || check;
|
|
13381
|
+
const forcedExtractor = opts.as ? extractorByName(opts.as) : void 0;
|
|
13382
|
+
if (opts.as && !forcedExtractor) {
|
|
13383
|
+
throw new DocmetaError(
|
|
13384
|
+
`Unknown format "${opts.as}". Known formats: ${listFormats().map((f) => f.name).join(", ")}.`
|
|
13385
|
+
);
|
|
13386
|
+
}
|
|
13387
|
+
const allowEmpty = opts.allowEmpty ?? config?.allowEmpty;
|
|
13388
|
+
const exts = opts.exts ?? forcedExtractor?.extensions;
|
|
13389
|
+
const exclude = opts.exclude ?? [];
|
|
13390
|
+
const membersFor = (label) => memberOf(collections, configDir ?? cwd, base, label);
|
|
13391
|
+
const { files: walked, gitignoreSkipped } = await resolveTargetSet({
|
|
13392
|
+
inputs,
|
|
13393
|
+
exts,
|
|
13394
|
+
exclude,
|
|
13395
|
+
cwd: base,
|
|
13396
|
+
allowEmpty,
|
|
13397
|
+
...gitignoreOptions({
|
|
13398
|
+
flag: opts.respectGitignore,
|
|
13399
|
+
configured: config?.respectGitignore,
|
|
13400
|
+
onNotice: opts.onNotice
|
|
13401
|
+
})
|
|
13402
|
+
});
|
|
13403
|
+
const files = fromCollections ? retainMembers(walked, membersFor) : walked;
|
|
13404
|
+
assertNonEmpty({
|
|
13405
|
+
files,
|
|
13406
|
+
inputs,
|
|
13407
|
+
usingStdin: false,
|
|
13408
|
+
allowEmpty,
|
|
13409
|
+
exclude,
|
|
13410
|
+
exts,
|
|
13411
|
+
gitignoreSkipped,
|
|
13412
|
+
action: "derived"
|
|
13413
|
+
});
|
|
13414
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
13415
|
+
const errors = /* @__PURE__ */ new Map();
|
|
13416
|
+
for (const label of files) {
|
|
13417
|
+
const absPath = resolve16(base, label);
|
|
13418
|
+
const extractor = forcedExtractor ?? extractorForExtension(extname8(label));
|
|
13419
|
+
if (!extractor) {
|
|
13420
|
+
errors.set(label, {
|
|
13421
|
+
format: "unknown",
|
|
13422
|
+
message: `Unsupported file type "${extname8(label)}". Supported: ${supportedExtensions().join(", ")}. Use --as to override.`
|
|
13423
|
+
});
|
|
13424
|
+
continue;
|
|
13425
|
+
}
|
|
13426
|
+
const content = await readFile12(absPath, "utf8");
|
|
13427
|
+
const elements = resolveElements(label, config, membersFor(label));
|
|
13428
|
+
try {
|
|
13429
|
+
loaded.set(label, {
|
|
13430
|
+
label,
|
|
13431
|
+
absPath,
|
|
13432
|
+
content,
|
|
13433
|
+
format: extractor.name,
|
|
13434
|
+
elements,
|
|
13435
|
+
extracted: extractor.extract(content, label, { elements }),
|
|
13436
|
+
apply: extractor.apply
|
|
13437
|
+
});
|
|
13438
|
+
} catch (err) {
|
|
13439
|
+
errors.set(label, { format: extractor.name, message: errorMessage(err) });
|
|
13440
|
+
}
|
|
13441
|
+
}
|
|
13442
|
+
const derived = await deriveMetadata([...loaded.values()], {
|
|
13443
|
+
cwd,
|
|
13444
|
+
base,
|
|
13445
|
+
configDir,
|
|
13446
|
+
sources,
|
|
13447
|
+
fields,
|
|
13448
|
+
codeowners: config?.derive?.codeowners,
|
|
13449
|
+
commands,
|
|
13450
|
+
cache: opts.cache ?? true,
|
|
13451
|
+
now: opts.now ?? (() => /* @__PURE__ */ new Date()),
|
|
13452
|
+
reviews: opts.reviews
|
|
13453
|
+
});
|
|
13454
|
+
assertSourcesAvailable(derived.sources, SOURCE_HINT);
|
|
13455
|
+
for (const [name2, status] of Object.entries(derived.sources)) {
|
|
13456
|
+
if (status.available && status.reason !== void 0) {
|
|
13457
|
+
opts.onNotice?.(`${name2}: ${status.reason}`);
|
|
13458
|
+
}
|
|
13459
|
+
}
|
|
13460
|
+
const results = [];
|
|
13461
|
+
for (const label of files) {
|
|
13462
|
+
const failure = errors.get(label);
|
|
13463
|
+
if (failure) {
|
|
13464
|
+
results.push(errorResult2(label, failure.format, failure.message, check));
|
|
13465
|
+
continue;
|
|
13466
|
+
}
|
|
13467
|
+
const doc = loaded.get(label);
|
|
13468
|
+
if (!doc) continue;
|
|
13469
|
+
const record = derived.records.get(label);
|
|
13470
|
+
const compared = fields.map(
|
|
13471
|
+
(field) => compareDerived(field, doc.extracted.data[field], record?.fields[field])
|
|
13472
|
+
);
|
|
13473
|
+
const pending = compared.filter((f) => f.status === "stale" || f.status === "unset");
|
|
13474
|
+
let changed = false;
|
|
13475
|
+
if (pending.length > 0) {
|
|
13476
|
+
if (typeof doc.apply !== "function") {
|
|
13477
|
+
if (!dryRun) {
|
|
13478
|
+
results.push(
|
|
13479
|
+
errorResult2(
|
|
13480
|
+
label,
|
|
13481
|
+
doc.format,
|
|
13482
|
+
`The "${doc.format}" format is read-only; manni meta derive cannot write metadata back to it.`,
|
|
13483
|
+
check,
|
|
13484
|
+
compared
|
|
13485
|
+
)
|
|
13486
|
+
);
|
|
13487
|
+
continue;
|
|
13488
|
+
}
|
|
13489
|
+
changed = true;
|
|
13490
|
+
} else {
|
|
13491
|
+
const patch = {};
|
|
13492
|
+
for (const f of pending) patch[f.field] = f.derived;
|
|
13493
|
+
let next;
|
|
13494
|
+
try {
|
|
13495
|
+
next = doc.apply(doc.content, patch, { filePath: label, elements: doc.elements });
|
|
13496
|
+
} catch (err) {
|
|
13497
|
+
results.push(errorResult2(label, doc.format, errorMessage(err), check, compared));
|
|
13498
|
+
continue;
|
|
13499
|
+
}
|
|
13500
|
+
changed = next !== doc.content;
|
|
13501
|
+
if (changed && !dryRun) {
|
|
13502
|
+
await writeFileAtomic(doc.absPath, next);
|
|
13503
|
+
for (const f of pending) f.written = true;
|
|
13504
|
+
}
|
|
13505
|
+
}
|
|
13506
|
+
}
|
|
13507
|
+
results.push({
|
|
13508
|
+
file: label,
|
|
13509
|
+
format: doc.format,
|
|
13510
|
+
fields: compared,
|
|
13511
|
+
changed,
|
|
13512
|
+
...check ? { findings: staleFindings(compared, doc.extracted.lineFor) } : {}
|
|
13513
|
+
});
|
|
13514
|
+
}
|
|
13515
|
+
return {
|
|
13516
|
+
results,
|
|
13517
|
+
summary: summarize2(results),
|
|
13518
|
+
dryRun,
|
|
13519
|
+
check,
|
|
13520
|
+
sources: derived.sources,
|
|
13521
|
+
frame: { cwd, base: configDir ?? cwd, runBase: base }
|
|
13522
|
+
};
|
|
13523
|
+
}
|
|
13524
|
+
function resolveFields(flag, configured, commands) {
|
|
13525
|
+
const names = flag ?? configured;
|
|
13526
|
+
if (names === void 0 || names.length === 0) {
|
|
13527
|
+
throw new DocmetaError(
|
|
13528
|
+
"nothing to derive: set derive.fields in manni.config.yaml or pass --fields"
|
|
13529
|
+
);
|
|
13530
|
+
}
|
|
13531
|
+
const out = [];
|
|
13532
|
+
for (const name2 of names) {
|
|
13533
|
+
if (!isBuiltinField(name2) && !(commands !== void 0 && Object.hasOwn(commands, name2))) {
|
|
13534
|
+
throw new DocmetaError(
|
|
13535
|
+
`"${name2}" is not derivable; derivable fields are ${DERIVABLE_FIELDS.join(", ")}, or any key with an entry in derive.commands`
|
|
13536
|
+
);
|
|
13537
|
+
}
|
|
13538
|
+
if (!out.includes(name2)) out.push(name2);
|
|
13539
|
+
}
|
|
13540
|
+
return out;
|
|
13541
|
+
}
|
|
13542
|
+
function resolveSources(flag, configured) {
|
|
13543
|
+
const names = flag ?? configured ?? DERIVE_SOURCES;
|
|
13544
|
+
const out = [];
|
|
13545
|
+
for (const name2 of names) {
|
|
13546
|
+
if (!isDeriveSource(name2)) {
|
|
13547
|
+
throw new DocmetaError(
|
|
13548
|
+
`"${name2}" is not a source; sources are ${DERIVE_SOURCES.join(", ")}`
|
|
13549
|
+
);
|
|
13550
|
+
}
|
|
13551
|
+
if (!out.includes(name2)) out.push(name2);
|
|
13552
|
+
}
|
|
13553
|
+
return out;
|
|
13554
|
+
}
|
|
13555
|
+
function errorResult2(file, format, message, check, fields = []) {
|
|
13556
|
+
return {
|
|
13557
|
+
file,
|
|
13558
|
+
format,
|
|
13559
|
+
fields,
|
|
13560
|
+
changed: false,
|
|
13561
|
+
error: message,
|
|
13562
|
+
...check ? {
|
|
13563
|
+
findings: [
|
|
13564
|
+
{ schema: "(parse)", instancePath: "", message, keyword: "parse" }
|
|
13565
|
+
]
|
|
13566
|
+
} : {}
|
|
13567
|
+
};
|
|
13568
|
+
}
|
|
13569
|
+
function summarize2(results) {
|
|
13570
|
+
const summary = {
|
|
13571
|
+
files: results.length,
|
|
13572
|
+
changed: 0,
|
|
13573
|
+
written: 0,
|
|
13574
|
+
stale: 0,
|
|
13575
|
+
unset: 0,
|
|
13576
|
+
unknown: 0,
|
|
13577
|
+
errors: 0
|
|
13578
|
+
};
|
|
13579
|
+
for (const r of results) {
|
|
13580
|
+
if (r.changed) summary.changed++;
|
|
13581
|
+
if (r.error !== void 0) summary.errors++;
|
|
13582
|
+
for (const f of r.fields) {
|
|
13583
|
+
if (f.written) summary.written++;
|
|
13584
|
+
if (f.status === "stale") summary.stale++;
|
|
13585
|
+
else if (f.status === "unset") summary.unset++;
|
|
13586
|
+
else if (f.status === "unknown") summary.unknown++;
|
|
13587
|
+
}
|
|
13588
|
+
}
|
|
13589
|
+
return summary;
|
|
13590
|
+
}
|
|
13591
|
+
|
|
11151
13592
|
// src/meta/reporters/sarif.ts
|
|
11152
13593
|
import { isAbsolute as isAbsolute6 } from "node:path";
|
|
11153
13594
|
|
|
@@ -11157,11 +13598,13 @@ var PARSE_ERROR_RULE = "manni/parse-error";
|
|
|
11157
13598
|
var SCHEMA_ERROR_RULE = "manni/schema-error";
|
|
11158
13599
|
var EXTERNAL_OWNED_RULE = "external:owned/external";
|
|
11159
13600
|
var EXTERNAL_DUPLICATE_RULE = "external:duplicate/external";
|
|
13601
|
+
var DERIVED_STALE_RULE = "derived:stale/derived";
|
|
11160
13602
|
var RESERVED_RULES = {
|
|
11161
13603
|
[PARSE_ERROR_RULE]: "The document's metadata block could not be parsed.",
|
|
11162
13604
|
[SCHEMA_ERROR_RULE]: "No schema set could be resolved for the document.",
|
|
11163
13605
|
[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."
|
|
13606
|
+
[EXTERNAL_DUPLICATE_RULE]: "Two documents carry the same value of a manifest's join field, so one manifest entry matched both.",
|
|
13607
|
+
[DERIVED_STALE_RULE]: "A managed field whose value differs from what git, CODEOWNERS or the GitHub or GitLab review record says; run `manni meta derive` to refresh it."
|
|
11165
13608
|
};
|
|
11166
13609
|
function ruleIdFor(e, frame) {
|
|
11167
13610
|
if (e.schema === SYNTHETIC_SCHEMA) {
|
|
@@ -11488,14 +13931,14 @@ var FILL_FORMAT_LIST = formatList(FILL_FORMATS);
|
|
|
11488
13931
|
function isFillFormat(value) {
|
|
11489
13932
|
return FILL_FORMATS.includes(value);
|
|
11490
13933
|
}
|
|
11491
|
-
function renderFillJson(
|
|
11492
|
-
return JSON.stringify(
|
|
13934
|
+
function renderFillJson(run2) {
|
|
13935
|
+
return JSON.stringify(run2, null, 2);
|
|
11493
13936
|
}
|
|
11494
13937
|
var score = (n) => n.toFixed(2);
|
|
11495
|
-
function renderFillPretty(
|
|
13938
|
+
function renderFillPretty(run2, opts = {}) {
|
|
11496
13939
|
const c = palette(opts.color ?? false);
|
|
11497
13940
|
const lines = [];
|
|
11498
|
-
for (const result of
|
|
13941
|
+
for (const result of run2.results) {
|
|
11499
13942
|
if (result.error != null) {
|
|
11500
13943
|
lines.push(`${c.red("\u2717")} ${result.file}`);
|
|
11501
13944
|
lines.push(` ${result.error}`);
|
|
@@ -11519,7 +13962,7 @@ function renderFillPretty(run, opts = {}) {
|
|
|
11519
13962
|
if (low.length > 0) {
|
|
11520
13963
|
lines.push(
|
|
11521
13964
|
c.dim(
|
|
11522
|
-
` below ${
|
|
13965
|
+
` below ${run2.threshold}: ${low.map((f) => `${f.field} ${score(f.confidence)}`).join(", ")}`
|
|
11523
13966
|
)
|
|
11524
13967
|
);
|
|
11525
13968
|
}
|
|
@@ -11527,14 +13970,14 @@ function renderFillPretty(run, opts = {}) {
|
|
|
11527
13970
|
lines.push(c.dim(` ${f.skipReason ?? "skipped"}: ${f.field}`));
|
|
11528
13971
|
}
|
|
11529
13972
|
}
|
|
11530
|
-
const { summary } =
|
|
11531
|
-
const verb =
|
|
13973
|
+
const { summary } = run2;
|
|
13974
|
+
const verb = run2.dryRun ? "would be written" : "written";
|
|
11532
13975
|
const parts = [
|
|
11533
13976
|
// Which provider ran decides what the proposals and the cost mean, and
|
|
11534
13977
|
// under `auto` it is not something the user chose — so say it rather than
|
|
11535
13978
|
// making them pass -f json to find out.
|
|
11536
|
-
`${
|
|
11537
|
-
`Threshold ${
|
|
13979
|
+
`${run2.provider}/${run2.model}`,
|
|
13980
|
+
`Threshold ${run2.threshold}`,
|
|
11538
13981
|
`${summary.files} file${summary.files === 1 ? "" : "s"}`,
|
|
11539
13982
|
`${summary.written} field${summary.written === 1 ? "" : "s"} ${verb}`,
|
|
11540
13983
|
`${summary.skipped} skipped`
|
|
@@ -11552,7 +13995,7 @@ function renderFillPretty(run, opts = {}) {
|
|
|
11552
13995
|
)
|
|
11553
13996
|
);
|
|
11554
13997
|
}
|
|
11555
|
-
if (
|
|
13998
|
+
if (run2.turnsSpent) {
|
|
11556
13999
|
lines.push(
|
|
11557
14000
|
c.yellow("--max-turns reached; some files were not processed.")
|
|
11558
14001
|
);
|
|
@@ -11563,9 +14006,9 @@ function formatValue(value) {
|
|
|
11563
14006
|
if (typeof value === "string") return value;
|
|
11564
14007
|
return toJsonText(value) ?? String(value);
|
|
11565
14008
|
}
|
|
11566
|
-
function renderFillGithub(
|
|
14009
|
+
function renderFillGithub(run2) {
|
|
11567
14010
|
const lines = [];
|
|
11568
|
-
for (const result of
|
|
14011
|
+
for (const result of run2.results) {
|
|
11569
14012
|
const file = escapeWorkflowCommandProperty(result.file);
|
|
11570
14013
|
if (result.error != null) {
|
|
11571
14014
|
lines.push(
|
|
@@ -11575,7 +14018,7 @@ function renderFillGithub(run) {
|
|
|
11575
14018
|
}
|
|
11576
14019
|
for (const f of result.fields) {
|
|
11577
14020
|
if (!f.required || f.written) continue;
|
|
11578
|
-
const why = f.skipReason === "low-confidence" ? `confidence ${score(f.confidence)} is below the ${
|
|
14021
|
+
const why = f.skipReason === "low-confidence" ? `confidence ${score(f.confidence)} is below the ${run2.threshold} threshold` : f.skipReason ?? "skipped";
|
|
11579
14022
|
const msg = escapeWorkflowCommandMessage(
|
|
11580
14023
|
`[fill] ${f.field} is required and was not filled (${why})`
|
|
11581
14024
|
);
|
|
@@ -11584,14 +14027,14 @@ function renderFillGithub(run) {
|
|
|
11584
14027
|
}
|
|
11585
14028
|
return lines.join("\n");
|
|
11586
14029
|
}
|
|
11587
|
-
function renderFill(format,
|
|
14030
|
+
function renderFill(format, run2, opts = {}) {
|
|
11588
14031
|
switch (format) {
|
|
11589
14032
|
case "json":
|
|
11590
|
-
return renderFillJson(
|
|
14033
|
+
return renderFillJson(run2);
|
|
11591
14034
|
case "github":
|
|
11592
|
-
return renderFillGithub(
|
|
14035
|
+
return renderFillGithub(run2);
|
|
11593
14036
|
case "pretty":
|
|
11594
|
-
return renderFillPretty(
|
|
14037
|
+
return renderFillPretty(run2, opts);
|
|
11595
14038
|
default: {
|
|
11596
14039
|
const unreachable = format;
|
|
11597
14040
|
throw new DocmetaError(
|
|
@@ -11601,12 +14044,137 @@ function renderFill(format, run, opts = {}) {
|
|
|
11601
14044
|
}
|
|
11602
14045
|
}
|
|
11603
14046
|
|
|
14047
|
+
// src/meta/reporters/derive.ts
|
|
14048
|
+
var DERIVE_FORMATS = ["pretty", "json", "github", "sarif", "junit"];
|
|
14049
|
+
var DERIVE_FORMAT_LIST = formatList(DERIVE_FORMATS);
|
|
14050
|
+
function isDeriveFormat(value) {
|
|
14051
|
+
return DERIVE_FORMATS.includes(value);
|
|
14052
|
+
}
|
|
14053
|
+
var JUNIT_CLASSNAME = "manni.derive";
|
|
14054
|
+
function renderDerive(run2, format, opts = {}) {
|
|
14055
|
+
if (run2.check) {
|
|
14056
|
+
const { results, summary } = asValidation(run2);
|
|
14057
|
+
return render(format, results, summary, {
|
|
14058
|
+
classname: JUNIT_CLASSNAME,
|
|
14059
|
+
...opts
|
|
14060
|
+
});
|
|
14061
|
+
}
|
|
14062
|
+
switch (format) {
|
|
14063
|
+
case "json":
|
|
14064
|
+
return JSON.stringify(run2, null, 2);
|
|
14065
|
+
case "pretty":
|
|
14066
|
+
return renderDerivePretty(run2, opts);
|
|
14067
|
+
case "github":
|
|
14068
|
+
case "sarif":
|
|
14069
|
+
case "junit":
|
|
14070
|
+
throw new DocmetaError(
|
|
14071
|
+
`${format} is a findings format, which only --check produces`
|
|
14072
|
+
);
|
|
14073
|
+
default: {
|
|
14074
|
+
const unreachable = format;
|
|
14075
|
+
throw new DocmetaError(
|
|
14076
|
+
`Unknown report format ${JSON.stringify(unreachable)}. Use ${DERIVE_FORMAT_LIST}.`
|
|
14077
|
+
);
|
|
14078
|
+
}
|
|
14079
|
+
}
|
|
14080
|
+
}
|
|
14081
|
+
function asValidation(run2) {
|
|
14082
|
+
const results = run2.results.map((r) => {
|
|
14083
|
+
const errors = r.findings ?? [];
|
|
14084
|
+
return { file: r.file, format: r.format, ok: errors.length === 0, schemas: [], errors };
|
|
14085
|
+
});
|
|
14086
|
+
const failed = results.filter((r) => !r.ok).length;
|
|
14087
|
+
return {
|
|
14088
|
+
results,
|
|
14089
|
+
summary: {
|
|
14090
|
+
files: results.length,
|
|
14091
|
+
passed: results.length - failed,
|
|
14092
|
+
failed,
|
|
14093
|
+
errors: results.reduce((n, r) => n + r.errors.length, 0)
|
|
14094
|
+
}
|
|
14095
|
+
};
|
|
14096
|
+
}
|
|
14097
|
+
var plural2 = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
14098
|
+
function renderDerivePretty(run2, opts = {}) {
|
|
14099
|
+
const c = palette(opts.color ?? false);
|
|
14100
|
+
const lines = [];
|
|
14101
|
+
for (const result of run2.results) {
|
|
14102
|
+
if (result.error !== void 0) {
|
|
14103
|
+
lines.push(`${c.red("\u2717")} ${result.file}`);
|
|
14104
|
+
lines.push(` ${result.error}`);
|
|
14105
|
+
continue;
|
|
14106
|
+
}
|
|
14107
|
+
const pending = result.fields.filter((f) => f.status === "stale" || f.status === "unset");
|
|
14108
|
+
if (pending.length === 0) {
|
|
14109
|
+
if (opts.quiet) continue;
|
|
14110
|
+
lines.push(`${result.file} ${c.dim("current")}${unknownNote(result, c.dim)}`);
|
|
14111
|
+
continue;
|
|
14112
|
+
}
|
|
14113
|
+
lines.push(result.file);
|
|
14114
|
+
const nameWidth = Math.max(...pending.map((f) => f.field.length));
|
|
14115
|
+
const changeWidth = Math.max(...pending.map((f) => change(f).length));
|
|
14116
|
+
for (const f of pending) {
|
|
14117
|
+
const name2 = c.cyan(f.field.padEnd(nameWidth));
|
|
14118
|
+
const from = f.status === "unset" ? c.dim("(unset)") : c.dim(fmt2(f.asserted));
|
|
14119
|
+
const to = c.green(fmt2(f.derived));
|
|
14120
|
+
const pad = " ".repeat(changeWidth - change(f).length);
|
|
14121
|
+
const trail = c.dim(`(${f.source ?? "a source"}: ${f.evidence ?? "no evidence"})`);
|
|
14122
|
+
lines.push(` ${name2} ${from} \u2192 ${to}${pad} ${trail}`);
|
|
14123
|
+
}
|
|
14124
|
+
const note = unknownNote(result, c.dim);
|
|
14125
|
+
if (note !== "") lines.push(` ${note}`);
|
|
14126
|
+
}
|
|
14127
|
+
const s = run2.summary;
|
|
14128
|
+
const parts = [plural2(s.files, "file")];
|
|
14129
|
+
if (run2.dryRun) {
|
|
14130
|
+
parts.push(`${s.changed} would change`, plural2(s.stale + s.unset, "field"));
|
|
14131
|
+
} else {
|
|
14132
|
+
parts.push(`${s.changed} changed`, `${plural2(s.written, "field")} written`);
|
|
14133
|
+
}
|
|
14134
|
+
if (s.errors > 0) parts.push(plural2(s.errors, "error"));
|
|
14135
|
+
let footer = parts.join(", ");
|
|
14136
|
+
if (run2.dryRun) footer += " \u2014 dry run, nothing written";
|
|
14137
|
+
if (lines.length > 0) lines.push("");
|
|
14138
|
+
lines.push(s.errors > 0 ? c.yellow(footer) : footer);
|
|
14139
|
+
return lines.join("\n");
|
|
14140
|
+
}
|
|
14141
|
+
function change(f) {
|
|
14142
|
+
const from = f.status === "unset" ? "(unset)" : fmt2(f.asserted);
|
|
14143
|
+
return `${from} \u2192 ${fmt2(f.derived)}`;
|
|
14144
|
+
}
|
|
14145
|
+
function unknownNote(result, dim) {
|
|
14146
|
+
const unknown = result.fields.filter((f) => f.status === "unknown").map((f) => f.field);
|
|
14147
|
+
if (unknown.length === 0) return "";
|
|
14148
|
+
return dim(` (${unknown.join(", ")}: no source could answer)`);
|
|
14149
|
+
}
|
|
14150
|
+
function fmt2(value) {
|
|
14151
|
+
if (typeof value === "string") return value;
|
|
14152
|
+
return toJsonText(value) ?? String(value);
|
|
14153
|
+
}
|
|
14154
|
+
|
|
11604
14155
|
// src/meta/reporters/get.ts
|
|
11605
14156
|
function stringifyValue(value) {
|
|
11606
14157
|
if (value === void 0) return "(unset)";
|
|
11607
14158
|
if (typeof value === "string") return value;
|
|
11608
14159
|
return JSON.stringify(value);
|
|
11609
14160
|
}
|
|
14161
|
+
function evidenceOf(d) {
|
|
14162
|
+
const repeated = ` (${stringifyValue(d.value)})`;
|
|
14163
|
+
return d.evidence.endsWith(repeated) ? d.evidence.slice(0, -repeated.length) : d.evidence;
|
|
14164
|
+
}
|
|
14165
|
+
function annotation(r, field) {
|
|
14166
|
+
const origin = r.origin?.[field];
|
|
14167
|
+
if (origin === void 0) return "";
|
|
14168
|
+
const d = r.derived?.[field] ?? null;
|
|
14169
|
+
if (origin === "derived") {
|
|
14170
|
+
return d == null ? "" : ` (derived, ${d.source}: ${evidenceOf(d)})`;
|
|
14171
|
+
}
|
|
14172
|
+
if (d == null) return " (asserted)";
|
|
14173
|
+
if (compareDerived(field, r.resolved?.[field], d).status === "current") {
|
|
14174
|
+
return " (asserted)";
|
|
14175
|
+
}
|
|
14176
|
+
return ` (asserted; ${d.source} says ${stringifyValue(d.value)}, ${evidenceOf(d)})`;
|
|
14177
|
+
}
|
|
11610
14178
|
function renderGet(results, fields, opts = {}) {
|
|
11611
14179
|
const c = palette(opts.color ?? false);
|
|
11612
14180
|
const lines = [];
|
|
@@ -11615,9 +14183,14 @@ function renderGet(results, fields, opts = {}) {
|
|
|
11615
14183
|
lines.push(`${c.dim(`${r.file}:`)} ${c.red(`(parse) ${r.error}`)}`);
|
|
11616
14184
|
continue;
|
|
11617
14185
|
}
|
|
11618
|
-
|
|
14186
|
+
const effective = r.resolved ?? r.values;
|
|
14187
|
+
if (opts.quiet && fields.every((f) => effective[f] === void 0)) continue;
|
|
11619
14188
|
for (const f of fields) {
|
|
11620
|
-
|
|
14189
|
+
const note = annotation(r, f);
|
|
14190
|
+
const suffix = note === "" ? "" : c.dim(note);
|
|
14191
|
+
lines.push(
|
|
14192
|
+
`${c.dim(`${r.file}:`)} ${f}=${stringifyValue(effective[f])}${suffix}`
|
|
14193
|
+
);
|
|
11621
14194
|
}
|
|
11622
14195
|
}
|
|
11623
14196
|
return lines.join("\n");
|
|
@@ -11628,21 +14201,21 @@ function cell(value) {
|
|
|
11628
14201
|
if (value === null) return "(null)";
|
|
11629
14202
|
return typeof value === "bigint" ? String(value) : stringifyValue(value);
|
|
11630
14203
|
}
|
|
11631
|
-
function renderQuery(
|
|
14204
|
+
function renderQuery(run2, opts = {}) {
|
|
11632
14205
|
const c = palette(opts.color ?? false);
|
|
11633
|
-
if (
|
|
14206
|
+
if (run2.changes) return renderChanges(run2.changes, c, opts);
|
|
11634
14207
|
const lines = [];
|
|
11635
|
-
if (
|
|
11636
|
-
const widths =
|
|
11637
|
-
(col) => Math.max(col.length, ...
|
|
14208
|
+
if (run2.rows.length > 0) {
|
|
14209
|
+
const widths = run2.columns.map(
|
|
14210
|
+
(col) => Math.max(col.length, ...run2.rows.map((r) => cell(r[col]).length))
|
|
11638
14211
|
);
|
|
11639
|
-
const pad = (
|
|
11640
|
-
lines.push(
|
|
11641
|
-
for (const row of
|
|
11642
|
-
lines.push(
|
|
14212
|
+
const pad = (text2, i) => i === run2.columns.length - 1 ? text2 : text2.padEnd(widths[i] ?? 0);
|
|
14213
|
+
lines.push(run2.columns.map((col, i) => c.bold(pad(col, i))).join(" "));
|
|
14214
|
+
for (const row of run2.rows) {
|
|
14215
|
+
lines.push(run2.columns.map((col, i) => pad(cell(row[col]), i)).join(" "));
|
|
11643
14216
|
}
|
|
11644
14217
|
}
|
|
11645
|
-
const n =
|
|
14218
|
+
const n = run2.rows.length;
|
|
11646
14219
|
const count = `${n} row${n === 1 ? "" : "s"}`;
|
|
11647
14220
|
lines.push(
|
|
11648
14221
|
opts.check ? n > 0 ? c.red(`\u2717 ${count} \u2014 check failed`) : c.green(`\u2713 ${count}`) : c.dim(count)
|
|
@@ -11650,15 +14223,15 @@ function renderQuery(run, opts = {}) {
|
|
|
11650
14223
|
return lines.join("\n");
|
|
11651
14224
|
}
|
|
11652
14225
|
var CSV_NEEDS_QUOTING = /[",\n\r]/;
|
|
11653
|
-
function renderQueryCsv(
|
|
14226
|
+
function renderQueryCsv(run2) {
|
|
11654
14227
|
const field = (value) => {
|
|
11655
14228
|
if (value === null || value === void 0) return "";
|
|
11656
|
-
const
|
|
11657
|
-
return CSV_NEEDS_QUOTING.test(
|
|
14229
|
+
const text2 = typeof value === "string" ? value : typeof value === "number" || typeof value === "bigint" ? String(value) : stringifyValue(value);
|
|
14230
|
+
return CSV_NEEDS_QUOTING.test(text2) ? `"${text2.replaceAll('"', '""')}"` : text2;
|
|
11658
14231
|
};
|
|
11659
14232
|
return [
|
|
11660
|
-
|
|
11661
|
-
...
|
|
14233
|
+
run2.columns.map(field).join(","),
|
|
14234
|
+
...run2.rows.map((row) => run2.columns.map((c) => field(row[c])).join(","))
|
|
11662
14235
|
].join("\n");
|
|
11663
14236
|
}
|
|
11664
14237
|
function renderChanges(changes, c, opts) {
|
|
@@ -11727,6 +14300,7 @@ export {
|
|
|
11727
14300
|
palette,
|
|
11728
14301
|
shouldColor,
|
|
11729
14302
|
ToolError,
|
|
14303
|
+
errorMessage,
|
|
11730
14304
|
programName,
|
|
11731
14305
|
setProgramName,
|
|
11732
14306
|
externalMetadataUrlProblem,
|
|
@@ -11778,12 +14352,46 @@ export {
|
|
|
11778
14352
|
orphanEntries,
|
|
11779
14353
|
orphanJoins,
|
|
11780
14354
|
orphanError,
|
|
14355
|
+
DERIVABLE_FIELDS,
|
|
14356
|
+
isBuiltinField,
|
|
14357
|
+
DERIVE_SOURCES,
|
|
14358
|
+
isDeriveSource,
|
|
14359
|
+
DERIVED_STALE_SCHEMA,
|
|
14360
|
+
DERIVED_KEYWORD,
|
|
14361
|
+
compareDerived,
|
|
14362
|
+
staleFindings,
|
|
11781
14363
|
parseConfig,
|
|
11782
14364
|
loadConfig,
|
|
11783
14365
|
resolveRunConfig,
|
|
11784
14366
|
isParamName,
|
|
11785
14367
|
memberOf,
|
|
11786
14368
|
Validator,
|
|
14369
|
+
REVIEW_CACHE_DIR,
|
|
14370
|
+
REVIEW_CACHE_VERSION,
|
|
14371
|
+
ReviewCache,
|
|
14372
|
+
cachedClient,
|
|
14373
|
+
BinMissing,
|
|
14374
|
+
run,
|
|
14375
|
+
PATH_PLACEHOLDER,
|
|
14376
|
+
isPerFile,
|
|
14377
|
+
argvFor,
|
|
14378
|
+
valueOf,
|
|
14379
|
+
deriveFromCommands,
|
|
14380
|
+
parseOriginUrl,
|
|
14381
|
+
identityFromOrigin,
|
|
14382
|
+
GitHubClient,
|
|
14383
|
+
GitLabClient,
|
|
14384
|
+
createReviewClient,
|
|
14385
|
+
deriveFromReviews,
|
|
14386
|
+
FIELD_SOURCES,
|
|
14387
|
+
sourcesFor,
|
|
14388
|
+
consultedSources,
|
|
14389
|
+
assertSourcesAvailable,
|
|
14390
|
+
deriveMetadata,
|
|
14391
|
+
RESOLVED_VIEW,
|
|
14392
|
+
resolvedColumns,
|
|
14393
|
+
mentionsResolved,
|
|
14394
|
+
createResolvedView,
|
|
11787
14395
|
rowsToFindings,
|
|
11788
14396
|
runValidate,
|
|
11789
14397
|
runGet,
|
|
@@ -11794,6 +14402,7 @@ export {
|
|
|
11794
14402
|
runInferSchema,
|
|
11795
14403
|
runQuery,
|
|
11796
14404
|
runFill,
|
|
14405
|
+
runDerive,
|
|
11797
14406
|
renderSarif,
|
|
11798
14407
|
renderJunit,
|
|
11799
14408
|
REPORT_FORMATS,
|
|
@@ -11814,9 +14423,13 @@ export {
|
|
|
11814
14423
|
isFillFormat,
|
|
11815
14424
|
renderFillGithub,
|
|
11816
14425
|
renderFill,
|
|
14426
|
+
DERIVE_FORMATS,
|
|
14427
|
+
DERIVE_FORMAT_LIST,
|
|
14428
|
+
isDeriveFormat,
|
|
14429
|
+
renderDerive,
|
|
11817
14430
|
stringifyValue,
|
|
11818
14431
|
renderGet,
|
|
11819
14432
|
renderQuery,
|
|
11820
14433
|
renderQueryCsv
|
|
11821
14434
|
};
|
|
11822
|
-
//# sourceMappingURL=chunk-
|
|
14435
|
+
//# sourceMappingURL=chunk-7SE7XDCL.js.map
|