@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.
- 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/core/shared/files.js +21 -2
- package/src/import/core/shared.js +2 -1
- package/src/import/enrichers/django-rest.js +4 -4
- package/src/import/enrichers/rails-controllers.js +3 -3
- package/src/import/enrichers/rails-models.js +3 -3
- package/src/import/extractors/api/aspnet-core.js +5 -5
- package/src/import/extractors/api/django-routes.js +5 -5
- package/src/import/extractors/api/express.js +4 -4
- package/src/import/extractors/api/fastify.js +7 -7
- package/src/import/extractors/api/flutter-dio.js +4 -4
- package/src/import/extractors/api/generic-route-fallback.js +2 -2
- package/src/import/extractors/api/graphql-code-first.js +3 -3
- package/src/import/extractors/api/graphql-sdl.js +5 -5
- package/src/import/extractors/api/jaxrs.js +3 -3
- package/src/import/extractors/api/micronaut.js +3 -3
- package/src/import/extractors/api/openapi-code.js +4 -4
- package/src/import/extractors/api/openapi.js +3 -3
- package/src/import/extractors/api/rails-routes.js +3 -3
- package/src/import/extractors/api/react-native-repository.js +3 -3
- package/src/import/extractors/api/retrofit.js +3 -3
- package/src/import/extractors/api/spring-web.js +3 -3
- package/src/import/extractors/api/swift-webapi.js +3 -3
- package/src/import/extractors/api/trpc.js +4 -4
- package/src/import/extractors/cli/generic.js +3 -3
- package/src/import/extractors/db/django-models.js +4 -4
- package/src/import/extractors/db/dotnet-models.js +4 -4
- package/src/import/extractors/db/drizzle.js +9 -7
- package/src/import/extractors/db/ef-core.js +5 -5
- package/src/import/extractors/db/flutter-entities.js +3 -3
- package/src/import/extractors/db/jpa.js +3 -3
- package/src/import/extractors/db/liquibase.js +3 -3
- package/src/import/extractors/db/maintained-seams.js +4 -4
- package/src/import/extractors/db/mybatis-xml.js +4 -4
- package/src/import/extractors/db/prisma.js +3 -3
- package/src/import/extractors/db/rails-schema.js +3 -3
- package/src/import/extractors/db/react-native-entities.js +3 -3
- package/src/import/extractors/db/room.js +5 -5
- package/src/import/extractors/db/snapshot.js +3 -3
- package/src/import/extractors/db/sql.js +3 -3
- package/src/import/extractors/db/swiftdata.js +3 -3
- package/src/import/extractors/ui/android-compose.js +4 -4
- package/src/import/extractors/ui/backend-only.js +3 -3
- package/src/import/extractors/ui/blazor.js +3 -3
- package/src/import/extractors/ui/flutter-screens.js +3 -3
- package/src/import/extractors/ui/maui-xaml.js +4 -4
- package/src/import/extractors/ui/next-pages-router.js +3 -3
- package/src/import/extractors/ui/razor-pages.js +3 -3
- package/src/import/extractors/ui/react-native-screens.js +4 -4
- package/src/import/extractors/ui/swiftui.js +3 -3
- package/src/import/extractors/ui/uikit.js +3 -3
- 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
|
@@ -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
|
|
@@ -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
|
+
}
|
package/src/resolver/index.js
CHANGED
|
@@ -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
|
|
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);
|
package/src/validator/common.js
CHANGED
|
@@ -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"],
|
package/src/validator/index.js
CHANGED
|
@@ -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
|
}
|