@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
@@ -6,6 +6,7 @@ import { stableStringify } from "../../format.js";
6
6
  import { parsePath } from "../../parser.js";
7
7
  import { resolveWorkspace } from "../../resolver.js";
8
8
  import { formatValidationErrors } from "../../validator.js";
9
+ import { resolveTopoRoot } from "../../workspace-paths.js";
9
10
 
10
11
  /**
11
12
  * @typedef {Record<string, any>} AnyRecord
@@ -21,6 +22,23 @@ function flagValue(args, flag) {
21
22
  return index >= 0 ? args[index + 1] || null : null;
22
23
  }
23
24
 
25
+ /**
26
+ * @param {string[]} args
27
+ * @param {string} flag
28
+ * @returns {string[]}
29
+ */
30
+ function flagValues(args, flag) {
31
+ /** @type {string[]} */
32
+ const values = [];
33
+ for (let index = 0; index < args.length; index += 1) {
34
+ if (args[index] === flag && args[index + 1] && !args[index + 1].startsWith("-")) {
35
+ values.push(args[index + 1]);
36
+ index += 1;
37
+ }
38
+ }
39
+ return values;
40
+ }
41
+
24
42
  /**
25
43
  * @param {string[]} args
26
44
  * @returns {boolean}
@@ -29,6 +47,65 @@ function includeHistory(args) {
29
47
  return args.includes("--history") || args.includes("--include-history");
30
48
  }
31
49
 
50
+ /**
51
+ * @param {Record<string, any>} payload
52
+ * @returns {void}
53
+ */
54
+ function printPolicyExplain(payload) {
55
+ console.log("Topogram SDLC policy");
56
+ console.log(`Status: ${payload.policy?.status || "not_adopted"}`);
57
+ console.log(`Mode: ${payload.policy?.mode || "none"}`);
58
+ console.log(`Policy: ${payload.policy?.path || "missing"}`);
59
+ console.log(payload.enforcement || "Project has not adopted enforced SDLC.");
60
+ if ((payload.policy?.protectedPaths || []).length > 0) {
61
+ console.log("Protected paths:");
62
+ for (const item of payload.policy.protectedPaths) {
63
+ console.log(` - ${item}`);
64
+ }
65
+ }
66
+ if ((payload.nextCommands || []).length > 0) {
67
+ console.log("Next commands:");
68
+ for (const command of payload.nextCommands) {
69
+ console.log(` - ${command}`);
70
+ }
71
+ }
72
+ }
73
+
74
+ /**
75
+ * @param {Record<string, any>} payload
76
+ * @returns {void}
77
+ */
78
+ function printCommitPrep(payload) {
79
+ console.log("Topogram SDLC commit prep");
80
+ console.log(`Status: ${payload.ok ? "ready" : "needs attention"}`);
81
+ console.log(`Changed task files: ${(payload.taskFiles || []).length}`);
82
+ if ((payload.openTasks || []).length > 0) {
83
+ console.log("Open changed tasks:");
84
+ for (const task of payload.openTasks) {
85
+ console.log(` - ${task.id} (${task.status}, ${task.disposition || "unclassified"})`);
86
+ console.log(` file: ${task.file}`);
87
+ }
88
+ }
89
+ if ((payload.errors || []).length > 0) {
90
+ console.log("Errors:");
91
+ for (const error of payload.errors) {
92
+ console.log(` - ${error}`);
93
+ }
94
+ }
95
+ if ((payload.warnings || []).length > 0) {
96
+ console.log("Warnings:");
97
+ for (const warning of payload.warnings) {
98
+ console.log(` - ${warning}`);
99
+ }
100
+ }
101
+ if ((payload.nextCommands || []).length > 0) {
102
+ console.log("Next commands:");
103
+ for (const command of payload.nextCommands) {
104
+ console.log(` - ${command}`);
105
+ }
106
+ }
107
+ }
108
+
32
109
  /**
33
110
  * @param {string} sdlcRoot
34
111
  * @returns {AnyRecord|null}
@@ -56,15 +133,144 @@ function resolveSdlcWorkspace(sdlcRoot) {
56
133
  */
57
134
  export async function runSdlcCommand(context) {
58
135
  const { commandArgs, args } = context;
59
- const sdlcRoot = path.resolve(context.inputPath || ".");
136
+ const sdlcRoot = resolveTopoRoot(context.inputPath || ".");
60
137
  const actor = flagValue(args, "--actor");
61
138
  const note = flagValue(args, "--note");
62
139
  const status = flagValue(args, "--status");
140
+ const id = flagValue(args, "--id");
63
141
  const before = flagValue(args, "--before");
64
142
  const appVersion = flagValue(args, "--app-version");
65
143
  const sinceTag = flagValue(args, "--since-tag");
144
+ const base = flagValue(args, "--base");
145
+ const head = flagValue(args, "--head");
146
+ const exemption = flagValue(args, "--exemption");
147
+ const verification = flagValue(args, "--verification");
66
148
  const dryRun = args.includes("--dry-run");
67
149
  const strict = args.includes("--strict");
150
+ const json = args.includes("--json");
151
+
152
+ if (String(commandArgs.sdlcCommand || "").startsWith("policy:")) {
153
+ const {
154
+ explainSdlcPolicy,
155
+ loadSdlcPolicy,
156
+ policyProjectRoot,
157
+ writeDefaultSdlcPolicy
158
+ } = await import("../../sdlc/policy.js");
159
+ const projectRoot = policyProjectRoot(context.inputPath || ".");
160
+ if (commandArgs.sdlcCommand === "policy:init") {
161
+ const result = writeDefaultSdlcPolicy(projectRoot);
162
+ console.log(stableStringify(result));
163
+ return result.ok ? 0 : 1;
164
+ }
165
+ if (commandArgs.sdlcCommand === "policy:check") {
166
+ const info = loadSdlcPolicy(projectRoot);
167
+ const result = {
168
+ type: "sdlc_policy_check",
169
+ ok: info.exists && info.diagnostics.every((diagnostic) => diagnostic.severity !== "error"),
170
+ exists: info.exists,
171
+ path: info.path,
172
+ status: info.status,
173
+ mode: info.mode,
174
+ diagnostics: info.diagnostics
175
+ };
176
+ if (json) {
177
+ console.log(stableStringify(result));
178
+ } else if (result.ok) {
179
+ console.log(`SDLC policy is valid: ${result.path}`);
180
+ } else {
181
+ console.error(stableStringify(result));
182
+ }
183
+ return result.ok ? 0 : 1;
184
+ }
185
+ if (commandArgs.sdlcCommand === "policy:explain") {
186
+ const result = explainSdlcPolicy(projectRoot);
187
+ if (json) {
188
+ console.log(stableStringify(result));
189
+ } else {
190
+ printPolicyExplain(result);
191
+ }
192
+ return result.ok ? 0 : 1;
193
+ }
194
+ }
195
+
196
+ if (commandArgs.sdlcCommand === "gate") {
197
+ const { runSdlcGate } = await import("../../sdlc/gate.js");
198
+ const result = await runSdlcGate(context.inputPath || ".", {
199
+ base,
200
+ head,
201
+ sdlcIds: flagValues(args, "--sdlc-id"),
202
+ exemption,
203
+ requireAdopted: args.includes("--require-adopted")
204
+ });
205
+ console.log(stableStringify(result));
206
+ return result.ok ? 0 : 1;
207
+ }
208
+
209
+ if (commandArgs.sdlcCommand === "prep:commit") {
210
+ const { runSdlcCommitPrep } = await import("../../sdlc/prep.js");
211
+ const result = runSdlcCommitPrep(context.inputPath || ".", {
212
+ base,
213
+ head
214
+ });
215
+ if (json) {
216
+ console.log(stableStringify(result));
217
+ } else {
218
+ printCommitPrep(result);
219
+ }
220
+ return result.ok ? 0 : 1;
221
+ }
222
+
223
+ if (commandArgs.sdlcCommand === "link") {
224
+ const { linkSdlcRecord } = await import("../../sdlc/link.js");
225
+ const result = linkSdlcRecord(sdlcRoot, commandArgs.sdlcFromId, commandArgs.sdlcToId, {
226
+ write: args.includes("--write")
227
+ });
228
+ console.log(stableStringify(result));
229
+ return result.ok ? 0 : 1;
230
+ }
231
+
232
+ if (commandArgs.sdlcCommand === "complete") {
233
+ const { completeTask } = await import("../../sdlc/complete.js");
234
+ const result = completeTask(sdlcRoot, commandArgs.sdlcId, verification || "", {
235
+ write: args.includes("--write") && !dryRun,
236
+ actor,
237
+ note
238
+ });
239
+ console.log(stableStringify(result));
240
+ return result.ok ? 0 : 1;
241
+ }
242
+
243
+ if (commandArgs.sdlcCommand === "plan:create") {
244
+ const { createPlan } = await import("../../sdlc/plan.js");
245
+ const result = createPlan(sdlcRoot, commandArgs.sdlcId, commandArgs.sdlcSlug, {
246
+ write: args.includes("--write") && !dryRun
247
+ });
248
+ console.log(stableStringify(result));
249
+ return result.ok ? 0 : 1;
250
+ }
251
+
252
+ if (commandArgs.sdlcCommand === "plan:explain") {
253
+ const { explainPlan } = await import("../../sdlc/plan.js");
254
+ const result = explainPlan(sdlcRoot, commandArgs.sdlcId);
255
+ console.log(stableStringify(result));
256
+ return result.ok ? 0 : 1;
257
+ }
258
+
259
+ if (String(commandArgs.sdlcCommand || "").startsWith("plan:step:")) {
260
+ const { transitionPlanStep } = await import("../../sdlc/plan.js");
261
+ if (commandArgs.sdlcCommand === "plan:step:skip" && !note) {
262
+ console.log(stableStringify({ ok: false, error: "sdlc plan step skip requires --note <reason>" }));
263
+ return 1;
264
+ }
265
+ const result = transitionPlanStep(sdlcRoot, commandArgs.sdlcId, commandArgs.sdlcStepId, commandArgs.sdlcTargetStatus, {
266
+ write: args.includes("--write") && !dryRun,
267
+ dryRun,
268
+ actor,
269
+ note
270
+ });
271
+ console.log(stableStringify(result));
272
+ return result.ok ? 0 : 1;
273
+ }
68
274
 
69
275
  if (commandArgs.sdlcCommand === "transition") {
70
276
  const { transitionStatement } = await import("../../sdlc/transition.js");
@@ -115,10 +321,12 @@ export async function runSdlcCommand(context) {
115
321
  if (!resolved) {
116
322
  return 1;
117
323
  }
118
- const ids = archiveEligibleStatements(resolved, {
119
- before,
120
- statuses: status ? status.split(",") : null
121
- });
324
+ const ids = id
325
+ ? [id]
326
+ : archiveEligibleStatements(resolved, {
327
+ before,
328
+ statuses: status ? status.split(",") : null
329
+ });
122
330
  const result = archiveBatch(sdlcRoot, ids, { dryRun, by: actor, reason: note });
123
331
  console.log(stableStringify({ candidates: ids, ...result }));
124
332
  return result.ok ? 0 : 1;
@@ -109,6 +109,7 @@ export async function runCliDispatch(context) {
109
109
  domainId,
110
110
  seamId,
111
111
  taskId,
112
+ planId,
112
113
  pitchId,
113
114
  requirementId,
114
115
  acceptanceId,
@@ -296,6 +297,13 @@ export async function runCliDispatch(context) {
296
297
  journeyId,
297
298
  surfaceId,
298
299
  domainId,
300
+ pitchId,
301
+ requirementId,
302
+ acceptanceId,
303
+ taskId,
304
+ planId,
305
+ bugId,
306
+ documentId,
299
307
  seamId,
300
308
  modeId,
301
309
  providerId,
package/src/cli/help.js CHANGED
@@ -11,11 +11,19 @@ export function printUsage(options = {}) {
11
11
  console.log(" or: topogram doctor --allow-local-npmrc");
12
12
  console.log("Usage: topogram setup package-auth|catalog-auth");
13
13
  console.log("Usage: topogram release status [--json] [--strict] [--markdown|--write-report <path>]");
14
- console.log(" or: topogram release roll-consumers <version|--latest> [--json] [--no-push] [--watch]");
14
+ console.log(" or: topogram release roll-consumers <version|--latest> [--json] [--no-push] [--watch|--no-watch]");
15
15
  console.log("Usage: topogram check [path] [--json]");
16
16
  console.log(" or: topogram widget check [path] [--projection <id>] [--widget <id>] [--json]");
17
17
  console.log(" or: topogram widget behavior [path] [--projection <id>] [--widget <id>] [--json]");
18
18
  console.log(" or: topogram agent brief [path] [--json]");
19
+ console.log(" or: topogram sdlc policy init|check|explain [path] [--json]");
20
+ console.log(" or: topogram sdlc gate [path] --base <ref> --head <ref> [--sdlc-id <id>] [--exemption <text>] [--require-adopted] [--json]");
21
+ console.log(" or: topogram sdlc prep commit [path] [--base <ref> --head <ref>] [--json]");
22
+ console.log(" or: topogram sdlc link <from-id> <to-id> [path] [--write]");
23
+ console.log(" or: topogram sdlc complete <task-id> [path] --verification <verification-id> [--dry-run|--write]");
24
+ console.log(" or: topogram sdlc plan create <task-id> <slug> [path] [--write]");
25
+ console.log(" or: topogram sdlc plan explain <plan-id> [path] [--json]");
26
+ console.log(" or: topogram sdlc plan step complete <plan-id> <step-id> [path] --actor <actor> [--write]");
19
27
  console.log(" or: topogram generate [path] [--out <path>]");
20
28
  console.log(" or: topogram emit <target> [path] [--json|--write --out-dir <path>]");
21
29
  console.log(" or: topogram query list [--json]");
@@ -77,6 +85,10 @@ export function printUsage(options = {}) {
77
85
  console.log(" topogram widget behavior --projection proj_web_surface");
78
86
  console.log(" topogram agent brief");
79
87
  console.log(" topogram agent brief --json");
88
+ console.log(" topogram sdlc policy explain");
89
+ console.log(" topogram sdlc prep commit . --base origin/main --head HEAD");
90
+ console.log(" topogram sdlc gate . --require-adopted");
91
+ console.log(" topogram sdlc plan explain plan_example --json");
80
92
  console.log(" topogram query list");
81
93
  console.log(" topogram query show widget-behavior");
82
94
  console.log(" topogram query widget-behavior ./topo --projection proj_web_surface --json");
@@ -164,7 +176,7 @@ export function printUsage(options = {}) {
164
176
  console.log(" or: node ./src/cli.js query maintained-drift <path> --from-topogram <path>");
165
177
  console.log(" or: node ./src/cli.js query seam-check <path> [--seam <id>] [--from-topogram <path>]");
166
178
  console.log(" or: node ./src/cli.js query diff <path> --from-topogram <path>");
167
- console.log(" or: node ./src/cli.js query slice <path> [--capability <id>] [--workflow <id>] [--projection <id>] [--widget <id>] [--entity <id>] [--journey <id>] [--domain <id>]");
179
+ console.log(" or: node ./src/cli.js query slice <path> [--capability <id>] [--workflow <id>] [--projection <id>] [--widget <id>] [--entity <id>] [--journey <id>] [--domain <id>] [--task <id>] [--plan <id>] [--bug <id>]");
168
180
  console.log(" or: node ./src/cli.js query domain-list <path>");
169
181
  console.log(" or: node ./src/cli.js query domain-coverage <path> --domain <id>");
170
182
  console.log(" or: node ./src/cli.js query review-boundary <path> [--capability <id>] [--workflow <id>] [--projection <id>] [--widget <id>] [--entity <id>] [--journey <id>]");
@@ -178,7 +190,7 @@ export function printUsage(options = {}) {
178
190
  console.log(" or: node ./src/cli.js query proceed-decision <path> [--mode <id>] [--capability <id>] [--workflow <id>] [--projection <id>] [--widget <id>] [--entity <id>] [--journey <id>] [--surface <id>] [--from-topogram <path>]");
179
191
  console.log(" or: node ./src/cli.js query review-packet <path> [--mode <id>] [--capability <id>] [--workflow <id>] [--projection <id>] [--widget <id>] [--entity <id>] [--journey <id>] [--surface <id>] [--from-topogram <path>]");
180
192
  console.log(" or: node ./src/cli.js query next-action <path> [--mode <id>] [--capability <id>] [--workflow <id>] [--projection <id>] [--widget <id>] [--entity <id>] [--journey <id>] [--from-topogram <path>]");
181
- console.log(" or: node ./src/cli.js query single-agent-plan <path> --mode <id> [--capability <id>] [--workflow <id>] [--projection <id>] [--widget <id>] [--entity <id>] [--journey <id>] [--surface <id>] [--from-topogram <path>]");
193
+ console.log(" or: node ./src/cli.js query single-agent-plan <path> --mode <id> [--capability <id>] [--workflow <id>] [--projection <id>] [--widget <id>] [--entity <id>] [--journey <id>] [--surface <id>] [--task <id>] [--plan <id>] [--bug <id>] [--from-topogram <path>]");
182
194
  console.log(" or: node ./src/cli.js query multi-agent-plan <path> --mode import-adopt");
183
195
  console.log(" or: node ./src/cli.js query resolved-workflow-context <path> --mode <id> [--capability <id>] [--workflow <id>] [--projection <id>] [--widget <id>] [--entity <id>] [--journey <id>] [--surface <id>] [--provider <id>] [--preset <id>] [--from-topogram <path>]");
184
196
  console.log(" or: node ./src/cli.js query workflow-preset-activation <path> --mode <id> [--provider <id>] [--preset <id>] [--from-topogram <path>]");
@@ -81,6 +81,7 @@ export function parseCliOptions(args, commandArgs) {
81
81
  domainId: optionValue(args, "--domain"),
82
82
  seamId: optionValue(args, "--seam"),
83
83
  taskId: optionValue(args, "--task"),
84
+ planId: optionValue(args, "--plan"),
84
85
  pitchId: optionValue(args, "--pitch"),
85
86
  requirementId: optionValue(args, "--requirement"),
86
87
  acceptanceId: optionValue(args, "--acceptance"),
@@ -135,6 +135,14 @@ export function acceptanceCriterionById(graph, id) {
135
135
  export function taskById(graph, id) {
136
136
  return (graph?.byKind?.task || []).find(/** @param {any} s */ (s) => s.id === id) || null;
137
137
  }
138
+ /**
139
+ * @param {import("./types.d.ts").ContextGraph} graph
140
+ * @param {string} id
141
+ * @returns {any}
142
+ */
143
+ export function planById(graph, id) {
144
+ return (graph?.byKind?.plan || []).find(/** @param {any} s */ (s) => s.id === id) || null;
145
+ }
138
146
  /**
139
147
  * @param {import("./types.d.ts").ContextGraph} graph
140
148
  * @param {string} id
@@ -207,10 +215,29 @@ export function summarizeTask(task) {
207
215
  status: task.status,
208
216
  priority: task.priority,
209
217
  work_type: task.workType,
218
+ disposition: task.disposition || null,
210
219
  claimed_by: (task.claimedBy || []).map(/** @param {any} r */ (r) => (typeof r === "string" ? r : r?.id)).filter(Boolean),
211
220
  domain: task.resolvedDomain ? task.resolvedDomain.id : null
212
221
  };
213
222
  }
223
+ /**
224
+ * @param {any} plan
225
+ * @returns {any}
226
+ */
227
+ export function summarizePlan(plan) {
228
+ if (!plan) return null;
229
+ const steps = plan.steps || [];
230
+ return {
231
+ id: plan.id,
232
+ name: plan.name,
233
+ status: plan.status,
234
+ priority: plan.priority,
235
+ task: plan.task?.id || null,
236
+ step_count: steps.length,
237
+ incomplete_steps: steps.filter(/** @param {any} step */ (step) => step.status !== "done" && step.status !== "skipped").map(/** @param {any} step */ (step) => step.id),
238
+ domain: plan.resolvedDomain ? plan.resolvedDomain.id : null
239
+ };
240
+ }
214
241
  /**
215
242
  * @param {any} bug
216
243
  * @returns {any}
@@ -436,13 +436,14 @@ export function ensureContextSelection(options = {}) {
436
436
  options.requirementId ? ["requirement", options.requirementId] : null,
437
437
  options.acceptanceId ? ["acceptance_criterion", options.acceptanceId] : null,
438
438
  options.taskId ? ["task", options.taskId] : null,
439
+ options.planId ? ["plan", options.planId] : null,
439
440
  options.bugId ? ["bug", options.bugId] : null,
440
441
  options.documentId ? ["document", options.documentId] : null
441
442
  ].filter(Boolean));
442
443
 
443
444
  if (selectors.length !== 1) {
444
445
  throw new Error(
445
- "Context selection requires exactly one of --capability, --workflow, --projection, --widget, --entity, --journey, --surface, --domain, --pitch, --requirement, --acceptance, --task, --bug, or --document"
446
+ "Context selection requires exactly one of --capability, --workflow, --projection, --widget, --entity, --journey, --surface, --domain, --pitch, --requirement, --acceptance, --task, --plan, --bug, or --document"
446
447
  );
447
448
  }
448
449
 
@@ -143,6 +143,7 @@ export type ContextSelectionOptions = {
143
143
  requirementId?: string | null;
144
144
  acceptanceId?: string | null;
145
145
  taskId?: string | null;
146
+ planId?: string | null;
146
147
  bugId?: string | null;
147
148
  documentId?: string | null;
148
149
  [key: string]: any;
@@ -9,6 +9,7 @@ export function getJourneyDoc(...args: any[]): any;
9
9
  export function getStatement(...args: any[]): any;
10
10
  export function getWorkflowDoc(...args: any[]): any;
11
11
  export function pitchById(...args: any[]): any;
12
+ export function planById(...args: any[]): any;
12
13
  export function relatedCapabilitiesForEntity(...args: any[]): any;
13
14
  export function relatedCapabilitiesForProjection(...args: any[]): any;
14
15
  export function relatedEntitiesForDomain(...args: any[]): any;
@@ -34,6 +35,7 @@ export function summarizeDocsByIds(...args: any[]): any;
34
35
  export function summarizeDocument(...args: any[]): any;
35
36
  export function summarizeDomain(...args: any[]): any;
36
37
  export function summarizePitch(...args: any[]): any;
38
+ export function summarizePlan(...args: any[]): any;
37
39
  export function summarizeProjection(...args: any[]): any;
38
40
  export function summarizeRequirement(...args: any[]): any;
39
41
  export function summarizeStatementsByIds(...args: any[]): any;
@@ -55,12 +55,14 @@ export {
55
55
  requirementById,
56
56
  acceptanceCriterionById,
57
57
  taskById,
58
+ planById,
58
59
  bugById,
59
60
  documentById,
60
61
  summarizePitch,
61
62
  summarizeRequirement,
62
63
  summarizeAcceptanceCriterion,
63
64
  summarizeTask,
65
+ summarizePlan,
64
66
  summarizeBug,
65
67
  summarizeDocument,
66
68
  getWorkflowDoc,
@@ -40,6 +40,7 @@ import {
40
40
  domainSlice,
41
41
  journeySlice,
42
42
  pitchSlice,
43
+ planSlice,
43
44
  requirementSlice,
44
45
  taskSlice
45
46
  } from "./sdlc.js";
@@ -375,6 +376,7 @@ export function generateContextSlice(graph, options = {}) {
375
376
  requirementId: options.requirementId,
376
377
  acceptanceId: options.acceptanceId,
377
378
  taskId: options.taskId,
379
+ planId: options.planId,
378
380
  bugId: options.bugId,
379
381
  documentId: options.documentId
380
382
  });
@@ -390,6 +392,7 @@ export function generateContextSlice(graph, options = {}) {
390
392
  if (selection.kind === "requirement") return requirementSlice(graph, selection.id);
391
393
  if (selection.kind === "acceptance_criterion") return acceptanceCriterionSlice(graph, selection.id);
392
394
  if (selection.kind === "task") return taskSlice(graph, selection.id);
395
+ if (selection.kind === "plan") return planSlice(graph, selection.id);
393
396
  if (selection.kind === "bug") return bugSlice(graph, selection.id);
394
397
  if (selection.kind === "document") return documentSlice(graph, selection.id);
395
398
 
@@ -8,6 +8,7 @@ import {
8
8
  domainById,
9
9
  getJourneyDoc,
10
10
  pitchById,
11
+ planById,
11
12
  recommendedVerificationTargets,
12
13
  relatedEntitiesForDomain,
13
14
  relatedProjectionsForDomain,
@@ -21,6 +22,7 @@ import {
21
22
  summarizeDocument,
22
23
  summarizeDomain,
23
24
  summarizePitch,
25
+ summarizePlan,
24
26
  summarizeRequirement,
25
27
  summarizeStatementsByIds,
26
28
  summarizeTask,
@@ -297,10 +299,12 @@ export function taskSlice(graph, taskId) {
297
299
 
298
300
  const satisfies = (task.satisfies || []).map(/** @param {any} r */ (r) => (typeof r === "string" ? r : r?.id)).filter(Boolean).sort();
299
301
  const acRefs = (task.acceptanceRefs || []).map(/** @param {any} r */ (r) => (typeof r === "string" ? r : r?.id)).filter(Boolean).sort();
302
+ const verificationRefs = (task.verificationRefs || []).map(/** @param {any} r */ (r) => (typeof r === "string" ? r : r?.id)).filter(Boolean).sort();
300
303
  const blockedBy = (task.blockedBy || []).map(/** @param {any} r */ (r) => (typeof r === "string" ? r : r?.id)).filter(Boolean).sort();
301
304
  const blocks = (task.blocks || []).map(/** @param {any} r */ (r) => (typeof r === "string" ? r : r?.id)).filter(Boolean).sort();
302
305
  const affects = (task.affects || []).map(/** @param {any} a */ (a) => (typeof a === "string" ? a : a?.id)).filter(Boolean).sort();
303
- const verifications = verificationIdsForTarget(graph, [taskId, ...affects, ...acRefs]);
306
+ const plans = (task.plans || []).slice().sort();
307
+ const verifications = [...new Set([...verificationRefs, ...verificationIdsForTarget(graph, [taskId, ...affects, ...acRefs])])].sort();
304
308
 
305
309
  return {
306
310
  type: "context_slice",
@@ -310,15 +314,19 @@ export function taskSlice(graph, taskId) {
310
314
  depends_on: {
311
315
  satisfies,
312
316
  acceptance_refs: acRefs,
317
+ verification_refs: verificationRefs,
313
318
  blocked_by: blockedBy,
314
319
  blocks,
320
+ plans,
315
321
  affects,
316
322
  verifications
317
323
  },
318
324
  related: {
319
325
  satisfies: summarizeStatementsByIds(graph, satisfies),
320
326
  acceptance_refs: summarizeStatementsByIds(graph, acRefs),
327
+ verification_refs: summarizeStatementsByIds(graph, verificationRefs),
321
328
  blocked_by: summarizeStatementsByIds(graph, blockedBy),
329
+ plans: summarizeStatementsByIds(graph, plans),
322
330
  affects: summarizeStatementsByIds(graph, affects)
323
331
  },
324
332
  verification: summarizeStatementsByIds(graph, verifications),
@@ -331,6 +339,54 @@ export function taskSlice(graph, taskId) {
331
339
  };
332
340
  }
333
341
 
342
+ /**
343
+ * @param {import("../shared/types.d.ts").ContextGraph} graph
344
+ * @param {string} planId
345
+ * @returns {any}
346
+ */
347
+ export function planSlice(graph, planId) {
348
+ const plan = planById(graph, planId);
349
+ if (!plan) throw new Error(`No plan found with id '${planId}'`);
350
+
351
+ const taskId = plan.task?.id || null;
352
+ const task = taskId ? taskById(graph, taskId) : null;
353
+ const satisfies = task ? (task.satisfies || []).map(/** @param {any} r */ (r) => (typeof r === "string" ? r : r?.id)).filter(Boolean).sort() : [];
354
+ const acRefs = task ? (task.acceptanceRefs || []).map(/** @param {any} r */ (r) => (typeof r === "string" ? r : r?.id)).filter(Boolean).sort() : [];
355
+ const verificationRefs = task ? (task.verificationRefs || []).map(/** @param {any} r */ (r) => (typeof r === "string" ? r : r?.id)).filter(Boolean).sort() : [];
356
+ const affects = task ? (task.affects || []).map(/** @param {any} a */ (a) => (typeof a === "string" ? a : a?.id)).filter(Boolean).sort() : [];
357
+ const verifications = [...new Set([...verificationRefs, ...verificationIdsForTarget(graph, [planId, ...(taskId ? [taskId] : []), ...affects, ...acRefs])])].sort();
358
+
359
+ return {
360
+ type: "context_slice",
361
+ version: 1,
362
+ focus: { kind: "plan", id: planId },
363
+ summary: summarizePlan(plan),
364
+ depends_on: {
365
+ task: taskId,
366
+ satisfies,
367
+ acceptance_refs: acRefs,
368
+ verification_refs: verificationRefs,
369
+ affects,
370
+ verifications
371
+ },
372
+ related: {
373
+ task: task ? [summarizeTask(task)] : [],
374
+ satisfies: summarizeStatementsByIds(graph, satisfies),
375
+ acceptance_refs: summarizeStatementsByIds(graph, acRefs),
376
+ verification_refs: summarizeStatementsByIds(graph, verificationRefs),
377
+ affects: summarizeStatementsByIds(graph, affects)
378
+ },
379
+ steps: plan.steps || [],
380
+ verification: summarizeStatementsByIds(graph, verifications),
381
+ verification_targets: recommendedVerificationTargets(graph, [planId, ...(taskId ? [taskId] : []), ...affects, ...acRefs], {
382
+ rationale: "Plan slice points at verification for the owning task and affected surfaces."
383
+ }),
384
+ write_scope: buildDefaultWriteScope(),
385
+ review_boundary: reviewBoundaryForTask(),
386
+ ownership_boundary: defaultOwnershipBoundary()
387
+ };
388
+ }
389
+
334
390
  /**
335
391
  * @param {import("../shared/types.d.ts").ContextGraph} graph
336
392
  * @param {string} bugId
@@ -414,4 +470,3 @@ export function documentSlice(graph, documentId) {
414
470
  ownership_boundary: defaultOwnershipBoundary()
415
471
  };
416
472
  }
417
-
@@ -234,6 +234,13 @@ function selectedSurface(options = {}) {
234
234
  if (options.entityId) return { kind: "entity", id: options.entityId };
235
235
  if (options.journeyId) return { kind: "journey", id: options.journeyId };
236
236
  if (options.surfaceId) return { kind: "surface", id: options.surfaceId };
237
+ if (options.pitchId) return { kind: "pitch", id: options.pitchId };
238
+ if (options.requirementId) return { kind: "requirement", id: options.requirementId };
239
+ if (options.acceptanceId) return { kind: "acceptance_criterion", id: options.acceptanceId };
240
+ if (options.taskId) return { kind: "task", id: options.taskId };
241
+ if (options.planId) return { kind: "plan", id: options.planId };
242
+ if (options.bugId) return { kind: "bug", id: options.bugId };
243
+ if (options.documentId) return { kind: "document", id: options.documentId };
237
244
  return null;
238
245
  }
239
246
 
@@ -7,6 +7,7 @@
7
7
 
8
8
  import {
9
9
  summarizeBug,
10
+ summarizePlan,
10
11
  summarizePitch,
11
12
  summarizeRequirement,
12
13
  summarizeTask
@@ -20,6 +21,7 @@ const SUMMARIZERS = {
20
21
  pitch: summarizePitch,
21
22
  requirement: summarizeRequirement,
22
23
  task: summarizeTask,
24
+ plan: summarizePlan,
23
25
  bug: summarizeBug
24
26
  };
25
27
 
@@ -19,7 +19,11 @@ export function generateSdlcTraceabilityMatrix(graph, options = {}) {
19
19
  const pitch = pitchId ? pitchesById.get(pitchId) : null;
20
20
 
21
21
  const taskIds = (ac.tasks || []).slice().sort();
22
- const verificationIds = (ac.verifications || []).slice().sort();
22
+ const taskVerificationIds = taskIds.flatMap((id) => {
23
+ const task = tasksById.get(id);
24
+ return (task?.verificationRefs || []).map((ref) => typeof ref === "string" ? ref : ref?.id).filter(Boolean);
25
+ });
26
+ const verificationIds = [...new Set([...(ac.verifications || []), ...taskVerificationIds])].sort();
23
27
 
24
28
  const linkedBugIds = [];
25
29
  for (const bug of bugsById.values()) {
@@ -20,7 +20,7 @@ export function findNearestGitRoot(startDir) {
20
20
  }
21
21
 
22
22
  export function normalizeWorkspacePaths(inputPath) {
23
- const context = resolveWorkspaceContext(inputPath);
23
+ const context = resolveWorkspaceContext(inputPath, { ignoreAncestorConfig: true });
24
24
  const absolute = path.resolve(inputPath);
25
25
  const topogramRoot = context.topoRoot;
26
26
  const workspaceRoot = context.projectRoot;
@@ -1,17 +1,17 @@
1
- export const IMPORT_TRACKS = new Set(["db", "api", "ui", "workflows", "verification"]);
1
+ export const IMPORT_TRACKS = new Set(["db", "api", "ui", "cli", "workflows", "verification"]);
2
2
 
3
3
  /**
4
4
  * @typedef {{score:number, reasons:string[]}} DetectionResult
5
5
  * @typedef {{findings:any[], candidates:any}} ExtractResult
6
6
  * @typedef {{
7
7
  * id:string,
8
- * track:"db"|"api"|"ui"|"workflows"|"verification",
8
+ * track:"db"|"api"|"ui"|"cli"|"workflows"|"verification",
9
9
  * detect:(context:any)=>DetectionResult,
10
10
  * extract:(context:any)=>ExtractResult
11
11
  * }} ImportExtractor
12
12
  * @typedef {{
13
13
  * id:string,
14
- * track:"db"|"api"|"ui"|"workflows"|"verification"|"docs",
14
+ * track:"db"|"api"|"ui"|"cli"|"workflows"|"verification"|"docs",
15
15
  * applies:(context:any, candidates:any)=>boolean|number,
16
16
  * enrich:(context:any, candidates:any)=>any
17
17
  * }} ImportEnricher