@nimiplatform/nimi-coding 0.4.0 → 0.6.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +92 -55
  4. package/README.zh-CN.md +93 -56
  5. package/cli/commands/authority.mjs +80 -125
  6. package/cli/commands/clear.mjs +3 -2
  7. package/cli/commands/code.mjs +225 -0
  8. package/cli/commands/start.mjs +3 -2
  9. package/cli/constants.mjs +2 -1
  10. package/cli/dev/check-authority-surface.mjs +17 -8
  11. package/cli/dev/check-product-conformance.mjs +4 -32
  12. package/cli/dev/run-cli-smoke.mjs +6 -0
  13. package/cli/help.mjs +8 -3
  14. package/cli/index.mjs +3 -2
  15. package/cli/lib/authority/audit.mjs +262 -185
  16. package/cli/lib/authority/change-candidates.mjs +807 -0
  17. package/cli/lib/authority/closed-sets.mjs +7 -8
  18. package/cli/lib/authority/compile.mjs +6 -24
  19. package/cli/lib/authority/compiled-snapshot.mjs +143 -0
  20. package/cli/lib/authority/consumers.mjs +69 -19
  21. package/cli/lib/authority/diagnostics.mjs +10 -2
  22. package/cli/lib/authority/diff.mjs +77 -54
  23. package/cli/lib/authority/format.mjs +4 -32
  24. package/cli/lib/authority/git-snapshot.mjs +34 -379
  25. package/cli/lib/authority/graph.mjs +10 -10
  26. package/cli/lib/authority/impact.mjs +17 -144
  27. package/cli/lib/authority/review.mjs +33 -43
  28. package/cli/lib/authority/sarif.mjs +1 -8
  29. package/cli/lib/authority/source-yaml.mjs +0 -4
  30. package/cli/lib/authority/surface.mjs +19 -1
  31. package/cli/lib/authority/terms.mjs +10 -9
  32. package/cli/lib/authority/validate.mjs +6 -14
  33. package/cli/lib/code/authority-annotation-response.mjs +43 -0
  34. package/cli/lib/code/authority-annotation-scan.mjs +162 -0
  35. package/cli/lib/code/authority-annotations.mjs +293 -0
  36. package/cli/lib/code/context.mjs +710 -0
  37. package/cli/lib/code/response.mjs +165 -0
  38. package/cli/lib/doctor.mjs +3 -2
  39. package/cli/lib/entrypoints.mjs +13 -5
  40. package/cli/seeds/seed-policy.yaml +2 -2
  41. package/contracts/authority-source.schema.yaml +0 -6
  42. package/contracts/authority-verifier-bindings.schema.yaml +21 -10
  43. package/methodology/authority-authoring.yaml +68 -70
  44. package/methodology/core.yaml +10 -6
  45. package/package.json +5 -3
  46. package/spec/authority-audit.yaml +56 -52
  47. package/spec/authority-authoring.yaml +3 -16
  48. package/spec/authority-change-candidates.yaml +146 -0
  49. package/spec/authority-consumers.yaml +7 -0
  50. package/spec/authority-discovery.yaml +2 -2
  51. package/spec/authority-graph.yaml +1 -1
  52. package/spec/authority-impact.yaml +16 -39
  53. package/spec/authority-review.yaml +29 -47
  54. package/spec/authority-terms.yaml +5 -1
  55. package/spec/code-authority-annotations.yaml +42 -0
  56. package/spec/code-context.yaml +38 -0
  57. package/spec/product-scope.yaml +15 -9
  58. package/cli/commands/validate-ai-governance.mjs +0 -335
  59. package/cli/lib/authority/evidence-snapshot.mjs +0 -1
  60. package/cli/lib/authority/evidence.mjs +0 -1432
  61. package/cli/lib/internal/governance/ai/ai-context-budget-core.mjs +0 -377
  62. package/cli/lib/internal/governance/ai/ai-structure-budget-core.mjs +0 -376
  63. package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +0 -155
  64. package/cli/lib/internal/governance/ai/check-high-risk-doc-metadata-core.mjs +0 -173
  65. package/cli/lib/internal/governance/config.mjs +0 -114
  66. package/cli/lib/internal/governance/shared/read-yaml-with-fragments.mjs +0 -49
  67. package/contracts/authority-evidence-bindings.schema.yaml +0 -53
  68. package/contracts/authority-evidence-probe-results.schema.yaml +0 -38
  69. package/contracts/authority-impact-dispositions.schema.yaml +0 -13
  70. package/spec/authority-evidence.yaml +0 -200
@@ -6,7 +6,7 @@ import { lstat, mkdir, mkdtemp, open, readdir, realpath, rm, writeFile } from "n
6
6
  import os from "node:os";
7
7
  import path from "node:path";
8
8
 
9
- import { compareText, makeDiagnostic } from "./diagnostics.mjs";
9
+ import { compareText, isPathWithin, makeDiagnostic } from "./diagnostics.mjs";
10
10
 
11
11
  const SNAPSHOT_IDENTITY_FORMAT = "nimicoding.authority-snapshot-content/v1";
12
12
  const SPEC_ROOT = ".nimi/spec";
@@ -285,11 +285,6 @@ function sameObject(left, right) {
285
285
  return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode;
286
286
  }
287
287
 
288
- function contained(root, candidate) {
289
- const relative = path.relative(root, candidate);
290
- return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
291
- }
292
-
293
288
  function filesystemFailure(error, initial, entryPath = SPEC_ROOT) {
294
289
  if (error instanceof AuthorityReviewRefusal) return error;
295
290
  if (!initial && ["ENOENT", "ENOTDIR", "ELOOP"].includes(error?.code)) {
@@ -303,13 +298,13 @@ async function securedDirectory(absolute, containmentRoot, entryPath, initial) {
303
298
  const before = await lstat(absolute, { bigint: true });
304
299
  if (before.isSymbolicLink() || !before.isDirectory()) throw refusal("AUTH_REVIEW_WORKTREE_ENTRY_INVALID", `worktree authority entry is not a regular directory: ${entryPath}`, entryPath);
305
300
  const beforeReal = await realpath(absolute);
306
- if (!contained(containmentRoot, beforeReal)) throw refusal("AUTH_REVIEW_WORKTREE_ENTRY_INVALID", `worktree authority directory escapes .nimi/spec: ${entryPath}`, entryPath);
301
+ if (!isPathWithin(containmentRoot, beforeReal)) throw refusal("AUTH_REVIEW_WORKTREE_ENTRY_INVALID", `worktree authority directory escapes .nimi/spec: ${entryPath}`, entryPath);
307
302
  const handle = await open(absolute, fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW);
308
303
  try {
309
304
  const held = await handle.stat({ bigint: true });
310
305
  const current = await lstat(absolute, { bigint: true });
311
306
  const currentReal = await realpath(absolute);
312
- if (!held.isDirectory() || !sameObject(before, held) || !sameObject(held, current) || !contained(containmentRoot, currentReal)) {
307
+ if (!held.isDirectory() || !sameObject(before, held) || !sameObject(held, current) || !isPathWithin(containmentRoot, currentReal)) {
313
308
  throw refusal("AUTH_REVIEW_CAPTURE_CHANGED", `worktree authority directory changed during capture: ${entryPath}`, entryPath);
314
309
  }
315
310
  return { handle, token: statToken(held) };
@@ -327,13 +322,13 @@ async function openSecuredFile(absolute, containmentRoot, entryPath, initial) {
327
322
  const before = await lstat(absolute, { bigint: true });
328
323
  if (before.isSymbolicLink() || !before.isFile()) throw refusal("AUTH_REVIEW_WORKTREE_ENTRY_INVALID", `worktree authority entry is not a regular non-symlink file: ${entryPath}`, entryPath);
329
324
  const beforeReal = await realpath(absolute);
330
- if (containmentRoot !== null && !contained(containmentRoot, beforeReal)) throw refusal("AUTH_REVIEW_WORKTREE_ENTRY_INVALID", `worktree authority file escapes .nimi/spec: ${entryPath}`, entryPath);
325
+ if (containmentRoot !== null && !isPathWithin(containmentRoot, beforeReal)) throw refusal("AUTH_REVIEW_WORKTREE_ENTRY_INVALID", `worktree authority file escapes .nimi/spec: ${entryPath}`, entryPath);
331
326
  const handle = await open(absolute, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
332
327
  try {
333
328
  const held = await handle.stat({ bigint: true });
334
329
  const current = await lstat(absolute, { bigint: true });
335
330
  const currentReal = await realpath(absolute);
336
- if (!held.isFile() || !sameObject(before, held) || !sameObject(held, current) || (containmentRoot !== null && !contained(containmentRoot, currentReal))) {
331
+ if (!held.isFile() || !sameObject(before, held) || !sameObject(held, current) || (containmentRoot !== null && !isPathWithin(containmentRoot, currentReal))) {
337
332
  throw refusal("AUTH_REVIEW_CAPTURE_CHANGED", `worktree authority file changed before capture: ${entryPath}`, entryPath);
338
333
  }
339
334
  return { handle, token: statToken(held) };
@@ -371,7 +366,7 @@ async function readSecuredFile(secured, absolute, containmentRoot, entryPath, in
371
366
  const heldBefore = await secured.handle.stat({ bigint: true });
372
367
  const currentBefore = await lstat(absolute, { bigint: true });
373
368
  const currentReal = await realpath(absolute);
374
- if (!heldBefore.isFile() || statToken(heldBefore) !== secured.token || !sameObject(heldBefore, currentBefore) || (containmentRoot !== null && !contained(containmentRoot, currentReal))) {
369
+ if (!heldBefore.isFile() || statToken(heldBefore) !== secured.token || !sameObject(heldBefore, currentBefore) || (containmentRoot !== null && !isPathWithin(containmentRoot, currentReal))) {
375
370
  throw refusal("AUTH_REVIEW_CAPTURE_CHANGED", `worktree authority file changed before bytes were captured: ${entryPath}`, entryPath);
376
371
  }
377
372
  if (budget !== null) budget.reserve(heldBefore.size, entryPath);
@@ -465,7 +460,7 @@ async function scanWorktree(repository, initial, { retainHandles = false, budget
465
460
  const current = await lstat(absolute, { bigint: true });
466
461
  const currentReal = await realpath(absolute);
467
462
  const held = await secured.handle.stat({ bigint: true });
468
- if (statToken(held) !== secured.token || !sameObject(held, current) || !contained(specReal, currentReal)) {
463
+ if (statToken(held) !== secured.token || !sameObject(held, current) || !isPathWithin(specReal, currentReal)) {
469
464
  throw refusal("AUTH_REVIEW_CAPTURE_CHANGED", `worktree authority directory changed during traversal: ${entryPath}`, entryPath);
470
465
  }
471
466
  } finally {
@@ -497,7 +492,7 @@ async function verifyHeldEntries(heldEntries) {
497
492
  const held = await entry.handle.stat({ bigint: true });
498
493
  const current = await lstat(entry.absolute, { bigint: true });
499
494
  const currentReal = await realpath(entry.absolute);
500
- if (!held.isDirectory() || statToken(held) !== entry.token || !sameObject(held, current) || !contained(entry.containmentRoot, currentReal)) {
495
+ if (!held.isDirectory() || statToken(held) !== entry.token || !sameObject(held, current) || !isPathWithin(entry.containmentRoot, currentReal)) {
501
496
  throw refusal("AUTH_REVIEW_CAPTURE_CHANGED", `worktree authority directory changed before capture commit: ${entry.path}`, entry.path);
502
497
  }
503
498
  } catch (error) {
@@ -594,7 +589,7 @@ function stableRegularFileBudget(maxBytes, label) {
594
589
  return {
595
590
  reserve(size) {
596
591
  if (size > BigInt(maxBytes)) {
597
- throw refusal("AUTH_EVIDENCE_INPUT_BUDGET", `evidence input requires ${size} bytes but maxInputBytes is ${maxBytes}: ${label}`, label);
592
+ throw refusal("AUTH_REVIEW_INPUT_BUDGET", `review input requires ${size} bytes but maxBytes is ${maxBytes}: ${label}`, label);
598
593
  }
599
594
  },
600
595
  };
@@ -618,204 +613,6 @@ export async function captureStableRegularFile(file, label, containmentRoot = nu
618
613
  }
619
614
  }
620
615
 
621
- function evidenceRefusal(code, reason, file = ".") {
622
- return refusal(code, reason, file);
623
- }
624
-
625
- function mapEvidenceRefusal(error, fallbackCode, fallbackFile = ".") {
626
- if (!(error instanceof AuthorityReviewRefusal)) return error;
627
- if (error.code === "AUTH_REVIEW_CAPTURE_CHANGED" || error.code === "AUTH_EVIDENCE_CAPTURE_CHANGED") {
628
- return evidenceRefusal("AUTH_EVIDENCE_CAPTURE_CHANGED", error.message, error.file ?? fallbackFile);
629
- }
630
- if (error.code.startsWith("AUTH_EVIDENCE_")) return error;
631
- return evidenceRefusal(fallbackCode, error.message, error.file ?? fallbackFile);
632
- }
633
-
634
- function validEvidencePath(value) {
635
- return typeof value === "string"
636
- && validateRelativePath(value)
637
- && value === value.normalize("NFC")
638
- && !value.includes("\0")
639
- && !value.includes("\n")
640
- && !value.includes("\r")
641
- && value.split("/").every((part) => part.toLowerCase() !== ".git");
642
- }
643
-
644
- function evidenceInputBudget(maxInputBytes) {
645
- if (!Number.isSafeInteger(maxInputBytes) || maxInputBytes <= 0) {
646
- throw evidenceRefusal("AUTH_EVIDENCE_INPUT_BUDGET", "maxInputBytes must be one positive safe integer");
647
- }
648
- let captured = 0;
649
- return {
650
- reserve(size, entryPath) {
651
- if (size < 0n || size > BigInt(Number.MAX_SAFE_INTEGER)) {
652
- throw evidenceRefusal("AUTH_EVIDENCE_INPUT_BUDGET", `evidence input is too large to capture exactly: ${entryPath}`, entryPath);
653
- }
654
- const required = BigInt(captured) + size;
655
- if (required > BigInt(maxInputBytes)) {
656
- throw evidenceRefusal(
657
- "AUTH_EVIDENCE_INPUT_BUDGET",
658
- `complete evidence snapshot requires at least ${required} input bytes but maxInputBytes is ${maxInputBytes}`,
659
- entryPath,
660
- );
661
- }
662
- captured = Number(required);
663
- },
664
- get captured() {
665
- return captured;
666
- },
667
- };
668
- }
669
-
670
- async function trackedRegularIndexRecord(repository, bindingPath) {
671
- const result = await runGit(repository, ["ls-files", "--stage", "-z", "--error-unmatch", "--", bindingPath]);
672
- if (result.signal !== null) throw new Error(`Git process terminated by signal ${result.signal}`);
673
- if (result.status !== 0) {
674
- throw evidenceRefusal("AUTH_EVIDENCE_BINDING_INVALID", "evidence binding must be one tracked regular repository path", bindingPath);
675
- }
676
- const records = splitNullRecords(result.stdout);
677
- if (records.length !== 1) {
678
- throw evidenceRefusal("AUTH_EVIDENCE_BINDING_INVALID", "evidence binding must resolve to exactly one non-conflicted index entry", bindingPath);
679
- }
680
- const tab = records[0].indexOf(9);
681
- if (tab <= 0 || tab === records[0].length - 1) {
682
- throw evidenceRefusal("AUTH_EVIDENCE_BINDING_INVALID", "tracked evidence binding index entry is malformed", bindingPath);
683
- }
684
- const metadata = decodeUtf8(records[0].subarray(0, tab), "tracked evidence binding metadata", "AUTH_EVIDENCE_BINDING_INVALID");
685
- const indexedPath = decodeUtf8(records[0].subarray(tab + 1), "tracked evidence binding path", "AUTH_EVIDENCE_BINDING_INVALID");
686
- if (!/^(?:100644|100755) (?:[0-9a-f]{40}|[0-9a-f]{64}) 0$/.test(metadata) || indexedPath !== bindingPath) {
687
- throw evidenceRefusal("AUTH_EVIDENCE_BINDING_INVALID", "evidence binding must be one exact stage-zero regular Git entry", bindingPath);
688
- }
689
- return Buffer.from(records[0]);
690
- }
691
-
692
- async function captureEvidenceInput(repository, descriptor, budget, retainHandles) {
693
- const heldEntries = [];
694
- const pathTokens = [];
695
- const parts = descriptor.path.split("/");
696
- let absolute = repository;
697
- let retained = false;
698
-
699
- async function retainDirectory(directory, entryPath) {
700
- const secured = await securedDirectory(directory, repository, entryPath, true);
701
- pathTokens.push({ path: entryPath, token: secured.token });
702
- if (retainHandles) heldEntries.push({ path: entryPath, type: "directory", absolute: directory, containmentRoot: repository, ...secured });
703
- else await secured.handle.close();
704
- }
705
-
706
- function missing(missingAbsolute, missingPath) {
707
- if (!descriptor.allowMissing) {
708
- throw evidenceRefusal(descriptor.invalidCode, `${descriptor.role} input is missing: ${descriptor.path}`, descriptor.path);
709
- }
710
- retained = retainHandles;
711
- return {
712
- role: descriptor.role,
713
- path: descriptor.path,
714
- type: "missing",
715
- bytes: null,
716
- token: null,
717
- pathTokens,
718
- heldEntries,
719
- missingAbsolute,
720
- missingPath,
721
- };
722
- }
723
-
724
- try {
725
- await retainDirectory(repository, ".");
726
- for (let index = 0; index < parts.length - 1; index += 1) {
727
- absolute = path.join(absolute, parts[index]);
728
- const entryPath = parts.slice(0, index + 1).join("/");
729
- try {
730
- const info = await lstat(absolute, { bigint: true });
731
- if (info.isSymbolicLink() || !info.isDirectory()) {
732
- throw evidenceRefusal(descriptor.invalidCode, `${descriptor.role} ancestor is not one regular non-symlink directory: ${entryPath}`, entryPath);
733
- }
734
- } catch (error) {
735
- if (error?.code === "ENOENT") return missing(absolute, entryPath);
736
- throw error;
737
- }
738
- await retainDirectory(absolute, entryPath);
739
- }
740
-
741
- const target = path.join(repository, ...parts);
742
- try {
743
- const info = await lstat(target, { bigint: true });
744
- if (info.isSymbolicLink() || !info.isFile()) {
745
- throw evidenceRefusal(descriptor.invalidCode, `${descriptor.role} input is not one regular non-symlink file: ${descriptor.path}`, descriptor.path);
746
- }
747
- } catch (error) {
748
- if (error?.code === "ENOENT") return missing(target, descriptor.path);
749
- throw error;
750
- }
751
- const captured = await securedFile(target, repository, descriptor.path, true, retainHandles, budget);
752
- if (retainHandles) heldEntries.push({ path: descriptor.path, type: "regular-file", absolute: target, containmentRoot: repository, ...captured });
753
- retained = retainHandles;
754
- return {
755
- role: descriptor.role,
756
- path: descriptor.path,
757
- type: "regular-file",
758
- bytes: captured.bytes,
759
- token: captured.token,
760
- pathTokens,
761
- heldEntries,
762
- missingAbsolute: null,
763
- missingPath: null,
764
- };
765
- } catch (error) {
766
- throw mapEvidenceRefusal(error, descriptor.invalidCode, descriptor.path);
767
- } finally {
768
- if (!retained) await closeHeldEntries(heldEntries);
769
- }
770
- }
771
-
772
- function sameEvidenceInput(left, right) {
773
- if (left.role !== right.role
774
- || left.path !== right.path
775
- || left.type !== right.type
776
- || left.token !== right.token
777
- || left.pathTokens.length !== right.pathTokens.length) return false;
778
- for (let index = 0; index < left.pathTokens.length; index += 1) {
779
- if (left.pathTokens[index].path !== right.pathTokens[index].path
780
- || left.pathTokens[index].token !== right.pathTokens[index].token) return false;
781
- }
782
- return left.type === "missing" || left.bytes.equals(right.bytes);
783
- }
784
-
785
- async function verifyEvidenceInput(entry) {
786
- try {
787
- await verifyHeldEntries(entry.heldEntries);
788
- } catch (error) {
789
- throw evidenceRefusal("AUTH_EVIDENCE_CAPTURE_CHANGED", `${entry.role} changed before evidence capture commit: ${entry.path}; ${error.message}`, entry.path);
790
- }
791
- if (entry.type !== "missing") return;
792
- try {
793
- await lstat(entry.missingAbsolute, { bigint: true });
794
- throw evidenceRefusal("AUTH_EVIDENCE_CAPTURE_CHANGED", `missing locator appeared during capture: ${entry.path}`, entry.path);
795
- } catch (error) {
796
- if (error?.code === "ENOENT") return;
797
- throw mapEvidenceRefusal(error, "AUTH_EVIDENCE_CAPTURE_CHANGED", entry.path);
798
- }
799
- }
800
-
801
- function semanticEvidenceInput(entry) {
802
- return {
803
- role: entry.role,
804
- path: entry.path,
805
- type: entry.type,
806
- bytes: entry.bytes,
807
- };
808
- }
809
-
810
- function evidenceInputMetadata(entries) {
811
- return entries.map((entry) => ({
812
- role: entry.role,
813
- path: entry.path,
814
- type: entry.type,
815
- bytes: entry.bytes?.length ?? 0,
816
- }));
817
- }
818
-
819
616
  async function createTemporaryRoot(protectedRoots, prefix = "nimicoding-authority-review-") {
820
617
  let base;
821
618
  try {
@@ -825,14 +622,14 @@ async function createTemporaryRoot(protectedRoots, prefix = "nimicoding-authorit
825
622
  } catch {
826
623
  throw refusal("AUTH_REVIEW_TEMPORARY_INVALID", "system temporary directory is not one readable physical directory");
827
624
  }
828
- if (protectedRoots.some((protectedRoot) => contained(protectedRoot, base))) {
625
+ if (protectedRoots.some((protectedRoot) => isPathWithin(protectedRoot, base))) {
829
626
  throw refusal("AUTH_REVIEW_TEMPORARY_INVALID", "system temporary directory must be outside the reviewed worktree and Git administration directories");
830
627
  }
831
628
  const created = await mkdtemp(path.join(base, prefix));
832
629
  try {
833
630
  const temporaryRoot = await realpath(created);
834
631
  const info = await lstat(temporaryRoot);
835
- if (!info.isDirectory() || protectedRoots.some((protectedRoot) => contained(protectedRoot, temporaryRoot) || contained(temporaryRoot, protectedRoot))) {
632
+ if (!info.isDirectory() || protectedRoots.some((protectedRoot) => isPathWithin(protectedRoot, temporaryRoot) || isPathWithin(temporaryRoot, protectedRoot))) {
836
633
  throw refusal("AUTH_REVIEW_TEMPORARY_INVALID", "authority snapshots require one isolated temporary directory outside the reviewed repository");
837
634
  }
838
635
  return temporaryRoot;
@@ -842,188 +639,46 @@ async function createTemporaryRoot(protectedRoots, prefix = "nimicoding-authorit
842
639
  }
843
640
  }
844
641
 
845
- function evidenceDescriptors(bindingPath, probeResultsPath, locatorPaths) {
846
- if (!validEvidencePath(bindingPath) || !bindingPath.startsWith(".nimi/config/")) {
847
- throw evidenceRefusal("AUTH_EVIDENCE_BINDING_INVALID", "bindingPath must be one repository-relative path nested under .nimi/config", String(bindingPath ?? "."));
848
- }
849
- if (probeResultsPath !== null && probeResultsPath !== undefined && !validEvidencePath(probeResultsPath)) {
850
- throw evidenceRefusal("AUTH_EVIDENCE_INPUT_INVALID", "probeResultsPath must be one safe repository-relative regular-file path", String(probeResultsPath));
851
- }
852
- if (!Array.isArray(locatorPaths) || locatorPaths.some((entry) => !validEvidencePath(entry))) {
853
- throw evidenceRefusal("AUTH_EVIDENCE_LOCATOR_INVALID", "locatorPaths must be one list of safe repository-relative paths");
854
- }
855
- const orderedLocators = [...locatorPaths].sort(compareText);
856
- if (new Set(orderedLocators.map((entry) => entry.toLowerCase())).size !== orderedLocators.length) {
857
- throw evidenceRefusal("AUTH_EVIDENCE_LOCATOR_INVALID", "locatorPaths must not contain duplicate or portable-colliding paths");
858
- }
859
- const allPaths = [bindingPath, ...(probeResultsPath === null || probeResultsPath === undefined ? [] : [probeResultsPath]), ...orderedLocators];
860
- if (new Set(allPaths.map((entry) => entry.toLowerCase())).size !== allPaths.length) {
861
- throw evidenceRefusal("AUTH_EVIDENCE_INPUT_INVALID", "binding, probe results, and locator paths must be logically distinct without portable collisions");
862
- }
863
- return [
864
- { role: "binding", path: bindingPath, allowMissing: false, invalidCode: "AUTH_EVIDENCE_BINDING_INVALID" },
865
- ...(probeResultsPath === null || probeResultsPath === undefined ? [] : [{ role: "probe-results", path: probeResultsPath, allowMissing: false, invalidCode: "AUTH_EVIDENCE_INPUT_INVALID" }]),
866
- ...orderedLocators.map((entryPath) => ({ role: "locator", path: entryPath, allowMissing: true, invalidCode: "AUTH_EVIDENCE_LOCATOR_INVALID" })),
867
- ];
868
- }
869
-
870
- export async function withGitEvidenceSnapshot({
871
- repositoryPath,
872
- bindingPath,
873
- probeResultsPath = null,
874
- locatorPaths,
875
- maxInputBytes,
876
- hooks = null,
877
- }, callback) {
878
- if (typeof callback !== "function") throw evidenceRefusal("AUTH_EVIDENCE_INPUT_INVALID", "evidence snapshot requires one callback");
879
- const budget = evidenceInputBudget(maxInputBytes);
880
- const descriptors = evidenceDescriptors(bindingPath, probeResultsPath, locatorPaths);
881
- let resolved;
882
- try {
883
- resolved = await resolveRepository(repositoryPath);
884
- } catch (error) {
885
- throw mapEvidenceRefusal(error, "AUTH_EVIDENCE_REPOSITORY_INVALID");
886
- }
887
- const { repository, protectedRoots } = resolved;
888
- let headOid;
889
- try {
890
- headOid = await resolveBaseOid(repository, "HEAD");
891
- } catch (error) {
892
- throw mapEvidenceRefusal(error, "AUTH_EVIDENCE_HEAD_INVALID");
893
- }
894
- if (hooks?.afterHeadResolved) await hooks.afterHeadResolved({ repository, headOid });
895
-
896
- let trackedBinding;
897
- try {
898
- trackedBinding = await trackedRegularIndexRecord(repository, bindingPath);
899
- } catch (error) {
900
- throw mapEvidenceRefusal(error, "AUTH_EVIDENCE_BINDING_INVALID", bindingPath);
901
- }
902
-
903
- let authorityEntries;
904
- let semanticInputs;
905
- let authorityHeld = [];
906
- const capturedInputs = [];
907
- try {
908
- let firstAuthority;
909
- try {
910
- const captured = await scanWorktree(repository, true, { retainHandles: true, budget });
911
- firstAuthority = captured.entries;
912
- authorityHeld = captured.heldEntries;
913
- } catch (error) {
914
- throw mapEvidenceRefusal(error, "AUTH_EVIDENCE_AUTHORITY_INVALID", SPEC_ROOT);
915
- }
916
- if (hooks?.afterAuthorityCapture) {
917
- await hooks.afterAuthorityCapture({
918
- repository,
919
- headOid,
920
- authority: semanticEntries(firstAuthority).map((entry) => ({ path: entry.path, type: entry.type, bytes: entry.bytes?.length ?? 0 })),
921
- });
922
- }
923
- for (const descriptor of descriptors) capturedInputs.push(await captureEvidenceInput(repository, descriptor, budget, true));
924
- if (hooks?.afterInitialCapture) {
925
- await hooks.afterInitialCapture({ repository, headOid, inputs: evidenceInputMetadata(capturedInputs) });
926
- }
927
-
928
- const recaptureBudget = evidenceInputBudget(maxInputBytes);
929
- let secondAuthority;
930
- try {
931
- secondAuthority = (await scanWorktree(repository, false, { budget: recaptureBudget })).entries;
932
- } catch (error) {
933
- throw evidenceRefusal("AUTH_EVIDENCE_CAPTURE_CHANGED", `authority inventory changed during evidence capture: ${error.message}`, SPEC_ROOT);
934
- }
935
- if (!compareCapturedEntries(firstAuthority, secondAuthority, true)) {
936
- throw evidenceRefusal("AUTH_EVIDENCE_CAPTURE_CHANGED", "authority paths, types, metadata, or bytes changed during evidence capture", SPEC_ROOT);
937
- }
938
- for (let index = 0; index < descriptors.length; index += 1) {
939
- let second;
940
- try {
941
- second = await captureEvidenceInput(repository, descriptors[index], recaptureBudget, false);
942
- } catch (error) {
943
- throw evidenceRefusal("AUTH_EVIDENCE_CAPTURE_CHANGED", `${descriptors[index].role} input changed during evidence capture: ${descriptors[index].path}; ${error.message}`, descriptors[index].path);
944
- }
945
- if (!sameEvidenceInput(capturedInputs[index], second)) {
946
- throw evidenceRefusal("AUTH_EVIDENCE_CAPTURE_CHANGED", `${descriptors[index].role} path, type, metadata, or bytes changed during evidence capture: ${descriptors[index].path}`, descriptors[index].path);
947
- }
948
- }
949
- const secondTrackedBinding = await trackedRegularIndexRecord(repository, bindingPath).catch((error) => {
950
- throw evidenceRefusal("AUTH_EVIDENCE_CAPTURE_CHANGED", `evidence binding tracking state changed during capture: ${error.message}`, bindingPath);
951
- });
952
- if (!trackedBinding.equals(secondTrackedBinding)) {
953
- throw evidenceRefusal("AUTH_EVIDENCE_CAPTURE_CHANGED", "evidence binding index identity changed during capture", bindingPath);
954
- }
955
- if (hooks?.beforeCaptureCommit) {
956
- await hooks.beforeCaptureCommit({ repository, headOid, inputs: evidenceInputMetadata(capturedInputs) });
957
- }
958
- await verifyHeldEntries(authorityHeld).catch((error) => {
959
- throw evidenceRefusal("AUTH_EVIDENCE_CAPTURE_CHANGED", `authority changed before evidence capture commit: ${error.message}`, SPEC_ROOT);
960
- });
961
- for (const entry of capturedInputs) await verifyEvidenceInput(entry);
962
- const committedTrackedBinding = await trackedRegularIndexRecord(repository, bindingPath).catch((error) => {
963
- throw evidenceRefusal("AUTH_EVIDENCE_CAPTURE_CHANGED", `evidence binding tracking state changed before capture commit: ${error.message}`, bindingPath);
964
- });
965
- if (!trackedBinding.equals(committedTrackedBinding)) {
966
- throw evidenceRefusal("AUTH_EVIDENCE_CAPTURE_CHANGED", "evidence binding index identity changed before capture commit", bindingPath);
967
- }
968
- authorityEntries = semanticEntries(firstAuthority);
969
- semanticInputs = capturedInputs.map(semanticEvidenceInput);
970
- } finally {
971
- await closeHeldEntries(authorityHeld);
972
- await Promise.all(capturedInputs.map((entry) => closeHeldEntries(entry.heldEntries)));
973
- }
974
-
975
- let temporaryRoot;
976
- try {
977
- temporaryRoot = await createTemporaryRoot(protectedRoots, "nimicoding-authority-evidence-");
978
- } catch (error) {
979
- throw mapEvidenceRefusal(error, "AUTH_EVIDENCE_TEMPORARY_INVALID");
980
- }
981
- const authorityRoot = path.join(temporaryRoot, "authority");
982
- try {
983
- await mkdir(authorityRoot, { mode: 0o700 });
984
- try {
985
- await materializeEntries(authorityRoot, authorityEntries, "AUTH_EVIDENCE_AUTHORITY_INVALID");
986
- } catch (error) {
987
- throw mapEvidenceRefusal(error, "AUTH_EVIDENCE_AUTHORITY_INVALID", SPEC_ROOT);
988
- }
989
- const counts = snapshotCounts(authorityEntries);
990
- return await callback({
991
- repository,
992
- headOid,
993
- temporaryRoot,
994
- authority: {
995
- root: authorityRoot,
996
- contentIdentity: authoritySnapshotContentIdentity(authorityEntries),
997
- ...counts,
998
- },
999
- inputs: semanticInputs,
1000
- capturedInputBytes: budget.captured,
1001
- });
1002
- } finally {
1003
- await rm(temporaryRoot, { recursive: true, force: true });
1004
- }
1005
- }
1006
-
1007
642
  export async function listGitTrackedFiles(repositoryPath) {
1008
643
  const { repository } = await resolveRepository(repositoryPath);
1009
644
  const bytes = await checkedGit(
1010
645
  repository,
1011
- ["ls-files", "--cached", "--full-name", "-z"],
646
+ ["ls-files", "--stage", "--full-name", "-z"],
1012
647
  "AUTH_REVIEW_GIT_INDEX_INVALID",
1013
648
  "Git tracked-file inventory cannot be read",
1014
649
  );
1015
650
  const files = [];
651
+ const gitlinks = [];
1016
652
  let offset = 0;
1017
653
  while (offset < bytes.length) {
1018
654
  const end = bytes.indexOf(0, offset);
1019
655
  if (end < 0) throw refusal("AUTH_REVIEW_GIT_INDEX_INVALID", "Git tracked-file inventory is not NUL-terminated");
1020
656
  if (end === offset) throw refusal("AUTH_REVIEW_GIT_INDEX_INVALID", "Git tracked-file inventory contains an empty path");
1021
- const file = decodeUtf8(bytes.subarray(offset, end), "Git tracked-file path", "AUTH_REVIEW_GIT_INDEX_INVALID");
657
+ const record = bytes.subarray(offset, end);
658
+ const tab = record.indexOf(9);
659
+ if (tab <= 0 || tab === record.length - 1) {
660
+ throw refusal("AUTH_REVIEW_GIT_INDEX_INVALID", "Git tracked-file inventory contains a malformed index entry");
661
+ }
662
+ const metadata = decodeUtf8(
663
+ record.subarray(0, tab),
664
+ "Git tracked-file metadata",
665
+ "AUTH_REVIEW_GIT_INDEX_INVALID",
666
+ );
667
+ const match = /^(100644|100755|120000|160000) (?:[0-9a-f]{40}|[0-9a-f]{64}) ([0-3])$/.exec(metadata);
668
+ if (match === null) {
669
+ throw refusal("AUTH_REVIEW_GIT_INDEX_INVALID", "Git tracked-file inventory contains an unsupported index entry");
670
+ }
671
+ const file = decodeUtf8(record.subarray(tab + 1), "Git tracked-file path", "AUTH_REVIEW_GIT_INDEX_INVALID");
1022
672
  if (!validateRelativePath(file)) throw refusal("AUTH_REVIEW_GIT_INDEX_INVALID", "Git tracked-file inventory contains an unsafe path");
1023
673
  files.push(file);
674
+ if (match[1] === "160000") gitlinks.push(file);
1024
675
  offset = end + 1;
1025
676
  }
1026
- return { repository, files: [...new Set(files)].sort(compareText) };
677
+ return {
678
+ repository,
679
+ files: [...new Set(files)].sort(compareText),
680
+ gitlinks: [...new Set(gitlinks)].sort(compareText),
681
+ };
1027
682
  }
1028
683
 
1029
684
  export async function withGitAuthoritySnapshots({ repositoryPath, baseRef, hooks = null }, callback) {
@@ -164,14 +164,14 @@ function finish(compiled, graph, location, root, traversal) {
164
164
  compiled,
165
165
  location,
166
166
  root.unit.id,
167
- `complete graph proof requires ${traversal.units} units but max-units is ${maxUnits}`,
167
+ `complete graph traversal requires ${traversal.units} units but max-units is ${maxUnits}`,
168
168
  root.idMap,
169
169
  );
170
170
  if (traversal.edges > maxEdges) return budgetFailure(
171
171
  compiled,
172
172
  location,
173
173
  root.unit.id,
174
- `complete graph proof requires ${traversal.edges} authored edges but max-edges is ${maxEdges}`,
174
+ `complete graph traversal requires ${traversal.edges} authored edges but max-edges is ${maxEdges}`,
175
175
  root.idMap,
176
176
  );
177
177
  const graphBytes = Buffer.byteLength(JSON.stringify(graph), "utf8");
@@ -245,7 +245,7 @@ export async function pathAuthorityPath(inputPath, fromId, toId, { traversal: tr
245
245
  const { compiled, location, snapshot } = admitted;
246
246
  const root = snapshot.byId.get(fromId);
247
247
  const visitedDepth = new Map([[fromId, 0]]);
248
- const proofEdges = new Set();
248
+ const traversedEdges = new Set();
249
249
  let layer = new Map([[fromId, []]]);
250
250
  let witness = fromId === toId ? [] : null;
251
251
  let exhausted = false;
@@ -255,7 +255,7 @@ export async function pathAuthorityPath(inputPath, fromId, toId, { traversal: tr
255
255
  for (const id of [...layer.keys()].sort(compareText)) {
256
256
  const prefix = layer.get(id);
257
257
  for (const value of pathAdjacency(snapshot, id, traversalMode)) {
258
- proofEdges.add(edgeKey(value.edge));
258
+ traversedEdges.add(edgeKey(value.edge));
259
259
  const priorDepth = visitedDepth.get(value.next);
260
260
  if (priorDepth !== undefined && priorDepth < depth + 1) continue;
261
261
  const candidate = [...prefix, { source: value.edge.source, type: value.edge.type, target: value.edge.target, traversal: value.traversal }];
@@ -273,7 +273,7 @@ export async function pathAuthorityPath(inputPath, fromId, toId, { traversal: tr
273
273
  let unchecked = false;
274
274
  for (const id of [...layer.keys()].sort(compareText)) {
275
275
  for (const value of pathAdjacency(snapshot, id, traversalMode)) {
276
- proofEdges.add(edgeKey(value.edge));
276
+ traversedEdges.add(edgeKey(value.edge));
277
277
  if (!visitedDepth.has(value.next)) unchecked = true;
278
278
  }
279
279
  }
@@ -293,7 +293,7 @@ export async function pathAuthorityPath(inputPath, fromId, toId, { traversal: tr
293
293
  ? [fromId, ...(witness ?? []).map((step) => step.traversal === "forward" ? step.target : step.source)]
294
294
  : [fromId, toId];
295
295
  const nodes = [...new Set(pathNodeIds)].map((id) => snapshot.byId.get(id).node);
296
- const traversal = { units: new Set([...visitedDepth.keys(), toId]).size, edges: proofEdges.size };
296
+ const traversal = { units: new Set([...visitedDepth.keys(), toId]).size, edges: traversedEdges.size };
297
297
  const budgets = { maxHops, maxUnits, maxEdges, maxBytes };
298
298
  const graph = graphBase("path", { fromId, toId, traversal: traversalMode, relations }, nodes, edges, {
299
299
  edgeRepresentation: "canonical_authored_source_to_target",
@@ -315,13 +315,13 @@ export async function subgraphAuthorityPath(inputPath, id, { direction, relation
315
315
  const { compiled, location, snapshot } = admitted;
316
316
  const root = snapshot.byId.get(id);
317
317
  const distances = new Map([[id, 0]]);
318
- const proofEdges = new Set();
318
+ const traversedEdges = new Set();
319
319
  let layer = [id];
320
320
  for (let currentDepth = 0; currentDepth < depth && layer.length > 0; currentDepth += 1) {
321
321
  const next = new Set();
322
322
  for (const current of [...layer].sort(compareText)) {
323
323
  for (const value of adjacent(snapshot, current, direction)) {
324
- proofEdges.add(edgeKey(value.edge));
324
+ traversedEdges.add(edgeKey(value.edge));
325
325
  if (!distances.has(value.next)) {
326
326
  distances.set(value.next, currentDepth + 1);
327
327
  next.add(value.next);
@@ -332,9 +332,9 @@ export async function subgraphAuthorityPath(inputPath, id, { direction, relation
332
332
  }
333
333
  const nodeIds = new Set(distances.keys());
334
334
  const edges = snapshot.edges.filter((edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target));
335
- for (const edge of edges) proofEdges.add(edgeKey(edge));
335
+ for (const edge of edges) traversedEdges.add(edgeKey(edge));
336
336
  const nodes = [...nodeIds].map((nodeId) => snapshot.byId.get(nodeId).node);
337
- const traversal = { units: nodes.length, edges: proofEdges.size };
337
+ const traversal = { units: nodes.length, edges: traversedEdges.size };
338
338
  const budgets = { depth, maxUnits, maxEdges, maxBytes };
339
339
  const graph = graphBase("subgraph", { id, direction, relations, depth }, nodes, edges, {
340
340
  edgeRepresentation: "canonical_authored_source_to_target",