@kontextmind/kxm 0.6.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 (227) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.kxm/README.md +14 -0
  3. package/.kxm/assets/README.md +5 -0
  4. package/.kxm/assets/retrospectives/README.md +5 -0
  5. package/.kxm/config/README.md +5 -0
  6. package/.kxm/config/agents.json +43 -0
  7. package/.kxm/config/env.example +56 -0
  8. package/.kxm/config/update.example.yaml +9 -0
  9. package/.kxm/config/workflows/fix.json +160 -0
  10. package/.kxm/config/workflows/jira-development.json +116 -0
  11. package/.kxm/config/workflows/provenance-quorum.json +150 -0
  12. package/.kxm/config/workflows/v04-dogfood.json +72 -0
  13. package/CHANGELOG.md +465 -0
  14. package/LICENSE +21 -0
  15. package/README.md +306 -0
  16. package/SECURITY.md +72 -0
  17. package/docs/README.md +48 -0
  18. package/docs/agent-communication-envelopes-and-gates.md +553 -0
  19. package/docs/architecture.md +242 -0
  20. package/docs/assignment-runner.md +241 -0
  21. package/docs/configuration.md +361 -0
  22. package/docs/continuous-improvement.md +114 -0
  23. package/docs/getting-started.md +253 -0
  24. package/docs/kxm-handbook.md +1090 -0
  25. package/docs/operations.md +205 -0
  26. package/docs/provenance-gates.md +291 -0
  27. package/docs/skills.md +45 -0
  28. package/docs/templates/README.md +95 -0
  29. package/docs/templates/adr.md +88 -0
  30. package/docs/templates/architecture.md +120 -0
  31. package/docs/templates/bug-fix.md +109 -0
  32. package/docs/templates/feature.md +108 -0
  33. package/docs/templates/handoff.md +72 -0
  34. package/docs/templates/postmortem.md +77 -0
  35. package/docs/templates/research.md +100 -0
  36. package/docs/templates/review.md +85 -0
  37. package/docs/templates/runbook.md +73 -0
  38. package/docs/templates/test-plan.md +87 -0
  39. package/docs/templates/test-report.md +72 -0
  40. package/docs/test-matrix.md +121 -0
  41. package/docs/troubleshooting.md +249 -0
  42. package/docs/vnext/README.md +62 -0
  43. package/docs/vnext/architecture.md +185 -0
  44. package/docs/vnext/effects-and-recovery.md +172 -0
  45. package/docs/vnext/lifecycles.md +235 -0
  46. package/docs/vnext/migration.md +220 -0
  47. package/docs/vnext/routing.md +184 -0
  48. package/docs/vnext/synchronization.md +172 -0
  49. package/docs/vnext/terminology.md +240 -0
  50. package/docs/vnext/validation.md +335 -0
  51. package/docs/webhook-workflows.md +240 -0
  52. package/docs/workflow-guide.md +1150 -0
  53. package/examples/README.md +102 -0
  54. package/examples/provenance-workflow.json +40 -0
  55. package/examples/requester.ts +30 -0
  56. package/examples/reviewer-agent.ts +29 -0
  57. package/examples/roundtrip.ts +46 -0
  58. package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
  59. package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
  60. package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
  61. package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
  62. package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
  63. package/examples/vnext/.kxm/agents/planner.yaml +13 -0
  64. package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
  65. package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
  66. package/examples/vnext/.kxm/gates.yaml +8 -0
  67. package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
  68. package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
  69. package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
  70. package/examples/vnext/.kxm/models/implementation.yaml +14 -0
  71. package/examples/vnext/.kxm/models/primary.yaml +17 -0
  72. package/examples/vnext/.kxm/prices.yaml +111 -0
  73. package/examples/vnext/.kxm/project/env.yaml +7 -0
  74. package/examples/vnext/.kxm/project.yaml +32 -0
  75. package/examples/vnext/.kxm/repo/repo.yaml +8 -0
  76. package/examples/vnext/.kxm/workflows/default.yaml +92 -0
  77. package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
  78. package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
  79. package/examples/vnext/README.md +53 -0
  80. package/examples/vnext/records/assignment-result-recorded.json +63 -0
  81. package/examples/vnext/records/assignment-result.json +46 -0
  82. package/examples/vnext/records/context-candidate.json +42 -0
  83. package/examples/vnext/records/delivery-manifest.json +66 -0
  84. package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
  85. package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
  86. package/examples/vnext/records/run-created.json +54 -0
  87. package/examples/vnext/records/sync-event.json +65 -0
  88. package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
  89. package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
  90. package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
  91. package/examples/workflow-signal.ts +63 -0
  92. package/package.json +129 -0
  93. package/plugins/kxm/.claude-plugin/plugin.json +73 -0
  94. package/plugins/kxm/.mcp.json +19 -0
  95. package/plugins/kxm/README.md +93 -0
  96. package/plugins/kxm/dist/cli.js +42853 -0
  97. package/plugins/kxm/dist/client.js +416 -0
  98. package/plugins/kxm/dist/core.js +1823 -0
  99. package/plugins/kxm/dist/extension.js +3797 -0
  100. package/plugins/kxm/dist/mcp-server.js +17104 -0
  101. package/plugins/kxm/dist/runtime.js +23361 -0
  102. package/plugins/kxm/dist/server.js +13640 -0
  103. package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
  104. package/plugins/kxm/package.json +12 -0
  105. package/plugins/kxm/skills/kxm/SKILL.md +97 -0
  106. package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
  107. package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
  108. package/plugins/kxm/src/arbiter.ts +355 -0
  109. package/plugins/kxm/src/artifacts-exist.ts +62 -0
  110. package/plugins/kxm/src/autocomplete.ts +236 -0
  111. package/plugins/kxm/src/cli.ts +3707 -0
  112. package/plugins/kxm/src/client.ts +614 -0
  113. package/plugins/kxm/src/commands.ts +1063 -0
  114. package/plugins/kxm/src/config.ts +290 -0
  115. package/plugins/kxm/src/context/providers.ts +101 -0
  116. package/plugins/kxm/src/context-packet.ts +332 -0
  117. package/plugins/kxm/src/context.ts +499 -0
  118. package/plugins/kxm/src/core.ts +6 -0
  119. package/plugins/kxm/src/database.ts +563 -0
  120. package/plugins/kxm/src/diagnostics.ts +184 -0
  121. package/plugins/kxm/src/envelope.ts +118 -0
  122. package/plugins/kxm/src/extension.ts +895 -0
  123. package/plugins/kxm/src/external-effects.ts +299 -0
  124. package/plugins/kxm/src/github-watch.ts +255 -0
  125. package/plugins/kxm/src/hub-binding.ts +160 -0
  126. package/plugins/kxm/src/hub.ts +2502 -0
  127. package/plugins/kxm/src/improve.ts +383 -0
  128. package/plugins/kxm/src/inbox.ts +10 -0
  129. package/plugins/kxm/src/kxm-install-kind.ts +113 -0
  130. package/plugins/kxm/src/kxm-update-config.ts +39 -0
  131. package/plugins/kxm/src/kxm-update.ts +238 -0
  132. package/plugins/kxm/src/local-snapshot.ts +406 -0
  133. package/plugins/kxm/src/logger.ts +198 -0
  134. package/plugins/kxm/src/mcp-server.ts +143 -0
  135. package/plugins/kxm/src/memory.ts +385 -0
  136. package/plugins/kxm/src/nous-pi.ts +287 -0
  137. package/plugins/kxm/src/nous-provider.ts +729 -0
  138. package/plugins/kxm/src/price-calc.ts +87 -0
  139. package/plugins/kxm/src/prices.ts +121 -0
  140. package/plugins/kxm/src/protocol.ts +172 -0
  141. package/plugins/kxm/src/recovery.ts +211 -0
  142. package/plugins/kxm/src/redact.ts +26 -0
  143. package/plugins/kxm/src/retrospective.ts +400 -0
  144. package/plugins/kxm/src/routing.ts +830 -0
  145. package/plugins/kxm/src/runtime.ts +9 -0
  146. package/plugins/kxm/src/server.ts +117 -0
  147. package/plugins/kxm/src/session-work.ts +571 -0
  148. package/plugins/kxm/src/session.ts +184 -0
  149. package/plugins/kxm/src/skills.ts +535 -0
  150. package/plugins/kxm/src/state.ts +326 -0
  151. package/plugins/kxm/src/store.ts +637 -0
  152. package/plugins/kxm/src/studio-layout.ts +268 -0
  153. package/plugins/kxm/src/suggest.ts +162 -0
  154. package/plugins/kxm/src/task-manager.ts +244 -0
  155. package/plugins/kxm/src/telemetry.ts +116 -0
  156. package/plugins/kxm/src/tui.ts +1046 -0
  157. package/plugins/kxm/src/vnext-bindings.ts +403 -0
  158. package/plugins/kxm/src/vnext-config.ts +1646 -0
  159. package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
  160. package/plugins/kxm/src/vnext-engine-command.ts +533 -0
  161. package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
  162. package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
  163. package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
  164. package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
  165. package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
  166. package/plugins/kxm/src/vnext-engine.ts +2458 -0
  167. package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
  168. package/plugins/kxm/src/vnext-harness.ts +1142 -0
  169. package/plugins/kxm/src/vnext-init.ts +430 -0
  170. package/plugins/kxm/src/vnext-migrate.ts +1848 -0
  171. package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
  172. package/plugins/kxm/src/vnext-permission.ts +936 -0
  173. package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
  174. package/plugins/kxm/src/vnext-repair.ts +1094 -0
  175. package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
  176. package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
  177. package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
  178. package/plugins/kxm/src/vnext-runtime.ts +663 -0
  179. package/plugins/kxm/src/vnext-template.ts +247 -0
  180. package/plugins/kxm/src/wiki.ts +313 -0
  181. package/plugins/kxm/src/workflow.ts +1548 -0
  182. package/schemas/vnext/README.md +46 -0
  183. package/schemas/vnext/agent.schema.json +40 -0
  184. package/schemas/vnext/assignment-result.schema.json +66 -0
  185. package/schemas/vnext/backup-manifest.schema.json +89 -0
  186. package/schemas/vnext/candidate.schema.json +109 -0
  187. package/schemas/vnext/common.schema.json +422 -0
  188. package/schemas/vnext/context-candidate.schema.json +76 -0
  189. package/schemas/vnext/context-packet.schema.json +192 -0
  190. package/schemas/vnext/delivery-manifest.schema.json +159 -0
  191. package/schemas/vnext/environment.schema.json +66 -0
  192. package/schemas/vnext/gate-registry.schema.json +109 -0
  193. package/schemas/vnext/handoff-manifest.schema.json +146 -0
  194. package/schemas/vnext/init-operation.schema.json +61 -0
  195. package/schemas/vnext/local-repository-bindings.schema.json +30 -0
  196. package/schemas/vnext/memory-record.schema.json +45 -0
  197. package/schemas/vnext/migration-decision.schema.json +26 -0
  198. package/schemas/vnext/migration-plan.schema.json +123 -0
  199. package/schemas/vnext/migration-receipt.schema.json +52 -0
  200. package/schemas/vnext/model.schema.json +42 -0
  201. package/schemas/vnext/permission-diff.schema.json +57 -0
  202. package/schemas/vnext/prices.schema.json +115 -0
  203. package/schemas/vnext/project.schema.json +85 -0
  204. package/schemas/vnext/repository.schema.json +24 -0
  205. package/schemas/vnext/run-event.schema.json +460 -0
  206. package/schemas/vnext/session-brief.schema.json +153 -0
  207. package/schemas/vnext/sync-event.schema.json +234 -0
  208. package/schemas/vnext/template-provenance.schema.json +38 -0
  209. package/schemas/vnext/workflow.schema.json +248 -0
  210. package/scripts/assignment-run.d.mts +354 -0
  211. package/scripts/assignment-run.mjs +4451 -0
  212. package/scripts/build-runtime.mjs +56 -0
  213. package/scripts/check-generated.mjs +77 -0
  214. package/scripts/check-versions.mjs +34 -0
  215. package/scripts/emit-codex-artifacts.d.mts +9 -0
  216. package/scripts/emit-codex-artifacts.mjs +91 -0
  217. package/scripts/harness-run.d.mts +83 -0
  218. package/scripts/harness-run.mjs +2095 -0
  219. package/scripts/kxm-hub.mjs +105 -0
  220. package/scripts/kxm-publish-npm.mjs +327 -0
  221. package/scripts/kxm-release-github.mjs +472 -0
  222. package/scripts/kxm-runtime-supervisor.mjs +7 -0
  223. package/scripts/kxm-worker.mjs +1127 -0
  224. package/scripts/kxm.mjs +27 -0
  225. package/scripts/roster-policy.d.mts +20 -0
  226. package/scripts/roster-policy.mjs +161 -0
  227. package/scripts/smoke-multi-pi.mjs +479 -0
@@ -0,0 +1,247 @@
1
+ import { createHash } from "node:crypto";
2
+ import { stringify } from "yaml";
3
+ import type { JsonObject, JsonValue } from "./vnext-config.ts";
4
+
5
+ export const VNEXT_TEMPLATE_ID = "builtin-minimal";
6
+ export const VNEXT_TEMPLATE_PROVENANCE_PATH = ".kxm/template-provenance.yaml";
7
+ export const CURRENT_VNEXT_TEMPLATE_VARIANT: VnextTemplateVariant = "v4-registry";
8
+ export const SUPPORTED_VNEXT_TEMPLATE_VARIANTS = ["v1", "v2", "v3-policy", "v4-registry"] as const;
9
+
10
+ export type VnextTemplateVariant = typeof SUPPORTED_VNEXT_TEMPLATE_VARIANTS[number];
11
+
12
+ export interface VnextTemplateFileRecord {
13
+ path: string;
14
+ sha256: string;
15
+ authoritySha256: string;
16
+ bytes: number;
17
+ }
18
+
19
+ export interface VnextTemplateProvenance {
20
+ schema: "kxm.template-provenance.v1";
21
+ templateId: typeof VNEXT_TEMPLATE_ID;
22
+ templateRevision: string;
23
+ inputs: {
24
+ projectId: string;
25
+ projectName: string;
26
+ };
27
+ files: readonly VnextTemplateFileRecord[];
28
+ }
29
+
30
+ export interface VnextRenderedTemplate {
31
+ projectId: string;
32
+ projectName: string;
33
+ templateRevision: string;
34
+ provenance: VnextTemplateProvenance;
35
+ files: ReadonlyMap<string, Buffer>;
36
+ values: ReadonlyMap<string, JsonObject>;
37
+ }
38
+
39
+ function compareCodeUnits(left: string, right: string): number {
40
+ return left < right ? -1 : left > right ? 1 : 0;
41
+ }
42
+
43
+ function canonicalJson(value: JsonValue): string {
44
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
45
+ return JSON.stringify(value);
46
+ }
47
+ if (Array.isArray(value)) return `[${value.map((candidate) => canonicalJson(candidate)).join(",")}]`;
48
+ return `{${Object.keys(value).sort(compareCodeUnits)
49
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key] as JsonValue)}`).join(",")}}`;
50
+ }
51
+
52
+ export function vnextContentSha256(input: string | Uint8Array): string {
53
+ return `sha256:${createHash("sha256").update(input).digest("hex")}`;
54
+ }
55
+
56
+ function authorityProjection(value: JsonObject): JsonObject {
57
+ const projected = structuredClone(value);
58
+ if (projected.schema === "kxm.project.v1") delete projected.name;
59
+ if (projected.schema === "kxm.repository.v1") delete projected.description;
60
+ if (projected.schema === "kxm.agent.v1") delete projected.purpose;
61
+ if (projected.schema === "kxm.workflow.v1") delete projected.description;
62
+ return projected;
63
+ }
64
+
65
+ export function vnextAuthoritySha256(value: JsonObject): string {
66
+ return vnextContentSha256(canonicalJson(authorityProjection(value)));
67
+ }
68
+
69
+ function coreTemplate(projectId: string, projectName: string, variant: VnextTemplateVariant): ReadonlyMap<string, JsonObject> {
70
+ const files = new Map<string, JsonObject>([
71
+ [".kxm/project.yaml", {
72
+ schema: "kxm.project.v1",
73
+ id: projectId,
74
+ name: projectName,
75
+ defaultWorkflow: "default",
76
+ defaultExecutor: "local",
77
+ defaultHarness: "pi",
78
+ repositories: [{ id: "control", role: "control", required: true, pathHint: "." }],
79
+ workspace: {
80
+ dirtySnapshot: {
81
+ untracked: "ask",
82
+ dirtySubmodules: "fail",
83
+ },
84
+ },
85
+ }],
86
+ [".kxm/repo/repo.yaml", {
87
+ schema: "kxm.repository.v1",
88
+ projectId,
89
+ repositoryId: "control",
90
+ description: "Authoritative project configuration and repository content.",
91
+ defaultAccess: "write",
92
+ }],
93
+ [".kxm/agents/coordinator.yaml", {
94
+ schema: "kxm.agent.v1",
95
+ purpose: variant === "v1" || variant === "v4-registry"
96
+ ? "Coordinate the pinned workflow and emit schema-validated commands."
97
+ : "Coordinate the pinned workflow and emit validated, reviewable commands.",
98
+ tools: { preset: "coordinator" },
99
+ defaultRepositoryAccess: "read",
100
+ repositories: { control: variant === "v3-policy" ? "write" : "read" },
101
+ network: "provider-only",
102
+ resultSchema: "kxm.assignment-result.v1",
103
+ }],
104
+ [".kxm/agents/implementer.yaml", {
105
+ schema: "kxm.agent.v1",
106
+ purpose: "Implement the approved change within the declared repository scope.",
107
+ tools: { preset: "workspace-writer" },
108
+ defaultRepositoryAccess: "none",
109
+ repositories: { control: "write" },
110
+ network: "provider-only",
111
+ resultSchema: "kxm.assignment-result.v1",
112
+ }],
113
+ [".kxm/workflows/default.yaml", {
114
+ schema: "kxm.workflow.v1",
115
+ description: "Plan, implement, and verify a local change.",
116
+ coordinator: "coordinator",
117
+ limits: {
118
+ maxTransitions: 8,
119
+ maxRunDurationMs: 14_400_000,
120
+ maxAgentTimeMs: 21_600_000,
121
+ },
122
+ steps: [
123
+ {
124
+ id: "plan",
125
+ kind: "agent",
126
+ agent: "coordinator",
127
+ maxAttempts: 2,
128
+ repositories: { control: "read" },
129
+ requiredEvidence: [{ key: "plan", kind: "artifact" }],
130
+ timeoutMs: 1_200_000,
131
+ on: {
132
+ passed: "implement",
133
+ blocked: { target: "$terminal", terminalStatus: "failed" },
134
+ },
135
+ },
136
+ {
137
+ id: "implement",
138
+ kind: "agent",
139
+ agent: "implementer",
140
+ maxAttempts: 3,
141
+ repositories: { control: "write" },
142
+ assignments: {
143
+ allowedAgents: ["implementer"],
144
+ minimum: 1,
145
+ target: 1,
146
+ maximum: 1,
147
+ maxParallel: 1,
148
+ maxAttemptsPerAssignment: 2,
149
+ maxWriteRepositories: 1,
150
+ },
151
+ requiredEvidence: [{ key: "implementation-diff", kind: "artifact" }],
152
+ timeoutMs: 3_600_000,
153
+ on: {
154
+ passed: "verify",
155
+ failed: { target: "$terminal", terminalStatus: "failed" },
156
+ blocked: { target: "$terminal", terminalStatus: "failed" },
157
+ },
158
+ },
159
+ {
160
+ id: "verify",
161
+ kind: "gate",
162
+ gate: "test",
163
+ maxAttempts: 3,
164
+ repositories: { control: "write" },
165
+ requiredEvidence: [{ key: "local-gates", kind: "gate" }],
166
+ timeoutMs: 3_600_000,
167
+ on: {
168
+ passed: { target: "$terminal", terminalStatus: "completed" },
169
+ "implementation-failure": { target: "implement", maxTransitions: 3 },
170
+ failed: { target: "$terminal", terminalStatus: "failed" },
171
+ },
172
+ },
173
+ ],
174
+ }],
175
+ ]);
176
+ if (variant === "v4-registry") {
177
+ files.set(".kxm/gates.yaml", {
178
+ schema: "kxm.gate-registry.v1",
179
+ gates: { test: { kind: "command", argv: ["npm", "test"], timeoutMs: 3_600_000 } },
180
+ });
181
+ }
182
+ return files;
183
+ }
184
+
185
+ function provenanceRevision(files: readonly VnextTemplateFileRecord[]): string {
186
+ return vnextContentSha256(canonicalJson(files.map((file) => ({
187
+ path: file.path,
188
+ sha256: file.sha256,
189
+ authoritySha256: file.authoritySha256,
190
+ bytes: file.bytes,
191
+ }))));
192
+ }
193
+
194
+ export function renderVnextTemplate(
195
+ projectId: string,
196
+ projectName: string,
197
+ variant: VnextTemplateVariant = CURRENT_VNEXT_TEMPLATE_VARIANT,
198
+ ): VnextRenderedTemplate {
199
+ const values = coreTemplate(projectId, projectName, variant);
200
+ const coreFiles = new Map<string, Buffer>();
201
+ const records = [...values.entries()].map(([path, value]) => {
202
+ const bytes = Buffer.from(stringify(value, { lineWidth: 0 }), "utf8");
203
+ coreFiles.set(path, bytes);
204
+ return {
205
+ path,
206
+ sha256: vnextContentSha256(bytes),
207
+ authoritySha256: vnextAuthoritySha256(value),
208
+ bytes: bytes.byteLength,
209
+ } satisfies VnextTemplateFileRecord;
210
+ }).sort((left, right) => compareCodeUnits(left.path, right.path));
211
+ const templateRevision = provenanceRevision(records);
212
+ const provenance: VnextTemplateProvenance = {
213
+ schema: "kxm.template-provenance.v1",
214
+ templateId: VNEXT_TEMPLATE_ID,
215
+ templateRevision,
216
+ inputs: { projectId, projectName },
217
+ files: records,
218
+ };
219
+ const fileEntries: [string, Buffer][] = [...coreFiles.entries()];
220
+ fileEntries.push([
221
+ VNEXT_TEMPLATE_PROVENANCE_PATH,
222
+ Buffer.from(stringify(provenance as unknown as JsonObject, { lineWidth: 0 }), "utf8"),
223
+ ]);
224
+ fileEntries.sort(([left], [right]) => compareCodeUnits(left, right));
225
+ const files = new Map<string, Buffer>(fileEntries);
226
+ return { projectId, projectName, templateRevision, provenance, files, values };
227
+ }
228
+
229
+ export function validatedTemplateProvenance(value: JsonObject): VnextTemplateProvenance | undefined {
230
+ if (value.schema !== "kxm.template-provenance.v1" || value.templateId !== VNEXT_TEMPLATE_ID) return undefined;
231
+ const candidate = value as unknown as VnextTemplateProvenance;
232
+ return provenanceRevision(candidate.files) === candidate.templateRevision ? candidate : undefined;
233
+ }
234
+
235
+ /** Resolve provenance only when its complete manifest equals a built-in baseline. */
236
+ export function resolveVnextTemplateBaseline(value: JsonObject): VnextTemplateProvenance | undefined {
237
+ const candidate = validatedTemplateProvenance(value);
238
+ if (!candidate) return undefined;
239
+ for (const variant of SUPPORTED_VNEXT_TEMPLATE_VARIANTS) {
240
+ const known = renderVnextTemplate(candidate.inputs.projectId, candidate.inputs.projectName, variant).provenance;
241
+ if (known.templateRevision === candidate.templateRevision
242
+ && canonicalJson(known as unknown as JsonObject) === canonicalJson(candidate as unknown as JsonObject)) {
243
+ return candidate;
244
+ }
245
+ }
246
+ return undefined;
247
+ }
@@ -0,0 +1,313 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { redactSecrets } from "./redact.ts";
4
+ import type { ContextItem } from "./context.ts";
5
+ import type { StateContradiction } from "./state.ts";
6
+
7
+ /**
8
+ * Compiled Karpathy-style knowledge wiki (v0.5, issue #35).
9
+ *
10
+ * The wiki is a compiled, human-readable view — never the authoritative
11
+ * database. Every generated claim links back to the context item / journal
12
+ * entry / state record that produced it. Open contradictions remain visible;
13
+ * compilation never resolves them. Generation is deterministic so diffs are
14
+ * reviewable and tests are reproducible.
15
+ */
16
+
17
+ export const WIKI_ROOT = ".kxm/knowledge/wiki";
18
+
19
+ export interface WikiSourcePool {
20
+ project: string;
21
+ /** State items for the project (all lifecycle states). */
22
+ stateItems: ContextItem[];
23
+ /** Journal- and store-derived knowledge/evidence/episode items. */
24
+ contextItems: ContextItem[];
25
+ /** Contradictions detected by the temporal state layer. */
26
+ contradictions: StateContradiction[];
27
+ /** Open contradiction journal item IDs (unresolved journal contradictions). */
28
+ openContradictionItemIds: string[];
29
+ /** Injected clock for reproducible builds. */
30
+ compiledAt: string;
31
+ }
32
+
33
+ export interface CompiledWiki {
34
+ /** Repo-relative path → file content, sorted by path for stable diffs. */
35
+ pages: Map<string, string>;
36
+ index: string;
37
+ /** Metadata-only compile audit: what was rendered from where. */
38
+ audit: {
39
+ project: string;
40
+ pages: string[];
41
+ stateItems: number;
42
+ contextItems: number;
43
+ contradictions: number;
44
+ compiledAt: string;
45
+ };
46
+ }
47
+
48
+ const SECTION_BY_KIND: Record<string, string> = {
49
+ knowledge: "architecture",
50
+ decision: "decisions",
51
+ episode: "incidents",
52
+ skill: "patterns",
53
+ };
54
+
55
+ function wikiSectionFor(item: ContextItem): string {
56
+ if (SECTION_BY_KIND[item.kind]) return SECTION_BY_KIND[item.kind]!;
57
+ if (item.provenance.sourceRef?.startsWith("journal:")) {
58
+ // Journal evidence items are incident-shaped learning.
59
+ return "incidents";
60
+ }
61
+ return "patterns";
62
+ }
63
+
64
+ function claimLine(item: ContextItem): string {
65
+ const refs = [`\`${item.id}\``];
66
+ if (item.provenance.sourceRef) refs.push(`source: \`${item.provenance.sourceRef}\``);
67
+ const lineage = item.provenance.derivedFrom ?? [];
68
+ if (lineage.length > 0) refs.push(`derived from: ${lineage.map((id) => `\`${id}\``).join(", ")}`);
69
+ const authority = item.authority === "policy" || item.authority === "instruction"
70
+ ? ` (${item.authority})`
71
+ : "";
72
+ return `- ${redactSecrets(item.summary)}${authority} — ${refs.join(" · ")}`;
73
+ }
74
+
75
+ function page(title: string, heading: string, claims: ContextItem[], footer: string): string {
76
+ const lines = [
77
+ `# ${title}`,
78
+ "",
79
+ heading,
80
+ "",
81
+ ...(claims.length > 0 ? claims.map(claimLine) : ["_No reviewed records yet._"]),
82
+ "",
83
+ footer,
84
+ ];
85
+ return `${lines.join("\n")}\n`;
86
+ }
87
+
88
+ const GENERATED_FOOTER =
89
+ "<!-- generated by kxm context wiki-compile; the wiki is a compiled view, not the authoritative database -->";
90
+
91
+ /** Compile the wiki deterministically. Same inputs always produce the same
92
+ * bytes (only the index records `compiledAt`). */
93
+ export function compileKnowledgeWiki(pool: WikiSourcePool): CompiledWiki {
94
+ const pages = new Map<string, string>();
95
+
96
+ const live = pool.contextItems.filter(
97
+ (item) => item.status !== "superseded" && item.status !== "rejected",
98
+ );
99
+ const superseded = pool.contextItems.filter((item) => item.status === "superseded");
100
+
101
+ // Group live items by wiki section, then by stable summary-derived slug.
102
+ const sections = new Map<string, ContextItem[]>();
103
+ for (const item of live) {
104
+ const section = wikiSectionFor(item);
105
+ const bucket = sections.get(section) ?? [];
106
+ bucket.push(item);
107
+ sections.set(section, bucket);
108
+ }
109
+
110
+ const indexLines: string[] = [
111
+ `# ${pool.project} knowledge wiki`,
112
+ "",
113
+ "Compiled synthesis of durable journal evidence, temporal state, and governed records. Every claim links back to its evidence; open contradictions are listed, never silently resolved.",
114
+ "",
115
+ ];
116
+
117
+ for (const section of [...sections.keys()].sort()) {
118
+ const items = sections.get(section)!.sort((left, right) => left.id.localeCompare(right.id));
119
+ const path = `${WIKI_ROOT}/${section}/${pool.project}.md`;
120
+ pages.set(
121
+ path,
122
+ page(
123
+ `${pool.project} — ${section}`,
124
+ `Claims below are compiled from reviewed records. Each line links the record ID and source.`,
125
+ items,
126
+ GENERATED_FOOTER,
127
+ ),
128
+ );
129
+ indexLines.push(`- [${section}](${section}/${pool.project}.md) — ${items.length} claim(s)`);
130
+ }
131
+
132
+ // Temporal state: rendered from the authoritative state layer, preserving
133
+ // current/superseded distinctions. Superseded state is explicitly labeled.
134
+ const currentState = pool.stateItems.filter((item) => item.status === "current");
135
+ const supersededState = pool.stateItems.filter((item) => item.status === "superseded");
136
+ const statePath = `${WIKI_ROOT}/architecture/${pool.project}-state.md`;
137
+ const stateLines = [
138
+ `# ${pool.project} — temporal state`,
139
+ "",
140
+ "Rendered from the authoritative state layer. Current values are live; superseded values are kept visible with their validity window and successor link.",
141
+ "",
142
+ "## Current",
143
+ "",
144
+ ...(currentState.length > 0
145
+ ? currentState
146
+ .sort((left, right) => (left.stateKey ?? "").localeCompare(right.stateKey ?? ""))
147
+ .map((item) => claimLine(item))
148
+ : ["_No current state records._"]),
149
+ "",
150
+ "## Superseded",
151
+ "",
152
+ ...(supersededState.length > 0
153
+ ? supersededState
154
+ .sort((left, right) => (left.stateKey ?? "").localeCompare(right.stateKey ?? ""))
155
+ .map((item) => claimLine(item))
156
+ : ["_No superseded state records._"]),
157
+ "",
158
+ GENERATED_FOOTER,
159
+ ];
160
+ pages.set(statePath, `${stateLines.join("\n")}\n`);
161
+ indexLines.push(`- [temporal state](${pool.project}-state.md) — ${currentState.length} current, ${supersededState.length} superseded`);
162
+
163
+ // Decisions page mirrors decision-shaped knowledge records explicitly.
164
+ const decisions = live.filter((item) => item.kind === "knowledge" && item.provenance.sourceRef?.startsWith("journal:") === false);
165
+ if (decisions.length > 0) {
166
+ pages.set(
167
+ `${WIKI_ROOT}/decisions/${pool.project}.md`,
168
+ page(
169
+ `${pool.project} — decisions`,
170
+ "Decision records with their evidence links.",
171
+ decisions.sort((left, right) => left.id.localeCompare(right.id)),
172
+ GENERATED_FOOTER,
173
+ ),
174
+ );
175
+ indexLines.push(`- [decisions (tracked records)](decisions/${pool.project}.md) — ${decisions.length}`);
176
+ }
177
+
178
+ // Contradictions: compiled visibility, zero silent resolution.
179
+ const contradictionLines: string[] = [
180
+ `# ${pool.project} — open contradictions`,
181
+ "",
182
+ "These remain unresolved by compilation. Resolution happens through evidence-backed state promotion or journal decisions, never through re-generating this page.",
183
+ "",
184
+ ];
185
+ let contradictionCount = 0;
186
+ for (const contradiction of [...pool.contradictions].sort((left, right) => left.stateKey.localeCompare(right.stateKey))) {
187
+ contradictionCount += 1;
188
+ contradictionLines.push(
189
+ `## \`${contradiction.stateKey}\``,
190
+ "",
191
+ `- Competing current records: ${contradiction.competingCurrentIds.map((id) => `\`${id}\``).join(", ") || "none"}`,
192
+ `- Competing proposals: ${contradiction.competingProposalIds.map((id) => `\`${id}\``).join(", ") || "none"}`,
193
+ "",
194
+ );
195
+ }
196
+ for (const id of [...pool.openContradictionItemIds].sort()) {
197
+ contradictionCount += 1;
198
+ contradictionLines.push(`- Unresolved journal contradiction: \`${id}\``);
199
+ }
200
+ if (contradictionCount === 0) contradictionLines.push("_No open contradictions._");
201
+ contradictionLines.push("", GENERATED_FOOTER);
202
+ pages.set(`${WIKI_ROOT}/contradictions/${pool.project}.md`, `${contradictionLines.join("\n")}\n`);
203
+ indexLines.push(`- [contradictions](contradictions/${pool.project}.md) — ${contradictionCount} open`);
204
+
205
+ // Superseded knowledge is retained in history so current/superseded
206
+ // distinctions survive rebuilds.
207
+ if (superseded.length > 0) {
208
+ pages.set(
209
+ `${WIKI_ROOT}/incidents/${pool.project}-history.md`,
210
+ page(
211
+ `${pool.project} — superseded knowledge history`,
212
+ "Superseded records retained for learning. Never presented as current truth.",
213
+ superseded.sort((left, right) => left.id.localeCompare(right.id)),
214
+ GENERATED_FOOTER,
215
+ ),
216
+ );
217
+ indexLines.push(`- [superseded history](incidents/${pool.project}-history.md) — ${superseded.length}`);
218
+ }
219
+
220
+ indexLines.push("", GENERATED_FOOTER);
221
+ const index = `${indexLines.join("\n")}\n`;
222
+ pages.set(`${WIKI_ROOT}/index.md`, index);
223
+
224
+ return {
225
+ pages,
226
+ index,
227
+ audit: {
228
+ project: pool.project,
229
+ pages: [...pages.keys()].sort(),
230
+ stateItems: pool.stateItems.length,
231
+ contextItems: pool.contextItems.length,
232
+ contradictions: contradictionCount,
233
+ compiledAt: pool.compiledAt,
234
+ },
235
+ };
236
+ }
237
+
238
+ /** Write compiled pages to disk. Deterministic content keeps diffs bounded
239
+ * and reviewable; only `index.md` embeds the compile timestamp. */
240
+ export function writeCompiledWiki(root: string, wiki: CompiledWiki): string[] {
241
+ const written: string[] = [];
242
+ for (const [relativePath, content] of [...wiki.pages].sort(([left], [right]) => left.localeCompare(right))) {
243
+ const absolute = join(root, relativePath);
244
+ mkdirSync(absolute.slice(0, absolute.lastIndexOf("/")), { recursive: true });
245
+ writeFileSync(absolute, content);
246
+ written.push(relativePath);
247
+ }
248
+ return written;
249
+ }
250
+
251
+ export interface WikiLintIssue {
252
+ severity: "error" | "warning";
253
+ rule: "broken_ref" | "orphan_page" | "stale_state_link" | "unresolved_contradiction";
254
+ path: string;
255
+ message: string;
256
+ }
257
+
258
+ /** Lint a compiled wiki: broken evidence refs, orphan pages, stale state
259
+ * links, and unresolved contradictions. Wiki content stays reviewable; lint
260
+ * failures block release gates, not runtime work. */
261
+ export function lintKnowledgeWiki(
262
+ pages: Map<string, string>,
263
+ pool: Pick<WikiSourcePool, "project" | "stateItems" | "contextItems" | "contradictions" | "openContradictionItemIds">,
264
+ ): WikiLintIssue[] {
265
+ const issues: WikiLintIssue[] = [];
266
+ const knownIds = new Set<string>([
267
+ ...pool.stateItems.map((item) => item.id),
268
+ ...pool.contextItems.map((item) => item.id),
269
+ ]);
270
+
271
+ const index = pages.get(`${WIKI_ROOT}/index.md`);
272
+ for (const [path, content] of pages) {
273
+ if (path.endsWith("index.md")) continue;
274
+ // Broken refs: backticked record IDs that the pool does not know.
275
+ for (const match of content.matchAll(/`((?:ctx|journal)_[A-Za-z0-9_]+)`/g)) {
276
+ const id = match[1]!;
277
+ if (!knownIds.has(id)) {
278
+ issues.push({ severity: "error", rule: "broken_ref", path, message: `references unknown record ${id}` });
279
+ }
280
+ }
281
+ // Orphan pages: exist but are not linked from the index.
282
+ if (index && !index.includes(path.split("/").pop()!)) {
283
+ issues.push({ severity: "warning", rule: "orphan_page", path, message: "not linked from index.md" });
284
+ }
285
+ // Stale state links: pages claiming "Current" for a superseded state key.
286
+ for (const item of pool.stateItems) {
287
+ if (item.status !== "superseded" || !item.stateKey) continue;
288
+ const claimPattern = new RegExp(`Current[\\s\\S]{0,400}\`${item.id}\``, "u");
289
+ if (claimPattern.test(content)) {
290
+ issues.push({
291
+ severity: "error",
292
+ rule: "stale_state_link",
293
+ path,
294
+ message: `renders superseded state item ${item.id} (key ${item.stateKey}) as current`,
295
+ });
296
+ }
297
+ }
298
+ }
299
+
300
+ // Unresolved contradictions must stay visible: an index without the
301
+ // contradictions entry while contradictions exist is a lint error.
302
+ const hasContradictions = pool.contradictions.length > 0 || pool.openContradictionItemIds.length > 0;
303
+ if (hasContradictions && index && !index.includes("contradictions")) {
304
+ issues.push({
305
+ severity: "error",
306
+ rule: "unresolved_contradiction",
307
+ path: `${WIKI_ROOT}/index.md`,
308
+ message: "open contradictions exist but the index does not surface them",
309
+ });
310
+ }
311
+
312
+ return issues.sort((left, right) => left.path.localeCompare(right.path) || left.message.localeCompare(right.message));
313
+ }