@topogram/cli 0.3.72 → 0.3.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -195
- package/package.json +1 -1
- package/src/adoption/plan/index.js +2 -1
- package/src/agent-brief.js +46 -2
- package/src/archive/archive.js +1 -1
- package/src/archive/jsonl.js +18 -8
- package/src/archive/resolver-bridge.js +34 -1
- package/src/archive/schema.js +1 -1
- package/src/archive/unarchive.js +26 -0
- package/src/cli/command-parsers/sdlc.js +66 -0
- package/src/cli/commands/import/help.js +1 -0
- package/src/cli/commands/import/plan.js +9 -0
- package/src/cli/commands/import/workspace.js +3 -0
- package/src/cli/commands/query/definitions.js +11 -10
- package/src/cli/commands/query/workspace.js +23 -2
- package/src/cli/commands/release-rollout.js +191 -10
- package/src/cli/commands/release-shared.js +51 -2
- package/src/cli/commands/release.js +16 -3
- package/src/cli/commands/sdlc.js +213 -5
- package/src/cli/dispatcher.js +8 -0
- package/src/cli/help.js +15 -3
- package/src/cli/options.js +1 -0
- package/src/generator/context/shared/domain-sdlc.js +27 -0
- package/src/generator/context/shared/relationships.js +2 -1
- package/src/generator/context/shared/types.d.ts +1 -0
- package/src/generator/context/shared.d.ts +2 -0
- package/src/generator/context/shared.js +2 -0
- package/src/generator/context/slice/core.js +3 -0
- package/src/generator/context/slice/sdlc.js +57 -2
- package/src/generator/context/task-mode.js +7 -0
- package/src/generator/sdlc/board.js +2 -0
- package/src/generator/sdlc/traceability-matrix.js +5 -1
- package/src/import/core/context.js +1 -1
- package/src/import/core/contracts.js +3 -3
- package/src/import/core/registry.js +3 -0
- package/src/import/core/runner/candidates.js +7 -0
- package/src/import/core/runner/reports.js +9 -1
- package/src/import/core/runner/tracks.js +3 -0
- package/src/import/extractors/cli/generic.js +340 -0
- package/src/new-project/project-files.js +10 -3
- package/src/resolver/enrich/task.js +3 -1
- package/src/resolver/index.js +6 -0
- package/src/resolver/normalize.js +31 -0
- package/src/resolver/projections-cli.js +158 -0
- package/src/sdlc/adopt.js +4 -1
- package/src/sdlc/check.js +24 -2
- package/src/sdlc/complete.js +47 -0
- package/src/sdlc/dod/index.js +2 -0
- package/src/sdlc/dod/plan.js +15 -0
- package/src/sdlc/dod/task.js +7 -3
- package/src/sdlc/explain.js +53 -1
- package/src/sdlc/gate.js +352 -0
- package/src/sdlc/history.d.ts +7 -0
- package/src/sdlc/history.js +50 -5
- package/src/sdlc/link.js +172 -0
- package/src/sdlc/paths.d.ts +4 -0
- package/src/sdlc/paths.js +8 -0
- package/src/sdlc/plan-steps.js +71 -0
- package/src/sdlc/plan.js +245 -0
- package/src/sdlc/policy.js +249 -0
- package/src/sdlc/prep.js +186 -0
- package/src/sdlc/scaffold.js +4 -2
- package/src/sdlc/status-filter.js +2 -0
- package/src/sdlc/transitions/index.js +3 -0
- package/src/sdlc/transitions/plan.js +32 -0
- package/src/validator/common.js +25 -4
- package/src/validator/index.js +10 -0
- package/src/validator/kinds.d.ts +7 -0
- package/src/validator/kinds.js +32 -0
- package/src/validator/per-kind/plan.js +128 -0
- package/src/validator/per-kind/task.js +19 -0
- package/src/validator/projections/cli.js +267 -0
- package/src/validator.d.ts +1 -0
- package/src/workflows/import-app/shared.js +1 -1
- package/src/workflows/reconcile/adoption-plan/build.js +3 -1
- package/src/workflows/reconcile/adoption-plan/reasons.js +5 -0
- package/src/workflows/reconcile/bundle-core/index.js +3 -0
- package/src/workflows/reconcile/candidate-model.js +15 -0
- package/src/workflows/reconcile/gap-report.js +4 -2
- package/src/workflows/reconcile/impacts/adoption-plan.js +13 -0
- package/src/workflows/reconcile/renderers.js +82 -0
- package/src/workflows/reconcile/summary.js +4 -0
- package/src/workflows/reconcile/workflow.js +2 -1
- package/src/workspace-paths.js +26 -2
|
@@ -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
|
+
}
|
package/src/validator.d.ts
CHANGED
|
@@ -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)}
|