@kb-labs/commit-core 2.116.14 → 2.118.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.
@@ -0,0 +1,43 @@
1
+ import { CommitPlan } from '@kb-labs/commit-contracts';
2
+
3
+ /**
4
+ * Commit plan validation — shared by applier, REST handlers, CLI, and MCP.
5
+ *
6
+ * Centralizes checks that used to live only inside apply.ts, so that any
7
+ * surface (Studio, CLI, MCP) can proactively report plan integrity/staleness
8
+ * before the user attempts to apply, instead of only failing at apply time.
9
+ *
10
+ * @module @kb-labs/commit-core/validator
11
+ */
12
+
13
+ /**
14
+ * Group files by their git repository (root or nested)
15
+ *
16
+ * Supports nested git repositories: files with a first path segment that is
17
+ * itself a git repo root are grouped under that nested repo instead of cwd.
18
+ */
19
+ declare function groupFilesByRepo(cwd: string, files: string[]): Map<string, {
20
+ relativePath: string;
21
+ originalPath: string;
22
+ }[]>;
23
+ /**
24
+ * Validate internal consistency of a commit plan: every commit has files and
25
+ * a message, and no file appears in more than one commit.
26
+ *
27
+ * Returns an array of human-readable error strings (empty = valid).
28
+ */
29
+ declare function validatePlanIntegrity(plan: CommitPlan): string[];
30
+ /**
31
+ * Check if files in the plan have changed since plan generation.
32
+ *
33
+ * Only checks files that are part of the plan, ignoring other changes in the
34
+ * repo — cheap even on a large repo. Used both proactively (status handlers,
35
+ * before the user attempts Apply) and as the last-second guard inside
36
+ * applyCommitPlan.
37
+ */
38
+ declare function checkPlanStaleness(cwd: string, plan: CommitPlan, scope?: string): Promise<{
39
+ isStale: boolean;
40
+ reason: string;
41
+ }>;
42
+
43
+ export { checkPlanStaleness, groupFilesByRepo, validatePlanIntegrity };
@@ -0,0 +1,131 @@
1
+ import { existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { useLogger } from '@kb-labs/sdk';
4
+ import { simpleGit } from 'simple-git';
5
+
6
+ // src/validator/index.ts
7
+ async function getGitStatus(cwd) {
8
+ const git = simpleGit(cwd);
9
+ const status = await git.status(["--ignore-submodules=all"]);
10
+ return {
11
+ staged: status.staged.filter((f) => !shouldIgnoreFile(f)),
12
+ unstaged: [...status.modified, ...status.deleted].filter((f) => !status.staged.includes(f)).filter((f) => !shouldIgnoreFile(f)),
13
+ untracked: status.not_added.filter((f) => !shouldIgnoreFile(f))
14
+ };
15
+ }
16
+ var IGNORED_SEGMENTS = /* @__PURE__ */ new Set([
17
+ "node_modules",
18
+ ".git",
19
+ "dist",
20
+ "build",
21
+ ".next",
22
+ ".turbo",
23
+ "coverage"
24
+ ]);
25
+ function shouldIgnoreFile(file) {
26
+ return file.split("/").some((segment) => IGNORED_SEGMENTS.has(segment));
27
+ }
28
+ function getAllChangedFiles(status) {
29
+ const allFiles = [
30
+ .../* @__PURE__ */ new Set([...status.staged, ...status.unstaged, ...status.untracked])
31
+ ];
32
+ return allFiles.filter((file) => !shouldIgnoreFile(file));
33
+ }
34
+
35
+ // src/validator/index.ts
36
+ function groupFilesByRepo(cwd, files) {
37
+ const filesByRepo = /* @__PURE__ */ new Map();
38
+ for (const file of files) {
39
+ const segments = file.split("/");
40
+ const potentialRepoDir = segments[0];
41
+ if (!potentialRepoDir) {
42
+ const group = filesByRepo.get(cwd) ?? [];
43
+ group.push({ relativePath: file, originalPath: file });
44
+ filesByRepo.set(cwd, group);
45
+ continue;
46
+ }
47
+ const potentialRepoPath = join(cwd, potentialRepoDir);
48
+ const potentialGitDir = join(potentialRepoPath, ".git");
49
+ const isNestedRepo = existsSync(potentialGitDir);
50
+ if (isNestedRepo) {
51
+ const relativePath = segments.slice(1).join("/");
52
+ const group = filesByRepo.get(potentialRepoPath) ?? [];
53
+ group.push({ relativePath, originalPath: file });
54
+ filesByRepo.set(potentialRepoPath, group);
55
+ } else {
56
+ const group = filesByRepo.get(cwd) ?? [];
57
+ group.push({ relativePath: file, originalPath: file });
58
+ filesByRepo.set(cwd, group);
59
+ }
60
+ }
61
+ return filesByRepo;
62
+ }
63
+ function validatePlanIntegrity(plan) {
64
+ const errors = [];
65
+ const seenInCommit = /* @__PURE__ */ new Map();
66
+ for (const commit of plan.commits) {
67
+ if (commit.files.length === 0) {
68
+ errors.push(`Commit ${commit.id} has no files`);
69
+ continue;
70
+ }
71
+ if (!commit.message.trim()) {
72
+ errors.push(`Commit ${commit.id} has empty message`);
73
+ continue;
74
+ }
75
+ for (const file of commit.files) {
76
+ const firstCommit = seenInCommit.get(file);
77
+ if (firstCommit) {
78
+ errors.push(
79
+ `File appears in multiple commits: ${file} (first: ${firstCommit}, duplicate: ${commit.id})`
80
+ );
81
+ } else {
82
+ seenInCommit.set(file, commit.id);
83
+ }
84
+ }
85
+ }
86
+ return errors;
87
+ }
88
+ async function checkPlanStaleness(cwd, plan, scope) {
89
+ const logger = useLogger();
90
+ const planFiles = new Set(plan.commits.flatMap((c) => c.files));
91
+ await logger.debug("checkPlanStaleness: start", {
92
+ scope,
93
+ cwd,
94
+ planFiles: [...planFiles]
95
+ });
96
+ if (planFiles.size === 0) {
97
+ return { isStale: false, reason: "" };
98
+ }
99
+ const filesByRepo = groupFilesByRepo(cwd, [...planFiles]);
100
+ for (const [repoPath, fileInfos] of filesByRepo) {
101
+ const currentStatus = await getGitStatus(repoPath);
102
+ const currentFiles = new Set(getAllChangedFiles(currentStatus));
103
+ await logger.debug("checkPlanStaleness: repo status", {
104
+ repoPath,
105
+ staged: currentStatus.staged,
106
+ unstaged: currentStatus.unstaged,
107
+ untracked: currentStatus.untracked,
108
+ expected: fileInfos.map((f) => f.relativePath)
109
+ });
110
+ for (const { relativePath, originalPath } of fileInfos) {
111
+ if (!currentFiles.has(relativePath)) {
112
+ await logger.warn("checkPlanStaleness: file not in current changes", {
113
+ scope,
114
+ repoPath,
115
+ originalPath,
116
+ relativePath,
117
+ currentFiles: [...currentFiles]
118
+ });
119
+ return {
120
+ isStale: true,
121
+ reason: `File no longer has changes: ${originalPath}. Regenerate plan or use --force.`
122
+ };
123
+ }
124
+ }
125
+ }
126
+ return { isStale: false, reason: "" };
127
+ }
128
+
129
+ export { checkPlanStaleness, groupFilesByRepo, validatePlanIntegrity };
130
+ //# sourceMappingURL=index.js.map
131
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/analyzer/git-status.ts","../../src/validator/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Git status analysis\n */\n\nimport { simpleGit, type SimpleGit, type StatusResult } from \"simple-git\";\nimport type { GitStatus } from \"@kb-labs/commit-contracts\";\n\n/**\n * Get current git status (staged, unstaged, untracked files).\n * cwd must already point to the resolved scope directory.\n */\nexport async function getGitStatus(cwd: string): Promise<GitStatus> {\n // --ignore-submodules=all: exclude submodule pointer drift in worktrees\n const git: SimpleGit = simpleGit(cwd);\n const status: StatusResult = await git.status(['--ignore-submodules=all']);\n\n return {\n staged: status.staged.filter((f) => !shouldIgnoreFile(f)),\n unstaged: [...status.modified, ...status.deleted]\n .filter((f) => !status.staged.includes(f))\n .filter((f) => !shouldIgnoreFile(f)),\n untracked: status.not_added.filter((f) => !shouldIgnoreFile(f)),\n };\n}\n\nconst IGNORED_SEGMENTS = new Set([\n \"node_modules\",\n \".git\",\n \"dist\",\n \"build\",\n \".next\",\n \".turbo\",\n \"coverage\",\n]);\n\n/**\n * Check if file should be ignored (node_modules, dist, etc.)\n * Uses exact path segment matching to avoid false positives like \"my-dist/file.ts\".\n */\nfunction shouldIgnoreFile(file: string): boolean {\n return file.split(\"/\").some((segment) => IGNORED_SEGMENTS.has(segment));\n}\n\n/**\n * Get all changed files (staged + unstaged + untracked)\n * Filters out node_modules and other build artifacts\n */\nexport function getAllChangedFiles(status: GitStatus): string[] {\n const allFiles = [\n ...new Set([...status.staged, ...status.unstaged, ...status.untracked]),\n ];\n return allFiles.filter((file) => !shouldIgnoreFile(file));\n}\n\n/**\n * Check if there are any changes\n */\nexport function hasChanges(status: GitStatus): boolean {\n return (\n status.staged.length > 0 ||\n status.unstaged.length > 0 ||\n status.untracked.length > 0\n );\n}\n\n/**\n * Get current branch name\n */\nexport async function getCurrentBranch(cwd: string): Promise<string> {\n const git: SimpleGit = simpleGit(cwd);\n const branch = await git.revparse([\"--abbrev-ref\", \"HEAD\"]);\n return branch.trim();\n}\n\n/**\n * Check if branch is protected (main/master)\n */\nexport function isProtectedBranch(branch: string): boolean {\n const protectedBranches = [\n \"main\",\n \"master\",\n \"develop\",\n \"release\",\n \"production\",\n ];\n return protectedBranches.includes(branch.toLowerCase());\n}\n","/**\n * Commit plan validation — shared by applier, REST handlers, CLI, and MCP.\n *\n * Centralizes checks that used to live only inside apply.ts, so that any\n * surface (Studio, CLI, MCP) can proactively report plan integrity/staleness\n * before the user attempts to apply, instead of only failing at apply time.\n *\n * @module @kb-labs/commit-core/validator\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { CommitPlan } from \"@kb-labs/commit-contracts\";\nimport { useLogger } from \"@kb-labs/sdk\";\nimport { getGitStatus, getAllChangedFiles } from \"../analyzer/git-status\";\n\n/**\n * Group files by their git repository (root or nested)\n *\n * Supports nested git repositories: files with a first path segment that is\n * itself a git repo root are grouped under that nested repo instead of cwd.\n */\nexport function groupFilesByRepo(\n cwd: string,\n files: string[],\n): Map<string, { relativePath: string; originalPath: string }[]> {\n const filesByRepo = new Map<\n string,\n { relativePath: string; originalPath: string }[]\n >();\n\n for (const file of files) {\n // Check if file is in a nested repo (first segment might be a git repo)\n const segments = file.split(\"/\");\n const potentialRepoDir = segments[0];\n\n // Handle edge case: empty file path or no segments\n if (!potentialRepoDir) {\n const group = filesByRepo.get(cwd) ?? [];\n group.push({ relativePath: file, originalPath: file });\n filesByRepo.set(cwd, group);\n continue;\n }\n\n const potentialRepoPath = join(cwd, potentialRepoDir);\n const potentialGitDir = join(potentialRepoPath, \".git\");\n\n // Check if it's actually a nested git repo\n const isNestedRepo = existsSync(potentialGitDir);\n\n if (isNestedRepo) {\n // Use nested repo as git root, strip first segment from path\n const relativePath = segments.slice(1).join(\"/\");\n const group = filesByRepo.get(potentialRepoPath) ?? [];\n group.push({ relativePath, originalPath: file });\n filesByRepo.set(potentialRepoPath, group);\n } else {\n // Use cwd as git root\n const group = filesByRepo.get(cwd) ?? [];\n group.push({ relativePath: file, originalPath: file });\n filesByRepo.set(cwd, group);\n }\n }\n\n return filesByRepo;\n}\n\n/**\n * Validate internal consistency of a commit plan: every commit has files and\n * a message, and no file appears in more than one commit.\n *\n * Returns an array of human-readable error strings (empty = valid).\n */\nexport function validatePlanIntegrity(plan: CommitPlan): string[] {\n const errors: string[] = [];\n const seenInCommit = new Map<string, string>();\n\n for (const commit of plan.commits) {\n if (commit.files.length === 0) {\n errors.push(`Commit ${commit.id} has no files`);\n continue;\n }\n\n if (!commit.message.trim()) {\n errors.push(`Commit ${commit.id} has empty message`);\n continue;\n }\n\n for (const file of commit.files) {\n const firstCommit = seenInCommit.get(file);\n if (firstCommit) {\n errors.push(\n `File appears in multiple commits: ${file} (first: ${firstCommit}, duplicate: ${commit.id})`,\n );\n } else {\n seenInCommit.set(file, commit.id);\n }\n }\n }\n\n return errors;\n}\n\n/**\n * Check if files in the plan have changed since plan generation.\n *\n * Only checks files that are part of the plan, ignoring other changes in the\n * repo — cheap even on a large repo. Used both proactively (status handlers,\n * before the user attempts Apply) and as the last-second guard inside\n * applyCommitPlan.\n */\nexport async function checkPlanStaleness(\n cwd: string,\n plan: CommitPlan,\n scope?: string,\n): Promise<{ isStale: boolean; reason: string }> {\n const logger = useLogger();\n const planFiles = new Set(plan.commits.flatMap((c) => c.files));\n\n await logger.debug(\"checkPlanStaleness: start\", {\n scope,\n cwd,\n planFiles: [...planFiles],\n });\n\n // If no files in plan, nothing to check\n if (planFiles.size === 0) {\n return { isStale: false, reason: \"\" };\n }\n\n // Determine which repo(s) we need to check\n const filesByRepo = groupFilesByRepo(cwd, [...planFiles]);\n\n // Check each repo for staleness\n for (const [repoPath, fileInfos] of filesByRepo) {\n // Get current git status from the repo\n // Note: repoPath already points to the correct git repository root\n const currentStatus = await getGitStatus(repoPath);\n const currentFiles = new Set(getAllChangedFiles(currentStatus));\n\n await logger.debug(\"checkPlanStaleness: repo status\", {\n repoPath,\n staged: currentStatus.staged,\n unstaged: currentStatus.unstaged,\n untracked: currentStatus.untracked,\n expected: fileInfos.map((f) => f.relativePath),\n });\n\n // Check that all expected files are still changed\n for (const { relativePath, originalPath } of fileInfos) {\n if (!currentFiles.has(relativePath)) {\n await logger.warn(\"checkPlanStaleness: file not in current changes\", {\n scope,\n repoPath,\n originalPath,\n relativePath,\n currentFiles: [...currentFiles],\n });\n return {\n isStale: true,\n reason: `File no longer has changes: ${originalPath}. Regenerate plan or use --force.`,\n };\n }\n }\n }\n\n return { isStale: false, reason: \"\" };\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kb-labs/commit-core",
3
3
  "description": "Core business logic for KB Labs Commit plugin - git analysis, plan generation, and commit application.",
4
- "version": "2.116.14",
4
+ "version": "2.118.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -26,6 +26,10 @@
26
26
  "types": "./dist/storage/index.d.ts",
27
27
  "import": "./dist/storage/index.js"
28
28
  },
29
+ "./validator": {
30
+ "types": "./dist/validator/index.d.ts",
31
+ "import": "./dist/validator/index.js"
32
+ },
29
33
  "./dist/*": "./dist/*"
30
34
  },
31
35
  "files": [
@@ -37,8 +41,8 @@
37
41
  "globby": "^11.0.0",
38
42
  "minimatch": "^10.0.1",
39
43
  "simple-git": "^3.36.0",
40
- "@kb-labs/commit-contracts": "2.116.14",
41
- "@kb-labs/sdk": "2.115.1"
44
+ "@kb-labs/commit-contracts": "2.118.0",
45
+ "@kb-labs/sdk": "2.115.3"
42
46
  },
43
47
  "devDependencies": {
44
48
  "@types/node": "^24.3.3",
@@ -46,7 +50,7 @@
46
50
  "tsup": "^8.5.0",
47
51
  "typescript": "^5.6.3",
48
52
  "vitest": "^3.2.6",
49
- "@kb-labs/devkit": "2.116.14"
53
+ "@kb-labs/devkit": "2.118.0"
50
54
  },
51
55
  "engines": {
52
56
  "node": ">=22.0.0",