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.
- package/dist/cli-operation-loader.d.ts.map +1 -1
- package/dist/cli-operation-loader.js +3 -0
- package/dist/cli-operation-metadata.d.ts +4 -0
- package/dist/cli-operation-metadata.d.ts.map +1 -1
- package/dist/cli-operation-metadata.js +5 -0
- package/dist/cli-register-reporting.d.ts.map +1 -1
- package/dist/cli-register-reporting.js +4 -0
- package/dist/commands/check-diff.d.ts +5 -0
- package/dist/commands/check-diff.d.ts.map +1 -0
- package/dist/commands/check-diff.js +62 -0
- package/dist/commands/check-generated.d.ts.map +1 -1
- package/dist/commands/check-generated.js +41 -18
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +210 -73
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +63 -29
- package/dist/commands/github-init.d.ts.map +1 -1
- package/dist/commands/github-init.js +9 -2
- package/dist/commands/sync/manifest.d.ts.map +1 -1
- package/dist/commands/sync/manifest.js +3 -1
- package/dist/engine.d.ts +15 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +252 -52
- package/dist/extractors/symbols-coordinator.d.ts +7 -0
- package/dist/extractors/symbols-coordinator.d.ts.map +1 -1
- package/dist/extractors/symbols-coordinator.js +75 -39
- package/dist/operations/modeling/predicate-inference-1.js +1 -1
- package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
- package/dist/operations/mutation/source-authoring.js +175 -53
- package/dist/operations/mutation/upsert.d.ts.map +1 -1
- package/dist/operations/mutation/upsert.js +5 -7
- package/dist/operations/planning/apply-plan.d.ts.map +1 -1
- package/dist/operations/planning/apply-plan.js +5 -9
- package/dist/operations/proof/ingest-proof.d.ts.map +1 -1
- package/dist/operations/proof/ingest-proof.js +1 -0
- package/dist/operations/semantic-advisor/analysis-receipt.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/analysis-receipt.js +7 -1
- package/dist/operations/semantic-advisor/analyze-prose.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/analyze-prose.js +26 -4
- package/dist/operations/semantic-advisor/clauses.d.ts +1 -0
- package/dist/operations/semantic-advisor/clauses.d.ts.map +1 -1
- package/dist/operations/semantic-advisor/clauses.js +7 -1
- package/dist/performance-trace.d.ts +17 -0
- package/dist/performance-trace.d.ts.map +1 -0
- package/dist/performance-trace.js +134 -0
- package/dist/plugins/approved-source-analyzers.d.ts +13 -0
- package/dist/plugins/approved-source-analyzers.d.ts.map +1 -0
- package/dist/plugins/approved-source-analyzers.generated.d.ts +71 -0
- package/dist/plugins/approved-source-analyzers.generated.d.ts.map +1 -0
- package/dist/plugins/approved-source-analyzers.generated.js +77 -0
- package/dist/plugins/approved-source-analyzers.js +3 -0
- package/dist/plugins/maintenance-source-analysis.d.ts +14 -0
- package/dist/plugins/maintenance-source-analysis.d.ts.map +1 -0
- package/dist/plugins/maintenance-source-analysis.js +191 -0
- package/dist/plugins/registry.d.ts +2 -1
- package/dist/plugins/registry.d.ts.map +1 -1
- package/dist/plugins/registry.js +6 -1
- package/dist/plugins/source-analysis-service.d.ts +36 -1
- package/dist/plugins/source-analysis-service.d.ts.map +1 -1
- package/dist/plugins/source-analysis-service.js +184 -27
- package/dist/plugins/source-change-analysis.d.ts +15 -0
- package/dist/plugins/source-change-analysis.d.ts.map +1 -0
- package/dist/plugins/source-change-analysis.js +54 -0
- package/dist/plugins/source-classification.d.ts +41 -0
- package/dist/plugins/source-classification.d.ts.map +1 -0
- package/dist/plugins/source-classification.js +490 -0
- package/dist/prolog/swipl-resolver.d.ts +88 -0
- package/dist/prolog/swipl-resolver.d.ts.map +1 -0
- package/dist/prolog/swipl-resolver.js +446 -0
- package/dist/prolog.d.ts +15 -1
- package/dist/prolog.d.ts.map +1 -1
- package/dist/prolog.js +67 -6
- package/dist/public/impact/analyzer.d.ts.map +1 -1
- package/dist/public/impact/analyzer.js +56 -5
- package/dist/public/impact/diagnostics.d.ts.map +1 -1
- package/dist/public/impact/diagnostics.js +3 -1
- package/dist/public/impact/manifest.d.ts.map +1 -1
- package/dist/public/impact/manifest.js +15 -1
- package/dist/public/impact/source-changes.d.ts.map +1 -1
- package/dist/public/impact/source-changes.js +35 -27
- package/dist/public/impact/types.d.ts +2 -0
- package/dist/public/impact/types.d.ts.map +1 -1
- package/dist/public/operations/catalog.d.ts +44 -0
- package/dist/public/operations/catalog.d.ts.map +1 -1
- package/dist/public/operations/catalog.js +2 -0
- package/dist/public/operations/contracts.d.ts.map +1 -1
- package/dist/public/operations/contracts.js +309 -8
- package/dist/public/operations/impact-review-preparation.d.ts +14 -0
- package/dist/public/operations/impact-review-preparation.d.ts.map +1 -0
- package/dist/public/operations/impact-review-preparation.js +379 -0
- package/dist/public/operations/index.d.ts +2 -0
- package/dist/public/operations/index.d.ts.map +1 -1
- package/dist/public/operations/index.js +2 -0
- package/dist/public/operations/node-ports.d.ts.map +1 -1
- package/dist/public/operations/node-ports.js +1 -35
- package/dist/public/operations/proof-receipt-projection.d.ts +5 -0
- package/dist/public/operations/proof-receipt-projection.d.ts.map +1 -0
- package/dist/public/operations/proof-receipt-projection.js +36 -0
- package/dist/public/operations/specs/impact-review-preparation.d.ts +47 -0
- package/dist/public/operations/specs/impact-review-preparation.d.ts.map +1 -0
- package/dist/public/operations/specs/impact-review-preparation.js +43 -0
- package/dist/public/operations/types.d.ts +1 -1
- package/dist/public/operations/types.d.ts.map +1 -1
- package/dist/public/skills/kibi-usage/resources/operation-access.md +1 -0
- package/dist/traceability/analysis-gate.d.ts +39 -0
- package/dist/traceability/analysis-gate.d.ts.map +1 -0
- package/dist/traceability/analysis-gate.js +51 -0
- package/dist/traceability/git-change-snapshot.d.ts +27 -0
- package/dist/traceability/git-change-snapshot.d.ts.map +1 -0
- package/dist/traceability/git-change-snapshot.js +411 -0
- package/dist/traceability/git-staged.d.ts +5 -1
- package/dist/traceability/git-staged.d.ts.map +1 -1
- package/dist/traceability/git-staged.js +16 -11
- package/dist/traceability/impact-evaluator.d.ts +54 -0
- package/dist/traceability/impact-evaluator.d.ts.map +1 -0
- package/dist/traceability/impact-evaluator.js +270 -0
- package/dist/traceability/impact-git-tree.d.ts +12 -0
- package/dist/traceability/impact-git-tree.d.ts.map +1 -0
- package/dist/traceability/impact-git-tree.js +46 -0
- package/dist/traceability/impact-knowledge.d.ts +16 -0
- package/dist/traceability/impact-knowledge.d.ts.map +1 -0
- package/dist/traceability/impact-knowledge.js +165 -0
- package/dist/traceability/impact-policy.d.ts +15 -0
- package/dist/traceability/impact-policy.d.ts.map +1 -0
- package/dist/traceability/impact-policy.js +48 -0
- package/dist/traceability/impact-policy.v1.schema.json +48 -0
- package/dist/traceability/impact-pr-diff.d.ts +5 -0
- package/dist/traceability/impact-pr-diff.d.ts.map +1 -0
- package/dist/traceability/impact-pr-diff.js +83 -0
- package/dist/traceability/impact-record-validation.d.ts +9 -0
- package/dist/traceability/impact-record-validation.d.ts.map +1 -0
- package/dist/traceability/impact-record-validation.js +113 -0
- package/dist/traceability/impact-review.d.ts +148 -0
- package/dist/traceability/impact-review.d.ts.map +1 -0
- package/dist/traceability/impact-review.js +494 -0
- package/dist/traceability/impact-review.schema.json +365 -0
- package/dist/traceability/impact-side-evidence.d.ts +13 -0
- package/dist/traceability/impact-side-evidence.d.ts.map +1 -0
- package/dist/traceability/impact-side-evidence.js +240 -0
- package/dist/traceability/reviewed-coordinate-refresh.d.ts +10 -0
- package/dist/traceability/reviewed-coordinate-refresh.d.ts.map +1 -0
- package/dist/traceability/reviewed-coordinate-refresh.js +49 -0
- package/dist/traceability/snapshot-knowledge.d.ts +4 -0
- package/dist/traceability/snapshot-knowledge.d.ts.map +1 -0
- package/dist/traceability/snapshot-knowledge.js +98 -0
- package/dist/traceability/staged-file-coverage.d.ts +23 -0
- package/dist/traceability/staged-file-coverage.d.ts.map +1 -1
- package/dist/traceability/staged-impact-contract.d.ts.map +1 -1
- package/dist/traceability/staged-impact-contract.js +2 -16
- package/dist/traceability/staged-symbols-manifest.d.ts +4 -0
- package/dist/traceability/staged-symbols-manifest.d.ts.map +1 -1
- package/dist/traceability/staged-symbols-manifest.js +17 -6
- package/dist/traceability/symbol-extract.d.ts +3 -5
- package/dist/traceability/symbol-extract.d.ts.map +1 -1
- package/dist/traceability/symbol-extract.js +49 -34
- package/dist/traceability/temp-kb.d.ts +1 -1
- package/dist/traceability/temp-kb.d.ts.map +1 -1
- package/dist/traceability/temp-kb.js +49 -13
- package/dist/traceability/trusted-pr-event.d.ts +13 -0
- package/dist/traceability/trusted-pr-event.d.ts.map +1 -0
- package/dist/traceability/trusted-pr-event.js +105 -0
- package/dist/utils/canonical-path.d.ts +3 -0
- package/dist/utils/canonical-path.d.ts.map +1 -0
- package/dist/utils/canonical-path.js +33 -0
- package/package.json +14 -5
- package/src/public/impact/analyzer.ts +88 -5
- package/src/public/impact/diagnostics.ts +7 -5
- package/src/public/impact/manifest.ts +14 -1
- package/src/public/impact/source-changes.ts +39 -34
- package/src/public/impact/types.ts +5 -0
- package/src/public/operations/catalog.ts +2 -0
- package/src/public/operations/contracts.ts +385 -9
- package/src/public/operations/impact-review-preparation.ts +548 -0
- package/src/public/operations/index.ts +2 -0
- package/src/public/operations/node-ports.ts +1 -39
- package/src/public/operations/proof-receipt-projection.ts +39 -0
- package/src/public/operations/specs/impact-review-preparation.ts +46 -0
- package/src/public/operations/types.ts +1 -0
- package/src/public/skills/kibi-usage/resources/operation-access.md +1 -0
- package/src/traceability/impact-policy.v1.schema.json +48 -0
- package/src/traceability/impact-review.schema.json +365 -0
- package/templates/github/kibi-badge.yml +0 -4
- 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":"
|
|
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 {
|
|
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
|
|
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=
|
|
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
|
|
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 } : {}),
|