@topogram/cli 0.3.78 → 0.3.80
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-parser.js +2 -0
- package/src/cli/command-parsers/core.js +9 -5
- package/src/cli/command-parsers/extractor.js +40 -0
- 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/extractor.js +451 -0
- 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 +45 -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 +24 -18
- package/src/cli/commands/import-runner.js +10 -7
- 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 +32 -3
- package/src/cli/help-dispatch.js +33 -8
- package/src/cli/help.js +79 -52
- package/src/cli/migration-guidance.js +9 -0
- package/src/cli/options.js +17 -0
- package/src/extractor/check.js +155 -0
- package/src/extractor/packages.js +295 -0
- package/src/extractor/registry.js +196 -0
- package/src/extractor-policy.js +249 -0
- package/src/generator/check.js +24 -87
- 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/generator/registry/index.js +16 -75
- package/src/generator-policy.js +9 -57
- package/src/import/core/registry.d.ts +3 -0
- package/src/import/core/registry.js +82 -8
- package/src/import/core/runner/reports.js +4 -4
- package/src/import/core/runner/run.js +2 -0
- package/src/import/core/runner/tracks.js +66 -4
- package/src/import/provenance.js +18 -17
- 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/package-adapters/adapter.js +64 -0
- package/src/package-adapters/file-map.js +30 -0
- package/src/package-adapters/index.js +27 -0
- package/src/package-adapters/manifest.js +108 -0
- package/src/package-adapters/policy.js +81 -0
- package/src/package-adapters/spec.js +51 -0
- 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
|
@@ -49,9 +49,16 @@ import {
|
|
|
49
49
|
export function journeySlice(graph, journeyId) {
|
|
50
50
|
const journey = getJourneyDoc(graph, journeyId);
|
|
51
51
|
const capabilities = [...(journey.relatedCapabilities || [])].sort();
|
|
52
|
+
const entities = [...(journey.relatedEntities || [])].sort();
|
|
53
|
+
const rules = [...(journey.relatedRules || [])].sort();
|
|
52
54
|
const workflows = [...(journey.relatedWorkflows || [])].sort();
|
|
53
55
|
const projections = [...(journey.relatedProjections || [])].sort();
|
|
54
|
-
const
|
|
56
|
+
const widgets = [...(journey.relatedWidgets || [])].sort();
|
|
57
|
+
const declaredVerifications = [...(journey.relatedVerifications || [])].sort();
|
|
58
|
+
const verifications = [...new Set([
|
|
59
|
+
...declaredVerifications,
|
|
60
|
+
...verificationIdsForTarget(graph, [...capabilities, ...workflows, ...projections, ...widgets, journeyId])
|
|
61
|
+
])].sort();
|
|
55
62
|
|
|
56
63
|
return {
|
|
57
64
|
type: "context_slice",
|
|
@@ -63,14 +70,36 @@ export function journeySlice(graph, journeyId) {
|
|
|
63
70
|
summary: summarizeById(graph, journeyId),
|
|
64
71
|
depends_on: {
|
|
65
72
|
capabilities,
|
|
73
|
+
entities,
|
|
74
|
+
rules,
|
|
66
75
|
workflows,
|
|
67
76
|
projections,
|
|
77
|
+
widgets,
|
|
68
78
|
verifications
|
|
69
79
|
},
|
|
80
|
+
steps: (journey.steps || []).map(/** @param {any} step */ (step) => ({
|
|
81
|
+
id: step.id,
|
|
82
|
+
intent: step.intent,
|
|
83
|
+
commands: [...(step.commands || [])],
|
|
84
|
+
expects: [...(step.expects || [])],
|
|
85
|
+
after: [...(step.after || [])],
|
|
86
|
+
notes: step.notes || null
|
|
87
|
+
})),
|
|
88
|
+
alternates: (journey.alternates || []).map(/** @param {any} alternate */ (alternate) => ({
|
|
89
|
+
id: alternate.id,
|
|
90
|
+
from: alternate.from,
|
|
91
|
+
condition: alternate.condition,
|
|
92
|
+
commands: [...(alternate.commands || [])],
|
|
93
|
+
expects: [...(alternate.expects || [])],
|
|
94
|
+
notes: alternate.notes || null
|
|
95
|
+
})),
|
|
70
96
|
related: {
|
|
71
97
|
capabilities: summarizeStatementsByIds(graph, capabilities),
|
|
98
|
+
entities: summarizeStatementsByIds(graph, entities),
|
|
99
|
+
rules: summarizeStatementsByIds(graph, rules),
|
|
72
100
|
workflows: summarizeDocsByIds(graph, workflows),
|
|
73
|
-
projections: summarizeStatementsByIds(graph, projections)
|
|
101
|
+
projections: summarizeStatementsByIds(graph, projections),
|
|
102
|
+
widgets: summarizeStatementsByIds(graph, widgets)
|
|
74
103
|
},
|
|
75
104
|
verification: summarizeStatementsByIds(graph, verifications),
|
|
76
105
|
verification_targets: recommendedVerificationTargets(graph, [...capabilities, ...workflows, ...projections, journeyId], {
|
|
@@ -325,7 +325,7 @@ function importAdoptMode(graph, options = {}) {
|
|
|
325
325
|
return {
|
|
326
326
|
type: "context_task_mode",
|
|
327
327
|
version: 1,
|
|
328
|
-
mode: "
|
|
328
|
+
mode: "extract-adopt",
|
|
329
329
|
summary: {
|
|
330
330
|
focus: "Proposal review and adoption planning",
|
|
331
331
|
preferred_start: "adoption-plan.agent.json",
|
|
@@ -450,7 +450,7 @@ function verificationMode(graph, options = {}) {
|
|
|
450
450
|
export function generateContextTaskMode(graph, options = {}) {
|
|
451
451
|
const mode = String(options.modeId || "").trim();
|
|
452
452
|
if (!mode) {
|
|
453
|
-
throw new Error("context-task-mode requires --mode <modeling|maintained-app-edit|
|
|
453
|
+
throw new Error("context-task-mode requires --mode <modeling|maintained-app-edit|extract-adopt|diff-review|verification>");
|
|
454
454
|
}
|
|
455
455
|
|
|
456
456
|
if (mode === "modeling") {
|
|
@@ -459,7 +459,7 @@ export function generateContextTaskMode(graph, options = {}) {
|
|
|
459
459
|
if (mode === "maintained-app-edit") {
|
|
460
460
|
return maintainedAppEditMode(graph, options);
|
|
461
461
|
}
|
|
462
|
-
if (mode === "
|
|
462
|
+
if (mode === "extract-adopt") {
|
|
463
463
|
return importAdoptMode(graph, options);
|
|
464
464
|
}
|
|
465
465
|
if (mode === "diff-review") {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import {
|
|
4
|
+
loadPackageManifest,
|
|
5
|
+
packageInstallCommand,
|
|
6
|
+
packageInstallHint,
|
|
7
|
+
resolvePackageManifestPath
|
|
8
|
+
} from "../../package-adapters/index.js";
|
|
6
9
|
import { UI_GENERATOR_RENDERED_COMPONENT_PATTERNS } from "../../ui/taxonomy.js";
|
|
7
10
|
|
|
8
11
|
/**
|
|
@@ -208,20 +211,12 @@ export function getGeneratorManifest(generatorId) {
|
|
|
208
211
|
return GENERATOR_BY_ID.get(generatorId) || null;
|
|
209
212
|
}
|
|
210
213
|
|
|
211
|
-
/**
|
|
212
|
-
* @param {string|null|undefined} rootDir
|
|
213
|
-
* @returns {string}
|
|
214
|
-
*/
|
|
215
|
-
function packageResolutionBase(rootDir) {
|
|
216
|
-
return path.join(rootDir || process.cwd(), "package.json");
|
|
217
|
-
}
|
|
218
|
-
|
|
219
214
|
/**
|
|
220
215
|
* @param {string|null|undefined} packageName
|
|
221
216
|
* @returns {string|null}
|
|
222
217
|
*/
|
|
223
218
|
export function packageGeneratorInstallCommand(packageName) {
|
|
224
|
-
return packageName
|
|
219
|
+
return packageInstallCommand(packageName);
|
|
225
220
|
}
|
|
226
221
|
|
|
227
222
|
/**
|
|
@@ -229,8 +224,7 @@ export function packageGeneratorInstallCommand(packageName) {
|
|
|
229
224
|
* @returns {string|null}
|
|
230
225
|
*/
|
|
231
226
|
export function packageGeneratorInstallHint(packageName) {
|
|
232
|
-
|
|
233
|
-
return command ? `Install it from the project root with: ${command}` : null;
|
|
227
|
+
return packageInstallHint(packageName);
|
|
234
228
|
}
|
|
235
229
|
|
|
236
230
|
/**
|
|
@@ -239,41 +233,7 @@ export function packageGeneratorInstallHint(packageName) {
|
|
|
239
233
|
* @returns {{ manifestPath: string|null, packageRoot: string|null, error: string|null }}
|
|
240
234
|
*/
|
|
241
235
|
export function resolvePackageGeneratorManifestPath(packageName, rootDir = process.cwd()) {
|
|
242
|
-
|
|
243
|
-
try {
|
|
244
|
-
const manifestPath = requireFromRoot.resolve(`${packageName}/topogram-generator.json`);
|
|
245
|
-
return {
|
|
246
|
-
manifestPath,
|
|
247
|
-
packageRoot: path.dirname(manifestPath),
|
|
248
|
-
error: null
|
|
249
|
-
};
|
|
250
|
-
} catch (manifestError) {
|
|
251
|
-
try {
|
|
252
|
-
const packageJsonPath = requireFromRoot.resolve(`${packageName}/package.json`);
|
|
253
|
-
const packageRoot = path.dirname(packageJsonPath);
|
|
254
|
-
const manifestPath = path.join(packageRoot, "topogram-generator.json");
|
|
255
|
-
if (!fs.existsSync(manifestPath)) {
|
|
256
|
-
return {
|
|
257
|
-
manifestPath: null,
|
|
258
|
-
packageRoot,
|
|
259
|
-
error: `Generator package '${packageName}' is missing topogram-generator.json`
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
return {
|
|
263
|
-
manifestPath,
|
|
264
|
-
packageRoot,
|
|
265
|
-
error: null
|
|
266
|
-
};
|
|
267
|
-
} catch {
|
|
268
|
-
const detail = manifestError instanceof Error ? manifestError.message : String(manifestError);
|
|
269
|
-
const installHint = packageGeneratorInstallHint(packageName);
|
|
270
|
-
return {
|
|
271
|
-
manifestPath: null,
|
|
272
|
-
packageRoot: null,
|
|
273
|
-
error: `Generator package '${packageName}' could not be resolved from '${rootDir || process.cwd()}': ${detail}${installHint ? `. ${installHint}` : ""}`
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
}
|
|
236
|
+
return resolvePackageManifestPath(packageName, "topogram-generator.json", rootDir, "Generator package");
|
|
277
237
|
}
|
|
278
238
|
|
|
279
239
|
/**
|
|
@@ -282,32 +242,13 @@ export function resolvePackageGeneratorManifestPath(packageName, rootDir = proce
|
|
|
282
242
|
* @returns {{ manifest: GeneratorManifest|null, errors: string[], manifestPath: string|null, packageRoot: string|null }}
|
|
283
243
|
*/
|
|
284
244
|
export function loadPackageGeneratorManifest(packageName, rootDir = process.cwd()) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
try {
|
|
295
|
-
const manifest = JSON.parse(fs.readFileSync(resolved.manifestPath, "utf8"));
|
|
296
|
-
const validation = validateGeneratorManifest(manifest);
|
|
297
|
-
return {
|
|
298
|
-
manifest: validation.ok ? manifest : null,
|
|
299
|
-
errors: validation.errors,
|
|
300
|
-
manifestPath: resolved.manifestPath,
|
|
301
|
-
packageRoot: resolved.packageRoot
|
|
302
|
-
};
|
|
303
|
-
} catch (error) {
|
|
304
|
-
return {
|
|
305
|
-
manifest: null,
|
|
306
|
-
errors: [`Generator package '${packageName}' manifest could not be read: ${error instanceof Error ? error.message : String(error)}`],
|
|
307
|
-
manifestPath: resolved.manifestPath,
|
|
308
|
-
packageRoot: resolved.packageRoot
|
|
309
|
-
};
|
|
310
|
-
}
|
|
245
|
+
return loadPackageManifest({
|
|
246
|
+
packageName,
|
|
247
|
+
rootDir,
|
|
248
|
+
manifestFile: "topogram-generator.json",
|
|
249
|
+
packageLabel: "Generator package",
|
|
250
|
+
validateManifest: validateGeneratorManifest
|
|
251
|
+
});
|
|
311
252
|
}
|
|
312
253
|
|
|
313
254
|
/**
|
package/src/generator-policy.js
CHANGED
|
@@ -4,6 +4,12 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
6
|
import { stableStringify } from "./format.js";
|
|
7
|
+
import {
|
|
8
|
+
optionalStringArray,
|
|
9
|
+
optionalStringRecord,
|
|
10
|
+
packageAllowedByPolicy,
|
|
11
|
+
packageScopeFromName as sharedPackageScopeFromName
|
|
12
|
+
} from "./package-adapters/index.js";
|
|
7
13
|
|
|
8
14
|
export const GENERATOR_POLICY_FILE = "topogram.generator-policy.json";
|
|
9
15
|
|
|
@@ -66,47 +72,6 @@ function generatorPolicyDiagnostic(input) {
|
|
|
66
72
|
};
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
/**
|
|
70
|
-
* @param {unknown} value
|
|
71
|
-
* @param {string} fieldName
|
|
72
|
-
* @param {string} policyPath
|
|
73
|
-
* @returns {string[]}
|
|
74
|
-
*/
|
|
75
|
-
function optionalStringArray(value, fieldName, policyPath) {
|
|
76
|
-
if (value == null) {
|
|
77
|
-
return [];
|
|
78
|
-
}
|
|
79
|
-
if (!Array.isArray(value)) {
|
|
80
|
-
throw new Error(`${policyPath} ${fieldName} must be an array of strings.`);
|
|
81
|
-
}
|
|
82
|
-
return value.map((item) => {
|
|
83
|
-
if (typeof item !== "string" || item.length === 0) {
|
|
84
|
-
throw new Error(`${policyPath} ${fieldName} must contain only non-empty strings.`);
|
|
85
|
-
}
|
|
86
|
-
return item;
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* @param {unknown} value
|
|
92
|
-
* @param {string} policyPath
|
|
93
|
-
* @returns {Record<string, string>}
|
|
94
|
-
*/
|
|
95
|
-
function optionalStringRecord(value, policyPath) {
|
|
96
|
-
if (value == null) {
|
|
97
|
-
return {};
|
|
98
|
-
}
|
|
99
|
-
if (typeof value !== "object" || Array.isArray(value)) {
|
|
100
|
-
throw new Error(`${policyPath} pinnedVersions must be an object of package-or-generator ids to versions.`);
|
|
101
|
-
}
|
|
102
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => {
|
|
103
|
-
if (typeof item !== "string" || item.length === 0) {
|
|
104
|
-
throw new Error(`${policyPath} pinnedVersions['${key}'] must be a non-empty string.`);
|
|
105
|
-
}
|
|
106
|
-
return [key, item];
|
|
107
|
-
}));
|
|
108
|
-
}
|
|
109
|
-
|
|
110
75
|
/**
|
|
111
76
|
* @returns {GeneratorPolicy}
|
|
112
77
|
*/
|
|
@@ -136,7 +101,7 @@ export function validateGeneratorPolicy(value, policyPath) {
|
|
|
136
101
|
? defaults.allowedPackageScopes
|
|
137
102
|
: optionalStringArray(raw.allowedPackageScopes, "allowedPackageScopes", policyPath),
|
|
138
103
|
allowedPackages: optionalStringArray(raw.allowedPackages, "allowedPackages", policyPath),
|
|
139
|
-
pinnedVersions: optionalStringRecord(raw.pinnedVersions, policyPath)
|
|
104
|
+
pinnedVersions: optionalStringRecord(raw.pinnedVersions, policyPath, "package-or-generator ids")
|
|
140
105
|
};
|
|
141
106
|
}
|
|
142
107
|
|
|
@@ -145,16 +110,7 @@ export function validateGeneratorPolicy(value, policyPath) {
|
|
|
145
110
|
* @returns {string|null}
|
|
146
111
|
*/
|
|
147
112
|
export function packageScopeFromName(packageName) {
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* @param {string} allowed
|
|
153
|
-
* @param {string|null} scope
|
|
154
|
-
* @returns {boolean}
|
|
155
|
-
*/
|
|
156
|
-
function packageScopeMatches(allowed, scope) {
|
|
157
|
-
return Boolean(scope && (allowed === scope || allowed === `${scope}/*`));
|
|
113
|
+
return sharedPackageScopeFromName(packageName);
|
|
158
114
|
}
|
|
159
115
|
|
|
160
116
|
/**
|
|
@@ -163,11 +119,7 @@ function packageScopeMatches(allowed, scope) {
|
|
|
163
119
|
* @returns {boolean}
|
|
164
120
|
*/
|
|
165
121
|
export function generatorPackageAllowed(policy, packageName) {
|
|
166
|
-
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
|
-
const scope = packageScopeFromName(packageName);
|
|
170
|
-
return policy.allowedPackageScopes.some((allowed) => packageScopeMatches(allowed, scope));
|
|
122
|
+
return packageAllowedByPolicy(policy, packageName);
|
|
171
123
|
}
|
|
172
124
|
|
|
173
125
|
/**
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export const extractorRegistry: Record<string, any[]>;
|
|
2
2
|
export const enricherRegistry: Record<string, any[]>;
|
|
3
|
+
export const BUILTIN_EXTRACTOR_PACKS: any[];
|
|
3
4
|
export function getExtractorsForTrack(...args: any[]): any[];
|
|
4
5
|
export function getEnrichersForTrack(...args: any[]): any[];
|
|
6
|
+
export function getBundledExtractorPack(...args: any[]): any;
|
|
7
|
+
export function getBundledExtractorById(...args: any[]): any;
|
|
@@ -57,14 +57,80 @@ import { railsControllerEnricher } from "../enrichers/rails-controllers.js";
|
|
|
57
57
|
import { workflowTargetStateEnricher } from "../enrichers/workflow-target-state.js";
|
|
58
58
|
import { docLinkingEnricher } from "../enrichers/doc-linking.js";
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
function extractorPack(id, tracks, extractors, candidateKinds, stack = {}, capabilities = {}) {
|
|
61
|
+
return {
|
|
62
|
+
manifest: {
|
|
63
|
+
id,
|
|
64
|
+
version: "1",
|
|
65
|
+
tracks,
|
|
66
|
+
source: "bundled",
|
|
67
|
+
extractors: extractors.map((extractor) => extractor.id),
|
|
68
|
+
stack,
|
|
69
|
+
capabilities,
|
|
70
|
+
candidateKinds,
|
|
71
|
+
evidenceTypes: ["runtime_source", "parser_config", "docs", "tests", "fixtures"]
|
|
72
|
+
},
|
|
73
|
+
extractors
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const BUILTIN_EXTRACTOR_PACKS = [
|
|
78
|
+
extractorPack(
|
|
79
|
+
"topogram/db-extractors",
|
|
80
|
+
["db"],
|
|
81
|
+
[prismaExtractor, djangoModelsExtractor, efCoreExtractor, roomExtractor, swiftDataExtractor, dotnetModelsExtractor, flutterEntitiesExtractor, reactNativeEntitiesExtractor, railsSchemaExtractor, liquibaseExtractor, myBatisXmlExtractor, jpaExtractor, drizzleExtractor, sqlExtractor, snapshotExtractor],
|
|
82
|
+
["entity", "enum", "relation", "index", "maintained_db_migration_seam"],
|
|
83
|
+
{ domain: "database" },
|
|
84
|
+
{ schema: true, migrations: true, maintainedSeams: true }
|
|
85
|
+
),
|
|
86
|
+
extractorPack(
|
|
87
|
+
"topogram/api-extractors",
|
|
88
|
+
["api"],
|
|
89
|
+
[openApiExtractor, openApiCodeExtractor, graphQlSdlExtractor, graphQlCodeFirstExtractor, trpcExtractor, aspNetCoreExtractor, retrofitExtractor, swiftWebApiExtractor, flutterDioExtractor, reactNativeRepositoryExtractor, fastifyExtractor, expressExtractor, djangoRoutesExtractor, railsRoutesExtractor, micronautExtractor, jaxRsExtractor, springWebExtractor, nextRouteExtractor, genericRouteFallbackExtractor, nextServerActionExtractor, nextAuthExtractor],
|
|
90
|
+
["capability", "route", "stack"],
|
|
91
|
+
{ domain: "api" },
|
|
92
|
+
{ routes: true, openapi: true, graphql: true }
|
|
93
|
+
),
|
|
94
|
+
extractorPack(
|
|
95
|
+
"topogram/ui-extractors",
|
|
96
|
+
["ui"],
|
|
97
|
+
[nextAppRouterUiExtractor, nextPagesRouterUiExtractor, androidComposeUiExtractor, blazorUiExtractor, razorPagesUiExtractor, swiftUiExtractor, uiKitExtractor, mauiXamlUiExtractor, flutterScreensUiExtractor, reactNativeScreensExtractor, reactRouterUiExtractor, svelteKitUiExtractor, backendOnlyUiExtractor],
|
|
98
|
+
["screen", "route", "action", "flow", "widget", "shape", "stack"],
|
|
99
|
+
{ domain: "ui" },
|
|
100
|
+
{ screens: true, widgets: true, flows: true }
|
|
101
|
+
),
|
|
102
|
+
extractorPack(
|
|
103
|
+
"topogram/cli-extractors",
|
|
104
|
+
["cli"],
|
|
105
|
+
[genericCliExtractor],
|
|
106
|
+
["command", "capability", "cli_surface"],
|
|
107
|
+
{ domain: "cli" },
|
|
108
|
+
{ commands: true, options: true, effects: true }
|
|
109
|
+
),
|
|
110
|
+
extractorPack(
|
|
111
|
+
"topogram/workflow-extractors",
|
|
112
|
+
["workflows"],
|
|
113
|
+
[genericWorkflowExtractor],
|
|
114
|
+
["workflow", "workflow_state", "workflow_transition"],
|
|
115
|
+
{ domain: "workflow" },
|
|
116
|
+
{ workflows: true }
|
|
117
|
+
),
|
|
118
|
+
extractorPack(
|
|
119
|
+
"topogram/verification-extractors",
|
|
120
|
+
["verification"],
|
|
121
|
+
[genericVerificationExtractor],
|
|
122
|
+
["verification", "scenario", "framework", "script"],
|
|
123
|
+
{ domain: "verification" },
|
|
124
|
+
{ verifications: true }
|
|
125
|
+
)
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
export const extractorRegistry = Object.fromEntries(
|
|
129
|
+
["db", "api", "ui", "cli", "workflows", "verification"].map((track) => [
|
|
130
|
+
track,
|
|
131
|
+
BUILTIN_EXTRACTOR_PACKS.flatMap((pack) => pack.extractors).filter((extractor) => extractor.track === track)
|
|
132
|
+
])
|
|
133
|
+
);
|
|
68
134
|
|
|
69
135
|
export const enricherRegistry = {
|
|
70
136
|
db: [railsModelEnricher],
|
|
@@ -82,3 +148,11 @@ export function getExtractorsForTrack(track) {
|
|
|
82
148
|
export function getEnrichersForTrack(track) {
|
|
83
149
|
return enricherRegistry[track] || [];
|
|
84
150
|
}
|
|
151
|
+
|
|
152
|
+
export function getBundledExtractorPack(id) {
|
|
153
|
+
return BUILTIN_EXTRACTOR_PACKS.find((pack) => pack.manifest.id === id) || null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function getBundledExtractorById(id) {
|
|
157
|
+
return BUILTIN_EXTRACTOR_PACKS.flatMap((pack) => pack.extractors).find((extractor) => extractor.id === id) || null;
|
|
158
|
+
}
|
|
@@ -17,12 +17,12 @@ export function reportMarkdown(track, candidates) {
|
|
|
17
17
|
` - manual next: ${(seam.manual_next_steps || []).slice(0, 2).join(" ")}`
|
|
18
18
|
]);
|
|
19
19
|
return ensureTrailingNewline(
|
|
20
|
-
`# DB
|
|
20
|
+
`# DB Extract Report\n\n- Entities: ${candidates.entities.length}\n- Enums: ${candidates.enums.length}\n- Relations: ${candidates.relations.length}\n- Indexes: ${candidates.indexes.length}\n- Maintained DB migration seams: ${(candidates.maintained_seams || []).length}\n\n## Maintained DB Migration Seam Candidates\n\n${seamLines.length ? seamLines.join("\n") : "- none"}\n`
|
|
21
21
|
);
|
|
22
22
|
}
|
|
23
23
|
if (track === "api") {
|
|
24
24
|
return ensureTrailingNewline(
|
|
25
|
-
`# API
|
|
25
|
+
`# API Extract Report\n\n- Capabilities: ${candidates.capabilities.length}\n- Routes: ${candidates.routes.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n`
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
28
|
if (track === "ui") {
|
|
@@ -36,7 +36,7 @@ export function reportMarkdown(track, candidates) {
|
|
|
36
36
|
`- \`${flow.id_hint}\` type \`${flow.flow_type}\` confidence ${flow.confidence || "unknown"} routes ${(flow.route_paths || []).map((/** @type {string} */ route) => `\`${route}\``).join(", ") || "_none_"} missing decisions ${(flow.missing_decisions || []).length}`
|
|
37
37
|
);
|
|
38
38
|
return ensureTrailingNewline(
|
|
39
|
-
`# UI
|
|
39
|
+
`# UI Extract Report\n\n- Screens: ${candidates.screens.length}\n- Routes: ${candidates.routes.length}\n- Actions: ${candidates.actions.length}\n- Flow candidates: ${flows.length}\n- Widgets: ${widgets.length}\n- Event payload shapes: ${shapes.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n\n## Flow Candidates\n\n${flowLines.length ? flowLines.join("\n") : "- none"}\n\n## Widget Candidates\n\n${widgetLines.length ? widgetLines.join("\n") : "- none"}\n\n## Next Validation\n\n- Review flow candidates in \`topo/candidates/app/ui/candidates.json\` before adding shared UI contract behavior.\n- Review candidates under \`topo/candidates/app/ui/drafts/widgets/**\`.\n- Run \`topogram extract plan <path>\` before adoption.\n- After adoption, run \`topogram check <path>\`, \`topogram widget check <path>\`, and \`topogram widget behavior <path>\`.\n`
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
42
|
if (track === "cli") {
|
|
@@ -44,7 +44,7 @@ export function reportMarkdown(track, candidates) {
|
|
|
44
44
|
`- \`${command.command_id || command.id_hint}\` usage \`${command.usage || "unknown"}\` effects ${(command.effects || []).map((/** @type {any} */ effect) => `\`${effect}\``).join(", ") || "_none_"}`
|
|
45
45
|
);
|
|
46
46
|
return ensureTrailingNewline(
|
|
47
|
-
`# CLI
|
|
47
|
+
`# CLI Extract Report\n\n- Commands: ${candidates.commands.length}\n- Capabilities: ${candidates.capabilities.length}\n- CLI surfaces: ${candidates.surfaces.length}\n\n## Command Candidates\n\n${commandLines.length ? commandLines.join("\n") : "- none"}\n`
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
50
|
if (track === "verification") {
|
|
@@ -6,6 +6,7 @@ import { parseImportTracks } from "./options.js";
|
|
|
6
6
|
import { appReportMarkdown, reportMarkdown } from "./reports.js";
|
|
7
7
|
import { runTrack } from "./tracks.js";
|
|
8
8
|
import { draftUiProjectionFiles } from "./ui-drafts.js";
|
|
9
|
+
import { packageExtractorsForContext } from "../../../extractor/packages.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* @param {string} inputPath
|
|
@@ -64,6 +65,7 @@ export function runImportApp(inputPath, options = {}) {
|
|
|
64
65
|
tracks,
|
|
65
66
|
findings_count: Object.values(findings).reduce((total, entries) => total + entries.length, 0),
|
|
66
67
|
extractor_detections: Object.fromEntries(Object.entries(resultsByTrack).map(([track, result]) => [track, result.extractor_detections])),
|
|
68
|
+
package_extractors: packageExtractorsForContext(context).provenance,
|
|
67
69
|
candidates
|
|
68
70
|
};
|
|
69
71
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { getEnrichersForTrack, getExtractorsForTrack } from "../registry.js";
|
|
4
4
|
import { normalizeCandidatesForTrack } from "./candidates.js";
|
|
5
|
+
import { packageExtractorsForContext } from "../../../extractor/packages.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @param {any} context
|
|
@@ -10,7 +11,12 @@ import { normalizeCandidatesForTrack } from "./candidates.js";
|
|
|
10
11
|
*/
|
|
11
12
|
function sortExtractors(context, extractors) {
|
|
12
13
|
return extractors
|
|
13
|
-
.map((extractor) =>
|
|
14
|
+
.map((extractor) => {
|
|
15
|
+
const extractorContext = extractor.source === "package" && typeof extractor.packageContext === "function"
|
|
16
|
+
? extractor.packageContext(context)
|
|
17
|
+
: context;
|
|
18
|
+
return { extractor, detection: extractor.detect(extractorContext) || { score: 0, reasons: [] } };
|
|
19
|
+
})
|
|
14
20
|
.filter((entry) => entry.detection.score > 0)
|
|
15
21
|
.sort((a, b) => b.detection.score - a.detection.score || a.extractor.id.localeCompare(b.extractor.id));
|
|
16
22
|
}
|
|
@@ -20,7 +26,7 @@ function sortExtractors(context, extractors) {
|
|
|
20
26
|
* @param {Array<{ extractor: any, detection: any }>} detections
|
|
21
27
|
* @returns {Array<{ extractor: any, detection: any }>}
|
|
22
28
|
*/
|
|
23
|
-
function
|
|
29
|
+
function selectBundledDetectionsForTrack(track, detections) {
|
|
24
30
|
if (track === "db") {
|
|
25
31
|
const prisma = detections.find((entry) => entry.extractor.id === "db.prisma");
|
|
26
32
|
if (prisma) return [prisma];
|
|
@@ -90,6 +96,20 @@ function selectDetectionsForTrack(track, detections) {
|
|
|
90
96
|
return detections;
|
|
91
97
|
}
|
|
92
98
|
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} track
|
|
101
|
+
* @param {Array<{ extractor: any, detection: any }>} detections
|
|
102
|
+
* @returns {Array<{ extractor: any, detection: any }>}
|
|
103
|
+
*/
|
|
104
|
+
function selectDetectionsForTrack(track, detections) {
|
|
105
|
+
const packageDetections = detections.filter((entry) => entry.extractor.source === "package");
|
|
106
|
+
const bundledDetections = selectBundledDetectionsForTrack(
|
|
107
|
+
track,
|
|
108
|
+
detections.filter((entry) => entry.extractor.source !== "package")
|
|
109
|
+
);
|
|
110
|
+
return [...bundledDetections, ...packageDetections];
|
|
111
|
+
}
|
|
112
|
+
|
|
93
113
|
/**
|
|
94
114
|
* @param {string} track
|
|
95
115
|
* @returns {any}
|
|
@@ -113,6 +133,35 @@ function initialCandidatesForTrack(track) {
|
|
|
113
133
|
return { workflows: [], workflow_states: [], workflow_transitions: [] };
|
|
114
134
|
}
|
|
115
135
|
|
|
136
|
+
/**
|
|
137
|
+
* @param {any} extractor
|
|
138
|
+
* @param {any} result
|
|
139
|
+
* @returns {void}
|
|
140
|
+
*/
|
|
141
|
+
function assertExtractorResultShape(extractor, result) {
|
|
142
|
+
const label = extractor?.id || "unknown";
|
|
143
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
144
|
+
throw new Error(`Extractor '${label}' extract(context) must return an object.`);
|
|
145
|
+
}
|
|
146
|
+
if (result.findings != null && !Array.isArray(result.findings)) {
|
|
147
|
+
throw new Error(`Extractor '${label}' extract(context) findings must be an array when present.`);
|
|
148
|
+
}
|
|
149
|
+
if (result.diagnostics != null && !Array.isArray(result.diagnostics)) {
|
|
150
|
+
throw new Error(`Extractor '${label}' extract(context) diagnostics must be an array when present.`);
|
|
151
|
+
}
|
|
152
|
+
if (result.candidates == null) {
|
|
153
|
+
result.candidates = {};
|
|
154
|
+
}
|
|
155
|
+
if (!result.candidates || typeof result.candidates !== "object" || Array.isArray(result.candidates)) {
|
|
156
|
+
throw new Error(`Extractor '${label}' extract(context) candidates must be an object.`);
|
|
157
|
+
}
|
|
158
|
+
for (const [key, value] of Object.entries(result.candidates)) {
|
|
159
|
+
if (!Array.isArray(value)) {
|
|
160
|
+
throw new Error(`Extractor '${label}' extract(context) candidates.${key} must be an array.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
116
165
|
/**
|
|
117
166
|
* @param {any} context
|
|
118
167
|
* @param {string} track
|
|
@@ -121,9 +170,22 @@ function initialCandidatesForTrack(track) {
|
|
|
121
170
|
export function runTrack(context, track) {
|
|
122
171
|
const findings = [];
|
|
123
172
|
const rawCandidates = initialCandidatesForTrack(track);
|
|
173
|
+
const packageState = packageExtractorsForContext(context);
|
|
174
|
+
const packageErrors = packageState.diagnostics.filter((diagnostic) => diagnostic.severity !== "warning");
|
|
175
|
+
if (packageErrors.length > 0) {
|
|
176
|
+
throw new Error(packageErrors.map((diagnostic) => diagnostic.message || String(diagnostic)).join("\n"));
|
|
177
|
+
}
|
|
178
|
+
const extractors = [
|
|
179
|
+
...getExtractorsForTrack(track),
|
|
180
|
+
...packageState.extractors.filter((extractor) => extractor.track === track)
|
|
181
|
+
];
|
|
124
182
|
|
|
125
|
-
for (const { extractor, detection } of selectDetectionsForTrack(track, sortExtractors(context,
|
|
126
|
-
const
|
|
183
|
+
for (const { extractor, detection } of selectDetectionsForTrack(track, sortExtractors(context, extractors))) {
|
|
184
|
+
const extractorContext = extractor.source === "package" && typeof extractor.packageContext === "function"
|
|
185
|
+
? extractor.packageContext(context)
|
|
186
|
+
: context;
|
|
187
|
+
const result = extractor.extract(extractorContext) || { findings: [], candidates: {} };
|
|
188
|
+
assertExtractorResultShape(extractor, result);
|
|
127
189
|
findings.push({
|
|
128
190
|
extractor: extractor.id,
|
|
129
191
|
detection,
|