@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.
Files changed (84) 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/release-rollout.js +191 -10
  17. package/src/cli/commands/release-shared.js +51 -2
  18. package/src/cli/commands/release.js +16 -3
  19. package/src/cli/commands/sdlc.js +213 -5
  20. package/src/cli/dispatcher.js +8 -0
  21. package/src/cli/help.js +15 -3
  22. package/src/cli/options.js +1 -0
  23. package/src/generator/context/shared/domain-sdlc.js +27 -0
  24. package/src/generator/context/shared/relationships.js +2 -1
  25. package/src/generator/context/shared/types.d.ts +1 -0
  26. package/src/generator/context/shared.d.ts +2 -0
  27. package/src/generator/context/shared.js +2 -0
  28. package/src/generator/context/slice/core.js +3 -0
  29. package/src/generator/context/slice/sdlc.js +57 -2
  30. package/src/generator/context/task-mode.js +7 -0
  31. package/src/generator/sdlc/board.js +2 -0
  32. package/src/generator/sdlc/traceability-matrix.js +5 -1
  33. package/src/import/core/context.js +1 -1
  34. package/src/import/core/contracts.js +3 -3
  35. package/src/import/core/registry.js +3 -0
  36. package/src/import/core/runner/candidates.js +7 -0
  37. package/src/import/core/runner/reports.js +9 -1
  38. package/src/import/core/runner/tracks.js +3 -0
  39. package/src/import/extractors/cli/generic.js +340 -0
  40. package/src/new-project/project-files.js +10 -3
  41. package/src/resolver/enrich/task.js +3 -1
  42. package/src/resolver/index.js +6 -0
  43. package/src/resolver/normalize.js +31 -0
  44. package/src/resolver/projections-cli.js +158 -0
  45. package/src/sdlc/adopt.js +4 -1
  46. package/src/sdlc/check.js +24 -2
  47. package/src/sdlc/complete.js +47 -0
  48. package/src/sdlc/dod/index.js +2 -0
  49. package/src/sdlc/dod/plan.js +15 -0
  50. package/src/sdlc/dod/task.js +7 -3
  51. package/src/sdlc/explain.js +53 -1
  52. package/src/sdlc/gate.js +352 -0
  53. package/src/sdlc/history.d.ts +7 -0
  54. package/src/sdlc/history.js +50 -5
  55. package/src/sdlc/link.js +172 -0
  56. package/src/sdlc/paths.d.ts +4 -0
  57. package/src/sdlc/paths.js +8 -0
  58. package/src/sdlc/plan-steps.js +71 -0
  59. package/src/sdlc/plan.js +245 -0
  60. package/src/sdlc/policy.js +249 -0
  61. package/src/sdlc/prep.js +186 -0
  62. package/src/sdlc/scaffold.js +4 -2
  63. package/src/sdlc/status-filter.js +2 -0
  64. package/src/sdlc/transitions/index.js +3 -0
  65. package/src/sdlc/transitions/plan.js +32 -0
  66. package/src/validator/common.js +25 -4
  67. package/src/validator/index.js +10 -0
  68. package/src/validator/kinds.d.ts +7 -0
  69. package/src/validator/kinds.js +32 -0
  70. package/src/validator/per-kind/plan.js +128 -0
  71. package/src/validator/per-kind/task.js +19 -0
  72. package/src/validator/projections/cli.js +267 -0
  73. package/src/validator.d.ts +1 -0
  74. package/src/workflows/import-app/shared.js +1 -1
  75. package/src/workflows/reconcile/adoption-plan/build.js +3 -1
  76. package/src/workflows/reconcile/adoption-plan/reasons.js +5 -0
  77. package/src/workflows/reconcile/bundle-core/index.js +3 -0
  78. package/src/workflows/reconcile/candidate-model.js +15 -0
  79. package/src/workflows/reconcile/gap-report.js +4 -2
  80. package/src/workflows/reconcile/impacts/adoption-plan.js +13 -0
  81. package/src/workflows/reconcile/renderers.js +82 -0
  82. package/src/workflows/reconcile/summary.js +4 -0
  83. package/src/workflows/reconcile/workflow.js +2 -1
  84. 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(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
  };