@gordon.gan/specflow 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +31 -0
- package/README.md +442 -0
- package/bin/specflow.js +2 -0
- package/dist/cli/commands/change-archive.d.ts +26 -0
- package/dist/cli/commands/change-archive.js +50 -0
- package/dist/cli/commands/change-new.d.ts +18 -0
- package/dist/cli/commands/change-new.js +53 -0
- package/dist/cli/commands/change-phase.d.ts +33 -0
- package/dist/cli/commands/change-phase.js +66 -0
- package/dist/cli/commands/change-status.d.ts +35 -0
- package/dist/cli/commands/change-status.js +95 -0
- package/dist/cli/commands/doctor.d.ts +9 -0
- package/dist/cli/commands/doctor.js +48 -0
- package/dist/cli/commands/init.d.ts +16 -0
- package/dist/cli/commands/init.js +122 -0
- package/dist/cli/commands/instructions.d.ts +21 -0
- package/dist/cli/commands/instructions.js +75 -0
- package/dist/cli/commands/parity-report.d.ts +10 -0
- package/dist/cli/commands/parity-report.js +67 -0
- package/dist/cli/commands/sync.d.ts +4 -0
- package/dist/cli/commands/sync.js +40 -0
- package/dist/cli/commands/validate.d.ts +21 -0
- package/dist/cli/commands/validate.js +46 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +52 -0
- package/dist/core/archive.d.ts +39 -0
- package/dist/core/archive.js +154 -0
- package/dist/core/artifact-graph/graph.d.ts +60 -0
- package/dist/core/artifact-graph/graph.js +141 -0
- package/dist/core/artifact-graph/index.d.ts +10 -0
- package/dist/core/artifact-graph/index.js +7 -0
- package/dist/core/artifact-graph/instruction-loader.d.ts +45 -0
- package/dist/core/artifact-graph/instruction-loader.js +37 -0
- package/dist/core/artifact-graph/outputs.d.ts +35 -0
- package/dist/core/artifact-graph/outputs.js +48 -0
- package/dist/core/artifact-graph/resolver.d.ts +54 -0
- package/dist/core/artifact-graph/resolver.js +139 -0
- package/dist/core/artifact-graph/schema.d.ts +23 -0
- package/dist/core/artifact-graph/schema.js +115 -0
- package/dist/core/artifact-graph/state.d.ts +9 -0
- package/dist/core/artifact-graph/state.js +93 -0
- package/dist/core/artifact-graph/types.d.ts +123 -0
- package/dist/core/artifact-graph/types.js +29 -0
- package/dist/core/parsers/change-parser.d.ts +20 -0
- package/dist/core/parsers/change-parser.js +22 -0
- package/dist/core/parsers/index.d.ts +7 -0
- package/dist/core/parsers/index.js +4 -0
- package/dist/core/parsers/markdown-parser.d.ts +35 -0
- package/dist/core/parsers/markdown-parser.js +193 -0
- package/dist/core/parsers/requirement-blocks.d.ts +21 -0
- package/dist/core/parsers/requirement-blocks.js +57 -0
- package/dist/core/parsers/spec-structure.d.ts +22 -0
- package/dist/core/parsers/spec-structure.js +105 -0
- package/dist/core/specs-apply.d.ts +22 -0
- package/dist/core/specs-apply.js +165 -0
- package/dist/core/templates/index.d.ts +31 -0
- package/dist/core/templates/index.js +80 -0
- package/dist/core/templates/types.d.ts +4 -0
- package/dist/core/templates/types.js +1 -0
- package/dist/core/validation/constants.d.ts +14 -0
- package/dist/core/validation/constants.js +14 -0
- package/dist/core/validation/types.d.ts +13 -0
- package/dist/core/validation/types.js +6 -0
- package/dist/core/validation/validator.d.ts +24 -0
- package/dist/core/validation/validator.js +250 -0
- package/dist/integrations/claude/adapter.d.ts +2 -0
- package/dist/integrations/claude/adapter.js +106 -0
- package/dist/integrations/codex/adapter.d.ts +2 -0
- package/dist/integrations/codex/adapter.js +141 -0
- package/dist/integrations/cursor/adapter.d.ts +2 -0
- package/dist/integrations/cursor/adapter.js +126 -0
- package/dist/integrations/shared/adapter-registry.d.ts +4 -0
- package/dist/integrations/shared/adapter-registry.js +27 -0
- package/dist/integrations/shared/asset-copy.d.ts +2 -0
- package/dist/integrations/shared/asset-copy.js +44 -0
- package/dist/integrations/shared/asset-hash.d.ts +4 -0
- package/dist/integrations/shared/asset-hash.js +54 -0
- package/dist/integrations/shared/capability-evidence.d.ts +1 -0
- package/dist/integrations/shared/capability-evidence.js +42 -0
- package/dist/integrations/shared/command-catalog.d.ts +6 -0
- package/dist/integrations/shared/command-catalog.js +13 -0
- package/dist/integrations/shared/managed-assets.d.ts +2 -0
- package/dist/integrations/shared/managed-assets.js +21 -0
- package/dist/integrations/shared/marker-write.d.ts +3 -0
- package/dist/integrations/shared/marker-write.js +48 -0
- package/dist/integrations/shared/migration-state.d.ts +2 -0
- package/dist/integrations/shared/migration-state.js +67 -0
- package/dist/integrations/shared/parity-comparator.d.ts +6 -0
- package/dist/integrations/shared/parity-comparator.js +97 -0
- package/dist/integrations/shared/parity-manifest.d.ts +13 -0
- package/dist/integrations/shared/parity-manifest.js +58 -0
- package/dist/integrations/shared/runtime-assets.d.ts +2 -0
- package/dist/integrations/shared/runtime-assets.js +21 -0
- package/dist/integrations/shared/skill-renderer.d.ts +5 -0
- package/dist/integrations/shared/skill-renderer.js +77 -0
- package/dist/integrations/shared/types.d.ts +35 -0
- package/dist/integrations/shared/types.js +1 -0
- package/dist/utils/change-metadata.d.ts +38 -0
- package/dist/utils/change-metadata.js +82 -0
- package/dist/utils/change-utils.d.ts +53 -0
- package/dist/utils/change-utils.js +97 -0
- package/dist/utils/file-system.d.ts +35 -0
- package/dist/utils/file-system.js +67 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/project-root.d.ts +22 -0
- package/dist/utils/project-root.js +44 -0
- package/dist/utils/task-progress.d.ts +39 -0
- package/dist/utils/task-progress.js +62 -0
- package/package.json +72 -0
- package/prompts/build/ecc-go-reviewer.md +80 -0
- package/prompts/build/ecc-java-reviewer.md +185 -0
- package/prompts/build/ecc-kotlin-reviewer.md +163 -0
- package/prompts/build/ecc-python-reviewer.md +102 -0
- package/prompts/build/ecc-rust-reviewer.md +98 -0
- package/prompts/build/ecc-typescript-reviewer.md +116 -0
- package/prompts/build/language-router.md +22 -0
- package/prompts/build/phase-a-plan.md +288 -0
- package/prompts/build/phase-b-execute.md +276 -0
- package/prompts/build/phase-b-review.md +119 -0
- package/prompts/build/phase-b-worktree.md +215 -0
- package/prompts/build/plan-document-reviewer.md +51 -0
- package/prompts/build/tdd.md +184 -0
- package/prompts/done/branch-finish.md +204 -0
- package/prompts/fix/debug.md +249 -0
- package/prompts/plan/design-draft.md +123 -0
- package/prompts/plan/proposal.md +85 -0
- package/prompts/plan/specs.md +114 -0
- package/prompts/plan/tasks-draft.md +123 -0
- package/prompts/reference/openspec/apply-instruction.md +2 -0
- package/prompts/reference/openspec/design-instruction.md +20 -0
- package/prompts/reference/openspec/proposal-instruction.md +18 -0
- package/prompts/reference/openspec/specs-instruction.md +47 -0
- package/prompts/reference/openspec/tasks-instruction.md +26 -0
- package/prompts/reference/specflow/example-design.md +284 -0
- package/prompts/reference/superpowers/anthropic-best-practices.md +1150 -0
- package/prompts/reference/superpowers/codex-tools.md +29 -0
- package/prompts/reference/superpowers/condition-based-waiting-example.ts +158 -0
- package/prompts/reference/superpowers/condition-based-waiting.md +115 -0
- package/prompts/reference/superpowers/defense-in-depth.md +122 -0
- package/prompts/reference/superpowers/find-polluter.sh +63 -0
- package/prompts/reference/superpowers/graphviz-conventions.dot +172 -0
- package/prompts/reference/superpowers/persuasion-principles.md +187 -0
- package/prompts/reference/superpowers/root-cause-tracing.md +169 -0
- package/prompts/reference/superpowers/testing-anti-patterns.md +299 -0
- package/prompts/reference/superpowers/testing-skills-with-subagents.md +384 -0
- package/prompts/reference/superpowers/using-superpowers.md +117 -0
- package/prompts/reference/superpowers/writing-skills.md +655 -0
- package/prompts/refine/brainstorm.md +241 -0
- package/prompts/refine/design-output.md +126 -0
- package/prompts/refine/spec-document-reviewer.md +51 -0
- package/prompts/refine/update-artifacts.md +185 -0
- package/prompts/review/code-review.md +223 -0
- package/prompts/shared/code-reviewer-prompt.md +98 -0
- package/prompts/shared/dispatching-parallel-agents.md +143 -0
- package/prompts/shared/executing-plans.md +67 -0
- package/prompts/shared/implementer-prompt.md +115 -0
- package/prompts/shared/receiving-code-review.md +174 -0
- package/prompts/shared/spec-reviewer-prompt.md +63 -0
- package/prompts/test/tdd.md +236 -0
- package/prompts/test/verification.md +129 -0
- package/prompts/verify/ecc-security-reviewer.md +112 -0
- package/prompts/verify/verification.md +196 -0
- package/schemas/specflow/schema.yaml +48 -0
- package/skills/specflow-build/SKILL.md +129 -0
- package/skills/specflow-done/SKILL.md +68 -0
- package/skills/specflow-fix/SKILL.md +74 -0
- package/skills/specflow-plan/SKILL.md +82 -0
- package/skills/specflow-refine/SKILL.md +128 -0
- package/skills/specflow-review/SKILL.md +40 -0
- package/skills/specflow-scan/SKILL.md +48 -0
- package/skills/specflow-snap/SKILL.md +46 -0
- package/skills/specflow-test/SKILL.md +48 -0
- package/skills/specflow-verify/SKILL.md +77 -0
- package/templates/design.md +19 -0
- package/templates/proposal.md +23 -0
- package/templates/spec.md +42 -0
- package/templates/tasks.md +9 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: specflow validate <file>
|
|
3
|
+
*
|
|
4
|
+
* Validates a spec file for structural correctness.
|
|
5
|
+
*/
|
|
6
|
+
import { promises as fs } from 'node:fs';
|
|
7
|
+
import { validateSpec } from '../../core/validation/validator.js';
|
|
8
|
+
/**
|
|
9
|
+
* Validates a spec file at the given path.
|
|
10
|
+
*
|
|
11
|
+
* Reads the file and runs the spec validator, returning structured results.
|
|
12
|
+
*
|
|
13
|
+
* @param filePath - Absolute path to the spec file
|
|
14
|
+
* @returns Validation result with valid flag and any errors
|
|
15
|
+
* @throws When the file cannot be read
|
|
16
|
+
*/
|
|
17
|
+
export async function validateSpecFile(filePath) {
|
|
18
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
19
|
+
return validateSpec(content);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Registers the `validate` command with Commander.
|
|
23
|
+
*/
|
|
24
|
+
export function registerValidateCommand(program) {
|
|
25
|
+
program
|
|
26
|
+
.command('validate <file>')
|
|
27
|
+
.description('Validate a spec file for structural correctness')
|
|
28
|
+
.option('--json', 'Output as JSON')
|
|
29
|
+
.action(async (file, opts) => {
|
|
30
|
+
const result = await validateSpecFile(file);
|
|
31
|
+
if (opts.json) {
|
|
32
|
+
console.info(JSON.stringify(result, null, 2));
|
|
33
|
+
}
|
|
34
|
+
else if (result.valid) {
|
|
35
|
+
console.info('Valid: no errors found.');
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
console.error(`Found ${result.errors.length} error(s):`);
|
|
39
|
+
for (const err of result.errors) {
|
|
40
|
+
const lineInfo = err.line ? ` (line ${err.line})` : '';
|
|
41
|
+
console.error(` - ${err.message}${lineInfo}`);
|
|
42
|
+
}
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { registerChangeNewCommand } from './commands/change-new.js';
|
|
6
|
+
import { registerChangeStatusCommand } from './commands/change-status.js';
|
|
7
|
+
import { registerChangeArchiveCommand } from './commands/change-archive.js';
|
|
8
|
+
import { registerChangePhaseCommand } from './commands/change-phase.js';
|
|
9
|
+
import { registerInstructionsCommand } from './commands/instructions.js';
|
|
10
|
+
import { registerValidateCommand } from './commands/validate.js';
|
|
11
|
+
import { registerInitCommand } from './commands/init.js';
|
|
12
|
+
import { registerSyncCommand } from './commands/sync.js';
|
|
13
|
+
import { registerDoctorCommand } from './commands/doctor.js';
|
|
14
|
+
import { registerParityReportCommand } from './commands/parity-report.js';
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf-8'));
|
|
18
|
+
const program = new Command();
|
|
19
|
+
program
|
|
20
|
+
.name('specflow')
|
|
21
|
+
.description('Unified spec-driven development: OpenSpec planning + Superpowers execution')
|
|
22
|
+
.version(pkg.version);
|
|
23
|
+
// Group command: specflow change <new|status|archive>
|
|
24
|
+
const changeCmd = program
|
|
25
|
+
.command('change')
|
|
26
|
+
.description('Manage changes (new, status, archive)');
|
|
27
|
+
registerChangeNewCommand(changeCmd);
|
|
28
|
+
registerChangeStatusCommand(changeCmd);
|
|
29
|
+
registerChangeArchiveCommand(changeCmd);
|
|
30
|
+
registerChangePhaseCommand(changeCmd);
|
|
31
|
+
// Top-level commands
|
|
32
|
+
registerInstructionsCommand(program);
|
|
33
|
+
registerValidateCommand(program);
|
|
34
|
+
registerInitCommand(program);
|
|
35
|
+
registerSyncCommand(program);
|
|
36
|
+
registerDoctorCommand(program);
|
|
37
|
+
registerParityReportCommand(program);
|
|
38
|
+
program.exitOverride();
|
|
39
|
+
try {
|
|
40
|
+
await program.parseAsync();
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error instanceof Error && 'code' in error) {
|
|
44
|
+
const code = error.code;
|
|
45
|
+
if (code === 'commander.helpDisplayed' || code === 'commander.version') {
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
+
console.error(`错误: ${message}`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Archive module.
|
|
3
|
+
*
|
|
4
|
+
* Archives a completed change by:
|
|
5
|
+
* 1. Validating all delta specs in the change
|
|
6
|
+
* 2. Applying deltas to their corresponding main specs
|
|
7
|
+
* 3. Moving the change directory to the archive
|
|
8
|
+
*/
|
|
9
|
+
export interface ArchiveResult {
|
|
10
|
+
readonly success: boolean;
|
|
11
|
+
readonly errors: readonly string[];
|
|
12
|
+
readonly archivePath?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ArchiveOptions {
|
|
15
|
+
/**
|
|
16
|
+
* If true, archive even when the change is not in phase=built.
|
|
17
|
+
* A warning is emitted to stderr when force is applied.
|
|
18
|
+
*/
|
|
19
|
+
readonly force?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Archive a completed change.
|
|
23
|
+
*
|
|
24
|
+
* Steps:
|
|
25
|
+
* 0. Validate phase gate: require phase=built unless options.force is true
|
|
26
|
+
* 1. Find all delta spec files in `specflow/changes/<name>/specs/`
|
|
27
|
+
* 2. Validate each delta spec
|
|
28
|
+
* 3. For each delta spec, find the corresponding main spec in `specflow/specs/`
|
|
29
|
+
* 4. Apply deltas to main specs
|
|
30
|
+
* 5. Move the change directory to `specflow/changes/archive/<date>-<name>/`
|
|
31
|
+
* 6. Update archived .specflow.yaml with phase=archived
|
|
32
|
+
* 7. Return success/failure with details
|
|
33
|
+
*
|
|
34
|
+
* @param changeName - The name of the change to archive
|
|
35
|
+
* @param projectRoot - Absolute path to the project root
|
|
36
|
+
* @param options - Optional archive options (e.g. force)
|
|
37
|
+
* @returns An ArchiveResult with success flag, errors, and archive path
|
|
38
|
+
*/
|
|
39
|
+
export declare function archiveChange(changeName: string, projectRoot: string, options?: ArchiveOptions): Promise<ArchiveResult>;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Archive module.
|
|
3
|
+
*
|
|
4
|
+
* Archives a completed change by:
|
|
5
|
+
* 1. Validating all delta specs in the change
|
|
6
|
+
* 2. Applying deltas to their corresponding main specs
|
|
7
|
+
* 3. Moving the change directory to the archive
|
|
8
|
+
*/
|
|
9
|
+
import { promises as fs } from 'node:fs';
|
|
10
|
+
import { join, relative } from 'node:path';
|
|
11
|
+
import { validateSpec } from './validation/validator.js';
|
|
12
|
+
import { applyDeltaSpec } from './specs-apply.js';
|
|
13
|
+
import { readChangeMetadata, writeChangeMetadata, } from '../utils/change-metadata.js';
|
|
14
|
+
/**
|
|
15
|
+
* Recursively list all `.md` files in a directory, returning paths relative to the base dir.
|
|
16
|
+
*/
|
|
17
|
+
async function listMarkdownFiles(dir, base) {
|
|
18
|
+
const root = base ?? dir;
|
|
19
|
+
try {
|
|
20
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
21
|
+
const results = [];
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const fullPath = join(dir, entry.name);
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
const nested = await listMarkdownFiles(fullPath, root);
|
|
26
|
+
results.push(...nested);
|
|
27
|
+
}
|
|
28
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
29
|
+
results.push(relative(root, fullPath));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return results;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error instanceof Error &&
|
|
36
|
+
'code' in error &&
|
|
37
|
+
error.code === 'ENOENT') {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get today's date as YYYY-MM-DD string.
|
|
45
|
+
*/
|
|
46
|
+
function todayDatePrefix() {
|
|
47
|
+
const now = new Date();
|
|
48
|
+
const year = now.getFullYear();
|
|
49
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
50
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
51
|
+
return `${year}-${month}-${day}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Archive a completed change.
|
|
55
|
+
*
|
|
56
|
+
* Steps:
|
|
57
|
+
* 0. Validate phase gate: require phase=built unless options.force is true
|
|
58
|
+
* 1. Find all delta spec files in `specflow/changes/<name>/specs/`
|
|
59
|
+
* 2. Validate each delta spec
|
|
60
|
+
* 3. For each delta spec, find the corresponding main spec in `specflow/specs/`
|
|
61
|
+
* 4. Apply deltas to main specs
|
|
62
|
+
* 5. Move the change directory to `specflow/changes/archive/<date>-<name>/`
|
|
63
|
+
* 6. Update archived .specflow.yaml with phase=archived
|
|
64
|
+
* 7. Return success/failure with details
|
|
65
|
+
*
|
|
66
|
+
* @param changeName - The name of the change to archive
|
|
67
|
+
* @param projectRoot - Absolute path to the project root
|
|
68
|
+
* @param options - Optional archive options (e.g. force)
|
|
69
|
+
* @returns An ArchiveResult with success flag, errors, and archive path
|
|
70
|
+
*/
|
|
71
|
+
export async function archiveChange(changeName, projectRoot, options = {}) {
|
|
72
|
+
const changeDir = join(projectRoot, 'specflow', 'changes', changeName);
|
|
73
|
+
const deltaSpecsDir = join(changeDir, 'specs');
|
|
74
|
+
const mainSpecsDir = join(projectRoot, 'specflow', 'specs');
|
|
75
|
+
// 0. Phase gate: require phase=built unless --force
|
|
76
|
+
const metadata = await readChangeMetadata(changeDir);
|
|
77
|
+
const currentPhase = metadata?.phase;
|
|
78
|
+
if (currentPhase !== 'built' && !options.force) {
|
|
79
|
+
const phaseLabel = currentPhase ?? 'unknown';
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
errors: [
|
|
83
|
+
`Cannot archive: change '${changeName}' is in phase '${phaseLabel}', expected 'built'. Complete '/specflow:build' first, or pass '--force' to archive anyway.`,
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (options.force && currentPhase !== 'built') {
|
|
88
|
+
const phaseLabel = currentPhase ?? 'unknown';
|
|
89
|
+
console.warn(`Warning: archiving "${changeName}" in phase ${phaseLabel} with --force. ` +
|
|
90
|
+
`Consider running /specflow:build first.`);
|
|
91
|
+
}
|
|
92
|
+
// 1. Find delta spec files
|
|
93
|
+
const deltaFiles = await listMarkdownFiles(deltaSpecsDir);
|
|
94
|
+
if (deltaFiles.length === 0) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
errors: [`No delta spec files found in ${deltaSpecsDir}`],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// 2. Validate each delta spec
|
|
101
|
+
const allErrors = [];
|
|
102
|
+
for (const file of deltaFiles) {
|
|
103
|
+
const deltaContent = await fs.readFile(join(deltaSpecsDir, file), 'utf-8');
|
|
104
|
+
const validation = validateSpec(deltaContent);
|
|
105
|
+
if (!validation.valid) {
|
|
106
|
+
for (const err of validation.errors) {
|
|
107
|
+
allErrors.push(`${file}: ${err.message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (allErrors.length > 0) {
|
|
112
|
+
return { success: false, errors: allErrors };
|
|
113
|
+
}
|
|
114
|
+
// 3 & 4. Apply each delta to its corresponding main spec
|
|
115
|
+
for (const file of deltaFiles) {
|
|
116
|
+
const deltaContent = await fs.readFile(join(deltaSpecsDir, file), 'utf-8');
|
|
117
|
+
const mainSpecPath = join(mainSpecsDir, file);
|
|
118
|
+
const mainSpecDir = join(mainSpecPath, '..');
|
|
119
|
+
await fs.mkdir(mainSpecDir, { recursive: true });
|
|
120
|
+
let mainContent;
|
|
121
|
+
try {
|
|
122
|
+
mainContent = await fs.readFile(mainSpecPath, 'utf-8');
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (error instanceof Error &&
|
|
126
|
+
'code' in error &&
|
|
127
|
+
error.code === 'ENOENT') {
|
|
128
|
+
// If main spec doesn't exist yet, start with empty content
|
|
129
|
+
mainContent = '';
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const updatedContent = applyDeltaSpec(mainContent, deltaContent);
|
|
136
|
+
await fs.writeFile(mainSpecPath, updatedContent, 'utf-8');
|
|
137
|
+
}
|
|
138
|
+
// 5. Move change directory to archive
|
|
139
|
+
const datePrefix = todayDatePrefix();
|
|
140
|
+
const archiveDir = join(projectRoot, 'specflow', 'changes', 'archive');
|
|
141
|
+
const archiveDest = join(archiveDir, `${datePrefix}-${changeName}`);
|
|
142
|
+
await fs.mkdir(archiveDir, { recursive: true });
|
|
143
|
+
await fs.rename(changeDir, archiveDest);
|
|
144
|
+
// 6. Update archived metadata to phase=archived (preserve other fields)
|
|
145
|
+
const archivedMeta = await readChangeMetadata(archiveDest);
|
|
146
|
+
if (archivedMeta !== null) {
|
|
147
|
+
await writeChangeMetadata(archiveDest, { ...archivedMeta, phase: 'archived' });
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
success: true,
|
|
151
|
+
errors: [],
|
|
152
|
+
archivePath: archiveDest,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Artifact, SchemaYaml, CompletedSet, BlockedArtifacts } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Represents an artifact dependency graph.
|
|
4
|
+
* Provides methods for querying build order, ready artifacts, and completion status.
|
|
5
|
+
*
|
|
6
|
+
* Immutable: all query methods return new arrays/objects, never mutate internal state.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ArtifactGraph {
|
|
9
|
+
private readonly artifacts;
|
|
10
|
+
private readonly schema;
|
|
11
|
+
private constructor();
|
|
12
|
+
/**
|
|
13
|
+
* Creates an ArtifactGraph from a YAML file path.
|
|
14
|
+
*/
|
|
15
|
+
static fromYaml(filePath: string): ArtifactGraph;
|
|
16
|
+
/**
|
|
17
|
+
* Creates an ArtifactGraph from YAML content string.
|
|
18
|
+
*/
|
|
19
|
+
static fromYamlContent(yamlContent: string): ArtifactGraph;
|
|
20
|
+
/**
|
|
21
|
+
* Creates an ArtifactGraph from a pre-validated schema object.
|
|
22
|
+
*/
|
|
23
|
+
static fromSchema(schema: SchemaYaml): ArtifactGraph;
|
|
24
|
+
/**
|
|
25
|
+
* Gets a single artifact by ID.
|
|
26
|
+
*/
|
|
27
|
+
getArtifact(id: string): Artifact | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Gets all artifacts in the graph (returns a new array).
|
|
30
|
+
*/
|
|
31
|
+
getAllArtifacts(): readonly Artifact[];
|
|
32
|
+
/**
|
|
33
|
+
* Gets the schema name.
|
|
34
|
+
*/
|
|
35
|
+
getName(): string;
|
|
36
|
+
/**
|
|
37
|
+
* Gets the schema version.
|
|
38
|
+
*/
|
|
39
|
+
getVersion(): number;
|
|
40
|
+
/**
|
|
41
|
+
* Computes the topological build order using Kahn's algorithm.
|
|
42
|
+
* Returns artifact IDs in a valid order: dependencies before dependents.
|
|
43
|
+
* Ties are broken alphabetically for deterministic output.
|
|
44
|
+
*/
|
|
45
|
+
getBuildOrder(): string[];
|
|
46
|
+
/**
|
|
47
|
+
* Gets artifacts that are ready to be created (all dependencies in completed set).
|
|
48
|
+
* Excludes already-completed artifacts.
|
|
49
|
+
*/
|
|
50
|
+
getNextArtifacts(completed: CompletedSet): string[];
|
|
51
|
+
/**
|
|
52
|
+
* Checks if all artifacts in the graph are completed.
|
|
53
|
+
*/
|
|
54
|
+
isComplete(completed: CompletedSet): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Gets blocked artifacts and their unmet dependencies.
|
|
57
|
+
* Returns a mapping of artifact ID to array of missing dependency IDs.
|
|
58
|
+
*/
|
|
59
|
+
getBlocked(completed: CompletedSet): BlockedArtifacts;
|
|
60
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { loadSchema, parseSchema } from './schema.js';
|
|
2
|
+
/**
|
|
3
|
+
* Represents an artifact dependency graph.
|
|
4
|
+
* Provides methods for querying build order, ready artifacts, and completion status.
|
|
5
|
+
*
|
|
6
|
+
* Immutable: all query methods return new arrays/objects, never mutate internal state.
|
|
7
|
+
*/
|
|
8
|
+
export class ArtifactGraph {
|
|
9
|
+
artifacts;
|
|
10
|
+
schema;
|
|
11
|
+
constructor(schema) {
|
|
12
|
+
this.schema = schema;
|
|
13
|
+
this.artifacts = new Map(schema.artifacts.map(a => [a.id, a]));
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Creates an ArtifactGraph from a YAML file path.
|
|
17
|
+
*/
|
|
18
|
+
static fromYaml(filePath) {
|
|
19
|
+
const schema = loadSchema(filePath);
|
|
20
|
+
return new ArtifactGraph(schema);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Creates an ArtifactGraph from YAML content string.
|
|
24
|
+
*/
|
|
25
|
+
static fromYamlContent(yamlContent) {
|
|
26
|
+
const schema = parseSchema(yamlContent);
|
|
27
|
+
return new ArtifactGraph(schema);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Creates an ArtifactGraph from a pre-validated schema object.
|
|
31
|
+
*/
|
|
32
|
+
static fromSchema(schema) {
|
|
33
|
+
return new ArtifactGraph(schema);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Gets a single artifact by ID.
|
|
37
|
+
*/
|
|
38
|
+
getArtifact(id) {
|
|
39
|
+
return this.artifacts.get(id);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Gets all artifacts in the graph (returns a new array).
|
|
43
|
+
*/
|
|
44
|
+
getAllArtifacts() {
|
|
45
|
+
return Array.from(this.artifacts.values());
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Gets the schema name.
|
|
49
|
+
*/
|
|
50
|
+
getName() {
|
|
51
|
+
return this.schema.name;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Gets the schema version.
|
|
55
|
+
*/
|
|
56
|
+
getVersion() {
|
|
57
|
+
return this.schema.version;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Computes the topological build order using Kahn's algorithm.
|
|
61
|
+
* Returns artifact IDs in a valid order: dependencies before dependents.
|
|
62
|
+
* Ties are broken alphabetically for deterministic output.
|
|
63
|
+
*/
|
|
64
|
+
getBuildOrder() {
|
|
65
|
+
const inDegree = new Map();
|
|
66
|
+
const dependents = new Map();
|
|
67
|
+
for (const artifact of this.artifacts.values()) {
|
|
68
|
+
inDegree.set(artifact.id, artifact.requires.length);
|
|
69
|
+
dependents.set(artifact.id, []);
|
|
70
|
+
}
|
|
71
|
+
for (const artifact of this.artifacts.values()) {
|
|
72
|
+
for (const req of artifact.requires) {
|
|
73
|
+
dependents.get(req).push(artifact.id);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Start with roots (in-degree 0), sorted for determinism
|
|
77
|
+
const queue = [...this.artifacts.keys()]
|
|
78
|
+
.filter(id => inDegree.get(id) === 0)
|
|
79
|
+
.sort();
|
|
80
|
+
const result = [];
|
|
81
|
+
while (queue.length > 0) {
|
|
82
|
+
const current = queue.shift();
|
|
83
|
+
result.push(current);
|
|
84
|
+
const newlyReady = [];
|
|
85
|
+
for (const dep of dependents.get(current)) {
|
|
86
|
+
const newDegree = inDegree.get(dep) - 1;
|
|
87
|
+
inDegree.set(dep, newDegree);
|
|
88
|
+
if (newDegree === 0) {
|
|
89
|
+
newlyReady.push(dep);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
queue.push(...newlyReady.sort());
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Gets artifacts that are ready to be created (all dependencies in completed set).
|
|
98
|
+
* Excludes already-completed artifacts.
|
|
99
|
+
*/
|
|
100
|
+
getNextArtifacts(completed) {
|
|
101
|
+
const ready = [];
|
|
102
|
+
for (const artifact of this.artifacts.values()) {
|
|
103
|
+
if (completed.has(artifact.id)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const allDepsCompleted = artifact.requires.every(req => completed.has(req));
|
|
107
|
+
if (allDepsCompleted) {
|
|
108
|
+
ready.push(artifact.id);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return ready.sort();
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Checks if all artifacts in the graph are completed.
|
|
115
|
+
*/
|
|
116
|
+
isComplete(completed) {
|
|
117
|
+
for (const artifact of this.artifacts.values()) {
|
|
118
|
+
if (!completed.has(artifact.id)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Gets blocked artifacts and their unmet dependencies.
|
|
126
|
+
* Returns a mapping of artifact ID to array of missing dependency IDs.
|
|
127
|
+
*/
|
|
128
|
+
getBlocked(completed) {
|
|
129
|
+
const blocked = {};
|
|
130
|
+
for (const artifact of this.artifacts.values()) {
|
|
131
|
+
if (completed.has(artifact.id)) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const unmetDeps = artifact.requires.filter(req => !completed.has(req));
|
|
135
|
+
if (unmetDeps.length > 0) {
|
|
136
|
+
blocked[artifact.id] = [...unmetDeps].sort();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return blocked;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { ArtifactSchema, ApplyPhaseSchema, SchemaYamlSchema, } from './types.js';
|
|
2
|
+
export type { Artifact, ApplyPhase, SchemaYaml, CompletedSet, BlockedArtifacts, } from './types.js';
|
|
3
|
+
export { loadSchema, parseSchema, SchemaValidationError, } from './schema.js';
|
|
4
|
+
export { resolveSchema, getSchemaDir, getPackageSchemasDir, getProjectSchemasDir, listSchemas, SchemaLoadError, } from './resolver.js';
|
|
5
|
+
export { ArtifactGraph } from './graph.js';
|
|
6
|
+
export { getCompletedArtifacts } from './state.js';
|
|
7
|
+
export { loadInstructions } from './instruction-loader.js';
|
|
8
|
+
export type { ArtifactInstructions, DependencyInfo, ProjectConfig, } from './instruction-loader.js';
|
|
9
|
+
export { formatStatus, formatStatusHuman } from './outputs.js';
|
|
10
|
+
export type { ArtifactStatusEntry, StatusOutput } from './outputs.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { ArtifactSchema, ApplyPhaseSchema, SchemaYamlSchema, } from './types.js';
|
|
2
|
+
export { loadSchema, parseSchema, SchemaValidationError, } from './schema.js';
|
|
3
|
+
export { resolveSchema, getSchemaDir, getPackageSchemasDir, getProjectSchemasDir, listSchemas, SchemaLoadError, } from './resolver.js';
|
|
4
|
+
export { ArtifactGraph } from './graph.js';
|
|
5
|
+
export { getCompletedArtifacts } from './state.js';
|
|
6
|
+
export { loadInstructions } from './instruction-loader.js';
|
|
7
|
+
export { formatStatus, formatStatusHuman } from './outputs.js';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { SchemaYaml } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Dependency information included in artifact instructions.
|
|
4
|
+
*/
|
|
5
|
+
export interface DependencyInfo {
|
|
6
|
+
/** Artifact ID */
|
|
7
|
+
readonly id: string;
|
|
8
|
+
/** File path pattern this dependency generates */
|
|
9
|
+
readonly generates: string;
|
|
10
|
+
/** Description of the dependency artifact */
|
|
11
|
+
readonly description: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Loaded instructions for creating an artifact.
|
|
15
|
+
*/
|
|
16
|
+
export interface ArtifactInstructions {
|
|
17
|
+
/** The artifact's instruction text from the schema */
|
|
18
|
+
readonly instruction: string | undefined;
|
|
19
|
+
/** Project context from the config */
|
|
20
|
+
readonly context: string | undefined;
|
|
21
|
+
/** Dependencies with their metadata */
|
|
22
|
+
readonly dependencies: readonly DependencyInfo[];
|
|
23
|
+
/** The artifact's generates path */
|
|
24
|
+
readonly generates: string;
|
|
25
|
+
/** The artifact's description */
|
|
26
|
+
readonly description: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Optional project config with context for instruction loading.
|
|
30
|
+
*/
|
|
31
|
+
export interface ProjectConfig {
|
|
32
|
+
readonly context?: string;
|
|
33
|
+
readonly [key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Loads enriched instructions for creating an artifact.
|
|
37
|
+
*
|
|
38
|
+
* @param artifactId - The artifact ID to load instructions for
|
|
39
|
+
* @param changeDir - Path to the change directory
|
|
40
|
+
* @param schema - The workflow schema
|
|
41
|
+
* @param config - Optional project configuration with context
|
|
42
|
+
* @returns The loaded artifact instructions
|
|
43
|
+
* @throws Error if the artifact ID is not found in the schema
|
|
44
|
+
*/
|
|
45
|
+
export declare function loadInstructions(artifactId: string, changeDir: string, schema: SchemaYaml, config?: ProjectConfig): ArtifactInstructions;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads enriched instructions for creating an artifact.
|
|
3
|
+
*
|
|
4
|
+
* @param artifactId - The artifact ID to load instructions for
|
|
5
|
+
* @param changeDir - Path to the change directory
|
|
6
|
+
* @param schema - The workflow schema
|
|
7
|
+
* @param config - Optional project configuration with context
|
|
8
|
+
* @returns The loaded artifact instructions
|
|
9
|
+
* @throws Error if the artifact ID is not found in the schema
|
|
10
|
+
*/
|
|
11
|
+
export function loadInstructions(artifactId, changeDir, schema, config) {
|
|
12
|
+
const artifact = schema.artifacts.find(a => a.id === artifactId);
|
|
13
|
+
if (!artifact) {
|
|
14
|
+
throw new Error(`Artifact '${artifactId}' not found in schema '${schema.name}'`);
|
|
15
|
+
}
|
|
16
|
+
const dependencies = buildDependencyInfo(artifact, schema);
|
|
17
|
+
return {
|
|
18
|
+
instruction: artifact.instruction,
|
|
19
|
+
context: config?.context ?? undefined,
|
|
20
|
+
dependencies,
|
|
21
|
+
generates: artifact.generates,
|
|
22
|
+
description: artifact.description,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Builds dependency information for an artifact's requirements.
|
|
27
|
+
*/
|
|
28
|
+
function buildDependencyInfo(artifact, schema) {
|
|
29
|
+
return artifact.requires.map(reqId => {
|
|
30
|
+
const depArtifact = schema.artifacts.find(a => a.id === reqId);
|
|
31
|
+
return {
|
|
32
|
+
id: reqId,
|
|
33
|
+
generates: depArtifact?.generates ?? reqId,
|
|
34
|
+
description: depArtifact?.description ?? '',
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Artifact } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Status of a single artifact.
|
|
4
|
+
*/
|
|
5
|
+
export interface ArtifactStatusEntry {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
readonly status: 'done' | 'ready' | 'blocked';
|
|
8
|
+
readonly missingDeps?: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* JSON-friendly status object for all artifacts.
|
|
12
|
+
*/
|
|
13
|
+
export interface StatusOutput {
|
|
14
|
+
readonly artifacts: readonly ArtifactStatusEntry[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Formats artifact statuses as a structured JSON-friendly object.
|
|
18
|
+
*
|
|
19
|
+
* @param artifacts - The artifact definitions from the schema
|
|
20
|
+
* @param completed - Array of completed artifact IDs
|
|
21
|
+
* @returns Structured status with each artifact's state
|
|
22
|
+
*/
|
|
23
|
+
export declare function formatStatus(artifacts: readonly Artifact[], completed: readonly string[]): StatusOutput;
|
|
24
|
+
/**
|
|
25
|
+
* Formats artifact statuses as human-readable checkbox text.
|
|
26
|
+
*
|
|
27
|
+
* Output format:
|
|
28
|
+
* [x] proposal
|
|
29
|
+
* [ ] specs (blocked by: proposal)
|
|
30
|
+
*
|
|
31
|
+
* @param artifacts - The artifact definitions from the schema
|
|
32
|
+
* @param completed - Array of completed artifact IDs
|
|
33
|
+
* @returns Checkbox-style text string
|
|
34
|
+
*/
|
|
35
|
+
export declare function formatStatusHuman(artifacts: readonly Artifact[], completed: readonly string[]): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats artifact statuses as a structured JSON-friendly object.
|
|
3
|
+
*
|
|
4
|
+
* @param artifacts - The artifact definitions from the schema
|
|
5
|
+
* @param completed - Array of completed artifact IDs
|
|
6
|
+
* @returns Structured status with each artifact's state
|
|
7
|
+
*/
|
|
8
|
+
export function formatStatus(artifacts, completed) {
|
|
9
|
+
const completedSet = new Set(completed);
|
|
10
|
+
const statuses = artifacts.map(artifact => {
|
|
11
|
+
if (completedSet.has(artifact.id)) {
|
|
12
|
+
return { id: artifact.id, status: 'done' };
|
|
13
|
+
}
|
|
14
|
+
const missingDeps = artifact.requires.filter(req => !completedSet.has(req));
|
|
15
|
+
if (missingDeps.length === 0) {
|
|
16
|
+
return { id: artifact.id, status: 'ready' };
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
id: artifact.id,
|
|
20
|
+
status: 'blocked',
|
|
21
|
+
missingDeps,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
return { artifacts: statuses };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Formats artifact statuses as human-readable checkbox text.
|
|
28
|
+
*
|
|
29
|
+
* Output format:
|
|
30
|
+
* [x] proposal
|
|
31
|
+
* [ ] specs (blocked by: proposal)
|
|
32
|
+
*
|
|
33
|
+
* @param artifacts - The artifact definitions from the schema
|
|
34
|
+
* @param completed - Array of completed artifact IDs
|
|
35
|
+
* @returns Checkbox-style text string
|
|
36
|
+
*/
|
|
37
|
+
export function formatStatusHuman(artifacts, completed) {
|
|
38
|
+
const completedSet = new Set(completed);
|
|
39
|
+
const lines = artifacts.map(artifact => {
|
|
40
|
+
const checkbox = completedSet.has(artifact.id) ? '[x]' : '[ ]';
|
|
41
|
+
const missingDeps = artifact.requires.filter(req => !completedSet.has(req));
|
|
42
|
+
const suffix = missingDeps.length > 0
|
|
43
|
+
? ` (blocked by: ${missingDeps.join(', ')})`
|
|
44
|
+
: '';
|
|
45
|
+
return `${checkbox} ${artifact.id}${suffix}`;
|
|
46
|
+
});
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|