knodin 0.6.0 → 0.7.3

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 (48) hide show
  1. package/README.md +26 -3
  2. package/dist/bin/cli.js +168 -12
  3. package/dist/bin/launcher.js +11 -0
  4. package/dist/src/agent-integration.js +3 -1
  5. package/dist/src/cli-args.js +8 -1
  6. package/dist/src/cli-model.js +35 -1
  7. package/dist/src/codeflow-replay.js +80 -0
  8. package/dist/src/competitive-cold-mcp.js +40 -0
  9. package/dist/src/competitive-manifest.js +106 -25
  10. package/dist/src/competitive-runner.js +37 -3
  11. package/dist/src/competitive-sandbox.js +1 -1
  12. package/dist/src/diagnostics.js +449 -0
  13. package/dist/src/engine/git-history.js +289 -0
  14. package/dist/src/engine/index.js +417 -70
  15. package/dist/src/engine/scip-import.js +408 -0
  16. package/dist/src/execution-profile.js +203 -0
  17. package/dist/src/failure-diagnosis.js +69 -10
  18. package/dist/src/hook-manager-integration.js +156 -0
  19. package/dist/src/init.js +319 -33
  20. package/dist/src/lifecycle-health.js +42 -4
  21. package/dist/src/output-telemetry.js +4 -0
  22. package/dist/src/progressive-evidence.js +473 -0
  23. package/dist/src/pure-compression-cli.js +101 -0
  24. package/dist/src/release-preflight.js +510 -0
  25. package/dist/src/repository-management.js +142 -0
  26. package/dist/src/response-budget.js +11 -1
  27. package/dist/src/server.js +22 -2
  28. package/dist/src/structural-fast-path.js +303 -0
  29. package/dist/src/structural-snapshot.js +33 -0
  30. package/dist/src/tools/knodin-tools.js +105 -14
  31. package/dist/src/update-ceremony.js +158 -0
  32. package/docs/CLI.md +22 -0
  33. package/docs/COMMAND-OUTPUT-COMPRESSION.md +31 -15
  34. package/docs/CONTAINED-EXECUTION.md +77 -0
  35. package/docs/DIAGNOSTICS.md +45 -0
  36. package/docs/DOCTOR-AND-UPDATES.md +5 -2
  37. package/docs/GIT-HISTORY-REVIEW.md +39 -0
  38. package/docs/MCP.md +15 -0
  39. package/docs/PROGRESSIVE-EVIDENCE.md +37 -0
  40. package/docs/REPOSITORIES-AND-WORKTREES.md +30 -0
  41. package/docs/SCIP-IMPORT.md +57 -0
  42. package/docs/SIGNED-UPDATES.md +5 -0
  43. package/docs/TELEMETRY.md +4 -0
  44. package/docs/releases/0.7.0.md +24 -0
  45. package/docs/releases/0.7.1.md +21 -0
  46. package/docs/releases/0.7.2.md +21 -0
  47. package/docs/releases/0.7.3.md +23 -0
  48. package/package.json +33 -2
@@ -0,0 +1,289 @@
1
+ import childProcess from "node:child_process";
2
+ import path from "node:path";
3
+ export const DEFAULT_GIT_HISTORY_BOUNDS = Object.freeze({
4
+ maxCommits: 250,
5
+ maxFiles: 25,
6
+ maxRelatedFiles: 100,
7
+ timeoutMs: 2_000,
8
+ });
9
+ const MAX_GIT_OUTPUT_BYTES = 8 * 1024 * 1024;
10
+ const HISTORY_CACHE_LIMIT = 8;
11
+ const historyCache = new Map();
12
+ const INTERPRETATION = "Co-change and coupling are bounded historical correlation, not evidence of causation, defects, ownership, or required remediation.";
13
+ function positiveBound(value, fallback, name, hardMaximum) {
14
+ const resolved = value ?? fallback;
15
+ if (!Number.isSafeInteger(resolved) || resolved < 1 || resolved > hardMaximum) {
16
+ throw new Error(`git history ${name} must be an integer between 1 and ${hardMaximum}`);
17
+ }
18
+ return resolved;
19
+ }
20
+ function resolveBounds(input) {
21
+ return {
22
+ maxCommits: positiveBound(input.maxCommits, DEFAULT_GIT_HISTORY_BOUNDS.maxCommits, "maxCommits", 10_000),
23
+ maxFiles: positiveBound(input.maxFiles, DEFAULT_GIT_HISTORY_BOUNDS.maxFiles, "maxFiles", 1_000),
24
+ maxRelatedFiles: positiveBound(input.maxRelatedFiles, DEFAULT_GIT_HISTORY_BOUNDS.maxRelatedFiles, "maxRelatedFiles", 10_000),
25
+ timeoutMs: positiveBound(input.timeoutMs, DEFAULT_GIT_HISTORY_BOUNDS.timeoutMs, "timeoutMs", 30_000),
26
+ };
27
+ }
28
+ function safeHistoryFile(file) {
29
+ const normalized = file.replaceAll("\\", "/").replace(/^\.\//, "");
30
+ if (!normalized ||
31
+ path.isAbsolute(normalized) ||
32
+ normalized === ".." ||
33
+ normalized.startsWith("../")) {
34
+ throw new Error(`git history file must be repo-relative: ${file}`);
35
+ }
36
+ return normalized;
37
+ }
38
+ function failureReason(error) {
39
+ const failure = error;
40
+ if (failure.code === "ENOENT")
41
+ return "git-unavailable";
42
+ if (failure.code === "ETIMEDOUT" || failure.signal === "SIGTERM" || failure.killed)
43
+ return "timeout";
44
+ const message = `${String(failure.message ?? error)}\n${String(failure.stderr ?? "")}`;
45
+ if (/not a git repository/i.test(message))
46
+ return "not-a-repository";
47
+ return "history-command-failed";
48
+ }
49
+ function isUnbornHeadFailure(error) {
50
+ const failure = error;
51
+ const diagnostic = `${String(failure.message ?? error)}\n${String(failure.stderr ?? "")}`;
52
+ return /needed a single revision|ambiguous argument ['"]?HEAD|unknown revision or path/i.test(diagnostic);
53
+ }
54
+ function emptyResult(availability, bounds, commands, omission, files = []) {
55
+ return {
56
+ availability,
57
+ bounds,
58
+ commands,
59
+ commitsAnalyzed: 0,
60
+ filesAnalyzed: files.length,
61
+ truncated: false,
62
+ churn: files.map((file) => ({ file, commitCount: 0, commits: [] })),
63
+ coChange: [],
64
+ coupling: [],
65
+ omissions: [omission],
66
+ interpretation: INTERPRETATION,
67
+ confidence: availability === "unborn" ? "exact-bounded-git-history" : "unavailable",
68
+ };
69
+ }
70
+ function parseLog(output) {
71
+ const commits = [];
72
+ for (const rawRecord of output.split("\x1e").slice(1)) {
73
+ const tokens = rawRecord.split("\0").filter(Boolean);
74
+ const [header, ...changes] = tokens;
75
+ if (!header)
76
+ continue;
77
+ const [hash, parentsText = ""] = header.split("\x1f");
78
+ const parsedChanges = [];
79
+ for (let index = 0; index < changes.length;) {
80
+ const status = (changes[index++] ?? "").trim();
81
+ if (/^[RC]/.test(status)) {
82
+ const oldPath = changes[index++];
83
+ const renamedPath = changes[index++];
84
+ if (oldPath && renamedPath)
85
+ parsedChanges.push({ status, oldPath, path: renamedPath });
86
+ }
87
+ else {
88
+ const changedPath = changes[index++];
89
+ if (changedPath)
90
+ parsedChanges.push({ status, path: changedPath });
91
+ }
92
+ }
93
+ commits.push({ hash, parents: parentsText.split(" ").filter(Boolean), changes: parsedChanges });
94
+ }
95
+ return commits;
96
+ }
97
+ /**
98
+ * Collect deterministic, itemized Git-history facts in one bounded history pass.
99
+ * The command receives no user-provided revisions or filesystem paths; `cwd` is
100
+ * the resolved repository and every requested file is validated before launch.
101
+ */
102
+ export function collectGitHistorySignals(repoPath, filePaths, limits = {}) {
103
+ const bounds = resolveBounds(limits);
104
+ const requestedAll = [...new Set(filePaths.map(safeHistoryFile))].sort();
105
+ const requested = requestedAll.slice(0, bounds.maxFiles);
106
+ const omissions = [];
107
+ if (requestedAll.length > requested.length) {
108
+ omissions.push({
109
+ reason: "file-limit",
110
+ detail: `${requestedAll.length - requested.length} requested file(s) omitted by maxFiles=${bounds.maxFiles}`,
111
+ });
112
+ }
113
+ let commands = 0;
114
+ const run = (args) => {
115
+ commands++;
116
+ return childProcess.execFileSync("git", args, {
117
+ cwd: path.resolve(repoPath),
118
+ encoding: "utf8",
119
+ stdio: ["ignore", "pipe", "pipe"],
120
+ timeout: bounds.timeoutMs,
121
+ maxBuffer: MAX_GIT_OUTPUT_BYTES,
122
+ });
123
+ };
124
+ let head;
125
+ try {
126
+ head = run(["rev-parse", "--verify", "HEAD"]).trim();
127
+ }
128
+ catch (error) {
129
+ const reason = failureReason(error);
130
+ if (reason === "history-command-failed" && isUnbornHeadFailure(error)) {
131
+ return emptyResult("unborn", bounds, commands, {
132
+ reason: "unborn-repository",
133
+ detail: "HEAD does not exist; no historical facts are available",
134
+ }, requested);
135
+ }
136
+ const availability = reason === "git-unavailable"
137
+ ? "git-unavailable"
138
+ : reason === "not-a-repository"
139
+ ? "not-a-repository"
140
+ : reason === "timeout"
141
+ ? "timeout"
142
+ : "unavailable";
143
+ return emptyResult(availability, bounds, commands, {
144
+ reason,
145
+ detail: "Git HEAD probe failed; history signals were omitted",
146
+ });
147
+ }
148
+ let shallow = false;
149
+ try {
150
+ shallow = run(["rev-parse", "--is-shallow-repository"]).trim() === "true";
151
+ }
152
+ catch {
153
+ // Older Git versions may not expose the probe; bounded history remains usable.
154
+ }
155
+ if (shallow) {
156
+ omissions.push({
157
+ reason: "shallow-history",
158
+ detail: "Only commits present in the shallow clone contribute facts",
159
+ });
160
+ }
161
+ const cacheKey = `${path.resolve(repoPath)}\0${head}\0${requestedAll.join("\0")}\0${JSON.stringify(bounds)}`;
162
+ if (!shallow) {
163
+ const cached = historyCache.get(cacheKey);
164
+ if (cached)
165
+ return structuredClone({ ...cached, commands });
166
+ }
167
+ let commits;
168
+ try {
169
+ const output = run([
170
+ "log",
171
+ `--max-count=${bounds.maxCommits + 1}`,
172
+ "--find-renames",
173
+ "--name-status",
174
+ "-z",
175
+ "--format=%x1e%H%x1f%P%x00",
176
+ "--no-decorate",
177
+ ]);
178
+ commits = parseLog(output);
179
+ }
180
+ catch (error) {
181
+ const reason = failureReason(error);
182
+ const availability = reason === "timeout" ? "timeout" : "unavailable";
183
+ return emptyResult(availability, bounds, commands, {
184
+ reason,
185
+ detail: "Bounded Git log failed; history signals were omitted",
186
+ });
187
+ }
188
+ if (commits.length > bounds.maxCommits) {
189
+ commits = commits.slice(0, bounds.maxCommits);
190
+ omissions.push({
191
+ reason: "commit-limit",
192
+ detail: `Older commits omitted by maxCommits=${bounds.maxCommits}`,
193
+ });
194
+ }
195
+ const aliases = new Map(requested.map((file) => [file, new Set([file])]));
196
+ const commitsByFile = new Map(requested.map((file) => [file, new Set()]));
197
+ const shared = new Map();
198
+ for (const commit of commits) {
199
+ for (const change of commit.changes) {
200
+ if (!change.oldPath)
201
+ continue;
202
+ for (const aliasSet of aliases.values()) {
203
+ if (change.status.startsWith("R") && aliasSet.has(change.path))
204
+ aliasSet.add(change.oldPath);
205
+ }
206
+ }
207
+ for (const file of requested) {
208
+ const aliasSet = aliases.get(file) ?? new Set();
209
+ const touchesTarget = commit.changes.some((change) => [change.path, change.oldPath].some((candidate) => candidate && aliasSet.has(candidate)));
210
+ if (!touchesTarget)
211
+ continue;
212
+ commitsByFile.get(file)?.add(commit.hash);
213
+ for (const change of commit.changes) {
214
+ for (const relatedFile of [change.path, change.oldPath]) {
215
+ if (!relatedFile || aliasSet.has(relatedFile))
216
+ continue;
217
+ const key = `${file}\0${relatedFile}`;
218
+ let hashes = shared.get(key);
219
+ if (!hashes) {
220
+ hashes = new Set();
221
+ shared.set(key, hashes);
222
+ }
223
+ hashes.add(commit.hash);
224
+ }
225
+ }
226
+ }
227
+ }
228
+ const allCoChange = [...shared.entries()]
229
+ .map(([key, hashes]) => {
230
+ const [file, relatedFile] = key.split("\0");
231
+ return {
232
+ file,
233
+ relatedFile,
234
+ sharedCommitCount: hashes.size,
235
+ commits: [...hashes].sort(),
236
+ };
237
+ })
238
+ .sort((a, b) => b.sharedCommitCount - a.sharedCommitCount ||
239
+ a.file.localeCompare(b.file) ||
240
+ a.relatedFile.localeCompare(b.relatedFile));
241
+ const coChange = allCoChange.slice(0, bounds.maxRelatedFiles);
242
+ if (coChange.length < allCoChange.length) {
243
+ omissions.push({
244
+ reason: "related-file-limit",
245
+ detail: `${allCoChange.length - coChange.length} co-change fact(s) omitted by maxRelatedFiles=${bounds.maxRelatedFiles}`,
246
+ });
247
+ }
248
+ const churn = requested.map((file) => {
249
+ const hashes = [...(commitsByFile.get(file) ?? [])].sort();
250
+ return { file, commitCount: hashes.length, commits: hashes };
251
+ });
252
+ const coupling = coChange.map((fact) => {
253
+ const fileCommitCount = commitsByFile.get(fact.file)?.size ?? 0;
254
+ return {
255
+ file: fact.file,
256
+ relatedFile: fact.relatedFile,
257
+ sharedCommitCount: fact.sharedCommitCount,
258
+ fileCommitCount,
259
+ ratio: fileCommitCount === 0 ? 0 : Number((fact.sharedCommitCount / fileCommitCount).toFixed(4)),
260
+ };
261
+ });
262
+ const result = {
263
+ availability: shallow ? "shallow" : "available",
264
+ bounds,
265
+ commands,
266
+ commitsAnalyzed: commits.length,
267
+ filesAnalyzed: requested.length,
268
+ truncated: omissions.some((item) => item.reason.endsWith("limit")),
269
+ churn,
270
+ coChange,
271
+ coupling,
272
+ omissions,
273
+ interpretation: INTERPRETATION,
274
+ confidence: "exact-bounded-git-history",
275
+ };
276
+ if (!shallow) {
277
+ historyCache.set(cacheKey, structuredClone(result));
278
+ while (historyCache.size > HISTORY_CACHE_LIMIT) {
279
+ const oldest = historyCache.keys().next().value;
280
+ if (oldest === undefined)
281
+ break;
282
+ historyCache.delete(oldest);
283
+ }
284
+ }
285
+ return result;
286
+ }
287
+ export function clearGitHistorySignalCache() {
288
+ historyCache.clear();
289
+ }