@kjerneverk/riotplan-verify 1.0.0-dev.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.
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Error thrown when acceptance criteria are not met
3
+ */
4
+ export declare class AcceptanceCriteriaError extends VerificationError {
5
+ constructor(uncheckedCriteria: AcceptanceCriterion[]);
6
+ }
7
+
8
+ /**
9
+ * Acceptance criterion from a step file
10
+ */
11
+ export declare interface AcceptanceCriterion {
12
+ text: string;
13
+ checked: boolean;
14
+ stepNumber: number;
15
+ }
16
+
17
+ /**
18
+ * Error thrown when required artifacts are missing
19
+ */
20
+ export declare class ArtifactVerificationError extends VerificationError {
21
+ constructor(missingArtifacts: string[]);
22
+ }
23
+
24
+ /**
25
+ * Check completion of plan execution
26
+ */
27
+ export declare function checkCompletion(planPath: string): Promise<CompletionReport>;
28
+
29
+ /**
30
+ * Check coverage of analysis criteria in plan steps
31
+ */
32
+ export declare function checkCoverage(planPath: string, options?: CoverageOptions): Promise<CoverageReport>;
33
+
34
+ /**
35
+ * Completion report for plan → execution verification
36
+ */
37
+ export declare interface CompletionReport {
38
+ /** Total steps */
39
+ totalSteps: number;
40
+ /** Steps by completion status */
41
+ complete: StepCompletionResult[];
42
+ partial: StepCompletionResult[];
43
+ incomplete: StepCompletionResult[];
44
+ pending: StepCompletionResult[];
45
+ /** Overall completion percentage */
46
+ completionScore: number;
47
+ /** Outstanding items */
48
+ outstandingItems: string[];
49
+ }
50
+
51
+ export declare interface CoverageOptions {
52
+ /** Minimum keyword match score to consider "covered" */
53
+ coverageThreshold?: number;
54
+ /** Minimum keyword match score to consider "partial" */
55
+ partialThreshold?: number;
56
+ }
57
+
58
+ /**
59
+ * Coverage report for analysis → plan verification
60
+ */
61
+ export declare interface CoverageReport {
62
+ /** Total criteria checked */
63
+ totalCriteria: number;
64
+ /** Fully covered criteria */
65
+ covered: CriterionResult[];
66
+ /** Partially covered criteria */
67
+ partial: CriterionResult[];
68
+ /** Missing criteria */
69
+ missing: CriterionResult[];
70
+ /** Coverage by priority */
71
+ byPriority: {
72
+ must: {
73
+ total: number;
74
+ covered: number;
75
+ partial: number;
76
+ missing: number;
77
+ };
78
+ should: {
79
+ total: number;
80
+ covered: number;
81
+ partial: number;
82
+ missing: number;
83
+ };
84
+ could: {
85
+ total: number;
86
+ covered: number;
87
+ partial: number;
88
+ missing: number;
89
+ };
90
+ };
91
+ /** Overall coverage percentage (weighted by priority) */
92
+ coverageScore: number;
93
+ /** Generated verification questions */
94
+ questions: string[];
95
+ }
96
+
97
+ /**
98
+ * Patterns for detecting criteria in markdown
99
+ */
100
+ export declare const CRITERIA_PATTERNS: {
101
+ /** Checkbox item: - [ ] or - [x] */
102
+ readonly checkbox: RegExp;
103
+ /** Section headers for criteria */
104
+ readonly mustHaveHeader: RegExp;
105
+ readonly shouldHaveHeader: RegExp;
106
+ readonly couldHaveHeader: RegExp;
107
+ /** Verification criteria section */
108
+ readonly verificationSection: RegExp;
109
+ };
110
+
111
+ /**
112
+ * Priority level for verification criteria
113
+ */
114
+ export declare type CriteriaPriority = "must" | "should" | "could";
115
+
116
+ /**
117
+ * Status of a verification criterion
118
+ */
119
+ export declare type CriteriaStatus = "covered" | "partial" | "missing" | "unknown";
120
+
121
+ /**
122
+ * Result of checking a single criterion
123
+ */
124
+ export declare interface CriterionResult {
125
+ criterion: VerificationCriterion;
126
+ status: CriteriaStatus;
127
+ matchedSteps: number[];
128
+ notes?: string;
129
+ }
130
+
131
+ /**
132
+ * Get criteria summary statistics
133
+ */
134
+ export declare function getCriteriaSummary(criteria: VerificationCriterion[]): {
135
+ total: number;
136
+ must: number;
137
+ should: number;
138
+ could: number;
139
+ };
140
+
141
+ /**
142
+ * Minimum scores for "healthy" status
143
+ */
144
+ export declare const HEALTH_THRESHOLDS: {
145
+ readonly coverage: {
146
+ readonly good: 80;
147
+ readonly warning: 60;
148
+ readonly critical: 40;
149
+ };
150
+ readonly completion: {
151
+ readonly good: 90;
152
+ readonly warning: 70;
153
+ readonly critical: 50;
154
+ };
155
+ };
156
+
157
+ /**
158
+ * Parse verification criteria from a plan's analysis
159
+ */
160
+ export declare function parseCriteria(planPath: string): Promise<ParsedCriteria>;
161
+
162
+ /**
163
+ * Parse criteria from markdown content
164
+ */
165
+ export declare function parseCriteriaFromContent(content: string, source: string): ParsedCriteria;
166
+
167
+ export declare interface ParsedCriteria {
168
+ criteria: VerificationCriterion[];
169
+ source: string;
170
+ parseErrors: string[];
171
+ }
172
+
173
+ declare interface Plan {
174
+ steps: PlanStep[];
175
+ }
176
+
177
+ declare interface PlanStep {
178
+ number: number;
179
+ filePath: string;
180
+ }
181
+
182
+ /**
183
+ * Weight multipliers for coverage scoring by priority
184
+ */
185
+ export declare const PRIORITY_WEIGHTS: {
186
+ readonly must: 1;
187
+ readonly should: 0.7;
188
+ readonly could: 0.3;
189
+ };
190
+
191
+ /**
192
+ * Result of checking step completion
193
+ */
194
+ export declare interface StepCompletionResult {
195
+ stepNumber: number;
196
+ stepTitle: string;
197
+ status: StepCompletionStatus;
198
+ acceptanceCriteria: AcceptanceCriterion[];
199
+ markedStatus: string;
200
+ notes?: string;
201
+ }
202
+
203
+ /**
204
+ * Step completion status
205
+ */
206
+ export declare type StepCompletionStatus = "complete" | "partial" | "incomplete" | "pending" | "skipped";
207
+
208
+ /**
209
+ * A single verification criterion extracted from analysis
210
+ */
211
+ export declare interface VerificationCriterion {
212
+ id: string;
213
+ text: string;
214
+ priority: CriteriaPriority;
215
+ source: string;
216
+ lineNumber?: number;
217
+ }
218
+
219
+ /**
220
+ * Verification engine for step completion
221
+ */
222
+ export declare class VerificationEngine {
223
+ /**
224
+ * Verify that a step is ready to be marked complete
225
+ *
226
+ * @param plan - The plan containing the step
227
+ * @param stepNumber - The step number to verify
228
+ * @param options - Verification options
229
+ * @returns Verification result with details
230
+ */
231
+ verifyStepCompletion(plan: Plan, stepNumber: number, options: VerificationOptions): Promise<VerificationResult>;
232
+ /**
233
+ * Check acceptance criteria in step file
234
+ *
235
+ * Parses markdown checkboxes from the step file and returns
236
+ * a list of criteria with their checked status.
237
+ *
238
+ * @param step - The step to check
239
+ * @returns List of acceptance criteria
240
+ */
241
+ private checkAcceptanceCriteria;
242
+ /**
243
+ * Check that artifacts mentioned in step file exist
244
+ *
245
+ * Parses the "Files Changed" section and verifies files exist.
246
+ *
247
+ * @param step - The step to check
248
+ * @returns List of missing artifacts
249
+ */
250
+ private checkArtifacts;
251
+ /**
252
+ * Determine if verification failure should block completion
253
+ *
254
+ * @param result - The verification result
255
+ * @param options - Verification options
256
+ * @returns True if completion should be blocked
257
+ */
258
+ shouldBlock(result: VerificationResult, options: VerificationOptions): boolean;
259
+ }
260
+
261
+ /**
262
+ * Base error for verification failures
263
+ */
264
+ export declare class VerificationError extends Error {
265
+ details: VerificationResult;
266
+ constructor(message: string, details: VerificationResult);
267
+ }
268
+
269
+ /**
270
+ * Options for verification
271
+ */
272
+ export declare interface VerificationOptions {
273
+ /** Enforcement level */
274
+ enforcement: 'advisory' | 'interactive' | 'strict';
275
+ /** Whether to check acceptance criteria */
276
+ checkAcceptanceCriteria: boolean;
277
+ /** Whether to check artifacts */
278
+ checkArtifacts: boolean;
279
+ /** Force flag to bypass verification */
280
+ force?: boolean;
281
+ }
282
+
283
+ /**
284
+ * Full verification report
285
+ */
286
+ export declare interface VerificationReport {
287
+ planPath: string;
288
+ timestamp: Date;
289
+ /** Analysis → Plan coverage (if analysis exists) */
290
+ coverage?: CoverageReport;
291
+ /** Plan → Execution completion */
292
+ completion?: CompletionReport;
293
+ /** Overall health score (0-100) */
294
+ healthScore: number;
295
+ /** Summary messages */
296
+ summary: string[];
297
+ /** Recommendations */
298
+ recommendations: string[];
299
+ }
300
+
301
+ /**
302
+ * Result of verification check
303
+ */
304
+ export declare interface VerificationResult {
305
+ /** Whether verification passed */
306
+ isValid: boolean;
307
+ /** Severity level of issues found */
308
+ level: 'passed' | 'warning' | 'error';
309
+ /** Human-readable messages about verification results */
310
+ messages: string[];
311
+ /** Acceptance criteria that were checked */
312
+ acceptanceCriteria?: AcceptanceCriterion[];
313
+ /** Artifacts that were verified */
314
+ artifacts?: string[];
315
+ }
316
+
317
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,650 @@
1
+ import { readFile, readdir, access } from "node:fs/promises";
2
+ import { join, resolve, dirname } from "node:path";
3
+ const PRIORITY_WEIGHTS = {
4
+ must: 1,
5
+ should: 0.7,
6
+ could: 0.3
7
+ };
8
+ const CRITERIA_PATTERNS = {
9
+ /** Checkbox item: - [ ] or - [x] */
10
+ checkbox: /^[-*]\s*\[([x ])\]\s*(.+)$/gm,
11
+ /** Section headers for criteria */
12
+ mustHaveHeader: /^###?\s*Must\s+Have/i,
13
+ shouldHaveHeader: /^###?\s*Should\s+Have/i,
14
+ couldHaveHeader: /^###?\s*Could\s+Have/i,
15
+ /** Verification criteria section */
16
+ verificationSection: /^##\s*Verification\s+Criteria/im
17
+ };
18
+ const HEALTH_THRESHOLDS = {
19
+ coverage: {
20
+ good: 80,
21
+ warning: 60,
22
+ critical: 40
23
+ },
24
+ completion: {
25
+ good: 90,
26
+ warning: 70,
27
+ critical: 50
28
+ }
29
+ };
30
+ async function parseCriteria(planPath) {
31
+ const reqPath = join(planPath, "analysis", "REQUIREMENTS.md");
32
+ try {
33
+ const content = await readFile(reqPath, "utf-8");
34
+ return parseCriteriaFromContent(content, reqPath);
35
+ } catch (error) {
36
+ return {
37
+ criteria: [],
38
+ source: reqPath,
39
+ parseErrors: [`Could not read ${reqPath}: ${error.message}`]
40
+ };
41
+ }
42
+ }
43
+ function parseCriteriaFromContent(content, source) {
44
+ const criteria = [];
45
+ const parseErrors = [];
46
+ const sectionMatch = content.match(CRITERIA_PATTERNS.verificationSection);
47
+ if (!sectionMatch) {
48
+ return {
49
+ criteria: [],
50
+ source,
51
+ parseErrors: ["No 'Verification Criteria' section found"]
52
+ };
53
+ }
54
+ const sectionStart = sectionMatch.index + sectionMatch[0].length;
55
+ const nextSectionMatch = content.slice(sectionStart).match(/^##\s+[^#]/m);
56
+ const sectionEnd = nextSectionMatch ? sectionStart + nextSectionMatch.index : content.length;
57
+ const sectionContent = content.slice(sectionStart, sectionEnd);
58
+ const lines = sectionContent.split("\n");
59
+ let currentPriority = "should";
60
+ let lineNumber = content.slice(0, sectionStart).split("\n").length;
61
+ for (const line of lines) {
62
+ lineNumber++;
63
+ if (CRITERIA_PATTERNS.mustHaveHeader.test(line)) {
64
+ currentPriority = "must";
65
+ continue;
66
+ }
67
+ if (CRITERIA_PATTERNS.shouldHaveHeader.test(line)) {
68
+ currentPriority = "should";
69
+ continue;
70
+ }
71
+ if (CRITERIA_PATTERNS.couldHaveHeader.test(line)) {
72
+ currentPriority = "could";
73
+ continue;
74
+ }
75
+ const checkboxMatch = line.match(/^[-*]\s*\[([x ])\]\s*(.+)$/i);
76
+ if (checkboxMatch) {
77
+ const text = checkboxMatch[2].trim();
78
+ const id = generateCriterionId(text, criteria.length);
79
+ criteria.push({
80
+ id,
81
+ text,
82
+ priority: currentPriority,
83
+ source,
84
+ lineNumber
85
+ });
86
+ }
87
+ }
88
+ if (criteria.length === 0) {
89
+ parseErrors.push("No checkbox criteria found in Verification Criteria section");
90
+ }
91
+ return { criteria, source, parseErrors };
92
+ }
93
+ function generateCriterionId(text, index) {
94
+ const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 30);
95
+ return `${String(index + 1).padStart(3, "0")}-${slug}`;
96
+ }
97
+ function getCriteriaSummary(criteria) {
98
+ return {
99
+ total: criteria.length,
100
+ must: criteria.filter((c) => c.priority === "must").length,
101
+ should: criteria.filter((c) => c.priority === "should").length,
102
+ could: criteria.filter((c) => c.priority === "could").length
103
+ };
104
+ }
105
+ const DEFAULT_OPTIONS = {
106
+ coverageThreshold: 0.6,
107
+ partialThreshold: 0.3
108
+ };
109
+ async function checkCoverage(planPath, options = {}) {
110
+ const opts = { ...DEFAULT_OPTIONS, ...options };
111
+ const { criteria, parseErrors } = await parseCriteria(planPath);
112
+ if (criteria.length === 0) {
113
+ return createEmptyReport(parseErrors);
114
+ }
115
+ const steps = await loadStepFiles(planPath);
116
+ const results = [];
117
+ for (const criterion of criteria) {
118
+ const result = checkCriterionCoverage(criterion, steps, opts);
119
+ results.push(result);
120
+ }
121
+ return buildCoverageReport(results);
122
+ }
123
+ async function loadStepFiles(planPath) {
124
+ const steps = /* @__PURE__ */ new Map();
125
+ const planDir = join(planPath, "plan");
126
+ try {
127
+ const files = await readdir(planDir);
128
+ const stepFiles = files.filter((f) => /^\d{2}-/.test(f) && f.endsWith(".md"));
129
+ for (const file of stepFiles) {
130
+ const stepNum = parseInt(file.slice(0, 2));
131
+ const content = await readFile(join(planDir, file), "utf-8");
132
+ steps.set(stepNum, content);
133
+ }
134
+ } catch {
135
+ }
136
+ return steps;
137
+ }
138
+ function checkCriterionCoverage(criterion, steps, options) {
139
+ const keywords = extractKeywords(criterion.text);
140
+ const matchedSteps = [];
141
+ let bestScore = 0;
142
+ for (const [stepNum, content] of steps) {
143
+ const score = calculateMatchScore(keywords, content);
144
+ if (score > bestScore) {
145
+ bestScore = score;
146
+ }
147
+ if (score >= options.partialThreshold) {
148
+ matchedSteps.push(stepNum);
149
+ }
150
+ }
151
+ let status;
152
+ if (bestScore >= options.coverageThreshold) {
153
+ status = "covered";
154
+ } else if (bestScore >= options.partialThreshold) {
155
+ status = "partial";
156
+ } else {
157
+ status = "missing";
158
+ }
159
+ return {
160
+ criterion,
161
+ status,
162
+ matchedSteps,
163
+ notes: matchedSteps.length > 0 ? `Best match in step(s): ${matchedSteps.join(", ")}` : void 0
164
+ };
165
+ }
166
+ function extractKeywords(text) {
167
+ const stopWords = /* @__PURE__ */ new Set([
168
+ "a",
169
+ "an",
170
+ "the",
171
+ "is",
172
+ "are",
173
+ "was",
174
+ "were",
175
+ "be",
176
+ "been",
177
+ "being",
178
+ "have",
179
+ "has",
180
+ "had",
181
+ "do",
182
+ "does",
183
+ "did",
184
+ "will",
185
+ "would",
186
+ "could",
187
+ "should",
188
+ "may",
189
+ "might",
190
+ "must",
191
+ "shall",
192
+ "can",
193
+ "need",
194
+ "to",
195
+ "of",
196
+ "in",
197
+ "for",
198
+ "on",
199
+ "with",
200
+ "at",
201
+ "by",
202
+ "from",
203
+ "as",
204
+ "or",
205
+ "and",
206
+ "but",
207
+ "if",
208
+ "then",
209
+ "so",
210
+ "that",
211
+ "this",
212
+ "it",
213
+ "its",
214
+ "all",
215
+ "any",
216
+ "each",
217
+ "every"
218
+ ]);
219
+ return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((word) => word.length > 2 && !stopWords.has(word));
220
+ }
221
+ function calculateMatchScore(keywords, content) {
222
+ if (keywords.length === 0) return 0;
223
+ const contentLower = content.toLowerCase();
224
+ let matches = 0;
225
+ for (const keyword of keywords) {
226
+ if (contentLower.includes(keyword)) {
227
+ matches++;
228
+ }
229
+ }
230
+ return matches / keywords.length;
231
+ }
232
+ function buildCoverageReport(results) {
233
+ const covered = results.filter((r) => r.status === "covered");
234
+ const partial = results.filter((r) => r.status === "partial");
235
+ const missing = results.filter((r) => r.status === "missing");
236
+ const byPriority = {
237
+ must: calculatePriorityStats(results, "must"),
238
+ should: calculatePriorityStats(results, "should"),
239
+ could: calculatePriorityStats(results, "could")
240
+ };
241
+ const coverageScore = calculateWeightedScore(byPriority);
242
+ const questions = generateVerificationQuestions(partial, missing);
243
+ return {
244
+ totalCriteria: results.length,
245
+ covered,
246
+ partial,
247
+ missing,
248
+ byPriority,
249
+ coverageScore,
250
+ questions
251
+ };
252
+ }
253
+ function calculatePriorityStats(results, priority) {
254
+ const forPriority = results.filter((r) => r.criterion.priority === priority);
255
+ return {
256
+ total: forPriority.length,
257
+ covered: forPriority.filter((r) => r.status === "covered").length,
258
+ partial: forPriority.filter((r) => r.status === "partial").length,
259
+ missing: forPriority.filter((r) => r.status === "missing").length
260
+ };
261
+ }
262
+ function calculateWeightedScore(byPriority) {
263
+ let totalWeight = 0;
264
+ let weightedCovered = 0;
265
+ for (const [priority, stats] of Object.entries(byPriority)) {
266
+ const weight = PRIORITY_WEIGHTS[priority];
267
+ totalWeight += stats.total * weight;
268
+ weightedCovered += (stats.covered + stats.partial * 0.5) * weight;
269
+ }
270
+ if (totalWeight === 0) return 100;
271
+ return Math.round(weightedCovered / totalWeight * 100);
272
+ }
273
+ function generateVerificationQuestions(partial, missing) {
274
+ const questions = [];
275
+ for (const result of missing.slice(0, 3)) {
276
+ questions.push(`Where is "${result.criterion.text}" addressed in the plan?`);
277
+ }
278
+ for (const result of partial.slice(0, 2)) {
279
+ questions.push(
280
+ `Is "${result.criterion.text}" fully covered in step(s) ${result.matchedSteps.join(", ")}?`
281
+ );
282
+ }
283
+ return questions;
284
+ }
285
+ function createEmptyReport(errors) {
286
+ return {
287
+ totalCriteria: 0,
288
+ covered: [],
289
+ partial: [],
290
+ missing: [],
291
+ byPriority: {
292
+ must: { total: 0, covered: 0, partial: 0, missing: 0 },
293
+ should: { total: 0, covered: 0, partial: 0, missing: 0 },
294
+ could: { total: 0, covered: 0, partial: 0, missing: 0 }
295
+ },
296
+ coverageScore: 100,
297
+ questions: errors
298
+ };
299
+ }
300
+ async function checkCompletion(planPath) {
301
+ const statusMap = await loadStatusMap(planPath);
302
+ const stepResults = await loadStepResults(planPath, statusMap);
303
+ return buildCompletionReport(stepResults);
304
+ }
305
+ async function loadStatusMap(planPath) {
306
+ const statusMap = /* @__PURE__ */ new Map();
307
+ try {
308
+ const statusPath = join(planPath, "STATUS.md");
309
+ const content = await readFile(statusPath, "utf-8");
310
+ const lines = content.split("\n");
311
+ let inTable = false;
312
+ for (const line of lines) {
313
+ if (/\|\s*Step\s*\|\s*Name\s*\|\s*Status/i.test(line)) {
314
+ inTable = true;
315
+ continue;
316
+ }
317
+ if (inTable && (/^##/.test(line) || line.trim() === "")) {
318
+ break;
319
+ }
320
+ if (inTable && line.includes("|")) {
321
+ const match = line.match(/\|\s*(\d+)\s*\|[^|]+\|\s*([^|]+)\|/);
322
+ if (match) {
323
+ const stepNum = parseInt(match[1]);
324
+ const status = match[2].trim();
325
+ statusMap.set(stepNum, status);
326
+ }
327
+ }
328
+ }
329
+ } catch {
330
+ }
331
+ return statusMap;
332
+ }
333
+ async function loadStepResults(planPath, statusMap) {
334
+ const results = [];
335
+ const planDir = join(planPath, "plan");
336
+ try {
337
+ const files = await readdir(planDir);
338
+ const stepFiles = files.filter((f) => /^\d{2}-/.test(f) && f.endsWith(".md")).sort();
339
+ for (const file of stepFiles) {
340
+ const stepNum = parseInt(file.slice(0, 2));
341
+ const content = await readFile(join(planDir, file), "utf-8");
342
+ const result = analyzeStep(stepNum, content, statusMap.get(stepNum) || "⬜ Pending");
343
+ results.push(result);
344
+ }
345
+ } catch {
346
+ }
347
+ return results;
348
+ }
349
+ function analyzeStep(stepNum, content, markedStatus) {
350
+ const titleMatch = content.match(/^#\s+(.+)$/m);
351
+ const stepTitle = titleMatch ? titleMatch[1] : `Step ${stepNum}`;
352
+ const criteria = extractAcceptanceCriteria(content, stepNum);
353
+ const status = determineCompletionStatus(criteria, markedStatus);
354
+ return {
355
+ stepNumber: stepNum,
356
+ stepTitle,
357
+ status,
358
+ acceptanceCriteria: criteria,
359
+ markedStatus
360
+ };
361
+ }
362
+ function extractAcceptanceCriteria(content, stepNum) {
363
+ const criteria = [];
364
+ const lines = content.split("\n");
365
+ const sectionLines = [];
366
+ let inSection = false;
367
+ for (const line of lines) {
368
+ if (/^##\s*Acceptance\s+Criteria$/i.test(line)) {
369
+ inSection = true;
370
+ continue;
371
+ }
372
+ if (inSection && /^##/.test(line)) {
373
+ break;
374
+ }
375
+ if (inSection) {
376
+ sectionLines.push(line);
377
+ }
378
+ }
379
+ if (sectionLines.length === 0) {
380
+ return criteria;
381
+ }
382
+ const sectionContent = sectionLines.join("\n");
383
+ const checkboxRegex = /^[-*]\s*\[([x ])\]\s*(.+)$/gim;
384
+ let match;
385
+ while ((match = checkboxRegex.exec(sectionContent)) !== null) {
386
+ criteria.push({
387
+ text: match[2].trim(),
388
+ checked: match[1].toLowerCase() === "x",
389
+ stepNumber: stepNum
390
+ });
391
+ }
392
+ return criteria;
393
+ }
394
+ function determineCompletionStatus(criteria, markedStatus) {
395
+ const isMarkedComplete = markedStatus.includes("✅") || markedStatus.toLowerCase().includes("complete");
396
+ const isMarkedPending = markedStatus.includes("⬜") || markedStatus.toLowerCase().includes("pending");
397
+ const isMarkedInProgress = markedStatus.includes("🔄") || markedStatus.toLowerCase().includes("progress");
398
+ const isMarkedSkipped = markedStatus.includes("⏭️") || markedStatus.toLowerCase().includes("skip");
399
+ if (isMarkedSkipped) {
400
+ return "skipped";
401
+ }
402
+ if (criteria.length === 0) {
403
+ if (isMarkedComplete) return "complete";
404
+ if (isMarkedPending) return "pending";
405
+ return "partial";
406
+ }
407
+ const checkedCount = criteria.filter((c) => c.checked).length;
408
+ const allChecked = checkedCount === criteria.length;
409
+ const noneChecked = checkedCount === 0;
410
+ if (isMarkedComplete) {
411
+ if (allChecked) return "complete";
412
+ if (noneChecked) return "incomplete";
413
+ return "partial";
414
+ }
415
+ if (isMarkedInProgress) {
416
+ return "partial";
417
+ }
418
+ return "pending";
419
+ }
420
+ function buildCompletionReport(results) {
421
+ const complete = results.filter((r) => r.status === "complete");
422
+ const partial = results.filter((r) => r.status === "partial");
423
+ const incomplete = results.filter((r) => r.status === "incomplete");
424
+ const pending = results.filter((r) => r.status === "pending" || r.status === "skipped");
425
+ const totalSteps = results.length;
426
+ const completedSteps = complete.length + partial.length * 0.5;
427
+ const completionScore = totalSteps > 0 ? Math.round(completedSteps / totalSteps * 100) : 100;
428
+ const outstandingItems = [];
429
+ for (const result of [...incomplete, ...partial]) {
430
+ const unchecked = result.acceptanceCriteria.filter((c) => !c.checked);
431
+ for (const criterion of unchecked) {
432
+ outstandingItems.push(`Step ${result.stepNumber}: ${criterion.text}`);
433
+ }
434
+ }
435
+ return {
436
+ totalSteps,
437
+ complete,
438
+ partial,
439
+ incomplete,
440
+ pending,
441
+ completionScore,
442
+ outstandingItems
443
+ };
444
+ }
445
+ class VerificationEngine {
446
+ /**
447
+ * Verify that a step is ready to be marked complete
448
+ *
449
+ * @param plan - The plan containing the step
450
+ * @param stepNumber - The step number to verify
451
+ * @param options - Verification options
452
+ * @returns Verification result with details
453
+ */
454
+ async verifyStepCompletion(plan, stepNumber, options) {
455
+ const step = plan.steps.find((s) => s.number === stepNumber);
456
+ if (!step) {
457
+ return {
458
+ isValid: false,
459
+ level: "error",
460
+ messages: [`Step ${stepNumber} not found in plan`]
461
+ };
462
+ }
463
+ const messages = [];
464
+ let level = "passed";
465
+ let acceptanceCriteria;
466
+ let artifacts;
467
+ if (options.checkAcceptanceCriteria) {
468
+ acceptanceCriteria = await this.checkAcceptanceCriteria(step);
469
+ const unchecked = acceptanceCriteria.filter((c) => !c.checked);
470
+ if (unchecked.length > 0) {
471
+ level = "error";
472
+ messages.push(
473
+ `${unchecked.length} acceptance criteria not checked:`
474
+ );
475
+ for (const criterion of unchecked) {
476
+ messages.push(` - [ ] ${criterion.text}`);
477
+ }
478
+ } else if (acceptanceCriteria.length === 0) {
479
+ level = "warning";
480
+ messages.push("No acceptance criteria found in step file");
481
+ } else {
482
+ messages.push(
483
+ `✓ All ${acceptanceCriteria.length} acceptance criteria checked`
484
+ );
485
+ }
486
+ }
487
+ if (options.checkArtifacts) {
488
+ artifacts = await this.checkArtifacts(step);
489
+ if (artifacts.length > 0) {
490
+ level = level === "error" ? "error" : "warning";
491
+ messages.push(`${artifacts.length} artifacts not found:`);
492
+ for (const artifact of artifacts) {
493
+ messages.push(` - ${artifact}`);
494
+ }
495
+ }
496
+ }
497
+ const isValid = level === "passed" || level === "warning";
498
+ return {
499
+ isValid,
500
+ level,
501
+ messages,
502
+ acceptanceCriteria,
503
+ artifacts
504
+ };
505
+ }
506
+ /**
507
+ * Check acceptance criteria in step file
508
+ *
509
+ * Parses markdown checkboxes from the step file and returns
510
+ * a list of criteria with their checked status.
511
+ *
512
+ * @param step - The step to check
513
+ * @returns List of acceptance criteria
514
+ */
515
+ async checkAcceptanceCriteria(step) {
516
+ try {
517
+ const content = await readFile(step.filePath, "utf-8");
518
+ const criteria = [];
519
+ const sectionMatch = content.match(
520
+ /##\s+Acceptance Criteria\s*\n([\s\S]*?)(?=\n##|$)/i
521
+ );
522
+ if (!sectionMatch) {
523
+ return criteria;
524
+ }
525
+ const section = sectionMatch[1];
526
+ const checkboxRegex = /^\s*-\s*\[([ xX])\]\s*(.+)$/gm;
527
+ let match;
528
+ while ((match = checkboxRegex.exec(section)) !== null) {
529
+ criteria.push({
530
+ text: match[2].trim(),
531
+ checked: match[1].toLowerCase() === "x",
532
+ stepNumber: step.number
533
+ });
534
+ }
535
+ return criteria;
536
+ } catch {
537
+ return [];
538
+ }
539
+ }
540
+ /**
541
+ * Check that artifacts mentioned in step file exist
542
+ *
543
+ * Parses the "Files Changed" section and verifies files exist.
544
+ *
545
+ * @param step - The step to check
546
+ * @returns List of missing artifacts
547
+ */
548
+ async checkArtifacts(step) {
549
+ try {
550
+ const content = await readFile(step.filePath, "utf-8");
551
+ const missing = [];
552
+ const sectionMatch = content.match(
553
+ /##\s+Files Changed\s*\n([\s\S]*?)(?=\n##|$)/i
554
+ );
555
+ if (!sectionMatch) {
556
+ return missing;
557
+ }
558
+ const section = sectionMatch[1];
559
+ const fileRegex = /^\s*-\s+`?([^\s`]+\.[a-zA-Z0-9]+)`?/gm;
560
+ let match;
561
+ while ((match = fileRegex.exec(section)) !== null) {
562
+ const filePath = match[1].trim();
563
+ const planRoot = resolve(dirname(step.filePath), "../..");
564
+ const fullPath = resolve(planRoot, filePath);
565
+ try {
566
+ await access(fullPath);
567
+ } catch {
568
+ missing.push(filePath);
569
+ }
570
+ }
571
+ return missing;
572
+ } catch {
573
+ return [];
574
+ }
575
+ }
576
+ /**
577
+ * Determine if verification failure should block completion
578
+ *
579
+ * @param result - The verification result
580
+ * @param options - Verification options
581
+ * @returns True if completion should be blocked
582
+ */
583
+ shouldBlock(result, options) {
584
+ if (options.force) {
585
+ return false;
586
+ }
587
+ if (options.enforcement === "advisory") {
588
+ return false;
589
+ }
590
+ if (options.enforcement === "strict" && result.level === "error") {
591
+ return true;
592
+ }
593
+ return false;
594
+ }
595
+ }
596
+ class VerificationError extends Error {
597
+ constructor(message, details) {
598
+ super(message);
599
+ this.details = details;
600
+ this.name = "VerificationError";
601
+ Error.captureStackTrace(this, this.constructor);
602
+ }
603
+ }
604
+ class AcceptanceCriteriaError extends VerificationError {
605
+ constructor(uncheckedCriteria) {
606
+ const message = `Step has ${uncheckedCriteria.length} unchecked acceptance criteria`;
607
+ const details = {
608
+ isValid: false,
609
+ level: "error",
610
+ messages: [
611
+ message,
612
+ ...uncheckedCriteria.map((c) => ` - [ ] ${c.text}`)
613
+ ],
614
+ acceptanceCriteria: uncheckedCriteria
615
+ };
616
+ super(message, details);
617
+ this.name = "AcceptanceCriteriaError";
618
+ }
619
+ }
620
+ class ArtifactVerificationError extends VerificationError {
621
+ constructor(missingArtifacts) {
622
+ const message = `Step has ${missingArtifacts.length} missing artifacts`;
623
+ const details = {
624
+ isValid: false,
625
+ level: "error",
626
+ messages: [
627
+ message,
628
+ ...missingArtifacts.map((a) => ` - ${a}`)
629
+ ],
630
+ artifacts: missingArtifacts
631
+ };
632
+ super(message, details);
633
+ this.name = "ArtifactVerificationError";
634
+ }
635
+ }
636
+ export {
637
+ AcceptanceCriteriaError,
638
+ ArtifactVerificationError,
639
+ CRITERIA_PATTERNS,
640
+ HEALTH_THRESHOLDS,
641
+ PRIORITY_WEIGHTS,
642
+ VerificationEngine,
643
+ VerificationError,
644
+ checkCompletion,
645
+ checkCoverage,
646
+ getCriteriaSummary,
647
+ parseCriteria,
648
+ parseCriteriaFromContent
649
+ };
650
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../src/constants.ts","../src/criteria-parser.ts","../src/coverage-checker.ts","../src/completion-checker.ts","../src/engine.ts","../src/errors.ts"],"sourcesContent":["/**\n * Weight multipliers for coverage scoring by priority\n */\nexport const PRIORITY_WEIGHTS = {\n must: 1.0,\n should: 0.7,\n could: 0.3,\n} as const;\n\n/**\n * Patterns for detecting criteria in markdown\n */\nexport const CRITERIA_PATTERNS = {\n /** Checkbox item: - [ ] or - [x] */\n checkbox: /^[-*]\\s*\\[([x ])\\]\\s*(.+)$/gm,\n \n /** Section headers for criteria */\n mustHaveHeader: /^###?\\s*Must\\s+Have/i,\n shouldHaveHeader: /^###?\\s*Should\\s+Have/i,\n couldHaveHeader: /^###?\\s*Could\\s+Have/i,\n \n /** Verification criteria section */\n verificationSection: /^##\\s*Verification\\s+Criteria/im,\n} as const;\n\n/**\n * Minimum scores for \"healthy\" status\n */\nexport const HEALTH_THRESHOLDS = {\n coverage: {\n good: 80,\n warning: 60,\n critical: 40,\n },\n completion: {\n good: 90,\n warning: 70,\n critical: 50,\n },\n} as const;\n","import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { VerificationCriterion, CriteriaPriority } from \"./types.js\";\nimport { CRITERIA_PATTERNS } from \"./constants.js\";\n\nexport interface ParsedCriteria {\n criteria: VerificationCriterion[];\n source: string;\n parseErrors: string[];\n}\n\n/**\n * Parse verification criteria from a plan's analysis\n */\nexport async function parseCriteria(planPath: string): Promise<ParsedCriteria> {\n const reqPath = join(planPath, \"analysis\", \"REQUIREMENTS.md\");\n \n try {\n const content = await readFile(reqPath, \"utf-8\");\n return parseCriteriaFromContent(content, reqPath);\n } catch (error) {\n return {\n criteria: [],\n source: reqPath,\n parseErrors: [`Could not read ${reqPath}: ${(error as Error).message}`],\n };\n }\n}\n\n/**\n * Parse criteria from markdown content\n */\nexport function parseCriteriaFromContent(\n content: string,\n source: string\n): ParsedCriteria {\n const criteria: VerificationCriterion[] = [];\n const parseErrors: string[] = [];\n \n // Find the Verification Criteria section\n const sectionMatch = content.match(CRITERIA_PATTERNS.verificationSection);\n if (!sectionMatch) {\n return {\n criteria: [],\n source,\n parseErrors: [\"No 'Verification Criteria' section found\"],\n };\n }\n \n // Extract content after the section header until next ## header\n const sectionStart = sectionMatch.index! + sectionMatch[0].length;\n const nextSectionMatch = content.slice(sectionStart).match(/^##\\s+[^#]/m);\n const sectionEnd = nextSectionMatch \n ? sectionStart + nextSectionMatch.index! \n : content.length;\n const sectionContent = content.slice(sectionStart, sectionEnd);\n \n // Split into priority sections\n const lines = sectionContent.split(\"\\n\");\n let currentPriority: CriteriaPriority = \"should\"; // default\n let lineNumber = content.slice(0, sectionStart).split(\"\\n\").length;\n \n for (const line of lines) {\n lineNumber++;\n \n // Check for priority headers\n if (CRITERIA_PATTERNS.mustHaveHeader.test(line)) {\n currentPriority = \"must\";\n continue;\n }\n if (CRITERIA_PATTERNS.shouldHaveHeader.test(line)) {\n currentPriority = \"should\";\n continue;\n }\n if (CRITERIA_PATTERNS.couldHaveHeader.test(line)) {\n currentPriority = \"could\";\n continue;\n }\n \n // Check for checkbox items\n const checkboxMatch = line.match(/^[-*]\\s*\\[([x ])\\]\\s*(.+)$/i);\n if (checkboxMatch) {\n const text = checkboxMatch[2].trim();\n const id = generateCriterionId(text, criteria.length);\n \n criteria.push({\n id,\n text,\n priority: currentPriority,\n source,\n lineNumber,\n });\n }\n }\n \n if (criteria.length === 0) {\n parseErrors.push(\"No checkbox criteria found in Verification Criteria section\");\n }\n \n return { criteria, source, parseErrors };\n}\n\n/**\n * Generate a unique ID for a criterion\n */\nfunction generateCriterionId(text: string, index: number): string {\n const slug = text\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-|-$/g, \"\")\n .slice(0, 30);\n return `${String(index + 1).padStart(3, \"0\")}-${slug}`;\n}\n\n/**\n * Get criteria summary statistics\n */\nexport function getCriteriaSummary(criteria: VerificationCriterion[]): {\n total: number;\n must: number;\n should: number;\n could: number;\n} {\n return {\n total: criteria.length,\n must: criteria.filter(c => c.priority === \"must\").length,\n should: criteria.filter(c => c.priority === \"should\").length,\n could: criteria.filter(c => c.priority === \"could\").length,\n };\n}\n","import { readFile, readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { \n VerificationCriterion, \n CriterionResult, \n CoverageReport,\n CriteriaStatus \n} from \"./types.js\";\nimport { PRIORITY_WEIGHTS } from \"./constants.js\";\nimport { parseCriteria } from \"./criteria-parser.js\";\n\nexport interface CoverageOptions {\n /** Minimum keyword match score to consider \"covered\" */\n coverageThreshold?: number;\n /** Minimum keyword match score to consider \"partial\" */\n partialThreshold?: number;\n}\n\nconst DEFAULT_OPTIONS: Required<CoverageOptions> = {\n coverageThreshold: 0.6,\n partialThreshold: 0.3,\n};\n\n/**\n * Check coverage of analysis criteria in plan steps\n */\nexport async function checkCoverage(\n planPath: string,\n options: CoverageOptions = {}\n): Promise<CoverageReport> {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n \n // Parse criteria from analysis\n const { criteria, parseErrors } = await parseCriteria(planPath);\n \n if (criteria.length === 0) {\n return createEmptyReport(parseErrors);\n }\n \n // Load all step files\n const steps = await loadStepFiles(planPath);\n \n // Check each criterion against steps\n const results: CriterionResult[] = [];\n \n for (const criterion of criteria) {\n const result = checkCriterionCoverage(criterion, steps, opts);\n results.push(result);\n }\n \n // Build report\n return buildCoverageReport(results);\n}\n\n/**\n * Load all step file contents\n */\nasync function loadStepFiles(planPath: string): Promise<Map<number, string>> {\n const steps = new Map<number, string>();\n const planDir = join(planPath, \"plan\");\n \n try {\n const files = await readdir(planDir);\n const stepFiles = files.filter(f => /^\\d{2}-/.test(f) && f.endsWith(\".md\"));\n \n for (const file of stepFiles) {\n const stepNum = parseInt(file.slice(0, 2));\n const content = await readFile(join(planDir, file), \"utf-8\");\n steps.set(stepNum, content);\n }\n } catch {\n // Plan directory doesn't exist or is empty\n }\n \n return steps;\n}\n\n/**\n * Check if a single criterion is covered by any step\n */\nfunction checkCriterionCoverage(\n criterion: VerificationCriterion,\n steps: Map<number, string>,\n options: Required<CoverageOptions>\n): CriterionResult {\n const keywords = extractKeywords(criterion.text);\n \n const matchedSteps: number[] = [];\n let bestScore = 0;\n \n for (const [stepNum, content] of steps) {\n const score = calculateMatchScore(keywords, content);\n if (score > bestScore) {\n bestScore = score;\n }\n if (score >= options.partialThreshold) {\n matchedSteps.push(stepNum);\n }\n }\n \n let status: CriteriaStatus;\n if (bestScore >= options.coverageThreshold) {\n status = \"covered\";\n } else if (bestScore >= options.partialThreshold) {\n status = \"partial\";\n } else {\n status = \"missing\";\n }\n \n return {\n criterion,\n status,\n matchedSteps,\n notes: matchedSteps.length > 0 \n ? `Best match in step(s): ${matchedSteps.join(\", \")}` \n : undefined,\n };\n}\n\n/**\n * Extract significant keywords from text\n */\nfunction extractKeywords(text: string): string[] {\n const stopWords = new Set([\n \"a\", \"an\", \"the\", \"is\", \"are\", \"was\", \"were\", \"be\", \"been\",\n \"being\", \"have\", \"has\", \"had\", \"do\", \"does\", \"did\", \"will\",\n \"would\", \"could\", \"should\", \"may\", \"might\", \"must\", \"shall\",\n \"can\", \"need\", \"to\", \"of\", \"in\", \"for\", \"on\", \"with\", \"at\",\n \"by\", \"from\", \"as\", \"or\", \"and\", \"but\", \"if\", \"then\", \"so\",\n \"that\", \"this\", \"it\", \"its\", \"all\", \"any\", \"each\", \"every\",\n ]);\n \n return text\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, \" \")\n .split(/\\s+/)\n .filter(word => word.length > 2 && !stopWords.has(word));\n}\n\n/**\n * Calculate match score between keywords and content\n */\nfunction calculateMatchScore(keywords: string[], content: string): number {\n if (keywords.length === 0) return 0;\n \n const contentLower = content.toLowerCase();\n let matches = 0;\n \n for (const keyword of keywords) {\n if (contentLower.includes(keyword)) {\n matches++;\n }\n }\n \n return matches / keywords.length;\n}\n\n/**\n * Build the full coverage report\n */\nfunction buildCoverageReport(results: CriterionResult[]): CoverageReport {\n const covered = results.filter(r => r.status === \"covered\");\n const partial = results.filter(r => r.status === \"partial\");\n const missing = results.filter(r => r.status === \"missing\");\n \n const byPriority = {\n must: calculatePriorityStats(results, \"must\"),\n should: calculatePriorityStats(results, \"should\"),\n could: calculatePriorityStats(results, \"could\"),\n };\n \n const coverageScore = calculateWeightedScore(byPriority);\n const questions = generateVerificationQuestions(partial, missing);\n \n return {\n totalCriteria: results.length,\n covered,\n partial,\n missing,\n byPriority,\n coverageScore,\n questions,\n };\n}\n\nfunction calculatePriorityStats(\n results: CriterionResult[],\n priority: string\n): { total: number; covered: number; partial: number; missing: number } {\n const forPriority = results.filter(r => r.criterion.priority === priority);\n return {\n total: forPriority.length,\n covered: forPriority.filter(r => r.status === \"covered\").length,\n partial: forPriority.filter(r => r.status === \"partial\").length,\n missing: forPriority.filter(r => r.status === \"missing\").length,\n };\n}\n\nfunction calculateWeightedScore(byPriority: CoverageReport[\"byPriority\"]): number {\n let totalWeight = 0;\n let weightedCovered = 0;\n \n for (const [priority, stats] of Object.entries(byPriority)) {\n const weight = PRIORITY_WEIGHTS[priority as keyof typeof PRIORITY_WEIGHTS];\n totalWeight += stats.total * weight;\n weightedCovered += (stats.covered + stats.partial * 0.5) * weight;\n }\n \n if (totalWeight === 0) return 100;\n return Math.round((weightedCovered / totalWeight) * 100);\n}\n\nfunction generateVerificationQuestions(\n partial: CriterionResult[],\n missing: CriterionResult[]\n): string[] {\n const questions: string[] = [];\n \n for (const result of missing.slice(0, 3)) {\n questions.push(`Where is \"${result.criterion.text}\" addressed in the plan?`);\n }\n \n for (const result of partial.slice(0, 2)) {\n questions.push(\n `Is \"${result.criterion.text}\" fully covered in step(s) ${result.matchedSteps.join(\", \")}?`\n );\n }\n \n return questions;\n}\n\nfunction createEmptyReport(errors: string[]): CoverageReport {\n return {\n totalCriteria: 0,\n covered: [],\n partial: [],\n missing: [],\n byPriority: {\n must: { total: 0, covered: 0, partial: 0, missing: 0 },\n should: { total: 0, covered: 0, partial: 0, missing: 0 },\n could: { total: 0, covered: 0, partial: 0, missing: 0 },\n },\n coverageScore: 100,\n questions: errors,\n };\n}\n","import { readFile, readdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type {\n StepCompletionResult,\n StepCompletionStatus,\n AcceptanceCriterion,\n CompletionReport,\n} from \"./types.js\";\n\n/**\n * Check completion of plan execution\n */\nexport async function checkCompletion(planPath: string): Promise<CompletionReport> {\n // Load STATUS.md\n const statusMap = await loadStatusMap(planPath);\n \n // Load step files and extract acceptance criteria\n const stepResults = await loadStepResults(planPath, statusMap);\n \n // Build report\n return buildCompletionReport(stepResults);\n}\n\n/**\n * Load step status from STATUS.md\n */\nasync function loadStatusMap(planPath: string): Promise<Map<number, string>> {\n const statusMap = new Map<number, string>();\n \n try {\n const statusPath = join(planPath, \"STATUS.md\");\n const content = await readFile(statusPath, \"utf-8\");\n \n // Parse the step progress table\n // Format: | 01 | Step Name | ✅ Completed | ... |\n // Use line-by-line parsing to avoid polynomial regex\n const lines = content.split('\\n');\n let inTable = false;\n \n for (const line of lines) {\n // Look for table header\n if (/\\|\\s*Step\\s*\\|\\s*Name\\s*\\|\\s*Status/i.test(line)) {\n inTable = true;\n continue;\n }\n \n // Stop at section headers or double newlines\n if (inTable && (/^##/.test(line) || line.trim() === '')) {\n break;\n }\n \n // Parse table rows\n if (inTable && line.includes('|')) {\n const match = line.match(/\\|\\s*(\\d+)\\s*\\|[^|]+\\|\\s*([^|]+)\\|/);\n if (match) {\n const stepNum = parseInt(match[1]);\n const status = match[2].trim();\n statusMap.set(stepNum, status);\n }\n }\n }\n } catch {\n // STATUS.md doesn't exist\n }\n \n return statusMap;\n}\n\n/**\n * Load step files and extract acceptance criteria\n */\nasync function loadStepResults(\n planPath: string,\n statusMap: Map<number, string>\n): Promise<StepCompletionResult[]> {\n const results: StepCompletionResult[] = [];\n const planDir = join(planPath, \"plan\");\n \n try {\n const files = await readdir(planDir);\n const stepFiles = files.filter(f => /^\\d{2}-/.test(f) && f.endsWith(\".md\")).sort();\n \n for (const file of stepFiles) {\n const stepNum = parseInt(file.slice(0, 2));\n const content = await readFile(join(planDir, file), \"utf-8\");\n \n const result = analyzeStep(stepNum, content, statusMap.get(stepNum) || \"⬜ Pending\");\n results.push(result);\n }\n } catch {\n // Plan directory doesn't exist\n }\n \n return results;\n}\n\n/**\n * Analyze a single step for completion\n */\nfunction analyzeStep(\n stepNum: number,\n content: string,\n markedStatus: string\n): StepCompletionResult {\n const titleMatch = content.match(/^#\\s+(.+)$/m);\n const stepTitle = titleMatch ? titleMatch[1] : `Step ${stepNum}`;\n \n const criteria = extractAcceptanceCriteria(content, stepNum);\n \n const status = determineCompletionStatus(criteria, markedStatus);\n \n return {\n stepNumber: stepNum,\n stepTitle,\n status,\n acceptanceCriteria: criteria,\n markedStatus,\n };\n}\n\n/**\n * Extract acceptance criteria from step content\n */\nfunction extractAcceptanceCriteria(content: string, stepNum: number): AcceptanceCriterion[] {\n const criteria: AcceptanceCriterion[] = [];\n \n // Find Acceptance Criteria section\n // Use line-by-line parsing to avoid polynomial regex\n const lines = content.split('\\n');\n const sectionLines: string[] = [];\n let inSection = false;\n \n for (const line of lines) {\n if (/^##\\s*Acceptance\\s+Criteria$/i.test(line)) {\n inSection = true;\n continue;\n }\n if (inSection && /^##/.test(line)) {\n break;\n }\n if (inSection) {\n sectionLines.push(line);\n }\n }\n \n if (sectionLines.length === 0) {\n return criteria;\n }\n \n const sectionContent = sectionLines.join('\\n');\n \n const checkboxRegex = /^[-*]\\s*\\[([x ])\\]\\s*(.+)$/gim;\n let match;\n \n while ((match = checkboxRegex.exec(sectionContent)) !== null) {\n criteria.push({\n text: match[2].trim(),\n checked: match[1].toLowerCase() === \"x\",\n stepNumber: stepNum,\n });\n }\n \n return criteria;\n}\n\n/**\n * Determine actual completion status based on criteria and marked status\n */\nfunction determineCompletionStatus(\n criteria: AcceptanceCriterion[],\n markedStatus: string\n): StepCompletionStatus {\n const isMarkedComplete = markedStatus.includes(\"✅\") || markedStatus.toLowerCase().includes(\"complete\");\n const isMarkedPending = markedStatus.includes(\"⬜\") || markedStatus.toLowerCase().includes(\"pending\");\n const isMarkedInProgress = markedStatus.includes(\"🔄\") || markedStatus.toLowerCase().includes(\"progress\");\n const isMarkedSkipped = markedStatus.includes(\"⏭️\") || markedStatus.toLowerCase().includes(\"skip\");\n \n if (isMarkedSkipped) {\n return \"skipped\";\n }\n \n if (criteria.length === 0) {\n if (isMarkedComplete) return \"complete\";\n if (isMarkedPending) return \"pending\";\n return \"partial\";\n }\n \n const checkedCount = criteria.filter(c => c.checked).length;\n const allChecked = checkedCount === criteria.length;\n const noneChecked = checkedCount === 0;\n \n if (isMarkedComplete) {\n if (allChecked) return \"complete\";\n if (noneChecked) return \"incomplete\";\n return \"partial\";\n }\n \n if (isMarkedInProgress) {\n return \"partial\";\n }\n \n return \"pending\";\n}\n\n/**\n * Build the completion report\n */\nfunction buildCompletionReport(results: StepCompletionResult[]): CompletionReport {\n const complete = results.filter(r => r.status === \"complete\");\n const partial = results.filter(r => r.status === \"partial\");\n const incomplete = results.filter(r => r.status === \"incomplete\");\n const pending = results.filter(r => r.status === \"pending\" || r.status === \"skipped\");\n \n const totalSteps = results.length;\n const completedSteps = complete.length + partial.length * 0.5;\n const completionScore = totalSteps > 0 \n ? Math.round((completedSteps / totalSteps) * 100) \n : 100;\n \n const outstandingItems: string[] = [];\n \n for (const result of [...incomplete, ...partial]) {\n const unchecked = result.acceptanceCriteria.filter(c => !c.checked);\n for (const criterion of unchecked) {\n outstandingItems.push(`Step ${result.stepNumber}: ${criterion.text}`);\n }\n }\n \n return {\n totalSteps,\n complete,\n partial,\n incomplete,\n pending,\n completionScore,\n outstandingItems,\n };\n}\n","/**\n * Verification Engine for RiotPlan\n *\n * Core engine that checks acceptance criteria and enforces completion rules\n * based on configuration settings.\n */\n\nimport { readFile, access } from 'node:fs/promises';\nimport { resolve, dirname } from 'node:path';\nimport type { AcceptanceCriterion } from './types.js';\n\ninterface PlanStep {\n number: number;\n filePath: string;\n}\n\ninterface Plan {\n steps: PlanStep[];\n}\n\n/**\n * Result of verification check\n */\nexport interface VerificationResult {\n /** Whether verification passed */\n isValid: boolean;\n /** Severity level of issues found */\n level: 'passed' | 'warning' | 'error';\n /** Human-readable messages about verification results */\n messages: string[];\n /** Acceptance criteria that were checked */\n acceptanceCriteria?: AcceptanceCriterion[];\n /** Artifacts that were verified */\n artifacts?: string[];\n}\n\n/**\n * Options for verification\n */\nexport interface VerificationOptions {\n /** Enforcement level */\n enforcement: 'advisory' | 'interactive' | 'strict';\n /** Whether to check acceptance criteria */\n checkAcceptanceCriteria: boolean;\n /** Whether to check artifacts */\n checkArtifacts: boolean;\n /** Force flag to bypass verification */\n force?: boolean;\n}\n\n/**\n * Verification engine for step completion\n */\nexport class VerificationEngine {\n /**\n * Verify that a step is ready to be marked complete\n *\n * @param plan - The plan containing the step\n * @param stepNumber - The step number to verify\n * @param options - Verification options\n * @returns Verification result with details\n */\n async verifyStepCompletion(\n plan: Plan,\n stepNumber: number,\n options: VerificationOptions\n ): Promise<VerificationResult> {\n const step = plan.steps.find((s) => s.number === stepNumber);\n if (!step) {\n return {\n isValid: false,\n level: 'error',\n messages: [`Step ${stepNumber} not found in plan`],\n };\n }\n\n const messages: string[] = [];\n let level: 'passed' | 'warning' | 'error' = 'passed';\n let acceptanceCriteria: AcceptanceCriterion[] | undefined;\n let artifacts: string[] | undefined;\n\n // Check acceptance criteria if enabled\n if (options.checkAcceptanceCriteria) {\n acceptanceCriteria = await this.checkAcceptanceCriteria(step);\n const unchecked = acceptanceCriteria.filter((c) => !c.checked);\n\n if (unchecked.length > 0) {\n level = 'error';\n messages.push(\n `${unchecked.length} acceptance criteria not checked:`\n );\n for (const criterion of unchecked) {\n messages.push(` - [ ] ${criterion.text}`);\n }\n } else if (acceptanceCriteria.length === 0) {\n level = 'warning';\n messages.push('No acceptance criteria found in step file');\n } else {\n messages.push(\n `✓ All ${acceptanceCriteria.length} acceptance criteria checked`\n );\n }\n }\n\n // Check artifacts if enabled\n if (options.checkArtifacts) {\n artifacts = await this.checkArtifacts(step);\n if (artifacts.length > 0) {\n level = level === 'error' ? 'error' : 'warning';\n messages.push(`${artifacts.length} artifacts not found:`);\n for (const artifact of artifacts) {\n messages.push(` - ${artifact}`);\n }\n }\n }\n\n // Determine if verification passed\n const isValid = level === 'passed' || level === 'warning';\n\n return {\n isValid,\n level,\n messages,\n acceptanceCriteria,\n artifacts,\n };\n }\n\n /**\n * Check acceptance criteria in step file\n *\n * Parses markdown checkboxes from the step file and returns\n * a list of criteria with their checked status.\n *\n * @param step - The step to check\n * @returns List of acceptance criteria\n */\n private async checkAcceptanceCriteria(\n step: PlanStep\n ): Promise<AcceptanceCriterion[]> {\n try {\n const content = await readFile(step.filePath, 'utf-8');\n const criteria: AcceptanceCriterion[] = [];\n\n // Find the Acceptance Criteria section\n const sectionMatch = content.match(\n /##\\s+Acceptance Criteria\\s*\\n([\\s\\S]*?)(?=\\n##|$)/i\n );\n\n if (!sectionMatch) {\n return criteria;\n }\n\n const section = sectionMatch[1];\n\n // Parse markdown checkboxes: - [x] completed, - [ ] pending\n const checkboxRegex = /^\\s*-\\s*\\[([ xX])\\]\\s*(.+)$/gm;\n let match;\n\n while ((match = checkboxRegex.exec(section)) !== null) {\n criteria.push({\n text: match[2].trim(),\n checked: match[1].toLowerCase() === 'x',\n stepNumber: step.number,\n });\n }\n\n return criteria;\n } catch {\n // If we can't read the file, return empty array\n return [];\n }\n }\n\n /**\n * Check that artifacts mentioned in step file exist\n *\n * Parses the \"Files Changed\" section and verifies files exist.\n *\n * @param step - The step to check\n * @returns List of missing artifacts\n */\n private async checkArtifacts(step: PlanStep): Promise<string[]> {\n try {\n const content = await readFile(step.filePath, 'utf-8');\n const missing: string[] = [];\n\n // Find the \"Files Changed\" section\n const sectionMatch = content.match(\n /##\\s+Files Changed\\s*\\n([\\s\\S]*?)(?=\\n##|$)/i\n );\n\n if (!sectionMatch) {\n return missing;\n }\n\n const section = sectionMatch[1];\n\n // Extract file paths from list items\n // Matches: - path/to/file.ts or - `path/to/file.ts`\n const fileRegex = /^\\s*-\\s+`?([^\\s`]+\\.[a-zA-Z0-9]+)`?/gm;\n let match;\n\n while ((match = fileRegex.exec(section)) !== null) {\n const filePath = match[1].trim();\n \n // Resolve path relative to plan directory (go up from plan/ to root)\n const planRoot = resolve(dirname(step.filePath), '../..');\n const fullPath = resolve(planRoot, filePath);\n\n try {\n await access(fullPath);\n } catch {\n missing.push(filePath);\n }\n }\n\n return missing;\n } catch {\n // If we can't read the file, return empty array\n return [];\n }\n }\n\n /**\n * Determine if verification failure should block completion\n *\n * @param result - The verification result\n * @param options - Verification options\n * @returns True if completion should be blocked\n */\n shouldBlock(\n result: VerificationResult,\n options: VerificationOptions\n ): boolean {\n // Force flag always bypasses\n if (options.force) {\n return false;\n }\n\n // Advisory mode never blocks\n if (options.enforcement === 'advisory') {\n return false;\n }\n\n // Strict mode blocks on any error\n if (options.enforcement === 'strict' && result.level === 'error') {\n return true;\n }\n\n // Interactive mode doesn't block - caller handles prompting\n return false;\n }\n}\n","/**\n * Custom error types for verification failures\n */\n\nimport type { VerificationResult } from './engine.js';\nimport type { AcceptanceCriterion } from './types.js';\n\n/**\n * Base error for verification failures\n */\nexport class VerificationError extends Error {\n constructor(\n message: string,\n public details: VerificationResult\n ) {\n super(message);\n this.name = 'VerificationError';\n Error.captureStackTrace(this, this.constructor);\n }\n}\n\n/**\n * Error thrown when acceptance criteria are not met\n */\nexport class AcceptanceCriteriaError extends VerificationError {\n constructor(uncheckedCriteria: AcceptanceCriterion[]) {\n const message = `Step has ${uncheckedCriteria.length} unchecked acceptance criteria`;\n const details: VerificationResult = {\n isValid: false,\n level: 'error',\n messages: [\n message,\n ...uncheckedCriteria.map((c) => ` - [ ] ${c.text}`),\n ],\n acceptanceCriteria: uncheckedCriteria,\n };\n super(message, details);\n this.name = 'AcceptanceCriteriaError';\n }\n}\n\n/**\n * Error thrown when required artifacts are missing\n */\nexport class ArtifactVerificationError extends VerificationError {\n constructor(missingArtifacts: string[]) {\n const message = `Step has ${missingArtifacts.length} missing artifacts`;\n const details: VerificationResult = {\n isValid: false,\n level: 'error',\n messages: [\n message,\n ...missingArtifacts.map((a) => ` - ${a}`),\n ],\n artifacts: missingArtifacts,\n };\n super(message, details);\n this.name = 'ArtifactVerificationError';\n }\n}\n"],"names":[],"mappings":";;AAGO,MAAM,mBAAmB;AAAA,EAC5B,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,OAAO;AACX;AAKO,MAAM,oBAAoB;AAAA;AAAA,EAE7B,UAAU;AAAA;AAAA,EAGV,gBAAgB;AAAA,EAChB,kBAAkB;AAAA,EAClB,iBAAiB;AAAA;AAAA,EAGjB,qBAAqB;AACzB;AAKO,MAAM,oBAAoB;AAAA,EAC7B,UAAU;AAAA,IACN,MAAM;AAAA,IACN,SAAS;AAAA,IACT,UAAU;AAAA,EAAA;AAAA,EAEd,YAAY;AAAA,IACR,MAAM;AAAA,IACN,SAAS;AAAA,IACT,UAAU;AAAA,EAAA;AAElB;ACzBA,eAAsB,cAAc,UAA2C;AAC3E,QAAM,UAAU,KAAK,UAAU,YAAY,iBAAiB;AAE5D,MAAI;AACA,UAAM,UAAU,MAAM,SAAS,SAAS,OAAO;AAC/C,WAAO,yBAAyB,SAAS,OAAO;AAAA,EACpD,SAAS,OAAO;AACZ,WAAO;AAAA,MACH,UAAU,CAAA;AAAA,MACV,QAAQ;AAAA,MACR,aAAa,CAAC,kBAAkB,OAAO,KAAM,MAAgB,OAAO,EAAE;AAAA,IAAA;AAAA,EAE9E;AACJ;AAKO,SAAS,yBACZ,SACA,QACc;AACd,QAAM,WAAoC,CAAA;AAC1C,QAAM,cAAwB,CAAA;AAG9B,QAAM,eAAe,QAAQ,MAAM,kBAAkB,mBAAmB;AACxE,MAAI,CAAC,cAAc;AACf,WAAO;AAAA,MACH,UAAU,CAAA;AAAA,MACV;AAAA,MACA,aAAa,CAAC,0CAA0C;AAAA,IAAA;AAAA,EAEhE;AAGA,QAAM,eAAe,aAAa,QAAS,aAAa,CAAC,EAAE;AAC3D,QAAM,mBAAmB,QAAQ,MAAM,YAAY,EAAE,MAAM,aAAa;AACxE,QAAM,aAAa,mBACb,eAAe,iBAAiB,QAChC,QAAQ;AACd,QAAM,iBAAiB,QAAQ,MAAM,cAAc,UAAU;AAG7D,QAAM,QAAQ,eAAe,MAAM,IAAI;AACvC,MAAI,kBAAoC;AACxC,MAAI,aAAa,QAAQ,MAAM,GAAG,YAAY,EAAE,MAAM,IAAI,EAAE;AAE5D,aAAW,QAAQ,OAAO;AACtB;AAGA,QAAI,kBAAkB,eAAe,KAAK,IAAI,GAAG;AAC7C,wBAAkB;AAClB;AAAA,IACJ;AACA,QAAI,kBAAkB,iBAAiB,KAAK,IAAI,GAAG;AAC/C,wBAAkB;AAClB;AAAA,IACJ;AACA,QAAI,kBAAkB,gBAAgB,KAAK,IAAI,GAAG;AAC9C,wBAAkB;AAClB;AAAA,IACJ;AAGA,UAAM,gBAAgB,KAAK,MAAM,6BAA6B;AAC9D,QAAI,eAAe;AACf,YAAM,OAAO,cAAc,CAAC,EAAE,KAAA;AAC9B,YAAM,KAAK,oBAAoB,MAAM,SAAS,MAAM;AAEpD,eAAS,KAAK;AAAA,QACV;AAAA,QACA;AAAA,QACA,UAAU;AAAA,QACV;AAAA,QACA;AAAA,MAAA,CACH;AAAA,IACL;AAAA,EACJ;AAEA,MAAI,SAAS,WAAW,GAAG;AACvB,gBAAY,KAAK,6DAA6D;AAAA,EAClF;AAEA,SAAO,EAAE,UAAU,QAAQ,YAAA;AAC/B;AAKA,SAAS,oBAAoB,MAAc,OAAuB;AAC9D,QAAM,OAAO,KACR,YAAA,EACA,QAAQ,eAAe,GAAG,EAC1B,QAAQ,UAAU,EAAE,EACpB,MAAM,GAAG,EAAE;AAChB,SAAO,GAAG,OAAO,QAAQ,CAAC,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,IAAI;AACxD;AAKO,SAAS,mBAAmB,UAKjC;AACE,SAAO;AAAA,IACH,OAAO,SAAS;AAAA,IAChB,MAAM,SAAS,OAAO,OAAK,EAAE,aAAa,MAAM,EAAE;AAAA,IAClD,QAAQ,SAAS,OAAO,OAAK,EAAE,aAAa,QAAQ,EAAE;AAAA,IACtD,OAAO,SAAS,OAAO,OAAK,EAAE,aAAa,OAAO,EAAE;AAAA,EAAA;AAE5D;AC/GA,MAAM,kBAA6C;AAAA,EAC/C,mBAAmB;AAAA,EACnB,kBAAkB;AACtB;AAKA,eAAsB,cAClB,UACA,UAA2B,IACJ;AACvB,QAAM,OAAO,EAAE,GAAG,iBAAiB,GAAG,QAAA;AAGtC,QAAM,EAAE,UAAU,YAAA,IAAgB,MAAM,cAAc,QAAQ;AAE9D,MAAI,SAAS,WAAW,GAAG;AACvB,WAAO,kBAAkB,WAAW;AAAA,EACxC;AAGA,QAAM,QAAQ,MAAM,cAAc,QAAQ;AAG1C,QAAM,UAA6B,CAAA;AAEnC,aAAW,aAAa,UAAU;AAC9B,UAAM,SAAS,uBAAuB,WAAW,OAAO,IAAI;AAC5D,YAAQ,KAAK,MAAM;AAAA,EACvB;AAGA,SAAO,oBAAoB,OAAO;AACtC;AAKA,eAAe,cAAc,UAAgD;AACzE,QAAM,4BAAY,IAAA;AAClB,QAAM,UAAU,KAAK,UAAU,MAAM;AAErC,MAAI;AACA,UAAM,QAAQ,MAAM,QAAQ,OAAO;AACnC,UAAM,YAAY,MAAM,OAAO,CAAA,MAAK,UAAU,KAAK,CAAC,KAAK,EAAE,SAAS,KAAK,CAAC;AAE1E,eAAW,QAAQ,WAAW;AAC1B,YAAM,UAAU,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC;AACzC,YAAM,UAAU,MAAM,SAAS,KAAK,SAAS,IAAI,GAAG,OAAO;AAC3D,YAAM,IAAI,SAAS,OAAO;AAAA,IAC9B;AAAA,EACJ,QAAQ;AAAA,EAER;AAEA,SAAO;AACX;AAKA,SAAS,uBACL,WACA,OACA,SACe;AACf,QAAM,WAAW,gBAAgB,UAAU,IAAI;AAE/C,QAAM,eAAyB,CAAA;AAC/B,MAAI,YAAY;AAEhB,aAAW,CAAC,SAAS,OAAO,KAAK,OAAO;AACpC,UAAM,QAAQ,oBAAoB,UAAU,OAAO;AACnD,QAAI,QAAQ,WAAW;AACnB,kBAAY;AAAA,IAChB;AACA,QAAI,SAAS,QAAQ,kBAAkB;AACnC,mBAAa,KAAK,OAAO;AAAA,IAC7B;AAAA,EACJ;AAEA,MAAI;AACJ,MAAI,aAAa,QAAQ,mBAAmB;AACxC,aAAS;AAAA,EACb,WAAW,aAAa,QAAQ,kBAAkB;AAC9C,aAAS;AAAA,EACb,OAAO;AACH,aAAS;AAAA,EACb;AAEA,SAAO;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,aAAa,SAAS,IACvB,0BAA0B,aAAa,KAAK,IAAI,CAAC,KACjD;AAAA,EAAA;AAEd;AAKA,SAAS,gBAAgB,MAAwB;AAC7C,QAAM,gCAAgB,IAAI;AAAA,IACtB;AAAA,IAAK;AAAA,IAAM;AAAA,IAAO;AAAA,IAAM;AAAA,IAAO;AAAA,IAAO;AAAA,IAAQ;AAAA,IAAM;AAAA,IACpD;AAAA,IAAS;AAAA,IAAQ;AAAA,IAAO;AAAA,IAAO;AAAA,IAAM;AAAA,IAAQ;AAAA,IAAO;AAAA,IACpD;AAAA,IAAS;AAAA,IAAS;AAAA,IAAU;AAAA,IAAO;AAAA,IAAS;AAAA,IAAQ;AAAA,IACpD;AAAA,IAAO;AAAA,IAAQ;AAAA,IAAM;AAAA,IAAM;AAAA,IAAM;AAAA,IAAO;AAAA,IAAM;AAAA,IAAQ;AAAA,IACtD;AAAA,IAAM;AAAA,IAAQ;AAAA,IAAM;AAAA,IAAM;AAAA,IAAO;AAAA,IAAO;AAAA,IAAM;AAAA,IAAQ;AAAA,IACtD;AAAA,IAAQ;AAAA,IAAQ;AAAA,IAAM;AAAA,IAAO;AAAA,IAAO;AAAA,IAAO;AAAA,IAAQ;AAAA,EAAA,CACtD;AAED,SAAO,KACF,YAAA,EACA,QAAQ,gBAAgB,GAAG,EAC3B,MAAM,KAAK,EACX,OAAO,CAAA,SAAQ,KAAK,SAAS,KAAK,CAAC,UAAU,IAAI,IAAI,CAAC;AAC/D;AAKA,SAAS,oBAAoB,UAAoB,SAAyB;AACtE,MAAI,SAAS,WAAW,EAAG,QAAO;AAElC,QAAM,eAAe,QAAQ,YAAA;AAC7B,MAAI,UAAU;AAEd,aAAW,WAAW,UAAU;AAC5B,QAAI,aAAa,SAAS,OAAO,GAAG;AAChC;AAAA,IACJ;AAAA,EACJ;AAEA,SAAO,UAAU,SAAS;AAC9B;AAKA,SAAS,oBAAoB,SAA4C;AACrE,QAAM,UAAU,QAAQ,OAAO,CAAA,MAAK,EAAE,WAAW,SAAS;AAC1D,QAAM,UAAU,QAAQ,OAAO,CAAA,MAAK,EAAE,WAAW,SAAS;AAC1D,QAAM,UAAU,QAAQ,OAAO,CAAA,MAAK,EAAE,WAAW,SAAS;AAE1D,QAAM,aAAa;AAAA,IACf,MAAM,uBAAuB,SAAS,MAAM;AAAA,IAC5C,QAAQ,uBAAuB,SAAS,QAAQ;AAAA,IAChD,OAAO,uBAAuB,SAAS,OAAO;AAAA,EAAA;AAGlD,QAAM,gBAAgB,uBAAuB,UAAU;AACvD,QAAM,YAAY,8BAA8B,SAAS,OAAO;AAEhE,SAAO;AAAA,IACH,eAAe,QAAQ;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAER;AAEA,SAAS,uBACL,SACA,UACoE;AACpE,QAAM,cAAc,QAAQ,OAAO,OAAK,EAAE,UAAU,aAAa,QAAQ;AACzE,SAAO;AAAA,IACH,OAAO,YAAY;AAAA,IACnB,SAAS,YAAY,OAAO,OAAK,EAAE,WAAW,SAAS,EAAE;AAAA,IACzD,SAAS,YAAY,OAAO,OAAK,EAAE,WAAW,SAAS,EAAE;AAAA,IACzD,SAAS,YAAY,OAAO,OAAK,EAAE,WAAW,SAAS,EAAE;AAAA,EAAA;AAEjE;AAEA,SAAS,uBAAuB,YAAkD;AAC9E,MAAI,cAAc;AAClB,MAAI,kBAAkB;AAEtB,aAAW,CAAC,UAAU,KAAK,KAAK,OAAO,QAAQ,UAAU,GAAG;AACxD,UAAM,SAAS,iBAAiB,QAAyC;AACzE,mBAAe,MAAM,QAAQ;AAC7B,wBAAoB,MAAM,UAAU,MAAM,UAAU,OAAO;AAAA,EAC/D;AAEA,MAAI,gBAAgB,EAAG,QAAO;AAC9B,SAAO,KAAK,MAAO,kBAAkB,cAAe,GAAG;AAC3D;AAEA,SAAS,8BACL,SACA,SACQ;AACR,QAAM,YAAsB,CAAA;AAE5B,aAAW,UAAU,QAAQ,MAAM,GAAG,CAAC,GAAG;AACtC,cAAU,KAAK,aAAa,OAAO,UAAU,IAAI,0BAA0B;AAAA,EAC/E;AAEA,aAAW,UAAU,QAAQ,MAAM,GAAG,CAAC,GAAG;AACtC,cAAU;AAAA,MACN,OAAO,OAAO,UAAU,IAAI,8BAA8B,OAAO,aAAa,KAAK,IAAI,CAAC;AAAA,IAAA;AAAA,EAEhG;AAEA,SAAO;AACX;AAEA,SAAS,kBAAkB,QAAkC;AACzD,SAAO;AAAA,IACH,eAAe;AAAA,IACf,SAAS,CAAA;AAAA,IACT,SAAS,CAAA;AAAA,IACT,SAAS,CAAA;AAAA,IACT,YAAY;AAAA,MACR,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,EAAA;AAAA,MACnD,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,EAAA;AAAA,MACrD,OAAO,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,EAAA;AAAA,IAAE;AAAA,IAE1D,eAAe;AAAA,IACf,WAAW;AAAA,EAAA;AAEnB;ACzOA,eAAsB,gBAAgB,UAA6C;AAE/E,QAAM,YAAY,MAAM,cAAc,QAAQ;AAG9C,QAAM,cAAc,MAAM,gBAAgB,UAAU,SAAS;AAG7D,SAAO,sBAAsB,WAAW;AAC5C;AAKA,eAAe,cAAc,UAAgD;AACzE,QAAM,gCAAgB,IAAA;AAEtB,MAAI;AACA,UAAM,aAAa,KAAK,UAAU,WAAW;AAC7C,UAAM,UAAU,MAAM,SAAS,YAAY,OAAO;AAKlD,UAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,QAAI,UAAU;AAEd,eAAW,QAAQ,OAAO;AAEtB,UAAI,uCAAuC,KAAK,IAAI,GAAG;AACnD,kBAAU;AACV;AAAA,MACJ;AAGA,UAAI,YAAY,MAAM,KAAK,IAAI,KAAK,KAAK,WAAW,KAAK;AACrD;AAAA,MACJ;AAGA,UAAI,WAAW,KAAK,SAAS,GAAG,GAAG;AAC/B,cAAM,QAAQ,KAAK,MAAM,oCAAoC;AAC7D,YAAI,OAAO;AACP,gBAAM,UAAU,SAAS,MAAM,CAAC,CAAC;AACjC,gBAAM,SAAS,MAAM,CAAC,EAAE,KAAA;AACxB,oBAAU,IAAI,SAAS,MAAM;AAAA,QACjC;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ,QAAQ;AAAA,EAER;AAEA,SAAO;AACX;AAKA,eAAe,gBACX,UACA,WAC+B;AAC/B,QAAM,UAAkC,CAAA;AACxC,QAAM,UAAU,KAAK,UAAU,MAAM;AAErC,MAAI;AACA,UAAM,QAAQ,MAAM,QAAQ,OAAO;AACnC,UAAM,YAAY,MAAM,OAAO,CAAA,MAAK,UAAU,KAAK,CAAC,KAAK,EAAE,SAAS,KAAK,CAAC,EAAE,KAAA;AAE5E,eAAW,QAAQ,WAAW;AAC1B,YAAM,UAAU,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC;AACzC,YAAM,UAAU,MAAM,SAAS,KAAK,SAAS,IAAI,GAAG,OAAO;AAE3D,YAAM,SAAS,YAAY,SAAS,SAAS,UAAU,IAAI,OAAO,KAAK,WAAW;AAClF,cAAQ,KAAK,MAAM;AAAA,IACvB;AAAA,EACJ,QAAQ;AAAA,EAER;AAEA,SAAO;AACX;AAKA,SAAS,YACL,SACA,SACA,cACoB;AACpB,QAAM,aAAa,QAAQ,MAAM,aAAa;AAC9C,QAAM,YAAY,aAAa,WAAW,CAAC,IAAI,QAAQ,OAAO;AAE9D,QAAM,WAAW,0BAA0B,SAAS,OAAO;AAE3D,QAAM,SAAS,0BAA0B,UAAU,YAAY;AAE/D,SAAO;AAAA,IACH,YAAY;AAAA,IACZ;AAAA,IACA;AAAA,IACA,oBAAoB;AAAA,IACpB;AAAA,EAAA;AAER;AAKA,SAAS,0BAA0B,SAAiB,SAAwC;AACxF,QAAM,WAAkC,CAAA;AAIxC,QAAM,QAAQ,QAAQ,MAAM,IAAI;AAChC,QAAM,eAAyB,CAAA;AAC/B,MAAI,YAAY;AAEhB,aAAW,QAAQ,OAAO;AACtB,QAAI,gCAAgC,KAAK,IAAI,GAAG;AAC5C,kBAAY;AACZ;AAAA,IACJ;AACA,QAAI,aAAa,MAAM,KAAK,IAAI,GAAG;AAC/B;AAAA,IACJ;AACA,QAAI,WAAW;AACX,mBAAa,KAAK,IAAI;AAAA,IAC1B;AAAA,EACJ;AAEA,MAAI,aAAa,WAAW,GAAG;AAC3B,WAAO;AAAA,EACX;AAEA,QAAM,iBAAiB,aAAa,KAAK,IAAI;AAE7C,QAAM,gBAAgB;AACtB,MAAI;AAEJ,UAAQ,QAAQ,cAAc,KAAK,cAAc,OAAO,MAAM;AAC1D,aAAS,KAAK;AAAA,MACV,MAAM,MAAM,CAAC,EAAE,KAAA;AAAA,MACf,SAAS,MAAM,CAAC,EAAE,kBAAkB;AAAA,MACpC,YAAY;AAAA,IAAA,CACf;AAAA,EACL;AAEA,SAAO;AACX;AAKA,SAAS,0BACL,UACA,cACoB;AACpB,QAAM,mBAAmB,aAAa,SAAS,GAAG,KAAK,aAAa,YAAA,EAAc,SAAS,UAAU;AACrG,QAAM,kBAAkB,aAAa,SAAS,GAAG,KAAK,aAAa,YAAA,EAAc,SAAS,SAAS;AACnG,QAAM,qBAAqB,aAAa,SAAS,IAAI,KAAK,aAAa,YAAA,EAAc,SAAS,UAAU;AACxG,QAAM,kBAAkB,aAAa,SAAS,IAAI,KAAK,aAAa,YAAA,EAAc,SAAS,MAAM;AAEjG,MAAI,iBAAiB;AACjB,WAAO;AAAA,EACX;AAEA,MAAI,SAAS,WAAW,GAAG;AACvB,QAAI,iBAAkB,QAAO;AAC7B,QAAI,gBAAiB,QAAO;AAC5B,WAAO;AAAA,EACX;AAEA,QAAM,eAAe,SAAS,OAAO,CAAA,MAAK,EAAE,OAAO,EAAE;AACrD,QAAM,aAAa,iBAAiB,SAAS;AAC7C,QAAM,cAAc,iBAAiB;AAErC,MAAI,kBAAkB;AAClB,QAAI,WAAY,QAAO;AACvB,QAAI,YAAa,QAAO;AACxB,WAAO;AAAA,EACX;AAEA,MAAI,oBAAoB;AACpB,WAAO;AAAA,EACX;AAEA,SAAO;AACX;AAKA,SAAS,sBAAsB,SAAmD;AAC9E,QAAM,WAAW,QAAQ,OAAO,CAAA,MAAK,EAAE,WAAW,UAAU;AAC5D,QAAM,UAAU,QAAQ,OAAO,CAAA,MAAK,EAAE,WAAW,SAAS;AAC1D,QAAM,aAAa,QAAQ,OAAO,CAAA,MAAK,EAAE,WAAW,YAAY;AAChE,QAAM,UAAU,QAAQ,OAAO,CAAA,MAAK,EAAE,WAAW,aAAa,EAAE,WAAW,SAAS;AAEpF,QAAM,aAAa,QAAQ;AAC3B,QAAM,iBAAiB,SAAS,SAAS,QAAQ,SAAS;AAC1D,QAAM,kBAAkB,aAAa,IAC/B,KAAK,MAAO,iBAAiB,aAAc,GAAG,IAC9C;AAEN,QAAM,mBAA6B,CAAA;AAEnC,aAAW,UAAU,CAAC,GAAG,YAAY,GAAG,OAAO,GAAG;AAC9C,UAAM,YAAY,OAAO,mBAAmB,OAAO,CAAA,MAAK,CAAC,EAAE,OAAO;AAClE,eAAW,aAAa,WAAW;AAC/B,uBAAiB,KAAK,QAAQ,OAAO,UAAU,KAAK,UAAU,IAAI,EAAE;AAAA,IACxE;AAAA,EACJ;AAEA,SAAO;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAER;ACxLO,MAAM,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAS5B,MAAM,qBACF,MACA,YACA,SAC2B;AAC3B,UAAM,OAAO,KAAK,MAAM,KAAK,CAAC,MAAM,EAAE,WAAW,UAAU;AAC3D,QAAI,CAAC,MAAM;AACP,aAAO;AAAA,QACH,SAAS;AAAA,QACT,OAAO;AAAA,QACP,UAAU,CAAC,QAAQ,UAAU,oBAAoB;AAAA,MAAA;AAAA,IAEzD;AAEA,UAAM,WAAqB,CAAA;AAC3B,QAAI,QAAwC;AAC5C,QAAI;AACJ,QAAI;AAGJ,QAAI,QAAQ,yBAAyB;AACjC,2BAAqB,MAAM,KAAK,wBAAwB,IAAI;AAC5D,YAAM,YAAY,mBAAmB,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO;AAE7D,UAAI,UAAU,SAAS,GAAG;AACtB,gBAAQ;AACR,iBAAS;AAAA,UACL,GAAG,UAAU,MAAM;AAAA,QAAA;AAEvB,mBAAW,aAAa,WAAW;AAC/B,mBAAS,KAAK,WAAW,UAAU,IAAI,EAAE;AAAA,QAC7C;AAAA,MACJ,WAAW,mBAAmB,WAAW,GAAG;AACxC,gBAAQ;AACR,iBAAS,KAAK,2CAA2C;AAAA,MAC7D,OAAO;AACH,iBAAS;AAAA,UACL,SAAS,mBAAmB,MAAM;AAAA,QAAA;AAAA,MAE1C;AAAA,IACJ;AAGA,QAAI,QAAQ,gBAAgB;AACxB,kBAAY,MAAM,KAAK,eAAe,IAAI;AAC1C,UAAI,UAAU,SAAS,GAAG;AACtB,gBAAQ,UAAU,UAAU,UAAU;AACtC,iBAAS,KAAK,GAAG,UAAU,MAAM,uBAAuB;AACxD,mBAAW,YAAY,WAAW;AAC9B,mBAAS,KAAK,OAAO,QAAQ,EAAE;AAAA,QACnC;AAAA,MACJ;AAAA,IACJ;AAGA,UAAM,UAAU,UAAU,YAAY,UAAU;AAEhD,WAAO;AAAA,MACH;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,EAER;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAc,wBACV,MAC8B;AAC9B,QAAI;AACA,YAAM,UAAU,MAAM,SAAS,KAAK,UAAU,OAAO;AACrD,YAAM,WAAkC,CAAA;AAGxC,YAAM,eAAe,QAAQ;AAAA,QACzB;AAAA,MAAA;AAGJ,UAAI,CAAC,cAAc;AACf,eAAO;AAAA,MACX;AAEA,YAAM,UAAU,aAAa,CAAC;AAG9B,YAAM,gBAAgB;AACtB,UAAI;AAEJ,cAAQ,QAAQ,cAAc,KAAK,OAAO,OAAO,MAAM;AACnD,iBAAS,KAAK;AAAA,UACV,MAAM,MAAM,CAAC,EAAE,KAAA;AAAA,UACf,SAAS,MAAM,CAAC,EAAE,kBAAkB;AAAA,UACpC,YAAY,KAAK;AAAA,QAAA,CACpB;AAAA,MACL;AAEA,aAAO;AAAA,IACX,QAAQ;AAEJ,aAAO,CAAA;AAAA,IACX;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAc,eAAe,MAAmC;AAC5D,QAAI;AACA,YAAM,UAAU,MAAM,SAAS,KAAK,UAAU,OAAO;AACrD,YAAM,UAAoB,CAAA;AAG1B,YAAM,eAAe,QAAQ;AAAA,QACzB;AAAA,MAAA;AAGJ,UAAI,CAAC,cAAc;AACf,eAAO;AAAA,MACX;AAEA,YAAM,UAAU,aAAa,CAAC;AAI9B,YAAM,YAAY;AAClB,UAAI;AAEJ,cAAQ,QAAQ,UAAU,KAAK,OAAO,OAAO,MAAM;AAC/C,cAAM,WAAW,MAAM,CAAC,EAAE,KAAA;AAG1B,cAAM,WAAW,QAAQ,QAAQ,KAAK,QAAQ,GAAG,OAAO;AACxD,cAAM,WAAW,QAAQ,UAAU,QAAQ;AAE3C,YAAI;AACA,gBAAM,OAAO,QAAQ;AAAA,QACzB,QAAQ;AACJ,kBAAQ,KAAK,QAAQ;AAAA,QACzB;AAAA,MACJ;AAEA,aAAO;AAAA,IACX,QAAQ;AAEJ,aAAO,CAAA;AAAA,IACX;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,YACI,QACA,SACO;AAEP,QAAI,QAAQ,OAAO;AACf,aAAO;AAAA,IACX;AAGA,QAAI,QAAQ,gBAAgB,YAAY;AACpC,aAAO;AAAA,IACX;AAGA,QAAI,QAAQ,gBAAgB,YAAY,OAAO,UAAU,SAAS;AAC9D,aAAO;AAAA,IACX;AAGA,WAAO;AAAA,EACX;AACJ;ACnPO,MAAM,0BAA0B,MAAM;AAAA,EACzC,YACI,SACO,SACT;AACE,UAAM,OAAO;AAFN,SAAA,UAAA;AAGP,SAAK,OAAO;AACZ,UAAM,kBAAkB,MAAM,KAAK,WAAW;AAAA,EAClD;AACJ;AAKO,MAAM,gCAAgC,kBAAkB;AAAA,EAC3D,YAAY,mBAA0C;AAClD,UAAM,UAAU,YAAY,kBAAkB,MAAM;AACpD,UAAM,UAA8B;AAAA,MAChC,SAAS;AAAA,MACT,OAAO;AAAA,MACP,UAAU;AAAA,QACN;AAAA,QACA,GAAG,kBAAkB,IAAI,CAAC,MAAM,WAAW,EAAE,IAAI,EAAE;AAAA,MAAA;AAAA,MAEvD,oBAAoB;AAAA,IAAA;AAExB,UAAM,SAAS,OAAO;AACtB,SAAK,OAAO;AAAA,EAChB;AACJ;AAKO,MAAM,kCAAkC,kBAAkB;AAAA,EAC7D,YAAY,kBAA4B;AACpC,UAAM,UAAU,YAAY,iBAAiB,MAAM;AACnD,UAAM,UAA8B;AAAA,MAChC,SAAS;AAAA,MACT,OAAO;AAAA,MACP,UAAU;AAAA,QACN;AAAA,QACA,GAAG,iBAAiB,IAAI,CAAC,MAAM,OAAO,CAAC,EAAE;AAAA,MAAA;AAAA,MAE7C,WAAW;AAAA,IAAA;AAEf,UAAM,SAAS,OAAO;AACtB,SAAK,OAAO;AAAA,EAChB;AACJ;"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@kjerneverk/riotplan-verify",
3
+ "version": "1.0.0-dev.0",
4
+ "description": "Step verification and coverage checking for RiotPlan",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "engines": {
15
+ "node": ">=24.0.0"
16
+ },
17
+ "scripts": {
18
+ "clean": "rm -rf dist",
19
+ "build": "npm run lint && vite build",
20
+ "test": "npm run test:coverage",
21
+ "test:coverage": "vitest run --coverage",
22
+ "test:debug": "vitest --run --coverage --reporter verbose",
23
+ "lint": "eslint src",
24
+ "lint:fix": "eslint src --fix",
25
+ "precommit": "npm run build && npm run lint && npm run test",
26
+ "prepublishOnly": "npm run clean && npm run build"
27
+ },
28
+ "keywords": ["riotplan", "verification", "coverage", "acceptance-criteria"],
29
+ "author": "Tim O'Brien <tobrien@discursive.com>",
30
+ "license": "Apache-2.0",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/kjerneverk/riotplan-verify"
34
+ },
35
+ "dependencies": {},
36
+ "devDependencies": {
37
+ "@eslint/eslintrc": "^3.3.1",
38
+ "@eslint/js": "^9.28.0",
39
+ "@types/node": "^24.10.9",
40
+ "@typescript-eslint/eslint-plugin": "^8.34.0",
41
+ "@typescript-eslint/parser": "^8.34.0",
42
+ "@vitest/coverage-v8": "^4.0.17",
43
+ "eslint": "^9.28.0",
44
+ "eslint-plugin-import": "^2.31.0",
45
+ "globals": "^17.0.0",
46
+ "typescript": "^5.8.3",
47
+ "vite": "^7.0.4",
48
+ "vite-plugin-dts": "^4.5.4",
49
+ "vitest": "^4.0.17"
50
+ }
51
+ }