@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,817 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation module for kspec files.
|
|
3
|
+
*
|
|
4
|
+
* Provides schema validation, reference validation, and orphan detection.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'node:fs/promises';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import { TaskSchema, TasksFileSchema, ManifestSchema, SpecItemSchema, MetaManifestSchema, AgentSchema, WorkflowSchema, ConventionSchema, ObservationSchema, UlidSchema, } from '../schema/index.js';
|
|
9
|
+
import { readYamlFile, findTaskFiles, loadSpecFile, expandIncludePattern, extractItemsFromRaw, } from './yaml.js';
|
|
10
|
+
import { ReferenceIndex, validateRefs } from './refs.js';
|
|
11
|
+
import { findMetaManifest, loadMetaContext } from './meta.js';
|
|
12
|
+
import { TraitIndex } from './traits.js';
|
|
13
|
+
// ============================================================
|
|
14
|
+
// SCHEMA VALIDATION
|
|
15
|
+
// ============================================================
|
|
16
|
+
/**
|
|
17
|
+
* Validate a manifest file against schema
|
|
18
|
+
*/
|
|
19
|
+
async function validateManifestFile(filePath) {
|
|
20
|
+
const errors = [];
|
|
21
|
+
try {
|
|
22
|
+
const raw = await readYamlFile(filePath);
|
|
23
|
+
const result = ManifestSchema.safeParse(raw);
|
|
24
|
+
if (!result.success) {
|
|
25
|
+
for (const issue of result.error.issues) {
|
|
26
|
+
errors.push({
|
|
27
|
+
file: filePath,
|
|
28
|
+
path: issue.path.join('.'),
|
|
29
|
+
message: issue.message,
|
|
30
|
+
details: issue,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
errors.push({
|
|
37
|
+
file: filePath,
|
|
38
|
+
message: `Failed to parse YAML: ${err instanceof Error ? err.message : String(err)}`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return errors;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Validate a tasks file against schema
|
|
45
|
+
*/
|
|
46
|
+
async function validateTasksFile(filePath) {
|
|
47
|
+
const errors = [];
|
|
48
|
+
try {
|
|
49
|
+
const raw = await readYamlFile(filePath);
|
|
50
|
+
// Handle both formats: { tasks: [...] } and plain array
|
|
51
|
+
let taskList;
|
|
52
|
+
if (Array.isArray(raw)) {
|
|
53
|
+
taskList = raw;
|
|
54
|
+
}
|
|
55
|
+
else if (raw && typeof raw === 'object' && 'tasks' in raw) {
|
|
56
|
+
// Try full TasksFile schema first
|
|
57
|
+
const fileResult = TasksFileSchema.safeParse(raw);
|
|
58
|
+
if (!fileResult.success) {
|
|
59
|
+
// If TasksFile fails, just validate individual tasks
|
|
60
|
+
taskList = raw.tasks || [];
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// File schema passed, validate individual tasks for detailed errors
|
|
64
|
+
taskList = fileResult.data.tasks;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
errors.push({
|
|
69
|
+
file: filePath,
|
|
70
|
+
message: 'Invalid tasks file format: expected array or { tasks: [...] }',
|
|
71
|
+
});
|
|
72
|
+
return errors;
|
|
73
|
+
}
|
|
74
|
+
// Validate each task
|
|
75
|
+
for (let i = 0; i < taskList.length; i++) {
|
|
76
|
+
const task = taskList[i];
|
|
77
|
+
const result = TaskSchema.safeParse(task);
|
|
78
|
+
if (!result.success) {
|
|
79
|
+
for (const issue of result.error.issues) {
|
|
80
|
+
errors.push({
|
|
81
|
+
file: filePath,
|
|
82
|
+
path: `tasks[${i}].${issue.path.join('.')}`,
|
|
83
|
+
message: issue.message,
|
|
84
|
+
details: issue,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
errors.push({
|
|
92
|
+
file: filePath,
|
|
93
|
+
message: `Failed to parse YAML: ${err instanceof Error ? err.message : String(err)}`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return errors;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Validate a spec module file against schema
|
|
100
|
+
*/
|
|
101
|
+
async function validateSpecFile(filePath) {
|
|
102
|
+
const errors = [];
|
|
103
|
+
try {
|
|
104
|
+
const raw = await readYamlFile(filePath);
|
|
105
|
+
// Recursively validate spec items
|
|
106
|
+
validateSpecItemRecursive(raw, filePath, '', errors);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
errors.push({
|
|
110
|
+
file: filePath,
|
|
111
|
+
message: `Failed to parse YAML: ${err instanceof Error ? err.message : String(err)}`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return errors;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Validate meta manifest file with strict ULID validation
|
|
118
|
+
* AC-meta-manifest-3: Invalid schema exits with code 1 and shows field path + expected type
|
|
119
|
+
*/
|
|
120
|
+
async function validateMetaManifestFile(filePath) {
|
|
121
|
+
const errors = [];
|
|
122
|
+
try {
|
|
123
|
+
const raw = await readYamlFile(filePath);
|
|
124
|
+
// Validate overall manifest structure
|
|
125
|
+
const manifestResult = MetaManifestSchema.safeParse(raw);
|
|
126
|
+
if (!manifestResult.success) {
|
|
127
|
+
for (const issue of manifestResult.error.issues) {
|
|
128
|
+
errors.push({
|
|
129
|
+
file: filePath,
|
|
130
|
+
path: issue.path.join('.'),
|
|
131
|
+
message: issue.message,
|
|
132
|
+
details: issue,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return errors;
|
|
136
|
+
}
|
|
137
|
+
// Validate each agent with strict ULID validation
|
|
138
|
+
if (raw && typeof raw === 'object' && 'agents' in raw && Array.isArray(raw.agents)) {
|
|
139
|
+
const agents = raw.agents;
|
|
140
|
+
for (let i = 0; i < agents.length; i++) {
|
|
141
|
+
const agent = agents[i];
|
|
142
|
+
const agentResult = AgentSchema.safeParse(agent);
|
|
143
|
+
if (!agentResult.success) {
|
|
144
|
+
for (const issue of agentResult.error.issues) {
|
|
145
|
+
errors.push({
|
|
146
|
+
file: filePath,
|
|
147
|
+
path: `agents[${i}].${issue.path.join('.')}`,
|
|
148
|
+
message: issue.message,
|
|
149
|
+
details: issue,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Strict ULID validation
|
|
154
|
+
if (agent && typeof agent === 'object' && '_ulid' in agent) {
|
|
155
|
+
const ulidResult = UlidSchema.safeParse(agent._ulid);
|
|
156
|
+
if (!ulidResult.success) {
|
|
157
|
+
errors.push({
|
|
158
|
+
file: filePath,
|
|
159
|
+
path: `agents[${i}]._ulid`,
|
|
160
|
+
message: 'Invalid ULID format (expected 26 characters)',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Validate each workflow with strict ULID validation
|
|
167
|
+
if (raw && typeof raw === 'object' && 'workflows' in raw && Array.isArray(raw.workflows)) {
|
|
168
|
+
const workflows = raw.workflows;
|
|
169
|
+
for (let i = 0; i < workflows.length; i++) {
|
|
170
|
+
const workflow = workflows[i];
|
|
171
|
+
const workflowResult = WorkflowSchema.safeParse(workflow);
|
|
172
|
+
if (!workflowResult.success) {
|
|
173
|
+
for (const issue of workflowResult.error.issues) {
|
|
174
|
+
errors.push({
|
|
175
|
+
file: filePath,
|
|
176
|
+
path: `workflows[${i}].${issue.path.join('.')}`,
|
|
177
|
+
message: issue.message,
|
|
178
|
+
details: issue,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Strict ULID validation
|
|
183
|
+
if (workflow && typeof workflow === 'object' && '_ulid' in workflow) {
|
|
184
|
+
const ulidResult = UlidSchema.safeParse(workflow._ulid);
|
|
185
|
+
if (!ulidResult.success) {
|
|
186
|
+
errors.push({
|
|
187
|
+
file: filePath,
|
|
188
|
+
path: `workflows[${i}]._ulid`,
|
|
189
|
+
message: 'Invalid ULID format (expected 26 characters)',
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Validate each convention with strict ULID validation
|
|
196
|
+
if (raw && typeof raw === 'object' && 'conventions' in raw && Array.isArray(raw.conventions)) {
|
|
197
|
+
const conventions = raw.conventions;
|
|
198
|
+
for (let i = 0; i < conventions.length; i++) {
|
|
199
|
+
const convention = conventions[i];
|
|
200
|
+
const conventionResult = ConventionSchema.safeParse(convention);
|
|
201
|
+
if (!conventionResult.success) {
|
|
202
|
+
for (const issue of conventionResult.error.issues) {
|
|
203
|
+
errors.push({
|
|
204
|
+
file: filePath,
|
|
205
|
+
path: `conventions[${i}].${issue.path.join('.')}`,
|
|
206
|
+
message: issue.message,
|
|
207
|
+
details: issue,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Strict ULID validation
|
|
212
|
+
if (convention && typeof convention === 'object' && '_ulid' in convention) {
|
|
213
|
+
const ulidResult = UlidSchema.safeParse(convention._ulid);
|
|
214
|
+
if (!ulidResult.success) {
|
|
215
|
+
errors.push({
|
|
216
|
+
file: filePath,
|
|
217
|
+
path: `conventions[${i}]._ulid`,
|
|
218
|
+
message: 'Invalid ULID format (expected 26 characters)',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Validate each observation with strict ULID validation
|
|
225
|
+
if (raw && typeof raw === 'object' && 'observations' in raw && Array.isArray(raw.observations)) {
|
|
226
|
+
const observations = raw.observations;
|
|
227
|
+
for (let i = 0; i < observations.length; i++) {
|
|
228
|
+
const observation = observations[i];
|
|
229
|
+
const observationResult = ObservationSchema.safeParse(observation);
|
|
230
|
+
if (!observationResult.success) {
|
|
231
|
+
for (const issue of observationResult.error.issues) {
|
|
232
|
+
errors.push({
|
|
233
|
+
file: filePath,
|
|
234
|
+
path: `observations[${i}].${issue.path.join('.')}`,
|
|
235
|
+
message: issue.message,
|
|
236
|
+
details: issue,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Strict ULID validation
|
|
241
|
+
if (observation && typeof observation === 'object' && '_ulid' in observation) {
|
|
242
|
+
const ulidResult = UlidSchema.safeParse(observation._ulid);
|
|
243
|
+
if (!ulidResult.success) {
|
|
244
|
+
errors.push({
|
|
245
|
+
file: filePath,
|
|
246
|
+
path: `observations[${i}]._ulid`,
|
|
247
|
+
message: 'Invalid ULID format (expected 26 characters)',
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch (err) {
|
|
255
|
+
errors.push({
|
|
256
|
+
file: filePath,
|
|
257
|
+
message: `Failed to parse YAML: ${err instanceof Error ? err.message : String(err)}`,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return errors;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Recursively validate spec items in a structure
|
|
264
|
+
*/
|
|
265
|
+
function validateSpecItemRecursive(raw, file, pathPrefix, errors) {
|
|
266
|
+
if (!raw || typeof raw !== 'object')
|
|
267
|
+
return;
|
|
268
|
+
// Check if this is a spec item (has _ulid)
|
|
269
|
+
if ('_ulid' in raw) {
|
|
270
|
+
const result = SpecItemSchema.safeParse(raw);
|
|
271
|
+
if (!result.success) {
|
|
272
|
+
for (const issue of result.error.issues) {
|
|
273
|
+
errors.push({
|
|
274
|
+
file,
|
|
275
|
+
path: pathPrefix ? `${pathPrefix}.${issue.path.join('.')}` : issue.path.join('.'),
|
|
276
|
+
message: issue.message,
|
|
277
|
+
details: issue,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// Recurse into nested structures
|
|
283
|
+
const nestedFields = ['modules', 'features', 'requirements', 'constraints', 'decisions', 'items'];
|
|
284
|
+
const obj = raw;
|
|
285
|
+
for (const field of nestedFields) {
|
|
286
|
+
if (field in obj && Array.isArray(obj[field])) {
|
|
287
|
+
const arr = obj[field];
|
|
288
|
+
for (let i = 0; i < arr.length; i++) {
|
|
289
|
+
const newPath = pathPrefix ? `${pathPrefix}.${field}[${i}]` : `${field}[${i}]`;
|
|
290
|
+
validateSpecItemRecursive(arr[i], file, newPath, errors);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// ============================================================
|
|
296
|
+
// ORPHAN DETECTION
|
|
297
|
+
// ============================================================
|
|
298
|
+
/**
|
|
299
|
+
* Find items that are not referenced by any other item
|
|
300
|
+
*/
|
|
301
|
+
function findOrphans(tasks, items, index) {
|
|
302
|
+
const orphans = [];
|
|
303
|
+
// Build set of all referenced ULIDs
|
|
304
|
+
const referenced = new Set();
|
|
305
|
+
const allItems = [...tasks, ...items];
|
|
306
|
+
// Fields that contain references
|
|
307
|
+
const refFields = [
|
|
308
|
+
'depends_on',
|
|
309
|
+
'blocked_by',
|
|
310
|
+
'implements',
|
|
311
|
+
'relates_to',
|
|
312
|
+
'tests',
|
|
313
|
+
'supersedes',
|
|
314
|
+
'spec_ref',
|
|
315
|
+
'context',
|
|
316
|
+
];
|
|
317
|
+
for (const item of allItems) {
|
|
318
|
+
const obj = item;
|
|
319
|
+
for (const field of refFields) {
|
|
320
|
+
const value = obj[field];
|
|
321
|
+
if (typeof value === 'string' && value.startsWith('@')) {
|
|
322
|
+
const resolved = index.resolve(value);
|
|
323
|
+
if (resolved.ok) {
|
|
324
|
+
referenced.add(resolved.ulid);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
else if (Array.isArray(value)) {
|
|
328
|
+
for (const v of value) {
|
|
329
|
+
if (typeof v === 'string' && v.startsWith('@')) {
|
|
330
|
+
const resolved = index.resolve(v);
|
|
331
|
+
if (resolved.ok) {
|
|
332
|
+
referenced.add(resolved.ulid);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// Find items not in the referenced set
|
|
340
|
+
// Skip entry point types: modules are spec entry points, tasks are work items
|
|
341
|
+
const entryPointTypes = ['module', 'task', 'epic', 'bug', 'spike', 'infra'];
|
|
342
|
+
for (const item of items) {
|
|
343
|
+
// Only check spec items, not tasks
|
|
344
|
+
if (!referenced.has(item._ulid)) {
|
|
345
|
+
// Skip entry point types
|
|
346
|
+
if (entryPointTypes.includes(item.type || ''))
|
|
347
|
+
continue;
|
|
348
|
+
// Skip nested items - they're implicitly referenced by their parent
|
|
349
|
+
// _path indicates nesting (e.g., "features[0].requirements[2]")
|
|
350
|
+
if (item._path)
|
|
351
|
+
continue;
|
|
352
|
+
orphans.push({
|
|
353
|
+
ulid: item._ulid,
|
|
354
|
+
title: item.title,
|
|
355
|
+
type: item.type || 'unknown',
|
|
356
|
+
file: item._sourceFile,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return orphans;
|
|
361
|
+
}
|
|
362
|
+
// ============================================================
|
|
363
|
+
// TRAIT CYCLE DETECTION
|
|
364
|
+
// ============================================================
|
|
365
|
+
/**
|
|
366
|
+
* Detect circular trait references
|
|
367
|
+
* AC: @trait-edge-cases ac-2
|
|
368
|
+
*/
|
|
369
|
+
function detectTraitCycles(items, index) {
|
|
370
|
+
const errors = [];
|
|
371
|
+
const traits = items.filter(item => item.type === 'trait');
|
|
372
|
+
// Build adjacency list: trait ULID → trait ULIDs it references
|
|
373
|
+
const graph = new Map();
|
|
374
|
+
const traitInfo = new Map();
|
|
375
|
+
for (const trait of traits) {
|
|
376
|
+
const ref = trait.slugs?.[0] ? `@${trait.slugs[0]}` : `@${trait._ulid.slice(0, 8)}`;
|
|
377
|
+
traitInfo.set(trait._ulid, { ref, title: trait.title });
|
|
378
|
+
const dependencies = [];
|
|
379
|
+
if (trait.traits && trait.traits.length > 0) {
|
|
380
|
+
for (const traitRef of trait.traits) {
|
|
381
|
+
const result = index.resolve(traitRef);
|
|
382
|
+
if (result.ok) {
|
|
383
|
+
dependencies.push(result.ulid);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
graph.set(trait._ulid, dependencies);
|
|
388
|
+
}
|
|
389
|
+
// DFS-based cycle detection
|
|
390
|
+
const visiting = new Set();
|
|
391
|
+
const visited = new Set();
|
|
392
|
+
function dfs(ulid, path) {
|
|
393
|
+
if (visiting.has(ulid)) {
|
|
394
|
+
// Found a cycle - return the cycle path
|
|
395
|
+
const cycleStart = path.indexOf(ulid);
|
|
396
|
+
return path.slice(cycleStart);
|
|
397
|
+
}
|
|
398
|
+
if (visited.has(ulid)) {
|
|
399
|
+
return null; // Already checked this path
|
|
400
|
+
}
|
|
401
|
+
visiting.add(ulid);
|
|
402
|
+
path.push(ulid);
|
|
403
|
+
const dependencies = graph.get(ulid) || [];
|
|
404
|
+
for (const depUlid of dependencies) {
|
|
405
|
+
const cycle = dfs(depUlid, path);
|
|
406
|
+
if (cycle) {
|
|
407
|
+
return cycle;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
visiting.delete(ulid);
|
|
411
|
+
visited.add(ulid);
|
|
412
|
+
path.pop();
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
// Check each trait for cycles
|
|
416
|
+
for (const trait of traits) {
|
|
417
|
+
if (!visited.has(trait._ulid)) {
|
|
418
|
+
const cycle = dfs(trait._ulid, []);
|
|
419
|
+
if (cycle) {
|
|
420
|
+
const info = traitInfo.get(cycle[0]);
|
|
421
|
+
if (info) {
|
|
422
|
+
const cycleRefs = cycle.map(ulid => {
|
|
423
|
+
const cycleInfo = traitInfo.get(ulid);
|
|
424
|
+
return cycleInfo ? cycleInfo.ref : `@${ulid.slice(0, 8)}`;
|
|
425
|
+
});
|
|
426
|
+
errors.push({
|
|
427
|
+
traitRef: info.ref,
|
|
428
|
+
traitTitle: info.title,
|
|
429
|
+
cycle: cycleRefs,
|
|
430
|
+
message: `Circular trait reference: ${cycleRefs.join(' → ')} → ${cycleRefs[0]}`,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
// Mark all traits in cycle as visited to avoid duplicate errors
|
|
434
|
+
for (const ulid of cycle) {
|
|
435
|
+
visited.add(ulid);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return errors;
|
|
441
|
+
}
|
|
442
|
+
// ============================================================
|
|
443
|
+
// COMPLETENESS VALIDATION
|
|
444
|
+
// ============================================================
|
|
445
|
+
/**
|
|
446
|
+
* Scan test files for AC annotations to build coverage index
|
|
447
|
+
* Returns a Set of covered ACs in format "@spec-ref ac-N"
|
|
448
|
+
*/
|
|
449
|
+
async function scanTestCoverage(rootDir) {
|
|
450
|
+
const coveredACs = new Set();
|
|
451
|
+
const testsDir = path.join(rootDir, 'tests');
|
|
452
|
+
try {
|
|
453
|
+
// Check if tests directory exists
|
|
454
|
+
await fs.access(testsDir);
|
|
455
|
+
// Read all test files
|
|
456
|
+
const files = await fs.readdir(testsDir);
|
|
457
|
+
const testFiles = files.filter(f => f.endsWith('.test.ts') || f.endsWith('.test.js'));
|
|
458
|
+
for (const file of testFiles) {
|
|
459
|
+
const filePath = path.join(testsDir, file);
|
|
460
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
461
|
+
// Match AC annotations: // AC: @spec-ref ac-N
|
|
462
|
+
// Also handle multiple ACs on one line: // AC: @spec-ref ac-1, ac-2
|
|
463
|
+
const acPattern = /\/\/\s*AC:\s*(@[\w-]+)(?:\s+(ac-\d+(?:\s*,\s*ac-\d+)*))?/g;
|
|
464
|
+
let match;
|
|
465
|
+
while ((match = acPattern.exec(content)) !== null) {
|
|
466
|
+
const specRef = match[1]; // @spec-ref
|
|
467
|
+
const acList = match[2]; // "ac-1, ac-2" or just "ac-1" or undefined
|
|
468
|
+
if (acList) {
|
|
469
|
+
// Split by comma and trim
|
|
470
|
+
const acs = acList.split(',').map(ac => ac.trim());
|
|
471
|
+
for (const ac of acs) {
|
|
472
|
+
coveredACs.add(`${specRef} ${ac}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
// No specific AC mentioned, just the spec ref
|
|
477
|
+
// We'll consider this as generic coverage
|
|
478
|
+
coveredACs.add(specRef);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
catch (err) {
|
|
484
|
+
// Tests directory doesn't exist or can't be read - that's ok
|
|
485
|
+
}
|
|
486
|
+
return coveredACs;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Check spec items for completeness
|
|
490
|
+
* AC: @spec-completeness ac-1, ac-2, ac-3
|
|
491
|
+
* AC: @trait-validation ac-1, ac-2, ac-3
|
|
492
|
+
*/
|
|
493
|
+
async function checkCompleteness(items, index, rootDir, traitIndex) {
|
|
494
|
+
const warnings = [];
|
|
495
|
+
// Scan test files for AC coverage
|
|
496
|
+
const coveredACs = await scanTestCoverage(rootDir);
|
|
497
|
+
for (const item of items) {
|
|
498
|
+
const itemRef = item.slugs?.[0] ? `@${item.slugs[0]}` : `@${item._ulid.slice(0, 8)}`;
|
|
499
|
+
const isTrait = item.type === 'trait';
|
|
500
|
+
// AC: @spec-completeness ac-1
|
|
501
|
+
// AC: @trait-type ac-2 - Traits should have acceptance criteria for completeness
|
|
502
|
+
// Check for missing acceptance criteria
|
|
503
|
+
if (!item.acceptance_criteria || item.acceptance_criteria.length === 0) {
|
|
504
|
+
warnings.push({
|
|
505
|
+
type: 'missing_acceptance_criteria',
|
|
506
|
+
itemRef,
|
|
507
|
+
itemTitle: item.title,
|
|
508
|
+
message: `${isTrait ? 'Trait' : 'Item'} ${itemRef} has no acceptance criteria`,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
// AC: @spec-completeness ac-2
|
|
512
|
+
// AC: @trait-type ac-3 - Traits should have description for completeness
|
|
513
|
+
// Check for missing description
|
|
514
|
+
if (!item.description || item.description.trim() === '') {
|
|
515
|
+
warnings.push({
|
|
516
|
+
type: 'missing_description',
|
|
517
|
+
itemRef,
|
|
518
|
+
itemTitle: item.title,
|
|
519
|
+
message: `${isTrait ? 'Trait' : 'Item'} ${itemRef} has no description`,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
// AC: @spec-completeness ac-3
|
|
523
|
+
// Check for status inconsistency between parent and children
|
|
524
|
+
if (item.status?.implementation === 'implemented') {
|
|
525
|
+
// Check if this item has children with not_started status
|
|
526
|
+
const childFields = [
|
|
527
|
+
'modules',
|
|
528
|
+
'features',
|
|
529
|
+
'requirements',
|
|
530
|
+
'constraints',
|
|
531
|
+
'epics',
|
|
532
|
+
'themes',
|
|
533
|
+
'capabilities',
|
|
534
|
+
];
|
|
535
|
+
for (const field of childFields) {
|
|
536
|
+
const children = item[field];
|
|
537
|
+
if (Array.isArray(children)) {
|
|
538
|
+
for (const child of children) {
|
|
539
|
+
if (child.status?.implementation === 'not_started') {
|
|
540
|
+
const childRef = child.slugs?.[0]
|
|
541
|
+
? `@${child.slugs[0]}`
|
|
542
|
+
: `@${child._ulid?.slice(0, 8) || 'unknown'}`;
|
|
543
|
+
warnings.push({
|
|
544
|
+
type: 'status_inconsistency',
|
|
545
|
+
itemRef,
|
|
546
|
+
itemTitle: item.title,
|
|
547
|
+
message: `Parent ${itemRef} is implemented but child ${childRef} is not_started`,
|
|
548
|
+
details: `Child: ${child.title}`,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// Check for test coverage of acceptance criteria
|
|
556
|
+
if (item.acceptance_criteria && item.acceptance_criteria.length > 0) {
|
|
557
|
+
const uncoveredACs = [];
|
|
558
|
+
for (const ac of item.acceptance_criteria) {
|
|
559
|
+
// Build all possible references for this AC
|
|
560
|
+
const possibleRefs = [];
|
|
561
|
+
// Try with primary slug
|
|
562
|
+
if (item.slugs && item.slugs.length > 0) {
|
|
563
|
+
possibleRefs.push(`@${item.slugs[0]} ${ac.id}`);
|
|
564
|
+
// Also check for just the slug without specific AC
|
|
565
|
+
possibleRefs.push(`@${item.slugs[0]}`);
|
|
566
|
+
}
|
|
567
|
+
// Try with ULID (short form)
|
|
568
|
+
possibleRefs.push(`@${item._ulid.slice(0, 8)} ${ac.id}`);
|
|
569
|
+
possibleRefs.push(`@${item._ulid.slice(0, 8)}`);
|
|
570
|
+
// Check if any of these references are covered
|
|
571
|
+
const isCovered = possibleRefs.some(ref => coveredACs.has(ref));
|
|
572
|
+
if (!isCovered) {
|
|
573
|
+
uncoveredACs.push(ac.id);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
// Only warn if there are uncovered ACs
|
|
577
|
+
if (uncoveredACs.length > 0) {
|
|
578
|
+
warnings.push({
|
|
579
|
+
type: 'missing_test_coverage',
|
|
580
|
+
itemRef,
|
|
581
|
+
itemTitle: item.title,
|
|
582
|
+
message: `Item ${itemRef} has ${uncoveredACs.length} AC(s) without test coverage`,
|
|
583
|
+
details: `Uncovered: ${uncoveredACs.join(', ')}`,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
// AC: @trait-validation ac-1, ac-2
|
|
588
|
+
// Check for test coverage of trait acceptance criteria
|
|
589
|
+
if (traitIndex && item.traits && item.traits.length > 0) {
|
|
590
|
+
const inheritedACs = traitIndex.getInheritedAC(item._ulid);
|
|
591
|
+
const uncoveredTraitACs = [];
|
|
592
|
+
for (const { trait, ac } of inheritedACs) {
|
|
593
|
+
// Build all possible references for this trait AC
|
|
594
|
+
const possibleRefs = [];
|
|
595
|
+
// Try with trait slug
|
|
596
|
+
possibleRefs.push(`@${trait.slug} ${ac.id}`);
|
|
597
|
+
possibleRefs.push(`@${trait.slug}`);
|
|
598
|
+
// Try with trait ULID (short form)
|
|
599
|
+
possibleRefs.push(`@${trait.ulid.slice(0, 8)} ${ac.id}`);
|
|
600
|
+
possibleRefs.push(`@${trait.ulid.slice(0, 8)}`);
|
|
601
|
+
// Check if any of these references are covered
|
|
602
|
+
const isCovered = possibleRefs.some(ref => coveredACs.has(ref));
|
|
603
|
+
if (!isCovered) {
|
|
604
|
+
uncoveredTraitACs.push({ traitSlug: trait.slug, acId: ac.id });
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
// Only warn if there are uncovered trait ACs
|
|
608
|
+
if (uncoveredTraitACs.length > 0) {
|
|
609
|
+
const details = uncoveredTraitACs
|
|
610
|
+
.map(({ traitSlug, acId }) => `@${traitSlug} ${acId}`)
|
|
611
|
+
.join(', ');
|
|
612
|
+
warnings.push({
|
|
613
|
+
type: 'missing_test_coverage',
|
|
614
|
+
itemRef,
|
|
615
|
+
itemTitle: item.title,
|
|
616
|
+
message: `Item ${itemRef} has ${uncoveredTraitACs.length} inherited trait AC(s) without test coverage`,
|
|
617
|
+
details: `Uncovered trait ACs: ${details}`,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return warnings;
|
|
623
|
+
}
|
|
624
|
+
// ============================================================
|
|
625
|
+
// AUTOMATION VALIDATION
|
|
626
|
+
// ============================================================
|
|
627
|
+
/**
|
|
628
|
+
* Check task automation status for warnings
|
|
629
|
+
* AC: @task-automation-eligibility ac-21, ac-23
|
|
630
|
+
*/
|
|
631
|
+
function checkAutomationEligibility(tasks, index) {
|
|
632
|
+
const warnings = [];
|
|
633
|
+
for (const task of tasks) {
|
|
634
|
+
const taskRef = task.slugs?.[0] ? `@${task.slugs[0]}` : `@${task._ulid.slice(0, 8)}`;
|
|
635
|
+
// AC: @task-automation-eligibility ac-21
|
|
636
|
+
// Warn if eligible but no spec_ref
|
|
637
|
+
if (task.automation === 'eligible' && !task.spec_ref) {
|
|
638
|
+
warnings.push({
|
|
639
|
+
type: 'automation_eligible_no_spec',
|
|
640
|
+
itemRef: taskRef,
|
|
641
|
+
itemTitle: task.title,
|
|
642
|
+
message: `Task ${taskRef} is automation: eligible but has no spec_ref - eligible tasks should have linked specs`,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
// AC: @task-automation-eligibility ac-23
|
|
646
|
+
// Warn if eligible but spec_ref doesn't resolve
|
|
647
|
+
if (task.automation === 'eligible' && task.spec_ref) {
|
|
648
|
+
const specResult = index.resolve(task.spec_ref);
|
|
649
|
+
if (!specResult.ok) {
|
|
650
|
+
warnings.push({
|
|
651
|
+
type: 'automation_eligible_no_spec',
|
|
652
|
+
itemRef: taskRef,
|
|
653
|
+
itemTitle: task.title,
|
|
654
|
+
message: `Task ${taskRef} is automation: eligible but spec_ref ${task.spec_ref} cannot be resolved`,
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return warnings;
|
|
660
|
+
}
|
|
661
|
+
// ============================================================
|
|
662
|
+
// MAIN VALIDATION
|
|
663
|
+
// ============================================================
|
|
664
|
+
/**
|
|
665
|
+
* Run full validation on a kspec project
|
|
666
|
+
*/
|
|
667
|
+
export async function validate(ctx, options = {}) {
|
|
668
|
+
// Default: run all checks
|
|
669
|
+
const runSchema = options.schema !== false;
|
|
670
|
+
const runRefs = options.refs !== false;
|
|
671
|
+
const runOrphans = options.orphans !== false;
|
|
672
|
+
const runCompleteness = options.completeness !== false;
|
|
673
|
+
const result = {
|
|
674
|
+
valid: true,
|
|
675
|
+
schemaErrors: [],
|
|
676
|
+
refErrors: [],
|
|
677
|
+
refWarnings: [],
|
|
678
|
+
orphans: [],
|
|
679
|
+
completenessWarnings: [],
|
|
680
|
+
traitCycleErrors: [],
|
|
681
|
+
stats: {
|
|
682
|
+
filesChecked: 0,
|
|
683
|
+
itemsChecked: 0,
|
|
684
|
+
tasksChecked: 0,
|
|
685
|
+
},
|
|
686
|
+
};
|
|
687
|
+
const allTasks = [];
|
|
688
|
+
const allItems = [];
|
|
689
|
+
// Validate manifest
|
|
690
|
+
if (ctx.manifestPath && runSchema) {
|
|
691
|
+
const manifestErrors = await validateManifestFile(ctx.manifestPath);
|
|
692
|
+
result.schemaErrors.push(...manifestErrors);
|
|
693
|
+
result.stats.filesChecked++;
|
|
694
|
+
}
|
|
695
|
+
// Load items from manifest (traits, inline modules, etc.)
|
|
696
|
+
if (ctx.manifest && ctx.manifestPath) {
|
|
697
|
+
const manifestItems = extractItemsFromRaw(ctx.manifest, ctx.manifestPath);
|
|
698
|
+
allItems.push(...manifestItems);
|
|
699
|
+
result.stats.itemsChecked += manifestItems.length;
|
|
700
|
+
}
|
|
701
|
+
// Find and validate task files
|
|
702
|
+
const taskFiles = await findTaskFiles(ctx.rootDir);
|
|
703
|
+
const specTaskFiles = await findTaskFiles(path.join(ctx.rootDir, 'spec'));
|
|
704
|
+
const allTaskFiles = [...new Set([...taskFiles, ...specTaskFiles])];
|
|
705
|
+
for (const taskFile of allTaskFiles) {
|
|
706
|
+
if (runSchema) {
|
|
707
|
+
const taskErrors = await validateTasksFile(taskFile);
|
|
708
|
+
result.schemaErrors.push(...taskErrors);
|
|
709
|
+
}
|
|
710
|
+
result.stats.filesChecked++;
|
|
711
|
+
// Load tasks for ref validation
|
|
712
|
+
try {
|
|
713
|
+
const raw = await readYamlFile(taskFile);
|
|
714
|
+
let taskList = [];
|
|
715
|
+
if (Array.isArray(raw)) {
|
|
716
|
+
taskList = raw;
|
|
717
|
+
}
|
|
718
|
+
else if (raw && typeof raw === 'object' && 'tasks' in raw) {
|
|
719
|
+
taskList = raw.tasks || [];
|
|
720
|
+
}
|
|
721
|
+
for (const t of taskList) {
|
|
722
|
+
const parsed = TaskSchema.safeParse(t);
|
|
723
|
+
if (parsed.success) {
|
|
724
|
+
allTasks.push({ ...parsed.data, _sourceFile: taskFile });
|
|
725
|
+
result.stats.tasksChecked++;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
catch {
|
|
730
|
+
// Already reported in schema validation
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
// Validate spec files (from includes)
|
|
734
|
+
if (ctx.manifest && ctx.manifestPath) {
|
|
735
|
+
const manifestDir = path.dirname(ctx.manifestPath);
|
|
736
|
+
const includes = ctx.manifest.includes || [];
|
|
737
|
+
for (const include of includes) {
|
|
738
|
+
const expandedPaths = await expandIncludePattern(include, manifestDir);
|
|
739
|
+
for (const filePath of expandedPaths) {
|
|
740
|
+
if (runSchema) {
|
|
741
|
+
const specErrors = await validateSpecFile(filePath);
|
|
742
|
+
result.schemaErrors.push(...specErrors);
|
|
743
|
+
}
|
|
744
|
+
result.stats.filesChecked++;
|
|
745
|
+
// Load items for ref validation
|
|
746
|
+
try {
|
|
747
|
+
const items = await loadSpecFile(filePath);
|
|
748
|
+
allItems.push(...items);
|
|
749
|
+
result.stats.itemsChecked += items.length;
|
|
750
|
+
}
|
|
751
|
+
catch {
|
|
752
|
+
// Already reported in schema validation
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
// Load meta items for reference validation
|
|
758
|
+
// AC: @agent-definitions ac-agent-3
|
|
759
|
+
const metaCtx = await loadMetaContext(ctx);
|
|
760
|
+
const allMetaItems = [
|
|
761
|
+
...metaCtx.agents,
|
|
762
|
+
...metaCtx.workflows,
|
|
763
|
+
...metaCtx.conventions,
|
|
764
|
+
...metaCtx.observations,
|
|
765
|
+
];
|
|
766
|
+
// Reference validation
|
|
767
|
+
if (runRefs && (allTasks.length > 0 || allItems.length > 0 || allMetaItems.length > 0)) {
|
|
768
|
+
const index = new ReferenceIndex(allTasks, allItems, allMetaItems);
|
|
769
|
+
const refResult = validateRefs(index, allTasks, allItems);
|
|
770
|
+
result.refErrors = refResult.errors;
|
|
771
|
+
result.refWarnings = refResult.warnings;
|
|
772
|
+
// AC: @trait-edge-cases ac-2
|
|
773
|
+
// Detect circular trait references
|
|
774
|
+
result.traitCycleErrors = detectTraitCycles(allItems, index);
|
|
775
|
+
// Orphan detection
|
|
776
|
+
if (runOrphans) {
|
|
777
|
+
result.orphans = findOrphans(allTasks, allItems, index);
|
|
778
|
+
}
|
|
779
|
+
// Completeness validation
|
|
780
|
+
// AC: @spec-completeness ac-1, ac-2, ac-3
|
|
781
|
+
// AC: @trait-validation ac-3
|
|
782
|
+
if (runCompleteness) {
|
|
783
|
+
// Build trait index for trait AC coverage validation
|
|
784
|
+
const traitIndex = new TraitIndex(allItems, index);
|
|
785
|
+
result.completenessWarnings = await checkCompleteness(allItems, index, ctx.rootDir, traitIndex);
|
|
786
|
+
// AC: @task-automation-eligibility ac-21, ac-23
|
|
787
|
+
// Check automation eligibility warnings for tasks
|
|
788
|
+
const automationWarnings = checkAutomationEligibility(allTasks, index);
|
|
789
|
+
result.completenessWarnings.push(...automationWarnings);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
// Meta manifest validation (AC-meta-manifest-2, AC-meta-manifest-3)
|
|
793
|
+
const metaManifestPath = await findMetaManifest(ctx.specDir);
|
|
794
|
+
if (metaManifestPath) {
|
|
795
|
+
// Use metaCtx already loaded above
|
|
796
|
+
result.metaStats = {
|
|
797
|
+
agents: metaCtx.agents.length,
|
|
798
|
+
workflows: metaCtx.workflows.length,
|
|
799
|
+
conventions: metaCtx.conventions.length,
|
|
800
|
+
observations: metaCtx.observations.length,
|
|
801
|
+
};
|
|
802
|
+
// Validate meta manifest schema with strict ULID validation
|
|
803
|
+
if (runSchema) {
|
|
804
|
+
const metaErrors = await validateMetaManifestFile(metaManifestPath);
|
|
805
|
+
// Prefix all meta errors with "meta:"
|
|
806
|
+
for (const err of metaErrors) {
|
|
807
|
+
err.path = err.path ? `meta:${err.path}` : 'meta:';
|
|
808
|
+
}
|
|
809
|
+
result.schemaErrors.push(...metaErrors);
|
|
810
|
+
result.stats.filesChecked++;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
// Set valid flag
|
|
814
|
+
result.valid = result.schemaErrors.length === 0 && result.refErrors.length === 0 && result.traitCycleErrors.length === 0;
|
|
815
|
+
return result;
|
|
816
|
+
}
|
|
817
|
+
//# sourceMappingURL=validate.js.map
|