@kynetic-ai/spec 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +263 -0
- package/dist/acp/client.d.ts +159 -0
- package/dist/acp/client.d.ts.map +1 -0
- package/dist/acp/client.js +255 -0
- package/dist/acp/client.js.map +1 -0
- package/dist/acp/framing.d.ts +119 -0
- package/dist/acp/framing.d.ts.map +1 -0
- package/dist/acp/framing.js +302 -0
- package/dist/acp/framing.js.map +1 -0
- package/dist/acp/index.d.ts +14 -0
- package/dist/acp/index.d.ts.map +1 -0
- package/dist/acp/index.js +13 -0
- package/dist/acp/index.js.map +1 -0
- package/dist/acp/types.d.ts +89 -0
- package/dist/acp/types.d.ts.map +1 -0
- package/dist/acp/types.js +99 -0
- package/dist/acp/types.js.map +1 -0
- package/dist/agents/adapters.d.ts +55 -0
- package/dist/agents/adapters.d.ts.map +1 -0
- package/dist/agents/adapters.js +84 -0
- package/dist/agents/adapters.js.map +1 -0
- package/dist/agents/index.d.ts +8 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +10 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/spawner.d.ts +53 -0
- package/dist/agents/spawner.d.ts.map +1 -0
- package/dist/agents/spawner.js +83 -0
- package/dist/agents/spawner.js.map +1 -0
- package/dist/cli/batch.d.ts +82 -0
- package/dist/cli/batch.d.ts.map +1 -0
- package/dist/cli/batch.js +162 -0
- package/dist/cli/batch.js.map +1 -0
- package/dist/cli/commands/clone-for-testing.d.ts +6 -0
- package/dist/cli/commands/clone-for-testing.d.ts.map +1 -0
- package/dist/cli/commands/clone-for-testing.js +176 -0
- package/dist/cli/commands/clone-for-testing.js.map +1 -0
- package/dist/cli/commands/derive.d.ts +6 -0
- package/dist/cli/commands/derive.d.ts.map +1 -0
- package/dist/cli/commands/derive.js +450 -0
- package/dist/cli/commands/derive.js.map +1 -0
- package/dist/cli/commands/help.d.ts +6 -0
- package/dist/cli/commands/help.d.ts.map +1 -0
- package/dist/cli/commands/help.js +196 -0
- package/dist/cli/commands/help.js.map +1 -0
- package/dist/cli/commands/inbox.d.ts +6 -0
- package/dist/cli/commands/inbox.d.ts.map +1 -0
- package/dist/cli/commands/inbox.js +235 -0
- package/dist/cli/commands/inbox.js.map +1 -0
- package/dist/cli/commands/index.d.ts +20 -0
- package/dist/cli/commands/index.d.ts.map +1 -0
- package/dist/cli/commands/index.js +21 -0
- package/dist/cli/commands/index.js.map +1 -0
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +245 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/item.d.ts +6 -0
- package/dist/cli/commands/item.d.ts.map +1 -0
- package/dist/cli/commands/item.js +1311 -0
- package/dist/cli/commands/item.js.map +1 -0
- package/dist/cli/commands/link.d.ts +6 -0
- package/dist/cli/commands/link.d.ts.map +1 -0
- package/dist/cli/commands/link.js +288 -0
- package/dist/cli/commands/link.js.map +1 -0
- package/dist/cli/commands/log.d.ts +16 -0
- package/dist/cli/commands/log.d.ts.map +1 -0
- package/dist/cli/commands/log.js +291 -0
- package/dist/cli/commands/log.js.map +1 -0
- package/dist/cli/commands/meta.d.ts +15 -0
- package/dist/cli/commands/meta.d.ts.map +1 -0
- package/dist/cli/commands/meta.js +1378 -0
- package/dist/cli/commands/meta.js.map +1 -0
- package/dist/cli/commands/module.d.ts +6 -0
- package/dist/cli/commands/module.d.ts.map +1 -0
- package/dist/cli/commands/module.js +102 -0
- package/dist/cli/commands/module.js.map +1 -0
- package/dist/cli/commands/ralph.d.ts +9 -0
- package/dist/cli/commands/ralph.d.ts.map +1 -0
- package/dist/cli/commands/ralph.js +465 -0
- package/dist/cli/commands/ralph.js.map +1 -0
- package/dist/cli/commands/search.d.ts +6 -0
- package/dist/cli/commands/search.d.ts.map +1 -0
- package/dist/cli/commands/search.js +134 -0
- package/dist/cli/commands/search.js.map +1 -0
- package/dist/cli/commands/session.d.ts +164 -0
- package/dist/cli/commands/session.d.ts.map +1 -0
- package/dist/cli/commands/session.js +745 -0
- package/dist/cli/commands/session.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +26 -0
- package/dist/cli/commands/setup.d.ts.map +1 -0
- package/dist/cli/commands/setup.js +586 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/shadow.d.ts +6 -0
- package/dist/cli/commands/shadow.d.ts.map +1 -0
- package/dist/cli/commands/shadow.js +299 -0
- package/dist/cli/commands/shadow.js.map +1 -0
- package/dist/cli/commands/task.d.ts +6 -0
- package/dist/cli/commands/task.d.ts.map +1 -0
- package/dist/cli/commands/task.js +1514 -0
- package/dist/cli/commands/task.js.map +1 -0
- package/dist/cli/commands/tasks.d.ts +6 -0
- package/dist/cli/commands/tasks.d.ts.map +1 -0
- package/dist/cli/commands/tasks.js +347 -0
- package/dist/cli/commands/tasks.js.map +1 -0
- package/dist/cli/commands/trait.d.ts +10 -0
- package/dist/cli/commands/trait.d.ts.map +1 -0
- package/dist/cli/commands/trait.js +295 -0
- package/dist/cli/commands/trait.js.map +1 -0
- package/dist/cli/commands/validate.d.ts +6 -0
- package/dist/cli/commands/validate.d.ts.map +1 -0
- package/dist/cli/commands/validate.js +626 -0
- package/dist/cli/commands/validate.js.map +1 -0
- package/dist/cli/exit-codes.d.ts +62 -0
- package/dist/cli/exit-codes.d.ts.map +1 -0
- package/dist/cli/exit-codes.js +65 -0
- package/dist/cli/exit-codes.js.map +1 -0
- package/dist/cli/help/content.d.ts +35 -0
- package/dist/cli/help/content.d.ts.map +1 -0
- package/dist/cli/help/content.js +312 -0
- package/dist/cli/help/content.js.map +1 -0
- package/dist/cli/index.d.ts +5 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +85 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/introspection.d.ts +87 -0
- package/dist/cli/introspection.d.ts.map +1 -0
- package/dist/cli/introspection.js +127 -0
- package/dist/cli/introspection.js.map +1 -0
- package/dist/cli/output.d.ts +56 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +467 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/suggest.d.ts +16 -0
- package/dist/cli/suggest.d.ts.map +1 -0
- package/dist/cli/suggest.js +72 -0
- package/dist/cli/suggest.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/alignment.d.ts +113 -0
- package/dist/parser/alignment.d.ts.map +1 -0
- package/dist/parser/alignment.js +261 -0
- package/dist/parser/alignment.js.map +1 -0
- package/dist/parser/assess.d.ts +81 -0
- package/dist/parser/assess.d.ts.map +1 -0
- package/dist/parser/assess.js +197 -0
- package/dist/parser/assess.js.map +1 -0
- package/dist/parser/convention-validation.d.ts +48 -0
- package/dist/parser/convention-validation.d.ts.map +1 -0
- package/dist/parser/convention-validation.js +167 -0
- package/dist/parser/convention-validation.js.map +1 -0
- package/dist/parser/fix.d.ts +38 -0
- package/dist/parser/fix.d.ts.map +1 -0
- package/dist/parser/fix.js +185 -0
- package/dist/parser/fix.js.map +1 -0
- package/dist/parser/index.d.ts +12 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +13 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/parser/items.d.ts +138 -0
- package/dist/parser/items.d.ts.map +1 -0
- package/dist/parser/items.js +321 -0
- package/dist/parser/items.js.map +1 -0
- package/dist/parser/meta.d.ts +120 -0
- package/dist/parser/meta.d.ts.map +1 -0
- package/dist/parser/meta.js +441 -0
- package/dist/parser/meta.js.map +1 -0
- package/dist/parser/refs.d.ts +185 -0
- package/dist/parser/refs.d.ts.map +1 -0
- package/dist/parser/refs.js +404 -0
- package/dist/parser/refs.js.map +1 -0
- package/dist/parser/shadow.d.ts +253 -0
- package/dist/parser/shadow.d.ts.map +1 -0
- package/dist/parser/shadow.js +1053 -0
- package/dist/parser/shadow.js.map +1 -0
- package/dist/parser/traits.d.ts +72 -0
- package/dist/parser/traits.d.ts.map +1 -0
- package/dist/parser/traits.js +120 -0
- package/dist/parser/traits.js.map +1 -0
- package/dist/parser/validate.d.ts +89 -0
- package/dist/parser/validate.d.ts.map +1 -0
- package/dist/parser/validate.js +817 -0
- package/dist/parser/validate.js.map +1 -0
- package/dist/parser/yaml.d.ts +326 -0
- package/dist/parser/yaml.d.ts.map +1 -0
- package/dist/parser/yaml.js +1383 -0
- package/dist/parser/yaml.js.map +1 -0
- package/dist/ralph/cli-renderer.d.ts +20 -0
- package/dist/ralph/cli-renderer.d.ts.map +1 -0
- package/dist/ralph/cli-renderer.js +179 -0
- package/dist/ralph/cli-renderer.js.map +1 -0
- package/dist/ralph/events.d.ts +65 -0
- package/dist/ralph/events.d.ts.map +1 -0
- package/dist/ralph/events.js +397 -0
- package/dist/ralph/events.js.map +1 -0
- package/dist/ralph/index.d.ts +8 -0
- package/dist/ralph/index.d.ts.map +1 -0
- package/dist/ralph/index.js +10 -0
- package/dist/ralph/index.js.map +1 -0
- package/dist/schema/common.d.ts +46 -0
- package/dist/schema/common.d.ts.map +1 -0
- package/dist/schema/common.js +71 -0
- package/dist/schema/common.js.map +1 -0
- package/dist/schema/inbox.d.ts +90 -0
- package/dist/schema/inbox.d.ts.map +1 -0
- package/dist/schema/inbox.js +30 -0
- package/dist/schema/inbox.js.map +1 -0
- package/dist/schema/index.d.ts +6 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +7 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/meta.d.ts +762 -0
- package/dist/schema/meta.d.ts.map +1 -0
- package/dist/schema/meta.js +144 -0
- package/dist/schema/meta.js.map +1 -0
- package/dist/schema/spec.d.ts +912 -0
- package/dist/schema/spec.d.ts.map +1 -0
- package/dist/schema/spec.js +104 -0
- package/dist/schema/spec.js.map +1 -0
- package/dist/schema/task.d.ts +664 -0
- package/dist/schema/task.d.ts.map +1 -0
- package/dist/schema/task.js +130 -0
- package/dist/schema/task.js.map +1 -0
- package/dist/sessions/index.d.ts +11 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/sessions/index.js +13 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/sessions/store.d.ts +144 -0
- package/dist/sessions/store.d.ts.map +1 -0
- package/dist/sessions/store.js +325 -0
- package/dist/sessions/store.js.map +1 -0
- package/dist/sessions/types.d.ts +157 -0
- package/dist/sessions/types.d.ts.map +1 -0
- package/dist/sessions/types.js +90 -0
- package/dist/sessions/types.js.map +1 -0
- package/dist/strings/errors.d.ts +420 -0
- package/dist/strings/errors.d.ts.map +1 -0
- package/dist/strings/errors.js +282 -0
- package/dist/strings/errors.js.map +1 -0
- package/dist/strings/guidance.d.ts +65 -0
- package/dist/strings/guidance.d.ts.map +1 -0
- package/dist/strings/guidance.js +66 -0
- package/dist/strings/guidance.js.map +1 -0
- package/dist/strings/index.d.ts +12 -0
- package/dist/strings/index.d.ts.map +1 -0
- package/dist/strings/index.js +12 -0
- package/dist/strings/index.js.map +1 -0
- package/dist/strings/labels.d.ts +74 -0
- package/dist/strings/labels.d.ts.map +1 -0
- package/dist/strings/labels.js +75 -0
- package/dist/strings/labels.js.map +1 -0
- package/dist/strings/validation.d.ts +126 -0
- package/dist/strings/validation.d.ts.map +1 -0
- package/dist/strings/validation.js +135 -0
- package/dist/strings/validation.js.map +1 -0
- package/dist/utils/commit.d.ts +23 -0
- package/dist/utils/commit.d.ts.map +1 -0
- package/dist/utils/commit.js +67 -0
- package/dist/utils/commit.js.map +1 -0
- package/dist/utils/git.d.ts +57 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +192 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/grep.d.ts +28 -0
- package/dist/utils/grep.d.ts.map +1 -0
- package/dist/utils/grep.js +86 -0
- package/dist/utils/grep.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/time.d.ts +18 -0
- package/dist/utils/time.d.ts.map +1 -0
- package/dist/utils/time.js +61 -0
- package/dist/utils/time.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { initContext, validate, loadAllTasks, loadAllItems, AlignmentIndex, ReferenceIndex, fixFiles, findTaskFiles, expandIncludePattern, loadMetaContext, validateConventions, } from '../../parser/index.js';
|
|
4
|
+
import { output, error } from '../output.js';
|
|
5
|
+
import { validation as validationStrings } from '../../strings/index.js';
|
|
6
|
+
import { EXIT_CODES } from '../exit-codes.js';
|
|
7
|
+
/**
|
|
8
|
+
* Check for stale status mismatches between specs and tasks
|
|
9
|
+
* AC: @stale-status-detection ac-1, ac-2, ac-3
|
|
10
|
+
*/
|
|
11
|
+
function checkStaleness(items, tasks, refIndex) {
|
|
12
|
+
const warnings = [];
|
|
13
|
+
// AC: @stale-status-detection ac-1 (parent-pending-children-done)
|
|
14
|
+
// Check if task with dependencies is pending but all dependencies are completed
|
|
15
|
+
for (const task of tasks) {
|
|
16
|
+
// Only check pending/in_progress tasks with dependencies
|
|
17
|
+
if (task.status !== 'pending' && task.status !== 'in_progress')
|
|
18
|
+
continue;
|
|
19
|
+
if (!task.depends_on || task.depends_on.length === 0)
|
|
20
|
+
continue;
|
|
21
|
+
// Resolve all dependency tasks
|
|
22
|
+
const depTasks = task.depends_on
|
|
23
|
+
.map(depRef => {
|
|
24
|
+
const result = refIndex.resolve(depRef);
|
|
25
|
+
if (!result.ok)
|
|
26
|
+
return null;
|
|
27
|
+
return tasks.find(t => t._ulid === result.ulid);
|
|
28
|
+
})
|
|
29
|
+
.filter((t) => t !== null);
|
|
30
|
+
if (depTasks.length === 0)
|
|
31
|
+
continue;
|
|
32
|
+
// Check if all dependencies are completed and their linked specs are implemented
|
|
33
|
+
const allDepsDone = depTasks.every(depTask => {
|
|
34
|
+
if (depTask.status !== 'completed')
|
|
35
|
+
return false;
|
|
36
|
+
// If the dep task has a spec_ref, check if that spec is implemented
|
|
37
|
+
if (depTask.spec_ref) {
|
|
38
|
+
const result = refIndex.resolve(depTask.spec_ref);
|
|
39
|
+
if (!result.ok)
|
|
40
|
+
return true; // Missing spec ref doesn't block
|
|
41
|
+
const spec = items.find(item => item._ulid === result.ulid);
|
|
42
|
+
return spec?.status?.implementation === 'implemented';
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
});
|
|
46
|
+
if (allDepsDone) {
|
|
47
|
+
const taskRef = task.slugs[0] || refIndex.shortUlid(task._ulid);
|
|
48
|
+
warnings.push({
|
|
49
|
+
type: 'parent-pending-children-done',
|
|
50
|
+
message: `Task @${taskRef} is ${task.status} but all dependencies are completed. Consider completing or reviewing.`,
|
|
51
|
+
refs: [task._ulid],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// AC: @stale-status-detection ac-2 (spec-implemented-no-task)
|
|
56
|
+
// Check if spec is implemented but has no completed tasks
|
|
57
|
+
for (const item of items) {
|
|
58
|
+
if (item.status?.implementation !== 'implemented')
|
|
59
|
+
continue;
|
|
60
|
+
// Find completed tasks that reference this spec
|
|
61
|
+
const completedTasks = tasks.filter(task => {
|
|
62
|
+
if (task.status !== 'completed' || !task.spec_ref)
|
|
63
|
+
return false;
|
|
64
|
+
const result = refIndex.resolve(task.spec_ref);
|
|
65
|
+
return result.ok && result.ulid === item._ulid;
|
|
66
|
+
});
|
|
67
|
+
if (completedTasks.length === 0) {
|
|
68
|
+
const specRef = item.slugs[0] || refIndex.shortUlid(item._ulid);
|
|
69
|
+
warnings.push({
|
|
70
|
+
type: 'spec-implemented-no-task',
|
|
71
|
+
message: `Spec @${specRef} is implemented but has no completed tasks. Verify implementation or link existing task.`,
|
|
72
|
+
refs: [item._ulid],
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// AC: @stale-status-detection ac-3 (task-done-spec-not-started)
|
|
77
|
+
// Check if task is completed but spec is still not_started
|
|
78
|
+
for (const task of tasks) {
|
|
79
|
+
if (task.status !== 'completed')
|
|
80
|
+
continue;
|
|
81
|
+
if (!task.spec_ref)
|
|
82
|
+
continue;
|
|
83
|
+
// Resolve spec reference
|
|
84
|
+
const result = refIndex.resolve(task.spec_ref);
|
|
85
|
+
if (!result.ok)
|
|
86
|
+
continue;
|
|
87
|
+
const spec = items.find(item => item._ulid === result.ulid);
|
|
88
|
+
if (!spec)
|
|
89
|
+
continue;
|
|
90
|
+
if (spec.status?.implementation === 'not_started') {
|
|
91
|
+
const taskRef = task.slugs[0] || refIndex.shortUlid(task._ulid);
|
|
92
|
+
const specRef = spec.slugs[0] || refIndex.shortUlid(spec._ulid);
|
|
93
|
+
warnings.push({
|
|
94
|
+
type: 'task-done-spec-not-started',
|
|
95
|
+
message: `Task @${taskRef} completed but spec @${specRef} is not_started. Update spec status.`,
|
|
96
|
+
refs: [task._ulid, spec._ulid],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return warnings;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Format staleness warnings for display
|
|
104
|
+
* AC: @stale-status-detection ac-4
|
|
105
|
+
*/
|
|
106
|
+
function formatStalenessWarnings(warnings, verbose) {
|
|
107
|
+
if (warnings.length === 0) {
|
|
108
|
+
console.log(chalk.green('Staleness: OK'));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
console.log(chalk.yellow(`\nStaleness warnings: ${warnings.length}`));
|
|
112
|
+
// Group by type
|
|
113
|
+
const parentPending = warnings.filter(w => w.type === 'parent-pending-children-done');
|
|
114
|
+
const specNoTask = warnings.filter(w => w.type === 'spec-implemented-no-task');
|
|
115
|
+
const taskDoneSpecNot = warnings.filter(w => w.type === 'task-done-spec-not-started');
|
|
116
|
+
if (parentPending.length > 0) {
|
|
117
|
+
console.log(chalk.yellow(` Parent pending, children done: ${parentPending.length}`));
|
|
118
|
+
const shown = verbose ? parentPending : parentPending.slice(0, 3);
|
|
119
|
+
for (const w of shown) {
|
|
120
|
+
console.log(chalk.yellow(` ! ${w.message}`));
|
|
121
|
+
}
|
|
122
|
+
if (!verbose && parentPending.length > 3) {
|
|
123
|
+
console.log(chalk.gray(` ... and ${parentPending.length - 3} more`));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (specNoTask.length > 0) {
|
|
127
|
+
console.log(chalk.yellow(` Spec implemented, no task: ${specNoTask.length}`));
|
|
128
|
+
const shown = verbose ? specNoTask : specNoTask.slice(0, 3);
|
|
129
|
+
for (const w of shown) {
|
|
130
|
+
console.log(chalk.yellow(` ! ${w.message}`));
|
|
131
|
+
}
|
|
132
|
+
if (!verbose && specNoTask.length > 3) {
|
|
133
|
+
console.log(chalk.gray(` ... and ${specNoTask.length - 3} more`));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (taskDoneSpecNot.length > 0) {
|
|
137
|
+
console.log(chalk.yellow(` Task done, spec not started: ${taskDoneSpecNot.length}`));
|
|
138
|
+
const shown = verbose ? taskDoneSpecNot : taskDoneSpecNot.slice(0, 3);
|
|
139
|
+
for (const w of shown) {
|
|
140
|
+
console.log(chalk.yellow(` ! ${w.message}`));
|
|
141
|
+
}
|
|
142
|
+
if (!verbose && taskDoneSpecNot.length > 3) {
|
|
143
|
+
console.log(chalk.gray(` ... and ${taskDoneSpecNot.length - 3} more`));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Format convention validation results for display
|
|
149
|
+
* AC: @convention-definitions ac-3, ac-4
|
|
150
|
+
*/
|
|
151
|
+
function formatConventionValidationResult(result) {
|
|
152
|
+
if (result.valid && result.skipped.length === 0) {
|
|
153
|
+
console.log(chalk.green('Conventions: OK'));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// AC: @convention-definitions ac-4
|
|
157
|
+
// Skipped prose conventions
|
|
158
|
+
if (result.skipped.length > 0) {
|
|
159
|
+
for (const domain of result.skipped) {
|
|
160
|
+
console.log(chalk.gray(`ℹ Skipping prose convention: ${domain}`));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// AC: @convention-definitions ac-3
|
|
164
|
+
// Validation errors
|
|
165
|
+
if (result.errors.length > 0) {
|
|
166
|
+
console.log(chalk.red(`\nConvention violations: ${result.errors.length}`));
|
|
167
|
+
for (const err of result.errors) {
|
|
168
|
+
console.log(chalk.red(` ✗ ${err.domain}`));
|
|
169
|
+
console.log(chalk.gray(` ${err.message}`));
|
|
170
|
+
if (err.expected) {
|
|
171
|
+
console.log(chalk.gray(` Expected: ${err.expected}`));
|
|
172
|
+
}
|
|
173
|
+
if (err.location) {
|
|
174
|
+
console.log(chalk.gray(` Location: ${err.location}`));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
console.log(chalk.green('\nConventions: OK'));
|
|
180
|
+
}
|
|
181
|
+
// Stats
|
|
182
|
+
console.log(chalk.gray(`\nConventions checked: ${result.stats.conventionsChecked}`));
|
|
183
|
+
console.log(chalk.gray(`Conventions skipped: ${result.stats.conventionsSkipped}`));
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Format completeness warnings for display
|
|
187
|
+
* AC: @spec-completeness ac-4
|
|
188
|
+
*/
|
|
189
|
+
function formatCompletenessWarnings(warnings, verbose) {
|
|
190
|
+
if (warnings.length === 0) {
|
|
191
|
+
console.log(chalk.green('Completeness: OK'));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
console.log(chalk.yellow(`\nCompleteness warnings: ${warnings.length}`));
|
|
195
|
+
// Group by type
|
|
196
|
+
const missingAC = warnings.filter(w => w.type === 'missing_acceptance_criteria');
|
|
197
|
+
const missingDesc = warnings.filter(w => w.type === 'missing_description');
|
|
198
|
+
const statusMismatch = warnings.filter(w => w.type === 'status_inconsistency');
|
|
199
|
+
const missingTestCoverage = warnings.filter(w => w.type === 'missing_test_coverage');
|
|
200
|
+
const automationNoSpec = warnings.filter(w => w.type === 'automation_eligible_no_spec');
|
|
201
|
+
// AC: @spec-completeness ac-4
|
|
202
|
+
// Show summary with counts by issue type
|
|
203
|
+
if (missingAC.length > 0) {
|
|
204
|
+
console.log(chalk.yellow(` Missing acceptance criteria: ${missingAC.length}`));
|
|
205
|
+
const shown = verbose ? missingAC : missingAC.slice(0, 3);
|
|
206
|
+
for (const w of shown) {
|
|
207
|
+
console.log(chalk.gray(` ○ ${w.itemRef} - ${w.itemTitle}`));
|
|
208
|
+
}
|
|
209
|
+
if (!verbose && missingAC.length > 3) {
|
|
210
|
+
console.log(chalk.gray(` ... and ${missingAC.length - 3} more`));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (missingDesc.length > 0) {
|
|
214
|
+
console.log(chalk.yellow(` Missing descriptions: ${missingDesc.length}`));
|
|
215
|
+
const shown = verbose ? missingDesc : missingDesc.slice(0, 3);
|
|
216
|
+
for (const w of shown) {
|
|
217
|
+
console.log(chalk.gray(` ○ ${w.itemRef} - ${w.itemTitle}`));
|
|
218
|
+
}
|
|
219
|
+
if (!verbose && missingDesc.length > 3) {
|
|
220
|
+
console.log(chalk.gray(` ... and ${missingDesc.length - 3} more`));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (statusMismatch.length > 0) {
|
|
224
|
+
console.log(chalk.yellow(` Status inconsistencies: ${statusMismatch.length}`));
|
|
225
|
+
for (const w of statusMismatch) {
|
|
226
|
+
console.log(chalk.yellow(` ! ${w.message}`));
|
|
227
|
+
if (w.details) {
|
|
228
|
+
console.log(chalk.gray(` ${w.details}`));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (missingTestCoverage.length > 0) {
|
|
233
|
+
console.log(chalk.yellow(` Missing test coverage: ${missingTestCoverage.length}`));
|
|
234
|
+
const shown = verbose ? missingTestCoverage : missingTestCoverage.slice(0, 3);
|
|
235
|
+
for (const w of shown) {
|
|
236
|
+
console.log(chalk.yellow(` ! ${w.itemRef} - ${w.itemTitle}`));
|
|
237
|
+
if (w.details) {
|
|
238
|
+
console.log(chalk.gray(` ${w.details}`));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (!verbose && missingTestCoverage.length > 3) {
|
|
242
|
+
console.log(chalk.gray(` ... and ${missingTestCoverage.length - 3} more`));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// AC: @task-automation-eligibility ac-21, ac-23
|
|
246
|
+
if (automationNoSpec.length > 0) {
|
|
247
|
+
console.log(chalk.yellow(` Automation without spec: ${automationNoSpec.length}`));
|
|
248
|
+
const shown = verbose ? automationNoSpec : automationNoSpec.slice(0, 3);
|
|
249
|
+
for (const w of shown) {
|
|
250
|
+
console.log(chalk.yellow(` ! ${w.itemRef} - ${w.itemTitle}`));
|
|
251
|
+
console.log(chalk.gray(` ${w.message}`));
|
|
252
|
+
}
|
|
253
|
+
if (!verbose && automationNoSpec.length > 3) {
|
|
254
|
+
console.log(chalk.gray(` ... and ${automationNoSpec.length - 3} more`));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Format alignment warnings for display
|
|
260
|
+
*/
|
|
261
|
+
function formatAlignmentWarnings(warnings, verbose) {
|
|
262
|
+
if (warnings.length === 0) {
|
|
263
|
+
console.log(chalk.green('Alignment: OK'));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
console.log(chalk.yellow(`\nAlignment warnings: ${warnings.length}`));
|
|
267
|
+
// Group by type
|
|
268
|
+
const orphaned = warnings.filter(w => w.type === 'orphaned_spec');
|
|
269
|
+
const mismatches = warnings.filter(w => w.type === 'status_mismatch');
|
|
270
|
+
const stale = warnings.filter(w => w.type === 'stale_implementation');
|
|
271
|
+
if (orphaned.length > 0) {
|
|
272
|
+
console.log(chalk.yellow(` Orphaned specs (no tasks): ${orphaned.length}`));
|
|
273
|
+
const shown = verbose ? orphaned : orphaned.slice(0, 3);
|
|
274
|
+
for (const w of shown) {
|
|
275
|
+
console.log(chalk.gray(` ○ ${w.specTitle}`));
|
|
276
|
+
}
|
|
277
|
+
if (!verbose && orphaned.length > 3) {
|
|
278
|
+
console.log(chalk.gray(` ... and ${orphaned.length - 3} more`));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (mismatches.length > 0) {
|
|
282
|
+
console.log(chalk.yellow(` Status mismatches: ${mismatches.length}`));
|
|
283
|
+
for (const w of mismatches) {
|
|
284
|
+
console.log(chalk.yellow(` ! ${w.specTitle}`));
|
|
285
|
+
console.log(chalk.gray(` ${w.message}`));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (stale.length > 0) {
|
|
289
|
+
console.log(chalk.yellow(` Stale implementation status: ${stale.length}`));
|
|
290
|
+
for (const w of stale) {
|
|
291
|
+
console.log(chalk.yellow(` ! ${w.message}`));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Format fix results for display
|
|
297
|
+
*/
|
|
298
|
+
function formatFixResult(result) {
|
|
299
|
+
if (result.fixesApplied.length === 0) {
|
|
300
|
+
console.log(chalk.gray('\nNo auto-fixable issues found.'));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
console.log(chalk.cyan(`\n✓ Applied ${result.fixesApplied.length} fix(es) to ${result.filesModified} file(s):`));
|
|
304
|
+
for (const fix of result.fixesApplied) {
|
|
305
|
+
const typeLabel = {
|
|
306
|
+
ulid_regenerated: 'ULID regenerated',
|
|
307
|
+
timestamp_added: 'Timestamp added',
|
|
308
|
+
status_added: 'Status added',
|
|
309
|
+
}[fix.type];
|
|
310
|
+
const shortFile = path.basename(fix.file);
|
|
311
|
+
console.log(chalk.cyan(` ✓ ${shortFile}:${fix.path} - ${typeLabel}`));
|
|
312
|
+
}
|
|
313
|
+
if (result.errors.length > 0) {
|
|
314
|
+
console.log(chalk.yellow(`\nFix errors: ${result.errors.length}`));
|
|
315
|
+
for (const err of result.errors) {
|
|
316
|
+
console.log(chalk.yellow(` ! ${err.file}: ${err.message}`));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Collect all files that can be fixed
|
|
322
|
+
*/
|
|
323
|
+
async function collectFixableFiles(ctx) {
|
|
324
|
+
const files = [];
|
|
325
|
+
// Task files (exclude test fixtures)
|
|
326
|
+
const taskFiles = await findTaskFiles(ctx.rootDir);
|
|
327
|
+
const specTaskFiles = await findTaskFiles(path.join(ctx.rootDir, 'spec'));
|
|
328
|
+
const allTaskFiles = [...new Set([...taskFiles, ...specTaskFiles])];
|
|
329
|
+
files.push(...allTaskFiles.filter(f => !f.includes('fixtures') && !f.includes('test')));
|
|
330
|
+
// Spec files from includes
|
|
331
|
+
if (ctx.manifest && ctx.manifestPath) {
|
|
332
|
+
const manifestDir = path.dirname(ctx.manifestPath);
|
|
333
|
+
const includes = ctx.manifest.includes || [];
|
|
334
|
+
for (const include of includes) {
|
|
335
|
+
const expandedPaths = await expandIncludePattern(include, manifestDir);
|
|
336
|
+
files.push(...expandedPaths);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Inbox file
|
|
340
|
+
const inboxPath = path.join(ctx.rootDir, 'spec', 'kynetic.inbox.yaml');
|
|
341
|
+
try {
|
|
342
|
+
await import('node:fs/promises').then(fs => fs.access(inboxPath));
|
|
343
|
+
files.push(inboxPath);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// Inbox file doesn't exist, skip
|
|
347
|
+
}
|
|
348
|
+
return [...new Set(files)];
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Format validation result for display
|
|
352
|
+
*/
|
|
353
|
+
function formatValidationResult(result, verbose) {
|
|
354
|
+
// Header
|
|
355
|
+
if (result.valid) {
|
|
356
|
+
console.log(chalk.green.bold('✓ Validation passed'));
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
console.log(chalk.red.bold('✗ Validation failed'));
|
|
360
|
+
}
|
|
361
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
362
|
+
console.log(`Files checked: ${result.stats.filesChecked}`);
|
|
363
|
+
console.log(`Items checked: ${result.stats.itemsChecked}`);
|
|
364
|
+
console.log(`Tasks checked: ${result.stats.tasksChecked}`);
|
|
365
|
+
// AC-meta-manifest-2: Display meta summary line
|
|
366
|
+
if (result.metaStats) {
|
|
367
|
+
console.log(`Meta: ${result.metaStats.agents} agents, ${result.metaStats.workflows} workflows, ${result.metaStats.conventions} conventions`);
|
|
368
|
+
}
|
|
369
|
+
// Schema errors
|
|
370
|
+
if (result.schemaErrors.length > 0) {
|
|
371
|
+
console.log(chalk.red(`\nSchema errors: ${result.schemaErrors.length}`));
|
|
372
|
+
for (const err of result.schemaErrors) {
|
|
373
|
+
const location = err.path ? `${err.file}:${err.path}` : err.file;
|
|
374
|
+
console.log(chalk.red(` ✗ ${location}`));
|
|
375
|
+
console.log(chalk.gray(` ${err.message}`));
|
|
376
|
+
if (verbose && err.details) {
|
|
377
|
+
console.log(chalk.gray(` ${JSON.stringify(err.details)}`));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
console.log(chalk.green('\nSchema: OK'));
|
|
383
|
+
}
|
|
384
|
+
// Reference errors
|
|
385
|
+
if (result.refErrors.length > 0) {
|
|
386
|
+
console.log(chalk.red(`\nReference errors: ${result.refErrors.length}`));
|
|
387
|
+
for (const err of result.refErrors) {
|
|
388
|
+
const location = err.sourceFile
|
|
389
|
+
? `${err.sourceFile} (${err.field})`
|
|
390
|
+
: `${err.sourceUlid?.slice(0, 8)} (${err.field})`;
|
|
391
|
+
console.log(chalk.red(` ✗ ${err.ref}`));
|
|
392
|
+
console.log(chalk.gray(` ${err.message}`));
|
|
393
|
+
console.log(chalk.gray(` in: ${location}`));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
console.log(chalk.green('References: OK'));
|
|
398
|
+
}
|
|
399
|
+
// Reference warnings (deprecated targets)
|
|
400
|
+
if (result.refWarnings.length > 0) {
|
|
401
|
+
console.log(chalk.yellow(`\nReference warnings: ${result.refWarnings.length}`));
|
|
402
|
+
const shown = verbose ? result.refWarnings : result.refWarnings.slice(0, 5);
|
|
403
|
+
for (const warn of shown) {
|
|
404
|
+
const location = warn.sourceFile
|
|
405
|
+
? `${warn.sourceFile} (${warn.field})`
|
|
406
|
+
: `${warn.sourceUlid?.slice(0, 8)} (${warn.field})`;
|
|
407
|
+
console.log(chalk.yellow(` ⚠ ${warn.ref}`));
|
|
408
|
+
console.log(chalk.gray(` ${warn.message}`));
|
|
409
|
+
console.log(chalk.gray(` in: ${location}`));
|
|
410
|
+
}
|
|
411
|
+
if (!verbose && result.refWarnings.length > 5) {
|
|
412
|
+
console.log(chalk.gray(` ... and ${result.refWarnings.length - 5} more (use -v to see all)`));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
// AC: @trait-edge-cases ac-2
|
|
416
|
+
// Trait cycle errors
|
|
417
|
+
if (result.traitCycleErrors.length > 0) {
|
|
418
|
+
console.log(chalk.red(`\nTrait cycle errors: ${result.traitCycleErrors.length}`));
|
|
419
|
+
for (const err of result.traitCycleErrors) {
|
|
420
|
+
console.log(chalk.red(` ✗ ${err.traitRef} - ${err.traitTitle}`));
|
|
421
|
+
console.log(chalk.gray(` ${err.message}`));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// Orphans (warnings, not errors)
|
|
425
|
+
if (result.orphans.length > 0) {
|
|
426
|
+
console.log(chalk.yellow(`\nOrphans (not referenced): ${result.orphans.length}`));
|
|
427
|
+
if (verbose) {
|
|
428
|
+
for (const orphan of result.orphans) {
|
|
429
|
+
console.log(chalk.yellow(` ○ ${orphan.ulid.slice(0, 8)} [${orphan.type}] ${orphan.title}`));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
// Show first few
|
|
434
|
+
const shown = result.orphans.slice(0, 5);
|
|
435
|
+
for (const orphan of shown) {
|
|
436
|
+
console.log(chalk.yellow(` ○ ${orphan.ulid.slice(0, 8)} [${orphan.type}] ${orphan.title}`));
|
|
437
|
+
}
|
|
438
|
+
if (result.orphans.length > 5) {
|
|
439
|
+
console.log(chalk.gray(` ... and ${result.orphans.length - 5} more (use -v to see all)`));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Register validate command
|
|
446
|
+
*/
|
|
447
|
+
export function registerValidateCommand(program) {
|
|
448
|
+
program
|
|
449
|
+
.command('validate')
|
|
450
|
+
.description('Validate spec files')
|
|
451
|
+
.option('--schema', 'Check schema conformance only')
|
|
452
|
+
.option('--refs', 'Check reference resolution only')
|
|
453
|
+
.option('--orphans', 'Find orphaned items only')
|
|
454
|
+
.option('--alignment', 'Check spec-task alignment')
|
|
455
|
+
.option('--completeness', 'Check spec completeness (missing AC, descriptions, status inconsistencies)')
|
|
456
|
+
.option('--conventions', 'Validate conventions')
|
|
457
|
+
.option('--staleness', 'Check for stale status mismatches between specs and tasks')
|
|
458
|
+
.option('--fix', 'Auto-fix issues where possible (invalid ULIDs, missing timestamps)')
|
|
459
|
+
.option('-v, --verbose', 'Show detailed output')
|
|
460
|
+
.option('--strict', 'Treat orphans and staleness warnings as errors')
|
|
461
|
+
.action(async (options) => {
|
|
462
|
+
try {
|
|
463
|
+
const ctx = await initContext();
|
|
464
|
+
if (!ctx.manifestPath) {
|
|
465
|
+
error(validationStrings.noManifest);
|
|
466
|
+
console.log(validationStrings.initHint);
|
|
467
|
+
process.exit(EXIT_CODES.ERROR);
|
|
468
|
+
}
|
|
469
|
+
// Determine which checks to run
|
|
470
|
+
const runAll = !options.schema && !options.refs && !options.orphans && !options.alignment && !options.completeness && !options.conventions;
|
|
471
|
+
const validateOptions = {
|
|
472
|
+
schema: runAll || options.schema,
|
|
473
|
+
refs: runAll || options.refs,
|
|
474
|
+
orphans: runAll || options.orphans,
|
|
475
|
+
completeness: runAll || options.completeness,
|
|
476
|
+
};
|
|
477
|
+
const result = await validate(ctx, validateOptions);
|
|
478
|
+
// In strict mode, orphans are errors
|
|
479
|
+
if (options.strict && result.orphans.length > 0) {
|
|
480
|
+
result.valid = false;
|
|
481
|
+
}
|
|
482
|
+
output(result, () => formatValidationResult(result, options.verbose));
|
|
483
|
+
// Run auto-fix if requested
|
|
484
|
+
if (options.fix) {
|
|
485
|
+
const filesToFix = await collectFixableFiles(ctx);
|
|
486
|
+
const fixResult = await fixFiles(filesToFix);
|
|
487
|
+
formatFixResult(fixResult);
|
|
488
|
+
// Re-run validation after fixes to show updated status
|
|
489
|
+
if (fixResult.fixesApplied.length > 0) {
|
|
490
|
+
console.log(validationStrings.revalidating);
|
|
491
|
+
const revalidateResult = await validate(ctx, validateOptions);
|
|
492
|
+
if (revalidateResult.valid) {
|
|
493
|
+
console.log(validationStrings.nowPasses);
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
console.log(validationStrings.issuesRemain);
|
|
497
|
+
}
|
|
498
|
+
// Update result for exit code
|
|
499
|
+
result.valid = revalidateResult.valid;
|
|
500
|
+
result.schemaErrors = revalidateResult.schemaErrors;
|
|
501
|
+
result.refErrors = revalidateResult.refErrors;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// Run alignment check if requested or running all checks
|
|
505
|
+
if (options.alignment || runAll) {
|
|
506
|
+
const tasks = await loadAllTasks(ctx);
|
|
507
|
+
const items = await loadAllItems(ctx);
|
|
508
|
+
const refIndex = new ReferenceIndex(tasks, items);
|
|
509
|
+
const alignmentIndex = new AlignmentIndex(tasks, items);
|
|
510
|
+
alignmentIndex.buildLinks(refIndex);
|
|
511
|
+
const alignmentWarnings = alignmentIndex.findAlignmentWarnings();
|
|
512
|
+
formatAlignmentWarnings(alignmentWarnings, options.verbose);
|
|
513
|
+
// Show alignment stats
|
|
514
|
+
const stats = alignmentIndex.getStats();
|
|
515
|
+
console.log(validationStrings.alignmentStats(stats.specsWithTasks, stats.totalSpecs, stats.alignedSpecs));
|
|
516
|
+
}
|
|
517
|
+
// Show completeness warnings if any
|
|
518
|
+
// AC: @spec-completeness ac-4
|
|
519
|
+
if (result.completenessWarnings.length > 0) {
|
|
520
|
+
formatCompletenessWarnings(result.completenessWarnings, options.verbose);
|
|
521
|
+
}
|
|
522
|
+
// Run convention validation if requested
|
|
523
|
+
// AC: @convention-definitions ac-3, ac-4
|
|
524
|
+
if (options.conventions) {
|
|
525
|
+
try {
|
|
526
|
+
const metaCtx = await loadMetaContext(ctx);
|
|
527
|
+
if (metaCtx && metaCtx.conventions.length > 0) {
|
|
528
|
+
// For now, we just validate that conventions are well-formed
|
|
529
|
+
// Full validation against actual content (commits, notes, etc.)
|
|
530
|
+
// would require additional content gathering logic
|
|
531
|
+
const conventionResult = validateConventions(metaCtx.conventions, {});
|
|
532
|
+
formatConventionValidationResult(conventionResult);
|
|
533
|
+
if (!conventionResult.valid) {
|
|
534
|
+
result.valid = false;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
console.log(chalk.gray('No conventions defined in meta manifest'));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
catch (err) {
|
|
542
|
+
console.log(chalk.yellow('Warning: Could not load meta manifest for convention validation'));
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
// Run staleness checks if requested
|
|
546
|
+
// AC: @stale-status-detection ac-4, ac-5
|
|
547
|
+
if (options.staleness) {
|
|
548
|
+
const tasks = await loadAllTasks(ctx);
|
|
549
|
+
const items = await loadAllItems(ctx);
|
|
550
|
+
const refIndex = new ReferenceIndex(tasks, items);
|
|
551
|
+
const stalenessWarnings = checkStaleness(items, tasks, refIndex);
|
|
552
|
+
formatStalenessWarnings(stalenessWarnings, options.verbose);
|
|
553
|
+
// AC: @stale-status-detection ac-5 (staleness-exit-code)
|
|
554
|
+
// With --strict, staleness warnings cause validation failure
|
|
555
|
+
if (options.strict && stalenessWarnings.length > 0) {
|
|
556
|
+
process.exit(EXIT_CODES.VALIDATION_FAILED);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (!result.valid) {
|
|
560
|
+
process.exit(EXIT_CODES.ERROR);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
catch (err) {
|
|
564
|
+
error(validationStrings.failed, err);
|
|
565
|
+
process.exit(EXIT_CODES.ERROR);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
// Alias: kspec lint
|
|
569
|
+
program
|
|
570
|
+
.command('lint')
|
|
571
|
+
.description('Alias for validate with style checks')
|
|
572
|
+
.option('--schema', 'Check schema conformance only')
|
|
573
|
+
.option('--refs', 'Check reference resolution only')
|
|
574
|
+
.option('--orphans', 'Find orphaned items only')
|
|
575
|
+
.option('--completeness', 'Check spec completeness (missing AC, descriptions, status inconsistencies)')
|
|
576
|
+
.option('--fix', 'Auto-fix issues where possible (invalid ULIDs, missing timestamps)')
|
|
577
|
+
.option('-v, --verbose', 'Show detailed output')
|
|
578
|
+
.option('--strict', 'Treat orphans as errors')
|
|
579
|
+
.action(async (options) => {
|
|
580
|
+
try {
|
|
581
|
+
const ctx = await initContext();
|
|
582
|
+
if (!ctx.manifestPath) {
|
|
583
|
+
error(validationStrings.noManifest);
|
|
584
|
+
process.exit(EXIT_CODES.ERROR);
|
|
585
|
+
}
|
|
586
|
+
const runAll = !options.schema && !options.refs && !options.orphans && !options.completeness;
|
|
587
|
+
const validateOptions = {
|
|
588
|
+
schema: runAll || options.schema,
|
|
589
|
+
refs: runAll || options.refs,
|
|
590
|
+
orphans: runAll || options.orphans,
|
|
591
|
+
completeness: runAll || options.completeness,
|
|
592
|
+
};
|
|
593
|
+
const result = await validate(ctx, validateOptions);
|
|
594
|
+
if (options.strict && result.orphans.length > 0) {
|
|
595
|
+
result.valid = false;
|
|
596
|
+
}
|
|
597
|
+
output(result, () => formatValidationResult(result, options.verbose));
|
|
598
|
+
// Run auto-fix if requested
|
|
599
|
+
if (options.fix) {
|
|
600
|
+
const filesToFix = await collectFixableFiles(ctx);
|
|
601
|
+
const fixResult = await fixFiles(filesToFix);
|
|
602
|
+
formatFixResult(fixResult);
|
|
603
|
+
// Re-run validation after fixes
|
|
604
|
+
if (fixResult.fixesApplied.length > 0) {
|
|
605
|
+
console.log(validationStrings.revalidating);
|
|
606
|
+
const revalidateResult = await validate(ctx, validateOptions);
|
|
607
|
+
if (revalidateResult.valid) {
|
|
608
|
+
console.log(validationStrings.nowPasses);
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
console.log(validationStrings.issuesRemain);
|
|
612
|
+
}
|
|
613
|
+
result.valid = revalidateResult.valid;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (!result.valid) {
|
|
617
|
+
process.exit(EXIT_CODES.ERROR);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
catch (err) {
|
|
621
|
+
error(validationStrings.lintFailed, err);
|
|
622
|
+
process.exit(EXIT_CODES.ERROR);
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
//# sourceMappingURL=validate.js.map
|