@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topogram/cli",
3
- "version": "0.3.81",
3
+ "version": "0.3.83",
4
4
  "description": "Topogram CLI for checking Topogram workspaces and generating app bundles.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -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 importPlan = buildImportPlanPayload(adoptionPlanArtifact, taskModeResult.artifact, null, workflowPresets);
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}
@@ -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 { UI_GENERATOR_RENDERED_COMPONENT_PATTERNS } from "../../ui/taxonomy.js";
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
+ }
@@ -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}
@@ -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[] }>;
@@ -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" && !["single", "multi", "none"].includes(tokenValue(valueToken))) {
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" && !["cursor", "paged", "infinite", "none"].includes(tokenValue(valueToken))) {
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
  }