@mmnto/mcp 1.14.10 → 1.14.11

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.
@@ -0,0 +1,117 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { DescribeProjectInputSchema, DescribeProjectOutputSchema, GitStateSchema, MilestoneStateSchema, RECENT_PRS_COUNT, RichProjectStateSchema, RuleCountsSchema, StrategyPointerSchema, UNCOMMITTED_FILES_CAP, } from './describe-project.js';
3
+ describe('DescribeProjectInputSchema', () => {
4
+ it('accepts empty input and defaults includeRichState to false', () => {
5
+ const parsed = DescribeProjectInputSchema.parse({});
6
+ expect(parsed.includeRichState).toBe(false);
7
+ });
8
+ it('accepts explicit includeRichState false', () => {
9
+ const parsed = DescribeProjectInputSchema.parse({ includeRichState: false });
10
+ expect(parsed.includeRichState).toBe(false);
11
+ });
12
+ it('accepts explicit includeRichState true', () => {
13
+ const parsed = DescribeProjectInputSchema.parse({ includeRichState: true });
14
+ expect(parsed.includeRichState).toBe(true);
15
+ });
16
+ it('rejects non-boolean includeRichState', () => {
17
+ expect(() => DescribeProjectInputSchema.parse({ includeRichState: 'yes' })).toThrow();
18
+ });
19
+ });
20
+ describe('DescribeProjectOutputSchema backward compatibility', () => {
21
+ const legacyShape = {
22
+ project: 'test',
23
+ tier: 'standard',
24
+ rules: 10,
25
+ lessons: 5,
26
+ targets: ['**/*.ts (code/typescript-ast)'],
27
+ partitions: { core: ['packages/core/'] },
28
+ hooks: ['pre-push'],
29
+ };
30
+ it('accepts legacy shape without richState', () => {
31
+ const parsed = DescribeProjectOutputSchema.parse(legacyShape);
32
+ expect(parsed.richState).toBeUndefined();
33
+ });
34
+ it('accepts legacy shape with richState populated', () => {
35
+ const rich = {
36
+ strategyPointer: { sha: 'abc1234', latestJournal: '2026-04-16-session.md' },
37
+ gitState: { branch: 'main', uncommittedFiles: [], truncated: false },
38
+ packageVersions: { '@mmnto/cli': '1.14.10' },
39
+ ruleCounts: { active: 10, archived: 2, nonCompilable: 3 },
40
+ lessonCount: 5,
41
+ testCount: null,
42
+ milestone: { name: '1.15.0', gateTickets: ['#1479'], bestEffort: true },
43
+ recentPrs: [{ title: 'feat: foo (#1)', date: '2026-04-16T00:00:00Z', squashSha: 'abcd123' }],
44
+ };
45
+ const parsed = DescribeProjectOutputSchema.parse({ ...legacyShape, richState: rich });
46
+ expect(parsed.richState?.ruleCounts.active).toBe(10);
47
+ });
48
+ it('rejects malformed richState', () => {
49
+ expect(() => DescribeProjectOutputSchema.parse({
50
+ ...legacyShape,
51
+ richState: { strategyPointer: 'not an object' },
52
+ })).toThrow();
53
+ });
54
+ });
55
+ describe('GitStateSchema', () => {
56
+ it('accepts null branch (outside git repo)', () => {
57
+ const parsed = GitStateSchema.parse({ branch: null, uncommittedFiles: [], truncated: false });
58
+ expect(parsed.branch).toBeNull();
59
+ });
60
+ it('accepts branch + files + truncation marker', () => {
61
+ const parsed = GitStateSchema.parse({
62
+ branch: 'main',
63
+ uncommittedFiles: ['a.ts', 'b.ts'],
64
+ truncated: true,
65
+ });
66
+ expect(parsed.truncated).toBe(true);
67
+ });
68
+ });
69
+ describe('RuleCountsSchema', () => {
70
+ it('rejects negative counts', () => {
71
+ expect(() => RuleCountsSchema.parse({ active: -1, archived: 0, nonCompilable: 0 })).toThrow();
72
+ });
73
+ it('rejects non-integer counts', () => {
74
+ expect(() => RuleCountsSchema.parse({ active: 1.5, archived: 0, nonCompilable: 0 })).toThrow();
75
+ });
76
+ });
77
+ describe('MilestoneStateSchema', () => {
78
+ it('requires bestEffort literal true', () => {
79
+ expect(() => MilestoneStateSchema.parse({ name: null, gateTickets: [], bestEffort: false })).toThrow();
80
+ });
81
+ it('accepts null name with empty gateTickets', () => {
82
+ const parsed = MilestoneStateSchema.parse({
83
+ name: null,
84
+ gateTickets: [],
85
+ bestEffort: true,
86
+ });
87
+ expect(parsed.name).toBeNull();
88
+ });
89
+ });
90
+ describe('StrategyPointerSchema', () => {
91
+ it('allows both fields null (no submodule)', () => {
92
+ const parsed = StrategyPointerSchema.parse({ sha: null, latestJournal: null });
93
+ expect(parsed.sha).toBeNull();
94
+ });
95
+ });
96
+ describe('RichProjectStateSchema', () => {
97
+ it('allows testCount: null explicitly', () => {
98
+ const parsed = RichProjectStateSchema.parse({
99
+ strategyPointer: { sha: null, latestJournal: null },
100
+ gitState: { branch: null, uncommittedFiles: [], truncated: false },
101
+ packageVersions: {},
102
+ ruleCounts: { active: 0, archived: 0, nonCompilable: 0 },
103
+ lessonCount: 0,
104
+ testCount: null,
105
+ milestone: { name: null, gateTickets: [], bestEffort: true },
106
+ recentPrs: [],
107
+ });
108
+ expect(parsed.testCount).toBeNull();
109
+ });
110
+ });
111
+ describe('constants', () => {
112
+ it('caps match the design doc', () => {
113
+ expect(UNCOMMITTED_FILES_CAP).toBe(50);
114
+ expect(RECENT_PRS_COUNT).toBe(5);
115
+ });
116
+ });
117
+ //# sourceMappingURL=describe-project.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"describe-project.test.js","sourceRoot":"","sources":["../../src/schemas/describe-project.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * State extractors for the rich `describe_project` payload.
3
+ *
4
+ * Each extractor reads from local git, filesystem, or stored-state files and
5
+ * returns its schema shape. On missing source files or non-zero git exit,
6
+ * extractors degrade gracefully (null / 0 / []) rather than throwing, so the
7
+ * MCP handler can compose a partial payload instead of crashing.
8
+ *
9
+ * ADR-090 substrate invariant: no LLM calls, no live npm/github registry
10
+ * calls. Every function reads only from disk or local git state.
11
+ */
12
+ import { type GitState, type MilestoneState, type RecentPr, type RuleCounts, type StrategyPointer } from './schemas/describe-project.js';
13
+ export declare function extractGitState(cwd: string): GitState;
14
+ export declare function extractStrategyPointer(cwd: string): StrategyPointer;
15
+ export declare function extractPackageVersions(cwd: string): Record<string, string>;
16
+ export declare function extractRuleCounts(cwd: string, totemDir: string): RuleCounts;
17
+ export declare function extractLessonCount(cwd: string, totemDir: string): number;
18
+ /**
19
+ * Regex-parse the milestone name and gate-ticket list from active_work.md.
20
+ * This is explicitly best-effort — the markdown format can drift.
21
+ * `bestEffort: true` signals to agents that the values are a hint, not a
22
+ * ground-truth source.
23
+ */
24
+ export declare function extractMilestoneState(cwd: string): MilestoneState;
25
+ /**
26
+ * Stored test-count artifact does not exist in v1. Follow-up ticket wires
27
+ * postmerge to stamp `.totem/store/test-stats.json` after `pnpm test` runs;
28
+ * until then the endpoint reports null honestly rather than fabricate a
29
+ * number.
30
+ */
31
+ export declare function extractTestCount(_cwd: string): number | null;
32
+ /**
33
+ * Capture squash-merge commits whose subject references a PR number
34
+ * (`... (#NNNN)`). Skips commits whose message lacks a PR tag so we do not
35
+ * include non-PR merges like the Version Packages auto-commit in some flows.
36
+ */
37
+ export declare function extractRecentPrs(cwd: string, limit?: number): RecentPr[];
38
+ //# sourceMappingURL=state-extractors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-extractors.d.ts","sourceRoot":"","sources":["../src/state-extractors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAOH,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,cAAc,EAEnB,KAAK,QAAQ,EACb,KAAK,UAAU,EACf,KAAK,eAAe,EAErB,MAAM,+BAA+B,CAAC;AAYvC,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,CA+BrD;AAID,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CA+BnE;AAID,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAkC1E;AAID,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,CAuB3E;AAID,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CASxE;AAID;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAoCjE;AAID;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE5D;AAID;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,SAAmB,GAAG,QAAQ,EAAE,CAyClF"}
@@ -0,0 +1,259 @@
1
+ /**
2
+ * State extractors for the rich `describe_project` payload.
3
+ *
4
+ * Each extractor reads from local git, filesystem, or stored-state files and
5
+ * returns its schema shape. On missing source files or non-zero git exit,
6
+ * extractors degrade gracefully (null / 0 / []) rather than throwing, so the
7
+ * MCP handler can compose a partial payload instead of crashing.
8
+ *
9
+ * ADR-090 substrate invariant: no LLM calls, no live npm/github registry
10
+ * calls. Every function reads only from disk or local git state.
11
+ */
12
+ import * as fs from 'node:fs';
13
+ import * as path from 'node:path';
14
+ import { CompiledRulesFileSchema, readJsonSafe, resolveGitRoot, safeExec } from '@mmnto/totem';
15
+ import { RECENT_PRS_COUNT, UNCOMMITTED_FILES_CAP, } from './schemas/describe-project.js';
16
+ /** Fixed-group package names whose versions show in the briefing. */
17
+ const FIXED_GROUP_PACKAGES = [
18
+ '@mmnto/totem',
19
+ '@mmnto/cli',
20
+ '@mmnto/mcp',
21
+ '@totem/pack-agent-security',
22
+ ];
23
+ // ─── Git state ─────────────────────────────────────────────────────────────
24
+ export function extractGitState(cwd) {
25
+ if (resolveGitRoot(cwd) === null) {
26
+ return { branch: null, uncommittedFiles: [], truncated: false };
27
+ }
28
+ let branch = null;
29
+ try {
30
+ const out = safeExec('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd });
31
+ branch = out === 'HEAD' ? null : out;
32
+ // totem-context: ADR-090 substrate graceful degradation — partial payload over crash.
33
+ }
34
+ catch {
35
+ branch = null;
36
+ }
37
+ let allFiles = [];
38
+ try {
39
+ const porcelain = safeExec('git', ['status', '--porcelain'], { cwd });
40
+ if (porcelain.length > 0) {
41
+ allFiles = porcelain
42
+ .split(/\r?\n/)
43
+ .map((line) => line.slice(3).trim())
44
+ .filter((name) => name.length > 0);
45
+ }
46
+ // totem-context: ADR-090 substrate graceful degradation — empty file list on git-status failure.
47
+ }
48
+ catch {
49
+ allFiles = [];
50
+ }
51
+ const truncated = allFiles.length > UNCOMMITTED_FILES_CAP;
52
+ const uncommittedFiles = truncated ? allFiles.slice(0, UNCOMMITTED_FILES_CAP) : allFiles;
53
+ return { branch, uncommittedFiles, truncated };
54
+ }
55
+ // ─── Strategy submodule pointer ────────────────────────────────────────────
56
+ export function extractStrategyPointer(cwd) {
57
+ const strategyDir = path.join(cwd, '.strategy');
58
+ if (!fs.existsSync(strategyDir)) {
59
+ return { sha: null, latestJournal: null };
60
+ }
61
+ let sha = null;
62
+ try {
63
+ const full = safeExec('git', ['rev-parse', 'HEAD'], { cwd: strategyDir });
64
+ sha = full.length >= 7 ? full.slice(0, 7) : null;
65
+ // totem-context: ADR-090 substrate graceful degradation — null sha on uninitialized submodule.
66
+ }
67
+ catch {
68
+ sha = null;
69
+ }
70
+ let latestJournal = null;
71
+ try {
72
+ const journalDir = path.join(strategyDir, '.journal');
73
+ if (fs.existsSync(journalDir)) {
74
+ const entries = fs
75
+ .readdirSync(journalDir)
76
+ .filter((f) => f.endsWith('.md'))
77
+ .sort();
78
+ latestJournal = entries.length > 0 ? entries[entries.length - 1] : null;
79
+ }
80
+ // totem-context: ADR-090 substrate graceful degradation — null when .journal/ unreachable.
81
+ }
82
+ catch {
83
+ latestJournal = null;
84
+ }
85
+ return { sha, latestJournal };
86
+ }
87
+ // ─── Package versions (fixed group only) ───────────────────────────────────
88
+ export function extractPackageVersions(cwd) {
89
+ const result = {};
90
+ const packagesDir = path.join(cwd, 'packages');
91
+ if (!fs.existsSync(packagesDir))
92
+ return result;
93
+ let subdirs;
94
+ try {
95
+ subdirs = fs.readdirSync(packagesDir);
96
+ // totem-context: ADR-090 substrate graceful degradation — empty map on unreadable packages/.
97
+ }
98
+ catch {
99
+ return result;
100
+ }
101
+ for (const subdir of subdirs) {
102
+ const pkgJson = path.join(packagesDir, subdir, 'package.json');
103
+ try {
104
+ const parsed = readJsonSafe(pkgJson);
105
+ // readJsonSafe types the shape but does not enforce runtime invariants.
106
+ // A hand-edited package.json with a numeric version or non-string name
107
+ // would otherwise leak a malformed value into the response (CR catch on
108
+ // #1506).
109
+ if (typeof parsed.name === 'string' &&
110
+ typeof parsed.version === 'string' &&
111
+ FIXED_GROUP_PACKAGES.includes(parsed.name)) {
112
+ result[parsed.name] = parsed.version;
113
+ }
114
+ // totem-context: ADR-090 substrate graceful degradation — per-package skip on parse failure.
115
+ }
116
+ catch {
117
+ // Missing / unparseable package.json is a non-fatal skip.
118
+ }
119
+ }
120
+ return result;
121
+ }
122
+ // ─── Rule counts from .totem/compiled-rules.json ───────────────────────────
123
+ export function extractRuleCounts(cwd, totemDir) {
124
+ const rulesPath = path.join(cwd, totemDir, 'compiled-rules.json');
125
+ if (!fs.existsSync(rulesPath)) {
126
+ return { active: 0, archived: 0, nonCompilable: 0 };
127
+ }
128
+ try {
129
+ const parsed = readJsonSafe(rulesPath, CompiledRulesFileSchema);
130
+ let active = 0;
131
+ let archived = 0;
132
+ for (const rule of parsed.rules) {
133
+ if (rule.status === 'archived')
134
+ archived += 1;
135
+ else
136
+ active += 1;
137
+ }
138
+ return {
139
+ active,
140
+ archived,
141
+ nonCompilable: parsed.nonCompilable?.length ?? 0,
142
+ };
143
+ // totem-context: ADR-090 substrate graceful degradation — zero counts on malformed manifest.
144
+ }
145
+ catch {
146
+ return { active: 0, archived: 0, nonCompilable: 0 };
147
+ }
148
+ }
149
+ // ─── Lesson count ──────────────────────────────────────────────────────────
150
+ export function extractLessonCount(cwd, totemDir) {
151
+ const lessonsDir = path.join(cwd, totemDir, 'lessons');
152
+ if (!fs.existsSync(lessonsDir))
153
+ return 0;
154
+ try {
155
+ return fs.readdirSync(lessonsDir).filter((f) => f.endsWith('.md')).length;
156
+ // totem-context: ADR-090 substrate graceful degradation — zero count on unreadable lessons/.
157
+ }
158
+ catch {
159
+ return 0;
160
+ }
161
+ }
162
+ // ─── Milestone + gate tickets from docs/active_work.md ─────────────────────
163
+ /**
164
+ * Regex-parse the milestone name and gate-ticket list from active_work.md.
165
+ * This is explicitly best-effort — the markdown format can drift.
166
+ * `bestEffort: true` signals to agents that the values are a hint, not a
167
+ * ground-truth source.
168
+ */
169
+ export function extractMilestoneState(cwd) {
170
+ // totem-context: ADR-090 + #1497 canonical briefing-source path, not a config omission.
171
+ const activeWorkPath = path.join(cwd, 'docs', 'active_work.md');
172
+ if (!fs.existsSync(activeWorkPath)) {
173
+ return { name: null, gateTickets: [], bestEffort: true };
174
+ }
175
+ let content;
176
+ try {
177
+ content = fs.readFileSync(activeWorkPath, 'utf-8');
178
+ // totem-context: ADR-090 substrate graceful degradation + intentional unstaged-disk read.
179
+ }
180
+ catch {
181
+ return { name: null, gateTickets: [], bestEffort: true };
182
+ }
183
+ // Milestone: first "### Current: X.Y.Z" heading (the doc has exactly one).
184
+ let name = null;
185
+ // totem-context: single-match by design — one Current heading; matchAll would obscure intent.
186
+ const currentMatch = content.match(/^###\s+Current:\s*(\d+\.\d+\.\d+)/m);
187
+ if (currentMatch?.[1] !== undefined)
188
+ name = currentMatch[1];
189
+ // Gate tickets: unique `#NNNN` refs inside code spans across the doc body.
190
+ // The lint-rule registry carries lesson hashes with similar `#` prefixes, so
191
+ // require 3-5 digits and cap at 200 entries to keep the payload tight.
192
+ const tickets = new Set();
193
+ const ticketRe = /#(\d{3,5})\b/g;
194
+ for (const match of content.matchAll(ticketRe)) {
195
+ tickets.add(`#${match[1]}`);
196
+ if (tickets.size >= 200)
197
+ break;
198
+ }
199
+ return {
200
+ name,
201
+ gateTickets: Array.from(tickets),
202
+ bestEffort: true,
203
+ };
204
+ }
205
+ // ─── Test count (v1: always null) ──────────────────────────────────────────
206
+ /**
207
+ * Stored test-count artifact does not exist in v1. Follow-up ticket wires
208
+ * postmerge to stamp `.totem/store/test-stats.json` after `pnpm test` runs;
209
+ * until then the endpoint reports null honestly rather than fabricate a
210
+ * number.
211
+ */
212
+ export function extractTestCount(_cwd) {
213
+ return null;
214
+ }
215
+ // ─── Recent merged PRs from git log ────────────────────────────────────────
216
+ /**
217
+ * Capture squash-merge commits whose subject references a PR number
218
+ * (`... (#NNNN)`). Skips commits whose message lacks a PR tag so we do not
219
+ * include non-PR merges like the Version Packages auto-commit in some flows.
220
+ */
221
+ export function extractRecentPrs(cwd, limit = RECENT_PRS_COUNT) {
222
+ if (resolveGitRoot(cwd) === null)
223
+ return [];
224
+ // Unit separator (0x1f) is ASCII-reserved for field delimiting and never
225
+ // appears in commit subjects, unlike `|` which GCA flagged on #1506 as a
226
+ // correctness risk since commit messages routinely contain pipes.
227
+ const FIELD_SEP = '\x1f';
228
+ let raw;
229
+ try {
230
+ raw = safeExec('git', [
231
+ 'log',
232
+ '-n',
233
+ String(limit * 3),
234
+ '--grep=#[0-9]\\+',
235
+ `--format=%s${FIELD_SEP}%cI${FIELD_SEP}%h`,
236
+ ], { cwd });
237
+ // totem-context: ADR-090 substrate graceful degradation — empty list on git-log failure.
238
+ }
239
+ catch {
240
+ return [];
241
+ }
242
+ if (raw.length === 0)
243
+ return [];
244
+ const results = [];
245
+ for (const line of raw.split(/\r?\n/)) {
246
+ const [title, date, squashSha] = line.split(FIELD_SEP);
247
+ if (title === undefined ||
248
+ date === undefined ||
249
+ squashSha === undefined ||
250
+ !/#\d+/.test(title)) {
251
+ continue;
252
+ }
253
+ results.push({ title, date, squashSha });
254
+ if (results.length >= limit)
255
+ break;
256
+ }
257
+ return results;
258
+ }
259
+ //# sourceMappingURL=state-extractors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-extractors.js","sourceRoot":"","sources":["../src/state-extractors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=state-extractors.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-extractors.test.d.ts","sourceRoot":"","sources":["../src/state-extractors.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,193 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ // packages/mcp/src -> packages/mcp -> packages -> repo root
6
+ const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
7
+ import { UNCOMMITTED_FILES_CAP } from './schemas/describe-project.js';
8
+ import { extractGitState, extractLessonCount, extractMilestoneState, extractPackageVersions, extractRecentPrs, extractRuleCounts, extractStrategyPointer, extractTestCount, } from './state-extractors.js';
9
+ describe('extractGitState', () => {
10
+ it('returns null/empty for a non-git directory', () => {
11
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-mcp-nogit-'));
12
+ try {
13
+ const state = extractGitState(tmp);
14
+ expect(state.branch).toBeNull();
15
+ expect(state.uncommittedFiles).toEqual([]);
16
+ expect(state.truncated).toBe(false);
17
+ }
18
+ finally {
19
+ fs.rmSync(tmp, { recursive: true, force: true });
20
+ }
21
+ });
22
+ it('returns branch (or null on detached HEAD) and staged/unstaged files on the live repo', () => {
23
+ const state = extractGitState(REPO_ROOT);
24
+ // GitHub Actions checks out the merge commit in detached HEAD, so branch
25
+ // legitimately resolves to null there. Locally it is a string. Either is
26
+ // valid; the important invariant is that the call does not throw.
27
+ if (state.branch !== null) {
28
+ expect(state.branch).toBeTypeOf('string');
29
+ expect(state.branch.length).toBeGreaterThan(0);
30
+ }
31
+ expect(state.uncommittedFiles.length).toBeLessThanOrEqual(UNCOMMITTED_FILES_CAP);
32
+ // If a truncation happened the cap must be hit exactly.
33
+ if (state.truncated) {
34
+ expect(state.uncommittedFiles.length).toBe(UNCOMMITTED_FILES_CAP);
35
+ }
36
+ });
37
+ });
38
+ describe('extractStrategyPointer', () => {
39
+ it('returns null/null when .strategy/ is absent', () => {
40
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-mcp-nostrat-'));
41
+ try {
42
+ const ptr = extractStrategyPointer(tmp);
43
+ expect(ptr.sha).toBeNull();
44
+ expect(ptr.latestJournal).toBeNull();
45
+ }
46
+ finally {
47
+ fs.rmSync(tmp, { recursive: true, force: true });
48
+ }
49
+ });
50
+ it('returns a 7-char SHA and a journal filename on the live repo', () => {
51
+ const ptr = extractStrategyPointer(REPO_ROOT);
52
+ if (ptr.sha !== null) {
53
+ expect(ptr.sha).toMatch(/^[0-9a-f]{7}$/);
54
+ }
55
+ if (ptr.latestJournal !== null) {
56
+ expect(ptr.latestJournal).toMatch(/\.md$/);
57
+ }
58
+ });
59
+ });
60
+ describe('extractPackageVersions', () => {
61
+ it('returns {} when packages/ is missing', () => {
62
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-mcp-nopkg-'));
63
+ try {
64
+ expect(extractPackageVersions(tmp)).toEqual({});
65
+ }
66
+ finally {
67
+ fs.rmSync(tmp, { recursive: true, force: true });
68
+ }
69
+ });
70
+ it('captures fixed-group versions on the live repo', () => {
71
+ const versions = extractPackageVersions(REPO_ROOT);
72
+ // Whichever of the fixed-group packages exist must carry a version string.
73
+ for (const pkgName of Object.keys(versions)) {
74
+ expect(versions[pkgName]).toMatch(/^\d+\.\d+\.\d+/);
75
+ }
76
+ // At least one of the headline packages must be present on the self-host.
77
+ expect(versions['@mmnto/cli'] !== undefined || versions['@mmnto/totem'] !== undefined).toBe(true);
78
+ });
79
+ });
80
+ describe('extractRuleCounts', () => {
81
+ it('returns zeros when compiled-rules.json is missing', () => {
82
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-mcp-norules-'));
83
+ try {
84
+ const counts = extractRuleCounts(tmp, '.totem');
85
+ expect(counts).toEqual({ active: 0, archived: 0, nonCompilable: 0 });
86
+ }
87
+ finally {
88
+ fs.rmSync(tmp, { recursive: true, force: true });
89
+ }
90
+ });
91
+ it('returns zeros when compiled-rules.json is malformed', () => {
92
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-mcp-badrules-'));
93
+ try {
94
+ fs.mkdirSync(path.join(tmp, '.totem'));
95
+ fs.writeFileSync(path.join(tmp, '.totem', 'compiled-rules.json'), '{ broken json');
96
+ const counts = extractRuleCounts(tmp, '.totem');
97
+ expect(counts).toEqual({ active: 0, archived: 0, nonCompilable: 0 });
98
+ }
99
+ finally {
100
+ fs.rmSync(tmp, { recursive: true, force: true });
101
+ }
102
+ });
103
+ it('splits active from archived on the live repo', () => {
104
+ const counts = extractRuleCounts(REPO_ROOT, '.totem');
105
+ expect(counts.active).toBeGreaterThan(0);
106
+ expect(counts.archived).toBeGreaterThanOrEqual(0);
107
+ expect(counts.nonCompilable).toBeGreaterThanOrEqual(0);
108
+ });
109
+ });
110
+ describe('extractLessonCount', () => {
111
+ it('returns 0 when lessons/ is missing', () => {
112
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-mcp-nolessons-'));
113
+ try {
114
+ expect(extractLessonCount(tmp, '.totem')).toBe(0);
115
+ }
116
+ finally {
117
+ fs.rmSync(tmp, { recursive: true, force: true });
118
+ }
119
+ });
120
+ it('returns the live lesson count', () => {
121
+ expect(extractLessonCount(REPO_ROOT, '.totem')).toBeGreaterThan(0);
122
+ });
123
+ });
124
+ describe('extractMilestoneState', () => {
125
+ it('returns null/empty with bestEffort=true when active_work.md is missing', () => {
126
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-mcp-noactive-'));
127
+ try {
128
+ const state = extractMilestoneState(tmp);
129
+ expect(state).toEqual({ name: null, gateTickets: [], bestEffort: true });
130
+ }
131
+ finally {
132
+ fs.rmSync(tmp, { recursive: true, force: true });
133
+ }
134
+ });
135
+ it('parses milestone and tickets on the live repo', () => {
136
+ const state = extractMilestoneState(REPO_ROOT);
137
+ expect(state.bestEffort).toBe(true);
138
+ // Milestone value depends on the current doc; just enforce the shape contract.
139
+ if (state.name !== null) {
140
+ expect(state.name).toMatch(/^\d+\.\d+\.\d+$/);
141
+ }
142
+ // Ticket list should never include legacy 1-2 digit fragments and should
143
+ // not explode past the 200-entry safety cap.
144
+ expect(state.gateTickets.length).toBeLessThanOrEqual(200);
145
+ for (const ticket of state.gateTickets) {
146
+ expect(ticket).toMatch(/^#\d{3,5}$/);
147
+ }
148
+ });
149
+ });
150
+ describe('extractTestCount', () => {
151
+ it('always returns null in v1', () => {
152
+ expect(extractTestCount(REPO_ROOT)).toBeNull();
153
+ });
154
+ });
155
+ describe('extractRecentPrs', () => {
156
+ it('returns [] for a non-git directory', () => {
157
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-mcp-norecent-'));
158
+ try {
159
+ expect(extractRecentPrs(tmp)).toEqual([]);
160
+ }
161
+ finally {
162
+ fs.rmSync(tmp, { recursive: true, force: true });
163
+ }
164
+ });
165
+ it('returns up to the requested limit, newest first on the live repo', () => {
166
+ const prs = extractRecentPrs(REPO_ROOT, 5);
167
+ expect(prs.length).toBeLessThanOrEqual(5);
168
+ for (const pr of prs) {
169
+ expect(pr.title).toMatch(/#\d+/);
170
+ expect(pr.squashSha).toMatch(/^[0-9a-f]{7,12}$/);
171
+ expect(() => new Date(pr.date).toISOString()).not.toThrow();
172
+ }
173
+ if (prs.length >= 2) {
174
+ const t0 = new Date(prs[0].date).getTime();
175
+ const t1 = new Date(prs[1].date).getTime();
176
+ expect(t0).toBeGreaterThanOrEqual(t1);
177
+ }
178
+ });
179
+ });
180
+ describe('temp dir cleanup safety', () => {
181
+ // Smoke test: verify rmSync pattern from earlier tests does not leak.
182
+ let tmp;
183
+ beforeEach(() => {
184
+ tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-mcp-cleanup-'));
185
+ });
186
+ afterEach(() => {
187
+ fs.rmSync(tmp, { recursive: true, force: true });
188
+ });
189
+ it('temp dir exists inside the test', () => {
190
+ expect(fs.existsSync(tmp)).toBe(true);
191
+ });
192
+ });
193
+ //# sourceMappingURL=state-extractors.test.js.map