@topogram/cli 0.3.65 → 0.3.66
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/package.json +1 -1
- package/src/adoption/plan/index.js +21 -8
- package/src/adoption/reporting.js +1 -1
- package/src/agent-brief.js +7 -21
- package/src/agent-ops/query-builders/change-risk/review-packets.js +2 -2
- package/src/agent-ops/query-builders/common.js +2 -2
- package/src/agent-ops/query-builders/multi-agent.js +1 -1
- package/src/agent-ops/query-builders/workflow-presets-core.js +3 -2
- package/src/archive/jsonl.js +2 -2
- package/src/archive/resolver-bridge.js +1 -1
- package/src/archive/unarchive.js +2 -1
- package/src/catalog/copy.js +11 -6
- package/src/catalog/provenance.js +2 -1
- package/src/cli/command-parsers/project.js +3 -0
- package/src/cli/command-parsers/shared.js +1 -1
- package/src/cli/commands/agent.js +2 -2
- package/src/cli/commands/check.js +3 -3
- package/src/cli/commands/doctor.js +2 -9
- package/src/cli/commands/generator-policy/runner.js +1 -1
- package/src/cli/commands/import/help.js +2 -2
- package/src/cli/commands/import/paths.js +3 -11
- package/src/cli/commands/import/plan.js +9 -1
- package/src/cli/commands/import/refresh.js +7 -6
- package/src/cli/commands/import/workspace.js +8 -5
- package/src/cli/commands/migrate.js +153 -0
- package/src/cli/commands/query/definitions.js +10 -10
- package/src/cli/commands/query/workspace.js +2 -6
- package/src/cli/commands/source.js +3 -12
- package/src/cli/commands/template/check.js +6 -5
- package/src/cli/commands/template-runner.js +6 -6
- package/src/cli/commands/trust.js +1 -1
- package/src/cli/commands/workflow.js +6 -1
- package/src/cli/dispatcher.js +6 -1
- package/src/cli/help.js +15 -14
- package/src/cli/migration-guidance.js +1 -1
- package/src/cli/output-safety.js +2 -1
- package/src/cli/path-normalization.js +3 -13
- package/src/generator/context/domain-page.js +1 -1
- package/src/generator/context/shared/maintained-boundary.js +2 -2
- package/src/generator/context/shared/metrics.js +2 -2
- package/src/generator/context/task-mode.js +2 -2
- package/src/generator/sdlc/doc-page.js +1 -1
- package/src/generator/surfaces/databases/lifecycle-shared.js +1 -1
- package/src/generator/surfaces/native/swiftui-templates/README.generated.md +1 -1
- package/src/import/core/context.js +5 -7
- package/src/import/core/runner/candidates.js +123 -3
- package/src/import/core/runner/reports.js +4 -3
- package/src/import/core/runner/ui-drafts.js +58 -2
- package/src/new-project/constants.js +1 -1
- package/src/new-project/create.js +9 -2
- package/src/new-project/project-files.js +16 -13
- package/src/new-project/template-resolution.js +6 -4
- package/src/new-project/template-snapshots.js +19 -7
- package/src/new-project/template-updates.js +1 -1
- package/src/project-config/index.js +27 -0
- package/src/sdlc/adopt.js +6 -5
- package/src/sdlc/paths.js +3 -5
- package/src/sdlc/scaffold.js +2 -1
- package/src/workflows/reconcile/adoption-plan/build.js +7 -3
- package/src/workflows/reconcile/adoption-plan/outputs.js +12 -2
- package/src/workflows/reconcile/adoption-plan/paths.js +1 -1
- package/src/workflows/reconcile/candidate-model.js +18 -2
- package/src/workflows/reconcile/impacts/adoption-plan.js +6 -2
- package/src/workflows/reconcile/impacts/indexes.js +5 -1
- package/src/workflows/reconcile/renderers.js +41 -6
- package/src/workflows/shared.js +5 -11
- package/src/workspace-paths.js +328 -0
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
validateGeneratorManifest
|
|
10
10
|
} from "../generator/registry.js";
|
|
11
11
|
import { validateProjectGeneratorPolicy } from "../generator-policy.js";
|
|
12
|
+
import { DEFAULT_WORKSPACE_PATH, normalizeWorkspaceConfigPath } from "../workspace-paths.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @typedef {Object} GeneratorBinding
|
|
@@ -32,6 +33,7 @@ import { validateProjectGeneratorPolicy } from "../generator-policy.js";
|
|
|
32
33
|
/**
|
|
33
34
|
* @typedef {Object} ProjectConfig
|
|
34
35
|
* @property {string} version
|
|
36
|
+
* @property {string} [workspace]
|
|
35
37
|
* @property {Record<string, { path: string, ownership: "generated"|"maintained" }>} outputs
|
|
36
38
|
* @property {{ runtimes: RuntimeTopologyRuntime[] }} topology
|
|
37
39
|
* @property {{ id?: string, module?: string, export?: string, implementation_module?: string, implementation_export?: string }} [implementation]
|
|
@@ -206,6 +208,7 @@ export function defaultProjectConfigForGraph(graph, implementation = null) {
|
|
|
206
208
|
|
|
207
209
|
return {
|
|
208
210
|
version: "0.1",
|
|
211
|
+
workspace: DEFAULT_WORKSPACE_PATH,
|
|
209
212
|
implementation: implementation?.exampleId
|
|
210
213
|
? {
|
|
211
214
|
id: implementation.exampleId
|
|
@@ -223,6 +226,29 @@ export function defaultProjectConfigForGraph(graph, implementation = null) {
|
|
|
223
226
|
};
|
|
224
227
|
}
|
|
225
228
|
|
|
229
|
+
/**
|
|
230
|
+
* @param {ValidationError[]} errors
|
|
231
|
+
* @param {any} config
|
|
232
|
+
* @returns {void}
|
|
233
|
+
*/
|
|
234
|
+
function validateWorkspaceConfig(errors, config) {
|
|
235
|
+
if (Object.prototype.hasOwnProperty.call(config, "workspaces")) {
|
|
236
|
+
pushError(errors, "topogram.project.json workspaces[] is not supported yet; use workspace instead");
|
|
237
|
+
}
|
|
238
|
+
if (!Object.prototype.hasOwnProperty.call(config, "workspace")) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (typeof config.workspace !== "string") {
|
|
242
|
+
pushError(errors, "topogram.project.json workspace must be a non-empty relative path");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
normalizeWorkspaceConfigPath(config.workspace);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
pushError(errors, error instanceof Error ? error.message : String(error));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
226
252
|
/**
|
|
227
253
|
* @param {string} root
|
|
228
254
|
* @returns {ProjectConfigInfo|null}
|
|
@@ -448,6 +474,7 @@ export function validateProjectConfig(config, graph = null, options = {}) {
|
|
|
448
474
|
if (typeof config.version !== "string" || config.version.length === 0) {
|
|
449
475
|
pushError(errors, "topogram.project.json version must be a non-empty string");
|
|
450
476
|
}
|
|
477
|
+
validateWorkspaceConfig(errors, config);
|
|
451
478
|
validateOutputConfig(errors, config);
|
|
452
479
|
if (config.topology?.components != null) {
|
|
453
480
|
pushError(errors, `topogram.project.json ${renameDiagnostic("'topology.components'", "'topology.runtimes'", `"topology": { "runtimes": [] }`)}`);
|
package/src/sdlc/adopt.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { existsSync, mkdirSync, readdirSync, statSync } from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
|
+
import { DEFAULT_TOPO_FOLDER_NAME, resolveTopoRoot } from "../workspace-paths.js";
|
|
9
10
|
|
|
10
11
|
const SDLC_FOLDERS = [
|
|
11
12
|
"pitches",
|
|
@@ -17,7 +18,7 @@ const SDLC_FOLDERS = [
|
|
|
17
18
|
];
|
|
18
19
|
|
|
19
20
|
function ensureFolder(root, name) {
|
|
20
|
-
const dir = path.join(root,
|
|
21
|
+
const dir = path.join(resolveTopoRoot(root), name);
|
|
21
22
|
if (!existsSync(dir)) {
|
|
22
23
|
mkdirSync(dir, { recursive: true });
|
|
23
24
|
return { name, created: true };
|
|
@@ -29,8 +30,8 @@ function scanPressure(root) {
|
|
|
29
30
|
// Pressure scan: count statements per kind so the operator knows the
|
|
30
31
|
// workspace's current shape. We do not attempt to backfill — that's the
|
|
31
32
|
// operator's job after they pick a starting point.
|
|
32
|
-
const tg =
|
|
33
|
-
if (!existsSync(tg)) return { error: `No '
|
|
33
|
+
const tg = resolveTopoRoot(root);
|
|
34
|
+
if (!existsSync(tg)) return { error: `No '${DEFAULT_TOPO_FOLDER_NAME}/' workspace folder found at ${root}` };
|
|
34
35
|
let totalFiles = 0;
|
|
35
36
|
function walk(dir) {
|
|
36
37
|
for (const entry of readdirSync(dir)) {
|
|
@@ -50,8 +51,8 @@ function scanPressure(root) {
|
|
|
50
51
|
|
|
51
52
|
export function sdlcAdopt(workspaceRoot) {
|
|
52
53
|
const root = path.resolve(workspaceRoot);
|
|
53
|
-
if (!existsSync(
|
|
54
|
-
return { ok: false, error: `No '
|
|
54
|
+
if (!existsSync(resolveTopoRoot(root))) {
|
|
55
|
+
return { ok: false, error: `No '${DEFAULT_TOPO_FOLDER_NAME}/' workspace folder at ${root}; run 'topogram new' first` };
|
|
55
56
|
}
|
|
56
57
|
const folders = SDLC_FOLDERS.map((name) => ensureFolder(root, name));
|
|
57
58
|
const pressure = scanPressure(root);
|
package/src/sdlc/paths.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { resolveTopoRoot, resolveWorkspaceContext } from "../workspace-paths.js";
|
|
2
2
|
|
|
3
3
|
export function topogramRootForSdlc(inputPath) {
|
|
4
|
-
|
|
5
|
-
return path.basename(absolute) === "topogram" ? absolute : path.join(absolute, "topogram");
|
|
4
|
+
return resolveTopoRoot(inputPath);
|
|
6
5
|
}
|
|
7
6
|
|
|
8
7
|
export function projectRootForSdlc(inputPath) {
|
|
9
|
-
|
|
10
|
-
return path.basename(absolute) === "topogram" ? path.dirname(absolute) : absolute;
|
|
8
|
+
return resolveWorkspaceContext(inputPath).projectRoot;
|
|
11
9
|
}
|
package/src/sdlc/scaffold.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { resolveTopoRoot } from "../workspace-paths.js";
|
|
6
7
|
|
|
7
8
|
const TEMPLATES = {
|
|
8
9
|
pitch: (slug) => `pitch pitch_${slug} {
|
|
@@ -73,7 +74,7 @@ export function scaffoldNew(workspaceRoot, kind, slug) {
|
|
|
73
74
|
if (!slug || !/^[a-z][a-z0-9_]*$/.test(slug)) {
|
|
74
75
|
return { ok: false, error: `Invalid slug '${slug}' — must match /^[a-z][a-z0-9_]*$/` };
|
|
75
76
|
}
|
|
76
|
-
const targetDir = path.join(workspaceRoot,
|
|
77
|
+
const targetDir = path.join(resolveTopoRoot(workspaceRoot), `${kind === "acceptance_criterion" ? "acceptance_criteria" : kind + "s"}`);
|
|
77
78
|
if (!existsSync(targetDir)) mkdirSync(targetDir, { recursive: true });
|
|
78
79
|
const targetFile = path.join(targetDir, `${slug}.tg`);
|
|
79
80
|
if (existsSync(targetFile)) {
|
|
@@ -82,8 +82,12 @@ export function buildAdoptionPlan(bundles) {
|
|
|
82
82
|
target: step.target || null,
|
|
83
83
|
confidence: step.confidence || null,
|
|
84
84
|
inference_summary: step.inference_summary || null,
|
|
85
|
+
source_kind: step.source_kind || null,
|
|
86
|
+
widget_id: step.widget_id || null,
|
|
87
|
+
event_name: step.event_name || null,
|
|
85
88
|
related_docs: step.related_docs || [],
|
|
86
89
|
related_capabilities: step.related_capabilities || [],
|
|
90
|
+
related_shapes: step.related_shapes || [],
|
|
87
91
|
permission: step.permission || null,
|
|
88
92
|
claim: step.claim || null,
|
|
89
93
|
claim_value: Object.prototype.hasOwnProperty.call(step, "claim_value") ? step.claim_value : null,
|
|
@@ -95,7 +99,7 @@ export function buildAdoptionPlan(bundles) {
|
|
|
95
99
|
canonical_path:
|
|
96
100
|
step.action === "skip_duplicate_shape"
|
|
97
101
|
? (step.target ? canonicalDisplayPathForItem("shape", step.target) : null)
|
|
98
|
-
: (step.canonical_rel_path ? `
|
|
102
|
+
: (step.canonical_rel_path ? `topo/${step.canonical_rel_path}` : canonicalDisplayPathForItem(itemKind, step.item)),
|
|
99
103
|
canonical_rel_path: step.canonical_rel_path || canonicalRelativePathForItem(itemKind, step.item),
|
|
100
104
|
reason: reasonForAdoptionItem(step),
|
|
101
105
|
recommendation: recommendationForAdoptionItem(step),
|
|
@@ -155,7 +159,7 @@ export function buildAdoptionPlan(bundles) {
|
|
|
155
159
|
target: patch.doc_id,
|
|
156
160
|
status: "pending",
|
|
157
161
|
source_path: `candidates/reconcile/model/bundles/${bundle.slug}/${patch.patch_rel_path}`,
|
|
158
|
-
canonical_path: patch.canonical_rel_path ? `
|
|
162
|
+
canonical_path: patch.canonical_rel_path ? `topo/${patch.canonical_rel_path}` : null,
|
|
159
163
|
canonical_rel_path: patch.canonical_rel_path || null,
|
|
160
164
|
reason: `Apply suggested related actor/role links to \`${patch.doc_id}\`.`,
|
|
161
165
|
recommendation: recommendationForAdoptionItem({ action: "apply_doc_link_patch", target: patch.doc_id }),
|
|
@@ -182,7 +186,7 @@ export function buildAdoptionPlan(bundles) {
|
|
|
182
186
|
status: "pending",
|
|
183
187
|
confidence: patch.imported_confidence || "low",
|
|
184
188
|
source_path: `candidates/reconcile/model/bundles/${bundle.slug}/${patch.patch_rel_path}`,
|
|
185
|
-
canonical_path: patch.canonical_rel_path ? `
|
|
189
|
+
canonical_path: patch.canonical_rel_path ? `topo/${patch.canonical_rel_path}` : null,
|
|
186
190
|
canonical_rel_path: patch.canonical_rel_path || null,
|
|
187
191
|
reason: `Apply suggested safe metadata updates to \`${patch.doc_id}\`.`,
|
|
188
192
|
recommendation: recommendationForAdoptionItem({ action: "apply_doc_metadata_patch", target: patch.doc_id }),
|
|
@@ -33,8 +33,18 @@ export function buildCanonicalAdoptionOutputs(paths, candidateFiles, planItems,
|
|
|
33
33
|
.filter((/** @type {any} */ item) => item?.kind === "capability")
|
|
34
34
|
.map((/** @type {any} */ item) => item.bundle)
|
|
35
35
|
);
|
|
36
|
+
const widgetShapeIdSet = new Set(
|
|
37
|
+
[...selectedSet]
|
|
38
|
+
.map((/** @type {any} */ key) => itemMap.get(key))
|
|
39
|
+
.filter((/** @type {any} */ item) => item?.kind === "widget")
|
|
40
|
+
.flatMap((/** @type {any} */ item) => item.related_shapes || [])
|
|
41
|
+
);
|
|
36
42
|
for (const item of planItems) {
|
|
37
|
-
if (
|
|
43
|
+
if (
|
|
44
|
+
item.kind === "shape" &&
|
|
45
|
+
item.status !== "skipped" &&
|
|
46
|
+
(capabilityBundleSet.has(item.bundle) || widgetShapeIdSet.has(item.item))
|
|
47
|
+
) {
|
|
38
48
|
selectedSet.add(adoptionItemKey(item));
|
|
39
49
|
}
|
|
40
50
|
}
|
|
@@ -131,7 +141,7 @@ export function buildPromotedCanonicalItems(planItems, selectedItems, writtenCan
|
|
|
131
141
|
track: item.track || null,
|
|
132
142
|
source_path: item.source_path || null,
|
|
133
143
|
canonical_rel_path: String(item.canonical_rel_path).replaceAll(path.sep, "/"),
|
|
134
|
-
canonical_path: item.canonical_path || `
|
|
144
|
+
canonical_path: item.canonical_path || `topo/${String(item.canonical_rel_path).replaceAll(path.sep, "/")}`,
|
|
135
145
|
suggested_action: item.suggested_action || null
|
|
136
146
|
}))
|
|
137
147
|
.sort((/** @type {any} */ a, /** @type {any} */ b) =>
|
|
@@ -29,7 +29,7 @@ export function canonicalRelativePathForItem(kind, item) {
|
|
|
29
29
|
/** @param {string} kind @param {string} item @returns {any} */
|
|
30
30
|
export function canonicalDisplayPathForItem(kind, item) {
|
|
31
31
|
const relativePath = canonicalRelativePathForItem(kind, item);
|
|
32
|
-
return relativePath ? `
|
|
32
|
+
return relativePath ? `topo/${relativePath}` : null;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/** @param {CandidateBundle} bundle @param {string} kind @param {WorkflowRecord} item @returns {any} */
|
|
@@ -82,8 +82,10 @@ export function bestContextBundleForCandidate(bundles, candidate) {
|
|
|
82
82
|
export function buildCandidateModelBundles(graph, appImport, topogramRoot) {
|
|
83
83
|
const dbCandidates = appImport.candidates.db || { entities: [], enums: [] };
|
|
84
84
|
const apiCandidates = appImport.candidates.api || { capabilities: [] };
|
|
85
|
-
const uiCandidates = appImport.candidates.ui || { screens: [], routes: [], actions: [], widgets: [] };
|
|
85
|
+
const uiCandidates = appImport.candidates.ui || { screens: [], routes: [], actions: [], widgets: [], shapes: [] };
|
|
86
86
|
const uiWidgetCandidates = uiCandidates.widgets || uiCandidates.components || [];
|
|
87
|
+
const uiShapeCandidates = uiCandidates.shapes || [];
|
|
88
|
+
const uiShapeCandidatesById = new Map(uiShapeCandidates.map((/** @type {any} */ shape) => [shape.id || shape.id_hint, shape]));
|
|
87
89
|
const workflowCandidates = appImport.candidates.workflows || { workflows: [], workflow_states: [], workflow_transitions: [] };
|
|
88
90
|
const verificationCandidates = appImport.candidates.verification || { verifications: [], scenarios: [], frameworks: [], scripts: [] };
|
|
89
91
|
const docCandidates = appImport.candidates.docs || [];
|
|
@@ -210,6 +212,20 @@ export function buildCandidateModelBundles(graph, appImport, topogramRoot) {
|
|
|
210
212
|
}
|
|
211
213
|
const conceptId = widgetConceptId(entry);
|
|
212
214
|
const bundle = getOrCreateCandidateBundle(bundles, conceptId, bundleLabelFromConceptId(conceptId || entry.screen_id || entry.id_hint));
|
|
215
|
+
for (const event of entry.inferred_events || []) {
|
|
216
|
+
const shapeId = event.payload_shape || null;
|
|
217
|
+
const shape = shapeId ? uiShapeCandidatesById.get(shapeId) : null;
|
|
218
|
+
if (shape && !bundle.shapes.some((/** @type {any} */ candidate) => candidate.id === shapeId)) {
|
|
219
|
+
bundle.shapes.push({
|
|
220
|
+
id: shapeId,
|
|
221
|
+
label: shape.label || `${entry.label || entry.id_hint} ${event.name || "event"} Payload`,
|
|
222
|
+
fields: shape.fields || event.payload_fields || ["id"],
|
|
223
|
+
source_kind: shape.source_kind || "ui_widget_event",
|
|
224
|
+
widget_id: entry.id_hint,
|
|
225
|
+
event_name: event.name || null
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
213
229
|
bundle.widgets.push(entry);
|
|
214
230
|
}
|
|
215
231
|
for (const entry of workflowCandidates.workflows || []) {
|
|
@@ -412,7 +428,7 @@ export function buildCandidateModelFiles(graph, appImport, topogramRoot) {
|
|
|
412
428
|
files[`${bundleRoot}/entities/${entry.id_hint}.tg`] = renderCandidateEntity(entry, knownEnums);
|
|
413
429
|
}
|
|
414
430
|
for (const shape of bundle.shapes) {
|
|
415
|
-
files[`${bundleRoot}/shapes/${shape.id}.tg`] = renderCandidateShape(shape.id, shape.label, shape.fields);
|
|
431
|
+
files[`${bundleRoot}/shapes/${shape.id}.tg`] = renderCandidateShape(shape.id, shape.label, shape.fields, shape.source_kind || null);
|
|
416
432
|
}
|
|
417
433
|
for (const entry of bundle.capabilities) {
|
|
418
434
|
const inputShapeId = bundle.shapes.find((/** @type {any} */ shape) => shape.id === shapeIdForCapability(entry, "input")) ? shapeIdForCapability(entry, "input") : null;
|
|
@@ -65,7 +65,7 @@ export function buildBundleAdoptionPlan(bundle, canonicalShapeIndex) {
|
|
|
65
65
|
for (const entry of bundle.shapes) {
|
|
66
66
|
const signature = shapeFieldSignature(entry.fields || []);
|
|
67
67
|
const duplicateTargets = canonicalShapeIndex.get(signature) || [];
|
|
68
|
-
if (duplicateTargets.length > 0) {
|
|
68
|
+
if (duplicateTargets.length > 0 && entry.source_kind !== "ui_widget_event") {
|
|
69
69
|
steps.push({
|
|
70
70
|
action: "skip_duplicate_shape",
|
|
71
71
|
item: entry.id,
|
|
@@ -76,7 +76,10 @@ export function buildBundleAdoptionPlan(bundle, canonicalShapeIndex) {
|
|
|
76
76
|
steps.push({
|
|
77
77
|
action: "promote_shape",
|
|
78
78
|
item: entry.id,
|
|
79
|
-
target: bundle.mergeHints?.canonicalEntityTarget || null
|
|
79
|
+
target: bundle.mergeHints?.canonicalEntityTarget || null,
|
|
80
|
+
source_kind: entry.source_kind || null,
|
|
81
|
+
widget_id: entry.widget_id || null,
|
|
82
|
+
event_name: entry.event_name || null
|
|
80
83
|
});
|
|
81
84
|
}
|
|
82
85
|
for (const entry of bundle.docs) {
|
|
@@ -126,6 +129,7 @@ export function buildBundleAdoptionPlan(bundle, canonicalShapeIndex) {
|
|
|
126
129
|
confidence: entry.confidence || "low",
|
|
127
130
|
inference_summary: entry.inference_summary || null,
|
|
128
131
|
related_capabilities: [entry.data_source].filter(Boolean),
|
|
132
|
+
related_shapes: [...new Set((entry.inferred_events || []).map((/** @type {any} */ event) => event.payload_shape).filter(Boolean))],
|
|
129
133
|
source_path: `candidates/reconcile/model/bundles/${bundle.slug}/widgets/${entry.id_hint}.tg`,
|
|
130
134
|
canonical_rel_path: `widgets/${dashedTopogramId(entry.id_hint)}.tg`
|
|
131
135
|
});
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
/** @param {any[]} fields @returns {any} */
|
|
4
4
|
export function shapeFieldSignature(fields) {
|
|
5
|
-
return [...new Set((fields || [])
|
|
5
|
+
return [...new Set((fields || [])
|
|
6
|
+
.map((field) => typeof field === "string" ? field : field?.name)
|
|
7
|
+
.filter(Boolean))]
|
|
8
|
+
.sort()
|
|
9
|
+
.join("|");
|
|
6
10
|
}
|
|
7
11
|
|
|
8
12
|
/** @param {ResolvedGraph} graph @returns {any} */
|
|
@@ -113,17 +113,31 @@ export function shapeIdForCapability(record, direction) {
|
|
|
113
113
|
return direction === "input" ? `shape_input_${stem}` : `shape_output_${stem}`;
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
/** @param {string} shapeId @param {string} label @param {any[]} fields @returns {any} */
|
|
117
|
-
export function renderCandidateShape(shapeId, label, fields) {
|
|
116
|
+
/** @param {string} shapeId @param {string} label @param {any[]} fields @param {string|null} [sourceKind] @returns {any} */
|
|
117
|
+
export function renderCandidateShape(shapeId, label, fields, sourceKind = null) {
|
|
118
|
+
/** @param {any} field @returns {{ name: string, fieldType: string, requiredness: string }} */
|
|
119
|
+
function normalizeField(field) {
|
|
120
|
+
if (typeof field === "string") {
|
|
121
|
+
return { name: field, fieldType: "string", requiredness: "optional" };
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
name: field?.name || "id",
|
|
125
|
+
fieldType: field?.field_type || field?.type || "string",
|
|
126
|
+
requiredness: field?.required ? "required" : "optional"
|
|
127
|
+
};
|
|
128
|
+
}
|
|
118
129
|
const lines = [
|
|
119
130
|
`shape ${shapeId} {`,
|
|
120
131
|
` name "${label}"`,
|
|
121
|
-
|
|
132
|
+
sourceKind === "ui_widget_event"
|
|
133
|
+
? ` description "Candidate event payload shape imported from brownfield UI interaction evidence"`
|
|
134
|
+
: ` description "Candidate shape imported from brownfield API evidence"`,
|
|
122
135
|
"",
|
|
123
136
|
" fields {"
|
|
124
137
|
];
|
|
125
138
|
for (const field of fields) {
|
|
126
|
-
|
|
139
|
+
const normalized = normalizeField(field);
|
|
140
|
+
lines.push(` ${normalized.name} ${normalized.fieldType} ${normalized.requiredness}`);
|
|
127
141
|
}
|
|
128
142
|
lines.push(" }", "", " status active", "}");
|
|
129
143
|
return ensureTrailingNewline(lines.join("\n"));
|
|
@@ -236,7 +250,6 @@ export function renderCandidateUiReportDoc(screen, routes, actions) {
|
|
|
236
250
|
source_of_truth: "imported",
|
|
237
251
|
confidence: screen.confidence || "medium",
|
|
238
252
|
review_required: true,
|
|
239
|
-
related_entities: [screen.entity_id].filter(Boolean),
|
|
240
253
|
provenance: screen.provenance || [],
|
|
241
254
|
tags: ["import", "ui"]
|
|
242
255
|
};
|
|
@@ -244,11 +257,12 @@ export function renderCandidateUiReportDoc(screen, routes, actions) {
|
|
|
244
257
|
"Candidate UI surface imported from brownfield route evidence.",
|
|
245
258
|
"",
|
|
246
259
|
`Screen: \`${screen.id_hint}\` (${screen.screen_kind})`,
|
|
260
|
+
screen.entity_id ? `Inferred entity: \`${screen.entity_id}\`` : null,
|
|
247
261
|
`Routes: ${routes.length ? routes.map((/** @type {any} */ route) => `\`${route.path}\``).join(", ") : "_none_"}`,
|
|
248
262
|
`Actions: ${actions.length ? actions.map((/** @type {any} */ action) => `\`${action.capability_hint}\``).join(", ") : "_none_"}`,
|
|
249
263
|
"",
|
|
250
264
|
"Review this UI surface before promoting it into canonical docs or projections."
|
|
251
|
-
].join("\n");
|
|
265
|
+
].filter(Boolean).join("\n");
|
|
252
266
|
return renderMarkdownDoc(metadata, body);
|
|
253
267
|
}
|
|
254
268
|
|
|
@@ -257,15 +271,36 @@ export function renderCandidateWidget(widget) {
|
|
|
257
271
|
const propName = widget.data_prop || "rows";
|
|
258
272
|
const pattern = widget.pattern || "search_results";
|
|
259
273
|
const region = widget.region || "results";
|
|
274
|
+
const inferredEvents = Array.isArray(widget.inferred_events) ? widget.inferred_events : [];
|
|
275
|
+
const eventLines = inferredEvents
|
|
276
|
+
.filter((/** @type {any} */ event) => event.name && event.payload_shape)
|
|
277
|
+
.map((/** @type {any} */ event) => ` ${event.name} ${event.payload_shape}`);
|
|
278
|
+
const selectionEvent = inferredEvents.find((/** @type {any} */ event) => event.name);
|
|
279
|
+
const eventComments = inferredEvents.map((/** @type {any} */ event) =>
|
|
280
|
+
` # Inferred event: ${event.name || "event"} ${event.action || "action"} ${event.target_screen || event.target || "target"}.`
|
|
281
|
+
);
|
|
282
|
+
const behaviorLines = eventLines.length > 0
|
|
283
|
+
? [
|
|
284
|
+
" events {",
|
|
285
|
+
...eventLines,
|
|
286
|
+
" }",
|
|
287
|
+
" behavior [selection]",
|
|
288
|
+
" behaviors {",
|
|
289
|
+
` selection mode single emits ${selectionEvent?.name || "row_select"}`,
|
|
290
|
+
" }"
|
|
291
|
+
]
|
|
292
|
+
: [];
|
|
260
293
|
return ensureTrailingNewline(
|
|
261
294
|
[
|
|
262
295
|
`widget ${widget.id_hint} {`,
|
|
263
296
|
` name "${widget.label || widget.id_hint}"`,
|
|
264
297
|
' description "Candidate reusable widget inferred from imported UI evidence. Review props, behavior, events, and reuse before adoption."',
|
|
265
298
|
" category collection",
|
|
299
|
+
...eventComments,
|
|
266
300
|
" props {",
|
|
267
301
|
` ${propName} array required`,
|
|
268
302
|
" }",
|
|
303
|
+
...behaviorLines,
|
|
269
304
|
` patterns [${pattern}]`,
|
|
270
305
|
` regions [${region}]`,
|
|
271
306
|
" status proposed",
|
package/src/workflows/shared.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
import { ensureTrailingNewline, titleCase } from "../text-helpers.js";
|
|
6
|
+
import { resolveWorkspaceContext } from "../workspace-paths.js";
|
|
6
7
|
|
|
7
8
|
/** @param {string} startDir @returns {any} */
|
|
8
9
|
export function findNearestGitRoot(startDir) {
|
|
@@ -21,17 +22,10 @@ export function findNearestGitRoot(startDir) {
|
|
|
21
22
|
|
|
22
23
|
/** @param {string} inputPath @returns {any} */
|
|
23
24
|
export function normalizeWorkspacePaths(inputPath) {
|
|
25
|
+
const context = resolveWorkspaceContext(inputPath);
|
|
24
26
|
const absolute = path.resolve(inputPath);
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const isTopogramDir = path.basename(absolute) === "topogram" && inputExists;
|
|
28
|
-
const bootstrapWorkspaceRoot = !isTopogramDir && !hasTopogramChild;
|
|
29
|
-
const topogramRoot = isTopogramDir
|
|
30
|
-
? absolute
|
|
31
|
-
: hasTopogramChild
|
|
32
|
-
? path.join(absolute, "topogram")
|
|
33
|
-
: path.join(absolute, "topogram");
|
|
34
|
-
const workspaceRoot = isTopogramDir ? path.dirname(topogramRoot) : absolute;
|
|
27
|
+
const topogramRoot = context.topoRoot;
|
|
28
|
+
const workspaceRoot = context.projectRoot;
|
|
35
29
|
const repoRoot = findNearestGitRoot(workspaceRoot);
|
|
36
30
|
return {
|
|
37
31
|
inputRoot: absolute,
|
|
@@ -39,7 +33,7 @@ export function normalizeWorkspacePaths(inputPath) {
|
|
|
39
33
|
workspaceRoot,
|
|
40
34
|
exampleRoot: workspaceRoot,
|
|
41
35
|
repoRoot,
|
|
42
|
-
bootstrappedTopogramRoot:
|
|
36
|
+
bootstrappedTopogramRoot: context.bootstrappedTopoRoot
|
|
43
37
|
};
|
|
44
38
|
}
|
|
45
39
|
|