@topogram/cli 0.3.72 → 0.3.74
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/release-rollout.js +191 -10
- package/src/cli/commands/release-shared.js +51 -2
- package/src/cli/commands/release.js +16 -3
- package/src/cli/commands/sdlc.js +213 -5
- package/src/cli/dispatcher.js +8 -0
- package/src/cli/help.js +15 -3
- 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
|
@@ -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
|
};
|