@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.
Files changed (81) hide show
  1. package/README.md +24 -195
  2. package/package.json +1 -1
  3. package/src/adoption/plan/index.js +2 -1
  4. package/src/agent-brief.js +46 -2
  5. package/src/archive/archive.js +1 -1
  6. package/src/archive/jsonl.js +18 -8
  7. package/src/archive/resolver-bridge.js +34 -1
  8. package/src/archive/schema.js +1 -1
  9. package/src/archive/unarchive.js +26 -0
  10. package/src/cli/command-parsers/sdlc.js +66 -0
  11. package/src/cli/commands/import/help.js +1 -0
  12. package/src/cli/commands/import/plan.js +9 -0
  13. package/src/cli/commands/import/workspace.js +3 -0
  14. package/src/cli/commands/query/definitions.js +11 -10
  15. package/src/cli/commands/query/workspace.js +23 -2
  16. package/src/cli/commands/sdlc.js +213 -5
  17. package/src/cli/dispatcher.js +8 -0
  18. package/src/cli/help.js +14 -2
  19. package/src/cli/options.js +1 -0
  20. package/src/generator/context/shared/domain-sdlc.js +27 -0
  21. package/src/generator/context/shared/relationships.js +2 -1
  22. package/src/generator/context/shared/types.d.ts +1 -0
  23. package/src/generator/context/shared.d.ts +2 -0
  24. package/src/generator/context/shared.js +2 -0
  25. package/src/generator/context/slice/core.js +3 -0
  26. package/src/generator/context/slice/sdlc.js +57 -2
  27. package/src/generator/context/task-mode.js +7 -0
  28. package/src/generator/sdlc/board.js +2 -0
  29. package/src/generator/sdlc/traceability-matrix.js +5 -1
  30. package/src/import/core/context.js +1 -1
  31. package/src/import/core/contracts.js +3 -3
  32. package/src/import/core/registry.js +3 -0
  33. package/src/import/core/runner/candidates.js +7 -0
  34. package/src/import/core/runner/reports.js +9 -1
  35. package/src/import/core/runner/tracks.js +3 -0
  36. package/src/import/extractors/cli/generic.js +340 -0
  37. package/src/new-project/project-files.js +10 -3
  38. package/src/resolver/enrich/task.js +3 -1
  39. package/src/resolver/index.js +6 -0
  40. package/src/resolver/normalize.js +31 -0
  41. package/src/resolver/projections-cli.js +158 -0
  42. package/src/sdlc/adopt.js +4 -1
  43. package/src/sdlc/check.js +24 -2
  44. package/src/sdlc/complete.js +47 -0
  45. package/src/sdlc/dod/index.js +2 -0
  46. package/src/sdlc/dod/plan.js +15 -0
  47. package/src/sdlc/dod/task.js +7 -3
  48. package/src/sdlc/explain.js +53 -1
  49. package/src/sdlc/gate.js +352 -0
  50. package/src/sdlc/history.d.ts +7 -0
  51. package/src/sdlc/history.js +50 -5
  52. package/src/sdlc/link.js +172 -0
  53. package/src/sdlc/paths.d.ts +4 -0
  54. package/src/sdlc/paths.js +8 -0
  55. package/src/sdlc/plan-steps.js +71 -0
  56. package/src/sdlc/plan.js +245 -0
  57. package/src/sdlc/policy.js +249 -0
  58. package/src/sdlc/prep.js +186 -0
  59. package/src/sdlc/scaffold.js +4 -2
  60. package/src/sdlc/status-filter.js +2 -0
  61. package/src/sdlc/transitions/index.js +3 -0
  62. package/src/sdlc/transitions/plan.js +32 -0
  63. package/src/validator/common.js +25 -4
  64. package/src/validator/index.js +10 -0
  65. package/src/validator/kinds.d.ts +7 -0
  66. package/src/validator/kinds.js +32 -0
  67. package/src/validator/per-kind/plan.js +128 -0
  68. package/src/validator/per-kind/task.js +19 -0
  69. package/src/validator/projections/cli.js +267 -0
  70. package/src/validator.d.ts +1 -0
  71. package/src/workflows/import-app/shared.js +1 -1
  72. package/src/workflows/reconcile/adoption-plan/build.js +3 -1
  73. package/src/workflows/reconcile/adoption-plan/reasons.js +5 -0
  74. package/src/workflows/reconcile/bundle-core/index.js +3 -0
  75. package/src/workflows/reconcile/candidate-model.js +15 -0
  76. package/src/workflows/reconcile/gap-report.js +4 -2
  77. package/src/workflows/reconcile/impacts/adoption-plan.js +13 -0
  78. package/src/workflows/reconcile/renderers.js +82 -0
  79. package/src/workflows/reconcile/summary.js +4 -0
  80. package/src/workflows/reconcile/workflow.js +2 -1
  81. package/src/workspace-paths.js +26 -2
@@ -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(resolveTopoRoot(root), name);
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
+ }
@@ -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
+ }
@@ -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
- warnings.push("done task without `satisfies` references is hard to trace");
30
+ errors.push("status 'done' requires field 'satisfies'");
31
31
  }
32
32
  const acs = task.acceptanceRefs || [];
33
- if (acs.length === 0 && task.workType !== "documentation" && task.workType !== "review") {
34
- warnings.push("done task without `acceptance_refs` cannot tie back to verification");
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
 
@@ -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
  };