@hawkeyexl/manni 1.0.0 → 1.1.0

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