@topogram/cli 0.3.71 → 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/project.js +0 -3
- 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 -5
- package/src/cli/help.js +14 -3
- package/src/cli/migration-guidance.js +3 -0
- 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/generator/surfaces/databases/lifecycle-shared.js +2 -2
- 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 +34 -3
- package/src/cli/commands/migrate.js +0 -153
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// Back-link arrays:
|
|
5
5
|
// blockingMe — tasks whose `blocks` references this task (reciprocal of blocked_by)
|
|
6
6
|
// blockedByMe — tasks whose `blocked_by` references this task (reciprocal of blocks)
|
|
7
|
+
// plans — implementation plans attached to this task
|
|
7
8
|
//
|
|
8
9
|
// Note: we deliberately compute *both* directions even though `blocks` and
|
|
9
10
|
// `blocked_by` are reciprocal, because authors only write one side. The
|
|
@@ -13,6 +14,7 @@
|
|
|
13
14
|
export function enrichTask(task, index) {
|
|
14
15
|
return {
|
|
15
16
|
blockingMe: (index.tasksThatBlockTarget.get(task.id) || []).slice().sort(),
|
|
16
|
-
blockedByMe: (index.tasksBlockedByTarget.get(task.id) || []).slice().sort()
|
|
17
|
+
blockedByMe: (index.tasksBlockedByTarget.get(task.id) || []).slice().sort(),
|
|
18
|
+
plans: (index.plansByTask.get(task.id) || []).slice().sort()
|
|
17
19
|
};
|
|
18
20
|
}
|
package/src/resolver/index.js
CHANGED
|
@@ -114,6 +114,7 @@ export function resolveWorkspace(workspaceAst) {
|
|
|
114
114
|
pitches: [],
|
|
115
115
|
requirements: [],
|
|
116
116
|
tasks: [],
|
|
117
|
+
plans: [],
|
|
117
118
|
bugs: [],
|
|
118
119
|
documents: []
|
|
119
120
|
});
|
|
@@ -130,6 +131,7 @@ export function resolveWorkspace(workspaceAst) {
|
|
|
130
131
|
pitch: "pitches",
|
|
131
132
|
requirement: "requirements",
|
|
132
133
|
task: "tasks",
|
|
134
|
+
plan: "plans",
|
|
133
135
|
bug: "bugs"
|
|
134
136
|
};
|
|
135
137
|
for (const statement of resolvedStatements) {
|
|
@@ -175,6 +177,7 @@ export function resolveWorkspace(workspaceAst) {
|
|
|
175
177
|
rulesByFromRequirement: new Map(),
|
|
176
178
|
tasksThatBlockTarget: new Map(),
|
|
177
179
|
tasksBlockedByTarget: new Map(),
|
|
180
|
+
plansByTask: new Map(),
|
|
178
181
|
affectedByPitches: new Map(),
|
|
179
182
|
affectedByRequirements: new Map(),
|
|
180
183
|
affectedByTasks: new Map(),
|
|
@@ -227,6 +230,9 @@ export function resolveWorkspace(workspaceAst) {
|
|
|
227
230
|
pushIndexFromList(sdlcIndex.tasksThatBlockTarget, statement.blocks, statement.id);
|
|
228
231
|
pushIndexFromList(sdlcIndex.tasksBlockedByTarget, statement.blockedBy, statement.id);
|
|
229
232
|
break;
|
|
233
|
+
case "plan":
|
|
234
|
+
pushIndex(sdlcIndex.plansByTask, statement.task?.id, statement.id);
|
|
235
|
+
break;
|
|
230
236
|
case "bug":
|
|
231
237
|
pushIndexFromList(sdlcIndex.affectedByBugs, statement.affects, statement.id);
|
|
232
238
|
pushIndexFromList(sdlcIndex.rulesViolatedByBug, statement.violates, statement.id);
|
|
@@ -43,6 +43,13 @@ import {
|
|
|
43
43
|
parseProjectionHttpResponsesBlock,
|
|
44
44
|
parseProjectionHttpStatusBlock
|
|
45
45
|
} from "./projections-api.js";
|
|
46
|
+
import {
|
|
47
|
+
parseProjectionCliCommandsBlock,
|
|
48
|
+
parseProjectionCliEffectsBlock,
|
|
49
|
+
parseProjectionCliExamplesBlock,
|
|
50
|
+
parseProjectionCliOptionsBlock,
|
|
51
|
+
parseProjectionCliOutputsBlock
|
|
52
|
+
} from "./projections-cli.js";
|
|
46
53
|
import {
|
|
47
54
|
parseProjectionUiActionsBlock,
|
|
48
55
|
parseProjectionUiAppShellBlock,
|
|
@@ -67,6 +74,7 @@ import {
|
|
|
67
74
|
parseProjectionDbTablesBlock,
|
|
68
75
|
parseProjectionGeneratorDefaultsBlock
|
|
69
76
|
} from "./projections-db.js";
|
|
77
|
+
import { parsePlanSteps } from "../sdlc/plan-steps.js";
|
|
70
78
|
|
|
71
79
|
export function normalizeStatement(statement, registry) {
|
|
72
80
|
const fieldMap = collectFieldMap(statement);
|
|
@@ -209,6 +217,11 @@ export function normalizeStatement(statement, registry) {
|
|
|
209
217
|
httpDownload: parseProjectionHttpDownloadBlock(statement, registry),
|
|
210
218
|
httpAuthz: parseProjectionHttpAuthzBlock(statement, registry),
|
|
211
219
|
httpCallbacks: parseProjectionHttpCallbacksBlock(statement, registry),
|
|
220
|
+
commands: parseProjectionCliCommandsBlock(statement, registry),
|
|
221
|
+
commandOptions: parseProjectionCliOptionsBlock(statement),
|
|
222
|
+
commandOutputs: parseProjectionCliOutputsBlock(statement, registry),
|
|
223
|
+
commandEffects: parseProjectionCliEffectsBlock(statement),
|
|
224
|
+
commandExamples: parseProjectionCliExamplesBlock(statement),
|
|
212
225
|
uiScreens: parseProjectionUiScreensBlock(statement, registry),
|
|
213
226
|
screens: parseProjectionUiScreensBlock(statement, registry),
|
|
214
227
|
uiCollections: parseProjectionUiCollectionsBlock(statement),
|
|
@@ -343,9 +356,11 @@ export function normalizeStatement(statement, registry) {
|
|
|
343
356
|
...base,
|
|
344
357
|
priority: symbolValue(getFieldValue(statement, "priority")),
|
|
345
358
|
workType: symbolValue(getFieldValue(statement, "work_type")),
|
|
359
|
+
disposition: symbolValue(getFieldValue(statement, "disposition")),
|
|
346
360
|
affects: resolveReferenceList(registry, getFieldValue(statement, "affects")),
|
|
347
361
|
satisfies: resolveReferenceList(registry, getFieldValue(statement, "satisfies")),
|
|
348
362
|
acceptanceRefs: resolveReferenceList(registry, getFieldValue(statement, "acceptance_refs")),
|
|
363
|
+
verificationRefs: resolveReferenceList(registry, getFieldValue(statement, "verification_refs")),
|
|
349
364
|
blocks: resolveReferenceList(registry, getFieldValue(statement, "blocks")),
|
|
350
365
|
blockedBy: resolveReferenceList(registry, getFieldValue(statement, "blocked_by")),
|
|
351
366
|
claimedBy: resolveReferenceList(registry, getFieldValue(statement, "claimed_by")),
|
|
@@ -356,6 +371,22 @@ export function normalizeStatement(statement, registry) {
|
|
|
356
371
|
updated: stringValue(getFieldValue(statement, "updated")),
|
|
357
372
|
resolvedDomain: resolveDomainTag(statement, registry)
|
|
358
373
|
};
|
|
374
|
+
case "plan":
|
|
375
|
+
return {
|
|
376
|
+
...base,
|
|
377
|
+
task: getFieldValue(statement, "task")
|
|
378
|
+
? {
|
|
379
|
+
id: symbolValue(getFieldValue(statement, "task")),
|
|
380
|
+
target: toRef(resolveReference(registry, symbolValue(getFieldValue(statement, "task"))))
|
|
381
|
+
}
|
|
382
|
+
: null,
|
|
383
|
+
priority: symbolValue(getFieldValue(statement, "priority")),
|
|
384
|
+
notes: stringValue(getFieldValue(statement, "notes")),
|
|
385
|
+
outcome: stringValue(getFieldValue(statement, "outcome")),
|
|
386
|
+
steps: parsePlanSteps(getFieldValue(statement, "steps")),
|
|
387
|
+
updated: stringValue(getFieldValue(statement, "updated")),
|
|
388
|
+
resolvedDomain: resolveDomainTag(statement, registry)
|
|
389
|
+
};
|
|
359
390
|
case "bug":
|
|
360
391
|
return {
|
|
361
392
|
...base,
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { blockEntries, getFieldValue } from "../validator/utils.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {TopogramToken | null | undefined} token
|
|
7
|
+
* @returns {string | null}
|
|
8
|
+
*/
|
|
9
|
+
function tokenText(token) {
|
|
10
|
+
return token?.type === "symbol" || token?.type === "string" ? token.value : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {TopogramToken[]} items
|
|
15
|
+
* @returns {(string | string[])[]}
|
|
16
|
+
*/
|
|
17
|
+
function normalizeSequence(items) {
|
|
18
|
+
return items.map((item) => {
|
|
19
|
+
if (item.type === "symbol" || item.type === "string") {
|
|
20
|
+
return item.value;
|
|
21
|
+
}
|
|
22
|
+
if (item.type === "list") {
|
|
23
|
+
return item.items.map((nested) => tokenText(nested)).filter((value) => value !== null);
|
|
24
|
+
}
|
|
25
|
+
return item.type;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {TopogramToken[]} items
|
|
31
|
+
* @param {number} startIndex
|
|
32
|
+
* @returns {Record<string, string | string[]>}
|
|
33
|
+
*/
|
|
34
|
+
function parseDirectives(items, startIndex) {
|
|
35
|
+
/** @type {Record<string, string | string[]>} */
|
|
36
|
+
const directives = {};
|
|
37
|
+
for (let index = startIndex; index < items.length; index += 2) {
|
|
38
|
+
const key = tokenText(items[index]);
|
|
39
|
+
const valueToken = items[index + 1];
|
|
40
|
+
const value = valueToken?.type === "list"
|
|
41
|
+
? valueToken.items.map((item) => tokenText(item)).filter((item) => item !== null)
|
|
42
|
+
: tokenText(valueToken);
|
|
43
|
+
if (key && value != null) {
|
|
44
|
+
directives[key] = value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return directives;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {TopogramStatement} statement
|
|
52
|
+
* @param {TopogramRegistry} registry
|
|
53
|
+
* @returns {Record<string, any>[]}
|
|
54
|
+
*/
|
|
55
|
+
export function parseProjectionCliCommandsBlock(statement, registry) {
|
|
56
|
+
return blockEntries(getFieldValue(statement, "commands")).map((entry) => {
|
|
57
|
+
const commandId = tokenText(entry.items[1]);
|
|
58
|
+
const directives = parseDirectives(entry.items, 2);
|
|
59
|
+
const capabilityId = typeof directives.capability === "string" ? directives.capability : null;
|
|
60
|
+
return {
|
|
61
|
+
type: "cli_command",
|
|
62
|
+
id: commandId,
|
|
63
|
+
capability: capabilityId
|
|
64
|
+
? {
|
|
65
|
+
id: capabilityId,
|
|
66
|
+
kind: registry.get(capabilityId)?.kind || null
|
|
67
|
+
}
|
|
68
|
+
: null,
|
|
69
|
+
usage: typeof directives.usage === "string" ? directives.usage : null,
|
|
70
|
+
mode: typeof directives.mode === "string" ? directives.mode : null,
|
|
71
|
+
description: typeof directives.description === "string" ? directives.description : null,
|
|
72
|
+
raw: normalizeSequence(entry.items),
|
|
73
|
+
loc: entry.loc
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @param {TopogramStatement} statement
|
|
80
|
+
* @returns {Record<string, any>[]}
|
|
81
|
+
*/
|
|
82
|
+
export function parseProjectionCliOptionsBlock(statement) {
|
|
83
|
+
return blockEntries(getFieldValue(statement, "command_options")).map((entry) => {
|
|
84
|
+
const directives = parseDirectives(entry.items, 4);
|
|
85
|
+
return {
|
|
86
|
+
type: "cli_option",
|
|
87
|
+
command: tokenText(entry.items[1]),
|
|
88
|
+
name: tokenText(entry.items[3]),
|
|
89
|
+
optionType: typeof directives.type === "string" ? directives.type : null,
|
|
90
|
+
flag: typeof directives.flag === "string" ? directives.flag : null,
|
|
91
|
+
required: directives.required === "true",
|
|
92
|
+
defaultValue: directives.default ?? null,
|
|
93
|
+
description: typeof directives.description === "string" ? directives.description : null,
|
|
94
|
+
values: Array.isArray(directives.values) ? directives.values : [],
|
|
95
|
+
raw: normalizeSequence(entry.items),
|
|
96
|
+
loc: entry.loc
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {TopogramStatement} statement
|
|
103
|
+
* @param {TopogramRegistry} registry
|
|
104
|
+
* @returns {Record<string, any>[]}
|
|
105
|
+
*/
|
|
106
|
+
export function parseProjectionCliOutputsBlock(statement, registry) {
|
|
107
|
+
return blockEntries(getFieldValue(statement, "command_outputs")).map((entry) => {
|
|
108
|
+
const directives = parseDirectives(entry.items, 4);
|
|
109
|
+
const schemaId = typeof directives.schema === "string" ? directives.schema : null;
|
|
110
|
+
return {
|
|
111
|
+
type: "cli_output",
|
|
112
|
+
command: tokenText(entry.items[1]),
|
|
113
|
+
format: tokenText(entry.items[3]),
|
|
114
|
+
schema: schemaId
|
|
115
|
+
? {
|
|
116
|
+
id: schemaId,
|
|
117
|
+
kind: registry.get(schemaId)?.kind || null
|
|
118
|
+
}
|
|
119
|
+
: null,
|
|
120
|
+
description: typeof directives.description === "string" ? directives.description : null,
|
|
121
|
+
raw: normalizeSequence(entry.items),
|
|
122
|
+
loc: entry.loc
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {TopogramStatement} statement
|
|
129
|
+
* @returns {Record<string, any>[]}
|
|
130
|
+
*/
|
|
131
|
+
export function parseProjectionCliEffectsBlock(statement) {
|
|
132
|
+
return blockEntries(getFieldValue(statement, "command_effects")).map((entry) => {
|
|
133
|
+
const directives = parseDirectives(entry.items, 4);
|
|
134
|
+
return {
|
|
135
|
+
type: "cli_effect",
|
|
136
|
+
command: tokenText(entry.items[1]),
|
|
137
|
+
effect: tokenText(entry.items[3]),
|
|
138
|
+
target: typeof directives.target === "string" ? directives.target : null,
|
|
139
|
+
description: typeof directives.description === "string" ? directives.description : null,
|
|
140
|
+
raw: normalizeSequence(entry.items),
|
|
141
|
+
loc: entry.loc
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @param {TopogramStatement} statement
|
|
148
|
+
* @returns {Record<string, any>[]}
|
|
149
|
+
*/
|
|
150
|
+
export function parseProjectionCliExamplesBlock(statement) {
|
|
151
|
+
return blockEntries(getFieldValue(statement, "command_examples")).map((entry) => ({
|
|
152
|
+
type: "cli_example",
|
|
153
|
+
command: tokenText(entry.items[1]),
|
|
154
|
+
example: tokenText(entry.items[3]),
|
|
155
|
+
raw: normalizeSequence(entry.items),
|
|
156
|
+
loc: entry.loc
|
|
157
|
+
}));
|
|
158
|
+
}
|
package/src/sdlc/adopt.js
CHANGED
|
@@ -7,18 +7,21 @@
|
|
|
7
7
|
import { existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import { DEFAULT_TOPO_FOLDER_NAME, resolveTopoRoot } from "../workspace-paths.js";
|
|
10
|
+
import { sdlcRootForSdlc } from "./paths.js";
|
|
10
11
|
|
|
11
12
|
const SDLC_FOLDERS = [
|
|
12
13
|
"pitches",
|
|
13
14
|
"requirements",
|
|
14
15
|
"acceptance_criteria",
|
|
15
16
|
"tasks",
|
|
17
|
+
"plans",
|
|
16
18
|
"bugs",
|
|
19
|
+
"decisions",
|
|
17
20
|
"_archive"
|
|
18
21
|
];
|
|
19
22
|
|
|
20
23
|
function ensureFolder(root, name) {
|
|
21
|
-
const dir = path.join(
|
|
24
|
+
const dir = path.join(sdlcRootForSdlc(root), name);
|
|
22
25
|
if (!existsSync(dir)) {
|
|
23
26
|
mkdirSync(dir, { recursive: true });
|
|
24
27
|
return { name, created: true };
|
package/src/sdlc/check.js
CHANGED
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
// `--strict` mode.
|
|
11
11
|
|
|
12
12
|
import { checkDoD } from "./dod/index.js";
|
|
13
|
-
import { detectDriftedStatus, readHistory } from "./history.js";
|
|
13
|
+
import { detectDriftedStatus, readHistory, validateHistory } from "./history.js";
|
|
14
|
+
import { planStepHistoryId } from "./plan-steps.js";
|
|
14
15
|
|
|
15
16
|
const SDLC_KINDS = new Set([
|
|
16
17
|
"pitch",
|
|
17
18
|
"requirement",
|
|
18
19
|
"acceptance_criterion",
|
|
19
20
|
"task",
|
|
21
|
+
"plan",
|
|
20
22
|
"bug"
|
|
21
23
|
]);
|
|
22
24
|
|
|
@@ -47,6 +49,10 @@ export function checkWorkspace(workspaceRoot, resolved) {
|
|
|
47
49
|
const history = readHistory(workspaceRoot);
|
|
48
50
|
if (history.__error) {
|
|
49
51
|
warnings.push({ message: `cannot read SDLC history sidecar: ${history.__error}` });
|
|
52
|
+
} else {
|
|
53
|
+
for (const warning of validateHistory(history)) {
|
|
54
|
+
warnings.push(warning);
|
|
55
|
+
}
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
const byId = new Map(resolved.graph.statements.map((s) => [s.id, s]));
|
|
@@ -59,10 +65,26 @@ export function checkWorkspace(workspaceRoot, resolved) {
|
|
|
59
65
|
if (drift) {
|
|
60
66
|
warnings.push({
|
|
61
67
|
id: statement.id,
|
|
62
|
-
message: `status drift: history records '${drift.historyStatus}' but current is '${drift.currentStatus}'
|
|
68
|
+
message: `status drift: history records '${drift.historyStatus}' but current is '${drift.currentStatus}'. Use topogram sdlc transition so status and history stay aligned.`
|
|
63
69
|
});
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
if (statement.kind === "plan") {
|
|
73
|
+
for (const step of statement.steps || []) {
|
|
74
|
+
const stepDrift = detectDriftedStatus(history, {
|
|
75
|
+
id: planStepHistoryId(statement.id, step.id),
|
|
76
|
+
kind: "plan_step",
|
|
77
|
+
status: step.status
|
|
78
|
+
});
|
|
79
|
+
if (stepDrift) {
|
|
80
|
+
warnings.push({
|
|
81
|
+
id: statement.id,
|
|
82
|
+
message: `step status drift: history records '${stepDrift.historyStatus}' for ${step.id} but current is '${stepDrift.currentStatus}'. Use topogram sdlc plan step transition so step status and history stay aligned.`
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
66
88
|
// Re-run DoD against the *current* status to surface "approved without
|
|
67
89
|
// ACs" or similar ongoing violations.
|
|
68
90
|
const dod = checkDoD(statement.kind, statement, statement.status, { byId });
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { linkSdlcRecord } from "./link.js";
|
|
4
|
+
import { transitionStatement } from "./transition.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} workspaceRoot
|
|
8
|
+
* @param {string} taskId
|
|
9
|
+
* @param {string} verificationId
|
|
10
|
+
* @param {{ write?: boolean, actor?: string|null, note?: string|null }} [options]
|
|
11
|
+
* @returns {Record<string, any>}
|
|
12
|
+
*/
|
|
13
|
+
export function completeTask(workspaceRoot, taskId, verificationId, options = {}) {
|
|
14
|
+
if (!verificationId) {
|
|
15
|
+
return { ok: false, error: "sdlc complete requires --verification <verification-id>" };
|
|
16
|
+
}
|
|
17
|
+
const link = linkSdlcRecord(workspaceRoot, taskId, verificationId, { write: Boolean(options.write) });
|
|
18
|
+
if (!link.ok) {
|
|
19
|
+
return { ok: false, taskId, verificationId, link };
|
|
20
|
+
}
|
|
21
|
+
if (!options.write) {
|
|
22
|
+
return {
|
|
23
|
+
ok: true,
|
|
24
|
+
dryRun: true,
|
|
25
|
+
taskId,
|
|
26
|
+
verificationId,
|
|
27
|
+
link,
|
|
28
|
+
transition: {
|
|
29
|
+
planned: true,
|
|
30
|
+
to: "done"
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const transition = transitionStatement(workspaceRoot, taskId, "done", {
|
|
36
|
+
actor: options.actor || null,
|
|
37
|
+
note: options.note || null
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
ok: Boolean(transition.ok),
|
|
41
|
+
dryRun: false,
|
|
42
|
+
taskId,
|
|
43
|
+
verificationId,
|
|
44
|
+
link,
|
|
45
|
+
transition
|
|
46
|
+
};
|
|
47
|
+
}
|
package/src/sdlc/dod/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { checkDoD as checkPitch } from "./pitch.js";
|
|
|
4
4
|
import { checkDoD as checkRequirement } from "./requirement.js";
|
|
5
5
|
import { checkDoD as checkAcceptanceCriterion } from "./acceptance-criterion.js";
|
|
6
6
|
import { checkDoD as checkTask } from "./task.js";
|
|
7
|
+
import { checkDoD as checkPlan } from "./plan.js";
|
|
7
8
|
import { checkDoD as checkBug } from "./bug.js";
|
|
8
9
|
import { checkDoD as checkDocument } from "./document.js";
|
|
9
10
|
|
|
@@ -12,6 +13,7 @@ const CHECKS = {
|
|
|
12
13
|
requirement: checkRequirement,
|
|
13
14
|
acceptance_criterion: checkAcceptanceCriterion,
|
|
14
15
|
task: checkTask,
|
|
16
|
+
plan: checkPlan,
|
|
15
17
|
bug: checkBug,
|
|
16
18
|
document: checkDocument
|
|
17
19
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Plan DoD per status.
|
|
2
|
+
|
|
3
|
+
export function checkDoD(plan, targetStatus) {
|
|
4
|
+
const errors = [];
|
|
5
|
+
const warnings = [];
|
|
6
|
+
|
|
7
|
+
if (targetStatus === "complete") {
|
|
8
|
+
const incomplete = (plan.steps || []).filter((step) => step.status !== "done" && step.status !== "skipped");
|
|
9
|
+
if (incomplete.length > 0) {
|
|
10
|
+
errors.push(`status 'complete' requires all plan steps to be done or skipped: ${incomplete.map((step) => step.id).join(", ")}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return { satisfied: errors.length === 0, errors, warnings };
|
|
15
|
+
}
|
package/src/sdlc/dod/task.js
CHANGED
|
@@ -27,11 +27,15 @@ export function checkDoD(task, targetStatus, graph) {
|
|
|
27
27
|
|
|
28
28
|
if (targetStatus === "done") {
|
|
29
29
|
if (!task.satisfies || task.satisfies.length === 0) {
|
|
30
|
-
|
|
30
|
+
errors.push("status 'done' requires field 'satisfies'");
|
|
31
31
|
}
|
|
32
32
|
const acs = task.acceptanceRefs || [];
|
|
33
|
-
if (acs.length === 0
|
|
34
|
-
|
|
33
|
+
if (acs.length === 0) {
|
|
34
|
+
errors.push("status 'done' requires field 'acceptance_refs'");
|
|
35
|
+
}
|
|
36
|
+
const verifications = task.verificationRefs || [];
|
|
37
|
+
if (verifications.length === 0) {
|
|
38
|
+
errors.push("status 'done' requires field 'verification_refs'");
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
|
package/src/sdlc/explain.js
CHANGED
|
@@ -14,6 +14,7 @@ import { checkDoD } from "./dod/index.js";
|
|
|
14
14
|
import { legalTransitionsFor, isTerminalStatus } from "./transitions/index.js";
|
|
15
15
|
import { defaultActiveStatuses } from "./status-filter.js";
|
|
16
16
|
import { readHistory, lastTransition, detectDriftedStatus } from "./history.js";
|
|
17
|
+
import { planStepHistoryId } from "./plan-steps.js";
|
|
17
18
|
|
|
18
19
|
function pickNextStatus(legal) {
|
|
19
20
|
// Prefer the canonical forward path (skipping rollback options).
|
|
@@ -22,9 +23,9 @@ function pickNextStatus(legal) {
|
|
|
22
23
|
"approved",
|
|
23
24
|
"submitted",
|
|
24
25
|
"shaped",
|
|
25
|
-
"claimed",
|
|
26
26
|
"in-progress",
|
|
27
27
|
"done",
|
|
28
|
+
"claimed",
|
|
28
29
|
"fixed",
|
|
29
30
|
"verified",
|
|
30
31
|
"review",
|
|
@@ -48,6 +49,54 @@ function buildBlockers(statement, byId) {
|
|
|
48
49
|
.filter(Boolean);
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
function summarizePlan(plan, history) {
|
|
53
|
+
const steps = (plan.steps || []).map((step) => ({
|
|
54
|
+
id: step.id,
|
|
55
|
+
status: step.status,
|
|
56
|
+
description: step.description,
|
|
57
|
+
notes: step.notes || null,
|
|
58
|
+
outcome: step.outcome || null,
|
|
59
|
+
last_transition: lastTransition(history, planStepHistoryId(plan.id, step.id))
|
|
60
|
+
}));
|
|
61
|
+
return {
|
|
62
|
+
id: plan.id,
|
|
63
|
+
status: plan.status,
|
|
64
|
+
task: plan.task?.id || null,
|
|
65
|
+
steps,
|
|
66
|
+
next_step: steps.find((step) => step.status !== "done" && step.status !== "skipped") || null
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function plansForTask(graph, task, history) {
|
|
71
|
+
const planIds = task.kind === "task" ? task.plans || [] : [];
|
|
72
|
+
return planIds
|
|
73
|
+
.map((id) => graph.statements.find((statement) => statement.id === id && statement.kind === "plan" && !statement.archived))
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.map((plan) => summarizePlan(plan, history));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function recommendedQueries(statement) {
|
|
79
|
+
if (statement.kind === "task") {
|
|
80
|
+
return [
|
|
81
|
+
`topogram query slice ./topo --task ${statement.id} --json`,
|
|
82
|
+
`topogram query single-agent-plan ./topo --mode modeling --task ${statement.id} --json`
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
if (statement.kind === "bug") {
|
|
86
|
+
return [
|
|
87
|
+
`topogram query slice ./topo --bug ${statement.id} --json`,
|
|
88
|
+
`topogram query single-agent-plan ./topo --mode modeling --bug ${statement.id} --json`
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
if (statement.kind === "plan") {
|
|
92
|
+
return [
|
|
93
|
+
`topogram sdlc plan explain ${statement.id} --json`,
|
|
94
|
+
`topogram query slice ./topo --plan ${statement.id} --json`
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
return [`topogram query slice ./topo --${statement.kind} ${statement.id} --json`];
|
|
98
|
+
}
|
|
99
|
+
|
|
51
100
|
export function explain(workspaceRoot, resolved, id, options = {}) {
|
|
52
101
|
const statement = resolved.graph.statements.find((s) => s.id === id);
|
|
53
102
|
if (!statement) {
|
|
@@ -110,6 +159,9 @@ export function explain(workspaceRoot, resolved, id, options = {}) {
|
|
|
110
159
|
last_transition: last,
|
|
111
160
|
drift,
|
|
112
161
|
blockers,
|
|
162
|
+
plans: statement.kind === "task" ? plansForTask(resolved.graph, statement, history) : undefined,
|
|
163
|
+
plan: statement.kind === "plan" ? summarizePlan(statement, history) : undefined,
|
|
164
|
+
recommended_queries: recommendedQueries(statement),
|
|
113
165
|
next_action: nextAction,
|
|
114
166
|
history: options.includeHistory ? history[id] || [] : undefined
|
|
115
167
|
};
|