@contentful/experience-design-system-cli 2.12.4-dev-build-5aa6a69.0 → 2.12.4-dev-build-ff57340.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.
- package/dist/package.json +1 -1
- package/dist/src/analyze/command.js +16 -1
- package/dist/src/analyze/cycle-detection.d.ts +93 -0
- package/dist/src/analyze/cycle-detection.js +326 -0
- package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +24 -1
- package/dist/src/analyze/select/tui/components/FieldEditor.js +136 -6
- package/dist/src/apply/command.d.ts +34 -1
- package/dist/src/apply/command.js +81 -0
- package/dist/src/apply/error-parser.d.ts +43 -0
- package/dist/src/apply/error-parser.js +163 -0
- package/dist/src/import/tui/WizardApp.js +59 -10
- package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +1 -1
- package/dist/src/import/tui/steps/GenerateReviewStep.js +211 -17
- package/dist/src/session/db.d.ts +9 -0
- package/dist/src/session/db.js +46 -0
- package/package.json +2 -2
package/dist/package.json
CHANGED
|
@@ -6,7 +6,8 @@ import { extractComponents } from './extract/pipeline.js';
|
|
|
6
6
|
import { AnalyzeView } from './tui/AnalyzeView.js';
|
|
7
7
|
import { registerAnalyzeEditCommand } from './select/command.js';
|
|
8
8
|
import { registerAnalyzeSelectAgentCommand } from './select-agent/command.js';
|
|
9
|
-
import { openPipelineDb, getOrCreateSession, createStep, updateStep, storeRawComponents, storeScannedFiles, } from '../session/db.js';
|
|
9
|
+
import { openPipelineDb, getOrCreateSession, createStep, updateStep, storeRawComponents, storeScannedFiles, storeSlotCycles, } from '../session/db.js';
|
|
10
|
+
import { findSlotCycles, suggestCycleBreakEdge } from './cycle-detection.js';
|
|
10
11
|
import { preClassifyComponent } from './pre-classify.js';
|
|
11
12
|
import { isNonAuthorableComponent } from './extract/non-authorable-filter.js';
|
|
12
13
|
import { computeExtractionScore, deriveNeedsReview } from './extract/scoring.js';
|
|
@@ -191,6 +192,20 @@ export function registerAnalyzeCommand(program) {
|
|
|
191
192
|
}
|
|
192
193
|
const validatedComponents = validateExtractedComponents(filteredComponents);
|
|
193
194
|
storeRawComponents(db, sessionId, validatedComponents);
|
|
195
|
+
// INTEG-4401: post-extract slot-dependency cycle detection. The apply
|
|
196
|
+
// worker rejects cyclic slot graphs at push time, so we surface the
|
|
197
|
+
// same violation locally as a soft warning here (TUI reads
|
|
198
|
+
// slot_cycles) and as a hard block at manifest finalization.
|
|
199
|
+
const cycleInput = validatedComponents.map((c) => ({
|
|
200
|
+
name: c.name,
|
|
201
|
+
slots: c.slots.map((s) => ({ name: s.name, allowedComponents: s.allowedComponents })),
|
|
202
|
+
}));
|
|
203
|
+
const cycles = findSlotCycles(cycleInput);
|
|
204
|
+
const withBreaks = cycles.map((cycle) => ({
|
|
205
|
+
...cycle,
|
|
206
|
+
suggestedBreak: suggestCycleBreakEdge(cycle, cycles),
|
|
207
|
+
}));
|
|
208
|
+
storeSlotCycles(db, sessionId, withBreaks);
|
|
194
209
|
storeScannedFiles(db, sessionId, sourceFiles.map((f) => relative(projectRoot, f)));
|
|
195
210
|
updateStep(db, stepId, 'complete', { sessionId });
|
|
196
211
|
db.close();
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slot-dependency cycle detection.
|
|
3
|
+
*
|
|
4
|
+
* The Contentful Experience Design System backend rejects component manifests
|
|
5
|
+
* that contain slot-dependency cycles at apply time — a topo-sort in the
|
|
6
|
+
* apply worker refuses to create component types whose slot's
|
|
7
|
+
* `$allowedComponents` transitively point back at themselves. Reference:
|
|
8
|
+
* `experience-design-system-integrations` / services/design-system-sources-apply-worker/src/topo-sort.ts.
|
|
9
|
+
*
|
|
10
|
+
* We surface the same check locally so the wizard can:
|
|
11
|
+
* 1. warn the operator at extract time (soft) with sidebar badges + banner,
|
|
12
|
+
* 2. hard-block the push at manifest-finalization time before any API call.
|
|
13
|
+
*
|
|
14
|
+
* The algorithm is Johnson's — an O((V + E)(C + 1)) enumeration of all
|
|
15
|
+
* elementary (simple) cycles in a directed graph. We need *elementary*
|
|
16
|
+
* cycles rather than just cycle-existence because an operator can have
|
|
17
|
+
* multiple independent cycles they need to address separately, and each
|
|
18
|
+
* gets its own suggested fix.
|
|
19
|
+
*
|
|
20
|
+
* Johnson's algorithm reference: Donald B. Johnson, "Finding all the
|
|
21
|
+
* elementary circuits of a directed graph." SIAM J. Comput., 4(1):77-84, 1975.
|
|
22
|
+
*/
|
|
23
|
+
/** A directed edge in the slot-dependency graph. */
|
|
24
|
+
export interface SlotEdge {
|
|
25
|
+
fromComponent: string;
|
|
26
|
+
slotName: string;
|
|
27
|
+
toComponent: string;
|
|
28
|
+
}
|
|
29
|
+
/** A single elementary cycle, with `path[0] === path[path.length - 1]`. */
|
|
30
|
+
export interface SlotCycle {
|
|
31
|
+
/** Component names visited, with the first repeated at the end. */
|
|
32
|
+
path: string[];
|
|
33
|
+
/** Ordered edges forming the cycle. `edges.length === path.length - 1`. */
|
|
34
|
+
edges: SlotEdge[];
|
|
35
|
+
}
|
|
36
|
+
/** Input shape mirroring the slot definition on CDFComponentEntry. */
|
|
37
|
+
export interface ComponentSlotInfo {
|
|
38
|
+
name: string;
|
|
39
|
+
slots: Array<{
|
|
40
|
+
name: string;
|
|
41
|
+
allowedComponents?: string[];
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Johnson's algorithm — enumerates all elementary (simple) cycles in the
|
|
46
|
+
* slot-dependency graph. Self-loops are handled as a degenerate first pass
|
|
47
|
+
* because Tarjan's SCC decomposition assigns a self-looping node to its own
|
|
48
|
+
* single-node SCC, which Johnson's inner loop otherwise skips.
|
|
49
|
+
*/
|
|
50
|
+
export declare function findSlotCycles(components: ComponentSlotInfo[]): SlotCycle[];
|
|
51
|
+
/**
|
|
52
|
+
* Pick which edge is the best candidate to remove to break a cycle.
|
|
53
|
+
* Heuristic: prefer removing the edge whose `toComponent` appears most often
|
|
54
|
+
* as a destination across all cycles — that "hub" node is the biggest
|
|
55
|
+
* contributor and removing an inbound edge to it likely breaks multiple
|
|
56
|
+
* cycles at once. Falls back to the first edge in the cycle for
|
|
57
|
+
* deterministic output when all candidates are tied.
|
|
58
|
+
*/
|
|
59
|
+
export declare function suggestCycleBreakEdge(cycle: SlotCycle, allCycles: SlotCycle[]): SlotEdge;
|
|
60
|
+
/**
|
|
61
|
+
* Format a cycle for the compact banner:
|
|
62
|
+
* `CardA → header → CardB → footer → CardA`
|
|
63
|
+
*
|
|
64
|
+
* Truncates at `maxHops` edges (default 8), inserting `…` mid-path so the
|
|
65
|
+
* beginning and end remain visible. A hop is one edge; the returned string
|
|
66
|
+
* for an n-edge cycle contains n+1 component names.
|
|
67
|
+
*
|
|
68
|
+
* Kept for backend/log/error use where a single string is required (see
|
|
69
|
+
* `assertNoSlotCycles` in `apply/command.ts`). The TUI banner + detail panel
|
|
70
|
+
* use `formatCyclePathSegments` instead so slot names can be visually
|
|
71
|
+
* distinguished from component names.
|
|
72
|
+
*/
|
|
73
|
+
export declare function formatCyclePath(cycle: SlotCycle, maxHops?: number): string;
|
|
74
|
+
/**
|
|
75
|
+
* A single token in the rendered cycle path — either a component name, a
|
|
76
|
+
* slot name (wrapped in brackets for monochrome legibility), or the arrow
|
|
77
|
+
* separator between them. The TUI colorizes each kind independently.
|
|
78
|
+
*
|
|
79
|
+
* Slots are rendered as `[slotName]` so the visual distinction survives when
|
|
80
|
+
* ANSI colors are stripped (log files, redirection, dumb terminals).
|
|
81
|
+
*/
|
|
82
|
+
export interface CyclePathSegment {
|
|
83
|
+
kind: 'component' | 'slot' | 'arrow';
|
|
84
|
+
text: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Segment-oriented counterpart to `formatCyclePath`. Preserves the same
|
|
88
|
+
* 8-hop truncation semantics but returns each token classified so the
|
|
89
|
+
* renderer can color slots (cyan) and components (default) independently.
|
|
90
|
+
* The truncation marker `…` is emitted as a bare component segment because
|
|
91
|
+
* it stands in for one or more component nodes.
|
|
92
|
+
*/
|
|
93
|
+
export declare function formatCyclePathSegments(cycle: SlotCycle, maxHops?: number): CyclePathSegment[];
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slot-dependency cycle detection.
|
|
3
|
+
*
|
|
4
|
+
* The Contentful Experience Design System backend rejects component manifests
|
|
5
|
+
* that contain slot-dependency cycles at apply time — a topo-sort in the
|
|
6
|
+
* apply worker refuses to create component types whose slot's
|
|
7
|
+
* `$allowedComponents` transitively point back at themselves. Reference:
|
|
8
|
+
* `experience-design-system-integrations` / services/design-system-sources-apply-worker/src/topo-sort.ts.
|
|
9
|
+
*
|
|
10
|
+
* We surface the same check locally so the wizard can:
|
|
11
|
+
* 1. warn the operator at extract time (soft) with sidebar badges + banner,
|
|
12
|
+
* 2. hard-block the push at manifest-finalization time before any API call.
|
|
13
|
+
*
|
|
14
|
+
* The algorithm is Johnson's — an O((V + E)(C + 1)) enumeration of all
|
|
15
|
+
* elementary (simple) cycles in a directed graph. We need *elementary*
|
|
16
|
+
* cycles rather than just cycle-existence because an operator can have
|
|
17
|
+
* multiple independent cycles they need to address separately, and each
|
|
18
|
+
* gets its own suggested fix.
|
|
19
|
+
*
|
|
20
|
+
* Johnson's algorithm reference: Donald B. Johnson, "Finding all the
|
|
21
|
+
* elementary circuits of a directed graph." SIAM J. Comput., 4(1):77-84, 1975.
|
|
22
|
+
*/
|
|
23
|
+
function buildGraph(components) {
|
|
24
|
+
const nodes = [];
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
for (const c of components) {
|
|
27
|
+
if (seen.has(c.name))
|
|
28
|
+
continue;
|
|
29
|
+
seen.add(c.name);
|
|
30
|
+
nodes.push(c.name);
|
|
31
|
+
}
|
|
32
|
+
const adjacency = new Map();
|
|
33
|
+
for (const node of nodes)
|
|
34
|
+
adjacency.set(node, []);
|
|
35
|
+
for (const comp of components) {
|
|
36
|
+
const outgoing = adjacency.get(comp.name);
|
|
37
|
+
if (!outgoing)
|
|
38
|
+
continue;
|
|
39
|
+
for (const slot of comp.slots) {
|
|
40
|
+
const allowed = slot.allowedComponents ?? [];
|
|
41
|
+
for (const target of allowed) {
|
|
42
|
+
if (!seen.has(target))
|
|
43
|
+
continue; // unknown / external — ignore
|
|
44
|
+
outgoing.push({ target, slotName: slot.name });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { nodes, adjacency };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Tarjan's strongly-connected-components algorithm — used by Johnson's
|
|
52
|
+
* algorithm to constrain cycle search to one SCC at a time. Returns SCCs in
|
|
53
|
+
* reverse-topological order.
|
|
54
|
+
*/
|
|
55
|
+
function stronglyConnectedComponents(nodes, adjacency) {
|
|
56
|
+
const index = new Map();
|
|
57
|
+
const lowlink = new Map();
|
|
58
|
+
const onStack = new Set();
|
|
59
|
+
const stack = [];
|
|
60
|
+
const sccs = [];
|
|
61
|
+
let counter = 0;
|
|
62
|
+
const nodeSet = new Set(nodes);
|
|
63
|
+
function strongconnect(v) {
|
|
64
|
+
index.set(v, counter);
|
|
65
|
+
lowlink.set(v, counter);
|
|
66
|
+
counter += 1;
|
|
67
|
+
stack.push(v);
|
|
68
|
+
onStack.add(v);
|
|
69
|
+
const neighbours = adjacency.get(v) ?? [];
|
|
70
|
+
for (const { target } of neighbours) {
|
|
71
|
+
if (!nodeSet.has(target))
|
|
72
|
+
continue;
|
|
73
|
+
if (!index.has(target)) {
|
|
74
|
+
strongconnect(target);
|
|
75
|
+
lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(target)));
|
|
76
|
+
}
|
|
77
|
+
else if (onStack.has(target)) {
|
|
78
|
+
lowlink.set(v, Math.min(lowlink.get(v), index.get(target)));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (lowlink.get(v) === index.get(v)) {
|
|
82
|
+
const scc = [];
|
|
83
|
+
let w;
|
|
84
|
+
do {
|
|
85
|
+
w = stack.pop();
|
|
86
|
+
onStack.delete(w);
|
|
87
|
+
scc.push(w);
|
|
88
|
+
} while (w !== v);
|
|
89
|
+
sccs.push(scc);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const node of nodes) {
|
|
93
|
+
if (!index.has(node))
|
|
94
|
+
strongconnect(node);
|
|
95
|
+
}
|
|
96
|
+
return sccs;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Given a specific traversal `path` (component names) and the edge multiset
|
|
100
|
+
* we walked to build it, materialize a `SlotCycle`. Because multiple parallel
|
|
101
|
+
* edges may exist between the same two components (different slot names), we
|
|
102
|
+
* carry the exact edges along the search rather than reconstructing them from
|
|
103
|
+
* the node path.
|
|
104
|
+
*/
|
|
105
|
+
function makeCycle(path, edges) {
|
|
106
|
+
return { path: [...path, path[0]], edges: [...edges] };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Johnson's algorithm — enumerates all elementary (simple) cycles in the
|
|
110
|
+
* slot-dependency graph. Self-loops are handled as a degenerate first pass
|
|
111
|
+
* because Tarjan's SCC decomposition assigns a self-looping node to its own
|
|
112
|
+
* single-node SCC, which Johnson's inner loop otherwise skips.
|
|
113
|
+
*/
|
|
114
|
+
export function findSlotCycles(components) {
|
|
115
|
+
if (components.length === 0)
|
|
116
|
+
return [];
|
|
117
|
+
const { nodes, adjacency } = buildGraph(components);
|
|
118
|
+
if (nodes.length === 0)
|
|
119
|
+
return [];
|
|
120
|
+
const cycles = [];
|
|
121
|
+
// Degenerate case: self-loops. For any component whose slot allows itself,
|
|
122
|
+
// emit a length-1 cycle before invoking Johnson's inner loop — Tarjan's
|
|
123
|
+
// trivial single-node SCC test in Johnson's original paper only counts as
|
|
124
|
+
// an SCC if the node participates in an edge, which we replicate here.
|
|
125
|
+
for (const node of nodes) {
|
|
126
|
+
const outgoing = adjacency.get(node) ?? [];
|
|
127
|
+
for (const edge of outgoing) {
|
|
128
|
+
if (edge.target === node) {
|
|
129
|
+
cycles.push({
|
|
130
|
+
path: [node, node],
|
|
131
|
+
edges: [{ fromComponent: node, slotName: edge.slotName, toComponent: node }],
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Johnson's main algorithm — iterate over remaining nodes in a fixed order,
|
|
137
|
+
// decomposing the subgraph induced by `{node, ...higherOrder}` into SCCs,
|
|
138
|
+
// and running the blocked-list circuit search rooted at the least node of
|
|
139
|
+
// each non-trivial SCC. This is the classic formulation from the paper.
|
|
140
|
+
const nodeOrder = [...nodes].sort();
|
|
141
|
+
let remaining = [...nodeOrder];
|
|
142
|
+
while (remaining.length > 0) {
|
|
143
|
+
const subgraphNodes = remaining;
|
|
144
|
+
const sccs = stronglyConnectedComponents(subgraphNodes, adjacency).filter((scc) => {
|
|
145
|
+
if (scc.length > 1)
|
|
146
|
+
return true;
|
|
147
|
+
// Non-trivial single-node SCC iff the node has a self-loop within the
|
|
148
|
+
// subgraph. We already emitted those above, so exclude here.
|
|
149
|
+
return false;
|
|
150
|
+
});
|
|
151
|
+
if (sccs.length === 0)
|
|
152
|
+
break;
|
|
153
|
+
// Pick the SCC containing the lexicographically-smallest node — mirrors
|
|
154
|
+
// the "least vertex" root selection in Johnson's paper.
|
|
155
|
+
let startNode = null;
|
|
156
|
+
let startScc = null;
|
|
157
|
+
for (const scc of sccs) {
|
|
158
|
+
const min = [...scc].sort()[0];
|
|
159
|
+
if (startNode === null || min < startNode) {
|
|
160
|
+
startNode = min;
|
|
161
|
+
startScc = scc;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (startNode === null || startScc === null)
|
|
165
|
+
break;
|
|
166
|
+
const sccSet = new Set(startScc);
|
|
167
|
+
const blocked = new Set();
|
|
168
|
+
const blockedMap = new Map();
|
|
169
|
+
const pathStack = [];
|
|
170
|
+
const edgeStack = [];
|
|
171
|
+
function unblock(u) {
|
|
172
|
+
blocked.delete(u);
|
|
173
|
+
const dependents = blockedMap.get(u);
|
|
174
|
+
if (!dependents)
|
|
175
|
+
return;
|
|
176
|
+
for (const w of dependents) {
|
|
177
|
+
if (blocked.has(w))
|
|
178
|
+
unblock(w);
|
|
179
|
+
}
|
|
180
|
+
dependents.clear();
|
|
181
|
+
}
|
|
182
|
+
function circuit(v, root) {
|
|
183
|
+
let foundCycle = false;
|
|
184
|
+
pathStack.push(v);
|
|
185
|
+
blocked.add(v);
|
|
186
|
+
const outgoing = adjacency.get(v) ?? [];
|
|
187
|
+
for (const { target: w, slotName } of outgoing) {
|
|
188
|
+
if (!sccSet.has(w))
|
|
189
|
+
continue;
|
|
190
|
+
edgeStack.push({ fromComponent: v, slotName, toComponent: w });
|
|
191
|
+
if (w === root) {
|
|
192
|
+
cycles.push(makeCycle(pathStack, edgeStack));
|
|
193
|
+
foundCycle = true;
|
|
194
|
+
}
|
|
195
|
+
else if (!blocked.has(w)) {
|
|
196
|
+
if (circuit(w, root))
|
|
197
|
+
foundCycle = true;
|
|
198
|
+
}
|
|
199
|
+
edgeStack.pop();
|
|
200
|
+
}
|
|
201
|
+
if (foundCycle) {
|
|
202
|
+
unblock(v);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
for (const { target: w } of outgoing) {
|
|
206
|
+
if (!sccSet.has(w))
|
|
207
|
+
continue;
|
|
208
|
+
let deps = blockedMap.get(w);
|
|
209
|
+
if (!deps) {
|
|
210
|
+
deps = new Set();
|
|
211
|
+
blockedMap.set(w, deps);
|
|
212
|
+
}
|
|
213
|
+
deps.add(v);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
pathStack.pop();
|
|
217
|
+
return foundCycle;
|
|
218
|
+
}
|
|
219
|
+
circuit(startNode, startNode);
|
|
220
|
+
// Remove `startNode` from consideration and repeat with the reduced
|
|
221
|
+
// subgraph. In classic Johnson's this is done by re-running SCC on the
|
|
222
|
+
// subgraph induced by the remaining nodes.
|
|
223
|
+
remaining = remaining.filter((n) => n !== startNode);
|
|
224
|
+
}
|
|
225
|
+
return cycles;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Pick which edge is the best candidate to remove to break a cycle.
|
|
229
|
+
* Heuristic: prefer removing the edge whose `toComponent` appears most often
|
|
230
|
+
* as a destination across all cycles — that "hub" node is the biggest
|
|
231
|
+
* contributor and removing an inbound edge to it likely breaks multiple
|
|
232
|
+
* cycles at once. Falls back to the first edge in the cycle for
|
|
233
|
+
* deterministic output when all candidates are tied.
|
|
234
|
+
*/
|
|
235
|
+
export function suggestCycleBreakEdge(cycle, allCycles) {
|
|
236
|
+
if (cycle.edges.length === 0) {
|
|
237
|
+
throw new Error('suggestCycleBreakEdge: cycle has no edges');
|
|
238
|
+
}
|
|
239
|
+
const indegree = new Map();
|
|
240
|
+
for (const c of allCycles) {
|
|
241
|
+
for (const edge of c.edges) {
|
|
242
|
+
indegree.set(edge.toComponent, (indegree.get(edge.toComponent) ?? 0) + 1);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
let best = cycle.edges[0];
|
|
246
|
+
let bestScore = indegree.get(best.toComponent) ?? 0;
|
|
247
|
+
for (const edge of cycle.edges) {
|
|
248
|
+
const score = indegree.get(edge.toComponent) ?? 0;
|
|
249
|
+
if (score > bestScore) {
|
|
250
|
+
best = edge;
|
|
251
|
+
bestScore = score;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return best;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Format a cycle for the compact banner:
|
|
258
|
+
* `CardA → header → CardB → footer → CardA`
|
|
259
|
+
*
|
|
260
|
+
* Truncates at `maxHops` edges (default 8), inserting `…` mid-path so the
|
|
261
|
+
* beginning and end remain visible. A hop is one edge; the returned string
|
|
262
|
+
* for an n-edge cycle contains n+1 component names.
|
|
263
|
+
*
|
|
264
|
+
* Kept for backend/log/error use where a single string is required (see
|
|
265
|
+
* `assertNoSlotCycles` in `apply/command.ts`). The TUI banner + detail panel
|
|
266
|
+
* use `formatCyclePathSegments` instead so slot names can be visually
|
|
267
|
+
* distinguished from component names.
|
|
268
|
+
*/
|
|
269
|
+
export function formatCyclePath(cycle, maxHops = 8) {
|
|
270
|
+
const arrow = ' → ';
|
|
271
|
+
const parts = [];
|
|
272
|
+
// Interleave: component, slot, component, slot, ..., final component
|
|
273
|
+
// path.length === edges.length + 1.
|
|
274
|
+
for (let i = 0; i < cycle.edges.length; i += 1) {
|
|
275
|
+
parts.push(cycle.path[i]);
|
|
276
|
+
parts.push(cycle.edges[i].slotName);
|
|
277
|
+
}
|
|
278
|
+
parts.push(cycle.path[cycle.path.length - 1]);
|
|
279
|
+
if (cycle.edges.length <= maxHops) {
|
|
280
|
+
return parts.join(arrow);
|
|
281
|
+
}
|
|
282
|
+
// Truncated: keep the first `keep` edges' worth of tokens and the final
|
|
283
|
+
// component. Two "tokens per hop" (component + slot) plus the trailing
|
|
284
|
+
// component name.
|
|
285
|
+
const keepHops = Math.max(1, maxHops - 1);
|
|
286
|
+
const keepTokens = keepHops * 2;
|
|
287
|
+
const head = parts.slice(0, keepTokens).join(arrow);
|
|
288
|
+
const tail = parts[parts.length - 1];
|
|
289
|
+
return `${head}${arrow}…${arrow}${tail}`;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Segment-oriented counterpart to `formatCyclePath`. Preserves the same
|
|
293
|
+
* 8-hop truncation semantics but returns each token classified so the
|
|
294
|
+
* renderer can color slots (cyan) and components (default) independently.
|
|
295
|
+
* The truncation marker `…` is emitted as a bare component segment because
|
|
296
|
+
* it stands in for one or more component nodes.
|
|
297
|
+
*/
|
|
298
|
+
export function formatCyclePathSegments(cycle, maxHops = 8) {
|
|
299
|
+
// Interleaved [component, slot, component, slot, ..., component] token list
|
|
300
|
+
// matching `formatCyclePath`'s parts array, but classified.
|
|
301
|
+
const raw = [];
|
|
302
|
+
for (let i = 0; i < cycle.edges.length; i += 1) {
|
|
303
|
+
raw.push({ kind: 'component', text: cycle.path[i] });
|
|
304
|
+
raw.push({ kind: 'slot', text: `[${cycle.edges[i].slotName}]` });
|
|
305
|
+
}
|
|
306
|
+
raw.push({ kind: 'component', text: cycle.path[cycle.path.length - 1] });
|
|
307
|
+
const withArrows = (segs) => {
|
|
308
|
+
const out = [];
|
|
309
|
+
for (let i = 0; i < segs.length; i += 1) {
|
|
310
|
+
if (i > 0)
|
|
311
|
+
out.push({ kind: 'arrow', text: ' → ' });
|
|
312
|
+
out.push(segs[i]);
|
|
313
|
+
}
|
|
314
|
+
return out;
|
|
315
|
+
};
|
|
316
|
+
if (cycle.edges.length <= maxHops) {
|
|
317
|
+
return withArrows(raw);
|
|
318
|
+
}
|
|
319
|
+
// Match `formatCyclePath` truncation: keep the first (maxHops-1)*2 tokens
|
|
320
|
+
// plus a "…" placeholder plus the trailing component.
|
|
321
|
+
const keepHops = Math.max(1, maxHops - 1);
|
|
322
|
+
const keepTokens = keepHops * 2;
|
|
323
|
+
const head = raw.slice(0, keepTokens);
|
|
324
|
+
const tail = raw[raw.length - 1];
|
|
325
|
+
return withArrows([...head, { kind: 'component', text: '…' }, tail]);
|
|
326
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { findSlotCycles, type ComponentSlotInfo } from '../../../cycle-detection.js';
|
|
2
3
|
/** Per-prop / per-component metadata captured by the extractor + generate phase.
|
|
3
4
|
* Optional; when omitted, the FieldEditor renders without rationale/source
|
|
4
5
|
* affordances (backwards-compatible with mounts that pre-date Feature 1). */
|
|
@@ -63,6 +64,28 @@ type FieldEditorProps = {
|
|
|
63
64
|
* description editors, string default editors, and value-list text entry.
|
|
64
65
|
*/
|
|
65
66
|
onTextEntryActiveChange?: (active: boolean) => void;
|
|
67
|
+
/**
|
|
68
|
+
* INTEG-4401: project-wide slot graph. When supplied, the
|
|
69
|
+
* $allowedComponents add-mode shows a cycle-filtered picker. Optional.
|
|
70
|
+
*/
|
|
71
|
+
projectSlotGraph?: ComponentSlotInfo[];
|
|
72
|
+
/**
|
|
73
|
+
* INTEG-4401: name of the component being edited. Self-loop guard +
|
|
74
|
+
* self-entry replacement in the cycle simulation.
|
|
75
|
+
*/
|
|
76
|
+
currentComponentName?: string;
|
|
66
77
|
};
|
|
67
|
-
|
|
78
|
+
/** Simulate adding a candidate to a slot; replaces self-entry with live edits. */
|
|
79
|
+
export declare function simulateGraphWithCandidate(projectSlotGraph: ComponentSlotInfo[], selfName: string, currentSlots: {
|
|
80
|
+
name: string;
|
|
81
|
+
allowedComponents: string[];
|
|
82
|
+
}[], targetSlotName: string, candidate: string): ComponentSlotInfo[];
|
|
83
|
+
/** True iff `next` contains a cycle whose canonical edge set is absent in `before`. */
|
|
84
|
+
export declare function introducesNewCycle(before: ReturnType<typeof findSlotCycles>, next: ReturnType<typeof findSlotCycles>): boolean;
|
|
85
|
+
/** Candidate names that can be added to a slot without creating a new cycle. */
|
|
86
|
+
export declare function computeAllowedComponentCandidates(projectSlotGraph: ComponentSlotInfo[], selfName: string, currentSlots: {
|
|
87
|
+
name: string;
|
|
88
|
+
allowedComponents: string[];
|
|
89
|
+
}[], targetSlotName: string): string[];
|
|
90
|
+
export declare function FieldEditor({ value, width, height, active, onChange, onSave, onDiscard, onExit, metadata, onTogglePropRationale, onToggleComponentRationale, onToggleSourceExternal, onTextEntryActiveChange, projectSlotGraph, currentComponentName, }: FieldEditorProps): React.ReactElement;
|
|
68
91
|
export {};
|