@applica-software-guru/sdd-core 0.1.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.
Files changed (96) hide show
  1. package/dist/config/config-manager.d.ts +9 -0
  2. package/dist/config/config-manager.d.ts.map +1 -0
  3. package/dist/config/config-manager.js +44 -0
  4. package/dist/config/config-manager.js.map +1 -0
  5. package/dist/delta/delta-engine.d.ts +3 -0
  6. package/dist/delta/delta-engine.d.ts.map +1 -0
  7. package/dist/delta/delta-engine.js +18 -0
  8. package/dist/delta/delta-engine.js.map +1 -0
  9. package/dist/delta/hasher.d.ts +2 -0
  10. package/dist/delta/hasher.d.ts.map +1 -0
  11. package/dist/delta/hasher.js +8 -0
  12. package/dist/delta/hasher.js.map +1 -0
  13. package/dist/errors.d.ts +13 -0
  14. package/dist/errors.d.ts.map +1 -0
  15. package/dist/errors.js +32 -0
  16. package/dist/errors.js.map +1 -0
  17. package/dist/git/git.d.ts +12 -0
  18. package/dist/git/git.d.ts.map +1 -0
  19. package/dist/git/git.js +125 -0
  20. package/dist/git/git.js.map +1 -0
  21. package/dist/index.d.ts +6 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +14 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/lock/lock-manager.d.ts +6 -0
  26. package/dist/lock/lock-manager.d.ts.map +1 -0
  27. package/dist/lock/lock-manager.js +39 -0
  28. package/dist/lock/lock-manager.js.map +1 -0
  29. package/dist/parser/cr-parser.d.ts +8 -0
  30. package/dist/parser/cr-parser.d.ts.map +1 -0
  31. package/dist/parser/cr-parser.js +45 -0
  32. package/dist/parser/cr-parser.js.map +1 -0
  33. package/dist/parser/frontmatter.d.ts +7 -0
  34. package/dist/parser/frontmatter.d.ts.map +1 -0
  35. package/dist/parser/frontmatter.js +25 -0
  36. package/dist/parser/frontmatter.js.map +1 -0
  37. package/dist/parser/ref-extractor.d.ts +2 -0
  38. package/dist/parser/ref-extractor.d.ts.map +1 -0
  39. package/dist/parser/ref-extractor.js +17 -0
  40. package/dist/parser/ref-extractor.js.map +1 -0
  41. package/dist/parser/section-extractor.d.ts +4 -0
  42. package/dist/parser/section-extractor.d.ts.map +1 -0
  43. package/dist/parser/section-extractor.js +37 -0
  44. package/dist/parser/section-extractor.js.map +1 -0
  45. package/dist/parser/story-parser.d.ts +5 -0
  46. package/dist/parser/story-parser.d.ts.map +1 -0
  47. package/dist/parser/story-parser.js +41 -0
  48. package/dist/parser/story-parser.js.map +1 -0
  49. package/dist/prompt/prompt-generator.d.ts +3 -0
  50. package/dist/prompt/prompt-generator.d.ts.map +1 -0
  51. package/dist/prompt/prompt-generator.js +40 -0
  52. package/dist/prompt/prompt-generator.js.map +1 -0
  53. package/dist/scaffold/init.d.ts +3 -0
  54. package/dist/scaffold/init.d.ts.map +1 -0
  55. package/dist/scaffold/init.js +64 -0
  56. package/dist/scaffold/init.js.map +1 -0
  57. package/dist/scaffold/templates.d.ts +6 -0
  58. package/dist/scaffold/templates.d.ts.map +1 -0
  59. package/dist/scaffold/templates.js +164 -0
  60. package/dist/scaffold/templates.js.map +1 -0
  61. package/dist/sdd.d.ts +20 -0
  62. package/dist/sdd.d.ts.map +1 -0
  63. package/dist/sdd.js +110 -0
  64. package/dist/sdd.js.map +1 -0
  65. package/dist/types.d.ts +65 -0
  66. package/dist/types.d.ts.map +1 -0
  67. package/dist/types.js +3 -0
  68. package/dist/types.js.map +1 -0
  69. package/dist/validate/validator.d.ts +3 -0
  70. package/dist/validate/validator.d.ts.map +1 -0
  71. package/dist/validate/validator.js +44 -0
  72. package/dist/validate/validator.js.map +1 -0
  73. package/package.json +18 -0
  74. package/src/config/config-manager.ts +39 -0
  75. package/src/delta/delta-engine.ts +18 -0
  76. package/src/errors.ts +27 -0
  77. package/src/git/git.ts +113 -0
  78. package/src/index.ts +19 -0
  79. package/src/parser/cr-parser.ts +41 -0
  80. package/src/parser/frontmatter.ts +24 -0
  81. package/src/parser/ref-extractor.ts +14 -0
  82. package/src/parser/section-extractor.ts +38 -0
  83. package/src/parser/story-parser.ts +40 -0
  84. package/src/prompt/prompt-generator.ts +49 -0
  85. package/src/scaffold/init.ts +71 -0
  86. package/src/scaffold/templates.ts +166 -0
  87. package/src/sdd.ts +123 -0
  88. package/src/types.ts +76 -0
  89. package/src/validate/validator.ts +46 -0
  90. package/tests/cr.test.ts +172 -0
  91. package/tests/delta.test.ts +94 -0
  92. package/tests/integration.test.ts +132 -0
  93. package/tests/parser.test.ts +92 -0
  94. package/tests/prompt.test.ts +57 -0
  95. package/tests/validator.test.ts +54 -0
  96. package/tsconfig.json +8 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validator.js","sourceRoot":"","sources":["../../src/validate/validator.ts"],"names":[],"mappings":";;AAEA,4BA2CC;AA3CD,SAAgB,QAAQ,CAAC,KAAkB;IACzC,MAAM,MAAM,GAAsB,EAAE,CAAC;IAErC,qDAAqD;IACrD,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;IACtC,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC;IAC/E,IAAI,YAAY,EAAE,CAAC;QACjB,uCAAuC;QACvC,MAAM,SAAS,GAAG,cAAc,CAAC;QACjC,IAAI,KAA6B,CAAC;QAClC,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YAC5D,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACnC,CAAC;IACH,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,gCAAgC;QAChC,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACjC,IAAI,WAAW,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBAClD,MAAM,CAAC,IAAI,CAAC;oBACV,QAAQ,EAAE,SAAS;oBACnB,QAAQ,EAAE,IAAI,CAAC,YAAY;oBAC3B,OAAO,EAAE,sBAAsB,GAAG,sCAAsC;oBACxE,IAAI,EAAE,YAAY;iBACnB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,wCAAwC;QACxC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;YAC5B,MAAM,CAAC,IAAI,CAAC;gBACV,QAAQ,EAAE,SAAS;gBACnB,QAAQ,EAAE,IAAI,CAAC,YAAY;gBAC3B,OAAO,EAAE,gCAAgC;gBACzC,IAAI,EAAE,qBAAqB;aAC5B,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO;QACL,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,MAAM,KAAK,CAAC;QAChE,MAAM;KACP,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@applica-software-guru/sdd-core",
3
+ "version": "0.1.0",
4
+ "description": "Core library for Story Driven Development",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc"
9
+ },
10
+ "dependencies": {
11
+ "glob": "^11.0.0",
12
+ "gray-matter": "^4.0.3",
13
+ "js-yaml": "^4.1.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/js-yaml": "^4.0.9"
17
+ }
18
+ }
@@ -0,0 +1,39 @@
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import yaml from 'js-yaml';
5
+ import type { SDDConfig } from '../types.js';
6
+
7
+ export const SDD_DIR = '.sdd';
8
+ export const CONFIG_FILENAME = 'config.yaml';
9
+
10
+ export function sddDirPath(root: string): string {
11
+ return resolve(root, SDD_DIR);
12
+ }
13
+
14
+ export function configFilePath(root: string): string {
15
+ return resolve(root, SDD_DIR, CONFIG_FILENAME);
16
+ }
17
+
18
+ export function isSDDProject(root: string): boolean {
19
+ return existsSync(sddDirPath(root));
20
+ }
21
+
22
+ export async function readConfig(root: string): Promise<SDDConfig> {
23
+ const path = configFilePath(root);
24
+ if (!existsSync(path)) {
25
+ return { description: '' };
26
+ }
27
+ const content = await readFile(path, 'utf-8');
28
+ const parsed = yaml.load(content) as SDDConfig | null;
29
+ return parsed ?? { description: '' };
30
+ }
31
+
32
+ export async function writeConfig(root: string, config: SDDConfig): Promise<void> {
33
+ const dir = sddDirPath(root);
34
+ if (!existsSync(dir)) {
35
+ await mkdir(dir, { recursive: true });
36
+ }
37
+ const content = yaml.dump(config, { sortKeys: true, lineWidth: -1 });
38
+ await writeFile(configFilePath(root), content, 'utf-8');
39
+ }
@@ -0,0 +1,18 @@
1
+ import type { Delta, DeltaFile } from '../types.js';
2
+ import { getChangedFiles, getGitDiff } from '../git/git.js';
3
+
4
+ export function computeDelta(root: string, lastSyncCommit: string | null): Delta {
5
+ const changed = getChangedFiles(root, lastSyncCommit);
6
+ const diff = getGitDiff(root, lastSyncCommit);
7
+
8
+ const files: DeltaFile[] = changed.map((f) => ({
9
+ relativePath: f.path,
10
+ status: f.status,
11
+ }));
12
+
13
+ return {
14
+ hasChanges: files.length > 0,
15
+ files,
16
+ diff,
17
+ };
18
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,27 @@
1
+ export class SDDError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = 'SDDError';
5
+ }
6
+ }
7
+
8
+ export class LockFileNotFoundError extends SDDError {
9
+ constructor(path: string) {
10
+ super(`Lock file not found: ${path}`);
11
+ this.name = 'LockFileNotFoundError';
12
+ }
13
+ }
14
+
15
+ export class ParseError extends SDDError {
16
+ constructor(filePath: string, reason: string) {
17
+ super(`Failed to parse ${filePath}: ${reason}`);
18
+ this.name = 'ParseError';
19
+ }
20
+ }
21
+
22
+ export class ProjectNotInitializedError extends SDDError {
23
+ constructor(root: string) {
24
+ super(`No SDD project found at ${root}. Run 'sdd init' first.`);
25
+ this.name = 'ProjectNotInitializedError';
26
+ }
27
+ }
package/src/git/git.ts ADDED
@@ -0,0 +1,113 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+
5
+ function run(cmd: string, cwd: string): string {
6
+ return execSync(cmd, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
7
+ }
8
+
9
+ export function isGitRepo(root: string): boolean {
10
+ return existsSync(resolve(root, '.git'));
11
+ }
12
+
13
+ export function gitInit(root: string): void {
14
+ run('git init', root);
15
+ }
16
+
17
+ export function gitAddAndCommit(root: string, files: string[], message: string): void {
18
+ try {
19
+ for (const f of files) {
20
+ run(`git add -- ${f}`, root);
21
+ }
22
+ run(`git commit -m "${message}"`, root);
23
+ } catch {
24
+ // ignore — no changes to commit or no git user configured
25
+ }
26
+ }
27
+
28
+ export function getCurrentCommit(root: string): string | null {
29
+ try {
30
+ return run('git rev-parse HEAD', root) || null;
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ export function getGitDiff(root: string, fromCommit: string | null): string {
37
+ const paths = '-- product/ system/';
38
+ try {
39
+ if (!fromCommit) {
40
+ // No previous sync — show all tracked + untracked files as new
41
+ const tracked = run(`git diff HEAD ${paths}`, root);
42
+ const untracked = run('git ls-files --others --exclude-standard product/ system/', root);
43
+ const parts: string[] = [];
44
+ if (tracked) parts.push(tracked);
45
+ if (untracked) {
46
+ for (const file of untracked.split('\n').filter(Boolean)) {
47
+ parts.push(`new file: ${file}`);
48
+ }
49
+ }
50
+ return parts.join('\n') || '';
51
+ }
52
+ return run(`git diff ${fromCommit} HEAD ${paths}`, root);
53
+ } catch {
54
+ return '';
55
+ }
56
+ }
57
+
58
+ export function getFileDiff(root: string, filePath: string): string {
59
+ try {
60
+ // Show diff of file vs last committed version
61
+ const diff = run(`git diff HEAD -- ${filePath}`, root);
62
+ if (diff) return diff;
63
+ // If no unstaged changes, try diff vs previous commit
64
+ return run(`git diff HEAD~1 HEAD -- ${filePath}`, root);
65
+ } catch {
66
+ return '';
67
+ }
68
+ }
69
+
70
+ export function getGitModifiedFiles(root: string): Set<string> {
71
+ const modified = new Set<string>();
72
+ try {
73
+ // Unstaged changes
74
+ const unstaged = run('git diff --name-only -- product/ system/', root);
75
+ for (const f of unstaged.split('\n').filter(Boolean)) modified.add(f);
76
+ // Staged changes
77
+ const staged = run('git diff --cached --name-only -- product/ system/', root);
78
+ for (const f of staged.split('\n').filter(Boolean)) modified.add(f);
79
+ // Untracked files
80
+ const untracked = run('git ls-files --others --exclude-standard product/ system/', root);
81
+ for (const f of untracked.split('\n').filter(Boolean)) modified.add(f);
82
+ } catch {
83
+ // not a git repo or no commits — ignore
84
+ }
85
+ return modified;
86
+ }
87
+
88
+ export function getChangedFiles(root: string, fromCommit: string | null): Array<{ path: string; status: 'new' | 'modified' | 'deleted' }> {
89
+ try {
90
+ if (!fromCommit) {
91
+ // All files in product/ and system/ are "new"
92
+ const files = run('git ls-files product/ system/', root);
93
+ const untracked = run('git ls-files --others --exclude-standard product/ system/', root);
94
+ const all = [...files.split('\n'), ...untracked.split('\n')].filter(Boolean);
95
+ const unique = [...new Set(all)];
96
+ return unique.map((p) => ({ path: p, status: 'new' as const }));
97
+ }
98
+
99
+ const output = run(`git diff --name-status ${fromCommit} HEAD -- product/ system/`, root);
100
+ if (!output) return [];
101
+
102
+ return output.split('\n').filter(Boolean).map((line) => {
103
+ const [flag, ...pathParts] = line.split('\t');
104
+ const path = pathParts.join('\t');
105
+ const status = flag === 'A' ? 'new' as const
106
+ : flag === 'D' ? 'deleted' as const
107
+ : 'modified' as const;
108
+ return { path, status };
109
+ });
110
+ } catch {
111
+ return [];
112
+ }
113
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ export { SDD } from './sdd.js';
2
+ export type {
3
+ StoryFrontmatter,
4
+ StoryFile,
5
+ PendingItem,
6
+ Delta,
7
+ DeltaFile,
8
+ ValidationResult,
9
+ ValidationIssue,
10
+ StoryStatus,
11
+ StoryFileStatus,
12
+ SDDConfig,
13
+ ChangeRequest,
14
+ ChangeRequestFrontmatter,
15
+ ChangeRequestStatus,
16
+ } from './types.js';
17
+ export { SDDError, LockFileNotFoundError, ParseError, ProjectNotInitializedError } from './errors.js';
18
+ export type { ProjectInfo } from './scaffold/templates.js';
19
+ export { isSDDProject, readConfig } from './config/config-manager.js';
@@ -0,0 +1,41 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve, relative } from 'node:path';
3
+ import { glob } from 'glob';
4
+ import matter from 'gray-matter';
5
+ import type { ChangeRequest, ChangeRequestFrontmatter } from '../types.js';
6
+ import { ParseError } from '../errors.js';
7
+
8
+ export async function discoverCRFiles(root: string): Promise<string[]> {
9
+ const pattern = 'change-requests/*.md';
10
+ const matches = await glob(pattern, { cwd: root, absolute: true });
11
+ return matches.sort();
12
+ }
13
+
14
+ export function parseCRFile(filePath: string, content: string): { frontmatter: ChangeRequestFrontmatter; body: string } {
15
+ try {
16
+ const { data, content: body } = matter(content);
17
+ const frontmatter: ChangeRequestFrontmatter = {
18
+ title: data.title ?? '',
19
+ status: data.status ?? 'draft',
20
+ author: data.author ?? '',
21
+ 'created-at': data['created-at'] ?? '',
22
+ };
23
+ return { frontmatter, body };
24
+ } catch (err) {
25
+ throw new ParseError(filePath, (err as Error).message);
26
+ }
27
+ }
28
+
29
+ export async function parseAllCRFiles(root: string): Promise<ChangeRequest[]> {
30
+ const paths = await discoverCRFiles(root);
31
+ const results: ChangeRequest[] = [];
32
+
33
+ for (const absPath of paths) {
34
+ const content = await readFile(absPath, 'utf-8');
35
+ const relPath = relative(root, absPath);
36
+ const { frontmatter, body } = parseCRFile(relPath, content);
37
+ results.push({ relativePath: relPath, frontmatter, body });
38
+ }
39
+
40
+ return results;
41
+ }
@@ -0,0 +1,24 @@
1
+ import matter from 'gray-matter';
2
+ import type { StoryFrontmatter } from '../types.js';
3
+ import { ParseError } from '../errors.js';
4
+
5
+ export interface ParsedFile {
6
+ frontmatter: StoryFrontmatter;
7
+ body: string;
8
+ }
9
+
10
+ export function parseFrontmatter(filePath: string, content: string): ParsedFile {
11
+ try {
12
+ const { data, content: body } = matter(content);
13
+ const frontmatter: StoryFrontmatter = {
14
+ title: data.title ?? '',
15
+ status: data.status ?? 'draft',
16
+ author: data.author ?? '',
17
+ 'last-modified': data['last-modified'] ?? '',
18
+ version: data.version ?? '1.0',
19
+ };
20
+ return { frontmatter, body };
21
+ } catch (err) {
22
+ throw new ParseError(filePath, (err as Error).message);
23
+ }
24
+ }
@@ -0,0 +1,14 @@
1
+ const REF_RE = /\[\[([^\]]+)\]\]/g;
2
+
3
+ export function extractCrossRefs(body: string): string[] {
4
+ const refs: string[] = [];
5
+ let match: RegExpExecArray | null;
6
+ const re = new RegExp(REF_RE.source, REF_RE.flags);
7
+ while ((match = re.exec(body)) !== null) {
8
+ const ref = match[1].trim();
9
+ if (!refs.includes(ref)) {
10
+ refs.push(ref);
11
+ }
12
+ }
13
+ return refs;
14
+ }
@@ -0,0 +1,38 @@
1
+ import type { PendingItem } from '../types.js';
2
+
3
+ const CHECKBOX_RE = /^- \[([ xX])\] (.+)$/gm;
4
+
5
+ export function extractPendingItems(body: string): PendingItem[] {
6
+ const section = extractSection(body, 'Pending Changes');
7
+ if (!section) return [];
8
+
9
+ const items: PendingItem[] = [];
10
+ let match: RegExpExecArray | null;
11
+ const re = new RegExp(CHECKBOX_RE.source, CHECKBOX_RE.flags);
12
+ while ((match = re.exec(section)) !== null) {
13
+ items.push({
14
+ checked: match[1] !== ' ',
15
+ text: match[2].trim(),
16
+ });
17
+ }
18
+ return items;
19
+ }
20
+
21
+ export function extractAgentNotes(body: string): string | null {
22
+ const section = extractSection(body, 'Agent Notes');
23
+ return section ? section.trim() : null;
24
+ }
25
+
26
+ function extractSection(body: string, heading: string): string | null {
27
+ const headingRe = new RegExp(`^## ${escapeRegex(heading)}\\s*$`, 'm');
28
+ const match = headingRe.exec(body);
29
+ if (!match) return null;
30
+
31
+ const start = match.index + match[0].length;
32
+ const nextHeading = body.indexOf('\n## ', start);
33
+ return nextHeading === -1 ? body.slice(start) : body.slice(start, nextHeading);
34
+ }
35
+
36
+ function escapeRegex(s: string): string {
37
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
38
+ }
@@ -0,0 +1,40 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve, relative } from 'node:path';
3
+ import { glob } from 'glob';
4
+ import { createHash } from 'node:crypto';
5
+ import type { StoryFile } from '../types.js';
6
+ import { parseFrontmatter } from './frontmatter.js';
7
+ import { extractPendingItems, extractAgentNotes } from './section-extractor.js';
8
+ import { extractCrossRefs } from './ref-extractor.js';
9
+
10
+ export async function discoverStoryFiles(root: string): Promise<string[]> {
11
+ const patterns = ['product/**/*.md', 'system/**/*.md'];
12
+ const files: string[] = [];
13
+ for (const pattern of patterns) {
14
+ const matches = await glob(pattern, { cwd: root, absolute: true });
15
+ files.push(...matches);
16
+ }
17
+ return files.sort();
18
+ }
19
+
20
+ export async function parseStoryFile(root: string, absolutePath: string): Promise<StoryFile> {
21
+ const content = await readFile(absolutePath, 'utf-8');
22
+ const relPath = relative(root, absolutePath);
23
+ const { frontmatter, body } = parseFrontmatter(relPath, content);
24
+ const hash = createHash('sha256').update(content).digest('hex');
25
+
26
+ return {
27
+ relativePath: relPath,
28
+ frontmatter,
29
+ body,
30
+ pendingItems: extractPendingItems(body),
31
+ agentNotes: extractAgentNotes(body),
32
+ crossRefs: extractCrossRefs(body),
33
+ hash,
34
+ };
35
+ }
36
+
37
+ export async function parseAllStoryFiles(root: string): Promise<StoryFile[]> {
38
+ const paths = await discoverStoryFiles(root);
39
+ return Promise.all(paths.map((p) => parseStoryFile(root, p)));
40
+ }
@@ -0,0 +1,49 @@
1
+ import type { StoryFile } from '../types.js';
2
+ import { getFileDiff } from '../git/git.js';
3
+
4
+ export function generatePrompt(files: StoryFile[], root?: string): string {
5
+ const sections: string[] = [];
6
+
7
+ sections.push(
8
+ '# SDD Sync Prompt\n\nThis project uses Story Driven Development. Implement the changes described below.'
9
+ );
10
+
11
+ if (files.length === 0) {
12
+ sections.push('Nothing to do — all files are synced.');
13
+ return sections.join('\n\n');
14
+ }
15
+
16
+ const lines = [`## Files to process (${files.length})\n`];
17
+ for (const f of files) {
18
+ lines.push(`- \`${f.relativePath}\` — **${f.frontmatter.status}**`);
19
+ }
20
+ lines.push('');
21
+ lines.push('Read each file listed above before implementing.');
22
+ sections.push(lines.join('\n'));
23
+
24
+ // Show git diff for changed files so the agent knows what was modified
25
+ if (root) {
26
+ const changed = files.filter((f) => f.frontmatter.status === 'changed');
27
+ for (const f of changed) {
28
+ const diff = getFileDiff(root, f.relativePath);
29
+ if (diff) {
30
+ sections.push(`## Changes in \`${f.relativePath}\`\n\n\`\`\`diff\n${diff}\n\`\`\``);
31
+ }
32
+ }
33
+ }
34
+
35
+ const deleted = files.filter((f) => f.frontmatter.status === 'deleted');
36
+ if (deleted.length > 0) {
37
+ const delLines = ['## Files to remove\n'];
38
+ for (const f of deleted) {
39
+ delLines.push(`- \`${f.relativePath}\` — remove all related code in \`code/\``);
40
+ }
41
+ sections.push(delLines.join('\n'));
42
+ }
43
+
44
+ sections.push(
45
+ `## Instructions\n\n1. Read each file listed above\n2. For **new** files: implement what the documentation describes\n3. For **changed** files: update the code to match the updated documentation (see diff above)\n4. For **deleted** files: remove the related code from \`code/\`\n5. If a file has a \`## Agent Notes\` section, respect those constraints\n6. All code goes inside \`code/\`\n7. After implementing each file, run \`sdd mark-synced <file>\`\n8. **Immediately after mark-synced, commit**: \`git add -A && git commit -m "sdd sync: <description>"\`\n9. When done with all files, list every file you created or modified`
46
+ );
47
+
48
+ return sections.join('\n\n');
49
+ }
@@ -0,0 +1,71 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { AGENT_MD_TEMPLATE, type ProjectInfo } from './templates.js';
5
+ import { writeConfig, sddDirPath } from '../config/config-manager.js';
6
+ import { isGitRepo, gitInit } from '../git/git.js';
7
+ import type { SDDConfig } from '../types.js';
8
+
9
+ export async function initProject(root: string, info?: ProjectInfo): Promise<string[]> {
10
+ const createdFiles: string[] = [];
11
+ const sddDir = sddDirPath(root);
12
+
13
+ // Ensure git repo
14
+ if (!isGitRepo(root)) {
15
+ gitInit(root);
16
+ createdFiles.push('.git');
17
+ }
18
+
19
+ // Create .sdd directory
20
+ if (!existsSync(sddDir)) {
21
+ await mkdir(sddDir, { recursive: true });
22
+ }
23
+
24
+ // Write config
25
+ const config: SDDConfig = {
26
+ description: info?.description ?? '',
27
+ };
28
+ await writeConfig(root, config);
29
+ createdFiles.push('.sdd/config.yaml');
30
+
31
+ // Create directory structure
32
+ const dirs = ['product', 'product/features', 'system', 'code', 'change-requests'];
33
+ for (const dir of dirs) {
34
+ const absDir = resolve(root, dir);
35
+ if (!existsSync(absDir)) {
36
+ await mkdir(absDir, { recursive: true });
37
+ }
38
+ }
39
+
40
+ // Create agent instructions
41
+ const instructionsPath = resolve(root, 'INSTRUCTIONS.md');
42
+ if (!existsSync(instructionsPath)) {
43
+ await writeFile(instructionsPath, AGENT_MD_TEMPLATE, 'utf-8');
44
+ createdFiles.push('INSTRUCTIONS.md');
45
+ }
46
+
47
+ // Create agent instruction pointers
48
+ const POINTER = 'Read INSTRUCTIONS.md in the project root for all instructions.\n';
49
+ const agentFiles: Array<{ path: string; dir?: string }> = [
50
+ { path: '.claude/CLAUDE.md', dir: '.claude' },
51
+ { path: '.github/copilot-instructions.md', dir: '.github' },
52
+ { path: '.cursorrules' },
53
+ ];
54
+
55
+ for (const entry of agentFiles) {
56
+ const absPath = resolve(root, entry.path);
57
+ if (existsSync(absPath)) continue;
58
+
59
+ if (entry.dir) {
60
+ const absDir = resolve(root, entry.dir);
61
+ if (!existsSync(absDir)) {
62
+ await mkdir(absDir, { recursive: true });
63
+ }
64
+ }
65
+
66
+ await writeFile(absPath, POINTER, 'utf-8');
67
+ createdFiles.push(entry.path);
68
+ }
69
+
70
+ return createdFiles;
71
+ }