@contentful/experience-design-system-cli 2.14.2 → 2.15.0

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 (193) hide show
  1. package/README.md +64 -2
  2. package/dist/package.json +3 -2
  3. package/dist/src/analyze/command.d.ts +11 -0
  4. package/dist/src/analyze/command.js +298 -3
  5. package/dist/src/analyze/composite-closure.d.ts +25 -0
  6. package/dist/src/analyze/composite-closure.js +0 -0
  7. package/dist/src/analyze/composition/agent-parser/author-prompt.d.ts +4 -0
  8. package/dist/src/analyze/composition/agent-parser/author-prompt.js +22 -0
  9. package/dist/src/analyze/composition/agent-parser/extract-parser.d.ts +8 -0
  10. package/dist/src/analyze/composition/agent-parser/extract-parser.js +44 -0
  11. package/dist/src/analyze/composition/agent-parser/load-prompt.d.ts +7 -0
  12. package/dist/src/analyze/composition/agent-parser/load-prompt.js +31 -0
  13. package/dist/src/analyze/composition/agent-parser/resolve-via-parser.d.ts +46 -0
  14. package/dist/src/analyze/composition/agent-parser/resolve-via-parser.js +88 -0
  15. package/dist/src/analyze/composition/agent-parser/sandbox.d.ts +45 -0
  16. package/dist/src/analyze/composition/agent-parser/sandbox.js +187 -0
  17. package/dist/src/analyze/composition/apply-mapping.d.ts +22 -0
  18. package/dist/src/analyze/composition/apply-mapping.js +67 -0
  19. package/dist/src/analyze/composition/candidate-critic-agent.d.ts +7 -0
  20. package/dist/src/analyze/composition/candidate-critic-agent.js +39 -0
  21. package/dist/src/analyze/composition/candidate-critic.d.ts +32 -0
  22. package/dist/src/analyze/composition/candidate-critic.js +63 -0
  23. package/dist/src/analyze/composition/candidate-files.d.ts +34 -0
  24. package/dist/src/analyze/composition/candidate-files.js +131 -0
  25. package/dist/src/analyze/composition/composition-cache-key.d.ts +19 -0
  26. package/dist/src/analyze/composition/composition-cache-key.js +19 -0
  27. package/dist/src/analyze/composition/interchange-schema.d.ts +37 -0
  28. package/dist/src/analyze/composition/interchange-schema.js +61 -0
  29. package/dist/src/analyze/composition/merge-edges.d.ts +19 -0
  30. package/dist/src/analyze/composition/merge-edges.js +50 -0
  31. package/dist/src/analyze/composition/parse-map-edges.d.ts +16 -0
  32. package/dist/src/analyze/composition/parse-map-edges.js +55 -0
  33. package/dist/src/analyze/composition/resolve-mapping-cli.d.ts +23 -0
  34. package/dist/src/analyze/composition/resolve-mapping-cli.js +29 -0
  35. package/dist/src/analyze/composition/resolve-mapping.d.ts +59 -0
  36. package/dist/src/analyze/composition/resolve-mapping.js +94 -0
  37. package/dist/src/analyze/cycle-detection.d.ts +27 -0
  38. package/dist/src/analyze/cycle-detection.js +234 -0
  39. package/dist/src/analyze/cycle-view.d.ts +7 -0
  40. package/dist/src/analyze/cycle-view.js +28 -0
  41. package/dist/src/analyze/fuzzy-search.d.ts +3 -0
  42. package/dist/src/analyze/fuzzy-search.js +55 -0
  43. package/dist/src/analyze/issue-inheritance.d.ts +12 -0
  44. package/dist/src/analyze/issue-inheritance.js +108 -0
  45. package/dist/src/analyze/lineage.d.ts +29 -0
  46. package/dist/src/analyze/lineage.js +0 -0
  47. package/dist/src/analyze/scope-gate-cascade.d.ts +11 -0
  48. package/dist/src/analyze/scope-gate-cascade.js +170 -0
  49. package/dist/src/analyze/search-neighborhood.d.ts +3 -0
  50. package/dist/src/analyze/search-neighborhood.js +56 -0
  51. package/dist/src/analyze/select/tui/App.js +5 -16
  52. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +0 -4
  53. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +3 -15
  54. package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +29 -38
  55. package/dist/src/analyze/select/tui/components/FieldEditor.js +309 -185
  56. package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +13 -1
  57. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +23 -2
  58. package/dist/src/analyze/select/tui/components/GotoBanner.d.ts +31 -0
  59. package/dist/src/analyze/select/tui/components/GotoBanner.js +30 -0
  60. package/dist/src/analyze/select/tui/components/GroupedSidebar.d.ts +91 -0
  61. package/dist/src/analyze/select/tui/components/GroupedSidebar.js +497 -0
  62. package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +13 -1
  63. package/dist/src/analyze/select/tui/components/HelpOverlay.js +9 -3
  64. package/dist/src/analyze/select/tui/components/JsonEditor.js +3 -3
  65. package/dist/src/analyze/select/tui/components/LineagePanel.d.ts +14 -0
  66. package/dist/src/analyze/select/tui/components/LineagePanel.js +22 -0
  67. package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +0 -7
  68. package/dist/src/analyze/select/tui/components/RationalePanel.js +3 -15
  69. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +0 -6
  70. package/dist/src/analyze/select/tui/components/Sidebar.js +11 -27
  71. package/dist/src/analyze/select/tui/components/StatusBar.js +2 -1
  72. package/dist/src/analyze/select/tui/components/removed-components-text.d.ts +7 -0
  73. package/dist/src/analyze/select/tui/components/removed-components-text.js +11 -0
  74. package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +1 -5
  75. package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +8 -10
  76. package/dist/src/analyze/select/tui/theme.d.ts +15 -0
  77. package/dist/src/analyze/select/tui/theme.js +28 -0
  78. package/dist/src/analyze/selection-cascade.d.ts +3 -0
  79. package/dist/src/analyze/selection-cascade.js +19 -0
  80. package/dist/src/analyze/slot-graph.d.ts +11 -0
  81. package/dist/src/analyze/slot-graph.js +17 -0
  82. package/dist/src/apply/api-client.d.ts +2 -1
  83. package/dist/src/apply/api-client.js +40 -6
  84. package/dist/src/apply/command.d.ts +17 -1
  85. package/dist/src/apply/command.js +69 -11
  86. package/dist/src/apply/error-parser.d.ts +16 -0
  87. package/dist/src/apply/error-parser.js +117 -0
  88. package/dist/src/apply/tui/ServerPreviewView.js +3 -1
  89. package/dist/src/credentials-store.d.ts +6 -0
  90. package/dist/src/credentials-store.js +5 -1
  91. package/dist/src/generate/agent-runner.d.ts +6 -0
  92. package/dist/src/generate/agent-runner.js +24 -8
  93. package/dist/src/import/command.js +69 -31
  94. package/dist/src/import/cycle-auto-reject.d.ts +4 -0
  95. package/dist/src/import/cycle-auto-reject.js +14 -0
  96. package/dist/src/import/orchestrator.d.ts +20 -7
  97. package/dist/src/import/orchestrator.js +139 -35
  98. package/dist/src/import/strip-allowed-components.d.ts +18 -0
  99. package/dist/src/import/strip-allowed-components.js +22 -0
  100. package/dist/src/import/tui/CustomPromptBanner.d.ts +0 -5
  101. package/dist/src/import/tui/CustomPromptBanner.js +2 -1
  102. package/dist/src/import/tui/WizardApp.d.ts +10 -74
  103. package/dist/src/import/tui/WizardApp.js +234 -246
  104. package/dist/src/import/tui/ai-flag.d.ts +5 -0
  105. package/dist/src/import/tui/ai-flag.js +6 -0
  106. package/dist/src/import/tui/autocomplete.d.ts +5 -0
  107. package/dist/src/import/tui/autocomplete.js +32 -0
  108. package/dist/src/import/tui/components/AutoFilterBanner.d.ts +10 -0
  109. package/dist/src/import/tui/components/AutoFilterBanner.js +19 -0
  110. package/dist/src/import/tui/components/CounterStrip.d.ts +12 -0
  111. package/dist/src/import/tui/components/CounterStrip.js +12 -0
  112. package/dist/src/import/tui/components/LegendEntry.d.ts +2 -0
  113. package/dist/src/import/tui/components/LegendEntry.js +6 -0
  114. package/dist/src/import/tui/cycle-panel-scroll.d.ts +8 -0
  115. package/dist/src/import/tui/cycle-panel-scroll.js +33 -0
  116. package/dist/src/import/tui/final-review-host.d.ts +3 -7
  117. package/dist/src/import/tui/final-review-host.js +9 -4
  118. package/dist/src/import/tui/group-collapse.d.ts +2 -0
  119. package/dist/src/import/tui/group-collapse.js +11 -0
  120. package/dist/src/import/tui/history.d.ts +22 -0
  121. package/dist/src/import/tui/history.js +53 -0
  122. package/dist/src/import/tui/hooks/useLineage.d.ts +32 -0
  123. package/dist/src/import/tui/hooks/useLineage.js +50 -0
  124. package/dist/src/import/tui/hooks/useOverlayPanel.d.ts +13 -0
  125. package/dist/src/import/tui/hooks/useOverlayPanel.js +27 -0
  126. package/dist/src/import/tui/lineage-layout.d.ts +75 -0
  127. package/dist/src/import/tui/lineage-layout.js +82 -0
  128. package/dist/src/import/tui/runLivePreview.d.ts +11 -0
  129. package/dist/src/import/tui/runLivePreview.js +7 -1
  130. package/dist/src/import/tui/scope-gate-columns.d.ts +31 -0
  131. package/dist/src/import/tui/scope-gate-columns.js +94 -0
  132. package/dist/src/import/tui/scope-gate-host.d.ts +3 -1
  133. package/dist/src/import/tui/scope-gate-host.js +11 -3
  134. package/dist/src/import/tui/sidebar-width.d.ts +1 -0
  135. package/dist/src/import/tui/sidebar-width.js +3 -0
  136. package/dist/src/import/tui/step-filters.d.ts +19 -0
  137. package/dist/src/import/tui/step-filters.js +42 -0
  138. package/dist/src/import/tui/steps/AtomicGenerateReviewStep.d.ts +42 -0
  139. package/dist/src/import/tui/steps/AtomicGenerateReviewStep.js +726 -0
  140. package/dist/src/import/tui/steps/AtomicScopeGateStep.d.ts +3 -0
  141. package/dist/src/import/tui/steps/AtomicScopeGateStep.js +211 -0
  142. package/dist/src/import/tui/steps/CredentialsStep.js +5 -18
  143. package/dist/src/import/tui/steps/DoneStep.d.ts +0 -2
  144. package/dist/src/import/tui/steps/DoneStep.js +3 -2
  145. package/dist/src/import/tui/steps/ErrorStep.js +2 -1
  146. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +19 -25
  147. package/dist/src/import/tui/steps/GenerateReviewStep.js +1258 -229
  148. package/dist/src/import/tui/steps/PathValidationStep.js +3 -2
  149. package/dist/src/import/tui/steps/PreviewStep.js +2 -1
  150. package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +0 -7
  151. package/dist/src/import/tui/steps/PushDecisionGateStep.js +3 -6
  152. package/dist/src/import/tui/steps/PushingStep.js +2 -1
  153. package/dist/src/import/tui/steps/RunningStep.d.ts +4 -1
  154. package/dist/src/import/tui/steps/RunningStep.js +3 -2
  155. package/dist/src/import/tui/steps/ScopeGateStep.d.ts +24 -0
  156. package/dist/src/import/tui/steps/ScopeGateStep.js +850 -193
  157. package/dist/src/import/tui/steps/TokenInputStep.js +2 -1
  158. package/dist/src/import/tui/steps/WelcomeStep.js +2 -1
  159. package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +6 -0
  160. package/dist/src/import/tui/steps/WizardPreviewStep.js +95 -56
  161. package/dist/src/import/tui/steps/auto-reject-decision.d.ts +7 -0
  162. package/dist/src/import/tui/steps/auto-reject-decision.js +9 -0
  163. package/dist/src/import/tui/steps/breaking-change-format.d.ts +2 -0
  164. package/dist/src/import/tui/steps/breaking-change-format.js +24 -0
  165. package/dist/src/import/tui/steps/enumerate-cycle-breaks.d.ts +14 -0
  166. package/dist/src/import/tui/steps/enumerate-cycle-breaks.js +0 -0
  167. package/dist/src/import/tui/steps/preview-diff.js +48 -5
  168. package/dist/src/import/tui/useFinalizePreview.d.ts +34 -0
  169. package/dist/src/import/tui/useFinalizePreview.js +0 -0
  170. package/dist/src/import/tui/useLivePreview.d.ts +3 -0
  171. package/dist/src/import/tui/useLivePreview.js +1 -0
  172. package/dist/src/import/tui/wizard-state-transitions.d.ts +5 -59
  173. package/dist/src/import/tui/wizard-state-transitions.js +5 -60
  174. package/dist/src/lib/composition-mode.d.ts +28 -0
  175. package/dist/src/lib/composition-mode.js +37 -0
  176. package/dist/src/lib/prompt-overrides.d.ts +38 -0
  177. package/dist/src/lib/prompt-overrides.js +69 -0
  178. package/dist/src/print/command.js +19 -1
  179. package/dist/src/runs/modify-launcher.d.ts +3 -0
  180. package/dist/src/runs/modify-launcher.js +2 -0
  181. package/dist/src/runs/replay-helpers.js +1 -0
  182. package/dist/src/runs/run-picker.js +2 -18
  183. package/dist/src/runs/store.d.ts +4 -0
  184. package/dist/src/session/db.d.ts +23 -40
  185. package/dist/src/session/db.js +110 -138
  186. package/dist/src/setup/composition-mode-prompt.d.ts +14 -0
  187. package/dist/src/setup/composition-mode-prompt.js +37 -0
  188. package/package.json +5 -4
  189. package/prompts/composition-dir-critic.md +7 -0
  190. package/prompts/composition-edges.md +8 -0
  191. package/prompts/composition-parser-repair-empty.md +1 -0
  192. package/prompts/composition-parser-repair-error.md +2 -0
  193. package/prompts/composition-parser.md +42 -0
@@ -0,0 +1,131 @@
1
+ export const CANDIDATE_NAME_PATTERNS = [/mapping/i, /meta/i, /registry/i, /schema/i, /composition/i];
2
+ export const CANDIDATE_CONTENT_MARKERS = [
3
+ 'requiredParent',
4
+ 'withParentType',
5
+ 'allowedTagNames',
6
+ 'createContext',
7
+ 'MappingContext',
8
+ 'allowedComponents',
9
+ ];
10
+ export const CANDIDATE_TOKEN_BUDGET = 6000;
11
+ /**
12
+ * Token ceiling for the candidate set INLINED into a single agent prompt.
13
+ * Sized to stay under a 200k-context model once the agent's own system prompt
14
+ * + tool definitions (~105k observed) are added, so a large design system
15
+ * (hundreds of components → many matched files) can't overflow the request and
16
+ * fail resolution. Files beyond the budget are dropped (with a warning), not
17
+ * silently truncated. Distinct from `CANDIDATE_TOKEN_BUDGET`, which sizes the
18
+ * unused per-batch chunking.
19
+ */
20
+ export const PROMPT_CANDIDATE_TOKEN_BUDGET = 80_000;
21
+ const CHARS_PER_TOKEN = 4;
22
+ const DEFAULT_SLICE_WINDOW = 3;
23
+ function matchReason(file) {
24
+ // Test the name patterns against EVERY path segment (directory names + the
25
+ // basename), not just the basename. A mapping-layer file that only DEFINES a
26
+ // component — e.g. src/mapping/call_to_action.ts, no withParentType of its
27
+ // own — must still be selected so the resolver can resolve OTHER files'
28
+ // parent references to it. Matching only the basename silently dropped these.
29
+ const segments = file.path.split('/').filter((s) => s !== '');
30
+ for (const pattern of CANDIDATE_NAME_PATTERNS) {
31
+ if (segments.some((seg) => pattern.test(seg))) {
32
+ return `name:${pattern.source.toLowerCase()}`;
33
+ }
34
+ }
35
+ for (const marker of CANDIDATE_CONTENT_MARKERS) {
36
+ if (file.content.includes(marker))
37
+ return `content:${marker}`;
38
+ }
39
+ return undefined;
40
+ }
41
+ export function selectCandidateFiles(files) {
42
+ const selected = [];
43
+ const seen = new Set();
44
+ for (const file of files) {
45
+ if (seen.has(file.path))
46
+ continue;
47
+ const reason = matchReason(file);
48
+ if (reason === undefined)
49
+ continue;
50
+ seen.add(file.path);
51
+ selected.push({ path: file.path, content: file.content, reason });
52
+ }
53
+ return selected;
54
+ }
55
+ /**
56
+ * Cap a candidate set to what fits in a single agent prompt (see
57
+ * `PROMPT_CANDIDATE_TOKEN_BUDGET`). Files are kept smallest-first so the budget
58
+ * admits the most declarations; deterministic tie-break by path. Returns the
59
+ * kept files plus the paths dropped so the caller can warn (silent truncation
60
+ * would read as "resolved everything" when it didn't).
61
+ */
62
+ export function capCandidatesToPromptBudget(files, budget = PROMPT_CANDIDATE_TOKEN_BUDGET) {
63
+ const ordered = [...files].sort((a, b) => a.content.length - b.content.length || a.path.localeCompare(b.path));
64
+ const kept = [];
65
+ const dropped = [];
66
+ let spent = 0;
67
+ for (const file of ordered) {
68
+ const cost = tokenCost(file);
69
+ if (spent + cost > budget) {
70
+ dropped.push(file);
71
+ continue;
72
+ }
73
+ kept.push(file);
74
+ spent += cost;
75
+ }
76
+ return { kept, dropped };
77
+ }
78
+ export function sliceDeclarationRegions(content, markers = CANDIDATE_CONTENT_MARKERS, window = DEFAULT_SLICE_WINDOW) {
79
+ const lines = content.split('\n');
80
+ const hitLines = [];
81
+ for (let i = 0; i < lines.length; i++) {
82
+ if (markers.some((marker) => lines[i].includes(marker)))
83
+ hitLines.push(i);
84
+ }
85
+ if (hitLines.length === 0)
86
+ return [];
87
+ const ranges = [];
88
+ for (const hit of hitLines) {
89
+ const start = Math.max(0, hit - window);
90
+ const end = Math.min(lines.length - 1, hit + window);
91
+ const last = ranges[ranges.length - 1];
92
+ if (last && start <= last.end + 1) {
93
+ last.end = Math.max(last.end, end);
94
+ }
95
+ else {
96
+ ranges.push({ start, end });
97
+ }
98
+ }
99
+ return ranges.map((r) => lines.slice(r.start, r.end + 1).join('\n'));
100
+ }
101
+ function tokenCost(file) {
102
+ return Math.ceil(file.content.length / CHARS_PER_TOKEN);
103
+ }
104
+ export function batchCandidates(files, budget = CANDIDATE_TOKEN_BUDGET) {
105
+ const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
106
+ const batches = [];
107
+ let current = [];
108
+ let currentCost = 0;
109
+ for (const file of sorted) {
110
+ const cost = tokenCost(file);
111
+ if (cost > budget) {
112
+ if (current.length > 0) {
113
+ batches.push(current);
114
+ current = [];
115
+ currentCost = 0;
116
+ }
117
+ batches.push([file]);
118
+ continue;
119
+ }
120
+ if (current.length > 0 && currentCost + cost > budget) {
121
+ batches.push(current);
122
+ current = [];
123
+ currentCost = 0;
124
+ }
125
+ current.push(file);
126
+ currentCost += cost;
127
+ }
128
+ if (current.length > 0)
129
+ batches.push(current);
130
+ return batches;
131
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Derive the `input_hash` for a composition-cache entry (stored in the pipeline
3
+ * DB's `composition_cache` table). Content-addresses the candidate file set
4
+ * (order-independent) plus the producing agent and the resolution kind, so a
5
+ * re-run over unchanged files reuses the (token-costly) agent output.
6
+ *
7
+ * Content-hashing — not mtime — means a no-op `git checkout` that only bumps
8
+ * mtime is still a hit. Prompt/skill changes are handled by the `cli_version`
9
+ * column on the row (see `getCliCacheVersion`), so no separate resolver-version
10
+ * constant is needed here.
11
+ */
12
+ export declare function buildCompositionInputHash(input: {
13
+ files: Array<{
14
+ path: string;
15
+ content: string;
16
+ }>;
17
+ agent: string;
18
+ kind: 'parser' | 'edges';
19
+ }): string;
@@ -0,0 +1,19 @@
1
+ import { hashContent } from '../../session/cache-keys.js';
2
+ /**
3
+ * Derive the `input_hash` for a composition-cache entry (stored in the pipeline
4
+ * DB's `composition_cache` table). Content-addresses the candidate file set
5
+ * (order-independent) plus the producing agent and the resolution kind, so a
6
+ * re-run over unchanged files reuses the (token-costly) agent output.
7
+ *
8
+ * Content-hashing — not mtime — means a no-op `git checkout` that only bumps
9
+ * mtime is still a hit. Prompt/skill changes are handled by the `cli_version`
10
+ * column on the row (see `getCliCacheVersion`), so no separate resolver-version
11
+ * constant is needed here.
12
+ */
13
+ export function buildCompositionInputHash(input) {
14
+ const fileDigest = input.files
15
+ .map((f) => `${f.path} ${hashContent(f.content)}`)
16
+ .sort()
17
+ .join('');
18
+ return hashContent(`${input.kind}:agent:${input.agent}\n${fileDigest}`);
19
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The CLI-owned composition interchange format (spec T1).
3
+ *
4
+ * Two views of the same data:
5
+ * - `InterchangeMap` — the human-authored / on-disk `{ version, groups }`
6
+ * shape (parent → children). This is what `--composition-map` consumes and
7
+ * `--generate-map` emits.
8
+ * - `CompositionEdge[]` — the flat internal runtime view, one edge per
9
+ * parent→child relationship, carrying optional `slot` (T7), `confidence`,
10
+ * and `provenance`. Easier to merge/dedupe across sources (T2).
11
+ *
12
+ * This module owns both shapes and the converters between them.
13
+ */
14
+ export type EdgeProvenance = 'user' | 'typed-slot' | `adapter:${string}` | 'agent';
15
+ export type CompositionEdge = {
16
+ parent: string;
17
+ child: string;
18
+ /** Optional named slot (T7). Default slot when omitted. */
19
+ slot?: string;
20
+ /** 1–5 scale, same as select/reject agent tools. */
21
+ confidence?: number;
22
+ provenance: EdgeProvenance;
23
+ };
24
+ export type InterchangeMap = {
25
+ version: 1;
26
+ groups: Record<string, string[]>;
27
+ };
28
+ export type ValidateResult = {
29
+ valid: true;
30
+ map: InterchangeMap;
31
+ } | {
32
+ valid: false;
33
+ errors: string[];
34
+ };
35
+ export declare function validateInterchangeMap(input: unknown): ValidateResult;
36
+ export declare function groupsToEdges(map: InterchangeMap, provenance: EdgeProvenance): CompositionEdge[];
37
+ export declare function edgesToGroups(edges: CompositionEdge[]): InterchangeMap;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The CLI-owned composition interchange format (spec T1).
3
+ *
4
+ * Two views of the same data:
5
+ * - `InterchangeMap` — the human-authored / on-disk `{ version, groups }`
6
+ * shape (parent → children). This is what `--composition-map` consumes and
7
+ * `--generate-map` emits.
8
+ * - `CompositionEdge[]` — the flat internal runtime view, one edge per
9
+ * parent→child relationship, carrying optional `slot` (T7), `confidence`,
10
+ * and `provenance`. Easier to merge/dedupe across sources (T2).
11
+ *
12
+ * This module owns both shapes and the converters between them.
13
+ */
14
+ export function validateInterchangeMap(input) {
15
+ const errors = [];
16
+ if (typeof input !== 'object' || input === null || Array.isArray(input)) {
17
+ return { valid: false, errors: ['interchange map must be a JSON object'] };
18
+ }
19
+ const obj = input;
20
+ if (obj.version !== 1) {
21
+ errors.push(`unsupported interchange version: expected 1, got ${JSON.stringify(obj.version)}`);
22
+ }
23
+ if (typeof obj.groups !== 'object' || obj.groups === null || Array.isArray(obj.groups)) {
24
+ errors.push('interchange map must have a `groups` object');
25
+ return { valid: false, errors };
26
+ }
27
+ const groups = obj.groups;
28
+ for (const [parent, children] of Object.entries(groups)) {
29
+ if (!Array.isArray(children) || !children.every((c) => typeof c === 'string')) {
30
+ errors.push(`groups["${parent}"] must be an array of strings`);
31
+ }
32
+ }
33
+ if (errors.length > 0)
34
+ return { valid: false, errors };
35
+ return { valid: true, map: { version: 1, groups: groups } };
36
+ }
37
+ export function groupsToEdges(map, provenance) {
38
+ const seen = new Set();
39
+ const edges = [];
40
+ for (const [parent, children] of Object.entries(map.groups)) {
41
+ for (const child of children) {
42
+ const key = `${parent}::${child}`;
43
+ if (seen.has(key))
44
+ continue;
45
+ seen.add(key);
46
+ edges.push({ parent, child, provenance });
47
+ }
48
+ }
49
+ return edges;
50
+ }
51
+ export function edgesToGroups(edges) {
52
+ const groups = {};
53
+ for (const { parent, child } of edges) {
54
+ (groups[parent] ??= new Set()).add(child);
55
+ }
56
+ const out = {};
57
+ for (const parent of Object.keys(groups).sort()) {
58
+ out[parent] = [...groups[parent]].sort();
59
+ }
60
+ return { version: 1, groups: out };
61
+ }
@@ -0,0 +1,19 @@
1
+ import type { CompositionEdge, EdgeProvenance } from './interchange-schema.js';
2
+ export type EdgeConflict = {
3
+ parent: string;
4
+ child: string;
5
+ winner: EdgeProvenance;
6
+ loser: EdgeProvenance;
7
+ };
8
+ export type MergeResult = {
9
+ edges: CompositionEdge[];
10
+ conflicts: EdgeConflict[];
11
+ };
12
+ /**
13
+ * Union all edges, resolving conflicts by provenance rank. A conflict is two
14
+ * edges that share `parent::child` but disagree on existence-detail (here:
15
+ * slot placement). The higher-ranked edge wins; the loser is recorded (never
16
+ * silently dropped) so the review UI can surface it. Identical edges (same
17
+ * parent/child/slot) collapse to the highest-ranked copy with no conflict.
18
+ */
19
+ export declare function mergeEdges(all: CompositionEdge[]): MergeResult;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Provenance rank (spec T2). Lower number = higher trust = wins conflicts.
3
+ * 1 user > 2 typed-slot > 3 adapter:* > 4 agent
4
+ */
5
+ function rank(p) {
6
+ if (p === 'user')
7
+ return 1;
8
+ if (p === 'typed-slot')
9
+ return 2;
10
+ if (p.startsWith('adapter:'))
11
+ return 3;
12
+ return 4; // agent
13
+ }
14
+ /**
15
+ * Union all edges, resolving conflicts by provenance rank. A conflict is two
16
+ * edges that share `parent::child` but disagree on existence-detail (here:
17
+ * slot placement). The higher-ranked edge wins; the loser is recorded (never
18
+ * silently dropped) so the review UI can surface it. Identical edges (same
19
+ * parent/child/slot) collapse to the highest-ranked copy with no conflict.
20
+ */
21
+ export function mergeEdges(all) {
22
+ const byKey = new Map();
23
+ const conflicts = [];
24
+ for (const edge of all) {
25
+ const key = `${edge.parent}::${edge.child}`;
26
+ const existing = byKey.get(key);
27
+ if (!existing) {
28
+ byKey.set(key, edge);
29
+ continue;
30
+ }
31
+ const sameSlot = (existing.slot ?? '') === (edge.slot ?? '');
32
+ if (sameSlot) {
33
+ // Identical relationship — keep the higher-ranked provenance, no conflict.
34
+ if (rank(edge.provenance) < rank(existing.provenance))
35
+ byKey.set(key, edge);
36
+ continue;
37
+ }
38
+ // Slot placement disagreement → conflict. Higher rank wins.
39
+ const winnerEdge = rank(edge.provenance) < rank(existing.provenance) ? edge : existing;
40
+ const loserEdge = winnerEdge === edge ? existing : edge;
41
+ byKey.set(key, winnerEdge);
42
+ conflicts.push({
43
+ parent: edge.parent,
44
+ child: edge.child,
45
+ winner: winnerEdge.provenance,
46
+ loser: loserEdge.provenance,
47
+ });
48
+ }
49
+ return { edges: [...byKey.values()], conflicts };
50
+ }
@@ -0,0 +1,16 @@
1
+ import type { CompositionEdge } from './interchange-schema.js';
2
+ /**
3
+ * Lenient JSONL parser for the composition-mapping agent's tool-call output
4
+ * (spec T4). Mirrors `parseSelectToolCallLines` / `parseToolCallLines` in
5
+ * `src/generate/agent-runner.ts`: one JSON object per line, each with a `tool`
6
+ * field from a fixed allowlist. A line that fails JSON.parse or validation is
7
+ * DROPPED into `warnings[]` (with a reason) and parsing CONTINUES — no retry,
8
+ * no throw. Non-`{` prose and blank lines are skipped silently.
9
+ */
10
+ export type ParseMapEdgesResult = {
11
+ edges: CompositionEdge[];
12
+ warnings: string[];
13
+ };
14
+ export declare function parseMapEdges(raw: string, opts: {
15
+ componentNames: Set<string>;
16
+ }): ParseMapEdgesResult;
@@ -0,0 +1,55 @@
1
+ const VALID_MAP_TOOL_NAMES = new Set(['map_edge']);
2
+ export function parseMapEdges(raw, opts) {
3
+ const edges = [];
4
+ const warnings = [];
5
+ const { componentNames } = opts;
6
+ for (const line of raw.split('\n')) {
7
+ const trimmed = line.trim();
8
+ if (!trimmed.startsWith('{'))
9
+ continue;
10
+ let obj;
11
+ try {
12
+ obj = JSON.parse(trimmed);
13
+ }
14
+ catch {
15
+ warnings.push(`unparseable line: ${trimmed.slice(0, 120)}`);
16
+ continue;
17
+ }
18
+ if (typeof obj !== 'object' || obj === null || !('tool' in obj))
19
+ continue;
20
+ const rec = obj;
21
+ if (!VALID_MAP_TOOL_NAMES.has(rec.tool)) {
22
+ warnings.push(`unknown tool: ${String(rec.tool)}`);
23
+ continue;
24
+ }
25
+ if (typeof rec.parent !== 'string' || !rec.parent || typeof rec.child !== 'string' || !rec.child) {
26
+ warnings.push('map_edge missing parent/child — skipped');
27
+ continue;
28
+ }
29
+ const { parent, child } = rec;
30
+ // Verification (spec §1.3): both endpoints must name a known component.
31
+ if (!componentNames.has(parent)) {
32
+ warnings.push(`map_edge names unknown component (parent): ${parent} — skipped`);
33
+ continue;
34
+ }
35
+ if (!componentNames.has(child)) {
36
+ warnings.push(`map_edge names unknown component (child): ${child} — skipped`);
37
+ continue;
38
+ }
39
+ const edge = { parent, child, provenance: 'agent' };
40
+ if (typeof rec.slot === 'string' && rec.slot) {
41
+ edge.slot = rec.slot;
42
+ }
43
+ if (rec.confidence !== undefined) {
44
+ if (typeof rec.confidence === 'number' && rec.confidence >= 1 && rec.confidence <= 5) {
45
+ edge.confidence = rec.confidence;
46
+ }
47
+ else {
48
+ // Lenient: keep the edge, drop the bad confidence, record a warning.
49
+ warnings.push(`map_edge (${parent}→${child}): invalid confidence ${String(rec.confidence)} — edge kept without confidence`);
50
+ }
51
+ }
52
+ edges.push(edge);
53
+ }
54
+ return { edges, warnings };
55
+ }
@@ -0,0 +1,23 @@
1
+ import { type InterchangeMap } from './interchange-schema.js';
2
+ export type CompositionCliOptions = {
3
+ /** `--composition-map <path>` — hand-authored interchange file. */
4
+ compositionMap?: string;
5
+ /** `--composition-agent` — opt into agentic resolution. */
6
+ compositionAgent?: boolean;
7
+ /** `--composition-refresh` — force the agent to run even over resolved residue. */
8
+ compositionRefresh?: boolean;
9
+ };
10
+ export type LoadUserMapResult = {
11
+ ok: true;
12
+ map: InterchangeMap;
13
+ } | {
14
+ ok: false;
15
+ error: string;
16
+ };
17
+ /** Read + validate a hand-authored interchange map (spec T1 / §1.2 third mechanism). */
18
+ export declare function loadUserMap(path: string): Promise<LoadUserMapResult>;
19
+ export type ResolvedCompositionSources = {
20
+ useAgent: boolean;
21
+ forceAgent: boolean;
22
+ };
23
+ export declare function resolveCompositionSources(opts: CompositionCliOptions): ResolvedCompositionSources;
@@ -0,0 +1,29 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { validateInterchangeMap } from './interchange-schema.js';
3
+ /** Read + validate a hand-authored interchange map (spec T1 / §1.2 third mechanism). */
4
+ export async function loadUserMap(path) {
5
+ let text;
6
+ try {
7
+ text = await readFile(path, 'utf8');
8
+ }
9
+ catch {
10
+ return { ok: false, error: `--composition-map: file not found: ${path}` };
11
+ }
12
+ let parsed;
13
+ try {
14
+ parsed = JSON.parse(text);
15
+ }
16
+ catch {
17
+ return { ok: false, error: `--composition-map: not valid JSON: ${path}` };
18
+ }
19
+ const res = validateInterchangeMap(parsed);
20
+ if (!res.valid)
21
+ return { ok: false, error: `--composition-map: ${res.errors.join('; ')}` };
22
+ return { ok: true, map: res.map };
23
+ }
24
+ export function resolveCompositionSources(opts) {
25
+ return {
26
+ useAgent: !!opts.compositionAgent,
27
+ forceAgent: !!opts.compositionRefresh,
28
+ };
29
+ }
@@ -0,0 +1,59 @@
1
+ import type { RawComponentDefinition } from '../../types.js';
2
+ import { type CompositionEdge, type InterchangeMap } from './interchange-schema.js';
3
+ import { type EdgeConflict } from './merge-edges.js';
4
+ export type ResolveMappingResult = {
5
+ components: RawComponentDefinition[];
6
+ edges: CompositionEdge[];
7
+ conflicts: EdgeConflict[];
8
+ warnings: string[];
9
+ };
10
+ /**
11
+ * Orchestrate composition-map acquisition (spec T2) and enrichment (T7).
12
+ *
13
+ * Sources by rank: user map (1) > typed-slot / "code slots" (2) > agent (4).
14
+ * ALL sources — including the code slots already on the incoming components —
15
+ * are fed into one ranked merge and unioned; non-conflicting edges from every
16
+ * source survive, and on a conflict (same parent+child, different slot) the
17
+ * higher-rank source wins and the loser is recorded. The agent runs only when
18
+ * `useAgent`/`forceAgent` is set AND there is residue a higher-rank source
19
+ * didn't cover (routing/cost optimization) — `forceAgent` bypasses that
20
+ * suppression but never changes rank.
21
+ *
22
+ * `runAgentFn` is injected (returns the agent's raw stdout) so this is
23
+ * testable without spawning a subprocess.
24
+ */
25
+ export declare function resolveMapping(input: {
26
+ components: RawComponentDefinition[];
27
+ userMap?: InterchangeMap;
28
+ useAgent?: boolean;
29
+ forceAgent?: boolean;
30
+ files: Array<{
31
+ path: string;
32
+ content: string;
33
+ }>;
34
+ runAgentFn: (opts: {
35
+ prompt: string;
36
+ files: Array<{
37
+ path: string;
38
+ content: string;
39
+ }>;
40
+ }) => Promise<string>;
41
+ buildPrompt?: (files: Array<{
42
+ path: string;
43
+ content: string;
44
+ }>, componentNames: string[]) => string;
45
+ /**
46
+ * Custom instruction preamble (from `--prompt composition=...`). Replaces the
47
+ * default guidance line ONLY; the machine-readable output contract + the
48
+ * component-name allowlist + candidate files are always appended so the
49
+ * JSONL parser keeps working regardless of the override.
50
+ */
51
+ promptOverride?: string;
52
+ /**
53
+ * Pre-resolved edges from an external source (e.g. the agent-authored parser
54
+ * path). They join the ranked merge at their own provenance rank alongside
55
+ * code slots and the user map. Callers using this typically set
56
+ * `useAgent: false` since they've already run their own resolution.
57
+ */
58
+ extraEdges?: CompositionEdge[];
59
+ }): Promise<ResolveMappingResult>;
@@ -0,0 +1,94 @@
1
+ import { groupsToEdges } from './interchange-schema.js';
2
+ import { mergeEdges } from './merge-edges.js';
3
+ import { parseMapEdges } from './parse-map-edges.js';
4
+ import { applyMapping } from './apply-mapping.js';
5
+ import { loadPrompt } from './agent-parser/load-prompt.js';
6
+ /**
7
+ * Orchestrate composition-map acquisition (spec T2) and enrichment (T7).
8
+ *
9
+ * Sources by rank: user map (1) > typed-slot / "code slots" (2) > agent (4).
10
+ * ALL sources — including the code slots already on the incoming components —
11
+ * are fed into one ranked merge and unioned; non-conflicting edges from every
12
+ * source survive, and on a conflict (same parent+child, different slot) the
13
+ * higher-rank source wins and the loser is recorded. The agent runs only when
14
+ * `useAgent`/`forceAgent` is set AND there is residue a higher-rank source
15
+ * didn't cover (routing/cost optimization) — `forceAgent` bypasses that
16
+ * suppression but never changes rank.
17
+ *
18
+ * `runAgentFn` is injected (returns the agent's raw stdout) so this is
19
+ * testable without spawning a subprocess.
20
+ */
21
+ export async function resolveMapping(input) {
22
+ const componentNames = new Set(input.components.map((c) => c.name));
23
+ const collected = [];
24
+ const agentWarnings = [];
25
+ // Rank 2 — typed-slot ("code slots") already resolved by the AST extractor.
26
+ // Feed them into the ranked merge so a conflicting lower-rank edge (agent
27
+ // placing the same child in a different slot) LOSES to code rather than being
28
+ // unioned in alongside it.
29
+ for (const c of input.components) {
30
+ for (const slot of c.slots) {
31
+ for (const child of slot.allowedComponents ?? []) {
32
+ collected.push({ parent: c.name, child, slot: slot.name, provenance: 'typed-slot' });
33
+ }
34
+ }
35
+ }
36
+ // Rank 1 — user-provided map.
37
+ if (input.userMap) {
38
+ collected.push(...groupsToEdges(input.userMap, 'user'));
39
+ }
40
+ // Externally pre-resolved edges (e.g. agent-authored parser, rank 3).
41
+ if (input.extraEdges) {
42
+ collected.push(...input.extraEdges);
43
+ }
44
+ // Routing: which parents are already covered by a higher-rank source?
45
+ const coveredParents = new Set(collected.map((e) => e.parent));
46
+ const residueParents = input.components.map((c) => c.name).filter((n) => !coveredParents.has(n));
47
+ // Rank 4 — agent. Runs when enabled AND (forced OR there is residue).
48
+ const shouldRunAgent = (input.useAgent || input.forceAgent) && (input.forceAgent || residueParents.length > 0);
49
+ if (shouldRunAgent) {
50
+ const prompt = input.buildPrompt
51
+ ? input.buildPrompt(input.files, [...componentNames])
52
+ : defaultPrompt(input.files, [...componentNames], input.promptOverride);
53
+ const raw = await input.runAgentFn({ prompt, files: input.files });
54
+ const parsed = parseMapEdges(raw, { componentNames });
55
+ collected.push(...parsed.edges);
56
+ agentWarnings.push(...parsed.warnings);
57
+ }
58
+ const merged = mergeEdges(collected);
59
+ // Apply the merged edges onto components whose allowedComponents are cleared,
60
+ // so the ranked merge is authoritative — a code edge that lost to a rank-1
61
+ // user override is actually gone, not left behind on the original slot.
62
+ // Slot structure is preserved; only the composition constraint is reset.
63
+ const base = input.components.map((c) => ({
64
+ ...c,
65
+ slots: c.slots.map((s) => {
66
+ const { allowedComponents: _drop, ...rest } = s;
67
+ return rest;
68
+ }),
69
+ }));
70
+ const applied = applyMapping(base, merged.edges);
71
+ return {
72
+ components: applied.components,
73
+ edges: merged.edges,
74
+ conflicts: merged.conflicts,
75
+ warnings: [...agentWarnings, ...applied.warnings],
76
+ };
77
+ }
78
+ function defaultPrompt(files, componentNames, promptOverride) {
79
+ const fileBlocks = files.map((f) => `--- ${f.path} ---\n${f.content}`).join('\n\n');
80
+ // The override replaces only the leading instruction; the output contract,
81
+ // name allowlist, and candidate files are always appended so the JSONL
82
+ // parser keeps working.
83
+ const instruction = promptOverride?.trim() ? promptOverride.trim() : loadPrompt('composition-edges.md').trim();
84
+ return [
85
+ instruction,
86
+ '',
87
+ 'Emit one JSON object per line, each: {"tool":"map_edge","parent":"<Name>","child":"<Name>","slot"?:"<slot>","confidence"?:1-5,"reason":"<cite the file + declaration>"}.',
88
+ 'Use ONLY these exact component names (an edge naming anything else is dropped):',
89
+ componentNames.join(', '),
90
+ '',
91
+ 'Candidate files:',
92
+ fileBlocks,
93
+ ].join('\n');
94
+ }
@@ -0,0 +1,27 @@
1
+ /** A directed edge in the slot-dependency graph. */
2
+ export interface SlotEdge {
3
+ fromComponent: string;
4
+ slotName: string;
5
+ toComponent: string;
6
+ }
7
+ /** A single elementary cycle, with `path[0] === path[path.length - 1]`. */
8
+ export interface SlotCycle {
9
+ path: string[];
10
+ edges: SlotEdge[];
11
+ }
12
+ /** Input shape mirroring the slot definition on CDFComponentEntry. */
13
+ export interface ComponentSlotInfo {
14
+ name: string;
15
+ slots: Array<{
16
+ name: string;
17
+ allowedComponents?: string[];
18
+ }>;
19
+ }
20
+ export declare function findSlotCycles(components: ComponentSlotInfo[]): SlotCycle[];
21
+ export declare function suggestCycleBreakEdge(cycle: SlotCycle, allCycles: SlotCycle[]): SlotEdge;
22
+ export declare function formatCyclePath(cycle: SlotCycle, maxHops?: number): string;
23
+ export interface CyclePathSegment {
24
+ kind: 'component' | 'slot' | 'arrow';
25
+ text: string;
26
+ }
27
+ export declare function formatCyclePathSegments(cycle: SlotCycle, maxHops?: number): CyclePathSegment[];