@hawkeyexl/manni 1.0.1 → 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-IRHWILCO.js → chunk-7SE7XDCL.js} +2964 -382
- package/dist/chunk-7SE7XDCL.js.map +1 -0
- package/dist/{chunk-FL75S2L4.js → chunk-WP2BP3ZX.js} +103 -5
- 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-FL75S2L4.js.map +0 -1
- package/dist/chunk-IRHWILCO.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.0
|
|
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.`
|
|
@@ -437,8 +442,8 @@ var RENAME_ATTEMPTS = 3;
|
|
|
437
442
|
function errorCode(err) {
|
|
438
443
|
return typeof err === "object" && err !== null && "code" in err ? String(err.code) : void 0;
|
|
439
444
|
}
|
|
440
|
-
var wait = (ms) => new Promise((
|
|
441
|
-
setTimeout(
|
|
445
|
+
var wait = (ms) => new Promise((resolve17) => {
|
|
446
|
+
setTimeout(resolve17, ms);
|
|
442
447
|
});
|
|
443
448
|
async function writeFileAtomic(path, contents) {
|
|
444
449
|
const tmp = join(
|
|
@@ -632,11 +637,11 @@ import { resolve as resolvePath } from "node:path";
|
|
|
632
637
|
import { Buffer as Buffer2 } from "node:buffer";
|
|
633
638
|
|
|
634
639
|
// src/meta/core/json-text.ts
|
|
635
|
-
function stripBom(
|
|
636
|
-
return
|
|
640
|
+
function stripBom(text2) {
|
|
641
|
+
return text2.charCodeAt(0) === 65279 ? text2.slice(1) : text2;
|
|
637
642
|
}
|
|
638
|
-
function detectJsonIndent(
|
|
639
|
-
return /^\{\s*\n([ \t]+)/.exec(
|
|
643
|
+
function detectJsonIndent(text2) {
|
|
644
|
+
return /^\{\s*\n([ \t]+)/.exec(text2)?.[1] ?? " ";
|
|
640
645
|
}
|
|
641
646
|
function toJsonText(value) {
|
|
642
647
|
return JSON.stringify(value);
|
|
@@ -2905,6 +2910,11 @@ function assertPublishableBuiltinId(id) {
|
|
|
2905
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).`
|
|
2906
2911
|
);
|
|
2907
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
|
+
}
|
|
2908
2918
|
}
|
|
2909
2919
|
for (const id of BUILTINS.keys()) assertPublishableBuiltinId(id);
|
|
2910
2920
|
function listBuiltins() {
|
|
@@ -3032,8 +3042,8 @@ var SCHEMA_KEYS = [
|
|
|
3032
3042
|
"multipleOf"
|
|
3033
3043
|
];
|
|
3034
3044
|
var RETRY_DELAY_MS = 500;
|
|
3035
|
-
var wait2 = (ms) => new Promise((
|
|
3036
|
-
setTimeout(
|
|
3045
|
+
var wait2 = (ms) => new Promise((resolve17) => {
|
|
3046
|
+
setTimeout(resolve17, ms);
|
|
3037
3047
|
});
|
|
3038
3048
|
var RetryableFetchError = class extends DocmetaError {
|
|
3039
3049
|
};
|
|
@@ -3042,12 +3052,12 @@ function isAbort(err) {
|
|
|
3042
3052
|
return name2 === "TimeoutError" || name2 === "AbortError";
|
|
3043
3053
|
}
|
|
3044
3054
|
var JSON_PARSE_EXCERPT = /^Unexpected token '[\s\S]', (?:\.\.\.)?"([\s\S]*)"(?:\.\.\.)? is not valid JSON$/;
|
|
3045
|
-
function lineColumn(
|
|
3055
|
+
function lineColumn(text2, index) {
|
|
3046
3056
|
let line = 1;
|
|
3047
3057
|
let column = 1;
|
|
3048
|
-
const stop = Math.min(index,
|
|
3058
|
+
const stop = Math.min(index, text2.length);
|
|
3049
3059
|
for (let i = 0; i < stop; i++) {
|
|
3050
|
-
if (
|
|
3060
|
+
if (text2.charCodeAt(i) === 10) {
|
|
3051
3061
|
line += 1;
|
|
3052
3062
|
column = 1;
|
|
3053
3063
|
} else {
|
|
@@ -3056,12 +3066,12 @@ function lineColumn(text, index) {
|
|
|
3056
3066
|
}
|
|
3057
3067
|
return `line ${line} column ${column}`;
|
|
3058
3068
|
}
|
|
3059
|
-
function withoutFileExcerpt(message,
|
|
3069
|
+
function withoutFileExcerpt(message, text2) {
|
|
3060
3070
|
const match = JSON_PARSE_EXCERPT.exec(message);
|
|
3061
3071
|
if (!match) return message;
|
|
3062
3072
|
const quoted = match[1];
|
|
3063
|
-
const at = quoted === void 0 || quoted === "" ? -1 :
|
|
3064
|
-
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)}`;
|
|
3065
3075
|
}
|
|
3066
3076
|
function excerpt(raw, limit = 200) {
|
|
3067
3077
|
const flat = raw.replace(/\s+/g, " ").trim();
|
|
@@ -3275,12 +3285,12 @@ async function loadSchema(ref, options = {}) {
|
|
|
3275
3285
|
if (pin?.integrity !== void 0) {
|
|
3276
3286
|
assertIntegrity(ref, bytes, pin, pin.integrity);
|
|
3277
3287
|
}
|
|
3278
|
-
const
|
|
3288
|
+
const text2 = stripBom(bytes.toString("utf8"));
|
|
3279
3289
|
try {
|
|
3280
|
-
return JSON.parse(
|
|
3290
|
+
return JSON.parse(text2);
|
|
3281
3291
|
} catch (err) {
|
|
3282
3292
|
throw new DocmetaError(
|
|
3283
|
-
`Schema file "${ref}" is not valid JSON: ${withoutFileExcerpt(errorMessage(err),
|
|
3293
|
+
`Schema file "${ref}" is not valid JSON: ${withoutFileExcerpt(errorMessage(err), text2)}`
|
|
3284
3294
|
);
|
|
3285
3295
|
}
|
|
3286
3296
|
}
|
|
@@ -4003,9 +4013,9 @@ function mergeToml(inner, patch, deletions = []) {
|
|
|
4003
4013
|
if (insertAt >= lines.length) out.push(...appended);
|
|
4004
4014
|
return out.join("\n");
|
|
4005
4015
|
}
|
|
4006
|
-
function parseJsonBlockText(
|
|
4007
|
-
if (
|
|
4008
|
-
const parsed2 = JSON.parse(
|
|
4016
|
+
function parseJsonBlockText(text2) {
|
|
4017
|
+
if (text2.trim() === "") return {};
|
|
4018
|
+
const parsed2 = JSON.parse(text2);
|
|
4009
4019
|
if (parsed2 == null || typeof parsed2 !== "object" || Array.isArray(parsed2)) {
|
|
4010
4020
|
throw new DocmetaError(
|
|
4011
4021
|
"Cannot rewrite front matter: the root must be an object."
|
|
@@ -4013,14 +4023,14 @@ function parseJsonBlockText(text) {
|
|
|
4013
4023
|
}
|
|
4014
4024
|
return parsed2;
|
|
4015
4025
|
}
|
|
4016
|
-
function parseBlock(flavor,
|
|
4026
|
+
function parseBlock(flavor, text2) {
|
|
4017
4027
|
switch (flavor) {
|
|
4018
4028
|
case "json":
|
|
4019
|
-
return parseJsonBlockText(
|
|
4029
|
+
return parseJsonBlockText(text2);
|
|
4020
4030
|
case "toml":
|
|
4021
|
-
return
|
|
4031
|
+
return text2.trim() === "" ? {} : parseToml2(text2);
|
|
4022
4032
|
case "yaml": {
|
|
4023
|
-
const doc = parseDocument2(
|
|
4033
|
+
const doc = parseDocument2(text2);
|
|
4024
4034
|
if (doc.errors.length > 0) {
|
|
4025
4035
|
throw new DocmetaError(
|
|
4026
4036
|
`Cannot rewrite invalid YAML front matter: ${doc.errors[0]?.message ?? "parse error"}`
|
|
@@ -4160,10 +4170,10 @@ function parseTitle(lines) {
|
|
|
4160
4170
|
if (FIELD.test(first)) return { next: i };
|
|
4161
4171
|
const over = adornment(first);
|
|
4162
4172
|
if (over) {
|
|
4163
|
-
const
|
|
4173
|
+
const text2 = lines[i + 1] ?? "";
|
|
4164
4174
|
const under2 = adornment(lines[i + 2] ?? "");
|
|
4165
|
-
if (
|
|
4166
|
-
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 };
|
|
4167
4177
|
}
|
|
4168
4178
|
return { next: i };
|
|
4169
4179
|
}
|
|
@@ -4529,13 +4539,13 @@ function isMetadataAttribute(name2) {
|
|
|
4529
4539
|
return name2 !== "xmlns" && !name2.startsWith("xmlns:");
|
|
4530
4540
|
}
|
|
4531
4541
|
function elementText(el, allowEmpty = false) {
|
|
4532
|
-
let
|
|
4542
|
+
let text2 = "";
|
|
4533
4543
|
for (let n = el.firstChild; n; n = n.nextSibling) {
|
|
4534
4544
|
if (n.nodeType === 1) return void 0;
|
|
4535
|
-
if (n.nodeType === 3 || n.nodeType === 4)
|
|
4545
|
+
if (n.nodeType === 3 || n.nodeType === 4) text2 += n.nodeValue ?? "";
|
|
4536
4546
|
}
|
|
4537
|
-
if (!allowEmpty &&
|
|
4538
|
-
return
|
|
4547
|
+
if (!allowEmpty && text2.trim() === "") return void 0;
|
|
4548
|
+
return text2.trim();
|
|
4539
4549
|
}
|
|
4540
4550
|
function ditaLiftedElements(container) {
|
|
4541
4551
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -4572,15 +4582,15 @@ function own(table, key) {
|
|
|
4572
4582
|
function liftableChildren(root) {
|
|
4573
4583
|
const out = /* @__PURE__ */ new Map();
|
|
4574
4584
|
for (const child of childElements(root)) {
|
|
4575
|
-
const
|
|
4576
|
-
if (
|
|
4585
|
+
const text2 = elementText(child);
|
|
4586
|
+
if (text2 === void 0) continue;
|
|
4577
4587
|
const key = liftKey(root.nodeName, child.nodeName);
|
|
4578
4588
|
const group = out.get(key);
|
|
4579
4589
|
if (group) {
|
|
4580
4590
|
group.els.push(child);
|
|
4581
|
-
group.values.push(typeValue3(
|
|
4591
|
+
group.values.push(typeValue3(text2));
|
|
4582
4592
|
} else {
|
|
4583
|
-
out.set(key, { els: [child], values: [typeValue3(
|
|
4593
|
+
out.set(key, { els: [child], values: [typeValue3(text2)] });
|
|
4584
4594
|
}
|
|
4585
4595
|
}
|
|
4586
4596
|
return out;
|
|
@@ -4930,7 +4940,7 @@ function ownLift(container, element) {
|
|
|
4930
4940
|
const lifts = DITA_LIFTS[container];
|
|
4931
4941
|
return lifts && Object.prototype.hasOwnProperty.call(lifts, element) ? lifts[element] : void 0;
|
|
4932
4942
|
}
|
|
4933
|
-
function newElementEdits(content, starts, root, shape,
|
|
4943
|
+
function newElementEdits(content, starts, root, shape, items2, emit, escape) {
|
|
4934
4944
|
const eol = content.includes(CRLF) ? CRLF : LF;
|
|
4935
4945
|
const liftRootEl = liftRoot(root, shape);
|
|
4936
4946
|
const tagsFor = (item) => {
|
|
@@ -4944,7 +4954,7 @@ function newElementEdits(content, starts, root, shape, items, emit, escape) {
|
|
|
4944
4954
|
});
|
|
4945
4955
|
};
|
|
4946
4956
|
const byContainer = /* @__PURE__ */ new Map();
|
|
4947
|
-
for (const item of
|
|
4957
|
+
for (const item of items2) {
|
|
4948
4958
|
const group = byContainer.get(item.container);
|
|
4949
4959
|
if (group) group.push(item);
|
|
4950
4960
|
else byContainer.set(item.container, [item]);
|
|
@@ -5291,13 +5301,13 @@ function assertNoElementCollision(key, root) {
|
|
|
5291
5301
|
);
|
|
5292
5302
|
}
|
|
5293
5303
|
function emitScalar(key, value) {
|
|
5294
|
-
const
|
|
5295
|
-
if (
|
|
5304
|
+
const text2 = stringifyYaml(value).replace(/\n$/, "");
|
|
5305
|
+
if (text2.includes("\n")) {
|
|
5296
5306
|
throw new DocmetaError(
|
|
5297
5307
|
`Refusing to write "${key}": the value needs more than one line, which an XML attribute cannot hold. Set it manually.`
|
|
5298
5308
|
);
|
|
5299
5309
|
}
|
|
5300
|
-
return
|
|
5310
|
+
return text2;
|
|
5301
5311
|
}
|
|
5302
5312
|
function escapeAttr(value, quote) {
|
|
5303
5313
|
const escaped = value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -5368,13 +5378,13 @@ var HEAD_NOT_METADATA = /* @__PURE__ */ new Set(["script", "style"]);
|
|
|
5368
5378
|
var HEAD_SINGLETONS = /* @__PURE__ */ new Set(["title", "base"]);
|
|
5369
5379
|
var elementName = (el) => el ? el.tagName.toLowerCase() : "";
|
|
5370
5380
|
function headText(el, allowEmpty = false) {
|
|
5371
|
-
let
|
|
5381
|
+
let text2 = "";
|
|
5372
5382
|
for (const node of el.childNodes) {
|
|
5373
5383
|
if (defaultTreeAdapter.isElementNode(node)) return void 0;
|
|
5374
|
-
if (defaultTreeAdapter.isTextNode(node))
|
|
5384
|
+
if (defaultTreeAdapter.isTextNode(node)) text2 += node.value;
|
|
5375
5385
|
}
|
|
5376
|
-
if (!allowEmpty &&
|
|
5377
|
-
return
|
|
5386
|
+
if (!allowEmpty && text2.trim() === "") return void 0;
|
|
5387
|
+
return text2.trim();
|
|
5378
5388
|
}
|
|
5379
5389
|
function liftableHeadChildren(head) {
|
|
5380
5390
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -5382,15 +5392,15 @@ function liftableHeadChildren(head) {
|
|
|
5382
5392
|
if (!defaultTreeAdapter.isElementNode(node)) continue;
|
|
5383
5393
|
const name2 = node.tagName.toLowerCase();
|
|
5384
5394
|
if (HEAD_NOT_METADATA.has(name2)) continue;
|
|
5385
|
-
const
|
|
5386
|
-
if (
|
|
5395
|
+
const text2 = headText(node);
|
|
5396
|
+
if (text2 === void 0) continue;
|
|
5387
5397
|
const key = liftKey("head", name2);
|
|
5388
5398
|
const group = out.get(key);
|
|
5389
5399
|
if (group) {
|
|
5390
5400
|
group.els.push(node);
|
|
5391
|
-
group.texts.push(
|
|
5401
|
+
group.texts.push(text2);
|
|
5392
5402
|
} else {
|
|
5393
|
-
out.set(key, { els: [node], texts: [
|
|
5403
|
+
out.set(key, { els: [node], texts: [text2] });
|
|
5394
5404
|
}
|
|
5395
5405
|
}
|
|
5396
5406
|
return out;
|
|
@@ -5439,8 +5449,8 @@ function readHtml(source, options) {
|
|
|
5439
5449
|
} else if (node.tagName === "title") {
|
|
5440
5450
|
if (data.title === void 0) {
|
|
5441
5451
|
const first = node.childNodes[0];
|
|
5442
|
-
const
|
|
5443
|
-
setKey("title",
|
|
5452
|
+
const text2 = first && defaultTreeAdapter.isTextNode(first) ? first.value : "";
|
|
5453
|
+
setKey("title", text2, { kind: "title", el: node }, line, location?.startCol);
|
|
5444
5454
|
}
|
|
5445
5455
|
} else if (node.tagName === "meta") {
|
|
5446
5456
|
const key = attrValue(node, "name") ?? attrValue(node, "property");
|
|
@@ -5584,8 +5594,8 @@ function elementTextEdit2(el, key, emitted) {
|
|
|
5584
5594
|
}
|
|
5585
5595
|
function insertEdit2(content, head, at, tags) {
|
|
5586
5596
|
const { eol, indent } = headStyle(content, head, at);
|
|
5587
|
-
const
|
|
5588
|
-
return { start: at, end: at, text };
|
|
5597
|
+
const text2 = tags.map((t) => `${eol}${indent}${t}`).join("");
|
|
5598
|
+
return { start: at, end: at, text: text2 };
|
|
5589
5599
|
}
|
|
5590
5600
|
function headStyle(content, head, at) {
|
|
5591
5601
|
for (const child of head.childNodes) {
|
|
@@ -5621,13 +5631,13 @@ function attrValueSpan2(content, start, end) {
|
|
|
5621
5631
|
return { start: i, end, quote: DQ3, wrap: true };
|
|
5622
5632
|
}
|
|
5623
5633
|
function emitScalar2(key, value) {
|
|
5624
|
-
const
|
|
5625
|
-
if (
|
|
5634
|
+
const text2 = stringifyYaml2(value).replace(/\n$/, "");
|
|
5635
|
+
if (text2.includes("\n")) {
|
|
5626
5636
|
throw new DocmetaError(
|
|
5627
5637
|
`Refusing to write "${key}": the value needs more than one line, which an HTML attribute cannot hold. Set it manually.`
|
|
5628
5638
|
);
|
|
5629
5639
|
}
|
|
5630
|
-
return
|
|
5640
|
+
return text2;
|
|
5631
5641
|
}
|
|
5632
5642
|
function escapeAttr2(value, quote) {
|
|
5633
5643
|
const escaped = value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -5990,11 +6000,11 @@ async function loadExternalMetadata(collections, opts) {
|
|
|
5990
6000
|
const byField = /* @__PURE__ */ new Map();
|
|
5991
6001
|
const entries = [];
|
|
5992
6002
|
for (const { collection, manifest } of configured) {
|
|
5993
|
-
let
|
|
6003
|
+
let text2;
|
|
5994
6004
|
let file;
|
|
5995
6005
|
if (classifyRef(manifest.file).kind === "url") {
|
|
5996
6006
|
file = manifest.file;
|
|
5997
|
-
|
|
6007
|
+
text2 = await fetchExternalMetadata(manifest.file, {
|
|
5998
6008
|
...manifest.tokenEnv !== void 0 ? { tokenEnv: manifest.tokenEnv } : {},
|
|
5999
6009
|
...opts.offline !== void 0 ? { offline: opts.offline } : {},
|
|
6000
6010
|
...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
|
|
@@ -6003,32 +6013,32 @@ async function loadExternalMetadata(collections, opts) {
|
|
|
6003
6013
|
} else {
|
|
6004
6014
|
const abs = isAbsolute2(manifest.file) ? manifest.file : resolve5(opts.configDir, manifest.file);
|
|
6005
6015
|
file = reportedPath(abs, opts.base);
|
|
6006
|
-
|
|
6016
|
+
text2 = await readManifest(abs, file);
|
|
6007
6017
|
}
|
|
6008
6018
|
for (const key of manifest.keys) {
|
|
6009
6019
|
const list = owners.get(key);
|
|
6010
6020
|
if (list) list.push({ collection, file });
|
|
6011
6021
|
else owners.set(key, [{ collection, file }]);
|
|
6012
6022
|
}
|
|
6013
|
-
const
|
|
6023
|
+
const join8 = externalMetadataJoin(manifest);
|
|
6014
6024
|
let target;
|
|
6015
|
-
if (
|
|
6025
|
+
if (join8 === PATH_JOIN) {
|
|
6016
6026
|
target = byPath;
|
|
6017
6027
|
} else {
|
|
6018
|
-
const existing = byField.get(
|
|
6028
|
+
const existing = byField.get(join8);
|
|
6019
6029
|
if (existing) target = existing;
|
|
6020
6030
|
else {
|
|
6021
6031
|
target = /* @__PURE__ */ new Map();
|
|
6022
|
-
byField.set(
|
|
6032
|
+
byField.set(join8, target);
|
|
6023
6033
|
}
|
|
6024
6034
|
}
|
|
6025
6035
|
parseManifest(
|
|
6026
6036
|
manifest,
|
|
6027
6037
|
collection,
|
|
6028
|
-
|
|
6038
|
+
text2,
|
|
6029
6039
|
file,
|
|
6030
6040
|
opts.configDir,
|
|
6031
|
-
|
|
6041
|
+
join8,
|
|
6032
6042
|
target,
|
|
6033
6043
|
entries
|
|
6034
6044
|
);
|
|
@@ -6045,9 +6055,9 @@ async function readManifest(abs, file) {
|
|
|
6045
6055
|
);
|
|
6046
6056
|
}
|
|
6047
6057
|
}
|
|
6048
|
-
function parseManifest(manifest, collection,
|
|
6058
|
+
function parseManifest(manifest, collection, text2, file, configDir, join8, target, entries) {
|
|
6049
6059
|
const lc = new LineCounter2();
|
|
6050
|
-
const doc = parseDocument3(
|
|
6060
|
+
const doc = parseDocument3(text2, { lineCounter: lc, uniqueKeys: false });
|
|
6051
6061
|
const problem = doc.errors[0];
|
|
6052
6062
|
if (problem) {
|
|
6053
6063
|
throw new DocmetaError(
|
|
@@ -6058,7 +6068,7 @@ function parseManifest(manifest, collection, text, file, configDir, join7, targe
|
|
|
6058
6068
|
if (root === null || isScalar2(root) && root.value === null) return;
|
|
6059
6069
|
if (!isMap3(root)) {
|
|
6060
6070
|
throw new DocmetaError(
|
|
6061
|
-
`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.`
|
|
6062
6072
|
);
|
|
6063
6073
|
}
|
|
6064
6074
|
const owned = new Set(manifest.keys);
|
|
@@ -6083,7 +6093,7 @@ function parseManifest(manifest, collection, text, file, configDir, join7, targe
|
|
|
6083
6093
|
`Manifest ${where}: "${spelled}" must be a mapping of owned keys to values.`
|
|
6084
6094
|
);
|
|
6085
6095
|
}
|
|
6086
|
-
const indexKey =
|
|
6096
|
+
const indexKey = join8 === PATH_JOIN ? resolve5(configDir, spelled) : spelled;
|
|
6087
6097
|
const values = target.get(indexKey) ?? /* @__PURE__ */ new Map();
|
|
6088
6098
|
for (const kv of pair.value.items) {
|
|
6089
6099
|
const key = isScalar2(kv.key) ? String(kv.key.value) : String(kv.key);
|
|
@@ -6109,8 +6119,8 @@ function parseManifest(manifest, collection, text, file, configDir, join7, targe
|
|
|
6109
6119
|
target.set(indexKey, values);
|
|
6110
6120
|
entries.push({
|
|
6111
6121
|
collection,
|
|
6112
|
-
join:
|
|
6113
|
-
...
|
|
6122
|
+
join: join8,
|
|
6123
|
+
...join8 === PATH_JOIN ? { abs: indexKey } : {},
|
|
6114
6124
|
spelled,
|
|
6115
6125
|
file,
|
|
6116
6126
|
...entryLine === void 0 ? {} : { line: entryLine }
|
|
@@ -6225,6 +6235,97 @@ function orphanError(orphans) {
|
|
|
6225
6235
|
);
|
|
6226
6236
|
}
|
|
6227
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
|
+
|
|
6228
6329
|
// src/meta/core/config.ts
|
|
6229
6330
|
import { resolve as resolve7 } from "node:path";
|
|
6230
6331
|
import { parse as parseYaml2 } from "yaml";
|
|
@@ -6253,18 +6354,18 @@ var MOOSE_CONFIG_NAMES = [
|
|
|
6253
6354
|
"moose.config.yml"
|
|
6254
6355
|
];
|
|
6255
6356
|
async function readDocument(path, source, opts) {
|
|
6256
|
-
let
|
|
6357
|
+
let text2;
|
|
6257
6358
|
try {
|
|
6258
|
-
|
|
6359
|
+
text2 = await readFile4(path, "utf8");
|
|
6259
6360
|
} catch {
|
|
6260
6361
|
return null;
|
|
6261
6362
|
}
|
|
6262
|
-
return { text, doc: parseMapping(
|
|
6363
|
+
return { text: text2, doc: parseMapping(text2, source, opts) };
|
|
6263
6364
|
}
|
|
6264
|
-
function parseMapping(
|
|
6365
|
+
function parseMapping(text2, source, opts) {
|
|
6265
6366
|
let raw;
|
|
6266
6367
|
try {
|
|
6267
|
-
raw = parseYaml(
|
|
6368
|
+
raw = parseYaml(text2);
|
|
6268
6369
|
} catch (err) {
|
|
6269
6370
|
throw opts.toError(`${source}: invalid YAML: ${errorMessage(err)}`);
|
|
6270
6371
|
}
|
|
@@ -6385,8 +6486,29 @@ function asElementPaths(value, where, source) {
|
|
|
6385
6486
|
}
|
|
6386
6487
|
var OVERRIDE_KEYS = ["collection", "files", "schemas", "elements"];
|
|
6387
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.`;
|
|
6388
6501
|
var CHECK_NAME = /^[a-z0-9][a-z0-9._-]*$/;
|
|
6389
|
-
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
|
+
]);
|
|
6390
6512
|
var FILL_KEYS = [
|
|
6391
6513
|
"provider",
|
|
6392
6514
|
"model",
|
|
@@ -6399,6 +6521,7 @@ var CONFIG_KEYS = [
|
|
|
6399
6521
|
"schemas",
|
|
6400
6522
|
"overrides",
|
|
6401
6523
|
"checks",
|
|
6524
|
+
"derive",
|
|
6402
6525
|
"baseline",
|
|
6403
6526
|
"allowEmpty",
|
|
6404
6527
|
"respectGitignore",
|
|
@@ -6509,10 +6632,10 @@ function parseSchemaEntry(entry, where, source) {
|
|
|
6509
6632
|
}
|
|
6510
6633
|
return parsed2;
|
|
6511
6634
|
}
|
|
6512
|
-
function parseConfig(
|
|
6635
|
+
function parseConfig(text2, source) {
|
|
6513
6636
|
let raw;
|
|
6514
6637
|
try {
|
|
6515
|
-
raw = parseYaml2(
|
|
6638
|
+
raw = parseYaml2(text2);
|
|
6516
6639
|
} catch (err) {
|
|
6517
6640
|
throw new DocmetaError(
|
|
6518
6641
|
`${source}: invalid YAML: ${errorMessage(err)}`
|
|
@@ -6594,6 +6717,154 @@ function assertOverrideCollections(config, collections, source, section) {
|
|
|
6594
6717
|
);
|
|
6595
6718
|
}
|
|
6596
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.`
|
|
6813
|
+
);
|
|
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;
|
|
6867
|
+
}
|
|
6597
6868
|
function parseConfigDocument(raw, source) {
|
|
6598
6869
|
if (raw == null) return {};
|
|
6599
6870
|
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
@@ -6649,6 +6920,9 @@ function parseConfigDocument(raw, source) {
|
|
|
6649
6920
|
if (obj.checks !== void 0) {
|
|
6650
6921
|
config.checks = parseChecks(obj.checks, source);
|
|
6651
6922
|
}
|
|
6923
|
+
if (obj.derive !== void 0) {
|
|
6924
|
+
config.derive = parseDerive(obj.derive, source);
|
|
6925
|
+
}
|
|
6652
6926
|
if (obj.elements !== void 0) {
|
|
6653
6927
|
config.elements = asElementPaths(obj.elements, "elements", source);
|
|
6654
6928
|
}
|
|
@@ -6710,9 +6984,10 @@ function parseChecks(value, source) {
|
|
|
6710
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>).`
|
|
6711
6985
|
);
|
|
6712
6986
|
}
|
|
6713
|
-
|
|
6987
|
+
const reserved = RESERVED_CHECK_NAMES.get(e.name);
|
|
6988
|
+
if (reserved !== void 0) {
|
|
6714
6989
|
throw new DocmetaError(
|
|
6715
|
-
`${source}: checks[${i}].name "${e.name}" is reserved \u2014
|
|
6990
|
+
`${source}: checks[${i}].name "${e.name}" is reserved \u2014 ${reserved}. Pick another name.`
|
|
6716
6991
|
);
|
|
6717
6992
|
}
|
|
6718
6993
|
if (seen.has(e.name)) {
|
|
@@ -6815,8 +7090,8 @@ function parseFill(value, source) {
|
|
|
6815
7090
|
return fill;
|
|
6816
7091
|
}
|
|
6817
7092
|
function schemaTrustRoot(cwd, configDir) {
|
|
6818
|
-
const
|
|
6819
|
-
if (
|
|
7093
|
+
const git2 = findGitRoot(cwd);
|
|
7094
|
+
if (git2 !== null) return { dir: git2, source: "git" };
|
|
6820
7095
|
if (configDir !== void 0) return { dir: resolve7(configDir), source: "config" };
|
|
6821
7096
|
return { dir: resolve7(cwd), source: "cwd" };
|
|
6822
7097
|
}
|
|
@@ -6826,6 +7101,7 @@ async function loadConfig(explicitPath, cwd = process.cwd()) {
|
|
|
6826
7101
|
const section = file.wrapped ? META_SECTION : void 0;
|
|
6827
7102
|
const config = parseConfigValue(file.value, file.source, section);
|
|
6828
7103
|
assertOverrideCollections(config, file.collections, file.source, section);
|
|
7104
|
+
assertManagedFieldsUnowned(config, file.collections, file.source, section);
|
|
6829
7105
|
return {
|
|
6830
7106
|
config,
|
|
6831
7107
|
path: file.path,
|
|
@@ -7010,7 +7286,7 @@ function isParamName(name2) {
|
|
|
7010
7286
|
return PARAM_NAME_HEAD.exec(name2)?.[0] === name2;
|
|
7011
7287
|
}
|
|
7012
7288
|
function collectNamedParameters(sql) {
|
|
7013
|
-
const
|
|
7289
|
+
const tokens2 = [];
|
|
7014
7290
|
const seen = /* @__PURE__ */ new Set();
|
|
7015
7291
|
const prefixOf = /* @__PURE__ */ new Map();
|
|
7016
7292
|
let i = 0;
|
|
@@ -7040,14 +7316,14 @@ function collectNamedParameters(sql) {
|
|
|
7040
7316
|
prefixOf.set(m[0], ch);
|
|
7041
7317
|
if (!seen.has(token)) {
|
|
7042
7318
|
seen.add(token);
|
|
7043
|
-
|
|
7319
|
+
tokens2.push(token);
|
|
7044
7320
|
}
|
|
7045
7321
|
i += 1 + m[0].length;
|
|
7046
7322
|
} else {
|
|
7047
7323
|
i++;
|
|
7048
7324
|
}
|
|
7049
7325
|
}
|
|
7050
|
-
return
|
|
7326
|
+
return tokens2;
|
|
7051
7327
|
}
|
|
7052
7328
|
function stripLeadingTrivia(sql) {
|
|
7053
7329
|
let i = 0;
|
|
@@ -7060,16 +7336,16 @@ function stripLeadingTrivia(sql) {
|
|
|
7060
7336
|
return sql.slice(i);
|
|
7061
7337
|
}
|
|
7062
7338
|
}
|
|
7063
|
-
function isTrivia(
|
|
7339
|
+
function isTrivia(text2) {
|
|
7064
7340
|
let i = 0;
|
|
7065
|
-
while (i <
|
|
7066
|
-
const ch =
|
|
7341
|
+
while (i < text2.length) {
|
|
7342
|
+
const ch = text2[i];
|
|
7067
7343
|
if (ch === void 0 || /\s/.test(ch) || ch === ";") {
|
|
7068
7344
|
i++;
|
|
7069
7345
|
continue;
|
|
7070
7346
|
}
|
|
7071
|
-
if (startsComment(
|
|
7072
|
-
i = skipComment(
|
|
7347
|
+
if (startsComment(text2, i)) {
|
|
7348
|
+
i = skipComment(text2, i);
|
|
7073
7349
|
continue;
|
|
7074
7350
|
}
|
|
7075
7351
|
return false;
|
|
@@ -7081,8 +7357,8 @@ function loadSqlite() {
|
|
|
7081
7357
|
sqliteModule ??= (async () => {
|
|
7082
7358
|
const original = process.emitWarning.bind(process);
|
|
7083
7359
|
const filtered = (warning, ...rest) => {
|
|
7084
|
-
const
|
|
7085
|
-
if (
|
|
7360
|
+
const text2 = typeof warning === "string" ? warning : warning.message;
|
|
7361
|
+
if (text2.includes("SQLite is an experimental feature")) return;
|
|
7086
7362
|
Reflect.apply(original, process, [warning, ...rest]);
|
|
7087
7363
|
};
|
|
7088
7364
|
process.emitWarning = filtered;
|
|
@@ -7358,119 +7634,1751 @@ function toFieldError(schema, e, lineFor, colFor, locate) {
|
|
|
7358
7634
|
};
|
|
7359
7635
|
}
|
|
7360
7636
|
|
|
7361
|
-
// src/meta/
|
|
7362
|
-
import { existsSync as existsSync2 } from "node:fs";
|
|
7363
|
-
import { readFile as readFile6 } from "node:fs/promises";
|
|
7364
|
-
import { resolve as resolve9, extname as extname2 } from "node:path";
|
|
7365
|
-
|
|
7366
|
-
// src/meta/core/baseline.ts
|
|
7637
|
+
// src/meta/core/derive/cache.ts
|
|
7367
7638
|
import { createHash as createHash3 } from "node:crypto";
|
|
7368
|
-
import {
|
|
7369
|
-
import {
|
|
7370
|
-
var
|
|
7371
|
-
var
|
|
7372
|
-
var
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
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
|
+
};
|
|
7384
7700
|
}
|
|
7385
|
-
function
|
|
7386
|
-
|
|
7387
|
-
canonicalSchemaRef(e.schema, ctx),
|
|
7388
|
-
e.instancePath,
|
|
7389
|
-
e.keyword,
|
|
7390
|
-
e.subject ?? ""
|
|
7391
|
-
];
|
|
7392
|
-
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);
|
|
7393
7703
|
}
|
|
7394
|
-
function
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
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 });
|
|
7398
7714
|
}
|
|
7399
|
-
return
|
|
7400
|
-
{
|
|
7401
|
-
version: baseline.version,
|
|
7402
|
-
generatedWith: baseline.generatedWith,
|
|
7403
|
-
entries
|
|
7404
|
-
},
|
|
7405
|
-
null,
|
|
7406
|
-
2
|
|
7407
|
-
)}
|
|
7408
|
-
`;
|
|
7715
|
+
return { id, mergedAt, approvals: read };
|
|
7409
7716
|
}
|
|
7410
|
-
|
|
7411
|
-
|
|
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
|
+
});
|
|
7412
7787
|
}
|
|
7413
|
-
function
|
|
7414
|
-
|
|
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;
|
|
7415
7808
|
try {
|
|
7416
|
-
|
|
7417
|
-
} catch
|
|
7418
|
-
|
|
7419
|
-
}
|
|
7420
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
7421
|
-
bad(source, "top level must be an object.");
|
|
7422
|
-
}
|
|
7423
|
-
const obj = raw;
|
|
7424
|
-
if (obj.version !== BASELINE_VERSION) {
|
|
7425
|
-
bad(
|
|
7426
|
-
source,
|
|
7427
|
-
`unsupported version ${JSON.stringify(obj.version)} (this manni writes version ${BASELINE_VERSION}). Re-record it with \`manni meta validate --write-baseline\`.`
|
|
7428
|
-
);
|
|
7809
|
+
return JSON.parse(text2);
|
|
7810
|
+
} catch {
|
|
7811
|
+
return text2;
|
|
7429
7812
|
}
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
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 };
|
|
7434
7819
|
}
|
|
7435
|
-
const
|
|
7436
|
-
for (const
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
if (
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
);
|
|
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;
|
|
7448
7834
|
}
|
|
7449
|
-
entries[file] = value;
|
|
7450
7835
|
}
|
|
7451
|
-
return {
|
|
7836
|
+
return { status: { available: true }, records };
|
|
7452
7837
|
}
|
|
7453
|
-
|
|
7454
|
-
|
|
7838
|
+
function unavailable2(reason) {
|
|
7839
|
+
return { status: { available: false, reason }, records: /* @__PURE__ */ new Map() };
|
|
7840
|
+
}
|
|
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;
|
|
7455
7849
|
try {
|
|
7456
|
-
|
|
7850
|
+
result = await run(opts.spawn?.bin ?? program, args, {
|
|
7851
|
+
...opts.spawn,
|
|
7852
|
+
cwd: opts.spawn?.cwd ?? opts.cwd,
|
|
7853
|
+
timeoutMs
|
|
7854
|
+
});
|
|
7457
7855
|
} catch (err) {
|
|
7458
|
-
if (err
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
);
|
|
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}` };
|
|
7462
7863
|
}
|
|
7463
|
-
|
|
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 } };
|
|
7464
7877
|
}
|
|
7465
|
-
|
|
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;
|
|
7466
7894
|
try {
|
|
7467
|
-
|
|
7468
|
-
} catch
|
|
7469
|
-
|
|
7470
|
-
`Baseline "${source}" could not be written: ${errorMessage(err)}`
|
|
7471
|
-
);
|
|
7895
|
+
parsed2 = new URL(trimmed);
|
|
7896
|
+
} catch {
|
|
7897
|
+
return null;
|
|
7472
7898
|
}
|
|
7473
|
-
|
|
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)}`
|
|
9379
|
+
);
|
|
9380
|
+
}
|
|
9381
|
+
}
|
|
7474
9382
|
function buildBaseline(results, generatedWith, ctx) {
|
|
7475
9383
|
const entries = /* @__PURE__ */ Object.create(null);
|
|
7476
9384
|
for (const r of results) {
|
|
@@ -7586,8 +9494,8 @@ function synthesizeMessage(extras, row) {
|
|
|
7586
9494
|
const pairs = extras.flatMap((c) => {
|
|
7587
9495
|
const value = row[c];
|
|
7588
9496
|
if (value === null || value === void 0) return [];
|
|
7589
|
-
const
|
|
7590
|
-
return [`${c}=${
|
|
9497
|
+
const text2 = typeof value === "string" ? value : typeof value === "number" || typeof value === "bigint" ? String(value) : JSON.stringify(value);
|
|
9498
|
+
return [`${c}=${text2}`];
|
|
7591
9499
|
});
|
|
7592
9500
|
return pairs.length > 0 ? pairs.join(", ") : "check matched";
|
|
7593
9501
|
}
|
|
@@ -7597,9 +9505,26 @@ async function runChecks(checks, entries, ctx = {}) {
|
|
|
7597
9505
|
const { DatabaseSync } = await loadSqlite();
|
|
7598
9506
|
const db = new DatabaseSync(":memory:");
|
|
7599
9507
|
try {
|
|
7600
|
-
|
|
9508
|
+
const dataColumns = corpusDataColumns(entries);
|
|
9509
|
+
createDocsTable(db, entries, dataColumns);
|
|
7601
9510
|
registerLineFor(db, entries);
|
|
7602
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
|
+
}
|
|
7603
9528
|
db.exec("PRAGMA query_only = 1");
|
|
7604
9529
|
const loaded = new Set(entries.map((e) => e.label));
|
|
7605
9530
|
for (const check of checks) {
|
|
@@ -7609,17 +9534,17 @@ async function runChecks(checks, entries, ctx = {}) {
|
|
|
7609
9534
|
`check "${check.name}": ${head.split(/\s/, 1)[0]?.toUpperCase() ?? "that statement"} is refused: it can write outside the docs table.`
|
|
7610
9535
|
);
|
|
7611
9536
|
}
|
|
7612
|
-
let
|
|
9537
|
+
let tokens2;
|
|
7613
9538
|
try {
|
|
7614
|
-
|
|
9539
|
+
tokens2 = collectNamedParameters(check.query);
|
|
7615
9540
|
} catch (err) {
|
|
7616
9541
|
throw new DocmetaError(
|
|
7617
9542
|
`check "${check.name}": ${errorMessage(err)}`
|
|
7618
9543
|
);
|
|
7619
9544
|
}
|
|
7620
|
-
if (
|
|
9545
|
+
if (tokens2.length > 0) {
|
|
7621
9546
|
throw new DocmetaError(
|
|
7622
|
-
`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.`
|
|
7623
9548
|
);
|
|
7624
9549
|
}
|
|
7625
9550
|
let columns;
|
|
@@ -7664,17 +9589,17 @@ function parseErrorResult(file, format, message, keyword) {
|
|
|
7664
9589
|
}
|
|
7665
9590
|
function resolveBaselineRequest(opts, configured, configDir, cwd) {
|
|
7666
9591
|
const named = (label) => ({
|
|
7667
|
-
absPath:
|
|
9592
|
+
absPath: resolve11(cwd, label),
|
|
7668
9593
|
label
|
|
7669
9594
|
});
|
|
7670
9595
|
const implied = () => {
|
|
7671
9596
|
const base = configDir ?? cwd;
|
|
7672
9597
|
if (configured !== void 0) {
|
|
7673
|
-
return { absPath:
|
|
9598
|
+
return { absPath: resolve11(base, configured), label: configured };
|
|
7674
9599
|
}
|
|
7675
|
-
const current =
|
|
7676
|
-
const legacy =
|
|
7677
|
-
if (!
|
|
9600
|
+
const current = resolve11(base, DEFAULT_BASELINE_PATH);
|
|
9601
|
+
const legacy = resolve11(base, LEGACY_BASELINE_PATH);
|
|
9602
|
+
if (!existsSync3(current) && existsSync3(legacy)) {
|
|
7678
9603
|
warn(
|
|
7679
9604
|
`"${LEGACY_BASELINE_PATH}" is the pre-rename baseline file name. Rename it to "${DEFAULT_BASELINE_PATH}".`
|
|
7680
9605
|
);
|
|
@@ -7693,6 +9618,13 @@ function resolveBaselineRequest(opts, configured, configDir, cwd) {
|
|
|
7693
9618
|
if (configured) return { ...implied(), write: false };
|
|
7694
9619
|
return null;
|
|
7695
9620
|
}
|
|
9621
|
+
function once(fn) {
|
|
9622
|
+
let pending;
|
|
9623
|
+
return () => {
|
|
9624
|
+
pending ??= fn();
|
|
9625
|
+
return pending;
|
|
9626
|
+
};
|
|
9627
|
+
}
|
|
7696
9628
|
async function runValidate(opts) {
|
|
7697
9629
|
const cwd = opts.cwd ?? process.cwd();
|
|
7698
9630
|
const {
|
|
@@ -7785,6 +9717,39 @@ async function runValidate(opts) {
|
|
|
7785
9717
|
}
|
|
7786
9718
|
const checksWillRun = configuredChecks.length > 0 && opts.checks !== false && !scoped;
|
|
7787
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
|
+
});
|
|
7788
9753
|
const processOne = async (label, content, extension) => {
|
|
7789
9754
|
const extractor = forcedExtractor ?? extractorForExtension(extension);
|
|
7790
9755
|
if (!extractor) {
|
|
@@ -7805,6 +9770,9 @@ async function runValidate(opts) {
|
|
|
7805
9770
|
);
|
|
7806
9771
|
return;
|
|
7807
9772
|
}
|
|
9773
|
+
if (keepDeriveInputs && label !== STDIN_LABEL) {
|
|
9774
|
+
deriveInputs.push({ label, absPath: resolve11(base, label), content, extracted });
|
|
9775
|
+
}
|
|
7808
9776
|
const merged = mergeExternalMetadata(label, extracted, externalMetadata, members, base);
|
|
7809
9777
|
extracted = merged.extracted;
|
|
7810
9778
|
for (const j of merged.joins) {
|
|
@@ -7885,8 +9853,8 @@ async function runValidate(opts) {
|
|
|
7885
9853
|
await processOne(STDIN_LABEL, content, forcedExtractor.extensions[0] ?? "");
|
|
7886
9854
|
}
|
|
7887
9855
|
for (const file of files) {
|
|
7888
|
-
const content = await
|
|
7889
|
-
await processOne(file, content,
|
|
9856
|
+
const content = await readFile7(resolve11(base, file), "utf8");
|
|
9857
|
+
await processOne(file, content, extname3(file));
|
|
7890
9858
|
}
|
|
7891
9859
|
if (externalMetadata) {
|
|
7892
9860
|
const byLabel = new Map(results.map((r) => [r.file, r]));
|
|
@@ -7930,7 +9898,16 @@ async function runValidate(opts) {
|
|
|
7930
9898
|
// corpus see the same collections.
|
|
7931
9899
|
collections: declaredCollections,
|
|
7932
9900
|
...configDir !== void 0 ? { configDir } : {},
|
|
7933
|
-
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 } : {}
|
|
7934
9911
|
});
|
|
7935
9912
|
const byFile = new Map(results.map((r) => [r.file, r]));
|
|
7936
9913
|
for (const [file, errs] of findings) {
|
|
@@ -7945,6 +9922,24 @@ async function runValidate(opts) {
|
|
|
7945
9922
|
}
|
|
7946
9923
|
}
|
|
7947
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
|
+
}
|
|
7948
9943
|
const frame = { cwd, base: configDir ?? cwd, runBase: base };
|
|
7949
9944
|
const { results: reported, baseline } = await settleBaseline(
|
|
7950
9945
|
results,
|
|
@@ -8005,8 +10000,8 @@ async function settleBaseline(results, request, ctx) {
|
|
|
8005
10000
|
}
|
|
8006
10001
|
|
|
8007
10002
|
// src/meta/commands/get.ts
|
|
8008
|
-
import { readFile as
|
|
8009
|
-
import { resolve as
|
|
10003
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
10004
|
+
import { resolve as resolve12, extname as extname4 } from "node:path";
|
|
8010
10005
|
async function runGet(opts) {
|
|
8011
10006
|
const cwd = opts.cwd ?? process.cwd();
|
|
8012
10007
|
if (opts.fields.length === 0) {
|
|
@@ -8066,6 +10061,11 @@ async function runGet(opts) {
|
|
|
8066
10061
|
base,
|
|
8067
10062
|
offline: opts.offline ?? config?.offline ?? false
|
|
8068
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();
|
|
8069
10069
|
const readOne = (label, content, extension) => {
|
|
8070
10070
|
const extractor = forced ?? extractorForExtension(extension);
|
|
8071
10071
|
if (!extractor) {
|
|
@@ -8076,15 +10076,18 @@ async function runGet(opts) {
|
|
|
8076
10076
|
const members = membersFor(label);
|
|
8077
10077
|
let extracted;
|
|
8078
10078
|
try {
|
|
8079
|
-
|
|
8080
|
-
label,
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
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;
|
|
8088
10091
|
} catch (err) {
|
|
8089
10092
|
if (err instanceof DocmetaError) throw err;
|
|
8090
10093
|
const reason = err instanceof Error ? err.message : String(err);
|
|
@@ -8092,7 +10095,12 @@ async function runGet(opts) {
|
|
|
8092
10095
|
return;
|
|
8093
10096
|
}
|
|
8094
10097
|
const values = {};
|
|
8095
|
-
|
|
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);
|
|
8096
10104
|
out.push({ file: label, present: extracted.present, values });
|
|
8097
10105
|
};
|
|
8098
10106
|
if (usingStdin) {
|
|
@@ -8101,14 +10109,78 @@ async function runGet(opts) {
|
|
|
8101
10109
|
"Reading from stdin (`-`) requires --as <format> to choose an extractor."
|
|
8102
10110
|
);
|
|
8103
10111
|
}
|
|
8104
|
-
readOne(
|
|
10112
|
+
readOne(STDIN_LABEL, opts.stdinContent ?? "", forced.extensions[0] ?? "");
|
|
8105
10113
|
}
|
|
8106
10114
|
for (const file of files) {
|
|
8107
|
-
const content = await
|
|
8108
|
-
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
|
+
});
|
|
8109
10132
|
}
|
|
8110
10133
|
return out;
|
|
8111
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
|
+
}
|
|
8112
10184
|
function resolveField(data, field) {
|
|
8113
10185
|
const segments = field.startsWith("/") ? parseJsonPointer(field) : field.split(".");
|
|
8114
10186
|
const descended = descend(data, segments);
|
|
@@ -8118,6 +10190,23 @@ function resolveField(data, field) {
|
|
|
8118
10190
|
}
|
|
8119
10191
|
return void 0;
|
|
8120
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
|
+
}
|
|
8121
10210
|
function descend(data, segments) {
|
|
8122
10211
|
let current = data;
|
|
8123
10212
|
for (const segment of segments) {
|
|
@@ -8140,9 +10229,9 @@ function parseJsonPointer(pointer) {
|
|
|
8140
10229
|
}
|
|
8141
10230
|
|
|
8142
10231
|
// src/meta/commands/schemas.ts
|
|
8143
|
-
import { existsSync as
|
|
8144
|
-
import { mkdir as mkdir2, readFile as
|
|
8145
|
-
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";
|
|
8146
10235
|
import { parseDocument as parseDocument4, parse as parseYaml3, stringify } from "yaml";
|
|
8147
10236
|
function getSchemasInfo() {
|
|
8148
10237
|
return { builtins: listBuiltins(), formats: listFormats() };
|
|
@@ -8150,7 +10239,7 @@ function getSchemasInfo() {
|
|
|
8150
10239
|
var DEFAULT_VENDOR_DIR = "./schema";
|
|
8151
10240
|
var DEFAULT_CONFIG_NAME = "manni.config.yaml";
|
|
8152
10241
|
function posixRelative(from, to) {
|
|
8153
|
-
return
|
|
10242
|
+
return relative10(from, to).split(sep6).join("/");
|
|
8154
10243
|
}
|
|
8155
10244
|
function vendorFileName(url) {
|
|
8156
10245
|
let parsed2;
|
|
@@ -8175,20 +10264,20 @@ function vendorFileName(url) {
|
|
|
8175
10264
|
if (!name2.toLowerCase().endsWith(".json")) name2 += ".json";
|
|
8176
10265
|
return name2;
|
|
8177
10266
|
}
|
|
8178
|
-
async function assertNotIgnored(absFile, absDir, cwd, onNotice,
|
|
10267
|
+
async function assertNotIgnored(absFile, absDir, cwd, onNotice, text2 = VENDOR_IGNORE_TEXT) {
|
|
8179
10268
|
const relFile = posixRelative(cwd, absFile);
|
|
8180
10269
|
const relDir = posixRelative(cwd, absDir);
|
|
8181
10270
|
const candidates = [relFile];
|
|
8182
10271
|
if (relDir !== "" && !relDir.startsWith("..")) candidates.push(relDir);
|
|
8183
10272
|
const answer = await gitIgnored(candidates, cwd);
|
|
8184
10273
|
if (!answer.available) {
|
|
8185
|
-
onNotice?.(
|
|
10274
|
+
onNotice?.(text2.unchecked(relDir || "."));
|
|
8186
10275
|
return;
|
|
8187
10276
|
}
|
|
8188
10277
|
const ignoredDir = answer.ignored.has(relDir);
|
|
8189
10278
|
const ignoredFile = answer.ignored.has(relFile);
|
|
8190
10279
|
if (!ignoredDir && !ignoredFile) return;
|
|
8191
|
-
throw new DocmetaError(
|
|
10280
|
+
throw new DocmetaError(text2.refusal(ignoredDir ? relDir : relFile));
|
|
8192
10281
|
}
|
|
8193
10282
|
var VENDOR_IGNORE_TEXT = {
|
|
8194
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.`,
|
|
@@ -8219,12 +10308,12 @@ function renderConfig(existing, entries, section) {
|
|
|
8219
10308
|
doc.setIn(section === void 0 ? ["schemas"] : [section, "schemas"], entries);
|
|
8220
10309
|
return doc.toString();
|
|
8221
10310
|
}
|
|
8222
|
-
function reparseConfig(
|
|
10311
|
+
function reparseConfig(text2, source, section) {
|
|
8223
10312
|
if (section === void 0) {
|
|
8224
|
-
parseConfig(
|
|
10313
|
+
parseConfig(text2, source);
|
|
8225
10314
|
return;
|
|
8226
10315
|
}
|
|
8227
|
-
const doc = parseYaml3(
|
|
10316
|
+
const doc = parseYaml3(text2);
|
|
8228
10317
|
parseConfigValue(doc[section], source, section);
|
|
8229
10318
|
}
|
|
8230
10319
|
async function runVendorSchema(opts) {
|
|
@@ -8236,10 +10325,10 @@ async function runVendorSchema(opts) {
|
|
|
8236
10325
|
);
|
|
8237
10326
|
}
|
|
8238
10327
|
const loaded = await loadConfig(opts.configPath, cwd);
|
|
8239
|
-
const configPath = loaded?.path ??
|
|
10328
|
+
const configPath = loaded?.path ?? resolve13(cwd, DEFAULT_CONFIG_NAME);
|
|
8240
10329
|
const configDir = loaded?.dir ?? cwd;
|
|
8241
|
-
const absDir =
|
|
8242
|
-
const absFile =
|
|
10330
|
+
const absDir = resolve13(cwd, opts.dir ?? DEFAULT_VENDOR_DIR);
|
|
10331
|
+
const absFile = join6(absDir, vendorFileName(url));
|
|
8243
10332
|
await assertNotIgnored(absFile, absDir, cwd, opts.onNotice);
|
|
8244
10333
|
const fromConfig = posixRelative(configDir, absFile);
|
|
8245
10334
|
const ref = isAbsolute4(fromConfig) || fromConfig === "" ? absFile : fromConfig.startsWith(".") ? fromConfig : `./${fromConfig}`;
|
|
@@ -8256,8 +10345,8 @@ async function runVendorSchema(opts) {
|
|
|
8256
10345
|
);
|
|
8257
10346
|
}
|
|
8258
10347
|
let unchanged = false;
|
|
8259
|
-
if (
|
|
8260
|
-
const current = await
|
|
10348
|
+
if (existsSync4(absFile)) {
|
|
10349
|
+
const current = await readFile9(absFile);
|
|
8261
10350
|
unchanged = bytes.equals(current);
|
|
8262
10351
|
if (!unchanged && !folded.replaced) {
|
|
8263
10352
|
throw new DocmetaError(
|
|
@@ -8267,11 +10356,11 @@ async function runVendorSchema(opts) {
|
|
|
8267
10356
|
}
|
|
8268
10357
|
await mkdir2(absDir, { recursive: true });
|
|
8269
10358
|
await writeFileAtomic(absFile, bytes);
|
|
8270
|
-
const existing = loaded ? await
|
|
10359
|
+
const existing = loaded ? await readFile9(configPath, "utf8") : null;
|
|
8271
10360
|
const section = loaded ? loaded.section : META_SECTION;
|
|
8272
|
-
const
|
|
8273
|
-
reparseConfig(
|
|
8274
|
-
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);
|
|
8275
10364
|
return {
|
|
8276
10365
|
url,
|
|
8277
10366
|
file: posixRelative(cwd, absFile),
|
|
@@ -8418,15 +10507,15 @@ async function runInferSchema(opts) {
|
|
|
8418
10507
|
}
|
|
8419
10508
|
let absOut;
|
|
8420
10509
|
if (opts.out !== void 0) {
|
|
8421
|
-
absOut =
|
|
10510
|
+
absOut = resolve13(cwd, opts.out);
|
|
8422
10511
|
await assertNotIgnored(
|
|
8423
10512
|
absOut,
|
|
8424
|
-
|
|
10513
|
+
dirname8(absOut),
|
|
8425
10514
|
cwd,
|
|
8426
10515
|
opts.onNotice,
|
|
8427
10516
|
DRAFT_IGNORE_TEXT
|
|
8428
10517
|
);
|
|
8429
|
-
if (
|
|
10518
|
+
if (existsSync4(absOut)) {
|
|
8430
10519
|
throw new DocmetaError(
|
|
8431
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.`
|
|
8432
10521
|
);
|
|
@@ -8505,8 +10594,8 @@ async function runInferSchema(opts) {
|
|
|
8505
10594
|
scanOne(STDIN_LABEL, opts.stdinContent ?? "", forced.extensions[0] ?? "");
|
|
8506
10595
|
}
|
|
8507
10596
|
for (const file of files) {
|
|
8508
|
-
const content = await
|
|
8509
|
-
scanOne(file, content,
|
|
10597
|
+
const content = await readFile9(resolve13(base, file), "utf8");
|
|
10598
|
+
scanOne(file, content, extname5(file));
|
|
8510
10599
|
}
|
|
8511
10600
|
for (const u of unreadable) {
|
|
8512
10601
|
opts.onNotice?.(`could not read metadata from "${u.file}": ${u.message}`);
|
|
@@ -8547,7 +10636,7 @@ async function runInferSchema(opts) {
|
|
|
8547
10636
|
};
|
|
8548
10637
|
const keys = kept.map(({ sawEmptyString: _drop, ...rest }) => rest);
|
|
8549
10638
|
if (absOut !== void 0) {
|
|
8550
|
-
await mkdir2(
|
|
10639
|
+
await mkdir2(dirname8(absOut), { recursive: true });
|
|
8551
10640
|
await writeFileAtomic(absOut, `${JSON.stringify(draft, null, 2)}
|
|
8552
10641
|
`);
|
|
8553
10642
|
}
|
|
@@ -8565,9 +10654,9 @@ async function runInferSchema(opts) {
|
|
|
8565
10654
|
|
|
8566
10655
|
// src/meta/commands/query.ts
|
|
8567
10656
|
import { randomBytes } from "node:crypto";
|
|
8568
|
-
import { existsSync as
|
|
8569
|
-
import { mkdir as mkdir3, open, readFile as
|
|
8570
|
-
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";
|
|
8571
10660
|
import { parseDocument as parseDocument5, isMap as isMap4, isSeq as isSeq2, isScalar as isScalar3 } from "yaml";
|
|
8572
10661
|
async function runQuery(opts) {
|
|
8573
10662
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -8672,16 +10761,11 @@ async function runQuery(opts) {
|
|
|
8672
10761
|
);
|
|
8673
10762
|
}
|
|
8674
10763
|
const members = membersFor(label);
|
|
8675
|
-
const
|
|
8676
|
-
label,
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
externalMetadata,
|
|
8681
|
-
members,
|
|
8682
|
-
base
|
|
8683
|
-
).extracted;
|
|
8684
|
-
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 });
|
|
8685
10769
|
};
|
|
8686
10770
|
if (usingStdin) {
|
|
8687
10771
|
if (!forced) {
|
|
@@ -8698,15 +10782,17 @@ async function runQuery(opts) {
|
|
|
8698
10782
|
readOne(STDIN_LABEL, opts.stdinContent ?? "", ext);
|
|
8699
10783
|
}
|
|
8700
10784
|
for (const file of files) {
|
|
8701
|
-
const content = await
|
|
8702
|
-
readOne(file, content,
|
|
10785
|
+
const content = await readFile10(resolve14(base, file), "utf8");
|
|
10786
|
+
readOne(file, content, extname6(file));
|
|
8703
10787
|
}
|
|
8704
|
-
const db = opts.db === void 0 ? void 0 : { resolved:
|
|
8705
|
-
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, {
|
|
8706
10790
|
target: db,
|
|
8707
10791
|
params: boundParams,
|
|
8708
10792
|
...opts.schemas !== void 0 && opts.schemas.length > 0 ? { cliSchemas: opts.schemas } : {},
|
|
8709
10793
|
write: !opts.dryRun,
|
|
10794
|
+
cache: opts.cache ?? true,
|
|
10795
|
+
...opts.now !== void 0 ? { now: opts.now } : {},
|
|
8710
10796
|
base,
|
|
8711
10797
|
config,
|
|
8712
10798
|
cwd,
|
|
@@ -8718,9 +10804,10 @@ async function runQuery(opts) {
|
|
|
8718
10804
|
collections,
|
|
8719
10805
|
declaredCollections,
|
|
8720
10806
|
memberships: membersFor,
|
|
10807
|
+
managed: new Set(config?.derive?.fields ?? []),
|
|
8721
10808
|
onNotice: opts.onNotice
|
|
8722
10809
|
});
|
|
8723
|
-
return { ...
|
|
10810
|
+
return { ...run2, frame: { cwd, base: configDir ?? cwd, runBase: base } };
|
|
8724
10811
|
}
|
|
8725
10812
|
function bindParams(params) {
|
|
8726
10813
|
const out = /* @__PURE__ */ Object.create(null);
|
|
@@ -8759,6 +10846,8 @@ async function prepareDbTarget(resolved, display) {
|
|
|
8759
10846
|
}
|
|
8760
10847
|
var MissingCollectionView = class extends Error {
|
|
8761
10848
|
};
|
|
10849
|
+
var MissingDerivedTable = class extends Error {
|
|
10850
|
+
};
|
|
8762
10851
|
function refuseSqlError(err) {
|
|
8763
10852
|
const message = errorMessage(err);
|
|
8764
10853
|
if (message.includes("UNIQUE constraint failed: docs._path")) {
|
|
@@ -8767,8 +10856,18 @@ function refuseSqlError(err) {
|
|
|
8767
10856
|
const viewWrite = /cannot modify (.+?) because it is a view/.exec(message);
|
|
8768
10857
|
const viewName = viewWrite?.[1];
|
|
8769
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
|
+
}
|
|
8770
10869
|
throw new DocmetaError(
|
|
8771
|
-
`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('"', '""')}").`
|
|
8772
10871
|
);
|
|
8773
10872
|
}
|
|
8774
10873
|
throw new DocmetaError(`SQL error: ${message}`);
|
|
@@ -8781,7 +10880,7 @@ async function runSql(sql, entries, ctx) {
|
|
|
8781
10880
|
);
|
|
8782
10881
|
const { DatabaseSync } = await loadSqlite();
|
|
8783
10882
|
if (target) {
|
|
8784
|
-
await mkdir3(
|
|
10883
|
+
await mkdir3(dirname9(target.resolved), { recursive: true });
|
|
8785
10884
|
await prepareDbTarget(target.resolved, target.display);
|
|
8786
10885
|
}
|
|
8787
10886
|
let db;
|
|
@@ -8797,6 +10896,7 @@ async function runSql(sql, entries, ctx) {
|
|
|
8797
10896
|
files: entries.length,
|
|
8798
10897
|
columns: SYSTEM_COLUMNS.length + dataColumns.length
|
|
8799
10898
|
} : void 0;
|
|
10899
|
+
const derivedTable = { built: false };
|
|
8800
10900
|
try {
|
|
8801
10901
|
createDocsTable(db, entries, dataColumns);
|
|
8802
10902
|
let viewsBuilt = false;
|
|
@@ -8813,9 +10913,36 @@ async function runSql(sql, entries, ctx) {
|
|
|
8813
10913
|
viewsBuilt = true;
|
|
8814
10914
|
};
|
|
8815
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
|
+
};
|
|
8816
10942
|
if (sql === "") {
|
|
8817
10943
|
return { columns: [], rows: [], ...dbInfo ? { db: dbInfo } : {} };
|
|
8818
10944
|
}
|
|
10945
|
+
if (mentionsDerived(sql) || mentionsResolved(sql)) await buildDerived();
|
|
8819
10946
|
const head = stripLeadingTrivia(sql);
|
|
8820
10947
|
if (/^(attach|vacuum)\b/i.test(head)) {
|
|
8821
10948
|
throw new DocmetaError(
|
|
@@ -8842,6 +10969,10 @@ async function runSql(sql, entries, ctx) {
|
|
|
8842
10969
|
if (missing !== void 0 && !viewsBuilt && namesConfiguredCollection(missing, ctx.declaredCollections)) {
|
|
8843
10970
|
throw new MissingCollectionView();
|
|
8844
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
|
+
}
|
|
8845
10976
|
refuseSqlError(err);
|
|
8846
10977
|
}
|
|
8847
10978
|
const before = snapshotRows(db);
|
|
@@ -8900,11 +11031,20 @@ async function runSql(sql, entries, ctx) {
|
|
|
8900
11031
|
try {
|
|
8901
11032
|
return await runOnce();
|
|
8902
11033
|
} catch (err) {
|
|
11034
|
+
if (err instanceof MissingDerivedTable) {
|
|
11035
|
+
await buildDerived();
|
|
11036
|
+
return await runOnce();
|
|
11037
|
+
}
|
|
8903
11038
|
if (!(err instanceof MissingCollectionView)) throw err;
|
|
8904
11039
|
buildViews();
|
|
8905
11040
|
return await runOnce();
|
|
8906
11041
|
}
|
|
8907
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
|
+
}
|
|
8908
11048
|
db.close();
|
|
8909
11049
|
}
|
|
8910
11050
|
}
|
|
@@ -9056,21 +11196,21 @@ function appendedColumnDef(before, after) {
|
|
|
9056
11196
|
const comma = /^\s*,\s*/.exec(appended);
|
|
9057
11197
|
return comma ? appended.slice(comma[0].length) : void 0;
|
|
9058
11198
|
}
|
|
9059
|
-
function skipWs(
|
|
11199
|
+
function skipWs(text2, from) {
|
|
9060
11200
|
let i = from;
|
|
9061
|
-
while (i <
|
|
11201
|
+
while (i < text2.length && /\s/.test(text2[i] ?? "")) i++;
|
|
9062
11202
|
return i;
|
|
9063
11203
|
}
|
|
9064
|
-
function indexOfCheck(
|
|
11204
|
+
function indexOfCheck(text2, from) {
|
|
9065
11205
|
let i = from;
|
|
9066
|
-
while (i <
|
|
9067
|
-
const ch =
|
|
9068
|
-
if (startsStringOrIdent(
|
|
9069
|
-
i = skipStringOrIdent(
|
|
9070
|
-
} else if (startsComment(
|
|
9071
|
-
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);
|
|
9072
11212
|
} else if (ch !== void 0 && /[A-Za-z_]/.test(ch)) {
|
|
9073
|
-
const m = /^[A-Za-z_][A-Za-z0-9_]*/.exec(
|
|
11213
|
+
const m = /^[A-Za-z_][A-Za-z0-9_]*/.exec(text2.slice(i));
|
|
9074
11214
|
const word = m?.[0] ?? "";
|
|
9075
11215
|
if (word.toUpperCase() === "CHECK") return i;
|
|
9076
11216
|
i += Math.max(word.length, 1);
|
|
@@ -9080,17 +11220,17 @@ function indexOfCheck(text, from) {
|
|
|
9080
11220
|
}
|
|
9081
11221
|
return -1;
|
|
9082
11222
|
}
|
|
9083
|
-
function matchingParenEnd(
|
|
11223
|
+
function matchingParenEnd(text2, open2) {
|
|
9084
11224
|
let depth = 0;
|
|
9085
11225
|
let i = open2;
|
|
9086
|
-
while (i <
|
|
9087
|
-
const ch =
|
|
9088
|
-
if (startsStringOrIdent(
|
|
9089
|
-
i = skipStringOrIdent(
|
|
11226
|
+
while (i < text2.length) {
|
|
11227
|
+
const ch = text2[i];
|
|
11228
|
+
if (startsStringOrIdent(text2, i)) {
|
|
11229
|
+
i = skipStringOrIdent(text2, i);
|
|
9090
11230
|
continue;
|
|
9091
11231
|
}
|
|
9092
|
-
if (startsComment(
|
|
9093
|
-
i = skipComment(
|
|
11232
|
+
if (startsComment(text2, i)) {
|
|
11233
|
+
i = skipComment(text2, i);
|
|
9094
11234
|
continue;
|
|
9095
11235
|
}
|
|
9096
11236
|
if (ch === "(") depth++;
|
|
@@ -9110,9 +11250,9 @@ function enumForAddedColumn(catalogBefore, catalogAfter, key, mapped) {
|
|
|
9110
11250
|
};
|
|
9111
11251
|
const def = appendedColumnDef(catalogBefore, catalogAfter);
|
|
9112
11252
|
if (def === void 0) {
|
|
9113
|
-
const count = (
|
|
11253
|
+
const count = (text2) => {
|
|
9114
11254
|
let n = 0;
|
|
9115
|
-
for (let i2 = indexOfCheck(
|
|
11255
|
+
for (let i2 = indexOfCheck(text2, 0); i2 !== -1; i2 = indexOfCheck(text2, i2 + 5)) {
|
|
9116
11256
|
n++;
|
|
9117
11257
|
}
|
|
9118
11258
|
return n;
|
|
@@ -9225,8 +11365,8 @@ var FORK_IGNORE_TEXT = {
|
|
|
9225
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.`,
|
|
9226
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.`
|
|
9227
11367
|
};
|
|
9228
|
-
function matchEol(reference,
|
|
9229
|
-
return reference.includes("\r\n") ?
|
|
11368
|
+
function matchEol(reference, text2) {
|
|
11369
|
+
return reference.includes("\r\n") ? text2.replace(/\n/g, "\r\n") : text2;
|
|
9230
11370
|
}
|
|
9231
11371
|
function displayPath(ctx, abs) {
|
|
9232
11372
|
const rel = posixRelative(ctx.base, abs);
|
|
@@ -9240,14 +11380,14 @@ function assertSchemaWriteWithin(ctx, abs) {
|
|
|
9240
11380
|
);
|
|
9241
11381
|
}
|
|
9242
11382
|
function foldPath(p) {
|
|
9243
|
-
const abs =
|
|
11383
|
+
const abs = resolve14(p);
|
|
9244
11384
|
return process.platform === "win32" ? abs.toLowerCase() : abs;
|
|
9245
11385
|
}
|
|
9246
11386
|
function filePinsByPath(ctx) {
|
|
9247
11387
|
const byPath = /* @__PURE__ */ new Map();
|
|
9248
11388
|
for (const [ref, pin] of collectSchemaPins(ctx.config)) {
|
|
9249
11389
|
if (classifyRef(ref).kind !== "file") continue;
|
|
9250
|
-
byPath.set(foldPath(
|
|
11390
|
+
byPath.set(foldPath(resolve14(ctx.cwd, ref)), pin);
|
|
9251
11391
|
}
|
|
9252
11392
|
return byPath;
|
|
9253
11393
|
}
|
|
@@ -9275,13 +11415,13 @@ async function loadSetMembers(refs, ctx) {
|
|
|
9275
11415
|
});
|
|
9276
11416
|
continue;
|
|
9277
11417
|
}
|
|
9278
|
-
const abs =
|
|
11418
|
+
const abs = resolve14(ctx.cwd, ref);
|
|
9279
11419
|
const canon = foldPath(abs);
|
|
9280
11420
|
if (seenFiles.has(canon)) continue;
|
|
9281
11421
|
seenFiles.add(canon);
|
|
9282
|
-
let
|
|
11422
|
+
let text2;
|
|
9283
11423
|
try {
|
|
9284
|
-
|
|
11424
|
+
text2 = await readFile10(abs, "utf8");
|
|
9285
11425
|
} catch {
|
|
9286
11426
|
throw new DocmetaError(
|
|
9287
11427
|
`Schema file not found: "${ref}" (looked at ${abs}).`
|
|
@@ -9289,7 +11429,7 @@ async function loadSetMembers(refs, ctx) {
|
|
|
9289
11429
|
}
|
|
9290
11430
|
const pin = pins.get(canon);
|
|
9291
11431
|
if (pin?.integrity !== void 0) {
|
|
9292
|
-
if (integrityOf(Buffer.from(
|
|
11432
|
+
if (integrityOf(Buffer.from(text2, "utf8")) !== pin.integrity) {
|
|
9293
11433
|
throw new DocmetaError(
|
|
9294
11434
|
`Schema "${ref}" does not match its recorded integrity; its contents have moved since it was vendored. Re-vendor it before evolving it.`
|
|
9295
11435
|
);
|
|
@@ -9297,7 +11437,7 @@ async function loadSetMembers(refs, ctx) {
|
|
|
9297
11437
|
}
|
|
9298
11438
|
let parsed2;
|
|
9299
11439
|
try {
|
|
9300
|
-
parsed2 = JSON.parse(stripBom(
|
|
11440
|
+
parsed2 = JSON.parse(stripBom(text2));
|
|
9301
11441
|
} catch {
|
|
9302
11442
|
throw new DocmetaError(
|
|
9303
11443
|
`Schema "${ref}" is not valid JSON, so DDL cannot read it. Fix the file first.`
|
|
@@ -9312,7 +11452,7 @@ async function loadSetMembers(refs, ctx) {
|
|
|
9312
11452
|
ref,
|
|
9313
11453
|
kind: "file",
|
|
9314
11454
|
abs,
|
|
9315
|
-
text,
|
|
11455
|
+
text: text2,
|
|
9316
11456
|
schema: parsed2
|
|
9317
11457
|
});
|
|
9318
11458
|
}
|
|
@@ -9480,16 +11620,16 @@ async function planSchemaMutation(ops, entries, ctx) {
|
|
|
9480
11620
|
const [, name2, ver] = target.builtinId.split(":");
|
|
9481
11621
|
const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, "-");
|
|
9482
11622
|
const forkName = `${safe(name2 ?? "schema")}-${safe(ver ?? "0")}.local.json`;
|
|
9483
|
-
schemaAbs =
|
|
11623
|
+
schemaAbs = resolve14(ctx.configDir ?? ctx.cwd, "schemas", forkName);
|
|
9484
11624
|
assertSchemaWriteWithin(ctx, schemaAbs);
|
|
9485
|
-
if (
|
|
11625
|
+
if (existsSync5(schemaAbs)) {
|
|
9486
11626
|
throw new DocmetaError(
|
|
9487
11627
|
`"${displayPath(ctx, schemaAbs)}" already exists; refusing to overwrite it with a fork of ${target.builtinId}.`
|
|
9488
11628
|
);
|
|
9489
11629
|
}
|
|
9490
11630
|
await assertNotIgnored(
|
|
9491
11631
|
schemaAbs,
|
|
9492
|
-
|
|
11632
|
+
dirname9(schemaAbs),
|
|
9493
11633
|
ctx.cwd,
|
|
9494
11634
|
ctx.onNotice,
|
|
9495
11635
|
FORK_IGNORE_TEXT
|
|
@@ -9636,8 +11776,8 @@ async function planConfigEdit(ctx, edit, runRefs) {
|
|
|
9636
11776
|
if (configPath === void 0 || configDir === void 0) {
|
|
9637
11777
|
return { changes: [], repointed: false };
|
|
9638
11778
|
}
|
|
9639
|
-
const
|
|
9640
|
-
const doc = parseDocument5(stripBom(
|
|
11779
|
+
const text2 = await readFile10(configPath, "utf8");
|
|
11780
|
+
const doc = parseDocument5(stripBom(text2));
|
|
9641
11781
|
const display = displayPath(ctx, configPath);
|
|
9642
11782
|
const rawRefOf = (item) => {
|
|
9643
11783
|
if (isScalar3(item) && typeof item.value === "string") return item.value;
|
|
@@ -9647,7 +11787,7 @@ async function planConfigEdit(ctx, edit, runRefs) {
|
|
|
9647
11787
|
}
|
|
9648
11788
|
return void 0;
|
|
9649
11789
|
};
|
|
9650
|
-
const rebaseRaw = (raw) => classifyRef(raw).kind === "file" && !isAbsolute5(raw) ?
|
|
11790
|
+
const rebaseRaw = (raw) => classifyRef(raw).kind === "file" && !isAbsolute5(raw) ? resolve14(configDir, raw) : raw;
|
|
9651
11791
|
const wantSet = runRefs.map(rebaseRaw).sort().join("\n");
|
|
9652
11792
|
const seqResolvesToRunSet = (node) => {
|
|
9653
11793
|
if (!isSeq2(node)) return false;
|
|
@@ -9692,7 +11832,7 @@ async function planConfigEdit(ctx, edit, runRefs) {
|
|
|
9692
11832
|
if (!isMap4(item)) continue;
|
|
9693
11833
|
const raw = rawRefOf(item);
|
|
9694
11834
|
if (raw === void 0 || classifyRef(raw).kind !== "file") continue;
|
|
9695
|
-
const abs = isAbsolute5(raw) ? raw :
|
|
11835
|
+
const abs = isAbsolute5(raw) ? raw : resolve14(configDir, raw);
|
|
9696
11836
|
if (foldPath(abs) !== wantPath) continue;
|
|
9697
11837
|
pinFrom = item.get("integrity");
|
|
9698
11838
|
item.set("integrity", edit.integrity.value);
|
|
@@ -9724,8 +11864,8 @@ async function planConfigEdit(ctx, edit, runRefs) {
|
|
|
9724
11864
|
return {
|
|
9725
11865
|
write: {
|
|
9726
11866
|
path: configPath,
|
|
9727
|
-
content: matchEol(
|
|
9728
|
-
expected:
|
|
11867
|
+
content: matchEol(text2, doc.toString({ lineWidth: 0 })),
|
|
11868
|
+
expected: text2
|
|
9729
11869
|
},
|
|
9730
11870
|
changes,
|
|
9731
11871
|
repointed
|
|
@@ -9908,12 +12048,12 @@ function buildChanges(diff, entries, sentinel, ctx, renameHints = [], booleanAdd
|
|
|
9908
12048
|
}
|
|
9909
12049
|
}
|
|
9910
12050
|
validateNewPath(file, ctx.base);
|
|
9911
|
-
if (!extractorForExtension(
|
|
12051
|
+
if (!extractorForExtension(extname6(file))?.apply) {
|
|
9912
12052
|
throw new DocmetaError(
|
|
9913
12053
|
`"${file}": no writable format for that extension.`
|
|
9914
12054
|
);
|
|
9915
12055
|
}
|
|
9916
|
-
if (
|
|
12056
|
+
if (existsSync5(resolve14(ctx.base, file))) {
|
|
9917
12057
|
throw new DocmetaError(
|
|
9918
12058
|
`"${file}" already exists; INSERT creates new files only.`
|
|
9919
12059
|
);
|
|
@@ -9927,12 +12067,12 @@ function buildChanges(diff, entries, sentinel, ctx, renameHints = [], booleanAdd
|
|
|
9927
12067
|
}
|
|
9928
12068
|
for (const { from, to } of diff.renamedFiles) {
|
|
9929
12069
|
validateNewPath(to, ctx.base);
|
|
9930
|
-
if (
|
|
12070
|
+
if (extname6(to).toLowerCase() !== extname6(from).toLowerCase()) {
|
|
9931
12071
|
throw new DocmetaError(
|
|
9932
12072
|
`"${from}" -> "${to}": a rename may not change the extension.`
|
|
9933
12073
|
);
|
|
9934
12074
|
}
|
|
9935
|
-
if (
|
|
12075
|
+
if (existsSync5(resolve14(ctx.base, to))) {
|
|
9936
12076
|
throw new DocmetaError(
|
|
9937
12077
|
`"${to}" already exists; the rename would overwrite it.`
|
|
9938
12078
|
);
|
|
@@ -9940,8 +12080,23 @@ function buildChanges(diff, entries, sentinel, ctx, renameHints = [], booleanAdd
|
|
|
9940
12080
|
changes.push({ file: from, renamed: to, written: false });
|
|
9941
12081
|
}
|
|
9942
12082
|
refuseExternalMetadataWrites(changes, entries, ctx);
|
|
12083
|
+
refuseManagedWrites(changes, entries, ctx);
|
|
9943
12084
|
return changes;
|
|
9944
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
|
+
}
|
|
9945
12100
|
function validateNewPath(p, base) {
|
|
9946
12101
|
const refuse = (why) => {
|
|
9947
12102
|
throw new DocmetaError(`"${p}" is not a usable path: ${why}.`);
|
|
@@ -9950,9 +12105,9 @@ function validateNewPath(p, base) {
|
|
|
9950
12105
|
if (p.split(/[\\/]/).some((segment) => segment === "..")) {
|
|
9951
12106
|
refuse("it traverses upward");
|
|
9952
12107
|
}
|
|
9953
|
-
const abs =
|
|
9954
|
-
if (abs ===
|
|
9955
|
-
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");
|
|
9956
12111
|
}
|
|
9957
12112
|
function refuseExternalMetadataWrites(changes, entries, ctx) {
|
|
9958
12113
|
const index = ctx.externalMetadata;
|
|
@@ -9981,7 +12136,7 @@ function refuseExternalMetadataWrites(changes, entries, ctx) {
|
|
|
9981
12136
|
continue;
|
|
9982
12137
|
}
|
|
9983
12138
|
if ("renamed" in c) {
|
|
9984
|
-
const entry = index.byPath.get(
|
|
12139
|
+
const entry = index.byPath.get(resolve14(ctx.base, c.file));
|
|
9985
12140
|
if (entry !== void 0 && entry.size > 0) {
|
|
9986
12141
|
const manifest = [...entry.values()][0]?.file ?? "manifest";
|
|
9987
12142
|
throw new DocmetaError(
|
|
@@ -10034,15 +12189,15 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
|
|
|
10034
12189
|
const pendingWrites = [];
|
|
10035
12190
|
const pendingRenames = [];
|
|
10036
12191
|
for (const [label, ops] of grouped) {
|
|
10037
|
-
const path =
|
|
12192
|
+
const path = resolve14(ctx.base, label);
|
|
10038
12193
|
if (ops.renamedTo !== void 0) {
|
|
10039
|
-
if (!
|
|
12194
|
+
if (!existsSync5(path)) {
|
|
10040
12195
|
throw new DocmetaError(
|
|
10041
12196
|
`"${label}" is gone from disk since it was read; re-run the query.`
|
|
10042
12197
|
);
|
|
10043
12198
|
}
|
|
10044
|
-
const toDisk =
|
|
10045
|
-
if (
|
|
12199
|
+
const toDisk = resolve14(ctx.base, ops.renamedTo);
|
|
12200
|
+
if (existsSync5(toDisk)) {
|
|
10046
12201
|
throw new DocmetaError(
|
|
10047
12202
|
`"${ops.renamedTo}" appeared on disk since the plan; re-run the query.`
|
|
10048
12203
|
);
|
|
@@ -10051,13 +12206,13 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
|
|
|
10051
12206
|
continue;
|
|
10052
12207
|
}
|
|
10053
12208
|
if (ops.created !== void 0) {
|
|
10054
|
-
const extractor = extractorForExtension(
|
|
12209
|
+
const extractor = extractorForExtension(extname6(label));
|
|
10055
12210
|
if (!extractor?.apply) {
|
|
10056
12211
|
throw new DocmetaError(
|
|
10057
12212
|
`"${label}": no writable format for that extension.`
|
|
10058
12213
|
);
|
|
10059
12214
|
}
|
|
10060
|
-
if (
|
|
12215
|
+
if (existsSync5(path)) {
|
|
10061
12216
|
throw new DocmetaError(
|
|
10062
12217
|
`"${label}" appeared on disk since the plan; re-run the query.`
|
|
10063
12218
|
);
|
|
@@ -10076,7 +12231,7 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
|
|
|
10076
12231
|
}
|
|
10077
12232
|
const entry = byLabel.get(label);
|
|
10078
12233
|
if (!entry) throw new DocmetaError(`No loaded entry for "${label}".`);
|
|
10079
|
-
const content = await
|
|
12234
|
+
const content = await readFile10(path, "utf8");
|
|
10080
12235
|
const members = ctx.memberships(label);
|
|
10081
12236
|
const current = mergeExternalMetadata(
|
|
10082
12237
|
label,
|
|
@@ -10144,7 +12299,7 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
|
|
|
10144
12299
|
for (const w of schemaWrites) {
|
|
10145
12300
|
const display = displayPath(ctx, w.path);
|
|
10146
12301
|
if (w.expected === void 0) {
|
|
10147
|
-
if (
|
|
12302
|
+
if (existsSync5(w.path)) {
|
|
10148
12303
|
throw new DocmetaError(
|
|
10149
12304
|
`"${display}" appeared on disk since the plan; re-run the query.`
|
|
10150
12305
|
);
|
|
@@ -10153,7 +12308,7 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
|
|
|
10153
12308
|
}
|
|
10154
12309
|
let now;
|
|
10155
12310
|
try {
|
|
10156
|
-
now = await
|
|
12311
|
+
now = await readFile10(w.path, "utf8");
|
|
10157
12312
|
} catch {
|
|
10158
12313
|
now = void 0;
|
|
10159
12314
|
}
|
|
@@ -10164,10 +12319,10 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
|
|
|
10164
12319
|
}
|
|
10165
12320
|
}
|
|
10166
12321
|
const dirs = /* @__PURE__ */ new Set();
|
|
10167
|
-
for (const w of schemaWrites) dirs.add(
|
|
10168
|
-
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));
|
|
10169
12324
|
for (const p of pendingWrites) {
|
|
10170
|
-
if (p.ensureDir) dirs.add(
|
|
12325
|
+
if (p.ensureDir) dirs.add(dirname9(p.path));
|
|
10171
12326
|
}
|
|
10172
12327
|
for (const d of dirs) await mkdir3(d, { recursive: true });
|
|
10173
12328
|
const writeSchema = async () => {
|
|
@@ -10282,8 +12437,8 @@ function skipExpression(sql, from) {
|
|
|
10282
12437
|
}
|
|
10283
12438
|
|
|
10284
12439
|
// src/meta/commands/fill.ts
|
|
10285
|
-
import { readFile as
|
|
10286
|
-
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";
|
|
10287
12442
|
import {
|
|
10288
12443
|
completeValidatedJSON,
|
|
10289
12444
|
makeProvider,
|
|
@@ -10485,6 +12640,7 @@ async function runFill(opts) {
|
|
|
10485
12640
|
);
|
|
10486
12641
|
const dryRun = Boolean(opts.dryRun);
|
|
10487
12642
|
const only = opts.fields != null ? new Set(opts.fields) : void 0;
|
|
12643
|
+
const managed = new Set(config?.derive?.fields ?? []);
|
|
10488
12644
|
const requestedProvider = opts.provider ?? config?.fill?.provider ?? DEFAULT_PROVIDER;
|
|
10489
12645
|
const providerName = opts.local === true && requestedProvider === "auto" ? "llama-cpp" : requestedProvider;
|
|
10490
12646
|
const model = opts.model ?? config?.fill?.model;
|
|
@@ -10550,7 +12706,7 @@ async function runFill(opts) {
|
|
|
10550
12706
|
}
|
|
10551
12707
|
return provider;
|
|
10552
12708
|
};
|
|
10553
|
-
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");
|
|
10554
12710
|
const pricing = pricingFor(identity.model);
|
|
10555
12711
|
const validator = new Validator(schemaOptions);
|
|
10556
12712
|
let costUsd = 0;
|
|
@@ -10634,12 +12790,21 @@ async function runFill(opts) {
|
|
|
10634
12790
|
}
|
|
10635
12791
|
return errorResult(label, extractor.name, err.message);
|
|
10636
12792
|
}
|
|
10637
|
-
const
|
|
12793
|
+
const proposed = collectCandidates(
|
|
10638
12794
|
schemas,
|
|
10639
12795
|
extracted.data,
|
|
10640
12796
|
existingErrors,
|
|
10641
12797
|
only
|
|
10642
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));
|
|
10643
12808
|
const ownedCandidates = externalMetadata ? candidates.flatMap((c) => {
|
|
10644
12809
|
const owner = externalMetadata.owners.get(c.key)?.[0];
|
|
10645
12810
|
return owner === void 0 ? [] : [{ key: c.key, file: owner.file }];
|
|
@@ -10658,7 +12823,7 @@ async function runFill(opts) {
|
|
|
10658
12823
|
file: label,
|
|
10659
12824
|
format: extractor.name,
|
|
10660
12825
|
schemas: schemaSet,
|
|
10661
|
-
fields:
|
|
12826
|
+
fields: managedSkips,
|
|
10662
12827
|
changed: false,
|
|
10663
12828
|
...opts.includeContent ? { content } : {}
|
|
10664
12829
|
};
|
|
@@ -10727,9 +12892,9 @@ async function runFill(opts) {
|
|
|
10727
12892
|
break;
|
|
10728
12893
|
}
|
|
10729
12894
|
inFlight++;
|
|
10730
|
-
let
|
|
12895
|
+
let run2;
|
|
10731
12896
|
try {
|
|
10732
|
-
|
|
12897
|
+
run2 = await completeValidatedJSON({
|
|
10733
12898
|
provider: getProvider(),
|
|
10734
12899
|
system: FILL_SYSTEM_PROMPT,
|
|
10735
12900
|
user: buildUserPrompt({
|
|
@@ -10742,22 +12907,22 @@ async function runFill(opts) {
|
|
|
10742
12907
|
schema: envelope,
|
|
10743
12908
|
validate
|
|
10744
12909
|
});
|
|
10745
|
-
costUsd += costOfUsage(
|
|
12910
|
+
costUsd += costOfUsage(run2.usage, pricing);
|
|
10746
12911
|
turnsUsed++;
|
|
10747
12912
|
} finally {
|
|
10748
12913
|
inFlight--;
|
|
10749
12914
|
}
|
|
10750
|
-
if (
|
|
12915
|
+
if (run2.error != null && looksLikeOverflow(run2.error)) {
|
|
10751
12916
|
overflowed = true;
|
|
10752
|
-
failure =
|
|
12917
|
+
failure = run2.error;
|
|
10753
12918
|
break;
|
|
10754
12919
|
}
|
|
10755
|
-
if (
|
|
10756
|
-
failure =
|
|
12920
|
+
if (run2.error != null || run2.result == null) {
|
|
12921
|
+
failure = run2.error ?? "The model returned no proposal.";
|
|
10757
12922
|
break;
|
|
10758
12923
|
}
|
|
10759
|
-
usageTotal = addUsage(usageTotal,
|
|
10760
|
-
collected.push(
|
|
12924
|
+
usageTotal = addUsage(usageTotal, run2.usage);
|
|
12925
|
+
collected.push(run2.result);
|
|
10761
12926
|
}
|
|
10762
12927
|
if (overflowed && attempt < 2) {
|
|
10763
12928
|
usageTotal = void 0;
|
|
@@ -10790,7 +12955,7 @@ async function runFill(opts) {
|
|
|
10790
12955
|
proposals = mergeProposals(sets);
|
|
10791
12956
|
cache?.set(cacheKey, { proposals, ...usageTotal ? { usage: usageTotal } : {} });
|
|
10792
12957
|
}
|
|
10793
|
-
const fields = gate(candidates, proposals, threshold);
|
|
12958
|
+
const fields = [...managedSkips, ...gate(candidates, proposals, threshold)];
|
|
10794
12959
|
const accepted = fields.filter((f) => f.written);
|
|
10795
12960
|
if (accepted.length > 0) {
|
|
10796
12961
|
const merged2 = { ...extracted.data, ...patchOf(accepted) };
|
|
@@ -10839,7 +13004,7 @@ async function runFill(opts) {
|
|
|
10839
13004
|
}
|
|
10840
13005
|
const changed = next !== content;
|
|
10841
13006
|
if (changed && !dryRun && label !== "<stdin>") {
|
|
10842
|
-
await writeFileAtomic(
|
|
13007
|
+
await writeFileAtomic(resolve15(base, label), next);
|
|
10843
13008
|
}
|
|
10844
13009
|
return {
|
|
10845
13010
|
file: label,
|
|
@@ -10861,8 +13026,8 @@ async function runFill(opts) {
|
|
|
10861
13026
|
);
|
|
10862
13027
|
}
|
|
10863
13028
|
const fileResults = await mapConcurrent(files, concurrency, async (file) => {
|
|
10864
|
-
const content = await
|
|
10865
|
-
return processOne(file, content,
|
|
13029
|
+
const content = await readFile11(resolve15(base, file), "utf8");
|
|
13030
|
+
return processOne(file, content, extname7(file));
|
|
10866
13031
|
});
|
|
10867
13032
|
results.push(...fileResults);
|
|
10868
13033
|
return {
|
|
@@ -11045,8 +13210,8 @@ function plannedFor(schemas) {
|
|
|
11045
13210
|
}
|
|
11046
13211
|
function withRenamedRefs(node, renames) {
|
|
11047
13212
|
if (Array.isArray(node)) {
|
|
11048
|
-
const
|
|
11049
|
-
return
|
|
13213
|
+
const items2 = node;
|
|
13214
|
+
return items2.map((item) => withRenamedRefs(item, renames));
|
|
11050
13215
|
}
|
|
11051
13216
|
if (typeof node !== "object" || node === null) return node;
|
|
11052
13217
|
const out = {};
|
|
@@ -11160,16 +13325,16 @@ function summarize(results, costUsd, cached) {
|
|
|
11160
13325
|
cached
|
|
11161
13326
|
};
|
|
11162
13327
|
}
|
|
11163
|
-
async function mapConcurrent(
|
|
11164
|
-
const out = new Array(
|
|
13328
|
+
async function mapConcurrent(items2, limit, fn) {
|
|
13329
|
+
const out = new Array(items2.length);
|
|
11165
13330
|
let next = 0;
|
|
11166
13331
|
const workers = Array.from(
|
|
11167
|
-
{ length: Math.max(1, Math.min(limit,
|
|
13332
|
+
{ length: Math.max(1, Math.min(limit, items2.length)) },
|
|
11168
13333
|
async () => {
|
|
11169
13334
|
for (; ; ) {
|
|
11170
13335
|
const i = next++;
|
|
11171
|
-
const item =
|
|
11172
|
-
if (i >=
|
|
13336
|
+
const item = items2[i];
|
|
13337
|
+
if (i >= items2.length || item === void 0) return;
|
|
11173
13338
|
out[i] = await fn(item);
|
|
11174
13339
|
}
|
|
11175
13340
|
}
|
|
@@ -11178,6 +13343,252 @@ async function mapConcurrent(items, limit, fn) {
|
|
|
11178
13343
|
return out;
|
|
11179
13344
|
}
|
|
11180
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
|
+
|
|
11181
13592
|
// src/meta/reporters/sarif.ts
|
|
11182
13593
|
import { isAbsolute as isAbsolute6 } from "node:path";
|
|
11183
13594
|
|
|
@@ -11187,11 +13598,13 @@ var PARSE_ERROR_RULE = "manni/parse-error";
|
|
|
11187
13598
|
var SCHEMA_ERROR_RULE = "manni/schema-error";
|
|
11188
13599
|
var EXTERNAL_OWNED_RULE = "external:owned/external";
|
|
11189
13600
|
var EXTERNAL_DUPLICATE_RULE = "external:duplicate/external";
|
|
13601
|
+
var DERIVED_STALE_RULE = "derived:stale/derived";
|
|
11190
13602
|
var RESERVED_RULES = {
|
|
11191
13603
|
[PARSE_ERROR_RULE]: "The document's metadata block could not be parsed.",
|
|
11192
13604
|
[SCHEMA_ERROR_RULE]: "No schema set could be resolved for the document.",
|
|
11193
13605
|
[EXTERNAL_OWNED_RULE]: "The document carries a key a manifest owns; the manifest is the only place that key may be set.",
|
|
11194
|
-
[EXTERNAL_DUPLICATE_RULE]: "Two documents carry the same value of a manifest's join field, so one manifest entry matched both."
|
|
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."
|
|
11195
13608
|
};
|
|
11196
13609
|
function ruleIdFor(e, frame) {
|
|
11197
13610
|
if (e.schema === SYNTHETIC_SCHEMA) {
|
|
@@ -11518,14 +13931,14 @@ var FILL_FORMAT_LIST = formatList(FILL_FORMATS);
|
|
|
11518
13931
|
function isFillFormat(value) {
|
|
11519
13932
|
return FILL_FORMATS.includes(value);
|
|
11520
13933
|
}
|
|
11521
|
-
function renderFillJson(
|
|
11522
|
-
return JSON.stringify(
|
|
13934
|
+
function renderFillJson(run2) {
|
|
13935
|
+
return JSON.stringify(run2, null, 2);
|
|
11523
13936
|
}
|
|
11524
13937
|
var score = (n) => n.toFixed(2);
|
|
11525
|
-
function renderFillPretty(
|
|
13938
|
+
function renderFillPretty(run2, opts = {}) {
|
|
11526
13939
|
const c = palette(opts.color ?? false);
|
|
11527
13940
|
const lines = [];
|
|
11528
|
-
for (const result of
|
|
13941
|
+
for (const result of run2.results) {
|
|
11529
13942
|
if (result.error != null) {
|
|
11530
13943
|
lines.push(`${c.red("\u2717")} ${result.file}`);
|
|
11531
13944
|
lines.push(` ${result.error}`);
|
|
@@ -11549,7 +13962,7 @@ function renderFillPretty(run, opts = {}) {
|
|
|
11549
13962
|
if (low.length > 0) {
|
|
11550
13963
|
lines.push(
|
|
11551
13964
|
c.dim(
|
|
11552
|
-
` below ${
|
|
13965
|
+
` below ${run2.threshold}: ${low.map((f) => `${f.field} ${score(f.confidence)}`).join(", ")}`
|
|
11553
13966
|
)
|
|
11554
13967
|
);
|
|
11555
13968
|
}
|
|
@@ -11557,14 +13970,14 @@ function renderFillPretty(run, opts = {}) {
|
|
|
11557
13970
|
lines.push(c.dim(` ${f.skipReason ?? "skipped"}: ${f.field}`));
|
|
11558
13971
|
}
|
|
11559
13972
|
}
|
|
11560
|
-
const { summary } =
|
|
11561
|
-
const verb =
|
|
13973
|
+
const { summary } = run2;
|
|
13974
|
+
const verb = run2.dryRun ? "would be written" : "written";
|
|
11562
13975
|
const parts = [
|
|
11563
13976
|
// Which provider ran decides what the proposals and the cost mean, and
|
|
11564
13977
|
// under `auto` it is not something the user chose — so say it rather than
|
|
11565
13978
|
// making them pass -f json to find out.
|
|
11566
|
-
`${
|
|
11567
|
-
`Threshold ${
|
|
13979
|
+
`${run2.provider}/${run2.model}`,
|
|
13980
|
+
`Threshold ${run2.threshold}`,
|
|
11568
13981
|
`${summary.files} file${summary.files === 1 ? "" : "s"}`,
|
|
11569
13982
|
`${summary.written} field${summary.written === 1 ? "" : "s"} ${verb}`,
|
|
11570
13983
|
`${summary.skipped} skipped`
|
|
@@ -11582,7 +13995,7 @@ function renderFillPretty(run, opts = {}) {
|
|
|
11582
13995
|
)
|
|
11583
13996
|
);
|
|
11584
13997
|
}
|
|
11585
|
-
if (
|
|
13998
|
+
if (run2.turnsSpent) {
|
|
11586
13999
|
lines.push(
|
|
11587
14000
|
c.yellow("--max-turns reached; some files were not processed.")
|
|
11588
14001
|
);
|
|
@@ -11593,9 +14006,9 @@ function formatValue(value) {
|
|
|
11593
14006
|
if (typeof value === "string") return value;
|
|
11594
14007
|
return toJsonText(value) ?? String(value);
|
|
11595
14008
|
}
|
|
11596
|
-
function renderFillGithub(
|
|
14009
|
+
function renderFillGithub(run2) {
|
|
11597
14010
|
const lines = [];
|
|
11598
|
-
for (const result of
|
|
14011
|
+
for (const result of run2.results) {
|
|
11599
14012
|
const file = escapeWorkflowCommandProperty(result.file);
|
|
11600
14013
|
if (result.error != null) {
|
|
11601
14014
|
lines.push(
|
|
@@ -11605,7 +14018,7 @@ function renderFillGithub(run) {
|
|
|
11605
14018
|
}
|
|
11606
14019
|
for (const f of result.fields) {
|
|
11607
14020
|
if (!f.required || f.written) continue;
|
|
11608
|
-
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";
|
|
11609
14022
|
const msg = escapeWorkflowCommandMessage(
|
|
11610
14023
|
`[fill] ${f.field} is required and was not filled (${why})`
|
|
11611
14024
|
);
|
|
@@ -11614,14 +14027,14 @@ function renderFillGithub(run) {
|
|
|
11614
14027
|
}
|
|
11615
14028
|
return lines.join("\n");
|
|
11616
14029
|
}
|
|
11617
|
-
function renderFill(format,
|
|
14030
|
+
function renderFill(format, run2, opts = {}) {
|
|
11618
14031
|
switch (format) {
|
|
11619
14032
|
case "json":
|
|
11620
|
-
return renderFillJson(
|
|
14033
|
+
return renderFillJson(run2);
|
|
11621
14034
|
case "github":
|
|
11622
|
-
return renderFillGithub(
|
|
14035
|
+
return renderFillGithub(run2);
|
|
11623
14036
|
case "pretty":
|
|
11624
|
-
return renderFillPretty(
|
|
14037
|
+
return renderFillPretty(run2, opts);
|
|
11625
14038
|
default: {
|
|
11626
14039
|
const unreachable = format;
|
|
11627
14040
|
throw new DocmetaError(
|
|
@@ -11631,12 +14044,137 @@ function renderFill(format, run, opts = {}) {
|
|
|
11631
14044
|
}
|
|
11632
14045
|
}
|
|
11633
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
|
+
|
|
11634
14155
|
// src/meta/reporters/get.ts
|
|
11635
14156
|
function stringifyValue(value) {
|
|
11636
14157
|
if (value === void 0) return "(unset)";
|
|
11637
14158
|
if (typeof value === "string") return value;
|
|
11638
14159
|
return JSON.stringify(value);
|
|
11639
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
|
+
}
|
|
11640
14178
|
function renderGet(results, fields, opts = {}) {
|
|
11641
14179
|
const c = palette(opts.color ?? false);
|
|
11642
14180
|
const lines = [];
|
|
@@ -11645,9 +14183,14 @@ function renderGet(results, fields, opts = {}) {
|
|
|
11645
14183
|
lines.push(`${c.dim(`${r.file}:`)} ${c.red(`(parse) ${r.error}`)}`);
|
|
11646
14184
|
continue;
|
|
11647
14185
|
}
|
|
11648
|
-
|
|
14186
|
+
const effective = r.resolved ?? r.values;
|
|
14187
|
+
if (opts.quiet && fields.every((f) => effective[f] === void 0)) continue;
|
|
11649
14188
|
for (const f of fields) {
|
|
11650
|
-
|
|
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
|
+
);
|
|
11651
14194
|
}
|
|
11652
14195
|
}
|
|
11653
14196
|
return lines.join("\n");
|
|
@@ -11658,21 +14201,21 @@ function cell(value) {
|
|
|
11658
14201
|
if (value === null) return "(null)";
|
|
11659
14202
|
return typeof value === "bigint" ? String(value) : stringifyValue(value);
|
|
11660
14203
|
}
|
|
11661
|
-
function renderQuery(
|
|
14204
|
+
function renderQuery(run2, opts = {}) {
|
|
11662
14205
|
const c = palette(opts.color ?? false);
|
|
11663
|
-
if (
|
|
14206
|
+
if (run2.changes) return renderChanges(run2.changes, c, opts);
|
|
11664
14207
|
const lines = [];
|
|
11665
|
-
if (
|
|
11666
|
-
const widths =
|
|
11667
|
-
(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))
|
|
11668
14211
|
);
|
|
11669
|
-
const pad = (
|
|
11670
|
-
lines.push(
|
|
11671
|
-
for (const row of
|
|
11672
|
-
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(" "));
|
|
11673
14216
|
}
|
|
11674
14217
|
}
|
|
11675
|
-
const n =
|
|
14218
|
+
const n = run2.rows.length;
|
|
11676
14219
|
const count = `${n} row${n === 1 ? "" : "s"}`;
|
|
11677
14220
|
lines.push(
|
|
11678
14221
|
opts.check ? n > 0 ? c.red(`\u2717 ${count} \u2014 check failed`) : c.green(`\u2713 ${count}`) : c.dim(count)
|
|
@@ -11680,15 +14223,15 @@ function renderQuery(run, opts = {}) {
|
|
|
11680
14223
|
return lines.join("\n");
|
|
11681
14224
|
}
|
|
11682
14225
|
var CSV_NEEDS_QUOTING = /[",\n\r]/;
|
|
11683
|
-
function renderQueryCsv(
|
|
14226
|
+
function renderQueryCsv(run2) {
|
|
11684
14227
|
const field = (value) => {
|
|
11685
14228
|
if (value === null || value === void 0) return "";
|
|
11686
|
-
const
|
|
11687
|
-
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;
|
|
11688
14231
|
};
|
|
11689
14232
|
return [
|
|
11690
|
-
|
|
11691
|
-
...
|
|
14233
|
+
run2.columns.map(field).join(","),
|
|
14234
|
+
...run2.rows.map((row) => run2.columns.map((c) => field(row[c])).join(","))
|
|
11692
14235
|
].join("\n");
|
|
11693
14236
|
}
|
|
11694
14237
|
function renderChanges(changes, c, opts) {
|
|
@@ -11809,12 +14352,46 @@ export {
|
|
|
11809
14352
|
orphanEntries,
|
|
11810
14353
|
orphanJoins,
|
|
11811
14354
|
orphanError,
|
|
14355
|
+
DERIVABLE_FIELDS,
|
|
14356
|
+
isBuiltinField,
|
|
14357
|
+
DERIVE_SOURCES,
|
|
14358
|
+
isDeriveSource,
|
|
14359
|
+
DERIVED_STALE_SCHEMA,
|
|
14360
|
+
DERIVED_KEYWORD,
|
|
14361
|
+
compareDerived,
|
|
14362
|
+
staleFindings,
|
|
11812
14363
|
parseConfig,
|
|
11813
14364
|
loadConfig,
|
|
11814
14365
|
resolveRunConfig,
|
|
11815
14366
|
isParamName,
|
|
11816
14367
|
memberOf,
|
|
11817
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,
|
|
11818
14395
|
rowsToFindings,
|
|
11819
14396
|
runValidate,
|
|
11820
14397
|
runGet,
|
|
@@ -11825,6 +14402,7 @@ export {
|
|
|
11825
14402
|
runInferSchema,
|
|
11826
14403
|
runQuery,
|
|
11827
14404
|
runFill,
|
|
14405
|
+
runDerive,
|
|
11828
14406
|
renderSarif,
|
|
11829
14407
|
renderJunit,
|
|
11830
14408
|
REPORT_FORMATS,
|
|
@@ -11845,9 +14423,13 @@ export {
|
|
|
11845
14423
|
isFillFormat,
|
|
11846
14424
|
renderFillGithub,
|
|
11847
14425
|
renderFill,
|
|
14426
|
+
DERIVE_FORMATS,
|
|
14427
|
+
DERIVE_FORMAT_LIST,
|
|
14428
|
+
isDeriveFormat,
|
|
14429
|
+
renderDerive,
|
|
11848
14430
|
stringifyValue,
|
|
11849
14431
|
renderGet,
|
|
11850
14432
|
renderQuery,
|
|
11851
14433
|
renderQueryCsv
|
|
11852
14434
|
};
|
|
11853
|
-
//# sourceMappingURL=chunk-
|
|
14435
|
+
//# sourceMappingURL=chunk-7SE7XDCL.js.map
|