@topogram/cli 0.3.78 → 0.3.79
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/CHANGELOG.md +20 -0
- package/package.json +2 -2
- package/src/agent-brief.js +29 -23
- package/src/agent-ops/query-builders/change-risk/{import-plan.js → extract-plan.js} +1 -1
- package/src/agent-ops/query-builders/change-risk/review-packets.js +5 -5
- package/src/agent-ops/query-builders/change-risk.js +1 -1
- 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-context-shared.js +4 -4
- package/src/catalog/provenance.js +1 -1
- package/src/cli/catalog-alias.d.ts +2 -0
- package/src/cli/catalog-alias.js +2 -2
- package/src/cli/command-parsers/core.js +9 -5
- package/src/cli/command-parsers/import.js +11 -17
- package/src/cli/command-parsers/project.js +0 -3
- package/src/cli/commands/catalog/copy.js +3 -3
- package/src/cli/commands/catalog/help.js +1 -2
- package/src/cli/commands/catalog/list.js +7 -4
- package/src/cli/commands/catalog/show.js +4 -4
- package/src/cli/commands/copy.js +356 -0
- package/src/cli/commands/doctor.js +1 -1
- package/src/cli/commands/import/adopt.js +9 -9
- package/src/cli/commands/import/check.js +15 -15
- package/src/cli/commands/import/diff.js +6 -6
- package/src/cli/commands/import/help.js +43 -34
- package/src/cli/commands/import/paths.js +3 -3
- package/src/cli/commands/import/plan.js +8 -8
- package/src/cli/commands/import/refresh.js +25 -24
- package/src/cli/commands/import/status-history.js +4 -4
- package/src/cli/commands/import/workspace.js +16 -16
- package/src/cli/commands/import-runner.js +6 -5
- package/src/cli/commands/import.js +4 -1
- package/src/cli/commands/init.js +67 -0
- package/src/cli/commands/query/{import-adopt.js → extract-adopt.js} +2 -2
- package/src/cli/commands/query/runner/change.js +2 -2
- package/src/cli/commands/query/runner/{import-adopt.js → extract-adopt.js} +9 -9
- package/src/cli/commands/query/runner/index.js +1 -1
- package/src/cli/commands/query/runner/workflow.js +7 -7
- package/src/cli/commands/query/workspace.js +4 -4
- package/src/cli/commands/release-status.js +2 -2
- package/src/cli/commands/source.js +2 -2
- package/src/cli/commands/template/check.js +2 -2
- package/src/cli/commands/template/list-show.js +4 -4
- package/src/cli/dispatcher.js +18 -3
- package/src/cli/help-dispatch.js +22 -8
- package/src/cli/help.js +68 -52
- package/src/cli/migration-guidance.js +9 -0
- package/src/generator/context/bundle.js +14 -7
- package/src/generator/context/diff.js +8 -1
- package/src/generator/context/digest.js +10 -1
- package/src/generator/context/shared/domain-sdlc.js +5 -1
- package/src/generator/context/shared/relationships.js +20 -5
- package/src/generator/context/shared/summaries.js +26 -0
- package/src/generator/context/shared.d.ts +1 -0
- package/src/generator/context/shared.js +1 -0
- package/src/generator/context/slice/core.js +9 -5
- package/src/generator/context/slice/sdlc.js +31 -2
- package/src/generator/context/task-mode.js +3 -3
- package/src/import/core/runner/reports.js +4 -4
- package/src/import/provenance.js +16 -16
- package/src/init-project.js +215 -0
- package/src/new-project/constants.js +1 -1
- package/src/new-project/create.js +2 -2
- package/src/new-project/project-files.js +7 -7
- package/src/reconcile/journeys.js +8 -3
- package/src/record-blocks.js +125 -0
- package/src/resolver/index.js +3 -0
- package/src/resolver/journeys.js +74 -0
- package/src/resolver/normalize.js +25 -0
- package/src/sdlc/adopt.js +1 -1
- package/src/validator/common.js +34 -1
- package/src/validator/index.js +4 -0
- package/src/validator/kinds.d.ts +2 -0
- package/src/validator/kinds.js +34 -1
- package/src/validator/per-kind/journey.js +233 -0
- package/src/workflows/docs-generate.js +4 -1
- package/src/workflows/reconcile/bundle-core/index.js +4 -2
- package/src/workflows/reconcile/canonical-surface.js +4 -1
- package/src/cli/commands/new.js +0 -94
|
@@ -97,6 +97,30 @@ export function summarizeJourneyDoc(doc) {
|
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* @param {import("./types.d.ts").ContextStatement} journey
|
|
102
|
+
* @returns {any}
|
|
103
|
+
*/
|
|
104
|
+
export function summarizeJourney(journey) {
|
|
105
|
+
return {
|
|
106
|
+
id: journey.id,
|
|
107
|
+
kind: journey.kind,
|
|
108
|
+
name: journey.name || journey.id,
|
|
109
|
+
description: journey.description || null,
|
|
110
|
+
status: journey.status || null,
|
|
111
|
+
goal: journey.goal || null,
|
|
112
|
+
actors: stableSortedStrings(journey.actors || []),
|
|
113
|
+
relatedCapabilities: stableSortedStrings(journey.relatedCapabilities || []),
|
|
114
|
+
relatedWorkflows: stableSortedStrings(journey.relatedWorkflows || []),
|
|
115
|
+
relatedProjections: stableSortedStrings(journey.relatedProjections || []),
|
|
116
|
+
relatedWidgets: stableSortedStrings(journey.relatedWidgets || []),
|
|
117
|
+
stepCount: (journey.steps || []).length,
|
|
118
|
+
alternateCount: (journey.alternates || []).length,
|
|
119
|
+
reviewBoundary: reviewBoundaryForJourneyDocPolicy(journey),
|
|
120
|
+
ownership_boundary: defaultOwnershipBoundary()
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
100
124
|
/**
|
|
101
125
|
* @param {import("./types.d.ts").ContextStatement} statement
|
|
102
126
|
* @returns {any}
|
|
@@ -163,6 +187,8 @@ export function summarizeStatement(statement) {
|
|
|
163
187
|
};
|
|
164
188
|
case "widget":
|
|
165
189
|
return summarizeComponent(statement);
|
|
190
|
+
case "journey":
|
|
191
|
+
return summarizeJourney(statement);
|
|
166
192
|
case "shape":
|
|
167
193
|
return {
|
|
168
194
|
id: statement.id,
|
|
@@ -34,6 +34,7 @@ export function summarizeById(...args: any[]): any;
|
|
|
34
34
|
export function summarizeDocsByIds(...args: any[]): any;
|
|
35
35
|
export function summarizeDocument(...args: any[]): any;
|
|
36
36
|
export function summarizeDomain(...args: any[]): any;
|
|
37
|
+
export function summarizeJourneyLikeByIds(...args: any[]): any;
|
|
37
38
|
export function summarizePitch(...args: any[]): any;
|
|
38
39
|
export function summarizePlan(...args: any[]): any;
|
|
39
40
|
export function summarizeProjection(...args: any[]): any;
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
relatedWorkflowDocsForCapability,
|
|
21
21
|
summarizeById,
|
|
22
22
|
summarizeDocsByIds,
|
|
23
|
+
summarizeJourneyLikeByIds,
|
|
23
24
|
summarizeProjection,
|
|
24
25
|
summarizeStatementsByIds,
|
|
25
26
|
verificationIdsForTarget,
|
|
@@ -87,7 +88,7 @@ function capabilitySlice(graph, capabilityId) {
|
|
|
87
88
|
entities: summarizeStatementsByIds(graph, entities),
|
|
88
89
|
rules: summarizeStatementsByIds(graph, rules),
|
|
89
90
|
workflows: summarizeDocsByIds(graph, workflows),
|
|
90
|
-
journeys:
|
|
91
|
+
journeys: summarizeJourneyLikeByIds(graph, journeys),
|
|
91
92
|
projections: summarizeStatementsByIds(graph, projections)
|
|
92
93
|
},
|
|
93
94
|
verification: summarizeStatementsByIds(graph, verifications),
|
|
@@ -118,9 +119,12 @@ function workflowSlice(graph, workflowId) {
|
|
|
118
119
|
];
|
|
119
120
|
}))].sort();
|
|
120
121
|
const rules = [...new Set(capabilities.flatMap(/** @param {string} capabilityId */ (capabilityId) => relatedRulesForTarget(graph, capabilityId)))].sort();
|
|
121
|
-
const journeys =
|
|
122
|
-
|
|
123
|
-
.
|
|
122
|
+
const journeys = [
|
|
123
|
+
...(graph.byKind?.journey || []),
|
|
124
|
+
...(graph.docs || []).filter(/** @param {any} doc */ (doc) => doc.kind === "journey")
|
|
125
|
+
]
|
|
126
|
+
.filter(/** @param {any} journey */ (journey) => (journey.relatedWorkflows || []).includes(workflowId))
|
|
127
|
+
.map(/** @param {any} journey */ (journey) => journey.id)
|
|
124
128
|
.sort();
|
|
125
129
|
const verifications = verificationIdsForTarget(graph, [...capabilities, ...entities, workflowId]);
|
|
126
130
|
|
|
@@ -143,7 +147,7 @@ function workflowSlice(graph, workflowId) {
|
|
|
143
147
|
capabilities: summarizeStatementsByIds(graph, capabilities),
|
|
144
148
|
entities: summarizeStatementsByIds(graph, entities),
|
|
145
149
|
rules: summarizeStatementsByIds(graph, rules),
|
|
146
|
-
journeys:
|
|
150
|
+
journeys: summarizeJourneyLikeByIds(graph, journeys)
|
|
147
151
|
},
|
|
148
152
|
verification: summarizeStatementsByIds(graph, verifications),
|
|
149
153
|
verification_targets: recommendedVerificationTargets(graph, [...capabilities, ...entities, workflowId], {
|
|
@@ -49,9 +49,16 @@ import {
|
|
|
49
49
|
export function journeySlice(graph, journeyId) {
|
|
50
50
|
const journey = getJourneyDoc(graph, journeyId);
|
|
51
51
|
const capabilities = [...(journey.relatedCapabilities || [])].sort();
|
|
52
|
+
const entities = [...(journey.relatedEntities || [])].sort();
|
|
53
|
+
const rules = [...(journey.relatedRules || [])].sort();
|
|
52
54
|
const workflows = [...(journey.relatedWorkflows || [])].sort();
|
|
53
55
|
const projections = [...(journey.relatedProjections || [])].sort();
|
|
54
|
-
const
|
|
56
|
+
const widgets = [...(journey.relatedWidgets || [])].sort();
|
|
57
|
+
const declaredVerifications = [...(journey.relatedVerifications || [])].sort();
|
|
58
|
+
const verifications = [...new Set([
|
|
59
|
+
...declaredVerifications,
|
|
60
|
+
...verificationIdsForTarget(graph, [...capabilities, ...workflows, ...projections, ...widgets, journeyId])
|
|
61
|
+
])].sort();
|
|
55
62
|
|
|
56
63
|
return {
|
|
57
64
|
type: "context_slice",
|
|
@@ -63,14 +70,36 @@ export function journeySlice(graph, journeyId) {
|
|
|
63
70
|
summary: summarizeById(graph, journeyId),
|
|
64
71
|
depends_on: {
|
|
65
72
|
capabilities,
|
|
73
|
+
entities,
|
|
74
|
+
rules,
|
|
66
75
|
workflows,
|
|
67
76
|
projections,
|
|
77
|
+
widgets,
|
|
68
78
|
verifications
|
|
69
79
|
},
|
|
80
|
+
steps: (journey.steps || []).map(/** @param {any} step */ (step) => ({
|
|
81
|
+
id: step.id,
|
|
82
|
+
intent: step.intent,
|
|
83
|
+
commands: [...(step.commands || [])],
|
|
84
|
+
expects: [...(step.expects || [])],
|
|
85
|
+
after: [...(step.after || [])],
|
|
86
|
+
notes: step.notes || null
|
|
87
|
+
})),
|
|
88
|
+
alternates: (journey.alternates || []).map(/** @param {any} alternate */ (alternate) => ({
|
|
89
|
+
id: alternate.id,
|
|
90
|
+
from: alternate.from,
|
|
91
|
+
condition: alternate.condition,
|
|
92
|
+
commands: [...(alternate.commands || [])],
|
|
93
|
+
expects: [...(alternate.expects || [])],
|
|
94
|
+
notes: alternate.notes || null
|
|
95
|
+
})),
|
|
70
96
|
related: {
|
|
71
97
|
capabilities: summarizeStatementsByIds(graph, capabilities),
|
|
98
|
+
entities: summarizeStatementsByIds(graph, entities),
|
|
99
|
+
rules: summarizeStatementsByIds(graph, rules),
|
|
72
100
|
workflows: summarizeDocsByIds(graph, workflows),
|
|
73
|
-
projections: summarizeStatementsByIds(graph, projections)
|
|
101
|
+
projections: summarizeStatementsByIds(graph, projections),
|
|
102
|
+
widgets: summarizeStatementsByIds(graph, widgets)
|
|
74
103
|
},
|
|
75
104
|
verification: summarizeStatementsByIds(graph, verifications),
|
|
76
105
|
verification_targets: recommendedVerificationTargets(graph, [...capabilities, ...workflows, ...projections, journeyId], {
|
|
@@ -325,7 +325,7 @@ function importAdoptMode(graph, options = {}) {
|
|
|
325
325
|
return {
|
|
326
326
|
type: "context_task_mode",
|
|
327
327
|
version: 1,
|
|
328
|
-
mode: "
|
|
328
|
+
mode: "extract-adopt",
|
|
329
329
|
summary: {
|
|
330
330
|
focus: "Proposal review and adoption planning",
|
|
331
331
|
preferred_start: "adoption-plan.agent.json",
|
|
@@ -450,7 +450,7 @@ function verificationMode(graph, options = {}) {
|
|
|
450
450
|
export function generateContextTaskMode(graph, options = {}) {
|
|
451
451
|
const mode = String(options.modeId || "").trim();
|
|
452
452
|
if (!mode) {
|
|
453
|
-
throw new Error("context-task-mode requires --mode <modeling|maintained-app-edit|
|
|
453
|
+
throw new Error("context-task-mode requires --mode <modeling|maintained-app-edit|extract-adopt|diff-review|verification>");
|
|
454
454
|
}
|
|
455
455
|
|
|
456
456
|
if (mode === "modeling") {
|
|
@@ -459,7 +459,7 @@ export function generateContextTaskMode(graph, options = {}) {
|
|
|
459
459
|
if (mode === "maintained-app-edit") {
|
|
460
460
|
return maintainedAppEditMode(graph, options);
|
|
461
461
|
}
|
|
462
|
-
if (mode === "
|
|
462
|
+
if (mode === "extract-adopt") {
|
|
463
463
|
return importAdoptMode(graph, options);
|
|
464
464
|
}
|
|
465
465
|
if (mode === "diff-review") {
|
|
@@ -17,12 +17,12 @@ export function reportMarkdown(track, candidates) {
|
|
|
17
17
|
` - manual next: ${(seam.manual_next_steps || []).slice(0, 2).join(" ")}`
|
|
18
18
|
]);
|
|
19
19
|
return ensureTrailingNewline(
|
|
20
|
-
`# DB
|
|
20
|
+
`# DB Extract Report\n\n- Entities: ${candidates.entities.length}\n- Enums: ${candidates.enums.length}\n- Relations: ${candidates.relations.length}\n- Indexes: ${candidates.indexes.length}\n- Maintained DB migration seams: ${(candidates.maintained_seams || []).length}\n\n## Maintained DB Migration Seam Candidates\n\n${seamLines.length ? seamLines.join("\n") : "- none"}\n`
|
|
21
21
|
);
|
|
22
22
|
}
|
|
23
23
|
if (track === "api") {
|
|
24
24
|
return ensureTrailingNewline(
|
|
25
|
-
`# API
|
|
25
|
+
`# API Extract Report\n\n- Capabilities: ${candidates.capabilities.length}\n- Routes: ${candidates.routes.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n`
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
28
|
if (track === "ui") {
|
|
@@ -36,7 +36,7 @@ export function reportMarkdown(track, candidates) {
|
|
|
36
36
|
`- \`${flow.id_hint}\` type \`${flow.flow_type}\` confidence ${flow.confidence || "unknown"} routes ${(flow.route_paths || []).map((/** @type {string} */ route) => `\`${route}\``).join(", ") || "_none_"} missing decisions ${(flow.missing_decisions || []).length}`
|
|
37
37
|
);
|
|
38
38
|
return ensureTrailingNewline(
|
|
39
|
-
`# UI
|
|
39
|
+
`# UI Extract Report\n\n- Screens: ${candidates.screens.length}\n- Routes: ${candidates.routes.length}\n- Actions: ${candidates.actions.length}\n- Flow candidates: ${flows.length}\n- Widgets: ${widgets.length}\n- Event payload shapes: ${shapes.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n\n## Flow Candidates\n\n${flowLines.length ? flowLines.join("\n") : "- none"}\n\n## Widget Candidates\n\n${widgetLines.length ? widgetLines.join("\n") : "- none"}\n\n## Next Validation\n\n- Review flow candidates in \`topo/candidates/app/ui/candidates.json\` before adding shared UI contract behavior.\n- Review candidates under \`topo/candidates/app/ui/drafts/widgets/**\`.\n- Run \`topogram extract plan <path>\` before adoption.\n- After adoption, run \`topogram check <path>\`, \`topogram widget check <path>\`, and \`topogram widget behavior <path>\`.\n`
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
42
|
if (track === "cli") {
|
|
@@ -44,7 +44,7 @@ export function reportMarkdown(track, candidates) {
|
|
|
44
44
|
`- \`${command.command_id || command.id_hint}\` usage \`${command.usage || "unknown"}\` effects ${(command.effects || []).map((/** @type {any} */ effect) => `\`${effect}\``).join(", ") || "_none_"}`
|
|
45
45
|
);
|
|
46
46
|
return ensureTrailingNewline(
|
|
47
|
-
`# CLI
|
|
47
|
+
`# CLI Extract Report\n\n- Commands: ${candidates.commands.length}\n- Capabilities: ${candidates.capabilities.length}\n- CLI surfaces: ${candidates.surfaces.length}\n\n## Command Candidates\n\n${commandLines.length ? commandLines.join("\n") : "- none"}\n`
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
50
|
if (track === "verification") {
|
package/src/import/provenance.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
|
|
5
5
|
import { listFilesRecursive, relativeTo } from "./core/shared.js";
|
|
6
6
|
|
|
7
|
-
export const TOPOGRAM_IMPORT_FILE = ".topogram-
|
|
7
|
+
export const TOPOGRAM_IMPORT_FILE = ".topogram-extract.json";
|
|
8
8
|
|
|
9
9
|
function fileHash(filePath) {
|
|
10
10
|
const bytes = fs.readFileSync(filePath);
|
|
@@ -42,22 +42,22 @@ export function writeTopogramImportRecord(projectRoot, input) {
|
|
|
42
42
|
const timestamp = input.timestamp || new Date().toISOString();
|
|
43
43
|
const record = {
|
|
44
44
|
version: "0.1",
|
|
45
|
-
kind: "brownfield-
|
|
46
|
-
|
|
45
|
+
kind: "brownfield-extract",
|
|
46
|
+
extractedAt: input.importedAt || timestamp,
|
|
47
47
|
...(input.refreshedAt ? { refreshedAt: input.refreshedAt } : {}),
|
|
48
48
|
source: {
|
|
49
49
|
path: path.resolve(input.sourceRoot),
|
|
50
50
|
hashAlgorithm: "sha256",
|
|
51
51
|
ignoredRoots: (input.ignoredRoots || []).map((item) => path.resolve(item))
|
|
52
52
|
},
|
|
53
|
-
|
|
53
|
+
extract: {
|
|
54
54
|
tracks: input.tracks || [],
|
|
55
55
|
findingsCount: input.findingsCount || 0,
|
|
56
56
|
candidateCounts: input.candidateCounts || {}
|
|
57
57
|
},
|
|
58
58
|
ownership: {
|
|
59
|
-
|
|
60
|
-
note: "Topogram artifacts created by
|
|
59
|
+
extractedArtifacts: "project-owned",
|
|
60
|
+
note: "Topogram artifacts created by extraction are editable after extraction. Source hashes record the brownfield app evidence trusted at extraction time."
|
|
61
61
|
},
|
|
62
62
|
...(input.refresh ? { refresh: input.refresh } : {}),
|
|
63
63
|
files: input.files || []
|
|
@@ -79,11 +79,11 @@ export function buildTopogramImportStatus(projectRoot) {
|
|
|
79
79
|
source: null,
|
|
80
80
|
content: { changed: [], added: [], removed: [] },
|
|
81
81
|
diagnostics: [{
|
|
82
|
-
code: "
|
|
82
|
+
code: "topogram_extract_missing",
|
|
83
83
|
severity: "error",
|
|
84
|
-
message: `${TOPOGRAM_IMPORT_FILE} was not found. This workspace does not have brownfield
|
|
84
|
+
message: `${TOPOGRAM_IMPORT_FILE} was not found. This workspace does not have brownfield extraction provenance.`,
|
|
85
85
|
path: importPath,
|
|
86
|
-
suggestedFix: "Run `topogram
|
|
86
|
+
suggestedFix: "Run `topogram extract <app-path> --out <target>` to create an extracted Topogram workspace."
|
|
87
87
|
}],
|
|
88
88
|
errors: [`${TOPOGRAM_IMPORT_FILE} was not found.`]
|
|
89
89
|
};
|
|
@@ -92,7 +92,7 @@ export function buildTopogramImportStatus(projectRoot) {
|
|
|
92
92
|
const source = JSON.parse(fs.readFileSync(importPath, "utf8"));
|
|
93
93
|
const sourceRoot = path.resolve(source.source?.path || "");
|
|
94
94
|
if (!sourceRoot || !fs.existsSync(sourceRoot) || !fs.statSync(sourceRoot).isDirectory()) {
|
|
95
|
-
const message = `
|
|
95
|
+
const message = `Extracted source path was not found: ${source.source?.path || "unknown"}`;
|
|
96
96
|
return {
|
|
97
97
|
ok: false,
|
|
98
98
|
exists: true,
|
|
@@ -101,11 +101,11 @@ export function buildTopogramImportStatus(projectRoot) {
|
|
|
101
101
|
source,
|
|
102
102
|
content: { changed: [], added: [], removed: [] },
|
|
103
103
|
diagnostics: [{
|
|
104
|
-
|
|
104
|
+
code: "topogram_extract_source_missing",
|
|
105
105
|
severity: "error",
|
|
106
106
|
message,
|
|
107
107
|
path: source.source?.path || null,
|
|
108
|
-
suggestedFix: "Restore the
|
|
108
|
+
suggestedFix: "Restore the extracted source path or rerun extract from the current brownfield app location."
|
|
109
109
|
}],
|
|
110
110
|
errors: [message]
|
|
111
111
|
};
|
|
@@ -147,12 +147,12 @@ export function buildTopogramImportStatus(projectRoot) {
|
|
|
147
147
|
source,
|
|
148
148
|
content,
|
|
149
149
|
diagnostics: clean ? [] : [{
|
|
150
|
-
code: "
|
|
150
|
+
code: "topogram_extract_source_changed",
|
|
151
151
|
severity: "error",
|
|
152
|
-
message: "
|
|
152
|
+
message: "Extracted source files changed since they were trusted for this extraction.",
|
|
153
153
|
path: sourceRoot,
|
|
154
|
-
suggestedFix: "Review the source changes. If they should drive Topogram changes, rerun
|
|
154
|
+
suggestedFix: "Review the source changes. If they should drive Topogram changes, rerun extract or update the Topogram artifacts manually."
|
|
155
155
|
}],
|
|
156
|
-
errors: clean ? [] : ["
|
|
156
|
+
errors: clean ? [] : ["Extracted source files changed since extraction."]
|
|
157
157
|
};
|
|
158
158
|
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import { defaultSdlcPolicy, SDLC_POLICY_FILE } from "./sdlc/policy.js";
|
|
7
|
+
import { DEFAULT_TOPO_FOLDER_NAME, DEFAULT_WORKSPACE_PATH, PROJECT_CONFIG_FILE } from "./workspace-paths.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} InitProjectOptions
|
|
11
|
+
* @property {string} [targetPath]
|
|
12
|
+
* @property {boolean} [withSdlc]
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} InitProjectResult
|
|
17
|
+
* @property {boolean} ok
|
|
18
|
+
* @property {string} projectRoot
|
|
19
|
+
* @property {string} workspaceRoot
|
|
20
|
+
* @property {string} projectConfigPath
|
|
21
|
+
* @property {string[]} created
|
|
22
|
+
* @property {string[]} skipped
|
|
23
|
+
* @property {Record<string, any>} projectConfig
|
|
24
|
+
* @property {{ enabled: boolean, path: string|null }} sdlc
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} projectRoot
|
|
29
|
+
* @param {string} targetPath
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
function relativeProjectPath(projectRoot, targetPath) {
|
|
33
|
+
const relative = path.relative(projectRoot, targetPath);
|
|
34
|
+
return relative ? relative.split(path.sep).join("/") : ".";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} projectRoot
|
|
39
|
+
* @param {string} filePath
|
|
40
|
+
* @param {string} content
|
|
41
|
+
* @param {string[]} created
|
|
42
|
+
* @param {string[]} skipped
|
|
43
|
+
* @returns {void}
|
|
44
|
+
*/
|
|
45
|
+
function writeIfMissing(projectRoot, filePath, content, created, skipped) {
|
|
46
|
+
if (fs.existsSync(filePath)) {
|
|
47
|
+
skipped.push(relativeProjectPath(projectRoot, filePath));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
51
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
52
|
+
created.push(relativeProjectPath(projectRoot, filePath));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @returns {Record<string, any>}
|
|
57
|
+
*/
|
|
58
|
+
function defaultMaintainedProjectConfig() {
|
|
59
|
+
return {
|
|
60
|
+
version: "0.1",
|
|
61
|
+
workspace: DEFAULT_WORKSPACE_PATH,
|
|
62
|
+
outputs: {
|
|
63
|
+
app: {
|
|
64
|
+
path: ".",
|
|
65
|
+
ownership: "maintained"
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
topology: {
|
|
69
|
+
runtimes: []
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @returns {string}
|
|
76
|
+
*/
|
|
77
|
+
function initializedReadme() {
|
|
78
|
+
return `# Topogram Project
|
|
79
|
+
|
|
80
|
+
Initialized with \`topogram init\`.
|
|
81
|
+
|
|
82
|
+
This repository is treated as a maintained app or workspace: Topogram will not
|
|
83
|
+
overwrite source code under \`./\`. Use \`topogram emit\` for contracts, reports,
|
|
84
|
+
snapshots, and proposals, and edit maintained app code directly after reading
|
|
85
|
+
focused query packets.
|
|
86
|
+
|
|
87
|
+
## First Commands
|
|
88
|
+
|
|
89
|
+
\`\`\`bash
|
|
90
|
+
topogram agent brief --json
|
|
91
|
+
topogram check --json
|
|
92
|
+
topogram query list --json
|
|
93
|
+
\`\`\`
|
|
94
|
+
|
|
95
|
+
To adopt enforced SDLC after initialization, run:
|
|
96
|
+
|
|
97
|
+
\`\`\`bash
|
|
98
|
+
topogram sdlc policy init .
|
|
99
|
+
\`\`\`
|
|
100
|
+
|
|
101
|
+
## Source
|
|
102
|
+
|
|
103
|
+
- \`topo/\` is the project-owned Topogram workspace.
|
|
104
|
+
- \`topogram.project.json\` declares workspace, output ownership, and runtime topology.
|
|
105
|
+
- Output \`app\` points at \`.\` with \`maintained\` ownership.
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @returns {string}
|
|
111
|
+
*/
|
|
112
|
+
function initializedAgentsGuide() {
|
|
113
|
+
return `# Agent Guide
|
|
114
|
+
|
|
115
|
+
This repository was initialized with \`topogram init\`.
|
|
116
|
+
|
|
117
|
+
Start with:
|
|
118
|
+
|
|
119
|
+
\`\`\`bash
|
|
120
|
+
topogram agent brief --json
|
|
121
|
+
topogram check --json
|
|
122
|
+
topogram query list --json
|
|
123
|
+
\`\`\`
|
|
124
|
+
|
|
125
|
+
Edit \`topo/**\` and \`topogram.project.json\` for Topogram source. The project
|
|
126
|
+
output is maintained, so app/source files under \`./\` are human-owned and may be
|
|
127
|
+
edited directly after reading focused packets.
|
|
128
|
+
|
|
129
|
+
Use \`topogram emit <target>\` for contracts, reports, snapshots, migration
|
|
130
|
+
plans, and agent context. Do not expect \`topogram generate\` to overwrite this
|
|
131
|
+
maintained app unless output ownership is deliberately changed.
|
|
132
|
+
|
|
133
|
+
If \`topogram.sdlc-policy.json\` exists, use SDLC commands for task and status
|
|
134
|
+
work before protected edits:
|
|
135
|
+
|
|
136
|
+
\`\`\`bash
|
|
137
|
+
topogram sdlc policy explain --json
|
|
138
|
+
topogram sdlc prep commit . --json
|
|
139
|
+
\`\`\`
|
|
140
|
+
`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {string} projectRoot
|
|
145
|
+
* @returns {void}
|
|
146
|
+
*/
|
|
147
|
+
function assertInitTarget(projectRoot) {
|
|
148
|
+
if (fs.existsSync(projectRoot) && !fs.statSync(projectRoot).isDirectory()) {
|
|
149
|
+
throw new Error(`Cannot initialize Topogram at '${projectRoot}' because it is not a directory.`);
|
|
150
|
+
}
|
|
151
|
+
const configPath = path.join(projectRoot, PROJECT_CONFIG_FILE);
|
|
152
|
+
if (fs.existsSync(configPath)) {
|
|
153
|
+
throw new Error(`Refusing to initialize Topogram because ${PROJECT_CONFIG_FILE} already exists.`);
|
|
154
|
+
}
|
|
155
|
+
const workspaceRoot = path.join(projectRoot, DEFAULT_TOPO_FOLDER_NAME);
|
|
156
|
+
if (fs.existsSync(workspaceRoot)) {
|
|
157
|
+
if (!fs.statSync(workspaceRoot).isDirectory()) {
|
|
158
|
+
throw new Error(`Refusing to initialize Topogram because ${DEFAULT_TOPO_FOLDER_NAME}/ exists and is not a directory.`);
|
|
159
|
+
}
|
|
160
|
+
const entries = fs.readdirSync(workspaceRoot).filter(/** @param {string} entry */ (entry) => entry !== ".DS_Store");
|
|
161
|
+
if (entries.length > 0) {
|
|
162
|
+
throw new Error(`Refusing to initialize Topogram because ${DEFAULT_TOPO_FOLDER_NAME}/ already exists and is not empty.`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {InitProjectOptions} [options]
|
|
169
|
+
* @returns {InitProjectResult}
|
|
170
|
+
*/
|
|
171
|
+
export function initTopogramProject(options = {}) {
|
|
172
|
+
const projectRoot = path.resolve(options.targetPath || ".");
|
|
173
|
+
assertInitTarget(projectRoot);
|
|
174
|
+
fs.mkdirSync(projectRoot, { recursive: true });
|
|
175
|
+
|
|
176
|
+
/** @type {string[]} */
|
|
177
|
+
const created = [];
|
|
178
|
+
/** @type {string[]} */
|
|
179
|
+
const skipped = [];
|
|
180
|
+
const workspaceRoot = path.join(projectRoot, DEFAULT_TOPO_FOLDER_NAME);
|
|
181
|
+
fs.mkdirSync(workspaceRoot, { recursive: true });
|
|
182
|
+
created.push(DEFAULT_TOPO_FOLDER_NAME);
|
|
183
|
+
|
|
184
|
+
writeIfMissing(projectRoot, path.join(workspaceRoot, ".gitkeep"), "", created, skipped);
|
|
185
|
+
const projectConfig = defaultMaintainedProjectConfig();
|
|
186
|
+
const projectConfigPath = path.join(projectRoot, PROJECT_CONFIG_FILE);
|
|
187
|
+
fs.writeFileSync(projectConfigPath, `${JSON.stringify(projectConfig, null, 2)}\n`, "utf8");
|
|
188
|
+
created.push(PROJECT_CONFIG_FILE);
|
|
189
|
+
writeIfMissing(projectRoot, path.join(projectRoot, "README.md"), initializedReadme(), created, skipped);
|
|
190
|
+
writeIfMissing(projectRoot, path.join(projectRoot, "AGENTS.md"), initializedAgentsGuide(), created, skipped);
|
|
191
|
+
const sdlcPolicyPath = path.join(projectRoot, SDLC_POLICY_FILE);
|
|
192
|
+
if (options.withSdlc) {
|
|
193
|
+
writeIfMissing(
|
|
194
|
+
projectRoot,
|
|
195
|
+
sdlcPolicyPath,
|
|
196
|
+
`${JSON.stringify(defaultSdlcPolicy(), null, 2)}\n`,
|
|
197
|
+
created,
|
|
198
|
+
skipped
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
projectRoot,
|
|
205
|
+
workspaceRoot,
|
|
206
|
+
projectConfigPath,
|
|
207
|
+
created,
|
|
208
|
+
skipped,
|
|
209
|
+
projectConfig,
|
|
210
|
+
sdlc: {
|
|
211
|
+
enabled: options.withSdlc ? fs.existsSync(sdlcPolicyPath) : false,
|
|
212
|
+
path: options.withSdlc ? sdlcPolicyPath : null
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -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 workspace and implementation/ content; symlinks can point outside the trusted template root. Replace the symlink with a real file or directory before running topogram
|
|
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 copy or topogram template check.`;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
@@ -36,7 +36,7 @@ export function createNewProject({
|
|
|
36
36
|
templateProvenance = null
|
|
37
37
|
}) {
|
|
38
38
|
if (!targetPath) {
|
|
39
|
-
throw new Error("topogram
|
|
39
|
+
throw new Error("topogram copy requires <target>.");
|
|
40
40
|
}
|
|
41
41
|
const projectRoot = path.resolve(targetPath);
|
|
42
42
|
assertProjectOutsideEngine(projectRoot, engineRoot);
|
|
@@ -68,7 +68,7 @@ export function createNewProject({
|
|
|
68
68
|
writeTemplateTrustRecord(projectRoot, projectConfig);
|
|
69
69
|
warnings.push(
|
|
70
70
|
`Template '${template.manifest.id}' copied implementation/ code into this project. ` +
|
|
71
|
-
"topogram
|
|
71
|
+
"topogram copy did not execute it, but topogram generate may load it later. " +
|
|
72
72
|
"Recorded local trust in .topogram-template-trust.json."
|
|
73
73
|
);
|
|
74
74
|
}
|
|
@@ -247,7 +247,7 @@ export function writeProjectReadme(projectRoot, projectConfig) {
|
|
|
247
247
|
provenanceLines.push(`- Executable implementation: \`${template.includesExecutableImplementation ? "yes" : "no"}\``);
|
|
248
248
|
const readme = `# ${packageNameFromPath(projectRoot)}
|
|
249
249
|
|
|
250
|
-
|
|
250
|
+
Copied by \`topogram copy\`.
|
|
251
251
|
|
|
252
252
|
## Template
|
|
253
253
|
|
|
@@ -263,7 +263,7 @@ Edit the workspace folder \`${DEFAULT_TOPO_FOLDER_NAME}/\` and \`topogram.projec
|
|
|
263
263
|
Generated app code is written to \`app/\`.
|
|
264
264
|
Use \`topogram emit <target>\` to inspect contracts, reports, snapshots, and other artifacts without regenerating the app.
|
|
265
265
|
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.
|
|
266
|
-
${template.includesExecutableImplementation ? "\nThis template copied `implementation/` code. `topogram
|
|
266
|
+
${template.includesExecutableImplementation ? "\nThis template copied `implementation/` code. `topogram copy` did not execute it; review `implementation/`, `topogram.template-policy.json`, and `.topogram-template-trust.json` before regenerating after edits.\n" : ""}
|
|
267
267
|
`;
|
|
268
268
|
fs.writeFileSync(path.join(projectRoot, "README.md"), readme, "utf8");
|
|
269
269
|
}
|
|
@@ -339,12 +339,12 @@ npm run query:show -- widget-behavior
|
|
|
339
339
|
|
|
340
340
|
- Local edits to template-derived Topogram files are project-owned.
|
|
341
341
|
- Use \`npm run source:status\` and \`npm run template:update:recommend\` before applying template updates.
|
|
342
|
-
${hasImplementation ? "- This project has executable `implementation/` code. `topogram
|
|
343
|
-
##
|
|
342
|
+
${hasImplementation ? "- This project has executable `implementation/` code. `topogram copy` did not execute it. Do not refresh trust until the implementation has been reviewed.\n" : "- This template does not declare executable implementation code.\n"}
|
|
343
|
+
## Extract And Adopt
|
|
344
344
|
|
|
345
|
-
- If \`.topogram-
|
|
346
|
-
-
|
|
347
|
-
-
|
|
345
|
+
- If \`.topogram-extract.json\` exists, agents should run \`topogram extract check . --json\`, \`topogram extract plan . --json\`, \`topogram adopt --list . --json\`, \`topogram extract status . --json\`, and \`topogram extract history . --verify --json\`.
|
|
346
|
+
- Extract JSON payloads expose \`workspaceRoot\`; prefer it as the canonical project-owned workspace path.
|
|
347
|
+
- Extracted Topogram files are project-owned after adoption; source hashes record trusted source evidence at the time of extraction.
|
|
348
348
|
|
|
349
349
|
## Verification Gates
|
|
350
350
|
|
|
@@ -28,9 +28,11 @@ function primaryEntityIdForBundle(bundle) {
|
|
|
28
28
|
|
|
29
29
|
function canonicalJourneyCoverage(graph) {
|
|
30
30
|
const journeyDocs = (graph?.docs || []).filter((doc) => doc.kind === "journey");
|
|
31
|
+
const journeyStatements = graph?.byKind?.journey || [];
|
|
32
|
+
const journeys = [...journeyStatements, ...journeyDocs];
|
|
31
33
|
return {
|
|
32
|
-
byEntityId: new Set(
|
|
33
|
-
byCapabilityId: new Set(
|
|
34
|
+
byEntityId: new Set(journeys.flatMap((journey) => journey.relatedEntities || [])),
|
|
35
|
+
byCapabilityId: new Set(journeys.flatMap((journey) => journey.relatedCapabilities || []))
|
|
34
36
|
};
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -42,7 +44,10 @@ function collectJourneyGenerationContext(graph) {
|
|
|
42
44
|
const uiSharedScreens = projections
|
|
43
45
|
.filter((projection) => projection.type === "ui_contract")
|
|
44
46
|
.flatMap((projection) => (projection.uiScreens || []).map((screen) => ({ ...screen, projectionId: projection.id })));
|
|
45
|
-
const canonicalJourneys =
|
|
47
|
+
const canonicalJourneys = [
|
|
48
|
+
...(graph.byKind?.journey || []),
|
|
49
|
+
...(graph.docs || []).filter((doc) => doc.kind === "journey")
|
|
50
|
+
];
|
|
46
51
|
const coveredEntityIds = new Set(canonicalJourneys.flatMap((doc) => doc.relatedEntities || []));
|
|
47
52
|
|
|
48
53
|
return entities
|