@topogram/cli 0.3.71 → 0.3.73

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) 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/project.js +0 -3
  11. package/src/cli/command-parsers/sdlc.js +66 -0
  12. package/src/cli/commands/import/help.js +1 -0
  13. package/src/cli/commands/import/plan.js +9 -0
  14. package/src/cli/commands/import/workspace.js +3 -0
  15. package/src/cli/commands/query/definitions.js +11 -10
  16. package/src/cli/commands/query/workspace.js +23 -2
  17. package/src/cli/commands/sdlc.js +213 -5
  18. package/src/cli/dispatcher.js +8 -5
  19. package/src/cli/help.js +14 -3
  20. package/src/cli/migration-guidance.js +3 -0
  21. package/src/cli/options.js +1 -0
  22. package/src/generator/context/shared/domain-sdlc.js +27 -0
  23. package/src/generator/context/shared/relationships.js +2 -1
  24. package/src/generator/context/shared/types.d.ts +1 -0
  25. package/src/generator/context/shared.d.ts +2 -0
  26. package/src/generator/context/shared.js +2 -0
  27. package/src/generator/context/slice/core.js +3 -0
  28. package/src/generator/context/slice/sdlc.js +57 -2
  29. package/src/generator/context/task-mode.js +7 -0
  30. package/src/generator/sdlc/board.js +2 -0
  31. package/src/generator/sdlc/traceability-matrix.js +5 -1
  32. package/src/generator/surfaces/databases/lifecycle-shared.js +2 -2
  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 +34 -3
  85. package/src/cli/commands/migrate.js +0 -153
@@ -1,6 +1,7 @@
1
1
  // @ts-check
2
2
  import {
3
3
  TASK_IDENTIFIER_PATTERN,
4
+ TASK_DISPOSITIONS,
4
5
  PRIORITY_VALUES,
5
6
  WORK_TYPES
6
7
  } from "../kinds.js";
@@ -55,6 +56,23 @@ function validateWorkType(errors, statement, fieldMap) {
55
56
  }
56
57
  }
57
58
 
59
+ /** @param {ValidationErrors} errors @param {TopogramStatement} statement @param {TopogramFieldMap} fieldMap */
60
+ function validateDisposition(errors, statement, fieldMap) {
61
+ const field = fieldMap.get("disposition")?.[0];
62
+ if (!field) return;
63
+ if (field.value.type !== "symbol") {
64
+ pushError(errors, `Field 'disposition' on task ${statement.id} must be a symbol`, field.loc);
65
+ return;
66
+ }
67
+ if (!TASK_DISPOSITIONS.has(field.value.value)) {
68
+ pushError(
69
+ errors,
70
+ `Invalid disposition '${field.value.value}' on task ${statement.id}`,
71
+ field.loc
72
+ );
73
+ }
74
+ }
75
+
58
76
  /** @param {ValidationErrors} errors @param {TopogramStatement} statement @param {TopogramFieldMap} fieldMap @param {TopogramRegistry} registry */
59
77
  function validateBlockingPair(errors, statement, fieldMap, registry) {
60
78
  // Self-block guard. Reciprocal `blocks` <-> `blocked_by` resolution is the
@@ -98,6 +116,7 @@ export function validateTask(errors, statement, fieldMap, registry) {
98
116
  validateTaskIdentifier(errors, statement);
99
117
  validatePriority(errors, statement, fieldMap);
100
118
  validateWorkType(errors, statement, fieldMap);
119
+ validateDisposition(errors, statement, fieldMap);
101
120
  validateBlockingPair(errors, statement, fieldMap, registry);
102
121
  validateClaimedByPresence(errors, statement, fieldMap);
103
122
  }
@@ -0,0 +1,267 @@
1
+ // @ts-check
2
+
3
+ import {
4
+ CLI_COMMAND_EFFECTS,
5
+ CLI_COMMAND_OPTION_TYPES,
6
+ CLI_COMMAND_OUTPUT_FORMATS,
7
+ IDENTIFIER_PATTERN
8
+ } from "../kinds.js";
9
+ import {
10
+ blockEntries,
11
+ getFieldValue,
12
+ pushError
13
+ } from "../utils.js";
14
+
15
+ /**
16
+ * @param {TopogramToken | null | undefined} token
17
+ * @returns {string | null}
18
+ */
19
+ function tokenText(token) {
20
+ return token?.type === "symbol" || token?.type === "string" ? token.value : null;
21
+ }
22
+
23
+ /**
24
+ * @param {TopogramToken | null | undefined} token
25
+ * @returns {string | null}
26
+ */
27
+ function tokenDirectiveValue(token) {
28
+ if (!token) {
29
+ return null;
30
+ }
31
+ if (token.type === "list") {
32
+ return token.items.map((item) => tokenText(item)).filter((value) => value !== null).join(",");
33
+ }
34
+ return tokenText(token);
35
+ }
36
+
37
+ /**
38
+ * @param {TopogramBlockEntry} entry
39
+ * @returns {string[]}
40
+ */
41
+ function entryTokens(entry) {
42
+ const values = [];
43
+ for (const token of entry.items) {
44
+ const value = tokenDirectiveValue(token);
45
+ if (value !== null) {
46
+ values.push(value);
47
+ }
48
+ }
49
+ return values;
50
+ }
51
+
52
+ /**
53
+ * @param {ValidationErrors} errors
54
+ * @param {TopogramStatement} statement
55
+ * @param {TopogramBlockEntry} entry
56
+ * @param {string[]} tokens
57
+ * @param {number} startIndex
58
+ * @param {string} context
59
+ * @returns {Map<string, string>}
60
+ */
61
+ function parseDirectives(errors, statement, entry, tokens, startIndex, context) {
62
+ const directives = new Map();
63
+ for (let index = startIndex; index < tokens.length; index += 2) {
64
+ const key = tokens[index];
65
+ const value = tokens[index + 1];
66
+ if (!key) {
67
+ continue;
68
+ }
69
+ if (!value) {
70
+ pushError(errors, `Projection ${statement.id} ${context} is missing a value for '${key}'`, entry.loc);
71
+ continue;
72
+ }
73
+ directives.set(key, value);
74
+ }
75
+ return directives;
76
+ }
77
+
78
+ /**
79
+ * @param {ValidationErrors} errors
80
+ * @param {TopogramStatement} statement
81
+ * @param {TopogramFieldMap} fieldMap
82
+ * @returns {boolean}
83
+ */
84
+ function validateCliOwnership(errors, statement, fieldMap) {
85
+ const cliFields = ["commands", "command_options", "command_outputs", "command_effects", "command_examples"];
86
+ const hasCliFields = cliFields.some((key) => fieldMap.has(key));
87
+ if (!hasCliFields) {
88
+ return false;
89
+ }
90
+
91
+ const typeValue = tokenText(fieldMap.get("type")?.[0]?.value);
92
+ if (typeValue !== "cli_surface") {
93
+ for (const key of cliFields) {
94
+ const field = fieldMap.get(key)?.[0];
95
+ if (field) {
96
+ pushError(errors, `Projection ${statement.id} ${key} belongs on cli_surface projections`, field.loc);
97
+ }
98
+ }
99
+ return false;
100
+ }
101
+ return true;
102
+ }
103
+
104
+ /**
105
+ * @param {ValidationErrors} errors
106
+ * @param {TopogramStatement} statement
107
+ * @param {TopogramRegistry} registry
108
+ * @returns {Set<string>}
109
+ */
110
+ function validateCommands(errors, statement, registry) {
111
+ const commandIds = new Set();
112
+ const entries = blockEntries(getFieldValue(statement, "commands"));
113
+ for (const entry of entries) {
114
+ const tokens = entryTokens(entry);
115
+ if (tokens[0] !== "command") {
116
+ pushError(errors, `Projection ${statement.id} commands entries must start with 'command'`, entry.loc);
117
+ continue;
118
+ }
119
+ const commandId = tokens[1];
120
+ if (!commandId || !IDENTIFIER_PATTERN.test(commandId)) {
121
+ pushError(errors, `Projection ${statement.id} command id '${commandId || ""}' must be a Topogram identifier`, entry.loc);
122
+ continue;
123
+ }
124
+ if (commandIds.has(commandId)) {
125
+ pushError(errors, `Projection ${statement.id} commands has duplicate command '${commandId}'`, entry.loc);
126
+ }
127
+ commandIds.add(commandId);
128
+
129
+ const directives = parseDirectives(errors, statement, entry, tokens, 2, `command '${commandId}'`);
130
+ const capabilityId = directives.get("capability");
131
+ if (capabilityId) {
132
+ const capability = registry.get(capabilityId);
133
+ if (!capability || capability.kind !== "capability") {
134
+ pushError(errors, `Projection ${statement.id} command '${commandId}' references unknown capability '${capabilityId}'`, entry.loc);
135
+ }
136
+ }
137
+ const mode = directives.get("mode");
138
+ if (mode && !CLI_COMMAND_EFFECTS.has(mode)) {
139
+ pushError(errors, `Projection ${statement.id} command '${commandId}' has invalid mode '${mode}'`, entry.loc);
140
+ }
141
+ }
142
+ return commandIds;
143
+ }
144
+
145
+ /**
146
+ * @param {ValidationErrors} errors
147
+ * @param {TopogramStatement} statement
148
+ * @param {Set<string>} commandIds
149
+ * @returns {void}
150
+ */
151
+ function validateCommandOptions(errors, statement, commandIds) {
152
+ for (const entry of blockEntries(getFieldValue(statement, "command_options"))) {
153
+ const tokens = entryTokens(entry);
154
+ const commandId = tokens[0] === "command" ? tokens[1] : null;
155
+ if (!commandId || tokens[2] !== "option" || !tokens[3]) {
156
+ pushError(errors, `Projection ${statement.id} command_options entries must use 'command <id> option <name> type <type>'`, entry.loc);
157
+ continue;
158
+ }
159
+ if (!commandIds.has(commandId)) {
160
+ pushError(errors, `Projection ${statement.id} command_options references unknown command '${commandId}'`, entry.loc);
161
+ }
162
+ const directives = parseDirectives(errors, statement, entry, tokens, 4, `command option '${commandId}.${tokens[3]}'`);
163
+ const type = directives.get("type");
164
+ if (!type) {
165
+ pushError(errors, `Projection ${statement.id} command option '${commandId}.${tokens[3]}' must include type`, entry.loc);
166
+ } else if (!CLI_COMMAND_OPTION_TYPES.has(type)) {
167
+ pushError(errors, `Projection ${statement.id} command option '${commandId}.${tokens[3]}' has invalid type '${type}'`, entry.loc);
168
+ }
169
+ }
170
+ }
171
+
172
+ /**
173
+ * @param {ValidationErrors} errors
174
+ * @param {TopogramStatement} statement
175
+ * @param {TopogramRegistry} registry
176
+ * @param {Set<string>} commandIds
177
+ * @returns {void}
178
+ */
179
+ function validateCommandOutputs(errors, statement, registry, commandIds) {
180
+ for (const entry of blockEntries(getFieldValue(statement, "command_outputs"))) {
181
+ const tokens = entryTokens(entry);
182
+ const commandId = tokens[0] === "command" ? tokens[1] : null;
183
+ if (!commandId || tokens[2] !== "format" || !tokens[3]) {
184
+ pushError(errors, `Projection ${statement.id} command_outputs entries must use 'command <id> format <format>'`, entry.loc);
185
+ continue;
186
+ }
187
+ if (!commandIds.has(commandId)) {
188
+ pushError(errors, `Projection ${statement.id} command_outputs references unknown command '${commandId}'`, entry.loc);
189
+ }
190
+ const format = tokens[3];
191
+ if (!CLI_COMMAND_OUTPUT_FORMATS.has(format)) {
192
+ pushError(errors, `Projection ${statement.id} command output '${commandId}' has invalid format '${format}'`, entry.loc);
193
+ }
194
+ const directives = parseDirectives(errors, statement, entry, tokens, 4, `command output '${commandId}'`);
195
+ const schemaId = directives.get("schema");
196
+ if (schemaId) {
197
+ const schema = registry.get(schemaId);
198
+ if (!schema || schema.kind !== "shape") {
199
+ pushError(errors, `Projection ${statement.id} command output '${commandId}' references unknown shape '${schemaId}'`, entry.loc);
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ /**
206
+ * @param {ValidationErrors} errors
207
+ * @param {TopogramStatement} statement
208
+ * @param {Set<string>} commandIds
209
+ * @returns {void}
210
+ */
211
+ function validateCommandEffects(errors, statement, commandIds) {
212
+ for (const entry of blockEntries(getFieldValue(statement, "command_effects"))) {
213
+ const tokens = entryTokens(entry);
214
+ const commandId = tokens[0] === "command" ? tokens[1] : null;
215
+ if (!commandId || tokens[2] !== "effect" || !tokens[3]) {
216
+ pushError(errors, `Projection ${statement.id} command_effects entries must use 'command <id> effect <effect>'`, entry.loc);
217
+ continue;
218
+ }
219
+ if (!commandIds.has(commandId)) {
220
+ pushError(errors, `Projection ${statement.id} command_effects references unknown command '${commandId}'`, entry.loc);
221
+ }
222
+ if (!CLI_COMMAND_EFFECTS.has(tokens[3])) {
223
+ pushError(errors, `Projection ${statement.id} command effect '${commandId}' has invalid effect '${tokens[3]}'`, entry.loc);
224
+ }
225
+ }
226
+ }
227
+
228
+ /**
229
+ * @param {ValidationErrors} errors
230
+ * @param {TopogramStatement} statement
231
+ * @param {Set<string>} commandIds
232
+ * @returns {void}
233
+ */
234
+ function validateCommandExamples(errors, statement, commandIds) {
235
+ for (const entry of blockEntries(getFieldValue(statement, "command_examples"))) {
236
+ const tokens = entryTokens(entry);
237
+ const commandId = tokens[0] === "command" ? tokens[1] : null;
238
+ if (!commandId || tokens[2] !== "example" || !tokens[3]) {
239
+ pushError(errors, `Projection ${statement.id} command_examples entries must use 'command <id> example <command-line>'`, entry.loc);
240
+ continue;
241
+ }
242
+ if (!commandIds.has(commandId)) {
243
+ pushError(errors, `Projection ${statement.id} command_examples references unknown command '${commandId}'`, entry.loc);
244
+ }
245
+ }
246
+ }
247
+
248
+ /**
249
+ * @param {ValidationErrors} errors
250
+ * @param {TopogramStatement} statement
251
+ * @param {TopogramFieldMap} fieldMap
252
+ * @param {TopogramRegistry} registry
253
+ * @returns {void}
254
+ */
255
+ export function validateCliProjection(errors, statement, fieldMap, registry) {
256
+ if (statement.kind !== "projection") {
257
+ return;
258
+ }
259
+ if (!validateCliOwnership(errors, statement, fieldMap)) {
260
+ return;
261
+ }
262
+ const commandIds = validateCommands(errors, statement, registry);
263
+ validateCommandOptions(errors, statement, commandIds);
264
+ validateCommandOutputs(errors, statement, registry, commandIds);
265
+ validateCommandEffects(errors, statement, commandIds);
266
+ validateCommandExamples(errors, statement, commandIds);
267
+ }
@@ -1,2 +1,3 @@
1
1
  export function formatValidationErrors(validation: any, configPath?: string): string;
2
2
  export function validateWorkspace(ast: any): any;
3
+ export * from "./validator/index.js";
@@ -6,7 +6,7 @@ import { relativeTo } from "../../path-helpers.js";
6
6
  import { canonicalCandidateTerm, idHintify } from "../../text-helpers.js";
7
7
  import { listFilesRecursive } from "../shared.js";
8
8
 
9
- export const IMPORT_TRACKS = new Set(["db", "api", "ui", "workflows", "verification"]);
9
+ export const IMPORT_TRACKS = new Set(["db", "api", "ui", "cli", "workflows", "verification"]);
10
10
  export const SCALAR_FIELD_TYPES = new Set([
11
11
  "bigint",
12
12
  "boolean",
@@ -30,6 +30,7 @@ export function buildAdoptionPlan(bundles) {
30
30
  step.action === "apply_projection_permission_patch" ? "projection_permission_patch" :
31
31
  step.action === "apply_projection_auth_patch" ? "projection_auth_patch" :
32
32
  step.action === "apply_projection_ownership_patch" ? "projection_ownership_patch" :
33
+ step.action === "promote_cli_surface" ? "projection" :
33
34
  step.action.includes("doc") ? "doc" :
34
35
  step.action.includes("decision") ? "decision" :
35
36
  step.action.includes("verification") ? "verification" :
@@ -70,6 +71,7 @@ export function buildAdoptionPlan(bundles) {
70
71
  item: step.item,
71
72
  kind: itemKind,
72
73
  track:
74
+ step.track ? step.track :
73
75
  step.action.includes("workflow") ? "workflows" :
74
76
  step.action.includes("verification") ? "verification" :
75
77
  step.action.includes("ui_") ? "ui" :
@@ -209,4 +211,4 @@ export function buildAdoptionPlan(bundles) {
209
211
  );
210
212
  }
211
213
 
212
- export const ADOPT_SELECTORS = new Set(["from-plan", "actors", "roles", "enums", "shapes", "entities", "capabilities", "widgets", "docs", "journeys", "workflows", "verification", "ui"]);
214
+ export const ADOPT_SELECTORS = new Set(["from-plan", "actors", "roles", "enums", "shapes", "entities", "capabilities", "widgets", "docs", "journeys", "workflows", "verification", "cli", "ui"]);
@@ -17,6 +17,8 @@ export function reasonForAdoptionItem(step) {
17
17
  return "Promote this imported capability into canonical Topogram.";
18
18
  case "promote_widget":
19
19
  return "Promote this imported reusable UI widget into canonical Topogram.";
20
+ case "promote_cli_surface":
21
+ return "Promote this imported CLI surface projection into canonical Topogram.";
20
22
  case "merge_capability_into_existing_entity":
21
23
  return `Adopt this capability while preserving the existing canonical entity ${step.target}.`;
22
24
  case "promote_doc":
@@ -66,6 +68,9 @@ export function recommendationForAdoptionItem(step) {
66
68
  if (step.action === "promote_widget") {
67
69
  return "Promote this reviewed widget candidate before binding or reusing it from canonical projections.";
68
70
  }
71
+ if (step.action === "promote_cli_surface") {
72
+ return "Promote this reviewed CLI surface candidate after confirming commands, options, outputs, and side effects.";
73
+ }
69
74
  if (!["promote_actor", "promote_role"].includes(step.action)) {
70
75
  return null;
71
76
  }
@@ -35,6 +35,7 @@ export function getOrCreateCandidateBundle(bundles, conceptId, label) {
35
35
  capabilities: [],
36
36
  shapes: [],
37
37
  widgets: [],
38
+ cliSurfaces: [],
38
39
  screens: [],
39
40
  uiRoutes: [],
40
41
  uiActions: [],
@@ -332,6 +333,7 @@ export function renderCandidateBundleReadme(bundle, proposalSurfaces = []) {
332
333
  `Capabilities: ${bundle.capabilities.length}`,
333
334
  `Shapes: ${bundle.shapes.length}`,
334
335
  `Widgets: ${bundle.widgets.length}`,
336
+ `CLI surfaces: ${(bundle.cliSurfaces || []).length}`,
335
337
  `Screens: ${bundle.screens.length}`,
336
338
  `UI routes: ${bundle.uiRoutes.length}`,
337
339
  `UI actions: ${bundle.uiActions.length}`,
@@ -350,6 +352,7 @@ export function renderCandidateBundleReadme(bundle, proposalSurfaces = []) {
350
352
  `- Participants: ${summary.participants.label}`,
351
353
  `- Main capabilities: ${summarizeBundleSurface(bundle, summary.capabilityIds)}`,
352
354
  `- Main widgets: ${summarizeBundleSurface(bundle, summary.widgetIds)}`,
355
+ `- Main CLI surfaces: ${summarizeBundleSurface(bundle, summary.cliSurfaceIds)}`,
353
356
  `- Main screens: ${summarizeBundleSurface(bundle, summary.screenIds)}`,
354
357
  `- Main routes: ${summarizeBundleSurface(bundle, summary.routePaths)}`,
355
358
  `- Main workflows: ${summarizeBundleSurface(bundle, summary.workflowIds)}`,
@@ -18,6 +18,7 @@ import { buildTopogramApiCapabilityIndex, buildBundleDocLinkSuggestions, collect
18
18
  import { buildBundleAdoptionPlan, buildCanonicalShapeIndex, buildProjectionEntityIndex, buildProjectionImpacts, buildProjectionPatchCandidates, buildUiImpacts, buildWorkflowImpacts } from "./impacts.js";
19
19
  import {
20
20
  renderCandidateCapability,
21
+ renderCandidateCliSurface,
21
22
  renderCandidateEntity,
22
23
  renderCandidateEnum,
23
24
  renderCandidateShape,
@@ -88,6 +89,7 @@ export function buildCandidateModelBundles(graph, appImport, topogramRoot) {
88
89
  const uiShapeCandidatesById = new Map(uiShapeCandidates.map((/** @type {any} */ shape) => [shape.id || shape.id_hint, shape]));
89
90
  const workflowCandidates = appImport.candidates.workflows || { workflows: [], workflow_states: [], workflow_transitions: [] };
90
91
  const verificationCandidates = appImport.candidates.verification || { verifications: [], scenarios: [], frameworks: [], scripts: [] };
92
+ const cliCandidates = appImport.candidates.cli || { commands: [], capabilities: [], surfaces: [] };
91
93
  const docCandidates = appImport.candidates.docs || [];
92
94
  const actorCandidates = appImport.candidates.actors || [];
93
95
  const roleCandidates = appImport.candidates.roles || [];
@@ -174,6 +176,14 @@ export function buildCandidateModelBundles(graph, appImport, topogramRoot) {
174
176
  });
175
177
  }
176
178
  }
179
+ for (const entry of cliCandidates.capabilities || []) {
180
+ const bundle = getOrCreateCandidateBundle(bundles, "cli", "CLI");
181
+ bundle.capabilities.push(entry);
182
+ }
183
+ for (const entry of cliCandidates.surfaces || []) {
184
+ const bundle = getOrCreateCandidateBundle(bundles, "cli", "CLI");
185
+ bundle.cliSurfaces.push(entry);
186
+ }
177
187
  for (const entry of uiCandidates.screens || []) {
178
188
  if (canonicalUi.screens.includes(entry.id_hint)) {
179
189
  continue;
@@ -316,6 +326,7 @@ export function buildCandidateModelBundles(graph, appImport, topogramRoot) {
316
326
  bundle.capabilities.length > 0 ||
317
327
  bundle.shapes.length > 0 ||
318
328
  bundle.widgets.length > 0 ||
329
+ bundle.cliSurfaces.length > 0 ||
319
330
  bundle.screens.length > 0 ||
320
331
  bundle.uiRoutes.length > 0 ||
321
332
  bundle.uiActions.length > 0 ||
@@ -334,6 +345,7 @@ export function buildCandidateModelBundles(graph, appImport, topogramRoot) {
334
345
  capabilities: bundle.capabilities.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
335
346
  shapes: bundle.shapes.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id.localeCompare(b.id)),
336
347
  widgets: bundle.widgets.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
348
+ cliSurfaces: bundle.cliSurfaces.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
337
349
  screens: bundle.screens.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
338
350
  uiRoutes: bundle.uiRoutes.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
339
351
  uiActions: bundle.uiActions.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint)),
@@ -441,6 +453,9 @@ export function buildCandidateModelFiles(graph, appImport, topogramRoot) {
441
453
  for (const entry of bundle.widgets || []) {
442
454
  files[`${bundleRoot}/widgets/${entry.id_hint}.tg`] = renderCandidateWidget(entry);
443
455
  }
456
+ for (const entry of bundle.cliSurfaces || []) {
457
+ files[`${bundleRoot}/projections/${entry.id_hint}.tg`] = renderCandidateCliSurface(entry);
458
+ }
444
459
  for (const entry of bundle.docs) {
445
460
  if (entry.existing_canonical) {
446
461
  continue;
@@ -22,10 +22,11 @@ export function loadImportArtifacts(paths, inputPath) {
22
22
  const dbCandidates = readJsonIfExists(path.join(paths.topogramRoot, "candidates", "app", "db", "candidates.json"));
23
23
  const apiCandidates = readJsonIfExists(path.join(paths.topogramRoot, "candidates", "app", "api", "candidates.json"));
24
24
  const uiCandidates = readJsonIfExists(path.join(paths.topogramRoot, "candidates", "app", "ui", "candidates.json"));
25
+ const cliCandidates = readJsonIfExists(path.join(paths.topogramRoot, "candidates", "app", "cli", "candidates.json"));
25
26
  const workflowCandidates = readJsonIfExists(path.join(paths.topogramRoot, "candidates", "app", "workflows", "candidates.json"));
26
27
  const verificationCandidates = readJsonIfExists(path.join(paths.topogramRoot, "candidates", "app", "verification", "candidates.json"));
27
28
  const docsReport = readJsonIfExists(path.join(paths.topogramRoot, "candidates", "docs", "import-report.json"));
28
- if (dbCandidates || apiCandidates || uiCandidates || workflowCandidates || verificationCandidates || docsReport) {
29
+ if (dbCandidates || apiCandidates || uiCandidates || cliCandidates || workflowCandidates || verificationCandidates || docsReport) {
29
30
  return {
30
31
  type: "import_app_report",
31
32
  workspace: paths.workspaceRoot,
@@ -33,6 +34,7 @@ export function loadImportArtifacts(paths, inputPath) {
33
34
  db: dbCandidates || { entities: [], enums: [], relations: [], indexes: [] },
34
35
  api: apiCandidates || { capabilities: [], routes: [], stacks: [] },
35
36
  ui: uiCandidates || { screens: [], routes: [], actions: [], stacks: [] },
37
+ cli: cliCandidates || { commands: [], capabilities: [], surfaces: [] },
36
38
  workflows: workflowCandidates || { workflows: [], workflow_states: [], workflow_transitions: [] },
37
39
  verification: verificationCandidates || { verifications: [], scenarios: [], frameworks: [], scripts: [] },
38
40
  docs: docsReport?.candidate_docs || [],
@@ -41,7 +43,7 @@ export function loadImportArtifacts(paths, inputPath) {
41
43
  }
42
44
  };
43
45
  }
44
- const imported = importAppWorkflow(inputPath, { from: "db,api,ui,workflows,verification" }).summary;
46
+ const imported = importAppWorkflow(inputPath, { from: "db,api,ui,cli,workflows,verification" }).summary;
45
47
  const docsSummary = scanDocsWorkflow(inputPath).summary;
46
48
  imported.candidates.docs = docsSummary.candidate_docs || [];
47
49
  imported.candidates.actors = docsSummary.candidate_actors || [];
@@ -134,6 +134,19 @@ export function buildBundleAdoptionPlan(bundle, canonicalShapeIndex) {
134
134
  canonical_rel_path: `widgets/${dashedTopogramId(entry.id_hint)}.tg`
135
135
  });
136
136
  }
137
+ for (const entry of bundle.cliSurfaces || []) {
138
+ steps.push({
139
+ action: "promote_cli_surface",
140
+ item: entry.id_hint,
141
+ target: null,
142
+ confidence: entry.confidence || "low",
143
+ inference_summary: entry.inference_summary || null,
144
+ related_capabilities: [...new Set((entry.command_records || []).map((/** @type {any} */ command) => command.capability_id).filter(Boolean))].sort(),
145
+ source_path: `candidates/reconcile/model/bundles/${bundle.slug}/projections/${entry.id_hint}.tg`,
146
+ canonical_rel_path: `projections/${dashedTopogramId(entry.id_hint)}.tg`,
147
+ track: "cli"
148
+ });
149
+ }
137
150
  for (const screen of bundle.screens) {
138
151
  steps.push({
139
152
  action: "promote_ui_report",
@@ -11,6 +11,11 @@ import {
11
11
  formatAuthPermissionHintInline
12
12
  } from "./auth.js";
13
13
 
14
+ /** @param {string|null|undefined} value @returns {string} */
15
+ function quoteString(value) {
16
+ return String(value || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
17
+ }
18
+
14
19
  /** @param {string} fieldType @param {Set<any>} knownEnums @returns {any} */
15
20
  export function normalizeCandidateFieldType(fieldType, knownEnums = new Set()) {
16
21
  const normalized = idHintify(fieldType);
@@ -145,6 +150,18 @@ export function renderCandidateShape(shapeId, label, fields, sourceKind = null)
145
150
 
146
151
  /** @param {WorkflowRecord} record @param {any} inputShapeId @param {any} outputShapeId @returns {any} */
147
152
  export function renderCandidateCapability(record, inputShapeId, outputShapeId) {
153
+ if (record.track === "cli" || record.source_kind === "cli_command") {
154
+ const lines = [
155
+ `capability ${record.id_hint} {`,
156
+ ` name "${record.label}"`,
157
+ ` description "Candidate capability imported from brownfield CLI command evidence"`,
158
+ "",
159
+ " status active",
160
+ "}"
161
+ ];
162
+ return ensureTrailingNewline(`${renderCandidateMetadataComments(record)}\n${lines.join("\n")}`);
163
+ }
164
+
148
165
  const operationKind = inferCapabilityVerb(record);
149
166
  const entityId = inferCapabilityEntityId(record);
150
167
  const lines = [
@@ -309,6 +326,71 @@ export function renderCandidateWidget(widget) {
309
326
  );
310
327
  }
311
328
 
329
+ /** @param {WorkflowRecord} surface @returns {any} */
330
+ export function renderCandidateCliSurface(surface) {
331
+ const commandRecords = surface.command_records || [];
332
+ const commands = commandRecords.map((/** @type {any} */ command) =>
333
+ ` command ${command.command_id} capability ${command.capability_id} usage "${quoteString(command.usage)}" mode ${command.mode || "read_only"}`
334
+ );
335
+ const options = (surface.options || []).map((/** @type {any} */ option) => {
336
+ const parts = [
337
+ ` command ${option.command_id} option ${option.name}`,
338
+ `type ${option.type || "boolean"}`
339
+ ];
340
+ if (option.flag) parts.push(`flag ${option.flag}`);
341
+ if (option.description) parts.push(`description "${quoteString(option.description)}"`);
342
+ return parts.join(" ");
343
+ });
344
+ const outputs = (surface.outputs || []).map((/** @type {any} */ output) => {
345
+ const parts = [` command ${output.command_id} format ${output.format || "human"}`];
346
+ if (output.schema_id) parts.push(`schema ${output.schema_id}`);
347
+ if (output.description) parts.push(`description "${quoteString(output.description)}"`);
348
+ return parts.join(" ");
349
+ });
350
+ const effects = (surface.effects || []).map((/** @type {any} */ effect) => {
351
+ const parts = [` command ${effect.command_id} effect ${effect.effect || "read_only"}`];
352
+ if (effect.target) parts.push(`target ${effect.target}`);
353
+ return parts.join(" ");
354
+ });
355
+ const examples = (surface.examples || []).map((/** @type {any} */ example) =>
356
+ ` command ${example.command_id} example "${quoteString(example.example)}"`
357
+ );
358
+ const realizedCapabilities = [...new Set(commandRecords.map((/** @type {any} */ command) => command.capability_id).filter(Boolean))].sort();
359
+ return ensureTrailingNewline(
360
+ `${renderCandidateMetadataComments(surface)}\n${[
361
+ `projection ${surface.id_hint} {`,
362
+ ` name "${quoteString(surface.label || "CLI Surface")}"`,
363
+ ' description "Candidate CLI surface inferred from imported command usage. Review commands, options, outputs, and side effects before adoption."',
364
+ " type cli_surface",
365
+ ` realizes [${realizedCapabilities.join(", ")}]`,
366
+ " outputs [maintained_app]",
367
+ "",
368
+ " commands {",
369
+ ...commands,
370
+ " }",
371
+ "",
372
+ " command_options {",
373
+ ...options,
374
+ " }",
375
+ "",
376
+ " command_outputs {",
377
+ ...outputs,
378
+ " }",
379
+ "",
380
+ " command_effects {",
381
+ ...effects,
382
+ " }",
383
+ "",
384
+ " command_examples {",
385
+ ...examples,
386
+ " }",
387
+ "",
388
+ " status proposed",
389
+ "}"
390
+ ].join("\n")}`
391
+ );
392
+ }
393
+
312
394
  /** @param {WorkflowRecord} patch @returns {any} */
313
395
  export function renderProjectionPatchDoc(patch) {
314
396
  const lines = [
@@ -33,6 +33,7 @@ export function buildBundleOperatorSummary(bundle) {
33
33
  const primaryConcept =
34
34
  primaryEntityId ||
35
35
  bundle.capabilities?.[0]?.id_hint ||
36
+ bundle.cliSurfaces?.[0]?.id_hint ||
36
37
  bundle.workflows?.[0]?.id_hint ||
37
38
  bundle.screens?.[0]?.id_hint ||
38
39
  bundle.enums?.[0]?.id_hint ||
@@ -40,6 +41,7 @@ export function buildBundleOperatorSummary(bundle) {
40
41
  const participants = summarizeBundleParticipants(bundle);
41
42
  const capabilityIds = [...new Set((bundle.capabilities || []).map((/** @type {any} */ entry) => entry.id_hint))].slice(0, 4);
42
43
  const widgetIds = [...new Set((bundle.widgets || []).map((/** @type {any} */ entry) => entry.id_hint))].slice(0, 4);
44
+ const cliSurfaceIds = [...new Set((bundle.cliSurfaces || []).map((/** @type {any} */ entry) => entry.id_hint))].slice(0, 4);
43
45
  const screenIds = [...new Set((bundle.screens || []).map((/** @type {any} */ entry) => entry.id_hint))].slice(0, 4);
44
46
  const routePaths = [...new Set((bundle.uiRoutes || []).map((/** @type {any} */ entry) => entry.path).filter(Boolean))].slice(0, 4);
45
47
  const workflowIds = [...new Set((bundle.workflows || []).map((/** @type {any} */ entry) => entry.id_hint))].slice(0, 4);
@@ -55,6 +57,7 @@ export function buildBundleOperatorSummary(bundle) {
55
57
  const evidenceKinds = [
56
58
  (bundle.entities || []).length > 0 ? "entity evidence" : null,
57
59
  (bundle.capabilities || []).length > 0 ? "API capability evidence" : null,
60
+ (bundle.cliSurfaces || []).length > 0 ? "CLI surface evidence" : null,
58
61
  (bundle.widgets || []).length > 0 ? "UI widget evidence" : null,
59
62
  (bundle.screens || []).length > 0 || (bundle.uiRoutes || []).length > 0 ? "UI screen/route evidence" : null,
60
63
  (bundle.workflows || []).length > 0 ? "workflow evidence" : null,
@@ -72,6 +75,7 @@ export function buildBundleOperatorSummary(bundle) {
72
75
  participants,
73
76
  capabilityIds,
74
77
  widgetIds,
78
+ cliSurfaceIds,
75
79
  screenIds,
76
80
  routePaths,
77
81
  workflowIds,
@@ -259,6 +259,7 @@ export function reconcileWorkflow(inputPath, options = {}) {
259
259
  capabilities: bundle.capabilities.map((/** @type {any} */ entry) => entry.id_hint),
260
260
  shapes: bundle.shapes.map((/** @type {any} */ entry) => entry.id),
261
261
  widgets: bundle.widgets.map((/** @type {any} */ entry) => entry.id_hint),
262
+ cli_surfaces: (bundle.cliSurfaces || []).map((/** @type {any} */ entry) => entry.id_hint),
262
263
  screens: bundle.screens.map((/** @type {any} */ entry) => entry.id_hint),
263
264
  workflows: bundle.workflows.map((/** @type {any} */ entry) => entry.id_hint),
264
265
  docs: bundle.docs.map((/** @type {any} */ entry) => entry.id),
@@ -282,7 +283,7 @@ export function reconcileWorkflow(inputPath, options = {}) {
282
283
  : "## Promoted Canonical Items";
283
284
  files["candidates/reconcile/report.json"] = `${stableStringify(report)}\n`;
284
285
  const candidateModelBundlesMarkdown = report.candidate_model_bundles.length
285
- ? report.candidate_model_bundles.map((/** @type {any} */ bundle) => `- \`${bundle.slug}\` (${bundle.actors.length} actors, ${bundle.roles.length} roles, ${bundle.entities.length} entities, ${bundle.enums.length} enums, ${bundle.capabilities.length} capabilities, ${bundle.shapes.length} shapes, ${bundle.widgets.length} widgets, ${bundle.screens.length} screens, ${bundle.workflows.length} workflows, ${bundle.docs.length} docs)
286
+ ? report.candidate_model_bundles.map((/** @type {any} */ bundle) => `- \`${bundle.slug}\` (${bundle.actors.length} actors, ${bundle.roles.length} roles, ${bundle.entities.length} entities, ${bundle.enums.length} enums, ${bundle.capabilities.length} capabilities, ${bundle.shapes.length} shapes, ${bundle.widgets.length} widgets, ${bundle.cli_surfaces.length} CLI surfaces, ${bundle.screens.length} screens, ${bundle.workflows.length} workflows, ${bundle.docs.length} docs)
286
287
  - primary concept \`${bundle.operator_summary.primaryConcept}\`${bundle.operator_summary.primaryEntityId ? `, primary entity \`${bundle.operator_summary.primaryEntityId}\`` : ""}
287
288
  - participants ${bundle.operator_summary.participants.label}
288
289
  - main capabilities ${summarizeBundleSurface(bundle, bundle.operator_summary.capabilityIds)}