@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.
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@hawkeyexl/manni",
6
- version: "1.0.1",
6
+ version: "1.1.0",
7
7
  description: "One command line for documentation that is meant to be checked. `manni meta` validates the presence and format of document metadata (Markdown frontmatter and more) against JSON Schema, built for CI. Formerly docmeta.",
8
8
  type: "module",
9
9
  bin: {
@@ -208,6 +208,11 @@ function parseCollections(raw, source, toError) {
208
208
  `${source}: ${where}.name "${name2}" collides with the docs table every query reads. Pick another name, such as "site" or "pages".`
209
209
  );
210
210
  }
211
+ if (folded === "derived" || folded === "_derived_rows" || folded === "resolved") {
212
+ throw toError(
213
+ `${source}: ${where}.name "${name2}" collides with the derived table a query builds beside docs (the derived view, its _derived_rows table, and the resolved view over both). Pick another name.`
214
+ );
215
+ }
211
216
  if (folded.startsWith("sqlite_")) {
212
217
  throw toError(
213
218
  `${source}: ${where}.name "${name2}" starts with "sqlite_", which SQLite reserves for its own objects. Pick another name.`
@@ -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((resolve14) => {
441
- setTimeout(resolve14, ms);
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(text) {
636
- return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
640
+ function stripBom(text2) {
641
+ return text2.charCodeAt(0) === 65279 ? text2.slice(1) : text2;
637
642
  }
638
- function detectJsonIndent(text) {
639
- return /^\{\s*\n([ \t]+)/.exec(text)?.[1] ?? " ";
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((resolve14) => {
3036
- setTimeout(resolve14, ms);
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(text, index) {
3055
+ function lineColumn(text2, index) {
3046
3056
  let line = 1;
3047
3057
  let column = 1;
3048
- const stop = Math.min(index, text.length);
3058
+ const stop = Math.min(index, text2.length);
3049
3059
  for (let i = 0; i < stop; i++) {
3050
- if (text.charCodeAt(i) === 10) {
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, text) {
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 : text.indexOf(quoted);
3064
- return at < 0 ? "unexpected token" : `unexpected token at ${lineColumn(text, at)}`;
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 text = stripBom(bytes.toString("utf8"));
3288
+ const text2 = stripBom(bytes.toString("utf8"));
3279
3289
  try {
3280
- return JSON.parse(text);
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), text)}`
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(text) {
4007
- if (text.trim() === "") return {};
4008
- const parsed2 = JSON.parse(text);
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, text) {
4026
+ function parseBlock(flavor, text2) {
4017
4027
  switch (flavor) {
4018
4028
  case "json":
4019
- return parseJsonBlockText(text);
4029
+ return parseJsonBlockText(text2);
4020
4030
  case "toml":
4021
- return text.trim() === "" ? {} : parseToml2(text);
4031
+ return text2.trim() === "" ? {} : parseToml2(text2);
4022
4032
  case "yaml": {
4023
- const doc = parseDocument2(text);
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 text = lines[i + 1] ?? "";
4173
+ const text2 = lines[i + 1] ?? "";
4164
4174
  const under2 = adornment(lines[i + 2] ?? "");
4165
- if (text.trim() !== "" && !FIELD.test(text) && under2 != null && under2.char === over.char && under2.length === over.length && over.length >= text.trim().length) {
4166
- return { title: text.trim(), line: i + 2, next: i + 3 };
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 text = "";
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) text += n.nodeValue ?? "";
4545
+ if (n.nodeType === 3 || n.nodeType === 4) text2 += n.nodeValue ?? "";
4536
4546
  }
4537
- if (!allowEmpty && text.trim() === "") return void 0;
4538
- return text.trim();
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 text = elementText(child);
4576
- if (text === void 0) continue;
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(text));
4591
+ group.values.push(typeValue3(text2));
4582
4592
  } else {
4583
- out.set(key, { els: [child], values: [typeValue3(text)] });
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, items, emit, escape) {
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 items) {
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 text = stringifyYaml(value).replace(/\n$/, "");
5295
- if (text.includes("\n")) {
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 text;
5310
+ return text2;
5301
5311
  }
5302
5312
  function escapeAttr(value, quote) {
5303
5313
  const escaped = value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -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 text = "";
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)) text += node.value;
5384
+ if (defaultTreeAdapter.isTextNode(node)) text2 += node.value;
5375
5385
  }
5376
- if (!allowEmpty && text.trim() === "") return void 0;
5377
- return text.trim();
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 text = headText(node);
5386
- if (text === void 0) continue;
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(text);
5401
+ group.texts.push(text2);
5392
5402
  } else {
5393
- out.set(key, { els: [node], texts: [text] });
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 text = first && defaultTreeAdapter.isTextNode(first) ? first.value : "";
5443
- setKey("title", text, { kind: "title", el: node }, line, location?.startCol);
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 text = tags.map((t) => `${eol}${indent}${t}`).join("");
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 text = stringifyYaml2(value).replace(/\n$/, "");
5625
- if (text.includes("\n")) {
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 text;
5640
+ return text2;
5631
5641
  }
5632
5642
  function escapeAttr2(value, quote) {
5633
5643
  const escaped = value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -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 text;
6003
+ let text2;
5994
6004
  let file;
5995
6005
  if (classifyRef(manifest.file).kind === "url") {
5996
6006
  file = manifest.file;
5997
- text = await fetchExternalMetadata(manifest.file, {
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
- text = await readManifest(abs, file);
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 join7 = externalMetadataJoin(manifest);
6023
+ const join8 = externalMetadataJoin(manifest);
6014
6024
  let target;
6015
- if (join7 === PATH_JOIN) {
6025
+ if (join8 === PATH_JOIN) {
6016
6026
  target = byPath;
6017
6027
  } else {
6018
- const existing = byField.get(join7);
6028
+ const existing = byField.get(join8);
6019
6029
  if (existing) target = existing;
6020
6030
  else {
6021
6031
  target = /* @__PURE__ */ new Map();
6022
- byField.set(join7, target);
6032
+ byField.set(join8, target);
6023
6033
  }
6024
6034
  }
6025
6035
  parseManifest(
6026
6036
  manifest,
6027
6037
  collection,
6028
- text,
6038
+ text2,
6029
6039
  file,
6030
6040
  opts.configDir,
6031
- join7,
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, text, file, configDir, join7, target, entries) {
6058
+ function parseManifest(manifest, collection, text2, file, configDir, join8, target, entries) {
6049
6059
  const lc = new LineCounter2();
6050
- const doc = parseDocument3(text, { lineCounter: lc, uniqueKeys: false });
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 ${join7 === PATH_JOIN ? "path" : `"${join7}"`} to owned keys.`
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 = join7 === PATH_JOIN ? resolve5(configDir, spelled) : spelled;
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: join7,
6113
- ...join7 === PATH_JOIN ? { abs: indexKey } : {},
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 text;
6357
+ let text2;
6257
6358
  try {
6258
- text = await readFile4(path, "utf8");
6359
+ text2 = await readFile4(path, "utf8");
6259
6360
  } catch {
6260
6361
  return null;
6261
6362
  }
6262
- return { text, doc: parseMapping(text, source, opts) };
6363
+ return { text: text2, doc: parseMapping(text2, source, opts) };
6263
6364
  }
6264
- function parseMapping(text, source, opts) {
6365
+ function parseMapping(text2, source, opts) {
6265
6366
  let raw;
6266
6367
  try {
6267
- raw = parseYaml(text);
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 Set(["query"]);
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(text, source) {
6635
+ function parseConfig(text2, source) {
6513
6636
  let raw;
6514
6637
  try {
6515
- raw = parseYaml2(text);
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
- if (RESERVED_CHECK_NAMES.has(e.name)) {
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 \`manni meta query --check\` files its ad-hoc findings as check:${e.name}, and a configured check with the same name would share their identity. Pick another name.`
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 git = findGitRoot(cwd);
6819
- if (git !== null) return { dir: git, source: "git" };
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 tokens = [];
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
- tokens.push(token);
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 tokens;
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(text) {
7339
+ function isTrivia(text2) {
7064
7340
  let i = 0;
7065
- while (i < text.length) {
7066
- const ch = text[i];
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(text, i)) {
7072
- i = skipComment(text, i);
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 text = typeof warning === "string" ? warning : warning.message;
7085
- if (text.includes("SQLite is an experimental feature")) return;
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/commands/validate.ts
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 { readFile as readFile5 } from "node:fs/promises";
7369
- import { relative as relative7, resolve as resolve8 } from "node:path";
7370
- var DEFAULT_BASELINE_PATH = ".manni-baseline.json";
7371
- var LEGACY_BASELINE_PATH = ".docmeta-baseline.json";
7372
- var BASELINE_VERSION = 1;
7373
- var FINGERPRINT_RE = /^[0-9a-f]{16}$/;
7374
- function canonicalFilePath(file, ctx) {
7375
- if (!ctx) return file;
7376
- const from = ctx.runBase ?? ctx.base;
7377
- return relative7(ctx.base, resolve8(from, file)).replace(/\\/g, "/");
7378
- }
7379
- function canonicalSchemaRef(ref, ctx) {
7380
- if (!ctx) return ref;
7381
- if (classifyRef(ref).kind !== "file") return ref;
7382
- const abs = resolve8(ctx.cwd, ref);
7383
- return relative7(ctx.base, abs).replace(/\\/g, "/");
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 fingerprint(e, ctx) {
7386
- const parts = [
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 serializeBaseline(baseline) {
7395
- const entries = /* @__PURE__ */ Object.create(null);
7396
- for (const file of Object.keys(baseline.entries).sort()) {
7397
- entries[file] = [...baseline.entries[file] ?? []].sort();
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 `${JSON.stringify(
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
- function bad(source, detail) {
7411
- throw new DocmetaError(`Baseline "${source}": ${detail}`);
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 parseBaseline(text, source) {
7414
- let raw;
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
- raw = JSON.parse(stripBom(text));
7417
- } catch (err) {
7418
- bad(source, `invalid JSON: ${errorMessage(err)}`);
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
- const generatedWith = typeof obj.generatedWith === "string" ? obj.generatedWith : "";
7431
- const rawEntries = obj.entries;
7432
- if (typeof rawEntries !== "object" || rawEntries === null || Array.isArray(rawEntries)) {
7433
- bad(source, '"entries" must be an object mapping file paths to fingerprints.');
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 entries = /* @__PURE__ */ Object.create(null);
7436
- for (const [file, value] of Object.entries(
7437
- rawEntries
7438
- )) {
7439
- if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
7440
- bad(source, `entries["${file}"] must be a list of fingerprint strings.`);
7441
- }
7442
- const malformed = value.find((v) => !FINGERPRINT_RE.test(v));
7443
- if (malformed !== void 0) {
7444
- bad(
7445
- source,
7446
- `entries["${file}"] contains ${JSON.stringify(malformed)}, which is not a fingerprint (16 lowercase hex characters).`
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 { version: BASELINE_VERSION, generatedWith, entries };
7836
+ return { status: { available: true }, records };
7452
7837
  }
7453
- async function readBaseline(absPath, source) {
7454
- let text;
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
- text = await readFile5(absPath, "utf8");
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.code === "ENOENT") return null;
7459
- throw new DocmetaError(
7460
- `Baseline "${source}" could not be read: ${errorMessage(err)}`
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
- return parseBaseline(text, source);
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
- async function writeBaselineFile(absPath, baseline, source) {
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
- await writeFileAtomic(absPath, serializeBaseline(baseline));
7468
- } catch (err) {
7469
- throw new DocmetaError(
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 text = typeof value === "string" ? value : typeof value === "number" || typeof value === "bigint" ? String(value) : JSON.stringify(value);
7590
- return [`${c}=${text}`];
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
- createDocsTable(db, entries, corpusDataColumns(entries));
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 tokens;
9537
+ let tokens2;
7613
9538
  try {
7614
- tokens = collectNamedParameters(check.query);
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 (tokens.length > 0) {
9545
+ if (tokens2.length > 0) {
7621
9546
  throw new DocmetaError(
7622
- `check "${check.name}": the SQL references ${tokens.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.`
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: resolve9(cwd, label),
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: resolve9(base, configured), label: configured };
9598
+ return { absPath: resolve11(base, configured), label: configured };
7674
9599
  }
7675
- const current = resolve9(base, DEFAULT_BASELINE_PATH);
7676
- const legacy = resolve9(base, LEGACY_BASELINE_PATH);
7677
- if (!existsSync2(current) && existsSync2(legacy)) {
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 readFile6(resolve9(base, file), "utf8");
7889
- await processOne(file, content, extname2(file));
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 readFile7 } from "node:fs/promises";
8009
- import { resolve as resolve10, extname as extname3 } from "node:path";
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
- extracted = mergeExternalMetadata(
8080
- label,
8081
- extractor.extract(content, label, {
8082
- elements: resolveElements(label, config, members)
8083
- }),
8084
- externalMetadata,
8085
- members,
8086
- base
8087
- ).extracted;
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
- for (const f of opts.fields) values[f] = resolveField(extracted.data, f);
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("<stdin>", opts.stdinContent ?? "", forced.extensions[0] ?? "");
10112
+ readOne(STDIN_LABEL, opts.stdinContent ?? "", forced.extensions[0] ?? "");
8105
10113
  }
8106
10114
  for (const file of files) {
8107
- const content = await readFile7(resolve10(base, file), "utf8");
8108
- readOne(file, content, extname3(file));
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 existsSync3 } from "node:fs";
8144
- import { mkdir as mkdir2, readFile as readFile8 } from "node:fs/promises";
8145
- import { dirname as dirname5, extname as extname4, isAbsolute as isAbsolute4, join as join5, relative as relative8, resolve as resolve11, sep as sep4 } from "node:path";
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 relative8(from, to).split(sep4).join("/");
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, text = VENDOR_IGNORE_TEXT) {
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?.(text.unchecked(relDir || "."));
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(text.refusal(ignoredDir ? relDir : relFile));
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(text, source, section) {
10311
+ function reparseConfig(text2, source, section) {
8223
10312
  if (section === void 0) {
8224
- parseConfig(text, source);
10313
+ parseConfig(text2, source);
8225
10314
  return;
8226
10315
  }
8227
- const doc = parseYaml3(text);
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 ?? resolve11(cwd, DEFAULT_CONFIG_NAME);
10328
+ const configPath = loaded?.path ?? resolve13(cwd, DEFAULT_CONFIG_NAME);
8240
10329
  const configDir = loaded?.dir ?? cwd;
8241
- const absDir = resolve11(cwd, opts.dir ?? DEFAULT_VENDOR_DIR);
8242
- const absFile = join5(absDir, vendorFileName(url));
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 (existsSync3(absFile)) {
8260
- const current = await readFile8(absFile);
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 readFile8(configPath, "utf8") : null;
10359
+ const existing = loaded ? await readFile9(configPath, "utf8") : null;
8271
10360
  const section = loaded ? loaded.section : META_SECTION;
8272
- const text = renderConfig(existing, folded.entries, section);
8273
- reparseConfig(text, posixRelative(cwd, configPath) || DEFAULT_CONFIG_NAME, section);
8274
- await writeFileAtomic(configPath, text);
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 = resolve11(cwd, opts.out);
10510
+ absOut = resolve13(cwd, opts.out);
8422
10511
  await assertNotIgnored(
8423
10512
  absOut,
8424
- dirname5(absOut),
10513
+ dirname8(absOut),
8425
10514
  cwd,
8426
10515
  opts.onNotice,
8427
10516
  DRAFT_IGNORE_TEXT
8428
10517
  );
8429
- if (existsSync3(absOut)) {
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 readFile8(resolve11(base, file), "utf8");
8509
- scanOne(file, content, extname4(file));
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(dirname5(absOut), { recursive: true });
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 existsSync4 } from "node:fs";
8569
- import { mkdir as mkdir3, open, readFile as readFile9, rename as rename2, rm as rm2 } from "node:fs/promises";
8570
- import { dirname as dirname6, isAbsolute as isAbsolute5, resolve as resolve12, extname as extname5, sep as sep5 } from "node:path";
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 extracted = mergeExternalMetadata(
8676
- label,
8677
- extractor.extract(content, label, {
8678
- elements: resolveElements(label, config, members)
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 readFile9(resolve12(base, file), "utf8");
8702
- readOne(file, content, extname5(file));
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: resolve12(cwd, opts.db), display: opts.db };
8705
- const run = await runSql(sql, entries, {
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 { ...run, frame: { cwd, base: configDir ?? cwd, runBase: base } };
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(dirname6(target.resolved), { recursive: true });
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(text, from) {
11199
+ function skipWs(text2, from) {
9060
11200
  let i = from;
9061
- while (i < text.length && /\s/.test(text[i] ?? "")) i++;
11201
+ while (i < text2.length && /\s/.test(text2[i] ?? "")) i++;
9062
11202
  return i;
9063
11203
  }
9064
- function indexOfCheck(text, from) {
11204
+ function indexOfCheck(text2, from) {
9065
11205
  let i = from;
9066
- while (i < text.length) {
9067
- const ch = text[i];
9068
- if (startsStringOrIdent(text, i)) {
9069
- i = skipStringOrIdent(text, i);
9070
- } else if (startsComment(text, i)) {
9071
- i = skipComment(text, i);
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(text.slice(i));
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(text, open2) {
11223
+ function matchingParenEnd(text2, open2) {
9084
11224
  let depth = 0;
9085
11225
  let i = open2;
9086
- while (i < text.length) {
9087
- const ch = text[i];
9088
- if (startsStringOrIdent(text, i)) {
9089
- i = skipStringOrIdent(text, i);
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(text, i)) {
9093
- i = skipComment(text, i);
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 = (text) => {
11253
+ const count = (text2) => {
9114
11254
  let n = 0;
9115
- for (let i2 = indexOfCheck(text, 0); i2 !== -1; i2 = indexOfCheck(text, i2 + 5)) {
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, text) {
9229
- return reference.includes("\r\n") ? text.replace(/\n/g, "\r\n") : text;
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 = resolve12(p);
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(resolve12(ctx.cwd, ref)), pin);
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 = resolve12(ctx.cwd, ref);
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 text;
11422
+ let text2;
9283
11423
  try {
9284
- text = await readFile9(abs, "utf8");
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(text, "utf8")) !== pin.integrity) {
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(text));
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 = resolve12(ctx.configDir ?? ctx.cwd, "schemas", forkName);
11623
+ schemaAbs = resolve14(ctx.configDir ?? ctx.cwd, "schemas", forkName);
9484
11624
  assertSchemaWriteWithin(ctx, schemaAbs);
9485
- if (existsSync4(schemaAbs)) {
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
- dirname6(schemaAbs),
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 text = await readFile9(configPath, "utf8");
9640
- const doc = parseDocument5(stripBom(text));
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) ? resolve12(configDir, raw) : 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 : resolve12(configDir, 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(text, doc.toString({ lineWidth: 0 })),
9728
- expected: text
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(extname5(file))?.apply) {
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 (existsSync4(resolve12(ctx.base, file))) {
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 (extname5(to).toLowerCase() !== extname5(from).toLowerCase()) {
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 (existsSync4(resolve12(ctx.base, to))) {
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 = resolve12(base, p);
9954
- if (abs === resolve12(base)) refuse("it points at the corpus root, not a file");
9955
- if (!abs.startsWith(resolve12(base) + sep5)) refuse("it escapes the corpus");
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(resolve12(ctx.base, c.file));
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 = resolve12(ctx.base, label);
12192
+ const path = resolve14(ctx.base, label);
10038
12193
  if (ops.renamedTo !== void 0) {
10039
- if (!existsSync4(path)) {
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 = resolve12(ctx.base, ops.renamedTo);
10045
- if (existsSync4(toDisk)) {
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(extname5(label));
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 (existsSync4(path)) {
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 readFile9(path, "utf8");
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 (existsSync4(w.path)) {
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 readFile9(w.path, "utf8");
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(dirname6(w.path));
10168
- for (const r of pendingRenames) dirs.add(dirname6(r.to));
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(dirname6(p.path));
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 readFile10 } from "node:fs/promises";
10286
- import { resolve as resolve13, extname as extname6, join as join6 } from "node:path";
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(join6(cwd, CACHE_DIR), true, "manni");
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 candidates = collectCandidates(
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 run;
12895
+ let run2;
10731
12896
  try {
10732
- run = await completeValidatedJSON({
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(run.usage, pricing);
12910
+ costUsd += costOfUsage(run2.usage, pricing);
10746
12911
  turnsUsed++;
10747
12912
  } finally {
10748
12913
  inFlight--;
10749
12914
  }
10750
- if (run.error != null && looksLikeOverflow(run.error)) {
12915
+ if (run2.error != null && looksLikeOverflow(run2.error)) {
10751
12916
  overflowed = true;
10752
- failure = run.error;
12917
+ failure = run2.error;
10753
12918
  break;
10754
12919
  }
10755
- if (run.error != null || run.result == null) {
10756
- failure = run.error ?? "The model returned no proposal.";
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, run.usage);
10760
- collected.push(run.result);
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(resolve13(base, label), next);
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 readFile10(resolve13(base, file), "utf8");
10865
- return processOne(file, content, extname6(file));
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 items = node;
11049
- return items.map((item) => withRenamedRefs(item, renames));
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(items, limit, fn) {
11164
- const out = new Array(items.length);
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, items.length)) },
13332
+ { length: Math.max(1, Math.min(limit, items2.length)) },
11168
13333
  async () => {
11169
13334
  for (; ; ) {
11170
13335
  const i = next++;
11171
- const item = items[i];
11172
- if (i >= items.length || item === void 0) return;
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(run) {
11522
- return JSON.stringify(run, null, 2);
13934
+ function renderFillJson(run2) {
13935
+ return JSON.stringify(run2, null, 2);
11523
13936
  }
11524
13937
  var score = (n) => n.toFixed(2);
11525
- function renderFillPretty(run, opts = {}) {
13938
+ function renderFillPretty(run2, opts = {}) {
11526
13939
  const c = palette(opts.color ?? false);
11527
13940
  const lines = [];
11528
- for (const result of run.results) {
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 ${run.threshold}: ${low.map((f) => `${f.field} ${score(f.confidence)}`).join(", ")}`
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 } = run;
11561
- const verb = run.dryRun ? "would be written" : "written";
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
- `${run.provider}/${run.model}`,
11567
- `Threshold ${run.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 (run.turnsSpent) {
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(run) {
14009
+ function renderFillGithub(run2) {
11597
14010
  const lines = [];
11598
- for (const result of run.results) {
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 ${run.threshold} threshold` : f.skipReason ?? "skipped";
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, run, opts = {}) {
14030
+ function renderFill(format, run2, opts = {}) {
11618
14031
  switch (format) {
11619
14032
  case "json":
11620
- return renderFillJson(run);
14033
+ return renderFillJson(run2);
11621
14034
  case "github":
11622
- return renderFillGithub(run);
14035
+ return renderFillGithub(run2);
11623
14036
  case "pretty":
11624
- return renderFillPretty(run, opts);
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
- if (opts.quiet && fields.every((f) => r.values[f] === void 0)) continue;
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
- lines.push(`${c.dim(`${r.file}:`)} ${f}=${stringifyValue(r.values[f])}`);
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(run, opts = {}) {
14204
+ function renderQuery(run2, opts = {}) {
11662
14205
  const c = palette(opts.color ?? false);
11663
- if (run.changes) return renderChanges(run.changes, c, opts);
14206
+ if (run2.changes) return renderChanges(run2.changes, c, opts);
11664
14207
  const lines = [];
11665
- if (run.rows.length > 0) {
11666
- const widths = run.columns.map(
11667
- (col) => Math.max(col.length, ...run.rows.map((r) => cell(r[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 = (text, i) => i === run.columns.length - 1 ? text : text.padEnd(widths[i] ?? 0);
11670
- lines.push(run.columns.map((col, i) => c.bold(pad(col, i))).join(" "));
11671
- for (const row of run.rows) {
11672
- lines.push(run.columns.map((col, i) => pad(cell(row[col]), i)).join(" "));
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 = run.rows.length;
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(run) {
14226
+ function renderQueryCsv(run2) {
11684
14227
  const field = (value) => {
11685
14228
  if (value === null || value === void 0) return "";
11686
- const text = typeof value === "string" ? value : typeof value === "number" || typeof value === "bigint" ? String(value) : stringifyValue(value);
11687
- return CSV_NEEDS_QUOTING.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
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
- run.columns.map(field).join(","),
11691
- ...run.rows.map((row) => run.columns.map((c) => field(row[c])).join(","))
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-IRHWILCO.js.map
14435
+ //# sourceMappingURL=chunk-7SE7XDCL.js.map