@mainahq/core 1.0.3 → 1.1.1

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 (71) hide show
  1. package/package.json +1 -1
  2. package/src/ai/__tests__/delegation.test.ts +55 -1
  3. package/src/ai/delegation.ts +5 -3
  4. package/src/context/__tests__/budget.test.ts +29 -6
  5. package/src/context/__tests__/engine.test.ts +1 -0
  6. package/src/context/__tests__/selector.test.ts +23 -3
  7. package/src/context/__tests__/wiki.test.ts +349 -0
  8. package/src/context/budget.ts +12 -8
  9. package/src/context/engine.ts +37 -0
  10. package/src/context/selector.ts +30 -4
  11. package/src/context/wiki.ts +296 -0
  12. package/src/db/index.ts +12 -0
  13. package/src/feedback/__tests__/capture.test.ts +166 -0
  14. package/src/feedback/__tests__/signals.test.ts +144 -0
  15. package/src/feedback/__tests__/tmp-capture-1775575256633-lah0etnzlj/feedback.db +0 -0
  16. package/src/feedback/__tests__/tmp-capture-1775575256640-2xmjme4qraa/feedback.db +0 -0
  17. package/src/feedback/capture.ts +102 -0
  18. package/src/feedback/signals.ts +68 -0
  19. package/src/index.ts +104 -0
  20. package/src/init/__tests__/init.test.ts +400 -3
  21. package/src/init/index.ts +368 -12
  22. package/src/language/__tests__/__fixtures__/detect/composer.lock +1 -0
  23. package/src/prompts/defaults/index.ts +3 -1
  24. package/src/prompts/defaults/wiki-compile.md +20 -0
  25. package/src/prompts/defaults/wiki-query.md +18 -0
  26. package/src/stats/__tests__/tool-usage.test.ts +133 -0
  27. package/src/stats/tracker.ts +92 -0
  28. package/src/verify/__tests__/pipeline.test.ts +11 -8
  29. package/src/verify/pipeline.ts +13 -1
  30. package/src/verify/tools/__tests__/wiki-lint.test.ts +784 -0
  31. package/src/verify/tools/wiki-lint-runner.ts +38 -0
  32. package/src/verify/tools/wiki-lint.ts +898 -0
  33. package/src/wiki/__tests__/compiler.test.ts +389 -0
  34. package/src/wiki/__tests__/extractors/code.test.ts +99 -0
  35. package/src/wiki/__tests__/extractors/decision.test.ts +323 -0
  36. package/src/wiki/__tests__/extractors/feature.test.ts +186 -0
  37. package/src/wiki/__tests__/extractors/workflow.test.ts +131 -0
  38. package/src/wiki/__tests__/graph.test.ts +344 -0
  39. package/src/wiki/__tests__/hooks.test.ts +119 -0
  40. package/src/wiki/__tests__/indexer.test.ts +285 -0
  41. package/src/wiki/__tests__/linker.test.ts +230 -0
  42. package/src/wiki/__tests__/louvain.test.ts +229 -0
  43. package/src/wiki/__tests__/query.test.ts +316 -0
  44. package/src/wiki/__tests__/schema.test.ts +114 -0
  45. package/src/wiki/__tests__/signals.test.ts +474 -0
  46. package/src/wiki/__tests__/state.test.ts +168 -0
  47. package/src/wiki/__tests__/tracking.test.ts +118 -0
  48. package/src/wiki/__tests__/types.test.ts +387 -0
  49. package/src/wiki/compiler.ts +1075 -0
  50. package/src/wiki/extractors/code.ts +90 -0
  51. package/src/wiki/extractors/decision.ts +217 -0
  52. package/src/wiki/extractors/feature.ts +206 -0
  53. package/src/wiki/extractors/workflow.ts +112 -0
  54. package/src/wiki/graph.ts +445 -0
  55. package/src/wiki/hooks.ts +49 -0
  56. package/src/wiki/indexer.ts +105 -0
  57. package/src/wiki/linker.ts +117 -0
  58. package/src/wiki/louvain.ts +190 -0
  59. package/src/wiki/prompts/compile-architecture.md +59 -0
  60. package/src/wiki/prompts/compile-decision.md +66 -0
  61. package/src/wiki/prompts/compile-entity.md +56 -0
  62. package/src/wiki/prompts/compile-feature.md +60 -0
  63. package/src/wiki/prompts/compile-module.md +42 -0
  64. package/src/wiki/prompts/wiki-query.md +25 -0
  65. package/src/wiki/query.ts +338 -0
  66. package/src/wiki/schema.ts +111 -0
  67. package/src/wiki/signals.ts +368 -0
  68. package/src/wiki/state.ts +89 -0
  69. package/src/wiki/tracking.ts +30 -0
  70. package/src/wiki/types.ts +169 -0
  71. package/src/workflow/context.ts +26 -0
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Wiki RL Signals — feedback signals for wiki-as-context effectiveness.
3
+ *
4
+ * Three signals:
5
+ * 1. Wiki usage effectiveness — tracks whether wiki context led to accepted outputs
6
+ * 2. Compilation prompt effectiveness — tracks accept rate per prompt hash
7
+ * 3. Ebbinghaus decay scoring — calculates memory retention score per article type
8
+ *
9
+ * Uses simple JSON file storage in .maina/wiki/.signals.json.
10
+ */
11
+
12
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
13
+ import { dirname, join } from "node:path";
14
+ import type { ArticleType } from "./types";
15
+ import { DECAY_HALF_LIVES } from "./types";
16
+
17
+ // ─── Types ───────────────────────────────────────────────────────────────
18
+
19
+ export interface WikiEffectivenessSignal {
20
+ articlePath: string;
21
+ command: string;
22
+ accepted: boolean;
23
+ timestamp: string;
24
+ }
25
+
26
+ export interface CompilationPromptSignal {
27
+ promptHash: string;
28
+ articlePath: string;
29
+ indirectAcceptRate: number;
30
+ }
31
+
32
+ export interface ArticleLoadSignal {
33
+ articles: string[];
34
+ command: string;
35
+ timestamp: string;
36
+ }
37
+
38
+ export interface WikiEffectivenessReport {
39
+ totalLoads: number;
40
+ totalAccepts: number;
41
+ totalRejects: number;
42
+ acceptRate: number;
43
+ articleStats: Array<{
44
+ article: string;
45
+ loads: number;
46
+ accepts: number;
47
+ rejects: number;
48
+ effectivenessScore: number; // accepts / loads
49
+ }>;
50
+ negativeArticles: string[]; // articles with < 50% accept rate
51
+ dormantArticles: string[]; // ebbinghaus score < 0.2
52
+ }
53
+
54
+ interface SignalsStore {
55
+ usageSignals: WikiEffectivenessSignal[];
56
+ promptSignals: CompilationPromptSignal[];
57
+ loadSignals?: ArticleLoadSignal[];
58
+ }
59
+
60
+ // ─── Storage ─────────────────────────────────────────────────────────────
61
+
62
+ const SIGNALS_FILE = ".signals.json";
63
+
64
+ function signalsPath(wikiDir: string): string {
65
+ return join(wikiDir, SIGNALS_FILE);
66
+ }
67
+
68
+ function loadSignals(wikiDir: string): SignalsStore {
69
+ const path = signalsPath(wikiDir);
70
+ if (!existsSync(path)) {
71
+ return { usageSignals: [], promptSignals: [] };
72
+ }
73
+
74
+ try {
75
+ const raw = readFileSync(path, "utf-8");
76
+ return JSON.parse(raw) as SignalsStore;
77
+ } catch {
78
+ return { usageSignals: [], promptSignals: [] };
79
+ }
80
+ }
81
+
82
+ function saveSignals(wikiDir: string, store: SignalsStore): void {
83
+ const path = signalsPath(wikiDir);
84
+ const dir = dirname(path);
85
+ if (!existsSync(dir)) {
86
+ mkdirSync(dir, { recursive: true });
87
+ }
88
+ writeFileSync(path, JSON.stringify(store, null, 2));
89
+ }
90
+
91
+ // ─── Signal 1: Wiki Usage Effectiveness ─────────────────────────────────
92
+
93
+ /**
94
+ * Record wiki-as-context usage signal.
95
+ *
96
+ * Called after a command uses wiki articles as context. Tracks whether the
97
+ * AI output (which included wiki context) was accepted by the developer.
98
+ * This feeds the RL flywheel: articles that improve acceptance rates
99
+ * get prioritized in future context assembly.
100
+ *
101
+ * @param feedbackDbPath - Path to the wiki directory (contains .signals.json)
102
+ * @param articles - Article paths that were included in context
103
+ * @param command - The maina command that used the articles
104
+ * @param accepted - Whether the developer accepted the AI output
105
+ */
106
+ export function recordWikiUsage(
107
+ feedbackDbPath: string,
108
+ articles: string[],
109
+ command: string,
110
+ accepted: boolean,
111
+ ): void {
112
+ const store = loadSignals(feedbackDbPath);
113
+ const timestamp = new Date().toISOString();
114
+
115
+ for (const articlePath of articles) {
116
+ store.usageSignals.push({
117
+ articlePath,
118
+ command,
119
+ accepted,
120
+ timestamp,
121
+ });
122
+ }
123
+
124
+ saveSignals(feedbackDbPath, store);
125
+ }
126
+
127
+ // ─── Signal 2: Compilation Prompt Effectiveness ─────────────────────────
128
+
129
+ /**
130
+ * Get effectiveness metrics for a compilation prompt hash.
131
+ *
132
+ * Computes the indirect accept rate: what percentage of AI outputs were
133
+ * accepted when articles compiled by this prompt hash were used as context.
134
+ *
135
+ * @param feedbackDbPath - Path to the wiki directory (contains .signals.json)
136
+ * @param promptHash - The hash of the compilation prompt to evaluate
137
+ * @returns Accept rate and sample size
138
+ */
139
+ export function getPromptEffectiveness(
140
+ feedbackDbPath: string,
141
+ promptHash: string,
142
+ ): { acceptRate: number; sampleSize: number } {
143
+ const store = loadSignals(feedbackDbPath);
144
+
145
+ // Find articles compiled with this prompt hash
146
+ const promptArticles = store.promptSignals.filter(
147
+ (s) => s.promptHash === promptHash,
148
+ );
149
+
150
+ if (promptArticles.length === 0) {
151
+ return { acceptRate: 0, sampleSize: 0 };
152
+ }
153
+
154
+ // Get article paths associated with this prompt
155
+ const articlePaths = new Set(promptArticles.map((s) => s.articlePath));
156
+
157
+ // Find usage signals for these articles
158
+ const relevantUsage = store.usageSignals.filter((s) =>
159
+ articlePaths.has(s.articlePath),
160
+ );
161
+
162
+ if (relevantUsage.length === 0) {
163
+ return { acceptRate: 0, sampleSize: 0 };
164
+ }
165
+
166
+ const acceptedCount = relevantUsage.filter((s) => s.accepted).length;
167
+ const acceptRate = acceptedCount / relevantUsage.length;
168
+
169
+ return {
170
+ acceptRate: Math.round(acceptRate * 100) / 100,
171
+ sampleSize: relevantUsage.length,
172
+ };
173
+ }
174
+
175
+ // ─── Signal 3: Ebbinghaus Decay ─────────────────────────────────────────
176
+
177
+ /**
178
+ * Calculate Ebbinghaus retention score for a wiki article.
179
+ *
180
+ * Uses the forgetting curve formula with article-type-specific half-lives:
181
+ * score = exp(-0.693 * daysSinceAccess / halfLife) + 0.1 * min(accessCount, 10)
182
+ *
183
+ * Decision articles (halfLife=180d) decay slowest because architectural decisions
184
+ * remain relevant longest. Feature articles (halfLife=60d) decay fastest.
185
+ *
186
+ * Score is clamped to [0, 1].
187
+ *
188
+ * @param articleType - The type of wiki article (determines half-life)
189
+ * @param daysSinceAccess - Days since the article was last accessed
190
+ * @param accessCount - Total number of times the article has been accessed
191
+ * @returns Retention score between 0 and 1
192
+ */
193
+ export function calculateEbbinghausScore(
194
+ articleType: ArticleType,
195
+ daysSinceAccess: number,
196
+ accessCount: number,
197
+ ): number {
198
+ const halfLife = DECAY_HALF_LIVES[articleType];
199
+
200
+ // Forgetting curve: exp(-ln(2) * t / halfLife)
201
+ const decayComponent = Math.exp((-Math.LN2 * daysSinceAccess) / halfLife);
202
+
203
+ // Reinforcement bonus: 0.1 per access, capped at 10 accesses (max 1.0)
204
+ const reinforcementComponent = 0.1 * Math.min(accessCount, 10);
205
+
206
+ // Combine and clamp to [0, 1]
207
+ const raw = decayComponent + reinforcementComponent;
208
+ return Math.min(1, Math.max(0, raw));
209
+ }
210
+
211
+ // ─── Signal 4: Articles Loaded Tracking ─────────────────────────────────
212
+
213
+ /**
214
+ * Record which wiki articles were loaded for a command.
215
+ *
216
+ * Non-blocking: never throws. Used by the context engine (L5) to record
217
+ * which articles were included as context, so the RL flywheel can later
218
+ * correlate them with accept/reject outcomes.
219
+ *
220
+ * @param signalsPath - Path to the .signals.json file
221
+ * @param articles - Relative paths of articles loaded
222
+ * @param command - The maina command that triggered the load
223
+ */
224
+ export function recordArticlesLoaded(
225
+ signalsPath: string,
226
+ articles: string[],
227
+ command: string,
228
+ ): void {
229
+ try {
230
+ if (articles.length === 0) return;
231
+
232
+ const wikiDir = dirname(signalsPath);
233
+ const store = loadSignals(wikiDir);
234
+
235
+ if (!store.loadSignals) {
236
+ store.loadSignals = [];
237
+ }
238
+
239
+ store.loadSignals.push({
240
+ articles,
241
+ command,
242
+ timestamp: new Date().toISOString(),
243
+ });
244
+
245
+ saveSignals(wikiDir, store);
246
+ } catch {
247
+ // Non-blocking — never throw from signal recording
248
+ }
249
+ }
250
+
251
+ // ─── Signal 5: Wiki Effectiveness Report ────────────────────────────────
252
+
253
+ /**
254
+ * Compute a comprehensive effectiveness report for wiki articles.
255
+ *
256
+ * Aggregates usage signals to determine which articles contribute to
257
+ * accepted outputs and which are negatively correlated. Also identifies
258
+ * dormant articles via Ebbinghaus decay scoring.
259
+ *
260
+ * @param signalsPath - Path to the .signals.json file
261
+ * @returns Effectiveness report with per-article stats
262
+ */
263
+ export function getWikiEffectivenessReport(
264
+ signalsPath: string,
265
+ ): WikiEffectivenessReport {
266
+ const emptyReport: WikiEffectivenessReport = {
267
+ totalLoads: 0,
268
+ totalAccepts: 0,
269
+ totalRejects: 0,
270
+ acceptRate: 0,
271
+ articleStats: [],
272
+ negativeArticles: [],
273
+ dormantArticles: [],
274
+ };
275
+
276
+ try {
277
+ const wikiDir = dirname(signalsPath);
278
+ const store = loadSignals(wikiDir);
279
+ const { usageSignals } = store;
280
+
281
+ if (usageSignals.length === 0) {
282
+ return emptyReport;
283
+ }
284
+
285
+ // Aggregate per-article stats
286
+ const statsMap = new Map<
287
+ string,
288
+ { loads: number; accepts: number; rejects: number }
289
+ >();
290
+
291
+ for (const signal of usageSignals) {
292
+ const existing = statsMap.get(signal.articlePath) ?? {
293
+ loads: 0,
294
+ accepts: 0,
295
+ rejects: 0,
296
+ };
297
+ existing.loads += 1;
298
+ if (signal.accepted) {
299
+ existing.accepts += 1;
300
+ } else {
301
+ existing.rejects += 1;
302
+ }
303
+ statsMap.set(signal.articlePath, existing);
304
+ }
305
+
306
+ const totalAccepts = usageSignals.filter((s) => s.accepted).length;
307
+ const totalRejects = usageSignals.length - totalAccepts;
308
+
309
+ const articleStats = Array.from(statsMap.entries())
310
+ .map(([article, stats]) => ({
311
+ article,
312
+ loads: stats.loads,
313
+ accepts: stats.accepts,
314
+ rejects: stats.rejects,
315
+ effectivenessScore: stats.loads > 0 ? stats.accepts / stats.loads : 0,
316
+ }))
317
+ .sort((a, b) => b.effectivenessScore - a.effectivenessScore);
318
+
319
+ // Negative articles: < 50% accept rate with at least 1 load
320
+ const negativeArticles = articleStats
321
+ .filter((s) => s.loads > 0 && s.effectivenessScore < 0.5)
322
+ .map((s) => s.article);
323
+
324
+ // Dormant articles: ebbinghaus score < 0.2 based on last usage timestamp
325
+ const dormantArticles: string[] = [];
326
+ for (const signal of usageSignals) {
327
+ const daysSinceAccess =
328
+ (Date.now() - new Date(signal.timestamp).getTime()) /
329
+ (1000 * 60 * 60 * 24);
330
+
331
+ // Determine article type from path prefix
332
+ const articleType = inferArticleType(signal.articlePath);
333
+ const score = calculateEbbinghausScore(articleType, daysSinceAccess, 0);
334
+ if (score < 0.2 && !dormantArticles.includes(signal.articlePath)) {
335
+ dormantArticles.push(signal.articlePath);
336
+ }
337
+ }
338
+
339
+ return {
340
+ totalLoads: usageSignals.length,
341
+ totalAccepts: totalAccepts,
342
+ totalRejects: totalRejects,
343
+ acceptRate:
344
+ usageSignals.length > 0 ? totalAccepts / usageSignals.length : 0,
345
+ articleStats,
346
+ negativeArticles,
347
+ dormantArticles,
348
+ };
349
+ } catch {
350
+ return emptyReport;
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Infer article type from its relative path.
356
+ * Falls back to "raw" if the directory prefix is unrecognized.
357
+ */
358
+ function inferArticleType(articlePath: string): ArticleType {
359
+ const firstSegment = articlePath.split("/")[0] ?? "";
360
+ const typeMap: Record<string, ArticleType> = {
361
+ modules: "module",
362
+ entities: "entity",
363
+ features: "feature",
364
+ decisions: "decision",
365
+ architecture: "architecture",
366
+ };
367
+ return typeMap[firstSegment] ?? "raw";
368
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Wiki State — manages .maina/wiki/.state.json for incremental compilation.
3
+ *
4
+ * Tracks SHA-256 hashes of source files and compiled articles to detect
5
+ * what needs recompilation. State file is gitignored.
6
+ */
7
+
8
+ import { createHash } from "node:crypto";
9
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import type { WikiState } from "./types";
12
+
13
+ const STATE_FILE = ".state.json";
14
+
15
+ // ─── Hashing ─────────────────────────────────────────────────────────────
16
+
17
+ export function hashContent(content: string): string {
18
+ return createHash("sha256").update(content).digest("hex");
19
+ }
20
+
21
+ export function hashFile(filePath: string): string | null {
22
+ try {
23
+ const content = readFileSync(filePath, "utf-8");
24
+ return hashContent(content);
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ // ─── State Factory ───────────────────────────────────────────────────────
31
+
32
+ export function createEmptyState(): WikiState {
33
+ return {
34
+ fileHashes: {},
35
+ articleHashes: {},
36
+ lastFullCompile: "",
37
+ lastIncrementalCompile: "",
38
+ compilationPromptHash: "",
39
+ };
40
+ }
41
+
42
+ // ─── Persistence ─────────────────────────────────────────────────────────
43
+
44
+ export function loadState(wikiDir: string): WikiState | null {
45
+ const statePath = join(wikiDir, STATE_FILE);
46
+ if (!existsSync(statePath)) {
47
+ return null;
48
+ }
49
+ try {
50
+ const raw = readFileSync(statePath, "utf-8");
51
+ return JSON.parse(raw) as WikiState;
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ export function saveState(wikiDir: string, state: WikiState): void {
58
+ const statePath = join(wikiDir, STATE_FILE);
59
+ writeFileSync(statePath, JSON.stringify(state, null, 2));
60
+ }
61
+
62
+ // ─── Change Detection ────────────────────────────────────────────────────
63
+
64
+ /**
65
+ * Compare previous file hashes with current to find changed, added, or deleted files.
66
+ * Returns an array of file paths that need recompilation.
67
+ */
68
+ export function getChangedFiles(
69
+ previousHashes: Record<string, string>,
70
+ currentHashes: Record<string, string>,
71
+ ): string[] {
72
+ const changed: string[] = [];
73
+
74
+ // Check for new or modified files
75
+ for (const [file, hash] of Object.entries(currentHashes)) {
76
+ if (previousHashes[file] !== hash) {
77
+ changed.push(file);
78
+ }
79
+ }
80
+
81
+ // Check for deleted files
82
+ for (const file of Object.keys(previousHashes)) {
83
+ if (!(file in currentHashes)) {
84
+ changed.push(file);
85
+ }
86
+ }
87
+
88
+ return changed;
89
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Wiki Reference Tracking — records which wiki articles are read/written
3
+ * during workflow commands.
4
+ *
5
+ * Wraps appendWikiRefs from workflow/context to provide a wiki-specific API.
6
+ */
7
+
8
+ import { appendWikiRefs } from "../workflow/context";
9
+
10
+ /**
11
+ * Track wiki articles that were read during a command.
12
+ */
13
+ export function trackWikiRefsRead(
14
+ mainaDir: string,
15
+ command: string,
16
+ articles: string[],
17
+ ): void {
18
+ appendWikiRefs(mainaDir, command, articles, []);
19
+ }
20
+
21
+ /**
22
+ * Track wiki articles that were written during a command.
23
+ */
24
+ export function trackWikiRefsWritten(
25
+ mainaDir: string,
26
+ command: string,
27
+ articles: string[],
28
+ ): void {
29
+ appendWikiRefs(mainaDir, command, [], articles);
30
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Wiki Types — core type definitions for the Maina Wiki knowledge compiler.
3
+ *
4
+ * Defines the data model for wiki articles, knowledge graph edges,
5
+ * extracted lifecycle artifacts, state tracking, and lint results.
6
+ */
7
+
8
+ // ─── Article Types ───────────────────────────────────────────────────────
9
+
10
+ export type ArticleType =
11
+ | "module"
12
+ | "entity"
13
+ | "feature"
14
+ | "decision"
15
+ | "architecture"
16
+ | "raw";
17
+
18
+ /**
19
+ * 11 edge types in the unified knowledge graph.
20
+ * 5 from code (calls, imports, inherits, references, member_of)
21
+ * 6 from lifecycle artifacts (modified_by, specified_by, decided_by, motivated_by, constrains, aligns_with)
22
+ */
23
+ export type EdgeType =
24
+ // Code edges (from dependency graph)
25
+ | "calls"
26
+ | "imports"
27
+ | "inherits"
28
+ | "references"
29
+ | "member_of"
30
+ // Lifecycle edges (from extractors)
31
+ | "modified_by"
32
+ | "specified_by"
33
+ | "decided_by"
34
+ | "motivated_by"
35
+ | "constrains"
36
+ | "aligns_with";
37
+
38
+ export interface WikiLink {
39
+ target: string;
40
+ type: EdgeType;
41
+ weight: number;
42
+ }
43
+
44
+ export interface WikiArticle {
45
+ path: string;
46
+ type: ArticleType;
47
+ title: string;
48
+ content: string;
49
+ contentHash: string;
50
+ sourceHashes: string[];
51
+ backlinks: WikiLink[];
52
+ forwardLinks: WikiLink[];
53
+ pageRank: number;
54
+ lastCompiled: string;
55
+ referenceCount: number;
56
+ ebbinghausScore: number;
57
+ }
58
+
59
+ // ─── Extracted Lifecycle Artifacts ───────────────────────────────────────
60
+
61
+ export interface TaskItem {
62
+ id: string;
63
+ description: string;
64
+ completed: boolean;
65
+ }
66
+
67
+ export interface ExtractedFeature {
68
+ id: string;
69
+ title: string;
70
+ scope: string;
71
+ specQualityScore: number;
72
+ specAssertions: string[];
73
+ tasks: TaskItem[];
74
+ entitiesModified: string[];
75
+ decisionsCreated: string[];
76
+ branch: string;
77
+ prNumber: number | null;
78
+ merged: boolean;
79
+ }
80
+
81
+ export type DecisionStatus =
82
+ | "proposed"
83
+ | "accepted"
84
+ | "deprecated"
85
+ | "superseded";
86
+
87
+ export interface ExtractedDecision {
88
+ id: string;
89
+ title: string;
90
+ status: DecisionStatus;
91
+ context: string;
92
+ decision: string;
93
+ rationale: string;
94
+ alternativesRejected: string[];
95
+ entityMentions: string[];
96
+ constitutionAlignment: string[];
97
+ }
98
+
99
+ export interface WorkflowStep {
100
+ command: string;
101
+ timestamp: string;
102
+ summary: string;
103
+ }
104
+
105
+ export interface RLSignal {
106
+ step: string;
107
+ accepted: boolean;
108
+ }
109
+
110
+ export interface ExtractedWorkflowTrace {
111
+ featureId: string;
112
+ steps: WorkflowStep[];
113
+ wikiRefsRead: string[];
114
+ wikiRefsWritten: string[];
115
+ rlSignals: RLSignal[];
116
+ }
117
+
118
+ // ─── State ───────────────────────────────────────────────────────────────
119
+
120
+ export interface WikiState {
121
+ fileHashes: Record<string, string>;
122
+ articleHashes: Record<string, string>;
123
+ lastFullCompile: string;
124
+ lastIncrementalCompile: string;
125
+ compilationPromptHash: string;
126
+ }
127
+
128
+ // ─── Lint ────────────────────────────────────────────────────────────────
129
+
130
+ export type WikiLintCheck =
131
+ | "stale"
132
+ | "orphan"
133
+ | "gap"
134
+ | "broken_link"
135
+ | "contradiction"
136
+ | "spec_drift"
137
+ | "decision_violation"
138
+ | "missing_rationale";
139
+
140
+ export interface WikiLintFinding {
141
+ check: string;
142
+ severity: "error" | "warning" | "info";
143
+ article: string;
144
+ message: string;
145
+ source?: string;
146
+ }
147
+
148
+ export interface WikiLintResult {
149
+ stale: WikiLintFinding[];
150
+ orphans: WikiLintFinding[];
151
+ gaps: WikiLintFinding[];
152
+ brokenLinks: WikiLintFinding[];
153
+ contradictions: WikiLintFinding[];
154
+ specDrift: WikiLintFinding[];
155
+ decisionViolations: WikiLintFinding[];
156
+ missingRationale: WikiLintFinding[];
157
+ coveragePercent: number;
158
+ }
159
+
160
+ // ─── Ebbinghaus Decay Half-Lives (days) ─────────────────────────────────
161
+
162
+ export const DECAY_HALF_LIVES: Record<ArticleType, number> = {
163
+ decision: 180,
164
+ architecture: 150,
165
+ module: 120,
166
+ entity: 90,
167
+ feature: 60,
168
+ raw: 90,
169
+ };
@@ -63,6 +63,32 @@ export function appendWorkflowStep(
63
63
  appendFileSync(filePath, entry);
64
64
  }
65
65
 
66
+ /**
67
+ * Append wiki reference tracking to the workflow file.
68
+ * Records which wiki articles were read and written during a workflow step.
69
+ */
70
+ export function appendWikiRefs(
71
+ mainaDir: string,
72
+ step: string,
73
+ refsRead: string[],
74
+ refsWritten: string[],
75
+ ): void {
76
+ const filePath = workflowFilePath(mainaDir);
77
+ if (!existsSync(filePath)) {
78
+ const dir = dirname(filePath);
79
+ if (!existsSync(dir)) {
80
+ mkdirSync(dir, { recursive: true });
81
+ }
82
+ writeFileSync(filePath, "# Workflow\n");
83
+ }
84
+
85
+ const readList = refsRead.length > 0 ? refsRead.join(", ") : "_none_";
86
+ const writtenList =
87
+ refsWritten.length > 0 ? refsWritten.join(", ") : "_none_";
88
+ const entry = `\nWiki refs for ${step}:\n Read: ${readList}\n Written: ${writtenList}\n`;
89
+ appendFileSync(filePath, entry);
90
+ }
91
+
66
92
  /**
67
93
  * Load the current workflow context.
68
94
  * Returns the full markdown content, or null if no workflow is active.