kibi-cli 2.2.0 → 2.4.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 (230) hide show
  1. package/dist/cli-json-command.d.ts.map +1 -1
  2. package/dist/cli-json-command.js +2 -2
  3. package/dist/cli-operation-loader.d.ts.map +1 -1
  4. package/dist/cli-operation-loader.js +3 -0
  5. package/dist/cli-operation-metadata.d.ts +5 -1
  6. package/dist/cli-operation-metadata.d.ts.map +1 -1
  7. package/dist/cli-operation-metadata.js +6 -1
  8. package/dist/cli-register-maintenance.d.ts.map +1 -1
  9. package/dist/cli-register-maintenance.js +7 -0
  10. package/dist/cli-register-reporting.d.ts.map +1 -1
  11. package/dist/cli-register-reporting.js +4 -0
  12. package/dist/commands/check-diff.d.ts +5 -0
  13. package/dist/commands/check-diff.d.ts.map +1 -0
  14. package/dist/commands/check-diff.js +62 -0
  15. package/dist/commands/check-generated.d.ts.map +1 -1
  16. package/dist/commands/check-generated.js +41 -18
  17. package/dist/commands/check.d.ts.map +1 -1
  18. package/dist/commands/check.js +210 -73
  19. package/dist/commands/doctor.d.ts.map +1 -1
  20. package/dist/commands/doctor.js +63 -29
  21. package/dist/commands/github-init.d.ts.map +1 -1
  22. package/dist/commands/github-init.js +9 -2
  23. package/dist/commands/merge-driver.d.ts +31 -0
  24. package/dist/commands/merge-driver.d.ts.map +1 -0
  25. package/dist/commands/merge-driver.js +268 -0
  26. package/dist/commands/sync/manifest.d.ts.map +1 -1
  27. package/dist/commands/sync/manifest.js +3 -1
  28. package/dist/engine.d.ts +15 -1
  29. package/dist/engine.d.ts.map +1 -1
  30. package/dist/engine.js +252 -52
  31. package/dist/extractors/symbols-coordinator.d.ts +7 -0
  32. package/dist/extractors/symbols-coordinator.d.ts.map +1 -1
  33. package/dist/extractors/symbols-coordinator.js +75 -39
  34. package/dist/operations/bootstrap/candidates.js +1 -1
  35. package/dist/operations/bootstrap/discovery.js +1 -1
  36. package/dist/operations/bootstrap/generate.d.ts.map +1 -1
  37. package/dist/operations/bootstrap/generate.js +21 -5
  38. package/dist/operations/bootstrap/guidance-actions.d.ts.map +1 -1
  39. package/dist/operations/bootstrap/guidance-actions.js +3 -0
  40. package/dist/operations/bootstrap/guidance.d.ts.map +1 -1
  41. package/dist/operations/bootstrap/guidance.js +7 -1
  42. package/dist/operations/bootstrap/intent-claims.d.ts +17 -0
  43. package/dist/operations/bootstrap/intent-claims.d.ts.map +1 -0
  44. package/dist/operations/bootstrap/intent-claims.js +95 -0
  45. package/dist/operations/bootstrap/presentation.d.ts +2 -0
  46. package/dist/operations/bootstrap/presentation.d.ts.map +1 -1
  47. package/dist/operations/bootstrap/presentation.js +60 -1
  48. package/dist/operations/bootstrap/types.d.ts +28 -0
  49. package/dist/operations/bootstrap/types.d.ts.map +1 -1
  50. package/dist/operations/bootstrap/types.js +16 -0
  51. package/dist/operations/modeling/predicate-inference-1.js +1 -1
  52. package/dist/operations/mutation/source-authoring.d.ts.map +1 -1
  53. package/dist/operations/mutation/source-authoring.js +175 -53
  54. package/dist/operations/mutation/symbol-refresh.d.ts.map +1 -1
  55. package/dist/operations/mutation/symbol-refresh.js +4 -1
  56. package/dist/operations/mutation/upsert.d.ts.map +1 -1
  57. package/dist/operations/mutation/upsert.js +5 -7
  58. package/dist/operations/planning/apply-plan.d.ts.map +1 -1
  59. package/dist/operations/planning/apply-plan.js +5 -9
  60. package/dist/operations/proof/ingest-proof.d.ts.map +1 -1
  61. package/dist/operations/proof/ingest-proof.js +1 -0
  62. package/dist/operations/semantic-advisor/analysis-receipt.d.ts.map +1 -1
  63. package/dist/operations/semantic-advisor/analysis-receipt.js +7 -1
  64. package/dist/operations/semantic-advisor/analyze-prose.d.ts.map +1 -1
  65. package/dist/operations/semantic-advisor/analyze-prose.js +26 -4
  66. package/dist/operations/semantic-advisor/clauses.d.ts +1 -0
  67. package/dist/operations/semantic-advisor/clauses.d.ts.map +1 -1
  68. package/dist/operations/semantic-advisor/clauses.js +7 -1
  69. package/dist/performance-trace.d.ts +17 -0
  70. package/dist/performance-trace.d.ts.map +1 -0
  71. package/dist/performance-trace.js +134 -0
  72. package/dist/plugins/approved-source-analyzers.d.ts +13 -0
  73. package/dist/plugins/approved-source-analyzers.d.ts.map +1 -0
  74. package/dist/plugins/approved-source-analyzers.generated.d.ts +71 -0
  75. package/dist/plugins/approved-source-analyzers.generated.d.ts.map +1 -0
  76. package/dist/plugins/approved-source-analyzers.generated.js +77 -0
  77. package/dist/plugins/approved-source-analyzers.js +3 -0
  78. package/dist/plugins/maintenance-source-analysis.d.ts +14 -0
  79. package/dist/plugins/maintenance-source-analysis.d.ts.map +1 -0
  80. package/dist/plugins/maintenance-source-analysis.js +191 -0
  81. package/dist/plugins/registry.d.ts +2 -1
  82. package/dist/plugins/registry.d.ts.map +1 -1
  83. package/dist/plugins/registry.js +6 -1
  84. package/dist/plugins/source-analysis-service.d.ts +36 -1
  85. package/dist/plugins/source-analysis-service.d.ts.map +1 -1
  86. package/dist/plugins/source-analysis-service.js +184 -27
  87. package/dist/plugins/source-change-analysis.d.ts +15 -0
  88. package/dist/plugins/source-change-analysis.d.ts.map +1 -0
  89. package/dist/plugins/source-change-analysis.js +54 -0
  90. package/dist/plugins/source-classification.d.ts +41 -0
  91. package/dist/plugins/source-classification.d.ts.map +1 -0
  92. package/dist/plugins/source-classification.js +490 -0
  93. package/dist/prolog/swipl-resolver.d.ts +88 -0
  94. package/dist/prolog/swipl-resolver.d.ts.map +1 -0
  95. package/dist/prolog/swipl-resolver.js +446 -0
  96. package/dist/prolog.d.ts +15 -1
  97. package/dist/prolog.d.ts.map +1 -1
  98. package/dist/prolog.js +67 -6
  99. package/dist/public/diagnostic-usage.d.ts +14 -0
  100. package/dist/public/diagnostic-usage.d.ts.map +1 -1
  101. package/dist/public/diagnostic-usage.js +55 -1
  102. package/dist/public/impact/analyzer.d.ts.map +1 -1
  103. package/dist/public/impact/analyzer.js +56 -5
  104. package/dist/public/impact/diagnostics.d.ts.map +1 -1
  105. package/dist/public/impact/diagnostics.js +3 -1
  106. package/dist/public/impact/manifest.d.ts.map +1 -1
  107. package/dist/public/impact/manifest.js +15 -1
  108. package/dist/public/impact/source-changes.d.ts.map +1 -1
  109. package/dist/public/impact/source-changes.js +35 -27
  110. package/dist/public/impact/types.d.ts +2 -0
  111. package/dist/public/impact/types.d.ts.map +1 -1
  112. package/dist/public/operations/catalog.d.ts +117 -1
  113. package/dist/public/operations/catalog.d.ts.map +1 -1
  114. package/dist/public/operations/catalog.js +2 -0
  115. package/dist/public/operations/contracts.d.ts.map +1 -1
  116. package/dist/public/operations/contracts.js +309 -8
  117. package/dist/public/operations/impact-review-preparation.d.ts +14 -0
  118. package/dist/public/operations/impact-review-preparation.d.ts.map +1 -0
  119. package/dist/public/operations/impact-review-preparation.js +379 -0
  120. package/dist/public/operations/index.d.ts +2 -0
  121. package/dist/public/operations/index.d.ts.map +1 -1
  122. package/dist/public/operations/index.js +2 -0
  123. package/dist/public/operations/node-ports.d.ts.map +1 -1
  124. package/dist/public/operations/node-ports.js +1 -35
  125. package/dist/public/operations/proof-receipt-projection.d.ts +5 -0
  126. package/dist/public/operations/proof-receipt-projection.d.ts.map +1 -0
  127. package/dist/public/operations/proof-receipt-projection.js +36 -0
  128. package/dist/public/operations/specs/bootstrap.d.ts +73 -1
  129. package/dist/public/operations/specs/bootstrap.d.ts.map +1 -1
  130. package/dist/public/operations/specs/bootstrap.js +66 -1
  131. package/dist/public/operations/specs/impact-review-preparation.d.ts +47 -0
  132. package/dist/public/operations/specs/impact-review-preparation.d.ts.map +1 -0
  133. package/dist/public/operations/specs/impact-review-preparation.js +43 -0
  134. package/dist/public/operations/symbol-repair-plan.d.ts.map +1 -1
  135. package/dist/public/operations/symbol-repair-plan.js +3 -1
  136. package/dist/public/operations/types.d.ts +1 -1
  137. package/dist/public/operations/types.d.ts.map +1 -1
  138. package/dist/public/skills/kibi-bootstrap/SKILL.md +47 -11
  139. package/dist/public/skills/kibi-bootstrap/resources/bootstrap.md +6 -1
  140. package/dist/public/skills/kibi-usage/SKILL.md +14 -0
  141. package/dist/public/skills/kibi-usage/resources/operation-access.md +5 -0
  142. package/dist/public/telemetry-acceptance.d.ts +8 -0
  143. package/dist/public/telemetry-acceptance.d.ts.map +1 -1
  144. package/dist/public/telemetry-acceptance.js +10 -0
  145. package/dist/traceability/analysis-gate.d.ts +39 -0
  146. package/dist/traceability/analysis-gate.d.ts.map +1 -0
  147. package/dist/traceability/analysis-gate.js +51 -0
  148. package/dist/traceability/git-change-snapshot.d.ts +27 -0
  149. package/dist/traceability/git-change-snapshot.d.ts.map +1 -0
  150. package/dist/traceability/git-change-snapshot.js +411 -0
  151. package/dist/traceability/git-staged.d.ts +5 -1
  152. package/dist/traceability/git-staged.d.ts.map +1 -1
  153. package/dist/traceability/git-staged.js +16 -11
  154. package/dist/traceability/impact-evaluator.d.ts +54 -0
  155. package/dist/traceability/impact-evaluator.d.ts.map +1 -0
  156. package/dist/traceability/impact-evaluator.js +270 -0
  157. package/dist/traceability/impact-git-tree.d.ts +12 -0
  158. package/dist/traceability/impact-git-tree.d.ts.map +1 -0
  159. package/dist/traceability/impact-git-tree.js +46 -0
  160. package/dist/traceability/impact-knowledge.d.ts +16 -0
  161. package/dist/traceability/impact-knowledge.d.ts.map +1 -0
  162. package/dist/traceability/impact-knowledge.js +165 -0
  163. package/dist/traceability/impact-policy.d.ts +15 -0
  164. package/dist/traceability/impact-policy.d.ts.map +1 -0
  165. package/dist/traceability/impact-policy.js +48 -0
  166. package/dist/traceability/impact-policy.v1.schema.json +48 -0
  167. package/dist/traceability/impact-pr-diff.d.ts +5 -0
  168. package/dist/traceability/impact-pr-diff.d.ts.map +1 -0
  169. package/dist/traceability/impact-pr-diff.js +83 -0
  170. package/dist/traceability/impact-record-validation.d.ts +9 -0
  171. package/dist/traceability/impact-record-validation.d.ts.map +1 -0
  172. package/dist/traceability/impact-record-validation.js +113 -0
  173. package/dist/traceability/impact-review.d.ts +148 -0
  174. package/dist/traceability/impact-review.d.ts.map +1 -0
  175. package/dist/traceability/impact-review.js +494 -0
  176. package/dist/traceability/impact-review.schema.json +365 -0
  177. package/dist/traceability/impact-side-evidence.d.ts +13 -0
  178. package/dist/traceability/impact-side-evidence.d.ts.map +1 -0
  179. package/dist/traceability/impact-side-evidence.js +240 -0
  180. package/dist/traceability/reviewed-coordinate-refresh.d.ts +10 -0
  181. package/dist/traceability/reviewed-coordinate-refresh.d.ts.map +1 -0
  182. package/dist/traceability/reviewed-coordinate-refresh.js +49 -0
  183. package/dist/traceability/snapshot-knowledge.d.ts +4 -0
  184. package/dist/traceability/snapshot-knowledge.d.ts.map +1 -0
  185. package/dist/traceability/snapshot-knowledge.js +98 -0
  186. package/dist/traceability/staged-file-coverage.d.ts +23 -0
  187. package/dist/traceability/staged-file-coverage.d.ts.map +1 -1
  188. package/dist/traceability/staged-impact-contract.d.ts.map +1 -1
  189. package/dist/traceability/staged-impact-contract.js +2 -16
  190. package/dist/traceability/staged-symbols-manifest.d.ts +4 -0
  191. package/dist/traceability/staged-symbols-manifest.d.ts.map +1 -1
  192. package/dist/traceability/staged-symbols-manifest.js +17 -6
  193. package/dist/traceability/symbol-extract.d.ts +3 -5
  194. package/dist/traceability/symbol-extract.d.ts.map +1 -1
  195. package/dist/traceability/symbol-extract.js +49 -34
  196. package/dist/traceability/temp-kb.d.ts +1 -1
  197. package/dist/traceability/temp-kb.d.ts.map +1 -1
  198. package/dist/traceability/temp-kb.js +49 -13
  199. package/dist/traceability/trusted-pr-event.d.ts +13 -0
  200. package/dist/traceability/trusted-pr-event.d.ts.map +1 -0
  201. package/dist/traceability/trusted-pr-event.js +105 -0
  202. package/dist/utils/canonical-path.d.ts +3 -0
  203. package/dist/utils/canonical-path.d.ts.map +1 -0
  204. package/dist/utils/canonical-path.js +33 -0
  205. package/package.json +14 -5
  206. package/src/public/diagnostic-usage.ts +59 -1
  207. package/src/public/impact/analyzer.ts +88 -5
  208. package/src/public/impact/diagnostics.ts +7 -5
  209. package/src/public/impact/manifest.ts +14 -1
  210. package/src/public/impact/source-changes.ts +39 -34
  211. package/src/public/impact/types.ts +5 -0
  212. package/src/public/operations/catalog.ts +2 -0
  213. package/src/public/operations/contracts.ts +385 -9
  214. package/src/public/operations/impact-review-preparation.ts +548 -0
  215. package/src/public/operations/index.ts +2 -0
  216. package/src/public/operations/node-ports.ts +1 -39
  217. package/src/public/operations/proof-receipt-projection.ts +39 -0
  218. package/src/public/operations/specs/bootstrap.ts +78 -4
  219. package/src/public/operations/specs/impact-review-preparation.ts +46 -0
  220. package/src/public/operations/symbol-repair-plan.ts +3 -1
  221. package/src/public/operations/types.ts +1 -0
  222. package/src/public/skills/kibi-bootstrap/SKILL.md +47 -11
  223. package/src/public/skills/kibi-bootstrap/resources/bootstrap.md +6 -1
  224. package/src/public/skills/kibi-usage/SKILL.md +14 -0
  225. package/src/public/skills/kibi-usage/resources/operation-access.md +5 -0
  226. package/src/public/telemetry-acceptance.ts +9 -0
  227. package/src/traceability/impact-policy.v1.schema.json +48 -0
  228. package/src/traceability/impact-review.schema.json +365 -0
  229. package/templates/github/kibi-badge.yml +0 -4
  230. package/templates/github/kibi-report.yml +0 -4
@@ -0,0 +1,105 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { readFileSync } from "node:fs";
3
+ const OBJECT_ID = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
4
+ function runGit(root, args) {
5
+ return execFileSync("git", [...args], {
6
+ cwd: root,
7
+ encoding: "utf8",
8
+ maxBuffer: 16 * 1024 * 1024,
9
+ stdio: ["ignore", "pipe", "ignore"],
10
+ }).trim();
11
+ }
12
+ function resolveCommit(root, input) {
13
+ if (!OBJECT_ID.test(input))
14
+ throw new Error("PR event contains an invalid Git object ID");
15
+ const resolved = runGit(root, [
16
+ "rev-parse",
17
+ "--verify",
18
+ "--end-of-options",
19
+ `${input}^{commit}`,
20
+ ]);
21
+ if (!OBJECT_ID.test(resolved))
22
+ throw new Error("Git returned an invalid commit ID");
23
+ return resolved;
24
+ }
25
+ function resolveTree(root, commit) {
26
+ const tree = runGit(root, [
27
+ "rev-parse",
28
+ "--verify",
29
+ "--end-of-options",
30
+ `${commit}^{tree}`,
31
+ ]);
32
+ if (!OBJECT_ID.test(tree))
33
+ throw new Error("Git returned an invalid tree ID");
34
+ return tree;
35
+ }
36
+ /** Verify a GitHub pull_request_target envelope against the protected checkout. */
37
+ export function resolveTrustedPullRequestSnapshot(workspaceRoot, environment = process.env) {
38
+ if (environment.GITHUB_EVENT_NAME !== "pull_request_target")
39
+ throw new Error("Trusted impact gate requires pull_request_target context");
40
+ const eventPath = environment.GITHUB_EVENT_PATH;
41
+ const repository = environment.GITHUB_REPOSITORY;
42
+ const trustedTargetRef = environment.KIBI_IMPACT_TRUSTED_TARGET_REF;
43
+ const githubSha = environment.GITHUB_SHA;
44
+ if (!eventPath || !repository || !trustedTargetRef || !githubSha)
45
+ throw new Error("Trusted PR event, repository, target ref and GitHub event SHA are required");
46
+ if (!/^[^/]+\/[^/]+$/.test(repository))
47
+ throw new Error("GitHub repository identity is invalid");
48
+ if (!/^refs\/heads\/[A-Za-z0-9._/-]+$/.test(trustedTargetRef))
49
+ throw new Error("Trusted target must be a full refs/heads/... name");
50
+ const payload = JSON.parse(readFileSync(eventPath, "utf8"));
51
+ const request = payload.pull_request;
52
+ const base = request?.base;
53
+ const head = request?.head;
54
+ const baseRef = base?.ref;
55
+ const targetInput = base?.sha;
56
+ const headInput = head?.sha;
57
+ if (typeof baseRef !== "string" ||
58
+ typeof targetInput !== "string" ||
59
+ typeof headInput !== "string" ||
60
+ base?.repo?.full_name !== repository)
61
+ throw new Error("PR event does not identify this repository's base and head");
62
+ if (trustedTargetRef !== `refs/heads/${baseRef}`)
63
+ throw new Error("PR target does not match the configured protected branch");
64
+ if (payload.repository?.full_name !== repository)
65
+ throw new Error("PR event repository identity does not match the trusted checkout");
66
+ const targetCommit = resolveCommit(workspaceRoot, targetInput);
67
+ const headCommit = resolveCommit(workspaceRoot, headInput);
68
+ if (resolveCommit(workspaceRoot, githubSha) !== targetCommit)
69
+ throw new Error("GitHub event target SHA does not match its trusted payload");
70
+ const checkoutCommit = resolveCommit(workspaceRoot, runGit(workspaceRoot, ["rev-parse", "HEAD"]));
71
+ if (checkoutCommit !== targetCommit)
72
+ throw new Error("Trusted evaluator checkout is not the PR target commit");
73
+ if (runGit(workspaceRoot, ["rev-parse", "--is-shallow-repository"]) !== "false")
74
+ throw new Error("Complete Git history is required for impact evaluation");
75
+ const missing = runGit(workspaceRoot, [
76
+ "rev-list",
77
+ "--objects",
78
+ "--missing=print",
79
+ targetCommit,
80
+ headCommit,
81
+ ]);
82
+ if (missing.split("\n").some((line) => line.startsWith("?")))
83
+ throw new Error("PR target or head has missing reachable Git objects");
84
+ const mergeBases = runGit(workspaceRoot, [
85
+ "merge-base",
86
+ "--all",
87
+ targetCommit,
88
+ headCommit,
89
+ ])
90
+ .split("\n")
91
+ .filter(Boolean);
92
+ if (mergeBases.length !== 1)
93
+ throw new Error("PR target and head must have exactly one merge base");
94
+ const mergeBaseCommit = resolveCommit(workspaceRoot, mergeBases[0] ?? "");
95
+ return {
96
+ repository,
97
+ targetRef: trustedTargetRef,
98
+ targetCommit,
99
+ targetTree: resolveTree(workspaceRoot, targetCommit),
100
+ headCommit,
101
+ headTree: resolveTree(workspaceRoot, headCommit),
102
+ mergeBaseCommit,
103
+ mergeBaseTree: resolveTree(workspaceRoot, mergeBaseCommit),
104
+ };
105
+ }
@@ -0,0 +1,3 @@
1
+ /** Resolve filesystem aliases while preserving a suffix that does not exist yet. */
2
+ export declare function canonicalFilesystemPath(candidate: string): string;
3
+ //# sourceMappingURL=canonical-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical-path.d.ts","sourceRoot":"","sources":["../../src/utils/canonical-path.ts"],"names":[],"mappings":"AAGA,oFAAoF;AACpF,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAwBjE"}
@@ -0,0 +1,33 @@
1
+ import { lstatSync, realpathSync } from "node:fs";
2
+ import path from "node:path";
3
+ /** Resolve filesystem aliases while preserving a suffix that does not exist yet. */
4
+ export function canonicalFilesystemPath(candidate) {
5
+ let existing = path.resolve(candidate);
6
+ const missing = [];
7
+ for (;;) {
8
+ try {
9
+ return path.resolve(realpathSync.native(existing), ...missing);
10
+ }
11
+ catch (error) {
12
+ if (error.code !== "ENOENT")
13
+ throw error;
14
+ // A dangling symlink is an existing entry, not a missing path segment.
15
+ // Refuse it rather than allowing a later write to follow an unknown target.
16
+ let entry;
17
+ try {
18
+ entry = lstatSync(existing);
19
+ }
20
+ catch (statError) {
21
+ if (statError.code !== "ENOENT")
22
+ throw statError;
23
+ }
24
+ if (entry !== undefined)
25
+ throw error;
26
+ const parent = path.dirname(existing);
27
+ if (parent === existing)
28
+ throw error;
29
+ missing.unshift(path.basename(existing));
30
+ existing = parent;
31
+ }
32
+ }
33
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-cli",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "type": "module",
5
5
  "description": "Kibi CLI for knowledge base management",
6
6
  "engines": {
@@ -10,7 +10,15 @@
10
10
  "bin": {
11
11
  "kibi": "bin/kibi"
12
12
  },
13
- "files": ["dist", "bin", "schema", "src/schemas", "src/public", "templates"],
13
+ "files": [
14
+ "dist",
15
+ "bin",
16
+ "schema",
17
+ "src/schemas",
18
+ "src/public",
19
+ "src/traceability/*.schema.json",
20
+ "templates"
21
+ ],
14
22
  "scripts": {
15
23
  "build": "tsc -p tsconfig.json && rm -rf dist/public/skills && mkdir -p dist/public/skills dist/report && cp -r src/public/skills/. dist/public/skills/ && cp src/report/html-report.css dist/report/html-report.css && node ../../scripts/generate-entity-schema.mjs --check",
16
24
  "test": "node ../../scripts/run-owned-engine-tests.mjs -- bun test --timeout 15000 .",
@@ -227,9 +235,10 @@
227
235
  "fast-glob": "^3.2.12",
228
236
  "gray-matter": "^4.0.3",
229
237
  "js-yaml": "^4.1.0",
230
- "kibi-core": "^0.14.0",
231
- "kibi-plugin-builtin": "^0.3.0",
232
- "kibi-plugin-sdk": "^0.3.0",
238
+ "kibi-core": "^0.14.1",
239
+ "kibi-plugin-builtin": "^0.4.0",
240
+ "kibi-plugin-sdk": "^0.4.0",
241
+ "kibi-swipl": "^1.0.0",
233
242
  "saxes": "^6.0.0",
234
243
  "ignore": "^5.3.0",
235
244
  "yaml": "^2.9.0"
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { appendFileSync, mkdirSync } from "node:fs";
2
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
3
3
  import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
 
5
6
  import {
6
7
  appendPayloadCountField,
@@ -241,6 +242,60 @@ export function deriveDiagnosticUsageFields(
241
242
  return fields;
242
243
  }
243
244
 
245
+ /**
246
+ * Whether the operator opted into usage telemetry through the environment.
247
+ *
248
+ * `KIBI_DIAGNOSTIC_MODE` is the one opt-in shared with the MCP server, so a
249
+ * single setting covers both peer surfaces. `KIBI_CLI_DIAGNOSTIC_MODE` is the
250
+ * older CLI-only spelling and keeps working.
251
+ */
252
+ export function diagnosticEnvOptIn(
253
+ env: NodeJS.ProcessEnv = process.env,
254
+ ): boolean {
255
+ return ["KIBI_DIAGNOSTIC_MODE", "KIBI_CLI_DIAGNOSTIC_MODE"].some((key) => {
256
+ const value = env[key]?.trim().toLowerCase();
257
+ return value === "1" || value === "true";
258
+ });
259
+ }
260
+
261
+ /**
262
+ * The host that ran this CLI call. Hosts that launch Kibi set `KIBI_HOST`;
263
+ * Claude Code marks every shell it runs with `CLAUDECODE=1`, which is the
264
+ * only ambient signal trusted here. Anything else stays `unknown`.
265
+ */
266
+ export function diagnosticCliHost(
267
+ env: NodeJS.ProcessEnv = process.env,
268
+ ): string {
269
+ const explicit = env.KIBI_HOST?.trim() || env.KIBI_MCP_HOST?.trim();
270
+ if (explicit) return explicit;
271
+ return env.CLAUDECODE === "1" ? "claude-code" : "unknown";
272
+ }
273
+
274
+ let cachedCliPackageVersion: string | null | undefined;
275
+
276
+ function cliPackageVersion(): string | null {
277
+ if (cachedCliPackageVersion !== undefined) return cachedCliPackageVersion;
278
+ cachedCliPackageVersion = null;
279
+ let current = path.dirname(fileURLToPath(import.meta.url));
280
+ while (true) {
281
+ const candidate = path.join(current, "package.json");
282
+ if (existsSync(candidate)) {
283
+ try {
284
+ const parsed: unknown = JSON.parse(readFileSync(candidate, "utf8"));
285
+ if (isRecord(parsed) && typeof parsed.version === "string") {
286
+ cachedCliPackageVersion = parsed.version;
287
+ }
288
+ } catch {
289
+ // An unreadable manifest leaves the version unknown.
290
+ }
291
+ return cachedCliPackageVersion;
292
+ }
293
+ const parent = path.dirname(current);
294
+ if (parent === current) return null;
295
+ current = parent;
296
+ }
297
+ }
298
+
244
299
  export function appendCliDiagnosticUsage(input: DiagnosticUsageInput): void {
245
300
  const finishedAt = input.finishedAt ?? new Date();
246
301
  const requestId = input.requestId ?? `cli-${randomUUID()}`;
@@ -256,6 +311,9 @@ export function appendCliDiagnosticUsage(input: DiagnosticUsageInput): void {
256
311
  request_id: requestId,
257
312
  tool: input.tool,
258
313
  interface: "cli_json",
314
+ host: diagnosticCliHost(),
315
+ package_version: cliPackageVersion(),
316
+ workspace_root: input.workspaceRoot,
259
317
  telemetry: input.telemetry,
260
318
  business_args: input.businessArgs,
261
319
  status: input.status,
@@ -1,4 +1,14 @@
1
1
  import * as path from "node:path";
2
+ import {
3
+ createMaintenanceSourceAnalysisService,
4
+ readSnapshotSourceConfig,
5
+ } from "../../plugins/maintenance-source-analysis.js";
6
+ import {
7
+ analyzeSourceChanges,
8
+ assertSourceAnalysisGate,
9
+ } from "../../plugins/source-change-analysis.js";
10
+ import { captureStagedSnapshot } from "../../traceability/git-change-snapshot.js";
11
+ import { readSnapshotKnowledge } from "../../traceability/snapshot-knowledge.js";
2
12
  import type { ExtractedSymbol } from "../../traceability/symbol-extract.js";
3
13
  import { extractSymbolsFromStagedFileAsync } from "../../traceability/symbol-extract.js";
4
14
  import {
@@ -9,6 +19,7 @@ import {
9
19
  createImpactManifestLookup,
10
20
  readImpactManifestResults,
11
21
  } from "./manifest.js";
22
+ import { normalizeSourceFile } from "./source-changes.js";
12
23
  import { collectSourceChanges, uniqueSorted } from "./source-changes.js";
13
24
  import {
14
25
  buildNextActions,
@@ -25,16 +36,73 @@ export async function analyzeChangedFileImpact(
25
36
  options: ChangedFileImpactOptions,
26
37
  ): Promise<ChangedFileImpactResult> {
27
38
  const workspaceRoot = path.resolve(options.workspaceRoot);
28
- const sourceChanges = collectSourceChanges({ ...options, workspaceRoot });
29
- const sourceFiles = uniqueSorted(sourceChanges.map((change) => change.file));
30
- const manifestResults = readImpactManifestResults(workspaceRoot);
39
+ const snapshot = options.staged
40
+ ? captureStagedSnapshot(workspaceRoot)
41
+ : undefined;
42
+ const filter = new Set(
43
+ (options.sourceFiles ?? []).map((file) =>
44
+ normalizeSourceFile(workspaceRoot, file),
45
+ ),
46
+ );
47
+ const snapshotAnalysisInventory = snapshot
48
+ ? snapshot.inventory.filter(
49
+ (file) => filter.size === 0 || filter.has(file.path),
50
+ )
51
+ : undefined;
52
+ const snapshotSourceInventory = snapshot
53
+ ? snapshot.inventory.filter(
54
+ (file) =>
55
+ file.analysisDepth !== "metadata" &&
56
+ !file.skipReason &&
57
+ file.content !== undefined &&
58
+ (filter.size === 0 || filter.has(file.path)),
59
+ )
60
+ : undefined;
61
+ const sourceChanges = snapshot
62
+ ? (snapshotSourceInventory ?? []).map((file) => ({
63
+ file: file.path,
64
+ status: file.status,
65
+ hunkRanges: file.hunkRanges,
66
+ content: file.content ?? "",
67
+ }))
68
+ : collectSourceChanges({ ...options, workspaceRoot });
69
+ const service = createMaintenanceSourceAnalysisService(
70
+ workspaceRoot,
71
+ snapshot ? readSnapshotSourceConfig(snapshot) : undefined,
72
+ );
73
+ const analyses = snapshot
74
+ ? await analyzeSourceChanges(snapshotAnalysisInventory ?? [], service)
75
+ : undefined;
76
+ if (analyses)
77
+ assertSourceAnalysisGate(snapshotAnalysisInventory ?? [], analyses);
78
+ const sourceFiles = uniqueSorted(
79
+ snapshot
80
+ ? snapshot.inventory
81
+ .filter(
82
+ (file) =>
83
+ file.analysisDepth !== "metadata" &&
84
+ (filter.size === 0 || filter.has(file.path)),
85
+ )
86
+ .map((file) => file.path)
87
+ : sourceChanges.map((change) => change.file),
88
+ );
89
+ const manifestResults = snapshot
90
+ ? readSnapshotKnowledge(
91
+ snapshot.readGit,
92
+ snapshot.headTree,
93
+ snapshot.readBlobs,
94
+ ).filter((result) => result.entity.type === "symbol")
95
+ : readImpactManifestResults(workspaceRoot);
31
96
  const manifestLookup = createImpactManifestLookup(manifestResults);
32
97
  const symbolsByFile = new Map<string, ExtractedSymbol[]>();
33
98
  const sourceContentByFile = new Map<string, string>();
99
+ const sourceSymbolsByFile = new Map<string, ExtractedSymbol[]>();
34
100
 
35
101
  for (const change of sourceChanges) {
36
102
  sourceContentByFile.set(change.file, change.content);
37
- // Maintenance path: builtin-only (do not pass registry / workspaceRoot).
103
+ const analysis =
104
+ analyses?.get(change.file)?.after ??
105
+ (await service.analyzeTextV2(change.file, change.content));
38
106
  const extracted = await extractSymbolsFromStagedFileAsync(
39
107
  {
40
108
  path: change.file,
@@ -43,8 +111,21 @@ export async function analyzeChangedFileImpact(
43
111
  content: change.content,
44
112
  },
45
113
  manifestLookup,
46
- {},
114
+ { analyzeText: async () => analysis },
115
+ );
116
+ const allSymbols = await extractSymbolsFromStagedFileAsync(
117
+ {
118
+ path: change.file,
119
+ status: change.status,
120
+ content: change.content,
121
+ hunkRanges: [
122
+ { start: 1, end: Math.max(1, change.content.split("\n").length) },
123
+ ],
124
+ },
125
+ manifestLookup,
126
+ { analyzeText: async () => analysis },
47
127
  );
128
+ sourceSymbolsByFile.set(change.file, allSymbols);
48
129
  symbolsByFile.set(
49
130
  change.file,
50
131
  extracted.filter((symbol) => symbol.hunkRanges.length > 0),
@@ -65,6 +146,7 @@ export async function analyzeChangedFileImpact(
65
146
  manifestResults: activeManifestResults,
66
147
  symbolsByFile,
67
148
  sourceContentByFile,
149
+ sourceSymbolsByFile,
68
150
  workspaceRoot,
69
151
  }),
70
152
  ...createSymbolQualityDiagnostics({
@@ -79,6 +161,7 @@ export async function analyzeChangedFileImpact(
79
161
  ? impactDiagnostics.slice(0, maxDiagnostics)
80
162
  : impactDiagnostics;
81
163
 
164
+ snapshot?.assertUnchanged();
82
165
  return {
83
166
  impactDiagnostics: cappedDiagnostics,
84
167
  sourceFiles,
@@ -74,11 +74,13 @@ function getGranularSymbolsForSourceFile(
74
74
  readonly candidateSymbols: readonly ExtractedSymbol[];
75
75
  readonly sourceSymbols: readonly ExtractedSymbol[];
76
76
  } {
77
- const sourceSymbols = readSourceSymbols(
78
- sourceFile,
79
- options.sourceContentByFile,
80
- options.workspaceRoot ?? process.cwd(),
81
- );
77
+ const sourceSymbols = options.sourceSymbolsByFile
78
+ ? (options.sourceSymbolsByFile.get(sourceFile) ?? [])
79
+ : readSourceSymbols(
80
+ sourceFile,
81
+ options.sourceContentByFile,
82
+ options.workspaceRoot ?? process.cwd(),
83
+ );
82
84
  const changedSymbols = options.symbolsByFile.get(sourceFile);
83
85
 
84
86
  return {
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { extractFromManifest } from "../../extractors/manifest.js";
4
4
  import type { ExtractionResult } from "../../extractors/markdown.js";
5
+ import { createManifestLookupSentinelKey } from "../../traceability/symbol-extract.js";
5
6
  import type { ManifestLookup } from "../../traceability/symbol-extract.js";
6
7
  import { CANONICAL_ENTITY_PATHS } from "../../utils/kb-paths.js";
7
8
  import { isTraceabilityRelationshipType } from "../symbol-granularity.js";
@@ -42,10 +43,22 @@ export function createImpactManifestLookup(
42
43
  manifestResults: readonly ExtractionResult[],
43
44
  ): ManifestLookup {
44
45
  const manifestLookup: ManifestLookup = new Map();
46
+ manifestLookup.set(createManifestLookupSentinelKey(".kb/symbols.yaml"), {
47
+ id: "__captured_manifest__",
48
+ relationships: [],
49
+ });
50
+ const ambiguous = new Set<string>();
45
51
 
46
52
  for (const result of manifestResults) {
47
53
  const sourceFile = result.sourceFile ?? result.entity.source;
48
- manifestLookup.set(`${sourceFile}:${result.entity.title}`, {
54
+ const key = `${sourceFile}:${result.entity.title}`;
55
+ if (ambiguous.has(key)) continue;
56
+ if (manifestLookup.has(key)) {
57
+ manifestLookup.delete(key);
58
+ ambiguous.add(key);
59
+ continue;
60
+ }
61
+ manifestLookup.set(key, {
49
62
  id: result.entity.id,
50
63
  relationships: toTraceabilityRelationships(result.relationships),
51
64
  });
@@ -1,6 +1,7 @@
1
- import { execSync } from "node:child_process";
2
- import { existsSync, readFileSync } from "node:fs";
1
+ import { execFileSync, execSync } from "node:child_process";
2
+ import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
3
3
  import * as path from "node:path";
4
+ import { isQualifiedSourcePath } from "../../plugins/source-classification.js";
4
5
  import type { HunkRange } from "../../traceability/git-staged.js";
5
6
  import {
6
7
  getStagedFiles,
@@ -9,19 +10,8 @@ import {
9
10
  import { hasMeaningfulSourceDiff } from "./diff-meaning.js";
10
11
  import type { ChangedFileImpactOptions, SourceChange } from "./types.js";
11
12
 
12
- const SOURCE_EXTENSIONS = new Set([
13
- ".ts",
14
- ".tsx",
15
- ".js",
16
- ".jsx",
17
- ".mts",
18
- ".cts",
19
- ".mjs",
20
- ".cjs",
21
- ]);
22
-
23
13
  function isSupportedSourcePath(filePath: string): boolean {
24
- return SOURCE_EXTENSIONS.has(path.extname(filePath));
14
+ return isQualifiedSourcePath(filePath);
25
15
  }
26
16
 
27
17
  export function normalizeSourceFile(
@@ -38,12 +28,12 @@ export function uniqueSorted(values: Iterable<string>): string[] {
38
28
  return [...new Set(values)].sort();
39
29
  }
40
30
 
41
- function escapeGitPath(filePath: string): string {
42
- return filePath.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
43
- }
44
-
45
- function runGit(workspaceRoot: string, command: string): string {
46
- return execSync(command, { cwd: workspaceRoot, encoding: "utf8" });
31
+ function runGit(workspaceRoot: string, args: readonly string[]): string {
32
+ return execFileSync("git", [...args], {
33
+ cwd: workspaceRoot,
34
+ encoding: "utf8",
35
+ env: { ...process.env, GIT_LITERAL_PATHSPECS: "1" },
36
+ });
47
37
  }
48
38
 
49
39
  function readWorkingTreeSource(
@@ -51,8 +41,24 @@ function readWorkingTreeSource(
51
41
  sourceFile: string,
52
42
  ): string | null {
53
43
  const absolutePath = path.resolve(workspaceRoot, sourceFile);
54
- if (!existsSync(absolutePath)) return null;
55
- return readFileSync(absolutePath, "utf8");
44
+ const relative = path.relative(workspaceRoot, absolutePath);
45
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative))
46
+ throw new Error("Source path must remain inside the workspace");
47
+ if (!existsSync(absolutePath) || !lstatSync(absolutePath).isFile())
48
+ return null;
49
+ const actualRelative = path.relative(
50
+ realpathSync(workspaceRoot),
51
+ realpathSync(absolutePath),
52
+ );
53
+ if (actualRelative.startsWith("..") || path.isAbsolute(actualRelative))
54
+ throw new Error("Source path escapes the workspace");
55
+ const bytes = readFileSync(absolutePath);
56
+ if (bytes.includes(0)) return null;
57
+ try {
58
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
59
+ } catch {
60
+ return null;
61
+ }
56
62
  }
57
63
 
58
64
  function fullFileHunk(content: string): HunkRange[] {
@@ -62,9 +68,9 @@ function fullFileHunk(content: string): HunkRange[] {
62
68
  function getWorkingTreeDiffSourceFiles(workspaceRoot: string): string[] {
63
69
  try {
64
70
  return uniqueSorted(
65
- runGit(workspaceRoot, "git diff --name-only --diff-filter=ACMR")
66
- .split(/\r?\n/)
67
- .map((filePath) => normalizeSourceFile(workspaceRoot, filePath.trim()))
71
+ runGit(workspaceRoot, ["diff", "--name-only", "-z", "--diff-filter=ACMR"])
72
+ .split("\0")
73
+ .map((filePath) => normalizeSourceFile(workspaceRoot, filePath))
68
74
  .filter((filePath) => filePath.length > 0)
69
75
  .filter(isSupportedSourcePath),
70
76
  );
@@ -78,10 +84,7 @@ function getWorkingTreeHunks(
78
84
  sourceFile: string,
79
85
  ): HunkRange[] {
80
86
  try {
81
- const diffText = runGit(
82
- workspaceRoot,
83
- `git diff -U0 -- "${escapeGitPath(sourceFile)}"`,
84
- );
87
+ const diffText = runGit(workspaceRoot, ["diff", "-U0", "--", sourceFile]);
85
88
  return parseHunksFromDiff(diffText);
86
89
  } catch {
87
90
  return [];
@@ -104,6 +107,7 @@ function getStagedSourceChanges(
104
107
  .flatMap((file): SourceChange[] => {
105
108
  if (file.content === undefined || file.hunkRanges.length === 0) return [];
106
109
  if (
110
+ !/\.(pyi?|go|rs)$/.test(file.path) &&
107
111
  file.diffText !== undefined &&
108
112
  !hasMeaningfulSourceDiff(file.diffText)
109
113
  ) {
@@ -132,11 +136,12 @@ function getWorkingTreeSourceChanges(
132
136
  return sourceFiles.flatMap((sourceFile): SourceChange[] => {
133
137
  const content = readWorkingTreeSource(workspaceRoot, sourceFile);
134
138
  if (content === null) return [];
135
- const diffText = runGit(
136
- workspaceRoot,
137
- `git diff -U0 -- "${escapeGitPath(sourceFile)}"`,
138
- );
139
- if (!hasMeaningfulSourceDiff(diffText)) return [];
139
+ const diffText = runGit(workspaceRoot, ["diff", "-U0", "--", sourceFile]);
140
+ if (
141
+ !/\.(pyi?|go|rs)$/.test(sourceFile) &&
142
+ !hasMeaningfulSourceDiff(diffText)
143
+ )
144
+ return [];
140
145
  const hunkRanges = getWorkingTreeHunks(workspaceRoot, sourceFile);
141
146
  if (hunkRanges.length === 0) return [];
142
147
  return [{ file: sourceFile, status: "M", hunkRanges, content }];
@@ -39,6 +39,11 @@ export type SymbolGranularityDiagnosticsOptions = {
39
39
  readonly activeEntityIds?: ReadonlySet<string>;
40
40
  readonly symbolsByFile: SymbolsByFile;
41
41
  readonly sourceContentByFile?: ReadonlyMap<string, string>;
42
+ /** Complete snapshot analysis; when supplied, filesystem fallback is forbidden. */
43
+ readonly sourceSymbolsByFile?: ReadonlyMap<
44
+ string,
45
+ readonly ExtractedSymbol[]
46
+ >;
42
47
  readonly workspaceRoot?: string;
43
48
  };
44
49
 
@@ -6,6 +6,7 @@ import {
6
6
  import { planBootstrapSpec } from "./specs/bootstrap.js";
7
7
  import { checkSpec } from "./specs/check.js";
8
8
  import { querySpec, searchSpec, statusSpec } from "./specs/discovery.js";
9
+ import { prepareImpactReviewSpec } from "./specs/impact-review-preparation.js";
9
10
  import {
10
11
  modelRequirementSpec,
11
12
  suggestPredicatesSpec,
@@ -49,6 +50,7 @@ export const OPERATION_CATALOG = [
49
50
  upsertSpec,
50
51
  deleteSpec,
51
52
  checkSpec,
53
+ prepareImpactReviewSpec,
52
54
  sparqlRemoteSpec,
53
55
  compileIntentSpec,
54
56
  applyPlanSpec,