@topogram/cli 0.3.72 → 0.3.74
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/README.md +24 -195
- package/package.json +1 -1
- package/src/adoption/plan/index.js +2 -1
- package/src/agent-brief.js +46 -2
- package/src/archive/archive.js +1 -1
- package/src/archive/jsonl.js +18 -8
- package/src/archive/resolver-bridge.js +34 -1
- package/src/archive/schema.js +1 -1
- package/src/archive/unarchive.js +26 -0
- package/src/cli/command-parsers/sdlc.js +66 -0
- package/src/cli/commands/import/help.js +1 -0
- package/src/cli/commands/import/plan.js +9 -0
- package/src/cli/commands/import/workspace.js +3 -0
- package/src/cli/commands/query/definitions.js +11 -10
- package/src/cli/commands/query/workspace.js +23 -2
- package/src/cli/commands/release-rollout.js +191 -10
- package/src/cli/commands/release-shared.js +51 -2
- package/src/cli/commands/release.js +16 -3
- package/src/cli/commands/sdlc.js +213 -5
- package/src/cli/dispatcher.js +8 -0
- package/src/cli/help.js +15 -3
- package/src/cli/options.js +1 -0
- package/src/generator/context/shared/domain-sdlc.js +27 -0
- package/src/generator/context/shared/relationships.js +2 -1
- package/src/generator/context/shared/types.d.ts +1 -0
- package/src/generator/context/shared.d.ts +2 -0
- package/src/generator/context/shared.js +2 -0
- package/src/generator/context/slice/core.js +3 -0
- package/src/generator/context/slice/sdlc.js +57 -2
- package/src/generator/context/task-mode.js +7 -0
- package/src/generator/sdlc/board.js +2 -0
- package/src/generator/sdlc/traceability-matrix.js +5 -1
- package/src/import/core/context.js +1 -1
- package/src/import/core/contracts.js +3 -3
- package/src/import/core/registry.js +3 -0
- package/src/import/core/runner/candidates.js +7 -0
- package/src/import/core/runner/reports.js +9 -1
- package/src/import/core/runner/tracks.js +3 -0
- package/src/import/extractors/cli/generic.js +340 -0
- package/src/new-project/project-files.js +10 -3
- package/src/resolver/enrich/task.js +3 -1
- package/src/resolver/index.js +6 -0
- package/src/resolver/normalize.js +31 -0
- package/src/resolver/projections-cli.js +158 -0
- package/src/sdlc/adopt.js +4 -1
- package/src/sdlc/check.js +24 -2
- package/src/sdlc/complete.js +47 -0
- package/src/sdlc/dod/index.js +2 -0
- package/src/sdlc/dod/plan.js +15 -0
- package/src/sdlc/dod/task.js +7 -3
- package/src/sdlc/explain.js +53 -1
- package/src/sdlc/gate.js +352 -0
- package/src/sdlc/history.d.ts +7 -0
- package/src/sdlc/history.js +50 -5
- package/src/sdlc/link.js +172 -0
- package/src/sdlc/paths.d.ts +4 -0
- package/src/sdlc/paths.js +8 -0
- package/src/sdlc/plan-steps.js +71 -0
- package/src/sdlc/plan.js +245 -0
- package/src/sdlc/policy.js +249 -0
- package/src/sdlc/prep.js +186 -0
- package/src/sdlc/scaffold.js +4 -2
- package/src/sdlc/status-filter.js +2 -0
- package/src/sdlc/transitions/index.js +3 -0
- package/src/sdlc/transitions/plan.js +32 -0
- package/src/validator/common.js +25 -4
- package/src/validator/index.js +10 -0
- package/src/validator/kinds.d.ts +7 -0
- package/src/validator/kinds.js +32 -0
- package/src/validator/per-kind/plan.js +128 -0
- package/src/validator/per-kind/task.js +19 -0
- package/src/validator/projections/cli.js +267 -0
- package/src/validator.d.ts +1 -0
- package/src/workflows/import-app/shared.js +1 -1
- package/src/workflows/reconcile/adoption-plan/build.js +3 -1
- package/src/workflows/reconcile/adoption-plan/reasons.js +5 -0
- package/src/workflows/reconcile/bundle-core/index.js +3 -0
- package/src/workflows/reconcile/candidate-model.js +15 -0
- package/src/workflows/reconcile/gap-report.js +4 -2
- package/src/workflows/reconcile/impacts/adoption-plan.js +13 -0
- package/src/workflows/reconcile/renderers.js +82 -0
- package/src/workflows/reconcile/summary.js +4 -0
- package/src/workflows/reconcile/workflow.js +2 -1
- package/src/workspace-paths.js +26 -2
|
@@ -47,6 +47,7 @@ import { reactNativeScreensExtractor } from "../extractors/ui/react-native-scree
|
|
|
47
47
|
import { reactRouterUiExtractor } from "../extractors/ui/react-router.js";
|
|
48
48
|
import { svelteKitUiExtractor } from "../extractors/ui/sveltekit.js";
|
|
49
49
|
import { backendOnlyUiExtractor } from "../extractors/ui/backend-only.js";
|
|
50
|
+
import { genericCliExtractor } from "../extractors/cli/generic.js";
|
|
50
51
|
import { genericWorkflowExtractor } from "../extractors/workflows/generic.js";
|
|
51
52
|
import { genericVerificationExtractor } from "../extractors/verification/generic.js";
|
|
52
53
|
import { authSessionEnricher } from "../enrichers/auth-session.js";
|
|
@@ -60,6 +61,7 @@ export const extractorRegistry = {
|
|
|
60
61
|
db: [prismaExtractor, djangoModelsExtractor, efCoreExtractor, roomExtractor, swiftDataExtractor, dotnetModelsExtractor, flutterEntitiesExtractor, reactNativeEntitiesExtractor, railsSchemaExtractor, liquibaseExtractor, myBatisXmlExtractor, jpaExtractor, drizzleExtractor, sqlExtractor, snapshotExtractor],
|
|
61
62
|
api: [openApiExtractor, openApiCodeExtractor, graphQlSdlExtractor, graphQlCodeFirstExtractor, trpcExtractor, aspNetCoreExtractor, retrofitExtractor, swiftWebApiExtractor, flutterDioExtractor, reactNativeRepositoryExtractor, fastifyExtractor, expressExtractor, djangoRoutesExtractor, railsRoutesExtractor, micronautExtractor, jaxRsExtractor, springWebExtractor, nextRouteExtractor, genericRouteFallbackExtractor, nextServerActionExtractor, nextAuthExtractor],
|
|
62
63
|
ui: [nextAppRouterUiExtractor, nextPagesRouterUiExtractor, androidComposeUiExtractor, blazorUiExtractor, razorPagesUiExtractor, swiftUiExtractor, uiKitExtractor, mauiXamlUiExtractor, flutterScreensUiExtractor, reactNativeScreensExtractor, reactRouterUiExtractor, svelteKitUiExtractor, backendOnlyUiExtractor],
|
|
64
|
+
cli: [genericCliExtractor],
|
|
63
65
|
workflows: [genericWorkflowExtractor],
|
|
64
66
|
verification: [genericVerificationExtractor]
|
|
65
67
|
};
|
|
@@ -68,6 +70,7 @@ export const enricherRegistry = {
|
|
|
68
70
|
db: [railsModelEnricher],
|
|
69
71
|
api: [djangoRestEnricher, railsControllerEnricher, authSessionEnricher],
|
|
70
72
|
ui: [],
|
|
73
|
+
cli: [],
|
|
71
74
|
workflows: [workflowTargetStateEnricher, docLinkingEnricher],
|
|
72
75
|
verification: []
|
|
73
76
|
};
|
|
@@ -288,6 +288,13 @@ export function normalizeCandidatesForTrack(track, candidates) {
|
|
|
288
288
|
stacks: [...new Set(candidates.stacks || [])].sort()
|
|
289
289
|
};
|
|
290
290
|
}
|
|
291
|
+
if (track === "cli") {
|
|
292
|
+
return {
|
|
293
|
+
commands: dedupeCandidateRecords(candidates.commands || [], (/** @type {any} */ record) => record.command_id || record.id_hint),
|
|
294
|
+
capabilities: dedupeCandidateRecords(candidates.capabilities || [], idHint),
|
|
295
|
+
surfaces: dedupeCandidateRecords(candidates.surfaces || [], idHint)
|
|
296
|
+
};
|
|
297
|
+
}
|
|
291
298
|
if (track === "verification") {
|
|
292
299
|
return {
|
|
293
300
|
verifications: dedupeCandidateRecords(candidates.verifications || [], idHint),
|
|
@@ -29,6 +29,14 @@ export function reportMarkdown(track, candidates) {
|
|
|
29
29
|
`# UI Import Report\n\n- Screens: ${candidates.screens.length}\n- Routes: ${candidates.routes.length}\n- Actions: ${candidates.actions.length}\n- Widgets: ${widgets.length}\n- Event payload shapes: ${shapes.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n\n## Widget Candidates\n\n${widgetLines.length ? widgetLines.join("\n") : "- none"}\n\n## Next Validation\n\n- Review candidates under \`topo/candidates/app/ui/drafts/widgets/**\`.\n- Run \`topogram import plan <path>\` before adoption.\n- After adoption, run \`topogram check <path>\`, \`topogram widget check <path>\`, and \`topogram widget behavior <path>\`.\n`
|
|
30
30
|
);
|
|
31
31
|
}
|
|
32
|
+
if (track === "cli") {
|
|
33
|
+
const commandLines = (candidates.commands || []).map((/** @type {any} */ command) =>
|
|
34
|
+
`- \`${command.command_id || command.id_hint}\` usage \`${command.usage || "unknown"}\` effects ${(command.effects || []).map((/** @type {any} */ effect) => `\`${effect}\``).join(", ") || "_none_"}`
|
|
35
|
+
);
|
|
36
|
+
return ensureTrailingNewline(
|
|
37
|
+
`# 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`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
32
40
|
if (track === "verification") {
|
|
33
41
|
return ensureTrailingNewline(
|
|
34
42
|
`# Verification Import Report\n\n- Verifications: ${candidates.verifications.length}\n- Scenarios: ${candidates.scenarios.length}\n- Frameworks: ${candidates.frameworks.length ? candidates.frameworks.join(", ") : "none"}\n- Scripts: ${candidates.scripts.length}\n`
|
|
@@ -46,6 +54,6 @@ export function reportMarkdown(track, candidates) {
|
|
|
46
54
|
*/
|
|
47
55
|
export function appReportMarkdown(candidates, tracks) {
|
|
48
56
|
return ensureTrailingNewline(
|
|
49
|
-
`# App Import Report\n\nTracks: ${tracks.join(", ")}\n\n## DB\n\n- Entities: ${candidates.db?.entities?.length || 0}\n- Enums: ${candidates.db?.enums?.length || 0}\n- Relations: ${candidates.db?.relations?.length || 0}\n\n## API\n\n- Capabilities: ${candidates.api?.capabilities?.length || 0}\n- Routes: ${candidates.api?.routes?.length || 0}\n- Stacks: ${candidates.api?.stacks?.length ? candidates.api.stacks.join(", ") : "none"}\n\n## UI\n\n- Screens: ${candidates.ui?.screens?.length || 0}\n- Routes: ${candidates.ui?.routes?.length || 0}\n- Actions: ${candidates.ui?.actions?.length || 0}\n- Widgets: ${uiWidgetCandidates(candidates.ui).length}\n- Event payload shapes: ${candidates.ui?.shapes?.length || 0}\n- Stacks: ${candidates.ui?.stacks?.length ? candidates.ui.stacks.join(", ") : "none"}\n\n## Workflows\n\n- Workflows: ${candidates.workflows?.workflows?.length || 0}\n- States: ${candidates.workflows?.workflow_states?.length || 0}\n- Transitions: ${candidates.workflows?.workflow_transitions?.length || 0}\n\n## Verification\n\n- Verifications: ${candidates.verification?.verifications?.length || 0}\n- Scenarios: ${candidates.verification?.scenarios?.length || 0}\n- Frameworks: ${candidates.verification?.frameworks?.length ? candidates.verification.frameworks.join(", ") : "none"}\n- Scripts: ${candidates.verification?.scripts?.length || 0}\n`
|
|
57
|
+
`# App Import Report\n\nTracks: ${tracks.join(", ")}\n\n## DB\n\n- Entities: ${candidates.db?.entities?.length || 0}\n- Enums: ${candidates.db?.enums?.length || 0}\n- Relations: ${candidates.db?.relations?.length || 0}\n\n## API\n\n- Capabilities: ${candidates.api?.capabilities?.length || 0}\n- Routes: ${candidates.api?.routes?.length || 0}\n- Stacks: ${candidates.api?.stacks?.length ? candidates.api.stacks.join(", ") : "none"}\n\n## UI\n\n- Screens: ${candidates.ui?.screens?.length || 0}\n- Routes: ${candidates.ui?.routes?.length || 0}\n- Actions: ${candidates.ui?.actions?.length || 0}\n- Widgets: ${uiWidgetCandidates(candidates.ui).length}\n- Event payload shapes: ${candidates.ui?.shapes?.length || 0}\n- Stacks: ${candidates.ui?.stacks?.length ? candidates.ui.stacks.join(", ") : "none"}\n\n## CLI\n\n- Commands: ${candidates.cli?.commands?.length || 0}\n- Capabilities: ${candidates.cli?.capabilities?.length || 0}\n- CLI surfaces: ${candidates.cli?.surfaces?.length || 0}\n\n## Workflows\n\n- Workflows: ${candidates.workflows?.workflows?.length || 0}\n- States: ${candidates.workflows?.workflow_states?.length || 0}\n- Transitions: ${candidates.workflows?.workflow_transitions?.length || 0}\n\n## Verification\n\n- Verifications: ${candidates.verification?.verifications?.length || 0}\n- Scenarios: ${candidates.verification?.scenarios?.length || 0}\n- Frameworks: ${candidates.verification?.frameworks?.length ? candidates.verification.frameworks.join(", ") : "none"}\n- Scripts: ${candidates.verification?.scripts?.length || 0}\n`
|
|
50
58
|
);
|
|
51
59
|
}
|
|
@@ -104,6 +104,9 @@ function initialCandidatesForTrack(track) {
|
|
|
104
104
|
if (track === "ui") {
|
|
105
105
|
return { screens: [], routes: [], actions: [], stacks: [] };
|
|
106
106
|
}
|
|
107
|
+
if (track === "cli") {
|
|
108
|
+
return { commands: [], capabilities: [], surfaces: [] };
|
|
109
|
+
}
|
|
107
110
|
if (track === "verification") {
|
|
108
111
|
return { verifications: [], scenarios: [], frameworks: [], scripts: [] };
|
|
109
112
|
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
findImportFiles,
|
|
5
|
+
idHintify,
|
|
6
|
+
makeCandidateRecord,
|
|
7
|
+
normalizeImportRelativePath,
|
|
8
|
+
readJsonIfExists,
|
|
9
|
+
readTextIfExists,
|
|
10
|
+
titleCase
|
|
11
|
+
} from "../../core/shared.js";
|
|
12
|
+
|
|
13
|
+
const CLI_SOURCE_PATTERN = /(^|\/)(bin|cli|command|commands|parser|help)(\/|[-_.A-Za-z0-9]*\.(?:js|mjs|cjs|ts))$/i;
|
|
14
|
+
const JS_SOURCE_PATTERN = /\.(?:js|mjs|cjs|ts)$/i;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} value
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
function normalizePath(value) {
|
|
21
|
+
return value.replaceAll("\\", "/");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} commandId
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function capabilityIdForCommand(commandId) {
|
|
29
|
+
return `cap_${idHintify(commandId)}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} text
|
|
34
|
+
* @returns {string[]}
|
|
35
|
+
*/
|
|
36
|
+
function extractUsageLines(text) {
|
|
37
|
+
const lines = [];
|
|
38
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
39
|
+
const line = rawLine
|
|
40
|
+
.replace(/^\s*(?:console\.log\(|print\(|echo\s+)?["'`]*/, "")
|
|
41
|
+
.replace(/["'`),;]*\s*$/, "")
|
|
42
|
+
.trim();
|
|
43
|
+
if (!line) continue;
|
|
44
|
+
const usageMatch = line.match(/(?:^|\b)Usage:\s*(.+)$/i);
|
|
45
|
+
if (usageMatch?.[1]) {
|
|
46
|
+
lines.push(usageMatch[1].trim());
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (/^[a-zA-Z][\w.-]+(?:\s+[a-zA-Z][\w:-]+)+(?:\s|$)/.test(line) && /(?:--[a-zA-Z][\w:-]*|<[^>]+>|\[[^\]]+\])/.test(line)) {
|
|
50
|
+
lines.push(line);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return [...new Set(lines)].sort();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {string} usage
|
|
58
|
+
* @param {Set<string>} binNames
|
|
59
|
+
* @returns {{ id: string, commandPath: string[] } | null}
|
|
60
|
+
*/
|
|
61
|
+
function commandIdFromUsage(usage, binNames) {
|
|
62
|
+
const tokens = usage.split(/\s+/).filter(Boolean);
|
|
63
|
+
if (tokens.length === 0) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const firstCommandToken = binNames.has(tokens[0]) ? 1 : 0;
|
|
67
|
+
const commandPath = [];
|
|
68
|
+
for (let index = firstCommandToken; index < tokens.length; index += 1) {
|
|
69
|
+
const token = tokens[index];
|
|
70
|
+
if (!token || token.startsWith("-") || token.startsWith("[") || token.startsWith("<")) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
commandPath.push(token.replace(/[^a-zA-Z0-9:_-]/g, ""));
|
|
74
|
+
}
|
|
75
|
+
if (commandPath.length === 0) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
id: idHintify(commandPath.join("_")),
|
|
80
|
+
commandPath
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {string} usage
|
|
86
|
+
* @param {string} commandId
|
|
87
|
+
* @returns {any[]}
|
|
88
|
+
*/
|
|
89
|
+
function optionsFromUsage(usage, commandId) {
|
|
90
|
+
const options = [];
|
|
91
|
+
const optionPattern = /--([a-zA-Z][\w:-]*)(?:\s+(<[^>]+>|\[[^\]]+\]))?/g;
|
|
92
|
+
for (const match of usage.matchAll(optionPattern)) {
|
|
93
|
+
const optionName = idHintify(match[1]);
|
|
94
|
+
options.push({
|
|
95
|
+
command_id: commandId,
|
|
96
|
+
name: optionName,
|
|
97
|
+
flag: `--${match[1]}`,
|
|
98
|
+
type: match[2] ? "string" : "boolean",
|
|
99
|
+
required: false,
|
|
100
|
+
values: [],
|
|
101
|
+
description: null
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return options;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {string} commandId
|
|
109
|
+
* @param {string} usage
|
|
110
|
+
* @param {string} sourceText
|
|
111
|
+
* @returns {string[]}
|
|
112
|
+
*/
|
|
113
|
+
function effectsForCommand(commandId, usage, sourceText) {
|
|
114
|
+
const text = `${commandId} ${usage}`.toLowerCase();
|
|
115
|
+
const effects = new Set();
|
|
116
|
+
if (/\b(check|list|show|help|doctor|status|brief|explain|query|emit)\b/.test(text)) {
|
|
117
|
+
effects.add("read_only");
|
|
118
|
+
}
|
|
119
|
+
if (/\b(import|adopt|new|generate|refresh|update|write|create|copy)\b/.test(text)) {
|
|
120
|
+
effects.add("writes_workspace");
|
|
121
|
+
effects.add("filesystem");
|
|
122
|
+
}
|
|
123
|
+
if (/\b(fetch|https?:\/\/|github|gh_token|github_token|network|catalog)\b/.test(text)) {
|
|
124
|
+
effects.add("network");
|
|
125
|
+
}
|
|
126
|
+
if (/\b(npm|package|install|publish|pack)\b/.test(text)) {
|
|
127
|
+
effects.add("package_install");
|
|
128
|
+
}
|
|
129
|
+
if (/\b(git|worktree|commit|push|pull|checkout|branch)\b/.test(text)) {
|
|
130
|
+
effects.add("git");
|
|
131
|
+
}
|
|
132
|
+
if (effects.size === 0) {
|
|
133
|
+
effects.add("read_only");
|
|
134
|
+
}
|
|
135
|
+
return [...effects].sort();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {string} commandId
|
|
140
|
+
* @param {any[]} options
|
|
141
|
+
* @returns {any[]}
|
|
142
|
+
*/
|
|
143
|
+
function outputsForCommand(commandId, options) {
|
|
144
|
+
const hasJson = options.some((option) => option.command_id === commandId && option.name === "json");
|
|
145
|
+
return [
|
|
146
|
+
...(hasJson ? [{ command_id: commandId, format: "json", schema_id: null, description: "Machine-readable JSON output" }] : []),
|
|
147
|
+
{ command_id: commandId, format: "human", schema_id: null, description: "Human-readable terminal output" }
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {any[]} records
|
|
153
|
+
* @param {(record: any) => string} keyFn
|
|
154
|
+
* @returns {any[]}
|
|
155
|
+
*/
|
|
156
|
+
function dedupeRecords(records, keyFn) {
|
|
157
|
+
const seen = new Set();
|
|
158
|
+
const deduped = [];
|
|
159
|
+
for (const record of records) {
|
|
160
|
+
const key = keyFn(record);
|
|
161
|
+
if (seen.has(key)) continue;
|
|
162
|
+
seen.add(key);
|
|
163
|
+
deduped.push(record);
|
|
164
|
+
}
|
|
165
|
+
return deduped;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {any} context
|
|
170
|
+
* @returns {{ packageFiles: string[], sourceFiles: string[] }}
|
|
171
|
+
*/
|
|
172
|
+
function discoverCliSources(context) {
|
|
173
|
+
const packageFiles = findImportFiles(context.paths, (/** @type {string} */ filePath) => /package\.json$/i.test(filePath));
|
|
174
|
+
const sourceFiles = findImportFiles(context.paths, (/** @type {string} */ filePath) => {
|
|
175
|
+
const normalized = normalizePath(filePath);
|
|
176
|
+
return JS_SOURCE_PATTERN.test(normalized) && (
|
|
177
|
+
CLI_SOURCE_PATTERN.test(normalized) ||
|
|
178
|
+
normalized.includes("/src/cli/") ||
|
|
179
|
+
normalized.includes("/commands/")
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
return { packageFiles, sourceFiles };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export const genericCliExtractor = {
|
|
186
|
+
id: "cli.generic",
|
|
187
|
+
track: "cli",
|
|
188
|
+
/** @param {any} context */
|
|
189
|
+
detect(context) {
|
|
190
|
+
const { packageFiles, sourceFiles } = discoverCliSources(context);
|
|
191
|
+
const hasBin = packageFiles.some((filePath) => {
|
|
192
|
+
const pkg = readJsonIfExists(filePath);
|
|
193
|
+
return Boolean(pkg?.bin);
|
|
194
|
+
});
|
|
195
|
+
const score = (hasBin ? 70 : 0) + Math.min(30, sourceFiles.length * 5);
|
|
196
|
+
return {
|
|
197
|
+
score,
|
|
198
|
+
reasons: [
|
|
199
|
+
hasBin ? "package.json declares a CLI bin" : null,
|
|
200
|
+
sourceFiles.length ? `${sourceFiles.length} CLI-like source files found` : null
|
|
201
|
+
].filter(Boolean)
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
/** @param {any} context */
|
|
205
|
+
extract(context) {
|
|
206
|
+
const { packageFiles, sourceFiles } = discoverCliSources(context);
|
|
207
|
+
const findings = [];
|
|
208
|
+
const commands = [];
|
|
209
|
+
const options = [];
|
|
210
|
+
const outputs = [];
|
|
211
|
+
const effects = [];
|
|
212
|
+
const examples = [];
|
|
213
|
+
const capabilities = [];
|
|
214
|
+
const binNames = new Set();
|
|
215
|
+
const provenance = [];
|
|
216
|
+
|
|
217
|
+
for (const packagePath of packageFiles) {
|
|
218
|
+
const pkg = readJsonIfExists(packagePath);
|
|
219
|
+
if (!pkg) continue;
|
|
220
|
+
const relPath = normalizeImportRelativePath(context.paths, packagePath);
|
|
221
|
+
const bin = pkg.bin;
|
|
222
|
+
if (typeof bin === "string") {
|
|
223
|
+
binNames.add(pkg.name ? String(pkg.name).split("/").pop() : "cli");
|
|
224
|
+
} else if (bin && typeof bin === "object") {
|
|
225
|
+
for (const name of Object.keys(bin)) {
|
|
226
|
+
binNames.add(name);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
for (const [name, command] of Object.entries(pkg.scripts || {})) {
|
|
230
|
+
if (/^(cli|bin|start|check|test|verify)(:|$)/.test(name) || /\b(node|tsx|ts-node)\b.+\b(cli|bin)\b/i.test(String(command))) {
|
|
231
|
+
findings.push({
|
|
232
|
+
kind: "cli_script",
|
|
233
|
+
name,
|
|
234
|
+
command,
|
|
235
|
+
source: relPath
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
provenance.push(`${relPath}#bin`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (const sourcePath of sourceFiles) {
|
|
243
|
+
const sourceText = readTextIfExists(sourcePath) || "";
|
|
244
|
+
const relPath = normalizeImportRelativePath(context.paths, sourcePath);
|
|
245
|
+
const usageLines = extractUsageLines(sourceText);
|
|
246
|
+
if (usageLines.length === 0) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
findings.push({
|
|
250
|
+
kind: "cli_usage",
|
|
251
|
+
source: relPath,
|
|
252
|
+
usages: usageLines
|
|
253
|
+
});
|
|
254
|
+
for (const usage of usageLines) {
|
|
255
|
+
const command = commandIdFromUsage(usage, binNames);
|
|
256
|
+
if (!command) continue;
|
|
257
|
+
const commandOptions = optionsFromUsage(usage, command.id);
|
|
258
|
+
const commandEffects = effectsForCommand(command.id, usage, sourceText);
|
|
259
|
+
const capabilityId = capabilityIdForCommand(command.id);
|
|
260
|
+
const commandProvenance = [`${relPath}#${usage}`];
|
|
261
|
+
commands.push(makeCandidateRecord({
|
|
262
|
+
kind: "cli_command",
|
|
263
|
+
idHint: `cmd_${command.id}`,
|
|
264
|
+
label: titleCase(command.commandPath.join(" ")),
|
|
265
|
+
confidence: "medium",
|
|
266
|
+
sourceKind: "cli_usage",
|
|
267
|
+
sourceOfTruth: "candidate",
|
|
268
|
+
provenance: commandProvenance,
|
|
269
|
+
track: "cli",
|
|
270
|
+
command_id: command.id,
|
|
271
|
+
command_path: command.commandPath,
|
|
272
|
+
capability_id: capabilityId,
|
|
273
|
+
usage,
|
|
274
|
+
mode: commandEffects.includes("writes_workspace") ? "writes_workspace" : commandEffects[0],
|
|
275
|
+
options: commandOptions.map((option) => option.name),
|
|
276
|
+
effects: commandEffects
|
|
277
|
+
}));
|
|
278
|
+
capabilities.push(makeCandidateRecord({
|
|
279
|
+
kind: "capability",
|
|
280
|
+
idHint: capabilityId,
|
|
281
|
+
label: titleCase(command.commandPath.join(" ")),
|
|
282
|
+
confidence: "medium",
|
|
283
|
+
sourceKind: "cli_command",
|
|
284
|
+
sourceOfTruth: "candidate",
|
|
285
|
+
provenance: commandProvenance,
|
|
286
|
+
track: "cli",
|
|
287
|
+
command_id: command.id
|
|
288
|
+
}));
|
|
289
|
+
options.push(...commandOptions.map((option) => ({
|
|
290
|
+
...option,
|
|
291
|
+
provenance: commandProvenance
|
|
292
|
+
})));
|
|
293
|
+
outputs.push(...outputsForCommand(command.id, commandOptions).map((output) => ({
|
|
294
|
+
...output,
|
|
295
|
+
provenance: commandProvenance
|
|
296
|
+
})));
|
|
297
|
+
effects.push(...commandEffects.map((effect) => ({
|
|
298
|
+
command_id: command.id,
|
|
299
|
+
effect,
|
|
300
|
+
target: effect === "read_only" ? "workspace" : null,
|
|
301
|
+
provenance: commandProvenance
|
|
302
|
+
})));
|
|
303
|
+
examples.push({
|
|
304
|
+
command_id: command.id,
|
|
305
|
+
example: usage,
|
|
306
|
+
provenance: commandProvenance
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const surfaceCommands = dedupeRecords(commands, (record) => record.command_id);
|
|
312
|
+
const surfaces = surfaceCommands.length > 0
|
|
313
|
+
? [makeCandidateRecord({
|
|
314
|
+
kind: "cli_surface",
|
|
315
|
+
idHint: "proj_cli_surface",
|
|
316
|
+
label: "CLI Surface",
|
|
317
|
+
confidence: "medium",
|
|
318
|
+
sourceKind: "cli_usage",
|
|
319
|
+
sourceOfTruth: "candidate",
|
|
320
|
+
provenance,
|
|
321
|
+
track: "cli",
|
|
322
|
+
commands: surfaceCommands.map((record) => record.command_id),
|
|
323
|
+
command_records: surfaceCommands,
|
|
324
|
+
options: dedupeRecords(options, (record) => `${record.command_id}:${record.name}`),
|
|
325
|
+
outputs: dedupeRecords(outputs, (record) => `${record.command_id}:${record.format}`),
|
|
326
|
+
effects: dedupeRecords(effects, (record) => `${record.command_id}:${record.effect}`),
|
|
327
|
+
examples: dedupeRecords(examples, (record) => `${record.command_id}:${record.example}`)
|
|
328
|
+
})]
|
|
329
|
+
: [];
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
findings,
|
|
333
|
+
candidates: {
|
|
334
|
+
commands: surfaceCommands,
|
|
335
|
+
capabilities: dedupeRecords(capabilities, (record) => record.id_hint),
|
|
336
|
+
surfaces
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
};
|
|
@@ -285,9 +285,10 @@ Start here before editing this Topogram project.
|
|
|
285
285
|
1. \`AGENTS.md\`
|
|
286
286
|
2. \`README.md\`
|
|
287
287
|
3. \`topogram.project.json\`
|
|
288
|
-
4. \`topogram.
|
|
289
|
-
5. \`topogram.
|
|
290
|
-
|
|
288
|
+
4. \`topogram.sdlc-policy.json\`, if this project has adopted SDLC enforcement
|
|
289
|
+
5. \`topogram.template-policy.json\`
|
|
290
|
+
6. \`topogram.generator-policy.json\`
|
|
291
|
+
${hasImplementation ? "7. `.topogram-template-trust.json`\n8. `implementation/`\n9. Focused `topogram query ...` output\n" : "7. Focused `topogram query ...` output\n"}
|
|
291
292
|
Machine-readable source:
|
|
292
293
|
|
|
293
294
|
\`\`\`bash
|
|
@@ -310,6 +311,8 @@ npm run doctor
|
|
|
310
311
|
npm run source:status
|
|
311
312
|
npm run template:explain
|
|
312
313
|
npm run generator:policy:check
|
|
314
|
+
topogram sdlc policy explain --json
|
|
315
|
+
topogram sdlc explain <task-or-bug-id> --json
|
|
313
316
|
${hasImplementation ? "npm run trust:status\n" : ""}npm run check
|
|
314
317
|
npm run query:list
|
|
315
318
|
npm run query:show -- widget-behavior
|
|
@@ -318,6 +321,10 @@ npm run query:show -- widget-behavior
|
|
|
318
321
|
## Edit Rules
|
|
319
322
|
|
|
320
323
|
- Edit \`topo/**\` and \`topogram.project.json\` first.
|
|
324
|
+
- If \`topogram.sdlc-policy.json\` exists, protected changes need an SDLC item, a \`topo/sdlc/**\` SDLC record update, or an allowed exemption.
|
|
325
|
+
- Adopted SDLC records default to \`topo/sdlc/**\`; custom folders can still validate, but agents should look there first.
|
|
326
|
+
- Status, history, archives, trust hashes, provenance, generated sentinels, and release/rollout state are command-owned. Use Topogram commands instead of hand-editing those sidecars.
|
|
327
|
+
- Plans are optional support records. Agents may edit plan text directly, but should use \`topogram sdlc plan step ... --write\` for step status changes.
|
|
321
328
|
- Review policy files before editing \`topogram.template-policy.json\` or \`topogram.generator-policy.json\`.
|
|
322
329
|
- Do not make lasting edits under generated-owned \`app/**\`; use \`npm run generate\` to replace generated output.
|
|
323
330
|
- If an output is changed to maintained ownership, agents may edit that app code directly after reading focused query packets.
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// Back-link arrays:
|
|
5
5
|
// blockingMe — tasks whose `blocks` references this task (reciprocal of blocked_by)
|
|
6
6
|
// blockedByMe — tasks whose `blocked_by` references this task (reciprocal of blocks)
|
|
7
|
+
// plans — implementation plans attached to this task
|
|
7
8
|
//
|
|
8
9
|
// Note: we deliberately compute *both* directions even though `blocks` and
|
|
9
10
|
// `blocked_by` are reciprocal, because authors only write one side. The
|
|
@@ -13,6 +14,7 @@
|
|
|
13
14
|
export function enrichTask(task, index) {
|
|
14
15
|
return {
|
|
15
16
|
blockingMe: (index.tasksThatBlockTarget.get(task.id) || []).slice().sort(),
|
|
16
|
-
blockedByMe: (index.tasksBlockedByTarget.get(task.id) || []).slice().sort()
|
|
17
|
+
blockedByMe: (index.tasksBlockedByTarget.get(task.id) || []).slice().sort(),
|
|
18
|
+
plans: (index.plansByTask.get(task.id) || []).slice().sort()
|
|
17
19
|
};
|
|
18
20
|
}
|
package/src/resolver/index.js
CHANGED
|
@@ -114,6 +114,7 @@ export function resolveWorkspace(workspaceAst) {
|
|
|
114
114
|
pitches: [],
|
|
115
115
|
requirements: [],
|
|
116
116
|
tasks: [],
|
|
117
|
+
plans: [],
|
|
117
118
|
bugs: [],
|
|
118
119
|
documents: []
|
|
119
120
|
});
|
|
@@ -130,6 +131,7 @@ export function resolveWorkspace(workspaceAst) {
|
|
|
130
131
|
pitch: "pitches",
|
|
131
132
|
requirement: "requirements",
|
|
132
133
|
task: "tasks",
|
|
134
|
+
plan: "plans",
|
|
133
135
|
bug: "bugs"
|
|
134
136
|
};
|
|
135
137
|
for (const statement of resolvedStatements) {
|
|
@@ -175,6 +177,7 @@ export function resolveWorkspace(workspaceAst) {
|
|
|
175
177
|
rulesByFromRequirement: new Map(),
|
|
176
178
|
tasksThatBlockTarget: new Map(),
|
|
177
179
|
tasksBlockedByTarget: new Map(),
|
|
180
|
+
plansByTask: new Map(),
|
|
178
181
|
affectedByPitches: new Map(),
|
|
179
182
|
affectedByRequirements: new Map(),
|
|
180
183
|
affectedByTasks: new Map(),
|
|
@@ -227,6 +230,9 @@ export function resolveWorkspace(workspaceAst) {
|
|
|
227
230
|
pushIndexFromList(sdlcIndex.tasksThatBlockTarget, statement.blocks, statement.id);
|
|
228
231
|
pushIndexFromList(sdlcIndex.tasksBlockedByTarget, statement.blockedBy, statement.id);
|
|
229
232
|
break;
|
|
233
|
+
case "plan":
|
|
234
|
+
pushIndex(sdlcIndex.plansByTask, statement.task?.id, statement.id);
|
|
235
|
+
break;
|
|
230
236
|
case "bug":
|
|
231
237
|
pushIndexFromList(sdlcIndex.affectedByBugs, statement.affects, statement.id);
|
|
232
238
|
pushIndexFromList(sdlcIndex.rulesViolatedByBug, statement.violates, statement.id);
|
|
@@ -43,6 +43,13 @@ import {
|
|
|
43
43
|
parseProjectionHttpResponsesBlock,
|
|
44
44
|
parseProjectionHttpStatusBlock
|
|
45
45
|
} from "./projections-api.js";
|
|
46
|
+
import {
|
|
47
|
+
parseProjectionCliCommandsBlock,
|
|
48
|
+
parseProjectionCliEffectsBlock,
|
|
49
|
+
parseProjectionCliExamplesBlock,
|
|
50
|
+
parseProjectionCliOptionsBlock,
|
|
51
|
+
parseProjectionCliOutputsBlock
|
|
52
|
+
} from "./projections-cli.js";
|
|
46
53
|
import {
|
|
47
54
|
parseProjectionUiActionsBlock,
|
|
48
55
|
parseProjectionUiAppShellBlock,
|
|
@@ -67,6 +74,7 @@ import {
|
|
|
67
74
|
parseProjectionDbTablesBlock,
|
|
68
75
|
parseProjectionGeneratorDefaultsBlock
|
|
69
76
|
} from "./projections-db.js";
|
|
77
|
+
import { parsePlanSteps } from "../sdlc/plan-steps.js";
|
|
70
78
|
|
|
71
79
|
export function normalizeStatement(statement, registry) {
|
|
72
80
|
const fieldMap = collectFieldMap(statement);
|
|
@@ -209,6 +217,11 @@ export function normalizeStatement(statement, registry) {
|
|
|
209
217
|
httpDownload: parseProjectionHttpDownloadBlock(statement, registry),
|
|
210
218
|
httpAuthz: parseProjectionHttpAuthzBlock(statement, registry),
|
|
211
219
|
httpCallbacks: parseProjectionHttpCallbacksBlock(statement, registry),
|
|
220
|
+
commands: parseProjectionCliCommandsBlock(statement, registry),
|
|
221
|
+
commandOptions: parseProjectionCliOptionsBlock(statement),
|
|
222
|
+
commandOutputs: parseProjectionCliOutputsBlock(statement, registry),
|
|
223
|
+
commandEffects: parseProjectionCliEffectsBlock(statement),
|
|
224
|
+
commandExamples: parseProjectionCliExamplesBlock(statement),
|
|
212
225
|
uiScreens: parseProjectionUiScreensBlock(statement, registry),
|
|
213
226
|
screens: parseProjectionUiScreensBlock(statement, registry),
|
|
214
227
|
uiCollections: parseProjectionUiCollectionsBlock(statement),
|
|
@@ -343,9 +356,11 @@ export function normalizeStatement(statement, registry) {
|
|
|
343
356
|
...base,
|
|
344
357
|
priority: symbolValue(getFieldValue(statement, "priority")),
|
|
345
358
|
workType: symbolValue(getFieldValue(statement, "work_type")),
|
|
359
|
+
disposition: symbolValue(getFieldValue(statement, "disposition")),
|
|
346
360
|
affects: resolveReferenceList(registry, getFieldValue(statement, "affects")),
|
|
347
361
|
satisfies: resolveReferenceList(registry, getFieldValue(statement, "satisfies")),
|
|
348
362
|
acceptanceRefs: resolveReferenceList(registry, getFieldValue(statement, "acceptance_refs")),
|
|
363
|
+
verificationRefs: resolveReferenceList(registry, getFieldValue(statement, "verification_refs")),
|
|
349
364
|
blocks: resolveReferenceList(registry, getFieldValue(statement, "blocks")),
|
|
350
365
|
blockedBy: resolveReferenceList(registry, getFieldValue(statement, "blocked_by")),
|
|
351
366
|
claimedBy: resolveReferenceList(registry, getFieldValue(statement, "claimed_by")),
|
|
@@ -356,6 +371,22 @@ export function normalizeStatement(statement, registry) {
|
|
|
356
371
|
updated: stringValue(getFieldValue(statement, "updated")),
|
|
357
372
|
resolvedDomain: resolveDomainTag(statement, registry)
|
|
358
373
|
};
|
|
374
|
+
case "plan":
|
|
375
|
+
return {
|
|
376
|
+
...base,
|
|
377
|
+
task: getFieldValue(statement, "task")
|
|
378
|
+
? {
|
|
379
|
+
id: symbolValue(getFieldValue(statement, "task")),
|
|
380
|
+
target: toRef(resolveReference(registry, symbolValue(getFieldValue(statement, "task"))))
|
|
381
|
+
}
|
|
382
|
+
: null,
|
|
383
|
+
priority: symbolValue(getFieldValue(statement, "priority")),
|
|
384
|
+
notes: stringValue(getFieldValue(statement, "notes")),
|
|
385
|
+
outcome: stringValue(getFieldValue(statement, "outcome")),
|
|
386
|
+
steps: parsePlanSteps(getFieldValue(statement, "steps")),
|
|
387
|
+
updated: stringValue(getFieldValue(statement, "updated")),
|
|
388
|
+
resolvedDomain: resolveDomainTag(statement, registry)
|
|
389
|
+
};
|
|
359
390
|
case "bug":
|
|
360
391
|
return {
|
|
361
392
|
...base,
|