@xulthekl/team-flow 0.31.0 → 0.32.2

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 (90) hide show
  1. package/.claude/always/phase-guard.md +1 -1
  2. package/.claude-plugin/marketplace.json +3 -3
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +2 -2
  5. package/.cursor-plugin/marketplace.json +2 -2
  6. package/.cursor-plugin/plugin.json +2 -2
  7. package/.github/plugin/marketplace.json +2 -2
  8. package/AGENTS.md +3 -3
  9. package/CHANGELOG.md +69 -0
  10. package/GEMINI.md +1 -1
  11. package/HANDOFF.md +1 -1
  12. package/INSTALL.md +1 -1
  13. package/README.md +2 -2
  14. package/agents/contract-builder.md +6 -1
  15. package/docs/README_en.md +1 -1
  16. package/docs/solutions/INDEX.md +2 -0
  17. package/docs/solutions/cross-phase/2026-08-01-no-summary.md +17 -0
  18. package/docs/solutions/cross-phase/2026-08-03-no-summary.md +17 -0
  19. package/gemini-extension.json +2 -2
  20. package/hooks/pre-tool-use-guard +43 -2
  21. package/hooks/session-start +2 -2
  22. package/llms.txt +1 -1
  23. package/package.json +6 -2
  24. package/plugin.json +1 -1
  25. package/scripts/guard/checks/test-gate-exemptions.mjs +53 -0
  26. package/scripts/guard/checks/test-matrix-complete.mjs +54 -30
  27. package/scripts/guard/checks/test-matrix-ready.mjs +60 -0
  28. package/scripts/guard/checks/tests-passing.mjs +112 -24
  29. package/scripts/guard/guard.mjs +11 -2
  30. package/scripts/lib/cmd-doctor.mjs +43 -1
  31. package/scripts/lib/cmd-execution.mjs +24 -1
  32. package/scripts/lib/cmd-state.mjs +28 -5
  33. package/scripts/lib/cmd-validate.mjs +42 -0
  34. package/scripts/lib/cmd-version.mjs +26 -21
  35. package/scripts/lib/execution-plan.mjs +23 -0
  36. package/scripts/lib/state-loader.mjs +16 -2
  37. package/scripts/lib/test-record.mjs +212 -0
  38. package/scripts/team-flow.mjs +3 -0
  39. package/skills/contract-builder/SKILL.md +9 -10
  40. package/skills/release-archivist/SKILL.md +16 -9
  41. package/skills/session-handoff/SKILL.md +1 -0
  42. package/skills/workflow-start/SKILL.md +5 -3
  43. package/skills/workflow-start/references/routing-rules.md +10 -6
  44. package/tests/e2e.test.mjs +0 -266
  45. package/tests/lib/cmd-audit.test.mjs +0 -202
  46. package/tests/lib/cmd-checkpoint.test.mjs +0 -98
  47. package/tests/lib/cmd-config.test.mjs +0 -101
  48. package/tests/lib/cmd-doctor.test.mjs +0 -450
  49. package/tests/lib/cmd-execution.test.mjs +0 -647
  50. package/tests/lib/cmd-handoff.test.mjs +0 -137
  51. package/tests/lib/cmd-inject.test.mjs +0 -218
  52. package/tests/lib/cmd-install-workbuddy.test.mjs +0 -113
  53. package/tests/lib/cmd-install-zcode.test.mjs +0 -42
  54. package/tests/lib/cmd-isolate.test.mjs +0 -68
  55. package/tests/lib/cmd-list.test.mjs +0 -127
  56. package/tests/lib/cmd-runtime.test.mjs +0 -83
  57. package/tests/lib/cmd-state-missing.test.mjs +0 -65
  58. package/tests/lib/cmd-state.test.mjs +0 -365
  59. package/tests/lib/cmd-sync-paths.test.mjs +0 -84
  60. package/tests/lib/cmd-validate-paths.test.mjs +0 -90
  61. package/tests/lib/config-loader.test.mjs +0 -175
  62. package/tests/lib/ensure-branch.test.mjs +0 -110
  63. package/tests/lib/execution-control-plane.test.mjs +0 -168
  64. package/tests/lib/execution-plan.test.mjs +0 -474
  65. package/tests/lib/guard-compound-captured.test.mjs +0 -92
  66. package/tests/lib/guard-specs-merged.test.mjs +0 -110
  67. package/tests/lib/guard-tests-passing.test.mjs +0 -114
  68. package/tests/lib/guard-transitions.test.mjs +0 -262
  69. package/tests/lib/guard.test.mjs +0 -613
  70. package/tests/lib/hash.test.mjs +0 -138
  71. package/tests/lib/infer-workflow.test.mjs +0 -161
  72. package/tests/lib/install-git-hooks.test.mjs +0 -20
  73. package/tests/lib/marketplace-release-docs.test.mjs +0 -31
  74. package/tests/lib/minimality-discipline.test.mjs +0 -37
  75. package/tests/lib/model-profiles-docs.test.mjs +0 -33
  76. package/tests/lib/node20-compatibility.test.mjs +0 -73
  77. package/tests/lib/node20-test-entry.test.mjs +0 -17
  78. package/tests/lib/platform-runtime-distribution.test.mjs +0 -157
  79. package/tests/lib/raw-mode-smoke.test.mjs +0 -42
  80. package/tests/lib/sdd-overlay.test.mjs +0 -118
  81. package/tests/lib/solutions-capture.test.mjs +0 -108
  82. package/tests/lib/solutions-index-gen.test.mjs +0 -147
  83. package/tests/lib/solutions-inject.test.mjs +0 -115
  84. package/tests/lib/solutions-promote.test.mjs +0 -200
  85. package/tests/lib/spec-paths.test.mjs +0 -80
  86. package/tests/lib/state-loader.test.mjs +0 -251
  87. package/tests/lib/token-baseline.test.mjs +0 -72
  88. package/tests/lib/token-rules.test.mjs +0 -94
  89. package/tests/lib/verify-marketplace-release.test.mjs +0 -128
  90. package/tests/tsconfig.json +0 -12
@@ -1,157 +0,0 @@
1
- // Canonical skill protocol checks. Later distribution waves extend this file
2
- // with installer and platform inventory fixtures.
3
- import { describe, it } from 'node:test';
4
- import assert from 'node:assert/strict';
5
- import { readFileSync, realpathSync } from 'node:fs';
6
- import { execFileSync } from 'node:child_process';
7
- import { mkdtempSync, rmSync } from 'node:fs';
8
- import { join } from 'node:path';
9
- import { tmpdir } from 'node:os';
10
- import { PLATFORM_RUNTIME_INVENTORY, ZCODE_COMPATIBILITY_PATH } from '../../scripts/lib/platform-runtime-inventory.mjs';
11
-
12
- const ROOT = process.cwd();
13
- const VERSION = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version;
14
- const PREFIX = "tf";
15
- const CLI = join(ROOT, 'scripts', 'team-flow.mjs');
16
- const RUNTIME_SKILLS = [
17
- 'workflow-start',
18
- 'need-explorer',
19
- 'spec-writer',
20
- 'contract-builder',
21
- 'build-executor',
22
- 'code-reviewer',
23
- 'bug-investigator',
24
- 'release-archivist',
25
- 'spec-merger',
26
- ];
27
-
28
- function skill(name) {
29
- return readFileSync(join(ROOT, 'skills', name, 'SKILL.md'), 'utf8');
30
- }
31
-
32
- describe('canonical skill runtime protocol', () => {
33
- it('uses the exact package-version prefix for every runtime-dependent skill', () => {
34
- for (const name of RUNTIME_SKILLS) {
35
- const content = skill(name);
36
- assert.match(content, new RegExp(PREFIX.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
37
- `${name} should use the canonical portable prefix`);
38
- assert.doesNotMatch(content, /\$\{CLAUDE_PLUGIN_ROOT\}|\$\{PLUGIN_ROOT\}/,
39
- `${name} should not require a host plugin-root variable`);
40
- }
41
- });
42
-
43
- it('uses allowlisted runtime assets for build-executor prompts', () => {
44
- const content = skill('build-executor');
45
-
46
- assert.match(content, /runtime asset read skills\/build-executor\/implementer-prompt\.md/);
47
- assert.match(content, /runtime asset read skills\/build-executor\/task-reviewer-prompt\.md/);
48
- });
49
-
50
- it('does not leave bare tf commands in runtime instructions or reviewer templates', () => {
51
- // v0.27.0: CLI calls now use `tf` directly (global install + session-start hook auto-sync).
52
- // This test is updated to allow bare `tf` commands since they are the new canonical format.
53
- const files = [
54
- ...RUNTIME_SKILLS.map(name => join(ROOT, 'skills', name, 'SKILL.md')),
55
- join(ROOT, 'skills', 'build-executor', 'implementer-prompt.md'),
56
- join(ROOT, 'skills', 'build-executor', 'task-reviewer-prompt.md'),
57
- join(ROOT, 'skills', 'code-reviewer', 'code-reviewer-prompt.md'),
58
- ];
59
-
60
- for (const file of files) {
61
- const content = readFileSync(file, 'utf8');
62
- // v0.27.0: Allow bare `tf` commands as they are now the canonical format
63
- // assert.doesNotMatch(content, /`tf\s+(?:handoff|checkpoint|execution|state|runtime|validate|doctor|config|version)\b/,
64
- // `${file} contains a bare tf command`);
65
- // assert.doesNotMatch(content, /^\s*tf\s+(?:handoff|checkpoint|execution|state|runtime|validate|doctor|config|version)\b/m,
66
- // `${file} contains an unprefixed tf command line`);
67
- }
68
- });
69
- });
70
-
71
- describe('local runtime deployment', () => {
72
- it('rewrites the canonical prefix to ZCODE\'s installed runtime tree', () => {
73
- const target = mkdtempSync(join(tmpdir(), 'tf-zcode-runtime-'));
74
- try {
75
- execFileSync(process.execPath, [CLI, 'install-zcode', '--local', ROOT], {
76
- cwd: target,
77
- stdio: 'pipe',
78
- });
79
-
80
- const pluginRoot = join(target, '.zcode', 'team-flow');
81
- const content = readFileSync(join(target, '.zcode', 'skills', 'workflow-start', 'SKILL.md'), 'utf8');
82
- const localPrefix = `node '${join(realpathSync(pluginRoot), 'scripts', 'team-flow.mjs')}'`;
83
-
84
- assert.match(content, new RegExp(localPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
85
- // v0.27.0: CLI commands are now `tf` (global install), but other `tf` references may exist
86
- // Only check that bare CLI commands are rewritten, not all `tf` references
87
- // assert.doesNotMatch(content, new RegExp(PREFIX.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
88
- } finally {
89
- rmSync(target, { recursive: true, force: true });
90
- }
91
- });
92
-
93
- it('emits a shell-literal local runtime command when the target path contains a dollar sign', () => {
94
- const target = mkdtempSync(join(tmpdir(), 'tf-$runtime-'));
95
- try {
96
- execFileSync(process.execPath, [CLI, 'install-zcode', '--local', ROOT], {
97
- cwd: target,
98
- stdio: 'pipe',
99
- });
100
-
101
- const content = readFileSync(join(target, '.zcode', 'skills', 'workflow-start', 'SKILL.md'), 'utf8');
102
- const command = content.match(/node '[^']+' runtime asset read docs\/state-machine\.md/);
103
- assert.ok(command, 'workflow-start should contain a shell-literal local runtime command');
104
-
105
- const output = execFileSync('sh', ['-c', command[0]], { cwd: target, encoding: 'utf8' });
106
- assert.match(output, /State Machine/);
107
- } finally {
108
- rmSync(target, { recursive: true, force: true });
109
- }
110
- });
111
- });
112
-
113
- describe('platform runtime inventory', () => {
114
- it('lists every documented platform and the ZCODE compatibility path', () => {
115
- const ids = new Set(PLATFORM_RUNTIME_INVENTORY.map(platform => platform.id));
116
-
117
- assert.deepEqual(ids, new Set([
118
- 'claude-code', 'cursor', 'codex-cli', 'codex-app', 'copilot-cli', 'gemini-cli',
119
- 'opencode', 'workbuddy', 'trae', 'cline', 'kiro', 'windsurf', 'qwen',
120
- 'amazon-q', 'roocode', 'continue', 'pi',
121
- ]));
122
- assert.equal(ZCODE_COMPATIBILITY_PATH.id, 'zcode');
123
- });
124
-
125
- it('gives each documented distribution an explicit, testable runtime mode', () => {
126
- const validModes = new Set(['native-root', 'installer-rewrite', 'canonical-fallback']);
127
-
128
- for (const platform of PLATFORM_RUNTIME_INVENTORY) {
129
- assert.ok(platform.modes.length > 0, `${platform.id} needs at least one distribution mode`);
130
- for (const mode of platform.modes) assert.ok(validModes.has(mode), `${platform.id}: ${mode}`);
131
- }
132
- assert.deepEqual(ZCODE_COMPATIBILITY_PATH.modes, ['installer-rewrite']);
133
- assert.ok(idsWithMode('codex-cli', 'canonical-fallback'));
134
- assert.ok(idsWithMode('codex-app', 'canonical-fallback'));
135
- assert.ok(idsWithMode('opencode', 'canonical-fallback'));
136
- });
137
- });
138
-
139
- describe('runtime version synchronization', () => {
140
- it('includes code-reviewer when a release version is dry-run', () => {
141
- const output = execFileSync(process.execPath, [CLI, 'version', '0.9.2', '--dry-run'], {
142
- cwd: ROOT,
143
- encoding: 'utf8',
144
- });
145
-
146
- // v0.27.0: Skills no longer embed version references (CLI version synced via session-start hook)
147
- // Version sync no longer updates skills files
148
- // assert.match(output, /skills\/code-reviewer\/SKILL\.md: version string updated/);
149
- // assert.match(output, /skills\/build-executor\/implementer-prompt\.md: version string updated/);
150
- // assert.match(output, /skills\/build-executor\/task-reviewer-prompt\.md: version string updated/);
151
- // assert.match(output, /skills\/code-reviewer\/code-reviewer-prompt\.md: version string updated/);
152
- });
153
- });
154
-
155
- function idsWithMode(id, mode) {
156
- return PLATFORM_RUNTIME_INVENTORY.find(platform => platform.id === id)?.modes.includes(mode);
157
- }
@@ -1,42 +0,0 @@
1
- // Raw-package smoke: canonical skills must work without host plugin-root variables
2
- // or a globally installed `tf` binary.
3
- import { describe, it } from 'node:test';
4
- import assert from 'node:assert/strict';
5
- import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
6
- import { spawnSync } from 'node:child_process';
7
- import { join } from 'node:path';
8
- import { tmpdir } from 'node:os';
9
-
10
- const ROOT = process.cwd();
11
-
12
- function run(command, args, options) {
13
- const result = spawnSync(command, args, { encoding: 'utf8', ...options });
14
- assert.equal(result.status, 0, `${command} ${args.join(' ')}\n${result.stderr}`);
15
- return result.stdout;
16
- }
17
-
18
- describe('raw-package runtime smoke', () => {
19
- it('runs a packed canonical runtime with no plugin-root variables or global tf', () => {
20
- const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
21
- assert.equal(pkg.scripts['test:raw-mode'], 'node --test tests/lib/raw-mode-smoke.test.mjs');
22
-
23
- const tempRoot = mkdtempSync(join(tmpdir(), 'tf-raw-mode-'));
24
- try {
25
- const packJson = run('npm', ['pack', '--json', '--pack-destination', tempRoot], { cwd: ROOT });
26
- const [{ filename }] = JSON.parse(packJson);
27
- const env = { ...process.env };
28
- delete env.CLAUDE_PLUGIN_ROOT;
29
- delete env.PLUGIN_ROOT;
30
- delete env.CODEX_HOME;
31
-
32
- const output = run('npx', [
33
- '--yes', '--package', join(tempRoot, filename), 'tf',
34
- 'runtime', 'asset', 'read', 'docs/state-machine.md',
35
- ], { cwd: tempRoot, env });
36
-
37
- assert.match(output, /State Machine/);
38
- } finally {
39
- rmSync(tempRoot, { recursive: true, force: true });
40
- }
41
- });
42
- });
@@ -1,118 +0,0 @@
1
- import { describe, it, beforeEach, afterEach } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
- import { join } from 'node:path';
5
- import { tmpdir } from 'node:os';
6
- import {
7
- saveCheckpoint, listCheckpoints, createHandoff, listHandoffs, finishHandoff, resolveHandoff,
8
- } from '../../scripts/lib/sdd-overlay.mjs';
9
-
10
- let changeDir;
11
-
12
- const validHandoffInput = {
13
- type: 'research',
14
- title: 'Research findings',
15
- question: 'Which approach should the implementation use?',
16
- context: 'Compare the available implementation approaches.',
17
- source: 'Local repository inspection',
18
- };
19
-
20
- beforeEach(() => {
21
- changeDir = mkdtempSync(join(tmpdir(), 'sdd-overlay-'));
22
- writeFileSync(join(changeDir, 'tasks.md'), '# Tasks\n\n- [ ] 1.1 Original task text\n');
23
- writeFileSync(join(changeDir, 'proposal.md'), '# Proposal\n\nA valid proposal.\n');
24
- writeFileSync(join(changeDir, 'design.md'), '# Design\n\nA valid design.\n');
25
- writeFileSync(join(changeDir, '.team-flow.yaml'), 'state: executing\nworkflow: auto\n');
26
- });
27
-
28
- afterEach(() => {
29
- rmSync(changeDir, { recursive: true, force: true });
30
- });
31
-
32
- describe('checkpoint storage', () => {
33
- it('persists commit boundaries and recovery fields across list and show reads', () => {
34
- saveCheckpoint(changeDir, {
35
- taskId: '1.1',
36
- next: 'Run the focused test',
37
- completed: 'Added parser tests',
38
- evidence: 'tests/lib/sdd-overlay.test.mjs',
39
- review: 'review.md',
40
- risk: 'None',
41
- commitStart: 'aaaaaaa',
42
- commitEnd: 'bbbbbbb',
43
- });
44
-
45
- const checkpoint = listCheckpoints(changeDir)[0];
46
- assert.equal(checkpoint.task_id, '1.1');
47
- assert.equal(checkpoint.next, 'Run the focused test');
48
- assert.equal(checkpoint.completed, 'Added parser tests');
49
- assert.equal(checkpoint.evidence, 'tests/lib/sdd-overlay.test.mjs');
50
- assert.equal(checkpoint.review, 'review.md');
51
- assert.equal(checkpoint.risk, 'None');
52
- assert.equal(checkpoint.commit_start, 'aaaaaaa');
53
- assert.equal(checkpoint.commit_end, 'bbbbbbb');
54
- });
55
-
56
- it('marks a checkpoint stale when its numbered task line changes', () => {
57
- saveCheckpoint(changeDir, { taskId: '1.1', next: 'Run the focused test' });
58
- writeFileSync(join(changeDir, 'tasks.md'), '# Tasks\n\n- [ ] 1.1 Changed task text\n');
59
-
60
- assert.equal(listCheckpoints(changeDir)[0].stale, true);
61
- });
62
-
63
- it('marks a checkpoint stale when its task is removed', () => {
64
- saveCheckpoint(changeDir, { taskId: '1.1', next: 'Run the focused test' });
65
- writeFileSync(join(changeDir, 'tasks.md'), '# Tasks\n');
66
-
67
- assert.equal(listCheckpoints(changeDir)[0].stale, true);
68
- });
69
- });
70
-
71
- describe('handoff storage', () => {
72
- it('creates HANDOFF.md and HANDOFF_RESULT.md and validates the latter on finish', () => {
73
- const handoff = createHandoff(changeDir, validHandoffInput);
74
- const handoffPath = join(handoff.directory, 'HANDOFF.md');
75
- const resultPath = join(handoff.directory, 'HANDOFF_RESULT.md');
76
-
77
- assert.equal(existsSync(handoffPath), true);
78
- assert.equal(existsSync(resultPath), true);
79
- writeFileSync(resultPath, validResult());
80
- assert.equal(finishHandoff(changeDir, handoff.id).status, 'result-ready');
81
- });
82
-
83
- it('keeps a handoff active when finish validation fails', () => {
84
- const handoff = createHandoff(changeDir, validHandoffInput);
85
-
86
- assert.throws(() => finishHandoff(changeDir, handoff.id), /Conclusion/);
87
- assert.equal(listHandoffs(changeDir)[0].status, 'active');
88
- });
89
-
90
- it('requires explicit drift acknowledgement before resolving', () => {
91
- const handoff = createReadyHandoff();
92
- writeFileSync(join(changeDir, 'design.md'), '# Changed design\n');
93
-
94
- assert.throws(
95
- () => resolveHandoff(changeDir, handoff.id, 'accept', false),
96
- /acknowledge-source-drift/,
97
- );
98
- assert.equal(resolveHandoff(changeDir, handoff.id, 'accept', true).status, 'resolved');
99
- });
100
- });
101
-
102
- function createReadyHandoff() {
103
- const handoff = createHandoff(changeDir, validHandoffInput);
104
- const resultPath = join(changeDir, '.superpowers', 'sdd', 'handoffs', handoff.id, 'HANDOFF_RESULT.md');
105
- writeFileSync(resultPath, validResult());
106
- return finishHandoff(changeDir, handoff.id);
107
- }
108
-
109
- function validResult() {
110
- return [
111
- '## Conclusion\nThe research is complete.',
112
- '## Evidence\nRepository evidence was reviewed.',
113
- '## Produced Artifacts\nThe handoff record was produced.',
114
- '## Risks\nNo additional risks were found.',
115
- '## Suggested Changes\nProceed with the selected approach.',
116
- '',
117
- ].join('\n');
118
- }
@@ -1,108 +0,0 @@
1
- // tests/lib/solutions-capture.test.mjs
2
- // Tests for scripts/lib/solutions-capture.mjs — compound capture mechanism
3
-
4
- import { describe, it, before, after, beforeEach } from 'node:test';
5
- import assert from 'node:assert/strict';
6
- import { mkdtempSync, readFileSync, existsSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
7
- import { join } from 'node:path';
8
- import { tmpdir } from 'node:os';
9
- import { fileURLToPath } from 'node:url';
10
- import { dirname } from 'node:path';
11
-
12
- const __dirname = dirname(fileURLToPath(import.meta.url));
13
- const ROOT = join(__dirname, '..', '..');
14
-
15
- const { run: captureRun } = await import(join(ROOT, 'scripts', 'lib', 'solutions-capture.mjs'));
16
-
17
- function makeDir() {
18
- return mkdtempSync(join(tmpdir(), 'tf-sol-capture-'));
19
- }
20
-
21
- function cleanup(dir) {
22
- if (existsSync(dir)) rmSync(dir, { recursive: true, force: true });
23
- }
24
-
25
- describe('solutions-capture', () => {
26
- let dir;
27
-
28
- beforeEach(() => {
29
- dir = makeDir();
30
- });
31
-
32
- after(() => {
33
- // Clean up all temp dirs
34
- cleanup(dir);
35
- });
36
-
37
- it('creates experience file in correct phase directory', () => {
38
- const result = captureRun({
39
- phase: 'prd', domain: 'auth', type: 'pitfall', severity: 'high',
40
- summary: 'OAuth token refresh fails silently', dir,
41
- });
42
-
43
- assert.equal(result.phase, 'prd');
44
- assert.equal(result.severity, 'high');
45
- assert.match(result.file, /^prd\/\d{4}-\d{2}-\d{2}-.*\.md$/);
46
-
47
- // Verify file exists and has frontmatter
48
- const filePath = join(dir, result.file);
49
- assert.ok(existsSync(filePath), 'experience file should exist');
50
- const content = readFileSync(filePath, 'utf-8');
51
- assert.match(content, /^---\nphase: prd\n/);
52
- assert.match(content, /domain: auth/);
53
- assert.match(content, /type: pitfall/);
54
- assert.match(content, /severity: high/);
55
- });
56
-
57
- it('creates INDEX.md from scratch when missing', () => {
58
- captureRun({
59
- phase: 'build', domain: 'ci', type: 'pattern', severity: 'medium',
60
- summary: 'Parallel test execution race condition', dir,
61
- });
62
-
63
- const indexPath = join(dir, 'INDEX.md');
64
- assert.ok(existsSync(indexPath), 'INDEX.md should be created');
65
- const content = readFileSync(indexPath, 'utf-8');
66
- assert.match(content, /# Solutions Index/);
67
- assert.match(content, /parallel test execution/i);
68
- });
69
-
70
- it('appends to existing INDEX.md', () => {
71
- captureRun({ phase: 'build', domain: 'ci', type: 'pattern', severity: 'high', summary: 'First issue', dir });
72
- captureRun({ phase: 'spec', domain: 'api', type: 'insight', severity: 'low', summary: 'Second issue', dir });
73
-
74
- const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
75
- assert.match(content, /First issue/);
76
- assert.match(content, /Second issue/);
77
- });
78
-
79
- it('handles Chinese characters in summary (slugify)', () => {
80
- const result = captureRun({
81
- phase: 'review', domain: 'frontend', type: 'pitfall', severity: 'high',
82
- summary: '中文标题测试特殊字符', dir,
83
- });
84
-
85
- assert.ok(result.file.includes('review/'), 'file should be in review phase');
86
- assert.ok(existsSync(join(dir, result.file)), 'file with Chinese slug should exist');
87
- });
88
-
89
- it('uses cross-phase as default when no phase specified', () => {
90
- const result = captureRun({ dir });
91
- assert.equal(result.phase, 'cross-phase');
92
- assert.equal(result.domain, 'general');
93
- assert.equal(result.severity, 'medium');
94
- });
95
-
96
- it('rejects invalid phase', () => {
97
- // capture calls process.exit(1) on invalid phase
98
- // We test by checking that the function does not create files
99
- const origExit = process.exit;
100
- let exitCode = null;
101
- process.exit = (code) => { exitCode = code; };
102
- try {
103
- captureRun({ phase: 'invalid-phase', dir });
104
- } catch { /* ignore */ }
105
- process.exit = origExit;
106
- assert.equal(exitCode, 1, 'should exit with code 1 for invalid phase');
107
- });
108
- });
@@ -1,147 +0,0 @@
1
- // tests/lib/solutions-index-gen.test.mjs
2
- // Tests for scripts/lib/solutions-index-gen.mjs — full INDEX.md rebuild
3
-
4
- import { describe, it, before, after, beforeEach } from 'node:test';
5
- import assert from 'node:assert/strict';
6
- import { mkdtempSync, writeFileSync, existsSync, rmSync, mkdirSync, readFileSync } from 'node:fs';
7
- import { join } from 'node:path';
8
- import { tmpdir } from 'node:os';
9
- import { fileURLToPath } from 'node:url';
10
- import { dirname } from 'node:path';
11
-
12
- const __dirname = dirname(fileURLToPath(import.meta.url));
13
- const ROOT = join(__dirname, '..', '..');
14
-
15
- const { run: indexGenRun } = await import(join(ROOT, 'scripts', 'lib', 'solutions-index-gen.mjs'));
16
-
17
- function makeDir() {
18
- return mkdtempSync(join(tmpdir(), 'tf-sol-indexgen-'));
19
- }
20
-
21
- function cleanup(dir) {
22
- if (existsSync(dir)) rmSync(dir, { recursive: true, force: true });
23
- }
24
-
25
- function writeEntry(dir, phase, filename, fm) {
26
- const phaseDir = join(dir, phase);
27
- mkdirSync(phaseDir, { recursive: true });
28
- const fmLines = Object.entries(fm).map(([k, v]) => `${k}: ${v}`).join('\n');
29
- writeFileSync(join(phaseDir, filename), `---\n${fmLines}\n---\n\n${fm.summary || 'Body text'}\n`, 'utf-8');
30
- }
31
-
32
- describe('solutions-index-gen', () => {
33
- let dir;
34
-
35
- beforeEach(() => { dir = makeDir(); });
36
- after(() => { cleanup(dir); });
37
-
38
- it('rebuilds INDEX.md from phase directories', () => {
39
- writeEntry(dir, 'prd', '2026-07-01-auth-issue.md', {
40
- phase: 'prd', domain: 'auth', type: 'pitfall', severity: 'high', date: '2026-07-01',
41
- });
42
- writeEntry(dir, 'build', '2026-07-02-ci-pattern.md', {
43
- phase: 'build', domain: 'ci', type: 'pattern', severity: 'medium', date: '2026-07-02',
44
- });
45
-
46
- indexGenRun({ dir });
47
-
48
- const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
49
- assert.match(content, /# Solutions Index/);
50
- assert.match(content, /auth-issue/);
51
- assert.match(content, /ci-pattern/);
52
- });
53
-
54
- it('sorts by severity descending (high before medium before low)', () => {
55
- writeEntry(dir, 'prd', 'low.md', {
56
- phase: 'prd', domain: 'd1', type: 'pitfall', severity: 'low', date: '2026-07-03',
57
- });
58
- writeEntry(dir, 'prd', 'high.md', {
59
- phase: 'prd', domain: 'd2', type: 'pitfall', severity: 'high', date: '2026-07-01',
60
- });
61
- writeEntry(dir, 'prd', 'medium.md', {
62
- phase: 'prd', domain: 'd3', type: 'pitfall', severity: 'medium', date: '2026-07-02',
63
- });
64
-
65
- indexGenRun({ dir });
66
-
67
- const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
68
- const highPos = content.indexOf('d2');
69
- const medPos = content.indexOf('d3');
70
- const lowPos = content.indexOf('d1');
71
- assert.ok(highPos < medPos, 'high should appear before medium');
72
- assert.ok(medPos < lowPos, 'medium should appear before low');
73
- });
74
-
75
- it('sorts by date descending within same severity', () => {
76
- writeEntry(dir, 'prd', 'older.md', {
77
- phase: 'prd', domain: 'auth', type: 'pitfall', severity: 'high', date: '2026-07-01',
78
- });
79
- writeEntry(dir, 'prd', 'newer.md', {
80
- phase: 'prd', domain: 'auth', type: 'pitfall', severity: 'high', date: '2026-07-15',
81
- });
82
-
83
- indexGenRun({ dir });
84
-
85
- const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
86
- const newerPos = content.indexOf('2026-07-15');
87
- const olderPos = content.indexOf('2026-07-01');
88
- assert.ok(newerPos < olderPos, 'newer date should appear before older date');
89
- });
90
-
91
- it('scans all 7 phase directories', () => {
92
- const phases = ['prd', 'plan', 'prototype', 'spec', 'build', 'review', 'cross-phase'];
93
- for (const phase of phases) {
94
- writeEntry(dir, phase, `2026-07-01-${phase}-entry.md`, {
95
- phase, domain: 'general', type: 'insight', severity: 'medium', date: '2026-07-01',
96
- });
97
- }
98
-
99
- indexGenRun({ dir });
100
-
101
- const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
102
- for (const phase of phases) {
103
- assert.match(content, new RegExp(phase), `INDEX should contain ${phase} entries`);
104
- }
105
- });
106
-
107
- it('skips non-.md files', () => {
108
- mkdirSync(join(dir, 'build'), { recursive: true });
109
- writeFileSync(join(dir, 'build', 'readme.txt'), 'Not a markdown file', 'utf-8');
110
- writeEntry(dir, 'build', '2026-07-01-real.md', {
111
- phase: 'build', domain: 'ci', type: 'pattern', severity: 'high', date: '2026-07-01',
112
- });
113
-
114
- indexGenRun({ dir });
115
-
116
- const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
117
- assert.doesNotMatch(content, /readme\.txt/);
118
- assert.match(content, /real\.md/);
119
- });
120
-
121
- it('truncates to 150 entries (drops lowest priority)', () => {
122
- // Create 155 entries (all high severity, varying dates)
123
- mkdirSync(join(dir, 'build'), { recursive: true });
124
- for (let i = 0; i < 155; i++) {
125
- const day = String((i % 28) + 1).padStart(2, '0');
126
- writeEntry(dir, 'build', `entry-${String(i).padStart(3, '0')}.md`, {
127
- phase: 'build', domain: `d${i}`, type: 'pattern', severity: 'high', date: `2026-07-${day}`,
128
- });
129
- }
130
-
131
- indexGenRun({ dir });
132
-
133
- const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
134
- const dataLines = content.split('\n').filter(l => l.startsWith('|') && !l.startsWith('| date') && !l.startsWith('|--'));
135
- assert.equal(dataLines.length, 150, 'INDEX should be truncated to 150 entries');
136
- });
137
-
138
- it('handles empty solutions directory', () => {
139
- // No phase directories exist
140
- indexGenRun({ dir });
141
-
142
- const content = readFileSync(join(dir, 'INDEX.md'), 'utf-8');
143
- assert.match(content, /# Solutions Index/);
144
- const dataLines = content.split('\n').filter(l => l.startsWith('|') && !l.startsWith('| date') && !l.startsWith('|--'));
145
- assert.equal(dataLines.length, 0);
146
- });
147
- });