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,548 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import {
3
+ createMaintenanceSourceAnalysisService,
4
+ fingerprintMaintenanceSourceSet,
5
+ readSnapshotSourceConfig,
6
+ } from "../../plugins/maintenance-source-analysis.js";
7
+ import { analyzeSourceChanges } from "../../plugins/source-change-analysis.js";
8
+ import type { SourceChangeAnalysis } from "../../plugins/source-change-analysis.js";
9
+ import { analysisObligation } from "../../traceability/analysis-gate.js";
10
+ import {
11
+ captureDiffSnapshot,
12
+ captureStagedSnapshot,
13
+ } from "../../traceability/git-change-snapshot.js";
14
+ import type { GitChangeSnapshot } from "../../traceability/git-change-snapshot.js";
15
+ import {
16
+ type PreparedImpactReview,
17
+ fingerprintImpactEvaluator,
18
+ prepareImpactReview,
19
+ } from "../../traceability/impact-evaluator.js";
20
+ import {
21
+ IMPACT_EVALUATOR_VERSION,
22
+ IMPACT_REVIEW_VERSION,
23
+ canonicalJson,
24
+ } from "../../traceability/impact-review.js";
25
+ import impactReviewRecordSchema from "../../traceability/impact-review.schema.json" with {
26
+ type: "json",
27
+ };
28
+ import { readSnapshotKnowledge } from "../../traceability/snapshot-knowledge.js";
29
+ import type { OperationContext } from "./runtime-types.js";
30
+ import type { OperationResult } from "./types.js";
31
+
32
+ const FULL_COMMIT_OID = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
33
+ const SEMANTIC_RELATIONSHIPS = new Set([
34
+ "constrains",
35
+ "depends_on",
36
+ "requires_predicate",
37
+ "requires_property",
38
+ "requires_rule",
39
+ "specified_by",
40
+ "supersedes",
41
+ "validates",
42
+ "verified_by",
43
+ ]);
44
+
45
+ function compareText(left: string, right: string): number {
46
+ return left < right ? -1 : left > right ? 1 : 0;
47
+ }
48
+
49
+ export type ImpactReviewPreparationScope =
50
+ | Readonly<{ kind: "staged" }>
51
+ | Readonly<{
52
+ kind: "diff";
53
+ baseCommit: string;
54
+ headCommit: string;
55
+ }>;
56
+
57
+ export type PrepareImpactReviewInput = Readonly<{
58
+ scope: ImpactReviewPreparationScope;
59
+ }>;
60
+
61
+ type SnapshotIdentity = Readonly<{
62
+ kind: "staged" | "diff";
63
+ baseCommit: string | null;
64
+ baseTree: string;
65
+ headCommit: string | null;
66
+ headTree: string;
67
+ headTreeSource: "index" | "commit";
68
+ }>;
69
+
70
+ type SnapshotEntity = Readonly<{
71
+ entity: Readonly<Record<string, unknown>>;
72
+ relationships: readonly Readonly<{
73
+ type: string;
74
+ from: string;
75
+ to: string;
76
+ }>[];
77
+ sourceFile?: string;
78
+ }>;
79
+
80
+ function gitText(root: string, args: readonly string[]): string {
81
+ return execFileSync("git", [...args], {
82
+ cwd: root,
83
+ encoding: "utf8",
84
+ maxBuffer: 64 * 1024 * 1024,
85
+ stdio: ["ignore", "pipe", "pipe"],
86
+ }).trim();
87
+ }
88
+
89
+ function resolveCommit(root: string, oid: string): string {
90
+ return gitText(root, [
91
+ "rev-parse",
92
+ "--verify",
93
+ "--end-of-options",
94
+ `${oid}^{commit}`,
95
+ ]);
96
+ }
97
+
98
+ function treeForCommit(root: string, oid: string): string {
99
+ return gitText(root, [
100
+ "rev-parse",
101
+ "--verify",
102
+ "--end-of-options",
103
+ `${oid}^{tree}`,
104
+ ]);
105
+ }
106
+
107
+ function assertExplicitDiffUnchanged(
108
+ root: string,
109
+ input: Extract<ImpactReviewPreparationScope, { kind: "diff" }>,
110
+ snapshot: GitChangeSnapshot,
111
+ ): void {
112
+ const baseCommit = resolveCommit(root, input.baseCommit);
113
+ const headCommit = resolveCommit(root, input.headCommit);
114
+ if (
115
+ baseCommit !== input.baseCommit ||
116
+ headCommit !== input.headCommit ||
117
+ snapshot.headCommit !== baseCommit ||
118
+ treeForCommit(root, baseCommit) !== snapshot.baseTree ||
119
+ treeForCommit(root, headCommit) !== snapshot.headTree
120
+ )
121
+ throw new Error(
122
+ "Selected diff commit or tree identity changed during impact review preparation",
123
+ );
124
+ }
125
+
126
+ function snapshotIdentity(
127
+ scope: ImpactReviewPreparationScope,
128
+ snapshot: GitChangeSnapshot,
129
+ ): SnapshotIdentity {
130
+ return {
131
+ kind: scope.kind,
132
+ baseCommit: scope.kind === "diff" ? scope.baseCommit : snapshot.headCommit,
133
+ baseTree: snapshot.baseTree,
134
+ headCommit: scope.kind === "diff" ? scope.headCommit : null,
135
+ headTree: snapshot.headTree,
136
+ headTreeSource: scope.kind === "diff" ? "commit" : "index",
137
+ };
138
+ }
139
+
140
+ function plainEntity(
141
+ entity: Readonly<Record<string, unknown>>,
142
+ ): Record<string, unknown> {
143
+ return Object.fromEntries(
144
+ Object.entries(entity)
145
+ .filter(
146
+ ([key]) =>
147
+ !["created_at", "updated_at", "proof_receipts"].includes(key),
148
+ )
149
+ .sort(([left], [right]) => compareText(left, right)),
150
+ );
151
+ }
152
+
153
+ function entityMap(
154
+ snapshot: GitChangeSnapshot,
155
+ tree: string,
156
+ ): Map<string, SnapshotEntity> {
157
+ return new Map(
158
+ readSnapshotKnowledge(snapshot.readGit, tree, snapshot.readBlobs).map(
159
+ (row) => [
160
+ row.entity.id,
161
+ {
162
+ entity: row.entity as unknown as Readonly<Record<string, unknown>>,
163
+ relationships: row.relationships.map(({ type, from, to }) => ({
164
+ type,
165
+ from,
166
+ to,
167
+ })),
168
+ ...(row.sourceFile ? { sourceFile: row.sourceFile } : {}),
169
+ },
170
+ ],
171
+ ),
172
+ );
173
+ }
174
+
175
+ function requirementContext(
176
+ id: string,
177
+ rows: ReadonlyMap<string, SnapshotEntity>,
178
+ entities: PreparedImpactReview["baseEntities"],
179
+ allRows: ReadonlyMap<string, SnapshotEntity>,
180
+ ): Record<string, unknown> {
181
+ const row = rows.get(id);
182
+ const binding = entities.get(id);
183
+ if (!row || !binding) {
184
+ return {
185
+ id,
186
+ state: "absent",
187
+ entityFingerprint: null,
188
+ artifactPath: null,
189
+ sourcePath: null,
190
+ relationships: [],
191
+ relatedEntities: [],
192
+ };
193
+ }
194
+ const relationships = [...allRows.entries()]
195
+ .flatMap(([ownerId, candidate]) =>
196
+ candidate.relationships
197
+ .filter(
198
+ (relationship) =>
199
+ SEMANTIC_RELATIONSHIPS.has(relationship.type) &&
200
+ (relationship.from === id || relationship.to === id),
201
+ )
202
+ .map((relationship) => ({ ownerId, ...relationship })),
203
+ )
204
+ .sort((left, right) =>
205
+ compareText(canonicalJson(left), canonicalJson(right)),
206
+ );
207
+ const relatedIds = [
208
+ ...new Set(
209
+ relationships
210
+ .flatMap(({ from, to }) => [from, to])
211
+ .filter((relatedId) => relatedId !== id),
212
+ ),
213
+ ].sort();
214
+ return {
215
+ id,
216
+ state: "present",
217
+ entityFingerprint: binding.fingerprint,
218
+ artifactPath: binding.artifactPath,
219
+ sourcePath: binding.sourcePath ?? row.sourceFile ?? null,
220
+ entity: plainEntity(row.entity),
221
+ relationships: relationships.map(({ ownerId: _ownerId, ...edge }) => edge),
222
+ relatedEntities: relatedIds.flatMap((relatedId) => {
223
+ const related = allRows.get(relatedId);
224
+ if (!related) return [];
225
+ const relatedBinding = entities.get(relatedId);
226
+ return [
227
+ {
228
+ id: relatedId,
229
+ type: related.entity.type ?? null,
230
+ entityFingerprint: relatedBinding?.fingerprint ?? null,
231
+ artifactPath: relatedBinding?.artifactPath ?? null,
232
+ sourcePath: relatedBinding?.sourcePath ?? related.sourceFile ?? null,
233
+ entity: plainEntity(related.entity),
234
+ },
235
+ ];
236
+ }),
237
+ };
238
+ }
239
+
240
+ function requirementScopes(
241
+ prepared: PreparedImpactReview,
242
+ snapshot: GitChangeSnapshot,
243
+ ): readonly Record<string, unknown>[] {
244
+ const before = entityMap(snapshot, snapshot.baseTree);
245
+ const after = entityMap(snapshot, snapshot.headTree);
246
+ return [...prepared.scopedRequirementIdsByPath.entries()]
247
+ .sort(([left], [right]) => compareText(left, right))
248
+ .map(([path, requirementIds]) => ({
249
+ path,
250
+ requirementIds: [...requirementIds].sort(),
251
+ before: [...requirementIds]
252
+ .sort()
253
+ .map((id) =>
254
+ requirementContext(id, before, prepared.baseEntities, before),
255
+ ),
256
+ after: [...requirementIds]
257
+ .sort()
258
+ .map((id) =>
259
+ requirementContext(id, after, prepared.headEntities, after),
260
+ ),
261
+ }));
262
+ }
263
+
264
+ function residualReviewObligations(
265
+ prepared: PreparedImpactReview,
266
+ ): readonly Record<string, unknown>[] {
267
+ const obligations: Record<string, unknown>[] = [];
268
+ for (const file of prepared.files) {
269
+ for (const side of ["before", "after"] as const) {
270
+ const analysis = file[side]?.analysis;
271
+ if (!analysis) continue;
272
+ const obligation = analysisObligation(side, analysis, file.newHunkRanges);
273
+ if (obligation.kind === "unsupported_review") {
274
+ obligations.push({
275
+ path: file.path,
276
+ side,
277
+ status: analysis.status,
278
+ diagnosticCodes: [...analysis.diagnosticCodes].sort(),
279
+ kind: "unsupported_review",
280
+ wholeFile: true,
281
+ pending: true,
282
+ });
283
+ continue;
284
+ }
285
+ if (obligation.kind !== "partial_review") continue;
286
+ const limitationClasses = new Set<string>();
287
+ for (const code of analysis.diagnosticCodes) {
288
+ const allowed = prepared.policy.allowedPartial.find(
289
+ (entry) =>
290
+ entry.providerId === analysis.providerId &&
291
+ entry.providerFingerprint === analysis.providerFingerprint &&
292
+ entry.diagnosticCode === code,
293
+ );
294
+ if (allowed) limitationClasses.add(allowed.limitationClass);
295
+ }
296
+ const limitationClass = [...limitationClasses][0];
297
+ if (limitationClasses.size !== 1 || limitationClass === undefined)
298
+ throw new Error(
299
+ `Partial review obligation is not covered by the trusted policy: ${file.path} (${side})`,
300
+ );
301
+ obligations.push({
302
+ path: file.path,
303
+ side,
304
+ status: analysis.status,
305
+ diagnosticCodes: [...analysis.diagnosticCodes].sort(),
306
+ kind: "partial_review",
307
+ limitationClass,
308
+ ranges: obligation.ranges.map((range) => ({ ...range })),
309
+ pending: true,
310
+ });
311
+ }
312
+ }
313
+ return obligations.sort((left, right) =>
314
+ compareText(`${left.path}\0${left.side}`, `${right.path}\0${right.side}`),
315
+ );
316
+ }
317
+
318
+ function cloneRecordSchema(): Record<string, unknown> {
319
+ return JSON.parse(JSON.stringify(impactReviewRecordSchema)) as Record<
320
+ string,
321
+ unknown
322
+ >;
323
+ }
324
+
325
+ function cloneRecordTemplateSchema(): Record<string, unknown> {
326
+ const schema = cloneRecordSchema();
327
+ schema.$id = "urn:kibi:impact-review-authoring-record-template:v1";
328
+ const properties = schema.properties as Record<string, unknown>;
329
+ properties.reviewedAt = { type: "null" };
330
+ const definitions = schema.$defs as Record<string, unknown>;
331
+ const reviewer = definitions.reviewer as {
332
+ properties: Record<string, unknown>;
333
+ };
334
+ reviewer.properties.id = { type: "null" };
335
+ const file = definitions.file as {
336
+ properties: Record<string, unknown>;
337
+ };
338
+ file.properties.analysisReviews = {
339
+ anyOf: [file.properties.analysisReviews, { type: "null" }],
340
+ };
341
+ file.properties.decision = {
342
+ anyOf: [file.properties.decision, { type: "null" }],
343
+ };
344
+ return schema;
345
+ }
346
+
347
+ function authoringContract(
348
+ prepared: PreparedImpactReview,
349
+ ): Record<string, unknown> {
350
+ const recordHeaders = {
351
+ contractVersion: IMPACT_REVIEW_VERSION,
352
+ policy: {
353
+ id: prepared.policy.id,
354
+ version: prepared.policy.version,
355
+ fingerprint: prepared.policyFingerprint,
356
+ },
357
+ evaluator: {
358
+ contractVersion: IMPACT_EVALUATOR_VERSION,
359
+ fingerprint: prepared.evaluatorFingerprint,
360
+ },
361
+ scope: {
362
+ fingerprint: prepared.scopeFingerprint,
363
+ knowledgeFingerprint: prepared.knowledgeFingerprint,
364
+ providerSetFingerprint: prepared.providerSetFingerprint,
365
+ },
366
+ };
367
+ return {
368
+ templateVersion: "kibi.impact-review-authoring-template.v1",
369
+ isValidImpactReviewRecord: false,
370
+ instructions:
371
+ "This template is reviewer input only. Author every decision, required analysis review, rationale, reviewer id, and reviewedAt before strict record validation. It is not an approval, signature, or proof receipt.",
372
+ authoredFields: [
373
+ "files[].decision",
374
+ "files[].analysisReviews",
375
+ "reviewer.id",
376
+ "reviewedAt",
377
+ ],
378
+ recordSchema: cloneRecordSchema(),
379
+ decisionSchema: { $ref: "urn:kibi:impact-review:v1#/$defs/decision" },
380
+ analysisReviewSchema: {
381
+ oneOf: [
382
+ { $ref: "urn:kibi:impact-review:v1#/$defs/unsupportedReview" },
383
+ { $ref: "urn:kibi:impact-review:v1#/$defs/partialReview" },
384
+ ],
385
+ },
386
+ templateSchema: {
387
+ $schema: "https://json-schema.org/draft/2020-12/schema",
388
+ $id: "urn:kibi:impact-review-authoring-template:v1",
389
+ type: "object",
390
+ additionalProperties: false,
391
+ required: ["templateVersion", "isValidImpactReviewRecord", "record"],
392
+ properties: {
393
+ templateVersion: {
394
+ const: "kibi.impact-review-authoring-template.v1",
395
+ },
396
+ isValidImpactReviewRecord: { const: false },
397
+ record: cloneRecordTemplateSchema(),
398
+ },
399
+ },
400
+ recordTemplate: {
401
+ templateVersion: "kibi.impact-review-authoring-template.v1",
402
+ isValidImpactReviewRecord: false,
403
+ record: {
404
+ ...recordHeaders,
405
+ reviewer: { id: null, source: "self-claimed-local" },
406
+ reviewedAt: null,
407
+ files: prepared.files
408
+ .slice()
409
+ .sort((left, right) => compareText(left.path, right.path))
410
+ .map((file) => ({
411
+ ...withoutAnalysisReviews(file),
412
+ analysisReviews: null,
413
+ decision: null,
414
+ })),
415
+ },
416
+ },
417
+ };
418
+ }
419
+
420
+ function withoutAnalysisReviews<T extends { analysisReviews: unknown }>(
421
+ file: T,
422
+ ): Omit<T, "analysisReviews"> {
423
+ const { analysisReviews: _analysisReviews, ...evidence } = file;
424
+ return evidence;
425
+ }
426
+
427
+ function completeFileEvidence(
428
+ prepared: PreparedImpactReview,
429
+ ): readonly Record<string, unknown>[] {
430
+ return prepared.files
431
+ .slice()
432
+ .sort((left, right) => compareText(left.path, right.path))
433
+ .map((file) => withoutAnalysisReviews(file));
434
+ }
435
+
436
+ function buildPreparation(
437
+ scope: ImpactReviewPreparationScope,
438
+ snapshot: GitChangeSnapshot,
439
+ prepared: PreparedImpactReview,
440
+ ): Record<string, unknown> {
441
+ return {
442
+ preparationVersion: "kibi.impact-review-preparation.v1",
443
+ status: "ready_for_authoring",
444
+ snapshot: snapshotIdentity(scope, snapshot),
445
+ recordHeaders: {
446
+ contractVersion: IMPACT_REVIEW_VERSION,
447
+ policy: {
448
+ id: prepared.policy.id,
449
+ version: prepared.policy.version,
450
+ fingerprint: prepared.policyFingerprint,
451
+ },
452
+ evaluator: {
453
+ contractVersion: IMPACT_EVALUATOR_VERSION,
454
+ fingerprint: prepared.evaluatorFingerprint,
455
+ },
456
+ scope: {
457
+ fingerprint: prepared.scopeFingerprint,
458
+ knowledgeFingerprint: prepared.knowledgeFingerprint,
459
+ providerSetFingerprint: prepared.providerSetFingerprint,
460
+ },
461
+ },
462
+ files: completeFileEvidence(prepared),
463
+ requirementScopes: requirementScopes(prepared, snapshot),
464
+ residualReviewObligations: residualReviewObligations(prepared),
465
+ authorship: authoringContract(prepared),
466
+ };
467
+ }
468
+
469
+ function validateDiffInput(scope: ImpactReviewPreparationScope): void {
470
+ if (
471
+ scope.kind === "diff" &&
472
+ (!FULL_COMMIT_OID.test(scope.baseCommit) ||
473
+ !FULL_COMMIT_OID.test(scope.headCommit))
474
+ )
475
+ throw new Error(
476
+ "Diff scope requires full lowercase hexadecimal baseCommit and headCommit object IDs",
477
+ );
478
+ }
479
+
480
+ export async function executePrepareImpactReview(
481
+ input: PrepareImpactReviewInput,
482
+ context: OperationContext,
483
+ ): Promise<OperationResult<Record<string, unknown>>> {
484
+ validateDiffInput(input.scope);
485
+ const workspaceRoot = context.workspaceRoot;
486
+ const snapshot =
487
+ input.scope.kind === "staged"
488
+ ? captureStagedSnapshot(workspaceRoot)
489
+ : captureDiffSnapshot(
490
+ workspaceRoot,
491
+ input.scope.baseCommit,
492
+ input.scope.headCommit,
493
+ );
494
+ if (input.scope.kind === "diff")
495
+ assertExplicitDiffUnchanged(workspaceRoot, input.scope, snapshot);
496
+
497
+ const trustedConfig = readSnapshotSourceConfig(snapshot, snapshot.baseTree);
498
+ const providerSetFingerprint = fingerprintMaintenanceSourceSet(
499
+ workspaceRoot,
500
+ trustedConfig,
501
+ );
502
+ const evaluatorFingerprint = fingerprintImpactEvaluator();
503
+ const service = createMaintenanceSourceAnalysisService(
504
+ workspaceRoot,
505
+ trustedConfig,
506
+ );
507
+ const analyses: ReadonlyMap<string, SourceChangeAnalysis> =
508
+ await analyzeSourceChanges(snapshot.inventory, service);
509
+ const prepared = prepareImpactReview(snapshot, {
510
+ providerSetFingerprint,
511
+ evaluatorFingerprint,
512
+ analyses,
513
+ });
514
+
515
+ // Build all KB-derived context before the final snapshot/closure checks.
516
+ // Projection can be substantial, so checking only before it could return
517
+ // evidence for a workspace or evaluator closure that changed mid-projection.
518
+ const data = buildPreparation(input.scope, snapshot, prepared);
519
+
520
+ snapshot.assertUnchanged();
521
+ if (input.scope.kind === "diff")
522
+ assertExplicitDiffUnchanged(workspaceRoot, input.scope, snapshot);
523
+ const finalProviderFingerprint = fingerprintMaintenanceSourceSet(
524
+ workspaceRoot,
525
+ trustedConfig,
526
+ );
527
+ const finalEvaluatorFingerprint = fingerprintImpactEvaluator();
528
+ if (
529
+ finalProviderFingerprint !== providerSetFingerprint ||
530
+ finalEvaluatorFingerprint !== evaluatorFingerprint
531
+ )
532
+ throw new Error(
533
+ "Trusted source-provider or impact-evaluator closure changed during impact review preparation",
534
+ );
535
+ snapshot.assertUnchanged();
536
+ if (input.scope.kind === "diff")
537
+ assertExplicitDiffUnchanged(workspaceRoot, input.scope, snapshot);
538
+
539
+ return {
540
+ content: [
541
+ {
542
+ type: "text",
543
+ text: `Prepared complete ${input.scope.kind} impact-review authoring inputs; every review decision remains unauthored.`,
544
+ },
545
+ ],
546
+ structuredContent: data,
547
+ };
548
+ }
@@ -1,5 +1,6 @@
1
1
  export * from "./catalog.js";
2
2
  export * from "./check-executor.js";
3
+ export * from "./impact-review-preparation.js";
3
4
  export * from "./discovery-entities.js";
4
5
  export * from "./discovery-executors.js";
5
6
  export * from "./runtime-types.js";
@@ -10,6 +11,7 @@ export * from "./migration-plan.js";
10
11
  export * from "./symbol-repair-plan.js";
11
12
  export * from "./specs/bootstrap.js";
12
13
  export * from "./specs/check.js";
14
+ export * from "./specs/impact-review-preparation.js";
13
15
  export * from "./specs/discovery.js";
14
16
  export * from "./specs/modeling.js";
15
17
  export * from "./specs/planning.js";
@@ -7,6 +7,7 @@ import { promisify } from "node:util";
7
7
  import fg from "fast-glob";
8
8
 
9
9
  import { createRepoIgnorePolicy } from "../ignore-policy.js";
10
+ import { snapshotFileContent } from "./proof-receipt-projection.js";
10
11
  import type { FilesystemPort, GitPort, NetworkPort } from "./runtime-types.js";
11
12
 
12
13
  type NodeFilesystemPort = FilesystemPort & {
@@ -58,45 +59,6 @@ function includedSnapshotPath(relativePath: string): boolean {
58
59
  );
59
60
  }
60
61
 
61
- function withoutProofReceiptFrontmatter(content: string): string {
62
- const lines = content.match(/[^\n]*\n|[^\n]+$/g) ?? [];
63
- if (lines[0]?.trim() !== "---") return content;
64
- let inFrontmatter = true;
65
- let skippingReceipts = false;
66
- const retained: string[] = [];
67
- for (const [index, line] of lines.entries()) {
68
- const trimmed = line.trim();
69
- if (index > 0 && inFrontmatter && trimmed === "---") {
70
- inFrontmatter = false;
71
- skippingReceipts = false;
72
- retained.push(line);
73
- continue;
74
- }
75
- if (inFrontmatter && /^proof_receipts\s*:/.test(line)) {
76
- skippingReceipts = true;
77
- continue;
78
- }
79
- if (
80
- inFrontmatter &&
81
- skippingReceipts &&
82
- /^[A-Za-z_][A-Za-z0-9_-]*\s*:/.test(line)
83
- ) {
84
- skippingReceipts = false;
85
- }
86
- if (!skippingReceipts) retained.push(line);
87
- }
88
- return retained.join("");
89
- }
90
-
91
- function snapshotFileContent(relativePath: string, content: Buffer): Buffer {
92
- if (relativePath.endsWith(".md")) {
93
- return Buffer.from(
94
- withoutProofReceiptFrontmatter(content.toString("utf8")),
95
- );
96
- }
97
- return content;
98
- }
99
-
100
62
  /**
101
63
  * A tracked markdown file whose only change is proof-receipt frontmatter
102
64
  * cannot alter the snapshot hash (receipts are stripped before hashing), so it
@@ -0,0 +1,39 @@
1
+ /** Remove only the top-level proof-receipt frontmatter block, byte-for-byte otherwise. */
2
+ export function withoutProofReceiptFrontmatter(content: string): string {
3
+ const lines = content.match(/[^\n]*\n|[^\n]+$/g) ?? [];
4
+ if (lines[0]?.trim() !== "---") return content;
5
+ let inFrontmatter = true;
6
+ let skippingReceipts = false;
7
+ const retained: string[] = [];
8
+ for (const [index, line] of lines.entries()) {
9
+ const trimmed = line.trim();
10
+ if (index > 0 && inFrontmatter && trimmed === "---") {
11
+ inFrontmatter = false;
12
+ skippingReceipts = false;
13
+ retained.push(line);
14
+ continue;
15
+ }
16
+ if (inFrontmatter && /^proof_receipts\s*:/.test(line)) {
17
+ skippingReceipts = true;
18
+ continue;
19
+ }
20
+ if (
21
+ inFrontmatter &&
22
+ skippingReceipts &&
23
+ /^[A-Za-z_][A-Za-z0-9_-]*\s*:/.test(line)
24
+ ) {
25
+ skippingReceipts = false;
26
+ }
27
+ if (!skippingReceipts) retained.push(line);
28
+ }
29
+ return retained.join("");
30
+ }
31
+
32
+ /** Match the workspace proof snapshot's markdown receipt projection exactly. */
33
+ export function snapshotFileContent(
34
+ relativePath: string,
35
+ content: Buffer,
36
+ ): Buffer {
37
+ if (!relativePath.endsWith(".md")) return content;
38
+ return Buffer.from(withoutProofReceiptFrontmatter(content.toString("utf8")));
39
+ }