infinity-harness 2.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/CHANGELOG.md +114 -0
- package/LICENSE +21 -0
- package/README.md +266 -0
- package/extensions/infinity-harness/index.ts +870 -0
- package/harness/docs/ARCHITECTURE.md +159 -0
- package/harness/docs/CONSTRAINTS.md +19 -0
- package/harness/docs/DECISIONS.md +107 -0
- package/harness/docs/DOMAIN.md +13 -0
- package/harness/docs/agents/evaluator.md +14 -0
- package/harness/docs/agents/generator.md +13 -0
- package/harness/docs/agents/planner.md +13 -0
- package/harness/docs/agents/simplifier.md +13 -0
- package/harness/docs/api-patterns.md +23 -0
- package/harness/docs/phases/build.md +47 -0
- package/harness/docs/phases/define.md +58 -0
- package/harness/docs/phases/plan.md +50 -0
- package/harness/docs/phases/review.md +47 -0
- package/harness/docs/phases/ship.md +43 -0
- package/harness/docs/phases/simplify.md +45 -0
- package/harness/docs/phases/verify.md +46 -0
- package/harness/model-router.json +28 -0
- package/harness/skills/README.md +60 -0
- package/harness/skills/auth-security.md +56 -0
- package/harness/skills/building-mcp-servers.md +70 -0
- package/harness/skills/building-tools.md +60 -0
- package/harness/skills/capability-acquisition.md +72 -0
- package/harness/skills/cli-design.md +55 -0
- package/harness/skills/code-review.md +57 -0
- package/harness/skills/codebase-design.md +70 -0
- package/harness/skills/concurrency-async.md +61 -0
- package/harness/skills/config-and-secrets.md +52 -0
- package/harness/skills/context-hygiene.md +51 -0
- package/harness/skills/databases.md +63 -0
- package/harness/skills/diagnosing-bugs.md +84 -0
- package/harness/skills/domain-modeling.md +65 -0
- package/harness/skills/error-handling-logging.md +56 -0
- package/harness/skills/frontend-ui.md +56 -0
- package/harness/skills/grilling.md +48 -0
- package/harness/skills/http-apis.md +60 -0
- package/harness/skills/performance.md +53 -0
- package/harness/skills/pi-todo-adapted.md +41 -0
- package/harness/skills/planning-tasks.md +86 -0
- package/harness/skills/prototype.md +39 -0
- package/harness/skills/research.md +32 -0
- package/harness/skills/resolving-merge-conflicts.md +30 -0
- package/harness/skills/scope-discipline.md +49 -0
- package/harness/skills/self-review.md +45 -0
- package/harness/skills/stuck-protocol.md +51 -0
- package/harness/skills/tdd.md +80 -0
- package/harness/skills/testing-infra.md +57 -0
- package/harness/skills/writing-skills.md +60 -0
- package/package.json +61 -0
- package/src/core/brief.ts +242 -0
- package/src/core/config.ts +265 -0
- package/src/core/exec.ts +130 -0
- package/src/core/featureList.ts +286 -0
- package/src/core/fsx.ts +119 -0
- package/src/core/gates.ts +444 -0
- package/src/core/lock.ts +192 -0
- package/src/core/paths.ts +95 -0
- package/src/core/phases.ts +143 -0
- package/src/core/settings.ts +445 -0
- package/src/core/types.ts +245 -0
- package/src/goalLoop.ts +628 -0
- package/src/goalSpec.ts +679 -0
- package/src/goalState.ts +338 -0
- package/src/loop.ts +355 -0
- package/src/modelRouter.ts +184 -0
- package/src/remote.ts +244 -0
- package/src/replan.ts +300 -0
- package/src/review.ts +53 -0
- package/src/rework.ts +274 -0
- package/src/taskList.ts +355 -0
- package/src/ui/config.ts +286 -0
- package/src/ui/dashboard.ts +1066 -0
- package/src/ui/theme.ts +317 -0
- package/src/ui/widget.ts +370 -0
- package/src/unstuck.ts +214 -0
- package/src/worker.ts +351 -0
- package/types/proper-lockfile.d.ts +19 -0
package/src/goalSpec.ts
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* goalSpec — goal specification helpers ported from pi-long-task (MIT)
|
|
3
|
+
*
|
|
4
|
+
* Domain types + validate + markdown for harness/goals/GOAL_SPEC.json.
|
|
5
|
+
* Pure helpers, no IO. Persists via src/goalState.ts (canonical path
|
|
6
|
+
* harness/goals/GOAL_SPEC.json) re-pointed from pi-long-task
|
|
7
|
+
* tmp/pi-goal-task/<runId>/GOAL_SPEC.json.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const GOAL_SPEC_SCHEMA_VERSION = 1 as const;
|
|
11
|
+
|
|
12
|
+
export type GoalRequirementPriority = "must" | "should" | "could" | "wont";
|
|
13
|
+
export type GoalSpecTraceabilitySource = "user_goal" | "discovery_consolidation" | "manual";
|
|
14
|
+
|
|
15
|
+
export interface GoalSpecSourceArtifact {
|
|
16
|
+
label: string;
|
|
17
|
+
path?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface GoalSpecTraceability {
|
|
22
|
+
originalUserGoal: string;
|
|
23
|
+
goalRunId: string;
|
|
24
|
+
source: GoalSpecTraceabilitySource;
|
|
25
|
+
sourceArtifacts: GoalSpecSourceArtifact[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface GoalRequirement {
|
|
29
|
+
id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
description: string;
|
|
32
|
+
priority: GoalRequirementPriority;
|
|
33
|
+
acceptanceCriterionIds: string[];
|
|
34
|
+
milestoneIds: string[];
|
|
35
|
+
source?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface GoalScopedRequirements {
|
|
39
|
+
inScope: GoalRequirement[];
|
|
40
|
+
outOfScope: GoalRequirement[];
|
|
41
|
+
assumptions: string[];
|
|
42
|
+
openQuestions: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface GoalMilestone {
|
|
46
|
+
id: string;
|
|
47
|
+
title: string;
|
|
48
|
+
description: string;
|
|
49
|
+
requirementIds: string[];
|
|
50
|
+
acceptanceCriterionIds: string[];
|
|
51
|
+
doneWhen: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface GoalAcceptanceCriterion {
|
|
55
|
+
id: string;
|
|
56
|
+
description: string;
|
|
57
|
+
requirementIds: string[];
|
|
58
|
+
verificationGateIds: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface GoalVerificationGate {
|
|
62
|
+
id: string;
|
|
63
|
+
title: string;
|
|
64
|
+
description: string;
|
|
65
|
+
required: boolean;
|
|
66
|
+
command?: string;
|
|
67
|
+
successCriteria: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface GoalConstraint {
|
|
71
|
+
id: string;
|
|
72
|
+
title: string;
|
|
73
|
+
description: string;
|
|
74
|
+
rationale?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface GoalDesignConstraints {
|
|
78
|
+
uxPrinciples: string[];
|
|
79
|
+
uiRequirements: string[];
|
|
80
|
+
accessibility: string[];
|
|
81
|
+
architecturalConstraints: string[];
|
|
82
|
+
constraints: GoalConstraint[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface GoalProductConstraints {
|
|
86
|
+
targetUsers: string[];
|
|
87
|
+
platforms: string[];
|
|
88
|
+
businessRules: string[];
|
|
89
|
+
compliance: string[];
|
|
90
|
+
dependencies: string[];
|
|
91
|
+
risks: string[];
|
|
92
|
+
constraints: GoalConstraint[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface GoalMarketingGrowthContext {
|
|
96
|
+
targetSegments: string[];
|
|
97
|
+
positioning: string[];
|
|
98
|
+
acquisitionChannels: string[];
|
|
99
|
+
growthMetrics: string[];
|
|
100
|
+
launchConsiderations: string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface GoalDefinitionOfDone {
|
|
104
|
+
summary: string;
|
|
105
|
+
requirementIds: string[];
|
|
106
|
+
acceptanceCriterionIds: string[];
|
|
107
|
+
verificationGateIds: string[];
|
|
108
|
+
requiredArtifacts: string[];
|
|
109
|
+
notes: string[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type GoalDiscoveryPlanningRole =
|
|
113
|
+
| "product_owner"
|
|
114
|
+
| "project_manager"
|
|
115
|
+
| "software_architect_tech_lead"
|
|
116
|
+
| "ux_ui_designer"
|
|
117
|
+
| "qa_reviewer"
|
|
118
|
+
| "marketing_growth";
|
|
119
|
+
|
|
120
|
+
export interface GoalDiscoveryRoleOutput {
|
|
121
|
+
role: GoalDiscoveryPlanningRole;
|
|
122
|
+
title: string;
|
|
123
|
+
objective: string;
|
|
124
|
+
findings: string[];
|
|
125
|
+
decisions: string[];
|
|
126
|
+
risks: string[];
|
|
127
|
+
requirementIds: string[];
|
|
128
|
+
milestoneIds: string[];
|
|
129
|
+
acceptanceCriterionIds: string[];
|
|
130
|
+
verificationGateIds: string[];
|
|
131
|
+
constraintIds: string[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface GoalDiscoveryConsolidation {
|
|
135
|
+
approach: string;
|
|
136
|
+
roleOutputs: GoalDiscoveryRoleOutput[];
|
|
137
|
+
consolidationNotes: string[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface GoalSpecification {
|
|
141
|
+
schemaVersion: typeof GOAL_SPEC_SCHEMA_VERSION;
|
|
142
|
+
goalRunId: string;
|
|
143
|
+
originalGoal: string;
|
|
144
|
+
summary: string;
|
|
145
|
+
createdAt: string;
|
|
146
|
+
updatedAt: string;
|
|
147
|
+
traceability: GoalSpecTraceability;
|
|
148
|
+
discovery?: GoalDiscoveryConsolidation;
|
|
149
|
+
scopedRequirements: GoalScopedRequirements;
|
|
150
|
+
milestones: GoalMilestone[];
|
|
151
|
+
acceptanceCriteria: GoalAcceptanceCriterion[];
|
|
152
|
+
verificationGates: GoalVerificationGate[];
|
|
153
|
+
designConstraints: GoalDesignConstraints;
|
|
154
|
+
productConstraints: GoalProductConstraints;
|
|
155
|
+
marketingGrowthContext?: GoalMarketingGrowthContext;
|
|
156
|
+
definitionOfDone: GoalDefinitionOfDone;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface CreateGoalSpecificationOptions {
|
|
160
|
+
goalRunId: string;
|
|
161
|
+
originalGoal: string;
|
|
162
|
+
summary?: string;
|
|
163
|
+
now?: () => Date;
|
|
164
|
+
traceability?: Partial<Omit<GoalSpecTraceability, "originalUserGoal" | "goalRunId">>;
|
|
165
|
+
scopedRequirements?: GoalScopedRequirements;
|
|
166
|
+
milestones?: GoalMilestone[];
|
|
167
|
+
acceptanceCriteria?: GoalAcceptanceCriterion[];
|
|
168
|
+
verificationGates?: GoalVerificationGate[];
|
|
169
|
+
designConstraints?: GoalDesignConstraints;
|
|
170
|
+
productConstraints?: GoalProductConstraints;
|
|
171
|
+
discovery?: GoalDiscoveryConsolidation;
|
|
172
|
+
marketingGrowthContext?: GoalMarketingGrowthContext;
|
|
173
|
+
definitionOfDone?: GoalDefinitionOfDone;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export class GoalSpecificationError extends Error {
|
|
177
|
+
constructor(message: string) {
|
|
178
|
+
super(message);
|
|
179
|
+
this.name = "GoalSpecificationError";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function createGoalSpecification(options: CreateGoalSpecificationOptions): GoalSpecification {
|
|
184
|
+
const originalGoal = options.originalGoal.trim();
|
|
185
|
+
if (!originalGoal) {
|
|
186
|
+
throw new GoalSpecificationError("Goal specification requires a non-empty originalGoal.");
|
|
187
|
+
}
|
|
188
|
+
if (!options.goalRunId.trim()) {
|
|
189
|
+
throw new GoalSpecificationError("Goal specification requires a non-empty goalRunId.");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const timestamp = (options.now?.() ?? new Date()).toISOString();
|
|
193
|
+
return validateGoalSpecification({
|
|
194
|
+
schemaVersion: GOAL_SPEC_SCHEMA_VERSION,
|
|
195
|
+
goalRunId: options.goalRunId,
|
|
196
|
+
originalGoal,
|
|
197
|
+
summary: options.summary?.trim() || originalGoal,
|
|
198
|
+
createdAt: timestamp,
|
|
199
|
+
updatedAt: timestamp,
|
|
200
|
+
traceability: {
|
|
201
|
+
originalUserGoal: originalGoal,
|
|
202
|
+
goalRunId: options.goalRunId,
|
|
203
|
+
source: options.traceability?.source ?? "user_goal",
|
|
204
|
+
sourceArtifacts: options.traceability?.sourceArtifacts ?? [],
|
|
205
|
+
},
|
|
206
|
+
...(options.discovery ? { discovery: options.discovery } : {}),
|
|
207
|
+
scopedRequirements: options.scopedRequirements ?? emptyScopedRequirements(),
|
|
208
|
+
milestones: options.milestones ?? [],
|
|
209
|
+
acceptanceCriteria: options.acceptanceCriteria ?? [],
|
|
210
|
+
verificationGates: options.verificationGates ?? [],
|
|
211
|
+
designConstraints: options.designConstraints ?? emptyDesignConstraints(),
|
|
212
|
+
productConstraints: options.productConstraints ?? emptyProductConstraints(),
|
|
213
|
+
...(options.marketingGrowthContext ? { marketingGrowthContext: options.marketingGrowthContext } : {}),
|
|
214
|
+
definitionOfDone:
|
|
215
|
+
options.definitionOfDone ??
|
|
216
|
+
emptyDefinitionOfDone(
|
|
217
|
+
"The goal is complete when all scoped requirements, acceptance criteria, and required verification gates are satisfied.",
|
|
218
|
+
),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function validateGoalSpecification(value: unknown): GoalSpecification {
|
|
223
|
+
const spec = requireObject(value, "goal specification");
|
|
224
|
+
if (spec.schemaVersion !== GOAL_SPEC_SCHEMA_VERSION) {
|
|
225
|
+
throw new GoalSpecificationError(`Unsupported goal specification schemaVersion: ${String(spec.schemaVersion)}.`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const goalRunId = requireString(spec, "goalRunId", "goal specification");
|
|
229
|
+
const originalGoal = requireString(spec, "originalGoal", "goal specification");
|
|
230
|
+
requireString(spec, "summary", "goal specification");
|
|
231
|
+
requireString(spec, "createdAt", "goal specification");
|
|
232
|
+
requireString(spec, "updatedAt", "goal specification");
|
|
233
|
+
|
|
234
|
+
validateTraceability(requireObjectField(spec, "traceability", "goal specification"), goalRunId, originalGoal);
|
|
235
|
+
if (spec.discovery !== undefined) {
|
|
236
|
+
validateDiscoveryConsolidation(requireObject(spec.discovery, "goal specification.discovery"));
|
|
237
|
+
}
|
|
238
|
+
validateScopedRequirements(requireObjectField(spec, "scopedRequirements", "goal specification"));
|
|
239
|
+
validateArrayField(spec, "milestones", "goal specification", validateMilestone);
|
|
240
|
+
validateArrayField(spec, "acceptanceCriteria", "goal specification", validateAcceptanceCriterion);
|
|
241
|
+
validateArrayField(spec, "verificationGates", "goal specification", validateVerificationGate);
|
|
242
|
+
validateDesignConstraints(requireObjectField(spec, "designConstraints", "goal specification"));
|
|
243
|
+
validateProductConstraints(requireObjectField(spec, "productConstraints", "goal specification"));
|
|
244
|
+
if (spec.marketingGrowthContext !== undefined) {
|
|
245
|
+
validateMarketingGrowthContext(
|
|
246
|
+
requireObject(spec.marketingGrowthContext, "goal specification.marketingGrowthContext"),
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
validateDefinitionOfDone(requireObjectField(spec, "definitionOfDone", "goal specification"));
|
|
250
|
+
|
|
251
|
+
return spec as unknown as GoalSpecification;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function goalSpecificationToMarkdown(specInput: GoalSpecification): string {
|
|
255
|
+
const spec = validateGoalSpecification(specInput);
|
|
256
|
+
const lines = [
|
|
257
|
+
"## Persisted goal specification",
|
|
258
|
+
"",
|
|
259
|
+
`Schema version: ${spec.schemaVersion}`,
|
|
260
|
+
`Goal run: ${spec.goalRunId}`,
|
|
261
|
+
`Original user goal: ${spec.originalGoal}`,
|
|
262
|
+
`Summary: ${spec.summary}`,
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
if (spec.discovery) {
|
|
266
|
+
lines.push("", "### Discovery planning", "", ...formatDiscoveryConsolidation(spec.discovery));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
lines.push(
|
|
270
|
+
"",
|
|
271
|
+
"### Scoped requirements",
|
|
272
|
+
"",
|
|
273
|
+
"In scope:",
|
|
274
|
+
...formatRequirements(spec.scopedRequirements.inScope),
|
|
275
|
+
"",
|
|
276
|
+
"Out of scope:",
|
|
277
|
+
...formatRequirements(spec.scopedRequirements.outOfScope),
|
|
278
|
+
"",
|
|
279
|
+
"Assumptions:",
|
|
280
|
+
...formatStringList(spec.scopedRequirements.assumptions),
|
|
281
|
+
"",
|
|
282
|
+
"Open questions:",
|
|
283
|
+
...formatStringList(spec.scopedRequirements.openQuestions),
|
|
284
|
+
"",
|
|
285
|
+
"### Milestones",
|
|
286
|
+
"",
|
|
287
|
+
...formatMilestones(spec.milestones),
|
|
288
|
+
"",
|
|
289
|
+
"### Acceptance criteria",
|
|
290
|
+
"",
|
|
291
|
+
...formatAcceptanceCriteria(spec.acceptanceCriteria),
|
|
292
|
+
"",
|
|
293
|
+
"### Verification gates",
|
|
294
|
+
"",
|
|
295
|
+
...formatVerificationGates(spec.verificationGates),
|
|
296
|
+
"",
|
|
297
|
+
"### Design constraints",
|
|
298
|
+
"",
|
|
299
|
+
...formatConstraintSection(spec.designConstraints),
|
|
300
|
+
"",
|
|
301
|
+
"### Product constraints",
|
|
302
|
+
"",
|
|
303
|
+
...formatProductConstraintSection(spec.productConstraints),
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
if (spec.marketingGrowthContext) {
|
|
307
|
+
lines.push("", "### Marketing/growth context", "", ...formatMarketingGrowthContext(spec.marketingGrowthContext));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
lines.push(
|
|
311
|
+
"",
|
|
312
|
+
"### Definition of done",
|
|
313
|
+
"",
|
|
314
|
+
spec.definitionOfDone.summary,
|
|
315
|
+
"",
|
|
316
|
+
`Requirement IDs: ${formatInlineIds(spec.definitionOfDone.requirementIds)}`,
|
|
317
|
+
`Acceptance criterion IDs: ${formatInlineIds(spec.definitionOfDone.acceptanceCriterionIds)}`,
|
|
318
|
+
`Verification gate IDs: ${formatInlineIds(spec.definitionOfDone.verificationGateIds)}`,
|
|
319
|
+
"",
|
|
320
|
+
"Required artifacts:",
|
|
321
|
+
...formatStringList(spec.definitionOfDone.requiredArtifacts),
|
|
322
|
+
"",
|
|
323
|
+
"Notes:",
|
|
324
|
+
...formatStringList(spec.definitionOfDone.notes),
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
return `${lines.join("\n")}\n`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function emptyScopedRequirements(): GoalScopedRequirements {
|
|
331
|
+
return { inScope: [], outOfScope: [], assumptions: [], openQuestions: [] };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function emptyDesignConstraints(): GoalDesignConstraints {
|
|
335
|
+
return { uxPrinciples: [], uiRequirements: [], accessibility: [], architecturalConstraints: [], constraints: [] };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function emptyProductConstraints(): GoalProductConstraints {
|
|
339
|
+
return {
|
|
340
|
+
targetUsers: [],
|
|
341
|
+
platforms: [],
|
|
342
|
+
businessRules: [],
|
|
343
|
+
compliance: [],
|
|
344
|
+
dependencies: [],
|
|
345
|
+
risks: [],
|
|
346
|
+
constraints: [],
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function emptyDefinitionOfDone(summary: string): GoalDefinitionOfDone {
|
|
351
|
+
return {
|
|
352
|
+
summary,
|
|
353
|
+
requirementIds: [],
|
|
354
|
+
acceptanceCriterionIds: [],
|
|
355
|
+
verificationGateIds: [],
|
|
356
|
+
requiredArtifacts: [],
|
|
357
|
+
notes: [],
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function validateTraceability(traceability: JsonObject, goalRunId: string, originalGoal: string): void {
|
|
362
|
+
const traceGoal = requireString(traceability, "originalUserGoal", "goal specification.traceability");
|
|
363
|
+
const traceRunId = requireString(traceability, "goalRunId", "goal specification.traceability");
|
|
364
|
+
if (traceGoal !== originalGoal) {
|
|
365
|
+
throw new GoalSpecificationError("Goal specification traceability.originalUserGoal must match originalGoal.");
|
|
366
|
+
}
|
|
367
|
+
if (traceRunId !== goalRunId) {
|
|
368
|
+
throw new GoalSpecificationError("Goal specification traceability.goalRunId must match goalRunId.");
|
|
369
|
+
}
|
|
370
|
+
const source = requireString(traceability, "source", "goal specification.traceability");
|
|
371
|
+
if (!isTraceabilitySource(source)) {
|
|
372
|
+
throw new GoalSpecificationError(`Invalid goal specification traceability source: ${source}.`);
|
|
373
|
+
}
|
|
374
|
+
validateArrayField(traceability, "sourceArtifacts", "goal specification.traceability", validateSourceArtifact);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function validateSourceArtifact(value: unknown, path: string): void {
|
|
378
|
+
const artifact = requireObject(value, path);
|
|
379
|
+
requireString(artifact, "label", path);
|
|
380
|
+
optionalString(artifact, "path", path);
|
|
381
|
+
optionalString(artifact, "description", path);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function validateDiscoveryConsolidation(value: JsonObject): void {
|
|
385
|
+
requireString(value, "approach", "goal specification.discovery");
|
|
386
|
+
validateArrayField(value, "roleOutputs", "goal specification.discovery", validateDiscoveryRoleOutput);
|
|
387
|
+
requireStringArray(value, "consolidationNotes", "goal specification.discovery");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function validateDiscoveryRoleOutput(value: unknown, path: string): void {
|
|
391
|
+
const output = requireObject(value, path);
|
|
392
|
+
const role = requireString(output, "role", path);
|
|
393
|
+
if (!isDiscoveryPlanningRole(role)) {
|
|
394
|
+
throw new GoalSpecificationError(`${path}.role is not a supported discovery planning role.`);
|
|
395
|
+
}
|
|
396
|
+
requireString(output, "title", path);
|
|
397
|
+
requireString(output, "objective", path);
|
|
398
|
+
requireStringArray(output, "findings", path);
|
|
399
|
+
requireStringArray(output, "decisions", path);
|
|
400
|
+
requireStringArray(output, "risks", path);
|
|
401
|
+
requireStringArray(output, "requirementIds", path);
|
|
402
|
+
requireStringArray(output, "milestoneIds", path);
|
|
403
|
+
requireStringArray(output, "acceptanceCriterionIds", path);
|
|
404
|
+
requireStringArray(output, "verificationGateIds", path);
|
|
405
|
+
requireStringArray(output, "constraintIds", path);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function validateScopedRequirements(value: JsonObject): void {
|
|
409
|
+
validateArrayField(value, "inScope", "goal specification.scopedRequirements", validateRequirement);
|
|
410
|
+
validateArrayField(value, "outOfScope", "goal specification.scopedRequirements", validateRequirement);
|
|
411
|
+
requireStringArray(value, "assumptions", "goal specification.scopedRequirements");
|
|
412
|
+
requireStringArray(value, "openQuestions", "goal specification.scopedRequirements");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function validateRequirement(value: unknown, path: string): void {
|
|
416
|
+
const requirement = requireObject(value, path);
|
|
417
|
+
requireString(requirement, "id", path);
|
|
418
|
+
requireString(requirement, "title", path);
|
|
419
|
+
requireString(requirement, "description", path);
|
|
420
|
+
const priority = requireString(requirement, "priority", path);
|
|
421
|
+
if (!isRequirementPriority(priority)) {
|
|
422
|
+
throw new GoalSpecificationError(`${path}.priority must be one of must, should, could, or wont.`);
|
|
423
|
+
}
|
|
424
|
+
requireStringArray(requirement, "acceptanceCriterionIds", path);
|
|
425
|
+
requireStringArray(requirement, "milestoneIds", path);
|
|
426
|
+
optionalString(requirement, "source", path);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function validateMilestone(value: unknown, path: string): void {
|
|
430
|
+
const milestone = requireObject(value, path);
|
|
431
|
+
requireString(milestone, "id", path);
|
|
432
|
+
requireString(milestone, "title", path);
|
|
433
|
+
requireString(milestone, "description", path);
|
|
434
|
+
requireStringArray(milestone, "requirementIds", path);
|
|
435
|
+
requireStringArray(milestone, "acceptanceCriterionIds", path);
|
|
436
|
+
requireStringArray(milestone, "doneWhen", path);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function validateAcceptanceCriterion(value: unknown, path: string): void {
|
|
440
|
+
const criterion = requireObject(value, path);
|
|
441
|
+
requireString(criterion, "id", path);
|
|
442
|
+
requireString(criterion, "description", path);
|
|
443
|
+
requireStringArray(criterion, "requirementIds", path);
|
|
444
|
+
requireStringArray(criterion, "verificationGateIds", path);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function validateVerificationGate(value: unknown, path: string): void {
|
|
448
|
+
const gate = requireObject(value, path);
|
|
449
|
+
requireString(gate, "id", path);
|
|
450
|
+
requireString(gate, "title", path);
|
|
451
|
+
requireString(gate, "description", path);
|
|
452
|
+
if (typeof gate.required !== "boolean") {
|
|
453
|
+
throw new GoalSpecificationError(`${path}.required must be a boolean.`);
|
|
454
|
+
}
|
|
455
|
+
optionalString(gate, "command", path);
|
|
456
|
+
requireStringArray(gate, "successCriteria", path);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function validateDesignConstraints(value: JsonObject): void {
|
|
460
|
+
requireStringArray(value, "uxPrinciples", "goal specification.designConstraints");
|
|
461
|
+
requireStringArray(value, "uiRequirements", "goal specification.designConstraints");
|
|
462
|
+
requireStringArray(value, "accessibility", "goal specification.designConstraints");
|
|
463
|
+
requireStringArray(value, "architecturalConstraints", "goal specification.designConstraints");
|
|
464
|
+
validateArrayField(value, "constraints", "goal specification.designConstraints", validateConstraint);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function validateProductConstraints(value: JsonObject): void {
|
|
468
|
+
requireStringArray(value, "targetUsers", "goal specification.productConstraints");
|
|
469
|
+
requireStringArray(value, "platforms", "goal specification.productConstraints");
|
|
470
|
+
requireStringArray(value, "businessRules", "goal specification.productConstraints");
|
|
471
|
+
requireStringArray(value, "compliance", "goal specification.productConstraints");
|
|
472
|
+
requireStringArray(value, "dependencies", "goal specification.productConstraints");
|
|
473
|
+
requireStringArray(value, "risks", "goal specification.productConstraints");
|
|
474
|
+
validateArrayField(value, "constraints", "goal specification.productConstraints", validateConstraint);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function validateConstraint(value: unknown, path: string): void {
|
|
478
|
+
const constraint = requireObject(value, path);
|
|
479
|
+
requireString(constraint, "id", path);
|
|
480
|
+
requireString(constraint, "title", path);
|
|
481
|
+
requireString(constraint, "description", path);
|
|
482
|
+
optionalString(constraint, "rationale", path);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function validateMarketingGrowthContext(value: JsonObject): void {
|
|
486
|
+
requireStringArray(value, "targetSegments", "goal specification.marketingGrowthContext");
|
|
487
|
+
requireStringArray(value, "positioning", "goal specification.marketingGrowthContext");
|
|
488
|
+
requireStringArray(value, "acquisitionChannels", "goal specification.marketingGrowthContext");
|
|
489
|
+
requireStringArray(value, "growthMetrics", "goal specification.marketingGrowthContext");
|
|
490
|
+
requireStringArray(value, "launchConsiderations", "goal specification.marketingGrowthContext");
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function validateDefinitionOfDone(value: JsonObject): void {
|
|
494
|
+
requireString(value, "summary", "goal specification.definitionOfDone");
|
|
495
|
+
requireStringArray(value, "requirementIds", "goal specification.definitionOfDone");
|
|
496
|
+
requireStringArray(value, "acceptanceCriterionIds", "goal specification.definitionOfDone");
|
|
497
|
+
requireStringArray(value, "verificationGateIds", "goal specification.definitionOfDone");
|
|
498
|
+
requireStringArray(value, "requiredArtifacts", "goal specification.definitionOfDone");
|
|
499
|
+
requireStringArray(value, "notes", "goal specification.definitionOfDone");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
type JsonObject = Record<string, unknown>;
|
|
503
|
+
|
|
504
|
+
function requireObject(value: unknown, path: string): JsonObject {
|
|
505
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
506
|
+
throw new GoalSpecificationError(`${path} must be an object.`);
|
|
507
|
+
}
|
|
508
|
+
return value as JsonObject;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function requireObjectField(value: JsonObject, key: string, path: string): JsonObject {
|
|
512
|
+
return requireObject(value[key], `${path}.${key}`);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function requireString(value: JsonObject, key: string, path: string): string {
|
|
516
|
+
const candidate = value[key];
|
|
517
|
+
if (typeof candidate !== "string" || !candidate.trim()) {
|
|
518
|
+
throw new GoalSpecificationError(`${path}.${key} must be a non-empty string.`);
|
|
519
|
+
}
|
|
520
|
+
return candidate;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function optionalString(value: JsonObject, key: string, path: string): void {
|
|
524
|
+
if (value[key] !== undefined && typeof value[key] !== "string") {
|
|
525
|
+
throw new GoalSpecificationError(`${path}.${key} must be a string when present.`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function requireStringArray(value: JsonObject, key: string, path: string): void {
|
|
530
|
+
const candidate = value[key];
|
|
531
|
+
if (!Array.isArray(candidate) || !candidate.every((item) => typeof item === "string")) {
|
|
532
|
+
throw new GoalSpecificationError(`${path}.${key} must be an array of strings.`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function validateArrayField(
|
|
537
|
+
value: JsonObject,
|
|
538
|
+
key: string,
|
|
539
|
+
path: string,
|
|
540
|
+
validateItem: (item: unknown, path: string) => void,
|
|
541
|
+
): void {
|
|
542
|
+
const candidate = value[key];
|
|
543
|
+
if (!Array.isArray(candidate)) {
|
|
544
|
+
throw new GoalSpecificationError(`${path}.${key} must be an array.`);
|
|
545
|
+
}
|
|
546
|
+
candidate.forEach((item, index) => validateItem(item, `${path}.${key}[${index}]`));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function isRequirementPriority(value: string): value is GoalRequirementPriority {
|
|
550
|
+
return value === "must" || value === "should" || value === "could" || value === "wont";
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function isTraceabilitySource(value: string): value is GoalSpecTraceabilitySource {
|
|
554
|
+
return value === "user_goal" || value === "discovery_consolidation" || value === "manual";
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function isDiscoveryPlanningRole(value: string): value is GoalDiscoveryPlanningRole {
|
|
558
|
+
return (
|
|
559
|
+
value === "product_owner" ||
|
|
560
|
+
value === "project_manager" ||
|
|
561
|
+
value === "software_architect_tech_lead" ||
|
|
562
|
+
value === "ux_ui_designer" ||
|
|
563
|
+
value === "qa_reviewer" ||
|
|
564
|
+
value === "marketing_growth"
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function formatRequirements(requirements: GoalRequirement[]): string[] {
|
|
569
|
+
if (requirements.length === 0) {
|
|
570
|
+
return ["- none"];
|
|
571
|
+
}
|
|
572
|
+
return requirements.map((item) => `- ${item.id} (${item.priority}): ${item.title} — ${item.description}`);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function formatMilestones(milestones: GoalMilestone[]): string[] {
|
|
576
|
+
if (milestones.length === 0) {
|
|
577
|
+
return ["- none"];
|
|
578
|
+
}
|
|
579
|
+
return milestones.map((item) => `- ${item.id}: ${item.title} — ${item.description}`);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function formatAcceptanceCriteria(criteria: GoalAcceptanceCriterion[]): string[] {
|
|
583
|
+
if (criteria.length === 0) {
|
|
584
|
+
return ["- none"];
|
|
585
|
+
}
|
|
586
|
+
return criteria.map((item) => `- ${item.id}: ${item.description}`);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function formatVerificationGates(gates: GoalVerificationGate[]): string[] {
|
|
590
|
+
if (gates.length === 0) {
|
|
591
|
+
return ["- none"];
|
|
592
|
+
}
|
|
593
|
+
return gates.map((item) => {
|
|
594
|
+
const command = item.command ? ` Command: ${item.command}.` : "";
|
|
595
|
+
return `- ${item.id}${item.required ? " (required)" : " (optional)"}: ${item.title} — ${item.description}.${command}`;
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function formatConstraintSection(constraints: GoalDesignConstraints): string[] {
|
|
600
|
+
return [
|
|
601
|
+
"UX principles:",
|
|
602
|
+
...formatStringList(constraints.uxPrinciples),
|
|
603
|
+
"UI requirements:",
|
|
604
|
+
...formatStringList(constraints.uiRequirements),
|
|
605
|
+
"Accessibility:",
|
|
606
|
+
...formatStringList(constraints.accessibility),
|
|
607
|
+
"Architectural constraints:",
|
|
608
|
+
...formatStringList(constraints.architecturalConstraints),
|
|
609
|
+
"Other constraints:",
|
|
610
|
+
...formatConstraints(constraints.constraints),
|
|
611
|
+
];
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function formatProductConstraintSection(constraints: GoalProductConstraints): string[] {
|
|
615
|
+
return [
|
|
616
|
+
"Target users:",
|
|
617
|
+
...formatStringList(constraints.targetUsers),
|
|
618
|
+
"Platforms:",
|
|
619
|
+
...formatStringList(constraints.platforms),
|
|
620
|
+
"Business rules:",
|
|
621
|
+
...formatStringList(constraints.businessRules),
|
|
622
|
+
"Compliance:",
|
|
623
|
+
...formatStringList(constraints.compliance),
|
|
624
|
+
"Dependencies:",
|
|
625
|
+
...formatStringList(constraints.dependencies),
|
|
626
|
+
"Risks:",
|
|
627
|
+
...formatStringList(constraints.risks),
|
|
628
|
+
"Other constraints:",
|
|
629
|
+
...formatConstraints(constraints.constraints),
|
|
630
|
+
];
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function formatMarketingGrowthContext(context: GoalMarketingGrowthContext): string[] {
|
|
634
|
+
return [
|
|
635
|
+
"Target segments:",
|
|
636
|
+
...formatStringList(context.targetSegments),
|
|
637
|
+
"Positioning:",
|
|
638
|
+
...formatStringList(context.positioning),
|
|
639
|
+
"Acquisition channels:",
|
|
640
|
+
...formatStringList(context.acquisitionChannels),
|
|
641
|
+
"Growth metrics:",
|
|
642
|
+
...formatStringList(context.growthMetrics),
|
|
643
|
+
"Launch considerations:",
|
|
644
|
+
...formatStringList(context.launchConsiderations),
|
|
645
|
+
];
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function formatDiscoveryConsolidation(discovery: GoalDiscoveryConsolidation): string[] {
|
|
649
|
+
return [
|
|
650
|
+
`Approach: ${discovery.approach}`,
|
|
651
|
+
"",
|
|
652
|
+
"Role outputs:",
|
|
653
|
+
...discovery.roleOutputs.map(
|
|
654
|
+
(output) =>
|
|
655
|
+
`- ${output.title}: ${output.objective} Requirements: ${formatInlineIds(output.requirementIds)}. Acceptance: ${formatInlineIds(output.acceptanceCriterionIds)}. Gates: ${formatInlineIds(output.verificationGateIds)}.`,
|
|
656
|
+
),
|
|
657
|
+
"",
|
|
658
|
+
"Consolidation notes:",
|
|
659
|
+
...formatStringList(discovery.consolidationNotes),
|
|
660
|
+
];
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function formatConstraints(constraints: GoalConstraint[]): string[] {
|
|
664
|
+
if (constraints.length === 0) {
|
|
665
|
+
return ["- none"];
|
|
666
|
+
}
|
|
667
|
+
return constraints.map((item) => `- ${item.id}: ${item.title} — ${item.description}`);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function formatStringList(items: string[]): string[] {
|
|
671
|
+
if (items.length === 0) {
|
|
672
|
+
return ["- none"];
|
|
673
|
+
}
|
|
674
|
+
return items.map((item) => `- ${item}`);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function formatInlineIds(ids: string[]): string {
|
|
678
|
+
return ids.length > 0 ? ids.join(", ") : "none";
|
|
679
|
+
}
|