@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.
- package/package.json +1 -1
- package/src/ai/__tests__/delegation.test.ts +55 -1
- package/src/ai/delegation.ts +5 -3
- package/src/context/__tests__/budget.test.ts +29 -6
- package/src/context/__tests__/engine.test.ts +1 -0
- package/src/context/__tests__/selector.test.ts +23 -3
- package/src/context/__tests__/wiki.test.ts +349 -0
- package/src/context/budget.ts +12 -8
- package/src/context/engine.ts +37 -0
- package/src/context/selector.ts +30 -4
- package/src/context/wiki.ts +296 -0
- package/src/db/index.ts +12 -0
- package/src/feedback/__tests__/capture.test.ts +166 -0
- package/src/feedback/__tests__/signals.test.ts +144 -0
- package/src/feedback/__tests__/tmp-capture-1775575256633-lah0etnzlj/feedback.db +0 -0
- package/src/feedback/__tests__/tmp-capture-1775575256640-2xmjme4qraa/feedback.db +0 -0
- package/src/feedback/capture.ts +102 -0
- package/src/feedback/signals.ts +68 -0
- package/src/index.ts +104 -0
- package/src/init/__tests__/init.test.ts +400 -3
- package/src/init/index.ts +368 -12
- package/src/language/__tests__/__fixtures__/detect/composer.lock +1 -0
- package/src/prompts/defaults/index.ts +3 -1
- package/src/prompts/defaults/wiki-compile.md +20 -0
- package/src/prompts/defaults/wiki-query.md +18 -0
- package/src/stats/__tests__/tool-usage.test.ts +133 -0
- package/src/stats/tracker.ts +92 -0
- package/src/verify/__tests__/pipeline.test.ts +11 -8
- package/src/verify/pipeline.ts +13 -1
- package/src/verify/tools/__tests__/wiki-lint.test.ts +784 -0
- package/src/verify/tools/wiki-lint-runner.ts +38 -0
- package/src/verify/tools/wiki-lint.ts +898 -0
- package/src/wiki/__tests__/compiler.test.ts +389 -0
- package/src/wiki/__tests__/extractors/code.test.ts +99 -0
- package/src/wiki/__tests__/extractors/decision.test.ts +323 -0
- package/src/wiki/__tests__/extractors/feature.test.ts +186 -0
- package/src/wiki/__tests__/extractors/workflow.test.ts +131 -0
- package/src/wiki/__tests__/graph.test.ts +344 -0
- package/src/wiki/__tests__/hooks.test.ts +119 -0
- package/src/wiki/__tests__/indexer.test.ts +285 -0
- package/src/wiki/__tests__/linker.test.ts +230 -0
- package/src/wiki/__tests__/louvain.test.ts +229 -0
- package/src/wiki/__tests__/query.test.ts +316 -0
- package/src/wiki/__tests__/schema.test.ts +114 -0
- package/src/wiki/__tests__/signals.test.ts +474 -0
- package/src/wiki/__tests__/state.test.ts +168 -0
- package/src/wiki/__tests__/tracking.test.ts +118 -0
- package/src/wiki/__tests__/types.test.ts +387 -0
- package/src/wiki/compiler.ts +1075 -0
- package/src/wiki/extractors/code.ts +90 -0
- package/src/wiki/extractors/decision.ts +217 -0
- package/src/wiki/extractors/feature.ts +206 -0
- package/src/wiki/extractors/workflow.ts +112 -0
- package/src/wiki/graph.ts +445 -0
- package/src/wiki/hooks.ts +49 -0
- package/src/wiki/indexer.ts +105 -0
- package/src/wiki/linker.ts +117 -0
- package/src/wiki/louvain.ts +190 -0
- package/src/wiki/prompts/compile-architecture.md +59 -0
- package/src/wiki/prompts/compile-decision.md +66 -0
- package/src/wiki/prompts/compile-entity.md +56 -0
- package/src/wiki/prompts/compile-feature.md +60 -0
- package/src/wiki/prompts/compile-module.md +42 -0
- package/src/wiki/prompts/wiki-query.md +25 -0
- package/src/wiki/query.ts +338 -0
- package/src/wiki/schema.ts +111 -0
- package/src/wiki/signals.ts +368 -0
- package/src/wiki/state.ts +89 -0
- package/src/wiki/tracking.ts +30 -0
- package/src/wiki/types.ts +169 -0
- package/src/workflow/context.ts +26 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { trackWikiRefsRead, trackWikiRefsWritten } from "../tracking";
|
|
12
|
+
|
|
13
|
+
// ─── Test Fixtures ──────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
let tmpDir: string;
|
|
16
|
+
let mainaDir: string;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
tmpDir = join(
|
|
20
|
+
tmpdir(),
|
|
21
|
+
`wiki-tracking-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
22
|
+
);
|
|
23
|
+
mainaDir = join(tmpDir, ".maina");
|
|
24
|
+
mkdirSync(mainaDir, { recursive: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// ─── Tests ──────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
describe("Wiki Tracking", () => {
|
|
34
|
+
describe("trackWikiRefsRead", () => {
|
|
35
|
+
it("should append read refs to workflow file", () => {
|
|
36
|
+
trackWikiRefsRead(mainaDir, "wiki-query", [
|
|
37
|
+
"wiki/modules/core.md",
|
|
38
|
+
"wiki/entities/compile.md",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const workflowFile = join(mainaDir, "workflow", "current.md");
|
|
42
|
+
expect(existsSync(workflowFile)).toBe(true);
|
|
43
|
+
|
|
44
|
+
const content = readFileSync(workflowFile, "utf-8");
|
|
45
|
+
expect(content).toContain("Wiki refs for wiki-query:");
|
|
46
|
+
expect(content).toContain(
|
|
47
|
+
"Read: wiki/modules/core.md, wiki/entities/compile.md",
|
|
48
|
+
);
|
|
49
|
+
expect(content).toContain("Written: _none_");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("should handle empty articles list", () => {
|
|
53
|
+
trackWikiRefsRead(mainaDir, "compile", []);
|
|
54
|
+
|
|
55
|
+
const workflowFile = join(mainaDir, "workflow", "current.md");
|
|
56
|
+
expect(existsSync(workflowFile)).toBe(true);
|
|
57
|
+
|
|
58
|
+
const content = readFileSync(workflowFile, "utf-8");
|
|
59
|
+
expect(content).toContain("Read: _none_");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("trackWikiRefsWritten", () => {
|
|
64
|
+
it("should append written refs to workflow file", () => {
|
|
65
|
+
trackWikiRefsWritten(mainaDir, "wiki-compile", [
|
|
66
|
+
"wiki/modules/auth.md",
|
|
67
|
+
"wiki/index.md",
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
const workflowFile = join(mainaDir, "workflow", "current.md");
|
|
71
|
+
expect(existsSync(workflowFile)).toBe(true);
|
|
72
|
+
|
|
73
|
+
const content = readFileSync(workflowFile, "utf-8");
|
|
74
|
+
expect(content).toContain("Wiki refs for wiki-compile:");
|
|
75
|
+
expect(content).toContain("Read: _none_");
|
|
76
|
+
expect(content).toContain("Written: wiki/modules/auth.md, wiki/index.md");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should handle empty articles list", () => {
|
|
80
|
+
trackWikiRefsWritten(mainaDir, "compile", []);
|
|
81
|
+
|
|
82
|
+
const workflowFile = join(mainaDir, "workflow", "current.md");
|
|
83
|
+
expect(existsSync(workflowFile)).toBe(true);
|
|
84
|
+
|
|
85
|
+
const content = readFileSync(workflowFile, "utf-8");
|
|
86
|
+
expect(content).toContain("Written: _none_");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("edge cases", () => {
|
|
91
|
+
it("should handle missing workflow file gracefully", () => {
|
|
92
|
+
// mainaDir exists but workflow/ does not
|
|
93
|
+
expect(() => {
|
|
94
|
+
trackWikiRefsRead(mainaDir, "test-step", ["wiki/test.md"]);
|
|
95
|
+
}).not.toThrow();
|
|
96
|
+
|
|
97
|
+
const workflowFile = join(mainaDir, "workflow", "current.md");
|
|
98
|
+
expect(existsSync(workflowFile)).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should append to existing workflow file", () => {
|
|
102
|
+
const workflowDir = join(mainaDir, "workflow");
|
|
103
|
+
mkdirSync(workflowDir, { recursive: true });
|
|
104
|
+
writeFileSync(
|
|
105
|
+
join(workflowDir, "current.md"),
|
|
106
|
+
"# Workflow: test-feature\n",
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
trackWikiRefsRead(mainaDir, "step-1", ["wiki/a.md"]);
|
|
110
|
+
trackWikiRefsWritten(mainaDir, "step-2", ["wiki/b.md"]);
|
|
111
|
+
|
|
112
|
+
const content = readFileSync(join(workflowDir, "current.md"), "utf-8");
|
|
113
|
+
expect(content).toContain("# Workflow: test-feature");
|
|
114
|
+
expect(content).toContain("Wiki refs for step-1:");
|
|
115
|
+
expect(content).toContain("Wiki refs for step-2:");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type {
|
|
3
|
+
ArticleType,
|
|
4
|
+
EdgeType,
|
|
5
|
+
ExtractedDecision,
|
|
6
|
+
ExtractedFeature,
|
|
7
|
+
ExtractedWorkflowTrace,
|
|
8
|
+
WikiArticle,
|
|
9
|
+
WikiLink,
|
|
10
|
+
WikiLintFinding,
|
|
11
|
+
WikiLintResult,
|
|
12
|
+
WikiState,
|
|
13
|
+
} from "../types";
|
|
14
|
+
|
|
15
|
+
describe("Wiki Types", () => {
|
|
16
|
+
describe("WikiArticle", () => {
|
|
17
|
+
it("happy path: should create a valid wiki article", () => {
|
|
18
|
+
const article: WikiArticle = {
|
|
19
|
+
path: "modules/auth.md",
|
|
20
|
+
type: "module",
|
|
21
|
+
title: "Auth Module",
|
|
22
|
+
content: "# Auth Module\n\nHandles authentication.",
|
|
23
|
+
contentHash: "abc123",
|
|
24
|
+
sourceHashes: ["def456", "ghi789"],
|
|
25
|
+
backlinks: [],
|
|
26
|
+
forwardLinks: [],
|
|
27
|
+
pageRank: 0.85,
|
|
28
|
+
lastCompiled: "2026-04-07T00:00:00.000Z",
|
|
29
|
+
referenceCount: 5,
|
|
30
|
+
ebbinghausScore: 0.9,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
expect(article.path).toBe("modules/auth.md");
|
|
34
|
+
expect(article.type).toBe("module");
|
|
35
|
+
expect(article.pageRank).toBe(0.85);
|
|
36
|
+
expect(article.ebbinghausScore).toBe(0.9);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should support all 6 article types", () => {
|
|
40
|
+
const types: ArticleType[] = [
|
|
41
|
+
"module",
|
|
42
|
+
"entity",
|
|
43
|
+
"feature",
|
|
44
|
+
"decision",
|
|
45
|
+
"architecture",
|
|
46
|
+
"raw",
|
|
47
|
+
];
|
|
48
|
+
expect(types).toHaveLength(6);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should support articles with forward and backward links", () => {
|
|
52
|
+
const link: WikiLink = {
|
|
53
|
+
target: "entities/runPipeline.md",
|
|
54
|
+
type: "calls",
|
|
55
|
+
weight: 0.7,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const article: WikiArticle = {
|
|
59
|
+
path: "modules/verify.md",
|
|
60
|
+
type: "module",
|
|
61
|
+
title: "Verify Module",
|
|
62
|
+
content: "# Verify",
|
|
63
|
+
contentHash: "hash1",
|
|
64
|
+
sourceHashes: ["hash2"],
|
|
65
|
+
backlinks: [{ target: "modules/cli.md", type: "imports", weight: 0.5 }],
|
|
66
|
+
forwardLinks: [link],
|
|
67
|
+
pageRank: 0.6,
|
|
68
|
+
lastCompiled: "2026-04-07T00:00:00.000Z",
|
|
69
|
+
referenceCount: 3,
|
|
70
|
+
ebbinghausScore: 0.75,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
expect(article.forwardLinks).toHaveLength(1);
|
|
74
|
+
expect(article.backlinks).toHaveLength(1);
|
|
75
|
+
expect(article.forwardLinks[0]?.type).toBe("calls");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("edge case: article with zero pageRank and expired ebbinghaus", () => {
|
|
79
|
+
const article: WikiArticle = {
|
|
80
|
+
path: "entities/deprecated.md",
|
|
81
|
+
type: "entity",
|
|
82
|
+
title: "Deprecated Entity",
|
|
83
|
+
content: "",
|
|
84
|
+
contentHash: "empty",
|
|
85
|
+
sourceHashes: [],
|
|
86
|
+
backlinks: [],
|
|
87
|
+
forwardLinks: [],
|
|
88
|
+
pageRank: 0,
|
|
89
|
+
lastCompiled: "2025-01-01T00:00:00.000Z",
|
|
90
|
+
referenceCount: 0,
|
|
91
|
+
ebbinghausScore: 0.1,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
expect(article.pageRank).toBe(0);
|
|
95
|
+
expect(article.ebbinghausScore).toBeLessThan(0.2);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should serialize/deserialize via JSON round-trip", () => {
|
|
99
|
+
const article: WikiArticle = {
|
|
100
|
+
path: "features/001-auth.md",
|
|
101
|
+
type: "feature",
|
|
102
|
+
title: "Auth Feature",
|
|
103
|
+
content: "# Auth\n\nToken refresh.",
|
|
104
|
+
contentHash: "abc",
|
|
105
|
+
sourceHashes: ["src1", "src2"],
|
|
106
|
+
backlinks: [
|
|
107
|
+
{ target: "entities/jwt.md", type: "modified_by", weight: 1.0 },
|
|
108
|
+
],
|
|
109
|
+
forwardLinks: [
|
|
110
|
+
{ target: "decisions/002.md", type: "decided_by", weight: 0.8 },
|
|
111
|
+
],
|
|
112
|
+
pageRank: 0.5,
|
|
113
|
+
lastCompiled: "2026-04-07T12:00:00.000Z",
|
|
114
|
+
referenceCount: 10,
|
|
115
|
+
ebbinghausScore: 0.65,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const json = JSON.stringify(article);
|
|
119
|
+
const parsed: WikiArticle = JSON.parse(json);
|
|
120
|
+
|
|
121
|
+
expect(parsed.path).toBe(article.path);
|
|
122
|
+
expect(parsed.type).toBe(article.type);
|
|
123
|
+
expect(parsed.backlinks).toHaveLength(1);
|
|
124
|
+
expect(parsed.forwardLinks).toHaveLength(1);
|
|
125
|
+
expect(parsed.backlinks[0]?.type).toBe("modified_by");
|
|
126
|
+
expect(parsed.pageRank).toBe(0.5);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("WikiLink", () => {
|
|
131
|
+
it("should support all 11 edge types", () => {
|
|
132
|
+
const edgeTypes: EdgeType[] = [
|
|
133
|
+
"calls",
|
|
134
|
+
"imports",
|
|
135
|
+
"inherits",
|
|
136
|
+
"references",
|
|
137
|
+
"member_of",
|
|
138
|
+
"modified_by",
|
|
139
|
+
"specified_by",
|
|
140
|
+
"decided_by",
|
|
141
|
+
"motivated_by",
|
|
142
|
+
"constrains",
|
|
143
|
+
"aligns_with",
|
|
144
|
+
];
|
|
145
|
+
expect(edgeTypes).toHaveLength(11);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("should have weight between 0 and 1", () => {
|
|
149
|
+
const link: WikiLink = {
|
|
150
|
+
target: "modules/core.md",
|
|
151
|
+
type: "imports",
|
|
152
|
+
weight: 0.5,
|
|
153
|
+
};
|
|
154
|
+
expect(link.weight).toBeGreaterThanOrEqual(0);
|
|
155
|
+
expect(link.weight).toBeLessThanOrEqual(1);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("ExtractedFeature", () => {
|
|
160
|
+
it("happy path: should represent a fully extracted feature", () => {
|
|
161
|
+
const feature: ExtractedFeature = {
|
|
162
|
+
id: "001-token-refresh",
|
|
163
|
+
title: "Token Refresh",
|
|
164
|
+
scope: "Add automatic JWT token refresh to the auth module",
|
|
165
|
+
specQualityScore: 85,
|
|
166
|
+
specAssertions: [
|
|
167
|
+
"Tokens refresh 5 minutes before expiry",
|
|
168
|
+
"Failed refresh triggers re-login",
|
|
169
|
+
],
|
|
170
|
+
tasks: [
|
|
171
|
+
{ id: "T001", description: "Add refresh timer", completed: false },
|
|
172
|
+
{ id: "T002", description: "Wire error handler", completed: true },
|
|
173
|
+
],
|
|
174
|
+
entitiesModified: ["src/auth/jwt.ts", "src/auth/refresh.ts"],
|
|
175
|
+
decisionsCreated: ["002-jwt-strategy"],
|
|
176
|
+
branch: "feature/001-token-refresh",
|
|
177
|
+
prNumber: 42,
|
|
178
|
+
merged: true,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
expect(feature.id).toBe("001-token-refresh");
|
|
182
|
+
expect(feature.tasks).toHaveLength(2);
|
|
183
|
+
expect(feature.specAssertions).toHaveLength(2);
|
|
184
|
+
expect(feature.merged).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("edge case: feature with no spec, no tasks, no PR", () => {
|
|
188
|
+
const feature: ExtractedFeature = {
|
|
189
|
+
id: "099-wip",
|
|
190
|
+
title: "Work in Progress",
|
|
191
|
+
scope: "",
|
|
192
|
+
specQualityScore: 0,
|
|
193
|
+
specAssertions: [],
|
|
194
|
+
tasks: [],
|
|
195
|
+
entitiesModified: [],
|
|
196
|
+
decisionsCreated: [],
|
|
197
|
+
branch: "",
|
|
198
|
+
prNumber: null,
|
|
199
|
+
merged: false,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
expect(feature.specQualityScore).toBe(0);
|
|
203
|
+
expect(feature.tasks).toHaveLength(0);
|
|
204
|
+
expect(feature.prNumber).toBeNull();
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("ExtractedDecision", () => {
|
|
209
|
+
it("happy path: should represent a fully extracted ADR", () => {
|
|
210
|
+
const decision: ExtractedDecision = {
|
|
211
|
+
id: "002-jwt-strategy",
|
|
212
|
+
title: "Use JWT for Authentication",
|
|
213
|
+
status: "accepted",
|
|
214
|
+
context: "We need stateless auth for microservices.",
|
|
215
|
+
decision: "Use JWT tokens with RS256 signing.",
|
|
216
|
+
rationale: "Stateless, scalable, widely supported.",
|
|
217
|
+
alternativesRejected: ["Session-based auth", "OAuth2 only"],
|
|
218
|
+
entityMentions: ["src/auth/jwt.ts", "src/middleware/auth.ts"],
|
|
219
|
+
constitutionAlignment: ["Error handling: Result<T,E>"],
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
expect(decision.status).toBe("accepted");
|
|
223
|
+
expect(decision.alternativesRejected).toHaveLength(2);
|
|
224
|
+
expect(decision.entityMentions).toHaveLength(2);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("should support all 4 decision statuses", () => {
|
|
228
|
+
const statuses: ExtractedDecision["status"][] = [
|
|
229
|
+
"proposed",
|
|
230
|
+
"accepted",
|
|
231
|
+
"deprecated",
|
|
232
|
+
"superseded",
|
|
233
|
+
];
|
|
234
|
+
expect(statuses).toHaveLength(4);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("ExtractedWorkflowTrace", () => {
|
|
239
|
+
it("happy path: should represent a workflow trace with wiki refs", () => {
|
|
240
|
+
const trace: ExtractedWorkflowTrace = {
|
|
241
|
+
featureId: "001-token-refresh",
|
|
242
|
+
steps: [
|
|
243
|
+
{
|
|
244
|
+
command: "brainstorm",
|
|
245
|
+
timestamp: "2026-04-07T10:00:00.000Z",
|
|
246
|
+
summary: "Explored auth options",
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
command: "plan",
|
|
250
|
+
timestamp: "2026-04-07T10:30:00.000Z",
|
|
251
|
+
summary: "Scaffolded feature",
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
command: "commit",
|
|
255
|
+
timestamp: "2026-04-07T12:00:00.000Z",
|
|
256
|
+
summary: "Initial implementation",
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
wikiRefsRead: ["wiki/modules/auth.md", "wiki/decisions/002.md"],
|
|
260
|
+
wikiRefsWritten: ["wiki/features/001-token-refresh.md"],
|
|
261
|
+
rlSignals: [
|
|
262
|
+
{ step: "review", accepted: true },
|
|
263
|
+
{ step: "verify", accepted: true },
|
|
264
|
+
],
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
expect(trace.steps).toHaveLength(3);
|
|
268
|
+
expect(trace.wikiRefsRead).toHaveLength(2);
|
|
269
|
+
expect(trace.wikiRefsWritten).toHaveLength(1);
|
|
270
|
+
expect(trace.rlSignals).toHaveLength(2);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("edge case: empty workflow trace", () => {
|
|
274
|
+
const trace: ExtractedWorkflowTrace = {
|
|
275
|
+
featureId: "",
|
|
276
|
+
steps: [],
|
|
277
|
+
wikiRefsRead: [],
|
|
278
|
+
wikiRefsWritten: [],
|
|
279
|
+
rlSignals: [],
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
expect(trace.steps).toHaveLength(0);
|
|
283
|
+
expect(trace.rlSignals).toHaveLength(0);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe("WikiState", () => {
|
|
288
|
+
it("should serialize/deserialize state via JSON round-trip", () => {
|
|
289
|
+
const state: WikiState = {
|
|
290
|
+
fileHashes: {
|
|
291
|
+
"src/auth/jwt.ts": "abc123",
|
|
292
|
+
"src/auth/refresh.ts": "def456",
|
|
293
|
+
},
|
|
294
|
+
articleHashes: {
|
|
295
|
+
"modules/auth.md": "ghi789",
|
|
296
|
+
},
|
|
297
|
+
lastFullCompile: "2026-04-07T00:00:00.000Z",
|
|
298
|
+
lastIncrementalCompile: "2026-04-07T12:00:00.000Z",
|
|
299
|
+
compilationPromptHash: "prompt_hash_v1",
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const json = JSON.stringify(state);
|
|
303
|
+
const parsed: WikiState = JSON.parse(json);
|
|
304
|
+
|
|
305
|
+
expect(parsed.fileHashes["src/auth/jwt.ts"]).toBe("abc123");
|
|
306
|
+
expect(parsed.articleHashes["modules/auth.md"]).toBe("ghi789");
|
|
307
|
+
expect(parsed.lastFullCompile).toBe("2026-04-07T00:00:00.000Z");
|
|
308
|
+
expect(parsed.compilationPromptHash).toBe("prompt_hash_v1");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("edge case: empty state", () => {
|
|
312
|
+
const state: WikiState = {
|
|
313
|
+
fileHashes: {},
|
|
314
|
+
articleHashes: {},
|
|
315
|
+
lastFullCompile: "",
|
|
316
|
+
lastIncrementalCompile: "",
|
|
317
|
+
compilationPromptHash: "",
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
expect(Object.keys(state.fileHashes)).toHaveLength(0);
|
|
321
|
+
expect(state.lastFullCompile).toBe("");
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe("WikiLintResult", () => {
|
|
326
|
+
it("happy path: should represent a full lint result", () => {
|
|
327
|
+
const finding: WikiLintFinding = {
|
|
328
|
+
check: "stale",
|
|
329
|
+
severity: "warning",
|
|
330
|
+
article: "modules/auth.md",
|
|
331
|
+
message: "Article not recompiled since source changed",
|
|
332
|
+
source: "src/auth/jwt.ts",
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const result: WikiLintResult = {
|
|
336
|
+
stale: [finding],
|
|
337
|
+
orphans: [],
|
|
338
|
+
gaps: [
|
|
339
|
+
{
|
|
340
|
+
check: "gap",
|
|
341
|
+
severity: "info",
|
|
342
|
+
article: "",
|
|
343
|
+
message: "High-PageRank entity without article",
|
|
344
|
+
source: "src/verify/pipeline.ts",
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
brokenLinks: [],
|
|
348
|
+
contradictions: [],
|
|
349
|
+
specDrift: [],
|
|
350
|
+
decisionViolations: [],
|
|
351
|
+
missingRationale: [],
|
|
352
|
+
coveragePercent: 72.5,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
expect(result.stale).toHaveLength(1);
|
|
356
|
+
expect(result.gaps).toHaveLength(1);
|
|
357
|
+
expect(result.coveragePercent).toBe(72.5);
|
|
358
|
+
expect(result.brokenLinks).toHaveLength(0);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("edge case: perfect lint result", () => {
|
|
362
|
+
const result: WikiLintResult = {
|
|
363
|
+
stale: [],
|
|
364
|
+
orphans: [],
|
|
365
|
+
gaps: [],
|
|
366
|
+
brokenLinks: [],
|
|
367
|
+
contradictions: [],
|
|
368
|
+
specDrift: [],
|
|
369
|
+
decisionViolations: [],
|
|
370
|
+
missingRationale: [],
|
|
371
|
+
coveragePercent: 100,
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
expect(result.coveragePercent).toBe(100);
|
|
375
|
+
const totalFindings =
|
|
376
|
+
result.stale.length +
|
|
377
|
+
result.orphans.length +
|
|
378
|
+
result.gaps.length +
|
|
379
|
+
result.brokenLinks.length +
|
|
380
|
+
result.contradictions.length +
|
|
381
|
+
result.specDrift.length +
|
|
382
|
+
result.decisionViolations.length +
|
|
383
|
+
result.missingRationale.length;
|
|
384
|
+
expect(totalFindings).toBe(0);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
});
|