@hawkeyexl/manni 2.12.1 → 3.0.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.
@@ -12,10 +12,12 @@ import {
12
12
  STDIN_LABEL,
13
13
  STDIN_TOKEN,
14
14
  ToolError,
15
+ __default,
15
16
  __export,
16
17
  assertNonEmpty,
17
18
  classifyRef,
18
19
  committedKeyWarning,
20
+ configMarks,
19
21
  decryptValue,
20
22
  encryptValue,
21
23
  ensureEncryptionKey,
@@ -50,11 +52,12 @@ import {
50
52
  loadExternalMetadata,
51
53
  locateFrontmatter,
52
54
  memberOf,
53
- mergeExternalMetadata,
55
+ mergeWithMarks,
54
56
  normalizeText,
55
57
  orphanEntries,
56
58
  orphanError,
57
59
  orphanJoins,
60
+ ownsKey,
58
61
  pageManifestPath,
59
62
  palette,
60
63
  parseLines,
@@ -81,7 +84,7 @@ import {
81
84
  withValueAt,
82
85
  writeEncryptionKey,
83
86
  writeFileAtomic
84
- } from "./chunk-F6CE4LEJ.js";
87
+ } from "./chunk-I6PYC7EP.js";
85
88
 
86
89
  // src/meta/core/reencrypt.ts
87
90
  import { extname } from "node:path";
@@ -577,123 +580,6 @@ async function readTracked(root, path) {
577
580
  import { extname as extname2 } from "node:path";
578
581
  import * as Ajv2020Ns from "ajv/dist/2020.js";
579
582
 
580
- // src/cite/schema/citations.json
581
- var citations_default = {
582
- $schema: "https://json-schema.org/draft/2020-12/schema",
583
- $id: "manni:citations:1.0.0-proposal.4",
584
- title: "manni citations vocabulary (1.0.0-proposal.4)",
585
- description: "What do this page's claims rest on? For each claim, the page lines that make it, the source lines it was written from, and a hash of each. A manni common vocabulary: any drift check can implement it, and other schemas may compose on top of it. A pin is a record of the past, not a copy of the present. It says what the lines were when the sentence was written. The check compares that record with the files as they are now. Recompute it on every run and there is nothing to compare against. `source-of-truth` (manni:stewardship) names the file a page answers to; a citation names the lines a sentence answers to. The page root stays open so sibling tools' keys pass untouched. The `citation-` prefix is reserved, so an unrecognized `citation-*` key is rejected and a typo fails loudly.",
586
- type: "object",
587
- additionalProperties: true,
588
- properties: {
589
- citations: {
590
- type: "array",
591
- minItems: 1,
592
- items: {
593
- $ref: "#/$defs/citationEntry"
594
- },
595
- description: "The page's citations, one entry per claim or pin. An empty list is not a declaration; a page with no citations omits the key. Ids are unique within a page, and two entries claiming one id is an error. JSON Schema cannot say that over a list whose members may omit the id. So it is a requirement on implementations, stated here so every tool agrees.",
596
- "x-manni-location": "external"
597
- }
598
- },
599
- patternProperties: {
600
- "^citation-": false
601
- },
602
- $defs: {
603
- citationEntry: {
604
- type: "object",
605
- additionalProperties: false,
606
- required: [
607
- "source"
608
- ],
609
- description: "One citation: the page text it supports, the source lines it rests on, and how the two are anchored. Both ends share one shape, a line range and a hash.",
610
- properties: {
611
- id: {
612
- type: "string",
613
- pattern: "^[a-z0-9][a-z0-9-]*$",
614
- description: "A kebab-case name for this citation, unique within the page. A body marker names an entry by it, and findings are reported under it. Optional for an entry anchored by its claim lines or pinned bare. Required when a marker names the entry."
615
- },
616
- claim: {
617
- $ref: "#/$defs/claim"
618
- },
619
- source: {
620
- $ref: "#/$defs/source"
621
- },
622
- quote: {
623
- type: "boolean",
624
- default: false,
625
- description: "The claim is a fenced block that reproduces the source lines. The pinned claim covers the whole block, fences included. An implementer compares the lines inside the fences with the source under the pin's rule. So a quoted snippet that drifts from its source is a finding even while the pin still holds. Under a marker, the block is the next fence after the marker. Default false."
626
- }
627
- }
628
- },
629
- claim: {
630
- type: "object",
631
- additionalProperties: false,
632
- required: [
633
- "integrity"
634
- ],
635
- description: "The page text the citation supports, pinned by lines and a hash. Absent, the entry is a bare pin, or a marker with no drift check on its sentence. Without `lines`, the text pinned is what a body marker naming this entry anchors.",
636
- properties: {
637
- lines: {
638
- $ref: "#/$defs/lines",
639
- description: "Lines of the page body. The body starts on the first line after the closing frontmatter fence, or on line 1 with no frontmatter. So editing the frontmatter never moves a claim. Required unless a marker names the entry, and never inside the frontmatter."
640
- },
641
- integrity: {
642
- type: "string",
643
- pattern: "^sha256-[0-9a-f]{64}$",
644
- description: "`sha256-` and sixty-four lowercase hex digits over the claimed lines, under the rule `source.integrity` states. Always plain, never keyed: the page is public, so there is nothing to hide. With a marker, the lines hashed are the ones the marker anchors."
645
- }
646
- }
647
- },
648
- source: {
649
- type: "object",
650
- additionalProperties: false,
651
- required: [
652
- "file",
653
- "integrity"
654
- ],
655
- description: "The lines the claim rests on: a file, an optional line range, a pin, and the commit the pin was taken at.",
656
- properties: {
657
- file: {
658
- $ref: "#/$defs/fileRef"
659
- },
660
- lines: {
661
- $ref: "#/$defs/lines",
662
- description: "File lines. Absent, the whole file is pinned. The lines stay readable even when `file` is encrypted."
663
- },
664
- integrity: {
665
- type: "string",
666
- pattern: "^(?:sha256|hmac-sha256)-[0-9a-f]{64}$",
667
- description: "The pin: an algorithm prefix and sixty-four lowercase hex digits over the cited lines under one rule. Decode as UTF-8 and drop one leading byte-order mark. Turn CRLF into LF, split on LF, and discard the empty element a trailing LF leaves. Take lines L1 to L2 inclusive, or every line for a whole file. Join with LF and no trailing LF, and keep trailing whitespace. A plain `file` is pinned `sha256-` over that text. An encrypted `file` is pinned `hmac-sha256-`: the HMAC-SHA256 of that text under a key derived from the encryption key. So a public page carries no verifier a reader could run against a guessed private line. The prefix follows `file`, and any other pairing is an invalid entry."
668
- },
669
- "commit-sha": {
670
- type: "string",
671
- pattern: "^[0-9a-f]{7,64}$",
672
- description: "The git commit hash the source pin was taken at, as lowercase hex. Seven digits is what git abbreviates to; sixty-four covers a SHA-256 repository. A tool writes the full hash, because an abbreviation that is unique today need not be next year. Without it, a check can still say the lines changed. With it, the check can say since when, and whether the pin was ever true."
673
- }
674
- }
675
- },
676
- lines: {
677
- oneOf: [
678
- {
679
- type: "integer",
680
- minimum: 1
681
- },
682
- {
683
- type: "string",
684
- pattern: "^[1-9][0-9]*-[1-9][0-9]*$"
685
- }
686
- ],
687
- description: 'A line range, 1-based and inclusive: an integer for one line, or `"L1-L2"` for several. `L1 <= L2` is a rule on implementations, since a pattern cannot compare two numbers. There is no line 0 to cite.'
688
- },
689
- fileRef: {
690
- type: "string",
691
- pattern: "^(?:~[A-Za-z0-9_-]{82,}|(?!~)(?:(?!\\.\\.?(?:/|$))[^/\\\\:\\r\\n\\t]+)(?:/(?:(?!\\.\\.?(?:/|$))[^/\\\\:\\r\\n\\t]+))*)$",
692
- description: "The file: a path relative to the repository root, posix-separated, and free of `.` and `..` segments. Segments may hold spaces and dots but not `/`, `\\`, `:` or a line break. So a URL, a drive letter, a Windows path and an absolute path all fail here. In place of the path, `~` and at least 82 base64url characters is the path encrypted with the family key. Proposal 0045 states the construction. Only a checkout with the key and the files can decrypt the path and read the lines. So a public docs repo can cite a private code repo without publishing its paths. The value carries no encryption mark, because the citation tool encrypts it, and a plain path stays valid."
693
- }
694
- }
695
- };
696
-
697
583
  // src/shared/code-regions.ts
698
584
  var SKIP_MARKDOWN_FENCE = /^[ \t]*(`{3,}|~{3,})/;
699
585
  var SKIP_ASCIIDOC_FENCE = /^(-{4,})/;
@@ -1215,9 +1101,9 @@ var entryValidator;
1215
1101
  function compileEntryValidator() {
1216
1102
  if (entryValidator) return entryValidator;
1217
1103
  const ajv = new Ajv2020({ allErrors: false, strict: false });
1218
- ajv.addSchema(citations_default);
1104
+ ajv.addSchema(__default);
1219
1105
  entryValidator = ajv.compile({
1220
- $ref: `${citations_default.$id}#/$defs/citationEntry`
1106
+ $ref: `${__default.$id}#/$defs/citationEntry`
1221
1107
  });
1222
1108
  return entryValidator;
1223
1109
  }
@@ -2516,52 +2402,80 @@ function twoManifestsRefusal(label, a, b) {
2516
2402
  return `${label} is in collections ${a} and ${b}, and both keep citations in a manifest.`;
2517
2403
  }
2518
2404
  function ownsCitations(manifest) {
2519
- return manifest.keys.includes(CITATIONS_KEY2);
2405
+ return manifest.keys === void 0 || manifest.keys.includes(CITATIONS_KEY2);
2406
+ }
2407
+ function emptyIndex() {
2408
+ return { owners: /* @__PURE__ */ new Map(), byPath: /* @__PURE__ */ new Map(), byField: /* @__PURE__ */ new Map(), entries: [] };
2520
2409
  }
2521
2410
  async function loadCitationSidecars(opts) {
2522
2411
  const toError = opts.toError ?? ((message) => new CiteError(message));
2523
2412
  const manifests = [];
2413
+ const urls = [];
2524
2414
  const scoped = [];
2525
2415
  for (const collection of opts.collections) {
2526
2416
  const owning = collection.externalMetadata.filter(ownsCitations);
2527
2417
  if (owning.length === 0) continue;
2418
+ const loaded = [];
2528
2419
  for (const manifest of owning) {
2420
+ const implied = manifest.keys === void 0 ? { implied: { collection, manifest } } : {};
2529
2421
  if (classifyRef(manifest.file).kind === "url") {
2530
- throw toError(urlManifestRefusal(opts.configSource, collection.name));
2422
+ if (manifest.keys !== void 0) {
2423
+ throw toError(urlManifestRefusal(opts.configSource, collection.name));
2424
+ }
2425
+ urls.push({ collection: collection.name, path: manifest.file, file: manifest.file, join: externalMetadataJoin(manifest), ...implied });
2426
+ continue;
2531
2427
  }
2428
+ loaded.push(manifest);
2532
2429
  const path = isAbsolute(manifest.file) ? manifest.file : resolve(opts.configDir, manifest.file);
2533
2430
  manifests.push({
2534
2431
  collection: collection.name,
2535
2432
  path,
2536
2433
  file: reportedPath(path, opts.base),
2537
2434
  join: externalMetadataJoin(manifest),
2538
- ...hasPagePlaceholder(manifest.file) ? { pattern: manifest.file } : {}
2435
+ ...hasPagePlaceholder(manifest.file) ? { pattern: manifest.file } : {},
2436
+ ...implied
2539
2437
  });
2540
2438
  }
2541
- scoped.push({ ...collection, externalMetadata: owning });
2439
+ if (loaded.length > 0) scoped.push({ ...collection, externalMetadata: loaded });
2542
2440
  }
2543
- if (manifests.length === 0) return null;
2441
+ if (manifests.length === 0 && urls.length === 0) return null;
2544
2442
  const index = await loadExternalMetadata(scoped, {
2545
2443
  configDir: opts.configDir,
2546
2444
  base: opts.base,
2547
2445
  ...opts.pages === void 0 ? {} : { pages: opts.pages }
2548
- });
2446
+ }) ?? (urls.length > 0 ? emptyIndex() : null);
2549
2447
  if (index === null) return null;
2550
- return sidecars(index, manifests, scoped, opts);
2448
+ return sidecars(index, manifests, urls, scoped, opts);
2551
2449
  }
2552
- function sidecars(index, manifests, collections, opts) {
2450
+ function sidecars(index, manifests, urls, collections, opts) {
2553
2451
  const toError = opts.toError ?? ((message) => new CiteError(message));
2554
2452
  const { configDir, base } = opts;
2453
+ let marks;
2454
+ const marksOnce = () => marks ??= opts.marks?.() ?? Promise.resolve(void 0);
2555
2455
  const pageOwner = (owner, label) => {
2556
2456
  if (owner.pattern === void 0) return owner;
2557
2457
  const resolved = pageManifestPath(owner.pattern, configDir, resolve(base, label));
2558
2458
  if ("outside" in resolved) return void 0;
2559
2459
  return { ...owner, path: resolved.abs, file: reportedPath(resolved.abs, base) };
2560
2460
  };
2561
- const forPage = (label, content, format) => {
2461
+ const forPage = async (label, content, format) => {
2462
+ const declaredMembers = memberOf(opts.collections, configDir, base, label);
2463
+ if (declaredMembers.length === 0) return { joins: [] };
2562
2464
  const members = memberOf(collections, configDir, base, label);
2563
- if (members.length === 0) return { joins: [] };
2564
- const mine = manifests.filter((m) => members.includes(m.collection));
2465
+ const memberOfCollection = (m) => declaredMembers.includes(m.collection);
2466
+ let extracted;
2467
+ const extract = () => extracted ??= pickExtractor(label, format).extract(content, label);
2468
+ const candidates2 = [...manifests, ...urls].filter((m) => m.implied !== void 0 && memberOfCollection(m));
2469
+ let marked;
2470
+ if (candidates2.length > 0) {
2471
+ const read = await marksOnce();
2472
+ const data = extract().data;
2473
+ marked = read === void 0 ? void 0 : await read(label, Object.hasOwn(data, CITATIONS_KEY2) ? data : { ...data, [CITATIONS_KEY2]: null }, declaredMembers);
2474
+ }
2475
+ const keeps = (m) => m.implied === void 0 || ownsKey(m.implied.collection, m.implied.manifest, CITATIONS_KEY2, marked);
2476
+ const url = urls.find((m) => memberOfCollection(m) && keeps(m));
2477
+ if (url !== void 0) throw toError(urlManifestRefusal(opts.configSource, url.collection));
2478
+ const mine = manifests.filter((m) => members.includes(m.collection) && keeps(m));
2565
2479
  const [declared, second] = mine;
2566
2480
  if (declared === void 0) return { joins: [] };
2567
2481
  if (second !== void 0) {
@@ -2569,17 +2483,17 @@ function sidecars(index, manifests, collections, opts) {
2569
2483
  }
2570
2484
  const owner = pageOwner(declared, label);
2571
2485
  if (owner === void 0) return { joins: [] };
2572
- const extractor = pickExtractor(label, format);
2573
- const extracted = extractor.extract(content, label);
2574
- const merged = mergeExternalMetadata(label, extracted, index, members, base, {
2575
- encryptionKey: () => opts.key
2486
+ const readMarks = await marksOnce();
2487
+ const merged = await mergeWithMarks(label, extract(), index, members, base, {
2488
+ encryptionKey: () => opts.key,
2489
+ ...readMarks === void 0 ? {} : { marks: readMarks }
2576
2490
  });
2577
2491
  const out = { owner, joins: merged.joins };
2578
2492
  if (owner.join === PATH_JOIN) {
2579
2493
  out.entry = toPosix(relative(configDir, resolve(base, label)));
2580
2494
  } else {
2581
2495
  const value = merged.joins.find((j) => j.field === owner.join)?.value;
2582
- const raw = value ?? extracted.data[owner.join];
2496
+ const raw = value ?? extract().data[owner.join];
2583
2497
  if (typeof raw === "string" && raw !== "") out.entry = raw;
2584
2498
  else if (typeof raw === "number" || typeof raw === "boolean") out.entry = String(raw);
2585
2499
  }
@@ -2638,6 +2552,7 @@ function duplicateJoinRefusal(field, value, file, labels) {
2638
2552
  function sidecarsFor(run3, over = {}) {
2639
2553
  const declared = run3.configFile?.collections ?? [];
2640
2554
  if (run3.configDir === void 0 || declared.length === 0) return Promise.resolve(null);
2555
+ const { configPath } = run3;
2641
2556
  return loadCitationSidecars({
2642
2557
  // Every declared collection, whatever `--collection` or the paths chose.
2643
2558
  collections: declared,
@@ -2646,7 +2561,10 @@ function sidecarsFor(run3, over = {}) {
2646
2561
  configSource: run3.configSource ?? run3.configPath ?? "the config",
2647
2562
  ...run3.key === void 0 ? {} : { key: run3.key },
2648
2563
  ...over.toError === void 0 ? {} : { toError: over.toError },
2649
- ...over.pages === void 0 ? {} : { pages: over.pages }
2564
+ ...over.pages === void 0 ? {} : { pages: over.pages },
2565
+ // A keyless manifest (proposal 0068) keeps what a page's schemas mark,
2566
+ // read from meta's section of the same file.
2567
+ ...configPath === void 0 ? {} : { marks: () => configMarks(configPath, run3.base) }
2650
2568
  });
2651
2569
  }
2652
2570
 
@@ -3414,7 +3332,7 @@ async function checkCitations(page, opts) {
3414
3332
  page: read,
3415
3333
  lines,
3416
3334
  root: opts.root,
3417
- index: index ?? emptyIndex,
3335
+ index: index ?? emptyIndex2,
3418
3336
  checkSources,
3419
3337
  ...opts.key === void 0 ? {} : { key: opts.key }
3420
3338
  };
@@ -3525,7 +3443,7 @@ function skippedSource(entry) {
3525
3443
  if (source["commit-sha"] !== void 0) out.commitSha = source["commit-sha"];
3526
3444
  return out;
3527
3445
  }
3528
- var emptyIndex = { files: () => [], has: () => false };
3446
+ var emptyIndex2 = { files: () => [], has: () => false };
3529
3447
 
3530
3448
  // src/cite/core/mint.ts
3531
3449
  var COMMIT_PATTERN = /^[0-9a-f]{7,64}$/;
@@ -3882,8 +3800,8 @@ async function prepareRun(opts, action, verb, sources = {}) {
3882
3800
  };
3883
3801
  if (checkSources) pageOptions.sourceIndex = await sourceIndexFor(run3.root, git);
3884
3802
  const sidecars2 = await sidecarsFor(run3, { pages: files.map((file) => resolve3(base, file)) });
3885
- const setupFor = (label, content) => {
3886
- const page = sidecars2?.forPage(label, content, forced?.name);
3803
+ const setupFor = async (label, content) => {
3804
+ const page = await sidecars2?.forPage(label, content, forced?.name);
3887
3805
  if (page?.owner === void 0) return { options: pageOptions };
3888
3806
  return {
3889
3807
  sidecar: page,
@@ -3947,7 +3865,7 @@ async function runCheck(opts) {
3947
3865
  const hits = joinHits();
3948
3866
  const pages = [];
3949
3867
  const checkOne = async (label, content) => {
3950
- const setup = prepared.setupFor(label, content);
3868
+ const setup = await prepared.setupFor(label, content);
3951
3869
  if (setup.sidecar !== void 0) hits.record(setup.sidecar, label);
3952
3870
  pages.push(await checkCitations({ file: label, content, format: forced?.name }, setup.options));
3953
3871
  };
@@ -4069,7 +3987,7 @@ async function runAdd(opts) {
4069
3987
  `A page read from stdin has no path, and its citations live in ${firstManifest.file}, which is keyed by ${keyedBy}.`
4070
3988
  );
4071
3989
  }
4072
- const sidecar = usingStdin ? void 0 : sidecars2?.forPage(label, content, opts.as);
3990
+ const sidecar = usingStdin ? void 0 : await sidecars2?.forPage(label, content, opts.as);
4073
3991
  const owner = sidecar?.owner;
4074
3992
  const page = readPage(label, content, {
4075
3993
  ...opts.as === void 0 ? {} : { format: opts.as },
@@ -4365,7 +4283,7 @@ async function runRemove(opts) {
4365
4283
  const pageCount = files.length + (usingStdin ? 1 : 0);
4366
4284
  const planned = [];
4367
4285
  const removeOne = async (label, content, path) => {
4368
- const setup = prepared.setupFor(label, content);
4286
+ const setup = await prepared.setupFor(label, content);
4369
4287
  if (setup.sidecar !== void 0) hits.record(setup.sidecar, label);
4370
4288
  const owner = setup.sidecar?.owner;
4371
4289
  const page = readPage(label, content, {
@@ -5253,7 +5171,7 @@ async function runUpdate(opts) {
5253
5171
  };
5254
5172
  const pages = [];
5255
5173
  const updateOne = async (label, content, path) => {
5256
- const setup = prepared.setupFor(label, content);
5174
+ const setup = await prepared.setupFor(label, content);
5257
5175
  if (setup.sidecar !== void 0) hits.record(setup.sidecar, label);
5258
5176
  const report = await checkCitations({ file: label, content, format: forced?.name }, setup.options);
5259
5177
  const { format } = report;
@@ -6204,7 +6122,7 @@ function encryptedCitations(file, content, format) {
6204
6122
  async function rotateManifestCitations(sidecars2, manifests, page, opts) {
6205
6123
  const none = { rewritten: [], skipped: [], wantsHistory: false };
6206
6124
  if (sidecars2 === null) return none;
6207
- const sidecar = sidecars2.forPage(page.file, page.content, page.format);
6125
+ const sidecar = await sidecars2.forPage(page.file, page.content, page.format);
6208
6126
  const owner = sidecar.owner;
6209
6127
  if (owner === void 0 || sidecar.citations === void 0) return none;
6210
6128
  if (!sidecar.citations.some((input) => ANY_CIPHERTEXT.test(JSON.stringify(input.entry)))) {
@@ -6424,6 +6342,8 @@ async function runKeyRotate(opts) {
6424
6342
  configSource: file.source,
6425
6343
  key: current,
6426
6344
  toError: toKeyError,
6345
+ // A keyless manifest (proposal 0068) keeps what a page's schemas mark.
6346
+ marks: () => configMarks(file.path, file.dir),
6427
6347
  // A `{page}` manifest (0058) is read for exactly the pages rotated.
6428
6348
  pages: files.map((rel) => resolve7(base, rel))
6429
6349
  });
@@ -6618,4 +6538,4 @@ export {
6618
6538
  runKeyRotate,
6619
6539
  runKeySet
6620
6540
  };
6621
- //# sourceMappingURL=chunk-7X4QMQX4.js.map
6541
+ //# sourceMappingURL=chunk-W75SLU7Z.js.map