@shahmarasy/prodo 0.1.4 → 0.1.6

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 (205) hide show
  1. package/README.md +201 -97
  2. package/bin/prodo.cjs +6 -6
  3. package/dist/agents/agent-registry.d.ts +13 -0
  4. package/dist/agents/agent-registry.js +79 -0
  5. package/dist/agents/anthropic/index.d.ts +9 -0
  6. package/dist/agents/anthropic/index.js +55 -0
  7. package/dist/agents/base.d.ts +25 -0
  8. package/dist/agents/base.js +71 -0
  9. package/dist/agents/google/index.d.ts +9 -0
  10. package/dist/agents/google/index.js +53 -0
  11. package/dist/agents/mock/index.d.ts +11 -0
  12. package/dist/agents/mock/index.js +26 -0
  13. package/dist/agents/openai/index.d.ts +9 -0
  14. package/dist/agents/openai/index.js +57 -0
  15. package/dist/agents/system-prompts.d.ts +3 -0
  16. package/dist/agents/system-prompts.js +32 -0
  17. package/dist/cli/agent-command-installer.d.ts +4 -0
  18. package/dist/cli/agent-command-installer.js +148 -0
  19. package/dist/cli/agent-ids.d.ts +15 -0
  20. package/dist/cli/agent-ids.js +49 -0
  21. package/dist/cli/doctor.d.ts +1 -0
  22. package/dist/cli/doctor.js +144 -0
  23. package/dist/cli/fix-tui.d.ts +4 -0
  24. package/dist/cli/fix-tui.js +79 -0
  25. package/dist/cli/index.d.ts +9 -0
  26. package/dist/cli/index.js +467 -0
  27. package/dist/cli/init-tui.d.ts +23 -0
  28. package/dist/cli/init-tui.js +183 -0
  29. package/dist/cli/init.d.ts +12 -0
  30. package/dist/cli/init.js +335 -0
  31. package/dist/cli/normalize-interactive.d.ts +8 -0
  32. package/dist/cli/normalize-interactive.js +167 -0
  33. package/dist/cli/preset-loader.d.ts +4 -0
  34. package/dist/cli/preset-loader.js +210 -0
  35. package/dist/core/artifact-registry.d.ts +11 -0
  36. package/dist/core/artifact-registry.js +49 -0
  37. package/dist/core/artifacts.d.ts +10 -0
  38. package/dist/core/artifacts.js +892 -0
  39. package/dist/core/clean.d.ts +10 -0
  40. package/dist/core/clean.js +74 -0
  41. package/dist/core/consistency.d.ts +8 -0
  42. package/dist/core/consistency.js +328 -0
  43. package/dist/core/constants.d.ts +7 -0
  44. package/dist/core/constants.js +64 -0
  45. package/dist/core/errors.d.ts +3 -0
  46. package/dist/core/errors.js +10 -0
  47. package/dist/core/fix.d.ts +31 -0
  48. package/dist/core/fix.js +188 -0
  49. package/dist/core/hook-executor.d.ts +1 -0
  50. package/dist/core/hook-executor.js +175 -0
  51. package/dist/core/markdown.d.ts +16 -0
  52. package/dist/core/markdown.js +81 -0
  53. package/dist/core/normalize.d.ts +8 -0
  54. package/dist/core/normalize.js +125 -0
  55. package/dist/core/normalized-brief.d.ts +48 -0
  56. package/dist/core/normalized-brief.js +182 -0
  57. package/dist/core/output-index.d.ts +13 -0
  58. package/dist/core/output-index.js +55 -0
  59. package/dist/core/paths.d.ts +17 -0
  60. package/dist/core/paths.js +80 -0
  61. package/dist/core/project-config.d.ts +14 -0
  62. package/dist/core/project-config.js +69 -0
  63. package/dist/core/registry.d.ts +13 -0
  64. package/dist/core/registry.js +115 -0
  65. package/dist/core/settings.d.ts +8 -0
  66. package/dist/core/settings.js +43 -0
  67. package/dist/core/template-engine.d.ts +3 -0
  68. package/dist/core/template-engine.js +43 -0
  69. package/dist/core/template-resolver.d.ts +15 -0
  70. package/dist/core/template-resolver.js +46 -0
  71. package/dist/core/templates.d.ts +33 -0
  72. package/dist/core/templates.js +440 -0
  73. package/dist/core/terminology.d.ts +21 -0
  74. package/dist/core/terminology.js +143 -0
  75. package/dist/core/tracing.d.ts +21 -0
  76. package/dist/core/tracing.js +74 -0
  77. package/dist/core/types.d.ts +35 -0
  78. package/dist/core/types.js +5 -0
  79. package/dist/core/utils.d.ts +7 -0
  80. package/dist/core/utils.js +66 -0
  81. package/dist/core/validate.d.ts +10 -0
  82. package/dist/core/validate.js +226 -0
  83. package/dist/core/validator.d.ts +5 -0
  84. package/dist/core/validator.js +76 -0
  85. package/dist/core/version.d.ts +1 -0
  86. package/dist/core/version.js +30 -0
  87. package/dist/core/workflow-commands.d.ts +7 -0
  88. package/dist/core/workflow-commands.js +29 -0
  89. package/dist/i18n/en.json +45 -0
  90. package/dist/i18n/index.d.ts +5 -0
  91. package/dist/i18n/index.js +63 -0
  92. package/dist/i18n/tr.json +45 -0
  93. package/dist/providers/index.d.ts +2 -1
  94. package/dist/providers/index.js +20 -6
  95. package/dist/providers/mock-provider.d.ts +1 -1
  96. package/dist/providers/mock-provider.js +12 -11
  97. package/dist/providers/openai-provider.d.ts +1 -1
  98. package/dist/providers/openai-provider.js +13 -13
  99. package/dist/skill-engine/context.d.ts +7 -0
  100. package/dist/skill-engine/context.js +76 -0
  101. package/dist/skill-engine/discovery.d.ts +2 -0
  102. package/dist/skill-engine/discovery.js +52 -0
  103. package/dist/skill-engine/graph.d.ts +4 -0
  104. package/dist/skill-engine/graph.js +114 -0
  105. package/dist/skill-engine/index.d.ts +11 -0
  106. package/dist/skill-engine/index.js +49 -0
  107. package/dist/skill-engine/pipeline.d.ts +9 -0
  108. package/dist/skill-engine/pipeline.js +84 -0
  109. package/dist/skill-engine/registry.d.ts +12 -0
  110. package/dist/skill-engine/registry.js +74 -0
  111. package/dist/skill-engine/types.d.ts +66 -0
  112. package/dist/skill-engine/types.js +2 -0
  113. package/dist/skill-engine/validator.d.ts +4 -0
  114. package/dist/skill-engine/validator.js +90 -0
  115. package/dist/skills/engine.d.ts +10 -0
  116. package/dist/skills/engine.js +75 -0
  117. package/dist/skills/fix-skill.d.ts +2 -0
  118. package/dist/skills/fix-skill.js +38 -0
  119. package/dist/skills/fix.d.ts +2 -0
  120. package/dist/skills/fix.js +41 -0
  121. package/dist/skills/generate-artifact-skill.d.ts +2 -0
  122. package/dist/skills/generate-artifact-skill.js +32 -0
  123. package/dist/skills/generate-artifact.d.ts +2 -0
  124. package/dist/skills/generate-artifact.js +42 -0
  125. package/dist/skills/generate-pipeline-skill.d.ts +2 -0
  126. package/dist/skills/generate-pipeline-skill.js +45 -0
  127. package/dist/skills/normalize-skill.d.ts +2 -0
  128. package/dist/skills/normalize-skill.js +29 -0
  129. package/dist/skills/normalize.d.ts +2 -0
  130. package/dist/skills/normalize.js +29 -0
  131. package/dist/skills/register-core.d.ts +2 -0
  132. package/dist/skills/register-core.js +21 -0
  133. package/dist/skills/types.d.ts +28 -0
  134. package/dist/skills/types.js +2 -0
  135. package/dist/skills/validate-skill.d.ts +2 -0
  136. package/dist/skills/validate-skill.js +29 -0
  137. package/dist/skills/validate.d.ts +2 -0
  138. package/dist/skills/validate.js +37 -0
  139. package/package.json +72 -45
  140. package/src/agents/agent-registry.ts +93 -0
  141. package/src/agents/anthropic/index.ts +86 -0
  142. package/src/agents/anthropic/manifest.json +7 -0
  143. package/src/agents/base.ts +77 -0
  144. package/src/agents/google/index.ts +79 -0
  145. package/src/agents/google/manifest.json +7 -0
  146. package/src/agents/mock/index.ts +32 -0
  147. package/src/agents/mock/manifest.json +7 -0
  148. package/src/agents/openai/index.ts +83 -0
  149. package/src/agents/openai/manifest.json +7 -0
  150. package/src/agents/system-prompts.ts +35 -0
  151. package/src/{agent-command-installer.ts → cli/agent-command-installer.ts} +164 -164
  152. package/src/{agents.ts → cli/agent-ids.ts} +58 -58
  153. package/src/{doctor.ts → cli/doctor.ts} +157 -137
  154. package/src/cli/fix-tui.ts +111 -0
  155. package/src/{cli.ts → cli/index.ts} +463 -410
  156. package/src/{init-tui.ts → cli/init-tui.ts} +49 -37
  157. package/src/{init.ts → cli/init.ts} +399 -398
  158. package/src/cli/normalize-interactive.ts +241 -0
  159. package/src/{preset-loader.ts → cli/preset-loader.ts} +237 -237
  160. package/src/{artifact-registry.ts → core/artifact-registry.ts} +69 -69
  161. package/src/{artifacts.ts → core/artifacts.ts} +1081 -1072
  162. package/src/core/clean.ts +88 -0
  163. package/src/{consistency.ts → core/consistency.ts} +374 -303
  164. package/src/{constants.ts → core/constants.ts} +72 -72
  165. package/src/{errors.ts → core/errors.ts} +7 -7
  166. package/src/core/fix.ts +253 -0
  167. package/src/{hook-executor.ts → core/hook-executor.ts} +196 -196
  168. package/src/{markdown.ts → core/markdown.ts} +93 -73
  169. package/src/{normalize.ts → core/normalize.ts} +145 -137
  170. package/src/{normalized-brief.ts → core/normalized-brief.ts} +227 -206
  171. package/src/{output-index.ts → core/output-index.ts} +59 -59
  172. package/src/{paths.ts → core/paths.ts} +75 -71
  173. package/src/{project-config.ts → core/project-config.ts} +78 -78
  174. package/src/{registry.ts → core/registry.ts} +119 -119
  175. package/src/{settings.ts → core/settings.ts} +8 -2
  176. package/src/core/template-engine.ts +45 -0
  177. package/src/{template-resolver.ts → core/template-resolver.ts} +54 -54
  178. package/src/{templates.ts → core/templates.ts} +452 -452
  179. package/src/core/terminology.ts +177 -0
  180. package/src/core/tracing.ts +110 -0
  181. package/src/{types.ts → core/types.ts} +46 -46
  182. package/src/{utils.ts → core/utils.ts} +64 -64
  183. package/src/{validate.ts → core/validate.ts} +252 -246
  184. package/src/{validator.ts → core/validator.ts} +92 -92
  185. package/src/{version.ts → core/version.ts} +24 -24
  186. package/src/{workflow-commands.ts → core/workflow-commands.ts} +32 -32
  187. package/src/i18n/en.json +45 -0
  188. package/src/i18n/index.ts +58 -0
  189. package/src/i18n/tr.json +45 -0
  190. package/src/providers/index.ts +29 -12
  191. package/src/providers/mock-provider.ts +200 -199
  192. package/src/providers/openai-provider.ts +88 -88
  193. package/src/skill-engine/context.ts +90 -0
  194. package/src/skill-engine/discovery.ts +57 -0
  195. package/src/skill-engine/graph.ts +136 -0
  196. package/src/skill-engine/index.ts +55 -0
  197. package/src/skill-engine/pipeline.ts +112 -0
  198. package/src/skill-engine/registry.ts +75 -0
  199. package/src/skill-engine/types.ts +81 -0
  200. package/src/skill-engine/validator.ts +135 -0
  201. package/src/skills/fix.ts +45 -0
  202. package/src/skills/generate-artifact.ts +48 -0
  203. package/src/skills/normalize.ts +32 -0
  204. package/src/skills/register-core.ts +27 -0
  205. package/src/skills/validate.ts +40 -0
@@ -0,0 +1,177 @@
1
+ import { parseMarkdownSections } from "./markdown";
2
+ import type { NormalizedBrief } from "./normalized-brief";
3
+ import type { ArtifactDoc, ArtifactType, ValidationIssue } from "./types";
4
+
5
+ type LoadedArtifact = {
6
+ type: ArtifactType;
7
+ file: string;
8
+ doc: ArtifactDoc;
9
+ };
10
+
11
+ export type TermEntry = {
12
+ term: string;
13
+ normalizedTerm: string;
14
+ definition?: string;
15
+ sources: Array<{ artifactType: ArtifactType | "brief"; file: string; heading?: string }>;
16
+ };
17
+
18
+ export type TermMap = Map<string, TermEntry>;
19
+
20
+ function normalizeTerm(term: string): string {
21
+ return term
22
+ .toLowerCase()
23
+ .replace(/[^a-z0-9\u00c0-\u024f\s]/g, "")
24
+ .replace(/\s+/g, " ")
25
+ .trim();
26
+ }
27
+
28
+ function extractBoldTerms(body: string): string[] {
29
+ const matches = body.match(/\*\*([^*]+)\*\*/g) ?? [];
30
+ return matches
31
+ .map((m) => m.replace(/\*\*/g, "").trim())
32
+ .filter((t) => t.length > 2 && t.length < 60);
33
+ }
34
+
35
+ export function buildTermMap(
36
+ normalizedBrief: NormalizedBrief,
37
+ loadedArtifacts: LoadedArtifact[]
38
+ ): TermMap {
39
+ const termMap: TermMap = new Map();
40
+
41
+ function addTerm(term: string, source: TermEntry["sources"][0], definition?: string): void {
42
+ const normalized = normalizeTerm(term);
43
+ if (!normalized || normalized.length < 2) return;
44
+
45
+ const existing = termMap.get(normalized);
46
+ if (existing) {
47
+ existing.sources.push(source);
48
+ if (definition && !existing.definition) {
49
+ existing.definition = definition;
50
+ }
51
+ } else {
52
+ termMap.set(normalized, {
53
+ term,
54
+ normalizedTerm: normalized,
55
+ definition,
56
+ sources: [source]
57
+ });
58
+ }
59
+ }
60
+
61
+ addTerm(normalizedBrief.product_name, { artifactType: "brief", file: "brief.md" });
62
+
63
+ for (const goal of normalizedBrief.goals) {
64
+ addTerm(goal, { artifactType: "brief", file: "brief.md" }, goal);
65
+ }
66
+ for (const feature of normalizedBrief.core_features) {
67
+ addTerm(feature, { artifactType: "brief", file: "brief.md" }, feature);
68
+ }
69
+ for (const constraint of normalizedBrief.constraints) {
70
+ addTerm(constraint, { artifactType: "brief", file: "brief.md" }, constraint);
71
+ }
72
+
73
+ for (const contract of normalizedBrief.contracts.goals) {
74
+ addTerm(contract.text, { artifactType: "brief", file: "brief.md" }, contract.text);
75
+ }
76
+ for (const contract of normalizedBrief.contracts.core_features) {
77
+ addTerm(contract.text, { artifactType: "brief", file: "brief.md" }, contract.text);
78
+ }
79
+ for (const contract of normalizedBrief.contracts.constraints) {
80
+ addTerm(contract.text, { artifactType: "brief", file: "brief.md" }, contract.text);
81
+ }
82
+
83
+ for (const artifact of loadedArtifacts) {
84
+ const sections = parseMarkdownSections(artifact.doc.body);
85
+ for (const section of sections) {
86
+ if (section.level === 2) {
87
+ addTerm(section.heading, {
88
+ artifactType: artifact.type,
89
+ file: artifact.file,
90
+ heading: section.heading
91
+ });
92
+ }
93
+ }
94
+
95
+ const boldTerms = extractBoldTerms(artifact.doc.body);
96
+ for (const boldTerm of boldTerms) {
97
+ addTerm(boldTerm, { artifactType: artifact.type, file: artifact.file });
98
+ }
99
+ }
100
+
101
+ return termMap;
102
+ }
103
+
104
+ function levenshtein(a: string, b: string): number {
105
+ const m = a.length;
106
+ const n = b.length;
107
+ if (m === 0) return n;
108
+ if (n === 0) return m;
109
+
110
+ const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0) as number[]);
111
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
112
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
113
+
114
+ for (let i = 1; i <= m; i++) {
115
+ for (let j = 1; j <= n; j++) {
116
+ dp[i][j] = a[i - 1] === b[j - 1]
117
+ ? dp[i - 1][j - 1]
118
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
119
+ }
120
+ }
121
+ return dp[m][n];
122
+ }
123
+
124
+ function sharesStem(a: string, b: string): boolean {
125
+ const wordsA = a.split(/\s+/).filter((w) => w.length > 3);
126
+ const wordsB = b.split(/\s+/).filter((w) => w.length > 3);
127
+ for (const wa of wordsA) {
128
+ for (const wb of wordsB) {
129
+ const shorter = Math.min(wa.length, wb.length);
130
+ const prefix = Math.min(4, shorter);
131
+ if (wa.slice(0, prefix) === wb.slice(0, prefix) && wa !== wb) {
132
+ return true;
133
+ }
134
+ }
135
+ }
136
+ return false;
137
+ }
138
+
139
+ export function checkTermReconciliation(termMap: TermMap): ValidationIssue[] {
140
+ const issues: ValidationIssue[] = [];
141
+ const entries = Array.from(termMap.values()).filter(
142
+ (e) => e.sources.length > 0 && e.normalizedTerm.length > 3
143
+ );
144
+
145
+ for (let i = 0; i < entries.length; i++) {
146
+ for (let j = i + 1; j < entries.length; j++) {
147
+ const a = entries[i];
148
+ const b = entries[j];
149
+
150
+ if (a.normalizedTerm === b.normalizedTerm) continue;
151
+
152
+ const dist = levenshtein(a.normalizedTerm, b.normalizedTerm);
153
+ const maxLen = Math.max(a.normalizedTerm.length, b.normalizedTerm.length);
154
+ const ratio = dist / maxLen;
155
+
156
+ const isSimilar = (ratio < 0.25 && maxLen > 5) || sharesStem(a.normalizedTerm, b.normalizedTerm);
157
+
158
+ if (isSimilar) {
159
+ const aTypes = [...new Set(a.sources.map((s) => s.artifactType))];
160
+ const bTypes = [...new Set(b.sources.map((s) => s.artifactType))];
161
+ const crossDoc = aTypes.some((t) => !bTypes.includes(t)) || bTypes.some((t) => !aTypes.includes(t));
162
+
163
+ if (crossDoc) {
164
+ issues.push({
165
+ level: "warning",
166
+ code: "term_inconsistency",
167
+ check: "terminology",
168
+ message: `Similar terms used across documents: "${a.term}" (${aTypes.join(", ")}) vs "${b.term}" (${bTypes.join(", ")})`,
169
+ suggestion: "Standardize terminology across all artifacts to avoid ambiguity."
170
+ });
171
+ }
172
+ }
173
+ }
174
+ }
175
+
176
+ return issues;
177
+ }
@@ -0,0 +1,110 @@
1
+ import { taggedLinesByContract } from "./markdown";
2
+ import { parseNormalizedBriefOrThrow, type NormalizedBrief, type BriefContractItem } from "./normalized-brief";
3
+ import type { ArtifactDoc, ArtifactType, ValidationIssue } from "./types";
4
+
5
+ type LoadedArtifact = {
6
+ type: ArtifactType;
7
+ file: string;
8
+ doc: ArtifactDoc;
9
+ };
10
+
11
+ export type TraceEntry = {
12
+ contractId: string;
13
+ contractText: string;
14
+ category: "goals" | "core_features" | "constraints";
15
+ references: Array<{
16
+ artifactType: ArtifactType;
17
+ file: string;
18
+ line: string;
19
+ }>;
20
+ };
21
+
22
+ export type TraceMap = Map<string, TraceEntry>;
23
+
24
+ export function buildTraceMap(
25
+ normalizedBrief: NormalizedBrief,
26
+ loadedArtifacts: LoadedArtifact[]
27
+ ): TraceMap {
28
+ const traceMap: TraceMap = new Map();
29
+
30
+ function registerContracts(
31
+ items: BriefContractItem[],
32
+ category: TraceEntry["category"]
33
+ ): void {
34
+ for (const item of items) {
35
+ traceMap.set(item.id, {
36
+ contractId: item.id,
37
+ contractText: item.text,
38
+ category,
39
+ references: []
40
+ });
41
+ }
42
+ }
43
+
44
+ registerContracts(normalizedBrief.contracts.goals, "goals");
45
+ registerContracts(normalizedBrief.contracts.core_features, "core_features");
46
+ registerContracts(normalizedBrief.contracts.constraints, "constraints");
47
+
48
+ for (const artifact of loadedArtifacts) {
49
+ const tagged = taggedLinesByContract(artifact.doc.body);
50
+ for (const { contractId, line } of tagged) {
51
+ const entry = traceMap.get(contractId);
52
+ if (entry) {
53
+ entry.references.push({
54
+ artifactType: artifact.type,
55
+ file: artifact.file,
56
+ line
57
+ });
58
+ }
59
+ }
60
+ }
61
+
62
+ return traceMap;
63
+ }
64
+
65
+ export function checkRequirementCompleteness(
66
+ traceMap: TraceMap,
67
+ normalizedBrief: NormalizedBrief,
68
+ presentArtifactTypes: ArtifactType[]
69
+ ): ValidationIssue[] {
70
+ const issues: ValidationIssue[] = [];
71
+
72
+ if (presentArtifactTypes.length === 0) return issues;
73
+
74
+ for (const [contractId, entry] of traceMap) {
75
+ if (entry.references.length === 0) {
76
+ issues.push({
77
+ level: "warning",
78
+ code: "untraced_requirement",
79
+ check: "tracing",
80
+ message: `Contract ${contractId} ("${truncate(entry.contractText, 60)}") is not referenced in any generated artifact.`,
81
+ suggestion: `Ensure [${contractId}] tag appears in at least one artifact body.`
82
+ });
83
+ }
84
+ }
85
+
86
+ const hasPrd = presentArtifactTypes.includes("prd");
87
+ if (hasPrd) {
88
+ for (const [contractId, entry] of traceMap) {
89
+ if (entry.category === "goals") {
90
+ const inPrd = entry.references.some((r) => r.artifactType === "prd");
91
+ if (!inPrd && entry.references.length > 0) {
92
+ issues.push({
93
+ level: "warning",
94
+ code: "goal_missing_in_prd",
95
+ check: "tracing",
96
+ message: `Goal ${contractId} appears in downstream artifacts but is not traced in the PRD.`,
97
+ suggestion: `Add [${contractId}] tag to the PRD to maintain traceability.`
98
+ });
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ return issues;
105
+ }
106
+
107
+ function truncate(text: string, maxLength: number): string {
108
+ if (text.length <= maxLength) return text;
109
+ return `${text.slice(0, maxLength - 3)}...`;
110
+ }
@@ -1,46 +1,46 @@
1
- export const CORE_ARTIFACT_TYPES = ["prd", "workflow", "wireframe", "stories", "techspec"] as const;
2
- export const ARTIFACT_TYPES = CORE_ARTIFACT_TYPES;
3
-
4
- export type CoreArtifactType = (typeof CORE_ARTIFACT_TYPES)[number];
5
- export type ArtifactType = CoreArtifactType | string;
6
-
7
- export type ArtifactDoc = {
8
- frontmatter: Record<string, unknown>;
9
- body: string;
10
- };
11
-
12
- export type ContractCoverage = {
13
- goals: string[];
14
- core_features: string[];
15
- constraints: string[];
16
- };
17
-
18
- export type ValidationIssue = {
19
- level: "error" | "warning";
20
- code: string;
21
- check?: "schema" | "tag_coverage" | "semantic_consistency" | "contract_relevance";
22
- artifactType?: ArtifactType;
23
- file?: string;
24
- field?: string;
25
- message: string;
26
- suggestion?: string;
27
- };
28
-
29
- export type GenerateResult = {
30
- body: string;
31
- frontmatter?: Record<string, unknown>;
32
- };
33
-
34
- export type ProviderSchemaHint = {
35
- artifactType: ArtifactType | "normalize" | "semantic_consistency" | "contract_relevance";
36
- requiredHeadings: string[];
37
- requiredContracts: Array<keyof ContractCoverage>;
38
- };
39
-
40
- export type LLMProvider = {
41
- generate: (
42
- prompt: string,
43
- inputContext: Record<string, unknown>,
44
- schemaHint: ProviderSchemaHint
45
- ) => Promise<GenerateResult>;
46
- };
1
+ export const CORE_ARTIFACT_TYPES = ["prd", "workflow", "wireframe", "stories", "techspec"] as const;
2
+ export const ARTIFACT_TYPES = CORE_ARTIFACT_TYPES;
3
+
4
+ export type CoreArtifactType = (typeof CORE_ARTIFACT_TYPES)[number];
5
+ export type ArtifactType = CoreArtifactType | string;
6
+
7
+ export type ArtifactDoc = {
8
+ frontmatter: Record<string, unknown>;
9
+ body: string;
10
+ };
11
+
12
+ export type ContractCoverage = {
13
+ goals: string[];
14
+ core_features: string[];
15
+ constraints: string[];
16
+ };
17
+
18
+ export type ValidationIssue = {
19
+ level: "error" | "warning";
20
+ code: string;
21
+ check?: "schema" | "tag_coverage" | "semantic_consistency" | "contract_relevance" | "terminology" | "tracing" | "cross_reference";
22
+ artifactType?: ArtifactType;
23
+ file?: string;
24
+ field?: string;
25
+ message: string;
26
+ suggestion?: string;
27
+ };
28
+
29
+ export type GenerateResult = {
30
+ body: string;
31
+ frontmatter?: Record<string, unknown>;
32
+ };
33
+
34
+ export type ProviderSchemaHint = {
35
+ artifactType: ArtifactType | "normalize" | "semantic_consistency" | "contract_relevance";
36
+ requiredHeadings: string[];
37
+ requiredContracts: Array<keyof ContractCoverage>;
38
+ };
39
+
40
+ export type LLMProvider = {
41
+ generate: (
42
+ prompt: string,
43
+ inputContext: Record<string, unknown>,
44
+ schemaHint: ProviderSchemaHint
45
+ ) => Promise<GenerateResult>;
46
+ };
@@ -1,64 +1,64 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
-
4
- export async function ensureDir(dirPath: string): Promise<void> {
5
- await fs.mkdir(dirPath, { recursive: true });
6
- }
7
-
8
- export async function fileExists(filePath: string): Promise<boolean> {
9
- try {
10
- await fs.access(filePath);
11
- return true;
12
- } catch {
13
- return false;
14
- }
15
- }
16
-
17
- export async function readJsonFile<T>(filePath: string): Promise<T> {
18
- const raw = await fs.readFile(filePath, "utf8");
19
- return JSON.parse(raw) as T;
20
- }
21
-
22
- export function timestampSlug(date = new Date()): string {
23
- return date.toISOString().replace(/[:.]/g, "-");
24
- }
25
-
26
- function pad2(value: number): string {
27
- return String(value).padStart(2, "0");
28
- }
29
-
30
- export function artifactFileStamp(date = new Date()): string {
31
- const year = date.getFullYear();
32
- const month = pad2(date.getMonth() + 1);
33
- const day = pad2(date.getDate());
34
- const hour = pad2(date.getHours());
35
- const minute = pad2(date.getMinutes());
36
- const second = pad2(date.getSeconds());
37
- return `${year}${month}${day}-${hour}${minute}${second}`;
38
- }
39
-
40
- export async function listFilesSortedByMtime(dirPath: string): Promise<string[]> {
41
- const exists = await fileExists(dirPath);
42
- if (!exists) return [];
43
-
44
- const entries = await fs.readdir(dirPath);
45
- const withStats = await Promise.all(
46
- entries.map(async (name) => {
47
- const fullPath = path.join(dirPath, name);
48
- const stat = await fs.stat(fullPath);
49
- return { fullPath, mtimeMs: stat.mtimeMs, isFile: stat.isFile() };
50
- })
51
- );
52
-
53
- return withStats
54
- .filter((entry) => entry.isFile)
55
- .sort((a, b) => b.mtimeMs - a.mtimeMs)
56
- .map((entry) => entry.fullPath);
57
- }
58
-
59
- export function isPathInside(parentDir: string, candidatePath: string): boolean {
60
- const parent = path.resolve(parentDir);
61
- const child = path.resolve(candidatePath);
62
- const relative = path.relative(parent, child);
63
- return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
64
- }
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export async function ensureDir(dirPath: string): Promise<void> {
5
+ await fs.mkdir(dirPath, { recursive: true });
6
+ }
7
+
8
+ export async function fileExists(filePath: string): Promise<boolean> {
9
+ try {
10
+ await fs.access(filePath);
11
+ return true;
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ export async function readJsonFile<T>(filePath: string): Promise<T> {
18
+ const raw = await fs.readFile(filePath, "utf8");
19
+ return JSON.parse(raw) as T;
20
+ }
21
+
22
+ export function timestampSlug(date = new Date()): string {
23
+ return date.toISOString().replace(/[:.]/g, "-");
24
+ }
25
+
26
+ function pad2(value: number): string {
27
+ return String(value).padStart(2, "0");
28
+ }
29
+
30
+ export function artifactFileStamp(date = new Date()): string {
31
+ const year = date.getFullYear();
32
+ const month = pad2(date.getMonth() + 1);
33
+ const day = pad2(date.getDate());
34
+ const hour = pad2(date.getHours());
35
+ const minute = pad2(date.getMinutes());
36
+ const second = pad2(date.getSeconds());
37
+ return `${year}${month}${day}-${hour}${minute}${second}`;
38
+ }
39
+
40
+ export async function listFilesSortedByMtime(dirPath: string): Promise<string[]> {
41
+ const exists = await fileExists(dirPath);
42
+ if (!exists) return [];
43
+
44
+ const entries = await fs.readdir(dirPath);
45
+ const withStats = await Promise.all(
46
+ entries.map(async (name) => {
47
+ const fullPath = path.join(dirPath, name);
48
+ const stat = await fs.stat(fullPath);
49
+ return { fullPath, mtimeMs: stat.mtimeMs, isFile: stat.isFile() };
50
+ })
51
+ );
52
+
53
+ return withStats
54
+ .filter((entry) => entry.isFile)
55
+ .sort((a, b) => b.mtimeMs - a.mtimeMs)
56
+ .map((entry) => entry.fullPath);
57
+ }
58
+
59
+ export function isPathInside(parentDir: string, candidatePath: string): boolean {
60
+ const parent = path.resolve(parentDir);
61
+ const child = path.resolve(candidatePath);
62
+ const relative = path.relative(parent, child);
63
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
64
+ }