@topogram/cli 0.3.63 → 0.3.64

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.
Files changed (121) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan.d.ts +6 -0
  3. package/src/adoption/reporting.d.ts +10 -0
  4. package/src/adoption/review-groups.d.ts +6 -0
  5. package/src/agent-brief.d.ts +3 -0
  6. package/src/agent-brief.js +495 -0
  7. package/src/agent-ops/query-builders.d.ts +26 -0
  8. package/src/archive/archive.d.ts +2 -0
  9. package/src/archive/compact.d.ts +1 -0
  10. package/src/archive/unarchive.d.ts +1 -0
  11. package/src/catalog.d.ts +10 -0
  12. package/src/catalog.js +62 -66
  13. package/src/cli/catalog-alias.d.ts +1 -0
  14. package/src/cli/command-parser.js +38 -0
  15. package/src/cli/command-parsers/core.js +102 -0
  16. package/src/cli/command-parsers/generator.js +39 -0
  17. package/src/cli/command-parsers/import.js +44 -0
  18. package/src/cli/command-parsers/legacy-workflow.js +21 -0
  19. package/src/cli/command-parsers/project.js +47 -0
  20. package/src/cli/command-parsers/sdlc.js +47 -0
  21. package/src/cli/command-parsers/shared.js +51 -0
  22. package/src/cli/command-parsers/template.js +48 -0
  23. package/src/cli/commands/agent.js +47 -0
  24. package/src/cli/commands/catalog.js +617 -0
  25. package/src/cli/commands/check.js +268 -0
  26. package/src/cli/commands/doctor.js +268 -0
  27. package/src/cli/commands/emit.js +149 -0
  28. package/src/cli/commands/generate.js +96 -0
  29. package/src/cli/commands/generator-policy.js +785 -0
  30. package/src/cli/commands/generator.js +443 -0
  31. package/src/cli/commands/import-runner.js +157 -0
  32. package/src/cli/commands/import.js +1734 -0
  33. package/src/cli/commands/inspect.js +55 -0
  34. package/src/cli/commands/new.js +94 -0
  35. package/src/cli/commands/package.js +815 -0
  36. package/src/cli/commands/query.js +1302 -0
  37. package/src/cli/commands/release-rollout.js +257 -0
  38. package/src/cli/commands/release-shared.js +528 -0
  39. package/src/cli/commands/release-status.js +429 -0
  40. package/src/cli/commands/release.js +107 -0
  41. package/src/cli/commands/sdlc.js +168 -0
  42. package/src/cli/commands/setup.js +76 -0
  43. package/src/cli/commands/source.js +291 -0
  44. package/src/cli/commands/template-runner.js +198 -0
  45. package/src/cli/commands/template.js +2145 -0
  46. package/src/cli/commands/trust.js +219 -0
  47. package/src/cli/commands/version.js +40 -0
  48. package/src/cli/commands/widget.js +168 -0
  49. package/src/cli/commands/workflow.js +63 -0
  50. package/src/cli/dispatcher.js +392 -0
  51. package/src/cli/help-dispatch.js +188 -0
  52. package/src/cli/help.js +296 -0
  53. package/src/cli/migration-guidance.js +59 -0
  54. package/src/cli/options.js +96 -0
  55. package/src/cli/output-safety.js +107 -0
  56. package/src/cli/path-normalization.js +29 -0
  57. package/src/cli.js +47 -11711
  58. package/src/example-implementation.d.ts +2 -0
  59. package/src/format.d.ts +1 -0
  60. package/src/generator/check.d.ts +1 -0
  61. package/src/generator/context/bundle.d.ts +1 -0
  62. package/src/generator/context/shared.d.ts +2 -0
  63. package/src/generator/native/parity-bundle.js +2 -1
  64. package/src/generator/surfaces/web/html-escape.js +22 -0
  65. package/src/generator/surfaces/web/react.js +10 -8
  66. package/src/generator/surfaces/web/sveltekit.js +7 -5
  67. package/src/generator/surfaces/web/vanilla.js +8 -4
  68. package/src/generator.d.ts +2 -0
  69. package/src/github-client.js +520 -0
  70. package/src/import/core/shared.js +20 -62
  71. package/src/import/extractors/api/flutter-dio.js +4 -8
  72. package/src/import/extractors/api/react-native-repository.js +4 -8
  73. package/src/import/index.d.ts +4 -0
  74. package/src/import/provenance.d.ts +4 -0
  75. package/src/new-project.js +100 -11
  76. package/src/npm-safety.js +79 -0
  77. package/src/parser.d.ts +1 -0
  78. package/src/path-helpers.d.ts +1 -0
  79. package/src/path-helpers.js +20 -0
  80. package/src/project-config.js +1 -0
  81. package/src/reconcile/docs.d.ts +8 -0
  82. package/src/reconcile/journeys.d.ts +1 -0
  83. package/src/resolver.d.ts +1 -0
  84. package/src/runtime-support.js +29 -0
  85. package/src/sdlc/adopt.d.ts +1 -0
  86. package/src/sdlc/check.d.ts +1 -0
  87. package/src/sdlc/explain.d.ts +1 -0
  88. package/src/sdlc/release.d.ts +1 -0
  89. package/src/sdlc/scaffold.d.ts +1 -0
  90. package/src/sdlc/transition.d.ts +1 -0
  91. package/src/text-helpers.d.ts +6 -0
  92. package/src/text-helpers.js +245 -0
  93. package/src/topogram-config.js +306 -0
  94. package/src/validator.d.ts +2 -0
  95. package/src/workflows/adoption/index.js +26 -0
  96. package/src/workflows/docs-generate.js +262 -0
  97. package/src/workflows/docs-scan.js +703 -0
  98. package/src/workflows/docs.js +15 -0
  99. package/src/workflows/import-app/api.js +799 -0
  100. package/src/workflows/import-app/db.js +538 -0
  101. package/src/workflows/import-app/index.js +30 -0
  102. package/src/workflows/import-app/shared.js +218 -0
  103. package/src/workflows/import-app/ui.js +443 -0
  104. package/src/workflows/import-app/workflow.js +159 -0
  105. package/src/workflows/reconcile/adoption-plan.js +742 -0
  106. package/src/workflows/reconcile/auth.js +692 -0
  107. package/src/workflows/reconcile/bundle-core.js +600 -0
  108. package/src/workflows/reconcile/bundle-shared.js +75 -0
  109. package/src/workflows/reconcile/candidate-model.js +477 -0
  110. package/src/workflows/reconcile/canonical-surface.js +264 -0
  111. package/src/workflows/reconcile/gap-report.js +333 -0
  112. package/src/workflows/reconcile/ids.js +6 -0
  113. package/src/workflows/reconcile/impacts.js +625 -0
  114. package/src/workflows/reconcile/index.js +7 -0
  115. package/src/workflows/reconcile/renderers.js +461 -0
  116. package/src/workflows/reconcile/summary.js +90 -0
  117. package/src/workflows/reconcile/workflow.js +309 -0
  118. package/src/workflows/shared.js +189 -0
  119. package/src/workflows/types.d.ts +93 -0
  120. package/src/workflows.d.ts +1 -0
  121. package/src/workflows.js +10 -7652
@@ -0,0 +1,600 @@
1
+ // @ts-check
2
+ import { ensureTrailingNewline, idHintify, slugify, titleCase } from "../../text-helpers.js";
3
+ import {
4
+ annotateBundleAuthHintClosures,
5
+ buildAuthClaimReviewGuidance,
6
+ buildAuthHintClosureSummary,
7
+ buildAuthOwnershipReviewGuidance,
8
+ buildAuthPermissionReviewGuidance,
9
+ buildAuthRoleReviewGuidance,
10
+ formatAuthClaimHintInline,
11
+ formatAuthOwnershipHintInline,
12
+ formatAuthPermissionHintInline,
13
+ formatAuthRoleGuidanceInline
14
+ } from "./auth.js";
15
+ import { collectBundleProvenance, collectBundleRuleIds, primaryEntityIdForBundle } from "./bundle-shared.js";
16
+ import { buildBundleOperatorSummary, summarizeBundleSurface } from "./summary.js";
17
+
18
+ /** @param {any} conceptId @returns {any} */
19
+ export function bundleKeyForConcept(conceptId) {
20
+ return slugify(String(conceptId || "").replace(/^entity_/, "").replace(/^enum_/, "")) || "candidate";
21
+ }
22
+
23
+ /** @param {Map<string, CandidateBundle>} bundles @param {any} conceptId @param {string} label @returns {any} */
24
+ export function getOrCreateCandidateBundle(bundles, conceptId, label) {
25
+ const key = conceptId || `bundle_${bundles.size + 1}`;
26
+ if (!bundles.has(key)) {
27
+ bundles.set(key, {
28
+ id: key,
29
+ slug: bundleKeyForConcept(key),
30
+ label: label || titleCase(String(key).replace(/^entity_/, "").replace(/^enum_/, "")),
31
+ actors: [],
32
+ roles: [],
33
+ entities: [],
34
+ enums: [],
35
+ capabilities: [],
36
+ shapes: [],
37
+ widgets: [],
38
+ screens: [],
39
+ uiRoutes: [],
40
+ uiActions: [],
41
+ workflows: [],
42
+ verifications: [],
43
+ workflowStates: [],
44
+ workflowTransitions: [],
45
+ docs: [],
46
+ docLinkSuggestions: [],
47
+ docMetadataPatches: [],
48
+ projectionPatches: [],
49
+ importedFieldEvidence: []
50
+ });
51
+ }
52
+ return bundles.get(key);
53
+ }
54
+
55
+ /** @param {any} conceptId @returns {any} */
56
+ export function bundleLabelFromConceptId(conceptId) {
57
+ return titleCase(String(conceptId || "").replace(/^(entity|flow|surface)_/, ""));
58
+ }
59
+
60
+ /** @param {ResolvedGraph} graph @returns {any} */
61
+ export function canonicalJourneyCoverage(graph) {
62
+ const journeyDocs = (graph?.docs || []).filter((/** @type {any} */ doc) => doc.kind === "journey");
63
+ return {
64
+ byEntityId: new Set(journeyDocs.flatMap((/** @type {any} */ doc) => doc.relatedEntities || [])),
65
+ byCapabilityId: new Set(journeyDocs.flatMap((/** @type {any} */ doc) => doc.relatedCapabilities || []))
66
+ };
67
+ }
68
+
69
+ /** @param {CandidateBundle} bundle @returns {any} */
70
+ export function buildBundleJourneyDraft(bundle) {
71
+ const relatedCapabilities = [...new Set((bundle.capabilities || []).map((/** @type {any} */ entry) => entry.id_hint))].sort();
72
+ const relatedWorkflows = [...new Set((bundle.workflows || []).map((/** @type {any} */ entry) => entry.id_hint))].sort();
73
+ const relatedRules = collectBundleRuleIds(bundle);
74
+ const relatedActors = [...new Set((bundle.actors || []).map((/** @type {any} */ entry) => entry.id_hint))].sort();
75
+ const relatedRoles = [...new Set((bundle.roles || []).map((/** @type {any} */ entry) => entry.id_hint))].sort();
76
+ const primaryEntityId = primaryEntityIdForBundle(bundle);
77
+ const relatedEntities = [...new Set([primaryEntityId, ...(bundle.entities || []).map((/** @type {any} */ entry) => entry.id_hint)].filter(Boolean))].slice(0, 4);
78
+ const routePaths = [...new Set((bundle.uiRoutes || []).map((/** @type {any} */ entry) => entry.path).filter(Boolean))];
79
+ const screenIds = [...new Set((bundle.screens || []).map((/** @type {any} */ entry) => entry.id_hint))];
80
+ const screenKinds = [...new Set((bundle.screens || []).map((/** @type {any} */ entry) => entry.screen_kind).filter(Boolean))];
81
+ const createCapabilities = relatedCapabilities.filter((/** @type {any} */ id) => /^cap_create_/.test(id));
82
+ const browseCapabilities = relatedCapabilities.filter((/** @type {any} */ id) => /^cap_(list|get)_/.test(id));
83
+ const lifecycleCapabilities = relatedCapabilities.filter((/** @type {any} */ id) => /^cap_(update|close|complete|archive|delete|submit|request)_/.test(id));
84
+ const interactionCapabilities = relatedCapabilities.filter((/** @type {any} */ id) => /^cap_(favorite|unfavorite|follow|unfollow|vote|like|unlike)_/.test(id));
85
+ const authCapabilities = relatedCapabilities.filter((/** @type {any} */ id) => /^cap_(sign_in|sign_out|register|authenticate|login|logout)_/.test(id));
86
+ const participantActors = relatedActors;
87
+ const participantRoles = relatedRoles;
88
+ const hasListDetail = browseCapabilities.some((/** @type {any} */ id) => /^cap_list_/.test(id)) && browseCapabilities.some((/** @type {any} */ id) => /^cap_get_/.test(id));
89
+ const hasCreateAndLifecycle = createCapabilities.length > 0 && lifecycleCapabilities.length > 0;
90
+ const hasWorkflowEvidence = relatedWorkflows.length > 0;
91
+ const flowShape =
92
+ authCapabilities.length > 0 ? "auth" :
93
+ hasCreateAndLifecycle && hasListDetail ? "create_browse_lifecycle" :
94
+ hasListDetail && lifecycleCapabilities.length > 0 ? "browse_lifecycle" :
95
+ hasListDetail ? "browse_detail" :
96
+ createCapabilities.length > 0 ? "create" :
97
+ lifecycleCapabilities.length > 0 ? "lifecycle" :
98
+ interactionCapabilities.length > 0 ? "interaction" :
99
+ hasWorkflowEvidence ? "workflow" :
100
+ "general";
101
+ const routeEvidence = routePaths.length > 0;
102
+ const screenEvidence = screenIds.length > 0;
103
+ const browsePhrase = browseCapabilities.length > 0
104
+ ? browseCapabilities.map((/** @type {any} */ item) => `\`${item}\``).join(", ")
105
+ : createCapabilities.length > 0
106
+ ? createCapabilities.map((/** @type {any} */ item) => `\`${item}\``).join(", ")
107
+ : relatedCapabilities.slice(0, 3).map((/** @type {any} */ item) => `\`${item}\``).join(", ");
108
+ const lifecyclePhrase = lifecycleCapabilities.length > 0
109
+ ? lifecycleCapabilities.map((/** @type {any} */ item) => `\`${item}\``).join(", ")
110
+ : interactionCapabilities.length > 0
111
+ ? interactionCapabilities.map((/** @type {any} */ item) => `\`${item}\``).join(", ")
112
+ : browseCapabilities.slice(0, 2).map((/** @type {any} */ item) => `\`${item}\``).join(", ");
113
+ const startSurface = routeEvidence
114
+ ? routePaths.slice(0, 2).map((/** @type {any} */ item) => `\`${item}\``).join(" or ")
115
+ : screenEvidence
116
+ ? screenIds.slice(0, 2).map((/** @type {any} */ item) => `\`${item}\``).join(" or ")
117
+ : `the ${bundle.label.toLowerCase()} API surface`;
118
+ const continuationSurface = screenEvidence
119
+ ? `${screenKinds.length > 0 ? screenKinds.join(", ") : "screen"} surfaces ${screenIds.slice(0, 3).map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`
120
+ : routeEvidence
121
+ ? `the recovered route structure around ${routePaths.slice(0, 3).map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`
122
+ : `the recovered ${bundle.label.toLowerCase()} lifecycle`;
123
+ const participantPhrase = [...participantActors, ...participantRoles].length > 0
124
+ ? [...participantActors, ...participantRoles].map((/** @type {any} */ item) => `\`${item}\``).join(", ")
125
+ : null;
126
+ const participantVerb = participantPhrase && participantPhrase.includes(", ") ? "enter" : "enters";
127
+ const title =
128
+ flowShape === "auth" ? `${bundle.label} Sign-In and Session Flow` :
129
+ flowShape === "create_browse_lifecycle" ? `${bundle.label} Creation, Detail, and Lifecycle Flow` :
130
+ flowShape === "browse_lifecycle" ? `${bundle.label} Detail and Lifecycle Flow` :
131
+ flowShape === "browse_detail" ? `${bundle.label} Discovery and Detail Flow` :
132
+ flowShape === "create" ? `${bundle.label} Creation Flow` :
133
+ flowShape === "lifecycle" ? `${bundle.label} Lifecycle Flow` :
134
+ flowShape === "interaction" ? `${bundle.label} Interaction Flow` :
135
+ flowShape === "workflow" ? `${bundle.label} Workflow Flow` :
136
+ `${bundle.label} Core Journey`;
137
+ const intentPhrase =
138
+ flowShape === "auth"
139
+ ? `signing in and establishing ${bundle.label.toLowerCase()} access cleanly`
140
+ : flowShape === "create_browse_lifecycle"
141
+ ? `creating ${bundle.label.toLowerCase()} work, finding it again, and moving it through lifecycle changes with confidence`
142
+ : flowShape === "browse_lifecycle"
143
+ ? `opening ${bundle.label.toLowerCase()} detail state and progressing it safely`
144
+ : flowShape === "browse_detail"
145
+ ? `finding and understanding ${bundle.label.toLowerCase()} state`
146
+ : flowShape === "create"
147
+ ? `creating ${bundle.label.toLowerCase()} work safely`
148
+ : flowShape === "lifecycle"
149
+ ? `moving ${bundle.label.toLowerCase()} work through its lifecycle without losing context`
150
+ : flowShape === "interaction"
151
+ ? `performing repeated ${bundle.label.toLowerCase()} interactions without losing context`
152
+ : `moving through the recovered ${bundle.label.toLowerCase()} flow with confidence`;
153
+ /** @type {WorkflowRecord} */
154
+ const metadata = {
155
+ id: `${idHintify(bundle.slug)}_journey`,
156
+ kind: "journey",
157
+ title,
158
+ status: "inferred",
159
+ summary: `Candidate ${bundle.label.toLowerCase()} journey inferred during reconcile from imported app evidence.`,
160
+ source_of_truth: "imported",
161
+ confidence: "medium",
162
+ review_required: true,
163
+ related_entities: relatedEntities,
164
+ related_capabilities: relatedCapabilities,
165
+ related_actors: relatedActors,
166
+ related_roles: relatedRoles,
167
+ related_rules: relatedRules,
168
+ related_workflows: relatedWorkflows,
169
+ provenance: [...collectBundleProvenance(bundle)].slice(0, 8),
170
+ tags: ["import", "journey"]
171
+ };
172
+ const canonicalDestination = `docs/journeys/${metadata.id}.md`;
173
+ const recoveredSignals = [
174
+ `Capabilities: ${relatedCapabilities.length ? relatedCapabilities.map((/** @type {any} */ item) => `\`${item}\``).join(", ") : "_none_"}`,
175
+ `Workflows: ${relatedWorkflows.length ? relatedWorkflows.map((/** @type {any} */ item) => `\`${item}\``).join(", ") : "_none_"}`,
176
+ `Rules: ${relatedRules.length ? relatedRules.map((/** @type {any} */ item) => `\`${item}\``).join(", ") : "_none_"}`,
177
+ `Screens: ${screenIds.length ? screenIds.map((/** @type {any} */ item) => `\`${item}\``).join(", ") : "_none_"}`,
178
+ `Routes: ${routePaths.length ? routePaths.map((/** @type {any} */ item) => `\`${item}\``).join(", ") : "_none_"}`
179
+ ];
180
+ const body = [
181
+ "Candidate journey inferred during reconcile from imported capabilities, UI surfaces, and workflow evidence.",
182
+ "",
183
+ "Review and rewrite this draft before promoting it as canonical.",
184
+ "",
185
+ `The user intent centers on ${intentPhrase} based on the brownfield capabilities, route evidence, and workflow signals recovered for this bundle.${participantPhrase ? ` The strongest inferred participants are ${participantPhrase}.` : ""}${relatedRules.length ? ` The strongest inferred constraints come from ${relatedRules.map((/** @type {any} */ item) => `\`${item}\``).join(", ")}.` : ""}`,
186
+ "",
187
+ "## Recovered Signals",
188
+ "",
189
+ ...recoveredSignals,
190
+ "",
191
+ "## Happy Path",
192
+ "",
193
+ flowShape === "auth"
194
+ ? `1. ${participantPhrase ? `The flow begins for ${participantPhrase}` : "The user"} through ${startSurface} and provides the credentials or session input required by ${authCapabilities.map((/** @type {any} */ item) => `\`${item}\``).join(", ")}.`
195
+ : `1. ${participantPhrase ? `${participantPhrase} ${participantVerb}` : "The user enters"} the flow through ${startSurface}.`,
196
+ flowShape === "create_browse_lifecycle"
197
+ ? `2. The recovered flow uses ${createCapabilities.map((/** @type {any} */ item) => `\`${item}\``).join(", ")} to create or submit new ${bundle.label.toLowerCase()} work, then ${browseCapabilities.map((/** @type {any} */ item) => `\`${item}\``).join(", ")} to find it again.`
198
+ : flowShape === "browse_detail"
199
+ ? `2. The recovered flow uses ${browsePhrase || `the inferred ${bundle.label.toLowerCase()} capabilities`} to load or establish the current ${bundle.label.toLowerCase()} state.`
200
+ : flowShape === "auth"
201
+ ? `2. The recovered flow returns the user to the authenticated ${bundle.label.toLowerCase()} state without losing the intended next step.`
202
+ : `2. The recovered flow uses ${browsePhrase || `the inferred ${bundle.label.toLowerCase()} capabilities`} to load or establish the current ${bundle.label.toLowerCase()} state.`,
203
+ flowShape === "create_browse_lifecycle"
204
+ ? `3. The user continues through ${lifecyclePhrase || `the remaining ${bundle.label.toLowerCase()} actions`} while keeping ${continuationSurface} coherent.`
205
+ : flowShape === "interaction"
206
+ ? `3. The user can repeat ${interactionCapabilities.map((/** @type {any} */ item) => `\`${item}\``).join(", ")} while keeping ${continuationSurface} coherent.`
207
+ : `3. The user continues through ${lifecyclePhrase || `the remaining ${bundle.label.toLowerCase()} actions`} while keeping ${continuationSurface} coherent.`,
208
+ "",
209
+ "## Alternate Paths",
210
+ "",
211
+ relatedWorkflows.length > 0
212
+ ? `- Workflow evidence such as ${relatedWorkflows.map((/** @type {any} */ item) => `\`${item}\``).join(", ")} should stay aligned with the journey instead of drifting into an undocumented lifecycle.`
213
+ : "- If the brownfield app exposes alternate lifecycle branches, capture them explicitly before promoting this journey.",
214
+ relatedRules.length > 0
215
+ ? `- Rule evidence such as ${relatedRules.map((/** @type {any} */ item) => `\`${item}\``).join(", ")} should remain visible in the journey instead of being lost during promotion.`
216
+ : "- If the brownfield app enforces important constraints outside the imported model, capture them explicitly before promotion.",
217
+ routeEvidence
218
+ ? `- Recovered routes ${routePaths.slice(0, 3).map((/** @type {any} */ item) => `\`${item}\``).join(", ")} should remain understandable to the user instead of fragmenting the flow.`
219
+ : screenEvidence
220
+ ? `- Recovered screens ${screenIds.slice(0, 3).map((/** @type {any} */ item) => `\`${item}\``).join(", ")} should still read as one user-goal flow rather than disconnected views.`
221
+ : "- If only API evidence exists today, add UI or docs context before promoting this journey as canonical.",
222
+ "",
223
+ "## Change Review Notes",
224
+ "",
225
+ `Review this journey when changing ${bundle.label.toLowerCase()} capabilities, screen surfaces, route structure, or workflow transitions.${relatedRules.length ? ` Re-check ${relatedRules.map((/** @type {any} */ item) => `\`${item}\``).join(", ")} when those changes could weaken the recovered constraints.` : ""}`,
226
+ "",
227
+ "## Promotion Notes",
228
+ "",
229
+ `- Canonical destination: \`${canonicalDestination}\`.`,
230
+ "- Promote this draft with `reconcile adopt journeys --write` after reviewing participants, recovered signals, and change-review notes.",
231
+ `- Keep the promoted journey aligned with bundle \`${bundle.slug}\` so future reconcile runs continue to explain the same user-goal flow.`
232
+ ].join("\n");
233
+
234
+ return {
235
+ id: metadata.id,
236
+ kind: "journey",
237
+ title: metadata.title,
238
+ existing_canonical: false,
239
+ related_entities: metadata.related_entities,
240
+ related_capabilities: metadata.related_capabilities,
241
+ related_actors: metadata.related_actors,
242
+ related_roles: metadata.related_roles,
243
+ related_rules: metadata.related_rules,
244
+ related_workflows: metadata.related_workflows,
245
+ provenance: metadata.provenance,
246
+ source_of_truth: metadata.source_of_truth,
247
+ confidence: metadata.confidence,
248
+ status: metadata.status,
249
+ review_required: metadata.review_required,
250
+ tags: metadata.tags,
251
+ metadata,
252
+ body
253
+ };
254
+ }
255
+
256
+ /** @param {Map<string, CandidateBundle>} bundles @param {ResolvedGraph} graph @returns {any} */
257
+ export function addBundleJourneyDrafts(bundles, graph) {
258
+ const coverage = canonicalJourneyCoverage(graph);
259
+ for (const bundle of bundles.values()) {
260
+ if ((bundle.docs || []).some((/** @type {any} */ entry) => entry.kind === "journey")) {
261
+ continue;
262
+ }
263
+ if ((bundle.capabilities || []).length === 0 && (bundle.screens || []).length === 0 && (bundle.workflows || []).length === 0) {
264
+ continue;
265
+ }
266
+ const primaryEntityId = primaryEntityIdForBundle(bundle);
267
+ if (primaryEntityId && coverage.byEntityId.has(primaryEntityId)) {
268
+ continue;
269
+ }
270
+ const bundleCapabilityIds = (bundle.capabilities || []).map((/** @type {any} */ entry) => entry.id_hint);
271
+ if (bundleCapabilityIds.some((/** @type {any} */ id) => coverage.byCapabilityId.has(id))) {
272
+ continue;
273
+ }
274
+ bundle.docs.push(buildBundleJourneyDraft(bundle));
275
+ }
276
+ }
277
+
278
+ /** @param {CandidateBundle[]} bundles @param {any} previousReport @returns {any} */
279
+ export function annotateBundleAuthAging(bundles, previousReport) {
280
+ const previousBundles = new Map(
281
+ ((previousReport?.candidate_model_bundles) || []).map((/** @type {any} */ bundle) => [bundle.slug, bundle])
282
+ );
283
+ return (bundles || []).map((/** @type {any} */ bundle) => {
284
+ const previousBundle = previousBundles.get(bundle.slug);
285
+ const currentSummary = buildBundleOperatorSummary(bundle);
286
+ const previousClosureStatus = previousBundle?.operator_summary?.authClosureSummary?.status || "no_auth_hints";
287
+ const previousRepeatCount = previousBundle?.operator_summary?.authAging?.repeatCount || 0;
288
+ const currentClosureStatus = currentSummary.authClosureSummary.status;
289
+ const repeatCount =
290
+ currentClosureStatus === "high_risk"
291
+ ? (previousClosureStatus === "high_risk" ? previousRepeatCount + 1 : 1)
292
+ : 0;
293
+ const escalationLevel =
294
+ currentClosureStatus !== "high_risk"
295
+ ? "none"
296
+ : repeatCount >= 2
297
+ ? "stale_high_risk"
298
+ : "fresh_high_risk";
299
+ const escalationReason =
300
+ escalationLevel === "stale_high_risk"
301
+ ? `This bundle has stayed high risk for ${repeatCount} reconcile runs in a row.`
302
+ : escalationLevel === "fresh_high_risk"
303
+ ? "This bundle is newly high risk in the current reconcile run."
304
+ : "This bundle is not currently high risk.";
305
+ return {
306
+ ...bundle,
307
+ operatorSummary: {
308
+ ...currentSummary,
309
+ authAging: {
310
+ repeatCount,
311
+ escalationLevel,
312
+ escalationReason
313
+ }
314
+ }
315
+ };
316
+ });
317
+ }
318
+
319
+ /** @param {CandidateBundle} bundle @param {any[]} proposalSurfaces @returns {any} */
320
+ export function renderCandidateBundleReadme(bundle, proposalSurfaces = []) {
321
+ const summary = buildBundleOperatorSummary(bundle);
322
+ const journeyDrafts = (bundle.docs || []).filter((/** @type {any} */ entry) => entry.kind === "journey" && entry.review_required !== false);
323
+ const lines = [
324
+ `# ${bundle.label} Candidate Bundle`,
325
+ "",
326
+ `Concept id: \`${bundle.id}\``,
327
+ "",
328
+ `Actors: ${bundle.actors.length}`,
329
+ `Roles: ${bundle.roles.length}`,
330
+ `Entities: ${bundle.entities.length}`,
331
+ `Enums: ${bundle.enums.length}`,
332
+ `Capabilities: ${bundle.capabilities.length}`,
333
+ `Shapes: ${bundle.shapes.length}`,
334
+ `Widgets: ${bundle.widgets.length}`,
335
+ `Screens: ${bundle.screens.length}`,
336
+ `UI routes: ${bundle.uiRoutes.length}`,
337
+ `UI actions: ${bundle.uiActions.length}`,
338
+ `Workflows: ${bundle.workflows.length}`,
339
+ `Verifications: ${bundle.verifications.length}`,
340
+ `Workflow states: ${bundle.workflowStates.length}`,
341
+ `Workflow transitions: ${bundle.workflowTransitions.length}`,
342
+ `Docs: ${bundle.docs.length}`
343
+ ];
344
+ lines.push(
345
+ "",
346
+ "## Operator Summary",
347
+ "",
348
+ `- Primary concept: \`${summary.primaryConcept}\``,
349
+ `- Primary entity: ${summary.primaryEntityId ? `\`${summary.primaryEntityId}\`` : "_none_"}`,
350
+ `- Participants: ${summary.participants.label}`,
351
+ `- Main capabilities: ${summarizeBundleSurface(bundle, summary.capabilityIds)}`,
352
+ `- Main widgets: ${summarizeBundleSurface(bundle, summary.widgetIds)}`,
353
+ `- Main screens: ${summarizeBundleSurface(bundle, summary.screenIds)}`,
354
+ `- Main routes: ${summarizeBundleSurface(bundle, summary.routePaths)}`,
355
+ `- Main workflows: ${summarizeBundleSurface(bundle, summary.workflowIds)}`,
356
+ `- Auth permission hints: ${summary.authPermissionHints.length ? summary.authPermissionHints.map((/** @type {any} */ entry) => formatAuthPermissionHintInline(entry)).join(", ") : "_none_"}`,
357
+ `- Auth claim hints: ${summary.authClaimHints.length ? summary.authClaimHints.map((/** @type {any} */ entry) => formatAuthClaimHintInline(entry)).join(", ") : "_none_"}`,
358
+ `- Ownership hints: ${summary.authOwnershipHints.length ? summary.authOwnershipHints.map((/** @type {any} */ entry) => formatAuthOwnershipHintInline(entry)).join(", ") : "_none_"}`,
359
+ `- Auth role guidance: ${summary.authRoleGuidance.length ? summary.authRoleGuidance.map((/** @type {any} */ entry) => formatAuthRoleGuidanceInline(entry)).join(", ") : "_none_"}`,
360
+ `- Auth closure: ${summary.authClosureSummary.label} (adopted=${summary.authClosureSummary.adopted}, deferred=${summary.authClosureSummary.deferred}, unresolved=${summary.authClosureSummary.unresolved})`,
361
+ ...(summary.authAging && summary.authAging.escalationLevel !== "none"
362
+ ? [`- Auth escalation: ${summary.authAging.escalationLevel === "stale_high_risk" ? "escalated" : "fresh attention"} (high-risk runs=${summary.authAging.repeatCount})`]
363
+ : []),
364
+ "",
365
+ "## Why This Bundle Exists",
366
+ "",
367
+ summary.whyThisBundle
368
+ );
369
+ if (summary.authPermissionHints.length > 0) {
370
+ lines.push("", "## Auth Permission Hints", "");
371
+ for (const hint of summary.authPermissionHints) {
372
+ lines.push(`- ${formatAuthPermissionHintInline(hint)} <- ${hint.related_capabilities.length ? hint.related_capabilities.map((/** @type {any} */ item) => `\`${item}\``).join(", ") : "_no direct capability match_"}`);
373
+ lines.push(` - evidence capabilities=${hint.evidence.capability_hits}, routes=${hint.evidence.route_hits}, docs=${hint.evidence.doc_hits}, provenance=${hint.evidence.provenance_hits}`);
374
+ lines.push(` - closure: ${hint.closure_state || "unresolved"}`);
375
+ lines.push(` - closure reason: ${hint.closure_reason || "No reviewed projection patch has been applied for this inferred auth hint yet."}`);
376
+ lines.push(` - why inferred: ${hint.why_inferred || hint.explanation}`);
377
+ lines.push(` - review next: ${hint.review_guidance || buildAuthPermissionReviewGuidance(hint)}`);
378
+ }
379
+ }
380
+ if (summary.authClaimHints.length > 0) {
381
+ lines.push("", "## Auth Claim Hints", "");
382
+ for (const hint of summary.authClaimHints) {
383
+ lines.push(`- ${formatAuthClaimHintInline(hint)} <- ${hint.related_capabilities.length ? hint.related_capabilities.map((/** @type {any} */ item) => `\`${item}\``).join(", ") : "_no direct capability match_"}`);
384
+ lines.push(` - evidence capability=${hint.evidence.capability_hits}, route=${hint.evidence.route_hits}, participants=${hint.evidence.participant_hits}, docs=${hint.evidence.doc_hits}`);
385
+ lines.push(` - closure: ${hint.closure_state || "unresolved"}`);
386
+ lines.push(` - closure reason: ${hint.closure_reason || "No reviewed projection patch has been applied for this inferred auth hint yet."}`);
387
+ lines.push(` - why inferred: ${hint.why_inferred || hint.explanation}`);
388
+ lines.push(` - review next: ${hint.review_guidance || buildAuthClaimReviewGuidance(hint)}`);
389
+ }
390
+ }
391
+ if (summary.authOwnershipHints.length > 0) {
392
+ lines.push("", "## Auth Ownership Hints", "");
393
+ for (const hint of summary.authOwnershipHints) {
394
+ lines.push(`- ${formatAuthOwnershipHintInline(hint)} <- ${hint.related_capabilities.length ? hint.related_capabilities.map((/** @type {any} */ item) => `\`${item}\``).join(", ") : "_no direct capability match_"}`);
395
+ lines.push(` - related entities: ${hint.related_entities.length ? hint.related_entities.map((/** @type {any} */ item) => `\`${item}\``).join(", ") : "_none_"}`);
396
+ lines.push(` - evidence fields=${hint.evidence.field_hits}, capabilities=${hint.evidence.capability_hits}, docs=${hint.evidence.doc_hits}`);
397
+ lines.push(` - closure: ${hint.closure_state || "unresolved"}`);
398
+ lines.push(` - closure reason: ${hint.closure_reason || "No reviewed projection patch has been applied for this inferred auth hint yet."}`);
399
+ lines.push(` - why inferred: ${hint.why_inferred || hint.explanation}`);
400
+ lines.push(` - review next: ${hint.review_guidance || buildAuthOwnershipReviewGuidance(hint)}`);
401
+ }
402
+ }
403
+ if (summary.authRoleGuidance.length > 0) {
404
+ lines.push("", "## Auth Role Guidance", "");
405
+ for (const entry of summary.authRoleGuidance) {
406
+ lines.push(`- ${formatAuthRoleGuidanceInline(entry)} <- ${entry.related_capabilities.length ? entry.related_capabilities.map((/** @type {any} */ item) => `\`${item}\``).join(", ") : "_role naming only_"}`);
407
+ if (entry.related_docs.length > 0) {
408
+ lines.push(` - related docs: ${entry.related_docs.map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`);
409
+ }
410
+ lines.push(` - why inferred: ${entry.why_inferred}`);
411
+ lines.push(` - suggested follow-up: ${entry.followup_label} (${entry.followup_reason})`);
412
+ lines.push(` - review next: ${entry.review_guidance}`);
413
+ }
414
+ }
415
+ if (bundle.mergeHints) {
416
+ lines.push("", "## Suggested Merge", "");
417
+ if (bundle.mergeHints.action) {
418
+ lines.push(`- Action: \`${bundle.mergeHints.action}\``);
419
+ }
420
+ if (bundle.mergeHints.canonicalEntityTarget) {
421
+ lines.push(`- Canonical entity target: \`${bundle.mergeHints.canonicalEntityTarget}\``);
422
+ }
423
+ if ((bundle.mergeHints.promoteEnums || []).length > 0) {
424
+ lines.push(`- Promote enums: ${(bundle.mergeHints.promoteEnums || []).map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`);
425
+ }
426
+ if ((bundle.mergeHints.promoteCapabilities || []).length > 0) {
427
+ lines.push(`- Promote capabilities: ${(bundle.mergeHints.promoteCapabilities || []).map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`);
428
+ }
429
+ if ((bundle.mergeHints.promoteShapes || []).length > 0) {
430
+ lines.push(`- Promote shapes: ${(bundle.mergeHints.promoteShapes || []).map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`);
431
+ }
432
+ if ((bundle.mergeHints.promoteActors || []).length > 0) {
433
+ lines.push(`- Promote actors: ${(bundle.mergeHints.promoteActors || []).map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`);
434
+ }
435
+ if ((bundle.mergeHints.promoteRoles || []).length > 0) {
436
+ lines.push(`- Promote roles: ${(bundle.mergeHints.promoteRoles || []).map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`);
437
+ }
438
+ }
439
+ if ((bundle.adoptionPlan || []).length > 0) {
440
+ lines.push("", "## Suggested Adoption", "");
441
+ for (const step of bundle.adoptionPlan) {
442
+ lines.push(`- \`${step.action}\` \`${step.item}\`${step.target ? ` -> \`${step.target}\`` : ""}`);
443
+ }
444
+ }
445
+ const bundleProposalSurfaces = proposalSurfaces.filter((/** @type {any} */ surface) => surface.bundle === bundle.slug && (surface.maintained_seam_candidates || []).length > 0);
446
+ if (bundleProposalSurfaces.length > 0) {
447
+ lines.push("", "## Candidate Maintained Seam Mappings", "");
448
+ for (const surface of bundleProposalSurfaces) {
449
+ lines.push(`- proposal \`${surface.id}\` (${surface.kind})`);
450
+ for (const candidate of surface.maintained_seam_candidates || []) {
451
+ lines.push(` - candidate maintained seam \`${candidate.seam_id}\` -> output \`${candidate.output_id}\` (${candidate.status}, ${candidate.ownership_class}, confidence=${candidate.confidence})`);
452
+ lines.push(` - label ${candidate.label}`);
453
+ lines.push(` - kind ${candidate.kind}`);
454
+ lines.push(` - why matched ${candidate.match_reasons.length ? candidate.match_reasons.join("; ") : "dependency overlap with maintained seam evidence"}`);
455
+ }
456
+ }
457
+ }
458
+ if (journeyDrafts.length > 0) {
459
+ lines.push("", "## Journey Drafts", "");
460
+ for (const entry of journeyDrafts) {
461
+ lines.push(`- \`${entry.id}\` (${entry.title}) -> \`docs/journeys/${entry.id}.md\``);
462
+ }
463
+ lines.push("- Promote reviewed journey drafts with `reconcile adopt journeys --write`.");
464
+ }
465
+ if ((bundle.docLinkSuggestions || []).length > 0) {
466
+ lines.push("", "## Suggested Doc Link Updates", "");
467
+ for (const suggestion of bundle.docLinkSuggestions) {
468
+ lines.push(`- ${suggestion.recommendation} Draft: \`${suggestion.patch_rel_path}\``);
469
+ if ((suggestion.auth_role_followups || []).length > 0) {
470
+ lines.push(` - auth role follow-up: ${suggestion.auth_role_followups.map((/** @type {any} */ entry) => `${entry.followup_label} for \`${entry.role_id}\``).join(", ")}`);
471
+ }
472
+ }
473
+ }
474
+ if ((bundle.docDriftSummaries || []).length > 0) {
475
+ lines.push("", "## Suggested Doc Drift Reviews", "");
476
+ for (const summary of bundle.docDriftSummaries) {
477
+ lines.push(`- ${summary.recommendation} Fields: ${summary.differing_fields.map((/** @type {any} */ entry) => `\`${entry.field}\``).join(", ")}`);
478
+ }
479
+ }
480
+ if ((bundle.docMetadataPatches || []).length > 0) {
481
+ lines.push("", "## Suggested Doc Metadata Patches", "");
482
+ for (const patch of bundle.docMetadataPatches) {
483
+ lines.push(`- Review safe metadata patch for \`${patch.doc_id}\`. Draft: \`${patch.patch_rel_path}\``);
484
+ }
485
+ }
486
+ if ((bundle.projectionImpacts || []).length > 0) {
487
+ lines.push("", "## Projection Impacts", "");
488
+ for (const impact of bundle.projectionImpacts) {
489
+ lines.push(`- \`${impact.projection_id}\` (${impact.kind}) missing ${(impact.missing_capabilities || []).map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`);
490
+ }
491
+ }
492
+ if ((bundle.uiImpacts || []).length > 0) {
493
+ lines.push("", "## UI Impacts", "");
494
+ for (const impact of bundle.uiImpacts) {
495
+ lines.push(`- \`${impact.projection_id}\` missing screens ${(impact.missing_screens || []).map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`);
496
+ }
497
+ }
498
+ if ((bundle.workflowImpacts || []).length > 0) {
499
+ lines.push("", "## Workflow Impacts", "");
500
+ for (const impact of bundle.workflowImpacts) {
501
+ lines.push(`- \`${impact.review_group_id}\` requires workflow review for ${(impact.items || []).map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`);
502
+ }
503
+ }
504
+ if ((bundle.projectionPatches || []).length > 0) {
505
+ lines.push("", "## Projection Patch Candidates", "");
506
+ for (const patch of bundle.projectionPatches) {
507
+ lines.push(`- \`${patch.projection_id}\` -> \`${patch.patch_rel_path}\``);
508
+ }
509
+ }
510
+ if (bundle.entities.length > 0) {
511
+ lines.push("", "## Entity Evidence", "");
512
+ for (const entry of bundle.entities) {
513
+ lines.push(`- \`${entry.id_hint}\` from ${(entry.provenance || []).slice(0, 2).map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`);
514
+ }
515
+ }
516
+ if (bundle.actors.length > 0) {
517
+ lines.push("", "## Actor Evidence", "");
518
+ for (const entry of bundle.actors) {
519
+ const details = [`- \`${entry.id_hint}\` from ${(entry.provenance || []).slice(0, 2).map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`];
520
+ if ((entry.related_docs || []).length > 0) {
521
+ details.push(`related docs ${(entry.related_docs || []).map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`);
522
+ }
523
+ if ((entry.related_capabilities || []).length > 0) {
524
+ details.push(`related capabilities ${(entry.related_capabilities || []).map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`);
525
+ }
526
+ lines.push(details.join("; "));
527
+ }
528
+ }
529
+ if (bundle.roles.length > 0) {
530
+ lines.push("", "## Role Evidence", "");
531
+ for (const entry of bundle.roles) {
532
+ const details = [`- \`${entry.id_hint}\` from ${(entry.provenance || []).slice(0, 2).map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`];
533
+ if ((entry.related_docs || []).length > 0) {
534
+ details.push(`related docs ${(entry.related_docs || []).map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`);
535
+ }
536
+ if ((entry.related_capabilities || []).length > 0) {
537
+ details.push(`related capabilities ${(entry.related_capabilities || []).map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`);
538
+ }
539
+ lines.push(details.join("; "));
540
+ }
541
+ }
542
+ if (bundle.capabilities.length > 0) {
543
+ lines.push("", "## API Evidence", "");
544
+ for (const entry of bundle.capabilities) {
545
+ lines.push(`- \`${entry.id_hint}\` at \`${entry.endpoint?.method || "?"} ${entry.endpoint?.path || "?"}\``);
546
+ }
547
+ }
548
+ if (bundle.screens.length > 0) {
549
+ lines.push("", "## UI Evidence", "");
550
+ for (const entry of bundle.screens) {
551
+ lines.push(`- \`${entry.id_hint}\` ${entry.screen_kind} at \`${entry.route_path}\``);
552
+ }
553
+ }
554
+ if (bundle.workflows.length > 0) {
555
+ lines.push("", "## Workflow Evidence", "");
556
+ for (const entry of bundle.workflows) {
557
+ lines.push(`- \`${entry.id_hint}\` for \`${entry.entity_id}\``);
558
+ }
559
+ }
560
+ if (bundle.docs.length > 0) {
561
+ lines.push("", "## Doc Evidence", "");
562
+ for (const entry of bundle.docs) {
563
+ lines.push(`- \`${entry.id}\` (${entry.kind}) from ${(entry.provenance || []).slice(0, 2).map((/** @type {any} */ item) => `\`${item}\``).join(", ")}`);
564
+ }
565
+ }
566
+ return ensureTrailingNewline(lines.join("\n"));
567
+ }
568
+
569
+ /** @param {CandidateBundle} bundle @returns {any} */
570
+ export function renderMaintainedSeamCandidatesInline(bundle) {
571
+ const entries = bundle.maintained_seam_candidates || [];
572
+ if (!entries.length) {
573
+ return "_none_";
574
+ }
575
+ return entries
576
+ .map((/** @type {any} */ surface) => {
577
+ const seams = (surface.maintained_seam_candidates || [])
578
+ .map((/** @type {any} */ candidate) => `\`${candidate.seam_id}\` (${candidate.status}, ${candidate.ownership_class}, confidence=${candidate.confidence})`)
579
+ .join(", ");
580
+ return `${surface.id}: ${seams}`;
581
+ })
582
+ .join("; ");
583
+ }
584
+
585
+ /** @param {CandidateBundle} bundle @param {Set<any>} canonicalEntityIds @returns {any} */
586
+ export function buildBundleMergeHints(bundle, canonicalEntityIds) {
587
+ const canonicalEntityTarget = bundle.id.startsWith("entity_") && canonicalEntityIds.has(bundle.id) ? bundle.id : null;
588
+ return {
589
+ action: canonicalEntityTarget ? "merge_into_existing_entity" : "promote_as_candidate_concept",
590
+ canonicalEntityTarget,
591
+ promoteActors: bundle.actors.map((/** @type {any} */ entry) => entry.id_hint),
592
+ promoteRoles: bundle.roles.map((/** @type {any} */ entry) => entry.id_hint),
593
+ promoteEnums: bundle.enums.map((/** @type {any} */ entry) => entry.id_hint),
594
+ promoteCapabilities: bundle.capabilities.map((/** @type {any} */ entry) => entry.id_hint),
595
+ promoteShapes: bundle.shapes.map((/** @type {any} */ entry) => entry.id),
596
+ promoteScreens: bundle.screens.map((/** @type {any} */ entry) => entry.id_hint),
597
+ promoteWorkflows: bundle.workflows.map((/** @type {any} */ entry) => entry.id_hint),
598
+ promoteDocs: bundle.docs.map((/** @type {any} */ entry) => entry.id)
599
+ };
600
+ }