@hawkeyexl/manni 1.0.1 → 1.1.1

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.1",
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.`
@@ -399,6 +404,7 @@ var ToolError = class extends Error {
399
404
  function errorMessage(err) {
400
405
  if (err instanceof Error) return err.message;
401
406
  if (typeof err === "string") return err;
407
+ if (typeof err === "number" || typeof err === "bigint") return String(err);
402
408
  if (typeof err === "object" && err !== null && "message" in err && typeof err.message === "string") {
403
409
  return err.message;
404
410
  }
@@ -437,8 +443,8 @@ var RENAME_ATTEMPTS = 3;
437
443
  function errorCode(err) {
438
444
  return typeof err === "object" && err !== null && "code" in err ? String(err.code) : void 0;
439
445
  }
440
- var wait = (ms) => new Promise((resolve14) => {
441
- setTimeout(resolve14, ms);
446
+ var wait = (ms) => new Promise((resolve17) => {
447
+ setTimeout(resolve17, ms);
442
448
  });
443
449
  async function writeFileAtomic(path, contents) {
444
450
  const tmp = join(
@@ -632,11 +638,11 @@ import { resolve as resolvePath } from "node:path";
632
638
  import { Buffer as Buffer2 } from "node:buffer";
633
639
 
634
640
  // src/meta/core/json-text.ts
635
- function stripBom(text) {
636
- return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
641
+ function stripBom(text2) {
642
+ return text2.charCodeAt(0) === 65279 ? text2.slice(1) : text2;
637
643
  }
638
- function detectJsonIndent(text) {
639
- return /^\{\s*\n([ \t]+)/.exec(text)?.[1] ?? " ";
644
+ function detectJsonIndent(text2) {
645
+ return /^\{\s*\n([ \t]+)/.exec(text2)?.[1] ?? " ";
640
646
  }
641
647
  function toJsonText(value) {
642
648
  return JSON.stringify(value);
@@ -2905,6 +2911,11 @@ function assertPublishableBuiltinId(id) {
2905
2911
  `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
2912
  );
2907
2913
  }
2914
+ if (first === "derived") {
2915
+ throw new Error(
2916
+ `Built-in id "${id}" is not publishable: the "derived" first segment is reserved for derived finding identities (derived:stale).`
2917
+ );
2918
+ }
2908
2919
  }
2909
2920
  for (const id of BUILTINS.keys()) assertPublishableBuiltinId(id);
2910
2921
  function listBuiltins() {
@@ -3032,8 +3043,8 @@ var SCHEMA_KEYS = [
3032
3043
  "multipleOf"
3033
3044
  ];
3034
3045
  var RETRY_DELAY_MS = 500;
3035
- var wait2 = (ms) => new Promise((resolve14) => {
3036
- setTimeout(resolve14, ms);
3046
+ var wait2 = (ms) => new Promise((resolve17) => {
3047
+ setTimeout(resolve17, ms);
3037
3048
  });
3038
3049
  var RetryableFetchError = class extends DocmetaError {
3039
3050
  };
@@ -3042,12 +3053,12 @@ function isAbort(err) {
3042
3053
  return name2 === "TimeoutError" || name2 === "AbortError";
3043
3054
  }
3044
3055
  var JSON_PARSE_EXCERPT = /^Unexpected token '[\s\S]', (?:\.\.\.)?"([\s\S]*)"(?:\.\.\.)? is not valid JSON$/;
3045
- function lineColumn(text, index) {
3056
+ function lineColumn(text2, index) {
3046
3057
  let line = 1;
3047
3058
  let column = 1;
3048
- const stop = Math.min(index, text.length);
3059
+ const stop = Math.min(index, text2.length);
3049
3060
  for (let i = 0; i < stop; i++) {
3050
- if (text.charCodeAt(i) === 10) {
3061
+ if (text2.charCodeAt(i) === 10) {
3051
3062
  line += 1;
3052
3063
  column = 1;
3053
3064
  } else {
@@ -3056,12 +3067,12 @@ function lineColumn(text, index) {
3056
3067
  }
3057
3068
  return `line ${line} column ${column}`;
3058
3069
  }
3059
- function withoutFileExcerpt(message, text) {
3070
+ function withoutFileExcerpt(message, text2) {
3060
3071
  const match = JSON_PARSE_EXCERPT.exec(message);
3061
3072
  if (!match) return message;
3062
3073
  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)}`;
3074
+ const at = quoted === void 0 || quoted === "" ? -1 : text2.indexOf(quoted);
3075
+ return at < 0 ? "unexpected token" : `unexpected token at ${lineColumn(text2, at)}`;
3065
3076
  }
3066
3077
  function excerpt(raw, limit = 200) {
3067
3078
  const flat = raw.replace(/\s+/g, " ").trim();
@@ -3275,12 +3286,12 @@ async function loadSchema(ref, options = {}) {
3275
3286
  if (pin?.integrity !== void 0) {
3276
3287
  assertIntegrity(ref, bytes, pin, pin.integrity);
3277
3288
  }
3278
- const text = stripBom(bytes.toString("utf8"));
3289
+ const text2 = stripBom(bytes.toString("utf8"));
3279
3290
  try {
3280
- return JSON.parse(text);
3291
+ return JSON.parse(text2);
3281
3292
  } catch (err) {
3282
3293
  throw new DocmetaError(
3283
- `Schema file "${ref}" is not valid JSON: ${withoutFileExcerpt(errorMessage(err), text)}`
3294
+ `Schema file "${ref}" is not valid JSON: ${withoutFileExcerpt(errorMessage(err), text2)}`
3284
3295
  );
3285
3296
  }
3286
3297
  }
@@ -4003,9 +4014,9 @@ function mergeToml(inner, patch, deletions = []) {
4003
4014
  if (insertAt >= lines.length) out.push(...appended);
4004
4015
  return out.join("\n");
4005
4016
  }
4006
- function parseJsonBlockText(text) {
4007
- if (text.trim() === "") return {};
4008
- const parsed2 = JSON.parse(text);
4017
+ function parseJsonBlockText(text2) {
4018
+ if (text2.trim() === "") return {};
4019
+ const parsed2 = JSON.parse(text2);
4009
4020
  if (parsed2 == null || typeof parsed2 !== "object" || Array.isArray(parsed2)) {
4010
4021
  throw new DocmetaError(
4011
4022
  "Cannot rewrite front matter: the root must be an object."
@@ -4013,14 +4024,14 @@ function parseJsonBlockText(text) {
4013
4024
  }
4014
4025
  return parsed2;
4015
4026
  }
4016
- function parseBlock(flavor, text) {
4027
+ function parseBlock(flavor, text2) {
4017
4028
  switch (flavor) {
4018
4029
  case "json":
4019
- return parseJsonBlockText(text);
4030
+ return parseJsonBlockText(text2);
4020
4031
  case "toml":
4021
- return text.trim() === "" ? {} : parseToml2(text);
4032
+ return text2.trim() === "" ? {} : parseToml2(text2);
4022
4033
  case "yaml": {
4023
- const doc = parseDocument2(text);
4034
+ const doc = parseDocument2(text2);
4024
4035
  if (doc.errors.length > 0) {
4025
4036
  throw new DocmetaError(
4026
4037
  `Cannot rewrite invalid YAML front matter: ${doc.errors[0]?.message ?? "parse error"}`
@@ -4160,10 +4171,10 @@ function parseTitle(lines) {
4160
4171
  if (FIELD.test(first)) return { next: i };
4161
4172
  const over = adornment(first);
4162
4173
  if (over) {
4163
- const text = lines[i + 1] ?? "";
4174
+ const text2 = lines[i + 1] ?? "";
4164
4175
  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 };
4176
+ if (text2.trim() !== "" && !FIELD.test(text2) && under2 != null && under2.char === over.char && under2.length === over.length && over.length >= text2.trim().length) {
4177
+ return { title: text2.trim(), line: i + 2, next: i + 3 };
4167
4178
  }
4168
4179
  return { next: i };
4169
4180
  }
@@ -4529,13 +4540,13 @@ function isMetadataAttribute(name2) {
4529
4540
  return name2 !== "xmlns" && !name2.startsWith("xmlns:");
4530
4541
  }
4531
4542
  function elementText(el, allowEmpty = false) {
4532
- let text = "";
4543
+ let text2 = "";
4533
4544
  for (let n = el.firstChild; n; n = n.nextSibling) {
4534
4545
  if (n.nodeType === 1) return void 0;
4535
- if (n.nodeType === 3 || n.nodeType === 4) text += n.nodeValue ?? "";
4546
+ if (n.nodeType === 3 || n.nodeType === 4) text2 += n.nodeValue ?? "";
4536
4547
  }
4537
- if (!allowEmpty && text.trim() === "") return void 0;
4538
- return text.trim();
4548
+ if (!allowEmpty && text2.trim() === "") return void 0;
4549
+ return text2.trim();
4539
4550
  }
4540
4551
  function ditaLiftedElements(container) {
4541
4552
  const out = /* @__PURE__ */ new Map();
@@ -4572,15 +4583,15 @@ function own(table, key) {
4572
4583
  function liftableChildren(root) {
4573
4584
  const out = /* @__PURE__ */ new Map();
4574
4585
  for (const child of childElements(root)) {
4575
- const text = elementText(child);
4576
- if (text === void 0) continue;
4586
+ const text2 = elementText(child);
4587
+ if (text2 === void 0) continue;
4577
4588
  const key = liftKey(root.nodeName, child.nodeName);
4578
4589
  const group = out.get(key);
4579
4590
  if (group) {
4580
4591
  group.els.push(child);
4581
- group.values.push(typeValue3(text));
4592
+ group.values.push(typeValue3(text2));
4582
4593
  } else {
4583
- out.set(key, { els: [child], values: [typeValue3(text)] });
4594
+ out.set(key, { els: [child], values: [typeValue3(text2)] });
4584
4595
  }
4585
4596
  }
4586
4597
  return out;
@@ -4930,7 +4941,7 @@ function ownLift(container, element) {
4930
4941
  const lifts = DITA_LIFTS[container];
4931
4942
  return lifts && Object.prototype.hasOwnProperty.call(lifts, element) ? lifts[element] : void 0;
4932
4943
  }
4933
- function newElementEdits(content, starts, root, shape, items, emit, escape) {
4944
+ function newElementEdits(content, starts, root, shape, items2, emit, escape) {
4934
4945
  const eol = content.includes(CRLF) ? CRLF : LF;
4935
4946
  const liftRootEl = liftRoot(root, shape);
4936
4947
  const tagsFor = (item) => {
@@ -4944,7 +4955,7 @@ function newElementEdits(content, starts, root, shape, items, emit, escape) {
4944
4955
  });
4945
4956
  };
4946
4957
  const byContainer = /* @__PURE__ */ new Map();
4947
- for (const item of items) {
4958
+ for (const item of items2) {
4948
4959
  const group = byContainer.get(item.container);
4949
4960
  if (group) group.push(item);
4950
4961
  else byContainer.set(item.container, [item]);
@@ -5291,13 +5302,13 @@ function assertNoElementCollision(key, root) {
5291
5302
  );
5292
5303
  }
5293
5304
  function emitScalar(key, value) {
5294
- const text = stringifyYaml(value).replace(/\n$/, "");
5295
- if (text.includes("\n")) {
5305
+ const text2 = stringifyYaml(value).replace(/\n$/, "");
5306
+ if (text2.includes("\n")) {
5296
5307
  throw new DocmetaError(
5297
5308
  `Refusing to write "${key}": the value needs more than one line, which an XML attribute cannot hold. Set it manually.`
5298
5309
  );
5299
5310
  }
5300
- return text;
5311
+ return text2;
5301
5312
  }
5302
5313
  function escapeAttr(value, quote) {
5303
5314
  const escaped = value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -5368,13 +5379,13 @@ var HEAD_NOT_METADATA = /* @__PURE__ */ new Set(["script", "style"]);
5368
5379
  var HEAD_SINGLETONS = /* @__PURE__ */ new Set(["title", "base"]);
5369
5380
  var elementName = (el) => el ? el.tagName.toLowerCase() : "";
5370
5381
  function headText(el, allowEmpty = false) {
5371
- let text = "";
5382
+ let text2 = "";
5372
5383
  for (const node of el.childNodes) {
5373
5384
  if (defaultTreeAdapter.isElementNode(node)) return void 0;
5374
- if (defaultTreeAdapter.isTextNode(node)) text += node.value;
5385
+ if (defaultTreeAdapter.isTextNode(node)) text2 += node.value;
5375
5386
  }
5376
- if (!allowEmpty && text.trim() === "") return void 0;
5377
- return text.trim();
5387
+ if (!allowEmpty && text2.trim() === "") return void 0;
5388
+ return text2.trim();
5378
5389
  }
5379
5390
  function liftableHeadChildren(head) {
5380
5391
  const out = /* @__PURE__ */ new Map();
@@ -5382,15 +5393,15 @@ function liftableHeadChildren(head) {
5382
5393
  if (!defaultTreeAdapter.isElementNode(node)) continue;
5383
5394
  const name2 = node.tagName.toLowerCase();
5384
5395
  if (HEAD_NOT_METADATA.has(name2)) continue;
5385
- const text = headText(node);
5386
- if (text === void 0) continue;
5396
+ const text2 = headText(node);
5397
+ if (text2 === void 0) continue;
5387
5398
  const key = liftKey("head", name2);
5388
5399
  const group = out.get(key);
5389
5400
  if (group) {
5390
5401
  group.els.push(node);
5391
- group.texts.push(text);
5402
+ group.texts.push(text2);
5392
5403
  } else {
5393
- out.set(key, { els: [node], texts: [text] });
5404
+ out.set(key, { els: [node], texts: [text2] });
5394
5405
  }
5395
5406
  }
5396
5407
  return out;
@@ -5439,8 +5450,8 @@ function readHtml(source, options) {
5439
5450
  } else if (node.tagName === "title") {
5440
5451
  if (data.title === void 0) {
5441
5452
  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);
5453
+ const text2 = first && defaultTreeAdapter.isTextNode(first) ? first.value : "";
5454
+ setKey("title", text2, { kind: "title", el: node }, line, location?.startCol);
5444
5455
  }
5445
5456
  } else if (node.tagName === "meta") {
5446
5457
  const key = attrValue(node, "name") ?? attrValue(node, "property");
@@ -5584,8 +5595,8 @@ function elementTextEdit2(el, key, emitted) {
5584
5595
  }
5585
5596
  function insertEdit2(content, head, at, tags) {
5586
5597
  const { eol, indent } = headStyle(content, head, at);
5587
- const text = tags.map((t) => `${eol}${indent}${t}`).join("");
5588
- return { start: at, end: at, text };
5598
+ const text2 = tags.map((t) => `${eol}${indent}${t}`).join("");
5599
+ return { start: at, end: at, text: text2 };
5589
5600
  }
5590
5601
  function headStyle(content, head, at) {
5591
5602
  for (const child of head.childNodes) {
@@ -5621,13 +5632,13 @@ function attrValueSpan2(content, start, end) {
5621
5632
  return { start: i, end, quote: DQ3, wrap: true };
5622
5633
  }
5623
5634
  function emitScalar2(key, value) {
5624
- const text = stringifyYaml2(value).replace(/\n$/, "");
5625
- if (text.includes("\n")) {
5635
+ const text2 = stringifyYaml2(value).replace(/\n$/, "");
5636
+ if (text2.includes("\n")) {
5626
5637
  throw new DocmetaError(
5627
5638
  `Refusing to write "${key}": the value needs more than one line, which an HTML attribute cannot hold. Set it manually.`
5628
5639
  );
5629
5640
  }
5630
- return text;
5641
+ return text2;
5631
5642
  }
5632
5643
  function escapeAttr2(value, quote) {
5633
5644
  const escaped = value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -5990,11 +6001,11 @@ async function loadExternalMetadata(collections, opts) {
5990
6001
  const byField = /* @__PURE__ */ new Map();
5991
6002
  const entries = [];
5992
6003
  for (const { collection, manifest } of configured) {
5993
- let text;
6004
+ let text2;
5994
6005
  let file;
5995
6006
  if (classifyRef(manifest.file).kind === "url") {
5996
6007
  file = manifest.file;
5997
- text = await fetchExternalMetadata(manifest.file, {
6008
+ text2 = await fetchExternalMetadata(manifest.file, {
5998
6009
  ...manifest.tokenEnv !== void 0 ? { tokenEnv: manifest.tokenEnv } : {},
5999
6010
  ...opts.offline !== void 0 ? { offline: opts.offline } : {},
6000
6011
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
@@ -6003,32 +6014,32 @@ async function loadExternalMetadata(collections, opts) {
6003
6014
  } else {
6004
6015
  const abs = isAbsolute2(manifest.file) ? manifest.file : resolve5(opts.configDir, manifest.file);
6005
6016
  file = reportedPath(abs, opts.base);
6006
- text = await readManifest(abs, file);
6017
+ text2 = await readManifest(abs, file);
6007
6018
  }
6008
6019
  for (const key of manifest.keys) {
6009
6020
  const list = owners.get(key);
6010
6021
  if (list) list.push({ collection, file });
6011
6022
  else owners.set(key, [{ collection, file }]);
6012
6023
  }
6013
- const join7 = externalMetadataJoin(manifest);
6024
+ const join8 = externalMetadataJoin(manifest);
6014
6025
  let target;
6015
- if (join7 === PATH_JOIN) {
6026
+ if (join8 === PATH_JOIN) {
6016
6027
  target = byPath;
6017
6028
  } else {
6018
- const existing = byField.get(join7);
6029
+ const existing = byField.get(join8);
6019
6030
  if (existing) target = existing;
6020
6031
  else {
6021
6032
  target = /* @__PURE__ */ new Map();
6022
- byField.set(join7, target);
6033
+ byField.set(join8, target);
6023
6034
  }
6024
6035
  }
6025
6036
  parseManifest(
6026
6037
  manifest,
6027
6038
  collection,
6028
- text,
6039
+ text2,
6029
6040
  file,
6030
6041
  opts.configDir,
6031
- join7,
6042
+ join8,
6032
6043
  target,
6033
6044
  entries
6034
6045
  );
@@ -6045,9 +6056,9 @@ async function readManifest(abs, file) {
6045
6056
  );
6046
6057
  }
6047
6058
  }
6048
- function parseManifest(manifest, collection, text, file, configDir, join7, target, entries) {
6059
+ function parseManifest(manifest, collection, text2, file, configDir, join8, target, entries) {
6049
6060
  const lc = new LineCounter2();
6050
- const doc = parseDocument3(text, { lineCounter: lc, uniqueKeys: false });
6061
+ const doc = parseDocument3(text2, { lineCounter: lc, uniqueKeys: false });
6051
6062
  const problem = doc.errors[0];
6052
6063
  if (problem) {
6053
6064
  throw new DocmetaError(
@@ -6058,7 +6069,7 @@ function parseManifest(manifest, collection, text, file, configDir, join7, targe
6058
6069
  if (root === null || isScalar2(root) && root.value === null) return;
6059
6070
  if (!isMap3(root)) {
6060
6071
  throw new DocmetaError(
6061
- `Manifest ${file}: the manifest must be a mapping from document ${join7 === PATH_JOIN ? "path" : `"${join7}"`} to owned keys.`
6072
+ `Manifest ${file}: the manifest must be a mapping from document ${join8 === PATH_JOIN ? "path" : `"${join8}"`} to owned keys.`
6062
6073
  );
6063
6074
  }
6064
6075
  const owned = new Set(manifest.keys);
@@ -6083,7 +6094,7 @@ function parseManifest(manifest, collection, text, file, configDir, join7, targe
6083
6094
  `Manifest ${where}: "${spelled}" must be a mapping of owned keys to values.`
6084
6095
  );
6085
6096
  }
6086
- const indexKey = join7 === PATH_JOIN ? resolve5(configDir, spelled) : spelled;
6097
+ const indexKey = join8 === PATH_JOIN ? resolve5(configDir, spelled) : spelled;
6087
6098
  const values = target.get(indexKey) ?? /* @__PURE__ */ new Map();
6088
6099
  for (const kv of pair.value.items) {
6089
6100
  const key = isScalar2(kv.key) ? String(kv.key.value) : String(kv.key);
@@ -6109,8 +6120,8 @@ function parseManifest(manifest, collection, text, file, configDir, join7, targe
6109
6120
  target.set(indexKey, values);
6110
6121
  entries.push({
6111
6122
  collection,
6112
- join: join7,
6113
- ...join7 === PATH_JOIN ? { abs: indexKey } : {},
6123
+ join: join8,
6124
+ ...join8 === PATH_JOIN ? { abs: indexKey } : {},
6114
6125
  spelled,
6115
6126
  file,
6116
6127
  ...entryLine === void 0 ? {} : { line: entryLine }
@@ -6225,6 +6236,97 @@ function orphanError(orphans) {
6225
6236
  );
6226
6237
  }
6227
6238
 
6239
+ // src/meta/core/derive/types.ts
6240
+ var DERIVABLE_FIELDS = [
6241
+ "created",
6242
+ "last-updated",
6243
+ "authors",
6244
+ "owner",
6245
+ "reviewed-by",
6246
+ "last-reviewed"
6247
+ ];
6248
+ function isBuiltinField(x) {
6249
+ return DERIVABLE_FIELDS.includes(x);
6250
+ }
6251
+ var DERIVE_SOURCES = ["git", "codeowners", "github", "gitlab", "command"];
6252
+ function isDeriveSource(x) {
6253
+ return DERIVE_SOURCES.includes(x);
6254
+ }
6255
+ function derivableFields(commands) {
6256
+ return [...DERIVABLE_FIELDS, ...Object.keys(commands ?? {}).sort()];
6257
+ }
6258
+ var DERIVED_STALE_SCHEMA = "derived:stale";
6259
+ var DERIVED_KEYWORD = "derived";
6260
+ function compareDerived(field, asserted, derived) {
6261
+ const base = {
6262
+ field,
6263
+ ...asserted !== void 0 ? { asserted } : {}
6264
+ };
6265
+ if (derived == null) {
6266
+ return { ...base, derived: null, status: "unknown", written: false };
6267
+ }
6268
+ const status = asserted === void 0 ? "unset" : deepEqual2(asserted, derived.value) ? "current" : "stale";
6269
+ return {
6270
+ ...base,
6271
+ derived: derived.value,
6272
+ source: derived.source,
6273
+ evidence: derived.evidence,
6274
+ status,
6275
+ written: false
6276
+ };
6277
+ }
6278
+ function staleFindings(fields, lineFor) {
6279
+ const findings = [];
6280
+ for (const f of fields) {
6281
+ if (f.status !== "stale" && f.status !== "unset") continue;
6282
+ const instancePath = `/${escapePointerSegment(f.field)}`;
6283
+ const line = lineFor(instancePath);
6284
+ const says = `${f.source ?? "a source"} says ${fmt(f.derived)}`;
6285
+ const trail = f.evidence === void 0 ? "" : ` (${f.evidence})`;
6286
+ const head = f.status === "stale" ? `${f.field} says ${fmt(f.asserted)}` : `${f.field} is not set`;
6287
+ findings.push({
6288
+ schema: DERIVED_STALE_SCHEMA,
6289
+ keyword: DERIVED_KEYWORD,
6290
+ subject: f.field,
6291
+ instancePath,
6292
+ message: `${head}; ${says}${trail} \u2014 run manni meta derive`,
6293
+ ...line !== void 0 ? { line } : {}
6294
+ });
6295
+ }
6296
+ return findings;
6297
+ }
6298
+ function fmt(value) {
6299
+ if (typeof value === "string") return value;
6300
+ return toJsonText(value) ?? String(value);
6301
+ }
6302
+ function deepEqual2(a, b) {
6303
+ if (isList(a) || isList(b)) {
6304
+ if (!isList(a) || !isList(b) || a.length !== b.length) return false;
6305
+ const unused = [...b];
6306
+ for (const x of a) {
6307
+ const i = unused.findIndex((y) => deepEqual2(x, y));
6308
+ if (i === -1) return false;
6309
+ unused.splice(i, 1);
6310
+ }
6311
+ return true;
6312
+ }
6313
+ if (isPlainObject(a) || isPlainObject(b)) {
6314
+ if (!isPlainObject(a) || !isPlainObject(b)) return false;
6315
+ const keys = Object.keys(a);
6316
+ if (keys.length !== Object.keys(b).length) return false;
6317
+ return keys.every(
6318
+ (k) => Object.hasOwn(b, k) && deepEqual2(a[k], b[k])
6319
+ );
6320
+ }
6321
+ return toJsonText(a) === toJsonText(b);
6322
+ }
6323
+ function isList(v) {
6324
+ return Array.isArray(v);
6325
+ }
6326
+ function isPlainObject(v) {
6327
+ return typeof v === "object" && v !== null && !isList(v) && !(v instanceof Date);
6328
+ }
6329
+
6228
6330
  // src/meta/core/config.ts
6229
6331
  import { resolve as resolve7 } from "node:path";
6230
6332
  import { parse as parseYaml2 } from "yaml";
@@ -6253,18 +6355,18 @@ var MOOSE_CONFIG_NAMES = [
6253
6355
  "moose.config.yml"
6254
6356
  ];
6255
6357
  async function readDocument(path, source, opts) {
6256
- let text;
6358
+ let text2;
6257
6359
  try {
6258
- text = await readFile4(path, "utf8");
6360
+ text2 = await readFile4(path, "utf8");
6259
6361
  } catch {
6260
6362
  return null;
6261
6363
  }
6262
- return { text, doc: parseMapping(text, source, opts) };
6364
+ return { text: text2, doc: parseMapping(text2, source, opts) };
6263
6365
  }
6264
- function parseMapping(text, source, opts) {
6366
+ function parseMapping(text2, source, opts) {
6265
6367
  let raw;
6266
6368
  try {
6267
- raw = parseYaml(text);
6369
+ raw = parseYaml(text2);
6268
6370
  } catch (err) {
6269
6371
  throw opts.toError(`${source}: invalid YAML: ${errorMessage(err)}`);
6270
6372
  }
@@ -6385,8 +6487,29 @@ function asElementPaths(value, where, source) {
6385
6487
  }
6386
6488
  var OVERRIDE_KEYS = ["collection", "files", "schemas", "elements"];
6387
6489
  var CHECK_KEYS = ["name", "query"];
6490
+ var DERIVE_KEYS = ["fields", "sources", "codeowners", "commands"];
6491
+ var DERIVE_COMMAND_KEYS = ["run", "timeout"];
6492
+ var BUILTIN_CLAIMED_BY = {
6493
+ created: "git",
6494
+ "last-updated": "git",
6495
+ authors: "git",
6496
+ owner: "codeowners",
6497
+ "reviewed-by": "GitHub or GitLab",
6498
+ "last-reviewed": "GitHub or GitLab"
6499
+ };
6500
+ var DERIVED_TABLE_RESERVED = /* @__PURE__ */ new Set(["_path", "_sources"]);
6501
+ var DERIVABLE_LIST = `Derivable fields: ${DERIVABLE_FIELDS.join(", ")}, or any key with an entry in derive.commands.`;
6388
6502
  var CHECK_NAME = /^[a-z0-9][a-z0-9._-]*$/;
6389
- var RESERVED_CHECK_NAMES = /* @__PURE__ */ new Set(["query"]);
6503
+ var RESERVED_CHECK_NAMES = /* @__PURE__ */ new Map([
6504
+ [
6505
+ "query",
6506
+ "`manni meta query --check` files its ad-hoc findings as check:query, and a configured check with the same name would share their identity"
6507
+ ],
6508
+ [
6509
+ "derive",
6510
+ "`manni meta derive` is a verb of its own, and a check reporting as check:derive would read as its findings"
6511
+ ]
6512
+ ]);
6390
6513
  var FILL_KEYS = [
6391
6514
  "provider",
6392
6515
  "model",
@@ -6399,6 +6522,7 @@ var CONFIG_KEYS = [
6399
6522
  "schemas",
6400
6523
  "overrides",
6401
6524
  "checks",
6525
+ "derive",
6402
6526
  "baseline",
6403
6527
  "allowEmpty",
6404
6528
  "respectGitignore",
@@ -6509,10 +6633,10 @@ function parseSchemaEntry(entry, where, source) {
6509
6633
  }
6510
6634
  return parsed2;
6511
6635
  }
6512
- function parseConfig(text, source) {
6636
+ function parseConfig(text2, source) {
6513
6637
  let raw;
6514
6638
  try {
6515
- raw = parseYaml2(text);
6639
+ raw = parseYaml2(text2);
6516
6640
  } catch (err) {
6517
6641
  throw new DocmetaError(
6518
6642
  `${source}: invalid YAML: ${errorMessage(err)}`
@@ -6594,6 +6718,154 @@ function assertOverrideCollections(config, collections, source, section) {
6594
6718
  );
6595
6719
  }
6596
6720
  }
6721
+ function manifestOwning(key, collections) {
6722
+ for (const [c, collection] of collections.entries()) {
6723
+ for (const [e, manifest] of collection.externalMetadata.entries()) {
6724
+ if (manifest.keys.includes(key)) {
6725
+ return { collection: c, name: collection.name, entry: e, file: manifest.file };
6726
+ }
6727
+ }
6728
+ }
6729
+ return void 0;
6730
+ }
6731
+ function assertManagedFieldsUnowned(config, collections, source, section) {
6732
+ const refuse = (path, owner) => {
6733
+ 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.`;
6734
+ throw new DocmetaError(
6735
+ inSection(message, source, section)
6736
+ );
6737
+ };
6738
+ for (const key of Object.keys(config.derive?.commands ?? {})) {
6739
+ const owner = manifestOwning(key, collections);
6740
+ if (owner !== void 0) refuse(`derive.commands.${key}`, owner);
6741
+ }
6742
+ for (const [i, field] of (config.derive?.fields ?? []).entries()) {
6743
+ const owner = manifestOwning(field, collections);
6744
+ if (owner !== void 0) refuse(`derive.fields[${i}] "${field}"`, owner);
6745
+ }
6746
+ }
6747
+ function parseDerive(raw, source) {
6748
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
6749
+ throw new DocmetaError(
6750
+ `${source}: "derive" must be a mapping. Write it as \`derive:\` with \`fields:\` beneath it.`
6751
+ );
6752
+ }
6753
+ const e = raw;
6754
+ rejectUnknownKeys2(e, DERIVE_KEYS, '"derive"', source);
6755
+ if (e.fields === void 0 && e.sources === void 0 && e.codeowners === void 0 && e.commands === void 0) {
6756
+ throw new DocmetaError(
6757
+ `${source}: "derive" sets nothing. Give it \`fields:\` to manage, \`commands:\` to derive from a command, or \`sources:\` or \`codeowners:\` to shape the reads.`
6758
+ );
6759
+ }
6760
+ const commands = e.commands === void 0 ? void 0 : parseDeriveCommands(e.commands, source);
6761
+ if (e.fields !== void 0 && (!Array.isArray(e.fields) || e.fields.length === 0 || e.fields.some((f) => typeof f !== "string"))) {
6762
+ throw new DocmetaError(
6763
+ `${source}: derive.fields must be a non-empty list of field names. ${DERIVABLE_LIST}`
6764
+ );
6765
+ }
6766
+ const fields = [];
6767
+ (e.fields ?? []).forEach((f, i) => {
6768
+ if (!isBuiltinField(f) && !(commands !== void 0 && Object.hasOwn(commands, f))) {
6769
+ throw new DocmetaError(
6770
+ `${source}: derive.fields[${i}] "${f}" is not derivable. ${DERIVABLE_LIST}`
6771
+ );
6772
+ }
6773
+ if (fields.includes(f)) {
6774
+ throw new DocmetaError(`${source}: derive.fields lists "${f}" twice.`);
6775
+ }
6776
+ fields.push(f);
6777
+ });
6778
+ const derive = { fields };
6779
+ if (e.sources !== void 0) {
6780
+ if (!Array.isArray(e.sources) || e.sources.length === 0 || e.sources.some((s) => typeof s !== "string")) {
6781
+ throw new DocmetaError(
6782
+ `${source}: derive.sources must be a non-empty list of source names. Sources: ${DERIVE_SOURCES.join(", ")}.`
6783
+ );
6784
+ }
6785
+ const sources = [];
6786
+ e.sources.forEach((s, i) => {
6787
+ if (!isDeriveSource(s)) {
6788
+ throw new DocmetaError(
6789
+ `${source}: derive.sources[${i}] "${s}" is not a source. Sources: ${DERIVE_SOURCES.join(", ")}.`
6790
+ );
6791
+ }
6792
+ if (sources.includes(s)) {
6793
+ throw new DocmetaError(`${source}: derive.sources lists "${s}" twice.`);
6794
+ }
6795
+ sources.push(s);
6796
+ });
6797
+ derive.sources = sources;
6798
+ }
6799
+ if (e.codeowners !== void 0) {
6800
+ if (typeof e.codeowners !== "string" || e.codeowners.trim() === "") {
6801
+ throw new DocmetaError(
6802
+ `${source}: derive.codeowners must be a non-empty path to a CODEOWNERS file, relative to the config file.`
6803
+ );
6804
+ }
6805
+ derive.codeowners = e.codeowners;
6806
+ }
6807
+ if (commands !== void 0) derive.commands = commands;
6808
+ return derive;
6809
+ }
6810
+ function parseDeriveCommands(raw, source) {
6811
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
6812
+ throw new DocmetaError(
6813
+ `${source}: derive.commands must be a mapping of field name to command.`
6814
+ );
6815
+ }
6816
+ const commands = {};
6817
+ for (const [key, value] of Object.entries(raw)) {
6818
+ if (key.trim() === "") {
6819
+ throw new DocmetaError(`${source}: derive.commands has an empty field name.`);
6820
+ }
6821
+ if (key === FILE_SCHEMA_KEY) {
6822
+ throw new DocmetaError(
6823
+ `${source}: derive.commands may not derive "${FILE_SCHEMA_KEY}" \u2014 a command never chooses the schema a document is judged by.`
6824
+ );
6825
+ }
6826
+ if (isBuiltinField(key)) {
6827
+ throw new DocmetaError(
6828
+ `${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.`
6829
+ );
6830
+ }
6831
+ if (DERIVED_TABLE_RESERVED.has(key)) {
6832
+ throw new DocmetaError(
6833
+ `${source}: derive.commands.${key} collides with a column of the derived table a query builds (${[...DERIVED_TABLE_RESERVED].join(", ")}). Pick another field name.`
6834
+ );
6835
+ }
6836
+ const where = `derive.commands.${key}`;
6837
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
6838
+ throw new DocmetaError(
6839
+ `${source}: ${where} must be a mapping with \`run:\` beneath it.`
6840
+ );
6841
+ }
6842
+ const c = value;
6843
+ rejectUnknownKeys2(c, DERIVE_COMMAND_KEYS, where, source);
6844
+ if (!Array.isArray(c.run) || c.run.length === 0) {
6845
+ throw new DocmetaError(
6846
+ `${source}: ${where}.run must be a non-empty list of strings, the program first.`
6847
+ );
6848
+ }
6849
+ const run2 = [];
6850
+ c.run.forEach((arg, j) => {
6851
+ if (typeof arg !== "string" || arg.trim() === "") {
6852
+ throw new DocmetaError(`${source}: ${where}.run[${j}] must be a non-empty string.`);
6853
+ }
6854
+ run2.push(arg);
6855
+ });
6856
+ const command = { run: run2 };
6857
+ if (c.timeout !== void 0) {
6858
+ if (typeof c.timeout !== "number" || !Number.isInteger(c.timeout) || c.timeout <= 0) {
6859
+ throw new DocmetaError(
6860
+ `${source}: ${where}.timeout must be a whole number of seconds, greater than zero.`
6861
+ );
6862
+ }
6863
+ command.timeout = c.timeout;
6864
+ }
6865
+ commands[key] = command;
6866
+ }
6867
+ return commands;
6868
+ }
6597
6869
  function parseConfigDocument(raw, source) {
6598
6870
  if (raw == null) return {};
6599
6871
  if (typeof raw !== "object" || Array.isArray(raw)) {
@@ -6649,6 +6921,9 @@ function parseConfigDocument(raw, source) {
6649
6921
  if (obj.checks !== void 0) {
6650
6922
  config.checks = parseChecks(obj.checks, source);
6651
6923
  }
6924
+ if (obj.derive !== void 0) {
6925
+ config.derive = parseDerive(obj.derive, source);
6926
+ }
6652
6927
  if (obj.elements !== void 0) {
6653
6928
  config.elements = asElementPaths(obj.elements, "elements", source);
6654
6929
  }
@@ -6710,9 +6985,10 @@ function parseChecks(value, source) {
6710
6985
  `${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
6986
  );
6712
6987
  }
6713
- if (RESERVED_CHECK_NAMES.has(e.name)) {
6988
+ const reserved = RESERVED_CHECK_NAMES.get(e.name);
6989
+ if (reserved !== void 0) {
6714
6990
  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.`
6991
+ `${source}: checks[${i}].name "${e.name}" is reserved \u2014 ${reserved}. Pick another name.`
6716
6992
  );
6717
6993
  }
6718
6994
  if (seen.has(e.name)) {
@@ -6815,8 +7091,8 @@ function parseFill(value, source) {
6815
7091
  return fill;
6816
7092
  }
6817
7093
  function schemaTrustRoot(cwd, configDir) {
6818
- const git = findGitRoot(cwd);
6819
- if (git !== null) return { dir: git, source: "git" };
7094
+ const git2 = findGitRoot(cwd);
7095
+ if (git2 !== null) return { dir: git2, source: "git" };
6820
7096
  if (configDir !== void 0) return { dir: resolve7(configDir), source: "config" };
6821
7097
  return { dir: resolve7(cwd), source: "cwd" };
6822
7098
  }
@@ -6826,6 +7102,7 @@ async function loadConfig(explicitPath, cwd = process.cwd()) {
6826
7102
  const section = file.wrapped ? META_SECTION : void 0;
6827
7103
  const config = parseConfigValue(file.value, file.source, section);
6828
7104
  assertOverrideCollections(config, file.collections, file.source, section);
7105
+ assertManagedFieldsUnowned(config, file.collections, file.source, section);
6829
7106
  return {
6830
7107
  config,
6831
7108
  path: file.path,
@@ -7010,7 +7287,7 @@ function isParamName(name2) {
7010
7287
  return PARAM_NAME_HEAD.exec(name2)?.[0] === name2;
7011
7288
  }
7012
7289
  function collectNamedParameters(sql) {
7013
- const tokens = [];
7290
+ const tokens2 = [];
7014
7291
  const seen = /* @__PURE__ */ new Set();
7015
7292
  const prefixOf = /* @__PURE__ */ new Map();
7016
7293
  let i = 0;
@@ -7040,14 +7317,14 @@ function collectNamedParameters(sql) {
7040
7317
  prefixOf.set(m[0], ch);
7041
7318
  if (!seen.has(token)) {
7042
7319
  seen.add(token);
7043
- tokens.push(token);
7320
+ tokens2.push(token);
7044
7321
  }
7045
7322
  i += 1 + m[0].length;
7046
7323
  } else {
7047
7324
  i++;
7048
7325
  }
7049
7326
  }
7050
- return tokens;
7327
+ return tokens2;
7051
7328
  }
7052
7329
  function stripLeadingTrivia(sql) {
7053
7330
  let i = 0;
@@ -7060,16 +7337,16 @@ function stripLeadingTrivia(sql) {
7060
7337
  return sql.slice(i);
7061
7338
  }
7062
7339
  }
7063
- function isTrivia(text) {
7340
+ function isTrivia(text2) {
7064
7341
  let i = 0;
7065
- while (i < text.length) {
7066
- const ch = text[i];
7342
+ while (i < text2.length) {
7343
+ const ch = text2[i];
7067
7344
  if (ch === void 0 || /\s/.test(ch) || ch === ";") {
7068
7345
  i++;
7069
7346
  continue;
7070
7347
  }
7071
- if (startsComment(text, i)) {
7072
- i = skipComment(text, i);
7348
+ if (startsComment(text2, i)) {
7349
+ i = skipComment(text2, i);
7073
7350
  continue;
7074
7351
  }
7075
7352
  return false;
@@ -7081,8 +7358,8 @@ function loadSqlite() {
7081
7358
  sqliteModule ??= (async () => {
7082
7359
  const original = process.emitWarning.bind(process);
7083
7360
  const filtered = (warning, ...rest) => {
7084
- const text = typeof warning === "string" ? warning : warning.message;
7085
- if (text.includes("SQLite is an experimental feature")) return;
7361
+ const text2 = typeof warning === "string" ? warning : warning.message;
7362
+ if (text2.includes("SQLite is an experimental feature")) return;
7086
7363
  Reflect.apply(original, process, [warning, ...rest]);
7087
7364
  };
7088
7365
  process.emitWarning = filtered;
@@ -7358,119 +7635,1751 @@ function toFieldError(schema, e, lineFor, colFor, locate) {
7358
7635
  };
7359
7636
  }
7360
7637
 
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
7638
+ // src/meta/core/derive/cache.ts
7367
7639
  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, "/");
7640
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
7641
+ import { join as join5 } from "node:path";
7642
+ var REVIEW_CACHE_DIR = ".manni/meta/review-cache";
7643
+ var REVIEW_CACHE_VERSION = 1;
7644
+ var ReviewCache = class {
7645
+ constructor(dir, enabled) {
7646
+ this.dir = dir;
7647
+ this.enabled = enabled;
7648
+ }
7649
+ dir;
7650
+ enabled;
7651
+ entryPath(key) {
7652
+ const digest = createHash3("sha256").update(key).digest("hex");
7653
+ return join5(this.dir, `${digest}.json`);
7654
+ }
7655
+ /** The cached change, or `undefined` for a miss. Never `null`: nulls are not stored. */
7656
+ get(key) {
7657
+ if (!this.enabled) return void 0;
7658
+ let raw;
7659
+ try {
7660
+ raw = JSON.parse(readFileSync(this.entryPath(key), "utf8"));
7661
+ } catch {
7662
+ return void 0;
7663
+ }
7664
+ if (!isRecord(raw)) return void 0;
7665
+ if (raw.version !== REVIEW_CACHE_VERSION || raw.key !== key) return void 0;
7666
+ const change2 = readChange(raw.change);
7667
+ return change2 ?? void 0;
7668
+ }
7669
+ set(key, value) {
7670
+ if (!this.enabled) return;
7671
+ const entry = { version: REVIEW_CACHE_VERSION, key, change: value };
7672
+ const path = this.entryPath(key);
7673
+ const tmp = `${path}.${process.pid}-${Math.random().toString(36).slice(2, 8)}.tmp`;
7674
+ try {
7675
+ mkdirSync(this.dir, { recursive: true });
7676
+ writeFileSync(tmp, `${JSON.stringify(entry, null, 2)}
7677
+ `, "utf8");
7678
+ renameSync(tmp, path);
7679
+ } catch {
7680
+ try {
7681
+ rmSync(tmp, { force: true });
7682
+ } catch {
7683
+ }
7684
+ }
7685
+ }
7686
+ };
7687
+ function cachedClient(client, cache, identity) {
7688
+ return {
7689
+ detect: () => client.detect(),
7690
+ status: () => client.status(),
7691
+ async mergedChangeFor(sha) {
7692
+ if (identity === null) return client.mergedChangeFor(sha);
7693
+ const key = `${identity.host}/${identity.project}/${sha}`;
7694
+ const hit = cache.get(key);
7695
+ if (hit !== void 0) return hit;
7696
+ const change2 = await client.mergedChangeFor(sha);
7697
+ if (change2 !== null) cache.set(key, change2);
7698
+ return change2;
7699
+ }
7700
+ };
7384
7701
  }
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);
7702
+ function isRecord(v) {
7703
+ return typeof v === "object" && v !== null && !Array.isArray(v);
7393
7704
  }
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();
7705
+ function readChange(v) {
7706
+ if (!isRecord(v)) return null;
7707
+ const { id, mergedAt, approvals } = v;
7708
+ if (typeof id !== "number" || typeof mergedAt !== "string") return null;
7709
+ if (!Array.isArray(approvals)) return null;
7710
+ const read = [];
7711
+ for (const a of approvals) {
7712
+ if (!isRecord(a)) return null;
7713
+ if (typeof a.login !== "string" || typeof a.submittedAt !== "string") return null;
7714
+ read.push({ login: a.login, submittedAt: a.submittedAt });
7398
7715
  }
7399
- return `${JSON.stringify(
7400
- {
7401
- version: baseline.version,
7402
- generatedWith: baseline.generatedWith,
7403
- entries
7404
- },
7405
- null,
7406
- 2
7407
- )}
7408
- `;
7716
+ return { id, mergedAt, approvals: read };
7409
7717
  }
7410
- function bad(source, detail) {
7411
- throw new DocmetaError(`Baseline "${source}": ${detail}`);
7718
+
7719
+ // src/meta/core/derive/spawn.ts
7720
+ import { spawn as spawn2 } from "node:child_process";
7721
+ var DEFAULT_TIMEOUT_MS2 = 3e4;
7722
+ var MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
7723
+ var BinMissing = class extends Error {
7724
+ };
7725
+ function run(bin, args, opts) {
7726
+ return new Promise((settle, reject) => {
7727
+ let done = false;
7728
+ const finish = (r) => {
7729
+ if (done) return;
7730
+ done = true;
7731
+ settle(r);
7732
+ };
7733
+ const fail = (err) => {
7734
+ if (done) return;
7735
+ done = true;
7736
+ reject(err);
7737
+ };
7738
+ let child;
7739
+ try {
7740
+ child = spawn2(bin, [...opts.prefixArgs ?? [], ...args], {
7741
+ cwd: opts.cwd,
7742
+ windowsHide: true
7743
+ });
7744
+ } catch (err) {
7745
+ fail(err instanceof Error ? err : new Error(String(err)));
7746
+ return;
7747
+ }
7748
+ let stdout = "";
7749
+ let stderr = "";
7750
+ let timedOut = false;
7751
+ let tooLarge = false;
7752
+ const stop = (r) => {
7753
+ clearTimeout(timer);
7754
+ child.kill();
7755
+ setTimeout(() => child.kill("SIGKILL"), 1e3).unref();
7756
+ finish({ code: null, ...r });
7757
+ };
7758
+ child.stdout.setEncoding("utf8");
7759
+ child.stdout.on("data", (chunk) => {
7760
+ stdout += chunk;
7761
+ if (stdout.length > MAX_OUTPUT_BYTES) {
7762
+ tooLarge = true;
7763
+ stop({ stdout, stderr, timedOut, tooLarge });
7764
+ }
7765
+ });
7766
+ child.stderr.setEncoding("utf8");
7767
+ child.stderr.on("data", (chunk) => {
7768
+ stderr += chunk;
7769
+ if (stderr.length > MAX_OUTPUT_BYTES) {
7770
+ tooLarge = true;
7771
+ stop({ stdout, stderr, timedOut, tooLarge });
7772
+ }
7773
+ });
7774
+ child.stdin.end();
7775
+ const timer = setTimeout(() => {
7776
+ timedOut = true;
7777
+ stop({ stdout, stderr, timedOut, tooLarge });
7778
+ }, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2);
7779
+ child.on("error", (err) => {
7780
+ clearTimeout(timer);
7781
+ fail(err.code === "ENOENT" ? new BinMissing(err.message) : err);
7782
+ });
7783
+ child.on("close", (code) => {
7784
+ clearTimeout(timer);
7785
+ finish({ code, stdout, stderr, timedOut, tooLarge });
7786
+ });
7787
+ });
7412
7788
  }
7413
- function parseBaseline(text, source) {
7414
- let raw;
7789
+ function commandLine(bin, args) {
7790
+ return [bin, ...args].join(" ");
7791
+ }
7792
+ function lastLine(text2) {
7793
+ const lines = text2.split(/\r?\n/).filter((l) => l.trim() !== "");
7794
+ return lines.at(-1) ?? "";
7795
+ }
7796
+
7797
+ // src/meta/core/derive/command.ts
7798
+ var PATH_PLACEHOLDER = "{path}";
7799
+ function isPerFile(command) {
7800
+ return command.run.some((arg) => arg.includes(PATH_PLACEHOLDER));
7801
+ }
7802
+ function argvFor(command, label) {
7803
+ return command.run.map((arg) => arg.split(PATH_PLACEHOLDER).join(label));
7804
+ }
7805
+ function valueOf(stdout) {
7806
+ const text2 = stdout.trim();
7807
+ if (text2 === "") return null;
7808
+ if (!/^[{["]/.test(text2)) return text2;
7415
7809
  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
- );
7810
+ return JSON.parse(text2);
7811
+ } catch {
7812
+ return text2;
7429
7813
  }
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.');
7814
+ }
7815
+ async function deriveFromCommands(inputs, commands, opts) {
7816
+ const records = /* @__PURE__ */ new Map();
7817
+ const fields = Object.keys(commands);
7818
+ if (fields.length === 0 || inputs.length === 0) {
7819
+ return { status: { available: true }, records };
7434
7820
  }
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
- );
7821
+ for (const input of inputs) records.set(input.label, {});
7822
+ for (const field of fields) {
7823
+ const command = commands[field];
7824
+ if (command === void 0) continue;
7825
+ if (isPerFile(command)) {
7826
+ for (const [label, record] of records) {
7827
+ const outcome = await runOne(field, argvFor(command, label), command.timeoutMs, opts);
7828
+ if ("reason" in outcome) return unavailable2(outcome.reason);
7829
+ record[field] = outcome.value;
7830
+ }
7831
+ } else {
7832
+ const outcome = await runOne(field, command.run, command.timeoutMs, opts);
7833
+ if ("reason" in outcome) return unavailable2(outcome.reason);
7834
+ for (const record of records.values()) record[field] = outcome.value;
7448
7835
  }
7449
- entries[file] = value;
7450
7836
  }
7451
- return { version: BASELINE_VERSION, generatedWith, entries };
7837
+ return { status: { available: true }, records };
7452
7838
  }
7453
- async function readBaseline(absPath, source) {
7454
- let text;
7839
+ function unavailable2(reason) {
7840
+ return { status: { available: false, reason }, records: /* @__PURE__ */ new Map() };
7841
+ }
7842
+ async function runOne(field, argv, timeoutMs, opts) {
7843
+ const [program, ...args] = argv;
7844
+ const where = `(derive.commands.${field})`;
7845
+ if (program === void 0) {
7846
+ return { reason: `no program to run ${where}` };
7847
+ }
7848
+ const line = argv.join(" ");
7849
+ let result;
7455
7850
  try {
7456
- text = await readFile5(absPath, "utf8");
7851
+ result = await run(opts.spawn?.bin ?? program, args, {
7852
+ ...opts.spawn,
7853
+ cwd: opts.spawn?.cwd ?? opts.cwd,
7854
+ timeoutMs
7855
+ });
7457
7856
  } catch (err) {
7458
- if (err.code === "ENOENT") return null;
7459
- throw new DocmetaError(
7460
- `Baseline "${source}" could not be read: ${errorMessage(err)}`
7461
- );
7857
+ if (err instanceof BinMissing) {
7858
+ return { reason: `\`${program}\` is not on PATH ${where}` };
7859
+ }
7860
+ return { reason: `\`${line}\` could not be run: ${errorMessage(err)} ${where}` };
7861
+ }
7862
+ if (result.timedOut) {
7863
+ return { reason: `\`${line}\` timed out after ${String(timeoutMs / 1e3)}s ${where}` };
7462
7864
  }
7463
- return parseBaseline(text, source);
7865
+ if (result.tooLarge) {
7866
+ return {
7867
+ reason: `\`${line}\` wrote more than ${String(MAX_OUTPUT_BYTES / (1024 * 1024))} MiB; a command reports one field's value ${where}`
7868
+ };
7869
+ }
7870
+ if (result.code !== 0) {
7871
+ const last = lastLine(result.stderr);
7872
+ const detail = last === "" ? "no output on stderr" : last;
7873
+ const exit = result.code === null ? "killed by a signal" : `exit ${String(result.code)}`;
7874
+ return { reason: `\`${line}\` failed (${exit}): ${detail} ${where}` };
7875
+ }
7876
+ const value = valueOf(result.stdout);
7877
+ return { value: value === null ? null : { value, source: "command", evidence: line } };
7464
7878
  }
7465
- async function writeBaselineFile(absPath, baseline, source) {
7879
+
7880
+ // src/meta/core/derive/reviews.ts
7881
+ import { resolve as resolvePath3 } from "node:path";
7882
+ function parseOriginUrl(url) {
7883
+ const trimmed = url.trim();
7884
+ if (trimmed === "") return null;
7885
+ if (isLocalPath(trimmed)) return null;
7886
+ const scp = /^(?:[^@/:]+@)?([^/:]+):(?!\/\/)(.+)$/.exec(trimmed);
7887
+ if (scp) {
7888
+ const host = scp[1];
7889
+ const path = scp[2];
7890
+ if (host === void 0 || path === void 0) return null;
7891
+ if (/^[A-Za-z]$/.test(host)) return null;
7892
+ return project(host, path);
7893
+ }
7894
+ let parsed2;
7466
7895
  try {
7467
- await writeFileAtomic(absPath, serializeBaseline(baseline));
7468
- } catch (err) {
7469
- throw new DocmetaError(
7470
- `Baseline "${source}" could not be written: ${errorMessage(err)}`
7471
- );
7896
+ parsed2 = new URL(trimmed);
7897
+ } catch {
7898
+ return null;
7472
7899
  }
7473
- }
7900
+ if (!/^(https?|ssh|git):$/.test(parsed2.protocol)) return null;
7901
+ if (parsed2.hostname === "") return null;
7902
+ return project(parsed2.hostname, parsed2.pathname);
7903
+ }
7904
+ function isLocalPath(s) {
7905
+ return s.startsWith("/") || s.startsWith("./") || s.startsWith("../") || s.startsWith("\\\\");
7906
+ }
7907
+ function project(host, path) {
7908
+ const segments = path.replace(/\.git$/, "").split("/").filter((s) => s !== "");
7909
+ if (segments.length < 2) return null;
7910
+ return { host, project: segments.join("/") };
7911
+ }
7912
+ function identityFromOrigin(host, project2, requested) {
7913
+ const name2 = host.toLowerCase();
7914
+ if (name2.includes("github")) return { kind: "github", host, project: project2 };
7915
+ if (name2.includes("gitlab")) return { kind: "gitlab", host, project: project2 };
7916
+ const only = requested !== void 0 && requested.length === 1 ? requested[0] : void 0;
7917
+ return only === void 0 ? null : { kind: only, host, project: project2 };
7918
+ }
7919
+ var UnknownHostClient = class {
7920
+ constructor(host) {
7921
+ this.host = host;
7922
+ }
7923
+ host;
7924
+ detect() {
7925
+ return Promise.resolve(null);
7926
+ }
7927
+ status() {
7928
+ return Promise.resolve({
7929
+ available: false,
7930
+ 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`
7931
+ });
7932
+ }
7933
+ mergedChangeFor() {
7934
+ return Promise.resolve(null);
7935
+ }
7936
+ };
7937
+ function httpStatus(stderr) {
7938
+ const m = /\b(?:HTTP\s+(\d{3})|(\d{3})\s+Not\s+Found)\b/i.exec(stderr);
7939
+ const code = m?.[1] ?? m?.[2];
7940
+ return code === void 0 ? null : Number(code);
7941
+ }
7942
+ function isNotFound(r) {
7943
+ const status = httpStatus(r.stderr);
7944
+ return status === 404 || status === 422;
7945
+ }
7946
+ var CliClient = class {
7947
+ constructor(identity, spawnOpts) {
7948
+ this.identity = identity;
7949
+ this.spawnOpts = spawnOpts;
7950
+ }
7951
+ identity;
7952
+ spawnOpts;
7953
+ detect() {
7954
+ return Promise.resolve(this.identity);
7955
+ }
7956
+ async status() {
7957
+ if (this.identity === null) {
7958
+ return { available: false, reason: "no origin remote to tell GitHub from GitLab" };
7959
+ }
7960
+ const { host } = this.identity;
7961
+ let r;
7962
+ try {
7963
+ r = await this.run(["auth", "status", "--hostname", host]);
7964
+ } catch (err) {
7965
+ if (err instanceof BinMissing) {
7966
+ return {
7967
+ available: false,
7968
+ reason: `${this.tool} is not on PATH (origin is ${host}); install ${this.tool} and run ${this.tool} auth login`
7969
+ };
7970
+ }
7971
+ throw err;
7972
+ }
7973
+ if (r.timedOut) throw this.timeout(["auth", "status", "--hostname", host]);
7974
+ if (r.code !== 0) {
7975
+ return {
7976
+ available: false,
7977
+ reason: `${this.tool} is not authenticated for ${host}; run ${this.tool} auth login`
7978
+ };
7979
+ }
7980
+ return { available: true };
7981
+ }
7982
+ async mergedChangeFor(sha) {
7983
+ if (this.identity === null) return null;
7984
+ return this.lookup(this.identity, sha);
7985
+ }
7986
+ get bin() {
7987
+ return this.spawnOpts.bin ?? this.tool;
7988
+ }
7989
+ run(args) {
7990
+ return run(this.bin, args, this.spawnOpts);
7991
+ }
7992
+ timeout(args) {
7993
+ const ms = this.spawnOpts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
7994
+ return new DocmetaError(
7995
+ `${commandLine(this.tool, args)} timed out after ${ms}ms`
7996
+ );
7997
+ }
7998
+ /**
7999
+ * Run a call whose stdout is JSON. A not-found answer is `null`; any other
8000
+ * failure throws, naming the command the user can re-run by hand. The
8001
+ * message quotes the tool's own name rather than `bin`, which under test
8002
+ * is `node`.
8003
+ */
8004
+ async json(args) {
8005
+ let r;
8006
+ try {
8007
+ r = await this.run(args);
8008
+ } catch (err) {
8009
+ if (err instanceof BinMissing) {
8010
+ throw new DocmetaError(
8011
+ `${commandLine(this.tool, args)} failed: ${this.tool} is not on PATH`
8012
+ );
8013
+ }
8014
+ throw err;
8015
+ }
8016
+ const cmd = commandLine(this.tool, args);
8017
+ if (r.timedOut) throw this.timeout(args);
8018
+ if (r.tooLarge) {
8019
+ throw new DocmetaError(`${cmd} wrote more output than manni will read`);
8020
+ }
8021
+ if (r.code !== 0) {
8022
+ if (isNotFound(r)) return null;
8023
+ const detail = lastLine(r.stderr) || lastLine(r.stdout);
8024
+ const exit = r.code === null ? "signal" : `exit ${r.code}`;
8025
+ throw new DocmetaError(
8026
+ `${cmd} failed (${exit})${detail === "" ? "" : `: ${detail}`}`
8027
+ );
8028
+ }
8029
+ try {
8030
+ return JSON.parse(r.stdout);
8031
+ } catch {
8032
+ throw new DocmetaError(`${cmd} did not return JSON`);
8033
+ }
8034
+ }
8035
+ };
8036
+ function isRecord2(v) {
8037
+ return typeof v === "object" && v !== null && !Array.isArray(v);
8038
+ }
8039
+ function items(v) {
8040
+ return Array.isArray(v) ? v.filter(isRecord2) : [];
8041
+ }
8042
+ function numberOf(v) {
8043
+ return typeof v === "number" && Number.isInteger(v) ? v : null;
8044
+ }
8045
+ function stringOf(v) {
8046
+ return typeof v === "string" && v !== "" ? v : null;
8047
+ }
8048
+ function timestampOf(v) {
8049
+ if (v === void 0) return void 0;
8050
+ return stringOf(v);
8051
+ }
8052
+ function normalizeLogin(login) {
8053
+ return login.replace(/\[bot\]$/, "");
8054
+ }
8055
+ function latestApprovals(reviews) {
8056
+ const latest = /* @__PURE__ */ new Map();
8057
+ for (const r of reviews) {
8058
+ if (r.state !== "APPROVED") continue;
8059
+ const key = normalizeLogin(r.login);
8060
+ const seen = latest.get(key);
8061
+ if (seen === void 0 || r.submittedAt > seen.submittedAt) {
8062
+ latest.set(key, { login: r.login, submittedAt: r.submittedAt });
8063
+ }
8064
+ }
8065
+ return byTime([...latest.values()]);
8066
+ }
8067
+ function byTime(approvals) {
8068
+ return [...approvals].sort(
8069
+ (a, b) => a.submittedAt < b.submittedAt ? -1 : a.submittedAt > b.submittedAt ? 1 : 0
8070
+ );
8071
+ }
8072
+ var GitHubClient = class extends CliClient {
8073
+ tool = "gh";
8074
+ async lookup(identity, sha) {
8075
+ const { host, project: project2 } = identity;
8076
+ const api = (endpoint) => this.json(["api", "--hostname", host, endpoint]);
8077
+ let candidates = items(await api(`repos/${project2}/commits/${sha}/pulls`)).map(
8078
+ (p) => ({ number: numberOf(p.number), mergedAt: timestampOf(p.merged_at) })
8079
+ );
8080
+ if (candidates.length === 0) {
8081
+ const found = await this.json([
8082
+ "pr",
8083
+ "list",
8084
+ "--search",
8085
+ sha,
8086
+ "--state",
8087
+ "merged",
8088
+ "--json",
8089
+ "number,mergedAt",
8090
+ "-R",
8091
+ `${host}/${project2}`
8092
+ ]);
8093
+ candidates = items(found).map((p) => ({
8094
+ number: numberOf(p.number),
8095
+ mergedAt: timestampOf(p.mergedAt)
8096
+ }));
8097
+ }
8098
+ for (const c of candidates) {
8099
+ if (c.number === null) continue;
8100
+ let mergedAt = c.mergedAt;
8101
+ if (mergedAt === void 0) {
8102
+ const pr = await api(`repos/${project2}/pulls/${c.number}`);
8103
+ mergedAt = isRecord2(pr) ? stringOf(pr.merged_at) ?? null : null;
8104
+ }
8105
+ if (mergedAt === null) continue;
8106
+ const raw = [];
8107
+ for (let page = 1; ; page += 1) {
8108
+ const batch = items(
8109
+ await api(`repos/${project2}/pulls/${c.number}/reviews?per_page=100&page=${page}`)
8110
+ );
8111
+ raw.push(...batch);
8112
+ if (batch.length < 100) break;
8113
+ }
8114
+ const reviews = raw.map((r) => ({
8115
+ login: isRecord2(r.user) ? stringOf(r.user.login) : null,
8116
+ state: stringOf(r.state),
8117
+ submittedAt: stringOf(r.submitted_at)
8118
+ })).filter(
8119
+ (r) => r.login !== null && r.state !== null && r.submittedAt !== null
8120
+ );
8121
+ return { id: c.number, mergedAt, approvals: latestApprovals(reviews) };
8122
+ }
8123
+ return null;
8124
+ }
8125
+ };
8126
+ var GitLabClient = class extends CliClient {
8127
+ tool = "glab";
8128
+ async lookup(identity, sha) {
8129
+ const { host } = identity;
8130
+ const api = (endpoint) => this.json(["api", "--hostname", host, endpoint]);
8131
+ const merged = items(
8132
+ await api(`projects/:fullpath/repository/commits/${sha}/merge_requests`)
8133
+ ).map((mr) => ({ iid: numberOf(mr.iid), mergedAt: stringOf(mr.merged_at) })).find(
8134
+ (mr) => mr.iid !== null && mr.mergedAt !== null
8135
+ );
8136
+ if (merged === void 0) return null;
8137
+ const answer = await api(`projects/:fullpath/merge_requests/${merged.iid}/approvals`);
8138
+ const approvedBy = isRecord2(answer) ? items(answer.approved_by) : [];
8139
+ const approvals = [];
8140
+ const seen = /* @__PURE__ */ new Set();
8141
+ for (const entry of approvedBy) {
8142
+ const login = isRecord2(entry.user) ? stringOf(entry.user.username) : null;
8143
+ if (login === null) continue;
8144
+ const key = normalizeLogin(login);
8145
+ if (seen.has(key)) continue;
8146
+ seen.add(key);
8147
+ approvals.push({ login, submittedAt: merged.mergedAt });
8148
+ }
8149
+ return { id: merged.iid, mergedAt: merged.mergedAt, approvals };
8150
+ }
8151
+ };
8152
+ async function createReviewClient(root, opts) {
8153
+ const spawnOpts = { ...opts.spawn, cwd: opts.spawn?.cwd ?? root };
8154
+ const origin = await originUrl(root);
8155
+ const parsed2 = origin === null ? null : parseOriginUrl(origin);
8156
+ const identity = parsed2 === null ? null : identityFromOrigin(parsed2.host, parsed2.project, opts.requested);
8157
+ if (parsed2 !== null && identity === null) return new UnknownHostClient(parsed2.host);
8158
+ const client = identity?.kind === "gitlab" ? new GitLabClient(identity, spawnOpts) : new GitHubClient(identity, spawnOpts);
8159
+ const cache = new ReviewCache(resolvePath3(root, opts.cacheDir), opts.cache);
8160
+ return cachedClient(client, cache, identity);
8161
+ }
8162
+ async function originUrl(root) {
8163
+ try {
8164
+ const r = await run("git", ["remote", "get-url", "origin"], { cwd: root });
8165
+ if (r.code !== 0 || r.timedOut || r.tooLarge) return null;
8166
+ return stringOf(r.stdout.trim());
8167
+ } catch {
8168
+ return null;
8169
+ }
8170
+ }
8171
+ var NOTHING = { "reviewed-by": null, "last-reviewed": null };
8172
+ async function deriveFromReviews(inputs, client) {
8173
+ const records = /* @__PURE__ */ new Map();
8174
+ const status = await client.status();
8175
+ if (!status.available) return { status, records };
8176
+ const identity = await client.detect();
8177
+ const kind = identity?.kind ?? "github";
8178
+ const bySha = /* @__PURE__ */ new Map();
8179
+ const changeFor = (sha) => {
8180
+ let pending = bySha.get(sha);
8181
+ if (pending === void 0) {
8182
+ pending = client.mergedChangeFor(sha);
8183
+ bySha.set(sha, pending);
8184
+ }
8185
+ return pending;
8186
+ };
8187
+ for (const input of inputs) {
8188
+ if (input.sha === null) {
8189
+ records.set(input.label, NOTHING);
8190
+ continue;
8191
+ }
8192
+ const change2 = await changeFor(input.sha);
8193
+ records.set(input.label, factsFrom(change2, kind));
8194
+ }
8195
+ return { status, records };
8196
+ }
8197
+ function factsFrom(change2, kind) {
8198
+ if (change2 === null || change2.approvals.length === 0) return NOTHING;
8199
+ const ordered = byTime(change2.approvals);
8200
+ const latest = ordered.at(-1);
8201
+ if (latest === void 0) return NOTHING;
8202
+ const evidence = kind === "gitlab" ? `gitlab MR !${change2.id}` : `github PR #${change2.id}`;
8203
+ const dateEvidence = kind === "gitlab" ? `${evidence} (merge date; GitLab records no approval time)` : evidence;
8204
+ return {
8205
+ "reviewed-by": {
8206
+ value: ordered.map((a) => a.login),
8207
+ source: kind,
8208
+ evidence
8209
+ },
8210
+ "last-reviewed": {
8211
+ value: utcDate(latest.submittedAt),
8212
+ source: kind,
8213
+ evidence: dateEvidence
8214
+ }
8215
+ };
8216
+ }
8217
+ function utcDate(iso) {
8218
+ const ms = Date.parse(iso);
8219
+ if (Number.isNaN(ms)) return iso;
8220
+ return new Date(ms).toISOString().slice(0, 10);
8221
+ }
8222
+
8223
+ // src/meta/core/derive/index.ts
8224
+ import { dirname as dirname7, resolve as resolve9 } from "node:path";
8225
+
8226
+ // src/meta/core/derive/codeowners.ts
8227
+ import { existsSync as existsSync2 } from "node:fs";
8228
+ import { readFile as readFile5 } from "node:fs/promises";
8229
+ import { dirname as dirname5, relative as relative7, resolve as resolve8, sep as sep4 } from "node:path";
8230
+ import picomatch4 from "picomatch";
8231
+ var CODEOWNERS_LOCATIONS = [
8232
+ ".github/CODEOWNERS",
8233
+ "CODEOWNERS",
8234
+ "docs/CODEOWNERS",
8235
+ ".gitlab/CODEOWNERS"
8236
+ ];
8237
+ function findCodeowners(root, explicit) {
8238
+ if (explicit !== void 0) {
8239
+ const path = resolve8(root, explicit);
8240
+ return existsSync2(path) ? path : null;
8241
+ }
8242
+ for (const loc of CODEOWNERS_LOCATIONS) {
8243
+ const path = resolve8(root, ...loc.split("/"));
8244
+ if (existsSync2(path)) return path;
8245
+ }
8246
+ return null;
8247
+ }
8248
+ var SECTION_HEADER = /^(\^?)\[([^\]]+)\](?:\[(\d+)\])?(.*)$/;
8249
+ var TOKEN = /(?:\\.|[^\s\\])+/g;
8250
+ function parseCodeowners(text2, path, label) {
8251
+ const rules = [];
8252
+ const sections = [];
8253
+ let current;
8254
+ const lines = text2.split(/\r?\n/);
8255
+ for (let i = 0; i < lines.length; i += 1) {
8256
+ const raw = lines[i] ?? "";
8257
+ const line = raw.trim();
8258
+ if (line === "" || line.startsWith("#")) continue;
8259
+ if (line.startsWith("[") || line.startsWith("^[")) {
8260
+ const m = SECTION_HEADER.exec(line);
8261
+ if (!m) continue;
8262
+ const [, caret, rawName, approvals, rest] = m;
8263
+ const name2 = (rawName ?? "").trim();
8264
+ if (name2 === "") continue;
8265
+ const owners2 = tokens(rest ?? "");
8266
+ const existing = sections.find(
8267
+ (s) => s.name.toLowerCase() === name2.toLowerCase()
8268
+ );
8269
+ if (existing) {
8270
+ if (owners2.length > 0) existing.defaultOwners = owners2;
8271
+ current = existing;
8272
+ continue;
8273
+ }
8274
+ const section = {
8275
+ name: name2,
8276
+ optional: caret === "^",
8277
+ ...approvals !== void 0 ? { approvals: Number(approvals) } : {},
8278
+ defaultOwners: owners2
8279
+ };
8280
+ sections.push(section);
8281
+ current = section;
8282
+ continue;
8283
+ }
8284
+ const [first, ...owners] = tokens(line);
8285
+ if (first === void 0) continue;
8286
+ const pattern = unescapePattern(first);
8287
+ if (pattern === "" || pattern === "/") continue;
8288
+ rules.push({
8289
+ pattern,
8290
+ owners,
8291
+ line: i + 1,
8292
+ ...current ? { section: current.name } : {}
8293
+ });
8294
+ }
8295
+ return { path, label, rules, sections };
8296
+ }
8297
+ function tokens(s) {
8298
+ return s.match(TOKEN) ?? [];
8299
+ }
8300
+ function unescapePattern(token) {
8301
+ return token.replace(/\\ /g, " ").replace(/^\\#/, "#");
8302
+ }
8303
+ function ownersFor(file, relPath) {
8304
+ const matchers = matchersFor(file);
8305
+ const defaults = new Map(file.sections.map((s) => [s.name, s.defaultOwners]));
8306
+ const order = [void 0, ...file.sections.map((s) => s.name)];
8307
+ const winners = /* @__PURE__ */ new Map();
8308
+ file.rules.forEach((rule, i) => {
8309
+ if (matchers[i]?.(relPath)) winners.set(rule.section, rule);
8310
+ });
8311
+ if (winners.size === 0) return null;
8312
+ const owners = [];
8313
+ let line = 0;
8314
+ let lineFromOwner = false;
8315
+ for (const section of order) {
8316
+ const rule = winners.get(section);
8317
+ if (!rule) continue;
8318
+ const chosen = rule.owners.length > 0 ? rule.owners : (section === void 0 ? void 0 : defaults.get(section)) ?? [];
8319
+ if (chosen.length > 0) {
8320
+ line = rule.line;
8321
+ lineFromOwner = true;
8322
+ } else if (!lineFromOwner) {
8323
+ line = rule.line;
8324
+ }
8325
+ for (const o of chosen) if (!owners.includes(o)) owners.push(o);
8326
+ }
8327
+ return { owners, line };
8328
+ }
8329
+ var matcherCache2 = /* @__PURE__ */ new WeakMap();
8330
+ function matchersFor(file) {
8331
+ let matchers = matcherCache2.get(file);
8332
+ if (!matchers) {
8333
+ matchers = file.rules.map((r) => picomatch4(toGlobs(r.pattern), PICOMATCH));
8334
+ matcherCache2.set(file, matchers);
8335
+ }
8336
+ return matchers;
8337
+ }
8338
+ var PICOMATCH = { dot: true, windows: false };
8339
+ function toGlobs(pattern) {
8340
+ let p = pattern;
8341
+ const dirOnly = p.endsWith("/");
8342
+ if (dirOnly) p = p.slice(0, -1);
8343
+ let anchored = false;
8344
+ if (p.startsWith("/")) {
8345
+ anchored = true;
8346
+ p = p.slice(1);
8347
+ } else if (p.includes("/") && !p.startsWith("**/")) {
8348
+ anchored = true;
8349
+ }
8350
+ const base = anchored || p.startsWith("**/") ? p : `**/${p}`;
8351
+ if (dirOnly) return [`${base}/**`];
8352
+ const last = p.slice(p.lastIndexOf("/") + 1);
8353
+ return /[*?[]/.test(last) ? [base] : [base, `${base}/**`];
8354
+ }
8355
+ var NOT_FOUND_REASON = `no CODEOWNERS file found (looked for ${CODEOWNERS_LOCATIONS.join(", ")})`;
8356
+ async function deriveFromCodeowners(inputs, opts) {
8357
+ const records = /* @__PURE__ */ new Map();
8358
+ const explicitPath = opts.explicit === void 0 ? void 0 : resolve8(opts.configDir ?? process.cwd(), opts.explicit);
8359
+ if (explicitPath !== void 0 && !existsSync2(explicitPath)) {
8360
+ return {
8361
+ status: { available: false, reason: `CODEOWNERS not found at ${explicitPath}` },
8362
+ records
8363
+ };
8364
+ }
8365
+ const byRoot = /* @__PURE__ */ new Map();
8366
+ const rootCache = /* @__PURE__ */ new Map();
8367
+ for (const input of inputs) {
8368
+ const dir = dirname5(input.absPath);
8369
+ let root = rootCache.get(dir);
8370
+ if (root === void 0) {
8371
+ root = opts.rootOf(input.absPath);
8372
+ rootCache.set(dir, root);
8373
+ }
8374
+ if (root === null) continue;
8375
+ const bucket = byRoot.get(root);
8376
+ if (bucket) bucket.push(input);
8377
+ else byRoot.set(root, [input]);
8378
+ }
8379
+ let found = 0;
8380
+ const parsed2 = /* @__PURE__ */ new Map();
8381
+ for (const [root, bucket] of byRoot) {
8382
+ const path = explicitPath ?? findCodeowners(root);
8383
+ if (path === null) {
8384
+ for (const input of bucket) records.set(input.label, null);
8385
+ continue;
8386
+ }
8387
+ found += 1;
8388
+ const label = toPosix2(relative7(opts.configDir ?? root, path));
8389
+ const key = `${path}\0${label}`;
8390
+ let file = parsed2.get(key);
8391
+ if (!file) {
8392
+ file = readFile5(path, "utf8").then(
8393
+ (text2) => parseCodeowners(text2, path, label)
8394
+ );
8395
+ parsed2.set(key, file);
8396
+ }
8397
+ let owners;
8398
+ try {
8399
+ owners = await file;
8400
+ } catch (err) {
8401
+ return {
8402
+ status: {
8403
+ available: false,
8404
+ reason: `CODEOWNERS at ${label} could not be read: ${errorMessage(err)}`
8405
+ },
8406
+ records
8407
+ };
8408
+ }
8409
+ for (const input of bucket) {
8410
+ const rel = toPosix2(relative7(root, input.absPath));
8411
+ const hit = ownersFor(owners, rel);
8412
+ records.set(
8413
+ input.label,
8414
+ hit === null ? null : {
8415
+ value: hit.owners,
8416
+ source: "codeowners",
8417
+ evidence: `${label}:${hit.line}`
8418
+ }
8419
+ );
8420
+ }
8421
+ }
8422
+ if (byRoot.size === 0) {
8423
+ return {
8424
+ status: { available: false, reason: "no git repository contains the documents" },
8425
+ records
8426
+ };
8427
+ }
8428
+ if (found === 0 && explicitPath === void 0) {
8429
+ return { status: { available: true, reason: NOT_FOUND_REASON }, records };
8430
+ }
8431
+ return { status: { available: true }, records };
8432
+ }
8433
+ function toPosix2(p) {
8434
+ return p.split(sep4).join("/");
8435
+ }
8436
+
8437
+ // src/meta/core/derive/git.ts
8438
+ import { constants as bufferConstants } from "node:buffer";
8439
+ import { spawn as spawn3 } from "node:child_process";
8440
+ import { dirname as dirname6, extname as extname2, relative as relative8, sep as sep5 } from "node:path";
8441
+ var DEFAULT_BULK_THRESHOLD = 32;
8442
+ var NULL_SHA = "0".repeat(40);
8443
+ var MAX_GIT_OUTPUT_BYTES = Math.min(
8444
+ 512 * 1024 * 1024,
8445
+ bufferConstants.MAX_STRING_LENGTH
8446
+ );
8447
+ var HistoryTooLarge = class extends Error {
8448
+ };
8449
+ var BlobsUnreadable = class extends Error {
8450
+ constructor(detail) {
8451
+ super(detail);
8452
+ this.detail = detail;
8453
+ }
8454
+ detail;
8455
+ };
8456
+ function bodyOf(content, fenced) {
8457
+ if (fenced !== true) return content;
8458
+ const loc = locateFrontmatter(content);
8459
+ if (!loc) return content;
8460
+ return content.slice(0, loc.openStart) + content.slice(loc.closeEnd);
8461
+ }
8462
+ async function deriveFromGit(inputs, opts) {
8463
+ const records = /* @__PURE__ */ new Map();
8464
+ const byRoot = groupByRoot(inputs);
8465
+ if (byRoot.size === 0) {
8466
+ return {
8467
+ status: {
8468
+ available: false,
8469
+ reason: "no git repository contains the documents"
8470
+ },
8471
+ records
8472
+ };
8473
+ }
8474
+ const threshold = opts.bulkThreshold ?? DEFAULT_BULK_THRESHOLD;
8475
+ const maxBytes = opts.maxOutputBytes ?? MAX_GIT_OUTPUT_BYTES;
8476
+ const reasons = [];
8477
+ for (const [root, bucket] of byRoot) {
8478
+ const run2 = (args, stdin) => runGit(args, root, stdin, maxBytes);
8479
+ const shallow = await run2(["rev-parse", "--is-shallow-repository"]);
8480
+ if (shallow.missing) {
8481
+ return {
8482
+ status: { available: false, reason: "git is not on PATH" },
8483
+ records: /* @__PURE__ */ new Map()
8484
+ };
8485
+ }
8486
+ if (shallow.code !== 0) {
8487
+ reasons.push(`git could not read the repository at ${root}`);
8488
+ continue;
8489
+ }
8490
+ if (text(shallow).trim() === "true") {
8491
+ reasons.push(
8492
+ `this checkout is shallow; use actions/checkout with fetch-depth: 0 (${root})`
8493
+ );
8494
+ continue;
8495
+ }
8496
+ let histories;
8497
+ let blobs;
8498
+ try {
8499
+ histories = bucket.length <= threshold ? await perFileHistories(run2, bucket) : await bulkHistories(run2, bucket);
8500
+ if (histories === null) {
8501
+ reasons.push(`git could not read the history at ${root}`);
8502
+ continue;
8503
+ }
8504
+ blobs = await fetchBlobs(run2, neededBlobs(histories));
8505
+ } catch (err) {
8506
+ if (err instanceof BlobsUnreadable) {
8507
+ reasons.push(`git cat-file could not read the history (${root}): ${err.detail}`);
8508
+ continue;
8509
+ }
8510
+ if (!(err instanceof HistoryTooLarge)) throw err;
8511
+ reasons.push(`git history is too large to read in one pass (${root})`);
8512
+ continue;
8513
+ }
8514
+ for (const entry of bucket) {
8515
+ const history = histories.get(entry.rel) ?? [];
8516
+ records.set(
8517
+ entry.input.label,
8518
+ judge(entry.input, root, history, blobs, opts.now)
8519
+ );
8520
+ }
8521
+ }
8522
+ if (reasons.length > 0) {
8523
+ return {
8524
+ status: { available: false, reason: reasons.join("; ") },
8525
+ records: /* @__PURE__ */ new Map()
8526
+ };
8527
+ }
8528
+ return { status: { available: true }, records };
8529
+ }
8530
+ function groupByRoot(inputs) {
8531
+ const rootOf = /* @__PURE__ */ new Map();
8532
+ const byRoot = /* @__PURE__ */ new Map();
8533
+ for (const input of inputs) {
8534
+ const dir = dirname6(input.absPath);
8535
+ let root = rootOf.get(dir);
8536
+ if (root === void 0) {
8537
+ root = findGitRoot(dir);
8538
+ rootOf.set(dir, root);
8539
+ }
8540
+ if (root === null) continue;
8541
+ const rel = relative8(root, input.absPath).split(sep5).join("/");
8542
+ const bucket = byRoot.get(root);
8543
+ if (bucket) bucket.push({ input, rel });
8544
+ else byRoot.set(root, [{ input, rel }]);
8545
+ }
8546
+ return byRoot;
8547
+ }
8548
+ 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)";
8549
+ var LOG_ARGS = [
8550
+ "-c",
8551
+ "core.quotePath=false",
8552
+ "log",
8553
+ "--raw",
8554
+ "-M",
8555
+ "--no-abbrev",
8556
+ RECORD_FORMAT
8557
+ ];
8558
+ async function perFileHistories(run2, bucket) {
8559
+ const histories = /* @__PURE__ */ new Map();
8560
+ for (const entry of bucket) {
8561
+ const out = await run2([...LOG_ARGS, "--follow", "--", pathspec(entry.rel)]);
8562
+ if (out.tooLarge) throw new HistoryTooLarge();
8563
+ if (out.code !== 0) {
8564
+ if (await isUnbornHead(run2)) return histories;
8565
+ return null;
8566
+ }
8567
+ const attributed = attribute(parseLog(text(out)), [entry.rel]);
8568
+ histories.set(entry.rel, attributed.get(entry.rel) ?? []);
8569
+ }
8570
+ return histories;
8571
+ }
8572
+ async function bulkHistories(run2, bucket) {
8573
+ const out = await run2(LOG_ARGS);
8574
+ if (out.tooLarge) throw new HistoryTooLarge();
8575
+ if (out.code !== 0) {
8576
+ if (await isUnbornHead(run2)) return /* @__PURE__ */ new Map();
8577
+ return null;
8578
+ }
8579
+ return attribute(
8580
+ parseLog(text(out)),
8581
+ bucket.map((e) => e.rel)
8582
+ );
8583
+ }
8584
+ function pathspec(rel) {
8585
+ return /[*?[\]]/.test(rel) ? `:(literal)${rel}` : rel;
8586
+ }
8587
+ async function isUnbornHead(run2) {
8588
+ const out = await run2(["rev-parse", "--verify", "--quiet", "HEAD^{commit}"]);
8589
+ return !out.missing && out.code !== 0;
8590
+ }
8591
+ function parseLog(text2) {
8592
+ const records = [];
8593
+ for (const chunk of text2.split("")) {
8594
+ if (chunk === "") continue;
8595
+ const nl = chunk.indexOf("\n");
8596
+ const header = nl === -1 ? chunk : chunk.slice(0, nl);
8597
+ const fields = header.split("\0");
8598
+ const sha = fields[0];
8599
+ if (sha === void 0 || sha === "") continue;
8600
+ const raw = [];
8601
+ if (nl !== -1) {
8602
+ for (const line of chunk.slice(nl + 1).split("\n")) {
8603
+ const entry = parseRawLine(line);
8604
+ if (entry) raw.push(entry);
8605
+ }
8606
+ }
8607
+ records.push({
8608
+ sha,
8609
+ authorDate: fields[1] ?? "",
8610
+ authorName: fields[2] ?? "",
8611
+ authorEmail: fields[3] ?? "",
8612
+ coAuthors: splitTrailers(fields[4]),
8613
+ reviewedBy: splitTrailers(fields[5]),
8614
+ raw
8615
+ });
8616
+ }
8617
+ return records;
8618
+ }
8619
+ function splitTrailers(field) {
8620
+ if (field === void 0 || field === "") return [];
8621
+ return field.split("").map((s) => s.trim()).filter((s) => s !== "");
8622
+ }
8623
+ function parseRawLine(line) {
8624
+ if (!line.startsWith(":")) return null;
8625
+ const tab = line.indexOf(" ");
8626
+ if (tab === -1) return null;
8627
+ const meta = line.slice(1, tab).split(" ");
8628
+ const oldBlob = meta[2];
8629
+ const newBlob = meta[3];
8630
+ const status = meta[4];
8631
+ if (oldBlob === void 0 || newBlob === void 0 || status === void 0) {
8632
+ return null;
8633
+ }
8634
+ const paths = line.slice(tab + 1).split(" ").map(unquotePath);
8635
+ const first = paths[0];
8636
+ if (first === void 0) return null;
8637
+ const second = paths[1];
8638
+ const renamed = status.startsWith("R") || status.startsWith("C");
8639
+ return {
8640
+ status,
8641
+ oldBlob,
8642
+ newBlob,
8643
+ oldPath: first,
8644
+ newPath: renamed && second !== void 0 ? second : first
8645
+ };
8646
+ }
8647
+ function unquotePath(p) {
8648
+ if (p.length < 2 || !p.startsWith('"') || !p.endsWith('"')) return p;
8649
+ const inner = p.slice(1, -1);
8650
+ const bytes = [];
8651
+ for (let i = 0; i < inner.length; i += 1) {
8652
+ const ch = inner[i];
8653
+ if (ch !== "\\") {
8654
+ bytes.push(...Buffer.from(ch ?? "", "utf8"));
8655
+ continue;
8656
+ }
8657
+ const next = inner[i + 1];
8658
+ if (next === void 0) break;
8659
+ const octal = /^[0-7]{3}/.exec(inner.slice(i + 1));
8660
+ if (octal) {
8661
+ bytes.push(parseInt(octal[0], 8));
8662
+ i += 3;
8663
+ continue;
8664
+ }
8665
+ const simple = {
8666
+ a: 7,
8667
+ b: 8,
8668
+ t: 9,
8669
+ n: 10,
8670
+ v: 11,
8671
+ f: 12,
8672
+ r: 13,
8673
+ '"': 34,
8674
+ "\\": 92
8675
+ };
8676
+ const code = simple[next];
8677
+ if (code === void 0) bytes.push(...Buffer.from(next, "utf8"));
8678
+ else bytes.push(code);
8679
+ i += 1;
8680
+ }
8681
+ return Buffer.from(bytes).toString("utf8");
8682
+ }
8683
+ function attribute(records, wanted) {
8684
+ const histories = /* @__PURE__ */ new Map();
8685
+ const tracked = /* @__PURE__ */ new Map();
8686
+ for (const rel of wanted) {
8687
+ histories.set(rel, []);
8688
+ tracked.set(rel, rel);
8689
+ }
8690
+ for (const record of records) {
8691
+ if (record.raw.length === 0) continue;
8692
+ const byNewPath = /* @__PURE__ */ new Map();
8693
+ for (const entry of record.raw) byNewPath.set(entry.newPath, entry);
8694
+ for (const [rel, current] of tracked) {
8695
+ const entry = byNewPath.get(current);
8696
+ if (!entry) continue;
8697
+ histories.get(rel)?.push({
8698
+ sha: record.sha,
8699
+ authorDate: record.authorDate,
8700
+ authorName: record.authorName,
8701
+ authorEmail: record.authorEmail,
8702
+ coAuthors: record.coAuthors,
8703
+ reviewedBy: record.reviewedBy,
8704
+ oldBlob: entry.oldBlob === NULL_SHA ? null : entry.oldBlob,
8705
+ newBlob: entry.newBlob === NULL_SHA ? null : entry.newBlob,
8706
+ pathAtCommit: entry.newPath
8707
+ });
8708
+ if (entry.oldPath !== entry.newPath) tracked.set(rel, entry.oldPath);
8709
+ }
8710
+ }
8711
+ return histories;
8712
+ }
8713
+ function neededBlobs(histories) {
8714
+ const shas = /* @__PURE__ */ new Set();
8715
+ for (const history of histories.values()) {
8716
+ for (const h of history) {
8717
+ if (h.oldBlob !== null) shas.add(h.oldBlob);
8718
+ if (h.newBlob !== null) shas.add(h.newBlob);
8719
+ }
8720
+ }
8721
+ return shas;
8722
+ }
8723
+ async function fetchBlobs(run2, shas) {
8724
+ const blobs = /* @__PURE__ */ new Map();
8725
+ if (shas.size === 0) return blobs;
8726
+ const out = await run2(["cat-file", "--batch"], `${[...shas].join("\n")}
8727
+ `);
8728
+ if (out.tooLarge) throw new HistoryTooLarge();
8729
+ if (out.code !== 0) throw new BlobsUnreadable(`exit ${String(out.code)}`);
8730
+ const buf = out.raw;
8731
+ let cursor = 0;
8732
+ while (cursor < buf.length) {
8733
+ const nl = buf.indexOf(10, cursor);
8734
+ if (nl === -1) break;
8735
+ const header = buf.toString("utf8", cursor, nl).split(" ");
8736
+ cursor = nl + 1;
8737
+ const sha = header[0];
8738
+ const kind = header[1];
8739
+ const size = Number(header[2]);
8740
+ if (sha === void 0 || kind !== "blob" || !Number.isFinite(size)) {
8741
+ continue;
8742
+ }
8743
+ blobs.set(sha, buf.toString("utf8", cursor, cursor + size));
8744
+ cursor += size + 1;
8745
+ }
8746
+ return blobs;
8747
+ }
8748
+ var git = (value, evidence) => ({
8749
+ value,
8750
+ source: "git",
8751
+ evidence
8752
+ });
8753
+ function judge(input, root, history, blobs, now) {
8754
+ const fenced = input.extracted.fenced;
8755
+ const extractor = extractorFor(input);
8756
+ const blobBody = (sha) => sha === null ? null : bodyOf(blobs.get(sha) ?? "", fenced);
8757
+ const stampsOf = (sha) => {
8758
+ if (sha === null) return {};
8759
+ const content = blobs.get(sha);
8760
+ return content === void 0 ? {} : extractStamps(extractor, content, input.absPath);
8761
+ };
8762
+ const bodyChanging = [...history].reverse().filter((h) => !sameBody(blobBody(h.newBlob), blobBody(h.oldBlob)));
8763
+ const oldest = history[history.length - 1];
8764
+ const newestBody = bodyChanging[bodyChanging.length - 1];
8765
+ const head = history[0];
8766
+ const headBlob = head === void 0 ? null : head.newBlob;
8767
+ const created = oldest === void 0 ? null : createdOf(oldest, stampsOf(oldest.newBlob));
8768
+ const workingChanged = !sameBody(
8769
+ bodyOf(input.content, fenced),
8770
+ headBlob === null ? null : blobBody(headBlob)
8771
+ );
8772
+ let lastUpdated;
8773
+ let lastBodyCommit;
8774
+ if (workingChanged) {
8775
+ lastBodyCommit = null;
8776
+ const working = input.extracted.data["last-updated"];
8777
+ const committed = stampsOf(headBlob)["last-updated"];
8778
+ lastUpdated = working !== void 0 && working !== null && !sameValue(working, committed) ? git(working, "stamped in working tree") : git(localDate(now()), "uncommitted body change");
8779
+ } else if (newestBody === void 0) {
8780
+ lastBodyCommit = null;
8781
+ lastUpdated = null;
8782
+ } else {
8783
+ lastBodyCommit = newestBody.sha;
8784
+ lastUpdated = lastUpdatedOf(
8785
+ newestBody,
8786
+ stampsOf(newestBody.newBlob),
8787
+ stampsOf(newestBody.oldBlob)
8788
+ );
8789
+ }
8790
+ const authors = bodyChanging.length === 0 ? null : authorsOf(bodyChanging);
8791
+ let reviewedBy = null;
8792
+ let lastReviewed = null;
8793
+ if (!workingChanged && newestBody !== void 0 && newestBody.reviewedBy.length > 0) {
8794
+ const evidence = `Reviewed-by trailer in ${short(newestBody.sha)}`;
8795
+ reviewedBy = git(newestBody.reviewedBy.map(nameOf), evidence);
8796
+ lastReviewed = git(datePart(newestBody.authorDate), evidence);
8797
+ }
8798
+ return {
8799
+ created,
8800
+ "last-updated": lastUpdated,
8801
+ authors,
8802
+ "reviewed-by": reviewedBy,
8803
+ "last-reviewed": lastReviewed,
8804
+ lastBodyCommit,
8805
+ root
8806
+ };
8807
+ }
8808
+ function createdOf(oldest, stamps) {
8809
+ const stamp = stamps["created"];
8810
+ if (stamp !== void 0 && stamp !== null) {
8811
+ return git(stamp, `stamped in ${short(oldest.sha)}`);
8812
+ }
8813
+ const date = datePart(oldest.authorDate);
8814
+ return git(date, `added in ${short(oldest.sha)} (${date})`);
8815
+ }
8816
+ function lastUpdatedOf(commit, after, before) {
8817
+ const stamp = after["last-updated"];
8818
+ if (stamp !== void 0 && stamp !== null && !sameValue(stamp, before["last-updated"])) {
8819
+ return git(stamp, `stamped in ${short(commit.sha)}`);
8820
+ }
8821
+ const date = datePart(commit.authorDate);
8822
+ return git(date, `body changed in ${short(commit.sha)} (${date})`);
8823
+ }
8824
+ function authorsOf(bodyChanging) {
8825
+ const seen = /* @__PURE__ */ new Set();
8826
+ const names = [];
8827
+ const add = (name2, email) => {
8828
+ if (name2 === "" || name2.endsWith("[bot]")) return;
8829
+ const key = email === null || email === "" ? `name:${name2.toLowerCase()}` : email.toLowerCase();
8830
+ if (seen.has(key)) return;
8831
+ seen.add(key);
8832
+ names.push(name2);
8833
+ };
8834
+ for (const h of bodyChanging) {
8835
+ add(h.authorName, h.authorEmail);
8836
+ for (const co of h.coAuthors) add(nameOf(co), emailOf(co));
8837
+ }
8838
+ const n = bodyChanging.length;
8839
+ return git(names, `${n} body-changing ${n === 1 ? "commit" : "commits"}`);
8840
+ }
8841
+ function extractorFor(input) {
8842
+ return extractorByName(input.extracted.format) ?? extractorForExtension(extname2(input.absPath));
8843
+ }
8844
+ function extractStamps(extractor, content, filePath) {
8845
+ if (!extractor) return {};
8846
+ try {
8847
+ return extractor.extract(content, filePath).data;
8848
+ } catch {
8849
+ return {};
8850
+ }
8851
+ }
8852
+ function sameBody(a, b) {
8853
+ if (a === null || b === null) return a === b;
8854
+ return a.replace(/\r\n/g, "\n") === b.replace(/\r\n/g, "\n");
8855
+ }
8856
+ function sameValue(a, b) {
8857
+ return toJsonText(a) === toJsonText(b);
8858
+ }
8859
+ var short = (sha) => sha.slice(0, 7);
8860
+ var datePart = (iso) => iso.slice(0, 10);
8861
+ function localDate(d) {
8862
+ const pad = (n) => String(n).padStart(2, "0");
8863
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
8864
+ }
8865
+ function nameOf(trailer) {
8866
+ const lt = trailer.lastIndexOf("<");
8867
+ return (lt === -1 ? trailer : trailer.slice(0, lt)).trim();
8868
+ }
8869
+ function emailOf(trailer) {
8870
+ const m = /<([^>]*)>\s*$/.exec(trailer);
8871
+ return m?.[1]?.trim() ?? null;
8872
+ }
8873
+ function text(out) {
8874
+ try {
8875
+ return out.raw.toString("utf8");
8876
+ } catch {
8877
+ throw new HistoryTooLarge();
8878
+ }
8879
+ }
8880
+ function runGit(args, cwd, stdin, maxBytes) {
8881
+ return new Promise((settle, reject) => {
8882
+ let done = false;
8883
+ const finish = (out) => {
8884
+ if (done) return;
8885
+ done = true;
8886
+ settle(out);
8887
+ };
8888
+ const fail = (err) => {
8889
+ if (done) return;
8890
+ done = true;
8891
+ reject(err instanceof Error ? err : new Error(String(err)));
8892
+ };
8893
+ const failed = (missing) => ({
8894
+ code: null,
8895
+ raw: Buffer.alloc(0),
8896
+ missing,
8897
+ tooLarge: false
8898
+ });
8899
+ let child;
8900
+ try {
8901
+ child = spawn3("git", args, { cwd, windowsHide: true });
8902
+ } catch {
8903
+ finish(failed(true));
8904
+ return;
8905
+ }
8906
+ let chunks = [];
8907
+ let total = 0;
8908
+ let tooLarge = false;
8909
+ child.stdout.on("data", (chunk) => {
8910
+ if (tooLarge) return;
8911
+ total += chunk.length;
8912
+ if (total > maxBytes) {
8913
+ tooLarge = true;
8914
+ chunks = [];
8915
+ child.kill();
8916
+ return;
8917
+ }
8918
+ chunks.push(chunk);
8919
+ });
8920
+ child.stderr.resume();
8921
+ child.on("error", (err) => {
8922
+ finish(failed(err.code === "ENOENT"));
8923
+ });
8924
+ child.stdin.on("error", () => {
8925
+ });
8926
+ child.on("close", (code) => {
8927
+ try {
8928
+ const raw = tooLarge ? Buffer.alloc(0) : Buffer.concat(chunks);
8929
+ finish({ code, raw, missing: false, tooLarge });
8930
+ } catch (err) {
8931
+ fail(err);
8932
+ }
8933
+ });
8934
+ if (stdin === void 0) child.stdin.end();
8935
+ else child.stdin.end(stdin);
8936
+ });
8937
+ }
8938
+
8939
+ // src/meta/core/derive/index.ts
8940
+ var FIELD_SOURCES = {
8941
+ created: ["git"],
8942
+ "last-updated": ["git"],
8943
+ authors: ["git"],
8944
+ owner: ["codeowners"],
8945
+ "reviewed-by": ["github", "gitlab", "git"],
8946
+ "last-reviewed": ["github", "gitlab", "git"]
8947
+ };
8948
+ var REVIEW_SOURCES = ["github", "gitlab"];
8949
+ var HOST_NAME = {
8950
+ github: "GitHub",
8951
+ gitlab: "GitLab"
8952
+ };
8953
+ function sourcesFor(field, commands) {
8954
+ if (isBuiltinField(field)) return FIELD_SOURCES[field];
8955
+ if (commands !== void 0 && Object.hasOwn(commands, field)) return ["command"];
8956
+ return [];
8957
+ }
8958
+ function consultedSources(fields, sources, commands) {
8959
+ const needed = /* @__PURE__ */ new Set();
8960
+ for (const field of fields) {
8961
+ for (const s of sourcesFor(field, commands)) needed.add(s);
8962
+ }
8963
+ return sources.filter((s) => needed.has(s));
8964
+ }
8965
+ function assertSourcesAvailable(sources, hint) {
8966
+ for (const name2 of Object.keys(sources)) {
8967
+ const status = sources[name2];
8968
+ if (status === void 0 || status.available) continue;
8969
+ const reason = status.reason ?? "it could not answer";
8970
+ throw new DocmetaError(
8971
+ `${name2} source unavailable: ${reason}${hint === void 0 ? "" : `; ${hint}`}`
8972
+ );
8973
+ }
8974
+ }
8975
+ async function deriveMetadata(inputs, ctx) {
8976
+ const consulted = new Set(consultedSources(ctx.fields, ctx.sources, ctx.commands));
8977
+ const sources = {};
8978
+ let git2 = /* @__PURE__ */ new Map();
8979
+ if (consulted.has("git")) {
8980
+ const result = await deriveFromGit(inputs, { cwd: ctx.cwd, now: ctx.now });
8981
+ sources.git = result.status;
8982
+ git2 = result.records;
8983
+ }
8984
+ const rootCache = /* @__PURE__ */ new Map();
8985
+ const rootOf = (absPath) => {
8986
+ const dir = dirname7(absPath);
8987
+ let root = rootCache.get(dir);
8988
+ if (root === void 0) {
8989
+ root = findGitRoot(dir);
8990
+ rootCache.set(dir, root);
8991
+ }
8992
+ return root;
8993
+ };
8994
+ const rootFor = (input) => git2.get(input.label)?.root ?? rootOf(input.absPath);
8995
+ let owners = /* @__PURE__ */ new Map();
8996
+ if (consulted.has("codeowners")) {
8997
+ const result = await deriveFromCodeowners(inputs, {
8998
+ explicit: ctx.codeowners,
8999
+ configDir: ctx.configDir,
9000
+ rootOf
9001
+ });
9002
+ sources.codeowners = result.status;
9003
+ owners = result.records;
9004
+ }
9005
+ const requested = REVIEW_SOURCES.filter((s) => consulted.has(s));
9006
+ let reviews = /* @__PURE__ */ new Map();
9007
+ if (requested.length > 0) {
9008
+ if (!consulted.has("git")) {
9009
+ for (const s of requested) {
9010
+ sources[s] = {
9011
+ available: false,
9012
+ reason: `the ${s} source needs the git source to find each document's commits; add git to sources`
9013
+ };
9014
+ }
9015
+ } else if (sources.git?.available === false) {
9016
+ for (const s of requested) {
9017
+ sources[s] = {
9018
+ available: false,
9019
+ reason: `the git source could not answer, so the ${s} source cannot locate any commit`
9020
+ };
9021
+ }
9022
+ } else {
9023
+ const result = await deriveReviewsByRoot(inputs, git2, rootFor, ctx, requested);
9024
+ Object.assign(sources, result.status);
9025
+ reviews = result.records;
9026
+ }
9027
+ }
9028
+ let commanded = /* @__PURE__ */ new Map();
9029
+ const doomed = Object.values(sources).some((s) => !s.available);
9030
+ if (consulted.has("command") && ctx.commands !== void 0 && !doomed) {
9031
+ const wanted = new Set(ctx.fields);
9032
+ const commands = Object.fromEntries(
9033
+ Object.entries(ctx.commands).filter(([field]) => wanted.has(field))
9034
+ );
9035
+ if (Object.keys(commands).length > 0) {
9036
+ const result = await deriveFromCommands(inputs, commands, { cwd: ctx.configDir ?? ctx.cwd });
9037
+ sources.command = result.status;
9038
+ commanded = result.records;
9039
+ }
9040
+ }
9041
+ const records = /* @__PURE__ */ new Map();
9042
+ for (const input of inputs) {
9043
+ const fields = {};
9044
+ const g = git2.get(input.label);
9045
+ const r = reviews.get(input.label);
9046
+ const c = commanded.get(input.label);
9047
+ for (const field of ctx.fields) {
9048
+ let value = null;
9049
+ for (const source of sourcesFor(field, ctx.commands)) {
9050
+ if (!consulted.has(source)) continue;
9051
+ const candidate = valueFrom(source, field, g, r, owners.get(input.label), c);
9052
+ if (candidate !== null) {
9053
+ value = candidate;
9054
+ break;
9055
+ }
9056
+ }
9057
+ fields[field] = value;
9058
+ }
9059
+ records.set(input.label, { file: input.label, fields });
9060
+ }
9061
+ return { records, sources };
9062
+ }
9063
+ function valueFrom(source, field, git2, reviews, owner, commanded) {
9064
+ switch (source) {
9065
+ case "git":
9066
+ if (git2 === void 0 || !isBuiltinField(field) || field === "owner") return null;
9067
+ return git2[field];
9068
+ case "command":
9069
+ return commanded?.[field] ?? null;
9070
+ case "codeowners":
9071
+ return field === "owner" ? owner ?? null : null;
9072
+ case "github":
9073
+ case "gitlab": {
9074
+ if (reviews === void 0) return null;
9075
+ if (field !== "reviewed-by" && field !== "last-reviewed") return null;
9076
+ const fact = reviews[field];
9077
+ return fact !== null && fact.source === source ? fact : null;
9078
+ }
9079
+ }
9080
+ }
9081
+ async function deriveReviewsByRoot(inputs, git2, rootFor, ctx, requested) {
9082
+ const byRoot = /* @__PURE__ */ new Map();
9083
+ for (const input of inputs) {
9084
+ const root = rootFor(input);
9085
+ if (root === null) continue;
9086
+ const bucket = byRoot.get(root) ?? [];
9087
+ bucket.push(input);
9088
+ byRoot.set(root, bucket);
9089
+ }
9090
+ const status = {};
9091
+ const records = /* @__PURE__ */ new Map();
9092
+ const unavailable3 = (which, reason) => {
9093
+ for (const s of which) status[s] = { available: false, reason };
9094
+ return { status, records: /* @__PURE__ */ new Map() };
9095
+ };
9096
+ if (byRoot.size === 0) {
9097
+ return unavailable3(requested, "no git repository contains the documents");
9098
+ }
9099
+ const cacheDir = resolve9(ctx.configDir ?? ctx.cwd, REVIEW_CACHE_DIR);
9100
+ for (const [root, bucket] of byRoot) {
9101
+ const client = ctx.reviews ?? await createReviewClient(root, { cwd: ctx.cwd, cache: ctx.cache, cacheDir, requested });
9102
+ const identity = await client.detect();
9103
+ if (identity === null) {
9104
+ const s = await client.status();
9105
+ return unavailable3(requested, s.reason ?? "no origin remote to tell GitHub from GitLab");
9106
+ }
9107
+ const source = identity.kind;
9108
+ if (!requested.includes(source)) {
9109
+ return unavailable3(
9110
+ requested,
9111
+ `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`
9112
+ );
9113
+ }
9114
+ const result = await deriveFromReviews(
9115
+ bucket.map((input) => ({
9116
+ label: input.label,
9117
+ sha: git2.get(input.label)?.lastBodyCommit ?? null
9118
+ })),
9119
+ client
9120
+ );
9121
+ if (!result.status.available) {
9122
+ status[source] = result.status;
9123
+ return { status, records: /* @__PURE__ */ new Map() };
9124
+ }
9125
+ status[source] = result.status;
9126
+ for (const [label, facts] of result.records) records.set(label, facts);
9127
+ }
9128
+ return { status, records };
9129
+ }
9130
+
9131
+ // src/meta/core/derive/config.ts
9132
+ var DEFAULT_COMMAND_TIMEOUT_MS = 6e4;
9133
+ function commandsOf(config) {
9134
+ if (config?.commands === void 0) return void 0;
9135
+ const out = {};
9136
+ for (const [field, c] of Object.entries(config.commands)) {
9137
+ out[field] = {
9138
+ run: c.run,
9139
+ timeoutMs: c.timeout === void 0 ? DEFAULT_COMMAND_TIMEOUT_MS : Math.round(c.timeout * 1e3)
9140
+ };
9141
+ }
9142
+ return out;
9143
+ }
9144
+
9145
+ // src/meta/core/derive/table.ts
9146
+ var DERIVED_VIEW = "derived";
9147
+ var DERIVED_ROWS = "_derived_rows";
9148
+ var RESOLVED_VIEW = "resolved";
9149
+ var ORIGIN_COLUMN = "_origin";
9150
+ var SOURCES_COLUMN = "_sources";
9151
+ function derivedColumns(commands) {
9152
+ return ["_path", ...derivableFields(commands), SOURCES_COLUMN];
9153
+ }
9154
+ function mentionsDerived(sql) {
9155
+ return /\b(?:from|join|update|into|table|view)\s+(?:"derived"|derived\b)/i.test(sql);
9156
+ }
9157
+ function fieldsForSql(sql, fields) {
9158
+ if (sql.includes("*") || /\b_(?:sources|origin)\b/i.test(sql)) return [...fields];
9159
+ return fields.filter(
9160
+ (field) => new RegExp(`(?<![\\w-])${escapeRegExp(field)}(?![\\w-])`, "i").test(sql)
9161
+ );
9162
+ }
9163
+ function escapeRegExp(s) {
9164
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9165
+ }
9166
+ function createDerivedView(db, records, columns) {
9167
+ const quoted = columns.map(quoteIdent);
9168
+ db.exec(
9169
+ `CREATE TABLE ${DERIVED_ROWS} (${quoted.map((c, i) => i === 0 ? `${c} TEXT PRIMARY KEY` : `${c} TEXT`).join(", ")})`
9170
+ );
9171
+ const insert = db.prepare(
9172
+ `INSERT INTO ${DERIVED_ROWS} VALUES (${quoted.map(() => "?").join(", ")})`
9173
+ );
9174
+ const fields = columns.slice(1, -1);
9175
+ db.exec("BEGIN");
9176
+ let committed = false;
9177
+ try {
9178
+ for (const [label, record] of records) {
9179
+ const sources = {};
9180
+ const values = fields.map((field) => {
9181
+ const derived = record.fields[field];
9182
+ if (derived == null) return null;
9183
+ sources[field] = { source: derived.source, evidence: derived.evidence };
9184
+ return bindValue(derived.value);
9185
+ });
9186
+ insert.run(label, ...values, JSON.stringify(sources));
9187
+ }
9188
+ db.exec("COMMIT");
9189
+ committed = true;
9190
+ } finally {
9191
+ if (!committed) db.exec("ROLLBACK");
9192
+ }
9193
+ db.exec(`CREATE VIEW ${DERIVED_VIEW} AS SELECT * FROM ${DERIVED_ROWS}`);
9194
+ }
9195
+ function resolvedColumns(docsColumns, commands) {
9196
+ const columns = ["_path"];
9197
+ const seen = /* @__PURE__ */ new Set(["_path", ORIGIN_COLUMN, SOURCES_COLUMN]);
9198
+ for (const column of docsColumns) {
9199
+ if (seen.has(column) || RESERVED.has(column)) continue;
9200
+ seen.add(column);
9201
+ columns.push(column);
9202
+ }
9203
+ for (const field of derivableFields(commands)) {
9204
+ if (seen.has(field)) continue;
9205
+ seen.add(field);
9206
+ columns.push(field);
9207
+ }
9208
+ columns.push(ORIGIN_COLUMN, SOURCES_COLUMN);
9209
+ return columns;
9210
+ }
9211
+ function mentionsResolved(sql) {
9212
+ return /\b(?:from|join|update|into|table|view)\s+(?:"resolved"|resolved\b)/i.test(sql);
9213
+ }
9214
+ function jsonPath(field) {
9215
+ return `$.${JSON.stringify(field)}`;
9216
+ }
9217
+ function sqlString(value) {
9218
+ return `'${value.replaceAll("'", "''")}'`;
9219
+ }
9220
+ function createResolvedView(db, docsColumns, commands) {
9221
+ const columns = resolvedColumns(docsColumns, commands);
9222
+ const fields = columns.slice(1, -2);
9223
+ const inDocs = new Set(docsColumns);
9224
+ const inDerived = new Set(derivableFields(commands));
9225
+ const selects = [`d.${quoteIdent("_path")} AS ${quoteIdent("_path")}`];
9226
+ const origin = [];
9227
+ for (const field of fields) {
9228
+ const path = sqlString(jsonPath(field));
9229
+ const asserted = `json_type(d.${quoteIdent("_data")}, ${path}) IS NOT NULL`;
9230
+ const assertedValue = inDocs.has(field) ? `d.${quoteIdent(field)}` : `d.${quoteIdent("_data")} ->> ${path}`;
9231
+ if (inDerived.has(field)) {
9232
+ const derivedValue = `r.${quoteIdent(field)}`;
9233
+ selects.push(
9234
+ `CASE WHEN ${asserted} THEN ${assertedValue} ELSE ${derivedValue} END AS ${quoteIdent(field)}`
9235
+ );
9236
+ origin.push(
9237
+ `${sqlString(field)}, CASE WHEN ${asserted} THEN 'asserted' WHEN ${derivedValue} IS NOT NULL THEN 'derived' END`
9238
+ );
9239
+ } else {
9240
+ selects.push(`${assertedValue} AS ${quoteIdent(field)}`);
9241
+ origin.push(`${sqlString(field)}, CASE WHEN ${asserted} THEN 'asserted' END`);
9242
+ }
9243
+ }
9244
+ selects.push(
9245
+ `json_patch('{}', json_object(${origin.join(", ")})) AS ${quoteIdent(ORIGIN_COLUMN)}`
9246
+ );
9247
+ selects.push(`r.${quoteIdent(SOURCES_COLUMN)} AS ${quoteIdent(SOURCES_COLUMN)}`);
9248
+ db.exec(
9249
+ `CREATE VIEW ${RESOLVED_VIEW} AS SELECT ${selects.join(", ")} FROM docs d LEFT JOIN ${DERIVED_ROWS} r USING (${quoteIdent("_path")})`
9250
+ );
9251
+ }
9252
+ async function deriveForTable(inputs, ctx, hint, fields) {
9253
+ const derive = ctx.config?.derive;
9254
+ const commands = commandsOf(derive);
9255
+ const result = await deriveMetadata(inputs, {
9256
+ cwd: ctx.cwd,
9257
+ base: ctx.base,
9258
+ ...ctx.configDir !== void 0 ? { configDir: ctx.configDir } : {},
9259
+ sources: derive?.sources ?? [...DERIVE_SOURCES],
9260
+ fields,
9261
+ ...derive?.codeowners !== void 0 ? { codeowners: derive.codeowners } : {},
9262
+ ...commands !== void 0 ? { commands } : {},
9263
+ cache: ctx.cache,
9264
+ now: ctx.now ?? (() => /* @__PURE__ */ new Date())
9265
+ });
9266
+ assertSourcesAvailable(result.sources, hint);
9267
+ return result.records;
9268
+ }
9269
+
9270
+ // src/meta/commands/validate.ts
9271
+ import { existsSync as existsSync3 } from "node:fs";
9272
+ import { readFile as readFile7 } from "node:fs/promises";
9273
+ import { resolve as resolve11, extname as extname3 } from "node:path";
9274
+
9275
+ // src/meta/core/baseline.ts
9276
+ import { createHash as createHash4 } from "node:crypto";
9277
+ import { readFile as readFile6 } from "node:fs/promises";
9278
+ import { relative as relative9, resolve as resolve10 } from "node:path";
9279
+ var DEFAULT_BASELINE_PATH = ".manni-baseline.json";
9280
+ var LEGACY_BASELINE_PATH = ".docmeta-baseline.json";
9281
+ var BASELINE_VERSION = 1;
9282
+ var FINGERPRINT_RE = /^[0-9a-f]{16}$/;
9283
+ function canonicalFilePath(file, ctx) {
9284
+ if (!ctx) return file;
9285
+ const from = ctx.runBase ?? ctx.base;
9286
+ return relative9(ctx.base, resolve10(from, file)).replace(/\\/g, "/");
9287
+ }
9288
+ function canonicalSchemaRef(ref, ctx) {
9289
+ if (!ctx) return ref;
9290
+ if (classifyRef(ref).kind !== "file") return ref;
9291
+ const abs = resolve10(ctx.cwd, ref);
9292
+ return relative9(ctx.base, abs).replace(/\\/g, "/");
9293
+ }
9294
+ function fingerprint(e, ctx) {
9295
+ const parts = [
9296
+ canonicalSchemaRef(e.schema, ctx),
9297
+ e.instancePath,
9298
+ e.keyword,
9299
+ e.subject ?? ""
9300
+ ];
9301
+ return createHash4("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
9302
+ }
9303
+ function serializeBaseline(baseline) {
9304
+ const entries = /* @__PURE__ */ Object.create(null);
9305
+ for (const file of Object.keys(baseline.entries).sort()) {
9306
+ entries[file] = [...baseline.entries[file] ?? []].sort();
9307
+ }
9308
+ return `${JSON.stringify(
9309
+ {
9310
+ version: baseline.version,
9311
+ generatedWith: baseline.generatedWith,
9312
+ entries
9313
+ },
9314
+ null,
9315
+ 2
9316
+ )}
9317
+ `;
9318
+ }
9319
+ function bad(source, detail) {
9320
+ throw new DocmetaError(`Baseline "${source}": ${detail}`);
9321
+ }
9322
+ function parseBaseline(text2, source) {
9323
+ let raw;
9324
+ try {
9325
+ raw = JSON.parse(stripBom(text2));
9326
+ } catch (err) {
9327
+ bad(source, `invalid JSON: ${errorMessage(err)}`);
9328
+ }
9329
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
9330
+ bad(source, "top level must be an object.");
9331
+ }
9332
+ const obj = raw;
9333
+ if (obj.version !== BASELINE_VERSION) {
9334
+ bad(
9335
+ source,
9336
+ `unsupported version ${JSON.stringify(obj.version)} (this manni writes version ${BASELINE_VERSION}). Re-record it with \`manni meta validate --write-baseline\`.`
9337
+ );
9338
+ }
9339
+ const generatedWith = typeof obj.generatedWith === "string" ? obj.generatedWith : "";
9340
+ const rawEntries = obj.entries;
9341
+ if (typeof rawEntries !== "object" || rawEntries === null || Array.isArray(rawEntries)) {
9342
+ bad(source, '"entries" must be an object mapping file paths to fingerprints.');
9343
+ }
9344
+ const entries = /* @__PURE__ */ Object.create(null);
9345
+ for (const [file, value] of Object.entries(
9346
+ rawEntries
9347
+ )) {
9348
+ if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
9349
+ bad(source, `entries["${file}"] must be a list of fingerprint strings.`);
9350
+ }
9351
+ const malformed = value.find((v) => !FINGERPRINT_RE.test(v));
9352
+ if (malformed !== void 0) {
9353
+ bad(
9354
+ source,
9355
+ `entries["${file}"] contains ${JSON.stringify(malformed)}, which is not a fingerprint (16 lowercase hex characters).`
9356
+ );
9357
+ }
9358
+ entries[file] = value;
9359
+ }
9360
+ return { version: BASELINE_VERSION, generatedWith, entries };
9361
+ }
9362
+ async function readBaseline(absPath, source) {
9363
+ let text2;
9364
+ try {
9365
+ text2 = await readFile6(absPath, "utf8");
9366
+ } catch (err) {
9367
+ if (err.code === "ENOENT") return null;
9368
+ throw new DocmetaError(
9369
+ `Baseline "${source}" could not be read: ${errorMessage(err)}`
9370
+ );
9371
+ }
9372
+ return parseBaseline(text2, source);
9373
+ }
9374
+ async function writeBaselineFile(absPath, baseline, source) {
9375
+ try {
9376
+ await writeFileAtomic(absPath, serializeBaseline(baseline));
9377
+ } catch (err) {
9378
+ throw new DocmetaError(
9379
+ `Baseline "${source}" could not be written: ${errorMessage(err)}`
9380
+ );
9381
+ }
9382
+ }
7474
9383
  function buildBaseline(results, generatedWith, ctx) {
7475
9384
  const entries = /* @__PURE__ */ Object.create(null);
7476
9385
  for (const r of results) {
@@ -7586,8 +9495,8 @@ function synthesizeMessage(extras, row) {
7586
9495
  const pairs = extras.flatMap((c) => {
7587
9496
  const value = row[c];
7588
9497
  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}`];
9498
+ const text2 = typeof value === "string" ? value : typeof value === "number" || typeof value === "bigint" ? String(value) : JSON.stringify(value);
9499
+ return [`${c}=${text2}`];
7591
9500
  });
7592
9501
  return pairs.length > 0 ? pairs.join(", ") : "check matched";
7593
9502
  }
@@ -7597,9 +9506,26 @@ async function runChecks(checks, entries, ctx = {}) {
7597
9506
  const { DatabaseSync } = await loadSqlite();
7598
9507
  const db = new DatabaseSync(":memory:");
7599
9508
  try {
7600
- createDocsTable(db, entries, corpusDataColumns(entries));
9509
+ const dataColumns = corpusDataColumns(entries);
9510
+ createDocsTable(db, entries, dataColumns);
7601
9511
  registerLineFor(db, entries);
7602
9512
  createCollectionViews(db, collectCollections(entries, ctx));
9513
+ const wantsDerived = checks.filter(
9514
+ (c) => mentionsDerived(c.query) || mentionsResolved(c.query)
9515
+ );
9516
+ if (wantsDerived.length > 0) {
9517
+ if (ctx.derive === void 0) {
9518
+ throw new DocmetaError(
9519
+ `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.`
9520
+ );
9521
+ }
9522
+ const commands = ctx.commands;
9523
+ const readable = derivableFields(commands);
9524
+ const fields = /* @__PURE__ */ new Set();
9525
+ for (const c of wantsDerived) for (const f of fieldsForSql(c.query, readable)) fields.add(f);
9526
+ createDerivedView(db, await ctx.derive([...fields]), derivedColumns(commands));
9527
+ createResolvedView(db, dataColumns, commands);
9528
+ }
7603
9529
  db.exec("PRAGMA query_only = 1");
7604
9530
  const loaded = new Set(entries.map((e) => e.label));
7605
9531
  for (const check of checks) {
@@ -7609,17 +9535,17 @@ async function runChecks(checks, entries, ctx = {}) {
7609
9535
  `check "${check.name}": ${head.split(/\s/, 1)[0]?.toUpperCase() ?? "that statement"} is refused: it can write outside the docs table.`
7610
9536
  );
7611
9537
  }
7612
- let tokens;
9538
+ let tokens2;
7613
9539
  try {
7614
- tokens = collectNamedParameters(check.query);
9540
+ tokens2 = collectNamedParameters(check.query);
7615
9541
  } catch (err) {
7616
9542
  throw new DocmetaError(
7617
9543
  `check "${check.name}": ${errorMessage(err)}`
7618
9544
  );
7619
9545
  }
7620
- if (tokens.length > 0) {
9546
+ if (tokens2.length > 0) {
7621
9547
  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.`
9548
+ `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
9549
  );
7624
9550
  }
7625
9551
  let columns;
@@ -7664,17 +9590,17 @@ function parseErrorResult(file, format, message, keyword) {
7664
9590
  }
7665
9591
  function resolveBaselineRequest(opts, configured, configDir, cwd) {
7666
9592
  const named = (label) => ({
7667
- absPath: resolve9(cwd, label),
9593
+ absPath: resolve11(cwd, label),
7668
9594
  label
7669
9595
  });
7670
9596
  const implied = () => {
7671
9597
  const base = configDir ?? cwd;
7672
9598
  if (configured !== void 0) {
7673
- return { absPath: resolve9(base, configured), label: configured };
9599
+ return { absPath: resolve11(base, configured), label: configured };
7674
9600
  }
7675
- const current = resolve9(base, DEFAULT_BASELINE_PATH);
7676
- const legacy = resolve9(base, LEGACY_BASELINE_PATH);
7677
- if (!existsSync2(current) && existsSync2(legacy)) {
9601
+ const current = resolve11(base, DEFAULT_BASELINE_PATH);
9602
+ const legacy = resolve11(base, LEGACY_BASELINE_PATH);
9603
+ if (!existsSync3(current) && existsSync3(legacy)) {
7678
9604
  warn(
7679
9605
  `"${LEGACY_BASELINE_PATH}" is the pre-rename baseline file name. Rename it to "${DEFAULT_BASELINE_PATH}".`
7680
9606
  );
@@ -7693,6 +9619,13 @@ function resolveBaselineRequest(opts, configured, configDir, cwd) {
7693
9619
  if (configured) return { ...implied(), write: false };
7694
9620
  return null;
7695
9621
  }
9622
+ function once(fn) {
9623
+ let pending;
9624
+ return () => {
9625
+ pending ??= fn();
9626
+ return pending;
9627
+ };
9628
+ }
7696
9629
  async function runValidate(opts) {
7697
9630
  const cwd = opts.cwd ?? process.cwd();
7698
9631
  const {
@@ -7785,6 +9718,39 @@ async function runValidate(opts) {
7785
9718
  }
7786
9719
  const checksWillRun = configuredChecks.length > 0 && opts.checks !== false && !scoped;
7787
9720
  const checkEntries = [];
9721
+ const deriveConfig = config?.derive;
9722
+ const deriveWillRun = deriveConfig !== void 0 && deriveConfig.fields.length > 0 && opts.derive !== false;
9723
+ const derivedChecks = checksWillRun ? configuredChecks.filter((c) => mentionsDerived(c.query) || mentionsResolved(c.query)) : [];
9724
+ const checksNeedDerived = derivedChecks.length > 0;
9725
+ const deriveInputs = [];
9726
+ const keepDeriveInputs = deriveWillRun || checksNeedDerived;
9727
+ const deriveCommands = commandsOf(deriveConfig);
9728
+ const deriveFields = new Set(deriveWillRun ? deriveConfig.fields : []);
9729
+ const readable = derivableFields(deriveCommands);
9730
+ for (const c of derivedChecks) for (const f of fieldsForSql(c.query, readable)) deriveFields.add(f);
9731
+ const derivedOnce = once(async () => {
9732
+ const derived = await deriveMetadata(deriveInputs, {
9733
+ cwd,
9734
+ base,
9735
+ ...configDir !== void 0 ? { configDir } : {},
9736
+ sources: deriveConfig?.sources ?? [...DERIVE_SOURCES],
9737
+ fields: [...deriveFields],
9738
+ ...deriveConfig?.codeowners !== void 0 ? { codeowners: deriveConfig.codeowners } : {},
9739
+ ...deriveCommands !== void 0 ? { commands: deriveCommands } : {},
9740
+ cache: opts.cache ?? true,
9741
+ now: opts.now ?? (() => /* @__PURE__ */ new Date())
9742
+ });
9743
+ assertSourcesAvailable(
9744
+ derived.sources,
9745
+ deriveWillRun ? "pass --no-derive to skip the comparison, or narrow derive.sources" : "narrow derive.sources in manni.config.yaml"
9746
+ );
9747
+ for (const [name2, status] of Object.entries(derived.sources)) {
9748
+ if (status.available && status.reason !== void 0) {
9749
+ opts.onNotice?.(`${name2}: ${status.reason}`);
9750
+ }
9751
+ }
9752
+ return derived;
9753
+ });
7788
9754
  const processOne = async (label, content, extension) => {
7789
9755
  const extractor = forcedExtractor ?? extractorForExtension(extension);
7790
9756
  if (!extractor) {
@@ -7805,6 +9771,9 @@ async function runValidate(opts) {
7805
9771
  );
7806
9772
  return;
7807
9773
  }
9774
+ if (keepDeriveInputs && label !== STDIN_LABEL) {
9775
+ deriveInputs.push({ label, absPath: resolve11(base, label), content, extracted });
9776
+ }
7808
9777
  const merged = mergeExternalMetadata(label, extracted, externalMetadata, members, base);
7809
9778
  extracted = merged.extracted;
7810
9779
  for (const j of merged.joins) {
@@ -7885,8 +9854,8 @@ async function runValidate(opts) {
7885
9854
  await processOne(STDIN_LABEL, content, forcedExtractor.extensions[0] ?? "");
7886
9855
  }
7887
9856
  for (const file of files) {
7888
- const content = await readFile6(resolve9(base, file), "utf8");
7889
- await processOne(file, content, extname2(file));
9857
+ const content = await readFile7(resolve11(base, file), "utf8");
9858
+ await processOne(file, content, extname3(file));
7890
9859
  }
7891
9860
  if (externalMetadata) {
7892
9861
  const byLabel = new Map(results.map((r) => [r.file, r]));
@@ -7930,7 +9899,16 @@ async function runValidate(opts) {
7930
9899
  // corpus see the same collections.
7931
9900
  collections: declaredCollections,
7932
9901
  ...configDir !== void 0 ? { configDir } : {},
7933
- base
9902
+ base,
9903
+ // A check that names the `derived` table (0040) gets the same view
9904
+ // `query` builds, from the run's one derivation — which already
9905
+ // covers every field the checks can read, so the list handed back
9906
+ // here is not needed. Built only when asked for, because building
9907
+ // it spawns git.
9908
+ derive: async () => (await derivedOnce()).records,
9909
+ // Its columns include the configured commands' keys (0042), which a
9910
+ // check's context cannot read from a config since 0041.
9911
+ ...deriveCommands !== void 0 ? { commands: deriveCommands } : {}
7934
9912
  });
7935
9913
  const byFile = new Map(results.map((r) => [r.file, r]));
7936
9914
  for (const [file, errs] of findings) {
@@ -7945,6 +9923,24 @@ async function runValidate(opts) {
7945
9923
  }
7946
9924
  }
7947
9925
  }
9926
+ if (deriveWillRun && deriveInputs.length > 0) {
9927
+ const derived = await derivedOnce();
9928
+ const byFile = new Map(results.map((r) => [r.file, r]));
9929
+ for (const input of deriveInputs) {
9930
+ const record = derived.records.get(input.label);
9931
+ const result = byFile.get(input.label);
9932
+ if (!record || !result) continue;
9933
+ const findings = staleFindings(
9934
+ deriveConfig.fields.map(
9935
+ (field) => compareDerived(field, input.extracted.data[field], record.fields[field])
9936
+ ),
9937
+ input.extracted.lineFor
9938
+ );
9939
+ if (findings.length === 0) continue;
9940
+ result.errors.push(...findings);
9941
+ result.ok = false;
9942
+ }
9943
+ }
7948
9944
  const frame = { cwd, base: configDir ?? cwd, runBase: base };
7949
9945
  const { results: reported, baseline } = await settleBaseline(
7950
9946
  results,
@@ -8005,8 +10001,8 @@ async function settleBaseline(results, request, ctx) {
8005
10001
  }
8006
10002
 
8007
10003
  // 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";
10004
+ import { readFile as readFile8 } from "node:fs/promises";
10005
+ import { resolve as resolve12, extname as extname4 } from "node:path";
8010
10006
  async function runGet(opts) {
8011
10007
  const cwd = opts.cwd ?? process.cwd();
8012
10008
  if (opts.fields.length === 0) {
@@ -8066,6 +10062,11 @@ async function runGet(opts) {
8066
10062
  base,
8067
10063
  offline: opts.offline ?? config?.offline ?? false
8068
10064
  });
10065
+ const commands = commandsOf(config?.derive) ?? {};
10066
+ const deriving = opts.derived !== false;
10067
+ const derivableFields2 = deriving ? opts.fields.filter((f) => isBuiltinField(f) || Object.hasOwn(commands, f)) : [];
10068
+ const deriveInputs = [];
10069
+ const carried = /* @__PURE__ */ new Map();
8069
10070
  const readOne = (label, content, extension) => {
8070
10071
  const extractor = forced ?? extractorForExtension(extension);
8071
10072
  if (!extractor) {
@@ -8076,15 +10077,18 @@ async function runGet(opts) {
8076
10077
  const members = membersFor(label);
8077
10078
  let extracted;
8078
10079
  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;
10080
+ const own2 = extractor.extract(content, label, {
10081
+ elements: resolveElements(label, config, members)
10082
+ });
10083
+ if (deriving && label !== STDIN_LABEL) {
10084
+ deriveInputs.push({
10085
+ label,
10086
+ absPath: resolve12(base, label),
10087
+ content,
10088
+ extracted: own2
10089
+ });
10090
+ }
10091
+ extracted = mergeExternalMetadata(label, own2, externalMetadata, members, base).extracted;
8088
10092
  } catch (err) {
8089
10093
  if (err instanceof DocmetaError) throw err;
8090
10094
  const reason = err instanceof Error ? err.message : String(err);
@@ -8092,7 +10096,12 @@ async function runGet(opts) {
8092
10096
  return;
8093
10097
  }
8094
10098
  const values = {};
8095
- for (const f of opts.fields) values[f] = resolveField(extracted.data, f);
10099
+ const carries = {};
10100
+ for (const f of opts.fields) {
10101
+ values[f] = resolveField(extracted.data, f);
10102
+ carries[f] = carriesField(extracted.data, f);
10103
+ }
10104
+ carried.set(label, carries);
8096
10105
  out.push({ file: label, present: extracted.present, values });
8097
10106
  };
8098
10107
  if (usingStdin) {
@@ -8101,14 +10110,78 @@ async function runGet(opts) {
8101
10110
  "Reading from stdin (`-`) requires --as <format> to choose an extractor."
8102
10111
  );
8103
10112
  }
8104
- readOne("<stdin>", opts.stdinContent ?? "", forced.extensions[0] ?? "");
10113
+ readOne(STDIN_LABEL, opts.stdinContent ?? "", forced.extensions[0] ?? "");
8105
10114
  }
8106
10115
  for (const file of files) {
8107
- const content = await readFile7(resolve10(base, file), "utf8");
8108
- readOne(file, content, extname3(file));
10116
+ const content = await readFile8(resolve12(base, file), "utf8");
10117
+ readOne(file, content, extname4(file));
10118
+ }
10119
+ if (deriving) {
10120
+ await attachResolved(out, deriveInputs, {
10121
+ fields: opts.fields,
10122
+ derivable: derivableFields2,
10123
+ carried,
10124
+ run: {
10125
+ cwd,
10126
+ base,
10127
+ configDir,
10128
+ config,
10129
+ cache: opts.cache ?? true,
10130
+ now: opts.now ?? (() => /* @__PURE__ */ new Date())
10131
+ }
10132
+ });
8109
10133
  }
8110
10134
  return out;
8111
10135
  }
10136
+ async function attachResolved(out, inputs, opts) {
10137
+ const { run: run2 } = opts;
10138
+ const derive = run2.config?.derive;
10139
+ const commands = commandsOf(derive);
10140
+ const records = opts.derivable.length === 0 || inputs.length === 0 ? /* @__PURE__ */ new Map() : await (async () => {
10141
+ const result = await deriveMetadata(inputs, {
10142
+ cwd: run2.cwd,
10143
+ base: run2.base,
10144
+ ...run2.configDir !== void 0 ? { configDir: run2.configDir } : {},
10145
+ sources: derive?.sources ?? [...DERIVE_SOURCES],
10146
+ fields: opts.derivable,
10147
+ ...derive?.codeowners !== void 0 ? { codeowners: derive.codeowners } : {},
10148
+ ...commands !== void 0 ? { commands } : {},
10149
+ cache: run2.cache,
10150
+ now: run2.now
10151
+ });
10152
+ assertSourcesAvailable(
10153
+ result.sources,
10154
+ "pass --no-derived, or narrow derive.sources"
10155
+ );
10156
+ return result.records;
10157
+ })();
10158
+ for (const r of out) {
10159
+ if (r.error !== void 0) continue;
10160
+ const record = records.get(r.file);
10161
+ const derived = {};
10162
+ for (const field of opts.derivable) {
10163
+ derived[field] = record?.fields[field] ?? null;
10164
+ }
10165
+ const carries = opts.carried.get(r.file) ?? {};
10166
+ const resolved = {};
10167
+ const origin = {};
10168
+ for (const field of opts.fields) {
10169
+ const evidence = derived[field];
10170
+ if (carries[field] === true) {
10171
+ resolved[field] = r.values[field];
10172
+ origin[field] = "asserted";
10173
+ } else if (evidence != null) {
10174
+ resolved[field] = evidence.value;
10175
+ origin[field] = "derived";
10176
+ } else {
10177
+ resolved[field] = void 0;
10178
+ }
10179
+ }
10180
+ if (opts.derivable.length > 0) r.derived = derived;
10181
+ r.resolved = resolved;
10182
+ r.origin = origin;
10183
+ }
10184
+ }
8112
10185
  function resolveField(data, field) {
8113
10186
  const segments = field.startsWith("/") ? parseJsonPointer(field) : field.split(".");
8114
10187
  const descended = descend(data, segments);
@@ -8118,6 +10191,23 @@ function resolveField(data, field) {
8118
10191
  }
8119
10192
  return void 0;
8120
10193
  }
10194
+ function carriesField(data, field) {
10195
+ const segments = field.startsWith("/") ? parseJsonPointer(field) : field.split(".");
10196
+ if (carriesPath(data, segments)) return true;
10197
+ return !field.startsWith("/") && field.includes(".") && Object.prototype.hasOwnProperty.call(data, field);
10198
+ }
10199
+ function carriesPath(data, segments) {
10200
+ const last = segments.at(-1);
10201
+ if (last === void 0) return true;
10202
+ const parent = segments.length === 1 ? data : descend(data, segments.slice(0, -1));
10203
+ if (Array.isArray(parent)) {
10204
+ return /^\d+$/.test(last) && Number(last) < parent.length;
10205
+ }
10206
+ if (parent !== null && typeof parent === "object") {
10207
+ return Object.prototype.hasOwnProperty.call(parent, last);
10208
+ }
10209
+ return false;
10210
+ }
8121
10211
  function descend(data, segments) {
8122
10212
  let current = data;
8123
10213
  for (const segment of segments) {
@@ -8140,9 +10230,9 @@ function parseJsonPointer(pointer) {
8140
10230
  }
8141
10231
 
8142
10232
  // 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";
10233
+ import { existsSync as existsSync4 } from "node:fs";
10234
+ import { mkdir as mkdir2, readFile as readFile9 } from "node:fs/promises";
10235
+ 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
10236
  import { parseDocument as parseDocument4, parse as parseYaml3, stringify } from "yaml";
8147
10237
  function getSchemasInfo() {
8148
10238
  return { builtins: listBuiltins(), formats: listFormats() };
@@ -8150,7 +10240,7 @@ function getSchemasInfo() {
8150
10240
  var DEFAULT_VENDOR_DIR = "./schema";
8151
10241
  var DEFAULT_CONFIG_NAME = "manni.config.yaml";
8152
10242
  function posixRelative(from, to) {
8153
- return relative8(from, to).split(sep4).join("/");
10243
+ return relative10(from, to).split(sep6).join("/");
8154
10244
  }
8155
10245
  function vendorFileName(url) {
8156
10246
  let parsed2;
@@ -8175,20 +10265,20 @@ function vendorFileName(url) {
8175
10265
  if (!name2.toLowerCase().endsWith(".json")) name2 += ".json";
8176
10266
  return name2;
8177
10267
  }
8178
- async function assertNotIgnored(absFile, absDir, cwd, onNotice, text = VENDOR_IGNORE_TEXT) {
10268
+ async function assertNotIgnored(absFile, absDir, cwd, onNotice, text2 = VENDOR_IGNORE_TEXT) {
8179
10269
  const relFile = posixRelative(cwd, absFile);
8180
10270
  const relDir = posixRelative(cwd, absDir);
8181
10271
  const candidates = [relFile];
8182
10272
  if (relDir !== "" && !relDir.startsWith("..")) candidates.push(relDir);
8183
10273
  const answer = await gitIgnored(candidates, cwd);
8184
10274
  if (!answer.available) {
8185
- onNotice?.(text.unchecked(relDir || "."));
10275
+ onNotice?.(text2.unchecked(relDir || "."));
8186
10276
  return;
8187
10277
  }
8188
10278
  const ignoredDir = answer.ignored.has(relDir);
8189
10279
  const ignoredFile = answer.ignored.has(relFile);
8190
10280
  if (!ignoredDir && !ignoredFile) return;
8191
- throw new DocmetaError(text.refusal(ignoredDir ? relDir : relFile));
10281
+ throw new DocmetaError(text2.refusal(ignoredDir ? relDir : relFile));
8192
10282
  }
8193
10283
  var VENDOR_IGNORE_TEXT = {
8194
10284
  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 +10309,12 @@ function renderConfig(existing, entries, section) {
8219
10309
  doc.setIn(section === void 0 ? ["schemas"] : [section, "schemas"], entries);
8220
10310
  return doc.toString();
8221
10311
  }
8222
- function reparseConfig(text, source, section) {
10312
+ function reparseConfig(text2, source, section) {
8223
10313
  if (section === void 0) {
8224
- parseConfig(text, source);
10314
+ parseConfig(text2, source);
8225
10315
  return;
8226
10316
  }
8227
- const doc = parseYaml3(text);
10317
+ const doc = parseYaml3(text2);
8228
10318
  parseConfigValue(doc[section], source, section);
8229
10319
  }
8230
10320
  async function runVendorSchema(opts) {
@@ -8236,10 +10326,10 @@ async function runVendorSchema(opts) {
8236
10326
  );
8237
10327
  }
8238
10328
  const loaded = await loadConfig(opts.configPath, cwd);
8239
- const configPath = loaded?.path ?? resolve11(cwd, DEFAULT_CONFIG_NAME);
10329
+ const configPath = loaded?.path ?? resolve13(cwd, DEFAULT_CONFIG_NAME);
8240
10330
  const configDir = loaded?.dir ?? cwd;
8241
- const absDir = resolve11(cwd, opts.dir ?? DEFAULT_VENDOR_DIR);
8242
- const absFile = join5(absDir, vendorFileName(url));
10331
+ const absDir = resolve13(cwd, opts.dir ?? DEFAULT_VENDOR_DIR);
10332
+ const absFile = join6(absDir, vendorFileName(url));
8243
10333
  await assertNotIgnored(absFile, absDir, cwd, opts.onNotice);
8244
10334
  const fromConfig = posixRelative(configDir, absFile);
8245
10335
  const ref = isAbsolute4(fromConfig) || fromConfig === "" ? absFile : fromConfig.startsWith(".") ? fromConfig : `./${fromConfig}`;
@@ -8256,8 +10346,8 @@ async function runVendorSchema(opts) {
8256
10346
  );
8257
10347
  }
8258
10348
  let unchanged = false;
8259
- if (existsSync3(absFile)) {
8260
- const current = await readFile8(absFile);
10349
+ if (existsSync4(absFile)) {
10350
+ const current = await readFile9(absFile);
8261
10351
  unchanged = bytes.equals(current);
8262
10352
  if (!unchanged && !folded.replaced) {
8263
10353
  throw new DocmetaError(
@@ -8267,11 +10357,11 @@ async function runVendorSchema(opts) {
8267
10357
  }
8268
10358
  await mkdir2(absDir, { recursive: true });
8269
10359
  await writeFileAtomic(absFile, bytes);
8270
- const existing = loaded ? await readFile8(configPath, "utf8") : null;
10360
+ const existing = loaded ? await readFile9(configPath, "utf8") : null;
8271
10361
  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);
10362
+ const text2 = renderConfig(existing, folded.entries, section);
10363
+ reparseConfig(text2, posixRelative(cwd, configPath) || DEFAULT_CONFIG_NAME, section);
10364
+ await writeFileAtomic(configPath, text2);
8275
10365
  return {
8276
10366
  url,
8277
10367
  file: posixRelative(cwd, absFile),
@@ -8418,15 +10508,15 @@ async function runInferSchema(opts) {
8418
10508
  }
8419
10509
  let absOut;
8420
10510
  if (opts.out !== void 0) {
8421
- absOut = resolve11(cwd, opts.out);
10511
+ absOut = resolve13(cwd, opts.out);
8422
10512
  await assertNotIgnored(
8423
10513
  absOut,
8424
- dirname5(absOut),
10514
+ dirname8(absOut),
8425
10515
  cwd,
8426
10516
  opts.onNotice,
8427
10517
  DRAFT_IGNORE_TEXT
8428
10518
  );
8429
- if (existsSync3(absOut)) {
10519
+ if (existsSync4(absOut)) {
8430
10520
  throw new DocmetaError(
8431
10521
  `"${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
10522
  );
@@ -8505,8 +10595,8 @@ async function runInferSchema(opts) {
8505
10595
  scanOne(STDIN_LABEL, opts.stdinContent ?? "", forced.extensions[0] ?? "");
8506
10596
  }
8507
10597
  for (const file of files) {
8508
- const content = await readFile8(resolve11(base, file), "utf8");
8509
- scanOne(file, content, extname4(file));
10598
+ const content = await readFile9(resolve13(base, file), "utf8");
10599
+ scanOne(file, content, extname5(file));
8510
10600
  }
8511
10601
  for (const u of unreadable) {
8512
10602
  opts.onNotice?.(`could not read metadata from "${u.file}": ${u.message}`);
@@ -8547,7 +10637,7 @@ async function runInferSchema(opts) {
8547
10637
  };
8548
10638
  const keys = kept.map(({ sawEmptyString: _drop, ...rest }) => rest);
8549
10639
  if (absOut !== void 0) {
8550
- await mkdir2(dirname5(absOut), { recursive: true });
10640
+ await mkdir2(dirname8(absOut), { recursive: true });
8551
10641
  await writeFileAtomic(absOut, `${JSON.stringify(draft, null, 2)}
8552
10642
  `);
8553
10643
  }
@@ -8565,9 +10655,9 @@ async function runInferSchema(opts) {
8565
10655
 
8566
10656
  // src/meta/commands/query.ts
8567
10657
  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";
10658
+ import { existsSync as existsSync5 } from "node:fs";
10659
+ import { mkdir as mkdir3, open, readFile as readFile10, rename as rename2, rm as rm2 } from "node:fs/promises";
10660
+ import { dirname as dirname9, isAbsolute as isAbsolute5, resolve as resolve14, extname as extname6, sep as sep7 } from "node:path";
8571
10661
  import { parseDocument as parseDocument5, isMap as isMap4, isSeq as isSeq2, isScalar as isScalar3 } from "yaml";
8572
10662
  async function runQuery(opts) {
8573
10663
  const cwd = opts.cwd ?? process.cwd();
@@ -8672,16 +10762,11 @@ async function runQuery(opts) {
8672
10762
  );
8673
10763
  }
8674
10764
  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 });
10765
+ const own2 = extractor.extract(content, label, {
10766
+ elements: resolveElements(label, config, members)
10767
+ });
10768
+ const extracted = mergeExternalMetadata(label, own2, externalMetadata, members, base).extracted;
10769
+ entries.push({ label, extracted, extractor, own: own2, content });
8685
10770
  };
8686
10771
  if (usingStdin) {
8687
10772
  if (!forced) {
@@ -8698,15 +10783,17 @@ async function runQuery(opts) {
8698
10783
  readOne(STDIN_LABEL, opts.stdinContent ?? "", ext);
8699
10784
  }
8700
10785
  for (const file of files) {
8701
- const content = await readFile9(resolve12(base, file), "utf8");
8702
- readOne(file, content, extname5(file));
10786
+ const content = await readFile10(resolve14(base, file), "utf8");
10787
+ readOne(file, content, extname6(file));
8703
10788
  }
8704
- const db = opts.db === void 0 ? void 0 : { resolved: resolve12(cwd, opts.db), display: opts.db };
8705
- const run = await runSql(sql, entries, {
10789
+ const db = opts.db === void 0 ? void 0 : { resolved: resolve14(cwd, opts.db), display: opts.db };
10790
+ const run2 = await runSql(sql, entries, {
8706
10791
  target: db,
8707
10792
  params: boundParams,
8708
10793
  ...opts.schemas !== void 0 && opts.schemas.length > 0 ? { cliSchemas: opts.schemas } : {},
8709
10794
  write: !opts.dryRun,
10795
+ cache: opts.cache ?? true,
10796
+ ...opts.now !== void 0 ? { now: opts.now } : {},
8710
10797
  base,
8711
10798
  config,
8712
10799
  cwd,
@@ -8718,9 +10805,10 @@ async function runQuery(opts) {
8718
10805
  collections,
8719
10806
  declaredCollections,
8720
10807
  memberships: membersFor,
10808
+ managed: new Set(config?.derive?.fields ?? []),
8721
10809
  onNotice: opts.onNotice
8722
10810
  });
8723
- return { ...run, frame: { cwd, base: configDir ?? cwd, runBase: base } };
10811
+ return { ...run2, frame: { cwd, base: configDir ?? cwd, runBase: base } };
8724
10812
  }
8725
10813
  function bindParams(params) {
8726
10814
  const out = /* @__PURE__ */ Object.create(null);
@@ -8759,6 +10847,8 @@ async function prepareDbTarget(resolved, display) {
8759
10847
  }
8760
10848
  var MissingCollectionView = class extends Error {
8761
10849
  };
10850
+ var MissingDerivedTable = class extends Error {
10851
+ };
8762
10852
  function refuseSqlError(err) {
8763
10853
  const message = errorMessage(err);
8764
10854
  if (message.includes("UNIQUE constraint failed: docs._path")) {
@@ -8767,8 +10857,18 @@ function refuseSqlError(err) {
8767
10857
  const viewWrite = /cannot modify (.+?) because it is a view/.exec(message);
8768
10858
  const viewName = viewWrite?.[1];
8769
10859
  if (viewName !== void 0) {
10860
+ if (viewName.toLowerCase() === DERIVED_VIEW) {
10861
+ throw new DocmetaError(
10862
+ `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.`
10863
+ );
10864
+ }
10865
+ if (viewName.toLowerCase() === RESOLVED_VIEW) {
10866
+ throw new DocmetaError(
10867
+ `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.`
10868
+ );
10869
+ }
8770
10870
  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('"', '""')}").`
10871
+ `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
10872
  );
8773
10873
  }
8774
10874
  throw new DocmetaError(`SQL error: ${message}`);
@@ -8781,7 +10881,7 @@ async function runSql(sql, entries, ctx) {
8781
10881
  );
8782
10882
  const { DatabaseSync } = await loadSqlite();
8783
10883
  if (target) {
8784
- await mkdir3(dirname6(target.resolved), { recursive: true });
10884
+ await mkdir3(dirname9(target.resolved), { recursive: true });
8785
10885
  await prepareDbTarget(target.resolved, target.display);
8786
10886
  }
8787
10887
  let db;
@@ -8797,6 +10897,7 @@ async function runSql(sql, entries, ctx) {
8797
10897
  files: entries.length,
8798
10898
  columns: SYSTEM_COLUMNS.length + dataColumns.length
8799
10899
  } : void 0;
10900
+ const derivedTable = { built: false };
8800
10901
  try {
8801
10902
  createDocsTable(db, entries, dataColumns);
8802
10903
  let viewsBuilt = false;
@@ -8813,9 +10914,36 @@ async function runSql(sql, entries, ctx) {
8813
10914
  viewsBuilt = true;
8814
10915
  };
8815
10916
  if (target) buildViews();
10917
+ const buildDerived = async () => {
10918
+ if (derivedTable.built) return;
10919
+ const inputs = entries.filter((e) => e.label !== STDIN_LABEL).map((e) => ({
10920
+ label: e.label,
10921
+ absPath: resolve14(ctx.base, e.label),
10922
+ content: e.content,
10923
+ extracted: e.own
10924
+ }));
10925
+ const commands = commandsOf(ctx.config?.derive);
10926
+ const records = await deriveForTable(
10927
+ inputs,
10928
+ {
10929
+ cwd: ctx.cwd,
10930
+ base: ctx.base,
10931
+ ...ctx.configDir !== void 0 ? { configDir: ctx.configDir } : {},
10932
+ config: ctx.config,
10933
+ cache: ctx.cache,
10934
+ ...ctx.now !== void 0 ? { now: ctx.now } : {}
10935
+ },
10936
+ "narrow derive.sources in manni.config.yaml",
10937
+ fieldsForSql(sql, derivableFields(commands))
10938
+ );
10939
+ createDerivedView(db, records, derivedColumns(commands));
10940
+ createResolvedView(db, dataColumns, commands);
10941
+ derivedTable.built = true;
10942
+ };
8816
10943
  if (sql === "") {
8817
10944
  return { columns: [], rows: [], ...dbInfo ? { db: dbInfo } : {} };
8818
10945
  }
10946
+ if (mentionsDerived(sql) || mentionsResolved(sql)) await buildDerived();
8819
10947
  const head = stripLeadingTrivia(sql);
8820
10948
  if (/^(attach|vacuum)\b/i.test(head)) {
8821
10949
  throw new DocmetaError(
@@ -8842,6 +10970,10 @@ async function runSql(sql, entries, ctx) {
8842
10970
  if (missing !== void 0 && !viewsBuilt && namesConfiguredCollection(missing, ctx.declaredCollections)) {
8843
10971
  throw new MissingCollectionView();
8844
10972
  }
10973
+ const missingBare = missing?.replace(/^(main|temp)\./i, "").toLowerCase();
10974
+ if (!derivedTable.built && (missingBare === DERIVED_VIEW || missingBare === RESOLVED_VIEW)) {
10975
+ throw new MissingDerivedTable();
10976
+ }
8845
10977
  refuseSqlError(err);
8846
10978
  }
8847
10979
  const before = snapshotRows(db);
@@ -8900,11 +11032,20 @@ async function runSql(sql, entries, ctx) {
8900
11032
  try {
8901
11033
  return await runOnce();
8902
11034
  } catch (err) {
11035
+ if (err instanceof MissingDerivedTable) {
11036
+ await buildDerived();
11037
+ return await runOnce();
11038
+ }
8903
11039
  if (!(err instanceof MissingCollectionView)) throw err;
8904
11040
  buildViews();
8905
11041
  return await runOnce();
8906
11042
  }
8907
11043
  } finally {
11044
+ if (target && derivedTable.built) {
11045
+ db.exec(
11046
+ `DROP VIEW IF EXISTS ${RESOLVED_VIEW}; DROP VIEW IF EXISTS ${DERIVED_VIEW}; DROP TABLE IF EXISTS ${DERIVED_ROWS}`
11047
+ );
11048
+ }
8908
11049
  db.close();
8909
11050
  }
8910
11051
  }
@@ -9056,21 +11197,21 @@ function appendedColumnDef(before, after) {
9056
11197
  const comma = /^\s*,\s*/.exec(appended);
9057
11198
  return comma ? appended.slice(comma[0].length) : void 0;
9058
11199
  }
9059
- function skipWs(text, from) {
11200
+ function skipWs(text2, from) {
9060
11201
  let i = from;
9061
- while (i < text.length && /\s/.test(text[i] ?? "")) i++;
11202
+ while (i < text2.length && /\s/.test(text2[i] ?? "")) i++;
9062
11203
  return i;
9063
11204
  }
9064
- function indexOfCheck(text, from) {
11205
+ function indexOfCheck(text2, from) {
9065
11206
  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);
11207
+ while (i < text2.length) {
11208
+ const ch = text2[i];
11209
+ if (startsStringOrIdent(text2, i)) {
11210
+ i = skipStringOrIdent(text2, i);
11211
+ } else if (startsComment(text2, i)) {
11212
+ i = skipComment(text2, i);
9072
11213
  } else if (ch !== void 0 && /[A-Za-z_]/.test(ch)) {
9073
- const m = /^[A-Za-z_][A-Za-z0-9_]*/.exec(text.slice(i));
11214
+ const m = /^[A-Za-z_][A-Za-z0-9_]*/.exec(text2.slice(i));
9074
11215
  const word = m?.[0] ?? "";
9075
11216
  if (word.toUpperCase() === "CHECK") return i;
9076
11217
  i += Math.max(word.length, 1);
@@ -9080,17 +11221,17 @@ function indexOfCheck(text, from) {
9080
11221
  }
9081
11222
  return -1;
9082
11223
  }
9083
- function matchingParenEnd(text, open2) {
11224
+ function matchingParenEnd(text2, open2) {
9084
11225
  let depth = 0;
9085
11226
  let i = open2;
9086
- while (i < text.length) {
9087
- const ch = text[i];
9088
- if (startsStringOrIdent(text, i)) {
9089
- i = skipStringOrIdent(text, i);
11227
+ while (i < text2.length) {
11228
+ const ch = text2[i];
11229
+ if (startsStringOrIdent(text2, i)) {
11230
+ i = skipStringOrIdent(text2, i);
9090
11231
  continue;
9091
11232
  }
9092
- if (startsComment(text, i)) {
9093
- i = skipComment(text, i);
11233
+ if (startsComment(text2, i)) {
11234
+ i = skipComment(text2, i);
9094
11235
  continue;
9095
11236
  }
9096
11237
  if (ch === "(") depth++;
@@ -9110,9 +11251,9 @@ function enumForAddedColumn(catalogBefore, catalogAfter, key, mapped) {
9110
11251
  };
9111
11252
  const def = appendedColumnDef(catalogBefore, catalogAfter);
9112
11253
  if (def === void 0) {
9113
- const count = (text) => {
11254
+ const count = (text2) => {
9114
11255
  let n = 0;
9115
- for (let i2 = indexOfCheck(text, 0); i2 !== -1; i2 = indexOfCheck(text, i2 + 5)) {
11256
+ for (let i2 = indexOfCheck(text2, 0); i2 !== -1; i2 = indexOfCheck(text2, i2 + 5)) {
9116
11257
  n++;
9117
11258
  }
9118
11259
  return n;
@@ -9225,8 +11366,8 @@ var FORK_IGNORE_TEXT = {
9225
11366
  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
11367
  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
11368
  };
9228
- function matchEol(reference, text) {
9229
- return reference.includes("\r\n") ? text.replace(/\n/g, "\r\n") : text;
11369
+ function matchEol(reference, text2) {
11370
+ return reference.includes("\r\n") ? text2.replace(/\n/g, "\r\n") : text2;
9230
11371
  }
9231
11372
  function displayPath(ctx, abs) {
9232
11373
  const rel = posixRelative(ctx.base, abs);
@@ -9240,14 +11381,14 @@ function assertSchemaWriteWithin(ctx, abs) {
9240
11381
  );
9241
11382
  }
9242
11383
  function foldPath(p) {
9243
- const abs = resolve12(p);
11384
+ const abs = resolve14(p);
9244
11385
  return process.platform === "win32" ? abs.toLowerCase() : abs;
9245
11386
  }
9246
11387
  function filePinsByPath(ctx) {
9247
11388
  const byPath = /* @__PURE__ */ new Map();
9248
11389
  for (const [ref, pin] of collectSchemaPins(ctx.config)) {
9249
11390
  if (classifyRef(ref).kind !== "file") continue;
9250
- byPath.set(foldPath(resolve12(ctx.cwd, ref)), pin);
11391
+ byPath.set(foldPath(resolve14(ctx.cwd, ref)), pin);
9251
11392
  }
9252
11393
  return byPath;
9253
11394
  }
@@ -9275,13 +11416,13 @@ async function loadSetMembers(refs, ctx) {
9275
11416
  });
9276
11417
  continue;
9277
11418
  }
9278
- const abs = resolve12(ctx.cwd, ref);
11419
+ const abs = resolve14(ctx.cwd, ref);
9279
11420
  const canon = foldPath(abs);
9280
11421
  if (seenFiles.has(canon)) continue;
9281
11422
  seenFiles.add(canon);
9282
- let text;
11423
+ let text2;
9283
11424
  try {
9284
- text = await readFile9(abs, "utf8");
11425
+ text2 = await readFile10(abs, "utf8");
9285
11426
  } catch {
9286
11427
  throw new DocmetaError(
9287
11428
  `Schema file not found: "${ref}" (looked at ${abs}).`
@@ -9289,7 +11430,7 @@ async function loadSetMembers(refs, ctx) {
9289
11430
  }
9290
11431
  const pin = pins.get(canon);
9291
11432
  if (pin?.integrity !== void 0) {
9292
- if (integrityOf(Buffer.from(text, "utf8")) !== pin.integrity) {
11433
+ if (integrityOf(Buffer.from(text2, "utf8")) !== pin.integrity) {
9293
11434
  throw new DocmetaError(
9294
11435
  `Schema "${ref}" does not match its recorded integrity; its contents have moved since it was vendored. Re-vendor it before evolving it.`
9295
11436
  );
@@ -9297,7 +11438,7 @@ async function loadSetMembers(refs, ctx) {
9297
11438
  }
9298
11439
  let parsed2;
9299
11440
  try {
9300
- parsed2 = JSON.parse(stripBom(text));
11441
+ parsed2 = JSON.parse(stripBom(text2));
9301
11442
  } catch {
9302
11443
  throw new DocmetaError(
9303
11444
  `Schema "${ref}" is not valid JSON, so DDL cannot read it. Fix the file first.`
@@ -9312,7 +11453,7 @@ async function loadSetMembers(refs, ctx) {
9312
11453
  ref,
9313
11454
  kind: "file",
9314
11455
  abs,
9315
- text,
11456
+ text: text2,
9316
11457
  schema: parsed2
9317
11458
  });
9318
11459
  }
@@ -9480,16 +11621,16 @@ async function planSchemaMutation(ops, entries, ctx) {
9480
11621
  const [, name2, ver] = target.builtinId.split(":");
9481
11622
  const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, "-");
9482
11623
  const forkName = `${safe(name2 ?? "schema")}-${safe(ver ?? "0")}.local.json`;
9483
- schemaAbs = resolve12(ctx.configDir ?? ctx.cwd, "schemas", forkName);
11624
+ schemaAbs = resolve14(ctx.configDir ?? ctx.cwd, "schemas", forkName);
9484
11625
  assertSchemaWriteWithin(ctx, schemaAbs);
9485
- if (existsSync4(schemaAbs)) {
11626
+ if (existsSync5(schemaAbs)) {
9486
11627
  throw new DocmetaError(
9487
11628
  `"${displayPath(ctx, schemaAbs)}" already exists; refusing to overwrite it with a fork of ${target.builtinId}.`
9488
11629
  );
9489
11630
  }
9490
11631
  await assertNotIgnored(
9491
11632
  schemaAbs,
9492
- dirname6(schemaAbs),
11633
+ dirname9(schemaAbs),
9493
11634
  ctx.cwd,
9494
11635
  ctx.onNotice,
9495
11636
  FORK_IGNORE_TEXT
@@ -9636,8 +11777,8 @@ async function planConfigEdit(ctx, edit, runRefs) {
9636
11777
  if (configPath === void 0 || configDir === void 0) {
9637
11778
  return { changes: [], repointed: false };
9638
11779
  }
9639
- const text = await readFile9(configPath, "utf8");
9640
- const doc = parseDocument5(stripBom(text));
11780
+ const text2 = await readFile10(configPath, "utf8");
11781
+ const doc = parseDocument5(stripBom(text2));
9641
11782
  const display = displayPath(ctx, configPath);
9642
11783
  const rawRefOf = (item) => {
9643
11784
  if (isScalar3(item) && typeof item.value === "string") return item.value;
@@ -9647,7 +11788,7 @@ async function planConfigEdit(ctx, edit, runRefs) {
9647
11788
  }
9648
11789
  return void 0;
9649
11790
  };
9650
- const rebaseRaw = (raw) => classifyRef(raw).kind === "file" && !isAbsolute5(raw) ? resolve12(configDir, raw) : raw;
11791
+ const rebaseRaw = (raw) => classifyRef(raw).kind === "file" && !isAbsolute5(raw) ? resolve14(configDir, raw) : raw;
9651
11792
  const wantSet = runRefs.map(rebaseRaw).sort().join("\n");
9652
11793
  const seqResolvesToRunSet = (node) => {
9653
11794
  if (!isSeq2(node)) return false;
@@ -9692,7 +11833,7 @@ async function planConfigEdit(ctx, edit, runRefs) {
9692
11833
  if (!isMap4(item)) continue;
9693
11834
  const raw = rawRefOf(item);
9694
11835
  if (raw === void 0 || classifyRef(raw).kind !== "file") continue;
9695
- const abs = isAbsolute5(raw) ? raw : resolve12(configDir, raw);
11836
+ const abs = isAbsolute5(raw) ? raw : resolve14(configDir, raw);
9696
11837
  if (foldPath(abs) !== wantPath) continue;
9697
11838
  pinFrom = item.get("integrity");
9698
11839
  item.set("integrity", edit.integrity.value);
@@ -9724,8 +11865,8 @@ async function planConfigEdit(ctx, edit, runRefs) {
9724
11865
  return {
9725
11866
  write: {
9726
11867
  path: configPath,
9727
- content: matchEol(text, doc.toString({ lineWidth: 0 })),
9728
- expected: text
11868
+ content: matchEol(text2, doc.toString({ lineWidth: 0 })),
11869
+ expected: text2
9729
11870
  },
9730
11871
  changes,
9731
11872
  repointed
@@ -9908,12 +12049,12 @@ function buildChanges(diff, entries, sentinel, ctx, renameHints = [], booleanAdd
9908
12049
  }
9909
12050
  }
9910
12051
  validateNewPath(file, ctx.base);
9911
- if (!extractorForExtension(extname5(file))?.apply) {
12052
+ if (!extractorForExtension(extname6(file))?.apply) {
9912
12053
  throw new DocmetaError(
9913
12054
  `"${file}": no writable format for that extension.`
9914
12055
  );
9915
12056
  }
9916
- if (existsSync4(resolve12(ctx.base, file))) {
12057
+ if (existsSync5(resolve14(ctx.base, file))) {
9917
12058
  throw new DocmetaError(
9918
12059
  `"${file}" already exists; INSERT creates new files only.`
9919
12060
  );
@@ -9927,12 +12068,12 @@ function buildChanges(diff, entries, sentinel, ctx, renameHints = [], booleanAdd
9927
12068
  }
9928
12069
  for (const { from, to } of diff.renamedFiles) {
9929
12070
  validateNewPath(to, ctx.base);
9930
- if (extname5(to).toLowerCase() !== extname5(from).toLowerCase()) {
12071
+ if (extname6(to).toLowerCase() !== extname6(from).toLowerCase()) {
9931
12072
  throw new DocmetaError(
9932
12073
  `"${from}" -> "${to}": a rename may not change the extension.`
9933
12074
  );
9934
12075
  }
9935
- if (existsSync4(resolve12(ctx.base, to))) {
12076
+ if (existsSync5(resolve14(ctx.base, to))) {
9936
12077
  throw new DocmetaError(
9937
12078
  `"${to}" already exists; the rename would overwrite it.`
9938
12079
  );
@@ -9940,8 +12081,23 @@ function buildChanges(diff, entries, sentinel, ctx, renameHints = [], booleanAdd
9940
12081
  changes.push({ file: from, renamed: to, written: false });
9941
12082
  }
9942
12083
  refuseExternalMetadataWrites(changes, entries, ctx);
12084
+ refuseManagedWrites(changes, entries, ctx);
9943
12085
  return changes;
9944
12086
  }
12087
+ function refuseManagedWrites(changes, entries, ctx) {
12088
+ if (ctx.managed.size === 0) return;
12089
+ const refuse = (key) => {
12090
+ throw new DocmetaError(
12091
+ `"${key}" is managed by derive; run manni meta derive instead.`
12092
+ );
12093
+ };
12094
+ const data = new Map(entries.map((e) => [e.label, e.extracted.data]));
12095
+ for (const c of changes) {
12096
+ if ("schema" in c || "config" in c || "renamed" in c) continue;
12097
+ 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];
12098
+ for (const key of keys) if (ctx.managed.has(key)) refuse(key);
12099
+ }
12100
+ }
9945
12101
  function validateNewPath(p, base) {
9946
12102
  const refuse = (why) => {
9947
12103
  throw new DocmetaError(`"${p}" is not a usable path: ${why}.`);
@@ -9950,9 +12106,9 @@ function validateNewPath(p, base) {
9950
12106
  if (p.split(/[\\/]/).some((segment) => segment === "..")) {
9951
12107
  refuse("it traverses upward");
9952
12108
  }
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");
12109
+ const abs = resolve14(base, p);
12110
+ if (abs === resolve14(base)) refuse("it points at the corpus root, not a file");
12111
+ if (!abs.startsWith(resolve14(base) + sep7)) refuse("it escapes the corpus");
9956
12112
  }
9957
12113
  function refuseExternalMetadataWrites(changes, entries, ctx) {
9958
12114
  const index = ctx.externalMetadata;
@@ -9981,7 +12137,7 @@ function refuseExternalMetadataWrites(changes, entries, ctx) {
9981
12137
  continue;
9982
12138
  }
9983
12139
  if ("renamed" in c) {
9984
- const entry = index.byPath.get(resolve12(ctx.base, c.file));
12140
+ const entry = index.byPath.get(resolve14(ctx.base, c.file));
9985
12141
  if (entry !== void 0 && entry.size > 0) {
9986
12142
  const manifest = [...entry.values()][0]?.file ?? "manifest";
9987
12143
  throw new DocmetaError(
@@ -10034,15 +12190,15 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
10034
12190
  const pendingWrites = [];
10035
12191
  const pendingRenames = [];
10036
12192
  for (const [label, ops] of grouped) {
10037
- const path = resolve12(ctx.base, label);
12193
+ const path = resolve14(ctx.base, label);
10038
12194
  if (ops.renamedTo !== void 0) {
10039
- if (!existsSync4(path)) {
12195
+ if (!existsSync5(path)) {
10040
12196
  throw new DocmetaError(
10041
12197
  `"${label}" is gone from disk since it was read; re-run the query.`
10042
12198
  );
10043
12199
  }
10044
- const toDisk = resolve12(ctx.base, ops.renamedTo);
10045
- if (existsSync4(toDisk)) {
12200
+ const toDisk = resolve14(ctx.base, ops.renamedTo);
12201
+ if (existsSync5(toDisk)) {
10046
12202
  throw new DocmetaError(
10047
12203
  `"${ops.renamedTo}" appeared on disk since the plan; re-run the query.`
10048
12204
  );
@@ -10051,13 +12207,13 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
10051
12207
  continue;
10052
12208
  }
10053
12209
  if (ops.created !== void 0) {
10054
- const extractor = extractorForExtension(extname5(label));
12210
+ const extractor = extractorForExtension(extname6(label));
10055
12211
  if (!extractor?.apply) {
10056
12212
  throw new DocmetaError(
10057
12213
  `"${label}": no writable format for that extension.`
10058
12214
  );
10059
12215
  }
10060
- if (existsSync4(path)) {
12216
+ if (existsSync5(path)) {
10061
12217
  throw new DocmetaError(
10062
12218
  `"${label}" appeared on disk since the plan; re-run the query.`
10063
12219
  );
@@ -10076,7 +12232,7 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
10076
12232
  }
10077
12233
  const entry = byLabel.get(label);
10078
12234
  if (!entry) throw new DocmetaError(`No loaded entry for "${label}".`);
10079
- const content = await readFile9(path, "utf8");
12235
+ const content = await readFile10(path, "utf8");
10080
12236
  const members = ctx.memberships(label);
10081
12237
  const current = mergeExternalMetadata(
10082
12238
  label,
@@ -10144,7 +12300,7 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
10144
12300
  for (const w of schemaWrites) {
10145
12301
  const display = displayPath(ctx, w.path);
10146
12302
  if (w.expected === void 0) {
10147
- if (existsSync4(w.path)) {
12303
+ if (existsSync5(w.path)) {
10148
12304
  throw new DocmetaError(
10149
12305
  `"${display}" appeared on disk since the plan; re-run the query.`
10150
12306
  );
@@ -10153,7 +12309,7 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
10153
12309
  }
10154
12310
  let now;
10155
12311
  try {
10156
- now = await readFile9(w.path, "utf8");
12312
+ now = await readFile10(w.path, "utf8");
10157
12313
  } catch {
10158
12314
  now = void 0;
10159
12315
  }
@@ -10164,10 +12320,10 @@ async function applyChanges(changes, entries, ctx, schemaPlan) {
10164
12320
  }
10165
12321
  }
10166
12322
  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));
12323
+ for (const w of schemaWrites) dirs.add(dirname9(w.path));
12324
+ for (const r of pendingRenames) dirs.add(dirname9(r.to));
10169
12325
  for (const p of pendingWrites) {
10170
- if (p.ensureDir) dirs.add(dirname6(p.path));
12326
+ if (p.ensureDir) dirs.add(dirname9(p.path));
10171
12327
  }
10172
12328
  for (const d of dirs) await mkdir3(d, { recursive: true });
10173
12329
  const writeSchema = async () => {
@@ -10282,8 +12438,8 @@ function skipExpression(sql, from) {
10282
12438
  }
10283
12439
 
10284
12440
  // 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";
12441
+ import { readFile as readFile11 } from "node:fs/promises";
12442
+ import { resolve as resolve15, extname as extname7, join as join7 } from "node:path";
10287
12443
  import {
10288
12444
  completeValidatedJSON,
10289
12445
  makeProvider,
@@ -10485,6 +12641,7 @@ async function runFill(opts) {
10485
12641
  );
10486
12642
  const dryRun = Boolean(opts.dryRun);
10487
12643
  const only = opts.fields != null ? new Set(opts.fields) : void 0;
12644
+ const managed = new Set(config?.derive?.fields ?? []);
10488
12645
  const requestedProvider = opts.provider ?? config?.fill?.provider ?? DEFAULT_PROVIDER;
10489
12646
  const providerName = opts.local === true && requestedProvider === "auto" ? "llama-cpp" : requestedProvider;
10490
12647
  const model = opts.model ?? config?.fill?.model;
@@ -10550,7 +12707,7 @@ async function runFill(opts) {
10550
12707
  }
10551
12708
  return provider;
10552
12709
  };
10553
- const cache = opts.cache === false ? void 0 : new JsonCache(join6(cwd, CACHE_DIR), true, "manni");
12710
+ const cache = opts.cache === false ? void 0 : new JsonCache(join7(cwd, CACHE_DIR), true, "manni");
10554
12711
  const pricing = pricingFor(identity.model);
10555
12712
  const validator = new Validator(schemaOptions);
10556
12713
  let costUsd = 0;
@@ -10634,12 +12791,21 @@ async function runFill(opts) {
10634
12791
  }
10635
12792
  return errorResult(label, extractor.name, err.message);
10636
12793
  }
10637
- const candidates = collectCandidates(
12794
+ const proposed = collectCandidates(
10638
12795
  schemas,
10639
12796
  extracted.data,
10640
12797
  existingErrors,
10641
12798
  only
10642
12799
  );
12800
+ const managedSkips = proposed.filter((c) => managed.has(c.key)).map((c) => ({
12801
+ field: `/${c.key}`,
12802
+ required: c.required,
12803
+ confidence: 0,
12804
+ reasoning: "managed by derive",
12805
+ written: false,
12806
+ skipReason: "managed"
12807
+ }));
12808
+ const candidates = proposed.filter((c) => !managed.has(c.key));
10643
12809
  const ownedCandidates = externalMetadata ? candidates.flatMap((c) => {
10644
12810
  const owner = externalMetadata.owners.get(c.key)?.[0];
10645
12811
  return owner === void 0 ? [] : [{ key: c.key, file: owner.file }];
@@ -10658,7 +12824,7 @@ async function runFill(opts) {
10658
12824
  file: label,
10659
12825
  format: extractor.name,
10660
12826
  schemas: schemaSet,
10661
- fields: [],
12827
+ fields: managedSkips,
10662
12828
  changed: false,
10663
12829
  ...opts.includeContent ? { content } : {}
10664
12830
  };
@@ -10727,9 +12893,9 @@ async function runFill(opts) {
10727
12893
  break;
10728
12894
  }
10729
12895
  inFlight++;
10730
- let run;
12896
+ let run2;
10731
12897
  try {
10732
- run = await completeValidatedJSON({
12898
+ run2 = await completeValidatedJSON({
10733
12899
  provider: getProvider(),
10734
12900
  system: FILL_SYSTEM_PROMPT,
10735
12901
  user: buildUserPrompt({
@@ -10742,22 +12908,22 @@ async function runFill(opts) {
10742
12908
  schema: envelope,
10743
12909
  validate
10744
12910
  });
10745
- costUsd += costOfUsage(run.usage, pricing);
12911
+ costUsd += costOfUsage(run2.usage, pricing);
10746
12912
  turnsUsed++;
10747
12913
  } finally {
10748
12914
  inFlight--;
10749
12915
  }
10750
- if (run.error != null && looksLikeOverflow(run.error)) {
12916
+ if (run2.error != null && looksLikeOverflow(run2.error)) {
10751
12917
  overflowed = true;
10752
- failure = run.error;
12918
+ failure = run2.error;
10753
12919
  break;
10754
12920
  }
10755
- if (run.error != null || run.result == null) {
10756
- failure = run.error ?? "The model returned no proposal.";
12921
+ if (run2.error != null || run2.result == null) {
12922
+ failure = run2.error ?? "The model returned no proposal.";
10757
12923
  break;
10758
12924
  }
10759
- usageTotal = addUsage(usageTotal, run.usage);
10760
- collected.push(run.result);
12925
+ usageTotal = addUsage(usageTotal, run2.usage);
12926
+ collected.push(run2.result);
10761
12927
  }
10762
12928
  if (overflowed && attempt < 2) {
10763
12929
  usageTotal = void 0;
@@ -10790,7 +12956,7 @@ async function runFill(opts) {
10790
12956
  proposals = mergeProposals(sets);
10791
12957
  cache?.set(cacheKey, { proposals, ...usageTotal ? { usage: usageTotal } : {} });
10792
12958
  }
10793
- const fields = gate(candidates, proposals, threshold);
12959
+ const fields = [...managedSkips, ...gate(candidates, proposals, threshold)];
10794
12960
  const accepted = fields.filter((f) => f.written);
10795
12961
  if (accepted.length > 0) {
10796
12962
  const merged2 = { ...extracted.data, ...patchOf(accepted) };
@@ -10839,7 +13005,7 @@ async function runFill(opts) {
10839
13005
  }
10840
13006
  const changed = next !== content;
10841
13007
  if (changed && !dryRun && label !== "<stdin>") {
10842
- await writeFileAtomic(resolve13(base, label), next);
13008
+ await writeFileAtomic(resolve15(base, label), next);
10843
13009
  }
10844
13010
  return {
10845
13011
  file: label,
@@ -10861,8 +13027,8 @@ async function runFill(opts) {
10861
13027
  );
10862
13028
  }
10863
13029
  const fileResults = await mapConcurrent(files, concurrency, async (file) => {
10864
- const content = await readFile10(resolve13(base, file), "utf8");
10865
- return processOne(file, content, extname6(file));
13030
+ const content = await readFile11(resolve15(base, file), "utf8");
13031
+ return processOne(file, content, extname7(file));
10866
13032
  });
10867
13033
  results.push(...fileResults);
10868
13034
  return {
@@ -11045,8 +13211,8 @@ function plannedFor(schemas) {
11045
13211
  }
11046
13212
  function withRenamedRefs(node, renames) {
11047
13213
  if (Array.isArray(node)) {
11048
- const items = node;
11049
- return items.map((item) => withRenamedRefs(item, renames));
13214
+ const items2 = node;
13215
+ return items2.map((item) => withRenamedRefs(item, renames));
11050
13216
  }
11051
13217
  if (typeof node !== "object" || node === null) return node;
11052
13218
  const out = {};
@@ -11160,16 +13326,16 @@ function summarize(results, costUsd, cached) {
11160
13326
  cached
11161
13327
  };
11162
13328
  }
11163
- async function mapConcurrent(items, limit, fn) {
11164
- const out = new Array(items.length);
13329
+ async function mapConcurrent(items2, limit, fn) {
13330
+ const out = new Array(items2.length);
11165
13331
  let next = 0;
11166
13332
  const workers = Array.from(
11167
- { length: Math.max(1, Math.min(limit, items.length)) },
13333
+ { length: Math.max(1, Math.min(limit, items2.length)) },
11168
13334
  async () => {
11169
13335
  for (; ; ) {
11170
13336
  const i = next++;
11171
- const item = items[i];
11172
- if (i >= items.length || item === void 0) return;
13337
+ const item = items2[i];
13338
+ if (i >= items2.length || item === void 0) return;
11173
13339
  out[i] = await fn(item);
11174
13340
  }
11175
13341
  }
@@ -11178,6 +13344,252 @@ async function mapConcurrent(items, limit, fn) {
11178
13344
  return out;
11179
13345
  }
11180
13346
 
13347
+ // src/meta/commands/derive.ts
13348
+ import { readFile as readFile12 } from "node:fs/promises";
13349
+ import { extname as extname8, resolve as resolve16 } from "node:path";
13350
+ var SOURCE_HINT = "narrow --sources or --fields";
13351
+ async function runDerive(opts) {
13352
+ const cwd = opts.cwd ?? process.cwd();
13353
+ const { config, inputs, base, configDir, collections, declaredCollections, fromCollections } = await resolveRunConfig({
13354
+ cwd,
13355
+ configPath: opts.configPath,
13356
+ noConfig: opts.noConfig,
13357
+ inputs: opts.inputs,
13358
+ ...opts.collections !== void 0 ? { collections: opts.collections } : {},
13359
+ onConfigLoaded: opts.onConfigLoaded
13360
+ });
13361
+ if (inputs.includes(STDIN_TOKEN)) {
13362
+ throw new DocmetaError("cannot derive <stdin>: no history behind it");
13363
+ }
13364
+ if (inputs.length === 0) {
13365
+ throw new DocmetaError(
13366
+ "No files to derive. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml."
13367
+ );
13368
+ }
13369
+ const commands = commandsOf(config?.derive);
13370
+ const fields = resolveFields(opts.fields, config?.derive?.fields, commands);
13371
+ for (const field of fields) {
13372
+ const owner = manifestOwning(field, declaredCollections);
13373
+ if (owner !== void 0) {
13374
+ throw new DocmetaError(
13375
+ `"${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.`
13376
+ );
13377
+ }
13378
+ }
13379
+ const sources = resolveSources(opts.sources, config?.derive?.sources);
13380
+ const check = Boolean(opts.check);
13381
+ const dryRun = Boolean(opts.dryRun) || check;
13382
+ const forcedExtractor = opts.as ? extractorByName(opts.as) : void 0;
13383
+ if (opts.as && !forcedExtractor) {
13384
+ throw new DocmetaError(
13385
+ `Unknown format "${opts.as}". Known formats: ${listFormats().map((f) => f.name).join(", ")}.`
13386
+ );
13387
+ }
13388
+ const allowEmpty = opts.allowEmpty ?? config?.allowEmpty;
13389
+ const exts = opts.exts ?? forcedExtractor?.extensions;
13390
+ const exclude = opts.exclude ?? [];
13391
+ const membersFor = (label) => memberOf(collections, configDir ?? cwd, base, label);
13392
+ const { files: walked, gitignoreSkipped } = await resolveTargetSet({
13393
+ inputs,
13394
+ exts,
13395
+ exclude,
13396
+ cwd: base,
13397
+ allowEmpty,
13398
+ ...gitignoreOptions({
13399
+ flag: opts.respectGitignore,
13400
+ configured: config?.respectGitignore,
13401
+ onNotice: opts.onNotice
13402
+ })
13403
+ });
13404
+ const files = fromCollections ? retainMembers(walked, membersFor) : walked;
13405
+ assertNonEmpty({
13406
+ files,
13407
+ inputs,
13408
+ usingStdin: false,
13409
+ allowEmpty,
13410
+ exclude,
13411
+ exts,
13412
+ gitignoreSkipped,
13413
+ action: "derived"
13414
+ });
13415
+ const loaded = /* @__PURE__ */ new Map();
13416
+ const errors = /* @__PURE__ */ new Map();
13417
+ for (const label of files) {
13418
+ const absPath = resolve16(base, label);
13419
+ const extractor = forcedExtractor ?? extractorForExtension(extname8(label));
13420
+ if (!extractor) {
13421
+ errors.set(label, {
13422
+ format: "unknown",
13423
+ message: `Unsupported file type "${extname8(label)}". Supported: ${supportedExtensions().join(", ")}. Use --as to override.`
13424
+ });
13425
+ continue;
13426
+ }
13427
+ const content = await readFile12(absPath, "utf8");
13428
+ const elements = resolveElements(label, config, membersFor(label));
13429
+ try {
13430
+ loaded.set(label, {
13431
+ label,
13432
+ absPath,
13433
+ content,
13434
+ format: extractor.name,
13435
+ elements,
13436
+ extracted: extractor.extract(content, label, { elements }),
13437
+ apply: extractor.apply
13438
+ });
13439
+ } catch (err) {
13440
+ errors.set(label, { format: extractor.name, message: errorMessage(err) });
13441
+ }
13442
+ }
13443
+ const derived = await deriveMetadata([...loaded.values()], {
13444
+ cwd,
13445
+ base,
13446
+ configDir,
13447
+ sources,
13448
+ fields,
13449
+ codeowners: config?.derive?.codeowners,
13450
+ commands,
13451
+ cache: opts.cache ?? true,
13452
+ now: opts.now ?? (() => /* @__PURE__ */ new Date()),
13453
+ reviews: opts.reviews
13454
+ });
13455
+ assertSourcesAvailable(derived.sources, SOURCE_HINT);
13456
+ for (const [name2, status] of Object.entries(derived.sources)) {
13457
+ if (status.available && status.reason !== void 0) {
13458
+ opts.onNotice?.(`${name2}: ${status.reason}`);
13459
+ }
13460
+ }
13461
+ const results = [];
13462
+ for (const label of files) {
13463
+ const failure = errors.get(label);
13464
+ if (failure) {
13465
+ results.push(errorResult2(label, failure.format, failure.message, check));
13466
+ continue;
13467
+ }
13468
+ const doc = loaded.get(label);
13469
+ if (!doc) continue;
13470
+ const record = derived.records.get(label);
13471
+ const compared = fields.map(
13472
+ (field) => compareDerived(field, doc.extracted.data[field], record?.fields[field])
13473
+ );
13474
+ const pending = compared.filter((f) => f.status === "stale" || f.status === "unset");
13475
+ let changed = false;
13476
+ if (pending.length > 0) {
13477
+ if (typeof doc.apply !== "function") {
13478
+ if (!dryRun) {
13479
+ results.push(
13480
+ errorResult2(
13481
+ label,
13482
+ doc.format,
13483
+ `The "${doc.format}" format is read-only; manni meta derive cannot write metadata back to it.`,
13484
+ check,
13485
+ compared
13486
+ )
13487
+ );
13488
+ continue;
13489
+ }
13490
+ changed = true;
13491
+ } else {
13492
+ const patch = {};
13493
+ for (const f of pending) patch[f.field] = f.derived;
13494
+ let next;
13495
+ try {
13496
+ next = doc.apply(doc.content, patch, { filePath: label, elements: doc.elements });
13497
+ } catch (err) {
13498
+ results.push(errorResult2(label, doc.format, errorMessage(err), check, compared));
13499
+ continue;
13500
+ }
13501
+ changed = next !== doc.content;
13502
+ if (changed && !dryRun) {
13503
+ await writeFileAtomic(doc.absPath, next);
13504
+ for (const f of pending) f.written = true;
13505
+ }
13506
+ }
13507
+ }
13508
+ results.push({
13509
+ file: label,
13510
+ format: doc.format,
13511
+ fields: compared,
13512
+ changed,
13513
+ ...check ? { findings: staleFindings(compared, doc.extracted.lineFor) } : {}
13514
+ });
13515
+ }
13516
+ return {
13517
+ results,
13518
+ summary: summarize2(results),
13519
+ dryRun,
13520
+ check,
13521
+ sources: derived.sources,
13522
+ frame: { cwd, base: configDir ?? cwd, runBase: base }
13523
+ };
13524
+ }
13525
+ function resolveFields(flag, configured, commands) {
13526
+ const names = flag ?? configured;
13527
+ if (names === void 0 || names.length === 0) {
13528
+ throw new DocmetaError(
13529
+ "nothing to derive: set derive.fields in manni.config.yaml or pass --fields"
13530
+ );
13531
+ }
13532
+ const out = [];
13533
+ for (const name2 of names) {
13534
+ if (!isBuiltinField(name2) && !(commands !== void 0 && Object.hasOwn(commands, name2))) {
13535
+ throw new DocmetaError(
13536
+ `"${name2}" is not derivable; derivable fields are ${DERIVABLE_FIELDS.join(", ")}, or any key with an entry in derive.commands`
13537
+ );
13538
+ }
13539
+ if (!out.includes(name2)) out.push(name2);
13540
+ }
13541
+ return out;
13542
+ }
13543
+ function resolveSources(flag, configured) {
13544
+ const names = flag ?? configured ?? DERIVE_SOURCES;
13545
+ const out = [];
13546
+ for (const name2 of names) {
13547
+ if (!isDeriveSource(name2)) {
13548
+ throw new DocmetaError(
13549
+ `"${name2}" is not a source; sources are ${DERIVE_SOURCES.join(", ")}`
13550
+ );
13551
+ }
13552
+ if (!out.includes(name2)) out.push(name2);
13553
+ }
13554
+ return out;
13555
+ }
13556
+ function errorResult2(file, format, message, check, fields = []) {
13557
+ return {
13558
+ file,
13559
+ format,
13560
+ fields,
13561
+ changed: false,
13562
+ error: message,
13563
+ ...check ? {
13564
+ findings: [
13565
+ { schema: "(parse)", instancePath: "", message, keyword: "parse" }
13566
+ ]
13567
+ } : {}
13568
+ };
13569
+ }
13570
+ function summarize2(results) {
13571
+ const summary = {
13572
+ files: results.length,
13573
+ changed: 0,
13574
+ written: 0,
13575
+ stale: 0,
13576
+ unset: 0,
13577
+ unknown: 0,
13578
+ errors: 0
13579
+ };
13580
+ for (const r of results) {
13581
+ if (r.changed) summary.changed++;
13582
+ if (r.error !== void 0) summary.errors++;
13583
+ for (const f of r.fields) {
13584
+ if (f.written) summary.written++;
13585
+ if (f.status === "stale") summary.stale++;
13586
+ else if (f.status === "unset") summary.unset++;
13587
+ else if (f.status === "unknown") summary.unknown++;
13588
+ }
13589
+ }
13590
+ return summary;
13591
+ }
13592
+
11181
13593
  // src/meta/reporters/sarif.ts
11182
13594
  import { isAbsolute as isAbsolute6 } from "node:path";
11183
13595
 
@@ -11187,11 +13599,13 @@ var PARSE_ERROR_RULE = "manni/parse-error";
11187
13599
  var SCHEMA_ERROR_RULE = "manni/schema-error";
11188
13600
  var EXTERNAL_OWNED_RULE = "external:owned/external";
11189
13601
  var EXTERNAL_DUPLICATE_RULE = "external:duplicate/external";
13602
+ var DERIVED_STALE_RULE = "derived:stale/derived";
11190
13603
  var RESERVED_RULES = {
11191
13604
  [PARSE_ERROR_RULE]: "The document's metadata block could not be parsed.",
11192
13605
  [SCHEMA_ERROR_RULE]: "No schema set could be resolved for the document.",
11193
13606
  [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."
13607
+ [EXTERNAL_DUPLICATE_RULE]: "Two documents carry the same value of a manifest's join field, so one manifest entry matched both.",
13608
+ [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
13609
  };
11196
13610
  function ruleIdFor(e, frame) {
11197
13611
  if (e.schema === SYNTHETIC_SCHEMA) {
@@ -11518,14 +13932,14 @@ var FILL_FORMAT_LIST = formatList(FILL_FORMATS);
11518
13932
  function isFillFormat(value) {
11519
13933
  return FILL_FORMATS.includes(value);
11520
13934
  }
11521
- function renderFillJson(run) {
11522
- return JSON.stringify(run, null, 2);
13935
+ function renderFillJson(run2) {
13936
+ return JSON.stringify(run2, null, 2);
11523
13937
  }
11524
13938
  var score = (n) => n.toFixed(2);
11525
- function renderFillPretty(run, opts = {}) {
13939
+ function renderFillPretty(run2, opts = {}) {
11526
13940
  const c = palette(opts.color ?? false);
11527
13941
  const lines = [];
11528
- for (const result of run.results) {
13942
+ for (const result of run2.results) {
11529
13943
  if (result.error != null) {
11530
13944
  lines.push(`${c.red("\u2717")} ${result.file}`);
11531
13945
  lines.push(` ${result.error}`);
@@ -11549,7 +13963,7 @@ function renderFillPretty(run, opts = {}) {
11549
13963
  if (low.length > 0) {
11550
13964
  lines.push(
11551
13965
  c.dim(
11552
- ` below ${run.threshold}: ${low.map((f) => `${f.field} ${score(f.confidence)}`).join(", ")}`
13966
+ ` below ${run2.threshold}: ${low.map((f) => `${f.field} ${score(f.confidence)}`).join(", ")}`
11553
13967
  )
11554
13968
  );
11555
13969
  }
@@ -11557,14 +13971,14 @@ function renderFillPretty(run, opts = {}) {
11557
13971
  lines.push(c.dim(` ${f.skipReason ?? "skipped"}: ${f.field}`));
11558
13972
  }
11559
13973
  }
11560
- const { summary } = run;
11561
- const verb = run.dryRun ? "would be written" : "written";
13974
+ const { summary } = run2;
13975
+ const verb = run2.dryRun ? "would be written" : "written";
11562
13976
  const parts = [
11563
13977
  // Which provider ran decides what the proposals and the cost mean, and
11564
13978
  // under `auto` it is not something the user chose — so say it rather than
11565
13979
  // making them pass -f json to find out.
11566
- `${run.provider}/${run.model}`,
11567
- `Threshold ${run.threshold}`,
13980
+ `${run2.provider}/${run2.model}`,
13981
+ `Threshold ${run2.threshold}`,
11568
13982
  `${summary.files} file${summary.files === 1 ? "" : "s"}`,
11569
13983
  `${summary.written} field${summary.written === 1 ? "" : "s"} ${verb}`,
11570
13984
  `${summary.skipped} skipped`
@@ -11582,7 +13996,7 @@ function renderFillPretty(run, opts = {}) {
11582
13996
  )
11583
13997
  );
11584
13998
  }
11585
- if (run.turnsSpent) {
13999
+ if (run2.turnsSpent) {
11586
14000
  lines.push(
11587
14001
  c.yellow("--max-turns reached; some files were not processed.")
11588
14002
  );
@@ -11593,9 +14007,9 @@ function formatValue(value) {
11593
14007
  if (typeof value === "string") return value;
11594
14008
  return toJsonText(value) ?? String(value);
11595
14009
  }
11596
- function renderFillGithub(run) {
14010
+ function renderFillGithub(run2) {
11597
14011
  const lines = [];
11598
- for (const result of run.results) {
14012
+ for (const result of run2.results) {
11599
14013
  const file = escapeWorkflowCommandProperty(result.file);
11600
14014
  if (result.error != null) {
11601
14015
  lines.push(
@@ -11605,7 +14019,7 @@ function renderFillGithub(run) {
11605
14019
  }
11606
14020
  for (const f of result.fields) {
11607
14021
  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";
14022
+ const why = f.skipReason === "low-confidence" ? `confidence ${score(f.confidence)} is below the ${run2.threshold} threshold` : f.skipReason ?? "skipped";
11609
14023
  const msg = escapeWorkflowCommandMessage(
11610
14024
  `[fill] ${f.field} is required and was not filled (${why})`
11611
14025
  );
@@ -11614,14 +14028,14 @@ function renderFillGithub(run) {
11614
14028
  }
11615
14029
  return lines.join("\n");
11616
14030
  }
11617
- function renderFill(format, run, opts = {}) {
14031
+ function renderFill(format, run2, opts = {}) {
11618
14032
  switch (format) {
11619
14033
  case "json":
11620
- return renderFillJson(run);
14034
+ return renderFillJson(run2);
11621
14035
  case "github":
11622
- return renderFillGithub(run);
14036
+ return renderFillGithub(run2);
11623
14037
  case "pretty":
11624
- return renderFillPretty(run, opts);
14038
+ return renderFillPretty(run2, opts);
11625
14039
  default: {
11626
14040
  const unreachable = format;
11627
14041
  throw new DocmetaError(
@@ -11631,12 +14045,137 @@ function renderFill(format, run, opts = {}) {
11631
14045
  }
11632
14046
  }
11633
14047
 
14048
+ // src/meta/reporters/derive.ts
14049
+ var DERIVE_FORMATS = ["pretty", "json", "github", "sarif", "junit"];
14050
+ var DERIVE_FORMAT_LIST = formatList(DERIVE_FORMATS);
14051
+ function isDeriveFormat(value) {
14052
+ return DERIVE_FORMATS.includes(value);
14053
+ }
14054
+ var JUNIT_CLASSNAME = "manni.derive";
14055
+ function renderDerive(run2, format, opts = {}) {
14056
+ if (run2.check) {
14057
+ const { results, summary } = asValidation(run2);
14058
+ return render(format, results, summary, {
14059
+ classname: JUNIT_CLASSNAME,
14060
+ ...opts
14061
+ });
14062
+ }
14063
+ switch (format) {
14064
+ case "json":
14065
+ return JSON.stringify(run2, null, 2);
14066
+ case "pretty":
14067
+ return renderDerivePretty(run2, opts);
14068
+ case "github":
14069
+ case "sarif":
14070
+ case "junit":
14071
+ throw new DocmetaError(
14072
+ `${format} is a findings format, which only --check produces`
14073
+ );
14074
+ default: {
14075
+ const unreachable = format;
14076
+ throw new DocmetaError(
14077
+ `Unknown report format ${JSON.stringify(unreachable)}. Use ${DERIVE_FORMAT_LIST}.`
14078
+ );
14079
+ }
14080
+ }
14081
+ }
14082
+ function asValidation(run2) {
14083
+ const results = run2.results.map((r) => {
14084
+ const errors = r.findings ?? [];
14085
+ return { file: r.file, format: r.format, ok: errors.length === 0, schemas: [], errors };
14086
+ });
14087
+ const failed = results.filter((r) => !r.ok).length;
14088
+ return {
14089
+ results,
14090
+ summary: {
14091
+ files: results.length,
14092
+ passed: results.length - failed,
14093
+ failed,
14094
+ errors: results.reduce((n, r) => n + r.errors.length, 0)
14095
+ }
14096
+ };
14097
+ }
14098
+ var plural2 = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
14099
+ function renderDerivePretty(run2, opts = {}) {
14100
+ const c = palette(opts.color ?? false);
14101
+ const lines = [];
14102
+ for (const result of run2.results) {
14103
+ if (result.error !== void 0) {
14104
+ lines.push(`${c.red("\u2717")} ${result.file}`);
14105
+ lines.push(` ${result.error}`);
14106
+ continue;
14107
+ }
14108
+ const pending = result.fields.filter((f) => f.status === "stale" || f.status === "unset");
14109
+ if (pending.length === 0) {
14110
+ if (opts.quiet) continue;
14111
+ lines.push(`${result.file} ${c.dim("current")}${unknownNote(result, c.dim)}`);
14112
+ continue;
14113
+ }
14114
+ lines.push(result.file);
14115
+ const nameWidth = Math.max(...pending.map((f) => f.field.length));
14116
+ const changeWidth = Math.max(...pending.map((f) => change(f).length));
14117
+ for (const f of pending) {
14118
+ const name2 = c.cyan(f.field.padEnd(nameWidth));
14119
+ const from = f.status === "unset" ? c.dim("(unset)") : c.dim(fmt2(f.asserted));
14120
+ const to = c.green(fmt2(f.derived));
14121
+ const pad = " ".repeat(changeWidth - change(f).length);
14122
+ const trail = c.dim(`(${f.source ?? "a source"}: ${f.evidence ?? "no evidence"})`);
14123
+ lines.push(` ${name2} ${from} \u2192 ${to}${pad} ${trail}`);
14124
+ }
14125
+ const note = unknownNote(result, c.dim);
14126
+ if (note !== "") lines.push(` ${note}`);
14127
+ }
14128
+ const s = run2.summary;
14129
+ const parts = [plural2(s.files, "file")];
14130
+ if (run2.dryRun) {
14131
+ parts.push(`${s.changed} would change`, plural2(s.stale + s.unset, "field"));
14132
+ } else {
14133
+ parts.push(`${s.changed} changed`, `${plural2(s.written, "field")} written`);
14134
+ }
14135
+ if (s.errors > 0) parts.push(plural2(s.errors, "error"));
14136
+ let footer = parts.join(", ");
14137
+ if (run2.dryRun) footer += " \u2014 dry run, nothing written";
14138
+ if (lines.length > 0) lines.push("");
14139
+ lines.push(s.errors > 0 ? c.yellow(footer) : footer);
14140
+ return lines.join("\n");
14141
+ }
14142
+ function change(f) {
14143
+ const from = f.status === "unset" ? "(unset)" : fmt2(f.asserted);
14144
+ return `${from} \u2192 ${fmt2(f.derived)}`;
14145
+ }
14146
+ function unknownNote(result, dim) {
14147
+ const unknown = result.fields.filter((f) => f.status === "unknown").map((f) => f.field);
14148
+ if (unknown.length === 0) return "";
14149
+ return dim(` (${unknown.join(", ")}: no source could answer)`);
14150
+ }
14151
+ function fmt2(value) {
14152
+ if (typeof value === "string") return value;
14153
+ return toJsonText(value) ?? String(value);
14154
+ }
14155
+
11634
14156
  // src/meta/reporters/get.ts
11635
14157
  function stringifyValue(value) {
11636
14158
  if (value === void 0) return "(unset)";
11637
14159
  if (typeof value === "string") return value;
11638
14160
  return JSON.stringify(value);
11639
14161
  }
14162
+ function evidenceOf(d) {
14163
+ const repeated = ` (${stringifyValue(d.value)})`;
14164
+ return d.evidence.endsWith(repeated) ? d.evidence.slice(0, -repeated.length) : d.evidence;
14165
+ }
14166
+ function annotation(r, field) {
14167
+ const origin = r.origin?.[field];
14168
+ if (origin === void 0) return "";
14169
+ const d = r.derived?.[field] ?? null;
14170
+ if (origin === "derived") {
14171
+ return d == null ? "" : ` (derived, ${d.source}: ${evidenceOf(d)})`;
14172
+ }
14173
+ if (d == null) return " (asserted)";
14174
+ if (compareDerived(field, r.resolved?.[field], d).status === "current") {
14175
+ return " (asserted)";
14176
+ }
14177
+ return ` (asserted; ${d.source} says ${stringifyValue(d.value)}, ${evidenceOf(d)})`;
14178
+ }
11640
14179
  function renderGet(results, fields, opts = {}) {
11641
14180
  const c = palette(opts.color ?? false);
11642
14181
  const lines = [];
@@ -11645,9 +14184,14 @@ function renderGet(results, fields, opts = {}) {
11645
14184
  lines.push(`${c.dim(`${r.file}:`)} ${c.red(`(parse) ${r.error}`)}`);
11646
14185
  continue;
11647
14186
  }
11648
- if (opts.quiet && fields.every((f) => r.values[f] === void 0)) continue;
14187
+ const effective = r.resolved ?? r.values;
14188
+ if (opts.quiet && fields.every((f) => effective[f] === void 0)) continue;
11649
14189
  for (const f of fields) {
11650
- lines.push(`${c.dim(`${r.file}:`)} ${f}=${stringifyValue(r.values[f])}`);
14190
+ const note = annotation(r, f);
14191
+ const suffix = note === "" ? "" : c.dim(note);
14192
+ lines.push(
14193
+ `${c.dim(`${r.file}:`)} ${f}=${stringifyValue(effective[f])}${suffix}`
14194
+ );
11651
14195
  }
11652
14196
  }
11653
14197
  return lines.join("\n");
@@ -11658,21 +14202,21 @@ function cell(value) {
11658
14202
  if (value === null) return "(null)";
11659
14203
  return typeof value === "bigint" ? String(value) : stringifyValue(value);
11660
14204
  }
11661
- function renderQuery(run, opts = {}) {
14205
+ function renderQuery(run2, opts = {}) {
11662
14206
  const c = palette(opts.color ?? false);
11663
- if (run.changes) return renderChanges(run.changes, c, opts);
14207
+ if (run2.changes) return renderChanges(run2.changes, c, opts);
11664
14208
  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))
14209
+ if (run2.rows.length > 0) {
14210
+ const widths = run2.columns.map(
14211
+ (col) => Math.max(col.length, ...run2.rows.map((r) => cell(r[col]).length))
11668
14212
  );
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(" "));
14213
+ const pad = (text2, i) => i === run2.columns.length - 1 ? text2 : text2.padEnd(widths[i] ?? 0);
14214
+ lines.push(run2.columns.map((col, i) => c.bold(pad(col, i))).join(" "));
14215
+ for (const row of run2.rows) {
14216
+ lines.push(run2.columns.map((col, i) => pad(cell(row[col]), i)).join(" "));
11673
14217
  }
11674
14218
  }
11675
- const n = run.rows.length;
14219
+ const n = run2.rows.length;
11676
14220
  const count = `${n} row${n === 1 ? "" : "s"}`;
11677
14221
  lines.push(
11678
14222
  opts.check ? n > 0 ? c.red(`\u2717 ${count} \u2014 check failed`) : c.green(`\u2713 ${count}`) : c.dim(count)
@@ -11680,15 +14224,15 @@ function renderQuery(run, opts = {}) {
11680
14224
  return lines.join("\n");
11681
14225
  }
11682
14226
  var CSV_NEEDS_QUOTING = /[",\n\r]/;
11683
- function renderQueryCsv(run) {
14227
+ function renderQueryCsv(run2) {
11684
14228
  const field = (value) => {
11685
14229
  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;
14230
+ const text2 = typeof value === "string" ? value : typeof value === "number" || typeof value === "bigint" ? String(value) : stringifyValue(value);
14231
+ return CSV_NEEDS_QUOTING.test(text2) ? `"${text2.replaceAll('"', '""')}"` : text2;
11688
14232
  };
11689
14233
  return [
11690
- run.columns.map(field).join(","),
11691
- ...run.rows.map((row) => run.columns.map((c) => field(row[c])).join(","))
14234
+ run2.columns.map(field).join(","),
14235
+ ...run2.rows.map((row) => run2.columns.map((c) => field(row[c])).join(","))
11692
14236
  ].join("\n");
11693
14237
  }
11694
14238
  function renderChanges(changes, c, opts) {
@@ -11809,12 +14353,46 @@ export {
11809
14353
  orphanEntries,
11810
14354
  orphanJoins,
11811
14355
  orphanError,
14356
+ DERIVABLE_FIELDS,
14357
+ isBuiltinField,
14358
+ DERIVE_SOURCES,
14359
+ isDeriveSource,
14360
+ DERIVED_STALE_SCHEMA,
14361
+ DERIVED_KEYWORD,
14362
+ compareDerived,
14363
+ staleFindings,
11812
14364
  parseConfig,
11813
14365
  loadConfig,
11814
14366
  resolveRunConfig,
11815
14367
  isParamName,
11816
14368
  memberOf,
11817
14369
  Validator,
14370
+ REVIEW_CACHE_DIR,
14371
+ REVIEW_CACHE_VERSION,
14372
+ ReviewCache,
14373
+ cachedClient,
14374
+ BinMissing,
14375
+ run,
14376
+ PATH_PLACEHOLDER,
14377
+ isPerFile,
14378
+ argvFor,
14379
+ valueOf,
14380
+ deriveFromCommands,
14381
+ parseOriginUrl,
14382
+ identityFromOrigin,
14383
+ GitHubClient,
14384
+ GitLabClient,
14385
+ createReviewClient,
14386
+ deriveFromReviews,
14387
+ FIELD_SOURCES,
14388
+ sourcesFor,
14389
+ consultedSources,
14390
+ assertSourcesAvailable,
14391
+ deriveMetadata,
14392
+ RESOLVED_VIEW,
14393
+ resolvedColumns,
14394
+ mentionsResolved,
14395
+ createResolvedView,
11818
14396
  rowsToFindings,
11819
14397
  runValidate,
11820
14398
  runGet,
@@ -11825,6 +14403,7 @@ export {
11825
14403
  runInferSchema,
11826
14404
  runQuery,
11827
14405
  runFill,
14406
+ runDerive,
11828
14407
  renderSarif,
11829
14408
  renderJunit,
11830
14409
  REPORT_FORMATS,
@@ -11845,9 +14424,13 @@ export {
11845
14424
  isFillFormat,
11846
14425
  renderFillGithub,
11847
14426
  renderFill,
14427
+ DERIVE_FORMATS,
14428
+ DERIVE_FORMAT_LIST,
14429
+ isDeriveFormat,
14430
+ renderDerive,
11848
14431
  stringifyValue,
11849
14432
  renderGet,
11850
14433
  renderQuery,
11851
14434
  renderQueryCsv
11852
14435
  };
11853
- //# sourceMappingURL=chunk-IRHWILCO.js.map
14436
+ //# sourceMappingURL=chunk-HYPDUKRU.js.map