kibi-cli 2.2.0 → 2.3.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 (183) hide show
  1. package/dist/cli-operation-loader.d.ts.map +1 -1
  2. package/dist/cli-operation-loader.js +3 -0
  3. package/dist/cli-operation-metadata.d.ts +4 -0
  4. package/dist/cli-operation-metadata.d.ts.map +1 -1
  5. package/dist/cli-operation-metadata.js +5 -0
  6. package/dist/cli-register-reporting.d.ts.map +1 -1
  7. package/dist/cli-register-reporting.js +4 -0
  8. package/dist/commands/check-diff.d.ts +5 -0
  9. package/dist/commands/check-diff.d.ts.map +1 -0
  10. package/dist/commands/check-diff.js +62 -0
  11. package/dist/commands/check-generated.d.ts.map +1 -1
  12. package/dist/commands/check-generated.js +41 -18
  13. package/dist/commands/check.d.ts.map +1 -1
  14. package/dist/commands/check.js +210 -73
  15. package/dist/commands/doctor.d.ts.map +1 -1
  16. package/dist/commands/doctor.js +63 -29
  17. package/dist/commands/github-init.d.ts.map +1 -1
  18. package/dist/commands/github-init.js +9 -2
  19. package/dist/commands/sync/manifest.d.ts.map +1 -1
  20. package/dist/commands/sync/manifest.js +3 -1
  21. package/dist/engine.d.ts +15 -1
  22. package/dist/engine.d.ts.map +1 -1
  23. package/dist/engine.js +252 -52
  24. package/dist/extractors/symbols-coordinator.d.ts +7 -0
  25. package/dist/extractors/symbols-coordinator.d.ts.map +1 -1
  26. package/dist/extractors/symbols-coordinator.js +75 -39
  27. package/dist/operations/modeling/predicate-inference-1.js +1 -1
  28. package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
  29. package/dist/operations/mutation/source-authoring.js +175 -53
  30. package/dist/operations/mutation/upsert.d.ts.map +1 -1
  31. package/dist/operations/mutation/upsert.js +5 -7
  32. package/dist/operations/planning/apply-plan.d.ts.map +1 -1
  33. package/dist/operations/planning/apply-plan.js +5 -9
  34. package/dist/operations/proof/ingest-proof.d.ts.map +1 -1
  35. package/dist/operations/proof/ingest-proof.js +1 -0
  36. package/dist/operations/semantic-advisor/analysis-receipt.d.ts.map +1 -1
  37. package/dist/operations/semantic-advisor/analysis-receipt.js +7 -1
  38. package/dist/operations/semantic-advisor/analyze-prose.d.ts.map +1 -1
  39. package/dist/operations/semantic-advisor/analyze-prose.js +26 -4
  40. package/dist/operations/semantic-advisor/clauses.d.ts +1 -0
  41. package/dist/operations/semantic-advisor/clauses.d.ts.map +1 -1
  42. package/dist/operations/semantic-advisor/clauses.js +7 -1
  43. package/dist/performance-trace.d.ts +17 -0
  44. package/dist/performance-trace.d.ts.map +1 -0
  45. package/dist/performance-trace.js +134 -0
  46. package/dist/plugins/approved-source-analyzers.d.ts +13 -0
  47. package/dist/plugins/approved-source-analyzers.d.ts.map +1 -0
  48. package/dist/plugins/approved-source-analyzers.generated.d.ts +71 -0
  49. package/dist/plugins/approved-source-analyzers.generated.d.ts.map +1 -0
  50. package/dist/plugins/approved-source-analyzers.generated.js +77 -0
  51. package/dist/plugins/approved-source-analyzers.js +3 -0
  52. package/dist/plugins/maintenance-source-analysis.d.ts +14 -0
  53. package/dist/plugins/maintenance-source-analysis.d.ts.map +1 -0
  54. package/dist/plugins/maintenance-source-analysis.js +191 -0
  55. package/dist/plugins/registry.d.ts +2 -1
  56. package/dist/plugins/registry.d.ts.map +1 -1
  57. package/dist/plugins/registry.js +6 -1
  58. package/dist/plugins/source-analysis-service.d.ts +36 -1
  59. package/dist/plugins/source-analysis-service.d.ts.map +1 -1
  60. package/dist/plugins/source-analysis-service.js +184 -27
  61. package/dist/plugins/source-change-analysis.d.ts +15 -0
  62. package/dist/plugins/source-change-analysis.d.ts.map +1 -0
  63. package/dist/plugins/source-change-analysis.js +54 -0
  64. package/dist/plugins/source-classification.d.ts +41 -0
  65. package/dist/plugins/source-classification.d.ts.map +1 -0
  66. package/dist/plugins/source-classification.js +490 -0
  67. package/dist/prolog/swipl-resolver.d.ts +88 -0
  68. package/dist/prolog/swipl-resolver.d.ts.map +1 -0
  69. package/dist/prolog/swipl-resolver.js +446 -0
  70. package/dist/prolog.d.ts +15 -1
  71. package/dist/prolog.d.ts.map +1 -1
  72. package/dist/prolog.js +67 -6
  73. package/dist/public/impact/analyzer.d.ts.map +1 -1
  74. package/dist/public/impact/analyzer.js +56 -5
  75. package/dist/public/impact/diagnostics.d.ts.map +1 -1
  76. package/dist/public/impact/diagnostics.js +3 -1
  77. package/dist/public/impact/manifest.d.ts.map +1 -1
  78. package/dist/public/impact/manifest.js +15 -1
  79. package/dist/public/impact/source-changes.d.ts.map +1 -1
  80. package/dist/public/impact/source-changes.js +35 -27
  81. package/dist/public/impact/types.d.ts +2 -0
  82. package/dist/public/impact/types.d.ts.map +1 -1
  83. package/dist/public/operations/catalog.d.ts +44 -0
  84. package/dist/public/operations/catalog.d.ts.map +1 -1
  85. package/dist/public/operations/catalog.js +2 -0
  86. package/dist/public/operations/contracts.d.ts.map +1 -1
  87. package/dist/public/operations/contracts.js +309 -8
  88. package/dist/public/operations/impact-review-preparation.d.ts +14 -0
  89. package/dist/public/operations/impact-review-preparation.d.ts.map +1 -0
  90. package/dist/public/operations/impact-review-preparation.js +379 -0
  91. package/dist/public/operations/index.d.ts +2 -0
  92. package/dist/public/operations/index.d.ts.map +1 -1
  93. package/dist/public/operations/index.js +2 -0
  94. package/dist/public/operations/node-ports.d.ts.map +1 -1
  95. package/dist/public/operations/node-ports.js +1 -35
  96. package/dist/public/operations/proof-receipt-projection.d.ts +5 -0
  97. package/dist/public/operations/proof-receipt-projection.d.ts.map +1 -0
  98. package/dist/public/operations/proof-receipt-projection.js +36 -0
  99. package/dist/public/operations/specs/impact-review-preparation.d.ts +47 -0
  100. package/dist/public/operations/specs/impact-review-preparation.d.ts.map +1 -0
  101. package/dist/public/operations/specs/impact-review-preparation.js +43 -0
  102. package/dist/public/operations/types.d.ts +1 -1
  103. package/dist/public/operations/types.d.ts.map +1 -1
  104. package/dist/public/skills/kibi-usage/resources/operation-access.md +1 -0
  105. package/dist/traceability/analysis-gate.d.ts +39 -0
  106. package/dist/traceability/analysis-gate.d.ts.map +1 -0
  107. package/dist/traceability/analysis-gate.js +51 -0
  108. package/dist/traceability/git-change-snapshot.d.ts +27 -0
  109. package/dist/traceability/git-change-snapshot.d.ts.map +1 -0
  110. package/dist/traceability/git-change-snapshot.js +411 -0
  111. package/dist/traceability/git-staged.d.ts +5 -1
  112. package/dist/traceability/git-staged.d.ts.map +1 -1
  113. package/dist/traceability/git-staged.js +16 -11
  114. package/dist/traceability/impact-evaluator.d.ts +54 -0
  115. package/dist/traceability/impact-evaluator.d.ts.map +1 -0
  116. package/dist/traceability/impact-evaluator.js +270 -0
  117. package/dist/traceability/impact-git-tree.d.ts +12 -0
  118. package/dist/traceability/impact-git-tree.d.ts.map +1 -0
  119. package/dist/traceability/impact-git-tree.js +46 -0
  120. package/dist/traceability/impact-knowledge.d.ts +16 -0
  121. package/dist/traceability/impact-knowledge.d.ts.map +1 -0
  122. package/dist/traceability/impact-knowledge.js +165 -0
  123. package/dist/traceability/impact-policy.d.ts +15 -0
  124. package/dist/traceability/impact-policy.d.ts.map +1 -0
  125. package/dist/traceability/impact-policy.js +48 -0
  126. package/dist/traceability/impact-policy.v1.schema.json +48 -0
  127. package/dist/traceability/impact-pr-diff.d.ts +5 -0
  128. package/dist/traceability/impact-pr-diff.d.ts.map +1 -0
  129. package/dist/traceability/impact-pr-diff.js +83 -0
  130. package/dist/traceability/impact-record-validation.d.ts +9 -0
  131. package/dist/traceability/impact-record-validation.d.ts.map +1 -0
  132. package/dist/traceability/impact-record-validation.js +113 -0
  133. package/dist/traceability/impact-review.d.ts +148 -0
  134. package/dist/traceability/impact-review.d.ts.map +1 -0
  135. package/dist/traceability/impact-review.js +494 -0
  136. package/dist/traceability/impact-review.schema.json +365 -0
  137. package/dist/traceability/impact-side-evidence.d.ts +13 -0
  138. package/dist/traceability/impact-side-evidence.d.ts.map +1 -0
  139. package/dist/traceability/impact-side-evidence.js +240 -0
  140. package/dist/traceability/reviewed-coordinate-refresh.d.ts +10 -0
  141. package/dist/traceability/reviewed-coordinate-refresh.d.ts.map +1 -0
  142. package/dist/traceability/reviewed-coordinate-refresh.js +49 -0
  143. package/dist/traceability/snapshot-knowledge.d.ts +4 -0
  144. package/dist/traceability/snapshot-knowledge.d.ts.map +1 -0
  145. package/dist/traceability/snapshot-knowledge.js +98 -0
  146. package/dist/traceability/staged-file-coverage.d.ts +23 -0
  147. package/dist/traceability/staged-file-coverage.d.ts.map +1 -1
  148. package/dist/traceability/staged-impact-contract.d.ts.map +1 -1
  149. package/dist/traceability/staged-impact-contract.js +2 -16
  150. package/dist/traceability/staged-symbols-manifest.d.ts +4 -0
  151. package/dist/traceability/staged-symbols-manifest.d.ts.map +1 -1
  152. package/dist/traceability/staged-symbols-manifest.js +17 -6
  153. package/dist/traceability/symbol-extract.d.ts +3 -5
  154. package/dist/traceability/symbol-extract.d.ts.map +1 -1
  155. package/dist/traceability/symbol-extract.js +49 -34
  156. package/dist/traceability/temp-kb.d.ts +1 -1
  157. package/dist/traceability/temp-kb.d.ts.map +1 -1
  158. package/dist/traceability/temp-kb.js +49 -13
  159. package/dist/traceability/trusted-pr-event.d.ts +13 -0
  160. package/dist/traceability/trusted-pr-event.d.ts.map +1 -0
  161. package/dist/traceability/trusted-pr-event.js +105 -0
  162. package/dist/utils/canonical-path.d.ts +3 -0
  163. package/dist/utils/canonical-path.d.ts.map +1 -0
  164. package/dist/utils/canonical-path.js +33 -0
  165. package/package.json +14 -5
  166. package/src/public/impact/analyzer.ts +88 -5
  167. package/src/public/impact/diagnostics.ts +7 -5
  168. package/src/public/impact/manifest.ts +14 -1
  169. package/src/public/impact/source-changes.ts +39 -34
  170. package/src/public/impact/types.ts +5 -0
  171. package/src/public/operations/catalog.ts +2 -0
  172. package/src/public/operations/contracts.ts +385 -9
  173. package/src/public/operations/impact-review-preparation.ts +548 -0
  174. package/src/public/operations/index.ts +2 -0
  175. package/src/public/operations/node-ports.ts +1 -39
  176. package/src/public/operations/proof-receipt-projection.ts +39 -0
  177. package/src/public/operations/specs/impact-review-preparation.ts +46 -0
  178. package/src/public/operations/types.ts +1 -0
  179. package/src/public/skills/kibi-usage/resources/operation-access.md +1 -0
  180. package/src/traceability/impact-policy.v1.schema.json +48 -0
  181. package/src/traceability/impact-review.schema.json +365 -0
  182. package/templates/github/kibi-badge.yml +0 -4
  183. package/templates/github/kibi-report.yml +0 -4
@@ -0,0 +1,39 @@
1
+ import type { HunkRange } from "./git-staged.js";
2
+ type LineRange = Readonly<{
3
+ startLine: number;
4
+ endLine: number;
5
+ }>;
6
+ export type GateAnalysis<Range extends LineRange> = Readonly<{
7
+ status: "ok" | "partial" | "unsupported" | "failed";
8
+ diagnosticCodes: readonly string[];
9
+ uncoveredRanges: readonly (Range & Readonly<{
10
+ reason: string;
11
+ }>)[];
12
+ }>;
13
+ /** What one side of a changed file needs before it can pass. */
14
+ type AnalysisObligation<Range extends LineRange> = Readonly<{
15
+ kind: "none";
16
+ }> | Readonly<{
17
+ kind: "blocked";
18
+ reason: string;
19
+ }> | Readonly<{
20
+ kind: "unsupported_review";
21
+ }> | Readonly<{
22
+ kind: "partial_review";
23
+ ranges: readonly Range[];
24
+ }>;
25
+ /**
26
+ * The single rule for when incomplete source analysis blocks a change.
27
+ *
28
+ * - The before side is historical and cannot be corrected by the author, so it
29
+ * is informational and never blocks or needs review.
30
+ * - On the after side, failed analysis and non-local gaps (syntax, parse,
31
+ * timeout, integrity) always block.
32
+ * - A local gap (a macro, decorator or other declared limitation) matters only
33
+ * where it overlaps the changed lines; elsewhere the file was already in that
34
+ * state and this change did not touch it.
35
+ * - Unsupported after-side analysis needs one whole-file review.
36
+ */
37
+ export declare function analysisObligation<Range extends LineRange>(side: "before" | "after", analysis: GateAnalysis<Range> | null | undefined, newHunkRanges: readonly HunkRange[]): AnalysisObligation<Range>;
38
+ export {};
39
+ //# sourceMappingURL=analysis-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analysis-gate.d.ts","sourceRoot":"","sources":["../../src/traceability/analysis-gate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAUjD,KAAK,SAAS,GAAG,QAAQ,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAGlE,MAAM,MAAM,YAAY,CAAC,KAAK,SAAS,SAAS,IAAI,QAAQ,CAAC;IAC3D,MAAM,EAAE,IAAI,GAAG,SAAS,GAAG,aAAa,GAAG,QAAQ,CAAC;IACpD,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,eAAe,EAAE,SAAS,CAAC,KAAK,GAAG,QAAQ,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,EAAE,CAAC;CACpE,CAAC,CAAC;AAEH,gEAAgE;AAChE,KAAK,kBAAkB,CAAC,KAAK,SAAS,SAAS,IAC3C,QAAQ,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GAC1B,QAAQ,CAAC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,GAC7C,QAAQ,CAAC;IAAE,IAAI,EAAE,oBAAoB,CAAA;CAAE,CAAC,GACxC,QAAQ,CAAC;IAAE,IAAI,EAAE,gBAAgB,CAAC;IAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAA;CAAE,CAAC,CAAC;AAuBnE;;;;;;;;;;;GAWG;AAEH,wBAAgB,kBAAkB,CAAC,KAAK,SAAS,SAAS,EACxD,IAAI,EAAE,QAAQ,GAAG,OAAO,EACxB,QAAQ,EAAE,YAAY,CAAC,KAAK,CAAC,GAAG,IAAI,GAAG,SAAS,EAChD,aAAa,EAAE,SAAS,SAAS,EAAE,GAClC,kBAAkB,CAAC,KAAK,CAAC,CAqB3B"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Diagnostics that make the whole parse unreliable rather than one region.
3
+ * A deleted brace can surface as a syntax error far from the edited lines, so
4
+ * these never narrow to the changed hunks and never accept a residual review.
5
+ */
6
+ const NON_LOCAL_ANALYSIS_MARKER = /(?:syntax|parse|timeout|timed[\s_-]*out|integrity|checksum|failed|failure|error|input[\s_-]*limit)/i;
7
+ function hasNonLocalAnalysisGap(analysis) {
8
+ return NON_LOCAL_ANALYSIS_MARKER.test([
9
+ ...analysis.diagnosticCodes,
10
+ ...analysis.uncoveredRanges.map((range) => range.reason),
11
+ ].join("\n"));
12
+ }
13
+ /** Uncovered ranges that overlap a changed line of the after side. */
14
+ function rangesTouchingHunks(ranges, hunks) {
15
+ return ranges.filter((range) => hunks.some((hunk) => range.startLine <= hunk.end && hunk.start <= range.endLine));
16
+ }
17
+ /**
18
+ * The single rule for when incomplete source analysis blocks a change.
19
+ *
20
+ * - The before side is historical and cannot be corrected by the author, so it
21
+ * is informational and never blocks or needs review.
22
+ * - On the after side, failed analysis and non-local gaps (syntax, parse,
23
+ * timeout, integrity) always block.
24
+ * - A local gap (a macro, decorator or other declared limitation) matters only
25
+ * where it overlaps the changed lines; elsewhere the file was already in that
26
+ * state and this change did not touch it.
27
+ * - Unsupported after-side analysis needs one whole-file review.
28
+ */
29
+ // implements REQ-source-analysis-v2
30
+ export function analysisObligation(side, analysis, newHunkRanges) {
31
+ if (side === "before" || !analysis || analysis.status === "ok")
32
+ return { kind: "none" };
33
+ if (analysis.status === "failed")
34
+ return { kind: "blocked", reason: "source analysis failed" };
35
+ if (analysis.status === "unsupported")
36
+ return { kind: "unsupported_review" };
37
+ if (analysis.uncoveredRanges.length === 0)
38
+ return {
39
+ kind: "blocked",
40
+ reason: "partial analysis did not report which ranges it left uncovered",
41
+ };
42
+ if (hasNonLocalAnalysisGap(analysis))
43
+ return {
44
+ kind: "blocked",
45
+ reason: "a parser, syntax, timeout or integrity diagnostic affects the whole file",
46
+ };
47
+ const ranges = rangesTouchingHunks(analysis.uncoveredRanges, newHunkRanges);
48
+ return ranges.length === 0
49
+ ? { kind: "none" }
50
+ : { kind: "partial_review", ranges };
51
+ }
@@ -0,0 +1,27 @@
1
+ import type { StagedPath } from "./git-staged.js";
2
+ export type SnapshotGitReader = (args: readonly string[]) => Buffer;
3
+ export interface GitChangeSnapshot {
4
+ /** Tree at the captured base commit, or the canonical empty tree when unborn. */
5
+ baseTree: string;
6
+ /** The single tree written from the captured index (staged snapshots). */
7
+ headTree: string;
8
+ /** Captured base commit, or null for an unborn repository. */
9
+ headCommit: string | null;
10
+ /** Run a Git command against immutable captured trees/blobs. */
11
+ readGit(args: readonly string[]): Buffer;
12
+ /** Read immutable object IDs in one process; no working-tree reads. */
13
+ readBlobs(oids: readonly string[]): ReadonlyMap<string, Buffer>;
14
+ /** Reject a changed HEAD or index tree after staged analysis. */
15
+ assertUnchanged(): void;
16
+ inventory: StagedPath[];
17
+ }
18
+ /** Capture one immutable tree from the index and inventory only Git objects. */
19
+ export declare function captureStagedSnapshot(workspaceRoot: string): GitChangeSnapshot;
20
+ /**
21
+ * Capture changes between explicit revisions. The caller chooses `base`; for
22
+ * merge-impact analysis this is commonly the merge base computed with
23
+ * `git merge-base target head`. This helper resolves each input once and does
24
+ * not silently choose a merge base of its own.
25
+ */
26
+ export declare function captureDiffSnapshot(workspaceRoot: string, base: string, head: string): GitChangeSnapshot;
27
+ //# sourceMappingURL=git-change-snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-change-snapshot.d.ts","sourceRoot":"","sources":["../../src/traceability/git-change-snapshot.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAIV,UAAU,EAEX,MAAM,iBAAiB,CAAC;AAKzB,MAAM,MAAM,iBAAiB,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,MAAM,CAAC;AAGpE,MAAM,WAAW,iBAAiB;IAChC,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,gEAAgE;IAChE,OAAO,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC;IACzC,uEAAuE;IACvE,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChE,iEAAiE;IACjE,eAAe,IAAI,IAAI,CAAC;IACxB,SAAS,EAAE,UAAU,EAAE,CAAC;CACzB;AA4cD,gFAAgF;AAEhF,wBAAgB,qBAAqB,CACnC,aAAa,EAAE,MAAM,GACpB,iBAAiB,CAuBnB;AAED;;;;;GAKG;AAEH,wBAAgB,mBAAmB,CACjC,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,iBAAiB,CAyBnB"}
@@ -0,0 +1,411 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
+ import { isEntityLanePath, isSymbolsManifestPath } from "../utils/kb-paths.js";
3
+ const GIT_EXEC_MAX_BUFFER = 64 * 1024 * 1024;
4
+ function runGit(root, args) {
5
+ try {
6
+ return execFileSync("git", [...args], {
7
+ cwd: root,
8
+ encoding: "buffer",
9
+ maxBuffer: GIT_EXEC_MAX_BUFFER,
10
+ stdio: ["ignore", "pipe", "ignore"],
11
+ });
12
+ }
13
+ catch (error) {
14
+ const detail = error instanceof Error ? error.message : String(error);
15
+ throw new Error(`git command failed: git ${args.join(" ")} -> ${detail}`);
16
+ }
17
+ }
18
+ function gitText(root, args) {
19
+ return runGit(root, args).toString("utf8").trim();
20
+ }
21
+ function readBlobs(root, oids) {
22
+ const unique = [...new Set(oids)];
23
+ if (unique.some((oid) => !/^[a-f0-9]{40,64}$/.test(oid)))
24
+ throw new Error("Invalid snapshot object ID");
25
+ const result = new Map();
26
+ if (unique.length === 0)
27
+ return result;
28
+ const output = execFileSync("git", ["cat-file", "--batch"], {
29
+ cwd: root,
30
+ input: `${unique.join("\n")}\n`,
31
+ maxBuffer: GIT_EXEC_MAX_BUFFER,
32
+ stdio: ["pipe", "pipe", "pipe"],
33
+ });
34
+ let offset = 0;
35
+ for (const oid of unique) {
36
+ const newline = output.indexOf(10, offset);
37
+ const header = output
38
+ .subarray(offset, newline)
39
+ .toString("ascii")
40
+ .split(" ");
41
+ const size = Number(header[2]);
42
+ if (newline < 0 ||
43
+ header[0] !== oid ||
44
+ header[1] !== "blob" ||
45
+ !Number.isSafeInteger(size) ||
46
+ size < 0 ||
47
+ newline + 1 + size >= output.length)
48
+ throw new Error(`Invalid snapshot blob response: ${oid}`);
49
+ const start = newline + 1;
50
+ if (output[start + size] !== 10)
51
+ throw new Error(`Invalid snapshot blob delimiter: ${oid}`);
52
+ result.set(oid, output.subarray(start, start + size));
53
+ offset = start + size + 1;
54
+ }
55
+ if (offset !== output.length)
56
+ throw new Error("Unexpected trailing snapshot blob data");
57
+ return result;
58
+ }
59
+ function resolveHeadCommit(root) {
60
+ try {
61
+ return gitText(root, ["rev-parse", "--verify", "HEAD^{commit}"]);
62
+ }
63
+ catch (headError) {
64
+ const ref = gitText(root, ["symbolic-ref", "-q", "HEAD"]);
65
+ const presence = spawnSync("git", ["show-ref", "--verify", "--quiet", ref], { cwd: root, stdio: "ignore" });
66
+ // Only a missing ref is unborn. A present ref pointing to a missing or
67
+ // non-commit object is corruption, not an empty repository.
68
+ if (presence.status === 1)
69
+ return null;
70
+ throw headError;
71
+ }
72
+ }
73
+ function emptyTreeOid(root) {
74
+ return gitText(root, ["hash-object", "-t", "tree", "--stdin"]);
75
+ }
76
+ function treeForCommit(root, commit) {
77
+ return commit
78
+ ? gitText(root, ["rev-parse", "--verify", `${commit}^{tree}`])
79
+ : emptyTreeOid(root);
80
+ }
81
+ function parseNameStatus(input) {
82
+ const entries = input.toString("utf8").split("\0").filter(Boolean);
83
+ const rows = [];
84
+ for (let index = 0; index < entries.length;) {
85
+ const token = entries[index] ?? "";
86
+ const tab = token.indexOf("\t");
87
+ const statusToken = tab >= 0 ? token.slice(0, tab) : token;
88
+ const inlinePath = tab >= 0 ? token.slice(tab + 1) : undefined;
89
+ const status = (statusToken[0] ?? "M");
90
+ const renamed = status === "R" || status === "C";
91
+ const firstPath = inlinePath ?? entries[index + 1] ?? "";
92
+ const oldPath = renamed ? firstPath : undefined;
93
+ const path = renamed ? (entries[index + 2] ?? "") : (firstPath ?? "");
94
+ rows.push({ status, ...(oldPath !== undefined ? { oldPath } : {}), path });
95
+ index += inlinePath !== undefined ? 1 : renamed ? 3 : 2;
96
+ }
97
+ return rows;
98
+ }
99
+ function literalPathspec(path) {
100
+ return `:(literal)${path}`;
101
+ }
102
+ function readTreeEntry(root, tree, path) {
103
+ const output = runGit(root, [
104
+ "ls-tree",
105
+ "-r",
106
+ "-z",
107
+ tree,
108
+ "--",
109
+ literalPathspec(path),
110
+ ]);
111
+ const first = output.toString("utf8").split("\0")[0];
112
+ if (!first)
113
+ return undefined;
114
+ const tab = first.indexOf("\t");
115
+ if (tab < 0)
116
+ return undefined;
117
+ const [mode, type, oid] = first.slice(0, tab).split(" ");
118
+ if (!mode || !type || !oid)
119
+ return undefined;
120
+ return { mode, type, oid };
121
+ }
122
+ function readBlob(root, entry) {
123
+ if (!entry || entry.type !== "blob")
124
+ return undefined;
125
+ return runGit(root, ["cat-file", "blob", entry.oid]);
126
+ }
127
+ function decodeText(buffer) {
128
+ if (buffer.includes(0))
129
+ return { skipReason: "binary" };
130
+ try {
131
+ return {
132
+ content: new TextDecoder("utf-8", { fatal: true }).decode(buffer),
133
+ };
134
+ }
135
+ catch {
136
+ return { skipReason: "unsupported_encoding" };
137
+ }
138
+ }
139
+ function hasSupportedExt(path) {
140
+ return /\.(?:ts|tsx|js|jsx|mts|cts|mjs|cjs)$/.test(path);
141
+ }
142
+ function isMetadataPath(path) {
143
+ const normalized = path.replaceAll("\\", "/");
144
+ const relationshipShard = normalized.startsWith(".kb/relationships/") &&
145
+ (normalized.endsWith(".yaml") || normalized.endsWith(".yml"));
146
+ return ((normalized.endsWith(".md") && isEntityLanePath(normalized)) ||
147
+ relationshipShard ||
148
+ normalized === ".kb/manifest.json" ||
149
+ normalized === ".kb/symbols.yaml" ||
150
+ normalized === ".kb/symbols.yml" ||
151
+ normalized === ".kb/symbol-coordinates.yaml" ||
152
+ isSymbolsManifestPath(normalized));
153
+ }
154
+ function analysisFor(path, deleted) {
155
+ if (deleted)
156
+ return "file";
157
+ if (hasSupportedExt(path))
158
+ return "symbol";
159
+ if (isMetadataPath(path))
160
+ return "metadata";
161
+ return "file";
162
+ }
163
+ function parseHunkSides(diffText) {
164
+ const oldHunkRanges = [];
165
+ const hunkRanges = [];
166
+ const regex = /^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/gm;
167
+ for (const match of diffText.matchAll(regex)) {
168
+ const oldStart = Number.parseInt(match[1] ?? "0", 10);
169
+ const oldCount = match[2] ? Number.parseInt(match[2], 10) : 1;
170
+ const newStart = Number.parseInt(match[3] ?? "0", 10);
171
+ const newCount = match[4] ? Number.parseInt(match[4], 10) : 1;
172
+ if (oldCount > 0) {
173
+ oldHunkRanges.push({ start: oldStart, end: oldStart + oldCount - 1 });
174
+ }
175
+ if (newCount > 0) {
176
+ hunkRanges.push({ start: newStart, end: newStart + newCount - 1 });
177
+ }
178
+ }
179
+ return { oldHunkRanges, hunkRanges };
180
+ }
181
+ function normalizeRanges(ranges, content) {
182
+ const lineCount = Math.max(1, (content ?? "").split(/\r?\n/).length);
183
+ for (const range of ranges) {
184
+ if (range.end === Number.MAX_SAFE_INTEGER)
185
+ range.end = lineCount;
186
+ }
187
+ }
188
+ function diffForPath(root, baseTree, headTree, entry) {
189
+ const paths = new Set([entry.oldPath, entry.path].filter((path) => path !== undefined));
190
+ return runGit(root, [
191
+ "diff",
192
+ "--no-ext-diff",
193
+ "--no-color",
194
+ "-U0",
195
+ "-M",
196
+ "-C",
197
+ "--find-copies-harder",
198
+ baseTree,
199
+ headTree,
200
+ "--",
201
+ ...[...paths].map(literalPathspec),
202
+ ]).toString("utf8");
203
+ }
204
+ function skipReasonForMode(mode) {
205
+ if (mode === "120000")
206
+ return "symlink";
207
+ if (mode === "160000")
208
+ return "submodule";
209
+ return undefined;
210
+ }
211
+ function buildInventory(root, baseTree, headTree) {
212
+ const rows = parseNameStatus(runGit(root, [
213
+ "diff",
214
+ "--name-status",
215
+ "-z",
216
+ "-M",
217
+ "-C",
218
+ "--find-copies-harder",
219
+ "--diff-filter=ACMRTD",
220
+ baseTree,
221
+ headTree,
222
+ ]));
223
+ return rows.map((row) => {
224
+ const previousPath = row.oldPath ?? row.path;
225
+ const oldEntry = readTreeEntry(root, baseTree, previousPath);
226
+ const newEntry = readTreeEntry(root, headTree, row.path);
227
+ const oldBlob = readBlob(root, oldEntry);
228
+ const newBlob = readBlob(root, newEntry);
229
+ const oldDecoded = oldBlob ? decodeText(oldBlob) : undefined;
230
+ const newDecoded = newBlob ? decodeText(newBlob) : undefined;
231
+ const diffText = diffForPath(root, baseTree, headTree, row);
232
+ const { oldHunkRanges, hunkRanges } = parseHunkSides(diffText);
233
+ const oldContent = oldDecoded && "content" in oldDecoded ? oldDecoded.content : undefined;
234
+ const newContent = newDecoded && "content" in newDecoded ? newDecoded.content : undefined;
235
+ const oldIsPresent = oldEntry !== undefined;
236
+ const newIsPresent = newEntry !== undefined;
237
+ if (!oldIsPresent && newIsPresent && hunkRanges.length === 0) {
238
+ hunkRanges.push({ start: 1, end: Number.MAX_SAFE_INTEGER });
239
+ }
240
+ if (oldIsPresent && !newIsPresent && oldHunkRanges.length === 0) {
241
+ oldHunkRanges.push({ start: 1, end: Number.MAX_SAFE_INTEGER });
242
+ }
243
+ normalizeRanges(hunkRanges, newContent);
244
+ normalizeRanges(oldHunkRanges, oldContent);
245
+ const gitMode = newEntry?.mode ?? oldEntry?.mode;
246
+ const previousMode = oldEntry?.mode;
247
+ const modeSkip = skipReasonForMode(newEntry?.mode ?? (!newIsPresent ? oldEntry?.mode : undefined));
248
+ const decodeSkip = (newDecoded && "skipReason" in newDecoded
249
+ ? newDecoded.skipReason
250
+ : undefined) ??
251
+ (oldDecoded && "skipReason" in oldDecoded
252
+ ? oldDecoded.skipReason
253
+ : undefined);
254
+ const skipReason = modeSkip ?? decodeSkip;
255
+ const pathFields = {
256
+ ...(row.status === "R" && row.oldPath !== undefined
257
+ ? { oldPath: row.oldPath }
258
+ : {}),
259
+ ...(row.status === "C" && row.oldPath !== undefined
260
+ ? { copyFromPath: row.oldPath }
261
+ : {}),
262
+ };
263
+ if (skipReason) {
264
+ return {
265
+ path: row.path,
266
+ status: row.status,
267
+ ...pathFields,
268
+ hunkRanges,
269
+ oldHunkRanges,
270
+ diffText,
271
+ ...(gitMode !== undefined ? { gitMode } : {}),
272
+ ...(previousMode !== undefined ? { previousMode } : {}),
273
+ ...(oldContent !== undefined ? { previousContent: oldContent } : {}),
274
+ analysisDepth: "none",
275
+ disposition: "skipped",
276
+ skipReason,
277
+ };
278
+ }
279
+ const deleted = !newIsPresent;
280
+ const analysisDepth = analysisFor(row.path, deleted);
281
+ const disposition = analysisDepth === "file" ? "advisory" : "checked";
282
+ return {
283
+ path: row.path,
284
+ status: row.status,
285
+ ...pathFields,
286
+ hunkRanges,
287
+ oldHunkRanges,
288
+ diffText,
289
+ ...(newContent !== undefined ? { content: newContent } : {}),
290
+ ...(oldContent !== undefined ? { previousContent: oldContent } : {}),
291
+ ...(gitMode !== undefined ? { gitMode } : {}),
292
+ ...(previousMode !== undefined ? { previousMode } : {}),
293
+ analysisDepth,
294
+ disposition,
295
+ };
296
+ });
297
+ }
298
+ function translateReadGit(root, args, baseTree, headTree, baseCommit) {
299
+ if (args[0] === "diff" && args.includes("--cached")) {
300
+ const translated = args.filter((arg) => arg !== "--cached");
301
+ const filterIndex = translated.findIndex((arg) => arg.startsWith("--diff-filter="));
302
+ if (filterIndex >= 0)
303
+ translated[filterIndex] = "--diff-filter=ACMRTD";
304
+ const separator = translated.indexOf("--");
305
+ const insertion = separator < 0 ? translated.length : separator;
306
+ translated.splice(insertion, 0, baseTree, headTree);
307
+ return runGit(root, translated);
308
+ }
309
+ if (args[0] === "ls-files" && args.includes("--stage")) {
310
+ const separator = args.indexOf("--");
311
+ const pathArgs = separator >= 0 ? args.slice(separator + 1) : [];
312
+ const output = runGit(root, [
313
+ "ls-tree",
314
+ "-r",
315
+ "-z",
316
+ headTree,
317
+ ...(pathArgs.length ? ["--", ...pathArgs] : []),
318
+ ]);
319
+ const rows = output
320
+ .toString("utf8")
321
+ .split("\0")
322
+ .filter(Boolean)
323
+ .map((row) => {
324
+ const tab = row.indexOf("\t");
325
+ if (tab < 0)
326
+ return row;
327
+ const [mode, _type, oid] = row.slice(0, tab).split(" ");
328
+ return `${mode ?? ""} ${oid ?? ""} 0${row.slice(tab)}`;
329
+ });
330
+ return Buffer.from(rows.length ? `${rows.join("\0")}\0` : "");
331
+ }
332
+ if (args[0] === "ls-tree" && args.includes("HEAD")) {
333
+ return runGit(root, args.map((arg) => (arg === "HEAD" ? baseTree : arg)));
334
+ }
335
+ if (args[0] === "rev-parse" && args.some((arg) => arg.startsWith("HEAD"))) {
336
+ const revision = args.find((arg) => arg.startsWith("HEAD")) ?? "HEAD";
337
+ if (revision === "HEAD^{tree}")
338
+ return Buffer.from(`${baseTree}\n`);
339
+ if (revision === "HEAD" || revision === "HEAD^{commit}") {
340
+ if (!baseCommit) {
341
+ throw new Error("HEAD does not resolve to a commit in this snapshot");
342
+ }
343
+ return Buffer.from(`${baseCommit}\n`);
344
+ }
345
+ }
346
+ if (args[0] === "show") {
347
+ const spec = args[1] ?? "";
348
+ if (spec.startsWith(":")) {
349
+ return runGit(root, ["show", `${headTree}:${spec.slice(1)}`]);
350
+ }
351
+ if (spec.startsWith("HEAD:")) {
352
+ return runGit(root, [
353
+ "show",
354
+ `${baseTree}:${spec.slice("HEAD:".length)}`,
355
+ ]);
356
+ }
357
+ }
358
+ return runGit(root, args);
359
+ }
360
+ function snapshotFromTrees(root, baseTree, headTree, headCommit, readGit, assertUnchanged) {
361
+ return {
362
+ baseTree,
363
+ headTree,
364
+ headCommit,
365
+ readGit,
366
+ readBlobs: (oids) => readBlobs(root, oids),
367
+ assertUnchanged,
368
+ inventory: buildInventory(root, baseTree, headTree),
369
+ };
370
+ }
371
+ /** Capture one immutable tree from the index and inventory only Git objects. */
372
+ // implements REQ-014
373
+ export function captureStagedSnapshot(workspaceRoot) {
374
+ const headCommit = resolveHeadCommit(workspaceRoot);
375
+ const baseTree = treeForCommit(workspaceRoot, headCommit);
376
+ const headTree = gitText(workspaceRoot, ["write-tree"]);
377
+ const readGit = (args) => translateReadGit(workspaceRoot, args, baseTree, headTree, headCommit);
378
+ const assertUnchanged = () => {
379
+ const currentHead = resolveHeadCommit(workspaceRoot);
380
+ const currentIndexTree = gitText(workspaceRoot, ["write-tree"]);
381
+ if (currentHead !== headCommit || currentIndexTree !== headTree) {
382
+ throw new Error("Git index or HEAD changed during staged analysis; retry the operation");
383
+ }
384
+ };
385
+ return snapshotFromTrees(workspaceRoot, baseTree, headTree, headCommit, readGit, assertUnchanged);
386
+ }
387
+ /**
388
+ * Capture changes between explicit revisions. The caller chooses `base`; for
389
+ * merge-impact analysis this is commonly the merge base computed with
390
+ * `git merge-base target head`. This helper resolves each input once and does
391
+ * not silently choose a merge base of its own.
392
+ */
393
+ // implements REQ-014
394
+ export function captureDiffSnapshot(workspaceRoot, base, head) {
395
+ const baseCommit = gitText(workspaceRoot, [
396
+ "rev-parse",
397
+ "--verify",
398
+ "--end-of-options",
399
+ `${base}^{commit}`,
400
+ ]);
401
+ const headCommit = gitText(workspaceRoot, [
402
+ "rev-parse",
403
+ "--verify",
404
+ "--end-of-options",
405
+ `${head}^{commit}`,
406
+ ]);
407
+ const baseTree = treeForCommit(workspaceRoot, baseCommit);
408
+ const headTree = treeForCommit(workspaceRoot, headCommit);
409
+ const readGit = (args) => translateReadGit(workspaceRoot, args, baseTree, headTree, baseCommit);
410
+ return snapshotFromTrees(workspaceRoot, baseTree, headTree, baseCommit, readGit, () => undefined);
411
+ }
@@ -1,4 +1,4 @@
1
- export type Status = "A" | "M" | "R" | "D";
1
+ export type Status = "A" | "M" | "R" | "C" | "T" | "D";
2
2
  export interface HunkRange {
3
3
  start: number;
4
4
  end: number;
@@ -16,9 +16,13 @@ export type StagedDisposition = "checked" | "advisory" | "skipped";
16
16
  export type StagedSkipReason = "binary" | "unsupported_encoding" | "symlink" | "submodule";
17
17
  /** Complete Git-index record used by staged validation before any filtering. */
18
18
  export interface StagedPath extends StagedFile {
19
+ /** Source path for a copy; unlike oldPath, it remains in the tree. */
20
+ copyFromPath?: string;
19
21
  analysisDepth: StagedAnalysisDepth;
20
22
  disposition: StagedDisposition;
21
23
  gitMode?: string;
24
+ previousMode?: string;
25
+ oldHunkRanges?: HunkRange[];
22
26
  previousContent?: string;
23
27
  skipReason?: StagedSkipReason;
24
28
  }
@@ -1 +1 @@
1
- {"version":3,"file":"git-staged.d.ts","sourceRoot":"","sources":["../../src/traceability/git-staged.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,MAAM,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAE3C,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAC1E,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AACnE,MAAM,MAAM,gBAAgB,GACxB,QAAQ,GACR,sBAAsB,GACtB,SAAS,GACT,WAAW,CAAC;AAEhB,gFAAgF;AAChF,MAAM,WAAW,UAAW,SAAQ,UAAU;IAC5C,aAAa,EAAE,mBAAmB,CAAC;IACnC,WAAW,EAAE,iBAAiB,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B;AAED,KAAK,MAAM,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,KAAK,MAAM,CAAC;AAClE,MAAM,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,MAAM,CAAC;AAwChE;;GAEG;AAEH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,GACZ,KAAK,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAwB5C;AAsFD;;;;GAIG;AAEH,wBAAgB,kBAAkB,CAChC,IAAI,GAAE,aAAkC,GACvC,UAAU,EAAE,CAuFd;AAED,kFAAkF;AAClF,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,SAAS,UAAU,EAAE,GAC/B,UAAU,EAAE,CAmBd;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAEhC,QAAQ,EAAE,MAAM,EAChB,SAAS,UAAQ,GAChB,SAAS,EAAE,CAuBb;AAED;;GAEG;AAEH,wBAAgB,cAAc,CAAC,IAAI,GAAE,MAAoB,GAAG,UAAU,EAAE,CAwGvE;AAED,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"git-staged.d.ts","sourceRoot":"","sources":["../../src/traceability/git-staged.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,MAAM,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAEvD,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,mBAAmB,GAAG,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAC1E,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AACnE,MAAM,MAAM,gBAAgB,GACxB,QAAQ,GACR,sBAAsB,GACtB,SAAS,GACT,WAAW,CAAC;AAEhB,gFAAgF;AAChF,MAAM,WAAW,UAAW,SAAQ,UAAU;IAC5C,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,mBAAmB,CAAC;IACnC,WAAW,EAAE,iBAAiB,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,SAAS,EAAE,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B;AAED,KAAK,MAAM,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,KAAK,MAAM,CAAC;AAClE,MAAM,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,MAAM,CAAC;AAgChE;;GAEG;AAEH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,GACZ,KAAK,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAwB5C;AAsFD;;;;GAIG;AAEH,wBAAgB,kBAAkB,CAAC,IAAI,CAAC,EAAE,aAAa,GAAG,UAAU,EAAE,CAmGrE;AAED,kFAAkF;AAClF,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,SAAS,UAAU,EAAE,GAC/B,UAAU,EAAE,CAmBd;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAEhC,QAAQ,EAAE,MAAM,EAChB,SAAS,UAAQ,GAChB,SAAS,EAAE,CAuBb;AAED;;GAEG;AAEH,wBAAgB,cAAc,CAAC,IAAI,GAAE,MAAoB,GAAG,UAAU,EAAE,CAwGvE;AAED,eAAe,cAAc,CAAC"}
@@ -1,6 +1,7 @@
1
- import { execFileSync, execSync } from "node:child_process";
1
+ import { execSync } from "node:child_process";
2
2
  import { isCliTraceOrDebugEnabled } from "../env.js";
3
3
  import { isEntityLanePath, isSymbolsManifestPath } from "../utils/kb-paths.js";
4
+ import { captureStagedSnapshot } from "./git-change-snapshot.js";
4
5
  // Staged manifests (e.g., .kb/symbols.yaml) can exceed Node's default 1 MiB
5
6
  // exec buffer; without a larger bound the staged file is silently skipped and
6
7
  // freshness checks compare against stale HEAD state.
@@ -8,13 +9,6 @@ const GIT_EXEC_MAX_BUFFER = 64 * 1024 * 1024;
8
9
  function defaultExec(cmd, opts) {
9
10
  return execSync(cmd, { ...opts, maxBuffer: GIT_EXEC_MAX_BUFFER });
10
11
  }
11
- function defaultGitArgsExec(args) {
12
- return execFileSync("git", args, {
13
- encoding: "buffer",
14
- maxBuffer: GIT_EXEC_MAX_BUFFER,
15
- stdio: ["ignore", "pipe", "ignore"],
16
- });
17
- }
18
12
  function runGitArgs(args, exec) {
19
13
  try {
20
14
  return exec(args);
@@ -138,10 +132,17 @@ function normalizeNewFileHunks(ranges, content) {
138
132
  * than the working tree.
139
133
  */
140
134
  // implements REQ-014
141
- export function getStagedInventory(exec = defaultGitArgsExec) {
135
+ export function getStagedInventory(exec) {
136
+ // The normal path reads only immutable tree/blob object IDs captured from the
137
+ // index. Keep the injectable reader path for existing callers and tests.
138
+ if (!exec) {
139
+ const snapshot = captureStagedSnapshot(process.cwd());
140
+ snapshot.assertUnchanged();
141
+ return snapshot.inventory;
142
+ }
142
143
  let parsed;
143
144
  try {
144
- parsed = parseNameStatusNull(runGitArgs(["diff", "--cached", "--name-status", "-z", "--diff-filter=ACMRD"], exec).toString("utf8"));
145
+ parsed = parseNameStatusNull(runGitArgs(["diff", "--cached", "--name-status", "-z", "--diff-filter=ACMRTDC"], exec).toString("utf8"));
145
146
  }
146
147
  catch (error) {
147
148
  throw new Error(`failed to list staged files: ${String(error)}`);
@@ -149,7 +150,8 @@ export function getStagedInventory(exec = defaultGitArgsExec) {
149
150
  return parsed.map((entry) => {
150
151
  const status = entry.status[0] || "M";
151
152
  const oldPath = status === "R" ? entry.parts[0] : undefined;
152
- const path = status === "R"
153
+ const copyFromPath = status === "C" ? entry.parts[0] : undefined;
154
+ const path = status === "R" || status === "C"
153
155
  ? (entry.parts[1] ?? entry.parts[0] ?? "")
154
156
  : (entry.parts[0] ?? "");
155
157
  const lookupPath = status === "D" ? (oldPath ?? path) : path;
@@ -162,6 +164,7 @@ export function getStagedInventory(exec = defaultGitArgsExec) {
162
164
  path,
163
165
  status,
164
166
  ...(oldPath ? { oldPath } : {}),
167
+ ...(copyFromPath ? { copyFromPath } : {}),
165
168
  hunkRanges: [],
166
169
  gitMode,
167
170
  analysisDepth: "none",
@@ -179,6 +182,7 @@ export function getStagedInventory(exec = defaultGitArgsExec) {
179
182
  path,
180
183
  status,
181
184
  ...(oldPath ? { oldPath } : {}),
185
+ ...(copyFromPath ? { copyFromPath } : {}),
182
186
  hunkRanges,
183
187
  diffText,
184
188
  ...(gitMode ? { gitMode } : {}),
@@ -202,6 +206,7 @@ export function getStagedInventory(exec = defaultGitArgsExec) {
202
206
  path,
203
207
  status,
204
208
  ...(oldPath ? { oldPath } : {}),
209
+ ...(copyFromPath ? { copyFromPath } : {}),
205
210
  hunkRanges,
206
211
  diffText,
207
212
  ...(content !== undefined ? { content } : {}),