@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.
Files changed (79) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/package.json +2 -2
  3. package/src/agent-brief.js +29 -23
  4. package/src/agent-ops/query-builders/change-risk/{import-plan.js → extract-plan.js} +1 -1
  5. package/src/agent-ops/query-builders/change-risk/review-packets.js +5 -5
  6. package/src/agent-ops/query-builders/change-risk.js +1 -1
  7. package/src/agent-ops/query-builders/common.js +2 -2
  8. package/src/agent-ops/query-builders/multi-agent.js +1 -1
  9. package/src/agent-ops/query-builders/workflow-context-shared.js +4 -4
  10. package/src/catalog/provenance.js +1 -1
  11. package/src/cli/catalog-alias.d.ts +2 -0
  12. package/src/cli/catalog-alias.js +2 -2
  13. package/src/cli/command-parsers/core.js +9 -5
  14. package/src/cli/command-parsers/import.js +11 -17
  15. package/src/cli/command-parsers/project.js +0 -3
  16. package/src/cli/commands/catalog/copy.js +3 -3
  17. package/src/cli/commands/catalog/help.js +1 -2
  18. package/src/cli/commands/catalog/list.js +7 -4
  19. package/src/cli/commands/catalog/show.js +4 -4
  20. package/src/cli/commands/copy.js +356 -0
  21. package/src/cli/commands/doctor.js +1 -1
  22. package/src/cli/commands/import/adopt.js +9 -9
  23. package/src/cli/commands/import/check.js +15 -15
  24. package/src/cli/commands/import/diff.js +6 -6
  25. package/src/cli/commands/import/help.js +43 -34
  26. package/src/cli/commands/import/paths.js +3 -3
  27. package/src/cli/commands/import/plan.js +8 -8
  28. package/src/cli/commands/import/refresh.js +25 -24
  29. package/src/cli/commands/import/status-history.js +4 -4
  30. package/src/cli/commands/import/workspace.js +16 -16
  31. package/src/cli/commands/import-runner.js +6 -5
  32. package/src/cli/commands/import.js +4 -1
  33. package/src/cli/commands/init.js +67 -0
  34. package/src/cli/commands/query/{import-adopt.js → extract-adopt.js} +2 -2
  35. package/src/cli/commands/query/runner/change.js +2 -2
  36. package/src/cli/commands/query/runner/{import-adopt.js → extract-adopt.js} +9 -9
  37. package/src/cli/commands/query/runner/index.js +1 -1
  38. package/src/cli/commands/query/runner/workflow.js +7 -7
  39. package/src/cli/commands/query/workspace.js +4 -4
  40. package/src/cli/commands/release-status.js +2 -2
  41. package/src/cli/commands/source.js +2 -2
  42. package/src/cli/commands/template/check.js +2 -2
  43. package/src/cli/commands/template/list-show.js +4 -4
  44. package/src/cli/dispatcher.js +18 -3
  45. package/src/cli/help-dispatch.js +22 -8
  46. package/src/cli/help.js +68 -52
  47. package/src/cli/migration-guidance.js +9 -0
  48. package/src/generator/context/bundle.js +14 -7
  49. package/src/generator/context/diff.js +8 -1
  50. package/src/generator/context/digest.js +10 -1
  51. package/src/generator/context/shared/domain-sdlc.js +5 -1
  52. package/src/generator/context/shared/relationships.js +20 -5
  53. package/src/generator/context/shared/summaries.js +26 -0
  54. package/src/generator/context/shared.d.ts +1 -0
  55. package/src/generator/context/shared.js +1 -0
  56. package/src/generator/context/slice/core.js +9 -5
  57. package/src/generator/context/slice/sdlc.js +31 -2
  58. package/src/generator/context/task-mode.js +3 -3
  59. package/src/import/core/runner/reports.js +4 -4
  60. package/src/import/provenance.js +16 -16
  61. package/src/init-project.js +215 -0
  62. package/src/new-project/constants.js +1 -1
  63. package/src/new-project/create.js +2 -2
  64. package/src/new-project/project-files.js +7 -7
  65. package/src/reconcile/journeys.js +8 -3
  66. package/src/record-blocks.js +125 -0
  67. package/src/resolver/index.js +3 -0
  68. package/src/resolver/journeys.js +74 -0
  69. package/src/resolver/normalize.js +25 -0
  70. package/src/sdlc/adopt.js +1 -1
  71. package/src/validator/common.js +34 -1
  72. package/src/validator/index.js +4 -0
  73. package/src/validator/kinds.d.ts +2 -0
  74. package/src/validator/kinds.js +34 -1
  75. package/src/validator/per-kind/journey.js +233 -0
  76. package/src/workflows/docs-generate.js +4 -1
  77. package/src/workflows/reconcile/bundle-core/index.js +4 -2
  78. package/src/workflows/reconcile/canonical-surface.js +4 -1
  79. 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 @@ export {
20
20
  summarizeById,
21
21
  summarizeStatementsByIds,
22
22
  summarizeDocsByIds,
23
+ summarizeJourneyLikeByIds,
23
24
  workspaceInventory
24
25
  } from "./shared/relationships.js";
25
26
  export {
@@ -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: summarizeDocsByIds(graph, 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 = (graph.docs || [])
122
- .filter(/** @param {any} doc */ (doc) => doc.kind === "journey" && (doc.relatedWorkflows || []).includes(workflowId))
123
- .map(/** @param {any} doc */ (doc) => doc.id)
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: summarizeDocsByIds(graph, 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 verifications = verificationIdsForTarget(graph, [...capabilities, ...workflows, ...projections, journeyId]);
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: "import-adopt",
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|import-adopt|diff-review|verification>");
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 === "import-adopt") {
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 Import 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`
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 Import Report\n\n- Capabilities: ${candidates.capabilities.length}\n- Routes: ${candidates.routes.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n`
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 Import 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 import plan <path>\` before adoption.\n- After adoption, run \`topogram check <path>\`, \`topogram widget check <path>\`, and \`topogram widget behavior <path>\`.\n`
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 Import 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`
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") {
@@ -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-import.json";
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-import",
46
- importedAt: input.importedAt || timestamp,
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
- import: {
53
+ extract: {
54
54
  tracks: input.tracks || [],
55
55
  findingsCount: input.findingsCount || 0,
56
56
  candidateCounts: input.candidateCounts || {}
57
57
  },
58
58
  ownership: {
59
- importedArtifacts: "project-owned",
60
- note: "Topogram artifacts created by import are editable after import. Source hashes record the brownfield app evidence trusted at import time."
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: "topogram_import_missing",
82
+ code: "topogram_extract_missing",
83
83
  severity: "error",
84
- message: `${TOPOGRAM_IMPORT_FILE} was not found. This workspace does not have brownfield import provenance.`,
84
+ message: `${TOPOGRAM_IMPORT_FILE} was not found. This workspace does not have brownfield extraction provenance.`,
85
85
  path: importPath,
86
- suggestedFix: "Run `topogram import <app-path> --out <target>` to create an imported Topogram workspace."
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 = `Imported source path was not found: ${source.source?.path || "unknown"}`;
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
- code: "topogram_import_source_missing",
104
+ code: "topogram_extract_source_missing",
105
105
  severity: "error",
106
106
  message,
107
107
  path: source.source?.path || null,
108
- suggestedFix: "Restore the imported source path or rerun import from the current brownfield app location."
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: "topogram_import_source_changed",
150
+ code: "topogram_extract_source_changed",
151
151
  severity: "error",
152
- message: "Imported source files changed since they were trusted for this import.",
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 import or update the Topogram artifacts manually."
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 ? [] : ["Imported source files changed since import."]
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 new or topogram template check.`;
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 new requires <path>.");
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 new did not execute it, but topogram generate may load it later. " +
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
- Generated by \`topogram new\`.
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 new` did not execute it; review `implementation/`, `topogram.template-policy.json`, and `.topogram-template-trust.json` before regenerating after edits.\n" : ""}
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 new` 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
- ## Import And Adoption
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-import.json\` exists, agents should run \`topogram import check . --json\`, \`topogram import plan . --json\`, \`topogram import adopt --list . --json\`, \`topogram import status . --json\`, and \`topogram import history . --verify --json\`.
346
- - Import JSON payloads expose \`workspaceRoot\`; prefer it as the canonical project-owned workspace path.
347
- - Imported Topogram files are project-owned after adoption; source hashes record trusted import evidence at the time of import.
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(journeyDocs.flatMap((doc) => doc.relatedEntities || [])),
33
- byCapabilityId: new Set(journeyDocs.flatMap((doc) => doc.relatedCapabilities || []))
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 = (graph.docs || []).filter((doc) => doc.kind === "journey");
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