@topogram/cli 0.3.77 → 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 (129) 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/core/shared/files.js +21 -2
  61. package/src/import/core/shared.js +2 -1
  62. package/src/import/enrichers/django-rest.js +4 -4
  63. package/src/import/enrichers/rails-controllers.js +3 -3
  64. package/src/import/enrichers/rails-models.js +3 -3
  65. package/src/import/extractors/api/aspnet-core.js +5 -5
  66. package/src/import/extractors/api/django-routes.js +5 -5
  67. package/src/import/extractors/api/express.js +4 -4
  68. package/src/import/extractors/api/fastify.js +7 -7
  69. package/src/import/extractors/api/flutter-dio.js +4 -4
  70. package/src/import/extractors/api/generic-route-fallback.js +2 -2
  71. package/src/import/extractors/api/graphql-code-first.js +3 -3
  72. package/src/import/extractors/api/graphql-sdl.js +5 -5
  73. package/src/import/extractors/api/jaxrs.js +3 -3
  74. package/src/import/extractors/api/micronaut.js +3 -3
  75. package/src/import/extractors/api/openapi-code.js +4 -4
  76. package/src/import/extractors/api/openapi.js +3 -3
  77. package/src/import/extractors/api/rails-routes.js +3 -3
  78. package/src/import/extractors/api/react-native-repository.js +3 -3
  79. package/src/import/extractors/api/retrofit.js +3 -3
  80. package/src/import/extractors/api/spring-web.js +3 -3
  81. package/src/import/extractors/api/swift-webapi.js +3 -3
  82. package/src/import/extractors/api/trpc.js +4 -4
  83. package/src/import/extractors/cli/generic.js +3 -3
  84. package/src/import/extractors/db/django-models.js +4 -4
  85. package/src/import/extractors/db/dotnet-models.js +4 -4
  86. package/src/import/extractors/db/drizzle.js +9 -7
  87. package/src/import/extractors/db/ef-core.js +5 -5
  88. package/src/import/extractors/db/flutter-entities.js +3 -3
  89. package/src/import/extractors/db/jpa.js +3 -3
  90. package/src/import/extractors/db/liquibase.js +3 -3
  91. package/src/import/extractors/db/maintained-seams.js +4 -4
  92. package/src/import/extractors/db/mybatis-xml.js +4 -4
  93. package/src/import/extractors/db/prisma.js +3 -3
  94. package/src/import/extractors/db/rails-schema.js +3 -3
  95. package/src/import/extractors/db/react-native-entities.js +3 -3
  96. package/src/import/extractors/db/room.js +5 -5
  97. package/src/import/extractors/db/snapshot.js +3 -3
  98. package/src/import/extractors/db/sql.js +3 -3
  99. package/src/import/extractors/db/swiftdata.js +3 -3
  100. package/src/import/extractors/ui/android-compose.js +4 -4
  101. package/src/import/extractors/ui/backend-only.js +3 -3
  102. package/src/import/extractors/ui/blazor.js +3 -3
  103. package/src/import/extractors/ui/flutter-screens.js +3 -3
  104. package/src/import/extractors/ui/maui-xaml.js +4 -4
  105. package/src/import/extractors/ui/next-pages-router.js +3 -3
  106. package/src/import/extractors/ui/razor-pages.js +3 -3
  107. package/src/import/extractors/ui/react-native-screens.js +4 -4
  108. package/src/import/extractors/ui/swiftui.js +3 -3
  109. package/src/import/extractors/ui/uikit.js +3 -3
  110. package/src/import/provenance.js +16 -16
  111. package/src/init-project.js +215 -0
  112. package/src/new-project/constants.js +1 -1
  113. package/src/new-project/create.js +2 -2
  114. package/src/new-project/project-files.js +7 -7
  115. package/src/reconcile/journeys.js +8 -3
  116. package/src/record-blocks.js +125 -0
  117. package/src/resolver/index.js +3 -0
  118. package/src/resolver/journeys.js +74 -0
  119. package/src/resolver/normalize.js +25 -0
  120. package/src/sdlc/adopt.js +1 -1
  121. package/src/validator/common.js +34 -1
  122. package/src/validator/index.js +4 -0
  123. package/src/validator/kinds.d.ts +2 -0
  124. package/src/validator/kinds.js +34 -1
  125. package/src/validator/per-kind/journey.js +233 -0
  126. package/src/workflows/docs-generate.js +4 -1
  127. package/src/workflows/reconcile/bundle-core/index.js +4 -2
  128. package/src/workflows/reconcile/canonical-surface.js +4 -1
  129. package/src/cli/commands/new.js +0 -94
@@ -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
@@ -0,0 +1,125 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @typedef {{
5
+ * key: string,
6
+ * value: import("./parser.js").AstValue | null,
7
+ * values: import("./parser.js").AstValue[],
8
+ * loc: import("./parser.js").AstLocation
9
+ * }} TopogramRecordField
10
+ *
11
+ * @typedef {{
12
+ * fields: Map<string, TopogramRecordField[]>,
13
+ * fieldOrder: TopogramRecordField[],
14
+ * loc: import("./parser.js").AstLocation
15
+ * }} TopogramRecordBlock
16
+ */
17
+
18
+ /**
19
+ * @param {import("./parser.js").AstValue[]} values
20
+ * @param {import("./parser.js").AstLocation} loc
21
+ * @returns {import("./parser.js").AstValue | null}
22
+ */
23
+ function valuesToRecordValue(values, loc) {
24
+ if (values.length === 0) {
25
+ return null;
26
+ }
27
+ if (values.length === 1) {
28
+ return values[0];
29
+ }
30
+ return {
31
+ type: "sequence",
32
+ items: values,
33
+ loc
34
+ };
35
+ }
36
+
37
+ /**
38
+ * @param {import("./parser.js").AstBlock} block
39
+ * @returns {TopogramRecordBlock}
40
+ */
41
+ export function parseRecordBlock(block) {
42
+ /** @type {Map<string, TopogramRecordField[]>} */
43
+ const fields = new Map();
44
+ /** @type {TopogramRecordField[]} */
45
+ const fieldOrder = [];
46
+
47
+ for (const entry of block.entries || []) {
48
+ const [keyToken, ...values] = entry.items || [];
49
+ const key = keyToken?.type === "symbol" ? keyToken.value : "";
50
+ const field = {
51
+ key,
52
+ value: valuesToRecordValue(values, entry.loc),
53
+ values,
54
+ loc: entry.loc
55
+ };
56
+ if (!fields.has(key)) {
57
+ fields.set(key, []);
58
+ }
59
+ fields.get(key)?.push(field);
60
+ fieldOrder.push(field);
61
+ }
62
+
63
+ return {
64
+ fields,
65
+ fieldOrder,
66
+ loc: block.loc
67
+ };
68
+ }
69
+
70
+ /**
71
+ * @param {TopogramRecordBlock} record
72
+ * @param {string} key
73
+ * @returns {TopogramRecordField | null}
74
+ */
75
+ export function recordField(record, key) {
76
+ return record.fields.get(key)?.[0] || null;
77
+ }
78
+
79
+ /**
80
+ * @param {TopogramRecordBlock} record
81
+ * @param {string} key
82
+ * @returns {string | null}
83
+ */
84
+ export function recordSymbol(record, key) {
85
+ const value = recordField(record, key)?.value;
86
+ return value?.type === "symbol" ? value.value : null;
87
+ }
88
+
89
+ /**
90
+ * @param {TopogramRecordBlock} record
91
+ * @param {string} key
92
+ * @returns {string | null}
93
+ */
94
+ export function recordString(record, key) {
95
+ const value = recordField(record, key)?.value;
96
+ return value?.type === "string" ? value.value : null;
97
+ }
98
+
99
+ /**
100
+ * @param {TopogramRecordBlock} record
101
+ * @param {string} key
102
+ * @returns {string[]}
103
+ */
104
+ export function recordStringList(record, key) {
105
+ const value = recordField(record, key)?.value;
106
+ if (!value) return [];
107
+ const items = value.type === "list" ? value.items : value.type === "sequence" ? value.items : [value];
108
+ return items
109
+ .map((item) => item.type === "string" ? item.value : null)
110
+ .filter(/** @param {string | null} value */ (value) => value !== null);
111
+ }
112
+
113
+ /**
114
+ * @param {TopogramRecordBlock} record
115
+ * @param {string} key
116
+ * @returns {string[]}
117
+ */
118
+ export function recordSymbolList(record, key) {
119
+ const value = recordField(record, key)?.value;
120
+ if (!value) return [];
121
+ const items = value.type === "list" ? value.items : value.type === "sequence" ? value.items : [value];
122
+ return items
123
+ .map((item) => item.type === "symbol" ? item.value : null)
124
+ .filter(/** @param {string | null} value */ (value) => value !== null);
125
+ }
@@ -111,6 +111,7 @@ export function resolveWorkspace(workspaceAst) {
111
111
  orchestrations: [],
112
112
  operations: [],
113
113
  decisions: [],
114
+ journeys: [],
114
115
  pitches: [],
115
116
  requirements: [],
116
117
  tasks: [],
@@ -128,6 +129,7 @@ export function resolveWorkspace(workspaceAst) {
128
129
  orchestration: "orchestrations",
129
130
  operation: "operations",
130
131
  decision: "decisions",
132
+ journey: "journeys",
131
133
  pitch: "pitches",
132
134
  requirement: "requirements",
133
135
  task: "tasks",
@@ -321,6 +323,7 @@ export function resolveWorkspace(workspaceAst) {
321
323
  orchestrations: [],
322
324
  operations: [],
323
325
  decisions: [],
326
+ journeys: [],
324
327
  pitches: [],
325
328
  requirements: [],
326
329
  tasks: [],
@@ -0,0 +1,74 @@
1
+ // @ts-check
2
+
3
+ import {
4
+ parseRecordBlock,
5
+ recordString,
6
+ recordStringList,
7
+ recordSymbol,
8
+ recordSymbolList
9
+ } from "../record-blocks.js";
10
+
11
+ /**
12
+ * @param {import("../parser.js").AstStatement} statement
13
+ * @param {string} key
14
+ * @returns {import("../parser.js").AstField[]}
15
+ */
16
+ function fieldsByKey(statement, key) {
17
+ return statement.fields.filter((field) => field.key === key);
18
+ }
19
+
20
+ /**
21
+ * @param {import("../parser.js").AstField} field
22
+ * @returns {any}
23
+ */
24
+ function parseStepField(field) {
25
+ if (field.value.type !== "block") {
26
+ return null;
27
+ }
28
+ const record = parseRecordBlock(field.value);
29
+ return {
30
+ id: recordSymbol(record, "id"),
31
+ intent: recordString(record, "intent"),
32
+ commands: recordStringList(record, "commands"),
33
+ expects: recordStringList(record, "expects"),
34
+ after: recordSymbolList(record, "after"),
35
+ notes: recordString(record, "notes"),
36
+ loc: field.loc
37
+ };
38
+ }
39
+
40
+ /**
41
+ * @param {import("../parser.js").AstField} field
42
+ * @returns {any}
43
+ */
44
+ function parseAlternateField(field) {
45
+ if (field.value.type !== "block") {
46
+ return null;
47
+ }
48
+ const record = parseRecordBlock(field.value);
49
+ return {
50
+ id: recordSymbol(record, "id"),
51
+ from: recordSymbol(record, "from"),
52
+ condition: recordString(record, "condition"),
53
+ commands: recordStringList(record, "commands"),
54
+ expects: recordStringList(record, "expects"),
55
+ notes: recordString(record, "notes"),
56
+ loc: field.loc
57
+ };
58
+ }
59
+
60
+ /**
61
+ * @param {import("../parser.js").AstStatement} statement
62
+ * @returns {any[]}
63
+ */
64
+ export function parseJourneySteps(statement) {
65
+ return fieldsByKey(statement, "step").map(parseStepField).filter(Boolean);
66
+ }
67
+
68
+ /**
69
+ * @param {import("../parser.js").AstStatement} statement
70
+ * @returns {any[]}
71
+ */
72
+ export function parseJourneyAlternates(statement) {
73
+ return fieldsByKey(statement, "alternate").map(parseAlternateField).filter(Boolean);
74
+ }
@@ -75,6 +75,7 @@ import {
75
75
  parseProjectionGeneratorDefaultsBlock
76
76
  } from "./projections-db.js";
77
77
  import { parsePlanSteps } from "../sdlc/plan-steps.js";
78
+ import { parseJourneyAlternates, parseJourneySteps } from "./journeys.js";
78
79
 
79
80
  export function normalizeStatement(statement, registry) {
80
81
  const fieldMap = collectFieldMap(statement);
@@ -308,6 +309,30 @@ export function normalizeStatement(statement, registry) {
308
309
  : null,
309
310
  aliases: normalizeDomainScopeList(statement, "aliases")
310
311
  };
312
+ case "journey":
313
+ return {
314
+ ...base,
315
+ actors: symbolValues(getFieldValue(statement, "actors")),
316
+ roles: symbolValues(getFieldValue(statement, "roles")),
317
+ goal: stringValue(getFieldValue(statement, "goal")),
318
+ trigger: stringValue(getFieldValue(statement, "trigger")),
319
+ steps: parseJourneySteps(statement),
320
+ alternates: parseJourneyAlternates(statement),
321
+ successSignals: normalizeDomainScopeList(statement, "success_signals"),
322
+ failureSignals: normalizeDomainScopeList(statement, "failure_signals"),
323
+ relatedCapabilities: symbolValues(getFieldValue(statement, "related_capabilities")),
324
+ relatedEntities: symbolValues(getFieldValue(statement, "related_entities")),
325
+ relatedRules: symbolValues(getFieldValue(statement, "related_rules")),
326
+ relatedWorkflows: symbolValues(getFieldValue(statement, "related_workflows")),
327
+ relatedProjections: symbolValues(getFieldValue(statement, "related_projections")),
328
+ relatedWidgets: symbolValues(getFieldValue(statement, "related_widgets")),
329
+ relatedVerifications: symbolValues(getFieldValue(statement, "related_verifications")),
330
+ relatedDecisions: symbolValues(getFieldValue(statement, "related_decisions")),
331
+ relatedDocs: symbolValues(getFieldValue(statement, "related_docs")),
332
+ tags: normalizeDomainScopeList(statement, "tags"),
333
+ updated: stringValue(getFieldValue(statement, "updated")),
334
+ resolvedDomain: resolveDomainTag(statement, registry)
335
+ };
311
336
  case "pitch":
312
337
  return {
313
338
  ...base,
package/src/sdlc/adopt.js CHANGED
@@ -55,7 +55,7 @@ function scanPressure(root) {
55
55
  export function sdlcAdopt(workspaceRoot) {
56
56
  const root = path.resolve(workspaceRoot);
57
57
  if (!existsSync(resolveTopoRoot(root))) {
58
- return { ok: false, error: `No '${DEFAULT_TOPO_FOLDER_NAME}/' workspace folder at ${root}; run 'topogram new' first` };
58
+ return { ok: false, error: `No '${DEFAULT_TOPO_FOLDER_NAME}/' workspace folder at ${root}; run 'topogram init' or 'topogram copy' first` };
59
59
  }
60
60
  const folders = SDLC_FOLDERS.map((name) => ensureFolder(root, name));
61
61
  const pressure = scanPressure(root);
@@ -205,6 +205,8 @@ function validateFieldShapes(errors, statement, fieldMap) {
205
205
  ensureSingleValueField(errors, statement, fieldMap, "updated", ["string"]);
206
206
  ensureSingleValueField(errors, statement, fieldMap, "notes", ["string"]);
207
207
  ensureSingleValueField(errors, statement, fieldMap, "outcome", ["string"]);
208
+ ensureSingleValueField(errors, statement, fieldMap, "goal", ["string"]);
209
+ ensureSingleValueField(errors, statement, fieldMap, "trigger", ["string"]);
208
210
 
209
211
  const listFields = [
210
212
  "aliases",
@@ -238,7 +240,19 @@ function validateFieldShapes(errors, statement, fieldMap) {
238
240
  "regions",
239
241
  "lookups",
240
242
  "dependencies",
241
- "approvals"
243
+ "approvals",
244
+ "success_signals",
245
+ "failure_signals",
246
+ "tags",
247
+ "related_capabilities",
248
+ "related_entities",
249
+ "related_rules",
250
+ "related_workflows",
251
+ "related_projections",
252
+ "related_widgets",
253
+ "related_verifications",
254
+ "related_decisions",
255
+ "related_docs"
242
256
  ];
243
257
  if (statement.kind === "orchestration") {
244
258
  listFields.push("steps");
@@ -251,6 +265,16 @@ function validateFieldShapes(errors, statement, fieldMap) {
251
265
  if (statement.kind === "plan") {
252
266
  blockFields.push("steps");
253
267
  }
268
+ if (statement.kind === "journey") {
269
+ for (const key of ["step", "alternate"]) {
270
+ const fields = fieldMap.get(key) || [];
271
+ for (const field of fields) {
272
+ if (field.value.type !== "block") {
273
+ pushError(errors, `Field '${key}' on ${statement.kind} ${statement.id} must be block, found ${field.value.type}`, field.loc);
274
+ }
275
+ }
276
+ }
277
+ }
254
278
  for (const key of blockFields) {
255
279
  ensureSingleValueField(errors, statement, fieldMap, key, ["block"]);
256
280
  }
@@ -429,6 +453,15 @@ function validateReferenceKinds(errors, statement, fieldMap, registry) {
429
453
  requirement: null,
430
454
  from_requirement: ["requirement"],
431
455
  affects: ["capability", "entity", "rule", "projection", "widget", "orchestration", "operation"],
456
+ related_capabilities: ["capability"],
457
+ related_entities: ["entity"],
458
+ related_rules: ["rule"],
459
+ related_workflows: null,
460
+ related_projections: ["projection"],
461
+ related_widgets: ["widget"],
462
+ related_verifications: ["verification"],
463
+ related_decisions: ["decision"],
464
+ related_docs: null,
432
465
  introduces_rules: ["rule"],
433
466
  respects_rules: ["rule"],
434
467
  decisions: ["decision"],
@@ -20,6 +20,7 @@ import { validateAcceptanceCriterion } from "./per-kind/acceptance-criterion.js"
20
20
  import { validateTask } from "./per-kind/task.js";
21
21
  import { validatePlan } from "./per-kind/plan.js";
22
22
  import { validateBug } from "./per-kind/bug.js";
23
+ import { validateJourney } from "./per-kind/journey.js";
23
24
 
24
25
  export {
25
26
  STATEMENT_KINDS,
@@ -32,6 +33,7 @@ export {
32
33
  TASK_IDENTIFIER_PATTERN,
33
34
  PLAN_IDENTIFIER_PATTERN,
34
35
  BUG_IDENTIFIER_PATTERN,
36
+ JOURNEY_IDENTIFIER_PATTERN,
35
37
  DOCUMENT_IDENTIFIER_PATTERN,
36
38
  GLOBAL_STATUSES,
37
39
  DECISION_STATUSES,
@@ -48,6 +50,7 @@ export {
48
50
  PLAN_STATUSES,
49
51
  PLAN_STEP_STATUSES,
50
52
  BUG_STATUSES,
53
+ JOURNEY_STATUSES,
51
54
  PRIORITY_VALUES,
52
55
  WORK_TYPES,
53
56
  BUG_SEVERITIES,
@@ -112,6 +115,7 @@ export function validateWorkspace(workspaceAst) {
112
115
  validateTask(errors, statement, fieldMap, registry);
113
116
  validatePlan(errors, statement, fieldMap, registry);
114
117
  validateBug(errors, statement, fieldMap, registry);
118
+ validateJourney(errors, statement, fieldMap, registry);
115
119
  validateExpressions(errors, statement, fieldMap);
116
120
  }
117
121
  }