@topogram/cli 0.3.72 → 0.3.73
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 +24 -195
- package/package.json +1 -1
- package/src/adoption/plan/index.js +2 -1
- package/src/agent-brief.js +46 -2
- package/src/archive/archive.js +1 -1
- package/src/archive/jsonl.js +18 -8
- package/src/archive/resolver-bridge.js +34 -1
- package/src/archive/schema.js +1 -1
- package/src/archive/unarchive.js +26 -0
- package/src/cli/command-parsers/sdlc.js +66 -0
- package/src/cli/commands/import/help.js +1 -0
- package/src/cli/commands/import/plan.js +9 -0
- package/src/cli/commands/import/workspace.js +3 -0
- package/src/cli/commands/query/definitions.js +11 -10
- package/src/cli/commands/query/workspace.js +23 -2
- package/src/cli/commands/sdlc.js +213 -5
- package/src/cli/dispatcher.js +8 -0
- package/src/cli/help.js +14 -2
- package/src/cli/options.js +1 -0
- package/src/generator/context/shared/domain-sdlc.js +27 -0
- package/src/generator/context/shared/relationships.js +2 -1
- package/src/generator/context/shared/types.d.ts +1 -0
- package/src/generator/context/shared.d.ts +2 -0
- package/src/generator/context/shared.js +2 -0
- package/src/generator/context/slice/core.js +3 -0
- package/src/generator/context/slice/sdlc.js +57 -2
- package/src/generator/context/task-mode.js +7 -0
- package/src/generator/sdlc/board.js +2 -0
- package/src/generator/sdlc/traceability-matrix.js +5 -1
- package/src/import/core/context.js +1 -1
- package/src/import/core/contracts.js +3 -3
- package/src/import/core/registry.js +3 -0
- package/src/import/core/runner/candidates.js +7 -0
- package/src/import/core/runner/reports.js +9 -1
- package/src/import/core/runner/tracks.js +3 -0
- package/src/import/extractors/cli/generic.js +340 -0
- package/src/new-project/project-files.js +10 -3
- package/src/resolver/enrich/task.js +3 -1
- package/src/resolver/index.js +6 -0
- package/src/resolver/normalize.js +31 -0
- package/src/resolver/projections-cli.js +158 -0
- package/src/sdlc/adopt.js +4 -1
- package/src/sdlc/check.js +24 -2
- package/src/sdlc/complete.js +47 -0
- package/src/sdlc/dod/index.js +2 -0
- package/src/sdlc/dod/plan.js +15 -0
- package/src/sdlc/dod/task.js +7 -3
- package/src/sdlc/explain.js +53 -1
- package/src/sdlc/gate.js +352 -0
- package/src/sdlc/history.d.ts +7 -0
- package/src/sdlc/history.js +50 -5
- package/src/sdlc/link.js +172 -0
- package/src/sdlc/paths.d.ts +4 -0
- package/src/sdlc/paths.js +8 -0
- package/src/sdlc/plan-steps.js +71 -0
- package/src/sdlc/plan.js +245 -0
- package/src/sdlc/policy.js +249 -0
- package/src/sdlc/prep.js +186 -0
- package/src/sdlc/scaffold.js +4 -2
- package/src/sdlc/status-filter.js +2 -0
- package/src/sdlc/transitions/index.js +3 -0
- package/src/sdlc/transitions/plan.js +32 -0
- package/src/validator/common.js +25 -4
- package/src/validator/index.js +10 -0
- package/src/validator/kinds.d.ts +7 -0
- package/src/validator/kinds.js +32 -0
- package/src/validator/per-kind/plan.js +128 -0
- package/src/validator/per-kind/task.js +19 -0
- package/src/validator/projections/cli.js +267 -0
- package/src/validator.d.ts +1 -0
- package/src/workflows/import-app/shared.js +1 -1
- package/src/workflows/reconcile/adoption-plan/build.js +3 -1
- package/src/workflows/reconcile/adoption-plan/reasons.js +5 -0
- package/src/workflows/reconcile/bundle-core/index.js +3 -0
- package/src/workflows/reconcile/candidate-model.js +15 -0
- package/src/workflows/reconcile/gap-report.js +4 -2
- package/src/workflows/reconcile/impacts/adoption-plan.js +13 -0
- package/src/workflows/reconcile/renderers.js +82 -0
- package/src/workflows/reconcile/summary.js +4 -0
- package/src/workflows/reconcile/workflow.js +2 -1
- package/src/workspace-paths.js +26 -2
package/src/sdlc/prep.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import childProcess from "node:child_process";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { parsePath } from "../parser.js";
|
|
7
|
+
import { resolveWorkspace } from "../resolver.js";
|
|
8
|
+
import { resolveTopoRoot, resolveWorkspaceContext } from "../workspace-paths.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} SdlcCommitPrepOptions
|
|
12
|
+
* @property {string|null} [base]
|
|
13
|
+
* @property {string|null} [head]
|
|
14
|
+
* @property {string[]} [changedFiles]
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} value
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
function normalizePath(value) {
|
|
22
|
+
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} projectRoot
|
|
27
|
+
* @param {string[]} args
|
|
28
|
+
* @returns {string[]}
|
|
29
|
+
*/
|
|
30
|
+
function gitFileList(projectRoot, args) {
|
|
31
|
+
const result = childProcess.spawnSync("git", args, {
|
|
32
|
+
cwd: projectRoot,
|
|
33
|
+
encoding: "utf8"
|
|
34
|
+
});
|
|
35
|
+
if (result.status !== 0) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
return result.stdout.split(/\r?\n/).map(/** @param {string} line */ (line) => line.trim()).filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {string} projectRoot
|
|
43
|
+
* @param {string|null|undefined} base
|
|
44
|
+
* @param {string|null|undefined} head
|
|
45
|
+
* @returns {string[]}
|
|
46
|
+
*/
|
|
47
|
+
function changedFiles(projectRoot, base, head) {
|
|
48
|
+
const localChanges = [
|
|
49
|
+
...gitFileList(projectRoot, ["diff", "--name-only", "--cached"]),
|
|
50
|
+
...gitFileList(projectRoot, ["diff", "--name-only"]),
|
|
51
|
+
...gitFileList(projectRoot, ["ls-files", "--others", "--exclude-standard"])
|
|
52
|
+
];
|
|
53
|
+
if (base && head) {
|
|
54
|
+
return [...new Set([
|
|
55
|
+
...gitFileList(projectRoot, ["diff", "--name-only", `${base}...${head}`]),
|
|
56
|
+
...localChanges
|
|
57
|
+
])];
|
|
58
|
+
}
|
|
59
|
+
return [...new Set(localChanges)];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {Record<string, any>} ast
|
|
64
|
+
* @param {string} projectRoot
|
|
65
|
+
* @returns {Map<string, { file: string, ids: string[] }>}
|
|
66
|
+
*/
|
|
67
|
+
function taskFilesFromAst(ast, projectRoot) {
|
|
68
|
+
const map = new Map();
|
|
69
|
+
for (const file of ast.files || []) {
|
|
70
|
+
const ids = (file.statements || [])
|
|
71
|
+
.filter(/** @param {Record<string, any>} statement */ (statement) => statement.kind === "task")
|
|
72
|
+
.map(/** @param {Record<string, any>} statement */ (statement) => statement.id);
|
|
73
|
+
if (ids.length === 0) continue;
|
|
74
|
+
const rel = normalizePath(path.relative(projectRoot, file.file));
|
|
75
|
+
map.set(rel, { file: rel, ids });
|
|
76
|
+
}
|
|
77
|
+
return map;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {Record<string, any>} task
|
|
82
|
+
* @returns {boolean}
|
|
83
|
+
*/
|
|
84
|
+
function isOpenTask(task) {
|
|
85
|
+
return task.status !== "done";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {Record<string, any>} task
|
|
90
|
+
* @returns {boolean}
|
|
91
|
+
*/
|
|
92
|
+
function needsExplicitDisposition(task) {
|
|
93
|
+
if (!isOpenTask(task)) return false;
|
|
94
|
+
if (task.disposition) return false;
|
|
95
|
+
return !["claimed", "in-progress"].includes(String(task.status));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @param {string} inputPath
|
|
100
|
+
* @param {SdlcCommitPrepOptions} [options]
|
|
101
|
+
* @returns {Record<string, any>}
|
|
102
|
+
*/
|
|
103
|
+
export function runSdlcCommitPrep(inputPath = ".", options = {}) {
|
|
104
|
+
const context = resolveWorkspaceContext(inputPath || ".");
|
|
105
|
+
const projectRoot = context.projectRoot;
|
|
106
|
+
const topogramRoot = resolveTopoRoot(inputPath || ".");
|
|
107
|
+
const files = (options.changedFiles || changedFiles(projectRoot, options.base, options.head)).map(normalizePath);
|
|
108
|
+
const changedFileSet = new Set(files);
|
|
109
|
+
const ast = parsePath(topogramRoot);
|
|
110
|
+
const resolved = resolveWorkspace(ast);
|
|
111
|
+
if (!resolved.ok) {
|
|
112
|
+
return {
|
|
113
|
+
type: "sdlc_commit_prep",
|
|
114
|
+
version: "1",
|
|
115
|
+
ok: false,
|
|
116
|
+
projectRoot,
|
|
117
|
+
topogramRoot,
|
|
118
|
+
changedFiles: files,
|
|
119
|
+
taskFiles: [],
|
|
120
|
+
changedTasks: [],
|
|
121
|
+
openTasks: [],
|
|
122
|
+
warnings: [],
|
|
123
|
+
errors: ["workspace resolution failed"]
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const taskFiles = taskFilesFromAst(ast, projectRoot);
|
|
128
|
+
const changedTaskFiles = [...taskFiles.values()]
|
|
129
|
+
.filter((entry) => changedFileSet.has(entry.file))
|
|
130
|
+
.map((entry) => entry.file)
|
|
131
|
+
.sort();
|
|
132
|
+
const taskIds = new Set(changedTaskFiles.flatMap((file) => taskFiles.get(file)?.ids || []));
|
|
133
|
+
const tasksById = new Map((resolved.graph.byKind.task || []).map(/** @param {Record<string, any>} task */ (task) => [task.id, task]));
|
|
134
|
+
const resolvedTasks = /** @type {Record<string, any>[]} */ ([...taskIds]
|
|
135
|
+
.map((id) => tasksById.get(id))
|
|
136
|
+
.filter(Boolean));
|
|
137
|
+
const changedTasks = resolvedTasks
|
|
138
|
+
.map((task) => {
|
|
139
|
+
const source = [...taskFiles.values()].find((entry) => entry.ids.includes(task.id))?.file || null;
|
|
140
|
+
const disposition = task.disposition || (["claimed", "in-progress"].includes(String(task.status)) ? "active" : null);
|
|
141
|
+
return {
|
|
142
|
+
id: task.id,
|
|
143
|
+
status: task.status,
|
|
144
|
+
priority: task.priority || null,
|
|
145
|
+
disposition,
|
|
146
|
+
explicitDisposition: Boolean(task.disposition),
|
|
147
|
+
file: source,
|
|
148
|
+
requiresDisposition: needsExplicitDisposition(task)
|
|
149
|
+
};
|
|
150
|
+
})
|
|
151
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
152
|
+
const openTasks = changedTasks.filter((task) => task.status !== "done");
|
|
153
|
+
const errors = [];
|
|
154
|
+
const warnings = [];
|
|
155
|
+
|
|
156
|
+
for (const task of openTasks) {
|
|
157
|
+
if (task.requiresDisposition) {
|
|
158
|
+
errors.push(`Open task ${task.id} in ${task.file} needs explicit disposition active|follow_up|deferred|backlog|blocker before commit.`);
|
|
159
|
+
}
|
|
160
|
+
if (task.disposition === "blocker") {
|
|
161
|
+
errors.push(`Open task ${task.id} is marked disposition blocker.`);
|
|
162
|
+
}
|
|
163
|
+
if (task.priority === "high" && ["follow_up", "deferred", "backlog"].includes(String(task.disposition))) {
|
|
164
|
+
warnings.push(`High priority task ${task.id} remains open as ${task.disposition}.`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
type: "sdlc_commit_prep",
|
|
170
|
+
version: "1",
|
|
171
|
+
ok: errors.length === 0,
|
|
172
|
+
projectRoot,
|
|
173
|
+
topogramRoot,
|
|
174
|
+
changedFiles: files,
|
|
175
|
+
taskFiles: changedTaskFiles,
|
|
176
|
+
changedTasks,
|
|
177
|
+
openTasks,
|
|
178
|
+
warnings,
|
|
179
|
+
errors,
|
|
180
|
+
nextCommands: [
|
|
181
|
+
"topogram sdlc explain <task-id> --json",
|
|
182
|
+
"topogram query slice ./topo --task <task-id> --json",
|
|
183
|
+
"topogram sdlc gate . --base <ref> --head <ref> --require-adopted"
|
|
184
|
+
]
|
|
185
|
+
};
|
|
186
|
+
}
|
package/src/sdlc/scaffold.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import { sdlcRootForSdlc } from "./paths.js";
|
|
7
7
|
|
|
8
8
|
const TEMPLATES = {
|
|
9
9
|
pitch: (slug) => `pitch pitch_${slug} {
|
|
@@ -41,8 +41,10 @@ const TEMPLATES = {
|
|
|
41
41
|
description "What the agent or human will do."
|
|
42
42
|
satisfies []
|
|
43
43
|
acceptance_refs []
|
|
44
|
+
verification_refs []
|
|
44
45
|
affects []
|
|
45
46
|
blocked_by []
|
|
47
|
+
disposition active
|
|
46
48
|
priority medium
|
|
47
49
|
work_type implementation
|
|
48
50
|
status unclaimed
|
|
@@ -74,7 +76,7 @@ export function scaffoldNew(workspaceRoot, kind, slug) {
|
|
|
74
76
|
if (!slug || !/^[a-z][a-z0-9_]*$/.test(slug)) {
|
|
75
77
|
return { ok: false, error: `Invalid slug '${slug}' — must match /^[a-z][a-z0-9_]*$/` };
|
|
76
78
|
}
|
|
77
|
-
const targetDir = path.join(
|
|
79
|
+
const targetDir = path.join(sdlcRootForSdlc(workspaceRoot), `${kind === "acceptance_criterion" ? "acceptance_criteria" : kind + "s"}`);
|
|
78
80
|
if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
|
|
79
81
|
const targetFile = path.join(targetDir, `${slug}.tg`);
|
|
80
82
|
if (existsSync(targetFile)) {
|
|
@@ -30,6 +30,8 @@ export function defaultActiveStatuses(kind) {
|
|
|
30
30
|
return new Set(["draft", "approved", "superseded"]);
|
|
31
31
|
case "task":
|
|
32
32
|
return new Set(["unclaimed", "claimed", "in-progress", "blocked"]);
|
|
33
|
+
case "plan":
|
|
34
|
+
return new Set(["draft", "active"]);
|
|
33
35
|
case "bug":
|
|
34
36
|
return new Set(["open", "in-progress", "fixed"]);
|
|
35
37
|
case "document":
|
|
@@ -4,6 +4,7 @@ import * as pitch from "./pitch.js";
|
|
|
4
4
|
import * as requirement from "./requirement.js";
|
|
5
5
|
import * as acceptanceCriterion from "./acceptance-criterion.js";
|
|
6
6
|
import * as task from "./task.js";
|
|
7
|
+
import * as plan from "./plan.js";
|
|
7
8
|
import * as bug from "./bug.js";
|
|
8
9
|
import * as document from "./document.js";
|
|
9
10
|
|
|
@@ -12,6 +13,7 @@ const MODULES = {
|
|
|
12
13
|
requirement,
|
|
13
14
|
acceptance_criterion: acceptanceCriterion,
|
|
14
15
|
task,
|
|
16
|
+
plan,
|
|
15
17
|
bug,
|
|
16
18
|
document
|
|
17
19
|
};
|
|
@@ -51,6 +53,7 @@ export {
|
|
|
51
53
|
requirement,
|
|
52
54
|
acceptanceCriterion,
|
|
53
55
|
task,
|
|
56
|
+
plan,
|
|
54
57
|
bug,
|
|
55
58
|
document
|
|
56
59
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Plan state machine.
|
|
2
|
+
//
|
|
3
|
+
// draft → active → complete
|
|
4
|
+
// └────→ superseded
|
|
5
|
+
//
|
|
6
|
+
// `complete` and `superseded` are archive-eligible. Plans are optional
|
|
7
|
+
// support artifacts; their state machine tracks the plan artifact, not the
|
|
8
|
+
// owning task's DoD.
|
|
9
|
+
|
|
10
|
+
export const LEGAL_TRANSITIONS = {
|
|
11
|
+
draft: ["active", "superseded"],
|
|
12
|
+
active: ["complete", "superseded", "draft"],
|
|
13
|
+
complete: [],
|
|
14
|
+
superseded: []
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const TERMINAL_STATUSES = new Set(["complete", "superseded"]);
|
|
18
|
+
export const ARCHIVABLE_STATUSES = new Set(["complete", "superseded"]);
|
|
19
|
+
|
|
20
|
+
export function validateTransition(from, to) {
|
|
21
|
+
const allowed = LEGAL_TRANSITIONS[from];
|
|
22
|
+
if (!allowed) {
|
|
23
|
+
return { ok: false, error: `Unknown plan status '${from}'` };
|
|
24
|
+
}
|
|
25
|
+
if (!allowed.includes(to)) {
|
|
26
|
+
return {
|
|
27
|
+
ok: false,
|
|
28
|
+
error: `Plan cannot transition from '${from}' to '${to}' — allowed: ${allowed.join(", ") || "(terminal)"}`
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return { ok: true };
|
|
32
|
+
}
|
package/src/validator/common.js
CHANGED
|
@@ -197,9 +197,16 @@ function validateFieldShapes(errors, statement, fieldMap) {
|
|
|
197
197
|
ensureSingleValueField(errors, statement, fieldMap, "method", ["symbol"]);
|
|
198
198
|
ensureSingleValueField(errors, statement, fieldMap, "severity", ["symbol"]);
|
|
199
199
|
ensureSingleValueField(errors, statement, fieldMap, "category", ["symbol"]);
|
|
200
|
+
ensureSingleValueField(errors, statement, fieldMap, "priority", ["symbol"]);
|
|
201
|
+
ensureSingleValueField(errors, statement, fieldMap, "work_type", ["symbol"]);
|
|
202
|
+
ensureSingleValueField(errors, statement, fieldMap, "disposition", ["symbol"]);
|
|
203
|
+
ensureSingleValueField(errors, statement, fieldMap, "task", ["symbol"]);
|
|
200
204
|
ensureSingleValueField(errors, statement, fieldMap, "version", ["string"]);
|
|
205
|
+
ensureSingleValueField(errors, statement, fieldMap, "updated", ["string"]);
|
|
206
|
+
ensureSingleValueField(errors, statement, fieldMap, "notes", ["string"]);
|
|
207
|
+
ensureSingleValueField(errors, statement, fieldMap, "outcome", ["string"]);
|
|
201
208
|
|
|
202
|
-
|
|
209
|
+
const listFields = [
|
|
203
210
|
"aliases",
|
|
204
211
|
"excludes",
|
|
205
212
|
"uses_terms",
|
|
@@ -220,7 +227,6 @@ function validateFieldShapes(errors, statement, fieldMap) {
|
|
|
220
227
|
"realizes",
|
|
221
228
|
"outputs",
|
|
222
229
|
"inputs",
|
|
223
|
-
"steps",
|
|
224
230
|
"validates",
|
|
225
231
|
"scenarios",
|
|
226
232
|
"observes",
|
|
@@ -233,11 +239,19 @@ function validateFieldShapes(errors, statement, fieldMap) {
|
|
|
233
239
|
"lookups",
|
|
234
240
|
"dependencies",
|
|
235
241
|
"approvals"
|
|
236
|
-
]
|
|
242
|
+
];
|
|
243
|
+
if (statement.kind === "orchestration") {
|
|
244
|
+
listFields.push("steps");
|
|
245
|
+
}
|
|
246
|
+
for (const key of listFields) {
|
|
237
247
|
ensureSingleValueField(errors, statement, fieldMap, key, ["list"]);
|
|
238
248
|
}
|
|
239
249
|
|
|
240
|
-
|
|
250
|
+
const blockFields = ["fields", "props", "events", "slots", "behaviors", "keys", "relations", "invariants", "rename", "overrides", "endpoints", "error_responses", "wire_fields", "responses", "preconditions", "idempotency", "cache", "delete_semantics", "async_jobs", "async_status", "downloads", "authorization", "callbacks", "commands", "command_options", "command_outputs", "command_effects", "command_examples", "screens", "collection_views", "screen_actions", "visibility_rules", "field_lookups", "screen_routes", "web_hints", "ios_hints", "app_shell", "navigation", "screen_regions", "widget_bindings", "design_tokens", "tables", "columns", "keys", "indexes", "relations", "lifecycle", "generator_defaults"];
|
|
251
|
+
if (statement.kind === "plan") {
|
|
252
|
+
blockFields.push("steps");
|
|
253
|
+
}
|
|
254
|
+
for (const key of blockFields) {
|
|
241
255
|
ensureSingleValueField(errors, statement, fieldMap, key, ["block"]);
|
|
242
256
|
}
|
|
243
257
|
|
|
@@ -267,6 +281,11 @@ function validateFieldShapes(errors, statement, fieldMap) {
|
|
|
267
281
|
validateBlockEntryLengths(errors, statement, fieldMap, "downloads", 7);
|
|
268
282
|
validateBlockEntryLengths(errors, statement, fieldMap, "authorization", 3);
|
|
269
283
|
validateBlockEntryLengths(errors, statement, fieldMap, "callbacks", 11);
|
|
284
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "commands", 2);
|
|
285
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "command_options", 6);
|
|
286
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "command_outputs", 4);
|
|
287
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "command_effects", 4);
|
|
288
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "command_examples", 4);
|
|
270
289
|
validateBlockEntryLengths(errors, statement, fieldMap, "screens", 4);
|
|
271
290
|
validateBlockEntryLengths(errors, statement, fieldMap, "collection_views", 4);
|
|
272
291
|
validateBlockEntryLengths(errors, statement, fieldMap, "screen_actions", 6);
|
|
@@ -416,11 +435,13 @@ function validateReferenceKinds(errors, statement, fieldMap, registry) {
|
|
|
416
435
|
introduces_decisions: ["decision"],
|
|
417
436
|
satisfies: ["requirement", "acceptance_criterion"],
|
|
418
437
|
acceptance_refs: ["acceptance_criterion"],
|
|
438
|
+
verification_refs: ["verification"],
|
|
419
439
|
requirement_refs: ["requirement"],
|
|
420
440
|
fixes_bugs: ["bug"],
|
|
421
441
|
blocks: ["task"],
|
|
422
442
|
blocked_by: ["task"],
|
|
423
443
|
claimed_by: ["actor", "role"],
|
|
444
|
+
task: ["task"],
|
|
424
445
|
violates: ["rule"],
|
|
425
446
|
surfaces_rule: ["rule"],
|
|
426
447
|
introduced_in: ["task", "bug"],
|
package/src/validator/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { validateExpressions } from "./expressions.js";
|
|
|
5
5
|
import { validateApiHttpProjection } from "./projections/api-http.js";
|
|
6
6
|
import { validateDbProjection } from "./projections/db.js";
|
|
7
7
|
import { validateProjectionGeneratorDefaults } from "./projections/generator-defaults.js";
|
|
8
|
+
import { validateCliProjection } from "./projections/cli.js";
|
|
8
9
|
import { validateUiProjection } from "./projections/ui.js";
|
|
9
10
|
import { buildRegistry } from "./registry.js";
|
|
10
11
|
import {
|
|
@@ -17,6 +18,7 @@ import { validatePitch } from "./per-kind/pitch.js";
|
|
|
17
18
|
import { validateRequirement } from "./per-kind/requirement.js";
|
|
18
19
|
import { validateAcceptanceCriterion } from "./per-kind/acceptance-criterion.js";
|
|
19
20
|
import { validateTask } from "./per-kind/task.js";
|
|
21
|
+
import { validatePlan } from "./per-kind/plan.js";
|
|
20
22
|
import { validateBug } from "./per-kind/bug.js";
|
|
21
23
|
|
|
22
24
|
export {
|
|
@@ -28,17 +30,23 @@ export {
|
|
|
28
30
|
REQUIREMENT_IDENTIFIER_PATTERN,
|
|
29
31
|
ACCEPTANCE_CRITERION_IDENTIFIER_PATTERN,
|
|
30
32
|
TASK_IDENTIFIER_PATTERN,
|
|
33
|
+
PLAN_IDENTIFIER_PATTERN,
|
|
31
34
|
BUG_IDENTIFIER_PATTERN,
|
|
32
35
|
DOCUMENT_IDENTIFIER_PATTERN,
|
|
33
36
|
GLOBAL_STATUSES,
|
|
34
37
|
DECISION_STATUSES,
|
|
35
38
|
RULE_SEVERITIES,
|
|
36
39
|
VERIFICATION_METHODS,
|
|
40
|
+
CLI_COMMAND_EFFECTS,
|
|
41
|
+
CLI_COMMAND_OPTION_TYPES,
|
|
42
|
+
CLI_COMMAND_OUTPUT_FORMATS,
|
|
37
43
|
STATUS_SETS_BY_KIND,
|
|
38
44
|
PITCH_STATUSES,
|
|
39
45
|
REQUIREMENT_STATUSES,
|
|
40
46
|
ACCEPTANCE_CRITERION_STATUSES,
|
|
41
47
|
TASK_STATUSES,
|
|
48
|
+
PLAN_STATUSES,
|
|
49
|
+
PLAN_STEP_STATUSES,
|
|
42
50
|
BUG_STATUSES,
|
|
43
51
|
PRIORITY_VALUES,
|
|
44
52
|
WORK_TYPES,
|
|
@@ -91,6 +99,7 @@ export function validateWorkspace(workspaceAst) {
|
|
|
91
99
|
validateReferenceRules(errors, statement, fieldMap, registry);
|
|
92
100
|
validateDataModelStatement(errors, statement, fieldMap, registry);
|
|
93
101
|
validateApiHttpProjection(errors, statement, fieldMap, registry);
|
|
102
|
+
validateCliProjection(errors, statement, fieldMap, registry);
|
|
94
103
|
validateUiProjection(errors, statement, fieldMap, registry);
|
|
95
104
|
validateDbProjection(errors, statement, fieldMap, registry);
|
|
96
105
|
validateProjectionGeneratorDefaults(errors, statement, fieldMap);
|
|
@@ -101,6 +110,7 @@ export function validateWorkspace(workspaceAst) {
|
|
|
101
110
|
validateRequirement(errors, statement, fieldMap, registry);
|
|
102
111
|
validateAcceptanceCriterion(errors, statement, fieldMap, registry);
|
|
103
112
|
validateTask(errors, statement, fieldMap, registry);
|
|
113
|
+
validatePlan(errors, statement, fieldMap, registry);
|
|
104
114
|
validateBug(errors, statement, fieldMap, registry);
|
|
105
115
|
validateExpressions(errors, statement, fieldMap);
|
|
106
116
|
}
|
package/src/validator/kinds.d.ts
CHANGED
|
@@ -6,17 +6,24 @@ export const PITCH_IDENTIFIER_PATTERN: RegExp;
|
|
|
6
6
|
export const REQUIREMENT_IDENTIFIER_PATTERN: RegExp;
|
|
7
7
|
export const ACCEPTANCE_CRITERION_IDENTIFIER_PATTERN: RegExp;
|
|
8
8
|
export const TASK_IDENTIFIER_PATTERN: RegExp;
|
|
9
|
+
export const PLAN_IDENTIFIER_PATTERN: RegExp;
|
|
9
10
|
export const BUG_IDENTIFIER_PATTERN: RegExp;
|
|
10
11
|
export const DOCUMENT_IDENTIFIER_PATTERN: RegExp;
|
|
11
12
|
export const GLOBAL_STATUSES: Set<string>;
|
|
12
13
|
export const DECISION_STATUSES: Set<string>;
|
|
13
14
|
export const RULE_SEVERITIES: Set<string>;
|
|
14
15
|
export const VERIFICATION_METHODS: Set<string>;
|
|
16
|
+
export const CLI_COMMAND_EFFECTS: Set<string>;
|
|
17
|
+
export const CLI_COMMAND_OPTION_TYPES: Set<string>;
|
|
18
|
+
export const CLI_COMMAND_OUTPUT_FORMATS: Set<string>;
|
|
15
19
|
export const STATUS_SETS_BY_KIND: Record<string, Set<string>>;
|
|
16
20
|
export const PITCH_STATUSES: Set<string>;
|
|
17
21
|
export const REQUIREMENT_STATUSES: Set<string>;
|
|
18
22
|
export const ACCEPTANCE_CRITERION_STATUSES: Set<string>;
|
|
19
23
|
export const TASK_STATUSES: Set<string>;
|
|
24
|
+
export const TASK_DISPOSITIONS: Set<string>;
|
|
25
|
+
export const PLAN_STATUSES: Set<string>;
|
|
26
|
+
export const PLAN_STEP_STATUSES: Set<string>;
|
|
20
27
|
export const BUG_STATUSES: Set<string>;
|
|
21
28
|
export const PRIORITY_VALUES: Set<string>;
|
|
22
29
|
export const WORK_TYPES: Set<string>;
|
package/src/validator/kinds.js
CHANGED
|
@@ -20,6 +20,7 @@ export const STATEMENT_KINDS = new Set([
|
|
|
20
20
|
"requirement",
|
|
21
21
|
"acceptance_criterion",
|
|
22
22
|
"task",
|
|
23
|
+
"plan",
|
|
23
24
|
"bug"
|
|
24
25
|
]);
|
|
25
26
|
|
|
@@ -29,6 +30,7 @@ export const PITCH_IDENTIFIER_PATTERN = /^pitch_[a-z][a-z0-9_]*$/;
|
|
|
29
30
|
export const REQUIREMENT_IDENTIFIER_PATTERN = /^req_[a-z][a-z0-9_]*$/;
|
|
30
31
|
export const ACCEPTANCE_CRITERION_IDENTIFIER_PATTERN = /^ac_[a-z][a-z0-9_]*$/;
|
|
31
32
|
export const TASK_IDENTIFIER_PATTERN = /^task_[a-z][a-z0-9_]*$/;
|
|
33
|
+
export const PLAN_IDENTIFIER_PATTERN = /^plan_[a-z][a-z0-9_]*$/;
|
|
32
34
|
export const BUG_IDENTIFIER_PATTERN = /^bug_[a-z][a-z0-9_]*$/;
|
|
33
35
|
export const DOCUMENT_IDENTIFIER_PATTERN = /^doc_[a-z][a-z0-9_]*$/;
|
|
34
36
|
|
|
@@ -41,7 +43,10 @@ export const PITCH_STATUSES = new Set(["draft", "shaped", "submitted", "approved
|
|
|
41
43
|
export const REQUIREMENT_STATUSES = new Set(["draft", "in-review", "approved", "superseded"]);
|
|
42
44
|
export const ACCEPTANCE_CRITERION_STATUSES = new Set(["draft", "approved", "superseded"]);
|
|
43
45
|
export const TASK_STATUSES = new Set(["unclaimed", "claimed", "in-progress", "done", "blocked"]);
|
|
46
|
+
export const PLAN_STATUSES = new Set(["draft", "active", "complete", "superseded"]);
|
|
47
|
+
export const PLAN_STEP_STATUSES = new Set(["pending", "in-progress", "blocked", "done", "skipped"]);
|
|
44
48
|
export const BUG_STATUSES = new Set(["open", "in-progress", "fixed", "verified", "wont-fix"]);
|
|
49
|
+
export const TASK_DISPOSITIONS = new Set(["active", "follow_up", "deferred", "backlog", "blocker"]);
|
|
45
50
|
|
|
46
51
|
export const PRIORITY_VALUES = new Set(["critical", "high", "medium", "low"]);
|
|
47
52
|
export const WORK_TYPES = new Set([
|
|
@@ -73,9 +78,13 @@ export const STATUS_SETS_BY_KIND = {
|
|
|
73
78
|
requirement: REQUIREMENT_STATUSES,
|
|
74
79
|
acceptance_criterion: ACCEPTANCE_CRITERION_STATUSES,
|
|
75
80
|
task: TASK_STATUSES,
|
|
81
|
+
plan: PLAN_STATUSES,
|
|
76
82
|
bug: BUG_STATUSES
|
|
77
83
|
};
|
|
78
84
|
export const VERIFICATION_METHODS = new Set(["smoke", "runtime", "contract", "journey", "manual"]);
|
|
85
|
+
export const CLI_COMMAND_EFFECTS = new Set(["read_only", "writes_workspace", "writes_app", "network", "package_install", "git", "filesystem"]);
|
|
86
|
+
export const CLI_COMMAND_OPTION_TYPES = new Set(["string", "boolean", "number", "integer", "enum", "path", "list"]);
|
|
87
|
+
export const CLI_COMMAND_OUTPUT_FORMATS = new Set(["json", "human", "file", "exit_code"]);
|
|
79
88
|
|
|
80
89
|
export {
|
|
81
90
|
UI_APP_SHELL_KINDS,
|
|
@@ -114,6 +123,7 @@ export const DOMAIN_TAGGABLE_KINDS = new Set([
|
|
|
114
123
|
"pitch",
|
|
115
124
|
"requirement",
|
|
116
125
|
"task",
|
|
126
|
+
"plan",
|
|
117
127
|
"bug"
|
|
118
128
|
]);
|
|
119
129
|
|
|
@@ -179,6 +189,11 @@ export const FIELD_SPECS = {
|
|
|
179
189
|
"downloads",
|
|
180
190
|
"authorization",
|
|
181
191
|
"callbacks",
|
|
192
|
+
"commands",
|
|
193
|
+
"command_options",
|
|
194
|
+
"command_outputs",
|
|
195
|
+
"command_effects",
|
|
196
|
+
"command_examples",
|
|
182
197
|
"screens",
|
|
183
198
|
"collection_views",
|
|
184
199
|
"screen_actions",
|
|
@@ -277,9 +292,11 @@ export const FIELD_SPECS = {
|
|
|
277
292
|
"status",
|
|
278
293
|
"priority",
|
|
279
294
|
"work_type",
|
|
295
|
+
"disposition",
|
|
280
296
|
"affects",
|
|
281
297
|
"satisfies",
|
|
282
298
|
"acceptance_refs",
|
|
299
|
+
"verification_refs",
|
|
283
300
|
"blocks",
|
|
284
301
|
"blocked_by",
|
|
285
302
|
"claimed_by",
|
|
@@ -291,6 +308,21 @@ export const FIELD_SPECS = {
|
|
|
291
308
|
"updated"
|
|
292
309
|
]
|
|
293
310
|
},
|
|
311
|
+
plan: {
|
|
312
|
+
required: ["name", "description", "task", "status", "steps"],
|
|
313
|
+
allowed: [
|
|
314
|
+
"name",
|
|
315
|
+
"description",
|
|
316
|
+
"task",
|
|
317
|
+
"status",
|
|
318
|
+
"priority",
|
|
319
|
+
"notes",
|
|
320
|
+
"outcome",
|
|
321
|
+
"steps",
|
|
322
|
+
"domain",
|
|
323
|
+
"updated"
|
|
324
|
+
]
|
|
325
|
+
},
|
|
294
326
|
bug: {
|
|
295
327
|
required: ["name", "description", "status", "severity", "priority"],
|
|
296
328
|
allowed: [
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
PLAN_IDENTIFIER_PATTERN,
|
|
5
|
+
PLAN_STEP_STATUSES,
|
|
6
|
+
PRIORITY_VALUES
|
|
7
|
+
} from "../kinds.js";
|
|
8
|
+
import {
|
|
9
|
+
blockEntries,
|
|
10
|
+
pushError,
|
|
11
|
+
symbolValue
|
|
12
|
+
} from "../utils.js";
|
|
13
|
+
import { parsePlanStepEntry } from "../../sdlc/plan-steps.js";
|
|
14
|
+
|
|
15
|
+
/** @param {ValidationErrors} errors @param {TopogramStatement} statement */
|
|
16
|
+
function validatePlanIdentifier(errors, statement) {
|
|
17
|
+
if (!PLAN_IDENTIFIER_PATTERN.test(statement.id)) {
|
|
18
|
+
pushError(
|
|
19
|
+
errors,
|
|
20
|
+
`Plan identifier '${statement.id}' must match ${PLAN_IDENTIFIER_PATTERN.source}`,
|
|
21
|
+
statement.loc
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @param {ValidationErrors} errors @param {TopogramStatement} statement @param {TopogramFieldMap} fieldMap */
|
|
27
|
+
function validatePriority(errors, statement, fieldMap) {
|
|
28
|
+
const field = fieldMap.get("priority")?.[0];
|
|
29
|
+
if (!field) return;
|
|
30
|
+
if (field.value.type !== "symbol") {
|
|
31
|
+
pushError(errors, `Field 'priority' on plan ${statement.id} must be a symbol`, field.loc);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (!PRIORITY_VALUES.has(field.value.value)) {
|
|
35
|
+
pushError(errors, `Invalid priority '${field.value.value}' on plan ${statement.id}`, field.loc);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {ValidationErrors} errors
|
|
41
|
+
* @param {TopogramStatement} statement
|
|
42
|
+
* @param {TopogramFieldMap} fieldMap
|
|
43
|
+
* @param {TopogramRegistry} registry
|
|
44
|
+
* @returns {void}
|
|
45
|
+
*/
|
|
46
|
+
function validatePlanTask(errors, statement, fieldMap, registry) {
|
|
47
|
+
const field = fieldMap.get("task")?.[0];
|
|
48
|
+
if (!field || field.value.type !== "symbol") return;
|
|
49
|
+
const target = registry.get(field.value.value);
|
|
50
|
+
if (!target) return;
|
|
51
|
+
if (target.kind !== "task") {
|
|
52
|
+
pushError(errors, `Plan ${statement.id} task must reference a task, found ${target.kind} '${target.id}'`, field.loc);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {ValidationErrors} errors
|
|
58
|
+
* @param {TopogramStatement} statement
|
|
59
|
+
* @param {TopogramFieldMap} fieldMap
|
|
60
|
+
* @returns {void}
|
|
61
|
+
*/
|
|
62
|
+
function validateStepRows(errors, statement, fieldMap) {
|
|
63
|
+
const field = fieldMap.get("steps")?.[0];
|
|
64
|
+
if (!field || field.value.type !== "block") return;
|
|
65
|
+
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
const entries = blockEntries(field.value);
|
|
68
|
+
if (entries.length === 0) {
|
|
69
|
+
pushError(errors, `Plan ${statement.id} must include at least one step`, field.loc);
|
|
70
|
+
}
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
const [kindToken, idToken, ...rest] = entry.items;
|
|
73
|
+
if (kindToken?.type !== "symbol" || kindToken.value !== "step") {
|
|
74
|
+
pushError(errors, `Each 'steps' entry on plan ${statement.id} must start with 'step'`, entry.loc);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (idToken?.type !== "symbol" || !/^[a-z][a-z0-9_]*$/.test(idToken.value)) {
|
|
78
|
+
pushError(errors, `Plan ${statement.id} step id must be a symbol matching /^[a-z][a-z0-9_]*$/`, idToken?.loc || entry.loc);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (seen.has(idToken.value)) {
|
|
82
|
+
pushError(errors, `Plan ${statement.id} has duplicate step '${idToken.value}'`, idToken.loc);
|
|
83
|
+
}
|
|
84
|
+
seen.add(idToken.value);
|
|
85
|
+
|
|
86
|
+
if (rest.length % 2 !== 0) {
|
|
87
|
+
pushError(errors, `Plan ${statement.id} step '${idToken.value}' fields must be key/value pairs`, entry.loc);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const allowedKeys = new Set(["status", "description", "notes", "outcome"]);
|
|
91
|
+
for (let index = 0; index < rest.length - 1; index += 2) {
|
|
92
|
+
const keyToken = rest[index];
|
|
93
|
+
if (keyToken?.type !== "symbol" || !allowedKeys.has(keyToken.value)) {
|
|
94
|
+
pushError(errors, `Plan ${statement.id} step '${idToken.value}' has unsupported field '${keyToken?.value || "unknown"}'`, keyToken?.loc || entry.loc);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const parsed = parsePlanStepEntry(entry);
|
|
99
|
+
if (!parsed.status) {
|
|
100
|
+
pushError(errors, `Plan ${statement.id} step '${idToken.value}' requires status`, entry.loc);
|
|
101
|
+
} else if (!PLAN_STEP_STATUSES.has(parsed.status)) {
|
|
102
|
+
pushError(errors, `Invalid step status '${parsed.status}' on plan ${statement.id} step '${idToken.value}'`, entry.loc);
|
|
103
|
+
}
|
|
104
|
+
if (!parsed.description) {
|
|
105
|
+
pushError(errors, `Plan ${statement.id} step '${idToken.value}' requires description`, entry.loc);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** @param {ValidationErrors} errors @param {TopogramStatement} statement @param {TopogramFieldMap} fieldMap @param {TopogramRegistry} registry */
|
|
111
|
+
export function validatePlan(errors, statement, fieldMap, registry) {
|
|
112
|
+
if (statement.kind !== "plan") {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
validatePlanIdentifier(errors, statement);
|
|
116
|
+
validatePriority(errors, statement, fieldMap);
|
|
117
|
+
validatePlanTask(errors, statement, fieldMap, registry);
|
|
118
|
+
validateStepRows(errors, statement, fieldMap);
|
|
119
|
+
|
|
120
|
+
const status = symbolValue(fieldMap.get("status")?.[0]?.value);
|
|
121
|
+
if (status === "complete") {
|
|
122
|
+
const steps = blockEntries(fieldMap.get("steps")?.[0]?.value).map((entry) => parsePlanStepEntry(entry));
|
|
123
|
+
const incomplete = steps.filter((step) => step.status !== "done" && step.status !== "skipped");
|
|
124
|
+
if (incomplete.length > 0) {
|
|
125
|
+
pushError(errors, `Plan ${statement.id} status 'complete' requires all steps to be done or skipped`, fieldMap.get("status")?.[0]?.loc || statement.loc);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|