@hawkeyexl/manni 2.10.2 → 2.11.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.
@@ -8,7 +8,7 @@ var __export = (target, all) => {
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "@hawkeyexl/manni",
11
- version: "2.10.2",
11
+ version: "2.11.0",
12
12
  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.",
13
13
  type: "module",
14
14
  bin: {
@@ -141,6 +141,99 @@ function matchesFileGlob(relPath, glob) {
141
141
  return m(relPath.replace(/\\/g, "/"));
142
142
  }
143
143
 
144
+ // src/shared/collections.ts
145
+ import { posix as posix2, win32 } from "node:path";
146
+
147
+ // src/shared/page-manifest.ts
148
+ import { readFileSync, realpathSync } from "node:fs";
149
+ import { isAbsolute, posix, relative, resolve, sep } from "node:path";
150
+ import fg from "fast-glob";
151
+ import { LineCounter, isMap, isNode, isScalar, parseDocument } from "yaml";
152
+ var PAGE_PLACEHOLDER = "{page}";
153
+ function hasPagePlaceholder(file) {
154
+ return file.includes(PAGE_PLACEHOLDER);
155
+ }
156
+ function slashes(path) {
157
+ return path.split(sep).join("/").replace(/\\/g, "/");
158
+ }
159
+ function normalizeManifestPattern(file) {
160
+ const forward = file.replace(/\\/g, "/");
161
+ return forward.startsWith("./") ? forward.slice(2) : forward;
162
+ }
163
+ function pageManifestPath(file, configDir, pageAbs) {
164
+ const between = relative(configDir, pageAbs);
165
+ const pageRel = slashes(between);
166
+ if (isAbsolute(between) || pageRel === ".." || pageRel.startsWith("../")) {
167
+ return { outside: true, pageRel };
168
+ }
169
+ const ext = posix.extname(pageRel);
170
+ const page = ext === "" ? pageRel : pageRel.slice(0, -ext.length);
171
+ const named = normalizeManifestPattern(file).replace(PAGE_PLACEHOLDER, () => page);
172
+ const abs = resolve(configDir, named);
173
+ return { abs, rel: slashes(relative(configDir, abs)) };
174
+ }
175
+ function strayManifestGlob(file) {
176
+ const normalized = normalizeManifestPattern(file);
177
+ const at2 = normalized.indexOf(PAGE_PLACEHOLDER);
178
+ if (at2 < 0) return literal(normalized);
179
+ const prefix = literal(normalized.slice(0, at2));
180
+ const suffix = literal(normalized.slice(at2 + PAGE_PLACEHOLDER.length));
181
+ return `${prefix}**/*${suffix}`;
182
+ }
183
+ function literal(text2) {
184
+ return text2 === "" ? "" : fg.escapePath(text2);
185
+ }
186
+ function findPageManifests(file, configDir, isMember2) {
187
+ const matched = fg.sync(strayManifestGlob(file), {
188
+ cwd: configDir,
189
+ absolute: true,
190
+ onlyFiles: true,
191
+ dot: false,
192
+ ignore: ["**/node_modules/**", "**/.git/**"]
193
+ }).map((path) => resolve(path)).sort();
194
+ const found = [];
195
+ for (const abs of matched) {
196
+ const entries = ownEntries(abs, file, configDir, isMember2);
197
+ if (entries.length > 0) found.push({ abs, entries });
198
+ }
199
+ return found;
200
+ }
201
+ function ownEntries(abs, file, configDir, isMember2) {
202
+ let text2;
203
+ try {
204
+ text2 = readFileSync(abs, "utf8");
205
+ } catch {
206
+ return [];
207
+ }
208
+ const lc = new LineCounter();
209
+ const doc = parseDocument(text2, { lineCounter: lc, uniqueKeys: false });
210
+ const root = doc.contents;
211
+ if (doc.errors.length > 0 || !isMap(root)) return [];
212
+ const entries = [];
213
+ for (const pair of root.items) {
214
+ const spelled = isScalar(pair.key) ? String(pair.key.value) : String(pair.key);
215
+ const pageAbs = resolve(configDir, spelled);
216
+ if (!isMember2(slashes(relative(configDir, pageAbs)))) continue;
217
+ const resolved = pageManifestPath(file, configDir, pageAbs);
218
+ if ("outside" in resolved || !sameFile(resolved.abs, abs)) continue;
219
+ const range = isNode(pair.key) ? pair.key.range : void 0;
220
+ entries.push({
221
+ spelled,
222
+ pageAbs,
223
+ ...range ? { line: lc.linePos(range[0]).line } : {}
224
+ });
225
+ }
226
+ return entries;
227
+ }
228
+ function sameFile(a, b) {
229
+ if (a === b) return true;
230
+ try {
231
+ return realpathSync(a) === realpathSync(b);
232
+ } catch {
233
+ return false;
234
+ }
235
+ }
236
+
144
237
  // src/shared/collections.ts
145
238
  var COLLECTION_KEYS = [
146
239
  "name",
@@ -150,6 +243,7 @@ var COLLECTION_KEYS = [
150
243
  "externalMetadata"
151
244
  ];
152
245
  var EXTERNAL_METADATA_KEYS = ["file", "keys", "tokenEnv", "join"];
246
+ var PATH_JOIN = "path";
153
247
  var SCHEMA_KEY = "$schema";
154
248
  var STDIN = "<stdin>";
155
249
  function isUrlRef(ref) {
@@ -285,6 +379,7 @@ function isHttpUrl(value) {
285
379
  }
286
380
  function parseExternalMetadata(raw, collectionWhere, source, toError) {
287
381
  const owners = /* @__PURE__ */ new Map();
382
+ const patterns = /* @__PURE__ */ new Map();
288
383
  return raw.map((entry, i) => {
289
384
  const where = `${collectionWhere}.externalMetadata[${i}]`;
290
385
  if (!isMapping(entry)) {
@@ -296,6 +391,16 @@ function parseExternalMetadata(raw, collectionWhere, source, toError) {
296
391
  `${source}: ${where}.file must be a non-empty string naming the manifest, relative to the config file.`
297
392
  );
298
393
  }
394
+ const placeholderProblem = pagePlaceholderProblem(entry.file);
395
+ if (placeholderProblem !== null) {
396
+ throw toError(`${source}: ${where}.file ${placeholderProblem}`);
397
+ }
398
+ const perPage = hasPagePlaceholder(entry.file);
399
+ if (perPage && (posix2.isAbsolute(entry.file) || win32.isAbsolute(entry.file))) {
400
+ throw toError(
401
+ `${source}: ${where}.file uses ${PAGE_PLACEHOLDER} in an absolute path, so every manifest would sit outside the repository. Make it relative to the config file.`
402
+ );
403
+ }
299
404
  if (!Array.isArray(entry.keys) || entry.keys.length === 0 || entry.keys.some((k) => !isNonEmptyString(k))) {
300
405
  throw toError(
301
406
  `${source}: ${where}.keys must be a non-empty list of key names.`
@@ -307,6 +412,11 @@ function parseExternalMetadata(raw, collectionWhere, source, toError) {
307
412
  if (problem !== null) {
308
413
  throw toError(`${source}: ${where}.file ${problem}.`);
309
414
  }
415
+ if (perPage) {
416
+ throw toError(
417
+ `${source}: ${where}.file uses ${PAGE_PLACEHOLDER} in a URL. A placeholder names a file this run can write, and a URL is not one.`
418
+ );
419
+ }
310
420
  }
311
421
  if (entry.tokenEnv !== void 0) {
312
422
  if (!isNonEmptyString(entry.tokenEnv)) {
@@ -336,6 +446,11 @@ function parseExternalMetadata(raw, collectionWhere, source, toError) {
336
446
  );
337
447
  }
338
448
  }
449
+ if (perPage && typeof entry.join === "string" && entry.join !== PATH_JOIN) {
450
+ throw toError(
451
+ `${source}: ${where}.file uses ${PAGE_PLACEHOLDER}, and join is "${entry.join}". A per-page manifest is found by path, so it cannot be keyed by a field. Remove ${PAGE_PLACEHOLDER}, or remove join.`
452
+ );
453
+ }
339
454
  const seen = /* @__PURE__ */ new Set();
340
455
  for (const key of keys) {
341
456
  if (key === SCHEMA_KEY) {
@@ -355,14 +470,46 @@ function parseExternalMetadata(raw, collectionWhere, source, toError) {
355
470
  }
356
471
  owners.set(key, i);
357
472
  }
358
- return {
473
+ if (perPage) {
474
+ const pattern = normalizeManifestPattern(entry.file);
475
+ const first = patterns.get(pattern);
476
+ if (first !== void 0) {
477
+ throw toError(
478
+ `${source}: ${where}.file resolves to the same manifest as externalMetadata[${String(first)}].file for every page. Give each entry its own file name.`
479
+ );
480
+ }
481
+ patterns.set(pattern, i);
482
+ }
483
+ const parsed2 = {
359
484
  file: entry.file,
360
485
  keys,
361
486
  ...typeof entry.tokenEnv === "string" ? { tokenEnv: entry.tokenEnv } : {},
362
487
  ...typeof entry.join === "string" ? { join: entry.join } : {}
363
488
  };
489
+ origins.set(parsed2, { source, where });
490
+ return parsed2;
364
491
  });
365
492
  }
493
+ function pagePlaceholderProblem(file) {
494
+ for (const match of file.matchAll(/\{[^{}]*\}/g)) {
495
+ if (match[0] !== PAGE_PLACEHOLDER) {
496
+ return `uses "${match[0]}", which is not a placeholder. The only placeholder is ${PAGE_PLACEHOLDER}.`;
497
+ }
498
+ }
499
+ const tokens2 = file.split(PAGE_PLACEHOLDER);
500
+ const stray = /[{}]/.exec(tokens2.join(""));
501
+ if (stray !== null) {
502
+ return `contains "${stray[0]}" outside a ${PAGE_PLACEHOLDER} placeholder. Remove it, or write ${PAGE_PLACEHOLDER}.`;
503
+ }
504
+ if (tokens2.length > 2) {
505
+ return `uses ${PAGE_PLACEHOLDER} twice. One manifest names one page.`;
506
+ }
507
+ return null;
508
+ }
509
+ var origins = /* @__PURE__ */ new WeakMap();
510
+ function externalMetadataOrigin(entry) {
511
+ return origins.get(entry);
512
+ }
366
513
  function selectCollections(collections, names, source, toError) {
367
514
  if (names === void 0 || names.length === 0) return [...collections];
368
515
  const wanted = new Set(names);
@@ -383,6 +530,9 @@ function selectCollections(collections, names, source, toError) {
383
530
  function isMember(collection, relPath) {
384
531
  if (relPath === STDIN) return false;
385
532
  if (relPath.startsWith("../")) return false;
533
+ return matchesCollectionPaths(collection, relPath);
534
+ }
535
+ function matchesCollectionPaths(collection, relPath) {
386
536
  const matched = collection.paths.some((entry) => {
387
537
  const normalized = normalizeEntry(entry);
388
538
  if (normalized === "") return false;
@@ -393,15 +543,15 @@ function isMember(collection, relPath) {
393
543
  return !collection.exclude.some((glob) => matchesFileGlob(relPath, glob));
394
544
  }
395
545
  function normalizeEntry(entry) {
396
- const posix5 = entry.replace(/\\/g, "/").replace(/^\.\//, "");
397
- return posix5.endsWith("/") ? posix5.slice(0, -1) : posix5;
546
+ const posix7 = entry.replace(/\\/g, "/").replace(/^\.\//, "");
547
+ return posix7.endsWith("/") ? posix7.slice(0, -1) : posix7;
398
548
  }
399
549
  function isGlob(entry) {
400
550
  return picomatch2.scan(entry).isGlob;
401
551
  }
402
552
 
403
553
  // src/shared/run.ts
404
- import { realpathSync } from "node:fs";
554
+ import { realpathSync as realpathSync2 } from "node:fs";
405
555
  import { fileURLToPath } from "node:url";
406
556
  import { CommanderError } from "commander";
407
557
 
@@ -472,7 +622,7 @@ function isMainModule(moduleUrl) {
472
622
  const entry = process.argv[1];
473
623
  if (!entry) return false;
474
624
  try {
475
- return realpathSync(entry) === fileURLToPath(moduleUrl);
625
+ return realpathSync2(entry) === fileURLToPath(moduleUrl);
476
626
  } catch {
477
627
  return false;
478
628
  }
@@ -532,7 +682,7 @@ function diagnoseIntegrity(bytes, expected) {
532
682
  }
533
683
 
534
684
  // src/meta/extractors/frontmatter.ts
535
- import { parseDocument, LineCounter, isMap, isSeq, isScalar } from "yaml";
685
+ import { parseDocument as parseDocument2, LineCounter as LineCounter2, isMap as isMap2, isSeq, isScalar as isScalar2 } from "yaml";
536
686
  import { parse as parseToml } from "smol-toml";
537
687
 
538
688
  // src/meta/extractors/date-value.ts
@@ -612,9 +762,9 @@ function buildLineMap(doc, lc, prefixLines) {
612
762
  map.set("", 1);
613
763
  const lineAt = (offset) => offset == null ? void 0 : lc.linePos(offset).line + prefixLines;
614
764
  const walk = (node, pointer) => {
615
- if (isMap(node)) {
765
+ if (isMap2(node)) {
616
766
  for (const pair of node.items) {
617
- const key = isScalar(pair.key) ? String(pair.key.value) : String(pair.key);
767
+ const key = isScalar2(pair.key) ? String(pair.key.value) : String(pair.key);
618
768
  const ptr = `${pointer}/${escapePointerSegment(key)}`;
619
769
  const line = lineAt(
620
770
  pair.key?.range?.[0]
@@ -668,8 +818,8 @@ function rootObject(parsed2, label) {
668
818
  throw new Error(`Invalid ${label} frontmatter: root must be an object`);
669
819
  }
670
820
  function parseYamlBlock(raw, prefixLines, format) {
671
- const lc = new LineCounter();
672
- const doc = parseDocument(raw, { lineCounter: lc });
821
+ const lc = new LineCounter2();
822
+ const doc = parseDocument2(raw, { lineCounter: lc });
673
823
  if (doc.errors.length > 0) {
674
824
  const e = doc.errors[0];
675
825
  throw new Error(`Invalid YAML frontmatter: ${e?.message ?? "parse error"}`);
@@ -693,8 +843,8 @@ function parseJsonBlock(raw, prefixLines, format) {
693
843
  );
694
844
  }
695
845
  const data = rootObject(parsed2, "JSON");
696
- const lc = new LineCounter();
697
- const doc = parseDocument(raw, { lineCounter: lc });
846
+ const lc = new LineCounter2();
847
+ const doc = parseDocument2(raw, { lineCounter: lc });
698
848
  const map = buildLineMap(doc, lc, prefixLines);
699
849
  return { data, present: true, format, lineFor: positionForFactory(map) };
700
850
  }
@@ -751,7 +901,7 @@ var DocmetaError = class extends ToolError {
751
901
  };
752
902
 
753
903
  // src/meta/extractors/frontmatter-write.ts
754
- import { parseDocument as parseDocument2, isMap as isMap2, isCollection } from "yaml";
904
+ import { parseDocument as parseDocument3, isMap as isMap3, isCollection } from "yaml";
755
905
  import { parse as parseToml2, stringify as stringifyToml } from "smol-toml";
756
906
 
757
907
  // src/meta/extractors/patch-util.ts
@@ -874,13 +1024,13 @@ function mergeBlock(flavor, inner, patch, deletions = []) {
874
1024
  }
875
1025
  }
876
1026
  function mergeYaml(inner, patch, deletions = []) {
877
- const doc = parseDocument2(inner);
1027
+ const doc = parseDocument3(inner);
878
1028
  if (doc.errors.length > 0) {
879
1029
  throw new DocmetaError(
880
1030
  `Cannot rewrite invalid YAML front matter: ${doc.errors[0]?.message ?? "parse error"}`
881
1031
  );
882
1032
  }
883
- if (doc.contents != null && !isMap2(doc.contents)) {
1033
+ if (doc.contents != null && !isMap3(doc.contents)) {
884
1034
  throw new DocmetaError(
885
1035
  "Cannot rewrite front matter: the root must be a mapping."
886
1036
  );
@@ -1041,7 +1191,7 @@ function parseBlock(flavor, text2) {
1041
1191
  case "toml":
1042
1192
  return text2.trim() === "" ? {} : parseToml2(text2);
1043
1193
  case "yaml": {
1044
- const doc = parseDocument2(text2);
1194
+ const doc = parseDocument3(text2);
1045
1195
  if (doc.errors.length > 0) {
1046
1196
  throw new DocmetaError(
1047
1197
  `Cannot rewrite invalid YAML front matter: ${doc.errors[0]?.message ?? "parse error"}`
@@ -1308,8 +1458,8 @@ function ditaShape(content, root, filePath) {
1308
1458
  const lower = filePath?.toLowerCase() ?? "";
1309
1459
  const byExtension2 = (lower.endsWith(".dita") || lower.endsWith(".ditamap")) && DITA_ROOTS.has(name2);
1310
1460
  if (!byDoctype && !byClass && !byArch && !byExtension2) return void 0;
1311
- const isMap9 = /^\s*[-+]\s+map\//.test(cls) || MAP_ROOTS.has(name2) || lower.endsWith(".ditamap");
1312
- return isMap9 ? { kind: "map", container: "topicmeta", preamble: /* @__PURE__ */ new Set(["title"]) } : {
1461
+ const isMap10 = /^\s*[-+]\s+map\//.test(cls) || MAP_ROOTS.has(name2) || lower.endsWith(".ditamap");
1462
+ return isMap10 ? { kind: "map", container: "topicmeta", preamble: /* @__PURE__ */ new Set(["title"]) } : {
1313
1463
  kind: "topic",
1314
1464
  container: "metadata",
1315
1465
  preamble: /* @__PURE__ */ new Set([
@@ -2731,8 +2881,8 @@ function listFormats() {
2731
2881
  }
2732
2882
 
2733
2883
  // src/meta/core/write-file.ts
2734
- import { writeFile, rename, rm, stat as stat2, chmod } from "node:fs/promises";
2735
- import { dirname, join, basename, resolve } from "node:path";
2884
+ import { writeFile, rename, rm, stat as stat2, chmod, mkdir } from "node:fs/promises";
2885
+ import { dirname, join, basename, resolve as resolve2 } from "node:path";
2736
2886
 
2737
2887
  // src/meta/core/manifest-cache.ts
2738
2888
  import { stat } from "node:fs/promises";
@@ -2789,15 +2939,16 @@ var RENAME_ATTEMPTS = 3;
2789
2939
  function errorCode(err) {
2790
2940
  return typeof err === "object" && err !== null && "code" in err ? String(err.code) : void 0;
2791
2941
  }
2792
- var wait = (ms) => new Promise((resolve22) => {
2793
- setTimeout(resolve22, ms);
2942
+ var wait = (ms) => new Promise((resolve23) => {
2943
+ setTimeout(resolve23, ms);
2794
2944
  });
2795
- async function writeFileAtomic(path, contents) {
2945
+ async function writeFileAtomic(path, contents, options = {}) {
2796
2946
  const tmp = join(
2797
2947
  dirname(path),
2798
2948
  `.${basename(path)}.manni-${process.pid}-${Math.random().toString(36).slice(2, 8)}.tmp`
2799
2949
  );
2800
2950
  try {
2951
+ if (options.createParents === true) await mkdir(dirname(path), { recursive: true });
2801
2952
  await writeFile(tmp, contents);
2802
2953
  try {
2803
2954
  const { mode } = await stat2(path);
@@ -2827,13 +2978,13 @@ async function writeFileAtomic(path, contents) {
2827
2978
  }
2828
2979
  } finally {
2829
2980
  await rm(tmp, { force: true });
2830
- invalidateManifestCache(resolve(path));
2981
+ invalidateManifestCache(resolve2(path));
2831
2982
  }
2832
2983
  }
2833
2984
 
2834
2985
  // src/meta/core/schema-cache.ts
2835
2986
  import { createHash as createHash2 } from "node:crypto";
2836
- import { mkdir, readFile, stat as stat3 } from "node:fs/promises";
2987
+ import { mkdir as mkdir2, readFile, stat as stat3 } from "node:fs/promises";
2837
2988
  import { join as join2 } from "node:path";
2838
2989
  var SCHEMA_CACHE_DIR = ".manni/meta/schema-cache";
2839
2990
  var SCHEMA_CACHE_VERSION = 1;
@@ -2926,7 +3077,7 @@ var SchemaCache = class {
2926
3077
  schema
2927
3078
  };
2928
3079
  try {
2929
- await mkdir(this.dir, { recursive: true });
3080
+ await mkdir2(this.dir, { recursive: true });
2930
3081
  await writeFileAtomic(
2931
3082
  this.entryPath(url),
2932
3083
  `${JSON.stringify(entry, null, 2)}
@@ -5344,8 +5495,8 @@ var SCHEMA_KEYS = [
5344
5495
  "multipleOf"
5345
5496
  ];
5346
5497
  var RETRY_DELAY_MS = 500;
5347
- var wait2 = (ms) => new Promise((resolve22) => {
5348
- setTimeout(resolve22, ms);
5498
+ var wait2 = (ms) => new Promise((resolve23) => {
5499
+ setTimeout(resolve23, ms);
5349
5500
  });
5350
5501
  var RetryableFetchError = class extends DocmetaError {
5351
5502
  };
@@ -5598,7 +5749,7 @@ async function loadSchema(ref, options = {}) {
5598
5749
  }
5599
5750
 
5600
5751
  // src/meta/core/resolve-schema.ts
5601
- import { isAbsolute, relative, resolve as resolve2 } from "node:path";
5752
+ import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve3 } from "node:path";
5602
5753
  var DEFAULT_SCHEMAS = Object.freeze([
5603
5754
  "google:okf:0.1",
5604
5755
  "passo-uno:seven-action:1.0"
@@ -5620,8 +5771,8 @@ function collectSchemaPins(config) {
5620
5771
  return pins;
5621
5772
  }
5622
5773
  function rebaseConfigSchemaRefs(config, configDir, cwd) {
5623
- if (resolve2(configDir) === resolve2(cwd)) return config;
5624
- const rebase2 = (ref) => classifyRef(ref).kind === "file" && !isAbsolute(ref) ? resolve2(configDir, ref) : ref;
5774
+ if (resolve3(configDir) === resolve3(cwd)) return config;
5775
+ const rebase2 = (ref) => classifyRef(ref).kind === "file" && !isAbsolute2(ref) ? resolve3(configDir, ref) : ref;
5625
5776
  const rebaseEntry = (entry) => typeof entry === "string" ? rebase2(entry) : {
5626
5777
  ...entry,
5627
5778
  ref: rebase2(entry.ref),
@@ -5697,9 +5848,9 @@ function assertDocumentRefAllowed(ref, mode, params) {
5697
5848
  }
5698
5849
  const root = params.trustRoot;
5699
5850
  if (!root) return;
5700
- const abs = resolve2(params.fileBase ?? process.cwd(), ref);
5701
- const within = relative(root.dir, abs);
5702
- if (within !== "" && !within.startsWith("..") && !isAbsolute(within)) return;
5851
+ const abs = resolve3(params.fileBase ?? process.cwd(), ref);
5852
+ const within = relative2(root.dir, abs);
5853
+ if (within !== "" && !within.startsWith("..") && !isAbsolute2(within)) return;
5703
5854
  const boundary = root.source === "git" ? `the repository root (${root.dir})` : root.source === "config" ? `${root.dir} \u2014 no git repository was found, so the config file's own directory is the boundary` : `${root.dir} \u2014 no git repository and no config file were found, so the directory the command was run from is the boundary`;
5704
5855
  const scope = root.source === "git" ? "the repository" : "the project";
5705
5856
  throw new DocmetaError(
@@ -5849,12 +6000,12 @@ function isPlainObject(v) {
5849
6000
  }
5850
6001
 
5851
6002
  // src/meta/core/config.ts
5852
- import { resolve as resolve6 } from "node:path";
6003
+ import { resolve as resolve7 } from "node:path";
5853
6004
  import { parse as parseYaml2 } from "yaml";
5854
6005
 
5855
6006
  // src/shared/config-file.ts
5856
6007
  import { readFile as readFile3 } from "node:fs/promises";
5857
- import { basename as basename2, dirname as dirname3, join as join4, relative as relative2, resolve as resolve5 } from "node:path";
6008
+ import { basename as basename2, dirname as dirname3, join as join4, relative as relative3, resolve as resolve6 } from "node:path";
5858
6009
  import { parse as parseYaml } from "yaml";
5859
6010
 
5860
6011
  // src/shared/encryption.ts
@@ -5985,9 +6136,9 @@ function keyedPin(text2, key) {
5985
6136
 
5986
6137
  // src/shared/git-root.ts
5987
6138
  import { existsSync } from "node:fs";
5988
- import { dirname as dirname2, join as join3, resolve as resolve3 } from "node:path";
6139
+ import { dirname as dirname2, join as join3, resolve as resolve4 } from "node:path";
5989
6140
  function findGitRoot(cwd) {
5990
- let dir = resolve3(cwd);
6141
+ let dir = resolve4(cwd);
5991
6142
  for (; ; ) {
5992
6143
  if (existsSync(join3(dir, ".git"))) return dir;
5993
6144
  const parent = dirname2(dir);
@@ -5996,7 +6147,7 @@ function findGitRoot(cwd) {
5996
6147
  }
5997
6148
  }
5998
6149
  function searchPath(cwd) {
5999
- const start = resolve3(cwd);
6150
+ const start = resolve4(cwd);
6000
6151
  const root = findGitRoot(start);
6001
6152
  if (root === null) return [start];
6002
6153
  const chain = [];
@@ -6011,7 +6162,7 @@ function searchPath(cwd) {
6011
6162
  }
6012
6163
 
6013
6164
  // src/shared/tools.ts
6014
- import { isAbsolute as isAbsolute2, resolve as resolve4 } from "node:path";
6165
+ import { isAbsolute as isAbsolute3, resolve as resolve5 } from "node:path";
6015
6166
  var TOOL_KEYS = ["vale"];
6016
6167
  var VALE_KEYS = ["config"];
6017
6168
  function isMapping2(value) {
@@ -6056,7 +6207,7 @@ function parseVale(raw, source, toError) {
6056
6207
  function valeConfigPath(tools, configDir) {
6057
6208
  const config = tools.vale?.config;
6058
6209
  if (config === void 0) return void 0;
6059
- return isAbsolute2(config) ? config : resolve4(configDir, config);
6210
+ return isAbsolute3(config) ? config : resolve5(configDir, config);
6060
6211
  }
6061
6212
 
6062
6213
  // src/shared/warn.ts
@@ -6161,7 +6312,7 @@ function tolerantKeysOf(document) {
6161
6312
  };
6162
6313
  }
6163
6314
  function relativeSource(cwd, path) {
6164
- return relative2(cwd, path).replace(/\\/g, "/");
6315
+ return relative3(cwd, path).replace(/\\/g, "/");
6165
6316
  }
6166
6317
  function mooseWarning(name2) {
6167
6318
  return `"${name2}" is the pre-rename name of the family config file. Rename it to "${FAMILY_CONFIG_NAMES[0] ?? "manni.config.yaml"}".`;
@@ -6171,7 +6322,7 @@ var FAMILY_FILES = [
6171
6322
  { names: MOOSE_CONFIG_NAMES, kind: "moose" }
6172
6323
  ];
6173
6324
  async function findConfigFile(cwd, opts) {
6174
- const start = resolve5(cwd);
6325
+ const start = resolve6(cwd);
6175
6326
  for (const dir of searchPath(start)) {
6176
6327
  for (const { names, kind } of FAMILY_FILES) {
6177
6328
  for (const name2 of names) {
@@ -6219,7 +6370,7 @@ async function findConfigFile(cwd, opts) {
6219
6370
  return null;
6220
6371
  }
6221
6372
  async function readConfigFile(explicitPath, cwd, opts) {
6222
- const path = resolve5(cwd, explicitPath);
6373
+ const path = resolve6(cwd, explicitPath);
6223
6374
  const document = await readDocument(path, explicitPath, opts.toError);
6224
6375
  if (document === null) {
6225
6376
  throw opts.toError(`Config file not found: "${explicitPath}".`);
@@ -6243,7 +6394,7 @@ async function readConfigFile(explicitPath, cwd, opts) {
6243
6394
  };
6244
6395
  }
6245
6396
  async function findFamilyConfigFile(cwd, toError) {
6246
- const start = resolve5(cwd);
6397
+ const start = resolve6(cwd);
6247
6398
  for (const dir of searchPath(start)) {
6248
6399
  for (const { names, kind } of FAMILY_FILES) {
6249
6400
  for (const name2 of names) {
@@ -6270,7 +6421,7 @@ async function findFamilyConfigFile(cwd, toError) {
6270
6421
  return null;
6271
6422
  }
6272
6423
  async function readFamilyConfigFile(explicitPath, cwd, toError) {
6273
- const path = resolve5(cwd, explicitPath);
6424
+ const path = resolve6(cwd, explicitPath);
6274
6425
  const document = await readDocument(path, explicitPath, toError);
6275
6426
  if (document === null) {
6276
6427
  throw toError(`Config file not found: "${explicitPath}".`);
@@ -6939,8 +7090,8 @@ function parseFill(value, source) {
6939
7090
  function schemaTrustRoot(cwd, configDir) {
6940
7091
  const git2 = findGitRoot(cwd);
6941
7092
  if (git2 !== null) return { dir: git2, source: "git" };
6942
- if (configDir !== void 0) return { dir: resolve6(configDir), source: "config" };
6943
- return { dir: resolve6(cwd), source: "cwd" };
7093
+ if (configDir !== void 0) return { dir: resolve7(configDir), source: "config" };
7094
+ return { dir: resolve7(cwd), source: "cwd" };
6944
7095
  }
6945
7096
  async function loadConfig(explicitPath, cwd = process.cwd()) {
6946
7097
  const file = explicitPath ? await readConfigFile(explicitPath, cwd, CONFIG_FILE) : await findConfigFile(cwd, CONFIG_FILE);
@@ -7003,23 +7154,23 @@ async function resolveRunConfig(opts) {
7003
7154
  }
7004
7155
 
7005
7156
  // src/meta/reporters/sarif.ts
7006
- import { isAbsolute as isAbsolute3 } from "node:path";
7157
+ import { isAbsolute as isAbsolute4 } from "node:path";
7007
7158
 
7008
7159
  // src/meta/core/baseline.ts
7009
7160
  import { createHash as createHash3 } from "node:crypto";
7010
7161
  import { existsSync as existsSync2 } from "node:fs";
7011
7162
  import { readFile as readFile4 } from "node:fs/promises";
7012
- import { relative as relative5, resolve as resolve9 } from "node:path";
7163
+ import { relative as relative6, resolve as resolve10 } from "node:path";
7013
7164
 
7014
7165
  // src/meta/core/load-files.ts
7015
7166
  import { stat as stat4 } from "node:fs/promises";
7016
- import { extname, relative as relative4, resolve as resolve8 } from "node:path";
7017
- import fg from "fast-glob";
7167
+ import { extname, relative as relative5, resolve as resolve9 } from "node:path";
7168
+ import fg2 from "fast-glob";
7018
7169
  import picomatch3 from "picomatch";
7019
7170
 
7020
7171
  // src/meta/core/gitignore.ts
7021
7172
  import { spawn } from "node:child_process";
7022
- import { dirname as dirname4, relative as relative3, resolve as resolve7, sep } from "node:path";
7173
+ import { dirname as dirname4, relative as relative4, resolve as resolve8, sep as sep2 } from "node:path";
7023
7174
  var GITIGNORE_UNAVAILABLE = ".gitignore filtering was requested but git could not answer here (no repository, or no git on PATH); no files were skipped.";
7024
7175
  var unavailable = () => ({
7025
7176
  ignored: /* @__PURE__ */ new Set(),
@@ -7033,7 +7184,7 @@ async function gitIgnored(paths, cwd) {
7033
7184
  const byRoot = /* @__PURE__ */ new Map();
7034
7185
  let inSomeRepo = false;
7035
7186
  for (const spelled of paths) {
7036
- const abs = resolve7(cwd, spelled);
7187
+ const abs = resolve8(cwd, spelled);
7037
7188
  const dir = dirname4(abs);
7038
7189
  let root = rootOf.get(dir);
7039
7190
  if (root === void 0) {
@@ -7042,7 +7193,7 @@ async function gitIgnored(paths, cwd) {
7042
7193
  }
7043
7194
  if (root === null) continue;
7044
7195
  inSomeRepo = true;
7045
- const rel = relative3(root, abs).split(sep).join("/");
7196
+ const rel = relative4(root, abs).split(sep2).join("/");
7046
7197
  const bucket = byRoot.get(root);
7047
7198
  if (bucket) bucket.push({ rel, spelled });
7048
7199
  else byRoot.set(root, [{ rel, spelled }]);
@@ -7145,14 +7296,14 @@ async function resolveTargetSet(opts) {
7145
7296
  for (const input of opts.inputs) {
7146
7297
  if (input === STDIN_TOKEN) continue;
7147
7298
  const posixInput = toPosix(input);
7148
- const abs = resolve8(cwd, posixInput);
7299
+ const abs = resolve9(cwd, posixInput);
7149
7300
  const st = await statOrNull(abs);
7150
7301
  if (st?.isFile()) {
7151
- named.add(toPosix(relative4(cwd, abs)));
7302
+ named.add(toPosix(relative5(cwd, abs)));
7152
7303
  continue;
7153
7304
  }
7154
7305
  if (st?.isDirectory()) {
7155
- const found2 = await fg(`${posixInput}/**/*`, {
7306
+ const found2 = await fg2(`${posixInput}/**/*`, {
7156
7307
  cwd,
7157
7308
  ignore,
7158
7309
  onlyFiles: true,
@@ -7165,7 +7316,7 @@ async function resolveTargetSet(opts) {
7165
7316
  missing.push(posixInput);
7166
7317
  continue;
7167
7318
  }
7168
- const found = await fg(posixInput, {
7319
+ const found = await fg2(posixInput, {
7169
7320
  cwd,
7170
7321
  ignore,
7171
7322
  onlyFiles: true,
@@ -7230,13 +7381,13 @@ var FINGERPRINT_RE = /^[0-9a-f]{16}$/;
7230
7381
  function canonicalFilePath(file, ctx) {
7231
7382
  if (!ctx) return file;
7232
7383
  const from = ctx.runBase ?? ctx.base;
7233
- return relative5(ctx.base, resolve9(from, file)).replace(/\\/g, "/");
7384
+ return relative6(ctx.base, resolve10(from, file)).replace(/\\/g, "/");
7234
7385
  }
7235
7386
  function canonicalSchemaRef(ref, ctx) {
7236
7387
  if (!ctx) return ref;
7237
7388
  if (classifyRef(ref).kind !== "file") return ref;
7238
- const abs = resolve9(ctx.cwd, ref);
7239
- return relative5(ctx.base, abs).replace(/\\/g, "/");
7389
+ const abs = resolve10(ctx.cwd, ref);
7390
+ return relative6(ctx.base, abs).replace(/\\/g, "/");
7240
7391
  }
7241
7392
  function fingerprint(e, ctx) {
7242
7393
  const parts = [
@@ -7393,17 +7544,17 @@ function countFingerprints(baseline) {
7393
7544
  }
7394
7545
  function resolveBaselineRequest(opts, configured, configDir, cwd, defaults) {
7395
7546
  const named = (label) => ({
7396
- absPath: resolve9(cwd, label),
7547
+ absPath: resolve10(cwd, label),
7397
7548
  label
7398
7549
  });
7399
7550
  const implied = () => {
7400
7551
  const base = configDir ?? cwd;
7401
7552
  if (configured !== void 0) {
7402
- return { absPath: resolve9(base, configured), label: configured };
7553
+ return { absPath: resolve10(base, configured), label: configured };
7403
7554
  }
7404
- const current = resolve9(base, defaults.current);
7555
+ const current = resolve10(base, defaults.current);
7405
7556
  if (defaults.legacy !== void 0) {
7406
- const legacy = resolve9(base, defaults.legacy);
7557
+ const legacy = resolve10(base, defaults.legacy);
7407
7558
  if (!existsSync2(current) && existsSync2(legacy)) {
7408
7559
  warn(
7409
7560
  `"${defaults.legacy}" is the pre-rename baseline file name. Rename it to "${defaults.current}".`
@@ -7516,7 +7667,7 @@ function uriFrame(frame) {
7516
7667
  }
7517
7668
  function artifactUri(file, frame) {
7518
7669
  const uri = canonicalFilePath(file, frame);
7519
- if (uri === "" || uri.startsWith("../") || isAbsolute3(uri)) return null;
7670
+ if (uri === "" || uri.startsWith("../") || isAbsolute4(uri)) return null;
7520
7671
  return uri;
7521
7672
  }
7522
7673
  function renderSarif(results, opts = {}) {
@@ -7910,22 +8061,92 @@ async function readCapped(url, res, maxBytes) {
7910
8061
 
7911
8062
  // src/meta/core/external-metadata.ts
7912
8063
  import { readFile as readFile6 } from "node:fs/promises";
7913
- import { isAbsolute as isAbsolute4, relative as relative7, resolve as resolve11, sep as sep2 } from "node:path";
8064
+ import { isAbsolute as isAbsolute5, relative as relative8, resolve as resolve12, sep as sep3 } from "node:path";
7914
8065
  import {
7915
- LineCounter as LineCounter2,
7916
- isMap as isMap4,
7917
- isNode,
7918
- isScalar as isScalar2,
8066
+ LineCounter as LineCounter3,
8067
+ isMap as isMap5,
8068
+ isNode as isNode2,
8069
+ isScalar as isScalar3,
7919
8070
  isSeq as isSeq2,
7920
- parseDocument as parseDocument4
8071
+ parseDocument as parseDocument5
7921
8072
  } from "yaml";
7922
8073
 
8074
+ // src/shared/manifest-cas.ts
8075
+ import { posix as posix3 } from "node:path";
8076
+ var COMMIT_ATTEMPTS = 5;
8077
+ function conflictRefusal(file) {
8078
+ return `${file} changed under the command while it was being written. Nothing was written to it. Re-run the command.`;
8079
+ }
8080
+ function heldManifest(path, file, before, absent = false) {
8081
+ return { path, file, before, text: before, ops: [], bases: /* @__PURE__ */ new Map(), absent };
8082
+ }
8083
+ function encodeValue(value) {
8084
+ return value === void 0 ? "absent" : JSON.stringify(value);
8085
+ }
8086
+ function baseKey(at2) {
8087
+ const entry = at2.join === "path" ? posix3.normalize(at2.entry) : at2.entry;
8088
+ return `${entry}\0${at2.key}`;
8089
+ }
8090
+ function rebaseable(held, op, codec) {
8091
+ const at2 = baseKey(op);
8092
+ if (held.bases.has(at2)) return;
8093
+ held.bases.set(at2, {
8094
+ entry: op.entry,
8095
+ key: op.key,
8096
+ join: op.join,
8097
+ value: encodeValue(codec.read(held.before, op))
8098
+ });
8099
+ }
8100
+ function rebase(held, current, codec) {
8101
+ for (const base of held.bases.values()) {
8102
+ if (encodeValue(codec.read(current, base)) !== base.value) return void 0;
8103
+ }
8104
+ let text2 = current;
8105
+ for (const op of held.ops) text2 = codec.apply(text2, op);
8106
+ return text2;
8107
+ }
8108
+ function rebaseline(held) {
8109
+ held.before = held.text;
8110
+ held.ops = [];
8111
+ held.bases.clear();
8112
+ held.absent = false;
8113
+ }
8114
+ async function settle(held, io) {
8115
+ for (let attempt = 1; attempt <= COMMIT_ATTEMPTS; attempt++) {
8116
+ let current;
8117
+ try {
8118
+ current = await io.read(held.path);
8119
+ } catch (error) {
8120
+ if (held.absent && isMissing(error)) {
8121
+ await io.write(held.path, held.text);
8122
+ held.absent = false;
8123
+ return;
8124
+ }
8125
+ const reason = error instanceof Error ? error.message : String(error);
8126
+ throw io.toError(`Manifest ${held.file} could not be read: ${reason}`);
8127
+ }
8128
+ if (!held.absent && current === held.before) {
8129
+ await io.write(held.path, held.text);
8130
+ return;
8131
+ }
8132
+ const replayed = rebase(held, current, io.codec);
8133
+ if (replayed === void 0) throw io.toError(conflictRefusal(held.file));
8134
+ held.before = current;
8135
+ held.text = replayed;
8136
+ held.absent = false;
8137
+ }
8138
+ throw io.toError(conflictRefusal(held.file));
8139
+ }
8140
+ function isMissing(error) {
8141
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
8142
+ }
8143
+
7923
8144
  // src/shared/encryption-key.ts
7924
8145
  import { spawnSync } from "node:child_process";
7925
8146
  import { readFile as readFile5 } from "node:fs/promises";
7926
- import { basename as basename4, dirname as dirname6, join as join6, relative as relative6, resolve as resolve10 } from "node:path";
8147
+ import { basename as basename4, dirname as dirname6, join as join6, relative as relative7, resolve as resolve11 } from "node:path";
7927
8148
  import { isDeepStrictEqual } from "node:util";
7928
- import { isMap as isMap3, parseDocument as parseDocument3, parse as parseYaml3, stringify } from "yaml";
8149
+ import { isMap as isMap4, parseDocument as parseDocument4, parse as parseYaml3, stringify } from "yaml";
7929
8150
 
7930
8151
  // src/shared/write-file.ts
7931
8152
  import { chmod as chmod2, rename as rename2, rm as rm2, stat as stat5, writeFile as writeFile2 } from "node:fs/promises";
@@ -7935,8 +8156,8 @@ var RENAME_ATTEMPTS2 = 3;
7935
8156
  function errorCode2(err) {
7936
8157
  return typeof err === "object" && err !== null && "code" in err ? String(err.code) : void 0;
7937
8158
  }
7938
- var wait3 = (ms) => new Promise((resolve22) => {
7939
- setTimeout(resolve22, ms);
8159
+ var wait3 = (ms) => new Promise((resolve23) => {
8160
+ setTimeout(resolve23, ms);
7940
8161
  });
7941
8162
  async function writeTextAtomic(path, text2) {
7942
8163
  const tmp = join5(
@@ -7985,7 +8206,7 @@ function resolveEncryptionKey(opts) {
7985
8206
  return { key: void 0, source: "none" };
7986
8207
  }
7987
8208
  function relativeSource2(cwd, path) {
7988
- return relative6(resolve10(cwd), path).replace(/\\/g, "/");
8209
+ return relative7(resolve11(cwd), path).replace(/\\/g, "/");
7989
8210
  }
7990
8211
  function errorCode3(err) {
7991
8212
  return typeof err === "object" && err !== null && "code" in err ? String(err.code) : void 0;
@@ -8008,7 +8229,7 @@ async function fileTarget(file, toError) {
8008
8229
  return { path: file.path, source: file.source, text: text2 ?? file.text };
8009
8230
  }
8010
8231
  async function defaultTarget(cwd) {
8011
- const root = findGitRoot(cwd) ?? resolve10(cwd);
8232
+ const root = findGitRoot(cwd) ?? resolve11(cwd);
8012
8233
  for (const name2 of [...FAMILY_CONFIG_NAMES, ...MOOSE_CONFIG_NAMES]) {
8013
8234
  const path2 = join6(root, name2);
8014
8235
  const text2 = await readIfPresent(path2);
@@ -8018,12 +8239,12 @@ async function defaultTarget(cwd) {
8018
8239
  return { path, source: relativeSource2(cwd, path), text: null };
8019
8240
  }
8020
8241
  function withKey(text2, key, previous, source, toError) {
8021
- const doc = parseDocument3(text2);
8242
+ const doc = parseDocument4(text2);
8022
8243
  const [first] = doc.errors;
8023
8244
  if (first !== void 0) {
8024
8245
  throw toError(`${source}: invalid YAML: ${first.message}`);
8025
8246
  }
8026
- if (doc.contents !== null && !isMap3(doc.contents)) {
8247
+ if (doc.contents !== null && !isMap4(doc.contents)) {
8027
8248
  throw toError(`${source}: top level must be a mapping.`);
8028
8249
  }
8029
8250
  doc.set(ENCRYPTION_KEY_FIELD, key);
@@ -8293,18 +8514,20 @@ function redactedDerived(f) {
8293
8514
  var EXTERNAL_OWNED_SCHEMA = "external:owned";
8294
8515
  var EXTERNAL_DUPLICATE_SCHEMA = "external:duplicate";
8295
8516
  var EXTERNAL_KEYWORD = "external";
8296
- var PATH_JOIN = "path";
8517
+ var PATH_JOIN2 = "path";
8297
8518
  var parsedManifests = new ManifestCache();
8298
- var posix = (p) => p.split(sep2).join("/");
8519
+ var perPageRules = /* @__PURE__ */ new WeakMap();
8520
+ var strayEntries = /* @__PURE__ */ new WeakSet();
8521
+ var posix4 = (p) => p.split(sep3).join("/");
8299
8522
  function reportedPath(abs, base) {
8300
- const rel = relative7(base, abs);
8301
- return rel === "" ? "." : posix(rel);
8523
+ const rel = relative8(base, abs);
8524
+ return rel === "" ? "." : posix4(rel);
8302
8525
  }
8303
8526
  function reported(of, file) {
8304
8527
  return of.file === file ? of : { ...of, file };
8305
8528
  }
8306
8529
  function externalMetadataJoin(manifest) {
8307
- return manifest.join ?? PATH_JOIN;
8530
+ return manifest.join ?? PATH_JOIN2;
8308
8531
  }
8309
8532
  async function loadExternalMetadata(collections, opts) {
8310
8533
  const configured = collections.flatMap(
@@ -8315,7 +8538,17 @@ async function loadExternalMetadata(collections, opts) {
8315
8538
  const byPath = /* @__PURE__ */ new Map();
8316
8539
  const byField = /* @__PURE__ */ new Map();
8317
8540
  const entries = [];
8541
+ const resolvedBy = /* @__PURE__ */ new Map();
8542
+ const rules = [];
8318
8543
  for (const { collection, manifest } of configured) {
8544
+ const declared = collections.find(
8545
+ (c) => c.name === collection && c.externalMetadata.includes(manifest)
8546
+ );
8547
+ if (declared !== void 0 && classifyRef(manifest.file).kind !== "url" && hasPagePlaceholder(manifest.file)) {
8548
+ await loadPerPage(declared, manifest, opts, resolvedBy, { owners, byPath, byField, entries });
8549
+ rules.push({ collection: declared, manifest, configDir: opts.configDir });
8550
+ continue;
8551
+ }
8319
8552
  const join11 = externalMetadataJoin(manifest);
8320
8553
  let parsed2;
8321
8554
  let file;
@@ -8329,7 +8562,7 @@ async function loadExternalMetadata(collections, opts) {
8329
8562
  });
8330
8563
  parsed2 = parseManifest(manifest, collection, text2, file, opts.configDir, join11);
8331
8564
  } else {
8332
- const abs = isAbsolute4(manifest.file) ? manifest.file : resolve11(opts.configDir, manifest.file);
8565
+ const abs = isAbsolute5(manifest.file) ? manifest.file : resolve12(opts.configDir, manifest.file);
8333
8566
  const label = reportedPath(abs, opts.base);
8334
8567
  file = label;
8335
8568
  parsed2 = await parsedManifests.parse(
@@ -8367,7 +8600,7 @@ async function loadExternalMetadata(collections, opts) {
8367
8600
  else owners.set(key, [{ collection, file }]);
8368
8601
  }
8369
8602
  let target;
8370
- if (join11 === PATH_JOIN) {
8603
+ if (join11 === PATH_JOIN2) {
8371
8604
  target = byPath;
8372
8605
  } else {
8373
8606
  const existing = byField.get(join11);
@@ -8384,23 +8617,112 @@ async function loadExternalMetadata(collections, opts) {
8384
8617
  }
8385
8618
  for (const entry of parsed2.entries) entries.push(reported(entry, file));
8386
8619
  }
8387
- return { owners, byPath, byField, entries };
8620
+ const index = { owners, byPath, byField, entries };
8621
+ if (rules.length > 0) perPageRules.set(index, rules);
8622
+ return index;
8623
+ }
8624
+ async function loadPerPage(declared, manifest, opts, resolvedBy, into) {
8625
+ const collection = declared.name;
8626
+ const join11 = externalMetadataJoin(manifest);
8627
+ for (const key of manifest.keys) {
8628
+ const list2 = into.owners.get(key);
8629
+ if (list2) list2.push({ collection, file: manifest.file });
8630
+ else into.owners.set(key, [{ collection, file: manifest.file }]);
8631
+ }
8632
+ const target = indexFor(join11, into);
8633
+ for (const pageAbs of opts.pages ?? []) {
8634
+ const resolved = pageManifestPath(manifest.file, opts.configDir, pageAbs);
8635
+ if ("outside" in resolved) {
8636
+ if (resolved.pageRel.startsWith("../") && matchesCollectionPaths(declared, resolved.pageRel)) {
8637
+ throw new DocmetaError(outsideRefusal(manifest, collection, resolved.pageRel));
8638
+ }
8639
+ continue;
8640
+ }
8641
+ const rel = posix4(relative8(opts.configDir, pageAbs));
8642
+ if (!isMember(declared, rel)) continue;
8643
+ const earlier = resolvedBy.get(resolved.abs);
8644
+ if (earlier !== void 0 && earlier !== pageAbs) {
8645
+ const [a, b] = [reportedPath(earlier, opts.base), reportedPath(pageAbs, opts.base)].sort();
8646
+ throw new DocmetaError(
8647
+ `${a ?? ""} and ${b ?? ""} both resolve ${PAGE_PLACEHOLDER} to ${reportedPath(resolved.abs, opts.base)}. A ${PAGE_PLACEHOLDER} manifest names one page, so rename one of them.`
8648
+ );
8649
+ }
8650
+ resolvedBy.set(resolved.abs, pageAbs);
8651
+ const label = reportedPath(resolved.abs, opts.base);
8652
+ const parsed2 = await parsedManifests.parse(
8653
+ resolved.abs,
8654
+ JSON.stringify([collection, join11, opts.configDir, [...manifest.keys].sort()]),
8655
+ async () => parseManifest(
8656
+ manifest,
8657
+ collection,
8658
+ await readPageManifest(resolved.abs, label),
8659
+ label,
8660
+ opts.configDir,
8661
+ join11
8662
+ )
8663
+ );
8664
+ const pageLabel = reportedPath(pageAbs, opts.base);
8665
+ for (const entry of parsed2.entries) {
8666
+ if (entry.abs === void 0 || sameFile(entry.abs, pageAbs)) continue;
8667
+ const at2 = entry.line === void 0 ? label : `${label}:${String(entry.line)}`;
8668
+ throw new DocmetaError(
8669
+ `Manifest ${at2} names "${entry.spelled}", but ${PAGE_PLACEHOLDER} resolved this file for "${pageLabel}". A per-page manifest holds one entry, for its own page.`
8670
+ );
8671
+ }
8672
+ for (const [indexKey, values] of parsed2.values) {
8673
+ const key = join11 === PATH_JOIN2 ? pageAbs : indexKey;
8674
+ const merged = target.get(key) ?? /* @__PURE__ */ new Map();
8675
+ for (const [owned, value] of values) merged.set(owned, reported(value, label));
8676
+ target.set(key, merged);
8677
+ }
8678
+ for (const entry of parsed2.entries) {
8679
+ into.entries.push({
8680
+ ...reported(entry, label),
8681
+ ...entry.abs === void 0 ? {} : { abs: pageAbs }
8682
+ });
8683
+ }
8684
+ }
8685
+ }
8686
+ function indexFor(join11, into) {
8687
+ if (join11 === PATH_JOIN2) return into.byPath;
8688
+ const existing = into.byField.get(join11);
8689
+ if (existing) return existing;
8690
+ const created = /* @__PURE__ */ new Map();
8691
+ into.byField.set(join11, created);
8692
+ return created;
8693
+ }
8694
+ function outsideRefusal(manifest, collection, pageRel) {
8695
+ const tail = `for "${pageRel}". A ${PAGE_PLACEHOLDER} manifest stays under the config file.`;
8696
+ const origin = externalMetadataOrigin(manifest);
8697
+ if (origin === void 0) {
8698
+ return `externalMetadata file "${manifest.file}" of collection ${collection} resolves outside the config file's directory ${tail}`;
8699
+ }
8700
+ return `${origin.source}: ${origin.where}.file resolves outside ${origin.source}'s directory ${tail}`;
8701
+ }
8702
+ async function readPageManifest(abs, file) {
8703
+ try {
8704
+ return await readFile6(abs, "utf8");
8705
+ } catch (err) {
8706
+ if (isMissing(err)) return "";
8707
+ throw unreadable(file, err);
8708
+ }
8388
8709
  }
8389
8710
  async function readManifest(abs, file) {
8390
8711
  try {
8391
8712
  return await readFile6(abs, "utf8");
8392
8713
  } catch (err) {
8393
- const reason = err instanceof Error ? err.message : String(err);
8394
- throw new DocmetaError(
8395
- `Manifest ${file} could not be read: ${reason}`
8396
- );
8714
+ throw unreadable(file, err);
8397
8715
  }
8398
8716
  }
8717
+ function unreadable(file, err) {
8718
+ const reason = err instanceof Error ? err.message : String(err);
8719
+ return new DocmetaError(`Manifest ${file} could not be read: ${reason}`);
8720
+ }
8399
8721
  function parseManifest(manifest, collection, text2, file, configDir, join11) {
8400
8722
  const target = /* @__PURE__ */ new Map();
8401
8723
  const entries = [];
8402
- const lc = new LineCounter2();
8403
- const doc = parseDocument4(text2, { lineCounter: lc, uniqueKeys: false });
8724
+ const lc = new LineCounter3();
8725
+ const doc = parseDocument5(text2, { lineCounter: lc, uniqueKeys: false });
8404
8726
  const problem = doc.errors[0];
8405
8727
  if (problem) {
8406
8728
  throw new DocmetaError(
@@ -8408,21 +8730,21 @@ function parseManifest(manifest, collection, text2, file, configDir, join11) {
8408
8730
  );
8409
8731
  }
8410
8732
  const root = doc.contents;
8411
- if (root === null || isScalar2(root) && root.value === null)
8733
+ if (root === null || isScalar3(root) && root.value === null)
8412
8734
  return { values: target, entries };
8413
- if (!isMap4(root)) {
8735
+ if (!isMap5(root)) {
8414
8736
  throw new DocmetaError(
8415
- `Manifest ${file}: the manifest must be a mapping from document ${join11 === PATH_JOIN ? "path" : `"${join11}"`} to owned keys.`
8737
+ `Manifest ${file}: the manifest must be a mapping from document ${join11 === PATH_JOIN2 ? "path" : `"${join11}"`} to owned keys.`
8416
8738
  );
8417
8739
  }
8418
8740
  const owned = new Set(manifest.keys);
8419
8741
  const lineAt = (node) => {
8420
- const range = isNode(node) ? node.range : void 0;
8742
+ const range = isNode2(node) ? node.range : void 0;
8421
8743
  return range ? lc.linePos(range[0]).line : void 0;
8422
8744
  };
8423
8745
  const seen = /* @__PURE__ */ new Map();
8424
8746
  for (const pair of root.items) {
8425
- const spelled = isScalar2(pair.key) ? String(pair.key.value) : String(pair.key);
8747
+ const spelled = isScalar3(pair.key) ? String(pair.key.value) : String(pair.key);
8426
8748
  const entryLine = lineAt(pair.key);
8427
8749
  const where = entryLine === void 0 ? file : `${file}:${entryLine}`;
8428
8750
  if (seen.has(spelled)) {
@@ -8432,15 +8754,15 @@ function parseManifest(manifest, collection, text2, file, configDir, join11) {
8432
8754
  );
8433
8755
  }
8434
8756
  seen.set(spelled, entryLine);
8435
- if (!isMap4(pair.value)) {
8757
+ if (!isMap5(pair.value)) {
8436
8758
  throw new DocmetaError(
8437
8759
  `Manifest ${where}: "${spelled}" must be a mapping of owned keys to values.`
8438
8760
  );
8439
8761
  }
8440
- const indexKey = join11 === PATH_JOIN ? resolve11(configDir, spelled) : spelled;
8762
+ const indexKey = join11 === PATH_JOIN2 ? resolve12(configDir, spelled) : spelled;
8441
8763
  const values = target.get(indexKey) ?? /* @__PURE__ */ new Map();
8442
8764
  for (const kv of pair.value.items) {
8443
- const key = isScalar2(kv.key) ? String(kv.key.value) : String(kv.key);
8765
+ const key = isScalar3(kv.key) ? String(kv.key.value) : String(kv.key);
8444
8766
  if (key === FILE_SCHEMA_KEY) {
8445
8767
  throw new DocmetaError(
8446
8768
  `Manifest ${where}: "${spelled}" sets "${FILE_SCHEMA_KEY}" \u2014 a manifest never chooses the schema a document is judged by; use "overrides" in the config.`
@@ -8466,7 +8788,7 @@ function parseManifest(manifest, collection, text2, file, configDir, join11) {
8466
8788
  entries.push({
8467
8789
  collection,
8468
8790
  join: join11,
8469
- ...join11 === PATH_JOIN ? { abs: indexKey } : {},
8791
+ ...join11 === PATH_JOIN2 ? { abs: indexKey } : {},
8470
8792
  spelled,
8471
8793
  file,
8472
8794
  ...entryLine === void 0 ? {} : { line: entryLine }
@@ -8475,9 +8797,9 @@ function parseManifest(manifest, collection, text2, file, configDir, join11) {
8475
8797
  return { values: target, entries };
8476
8798
  }
8477
8799
  function nodeLines(node, lineAt, prefix = "", out = /* @__PURE__ */ new Map()) {
8478
- if (isMap4(node)) {
8800
+ if (isMap5(node)) {
8479
8801
  for (const kv of node.items) {
8480
- const seg = isScalar2(kv.key) ? String(kv.key.value) : String(kv.key);
8802
+ const seg = isScalar3(kv.key) ? String(kv.key.value) : String(kv.key);
8481
8803
  const pointer = `${prefix}/${escapePointerSegment(seg)}`;
8482
8804
  const line = lineAt(kv.key) ?? lineAt(kv.value);
8483
8805
  if (line !== void 0) out.set(pointer, line);
@@ -8516,7 +8838,7 @@ function mergeExternalMetadata(label, extracted, index, memberOf2, base, options
8516
8838
  const sources = [];
8517
8839
  const joins = [];
8518
8840
  if (label !== STDIN_LABEL) {
8519
- const byPath = index.byPath.get(resolve11(base, label));
8841
+ const byPath = index.byPath.get(resolve12(base, label));
8520
8842
  if (byPath) sources.push(byPath);
8521
8843
  }
8522
8844
  for (const [field, byValue] of index.byField) {
@@ -8611,10 +8933,41 @@ function externalMetadataPointer(key) {
8611
8933
  }
8612
8934
  function orphanEntries(index, loaded, base, collections) {
8613
8935
  if (!index) return [];
8614
- const have = new Set(loaded.map((l) => resolve11(base, l)));
8615
- return index.entries.filter(
8616
- (e) => inCollections(e, collections) && e.join === PATH_JOIN && e.abs !== void 0 && !have.has(e.abs)
8936
+ const have = new Set(loaded.map((l) => resolve12(base, l)));
8937
+ const orphans = index.entries.filter(
8938
+ (e) => inCollections(e, collections) && e.join === PATH_JOIN2 && e.abs !== void 0 && !have.has(e.abs)
8617
8939
  );
8940
+ if (collections !== void 0) return orphans;
8941
+ return [...orphans, ...strayManifests(perPageRules.get(index) ?? [], have, base)];
8942
+ }
8943
+ function strayManifests(rules, loaded, base) {
8944
+ const strays = [];
8945
+ for (const { collection, manifest, configDir } of rules) {
8946
+ const read = /* @__PURE__ */ new Set();
8947
+ for (const pageAbs of loaded) {
8948
+ const resolved = pageManifestPath(manifest.file, configDir, pageAbs);
8949
+ if (!("outside" in resolved)) read.add(resolved.abs);
8950
+ }
8951
+ const found = findPageManifests(manifest.file, configDir, (rel) => isMember(collection, rel));
8952
+ for (const { abs, entries } of found) {
8953
+ if (read.has(abs)) continue;
8954
+ const file = reportedPath(abs, base);
8955
+ for (const entry of entries) {
8956
+ if (loaded.has(entry.pageAbs)) continue;
8957
+ const stray = {
8958
+ collection: collection.name,
8959
+ join: PATH_JOIN2,
8960
+ abs: entry.pageAbs,
8961
+ spelled: entry.spelled,
8962
+ file,
8963
+ ...entry.line === void 0 ? {} : { line: entry.line }
8964
+ };
8965
+ strayEntries.add(stray);
8966
+ strays.push(stray);
8967
+ }
8968
+ }
8969
+ }
8970
+ return strays;
8618
8971
  }
8619
8972
  function inCollections(entry, collections) {
8620
8973
  return collections === void 0 || collections.includes(entry.collection);
@@ -8622,7 +8975,7 @@ function inCollections(entry, collections) {
8622
8975
  function orphanJoins(index, matched, collections) {
8623
8976
  if (!index) return [];
8624
8977
  return index.entries.filter(
8625
- (e) => inCollections(e, collections) && e.join !== PATH_JOIN && !(matched.get(e.join)?.has(e.spelled) ?? false)
8978
+ (e) => inCollections(e, collections) && e.join !== PATH_JOIN2 && !(matched.get(e.join)?.has(e.spelled) ?? false)
8626
8979
  );
8627
8980
  }
8628
8981
  function orphanError(orphans) {
@@ -8630,9 +8983,10 @@ function orphanError(orphans) {
8630
8983
  if (!first) throw new Error("orphanError called with no orphans");
8631
8984
  const where = first.line === void 0 ? first.file : `${first.file}:${first.line}`;
8632
8985
  const more = orphans.length > 1 ? ` (${orphans.length - 1} more)` : "";
8633
- const what = first.join === PATH_JOIN ? `names "${first.spelled}", which this run did not load` : `names ${first.join} "${first.spelled}", which no loaded document carries`;
8986
+ const what = first.join === PATH_JOIN2 ? `names "${first.spelled}", which this run did not load` : `names ${first.join} "${first.spelled}", which no loaded document carries`;
8987
+ const remove = strayEntries.has(first) ? "the file" : "it";
8634
8988
  return new DocmetaError(
8635
- `Manifest ${where} ${what}${more}. Fix the entry, or remove it.`
8989
+ `Manifest ${where} ${what}${more}. Fix the entry, or remove ${remove}.`
8636
8990
  );
8637
8991
  }
8638
8992
 
@@ -8646,7 +9000,7 @@ function isFieldLocation(value) {
8646
9000
  }
8647
9001
 
8648
9002
  // src/meta/core/collections.ts
8649
- import { isAbsolute as isAbsolute5, relative as relative8, resolve as resolvePath2, sep as sep3 } from "node:path";
9003
+ import { isAbsolute as isAbsolute6, relative as relative9, resolve as resolvePath2, sep as sep4 } from "node:path";
8650
9004
 
8651
9005
  // src/meta/core/projection.ts
8652
9006
  var SYSTEM_COLUMNS = ["_path", "_format", "_present", "_data"];
@@ -8867,8 +9221,8 @@ function loadSqlite() {
8867
9221
  function memberOf(collections, configDir, base, label) {
8868
9222
  if (collections.length === 0) return [];
8869
9223
  if (label === STDIN_LABEL) return [];
8870
- const rel = relative8(configDir, resolvePath2(base, label)).split(sep3).join("/");
8871
- if (isAbsolute5(rel)) return [];
9224
+ const rel = relative9(configDir, resolvePath2(base, label)).split(sep4).join("/");
9225
+ if (isAbsolute6(rel)) return [];
8872
9226
  return collections.filter((c) => isMember(c, rel)).map((c) => c.name);
8873
9227
  }
8874
9228
  function retainMembers(files, membersFor) {
@@ -9322,7 +9676,7 @@ function toFieldError(schema, e, lineFor, colFor, locate) {
9322
9676
 
9323
9677
  // src/meta/core/derive/cache.ts
9324
9678
  import { createHash as createHash4 } from "node:crypto";
9325
- import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
9679
+ import { mkdirSync, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync } from "node:fs";
9326
9680
  import { join as join7 } from "node:path";
9327
9681
  var REVIEW_CACHE_DIR = ".manni/meta/review-cache";
9328
9682
  var REVIEW_CACHE_VERSION = 1;
@@ -9342,7 +9696,7 @@ var ReviewCache = class {
9342
9696
  if (!this.enabled) return void 0;
9343
9697
  let raw;
9344
9698
  try {
9345
- raw = JSON.parse(readFileSync(this.entryPath(key), "utf8"));
9699
+ raw = JSON.parse(readFileSync2(this.entryPath(key), "utf8"));
9346
9700
  } catch {
9347
9701
  return void 0;
9348
9702
  }
@@ -9906,12 +10260,12 @@ function utcDate(iso) {
9906
10260
  }
9907
10261
 
9908
10262
  // src/meta/core/derive/index.ts
9909
- import { dirname as dirname9, resolve as resolve13 } from "node:path";
10263
+ import { dirname as dirname9, resolve as resolve14 } from "node:path";
9910
10264
 
9911
10265
  // src/meta/core/derive/codeowners.ts
9912
10266
  import { existsSync as existsSync3 } from "node:fs";
9913
10267
  import { readFile as readFile7 } from "node:fs/promises";
9914
- import { dirname as dirname7, relative as relative9, resolve as resolve12, sep as sep4 } from "node:path";
10268
+ import { dirname as dirname7, relative as relative10, resolve as resolve13, sep as sep5 } from "node:path";
9915
10269
  import picomatch4 from "picomatch";
9916
10270
  var CODEOWNERS_LOCATIONS = [
9917
10271
  ".github/CODEOWNERS",
@@ -9921,11 +10275,11 @@ var CODEOWNERS_LOCATIONS = [
9921
10275
  ];
9922
10276
  function findCodeowners(root, explicit) {
9923
10277
  if (explicit !== void 0) {
9924
- const path = resolve12(root, explicit);
10278
+ const path = resolve13(root, explicit);
9925
10279
  return existsSync3(path) ? path : null;
9926
10280
  }
9927
10281
  for (const loc of CODEOWNERS_LOCATIONS) {
9928
- const path = resolve12(root, ...loc.split("/"));
10282
+ const path = resolve13(root, ...loc.split("/"));
9929
10283
  if (existsSync3(path)) return path;
9930
10284
  }
9931
10285
  return null;
@@ -10040,7 +10394,7 @@ function toGlobs(pattern) {
10040
10394
  var NOT_FOUND_REASON = `no CODEOWNERS file found (looked for ${CODEOWNERS_LOCATIONS.join(", ")})`;
10041
10395
  async function deriveFromCodeowners(inputs, opts) {
10042
10396
  const records = /* @__PURE__ */ new Map();
10043
- const explicitPath = opts.explicit === void 0 ? void 0 : resolve12(opts.configDir ?? process.cwd(), opts.explicit);
10397
+ const explicitPath = opts.explicit === void 0 ? void 0 : resolve13(opts.configDir ?? process.cwd(), opts.explicit);
10044
10398
  if (explicitPath !== void 0 && !existsSync3(explicitPath)) {
10045
10399
  return {
10046
10400
  status: { available: false, reason: `CODEOWNERS not found at ${explicitPath}` },
@@ -10070,7 +10424,7 @@ async function deriveFromCodeowners(inputs, opts) {
10070
10424
  continue;
10071
10425
  }
10072
10426
  found += 1;
10073
- const label = toPosix2(relative9(opts.configDir ?? root, path));
10427
+ const label = toPosix2(relative10(opts.configDir ?? root, path));
10074
10428
  const key = `${path}\0${label}`;
10075
10429
  let file = parsed2.get(key);
10076
10430
  if (!file) {
@@ -10092,7 +10446,7 @@ async function deriveFromCodeowners(inputs, opts) {
10092
10446
  };
10093
10447
  }
10094
10448
  for (const input of bucket) {
10095
- const rel = toPosix2(relative9(root, input.absPath));
10449
+ const rel = toPosix2(relative10(root, input.absPath));
10096
10450
  const hit = ownersFor(owners, rel);
10097
10451
  records.set(
10098
10452
  input.label,
@@ -10116,13 +10470,13 @@ async function deriveFromCodeowners(inputs, opts) {
10116
10470
  return { status: { available: true }, records };
10117
10471
  }
10118
10472
  function toPosix2(p) {
10119
- return p.split(sep4).join("/");
10473
+ return p.split(sep5).join("/");
10120
10474
  }
10121
10475
 
10122
10476
  // src/meta/core/derive/git.ts
10123
10477
  import { constants as bufferConstants } from "node:buffer";
10124
10478
  import { spawn as spawn3 } from "node:child_process";
10125
- import { dirname as dirname8, extname as extname2, isAbsolute as isAbsolute6, posix as posix2, relative as relative10, sep as sep5 } from "node:path";
10479
+ import { dirname as dirname8, extname as extname2, isAbsolute as isAbsolute7, posix as posix5, relative as relative11, sep as sep6 } from "node:path";
10126
10480
  import { parse as parseYaml4 } from "yaml";
10127
10481
 
10128
10482
  // src/shared/pin.ts
@@ -10772,7 +11126,7 @@ function groupByRoot(inputs) {
10772
11126
  rootOf.set(dir, root);
10773
11127
  }
10774
11128
  if (root === null) continue;
10775
- const rel = relative10(root, input.absPath).split(sep5).join("/");
11129
+ const rel = relative11(root, input.absPath).split(sep6).join("/");
10776
11130
  const bucket = byRoot.get(root);
10777
11131
  if (bucket) bucket.push({ input, rel });
10778
11132
  else byRoot.set(root, [{ input, rel }]);
@@ -11225,14 +11579,14 @@ async function commitEvidence(run2, root, entry, blame, history, blobs, manifest
11225
11579
  return commits;
11226
11580
  }
11227
11581
  function insideRoot(root, abs) {
11228
- const rel = relative10(root, abs);
11229
- if (rel === "" || rel.startsWith("..") || isAbsolute6(rel)) return void 0;
11230
- return rel.split(sep5).join("/");
11582
+ const rel = relative11(root, abs);
11583
+ if (rel === "" || rel.startsWith("..") || isAbsolute7(rel)) return void 0;
11584
+ return rel.split(sep6).join("/");
11231
11585
  }
11232
11586
  function entryAt(entry, rel, relAtCommit) {
11233
11587
  if (relAtCommit === rel) return entry;
11234
- const fromHere = posix2.relative(posix2.dirname(rel), relAtCommit);
11235
- return posix2.join(posix2.dirname(entry), fromHere);
11588
+ const fromHere = posix5.relative(posix5.dirname(rel), relAtCommit);
11589
+ return posix5.join(posix5.dirname(entry), fromHere);
11236
11590
  }
11237
11591
  function manifestStamp(text2, key, join11) {
11238
11592
  if (text2 === void 0) return [];
@@ -11244,7 +11598,7 @@ function manifestStamp(text2, key, join11) {
11244
11598
  }
11245
11599
  if (!isRecord5(data)) return [];
11246
11600
  for (const [candidate, value] of Object.entries(data)) {
11247
- const same2 = join11 === "path" ? posix2.normalize(candidate) === posix2.normalize(key) : candidate === key;
11601
+ const same2 = join11 === "path" ? posix5.normalize(candidate) === posix5.normalize(key) : candidate === key;
11248
11602
  if (same2 && isRecord5(value)) return provenanceEntries(value[PROVENANCE_FIELD]);
11249
11603
  }
11250
11604
  return [];
@@ -11554,7 +11908,7 @@ async function deriveReviewsByRoot(inputs, git2, rootFor, ctx, requested) {
11554
11908
  if (byRoot.size === 0) {
11555
11909
  return unavailable3(requested, "no git repository contains the documents");
11556
11910
  }
11557
- const cacheDir = resolve13(ctx.configDir ?? ctx.cwd, REVIEW_CACHE_DIR);
11911
+ const cacheDir = resolve14(ctx.configDir ?? ctx.cwd, REVIEW_CACHE_DIR);
11558
11912
  for (const [root, bucket] of byRoot) {
11559
11913
  const client = ctx.reviews ?? await createReviewClient(root, { cwd: ctx.cwd, cache: ctx.cache, cacheDir, requested });
11560
11914
  const identity = await client.detect();
@@ -11736,33 +12090,33 @@ import { readFile as readFile9 } from "node:fs/promises";
11736
12090
 
11737
12091
  // src/meta/core/relocation.ts
11738
12092
  import { access, readFile as readFile8, stat as stat6, unlink } from "node:fs/promises";
11739
- import { basename as basename5, extname as extname3, isAbsolute as isAbsolute7, join as join8, relative as relative11, resolve as resolve14, sep as sep6 } from "node:path";
12093
+ import { basename as basename5, extname as extname3, isAbsolute as isAbsolute8, join as join8, relative as relative12, resolve as resolve15, sep as sep7 } from "node:path";
11740
12094
  import picomatch6 from "picomatch";
11741
12095
  import {
11742
- LineCounter as LineCounter4,
11743
- isMap as isMap6,
11744
- isNode as isNode3,
11745
- isScalar as isScalar4,
12096
+ LineCounter as LineCounter5,
12097
+ isMap as isMap7,
12098
+ isNode as isNode4,
12099
+ isScalar as isScalar5,
11746
12100
  isSeq as isSeq4,
11747
12101
  parse as parseYaml5,
11748
- parseDocument as parseDocument6
12102
+ parseDocument as parseDocument7
11749
12103
  } from "yaml";
11750
12104
 
11751
12105
  // src/meta/core/external-metadata-write.ts
11752
- import { posix as posix3 } from "node:path";
12106
+ import { posix as posix6 } from "node:path";
11753
12107
  import {
11754
- LineCounter as LineCounter3,
11755
- isMap as isMap5,
11756
- isNode as isNode2,
12108
+ LineCounter as LineCounter4,
12109
+ isMap as isMap6,
12110
+ isNode as isNode3,
11757
12111
  isPair,
11758
- isScalar as isScalar3,
12112
+ isScalar as isScalar4,
11759
12113
  isSeq as isSeq3,
11760
- parseDocument as parseDocument5,
12114
+ parseDocument as parseDocument6,
11761
12115
  stringify as stringify2
11762
12116
  } from "yaml";
11763
12117
  function spliceManifestValue(text2, options) {
11764
12118
  const { entry, key, value, file } = options;
11765
- const join11 = options.join ?? PATH_JOIN;
12119
+ const join11 = options.join ?? PATH_JOIN2;
11766
12120
  const at2 = (line2) => {
11767
12121
  const name2 = file === void 0 ? "Manifest" : `Manifest ${file}`;
11768
12122
  if (line2 === void 0) return name2;
@@ -11777,8 +12131,8 @@ function spliceManifestValue(text2, options) {
11777
12131
  throw new DocmetaError(`${at2()}: refusing to write "${key}" for "${entry}" with no value.`);
11778
12132
  }
11779
12133
  const eol = detectEol(text2);
11780
- const lc = new LineCounter3();
11781
- const doc = parseDocument5(text2, { lineCounter: lc, uniqueKeys: false });
12134
+ const lc = new LineCounter4();
12135
+ const doc = parseDocument6(text2, { lineCounter: lc, uniqueKeys: false });
11782
12136
  const problem = doc.errors[0];
11783
12137
  if (problem) {
11784
12138
  throw new DocmetaError(`${at2()} is not valid YAML: ${problem.message}`);
@@ -11787,15 +12141,15 @@ function spliceManifestValue(text2, options) {
11787
12141
  const r = rangeOf(node);
11788
12142
  return r ? lc.linePos(r[0]).line : void 0;
11789
12143
  };
11790
- const same2 = join11 === PATH_JOIN ? (a, b) => posix3.normalize(a) === posix3.normalize(b) : (a, b) => a === b;
12144
+ const same2 = join11 === PATH_JOIN2 ? (a, b) => posix6.normalize(a) === posix6.normalize(b) : (a, b) => a === b;
11791
12145
  const root = doc.contents;
11792
12146
  let spelled = entry;
11793
12147
  let edit;
11794
- if (root === null || isScalar3(root) && root.value === null && emptySource(text2, root)) {
12148
+ if (root === null || isScalar4(root) && root.value === null && emptySource(text2, root)) {
11795
12149
  edit = startManifest(text2, entry, key, value, eol);
11796
- } else if (!isMap5(root)) {
12150
+ } else if (!isMap6(root)) {
11797
12151
  throw new DocmetaError(
11798
- `${at2()}: the manifest must be a mapping from document ${join11 === PATH_JOIN ? "path" : `"${join11}"`} to owned keys.`
12152
+ `${at2()}: the manifest must be a mapping from document ${join11 === PATH_JOIN2 ? "path" : `"${join11}"`} to owned keys.`
11799
12153
  );
11800
12154
  } else {
11801
12155
  const style = detectStyle(text2, root, eol);
@@ -11817,7 +12171,7 @@ function spliceManifestValue(text2, options) {
11817
12171
  spelled = keyString(first.key);
11818
12172
  const where = at2(keyLine(first.key));
11819
12173
  const v = first.value;
11820
- if (!isMap5(v)) {
12174
+ if (!isMap6(v)) {
11821
12175
  throw new DocmetaError(`${where}: "${spelled}" must be a mapping of owned keys to values.`);
11822
12176
  }
11823
12177
  const hits = v.items.filter((kv) => keyString(kv.key) === key);
@@ -11860,30 +12214,30 @@ function spliceManifestValue(text2, options) {
11860
12214
  return { text: out, line };
11861
12215
  }
11862
12216
  function readManifestValue(text2, options) {
11863
- const join11 = options.join ?? PATH_JOIN;
11864
- const doc = parseDocument5(text2, { uniqueKeys: false });
12217
+ const join11 = options.join ?? PATH_JOIN2;
12218
+ const doc = parseDocument6(text2, { uniqueKeys: false });
11865
12219
  if (doc.errors.length > 0) return void 0;
11866
12220
  const root = doc.contents;
11867
- if (!isMap5(root)) return void 0;
11868
- const same2 = join11 === PATH_JOIN ? (a, b) => posix3.normalize(a) === posix3.normalize(b) : (a, b) => a === b;
12221
+ if (!isMap6(root)) return void 0;
12222
+ const same2 = join11 === PATH_JOIN2 ? (a, b) => posix6.normalize(a) === posix6.normalize(b) : (a, b) => a === b;
11869
12223
  const pair = root.items.find((p) => same2(keyString(p.key), options.entry));
11870
12224
  const held = pair?.value;
11871
- if (!isMap5(held)) return void 0;
12225
+ if (!isMap6(held)) return void 0;
11872
12226
  const kv = held.items.find((p) => keyString(p.key) === options.key);
11873
12227
  const node = kv?.value;
11874
- return isNode2(node) ? node.toJSON() : void 0;
12228
+ return isNode3(node) ? node.toJSON() : void 0;
11875
12229
  }
11876
12230
  function removeManifestKey(text2, options) {
11877
12231
  const { entry, key, file } = options;
11878
- const join11 = options.join ?? PATH_JOIN;
12232
+ const join11 = options.join ?? PATH_JOIN2;
11879
12233
  const at2 = (line) => {
11880
12234
  const name2 = file === void 0 ? "Manifest" : `Manifest ${file}`;
11881
12235
  if (line === void 0) return name2;
11882
12236
  return file === void 0 ? `${name2} line ${String(line)}` : `${name2}:${String(line)}`;
11883
12237
  };
11884
12238
  const eol = detectEol(text2);
11885
- const lc = new LineCounter3();
11886
- const doc = parseDocument5(text2, { lineCounter: lc, uniqueKeys: false });
12239
+ const lc = new LineCounter4();
12240
+ const doc = parseDocument6(text2, { lineCounter: lc, uniqueKeys: false });
11887
12241
  const problem = doc.errors[0];
11888
12242
  if (problem) {
11889
12243
  throw new DocmetaError(`${at2()} is not valid YAML: ${problem.message}`);
@@ -11893,13 +12247,13 @@ function removeManifestKey(text2, options) {
11893
12247
  return r ? lc.linePos(r[0]).line : void 0;
11894
12248
  };
11895
12249
  const root = doc.contents;
11896
- if (root === null || isScalar3(root) && root.value === null) return { text: text2 };
11897
- if (!isMap5(root)) {
12250
+ if (root === null || isScalar4(root) && root.value === null) return { text: text2 };
12251
+ if (!isMap6(root)) {
11898
12252
  throw new DocmetaError(
11899
- `${at2()}: the manifest must be a mapping from document ${join11 === PATH_JOIN ? "path" : `"${join11}"`} to owned keys.`
12253
+ `${at2()}: the manifest must be a mapping from document ${join11 === PATH_JOIN2 ? "path" : `"${join11}"`} to owned keys.`
11900
12254
  );
11901
12255
  }
11902
- const same2 = join11 === PATH_JOIN ? (a, b) => posix3.normalize(a) === posix3.normalize(b) : (a, b) => a === b;
12256
+ const same2 = join11 === PATH_JOIN2 ? (a, b) => posix6.normalize(a) === posix6.normalize(b) : (a, b) => a === b;
11903
12257
  const matches = root.items.filter((p) => same2(keyString(p.key), entry));
11904
12258
  const [first, second] = matches;
11905
12259
  if (first && second) {
@@ -11911,7 +12265,7 @@ function removeManifestKey(text2, options) {
11911
12265
  const spelled = keyString(first.key);
11912
12266
  const where = at2(keyLine(first.key));
11913
12267
  const v = first.value;
11914
- if (!isMap5(v)) {
12268
+ if (!isMap6(v)) {
11915
12269
  throw new DocmetaError(`${where}: "${spelled}" must be a mapping of owned keys to values.`);
11916
12270
  }
11917
12271
  const hits = v.items.filter((kv) => keyString(kv.key) === key);
@@ -11944,7 +12298,7 @@ function removeManifestKey(text2, options) {
11944
12298
  else if (start === 0 && text2.startsWith(eol, end)) end += eol.length;
11945
12299
  }
11946
12300
  const out = text2.slice(0, start) + text2.slice(end);
11947
- const after = parseDocument5(out, { uniqueKeys: false });
12301
+ const after = parseDocument6(out, { uniqueKeys: false });
11948
12302
  const fail2 = (why) => new DocmetaError(
11949
12303
  `${at2()}: removing "${key}" for "${entry}" did not read back as removed, so nothing was written. ${why}`
11950
12304
  );
@@ -11964,14 +12318,14 @@ function removeManifestKey(text2, options) {
11964
12318
  }
11965
12319
  function renameManifestEntry(text2, options) {
11966
12320
  const { entry, to, file } = options;
11967
- const join11 = options.join ?? PATH_JOIN;
12321
+ const join11 = options.join ?? PATH_JOIN2;
11968
12322
  const at2 = (line) => {
11969
12323
  const name2 = file === void 0 ? "Manifest" : `Manifest ${file}`;
11970
12324
  if (line === void 0) return name2;
11971
12325
  return file === void 0 ? `${name2} line ${String(line)}` : `${name2}:${String(line)}`;
11972
12326
  };
11973
- const lc = new LineCounter3();
11974
- const doc = parseDocument5(text2, { lineCounter: lc, uniqueKeys: false });
12327
+ const lc = new LineCounter4();
12328
+ const doc = parseDocument6(text2, { lineCounter: lc, uniqueKeys: false });
11975
12329
  const problem = doc.errors[0];
11976
12330
  if (problem) {
11977
12331
  throw new DocmetaError(`${at2()} is not valid YAML: ${problem.message}`);
@@ -11981,13 +12335,13 @@ function renameManifestEntry(text2, options) {
11981
12335
  return r2 ? lc.linePos(r2[0]).line : void 0;
11982
12336
  };
11983
12337
  const root = doc.contents;
11984
- if (root === null || isScalar3(root) && root.value === null) return { text: text2 };
11985
- if (!isMap5(root)) {
12338
+ if (root === null || isScalar4(root) && root.value === null) return { text: text2 };
12339
+ if (!isMap6(root)) {
11986
12340
  throw new DocmetaError(
11987
- `${at2()}: the manifest must be a mapping from document ${join11 === PATH_JOIN ? "path" : `"${join11}"`} to owned keys.`
12341
+ `${at2()}: the manifest must be a mapping from document ${join11 === PATH_JOIN2 ? "path" : `"${join11}"`} to owned keys.`
11988
12342
  );
11989
12343
  }
11990
- const same2 = join11 === PATH_JOIN ? (a, b) => posix3.normalize(a) === posix3.normalize(b) : (a, b) => a === b;
12344
+ const same2 = join11 === PATH_JOIN2 ? (a, b) => posix6.normalize(a) === posix6.normalize(b) : (a, b) => a === b;
11991
12345
  const [first, second] = root.items.filter((p) => same2(keyString(p.key), entry));
11992
12346
  if (first && second) {
11993
12347
  throw new DocmetaError(
@@ -12009,11 +12363,11 @@ function renameManifestEntry(text2, options) {
12009
12363
  `${at2(keyLine(first.key))}: "${spelled}" is written in a form this writer does not edit. Write it as "${spelled}: \u2026".`
12010
12364
  );
12011
12365
  }
12012
- const quoted = isScalar3(first.key) && (first.key.type === "QUOTE_DOUBLE" || first.key.type === "QUOTE_SINGLE");
12366
+ const quoted = isScalar4(first.key) && (first.key.type === "QUOTE_DOUBLE" || first.key.type === "QUOTE_SINGLE");
12013
12367
  const plain = stringify2(to, { lineWidth: 0 }).replace(/\n$/, "");
12014
12368
  const insert = quoted || plain.includes("\n") ? JSON.stringify(to) : plain;
12015
12369
  const out = text2.slice(0, r[0]) + insert + text2.slice(end);
12016
- const after = parseDocument5(out, { uniqueKeys: false });
12370
+ const after = parseDocument6(out, { uniqueKeys: false });
12017
12371
  const fail2 = (why) => new DocmetaError(
12018
12372
  `${at2()}: renaming "${spelled}" to "${to}" did not read back as renamed, so nothing was written. ${why}`
12019
12373
  );
@@ -12047,7 +12401,7 @@ function appendEntry(text2, root, entry, key, value, style) {
12047
12401
  const col = firstKey ? colOf(text2, firstKey[0]) : 0;
12048
12402
  const pos = lineEnd(text2, contentEnd(text2, root) ?? text2.length);
12049
12403
  const last = root.items.at(-1);
12050
- const spaced = root.items.length > 1 && isNode2(last?.key) && last.key.spaceBefore === true;
12404
+ const spaced = root.items.length > 1 && isNode3(last?.key) && last.key.spaceBefore === true;
12051
12405
  return {
12052
12406
  start: pos,
12053
12407
  end: pos,
@@ -12087,7 +12441,7 @@ function replaceBlockValue(text2, pair, value, style) {
12087
12441
  ownLine = true;
12088
12442
  }
12089
12443
  const old = pair.value;
12090
- const oldEnd = isScalar3(old) && old.value === null && emptySource(text2, old) ? start : contentEnd(text2, old) ?? start;
12444
+ const oldEnd = isScalar4(old) && old.value === null && emptySource(text2, old) ? start : contentEnd(text2, old) ?? start;
12091
12445
  const end = Math.max(start, oldEnd);
12092
12446
  let insert = afterColon(value, keyCol, style);
12093
12447
  if (ownLine && !insert.startsWith(style.eol)) {
@@ -12102,8 +12456,8 @@ function replaceFlowValue(text2, pair, value) {
12102
12456
  return { start: r[0], end, insert: flowText(value) };
12103
12457
  }
12104
12458
  function readBack(before, out, spelled, key, value, fail2) {
12105
- const lc = new LineCounter3();
12106
- const after = parseDocument5(out, { lineCounter: lc, uniqueKeys: false });
12459
+ const lc = new LineCounter4();
12460
+ const after = parseDocument6(out, { lineCounter: lc, uniqueKeys: false });
12107
12461
  const problem = after.errors[0];
12108
12462
  if (problem) throw fail2(`The result is not valid YAML: ${problem.message}`);
12109
12463
  const was = before.toJS({ maxAliasCount: 100 });
@@ -12124,9 +12478,9 @@ function readBack(before, out, spelled, key, value, fail2) {
12124
12478
  throw fail2("Another value in the manifest would have changed.");
12125
12479
  }
12126
12480
  const root = after.contents;
12127
- const pair = isMap5(root) ? root.items.find((p) => keyString(p.key) === spelled) : void 0;
12481
+ const pair = isMap6(root) ? root.items.find((p) => keyString(p.key) === spelled) : void 0;
12128
12482
  const entryMap = pair?.value;
12129
- const kv = isMap5(entryMap) ? entryMap.items.find((p) => keyString(p.key) === key) : void 0;
12483
+ const kv = isMap6(entryMap) ? entryMap.items.find((p) => keyString(p.key) === key) : void 0;
12130
12484
  const r = rangeOf(kv?.value) ?? rangeOf(kv?.key);
12131
12485
  if (!r) throw fail2("The written value could not be found again.");
12132
12486
  return lc.linePos(r[0]).line;
@@ -12157,7 +12511,7 @@ function detectStyle(text2, root, eol) {
12157
12511
  for (const pair of root.items) {
12158
12512
  const v = pair.value;
12159
12513
  const entryKey = rangeOf(pair.key);
12160
- const ownedKey = isMap5(v) && v.flow !== true ? rangeOf(v.items[0]?.key) : void 0;
12514
+ const ownedKey = isMap6(v) && v.flow !== true ? rangeOf(v.items[0]?.key) : void 0;
12161
12515
  if (entryKey && ownedKey) {
12162
12516
  const d = colOf(text2, ownedKey[0]) - colOf(text2, entryKey[0]);
12163
12517
  if (d > 0) {
@@ -12169,7 +12523,7 @@ function detectStyle(text2, root, eol) {
12169
12523
  return { step, indentSeq: seqIndented(text2, root) ?? true, eol };
12170
12524
  }
12171
12525
  function seqIndented(text2, node) {
12172
- if (isMap5(node)) {
12526
+ if (isMap6(node)) {
12173
12527
  for (const pair of node.items) {
12174
12528
  const v = pair.value;
12175
12529
  if (isSeq3(v) && v.flow !== true && v.items.length > 0) {
@@ -12189,7 +12543,7 @@ function seqIndented(text2, node) {
12189
12543
  return void 0;
12190
12544
  }
12191
12545
  function contentEnd(text2, node) {
12192
- if (isMap5(node) && node.flow !== true) {
12546
+ if (isMap6(node) && node.flow !== true) {
12193
12547
  const last = node.items.at(-1);
12194
12548
  if (last) return contentEnd(text2, last.value) ?? contentEnd(text2, last.key);
12195
12549
  } else if (isSeq3(node) && node.flow !== true) {
@@ -12223,14 +12577,14 @@ function colOf(text2, offset) {
12223
12577
  return offset - start;
12224
12578
  }
12225
12579
  function rangeOf(node) {
12226
- return isNode2(node) ? node.range ?? void 0 : void 0;
12580
+ return isNode3(node) ? node.range ?? void 0 : void 0;
12227
12581
  }
12228
12582
  function emptySource(text2, node) {
12229
12583
  const r = rangeOf(node);
12230
12584
  return !r || text2.slice(r[0], r[1]).trim() === "";
12231
12585
  }
12232
12586
  function keyString(key) {
12233
- return isScalar3(key) ? String(key.value) : String(key);
12587
+ return isScalar4(key) ? String(key.value) : String(key);
12234
12588
  }
12235
12589
  function isRecord6(x) {
12236
12590
  return typeof x === "object" && x !== null && !Array.isArray(x);
@@ -12260,13 +12614,13 @@ function relocationContext(run2, extras) {
12260
12614
  ...extras.onNotice !== void 0 ? { onNotice: extras.onNotice } : {}
12261
12615
  };
12262
12616
  }
12263
- var toPosix3 = (p) => p.split(sep6).join("/");
12617
+ var toPosix3 = (p) => p.split(sep7).join("/");
12264
12618
  function reported2(abs, base) {
12265
- const rel = relative11(base, abs);
12619
+ const rel = relative12(base, abs);
12266
12620
  return rel === "" ? "." : toPosix3(rel);
12267
12621
  }
12268
12622
  function configSource(path, cwd) {
12269
- return toPosix3(relative11(cwd, path)) || basename5(path);
12623
+ return toPosix3(relative12(cwd, path)) || basename5(path);
12270
12624
  }
12271
12625
  function cloneCollection(c) {
12272
12626
  return {
@@ -12284,7 +12638,8 @@ var Model = class {
12284
12638
  for (const c of this.collections) {
12285
12639
  c.externalMetadata.forEach((m, index) => {
12286
12640
  const url = classifyRef(m.file).kind === "url";
12287
- const absPath = url ? void 0 : isAbsolute7(m.file) ? m.file : resolve14(configDir, m.file);
12641
+ const perPage = !url && hasPagePlaceholder(m.file);
12642
+ const absPath = url || perPage ? void 0 : isAbsolute8(m.file) ? m.file : resolve15(configDir, m.file);
12288
12643
  this.manifests.push({
12289
12644
  collection: c.name,
12290
12645
  index,
@@ -12293,6 +12648,7 @@ var Model = class {
12293
12648
  absPath,
12294
12649
  file: absPath === void 0 ? m.file : reported2(absPath, ctx.base),
12295
12650
  join: externalMetadataJoin(m),
12651
+ perPage,
12296
12652
  created: false,
12297
12653
  keysBefore: [...m.keys],
12298
12654
  keysAdded: [],
@@ -12319,6 +12675,22 @@ var Model = class {
12319
12675
  manifestsOf(collection) {
12320
12676
  return this.manifests.filter((m) => m.collection === collection);
12321
12677
  }
12678
+ /**
12679
+ * The one file `m` keeps a page's values in, and how the run reports it:
12680
+ * the page's own for a `{page}` manifest (proposal 0058), the declared file
12681
+ * for any other. `abs` is undefined only for a URL manifest.
12682
+ */
12683
+ fileFor(m, pageAbs) {
12684
+ if (!m.perPage) return { abs: m.absPath, file: m.file };
12685
+ const resolved = pageManifestPath(m.written, this.configDir, pageAbs);
12686
+ if ("outside" in resolved) {
12687
+ const declared = this.ctx.declaredCollections.find((c) => c.name === m.collection)?.externalMetadata[m.index];
12688
+ throw new DocmetaError(
12689
+ outsideRefusal(declared ?? { file: m.written, keys: [] }, m.collection, resolved.pageRel)
12690
+ );
12691
+ }
12692
+ return { abs: resolved.abs, file: reported2(resolved.abs, this.ctx.base) };
12693
+ }
12322
12694
  /** The manifest of one of `members` that owns `key`, in declaration order. */
12323
12695
  ownerOf(key, members) {
12324
12696
  return this.manifests.find(
@@ -12404,7 +12776,8 @@ var Model = class {
12404
12776
  url: false,
12405
12777
  absPath,
12406
12778
  file,
12407
- join: PATH_JOIN,
12779
+ join: PATH_JOIN2,
12780
+ perPage: false,
12408
12781
  created: true,
12409
12782
  keysBefore: [],
12410
12783
  keysAdded: [],
@@ -12441,8 +12814,8 @@ var Model = class {
12441
12814
  /** A label relative to the config directory, posix; undefined outside it. */
12442
12815
  relative(label) {
12443
12816
  if (label === "<stdin>") return void 0;
12444
- const rel = toPosix3(relative11(this.configDir, resolve14(this.ctx.base, label)));
12445
- return isAbsolute7(rel) || rel.startsWith("../") ? void 0 : rel;
12817
+ const rel = toPosix3(relative12(this.configDir, resolve15(this.ctx.base, label)));
12818
+ return isAbsolute8(rel) || rel.startsWith("../") ? void 0 : rel;
12446
12819
  }
12447
12820
  };
12448
12821
  function dedupe2(xs) {
@@ -12453,17 +12826,17 @@ function bare(path) {
12453
12826
  }
12454
12827
  async function collectionPath(target, cwd, configDir) {
12455
12828
  if (target === STDIN_TOKEN) return void 0;
12456
- const posix5 = target.replace(/\\/g, "/");
12457
- const scanned = picomatch6.scan(posix5);
12829
+ const posix7 = target.replace(/\\/g, "/");
12830
+ const scanned = picomatch6.scan(posix7);
12458
12831
  if (scanned.isGlob) {
12459
- const rel2 = toPosix3(relative11(configDir, resolve14(cwd, scanned.base === "" ? "." : scanned.base)));
12460
- if (isAbsolute7(rel2) || rel2 === ".." || rel2.startsWith("../")) return void 0;
12832
+ const rel2 = toPosix3(relative12(configDir, resolve15(cwd, scanned.base === "" ? "." : scanned.base)));
12833
+ if (isAbsolute8(rel2) || rel2 === ".." || rel2.startsWith("../")) return void 0;
12461
12834
  return rel2 === "" ? scanned.glob : `${rel2}/${scanned.glob}`;
12462
12835
  }
12463
- const abs = resolve14(cwd, posix5);
12464
- const rel = toPosix3(relative11(configDir, abs));
12465
- if (isAbsolute7(rel) || rel.startsWith("..")) return void 0;
12466
- let directory = posix5.endsWith("/");
12836
+ const abs = resolve15(cwd, posix7);
12837
+ const rel = toPosix3(relative12(configDir, abs));
12838
+ if (isAbsolute8(rel) || rel.startsWith("..")) return void 0;
12839
+ let directory = posix7.endsWith("/");
12467
12840
  if (!directory) {
12468
12841
  try {
12469
12842
  directory = (await stat6(abs)).isDirectory();
@@ -12484,28 +12857,30 @@ async function exists(absPath, file) {
12484
12857
  }
12485
12858
  }
12486
12859
  function plannedConfigDir(ctx) {
12487
- return ctx.configDir ?? findGitRoot(ctx.cwd) ?? resolve14(ctx.cwd);
12860
+ return ctx.configDir ?? findGitRoot(ctx.cwd) ?? resolve15(ctx.cwd);
12488
12861
  }
12489
12862
  async function keyHome(ctx, label, data, key) {
12490
12863
  const model = new Model(ctx, plannedConfigDir(ctx));
12491
12864
  const owner = model.ownerOf(key, model.members(label));
12492
12865
  if (owner !== void 0) {
12493
- if (owner.url || owner.absPath === void 0) {
12866
+ const at2 = model.fileFor(owner, resolve15(ctx.base, label));
12867
+ if (owner.url || at2.abs === void 0) {
12494
12868
  return { kind: "url", collection: owner.collection, file: owner.file };
12495
12869
  }
12496
12870
  return {
12497
12871
  kind: "manifest",
12498
12872
  collection: owner.collection,
12499
- file: owner.file,
12500
- absPath: owner.absPath,
12873
+ file: at2.file,
12874
+ absPath: at2.abs,
12501
12875
  join: owner.join,
12502
- entry: entryFor(owner, label, data, model.configDir, ctx.base, lazyKey(ctx.configFile, ctx.env))
12876
+ entry: entryFor(owner, label, data, model.configDir, ctx.base, lazyKey(ctx.configFile, ctx.env)),
12877
+ perPage: owner.perPage
12503
12878
  };
12504
12879
  }
12505
12880
  return { kind: "unowned", home: (await model.home(label, false)).proposed };
12506
12881
  }
12507
12882
  function entryFor(m, label, data, configDir, base, key) {
12508
- if (m.join === PATH_JOIN) return toPosix3(relative11(configDir, resolve14(base, label)));
12883
+ if (m.join === PATH_JOIN2) return toPosix3(relative12(configDir, resolve15(base, label)));
12509
12884
  let raw = data[m.join];
12510
12885
  if (isEncryptedValue(raw)) {
12511
12886
  const k = key();
@@ -12522,6 +12897,27 @@ function entryFor(m, label, data, configDir, base, key) {
12522
12897
  if (typeof raw === "number" || typeof raw === "boolean") return String(raw);
12523
12898
  return void 0;
12524
12899
  }
12900
+ async function relocationPages(ctx, request) {
12901
+ const pages2 = new Set(request.files.map((label) => resolve15(ctx.base, label)));
12902
+ const root = ctx.configDir ?? ctx.cwd;
12903
+ for (const collection of ctx.declaredCollections) {
12904
+ const perPage = collection.externalMetadata.some(
12905
+ (m) => classifyRef(m.file).kind !== "url" && hasPagePlaceholder(m.file)
12906
+ );
12907
+ if (!perPage) continue;
12908
+ const walked = await resolveTargetSet({
12909
+ inputs: [...collection.paths],
12910
+ cwd: root,
12911
+ allowEmpty: true,
12912
+ ...gitignoreOptions({
12913
+ ...ctx.respectGitignore !== void 0 ? { flag: ctx.respectGitignore } : {},
12914
+ ...ctx.config?.respectGitignore !== void 0 ? { configured: ctx.config.respectGitignore } : {}
12915
+ })
12916
+ });
12917
+ for (const file of walked.files) pages2.add(resolve15(root, file));
12918
+ }
12919
+ return [...pages2];
12920
+ }
12525
12921
  async function planRelocation(ctx, request) {
12526
12922
  const configDir = plannedConfigDir(ctx);
12527
12923
  const model = new Model(ctx, configDir);
@@ -12529,7 +12925,8 @@ async function planRelocation(ctx, request) {
12529
12925
  const index = await loadExternalMetadata(ctx.declaredCollections, {
12530
12926
  configDir: ctx.configDir ?? ctx.cwd,
12531
12927
  base: ctx.base,
12532
- offline: ctx.config?.offline ?? false
12928
+ offline: ctx.config?.offline ?? false,
12929
+ pages: await relocationPages(ctx, request)
12533
12930
  });
12534
12931
  const trustRoot = schemaTrustRoot(ctx.cwd, ctx.configDir);
12535
12932
  const forced = ctx.as !== void 0 ? extractorByName(ctx.as) : void 0;
@@ -12546,17 +12943,17 @@ async function planRelocation(ctx, request) {
12546
12943
  if (beyond) return void 0;
12547
12944
  throw new DocmetaError(`Unsupported file type "${extname3(label)}" for "${label}". Use --as to override.`);
12548
12945
  }
12549
- const absPath = resolve14(ctx.base, label);
12946
+ const absPath = resolve15(ctx.base, label);
12550
12947
  const elements = resolveElements(label, ctx.config, currentMembers(label));
12551
12948
  let content = "";
12552
12949
  let own2 = {};
12553
- let unreadable;
12950
+ let unreadable2;
12554
12951
  if (beyond) {
12555
12952
  try {
12556
12953
  content = await readFile8(absPath, "utf8");
12557
12954
  own2 = extractor.extract(content, label, { elements }).data;
12558
12955
  } catch (err) {
12559
- unreadable = (errorMessage(err).split("\n")[0] ?? "").replace(/:\s*$/, "");
12956
+ unreadable2 = (errorMessage(err).split("\n")[0] ?? "").replace(/:\s*$/, "");
12560
12957
  }
12561
12958
  } else {
12562
12959
  content = await readFile8(absPath, "utf8");
@@ -12575,7 +12972,7 @@ async function planRelocation(ctx, request) {
12575
12972
  extractor,
12576
12973
  own: own2,
12577
12974
  beyond,
12578
- ...unreadable !== void 0 ? { unreadable } : {},
12975
+ ...unreadable2 !== void 0 ? { unreadable: unreadable2 } : {},
12579
12976
  intents: /* @__PURE__ */ new Map()
12580
12977
  };
12581
12978
  pages2.set(label, page);
@@ -12584,13 +12981,13 @@ async function planRelocation(ctx, request) {
12584
12981
  const supplied = (m, page, key) => {
12585
12982
  if (m.created || index === null) return void 0;
12586
12983
  let sv;
12587
- if (m.join === PATH_JOIN) {
12984
+ if (m.join === PATH_JOIN2) {
12588
12985
  sv = index.byPath.get(page.absPath)?.get(key);
12589
12986
  } else {
12590
12987
  const entry = entryFor(m, page.label, page.own, configDir, ctx.base, encryptionKey);
12591
12988
  sv = entry === void 0 ? void 0 : index.byField.get(m.join)?.get(entry)?.get(key);
12592
12989
  }
12593
- return sv !== void 0 && sv.collection === m.collection && sv.file === m.file ? sv : void 0;
12990
+ return sv !== void 0 && sv.collection === m.collection && sv.file === model.fileFor(m, page.absPath).file ? sv : void 0;
12594
12991
  };
12595
12992
  const stay = (page, key, reason, detail, links = {}) => {
12596
12993
  page.intents.set(key, { kind: "stay", reason, detail, ...links });
@@ -12610,7 +13007,7 @@ async function planRelocation(ctx, request) {
12610
13007
  }
12611
13008
  const held = supplied(m, page, key);
12612
13009
  if (held !== void 0 && !deepEqual(held.value, page.own[key])) {
12613
- stay(page, key, "values-differ", `the page and ${m.file} hold different values`, { toward: m });
13010
+ stay(page, key, "values-differ", `the page and ${held.file} hold different values`, { toward: m });
12614
13011
  return;
12615
13012
  }
12616
13013
  page.intents.set(key, { kind: "out", manifest: m, reason, drop: held !== void 0, entry });
@@ -12623,7 +13020,7 @@ async function planRelocation(ctx, request) {
12623
13020
  if (entry === void 0) return;
12624
13021
  if (Object.hasOwn(page.own, key)) {
12625
13022
  if (!deepEqual(held.value, page.own[key])) {
12626
- stay(page, key, "values-differ", `the page and ${m.file} hold different values`, { blocks: m });
13023
+ stay(page, key, "values-differ", `the page and ${held.file} hold different values`, { blocks: m });
12627
13024
  return;
12628
13025
  }
12629
13026
  page.intents.set(key, { kind: "in", manifest: m, drop: true, entry, value: held.value });
@@ -12669,7 +13066,7 @@ async function planRelocation(ctx, request) {
12669
13066
  preferencesOf.set(label, prefs);
12670
13067
  const planned = model.members(label);
12671
13068
  const joins = new Set(
12672
- model.manifests.filter((m) => planned.includes(m.collection) && m.join !== PATH_JOIN).map((m) => m.join)
13069
+ model.manifests.filter((m) => planned.includes(m.collection) && m.join !== PATH_JOIN2).map((m) => m.join)
12673
13070
  );
12674
13071
  for (const m of model.manifests) {
12675
13072
  if (planned.includes(m.collection)) for (const k of [...m.keysBefore, ...m.keysAdded]) marked.add(k);
@@ -12759,7 +13156,7 @@ async function planRelocation(ctx, request) {
12759
13156
  });
12760
13157
  const labels = dedupe2([
12761
13158
  ...request.files,
12762
- ...walked.files.map((f) => reported2(resolve14(configDir, f), ctx.base))
13159
+ ...walked.files.map((f) => reported2(resolve15(configDir, f), ctx.base))
12763
13160
  ]).filter((label) => model.members(label).includes(name2));
12764
13161
  for (const label of labels) {
12765
13162
  const page = await load(label, !runSet.has(label));
@@ -12776,7 +13173,7 @@ async function planRelocation(ctx, request) {
12776
13173
  const changing = covered ? out : out.filter((k) => m.keysAdded.includes(k));
12777
13174
  for (const key of changing) if (!page.intents.has(key)) stay(page, key, "unreadable", detail);
12778
13175
  for (const key of m.keysRemoved) {
12779
- const held = m.join !== PATH_JOIN || !m.created && index?.byPath.get(page.absPath)?.get(key)?.file === m.file;
13176
+ const held = m.join !== PATH_JOIN2 || !m.created && index?.byPath.get(page.absPath)?.get(key)?.file === model.fileFor(m, page.absPath).file;
12780
13177
  if (held && !page.intents.has(key)) stay(page, key, "unreadable", detail, { blocks: m });
12781
13178
  }
12782
13179
  continue;
@@ -12827,35 +13224,50 @@ async function planRelocation(ctx, request) {
12827
13224
  const writes = [];
12828
13225
  const lines = /* @__PURE__ */ new Map();
12829
13226
  for (const m of model.manifests) {
12830
- if (m.url || m.absPath === void 0) continue;
13227
+ if (m.url) continue;
12831
13228
  const undeclared = model.finalKeys(m).length === 0 && !m.created;
12832
- const outs = [];
12833
- const ins = [];
13229
+ const files = /* @__PURE__ */ new Map();
13230
+ const group = (at2) => {
13231
+ if (at2.abs === void 0) return void 0;
13232
+ const have = files.get(at2.abs);
13233
+ if (have !== void 0) return have;
13234
+ const made = { file: at2.file, outs: [], ins: [] };
13235
+ files.set(at2.abs, made);
13236
+ return made;
13237
+ };
13238
+ if (m.created) group({ abs: m.absPath, file: m.file });
12834
13239
  for (const page of orderedPages()) {
12835
13240
  for (const [key, intent] of orderedIntents(page)) {
12836
- if (intent.kind === "out" && intent.manifest === m) {
12837
- outs.push({ page: page.label, entry: intent.entry, key, value: page.own[key], drop: intent.drop });
12838
- } else if (intent.kind === "in" && intent.manifest === m) {
12839
- ins.push({ entry: intent.entry, key });
13241
+ if ((intent.kind === "out" || intent.kind === "in") && intent.manifest === m) {
13242
+ const into = group(model.fileFor(m, page.absPath));
13243
+ if (intent.kind === "out") {
13244
+ into?.outs.push({ page: page.label, entry: intent.entry, key, value: page.own[key], drop: intent.drop });
13245
+ } else {
13246
+ into?.ins.push({ entry: intent.entry, key });
13247
+ }
12840
13248
  }
12841
13249
  }
12842
13250
  }
12843
- if (!undeclared) assertNoStrandedEntries(m, index, pages2, configDir);
12844
- if (undeclared || !m.created && outs.length === 0 && ins.length === 0) continue;
12845
- let text2 = m.created ? "" : await readManifestText(m);
12846
- const before = text2;
12847
- const where = { join: m.join, file: m.file };
12848
- for (const o of outs) {
12849
- if (!o.drop) text2 = spliceManifestValue(text2, { ...where, entry: o.entry, key: o.key, value: o.value }).text;
12850
- }
12851
- for (const i of ins) text2 = removeManifestKey(text2, { ...where, entry: i.entry, key: i.key }).text;
12852
- const found = keyLines(text2, m.join);
12853
- for (const o of outs) {
12854
- const line = found.get(entryId(o.entry, m.join))?.get(o.key);
12855
- if (line !== void 0) lines.set(lineId(o.page, m.file, o.key), line);
12856
- }
12857
- if (m.created || text2 !== before) {
12858
- writes.push({ path: m.absPath, text: text2, kind: "manifest", before: m.created ? null : before });
13251
+ if (!undeclared) assertNoStrandedEntries(m, index, pages2, configDir, (abs) => model.fileFor(m, abs).file);
13252
+ if (undeclared) continue;
13253
+ for (const [path, { file, outs, ins }] of files) {
13254
+ if (!m.created && outs.length === 0 && ins.length === 0) continue;
13255
+ const disk = m.created ? null : await readManifestText(path, file, m.perPage);
13256
+ let text2 = disk ?? "";
13257
+ const before = text2;
13258
+ const where = { join: m.join, file };
13259
+ for (const o of outs) {
13260
+ if (!o.drop) text2 = spliceManifestValue(text2, { ...where, entry: o.entry, key: o.key, value: o.value }).text;
13261
+ }
13262
+ for (const i of ins) text2 = removeManifestKey(text2, { ...where, entry: i.entry, key: i.key }).text;
13263
+ const found = keyLines(text2, m.join);
13264
+ for (const o of outs) {
13265
+ const line = found.get(entryId(o.entry, m.join))?.get(o.key);
13266
+ if (line !== void 0) lines.set(lineId(o.page, file, o.key), line);
13267
+ }
13268
+ if (m.created || text2 !== before) {
13269
+ writes.push({ path, text: text2, kind: "manifest", before: disk });
13270
+ }
12859
13271
  }
12860
13272
  }
12861
13273
  const finalCollections = model.final();
@@ -12954,7 +13366,7 @@ function planPageText(page) {
12954
13366
  refuse();
12955
13367
  }
12956
13368
  }
12957
- function assertNoStrandedEntries(m, index, pages2, configDir) {
13369
+ function assertNoStrandedEntries(m, index, pages2, configDir, fileOf) {
12958
13370
  if (index === null || m.keysRemoved.length === 0) return;
12959
13371
  for (const key of m.keysRemoved) {
12960
13372
  const moved = /* @__PURE__ */ new Set();
@@ -12962,10 +13374,10 @@ function assertNoStrandedEntries(m, index, pages2, configDir) {
12962
13374
  const i = page.intents.get(key);
12963
13375
  if (i?.kind === "in" && i.manifest === m) moved.add(entryId(i.entry, m.join));
12964
13376
  }
12965
- const stranded = m.join === PATH_JOIN ? [...index.byPath].filter(([, values]) => {
13377
+ const stranded = m.join === PATH_JOIN2 ? [...index.byPath].filter(([abs, values]) => {
12966
13378
  const sv = values.get(key);
12967
- return sv !== void 0 && sv.file === m.file && sv.collection === m.collection;
12968
- }).map(([abs]) => toPosix3(relative11(configDir, abs))) : [...index.byField.get(m.join) ?? /* @__PURE__ */ new Map()].filter(([, values]) => {
13379
+ return sv !== void 0 && sv.collection === m.collection && sv.file === fileOf(abs);
13380
+ }).map(([abs]) => toPosix3(relative12(configDir, abs))) : [...index.byField.get(m.join) ?? /* @__PURE__ */ new Map()].filter(([, values]) => {
12969
13381
  const sv = values.get(key);
12970
13382
  return sv !== void 0 && sv.file === m.file && sv.collection === m.collection;
12971
13383
  }).map(([value]) => value);
@@ -12977,29 +13389,29 @@ function assertNoStrandedEntries(m, index, pages2, configDir) {
12977
13389
  }
12978
13390
  }
12979
13391
  }
12980
- async function readManifestText(m) {
12981
- if (m.absPath === void 0) return "";
13392
+ async function readManifestText(path, file, perPage) {
12982
13393
  try {
12983
- return await readFile8(m.absPath, "utf8");
13394
+ return await readFile8(path, "utf8");
12984
13395
  } catch (err) {
12985
- throw new DocmetaError(`Manifest ${m.file} could not be read: ${errorMessage(err)}`);
13396
+ if (perPage && isMissing(err)) return null;
13397
+ throw new DocmetaError(`Manifest ${file} could not be read: ${errorMessage(err)}`);
12986
13398
  }
12987
13399
  }
12988
- var entryId = (entry, join11) => join11 === PATH_JOIN ? toPosix3(entry).replace(/^\.\//, "").replace(/\/+/g, "/") : entry;
13400
+ var entryId = (entry, join11) => join11 === PATH_JOIN2 ? toPosix3(entry).replace(/^\.\//, "").replace(/\/+/g, "/") : entry;
12989
13401
  var lineId = (page, manifest, key) => `${page}\0${manifest}\0${key}`;
12990
13402
  function keyLines(text2, join11) {
12991
13403
  const out = /* @__PURE__ */ new Map();
12992
- const lc = new LineCounter4();
12993
- const doc = parseDocument6(text2, { lineCounter: lc, uniqueKeys: false });
13404
+ const lc = new LineCounter5();
13405
+ const doc = parseDocument7(text2, { lineCounter: lc, uniqueKeys: false });
12994
13406
  const root = doc.contents;
12995
- if (!isMap6(root)) return out;
13407
+ if (!isMap7(root)) return out;
12996
13408
  for (const pair of root.items) {
12997
- const spelled = isScalar4(pair.key) ? String(pair.key.value) : String(pair.key);
12998
- if (!isMap6(pair.value)) continue;
13409
+ const spelled = isScalar5(pair.key) ? String(pair.key.value) : String(pair.key);
13410
+ if (!isMap7(pair.value)) continue;
12999
13411
  const keys = /* @__PURE__ */ new Map();
13000
13412
  for (const kv of pair.value.items) {
13001
- const name2 = isScalar4(kv.key) ? String(kv.key.value) : String(kv.key);
13002
- const range = isNode3(kv.key) ? kv.key.range : void 0;
13413
+ const name2 = isScalar5(kv.key) ? String(kv.key.value) : String(kv.key);
13414
+ const range = isNode4(kv.key) ? kv.key.range : void 0;
13003
13415
  if (range) keys.set(name2, lc.linePos(range[0]).line);
13004
13416
  }
13005
13417
  out.set(entryId(spelled, join11), keys);
@@ -13031,10 +13443,10 @@ async function configTarget(ctx, configDir) {
13031
13443
  return { path: join8(configDir, FAMILY_CONFIG_NAMES[0] ?? "manni.config.yaml"), text: null, disk: null };
13032
13444
  }
13033
13445
  function editConfig(text2, model, source) {
13034
- const doc = parseDocument6(text2 ?? "");
13446
+ const doc = parseDocument7(text2 ?? "");
13035
13447
  const [problem] = doc.errors;
13036
13448
  if (problem !== void 0) throw new DocmetaError(`${source}: invalid YAML: ${problem.message}`);
13037
- if (doc.contents !== null && !isMap6(doc.contents)) {
13449
+ if (doc.contents !== null && !isMap7(doc.contents)) {
13038
13450
  throw new DocmetaError(`${source}: top level must be a mapping.`);
13039
13451
  }
13040
13452
  const fail2 = () => {
@@ -13044,7 +13456,7 @@ function editConfig(text2, model, source) {
13044
13456
  if (!isSeq4(node)) return;
13045
13457
  node.flow = true;
13046
13458
  if (quoted) {
13047
- for (const item of node.items) if (isScalar4(item)) item.type = "QUOTE_DOUBLE";
13459
+ for (const item of node.items) if (isScalar5(item)) item.type = "QUOTE_DOUBLE";
13048
13460
  }
13049
13461
  };
13050
13462
  const manifestNode = (d, file, keys) => {
@@ -13058,13 +13470,13 @@ function editConfig(text2, model, source) {
13058
13470
  for (let i = 0; i < declared; i++) {
13059
13471
  const c = model.collections[i];
13060
13472
  const node = isSeq4(existing) ? existing.items[i] : void 0;
13061
- if (c === void 0 || !isMap6(node)) return fail2();
13473
+ if (c === void 0 || !isMap7(node)) return fail2();
13062
13474
  for (const added of model.pathsAdded.filter((p) => p.collection === c.name)) {
13063
13475
  const paths = node.get("paths", true);
13064
13476
  if (!isSeq4(paths)) return fail2();
13065
13477
  const scalar = doc.createNode(added.path);
13066
13478
  const first = paths.items[0];
13067
- if (isScalar4(first) && first.type !== void 0) scalar.type = first.type;
13479
+ if (isScalar5(first) && first.type !== void 0) scalar.type = first.type;
13068
13480
  paths.add(scalar);
13069
13481
  }
13070
13482
  const refs = model.manifestsOf(c.name);
@@ -13081,7 +13493,7 @@ function editConfig(text2, model, source) {
13081
13493
  }
13082
13494
  if (m.keysAdded.length === 0) continue;
13083
13495
  const entry = node.getIn(["externalMetadata", m.index], true);
13084
- const keys = isMap6(entry) ? entry.get("keys", true) : void 0;
13496
+ const keys = isMap7(entry) ? entry.get("keys", true) : void 0;
13085
13497
  if (!isSeq4(keys)) return fail2();
13086
13498
  for (const key of m.keysAdded) keys.add(doc.createNode(key));
13087
13499
  }
@@ -13090,10 +13502,10 @@ function editConfig(text2, model, source) {
13090
13502
  if (m.keysRemoved.length === 0) continue;
13091
13503
  if (!isSeq4(list2)) return fail2();
13092
13504
  const entry = list2.items[m.index];
13093
- const keys = isMap6(entry) ? entry.get("keys", true) : void 0;
13505
+ const keys = isMap7(entry) ? entry.get("keys", true) : void 0;
13094
13506
  if (!isSeq4(keys)) return fail2();
13095
13507
  for (const key of m.keysRemoved) {
13096
- const at2 = keys.items.findIndex((item) => (isScalar4(item) ? String(item.value) : String(item)) === key);
13508
+ const at2 = keys.items.findIndex((item) => (isScalar5(item) ? String(item.value) : String(item)) === key);
13097
13509
  if (at2 >= 0) keys.delete(at2);
13098
13510
  }
13099
13511
  if (keys.items.length === 0) list2.delete(m.index);
@@ -13112,7 +13524,7 @@ function editConfig(text2, model, source) {
13112
13524
  flowSeq(node.get("paths", true), true);
13113
13525
  const made = node.get("externalMetadata", true);
13114
13526
  if (isSeq4(made)) {
13115
- for (const item of made.items) if (isMap6(item)) flowSeq(item.get("keys", true), false);
13527
+ for (const item of made.items) if (isMap7(item)) flowSeq(item.get("keys", true), false);
13116
13528
  }
13117
13529
  const seq = doc.get("collections", true);
13118
13530
  if (isSeq4(seq)) seq.add(node);
@@ -13147,16 +13559,18 @@ function buildResult(input) {
13147
13559
  const moved = [];
13148
13560
  const stayed = [];
13149
13561
  const intents = orderedIntents(page);
13562
+ const fileOf = (m) => model.fileFor(m, page.absPath).file;
13150
13563
  for (const [key, intent] of intents) {
13151
- if (intent.kind === "in") moved.push({ key, to: "page", from: intent.manifest.file, reason: "preferred" });
13564
+ if (intent.kind === "in") moved.push({ key, to: "page", from: fileOf(intent.manifest), reason: "preferred" });
13152
13565
  }
13153
13566
  for (const [key, intent] of intents) {
13154
13567
  if (intent.kind !== "out") continue;
13155
- const line = dryRun ? void 0 : lines.get(lineId(page.label, intent.manifest.file, key));
13568
+ const manifest = fileOf(intent.manifest);
13569
+ const line = dryRun ? void 0 : lines.get(lineId(page.label, manifest, key));
13156
13570
  moved.push({
13157
13571
  key,
13158
13572
  to: "manifest",
13159
- manifest: intent.manifest.file,
13573
+ manifest,
13160
13574
  ...line !== void 0 ? { line } : {},
13161
13575
  reason: intent.reason
13162
13576
  });
@@ -13201,7 +13615,7 @@ function buildResult(input) {
13201
13615
  };
13202
13616
  }
13203
13617
  async function applyRelocation(ctx, plan, io = {}) {
13204
- const write = io.write ?? ((path, text2) => writeFileAtomic(path, text2));
13618
+ const write = io.write ?? ((path, text2) => writeFileAtomic(path, text2, { createParents: true }));
13205
13619
  const remove = io.remove ?? ((path) => unlink(path));
13206
13620
  const label = (w) => w.kind === "config" ? configSource(w.path, ctx.cwd) : reported2(w.path, ctx.base);
13207
13621
  for (const w of plan.writes) {
@@ -13374,7 +13788,7 @@ function relocationOffers(plan) {
13374
13788
  }
13375
13789
 
13376
13790
  // src/meta/commands/validate.ts
13377
- import { resolve as resolve16, extname as extname4 } from "node:path";
13791
+ import { resolve as resolve17, extname as extname4 } from "node:path";
13378
13792
 
13379
13793
  // src/meta/core/checks.ts
13380
13794
  var CHECK_KEYWORD = "check";
@@ -13512,8 +13926,8 @@ async function runChecks(checks, entries, ctx = {}) {
13512
13926
  }
13513
13927
 
13514
13928
  // src/meta/core/derive/provenance-place.ts
13515
- import { isAbsolute as isAbsolute8, relative as relative12, resolve as resolve15, sep as sep7 } from "node:path";
13516
- var toPosix4 = (p) => p.split(sep7).join("/");
13929
+ import { isAbsolute as isAbsolute9, relative as relative13, resolve as resolve16, sep as sep8 } from "node:path";
13930
+ var toPosix4 = (p) => p.split(sep8).join("/");
13517
13931
  function provenanceManifests(collections, configDir, base) {
13518
13932
  const out = [];
13519
13933
  for (const collection of collections) {
@@ -13524,13 +13938,15 @@ function provenanceManifests(collections, configDir, base) {
13524
13938
  `collection ${collection.name}: ${PROVENANCE_FIELD} cannot come from a URL manifest, because manni meta derive writes it.`
13525
13939
  );
13526
13940
  }
13527
- const absPath = isAbsolute8(manifest.file) ? manifest.file : resolve15(configDir, manifest.file);
13528
- const rel = relative12(base, absPath);
13941
+ const absPath = isAbsolute9(manifest.file) ? manifest.file : resolve16(configDir, manifest.file);
13942
+ const perPage = hasPagePlaceholder(manifest.file);
13529
13943
  out.push({
13530
13944
  collection: collection.name,
13531
13945
  absPath,
13532
- file: rel === "" ? "." : toPosix4(rel),
13533
- join: externalMetadataJoin(manifest)
13946
+ file: perPage ? manifest.file : reportedPath(absPath, base),
13947
+ join: externalMetadataJoin(manifest),
13948
+ perPage,
13949
+ written: manifest.file
13534
13950
  });
13535
13951
  }
13536
13952
  }
@@ -13540,15 +13956,16 @@ function provenancePlace(label, data, manifests, collections, configDir, base) {
13540
13956
  if (manifests.length === 0) return void 0;
13541
13957
  const members = new Set(memberOf(collections, configDir, base, label));
13542
13958
  const owning = manifests.filter((m) => members.has(m.collection));
13543
- const [manifest, second] = owning;
13544
- if (manifest === void 0) return void 0;
13959
+ const [declared, second] = owning;
13960
+ if (declared === void 0) return void 0;
13545
13961
  if (second !== void 0) {
13546
13962
  throw new DocmetaError(
13547
- `${label} is in collections ${manifest.collection} and ${second.collection}, and both keep ${PROVENANCE_FIELD} in a manifest.`
13963
+ `${label} is in collections ${declared.collection} and ${second.collection}, and both keep ${PROVENANCE_FIELD} in a manifest.`
13548
13964
  );
13549
13965
  }
13550
- if (manifest.join === PATH_JOIN) {
13551
- const entry = toPosix4(relative12(configDir, resolve15(base, label)));
13966
+ const manifest = declared.perPage ? ownManifest(declared, label, collections, configDir, base) : declared;
13967
+ if (manifest.join === PATH_JOIN2) {
13968
+ const entry = toPosix4(relative13(configDir, resolve16(base, label)));
13552
13969
  return { manifest, absPath: manifest.absPath, entry, join: manifest.join };
13553
13970
  }
13554
13971
  const value = data[manifest.join];
@@ -13559,6 +13976,16 @@ function provenancePlace(label, data, manifests, collections, configDir, base) {
13559
13976
  }
13560
13977
  return { manifest, absPath: manifest.absPath, entry: String(value), join: manifest.join };
13561
13978
  }
13979
+ function ownManifest(manifest, label, collections, configDir, base) {
13980
+ const resolved = pageManifestPath(manifest.written, configDir, resolve16(base, label));
13981
+ if ("outside" in resolved) {
13982
+ const declared = collections.find((c) => c.name === manifest.collection)?.externalMetadata.find((m) => m.file === manifest.written);
13983
+ throw new DocmetaError(
13984
+ outsideRefusal(declared ?? { file: manifest.written, keys: [] }, manifest.collection, resolved.pageRel)
13985
+ );
13986
+ }
13987
+ return { ...manifest, absPath: resolved.abs, file: reportedPath(resolved.abs, base) };
13988
+ }
13562
13989
  function readerManifests(collections, configDir, base) {
13563
13990
  const local = collections.map((c) => ({
13564
13991
  ...c,
@@ -13657,7 +14084,9 @@ async function runValidate(opts) {
13657
14084
  const externalMetadata = await loadExternalMetadata(collections, {
13658
14085
  configDir: configDir ?? cwd,
13659
14086
  base,
13660
- offline: opts.offline ?? config?.offline ?? false
14087
+ offline: opts.offline ?? config?.offline ?? false,
14088
+ // A `{page}` manifest (0058) is read for exactly the pages this run reads.
14089
+ pages: files.map((file) => resolve17(base, file))
13661
14090
  });
13662
14091
  const validator = new Validator(
13663
14092
  schemaLoadOptions({
@@ -13688,7 +14117,7 @@ async function runValidate(opts) {
13688
14117
  const owned = new Set(merged.collisions.map((c) => c.key));
13689
14118
  const declaredMembers = declaredCollections === collections ? members : memberOf(declaredCollections, configDir ?? cwd, base, label);
13690
14119
  const joins = new Set(
13691
- declaredCollections.filter((c) => declaredMembers.includes(c.name)).flatMap((c) => c.externalMetadata.map(externalMetadataJoin)).filter((join11) => join11 !== PATH_JOIN)
14120
+ declaredCollections.filter((c) => declaredMembers.includes(c.name)).flatMap((c) => c.externalMetadata.map(externalMetadataJoin)).filter((join11) => join11 !== PATH_JOIN2)
13692
14121
  );
13693
14122
  const findings = [];
13694
14123
  for (const key of Object.keys(data)) {
@@ -13812,7 +14241,7 @@ async function runValidate(opts) {
13812
14241
  const place = provenanceManifests2.length === 0 ? void 0 : readerPlace(label, extracted.data, provenanceManifests2, declaredCollections, configDir ?? cwd, base);
13813
14242
  deriveInputs.push({
13814
14243
  label,
13815
- absPath: resolve16(base, label),
14244
+ absPath: resolve17(base, label),
13816
14245
  content,
13817
14246
  extracted,
13818
14247
  ...place !== void 0 ? { provenanceManifest: place } : {}
@@ -13943,7 +14372,7 @@ async function runValidate(opts) {
13943
14372
  await processOne(STDIN_LABEL, content, forcedExtractor.extensions[0] ?? "");
13944
14373
  }
13945
14374
  for (const file of files) {
13946
- const content = await readFile9(resolve16(base, file), "utf8");
14375
+ const content = await readFile9(resolve17(base, file), "utf8");
13947
14376
  await processOne(file, content, extname4(file));
13948
14377
  }
13949
14378
  if (unverified > 0) opts.onNotice?.(unverifiedWarning(unverified));
@@ -14089,7 +14518,7 @@ async function runValidate(opts) {
14089
14518
 
14090
14519
  // src/meta/commands/get.ts
14091
14520
  import { readFile as readFile10 } from "node:fs/promises";
14092
- import { resolve as resolve17, extname as extname5 } from "node:path";
14521
+ import { resolve as resolve18, extname as extname5 } from "node:path";
14093
14522
  async function runGet(opts) {
14094
14523
  const cwd = opts.cwd ?? process.cwd();
14095
14524
  if (opts.fields.length === 0) {
@@ -14148,7 +14577,9 @@ async function runGet(opts) {
14148
14577
  const externalMetadata = await loadExternalMetadata(collections, {
14149
14578
  configDir: configDir ?? cwd,
14150
14579
  base,
14151
- offline: opts.offline ?? config?.offline ?? false
14580
+ offline: opts.offline ?? config?.offline ?? false,
14581
+ // A `{page}` manifest (0058) is read for exactly the pages this run reads.
14582
+ pages: files.map((file) => resolve18(base, file))
14152
14583
  });
14153
14584
  const commands = commandsOf(config?.derive) ?? {};
14154
14585
  const deriving = opts.derived !== false;
@@ -14175,7 +14606,7 @@ async function runGet(opts) {
14175
14606
  const place = provenanceManifests2.length === 0 ? void 0 : readerPlace(label, own2.data, provenanceManifests2, declaredCollections, configDir ?? cwd, base);
14176
14607
  deriveInputs.push({
14177
14608
  label,
14178
- absPath: resolve17(base, label),
14609
+ absPath: resolve18(base, label),
14179
14610
  content,
14180
14611
  extracted: own2,
14181
14612
  ...place !== void 0 ? { provenanceManifest: place } : {}
@@ -14217,7 +14648,7 @@ async function runGet(opts) {
14217
14648
  readOne(STDIN_LABEL, opts.stdinContent ?? "", forced.extensions[0] ?? "");
14218
14649
  }
14219
14650
  for (const file of files) {
14220
- const content = await readFile10(resolve17(base, file), "utf8");
14651
+ const content = await readFile10(resolve18(base, file), "utf8");
14221
14652
  readOne(file, content, extname5(file));
14222
14653
  }
14223
14654
  if (deriving) {
@@ -14343,16 +14774,16 @@ function parseJsonPointer(pointer) {
14343
14774
 
14344
14775
  // src/meta/commands/schemas.ts
14345
14776
  import { existsSync as existsSync4 } from "node:fs";
14346
- import { mkdir as mkdir2, readFile as readFile11 } from "node:fs/promises";
14347
- import { dirname as dirname10, extname as extname6, isAbsolute as isAbsolute9, join as join9, relative as relative13, resolve as resolve18, sep as sep8 } from "node:path";
14348
- import { parseDocument as parseDocument7, parse as parseYaml6, stringify as stringify3 } from "yaml";
14777
+ import { mkdir as mkdir3, readFile as readFile11 } from "node:fs/promises";
14778
+ import { dirname as dirname10, extname as extname6, isAbsolute as isAbsolute10, join as join9, relative as relative14, resolve as resolve19, sep as sep9 } from "node:path";
14779
+ import { parseDocument as parseDocument8, parse as parseYaml6, stringify as stringify3 } from "yaml";
14349
14780
  function getSchemasInfo() {
14350
14781
  return { builtins: listBuiltins(), formats: listFormats() };
14351
14782
  }
14352
14783
  var DEFAULT_VENDOR_DIR = "./schema";
14353
14784
  var DEFAULT_CONFIG_NAME = "manni.config.yaml";
14354
14785
  function posixRelative(from, to) {
14355
- return relative13(from, to).split(sep8).join("/");
14786
+ return relative14(from, to).split(sep9).join("/");
14356
14787
  }
14357
14788
  function vendorFileName(url) {
14358
14789
  let parsed2;
@@ -14417,7 +14848,7 @@ function renderConfig(existing, entries, section) {
14417
14848
  if (existing === null) {
14418
14849
  return stringify3({ [META_SECTION]: { schemas: entries } });
14419
14850
  }
14420
- const doc = parseDocument7(existing);
14851
+ const doc = parseDocument8(existing);
14421
14852
  doc.setIn(section === void 0 ? ["schemas"] : [section, "schemas"], entries);
14422
14853
  return doc.toString();
14423
14854
  }
@@ -14438,13 +14869,13 @@ async function runVendorSchema(opts) {
14438
14869
  );
14439
14870
  }
14440
14871
  const loaded = await loadConfig(opts.configPath, cwd);
14441
- const configPath = loaded?.path ?? resolve18(cwd, DEFAULT_CONFIG_NAME);
14872
+ const configPath = loaded?.path ?? resolve19(cwd, DEFAULT_CONFIG_NAME);
14442
14873
  const configDir = loaded?.dir ?? cwd;
14443
- const absDir = resolve18(cwd, opts.dir ?? DEFAULT_VENDOR_DIR);
14874
+ const absDir = resolve19(cwd, opts.dir ?? DEFAULT_VENDOR_DIR);
14444
14875
  const absFile = join9(absDir, vendorFileName(url));
14445
14876
  await assertNotIgnored(absFile, absDir, cwd, opts.onNotice);
14446
14877
  const fromConfig = posixRelative(configDir, absFile);
14447
- const ref = isAbsolute9(fromConfig) || fromConfig === "" ? absFile : fromConfig.startsWith(".") ? fromConfig : `./${fromConfig}`;
14878
+ const ref = isAbsolute10(fromConfig) || fromConfig === "" ? absFile : fromConfig.startsWith(".") ? fromConfig : `./${fromConfig}`;
14448
14879
  const { bytes } = await fetchSchemaBytes(url, {
14449
14880
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {},
14450
14881
  ...opts.maxBytes !== void 0 ? { maxBytes: opts.maxBytes } : {}
@@ -14467,7 +14898,7 @@ async function runVendorSchema(opts) {
14467
14898
  );
14468
14899
  }
14469
14900
  }
14470
- await mkdir2(absDir, { recursive: true });
14901
+ await mkdir3(absDir, { recursive: true });
14471
14902
  await writeFileAtomic(absFile, bytes);
14472
14903
  const existing = loaded ? await readFile11(configPath, "utf8") : null;
14473
14904
  const section = loaded ? loaded.section : META_SECTION;
@@ -14598,11 +15029,6 @@ async function runInferSchema(opts) {
14598
15029
  });
14599
15030
  const joinKey = lazyKey(configFile, void 0);
14600
15031
  const usingStdin = inputs.includes(STDIN_TOKEN);
14601
- const externalMetadata = await loadExternalMetadata(collections, {
14602
- configDir: configDir ?? cwd,
14603
- base,
14604
- offline: opts.offline ?? config?.offline ?? false
14605
- });
14606
15032
  if (inputs.length === 0) {
14607
15033
  throw new DocmetaError(
14608
15034
  "No files to scan. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml."
@@ -14621,7 +15047,7 @@ async function runInferSchema(opts) {
14621
15047
  }
14622
15048
  let absOut;
14623
15049
  if (opts.out !== void 0) {
14624
- absOut = resolve18(cwd, opts.out);
15050
+ absOut = resolve19(cwd, opts.out);
14625
15051
  await assertNotIgnored(
14626
15052
  absOut,
14627
15053
  dirname10(absOut),
@@ -14663,8 +15089,14 @@ async function runInferSchema(opts) {
14663
15089
  gitignoreSkipped,
14664
15090
  action: "scanned"
14665
15091
  });
15092
+ const externalMetadata = await loadExternalMetadata(collections, {
15093
+ configDir: configDir ?? cwd,
15094
+ base,
15095
+ offline: opts.offline ?? config?.offline ?? false,
15096
+ pages: files.map((file) => resolve19(base, file))
15097
+ });
14666
15098
  const stats = /* @__PURE__ */ new Map();
14667
- const unreadable = [];
15099
+ const unreadable2 = [];
14668
15100
  let filesScanned = 0;
14669
15101
  let filesWithoutMetadata = 0;
14670
15102
  const scanOne = (label, content, extension) => {
@@ -14689,7 +15121,7 @@ async function runInferSchema(opts) {
14689
15121
  { encryptionKey: joinKey }
14690
15122
  ).extracted;
14691
15123
  } catch (err) {
14692
- unreadable.push({
15124
+ unreadable2.push({
14693
15125
  file: label,
14694
15126
  message: err instanceof Error ? err.message : String(err)
14695
15127
  });
@@ -14709,10 +15141,10 @@ async function runInferSchema(opts) {
14709
15141
  scanOne(STDIN_LABEL, opts.stdinContent ?? "", forced.extensions[0] ?? "");
14710
15142
  }
14711
15143
  for (const file of files) {
14712
- const content = await readFile11(resolve18(base, file), "utf8");
15144
+ const content = await readFile11(resolve19(base, file), "utf8");
14713
15145
  scanOne(file, content, extname6(file));
14714
15146
  }
14715
- for (const u of unreadable) {
15147
+ for (const u of unreadable2) {
14716
15148
  opts.onNotice?.(`could not read metadata from "${u.file}": ${u.message}`);
14717
15149
  }
14718
15150
  const all = [...stats.entries()].map(([key, stat7]) => {
@@ -14751,14 +15183,14 @@ async function runInferSchema(opts) {
14751
15183
  };
14752
15184
  const keys = kept.map(({ sawEmptyString: _drop, ...rest }) => rest);
14753
15185
  if (absOut !== void 0) {
14754
- await mkdir2(dirname10(absOut), { recursive: true });
15186
+ await mkdir3(dirname10(absOut), { recursive: true });
14755
15187
  await writeFileAtomic(absOut, `${JSON.stringify(draft, null, 2)}
14756
15188
  `);
14757
15189
  }
14758
15190
  return {
14759
15191
  filesScanned,
14760
15192
  filesWithoutMetadata,
14761
- unreadable,
15193
+ unreadable: unreadable2,
14762
15194
  keys,
14763
15195
  hiddenByMinCoverage: all.length - kept.length,
14764
15196
  draft,
@@ -14770,8 +15202,8 @@ async function runInferSchema(opts) {
14770
15202
  // src/meta/commands/query.ts
14771
15203
  import { randomBytes as randomBytes2 } from "node:crypto";
14772
15204
  import { existsSync as existsSync5 } from "node:fs";
14773
- import { mkdir as mkdir3, open as open2, readFile as readFile12, rename as rename3, rm as rm3 } from "node:fs/promises";
14774
- import { dirname as dirname11, isAbsolute as isAbsolute10, resolve as resolve19, extname as extname7, sep as sep9 } from "node:path";
15205
+ import { mkdir as mkdir4, open as open2, readFile as readFile12, rename as rename3, rm as rm3 } from "node:fs/promises";
15206
+ import { dirname as dirname11, isAbsolute as isAbsolute11, resolve as resolve20, extname as extname7, sep as sep10 } from "node:path";
14775
15207
 
14776
15208
  // src/shared/prompt.ts
14777
15209
  import { createInterface } from "node:readline";
@@ -14829,10 +15261,10 @@ async function ensureEncryptionKey(opts) {
14829
15261
  }
14830
15262
 
14831
15263
  // src/meta/commands/query.ts
14832
- import { parseDocument as parseDocument9, isMap as isMap8, isSeq as isSeq5, isScalar as isScalar6 } from "yaml";
15264
+ import { parseDocument as parseDocument10, isMap as isMap9, isSeq as isSeq5, isScalar as isScalar7 } from "yaml";
14833
15265
 
14834
15266
  // src/meta/core/location-writes.ts
14835
- import { LineCounter as LineCounter5, isMap as isMap7, isNode as isNode4, isScalar as isScalar5, parseDocument as parseDocument8 } from "yaml";
15267
+ import { LineCounter as LineCounter6, isMap as isMap8, isNode as isNode5, isScalar as isScalar6, parseDocument as parseDocument9 } from "yaml";
14836
15268
  var dedupe3 = (xs) => [...new Set(xs)];
14837
15269
  var pages = (n) => `${String(n)} page${n === 1 ? "" : "s"}`;
14838
15270
  async function offerExternalHomes(ctx, writes, hooks) {
@@ -14915,17 +15347,17 @@ function externalWriteWarning(line) {
14915
15347
  return home.reason === "no-config" ? `${head}; ${prefer}, and --no-config leaves ${them} no manifest.` : `${head} that ${line.pages === 1 ? "is" : "are"} in none of the ${String(home.collections)} collections; ${prefer}, and only a collection has a manifest.`;
14916
15348
  }
14917
15349
  function manifestKeyLine(text2, entry, key, join11) {
14918
- const norm = (e) => join11 === PATH_JOIN ? e.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+/g, "/") : e;
14919
- const lc = new LineCounter5();
14920
- const doc = parseDocument8(text2, { lineCounter: lc, uniqueKeys: false });
15350
+ const norm = (e) => join11 === PATH_JOIN2 ? e.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+/g, "/") : e;
15351
+ const lc = new LineCounter6();
15352
+ const doc = parseDocument9(text2, { lineCounter: lc, uniqueKeys: false });
14921
15353
  const root = doc.contents;
14922
- if (!isMap7(root)) return void 0;
15354
+ if (!isMap8(root)) return void 0;
14923
15355
  for (const pair of root.items) {
14924
- const spelled = isScalar5(pair.key) ? String(pair.key.value) : String(pair.key);
14925
- if (norm(spelled) !== norm(entry) || !isMap7(pair.value)) continue;
15356
+ const spelled = isScalar6(pair.key) ? String(pair.key.value) : String(pair.key);
15357
+ if (norm(spelled) !== norm(entry) || !isMap8(pair.value)) continue;
14926
15358
  for (const kv of pair.value.items) {
14927
- const name2 = isScalar5(kv.key) ? String(kv.key.value) : String(kv.key);
14928
- const range = isNode4(kv.key) ? kv.key.range : void 0;
15359
+ const name2 = isScalar6(kv.key) ? String(kv.key.value) : String(kv.key);
15360
+ const range = isNode5(kv.key) ? kv.key.range : void 0;
14929
15361
  if (name2 === key && range) return lc.linePos(range[0]).line;
14930
15362
  }
14931
15363
  }
@@ -15028,7 +15460,9 @@ async function runQuery(opts) {
15028
15460
  const externalMetadata = await loadExternalMetadata(collections, {
15029
15461
  configDir: configDir ?? cwd,
15030
15462
  base,
15031
- offline: opts.offline ?? config?.offline ?? false
15463
+ offline: opts.offline ?? config?.offline ?? false,
15464
+ // A `{page}` manifest (0058) is read for exactly the pages this run reads.
15465
+ pages: files.map((file) => resolve20(base, file))
15032
15466
  });
15033
15467
  const configuredKey = lazyKey(configFile, opts.env);
15034
15468
  let ensuredKey;
@@ -15110,10 +15544,10 @@ async function runQuery(opts) {
15110
15544
  readOne(STDIN_LABEL, opts.stdinContent ?? "", ext);
15111
15545
  }
15112
15546
  for (const file of files) {
15113
- const content = await readFile12(resolve19(base, file), "utf8");
15547
+ const content = await readFile12(resolve20(base, file), "utf8");
15114
15548
  readOne(file, content, extname7(file));
15115
15549
  }
15116
- const db = opts.db === void 0 ? void 0 : { resolved: resolve19(cwd, opts.db), display: opts.db };
15550
+ const db = opts.db === void 0 ? void 0 : { resolved: resolve20(cwd, opts.db), display: opts.db };
15117
15551
  const run2 = await runSql(sql, entries, {
15118
15552
  target: db,
15119
15553
  params: boundParams,
@@ -15210,7 +15644,7 @@ async function runSql(sql, entries, ctx) {
15210
15644
  );
15211
15645
  const { DatabaseSync } = await loadSqlite();
15212
15646
  if (target) {
15213
- await mkdir3(dirname11(target.resolved), { recursive: true });
15647
+ await mkdir4(dirname11(target.resolved), { recursive: true });
15214
15648
  await prepareDbTarget(target.resolved, target.display);
15215
15649
  }
15216
15650
  let db;
@@ -15254,7 +15688,7 @@ async function runSql(sql, entries, ctx) {
15254
15688
  const place = manifests.length === 0 ? void 0 : readerPlace(e.label, e.own.data, manifests, ctx.declaredCollections, manifestRoot, ctx.base);
15255
15689
  return {
15256
15690
  label: e.label,
15257
- absPath: resolve19(ctx.base, e.label),
15691
+ absPath: resolve20(ctx.base, e.label),
15258
15692
  content: e.content,
15259
15693
  extracted: e.own,
15260
15694
  ...place !== void 0 ? { provenanceManifest: place } : {}
@@ -15719,20 +16153,20 @@ function displayPath(ctx, abs) {
15719
16153
  }
15720
16154
  function assertSchemaWriteWithin(ctx, abs) {
15721
16155
  const within = posixRelative(ctx.trustRoot.dir, abs);
15722
- if (within !== "" && !within.startsWith("..") && !isAbsolute10(within)) return;
16156
+ if (within !== "" && !within.startsWith("..") && !isAbsolute11(within)) return;
15723
16157
  throw new DocmetaError(
15724
16158
  `The resolved schema "${abs}" lives outside ${ctx.trustRoot.dir}; DDL edits schemas inside the repository. Evolve it where it lives, or vendor a copy in.`
15725
16159
  );
15726
16160
  }
15727
16161
  function foldPath(p) {
15728
- const abs = resolve19(p);
16162
+ const abs = resolve20(p);
15729
16163
  return process.platform === "win32" ? abs.toLowerCase() : abs;
15730
16164
  }
15731
16165
  function filePinsByPath(ctx) {
15732
16166
  const byPath = /* @__PURE__ */ new Map();
15733
16167
  for (const [ref, pin] of collectSchemaPins(ctx.config)) {
15734
16168
  if (classifyRef(ref).kind !== "file") continue;
15735
- byPath.set(foldPath(resolve19(ctx.cwd, ref)), pin);
16169
+ byPath.set(foldPath(resolve20(ctx.cwd, ref)), pin);
15736
16170
  }
15737
16171
  return byPath;
15738
16172
  }
@@ -15760,7 +16194,7 @@ async function loadSetMembers(refs, ctx) {
15760
16194
  });
15761
16195
  continue;
15762
16196
  }
15763
- const abs = resolve19(ctx.cwd, ref);
16197
+ const abs = resolve20(ctx.cwd, ref);
15764
16198
  const canon = foldPath(abs);
15765
16199
  if (seenFiles.has(canon)) continue;
15766
16200
  seenFiles.add(canon);
@@ -15965,7 +16399,7 @@ async function planSchemaMutation(ops, entries, ctx) {
15965
16399
  const [, name2, ver] = target.builtinId.split(":");
15966
16400
  const safe = (s) => s.replace(/[^A-Za-z0-9._-]/g, "-");
15967
16401
  const forkName = `${safe(name2 ?? "schema")}-${safe(ver ?? "0")}.local.json`;
15968
- schemaAbs = resolve19(ctx.configDir ?? ctx.cwd, "schemas", forkName);
16402
+ schemaAbs = resolve20(ctx.configDir ?? ctx.cwd, "schemas", forkName);
15969
16403
  assertSchemaWriteWithin(ctx, schemaAbs);
15970
16404
  if (existsSync5(schemaAbs)) {
15971
16405
  throw new DocmetaError(
@@ -16122,17 +16556,17 @@ async function planConfigEdit(ctx, edit, runRefs) {
16122
16556
  return { changes: [], repointed: false };
16123
16557
  }
16124
16558
  const text2 = await readFile12(configPath, "utf8");
16125
- const doc = parseDocument9(stripBom2(text2));
16559
+ const doc = parseDocument10(stripBom2(text2));
16126
16560
  const display = displayPath(ctx, configPath);
16127
16561
  const rawRefOf = (item) => {
16128
- if (isScalar6(item) && typeof item.value === "string") return item.value;
16129
- if (isMap8(item)) {
16562
+ if (isScalar7(item) && typeof item.value === "string") return item.value;
16563
+ if (isMap9(item)) {
16130
16564
  const ref = item.get("ref");
16131
16565
  if (typeof ref === "string") return ref;
16132
16566
  }
16133
16567
  return void 0;
16134
16568
  };
16135
- const rebaseRaw = (raw) => classifyRef(raw).kind === "file" && !isAbsolute10(raw) ? resolve19(configDir, raw) : raw;
16569
+ const rebaseRaw = (raw) => classifyRef(raw).kind === "file" && !isAbsolute11(raw) ? resolve20(configDir, raw) : raw;
16136
16570
  const wantSet = runRefs.map(rebaseRaw).sort().join("\n");
16137
16571
  const seqResolvesToRunSet = (node) => {
16138
16572
  if (!isSeq5(node)) return false;
@@ -16148,10 +16582,10 @@ async function planConfigEdit(ctx, edit, runRefs) {
16148
16582
  for (const item of node.items) {
16149
16583
  const raw = rawRefOf(item);
16150
16584
  if (raw === void 0 || !wants(raw)) continue;
16151
- if (isScalar6(item)) {
16585
+ if (isScalar7(item)) {
16152
16586
  item.value = spec.newRef;
16153
16587
  n += 1;
16154
- } else if (isMap8(item)) {
16588
+ } else if (isMap9(item)) {
16155
16589
  item.set("ref", spec.newRef);
16156
16590
  n += 1;
16157
16591
  }
@@ -16159,13 +16593,13 @@ async function planConfigEdit(ctx, edit, runRefs) {
16159
16593
  return n;
16160
16594
  };
16161
16595
  const root = ctx.configSection === void 0 ? doc : doc.get(ctx.configSection, true);
16162
- const keyOf2 = (key) => root === doc ? doc.get(key, true) : isMap8(root) ? root.get(key, true) : void 0;
16596
+ const keyOf2 = (key) => root === doc ? doc.get(key, true) : isMap9(root) ? root.get(key, true) : void 0;
16163
16597
  const topSeq = keyOf2("schemas");
16164
16598
  let repointCount = repoint(topSeq);
16165
16599
  const overrides = keyOf2("overrides");
16166
16600
  if (isSeq5(overrides)) {
16167
16601
  for (const entry of overrides.items) {
16168
- if (isMap8(entry)) repointCount += repoint(entry.get("schemas", true));
16602
+ if (isMap9(entry)) repointCount += repoint(entry.get("schemas", true));
16169
16603
  }
16170
16604
  }
16171
16605
  const repointed = repointCount > 0;
@@ -16174,10 +16608,10 @@ async function planConfigEdit(ctx, edit, runRefs) {
16174
16608
  if (edit.integrity && isSeq5(topSeq)) {
16175
16609
  const wantPath = foldPath(edit.integrity.path);
16176
16610
  for (const item of topSeq.items) {
16177
- if (!isMap8(item)) continue;
16611
+ if (!isMap9(item)) continue;
16178
16612
  const raw = rawRefOf(item);
16179
16613
  if (raw === void 0 || classifyRef(raw).kind !== "file") continue;
16180
- const abs = isAbsolute10(raw) ? raw : resolve19(configDir, raw);
16614
+ const abs = isAbsolute11(raw) ? raw : resolve20(configDir, raw);
16181
16615
  if (foldPath(abs) !== wantPath) continue;
16182
16616
  pinFrom = item.get("integrity");
16183
16617
  item.set("integrity", edit.integrity.value);
@@ -16406,7 +16840,7 @@ function buildChanges(diff, entries, sentinel, ctx, renameHints = [], booleanAdd
16406
16840
  `"${file}": no writable format for that extension.`
16407
16841
  );
16408
16842
  }
16409
- if (existsSync5(resolve19(ctx.base, file))) {
16843
+ if (existsSync5(resolve20(ctx.base, file))) {
16410
16844
  throw new DocmetaError(
16411
16845
  `"${file}" already exists; INSERT creates new files only.`
16412
16846
  );
@@ -16425,7 +16859,7 @@ function buildChanges(diff, entries, sentinel, ctx, renameHints = [], booleanAdd
16425
16859
  `"${from}" -> "${to}": a rename may not change the extension.`
16426
16860
  );
16427
16861
  }
16428
- if (existsSync5(resolve19(ctx.base, to))) {
16862
+ if (existsSync5(resolve20(ctx.base, to))) {
16429
16863
  throw new DocmetaError(
16430
16864
  `"${to}" already exists; the rename would overwrite it.`
16431
16865
  );
@@ -16454,13 +16888,13 @@ function validateNewPath(p, base) {
16454
16888
  const refuse = (why) => {
16455
16889
  throw new DocmetaError(`"${p}" is not a usable path: ${why}.`);
16456
16890
  };
16457
- if (isAbsolute10(p)) refuse("it is absolute");
16891
+ if (isAbsolute11(p)) refuse("it is absolute");
16458
16892
  if (p.split(/[\\/]/).some((segment) => segment === "..")) {
16459
16893
  refuse("it traverses upward");
16460
16894
  }
16461
- const abs = resolve19(base, p);
16462
- if (abs === resolve19(base)) refuse("it points at the corpus root, not a file");
16463
- if (!abs.startsWith(resolve19(base) + sep9)) refuse("it escapes the corpus");
16895
+ const abs = resolve20(base, p);
16896
+ if (abs === resolve20(base)) refuse("it points at the corpus root, not a file");
16897
+ if (!abs.startsWith(resolve20(base) + sep10)) refuse("it escapes the corpus");
16464
16898
  }
16465
16899
  function refuseJoinFieldChanges(changes, entries, ctx) {
16466
16900
  const index = ctx.externalMetadata;
@@ -16531,6 +16965,12 @@ function postChangeData(changes, entries) {
16531
16965
  }
16532
16966
  return out;
16533
16967
  }
16968
+ function runPages(changes, entries, base) {
16969
+ const labels = new Set(entries.map((e) => e.label));
16970
+ for (const c of changes) if (!("schema" in c) && !("config" in c)) labels.add(c.file);
16971
+ labels.delete(STDIN_LABEL);
16972
+ return [...labels].map((label) => resolve20(base, label));
16973
+ }
16534
16974
  async function planManifestEdits(changes, entries, ctx, renameHints) {
16535
16975
  for (const c of changes) {
16536
16976
  delete c.manifest;
@@ -16579,12 +17019,18 @@ async function planManifestEdits(changes, entries, ctx, renameHints) {
16579
17019
  let write = texts.get(home.absPath);
16580
17020
  if (write === void 0) {
16581
17021
  let text2;
17022
+ let expected;
16582
17023
  try {
16583
17024
  text2 = await readFile12(home.absPath, "utf8");
17025
+ expected = text2;
16584
17026
  } catch (err) {
16585
- throw new DocmetaError(`Manifest ${home.file} could not be read: ${errorMessage(err)}`);
17027
+ if (!home.perPage || !isMissing(err)) {
17028
+ throw new DocmetaError(`Manifest ${home.file} could not be read: ${errorMessage(err)}`);
17029
+ }
17030
+ text2 = "";
17031
+ expected = null;
16586
17032
  }
16587
- write = { path: home.absPath, display: home.file, text: text2, expected: text2 };
17033
+ write = { path: home.absPath, display: home.file, text: text2, expected };
16588
17034
  texts.set(home.absPath, write);
16589
17035
  }
16590
17036
  write.text = change(write.text);
@@ -16607,13 +17053,13 @@ async function planManifestEdits(changes, entries, ctx, renameHints) {
16607
17053
  ...col,
16608
17054
  externalMetadata: col.externalMetadata.filter((m) => classifyRef(m.file).kind !== "url")
16609
17055
  })),
16610
- { configDir: ctx.configDir ?? ctx.cwd, base: ctx.base }
17056
+ { configDir: ctx.configDir ?? ctx.cwd, base: ctx.base, pages: runPages(changes, entries, ctx.base) }
16611
17057
  );
16612
17058
  const index = await leftOut;
16613
17059
  if (index === null) return void 0;
16614
- const abs = resolve19(ctx.base, file);
17060
+ const abs = resolve20(ctx.base, file);
16615
17061
  const named = index.entries.find(
16616
- (e) => e.join === PATH_JOIN && e.abs === abs && excluded.includes(e.collection)
17062
+ (e) => e.join === PATH_JOIN2 && e.abs === abs && excluded.includes(e.collection)
16617
17063
  );
16618
17064
  if (named !== void 0) return named;
16619
17065
  if (!joins || own2 === void 0) return void 0;
@@ -16658,7 +17104,7 @@ async function planManifestEdits(changes, entries, ctx, renameHints) {
16658
17104
  continue;
16659
17105
  }
16660
17106
  if ("renamed" in c) {
16661
- const supplied = ctx.externalMetadata?.byPath.get(resolve19(ctx.base, c.file));
17107
+ const supplied = ctx.externalMetadata?.byPath.get(resolve20(ctx.base, c.file));
16662
17108
  const seen = /* @__PURE__ */ new Set();
16663
17109
  for (const [key, sv] of supplied ?? []) {
16664
17110
  if (seen.has(sv.file) || !ctx.memberships(c.file).includes(sv.collection)) continue;
@@ -16669,7 +17115,12 @@ async function planManifestEdits(changes, entries, ctx, renameHints) {
16669
17115
  `"${c.file}": manifest ${home.file} names it, which is fetched and cannot be written; rename the entry in that repository.`
16670
17116
  );
16671
17117
  }
16672
- if (home.kind !== "manifest" || home.join !== PATH_JOIN || home.entry === void 0) continue;
17118
+ if (home.kind === "manifest" && home.perPage) {
17119
+ throw new DocmetaError(
17120
+ `${c.file} cannot move to ${c.renamed}, because its "${key}" lives in ${home.file}, a {page} manifest named after the page's path. Move the page and its manifest together, then rename the entry's key.`
17121
+ );
17122
+ }
17123
+ if (home.kind !== "manifest" || home.join !== PATH_JOIN2 || home.entry === void 0) continue;
16673
17124
  const moved = await keyHome(loc, c.renamed, own2, key);
16674
17125
  if (moved.kind !== "manifest" || moved.absPath !== home.absPath || moved.entry === void 0) {
16675
17126
  throw new DocmetaError(
@@ -16733,7 +17184,7 @@ async function planManifestEdits(changes, entries, ctx, renameHints) {
16733
17184
  }
16734
17185
  pageRoutes.set(c, ops);
16735
17186
  }
16736
- return [...texts.values()].filter((w) => w.text !== w.expected);
17187
+ return [...texts.values()].filter((w) => w.text !== (w.expected ?? ""));
16737
17188
  }
16738
17189
  async function unhomedWrites(changes, entries, ctx) {
16739
17190
  const written = /* @__PURE__ */ new Map();
@@ -16765,7 +17216,7 @@ async function unhomedWrites(changes, entries, ctx) {
16765
17216
  const prefs = await validator.locationPreferences(data, refs);
16766
17217
  const joins = new Set(
16767
17218
  memberOf(loc.declaredCollections, loc.configDir ?? ctx.cwd, ctx.base, file).flatMap(
16768
- (name2) => (loc.declaredCollections.find((col) => col.name === name2)?.externalMetadata ?? []).map((m) => externalMetadataJoin(m)).filter((j) => j !== PATH_JOIN)
17219
+ (name2) => (loc.declaredCollections.find((col) => col.name === name2)?.externalMetadata ?? []).map((m) => externalMetadataJoin(m)).filter((j) => j !== PATH_JOIN2)
16769
17220
  )
16770
17221
  );
16771
17222
  for (const key of new Set(w.keys)) {
@@ -16816,7 +17267,8 @@ async function offerRelocation(changes, entries, ctx) {
16816
17267
  ctx.externalMetadata = await loadExternalMetadata(loc.collections, {
16817
17268
  configDir: loc.configDir ?? ctx.cwd,
16818
17269
  base: ctx.base,
16819
- offline: ctx.location.offline
17270
+ offline: ctx.location.offline,
17271
+ pages: runPages(changes, entries, ctx.base)
16820
17272
  });
16821
17273
  ctx.memberships = (label) => memberOf(loc.collections, loc.configDir ?? ctx.cwd, ctx.base, label);
16822
17274
  unhomed = await unhomedWrites(changes, entries, ctx);
@@ -16960,14 +17412,14 @@ async function applyChanges(changes, entries, ctx, schemaPlan, manifestWrites =
16960
17412
  const pendingWrites = [];
16961
17413
  const pendingRenames = [];
16962
17414
  for (const [label, ops] of grouped) {
16963
- const path = resolve19(ctx.base, label);
17415
+ const path = resolve20(ctx.base, label);
16964
17416
  if (ops.renamedTo !== void 0) {
16965
17417
  if (!existsSync5(path)) {
16966
17418
  throw new DocmetaError(
16967
17419
  `"${label}" is gone from disk since it was read; re-run the query.`
16968
17420
  );
16969
17421
  }
16970
- const toDisk = resolve19(ctx.base, ops.renamedTo);
17422
+ const toDisk = resolve20(ctx.base, ops.renamedTo);
16971
17423
  if (existsSync5(toDisk)) {
16972
17424
  throw new DocmetaError(
16973
17425
  `"${ops.renamedTo}" appeared on disk since the plan; re-run the query.`
@@ -17092,7 +17544,7 @@ async function applyChanges(changes, entries, ctx, schemaPlan, manifestWrites =
17092
17544
  try {
17093
17545
  now = await readFile12(m.path, "utf8");
17094
17546
  } catch {
17095
- now = void 0;
17547
+ now = null;
17096
17548
  }
17097
17549
  if (now !== m.expected) {
17098
17550
  throw new DocmetaError(
@@ -17106,7 +17558,10 @@ async function applyChanges(changes, entries, ctx, schemaPlan, manifestWrites =
17106
17558
  for (const p of pendingWrites) {
17107
17559
  if (p.ensureDir) dirs.add(dirname11(p.path));
17108
17560
  }
17109
- for (const d of dirs) await mkdir3(d, { recursive: true });
17561
+ for (const m of manifestWrites) {
17562
+ if (m.expected === null) dirs.add(dirname11(m.path));
17563
+ }
17564
+ for (const d of dirs) await mkdir4(d, { recursive: true });
17110
17565
  const writeSchema = async () => {
17111
17566
  for (const w of schemaWrites) await writeFileAtomic(w.path, w.content);
17112
17567
  };
@@ -17221,7 +17676,7 @@ function skipExpression(sql, from) {
17221
17676
 
17222
17677
  // src/meta/commands/fill.ts
17223
17678
  import { readFile as readFile14 } from "node:fs/promises";
17224
- import { resolve as resolve20, extname as extname8, join as join10 } from "node:path";
17679
+ import { resolve as resolve21, extname as extname8, join as join10 } from "node:path";
17225
17680
  import {
17226
17681
  completeValidatedJSON,
17227
17682
  makeProvider,
@@ -17237,68 +17692,6 @@ import {
17237
17692
 
17238
17693
  // src/meta/core/manifest-writes.ts
17239
17694
  import { readFile as readFile13 } from "node:fs/promises";
17240
-
17241
- // src/shared/manifest-cas.ts
17242
- import { posix as posix4 } from "node:path";
17243
- var COMMIT_ATTEMPTS = 5;
17244
- function conflictRefusal(file) {
17245
- return `${file} changed under the command while it was being written. Nothing was written to it. Re-run the command.`;
17246
- }
17247
- function heldManifest(path, file, before) {
17248
- return { path, file, before, text: before, ops: [], bases: /* @__PURE__ */ new Map() };
17249
- }
17250
- function encodeValue(value) {
17251
- return value === void 0 ? "absent" : JSON.stringify(value);
17252
- }
17253
- function baseKey(at2) {
17254
- const entry = at2.join === "path" ? posix4.normalize(at2.entry) : at2.entry;
17255
- return `${entry}\0${at2.key}`;
17256
- }
17257
- function rebaseable(held, op, codec) {
17258
- const at2 = baseKey(op);
17259
- if (held.bases.has(at2)) return;
17260
- held.bases.set(at2, {
17261
- entry: op.entry,
17262
- key: op.key,
17263
- join: op.join,
17264
- value: encodeValue(codec.read(held.before, op))
17265
- });
17266
- }
17267
- function rebase(held, current, codec) {
17268
- for (const base of held.bases.values()) {
17269
- if (encodeValue(codec.read(current, base)) !== base.value) return void 0;
17270
- }
17271
- let text2 = current;
17272
- for (const op of held.ops) text2 = codec.apply(text2, op);
17273
- return text2;
17274
- }
17275
- function rebaseline(held) {
17276
- held.before = held.text;
17277
- held.ops = [];
17278
- held.bases.clear();
17279
- }
17280
- async function settle(held, io) {
17281
- for (let attempt = 1; attempt <= COMMIT_ATTEMPTS; attempt++) {
17282
- let current;
17283
- try {
17284
- current = await io.read(held.path);
17285
- } catch (error) {
17286
- const reason = error instanceof Error ? error.message : String(error);
17287
- throw io.toError(`Manifest ${held.file} could not be read: ${reason}`);
17288
- }
17289
- if (current === held.before) {
17290
- await io.write(held.path, held.text);
17291
- return;
17292
- }
17293
- const replayed = rebase(held, current, io.codec);
17294
- if (replayed === void 0) throw io.toError(conflictRefusal(held.file));
17295
- held.before = current;
17296
- held.text = replayed;
17297
- }
17298
- throw io.toError(conflictRefusal(held.file));
17299
- }
17300
-
17301
- // src/meta/core/manifest-writes.ts
17302
17695
  var manifestCodec = {
17303
17696
  apply(text2, op) {
17304
17697
  const where = { entry: op.entry, key: op.key, join: op.join, file: op.file };
@@ -17311,11 +17704,12 @@ var manifestCodec = {
17311
17704
  function applyManifestOp(text2, op) {
17312
17705
  return manifestCodec.apply(text2, op);
17313
17706
  }
17314
- async function holdManifestFile(path, file) {
17707
+ async function holdManifestFile(path, file, perPage = false) {
17315
17708
  let before;
17316
17709
  try {
17317
17710
  before = await readFile13(path, "utf8");
17318
17711
  } catch (err) {
17712
+ if (perPage && isMissing(err)) return heldManifest(path, file, "", true);
17319
17713
  throw new DocmetaError(`Manifest ${file} could not be read: ${errorMessage(err)}`);
17320
17714
  }
17321
17715
  return heldManifest(path, file, before);
@@ -17328,7 +17722,8 @@ async function settleManifest(held, io = {}) {
17328
17722
  if (held.text === held.before) return false;
17329
17723
  await settle(held, {
17330
17724
  codec: manifestCodec,
17331
- write: io.write ?? writeFileAtomic,
17725
+ // A per-page manifest (proposal 0058) may be the first file in its directory.
17726
+ write: io.write ?? ((at2, text2) => writeFileAtomic(at2, text2, { createParents: true })),
17332
17727
  read: io.read ?? ((at2) => readFile13(at2, "utf8")),
17333
17728
  toError: (message) => new DocmetaError(message)
17334
17729
  });
@@ -17612,7 +18007,9 @@ async function runFill(opts) {
17612
18007
  const loadManifests = () => loadExternalMetadata(mergedCollections(), {
17613
18008
  configDir: configDir ?? cwd,
17614
18009
  base,
17615
- offline: opts.offline ?? config?.offline ?? false
18010
+ offline: opts.offline ?? config?.offline ?? false,
18011
+ // A `{page}` manifest (0058) is read for exactly the pages this run fills.
18012
+ pages: files.map((file) => resolve21(base, file))
17616
18013
  });
17617
18014
  const mergeMembersFor = (label) => memberOf(runConfig.declaredCollections, configDir ?? cwd, base, label);
17618
18015
  let externalMetadata = await loadManifests();
@@ -17649,7 +18046,7 @@ async function runFill(opts) {
17649
18046
  };
17650
18047
  const restorePage = async (label, before) => {
17651
18048
  try {
17652
- await writeFileAtomic(resolve20(base, label), before);
18049
+ await writeFileAtomic(resolve21(base, label), before);
17653
18050
  return `${label} was restored.`;
17654
18051
  } catch (err) {
17655
18052
  return `${label} could not be restored: ${errorMessage(err)}. Restore it from version control.`;
@@ -17658,7 +18055,7 @@ async function runFill(opts) {
17658
18055
  const holdManifest = (home) => {
17659
18056
  let held = holding.get(home.absPath);
17660
18057
  if (held === void 0) {
17661
- held = holdManifestFile(home.absPath, home.file).then((m) => {
18058
+ held = holdManifestFile(home.absPath, home.file, home.perPage).then((m) => {
17662
18059
  const h = { held: m, saving: Promise.resolve() };
17663
18060
  heldManifests.set(home.absPath, h);
17664
18061
  return h;
@@ -18194,7 +18591,7 @@ async function runFill(opts) {
18194
18591
  const changed = next !== content || manifestChanged;
18195
18592
  const pageWritten = next !== content && !dryRun && label !== "<stdin>";
18196
18593
  if (pageWritten) {
18197
- await writeFileAtomic(resolve20(base, label), next);
18594
+ await writeFileAtomic(resolve21(base, label), next);
18198
18595
  }
18199
18596
  if (manifestChanged) {
18200
18597
  const perManifest = /* @__PURE__ */ new Map();
@@ -18226,7 +18623,7 @@ async function runFill(opts) {
18226
18623
  const prepareAll = async () => ({
18227
18624
  stdin: usingStdin ? await prepareOne("<stdin>", opts.stdinContent ?? "", forcedExtractor?.extensions[0] ?? "") : void 0,
18228
18625
  files: await mapConcurrent(files, concurrency, async (file) => {
18229
- const content = await readFile14(resolve20(base, file), "utf8");
18626
+ const content = await readFile14(resolve21(base, file), "utf8");
18230
18627
  return prepareOne(file, content, extname8(file));
18231
18628
  })
18232
18629
  });
@@ -18656,7 +19053,7 @@ async function mapConcurrent(items2, limit, fn) {
18656
19053
 
18657
19054
  // src/meta/commands/derive.ts
18658
19055
  import { readFile as readFile15 } from "node:fs/promises";
18659
- import { extname as extname9, relative as relative14, resolve as resolve21, sep as sep10 } from "node:path";
19056
+ import { extname as extname9, relative as relative15, resolve as resolve22, sep as sep11 } from "node:path";
18660
19057
  var GENERATED_BY_ENV = "MANNI_GENERATED_BY";
18661
19058
  var SOURCE_HINT = "narrow --sources or --fields";
18662
19059
  async function runDerive(opts) {
@@ -18767,7 +19164,7 @@ async function runDerive(opts) {
18767
19164
  const files = fromCollections ? retainMembers(walked, membersFor) : walked;
18768
19165
  const rangesByPage = /* @__PURE__ */ new Map();
18769
19166
  for (const t of targets) {
18770
- const label = relative14(base, resolve21(base, t.page)).split(sep10).join("/");
19167
+ const label = relative15(base, resolve22(base, t.page)).split(sep11).join("/");
18771
19168
  if (!files.includes(label)) {
18772
19169
  throw new DocmetaError(
18773
19170
  `${t.target} does not name one file that derive reads; a range names lines of one file.`
@@ -18792,7 +19189,7 @@ async function runDerive(opts) {
18792
19189
  const loaded2 = /* @__PURE__ */ new Map();
18793
19190
  const errors2 = /* @__PURE__ */ new Map();
18794
19191
  for (const label of files) {
18795
- const absPath = resolve21(base, label);
19192
+ const absPath = resolve22(base, label);
18796
19193
  const extractor = forcedExtractor ?? extractorForExtension(extname9(label));
18797
19194
  if (!extractor) {
18798
19195
  errors2.set(label, {
@@ -18818,12 +19215,13 @@ async function runDerive(opts) {
18818
19215
  }
18819
19216
  }
18820
19217
  const provenanceRoot = configDir ?? cwd;
19218
+ const pages2 = files.map((label) => resolve22(base, label));
18821
19219
  const manifests = wantsProvenance ? provenanceManifests(declaredCollections, provenanceRoot, base) : [];
18822
19220
  const provenanceCollections = declaredCollections.map((c) => ({
18823
19221
  ...c,
18824
19222
  externalMetadata: c.externalMetadata.filter((m) => m.keys.includes(PROVENANCE_FIELD))
18825
19223
  })).filter((c) => c.externalMetadata.length > 0);
18826
- const provenanceIndex = manifests.length === 0 ? null : await loadExternalMetadata(provenanceCollections, { configDir: provenanceRoot, base });
19224
+ const provenanceIndex = manifests.length === 0 ? null : await loadExternalMetadata(provenanceCollections, { configDir: provenanceRoot, base, pages: pages2 });
18827
19225
  const places2 = /* @__PURE__ */ new Map();
18828
19226
  for (const doc of loaded2.values()) {
18829
19227
  if (!wantsProvenance) break;
@@ -18850,7 +19248,7 @@ async function runDerive(opts) {
18850
19248
  (m) => classifyRef(m.file).kind !== "url" && m.keys.some((k) => k !== PROVENANCE_FIELD && fields.includes(k))
18851
19249
  )
18852
19250
  })).filter((c) => c.externalMetadata.length > 0);
18853
- const ownedIndex = ownedCollections.length === 0 ? null : await loadExternalMetadata(ownedCollections, { configDir: provenanceRoot, base });
19251
+ const ownedIndex = ownedCollections.length === 0 ? null : await loadExternalMetadata(ownedCollections, { configDir: provenanceRoot, base, pages: pages2 });
18854
19252
  const derived2 = await deriveMetadata([...loaded2.values()], {
18855
19253
  cwd,
18856
19254
  base,
@@ -18945,8 +19343,8 @@ async function runDerive(opts) {
18945
19343
  error: `${at2} is encrypted, and no encryption key is available to compare it. Set ${ENCRYPTION_KEY_ENV}, or run \`manni key set\`.`
18946
19344
  };
18947
19345
  }
18948
- const unreadable = view.unreadable.find((p) => isAtOrUnder(p, at2));
18949
- if (unreadable !== void 0) return { error: unreadableMessage(unreadable) };
19346
+ const unreadable2 = view.unreadable.find((p) => isAtOrUnder(p, at2));
19347
+ if (unreadable2 !== void 0) return { error: unreadableMessage(unreadable2) };
18950
19348
  }
18951
19349
  const stamped = { ...view.data };
18952
19350
  for (const field of fields) {
@@ -19022,7 +19420,7 @@ async function runDerive(opts) {
19022
19420
  const holdManifest = async (manifest) => {
19023
19421
  const already = heldManifests.get(manifest.absPath);
19024
19422
  if (already !== void 0) return already;
19025
- const held = await holdManifestFile(manifest.absPath, manifest.file);
19423
+ const held = await holdManifestFile(manifest.absPath, manifest.file, manifest.perPage);
19026
19424
  heldManifests.set(manifest.absPath, held);
19027
19425
  return held;
19028
19426
  };
@@ -19953,6 +20351,9 @@ export {
19953
20351
  palette,
19954
20352
  shouldColor,
19955
20353
  matchesFileGlob,
20354
+ hasPagePlaceholder,
20355
+ pageManifestPath,
20356
+ findPageManifests,
19956
20357
  externalMetadataUrlProblem,
19957
20358
  parseCollections,
19958
20359
  selectCollections,
@@ -19965,6 +20366,7 @@ export {
19965
20366
  findGitRoot,
19966
20367
  valeConfigPath,
19967
20368
  programName,
20369
+ warn,
19968
20370
  notice,
19969
20371
  ToolError,
19970
20372
  errorMessage,
@@ -20005,6 +20407,7 @@ export {
20005
20407
  committedKeyWarning,
20006
20408
  terminalConfirm,
20007
20409
  ensureEncryptionKey,
20410
+ gitIgnored,
20008
20411
  escapePointerSegment,
20009
20412
  hasFrontmatterFence,
20010
20413
  locateFrontmatter,
@@ -20068,6 +20471,10 @@ export {
20068
20471
  isMachineFormat,
20069
20472
  OMITTED_WHEN_CLEAN,
20070
20473
  render,
20474
+ heldManifest,
20475
+ rebaseable,
20476
+ settle,
20477
+ isMissing,
20071
20478
  EXTERNAL_METADATA_FETCH_TIMEOUT_MS,
20072
20479
  EXTERNAL_METADATA_FETCH_MAX_BYTES,
20073
20480
  fetchExternalMetadata,
@@ -20076,7 +20483,7 @@ export {
20076
20483
  EXTERNAL_OWNED_SCHEMA,
20077
20484
  EXTERNAL_DUPLICATE_SCHEMA,
20078
20485
  EXTERNAL_KEYWORD,
20079
- PATH_JOIN,
20486
+ PATH_JOIN2 as PATH_JOIN,
20080
20487
  externalMetadataJoin,
20081
20488
  loadExternalMetadata,
20082
20489
  mergeExternalMetadata,
@@ -20134,9 +20541,6 @@ export {
20134
20541
  runVendorSchema,
20135
20542
  runInferSchema,
20136
20543
  runQuery,
20137
- heldManifest,
20138
- rebaseable,
20139
- settle,
20140
20544
  runFill,
20141
20545
  runDerive,
20142
20546
  deriveFailed,
@@ -20155,4 +20559,4 @@ export {
20155
20559
  renderQuery,
20156
20560
  renderQueryCsv
20157
20561
  };
20158
- //# sourceMappingURL=chunk-KJLPOY2I.js.map
20562
+ //# sourceMappingURL=chunk-BUXVMARH.js.map