@topogram/cli 0.3.81 → 0.3.83
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/package.json +1 -1
- package/src/agent-ops/query-builders/change-risk/extract-plan.js +2 -1
- package/src/agent-ops/query-builders/common.js +2 -0
- package/src/agent-ops/query-builders/multi-agent.js +2 -0
- package/src/agent-ops/query-builders/work-packets.js +2 -0
- package/src/agent-ops/query-builders/workflow-context.js +4 -0
- package/src/cli/command-parsers/sdlc.js +3 -0
- package/src/cli/commands/query/definitions.js +45 -0
- package/src/cli/commands/query/extract-adopt.js +9 -2
- package/src/cli/commands/query/runner/workflow.js +12 -3
- package/src/cli/commands/query/workspace.js +68 -0
- package/src/cli/commands/sdlc.js +42 -0
- package/src/cli/help.js +2 -0
- package/src/generator/context/diff.js +97 -0
- package/src/generator/registry/index.js +23 -1
- package/src/sdlc/audit.js +205 -0
- package/src/ui/taxonomy.js +45 -0
- package/src/validator/kinds.d.ts +5 -0
- package/src/validator/kinds.js +6 -1
- package/src/validator/per-kind/widget.js +7 -45
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@ import { buildImportMaintainedRisk } from "../maintained-risk.js";
|
|
|
2
2
|
import { buildImportPlanNextAction } from "../workflow-presets-core.js";
|
|
3
3
|
import { buildPresetGuidanceSummary } from "./risk.js";
|
|
4
4
|
|
|
5
|
-
export function buildImportPlanPayload(adoptionPlan, taskModeArtifact, maintainedBoundaryArtifact = null, workflowPresetState = null) {
|
|
5
|
+
export function buildImportPlanPayload(adoptionPlan, taskModeArtifact, maintainedBoundaryArtifact = null, workflowPresetState = null, extractionContext = null) {
|
|
6
6
|
const importMaintained = buildImportMaintainedRisk(adoptionPlan.imported_proposal_surfaces || [], maintainedBoundaryArtifact);
|
|
7
7
|
const importNextAction = buildImportPlanNextAction(taskModeArtifact.next_action || null, workflowPresetState);
|
|
8
8
|
const presetGuidanceSummary = buildPresetGuidanceSummary(workflowPresetState, null);
|
|
@@ -13,6 +13,7 @@ export function buildImportPlanPayload(adoptionPlan, taskModeArtifact, maintaine
|
|
|
13
13
|
next_action: importNextAction,
|
|
14
14
|
write_scope: taskModeArtifact.write_scope || null,
|
|
15
15
|
verification_targets: taskModeArtifact.verification_targets || null,
|
|
16
|
+
extraction_context: extractionContext,
|
|
16
17
|
review_groups: adoptionPlan.approved_review_groups || [],
|
|
17
18
|
staged_items: adoptionPlan.staged_items || [],
|
|
18
19
|
accepted_items: adoptionPlan.accepted_items || [],
|
|
@@ -15,6 +15,8 @@ export function summarizeDiffArtifact(diffArtifact) {
|
|
|
15
15
|
review_boundary_change_count: (diffArtifact.review_boundary_changes || []).length,
|
|
16
16
|
maintained_file_count: (diffArtifact.affected_maintained_surfaces?.maintained_files_in_scope || []).length,
|
|
17
17
|
affected_verification_count: (diffArtifact.affected_verifications || []).length,
|
|
18
|
+
widget_migration_count: (diffArtifact.widget_contract_migration_plan?.widgets || []).length,
|
|
19
|
+
widget_migration_projection_count: (diffArtifact.widget_contract_migration_plan?.affected_projection_ids || []).length,
|
|
18
20
|
affected_output_count: maintainedOutputs.length,
|
|
19
21
|
affected_seam_count: maintainedSeams.length,
|
|
20
22
|
highest_maintained_severity: highestMaintainedSeverity
|
|
@@ -496,6 +496,7 @@ export function buildMultiAgentPlanPayload({
|
|
|
496
496
|
importPlan,
|
|
497
497
|
report,
|
|
498
498
|
adoptionStatus,
|
|
499
|
+
extractionContext = null,
|
|
499
500
|
resolvedWorkflowContext = null
|
|
500
501
|
}) {
|
|
501
502
|
const presetGuidanceSummary = buildPresetGuidanceSummary(importPlan?.workflow_presets || null, resolvedWorkflowContext || singleAgentPlan?.resolved_workflow_context || null);
|
|
@@ -530,6 +531,7 @@ export function buildMultiAgentPlanPayload({
|
|
|
530
531
|
active_preset_ids: presetGuidanceSummary.active_preset_ids,
|
|
531
532
|
preset_blockers: presetGuidanceSummary.preset_blockers,
|
|
532
533
|
recommended_preset_action: presetGuidanceSummary.recommended_preset_action,
|
|
534
|
+
extraction_context: extractionContext || singleAgentPlan?.extraction_context || resolvedWorkflowContext?.extraction_context || null,
|
|
533
535
|
resolved_workflow_context: resolvedWorkflowContext || singleAgentPlan?.resolved_workflow_context || null,
|
|
534
536
|
lanes,
|
|
535
537
|
parallel_workstreams: parallelWorkstreams,
|
|
@@ -147,6 +147,7 @@ export function buildWorkPacketPayload({
|
|
|
147
147
|
const publishedHandoffPacket = (multiAgentPlan?.handoff_packets || []).find((packet) => packet.from_lane === laneId)
|
|
148
148
|
|| handoffTemplateFromLane(lane, multiAgentPlan?.mode || null);
|
|
149
149
|
const resolvedWorkflowContext = multiAgentPlan?.resolved_workflow_context || null;
|
|
150
|
+
const extractionContext = multiAgentPlan?.extraction_context || resolvedWorkflowContext?.extraction_context || null;
|
|
150
151
|
const presetGuidanceSummary = multiAgentPlan?.preset_guidance_summary || buildPresetGuidanceSummary(null, resolvedWorkflowContext);
|
|
151
152
|
const effectiveWriteScope = lane.workflow_context_overrides?.effective_write_scope || lane.write_scope || resolvedWorkflowContext?.effective_write_scope || null;
|
|
152
153
|
const effectiveVerificationPolicy = lane.workflow_context_overrides?.effective_verification_policy || {
|
|
@@ -172,6 +173,7 @@ export function buildWorkPacketPayload({
|
|
|
172
173
|
preset_guidance_summary: presetGuidanceSummary,
|
|
173
174
|
active_preset_ids: presetGuidanceSummary.active_preset_ids,
|
|
174
175
|
recommended_preset_action: presetGuidanceSummary.recommended_preset_action,
|
|
176
|
+
extraction_context: extractionContext,
|
|
175
177
|
write_scope: lane.write_scope || null,
|
|
176
178
|
effective_write_scope: effectiveWriteScope,
|
|
177
179
|
owned_targets: lane.owned_targets || null,
|
|
@@ -154,6 +154,7 @@ export function buildResolvedWorkflowContextPayload({
|
|
|
154
154
|
reviewBoundary = null,
|
|
155
155
|
maintainedBoundary = null,
|
|
156
156
|
generatorTargets = [],
|
|
157
|
+
extractionContext = null,
|
|
157
158
|
providerPresets = null,
|
|
158
159
|
teamPresets = null,
|
|
159
160
|
providerManifests = null,
|
|
@@ -326,6 +327,7 @@ export function buildResolvedWorkflowContextPayload({
|
|
|
326
327
|
preferred_queries: preferredQueries,
|
|
327
328
|
artifact_load_order: artifactLoadOrder,
|
|
328
329
|
recommended_artifact_queries: recommendedArtifactQueries,
|
|
330
|
+
extraction_context: extractionContext,
|
|
329
331
|
effective_write_scope: taskModeArtifact?.write_scope || null,
|
|
330
332
|
effective_review_policy: {
|
|
331
333
|
block_on: reviewBlockers,
|
|
@@ -362,6 +364,7 @@ export function buildSingleAgentPlanPayload({
|
|
|
362
364
|
workspace,
|
|
363
365
|
taskModeArtifact,
|
|
364
366
|
importPlan = null,
|
|
367
|
+
extractionContext = null,
|
|
365
368
|
resolvedWorkflowContext = null
|
|
366
369
|
}) {
|
|
367
370
|
const primaryArtifacts = stableOrderedUnion([
|
|
@@ -377,6 +380,7 @@ export function buildSingleAgentPlanPayload({
|
|
|
377
380
|
current_focus: currentFocusFromTaskMode(taskModeArtifact),
|
|
378
381
|
next_action: taskModeArtifact?.next_action || null,
|
|
379
382
|
write_scope: taskModeArtifact?.write_scope || null,
|
|
383
|
+
extraction_context: extractionContext || resolvedWorkflowContext?.extraction_context || null,
|
|
380
384
|
review_boundaries: buildReviewBoundaries(taskModeArtifact, importPlan),
|
|
381
385
|
proof_targets: taskModeArtifact?.verification_targets || null,
|
|
382
386
|
operator_loop: buildOperatorLoopSummary({
|
|
@@ -19,6 +19,9 @@ export function parseSdlcCommandArgs(args) {
|
|
|
19
19
|
if (args[0] === "sdlc" && args[1] === "prep" && args[2] === "commit") {
|
|
20
20
|
return { sdlcCommand: "prep:commit", inputPath: commandPath(args, 3, ".") };
|
|
21
21
|
}
|
|
22
|
+
if (args[0] === "sdlc" && args[1] === "audit") {
|
|
23
|
+
return { sdlcCommand: "audit", inputPath: commandPath(args, 2, ".") };
|
|
24
|
+
}
|
|
22
25
|
if (args[0] === "sdlc" && args[1] === "link") {
|
|
23
26
|
return {
|
|
24
27
|
sdlcCommand: "link",
|
|
@@ -97,6 +97,51 @@ export function queryDefinitions() {
|
|
|
97
97
|
output: "single_agent_plan_query",
|
|
98
98
|
example: "topogram query single-agent-plan ./topo --mode modeling --widget widget_data_grid --json"
|
|
99
99
|
},
|
|
100
|
+
{
|
|
101
|
+
name: "extract-plan",
|
|
102
|
+
purpose: "Summarize brownfield extraction candidates, package extractor context, and adoption review state.",
|
|
103
|
+
description: "Return the extract/adopt plan for an extracted workspace, including trusted extraction provenance and next review commands.",
|
|
104
|
+
selectors: ["provider", "preset"],
|
|
105
|
+
args: ["[path]", "[--json]"],
|
|
106
|
+
output: "extract_plan_query",
|
|
107
|
+
example: "topogram query extract-plan ./extracted-topogram --json"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "multi-agent-plan",
|
|
111
|
+
purpose: "Split extract/adopt review into serialized and parallel agent lanes.",
|
|
112
|
+
description: "Return lane ownership, handoff packets, overlap rules, and package extractor context for extract/adopt mode.",
|
|
113
|
+
selectors: ["mode", "provider", "preset"],
|
|
114
|
+
args: ["[path]", "--mode extract-adopt", "[--json]"],
|
|
115
|
+
output: "multi_agent_plan",
|
|
116
|
+
example: "topogram query multi-agent-plan ./extracted-topogram --mode extract-adopt --json"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "work-packet",
|
|
120
|
+
purpose: "Give one extract/adopt lane its allowed inputs, write scope, blockers, and handoff packet.",
|
|
121
|
+
description: "Return a lane-scoped work packet for extract/adopt mode.",
|
|
122
|
+
selectors: ["mode", "lane"],
|
|
123
|
+
args: ["[path]", "--mode extract-adopt", "--lane <id>", "[--json]"],
|
|
124
|
+
output: "work_packet",
|
|
125
|
+
example: "topogram query work-packet ./extracted-topogram --mode extract-adopt --lane adoption_operator --json"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "lane-status",
|
|
129
|
+
purpose: "Show which extract/adopt lanes are ready, blocked, or complete.",
|
|
130
|
+
description: "Return artifact-derived lane status for extract/adopt mode.",
|
|
131
|
+
selectors: ["mode"],
|
|
132
|
+
args: ["[path]", "--mode extract-adopt", "[--json]"],
|
|
133
|
+
output: "lane_status_query",
|
|
134
|
+
example: "topogram query lane-status ./extracted-topogram --mode extract-adopt --json"
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: "handoff-status",
|
|
138
|
+
purpose: "Show extract/adopt handoff packet status across lanes.",
|
|
139
|
+
description: "Return handoff readiness and blockers for extract/adopt mode.",
|
|
140
|
+
selectors: ["mode"],
|
|
141
|
+
args: ["[path]", "--mode extract-adopt", "[--json]"],
|
|
142
|
+
output: "handoff_status_query",
|
|
143
|
+
example: "topogram query handoff-status ./extracted-topogram --mode extract-adopt --json"
|
|
144
|
+
},
|
|
100
145
|
{
|
|
101
146
|
name: "risk-summary",
|
|
102
147
|
purpose: "Surface behavioral, ownership, and verification risks for a selected change.",
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
buildTaskMode,
|
|
17
17
|
normalizeTopogramPath,
|
|
18
18
|
readJson,
|
|
19
|
+
readExtractionContext,
|
|
19
20
|
requireReconcileArtifacts,
|
|
20
21
|
resultOk,
|
|
21
22
|
workflowPresetSelectors
|
|
@@ -50,6 +51,7 @@ export function buildImportPlanForContext(context, queryFamily) {
|
|
|
50
51
|
workspace: topogramRoot,
|
|
51
52
|
selectors: workflowPresetSelectors(taskModeResult.artifact, context.providerId, context.presetId, queryFamily)
|
|
52
53
|
});
|
|
54
|
+
const extractionContext = readExtractionContext(topogramRoot);
|
|
53
55
|
return {
|
|
54
56
|
ok: true,
|
|
55
57
|
topogramRoot,
|
|
@@ -60,7 +62,8 @@ export function buildImportPlanForContext(context, queryFamily) {
|
|
|
60
62
|
adoptionPlan,
|
|
61
63
|
taskModeResult.artifact,
|
|
62
64
|
maintainedBundleResult.artifact.maintained_boundary || null,
|
|
63
|
-
workflowPresets
|
|
65
|
+
workflowPresets,
|
|
66
|
+
extractionContext
|
|
64
67
|
)
|
|
65
68
|
};
|
|
66
69
|
}
|
|
@@ -85,17 +88,20 @@ export function buildImportAdoptAgentContext(context, queryFamily) {
|
|
|
85
88
|
workspace: topogramRoot,
|
|
86
89
|
selectors: workflowPresetSelectors(taskModeResult.artifact, context.providerId, context.presetId, queryFamily)
|
|
87
90
|
});
|
|
88
|
-
const
|
|
91
|
+
const extractionContext = readExtractionContext(topogramRoot);
|
|
92
|
+
const importPlan = buildImportPlanPayload(adoptionPlanArtifact, taskModeResult.artifact, null, workflowPresets, extractionContext);
|
|
89
93
|
const resolvedWorkflowContext = buildResolvedWorkflowContextPayload({
|
|
90
94
|
workspace: topogramRoot,
|
|
91
95
|
taskModeArtifact: taskModeResult.artifact,
|
|
92
96
|
importPlan,
|
|
97
|
+
extractionContext,
|
|
93
98
|
selectors: workflowPresetSelectors(taskModeResult.artifact, context.providerId, context.presetId, queryFamily)
|
|
94
99
|
});
|
|
95
100
|
const singleAgentPlan = buildSingleAgentPlanPayload({
|
|
96
101
|
workspace: topogramRoot,
|
|
97
102
|
taskModeArtifact: taskModeResult.artifact,
|
|
98
103
|
importPlan,
|
|
104
|
+
extractionContext,
|
|
99
105
|
resolvedWorkflowContext
|
|
100
106
|
});
|
|
101
107
|
const multiAgentPlan = buildMultiAgentPlanPayload({
|
|
@@ -104,6 +110,7 @@ export function buildImportAdoptAgentContext(context, queryFamily) {
|
|
|
104
110
|
importPlan,
|
|
105
111
|
report: reconcileReport,
|
|
106
112
|
adoptionStatus,
|
|
113
|
+
extractionContext,
|
|
107
114
|
resolvedWorkflowContext
|
|
108
115
|
});
|
|
109
116
|
return {
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
normalizeTopogramPath,
|
|
27
27
|
printValidationFailure,
|
|
28
28
|
readJson,
|
|
29
|
+
readExtractionContext,
|
|
29
30
|
resultOk,
|
|
30
31
|
selectorOptions,
|
|
31
32
|
workflowPresetSelectors
|
|
@@ -83,7 +84,7 @@ export function runWorkflowQuery(context) {
|
|
|
83
84
|
workspace: topogramRoot,
|
|
84
85
|
selectors: workflowPresetSelectors(taskModeResult.artifact, context.providerId, context.presetId, "workflow-preset-activation")
|
|
85
86
|
});
|
|
86
|
-
importPlan = buildImportPlanPayload(readJson(adoptionPlanPath(topogramRoot)), taskModeResult.artifact, null, workflowPresets);
|
|
87
|
+
importPlan = buildImportPlanPayload(readJson(adoptionPlanPath(topogramRoot)), taskModeResult.artifact, null, workflowPresets, readExtractionContext(topogramRoot));
|
|
87
88
|
}
|
|
88
89
|
return printJson(buildWorkflowPresetActivationPayload({
|
|
89
90
|
workspace: topogramRoot,
|
|
@@ -134,24 +135,28 @@ function runSingleAgentPlan(context, selectors) {
|
|
|
134
135
|
});
|
|
135
136
|
const topogramRoot = normalizeTopogramPath(context.inputPath);
|
|
136
137
|
let importPlan = null;
|
|
138
|
+
let extractionContext = null;
|
|
137
139
|
if (context.modeId === "extract-adopt" && fs.existsSync(adoptionPlanPath(topogramRoot))) {
|
|
140
|
+
extractionContext = readExtractionContext(topogramRoot);
|
|
138
141
|
const workflowPresets = buildWorkflowPresetState({
|
|
139
142
|
workspace: topogramRoot,
|
|
140
143
|
selectors: workflowPresetSelectors(result.artifact, context.providerId, context.presetId, "single-agent-plan")
|
|
141
144
|
});
|
|
142
|
-
importPlan = buildImportPlanPayload(readJson(adoptionPlanPath(topogramRoot)), result.artifact, null, workflowPresets);
|
|
145
|
+
importPlan = buildImportPlanPayload(readJson(adoptionPlanPath(topogramRoot)), result.artifact, null, workflowPresets, extractionContext);
|
|
143
146
|
}
|
|
144
147
|
const resolvedWorkflowContext = buildResolvedWorkflowContextPayload({
|
|
145
148
|
workspace: topogramRoot,
|
|
146
149
|
taskModeArtifact: result.artifact,
|
|
147
150
|
generatorTargets,
|
|
148
151
|
selectors: workflowPresetSelectors(result.artifact, context.providerId, context.presetId, "single-agent-plan"),
|
|
152
|
+
extractionContext,
|
|
149
153
|
importPlan
|
|
150
154
|
});
|
|
151
155
|
return printJson(buildSingleAgentPlanPayload({
|
|
152
156
|
workspace: topogramRoot,
|
|
153
157
|
taskModeArtifact: result.artifact,
|
|
154
158
|
importPlan,
|
|
159
|
+
extractionContext,
|
|
155
160
|
resolvedWorkflowContext
|
|
156
161
|
}));
|
|
157
162
|
}
|
|
@@ -186,7 +191,9 @@ function runResolvedWorkflowContext(context, selectors) {
|
|
|
186
191
|
maintainedBoundaryArtifact: maintainedBundleResult?.artifact?.maintained_boundary || null
|
|
187
192
|
});
|
|
188
193
|
let importPlan = null;
|
|
194
|
+
let extractionContext = null;
|
|
189
195
|
if (context.modeId === "extract-adopt" && fs.existsSync(adoptionPlanPath(topogramRoot))) {
|
|
196
|
+
extractionContext = readExtractionContext(topogramRoot);
|
|
190
197
|
const workflowPresets = buildWorkflowPresetState({
|
|
191
198
|
workspace: topogramRoot,
|
|
192
199
|
selectors: workflowPresetSelectors(taskModeResult.artifact, context.providerId, context.presetId, "resolved-workflow-context")
|
|
@@ -195,7 +202,8 @@ function runResolvedWorkflowContext(context, selectors) {
|
|
|
195
202
|
readJson(adoptionPlanPath(topogramRoot)),
|
|
196
203
|
taskModeResult.artifact,
|
|
197
204
|
maintainedBundleResult?.artifact?.maintained_boundary || null,
|
|
198
|
-
workflowPresets
|
|
205
|
+
workflowPresets,
|
|
206
|
+
extractionContext
|
|
199
207
|
);
|
|
200
208
|
}
|
|
201
209
|
return printJson(buildResolvedWorkflowContextPayload({
|
|
@@ -205,6 +213,7 @@ function runResolvedWorkflowContext(context, selectors) {
|
|
|
205
213
|
reviewBoundary: sliceResult?.artifact?.review_boundary || null,
|
|
206
214
|
maintainedBoundary: maintainedBundleResult?.artifact?.maintained_boundary || null,
|
|
207
215
|
generatorTargets,
|
|
216
|
+
extractionContext,
|
|
208
217
|
selectors: workflowPresetSelectors(taskModeResult.artifact, context.providerId, context.presetId, "resolved-workflow-context")
|
|
209
218
|
}));
|
|
210
219
|
}
|
|
@@ -4,6 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
6
|
import { generateWorkspace } from "../../../generator.js";
|
|
7
|
+
import { TOPOGRAM_IMPORT_FILE } from "../../../import/provenance.js";
|
|
7
8
|
import { formatValidationErrors } from "../../../validator.js";
|
|
8
9
|
import { buildChangePlanPayload } from "../../../agent-ops/query-builders.js";
|
|
9
10
|
import { resolveTopoRoot } from "../../../workspace-paths.js";
|
|
@@ -196,6 +197,73 @@ export function readJson(filePath) {
|
|
|
196
197
|
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
197
198
|
}
|
|
198
199
|
|
|
200
|
+
/**
|
|
201
|
+
* @param {AnyRecord} record
|
|
202
|
+
* @param {string} provenancePath
|
|
203
|
+
* @returns {AnyRecord}
|
|
204
|
+
*/
|
|
205
|
+
export function buildExtractionContext(record, provenancePath) {
|
|
206
|
+
const extractorPackages = /** @type {AnyRecord[]} */ (Array.isArray(record.extract?.extractorPackages)
|
|
207
|
+
? record.extract.extractorPackages
|
|
208
|
+
: []);
|
|
209
|
+
const packageBackedExtractors = extractorPackages
|
|
210
|
+
.filter((entry) => entry?.source === "package")
|
|
211
|
+
.map((entry) => ({
|
|
212
|
+
id: entry.id || null,
|
|
213
|
+
version: entry.version || null,
|
|
214
|
+
packageName: entry.packageName || null,
|
|
215
|
+
extractors: Array.isArray(entry.extractors) ? entry.extractors : [],
|
|
216
|
+
manifestPath: entry.manifestPath || null
|
|
217
|
+
}));
|
|
218
|
+
const bundledExtractors = extractorPackages
|
|
219
|
+
.filter((entry) => entry?.source === "bundled")
|
|
220
|
+
.map((entry) => ({
|
|
221
|
+
id: entry.id || null,
|
|
222
|
+
version: entry.version || null,
|
|
223
|
+
extractors: Array.isArray(entry.extractors) ? entry.extractors : []
|
|
224
|
+
}));
|
|
225
|
+
return {
|
|
226
|
+
type: "extraction_context",
|
|
227
|
+
provenance_path: provenancePath,
|
|
228
|
+
kind: record.kind || null,
|
|
229
|
+
extracted_at: record.extractedAt || null,
|
|
230
|
+
refreshed_at: record.refreshedAt || null,
|
|
231
|
+
source_path: record.source?.path || null,
|
|
232
|
+
tracks: Array.isArray(record.extract?.tracks) ? record.extract.tracks : [],
|
|
233
|
+
findings_count: record.extract?.findingsCount || 0,
|
|
234
|
+
candidate_counts: record.extract?.candidateCounts || {},
|
|
235
|
+
package_backed_extractors: packageBackedExtractors,
|
|
236
|
+
bundled_extractors: bundledExtractors,
|
|
237
|
+
summary: {
|
|
238
|
+
package_backed_extractor_count: packageBackedExtractors.length,
|
|
239
|
+
bundled_extractor_count: bundledExtractors.length,
|
|
240
|
+
source_file_count: Array.isArray(record.files) ? record.files.length : 0
|
|
241
|
+
},
|
|
242
|
+
next_commands: [
|
|
243
|
+
"topogram extract check",
|
|
244
|
+
"topogram extract plan",
|
|
245
|
+
"topogram adopt --list",
|
|
246
|
+
"topogram adopt <selector> --dry-run"
|
|
247
|
+
],
|
|
248
|
+
safety_notes: [
|
|
249
|
+
"Extractor packages are evidence producers only; review candidates before canonical adoption.",
|
|
250
|
+
"Use dry-run adoption before --write, especially when package-backed extractors contributed candidates."
|
|
251
|
+
]
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* @param {string} topogramRoot
|
|
257
|
+
* @returns {AnyRecord|null}
|
|
258
|
+
*/
|
|
259
|
+
export function readExtractionContext(topogramRoot) {
|
|
260
|
+
const provenancePath = path.join(path.dirname(topogramRoot), TOPOGRAM_IMPORT_FILE);
|
|
261
|
+
if (!fs.existsSync(provenancePath)) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
return buildExtractionContext(readJson(provenancePath), provenancePath);
|
|
265
|
+
}
|
|
266
|
+
|
|
199
267
|
/**
|
|
200
268
|
* @param {AnyRecord} options
|
|
201
269
|
* @returns {boolean}
|
package/src/cli/commands/sdlc.js
CHANGED
|
@@ -220,6 +220,21 @@ export async function runSdlcCommand(context) {
|
|
|
220
220
|
return result.ok ? 0 : 1;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
if (commandArgs.sdlcCommand === "audit") {
|
|
224
|
+
const { auditWorkspace } = await import("../../sdlc/audit.js");
|
|
225
|
+
const resolved = resolveSdlcWorkspace(sdlcRoot);
|
|
226
|
+
if (!resolved) {
|
|
227
|
+
return 1;
|
|
228
|
+
}
|
|
229
|
+
const result = auditWorkspace(sdlcRoot, resolved);
|
|
230
|
+
if (json) {
|
|
231
|
+
console.log(stableStringify(result));
|
|
232
|
+
} else {
|
|
233
|
+
printSdlcAudit(result);
|
|
234
|
+
}
|
|
235
|
+
return result.ok ? 0 : 1;
|
|
236
|
+
}
|
|
237
|
+
|
|
223
238
|
if (commandArgs.sdlcCommand === "link") {
|
|
224
239
|
const { linkSdlcRecord } = await import("../../sdlc/link.js");
|
|
225
240
|
const result = linkSdlcRecord(sdlcRoot, commandArgs.sdlcFromId, commandArgs.sdlcToId, {
|
|
@@ -374,3 +389,30 @@ export async function runSdlcCommand(context) {
|
|
|
374
389
|
|
|
375
390
|
throw new Error(`Unknown sdlc command '${commandArgs.sdlcCommand}'`);
|
|
376
391
|
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* @param {AnyRecord} result
|
|
395
|
+
*/
|
|
396
|
+
function printSdlcAudit(result) {
|
|
397
|
+
console.log("SDLC audit");
|
|
398
|
+
console.log(`Workspace: ${result.workspaceRoot}`);
|
|
399
|
+
console.log(`Draft requirements with completed task evidence: ${result.counts?.draftRequirementsWithCompletedTasks || 0}`);
|
|
400
|
+
console.log(`Draft acceptance criteria with completed task evidence: ${result.counts?.draftAcceptanceCriteriaWithCompletedTasks || 0}`);
|
|
401
|
+
console.log(`Approved acceptance criteria with draft parent requirements: ${result.counts?.approvedAcceptanceCriteriaWithDraftRequirements || 0}`);
|
|
402
|
+
console.log(`Done tasks with draft refs: ${result.counts?.doneTasksWithDraftReferences || 0}`);
|
|
403
|
+
console.log(`Remaining draft backlog: ${result.counts?.remainingDraftPitches || 0} pitch(es), ${result.counts?.remainingDraftRequirements || 0} requirement(s), ${result.counts?.remainingDraftAcceptanceCriteria || 0} acceptance criterion/criteria`);
|
|
404
|
+
const findings = [
|
|
405
|
+
...(result.findings?.draftRequirementsWithCompletedTasks || []),
|
|
406
|
+
...(result.findings?.draftAcceptanceCriteriaWithCompletedTasks || []),
|
|
407
|
+
...(result.findings?.approvedAcceptanceCriteriaWithDraftRequirements || []),
|
|
408
|
+
...(result.findings?.doneTasksWithDraftReferences || [])
|
|
409
|
+
];
|
|
410
|
+
if (findings.length > 0) {
|
|
411
|
+
console.log("Actionable findings:");
|
|
412
|
+
for (const finding of findings.slice(0, 10)) {
|
|
413
|
+
console.log(`- ${finding.id}: ${finding.recommendedCommand || "review status"}`);
|
|
414
|
+
}
|
|
415
|
+
} else {
|
|
416
|
+
console.log("Actionable findings: none");
|
|
417
|
+
}
|
|
418
|
+
}
|
package/src/cli/help.js
CHANGED
|
@@ -19,6 +19,7 @@ export function printUsage(options = {}) {
|
|
|
19
19
|
console.log(" or: topogram sdlc policy init|check|explain [path] [--json]");
|
|
20
20
|
console.log(" or: topogram sdlc gate [path] --base <ref> --head <ref> [--sdlc-id <id>] [--exemption <text>] [--require-adopted] [--json]");
|
|
21
21
|
console.log(" or: topogram sdlc prep commit [path] [--base <ref> --head <ref>] [--json]");
|
|
22
|
+
console.log(" or: topogram sdlc audit [path] [--json]");
|
|
22
23
|
console.log(" or: topogram sdlc link <from-id> <to-id> [path] [--write]");
|
|
23
24
|
console.log(" or: topogram sdlc complete <task-id> [path] --verification <verification-id> [--dry-run|--write]");
|
|
24
25
|
console.log(" or: topogram sdlc plan create <task-id> <slug> [path] [--write]");
|
|
@@ -94,6 +95,7 @@ export function printUsage(options = {}) {
|
|
|
94
95
|
console.log(" topogram agent brief");
|
|
95
96
|
console.log(" topogram agent brief --json");
|
|
96
97
|
console.log(" topogram sdlc policy explain");
|
|
98
|
+
console.log(" topogram sdlc audit . --json");
|
|
97
99
|
console.log(" topogram sdlc prep commit . --base origin/main --head HEAD");
|
|
98
100
|
console.log(" topogram sdlc gate . --require-adopted");
|
|
99
101
|
console.log(" topogram sdlc plan explain plan_example --json");
|
|
@@ -128,6 +128,101 @@ function collectAffectedProjectionIds(graph, baselineGraph, diff) {
|
|
|
128
128
|
]);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
const WIDGET_CONTRACT_SECTIONS = [
|
|
132
|
+
"category",
|
|
133
|
+
"version",
|
|
134
|
+
"status",
|
|
135
|
+
"props",
|
|
136
|
+
"events",
|
|
137
|
+
"slots",
|
|
138
|
+
"behavior",
|
|
139
|
+
"behaviors",
|
|
140
|
+
"patterns",
|
|
141
|
+
"regions",
|
|
142
|
+
"approvals",
|
|
143
|
+
"lookups",
|
|
144
|
+
"dependencies"
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
function changedWidgetContractSections(entry) {
|
|
148
|
+
if (entry.classification === "additive") {
|
|
149
|
+
return WIDGET_CONTRACT_SECTIONS.filter((section) => entry.current?.[section] != null);
|
|
150
|
+
}
|
|
151
|
+
if (entry.classification === "removed") {
|
|
152
|
+
return WIDGET_CONTRACT_SECTIONS.filter((section) => entry.baseline?.[section] != null);
|
|
153
|
+
}
|
|
154
|
+
return WIDGET_CONTRACT_SECTIONS.filter((section) =>
|
|
155
|
+
JSON.stringify(entry.current?.[section] ?? null) !== JSON.stringify(entry.baseline?.[section] ?? null)
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function affectedProjectionIdsForWidgetChange(graph, baselineGraph, entry) {
|
|
160
|
+
if (entry.classification === "additive") {
|
|
161
|
+
return relatedProjectionsForWidget(graph, entry.id);
|
|
162
|
+
}
|
|
163
|
+
if (entry.classification === "removed") {
|
|
164
|
+
return relatedProjectionsForWidget(baselineGraph, entry.id);
|
|
165
|
+
}
|
|
166
|
+
return stableSortedStrings([
|
|
167
|
+
...relatedProjectionsForWidget(graph, entry.id),
|
|
168
|
+
...relatedProjectionsForWidget(baselineGraph, entry.id)
|
|
169
|
+
]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function projectionMigrationCommands(widgetId, projectionId) {
|
|
173
|
+
return [
|
|
174
|
+
{
|
|
175
|
+
target: "ui-widget-contract",
|
|
176
|
+
command: `topogram emit ui-widget-contract ./topo --widget ${widgetId} --json`,
|
|
177
|
+
reason: `Refresh the normalized widget contract for ${widgetId}.`
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
target: "widget-conformance-report",
|
|
181
|
+
command: `topogram emit widget-conformance-report ./topo --projection ${projectionId} --json`,
|
|
182
|
+
reason: `Review projection ${projectionId} widget placement, required props, events, and region/pattern compatibility.`
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
target: "widget-behavior-report",
|
|
186
|
+
command: `topogram widget behavior ./topo --projection ${projectionId} --widget ${widgetId} --json`,
|
|
187
|
+
reason: `Review behavior data/event/action wiring for ${widgetId} on projection ${projectionId}.`
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
target: "ui-surface-contract",
|
|
191
|
+
command: `topogram emit ui-surface-contract ./topo --projection ${projectionId} --json`,
|
|
192
|
+
reason: `Refresh the surface contract consumed by stack generators for projection ${projectionId}.`
|
|
193
|
+
}
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function buildWidgetContractMigrationPlan(graph, baselineGraph, diff) {
|
|
198
|
+
const widgets = (diff.widgets || []).map((entry) => {
|
|
199
|
+
const affectedProjectionIds = affectedProjectionIdsForWidgetChange(graph, baselineGraph, entry);
|
|
200
|
+
return {
|
|
201
|
+
widget_id: entry.id,
|
|
202
|
+
classification: entry.classification,
|
|
203
|
+
changed_sections: changedWidgetContractSections(entry),
|
|
204
|
+
affected_projection_ids: affectedProjectionIds,
|
|
205
|
+
affected_projections: affectedProjectionIds
|
|
206
|
+
.map((id) => summarizeById(graph, id) || summarizeById(baselineGraph, id))
|
|
207
|
+
.filter(Boolean),
|
|
208
|
+
review_commands: affectedProjectionIds.flatMap((projectionId) => projectionMigrationCommands(entry.id, projectionId)),
|
|
209
|
+
migration_steps: [
|
|
210
|
+
"Review changed_sections to understand the semantic widget contract delta.",
|
|
211
|
+
"Refresh the widget contract and affected surface contracts.",
|
|
212
|
+
"Run conformance and behavior reports for every affected projection.",
|
|
213
|
+
"Regenerate generated-owned outputs or manually update maintained surfaces after review."
|
|
214
|
+
]
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
widgets,
|
|
220
|
+
affected_widget_ids: stableSortedStrings(widgets.map((entry) => entry.widget_id)),
|
|
221
|
+
affected_projection_ids: stableSortedStrings(widgets.flatMap((entry) => entry.affected_projection_ids)),
|
|
222
|
+
review_command_count: widgets.reduce((count, entry) => count + entry.review_commands.length, 0)
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
131
226
|
function collectAffectedCapabilityIds(graph, diff) {
|
|
132
227
|
const changedCapabilities = stableSortedStrings((diff.capabilities || []).map((entry) => entry.id));
|
|
133
228
|
const changedEntities = stableSortedStrings((diff.entities || []).map((entry) => entry.id));
|
|
@@ -213,6 +308,7 @@ export function generateContextDiff(graph, options = {}) {
|
|
|
213
308
|
const affectedCapabilities = collectAffectedCapabilityIds(graph, diff);
|
|
214
309
|
const affectedProjections = collectAffectedProjectionIds(graph, baselineGraph, diff);
|
|
215
310
|
const affectedVerifications = collectAffectedVerificationIds(graph, diff);
|
|
311
|
+
const widgetContractMigrationPlan = buildWidgetContractMigrationPlan(graph, baselineGraph, diff);
|
|
216
312
|
const changedSemanticIds = changedIdsForDiffSections(diff);
|
|
217
313
|
const affectedMaintainedStories = maintainedProofMetadata(graph).filter((item) => {
|
|
218
314
|
const emittedDependencies = item.emittedDependencies || [];
|
|
@@ -257,6 +353,7 @@ export function generateContextDiff(graph, options = {}) {
|
|
|
257
353
|
}))
|
|
258
354
|
},
|
|
259
355
|
affected_verifications: affectedVerifications.map((id) => summarizeById(graph, id) || summarizeById(baselineGraph, id)).filter(Boolean),
|
|
356
|
+
widget_contract_migration_plan: widgetContractMigrationPlan,
|
|
260
357
|
review_boundary_changes: reviewBoundaryChangeItems,
|
|
261
358
|
sdlc: sdlcChanges
|
|
262
359
|
};
|
|
@@ -6,7 +6,11 @@ import {
|
|
|
6
6
|
packageInstallHint,
|
|
7
7
|
resolvePackageManifestPath
|
|
8
8
|
} from "../../package-adapters/index.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
UI_GENERATOR_RENDERED_COMPONENT_PATTERNS,
|
|
11
|
+
UI_PATTERN_KINDS,
|
|
12
|
+
WIDGET_BEHAVIOR_KINDS
|
|
13
|
+
} from "../../ui/taxonomy.js";
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* @typedef {Object} GeneratorManifest
|
|
@@ -193,6 +197,18 @@ function isStringArray(value, nonEmpty = false) {
|
|
|
193
197
|
value.every((entry) => typeof entry === "string" && entry.length > 0);
|
|
194
198
|
}
|
|
195
199
|
|
|
200
|
+
/**
|
|
201
|
+
* @param {any} value
|
|
202
|
+
* @param {Set<string>} allowed
|
|
203
|
+
* @returns {string[]}
|
|
204
|
+
*/
|
|
205
|
+
function unsupportedStringValues(value, allowed) {
|
|
206
|
+
if (!Array.isArray(value)) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
return value.filter((entry) => typeof entry === "string" && !allowed.has(entry));
|
|
210
|
+
}
|
|
211
|
+
|
|
196
212
|
/**
|
|
197
213
|
* @param {string} oldName
|
|
198
214
|
* @param {string} newName
|
|
@@ -377,9 +393,15 @@ export function validateGeneratorManifest(manifest) {
|
|
|
377
393
|
if (manifest.widgetSupport.patterns != null && !isStringArray(manifest.widgetSupport.patterns)) {
|
|
378
394
|
errors.push(`${label} widgetSupport.patterns must be a string array`);
|
|
379
395
|
}
|
|
396
|
+
for (const pattern of unsupportedStringValues(manifest.widgetSupport.patterns, UI_PATTERN_KINDS)) {
|
|
397
|
+
errors.push(`${label} widgetSupport.patterns contains unsupported pattern '${pattern}'`);
|
|
398
|
+
}
|
|
380
399
|
if (manifest.widgetSupport.behaviors != null && !isStringArray(manifest.widgetSupport.behaviors)) {
|
|
381
400
|
errors.push(`${label} widgetSupport.behaviors must be a string array`);
|
|
382
401
|
}
|
|
402
|
+
for (const behavior of unsupportedStringValues(manifest.widgetSupport.behaviors, WIDGET_BEHAVIOR_KINDS)) {
|
|
403
|
+
errors.push(`${label} widgetSupport.behaviors contains unsupported behavior '${behavior}'`);
|
|
404
|
+
}
|
|
383
405
|
if (
|
|
384
406
|
manifest.widgetSupport.unsupported != null &&
|
|
385
407
|
!["error", "warning", "contract-only"].includes(manifest.widgetSupport.unsupported)
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Record<string, any>} AnyRecord
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const DONE_TASK_STATUSES = new Set(["done"]);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {unknown} refs
|
|
11
|
+
* @returns {string[]}
|
|
12
|
+
*/
|
|
13
|
+
function refIds(refs) {
|
|
14
|
+
if (!Array.isArray(refs)) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
return refs
|
|
18
|
+
.map((ref) => {
|
|
19
|
+
if (typeof ref === "string") {
|
|
20
|
+
return ref;
|
|
21
|
+
}
|
|
22
|
+
if (ref && typeof ref === "object" && "id" in ref) {
|
|
23
|
+
return String(ref.id);
|
|
24
|
+
}
|
|
25
|
+
return "";
|
|
26
|
+
})
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {AnyRecord} statement
|
|
32
|
+
* @returns {{ id: string, name: string|null, status: string|null, file: string|null }}
|
|
33
|
+
*/
|
|
34
|
+
function summary(statement) {
|
|
35
|
+
return {
|
|
36
|
+
id: String(statement.id || ""),
|
|
37
|
+
name: typeof statement.name === "string" ? statement.name : null,
|
|
38
|
+
status: typeof statement.status === "string" ? statement.status : null,
|
|
39
|
+
file: typeof statement.loc?.file === "string" ? statement.loc.file : null
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {Map<string, AnyRecord[]>} map
|
|
45
|
+
* @param {string} key
|
|
46
|
+
* @param {AnyRecord} value
|
|
47
|
+
*/
|
|
48
|
+
function pushMap(map, key, value) {
|
|
49
|
+
const values = map.get(key) || [];
|
|
50
|
+
values.push(value);
|
|
51
|
+
map.set(key, values);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {AnyRecord} graph
|
|
56
|
+
* @param {string} id
|
|
57
|
+
* @returns {AnyRecord|null}
|
|
58
|
+
*/
|
|
59
|
+
function byId(graph, id) {
|
|
60
|
+
const value = graph?.byId?.get ? graph.byId.get(id) : graph?.byId?.[id];
|
|
61
|
+
if (value && typeof value === "object") {
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
const byKind = graph?.byKind || {};
|
|
65
|
+
for (const statements of Object.values(byKind)) {
|
|
66
|
+
if (!Array.isArray(statements)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const found = statements.find((statement) => statement?.id === id);
|
|
70
|
+
if (found && typeof found === "object") {
|
|
71
|
+
return found;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return value && typeof value === "object" ? value : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Reports SDLC status hygiene issues that are easy to miss after implementation:
|
|
79
|
+
* draft requirements or acceptance criteria that already have completed task
|
|
80
|
+
* evidence, and approved acceptance criteria whose parent requirement is still
|
|
81
|
+
* draft. The audit is read-only.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} workspaceRoot
|
|
84
|
+
* @param {AnyRecord} resolved
|
|
85
|
+
* @returns {AnyRecord}
|
|
86
|
+
*/
|
|
87
|
+
export function auditWorkspace(workspaceRoot, resolved) {
|
|
88
|
+
const graph = resolved.graph || {};
|
|
89
|
+
const byKind = graph.byKind || {};
|
|
90
|
+
/** @type {AnyRecord[]} */
|
|
91
|
+
const tasks = Array.isArray(byKind.task) ? byKind.task : [];
|
|
92
|
+
/** @type {AnyRecord[]} */
|
|
93
|
+
const requirements = Array.isArray(byKind.requirement) ? byKind.requirement : [];
|
|
94
|
+
/** @type {AnyRecord[]} */
|
|
95
|
+
const acceptanceCriteria = Array.isArray(byKind.acceptance_criterion) ? byKind.acceptance_criterion : [];
|
|
96
|
+
/** @type {AnyRecord[]} */
|
|
97
|
+
const pitches = Array.isArray(byKind.pitch) ? byKind.pitch : [];
|
|
98
|
+
|
|
99
|
+
const doneTasks = tasks.filter((task) => DONE_TASK_STATUSES.has(String(task.status || "")));
|
|
100
|
+
/** @type {Map<string, AnyRecord[]>} */
|
|
101
|
+
const doneTasksByRequirement = new Map();
|
|
102
|
+
/** @type {Map<string, AnyRecord[]>} */
|
|
103
|
+
const doneTasksByAcceptance = new Map();
|
|
104
|
+
|
|
105
|
+
for (const task of doneTasks) {
|
|
106
|
+
for (const id of refIds(task.satisfies)) {
|
|
107
|
+
pushMap(doneTasksByRequirement, id, task);
|
|
108
|
+
}
|
|
109
|
+
for (const id of refIds(task.acceptanceRefs)) {
|
|
110
|
+
pushMap(doneTasksByAcceptance, id, task);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const draftRequirementsWithCompletedTasks = requirements
|
|
115
|
+
.filter((requirement) => requirement.status === "draft" && doneTasksByRequirement.has(String(requirement.id || "")))
|
|
116
|
+
.map((requirement) => ({
|
|
117
|
+
...summary(requirement),
|
|
118
|
+
completedTasks: (doneTasksByRequirement.get(String(requirement.id || "")) || []).map(summary),
|
|
119
|
+
recommendedCommand: `topogram sdlc transition ${requirement.id} in-review . --actor <actor> --note "<reason>"`
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
const draftAcceptanceCriteriaWithCompletedTasks = acceptanceCriteria
|
|
123
|
+
.filter((criterion) => criterion.status === "draft" && doneTasksByAcceptance.has(String(criterion.id || "")))
|
|
124
|
+
.map((criterion) => ({
|
|
125
|
+
...summary(criterion),
|
|
126
|
+
completedTasks: (doneTasksByAcceptance.get(String(criterion.id || "")) || []).map(summary),
|
|
127
|
+
recommendedCommand: `topogram sdlc transition ${criterion.id} approved . --actor <actor> --note "<reason>"`
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
const approvedAcceptanceCriteriaWithDraftRequirements = acceptanceCriteria
|
|
131
|
+
.filter((criterion) => {
|
|
132
|
+
if (criterion.status !== "approved") {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
const requirementId = criterion.requirement?.id || "";
|
|
136
|
+
const requirement = requirementId ? byId(graph, requirementId) : null;
|
|
137
|
+
return requirement?.status === "draft";
|
|
138
|
+
})
|
|
139
|
+
.map((criterion) => {
|
|
140
|
+
const requirement = byId(graph, criterion.requirement?.id || "");
|
|
141
|
+
return {
|
|
142
|
+
...summary(criterion),
|
|
143
|
+
requirement: requirement ? summary(requirement) : null,
|
|
144
|
+
recommendedCommand: requirement
|
|
145
|
+
? `topogram sdlc transition ${requirement.id} in-review . --actor <actor> --note "<reason>"`
|
|
146
|
+
: null
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const doneTasksWithDraftReferences = doneTasks
|
|
151
|
+
.map((task) => {
|
|
152
|
+
const draftRequirements = refIds(task.satisfies)
|
|
153
|
+
.map((id) => byId(graph, id))
|
|
154
|
+
.filter((statement) => statement?.status === "draft")
|
|
155
|
+
.map((statement) => summary(/** @type {AnyRecord} */ (statement)));
|
|
156
|
+
const draftAcceptanceCriteria = refIds(task.acceptanceRefs)
|
|
157
|
+
.map((id) => byId(graph, id))
|
|
158
|
+
.filter((statement) => statement?.status === "draft")
|
|
159
|
+
.map((statement) => summary(/** @type {AnyRecord} */ (statement)));
|
|
160
|
+
return {
|
|
161
|
+
...summary(task),
|
|
162
|
+
draftRequirements,
|
|
163
|
+
draftAcceptanceCriteria
|
|
164
|
+
};
|
|
165
|
+
})
|
|
166
|
+
.filter((task) => task.draftRequirements.length > 0 || task.draftAcceptanceCriteria.length > 0);
|
|
167
|
+
|
|
168
|
+
const counts = {
|
|
169
|
+
draftRequirementsWithCompletedTasks: draftRequirementsWithCompletedTasks.length,
|
|
170
|
+
draftAcceptanceCriteriaWithCompletedTasks: draftAcceptanceCriteriaWithCompletedTasks.length,
|
|
171
|
+
approvedAcceptanceCriteriaWithDraftRequirements: approvedAcceptanceCriteriaWithDraftRequirements.length,
|
|
172
|
+
doneTasksWithDraftReferences: doneTasksWithDraftReferences.length,
|
|
173
|
+
remainingDraftPitches: pitches.filter((pitch) => pitch.status === "draft").length,
|
|
174
|
+
remainingDraftRequirements: requirements.filter((requirement) => requirement.status === "draft").length,
|
|
175
|
+
remainingDraftAcceptanceCriteria: acceptanceCriteria.filter((criterion) => criterion.status === "draft").length
|
|
176
|
+
};
|
|
177
|
+
const actionableFindings =
|
|
178
|
+
counts.draftRequirementsWithCompletedTasks +
|
|
179
|
+
counts.draftAcceptanceCriteriaWithCompletedTasks +
|
|
180
|
+
counts.approvedAcceptanceCriteriaWithDraftRequirements +
|
|
181
|
+
counts.doneTasksWithDraftReferences;
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
type: "sdlc_audit",
|
|
185
|
+
version: "1",
|
|
186
|
+
ok: actionableFindings === 0,
|
|
187
|
+
workspaceRoot,
|
|
188
|
+
counts,
|
|
189
|
+
findings: {
|
|
190
|
+
draftRequirementsWithCompletedTasks,
|
|
191
|
+
draftAcceptanceCriteriaWithCompletedTasks,
|
|
192
|
+
approvedAcceptanceCriteriaWithDraftRequirements,
|
|
193
|
+
doneTasksWithDraftReferences
|
|
194
|
+
},
|
|
195
|
+
remainingDrafts: {
|
|
196
|
+
pitches: pitches.filter((pitch) => pitch.status === "draft").map(summary),
|
|
197
|
+
requirements: requirements.filter((requirement) => requirement.status === "draft").map(summary),
|
|
198
|
+
acceptanceCriteria: acceptanceCriteria.filter((criterion) => criterion.status === "draft").map(summary)
|
|
199
|
+
},
|
|
200
|
+
nextCommands: [
|
|
201
|
+
"topogram sdlc check --strict",
|
|
202
|
+
"topogram sdlc prep commit . --json"
|
|
203
|
+
]
|
|
204
|
+
};
|
|
205
|
+
}
|
package/src/ui/taxonomy.js
CHANGED
|
@@ -155,6 +155,51 @@ export const UI_GENERATOR_RENDERED_COMPONENT_PATTERNS = new Set([
|
|
|
155
155
|
"data_grid_view"
|
|
156
156
|
]);
|
|
157
157
|
|
|
158
|
+
export const WIDGET_CATEGORIES = new Set([
|
|
159
|
+
"collection",
|
|
160
|
+
"form",
|
|
161
|
+
"display",
|
|
162
|
+
"navigation",
|
|
163
|
+
"dialog",
|
|
164
|
+
"feedback",
|
|
165
|
+
"lookup",
|
|
166
|
+
"layout",
|
|
167
|
+
"service"
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
export const WIDGET_BEHAVIOR_KINDS = new Set([
|
|
171
|
+
"selection",
|
|
172
|
+
"sorting",
|
|
173
|
+
"filtering",
|
|
174
|
+
"search",
|
|
175
|
+
"pagination",
|
|
176
|
+
"grouping",
|
|
177
|
+
"drag_drop",
|
|
178
|
+
"inline_edit",
|
|
179
|
+
"bulk_action",
|
|
180
|
+
"optimistic_update",
|
|
181
|
+
"realtime_update",
|
|
182
|
+
"keyboard_navigation"
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
export const WIDGET_BEHAVIOR_DIRECTIVES = {
|
|
186
|
+
selection: new Set(["mode", "state", "emits"]),
|
|
187
|
+
sorting: new Set(["fields", "default"]),
|
|
188
|
+
filtering: new Set(["fields"]),
|
|
189
|
+
search: new Set(["fields"]),
|
|
190
|
+
pagination: new Set(["mode", "page_size"]),
|
|
191
|
+
grouping: new Set(["fields"]),
|
|
192
|
+
drag_drop: new Set(["axis", "reorder"]),
|
|
193
|
+
inline_edit: new Set(["fields", "submit", "emits"]),
|
|
194
|
+
bulk_action: new Set(["actions", "state", "emits"]),
|
|
195
|
+
optimistic_update: new Set(["actions", "rollback"]),
|
|
196
|
+
realtime_update: new Set(["source", "merge"]),
|
|
197
|
+
keyboard_navigation: new Set(["scope", "shortcuts"])
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const WIDGET_SELECTION_MODES = new Set(["single", "multi", "none"]);
|
|
201
|
+
export const WIDGET_PAGINATION_MODES = new Set(["cursor", "paged", "infinite", "none"]);
|
|
202
|
+
|
|
158
203
|
/**
|
|
159
204
|
* @param {string[]} presentations
|
|
160
205
|
* @returns {string}
|
package/src/validator/kinds.d.ts
CHANGED
|
@@ -47,4 +47,9 @@ export const UI_DESIGN_COLOR_ROLES: Set<string>;
|
|
|
47
47
|
export const UI_DESIGN_TYPOGRAPHY_ROLES: Set<string>;
|
|
48
48
|
export const UI_DESIGN_ACTION_ROLES: Set<string>;
|
|
49
49
|
export const UI_DESIGN_ACCESSIBILITY_VALUES: Record<string, Set<string>>;
|
|
50
|
+
export const WIDGET_CATEGORIES: Set<string>;
|
|
51
|
+
export const WIDGET_BEHAVIOR_KINDS: Set<string>;
|
|
52
|
+
export const WIDGET_BEHAVIOR_DIRECTIVES: Record<string, Set<string>>;
|
|
53
|
+
export const WIDGET_SELECTION_MODES: Set<string>;
|
|
54
|
+
export const WIDGET_PAGINATION_MODES: Set<string>;
|
|
50
55
|
export const FIELD_SPECS: Record<string, { required: string[]; allowed: string[] }>;
|
package/src/validator/kinds.js
CHANGED
|
@@ -107,7 +107,12 @@ export {
|
|
|
107
107
|
UI_DESIGN_COLOR_ROLES,
|
|
108
108
|
UI_DESIGN_TYPOGRAPHY_ROLES,
|
|
109
109
|
UI_DESIGN_ACTION_ROLES,
|
|
110
|
-
UI_DESIGN_ACCESSIBILITY_VALUES
|
|
110
|
+
UI_DESIGN_ACCESSIBILITY_VALUES,
|
|
111
|
+
WIDGET_CATEGORIES,
|
|
112
|
+
WIDGET_BEHAVIOR_KINDS,
|
|
113
|
+
WIDGET_BEHAVIOR_DIRECTIVES,
|
|
114
|
+
WIDGET_SELECTION_MODES,
|
|
115
|
+
WIDGET_PAGINATION_MODES
|
|
111
116
|
} from "../ui/taxonomy.js";
|
|
112
117
|
|
|
113
118
|
// Kinds that may carry an optional singular `domain dom_x` field. Keep this
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
import {
|
|
3
|
+
WIDGET_BEHAVIOR_DIRECTIVES,
|
|
4
|
+
WIDGET_BEHAVIOR_KINDS,
|
|
5
|
+
WIDGET_CATEGORIES,
|
|
6
|
+
WIDGET_PAGINATION_MODES,
|
|
7
|
+
WIDGET_SELECTION_MODES,
|
|
3
8
|
UI_PATTERN_KINDS,
|
|
4
9
|
UI_REGION_KINDS
|
|
5
10
|
} from "../kinds.js";
|
|
@@ -10,49 +15,6 @@ import {
|
|
|
10
15
|
symbolValues
|
|
11
16
|
} from "../utils.js";
|
|
12
17
|
|
|
13
|
-
const WIDGET_CATEGORIES = new Set([
|
|
14
|
-
"collection",
|
|
15
|
-
"form",
|
|
16
|
-
"display",
|
|
17
|
-
"navigation",
|
|
18
|
-
"dialog",
|
|
19
|
-
"feedback",
|
|
20
|
-
"lookup",
|
|
21
|
-
"layout",
|
|
22
|
-
"service"
|
|
23
|
-
]);
|
|
24
|
-
|
|
25
|
-
const WIDGET_BEHAVIOR_KINDS = new Set([
|
|
26
|
-
"selection",
|
|
27
|
-
"sorting",
|
|
28
|
-
"filtering",
|
|
29
|
-
"search",
|
|
30
|
-
"pagination",
|
|
31
|
-
"grouping",
|
|
32
|
-
"drag_drop",
|
|
33
|
-
"inline_edit",
|
|
34
|
-
"bulk_action",
|
|
35
|
-
"optimistic_update",
|
|
36
|
-
"realtime_update",
|
|
37
|
-
"keyboard_navigation"
|
|
38
|
-
]);
|
|
39
|
-
|
|
40
|
-
/** @type {Record<string, Set<string>>} */
|
|
41
|
-
const WIDGET_BEHAVIOR_DIRECTIVES = {
|
|
42
|
-
selection: new Set(["mode", "state", "emits"]),
|
|
43
|
-
sorting: new Set(["fields", "default"]),
|
|
44
|
-
filtering: new Set(["fields"]),
|
|
45
|
-
search: new Set(["fields"]),
|
|
46
|
-
pagination: new Set(["mode", "page_size"]),
|
|
47
|
-
grouping: new Set(["fields"]),
|
|
48
|
-
drag_drop: new Set(["axis", "reorder"]),
|
|
49
|
-
inline_edit: new Set(["fields", "submit", "emits"]),
|
|
50
|
-
bulk_action: new Set(["actions", "state", "emits"]),
|
|
51
|
-
optimistic_update: new Set(["actions", "rollback"]),
|
|
52
|
-
realtime_update: new Set(["source", "merge"]),
|
|
53
|
-
keyboard_navigation: new Set(["scope", "shortcuts"])
|
|
54
|
-
};
|
|
55
|
-
|
|
56
18
|
/** @param {TopogramToken | null | undefined} token @returns {any} */
|
|
57
19
|
function tokenValue(token) {
|
|
58
20
|
return token?.value ?? null;
|
|
@@ -262,10 +224,10 @@ function validateWidgetBehaviors(errors, statement, fieldMap, registry) {
|
|
|
262
224
|
if (directive === "actions" || directive === "submit") {
|
|
263
225
|
validateBehaviorActionReferences(errors, statement, registry, kind, directive, valueToken, eventNames);
|
|
264
226
|
}
|
|
265
|
-
if (kind === "selection" && directive === "mode" && !
|
|
227
|
+
if (kind === "selection" && directive === "mode" && !WIDGET_SELECTION_MODES.has(tokenValue(valueToken))) {
|
|
266
228
|
pushError(errors, `Widget ${statement.id} behavior 'selection' has invalid mode '${tokenValue(valueToken)}'`, valueToken.loc);
|
|
267
229
|
}
|
|
268
|
-
if (kind === "pagination" && directive === "mode" && !
|
|
230
|
+
if (kind === "pagination" && directive === "mode" && !WIDGET_PAGINATION_MODES.has(tokenValue(valueToken))) {
|
|
269
231
|
pushError(errors, `Widget ${statement.id} behavior 'pagination' has invalid mode '${tokenValue(valueToken)}'`, valueToken.loc);
|
|
270
232
|
}
|
|
271
233
|
}
|