@topogram/cli 0.3.65 → 0.3.67
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 +38 -8
- 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
|
@@ -338,7 +338,7 @@ function importAdoptMode(graph, options = {}) {
|
|
|
338
338
|
write_scope: {
|
|
339
339
|
safe_to_edit: ["candidates/**"],
|
|
340
340
|
generator_owned: ["artifacts/**", "apps/**"],
|
|
341
|
-
human_owned_review_required: ["
|
|
341
|
+
human_owned_review_required: ["topo/**"],
|
|
342
342
|
out_of_bounds: [".git/**", "node_modules/**"]
|
|
343
343
|
},
|
|
344
344
|
verification_targets: {
|
|
@@ -384,7 +384,7 @@ function diffReviewMode(graph, options = {}) {
|
|
|
384
384
|
write_scope: {
|
|
385
385
|
safe_to_edit: [],
|
|
386
386
|
generator_owned: ["artifacts/**", "apps/**"],
|
|
387
|
-
human_owned_review_required: ["
|
|
387
|
+
human_owned_review_required: ["topo/**", "examples/maintained/proof-app/**"],
|
|
388
388
|
out_of_bounds: [".git/**", "node_modules/**"]
|
|
389
389
|
},
|
|
390
390
|
verification_targets: slice?.verification_targets || recommendedVerificationTargets(graph, [], {
|
|
@@ -47,7 +47,7 @@ export function generateSdlcDocPage(graph, options = {}) {
|
|
|
47
47
|
type: "sdlc_doc_page",
|
|
48
48
|
version: 1,
|
|
49
49
|
document_id: doc.id,
|
|
50
|
-
output_path: `
|
|
50
|
+
output_path: `topo/docs-generated/sdlc/${doc.id}.md`,
|
|
51
51
|
markdown
|
|
52
52
|
};
|
|
53
53
|
}
|
|
@@ -99,7 +99,7 @@ function renderEmptySnapshotForProjection(projection) {
|
|
|
99
99
|
|
|
100
100
|
function renderDbLifecycleEnvExample(projection, plan) {
|
|
101
101
|
const engine = plan.engine || (dbProfileForProjection(projection).startsWith("sqlite") ? "sqlite" : "postgres");
|
|
102
|
-
const inputPath = "../../../../
|
|
102
|
+
const inputPath = "../../../../topo";
|
|
103
103
|
if (engine === "sqlite") {
|
|
104
104
|
return `DATABASE_URL=file:./var/${projection.id}.sqlite\nTOPOGRAM_INPUT_PATH=${inputPath}\n`;
|
|
105
105
|
}
|
|
@@ -22,5 +22,5 @@ Configure the API base URL and optional auth token via scheme environment variab
|
|
|
22
22
|
From `engine/`:
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
topogram emit swiftui-app ./
|
|
25
|
+
topogram emit swiftui-app ./topo --projection proj_ios_surface__swiftui --write --out-dir ./app/ios-swiftui
|
|
26
26
|
```
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
import { readJsonIfExists, readTextIfExists } from "./shared.js";
|
|
5
|
+
import { resolveWorkspaceContext } from "../../workspace-paths.js";
|
|
5
6
|
|
|
6
7
|
export function findNearestGitRoot(startDir) {
|
|
7
8
|
let currentDir = path.resolve(startDir);
|
|
@@ -19,13 +20,10 @@ export function findNearestGitRoot(startDir) {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export function normalizeWorkspacePaths(inputPath) {
|
|
23
|
+
const context = resolveWorkspaceContext(inputPath);
|
|
22
24
|
const absolute = path.resolve(inputPath);
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const hasTopogramChild = fs.existsSync(topogramChild) && fs.statSync(topogramChild).isDirectory();
|
|
26
|
-
const isTopogramDir = path.basename(absolute) === "topogram" && inputExists;
|
|
27
|
-
const topogramRoot = isTopogramDir ? absolute : hasTopogramChild ? topogramChild : path.join(absolute, "topogram");
|
|
28
|
-
const workspaceRoot = isTopogramDir ? path.dirname(topogramRoot) : absolute;
|
|
25
|
+
const topogramRoot = context.topoRoot;
|
|
26
|
+
const workspaceRoot = context.projectRoot;
|
|
29
27
|
const repoRoot = findNearestGitRoot(workspaceRoot);
|
|
30
28
|
return {
|
|
31
29
|
inputRoot: absolute,
|
|
@@ -33,7 +31,7 @@ export function normalizeWorkspacePaths(inputPath) {
|
|
|
33
31
|
workspaceRoot,
|
|
34
32
|
exampleRoot: workspaceRoot,
|
|
35
33
|
repoRoot,
|
|
36
|
-
bootstrappedTopogramRoot:
|
|
34
|
+
bootstrappedTopogramRoot: context.bootstrappedTopoRoot
|
|
37
35
|
};
|
|
38
36
|
}
|
|
39
37
|
|
|
@@ -47,6 +47,119 @@ export function capabilityHintsForScreen(screen) {
|
|
|
47
47
|
return rawHints.map(normalizeCapabilityHint).filter(Boolean);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* @param {string|null|undefined} screenId
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
function screenConceptStem(screenId) {
|
|
55
|
+
return String(screenId || "")
|
|
56
|
+
.replace(/_(list|index|table|grid|results|detail|show|create|new|edit|form)$/, "")
|
|
57
|
+
.replace(/^(list|show|create|edit)_/, "");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {string|null|undefined} screenId
|
|
62
|
+
* @param {string} eventName
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
function eventPayloadShapeId(screenId, eventName) {
|
|
66
|
+
const stem = screenConceptStem(screenId) || idHintify(String(screenId || "widget"));
|
|
67
|
+
return `shape_event_${idHintify(`${stem}_${eventName}`)}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {string|null|undefined} routePath
|
|
72
|
+
* @returns {{ name: string, field_type: string, required: boolean }[]}
|
|
73
|
+
*/
|
|
74
|
+
function routeParamFields(routePath) {
|
|
75
|
+
const fields = [];
|
|
76
|
+
const pathText = String(routePath || "");
|
|
77
|
+
const routeParamPattern = /[:{]([A-Za-z_][A-Za-z0-9_]*)}?/g;
|
|
78
|
+
for (const match of pathText.matchAll(routeParamPattern)) {
|
|
79
|
+
fields.push({
|
|
80
|
+
name: idHintify(match[1]),
|
|
81
|
+
field_type: "string",
|
|
82
|
+
required: true
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return fields.length > 0 ? fields : [{ name: "id", field_type: "string", required: true }];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {any} sourceScreen
|
|
90
|
+
* @param {any[]} screens
|
|
91
|
+
* @returns {any|null}
|
|
92
|
+
*/
|
|
93
|
+
function matchingDetailScreen(sourceScreen, screens) {
|
|
94
|
+
const sourceStem = screenConceptStem(sourceScreen?.id_hint);
|
|
95
|
+
if (!sourceStem) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return screens.find((screen) =>
|
|
99
|
+
screen?.id_hint !== sourceScreen?.id_hint &&
|
|
100
|
+
screen?.screen_kind === "detail" &&
|
|
101
|
+
screenConceptStem(screen.id_hint) === sourceStem
|
|
102
|
+
) || null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {any} screen
|
|
107
|
+
* @param {any[]} screens
|
|
108
|
+
* @returns {any[]}
|
|
109
|
+
*/
|
|
110
|
+
function inferredEventsForWidgetScreen(screen, screens) {
|
|
111
|
+
const detailScreen = matchingDetailScreen(screen, screens);
|
|
112
|
+
if (!detailScreen) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
return [{
|
|
116
|
+
name: "row_select",
|
|
117
|
+
kind: "selection",
|
|
118
|
+
action: "navigate",
|
|
119
|
+
target_screen: detailScreen.id_hint,
|
|
120
|
+
payload_shape: eventPayloadShapeId(screen?.id_hint, "row_select"),
|
|
121
|
+
confidence: "medium",
|
|
122
|
+
evidence: detailScreen.provenance || [],
|
|
123
|
+
payload_fields: routeParamFields(detailScreen.route_path),
|
|
124
|
+
requires_payload_shape_review: true
|
|
125
|
+
}];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {any[]} widgets
|
|
130
|
+
* @returns {any[]}
|
|
131
|
+
*/
|
|
132
|
+
function deriveUiWidgetEventShapeCandidates(widgets) {
|
|
133
|
+
return widgets.flatMap((widget) =>
|
|
134
|
+
(widget.inferred_events || [])
|
|
135
|
+
.filter((/** @type {any} */ event) => event.payload_shape)
|
|
136
|
+
.map((/** @type {any} */ event) => makeCandidateRecord({
|
|
137
|
+
kind: "shape",
|
|
138
|
+
idHint: event.payload_shape,
|
|
139
|
+
label: `${widget.label || widget.id_hint} ${event.name || "event"} payload`,
|
|
140
|
+
confidence: event.confidence || widget.confidence || "low",
|
|
141
|
+
sourceKind: "ui_widget_event",
|
|
142
|
+
sourceOfTruth: "candidate",
|
|
143
|
+
provenance: [
|
|
144
|
+
...(widget.provenance || []),
|
|
145
|
+
...(event.evidence || [])
|
|
146
|
+
],
|
|
147
|
+
track: "ui",
|
|
148
|
+
widget_id: widget.id_hint,
|
|
149
|
+
event_name: event.name,
|
|
150
|
+
fields: event.payload_fields || [{ name: "id", field_type: "string", required: true }],
|
|
151
|
+
missing_decisions: [
|
|
152
|
+
"confirm selected row identity fields",
|
|
153
|
+
"confirm event payload shape name"
|
|
154
|
+
],
|
|
155
|
+
notes: [
|
|
156
|
+
"Imported widget event payload shapes are review-only.",
|
|
157
|
+
"Adopt with the widget when the event binding is accepted."
|
|
158
|
+
]
|
|
159
|
+
}))
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
50
163
|
/**
|
|
51
164
|
* @param {any} candidates
|
|
52
165
|
* @returns {any[]}
|
|
@@ -95,6 +208,7 @@ function deriveUiWidgetCandidates(candidates) {
|
|
|
95
208
|
const pattern = collectionPatternFromPresentations(presentations);
|
|
96
209
|
const widgetStem = idHintify(`${screen.id_hint}_results`);
|
|
97
210
|
const loadCapability = loadCapabilityForScreen(screen);
|
|
211
|
+
const inferredEvents = inferredEventsForWidgetScreen(screen, screens);
|
|
98
212
|
return makeCandidateRecord({
|
|
99
213
|
kind: "widget",
|
|
100
214
|
idHint: `widget_${widgetStem}`,
|
|
@@ -109,10 +223,13 @@ function deriveUiWidgetCandidates(candidates) {
|
|
|
109
223
|
data_prop: "rows",
|
|
110
224
|
data_source: loadCapability,
|
|
111
225
|
inferred_props: [{ name: "rows", type: "array", required: true, source: loadCapability }],
|
|
112
|
-
inferred_events:
|
|
226
|
+
inferred_events: inferredEvents,
|
|
113
227
|
inferred_region: "results",
|
|
114
228
|
inferred_pattern: pattern,
|
|
115
|
-
evidence:
|
|
229
|
+
evidence: [
|
|
230
|
+
...(screen.provenance || []),
|
|
231
|
+
...inferredEvents.flatMap((event) => event.evidence || [])
|
|
232
|
+
],
|
|
116
233
|
missing_decisions: [
|
|
117
234
|
"confirm widget reuse boundary",
|
|
118
235
|
"confirm prop names and data source",
|
|
@@ -160,11 +277,14 @@ export function normalizeCandidatesForTrack(track, candidates) {
|
|
|
160
277
|
if (track === "ui") {
|
|
161
278
|
const explicitWidgets = uiWidgetCandidates(candidates);
|
|
162
279
|
const derivedWidgets = deriveUiWidgetCandidates(candidates);
|
|
280
|
+
const widgets = dedupeCandidateRecords([...explicitWidgets, ...derivedWidgets], idHint);
|
|
281
|
+
const eventShapes = deriveUiWidgetEventShapeCandidates(widgets);
|
|
163
282
|
return {
|
|
164
283
|
screens: dedupeCandidateRecords(candidates.screens || [], idHint),
|
|
165
284
|
routes: dedupeCandidateRecords(candidates.routes || [], idHint),
|
|
166
285
|
actions: dedupeCandidateRecords(candidates.actions || [], idHint),
|
|
167
|
-
widgets
|
|
286
|
+
widgets,
|
|
287
|
+
shapes: dedupeCandidateRecords([...(candidates.shapes || []), ...eventShapes], idHint),
|
|
168
288
|
stacks: [...new Set(candidates.stacks || [])].sort()
|
|
169
289
|
};
|
|
170
290
|
}
|
|
@@ -21,11 +21,12 @@ export function reportMarkdown(track, candidates) {
|
|
|
21
21
|
}
|
|
22
22
|
if (track === "ui") {
|
|
23
23
|
const widgets = uiWidgetCandidates(candidates);
|
|
24
|
+
const shapes = candidates.shapes || [];
|
|
24
25
|
const widgetLines = widgets.map((widget) =>
|
|
25
|
-
`- \`${widget.id_hint}\` confidence ${widget.confidence || "unknown"} pattern \`${widget.pattern || widget.inferred_pattern || "unknown"}\` region \`${widget.region || widget.inferred_region || "unknown"}\` evidence ${(widget.evidence || widget.provenance || []).length} missing decisions ${(widget.missing_decisions || []).length}`
|
|
26
|
+
`- \`${widget.id_hint}\` confidence ${widget.confidence || "unknown"} pattern \`${widget.pattern || widget.inferred_pattern || "unknown"}\` region \`${widget.region || widget.inferred_region || "unknown"}\` events ${(widget.inferred_events || []).length} evidence ${(widget.evidence || widget.provenance || []).length} missing decisions ${(widget.missing_decisions || []).length}`
|
|
26
27
|
);
|
|
27
28
|
return ensureTrailingNewline(
|
|
28
|
-
`# UI Import Report\n\n- Screens: ${candidates.screens.length}\n- Routes: ${candidates.routes.length}\n- Actions: ${candidates.actions.length}\n- Widgets: ${widgets.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n\n## Widget Candidates\n\n${widgetLines.length ? widgetLines.join("\n") : "- none"}\n\n## Next Validation\n\n- Review candidates under \`
|
|
29
|
+
`# UI Import Report\n\n- Screens: ${candidates.screens.length}\n- Routes: ${candidates.routes.length}\n- Actions: ${candidates.actions.length}\n- Widgets: ${widgets.length}\n- Event payload shapes: ${shapes.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n\n## Widget Candidates\n\n${widgetLines.length ? widgetLines.join("\n") : "- none"}\n\n## Next Validation\n\n- Review candidates under \`topo/candidates/app/ui/drafts/widgets/**\`.\n- Run \`topogram import plan <path>\` before adoption.\n- After adoption, run \`topogram check <path>\`, \`topogram widget check <path>\`, and \`topogram widget behavior <path>\`.\n`
|
|
29
30
|
);
|
|
30
31
|
}
|
|
31
32
|
if (track === "verification") {
|
|
@@ -45,6 +46,6 @@ export function reportMarkdown(track, candidates) {
|
|
|
45
46
|
*/
|
|
46
47
|
export function appReportMarkdown(candidates, tracks) {
|
|
47
48
|
return ensureTrailingNewline(
|
|
48
|
-
`# App Import Report\n\nTracks: ${tracks.join(", ")}\n\n## DB\n\n- Entities: ${candidates.db?.entities?.length || 0}\n- Enums: ${candidates.db?.enums?.length || 0}\n- Relations: ${candidates.db?.relations?.length || 0}\n\n## API\n\n- Capabilities: ${candidates.api?.capabilities?.length || 0}\n- Routes: ${candidates.api?.routes?.length || 0}\n- Stacks: ${candidates.api?.stacks?.length ? candidates.api.stacks.join(", ") : "none"}\n\n## UI\n\n- Screens: ${candidates.ui?.screens?.length || 0}\n- Routes: ${candidates.ui?.routes?.length || 0}\n- Actions: ${candidates.ui?.actions?.length || 0}\n- Widgets: ${uiWidgetCandidates(candidates.ui).length}\n- Stacks: ${candidates.ui?.stacks?.length ? candidates.ui.stacks.join(", ") : "none"}\n\n## Workflows\n\n- Workflows: ${candidates.workflows?.workflows?.length || 0}\n- States: ${candidates.workflows?.workflow_states?.length || 0}\n- Transitions: ${candidates.workflows?.workflow_transitions?.length || 0}\n\n## Verification\n\n- Verifications: ${candidates.verification?.verifications?.length || 0}\n- Scenarios: ${candidates.verification?.scenarios?.length || 0}\n- Frameworks: ${candidates.verification?.frameworks?.length ? candidates.verification.frameworks.join(", ") : "none"}\n- Scripts: ${candidates.verification?.scripts?.length || 0}\n`
|
|
49
|
+
`# App Import Report\n\nTracks: ${tracks.join(", ")}\n\n## DB\n\n- Entities: ${candidates.db?.entities?.length || 0}\n- Enums: ${candidates.db?.enums?.length || 0}\n- Relations: ${candidates.db?.relations?.length || 0}\n\n## API\n\n- Capabilities: ${candidates.api?.capabilities?.length || 0}\n- Routes: ${candidates.api?.routes?.length || 0}\n- Stacks: ${candidates.api?.stacks?.length ? candidates.api.stacks.join(", ") : "none"}\n\n## UI\n\n- Screens: ${candidates.ui?.screens?.length || 0}\n- Routes: ${candidates.ui?.routes?.length || 0}\n- Actions: ${candidates.ui?.actions?.length || 0}\n- Widgets: ${uiWidgetCandidates(candidates.ui).length}\n- Event payload shapes: ${candidates.ui?.shapes?.length || 0}\n- Stacks: ${candidates.ui?.stacks?.length ? candidates.ui.stacks.join(", ") : "none"}\n\n## Workflows\n\n- Workflows: ${candidates.workflows?.workflows?.length || 0}\n- States: ${candidates.workflows?.workflow_states?.length || 0}\n- Transitions: ${candidates.workflows?.workflow_transitions?.length || 0}\n\n## Verification\n\n- Verifications: ${candidates.verification?.verifications?.length || 0}\n- Scenarios: ${candidates.verification?.scenarios?.length || 0}\n- Frameworks: ${candidates.verification?.frameworks?.length ? candidates.verification.frameworks.join(", ") : "none"}\n- Scripts: ${candidates.verification?.scripts?.length || 0}\n`
|
|
49
50
|
);
|
|
50
51
|
}
|
|
@@ -49,22 +49,73 @@ function renderWidgetCandidate(widget) {
|
|
|
49
49
|
"confirm prop names and data source",
|
|
50
50
|
"confirm events and behavior"
|
|
51
51
|
];
|
|
52
|
+
const inferredEvents = Array.isArray(widget.inferred_events) ? widget.inferred_events : [];
|
|
53
|
+
const eventLines = inferredEvents
|
|
54
|
+
.filter((/** @type {any} */ event) => event.name && event.payload_shape)
|
|
55
|
+
.map((/** @type {any} */ event) => ` ${event.name} ${event.payload_shape}`);
|
|
56
|
+
const selectionEvent = inferredEvents.find((/** @type {any} */ event) => event.name);
|
|
57
|
+
const inferredEventComments = inferredEvents.length > 0
|
|
58
|
+
? `${inferredEvents.map((/** @type {any} */ event) =>
|
|
59
|
+
` # Inferred event: ${event.name || "event"} ${event.action || "action"} ${event.target_screen || event.target || "target"}; review payload shape ${event.payload_shape || "before adding an events block"}.`
|
|
60
|
+
).join("\n")}\n`
|
|
61
|
+
: "";
|
|
62
|
+
const behaviorBlock = eventLines.length > 0
|
|
63
|
+
? ` events {\n${eventLines.join("\n")}\n }\n behavior [selection]\n behaviors {\n selection mode single emits ${selectionEvent?.name || "row_select"}\n }\n`
|
|
64
|
+
: "";
|
|
52
65
|
return `widget ${widget.id_hint} {
|
|
53
66
|
# Import metadata: confidence ${widget.confidence || "unknown"}; evidence ${evidenceCount}; inferred pattern ${widget.pattern || widget.inferred_pattern || "search_results"}; inferred region ${widget.region || widget.inferred_region || "results"}.
|
|
54
67
|
# Missing decisions: ${missingDecisions.join("; ")}.
|
|
68
|
+
${inferredEventComments} # Event declarations are draft bindings and require payload shape review before adoption.
|
|
55
69
|
name "${widget.label || widget.id_hint}"
|
|
56
70
|
description "Candidate reusable widget inferred from imported UI evidence. Review props, behavior, events, and reuse before adoption."
|
|
57
71
|
category collection
|
|
58
72
|
props {
|
|
59
73
|
${widget.data_prop || "rows"} array required
|
|
60
74
|
}
|
|
61
|
-
patterns [${widget.pattern || "search_results"}]
|
|
75
|
+
${behaviorBlock} patterns [${widget.pattern || "search_results"}]
|
|
62
76
|
regions [${widget.region || "results"}]
|
|
63
77
|
status proposed
|
|
64
78
|
}
|
|
65
79
|
`;
|
|
66
80
|
}
|
|
67
81
|
|
|
82
|
+
/**
|
|
83
|
+
* @param {any} shape
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
function renderShapeCandidate(shape) {
|
|
87
|
+
const fields = Array.isArray(shape.fields) && shape.fields.length > 0
|
|
88
|
+
? shape.fields
|
|
89
|
+
: [{ name: "id", field_type: "string", required: true }];
|
|
90
|
+
return `shape ${shape.id_hint} {
|
|
91
|
+
# Import metadata: confidence ${shape.confidence || "unknown"}; source ${shape.source_kind || "unknown"}.
|
|
92
|
+
# Missing decisions: ${(shape.missing_decisions || ["confirm event payload fields"]).join("; ")}.
|
|
93
|
+
name "${shape.label || shape.id_hint}"
|
|
94
|
+
description "Candidate event payload shape inferred from imported UI interaction evidence."
|
|
95
|
+
fields {
|
|
96
|
+
${fields.map((/** @type {any} */ field) => {
|
|
97
|
+
const fieldName = typeof field === "string" ? field : field.name;
|
|
98
|
+
const fieldType = typeof field === "string" ? "string" : (field.field_type || field.type || "string");
|
|
99
|
+
const requiredness = typeof field === "string" || field.required ? "required" : "optional";
|
|
100
|
+
return ` ${fieldName} ${fieldType} ${requiredness}`;
|
|
101
|
+
}).join("\n")}
|
|
102
|
+
}
|
|
103
|
+
status proposed
|
|
104
|
+
}
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {any} widget
|
|
110
|
+
* @returns {string}
|
|
111
|
+
*/
|
|
112
|
+
function widgetEventDirectives(widget) {
|
|
113
|
+
return (widget.inferred_events || [])
|
|
114
|
+
.filter((/** @type {any} */ event) => event.name && event.action && (event.target_screen || event.target) && event.payload_shape)
|
|
115
|
+
.map((/** @type {any} */ event) => ` event ${event.name} ${event.action} ${event.target_screen || event.target}`)
|
|
116
|
+
.join("");
|
|
117
|
+
}
|
|
118
|
+
|
|
68
119
|
/**
|
|
69
120
|
* @param {any[]} widgetCandidates
|
|
70
121
|
* @param {Record<string, any>} allCandidates
|
|
@@ -78,7 +129,7 @@ function uiWidgetLinesForCandidates(widgetCandidates, allCandidates) {
|
|
|
78
129
|
const dataBinding = dataSource
|
|
79
130
|
? ` data ${widget.data_prop || "rows"} from ${dataSource}`
|
|
80
131
|
: "";
|
|
81
|
-
return ` screen ${widget.screen_id} region ${widget.region} widget ${widget.id_hint}${dataBinding}`;
|
|
132
|
+
return ` screen ${widget.screen_id} region ${widget.region} widget ${widget.id_hint}${dataBinding}${widgetEventDirectives(widget)}`;
|
|
82
133
|
});
|
|
83
134
|
}
|
|
84
135
|
|
|
@@ -99,6 +150,7 @@ export function draftUiProjectionFiles(context, candidates, allCandidates = {})
|
|
|
99
150
|
/** @type {any[]} */
|
|
100
151
|
const actions = ui.actions || [];
|
|
101
152
|
const widgetCandidates = [...uiWidgetCandidates(ui)].sort((a, b) => a.id_hint.localeCompare(b.id_hint));
|
|
153
|
+
const shapeCandidates = [...(ui.shapes || [])].sort((a, b) => String(a.id_hint || "").localeCompare(String(b.id_hint || "")));
|
|
102
154
|
const shell = actions.find((entry) => entry.kind === "ui_shell")?.shell_kind || "topbar";
|
|
103
155
|
const navigationPatterns = uniqueSorted(actions.filter((entry) => entry.kind === "navigation").map((entry) => entry.navigation_pattern));
|
|
104
156
|
const presentations = uniqueSorted(actions.filter((entry) => entry.kind === "ui_presentation").map((entry) => entry.presentation));
|
|
@@ -308,6 +360,7 @@ ${uiWebLines.length > 0 ? ` web_hints {\n${uiWebLines.join("\n")}\n }\n\n` : "
|
|
|
308
360
|
- Draft UI contract projection: \`candidates/app/ui/drafts/proj-ui-contract.tg\`
|
|
309
361
|
- Draft web surface projection: \`candidates/app/ui/drafts/proj-web-surface.tg\`
|
|
310
362
|
- Draft widget candidates: ${widgetCandidates.length}
|
|
363
|
+
- Draft event payload shape candidates: ${shapeCandidates.length}
|
|
311
364
|
- Imported screens: ${screens.length}
|
|
312
365
|
- Imported routes: ${(ui.routes || []).length}
|
|
313
366
|
- Imported UI actions/presentations: ${actions.length}
|
|
@@ -330,6 +383,9 @@ ${uiWebLines.length > 0 ? ` web_hints {\n${uiWebLines.join("\n")}\n }\n\n` : "
|
|
|
330
383
|
"candidates/app/ui/drafts/proj-web-surface.tg": ensureTrailingNewline(uiWebDraft),
|
|
331
384
|
"candidates/app/ui/drafts/README.md": ensureTrailingNewline(coverage)
|
|
332
385
|
};
|
|
386
|
+
for (const shape of shapeCandidates) {
|
|
387
|
+
files[`candidates/app/ui/drafts/shapes/${widgetCandidateFileName(shape)}`] = ensureTrailingNewline(renderShapeCandidate(shape));
|
|
388
|
+
}
|
|
333
389
|
for (const widget of widgetCandidates) {
|
|
334
390
|
files[`candidates/app/ui/drafts/widgets/${widgetCandidateFileName(widget)}`] = ensureTrailingNewline(renderWidgetCandidate(widget));
|
|
335
391
|
}
|
|
@@ -31,7 +31,7 @@ export const SURFACE_ORDER = new Map([
|
|
|
31
31
|
* @returns {string}
|
|
32
32
|
*/
|
|
33
33
|
export function unsupportedTemplateSymlinkMessage(templateId, relativePath) {
|
|
34
|
-
return `Template '${templateId}' contains unsupported symlink '${relativePath}'. Template packs must copy real files because Topogram records hashes for copied
|
|
34
|
+
return `Template '${templateId}' contains unsupported symlink '${relativePath}'. Template packs must copy real files because Topogram records hashes for copied workspace and implementation/ content; symlinks can point outside the trusted template root. Replace the symlink with a real file or directory before running topogram new or topogram template check.`;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
|
|
5
5
|
import { defaultGeneratorPolicy, writeGeneratorPolicy } from "../generator-policy.js";
|
|
6
6
|
import { writeTemplateTrustRecord } from "../template-trust.js";
|
|
7
|
+
import { DEFAULT_TOPO_FOLDER_NAME } from "../workspace-paths.js";
|
|
7
8
|
import { DEFAULT_TEMPLATE_NAME } from "./constants.js";
|
|
8
9
|
import { writeProjectTemplateMetadata } from "./metadata.js";
|
|
9
10
|
import { assertProjectOutsideEngine, copyTopogramWorkspace, ensureCreatableProjectRoot, writeAgentsGuide, writeExplainScript, writeProjectPackage, writeProjectReadme } from "./project-files.js";
|
|
@@ -52,7 +53,7 @@ export function createNewProject({
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
ensureCreatableProjectRoot(projectRoot);
|
|
55
|
-
copyTopogramWorkspace(template.root, projectRoot);
|
|
56
|
+
const workspaceCopy = copyTopogramWorkspace(template.root, projectRoot);
|
|
56
57
|
const projectConfig = writeProjectTemplateMetadata(projectRoot, template, templateProvenance);
|
|
57
58
|
writeProjectPackage(projectRoot, engineRoot, template);
|
|
58
59
|
writeExplainScript(projectRoot);
|
|
@@ -63,6 +64,12 @@ export function createNewProject({
|
|
|
63
64
|
writeGeneratorPolicy(projectRoot, defaultGeneratorPolicy());
|
|
64
65
|
|
|
65
66
|
const warnings = [];
|
|
67
|
+
if (workspaceCopy.legacyWorkspace) {
|
|
68
|
+
warnings.push(
|
|
69
|
+
`Template '${template.manifest.id}' still ships legacy topogram/ source. Copied it into this project as ${DEFAULT_TOPO_FOLDER_NAME}/. ` +
|
|
70
|
+
"This one-release package-ingress bridge will be removed after first-party packages migrate."
|
|
71
|
+
);
|
|
72
|
+
}
|
|
66
73
|
if (template.manifest.includesExecutableImplementation) {
|
|
67
74
|
writeTemplateTrustRecord(projectRoot, projectConfig);
|
|
68
75
|
warnings.push(
|
|
@@ -76,7 +83,7 @@ export function createNewProject({
|
|
|
76
83
|
projectRoot,
|
|
77
84
|
templateName: template.manifest.id,
|
|
78
85
|
template: projectConfig.template,
|
|
79
|
-
topogramPath: path.join(projectRoot,
|
|
86
|
+
topogramPath: path.join(projectRoot, DEFAULT_TOPO_FOLDER_NAME),
|
|
80
87
|
appPath: path.join(projectRoot, "app"),
|
|
81
88
|
warnings
|
|
82
89
|
};
|
|
@@ -4,6 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
6
|
import { githubRepoSlug } from "../topogram-config.js";
|
|
7
|
+
import { DEFAULT_TOPO_FOLDER_NAME, DEFAULT_WORKSPACE_PATH, resolvePackageWorkspace } from "../workspace-paths.js";
|
|
7
8
|
import { cliDependencyForProject, generatorDependenciesForTemplate, isSameOrInside, packageNameFromPath, writeProjectNpmConfig } from "./package-spec.js";
|
|
8
9
|
|
|
9
10
|
/** @typedef {import("./types.js").CreateNewProjectOptions} CreateNewProjectOptions */
|
|
@@ -54,16 +55,17 @@ export function ensureCreatableProjectRoot(projectRoot) {
|
|
|
54
55
|
/**
|
|
55
56
|
* @param {string} templateRoot
|
|
56
57
|
* @param {string} projectRoot
|
|
57
|
-
* @returns {
|
|
58
|
+
* @returns {{ legacyWorkspace: boolean }}
|
|
58
59
|
*/
|
|
59
60
|
export function copyTopogramWorkspace(templateRoot, projectRoot) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
const templateWorkspace = resolvePackageWorkspace(templateRoot);
|
|
62
|
+
const topoRoot = path.join(projectRoot, DEFAULT_TOPO_FOLDER_NAME);
|
|
63
|
+
fs.cpSync(templateWorkspace.root, topoRoot, { recursive: true });
|
|
64
|
+
|
|
65
|
+
const projectConfigPath = path.join(templateRoot, "topogram.project.json");
|
|
66
|
+
const projectConfig = JSON.parse(fs.readFileSync(projectConfigPath, "utf8"));
|
|
67
|
+
projectConfig.workspace = DEFAULT_WORKSPACE_PATH;
|
|
68
|
+
fs.writeFileSync(path.join(projectRoot, "topogram.project.json"), `${JSON.stringify(projectConfig, null, 2)}\n`, "utf8");
|
|
67
69
|
const implementationRoot = path.join(templateRoot, "implementation");
|
|
68
70
|
if (fs.existsSync(implementationRoot)) {
|
|
69
71
|
fs.cpSync(
|
|
@@ -72,6 +74,7 @@ export function copyTopogramWorkspace(templateRoot, projectRoot) {
|
|
|
72
74
|
{ recursive: true }
|
|
73
75
|
);
|
|
74
76
|
}
|
|
77
|
+
return { legacyWorkspace: templateWorkspace.legacy };
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
/**
|
|
@@ -148,7 +151,7 @@ export function writeExplainScript(projectRoot) {
|
|
|
148
151
|
Topogram app workflow
|
|
149
152
|
|
|
150
153
|
1. Edit:
|
|
151
|
-
|
|
154
|
+
topo/
|
|
152
155
|
topogram.project.json
|
|
153
156
|
|
|
154
157
|
2. Start with project guidance:
|
|
@@ -179,8 +182,8 @@ Or run self-contained local runtime verification:
|
|
|
179
182
|
Useful inspection:
|
|
180
183
|
npm run agent:brief
|
|
181
184
|
npm run check:json
|
|
182
|
-
topogram emit ui-widget-contract ./
|
|
183
|
-
topogram emit widget-conformance-report ./
|
|
185
|
+
topogram emit ui-widget-contract ./topo --json
|
|
186
|
+
topogram emit widget-conformance-report ./topo --json
|
|
184
187
|
npm run doctor
|
|
185
188
|
npm run source:status
|
|
186
189
|
npm run source:status:remote
|
|
@@ -257,7 +260,7 @@ ${provenanceLines.join("\n")}
|
|
|
257
260
|
${workflowCommands.join("\n")}
|
|
258
261
|
\`\`\`
|
|
259
262
|
|
|
260
|
-
Edit
|
|
263
|
+
Edit the workspace folder \`${DEFAULT_TOPO_FOLDER_NAME}/\` and \`topogram.project.json\`, then regenerate with \`npm run generate\`.
|
|
261
264
|
Generated app code is written to \`app/\`.
|
|
262
265
|
Use \`topogram emit <target>\` to inspect contracts, reports, snapshots, and other artifacts without regenerating the app.
|
|
263
266
|
Agents should start with \`AGENTS.md\` and \`npm run agent:brief\`. The direct \`topogram agent brief --json\` command is the canonical machine-readable first-run guidance.
|
|
@@ -315,7 +318,7 @@ npm run query:show -- widget-behavior
|
|
|
315
318
|
|
|
316
319
|
## Edit Rules
|
|
317
320
|
|
|
318
|
-
- Edit \`
|
|
321
|
+
- Edit \`topo/**\` and \`topogram.project.json\` first.
|
|
319
322
|
- Review policy files before editing \`topogram.template-policy.json\` or \`topogram.generator-policy.json\`.
|
|
320
323
|
- Do not make lasting edits under generated-owned \`app/**\`; use \`npm run generate\` to replace generated output.
|
|
321
324
|
- If an output is changed to maintained ownership, agents may edit that app code directly after reading focused query packets.
|
|
@@ -6,6 +6,7 @@ import os from "node:os";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
|
|
8
8
|
import { assertSafeNpmSpec, localNpmrcEnv } from "../npm-safety.js";
|
|
9
|
+
import { DEFAULT_TOPO_FOLDER_NAME, LEGACY_TOPOGRAM_FOLDER_NAME, resolvePackageWorkspace } from "../workspace-paths.js";
|
|
9
10
|
import { GENERATOR_LABELS, SURFACE_ORDER, TEMPLATE_MANIFEST, unsupportedTemplateSymlinkMessage } from "./constants.js";
|
|
10
11
|
import { isLocalTemplateSpec, packageNameFromSpec } from "./package-spec.js";
|
|
11
12
|
|
|
@@ -106,21 +107,22 @@ function assertTemplateTreeHasNoSymlinks(root, currentDir, label, templateId) {
|
|
|
106
107
|
*/
|
|
107
108
|
export function validateTemplateRoot(templateRoot) {
|
|
108
109
|
const manifest = readTemplateManifest(templateRoot);
|
|
109
|
-
const
|
|
110
|
+
const workspace = resolvePackageWorkspace(templateRoot);
|
|
111
|
+
const topogramRoot = workspace.root;
|
|
110
112
|
const projectConfigPath = path.join(templateRoot, "topogram.project.json");
|
|
111
113
|
if (fs.existsSync(topogramRoot) && fs.lstatSync(topogramRoot).isSymbolicLink()) {
|
|
112
|
-
throw new Error(unsupportedTemplateSymlinkMessage(manifest.id,
|
|
114
|
+
throw new Error(unsupportedTemplateSymlinkMessage(manifest.id, workspace.legacy ? LEGACY_TOPOGRAM_FOLDER_NAME : DEFAULT_TOPO_FOLDER_NAME));
|
|
113
115
|
}
|
|
114
116
|
if (fs.existsSync(projectConfigPath) && fs.lstatSync(projectConfigPath).isSymbolicLink()) {
|
|
115
117
|
throw new Error(unsupportedTemplateSymlinkMessage(manifest.id, "topogram.project.json"));
|
|
116
118
|
}
|
|
117
119
|
if (!fs.existsSync(topogramRoot) || !fs.statSync(topogramRoot).isDirectory()) {
|
|
118
|
-
throw new Error(`Template '${manifest.id}' is missing
|
|
120
|
+
throw new Error(`Template '${manifest.id}' is missing ${DEFAULT_TOPO_FOLDER_NAME}/.`);
|
|
119
121
|
}
|
|
120
122
|
if (!fs.existsSync(projectConfigPath) || !fs.statSync(projectConfigPath).isFile()) {
|
|
121
123
|
throw new Error(`Template '${manifest.id}' is missing topogram.project.json.`);
|
|
122
124
|
}
|
|
123
|
-
assertTemplateTreeHasNoSymlinks(templateRoot, topogramRoot,
|
|
125
|
+
assertTemplateTreeHasNoSymlinks(templateRoot, topogramRoot, workspace.legacy ? LEGACY_TOPOGRAM_FOLDER_NAME : DEFAULT_TOPO_FOLDER_NAME, manifest.id);
|
|
124
126
|
if (manifest.includesExecutableImplementation) {
|
|
125
127
|
const implementationRoot = path.join(templateRoot, "implementation");
|
|
126
128
|
if (fs.existsSync(implementationRoot) && fs.lstatSync(implementationRoot).isSymbolicLink()) {
|
|
@@ -7,6 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { MAX_TEXT_DIFF_BYTES, TEMPLATE_FILES_MANIFEST } from "./constants.js";
|
|
8
8
|
import { stableJsonStringify } from "./json.js";
|
|
9
9
|
import { candidateProjectTemplateMetadata } from "./metadata.js";
|
|
10
|
+
import { DEFAULT_TOPO_FOLDER_NAME, resolvePackageWorkspace } from "../workspace-paths.js";
|
|
10
11
|
|
|
11
12
|
/** @typedef {import("./types.js").CreateNewProjectOptions} CreateNewProjectOptions */
|
|
12
13
|
/** @typedef {import("./types.js").TemplateUpdatePlanOptions} TemplateUpdatePlanOptions */
|
|
@@ -235,6 +236,24 @@ export function fileHash(file) {
|
|
|
235
236
|
};
|
|
236
237
|
}
|
|
237
238
|
|
|
239
|
+
/**
|
|
240
|
+
* @param {string} relativePath
|
|
241
|
+
* @param {{ absolutePath: string|null, content: string|null }} file
|
|
242
|
+
* @returns {{ sha256: string, size: number }}
|
|
243
|
+
*/
|
|
244
|
+
function templateOwnedFileHash(relativePath, file) {
|
|
245
|
+
if (relativePath !== "topogram.project.json" || file.content !== null) {
|
|
246
|
+
return fileHash(file);
|
|
247
|
+
}
|
|
248
|
+
if (!file.absolutePath) {
|
|
249
|
+
return fileHash(file);
|
|
250
|
+
}
|
|
251
|
+
return fileHash({
|
|
252
|
+
absolutePath: null,
|
|
253
|
+
content: `${stableJsonStringify(JSON.parse(fs.readFileSync(file.absolutePath, "utf8")))}\n`
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
238
257
|
/**
|
|
239
258
|
* @param {ResolvedTemplate} template
|
|
240
259
|
* @param {Record<string, any>|null} [currentProjectConfig]
|
|
@@ -242,14 +261,24 @@ export function fileHash(file) {
|
|
|
242
261
|
*/
|
|
243
262
|
export function candidateTemplateFiles(template, currentProjectConfig = null) {
|
|
244
263
|
const files = new Map();
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
264
|
+
const templateWorkspace = resolvePackageWorkspace(template.root);
|
|
265
|
+
/** @type {string[]} */
|
|
266
|
+
const workspaceFiles = [];
|
|
267
|
+
collectFiles(template.root, templateWorkspace.root, workspaceFiles);
|
|
268
|
+
for (const sourceRelativePath of workspaceFiles) {
|
|
269
|
+
const workspaceRelative = path.relative(templateWorkspace.root, path.join(template.root, sourceRelativePath)).replace(/\\/g, "/");
|
|
270
|
+
const targetRelativePath = path.posix.join(DEFAULT_TOPO_FOLDER_NAME, workspaceRelative);
|
|
271
|
+
files.set(targetRelativePath, {
|
|
272
|
+
path: targetRelativePath,
|
|
273
|
+
content: null,
|
|
274
|
+
absolutePath: path.join(template.root, sourceRelativePath)
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
const implementationRoot = path.join(template.root, "implementation");
|
|
278
|
+
if (fs.existsSync(implementationRoot)) {
|
|
250
279
|
/** @type {string[]} */
|
|
251
280
|
const relativeFiles = [];
|
|
252
|
-
collectFiles(template.root,
|
|
281
|
+
collectFiles(template.root, implementationRoot, relativeFiles);
|
|
253
282
|
for (const relativePath of relativeFiles) {
|
|
254
283
|
files.set(relativePath, {
|
|
255
284
|
path: relativePath,
|
|
@@ -259,6 +288,7 @@ export function candidateTemplateFiles(template, currentProjectConfig = null) {
|
|
|
259
288
|
}
|
|
260
289
|
}
|
|
261
290
|
const candidateProjectConfig = JSON.parse(fs.readFileSync(path.join(template.root, "topogram.project.json"), "utf8"));
|
|
291
|
+
candidateProjectConfig.workspace = `./${DEFAULT_TOPO_FOLDER_NAME}`;
|
|
262
292
|
candidateProjectConfig.template = candidateProjectTemplateMetadata(template, currentProjectConfig);
|
|
263
293
|
files.set("topogram.project.json", {
|
|
264
294
|
path: "topogram.project.json",
|
|
@@ -276,7 +306,7 @@ export function candidateTemplateFiles(template, currentProjectConfig = null) {
|
|
|
276
306
|
*/
|
|
277
307
|
export function currentTemplateOwnedFiles(projectRoot, includeImplementation, projectConfig) {
|
|
278
308
|
const files = new Map();
|
|
279
|
-
for (const rootName of includeImplementation ? [
|
|
309
|
+
for (const rootName of includeImplementation ? [DEFAULT_TOPO_FOLDER_NAME, "implementation"] : [DEFAULT_TOPO_FOLDER_NAME]) {
|
|
280
310
|
const root = path.join(projectRoot, rootName);
|
|
281
311
|
if (!fs.existsSync(root)) {
|
|
282
312
|
continue;
|
|
@@ -323,7 +353,7 @@ export function includesTemplateImplementation(projectConfig) {
|
|
|
323
353
|
export function currentTemplateOwnedFileHashes(projectRoot, projectConfig) {
|
|
324
354
|
const files = currentTemplateOwnedFiles(projectRoot, includesTemplateImplementation(projectConfig), projectConfig);
|
|
325
355
|
return new Map([...files.entries()].map(([relativePath, file]) => {
|
|
326
|
-
const hash =
|
|
356
|
+
const hash = templateOwnedFileHash(relativePath, file);
|
|
327
357
|
return [relativePath, { path: relativePath, ...hash }];
|
|
328
358
|
}));
|
|
329
359
|
}
|
|
@@ -343,7 +343,7 @@ export function applyTemplateUpdateFileAction(options) {
|
|
|
343
343
|
code: "template_file_not_current",
|
|
344
344
|
message: `Cannot accept current file '${relativePath}' because it is not a current template-owned file.`,
|
|
345
345
|
path: path.join(options.projectRoot, relativePath),
|
|
346
|
-
suggestedFix: "Pass a file under
|
|
346
|
+
suggestedFix: "Pass a file under topo/, topogram.project.json, or trusted implementation/.",
|
|
347
347
|
step: "accept-current"
|
|
348
348
|
}));
|
|
349
349
|
return templateUpdateFileActionResult(diagnostics, null, options.action, relativePath, applied, accepted, deleted, conflicts, current);
|