@contentful/experience-design-system-cli 2.12.4-dev-build-ff57340.0 → 2.13.1-dev-build-847dead.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 (55) hide show
  1. package/dist/package.json +3 -6
  2. package/dist/src/analyze/command.js +2 -22
  3. package/dist/src/analyze/select/command.js +1 -1
  4. package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +1 -24
  5. package/dist/src/analyze/select/tui/components/FieldEditor.js +10 -137
  6. package/dist/src/analyze/select-agent/command.js +1 -1
  7. package/dist/src/apply/command.d.ts +1 -34
  8. package/dist/src/apply/command.js +0 -81
  9. package/dist/src/import/tui/WizardApp.js +10 -59
  10. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +1 -1
  11. package/dist/src/import/tui/steps/GenerateReviewStep.js +23 -213
  12. package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +0 -10
  13. package/dist/src/import/tui/steps/WizardPreviewStep.js +51 -95
  14. package/dist/src/import/tui/steps/preview-diff.js +0 -48
  15. package/dist/src/session/db.d.ts +0 -9
  16. package/dist/src/session/db.js +0 -46
  17. package/dist/src/types.d.ts +2 -76
  18. package/dist/src/types.js +1 -4
  19. package/package.json +3 -6
  20. package/dist/src/analyze/cycle-detection.d.ts +0 -93
  21. package/dist/src/analyze/cycle-detection.js +0 -326
  22. package/dist/src/analyze/extract/astro.d.ts +0 -5
  23. package/dist/src/analyze/extract/astro.js +0 -289
  24. package/dist/src/analyze/extract/non-authorable-filter.d.ts +0 -16
  25. package/dist/src/analyze/extract/non-authorable-filter.js +0 -60
  26. package/dist/src/analyze/extract/pipeline.d.ts +0 -6
  27. package/dist/src/analyze/extract/pipeline.js +0 -314
  28. package/dist/src/analyze/extract/react.d.ts +0 -2
  29. package/dist/src/analyze/extract/react.js +0 -2041
  30. package/dist/src/analyze/extract/scoring.d.ts +0 -12
  31. package/dist/src/analyze/extract/scoring.js +0 -108
  32. package/dist/src/analyze/extract/slot-allowed-components.d.ts +0 -8
  33. package/dist/src/analyze/extract/slot-allowed-components.js +0 -40
  34. package/dist/src/analyze/extract/slot-detection.d.ts +0 -35
  35. package/dist/src/analyze/extract/slot-detection.js +0 -101
  36. package/dist/src/analyze/extract/source-inspection.d.ts +0 -13
  37. package/dist/src/analyze/extract/source-inspection.js +0 -189
  38. package/dist/src/analyze/extract/stencil.d.ts +0 -2
  39. package/dist/src/analyze/extract/stencil.js +0 -296
  40. package/dist/src/analyze/extract/svelte.d.ts +0 -5
  41. package/dist/src/analyze/extract/svelte.js +0 -1626
  42. package/dist/src/analyze/extract/tsx-shared.d.ts +0 -8
  43. package/dist/src/analyze/extract/tsx-shared.js +0 -263
  44. package/dist/src/analyze/extract/validate.d.ts +0 -16
  45. package/dist/src/analyze/extract/validate.js +0 -94
  46. package/dist/src/analyze/extract/vue-tsx.d.ts +0 -2
  47. package/dist/src/analyze/extract/vue-tsx.js +0 -498
  48. package/dist/src/analyze/extract/vue.d.ts +0 -5
  49. package/dist/src/analyze/extract/vue.js +0 -653
  50. package/dist/src/analyze/extract/web-components.d.ts +0 -2
  51. package/dist/src/analyze/extract/web-components.js +0 -876
  52. package/dist/src/analyze/pre-classify.d.ts +0 -17
  53. package/dist/src/analyze/pre-classify.js +0 -211
  54. package/dist/src/apply/error-parser.d.ts +0 -43
  55. package/dist/src/apply/error-parser.js +0 -163
@@ -138,15 +138,6 @@ CREATE TABLE IF NOT EXISTS scanned_files (
138
138
  PRIMARY KEY (session_id, path)
139
139
  );
140
140
 
141
- CREATE TABLE IF NOT EXISTS slot_cycles (
142
- session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
143
- cycle_index INTEGER NOT NULL,
144
- path_json TEXT NOT NULL,
145
- edges_json TEXT NOT NULL,
146
- suggested_break_json TEXT,
147
- PRIMARY KEY (session_id, cycle_index)
148
- );
149
-
150
141
  CREATE INDEX IF NOT EXISTS idx_steps_session ON steps(session_id);
151
142
  CREATE INDEX IF NOT EXISTS idx_steps_command ON steps(session_id, command);
152
143
  CREATE INDEX IF NOT EXISTS idx_raw_components_session ON raw_components(session_id);
@@ -1397,43 +1388,6 @@ export function loadScannedFiles(db, sessionId) {
1397
1388
  const rows = db.prepare('SELECT path FROM scanned_files WHERE session_id = ? ORDER BY path').all(sessionId);
1398
1389
  return rows.map((r) => r.path);
1399
1390
  }
1400
- // --- Slot-dependency cycle persistence (INTEG-4401) ---
1401
- //
1402
- // The `slot_cycles` table caches the result of running the cycle detector
1403
- // over the extractor's slot output so the TUI can render sidebar badges,
1404
- // a banner, and an expandable detail panel without re-running the analysis
1405
- // on every render. Rows are session-scoped and rewritten wholesale on each
1406
- // re-extract via `storeSlotCycles`.
1407
- export function storeSlotCycles(db, sessionId, cycles) {
1408
- db.exec('BEGIN');
1409
- try {
1410
- db.prepare('DELETE FROM slot_cycles WHERE session_id = ?').run(sessionId);
1411
- const insert = db.prepare(`INSERT INTO slot_cycles (session_id, cycle_index, path_json, edges_json, suggested_break_json)
1412
- VALUES (?, ?, ?, ?, ?)`);
1413
- for (let i = 0; i < cycles.length; i += 1) {
1414
- const cycle = cycles[i];
1415
- insert.run(sessionId, i, JSON.stringify(cycle.path), JSON.stringify(cycle.edges), cycle.suggestedBreak ? JSON.stringify(cycle.suggestedBreak) : null);
1416
- }
1417
- db.exec('COMMIT');
1418
- }
1419
- catch (e) {
1420
- db.exec('ROLLBACK');
1421
- throw e;
1422
- }
1423
- }
1424
- export function loadSlotCycles(db, sessionId) {
1425
- const rows = db
1426
- .prepare('SELECT cycle_index, path_json, edges_json, suggested_break_json FROM slot_cycles WHERE session_id = ? ORDER BY cycle_index')
1427
- .all(sessionId);
1428
- return rows.map((r) => ({
1429
- path: JSON.parse(r.path_json),
1430
- edges: JSON.parse(r.edges_json),
1431
- suggestedBreak: r.suggested_break_json ? JSON.parse(r.suggested_break_json) : null,
1432
- }));
1433
- }
1434
- export function clearSlotCycles(db, sessionId) {
1435
- db.prepare('DELETE FROM slot_cycles WHERE session_id = ?').run(sessionId);
1436
- }
1437
1391
  export function markCacheHumanEdited(db, entityType, entityId) {
1438
1392
  const now = new Date().toISOString();
1439
1393
  db.prepare(`UPDATE generation_cache SET human_edited = 1, updated_at = ? WHERE entity_type = ? AND entity_id = ?`).run(now, entityType, entityId);
@@ -1,11 +1,6 @@
1
1
  import type { DesignTokenType } from '@contentful/experience-design-system-types';
2
- export type ExtractionValidationIssueCode = 'EMPTY_COMPONENT_NAME' | 'EMPTY_PROP_NAME' | 'EMPTY_SLOT_NAME' | 'PROP_SLOT_NAME_COLLISION' | 'DUPLICATE_COMPONENT_NAME' | 'EMPTY_COMPONENT' | 'SERVER_VALIDATION_FAILED';
3
- export type ExtractionValidationIssue = {
4
- severity: 'error' | 'warning';
5
- code: ExtractionValidationIssueCode;
6
- message: string;
7
- field?: string;
8
- };
2
+ export type { ExtractionValidationIssueCode, ExtractionValidationIssue, RawPropDefinition, RawSlotDefinition, RawComponentDefinition, ComponentExtractionResult, ExtractorProgress, ExtractorOptions, ComponentExtractor, } from '@contentful/experience-design-system-extraction';
3
+ export { stripScoringFields } from '@contentful/experience-design-system-extraction';
9
4
  export interface RawTokenDefinition {
10
5
  name: string;
11
6
  value: string;
@@ -13,76 +8,7 @@ export interface RawTokenDefinition {
13
8
  inferredKind: DesignTokenType | string;
14
9
  ambiguous: boolean;
15
10
  }
16
- export interface RawPropDefinition {
17
- name: string;
18
- type: string;
19
- required: boolean;
20
- category?: 'content' | 'design' | 'state';
21
- defaultValue?: string;
22
- allowedValues?: string[];
23
- description?: string;
24
- tokenReference?: string;
25
- /** 1-indexed source line where this prop's declaration begins; relative to RawComponentDefinition.source. */
26
- sourceStartLine?: number;
27
- /** 1-indexed inclusive source line where this prop's declaration ends; relative to RawComponentDefinition.source. */
28
- sourceEndLine?: number;
29
- }
30
- export interface RawSlotDefinition {
31
- name: string;
32
- isDefault: boolean;
33
- description?: string;
34
- allowedComponents?: string[];
35
- }
36
- export interface RawComponentDefinition {
37
- name: string;
38
- source: string;
39
- framework: 'react' | 'next' | 'vue' | 'astro' | 'web-component' | 'stencil' | 'svelte';
40
- props: RawPropDefinition[];
41
- slots: RawSlotDefinition[];
42
- /**
43
- * True when the source file declaring this component calls
44
- * React.createContext / createContext. Used downstream to filter
45
- * non-authorable context-provider components.
46
- */
47
- usesCreateContext?: boolean;
48
- extractionConfidence?: number | null;
49
- reviewReasons?: string[];
50
- needsReview?: boolean;
51
- validationIssues?: ExtractionValidationIssue[];
52
- /** Absolute path to the source file this component was extracted from. Null for synthesized / multi-file. */
53
- sourcePath?: string;
54
- }
55
- export interface ComponentExtractionResult {
56
- components: RawComponentDefinition[];
57
- warnings: string[];
58
- }
59
- export type ExtractorProgress = {
60
- filesProcessed: number;
61
- componentsFound: number;
62
- };
63
- /**
64
- * Optional extraction-time settings forwarded from the CLI through the
65
- * pipeline to each extractor. Only the Svelte extractor currently consumes
66
- * any of these; other extractors ignore the value.
67
- */
68
- export interface ExtractorOptions {
69
- /**
70
- * Whether the Svelte extractor should run a retry pass for components whose
71
- * declared Props type couldn't be resolved on the first pass (cross-package
72
- * extends, path-alias-only types, etc.). See svelte.ts for the policy.
73
- */
74
- resolveUnreachable?: 'auto' | 'always' | 'never';
75
- /** Absolute project root — used by the retry pass to locate tsconfig.json and node_modules. */
76
- projectRoot?: string;
77
- }
78
- export interface ComponentExtractor {
79
- name: string;
80
- fileFilter: (filePath: string) => boolean;
81
- extract(filePaths: string[], onProgress?: (p: ExtractorProgress) => void, opts?: ExtractorOptions): Promise<ComponentExtractionResult>;
82
- }
83
11
  export interface TokenExtractor {
84
12
  name: string;
85
13
  extract(projectRoot: string): Promise<RawTokenDefinition[]>;
86
14
  }
87
- /** Strip internal scoring fields before serialising a RawComponentDefinition for display or editing. */
88
- export declare function stripScoringFields({ extractionConfidence: _c, reviewReasons: _r, needsReview: _n, ...rest }: RawComponentDefinition): Omit<RawComponentDefinition, 'extractionConfidence' | 'reviewReasons' | 'needsReview'>;
package/dist/src/types.js CHANGED
@@ -1,4 +1 @@
1
- /** Strip internal scoring fields before serialising a RawComponentDefinition for display or editing. */
2
- export function stripScoringFields({ extractionConfidence: _c, reviewReasons: _r, needsReview: _n, ...rest }) {
3
- return rest;
4
- }
1
+ export { stripScoringFields } from '@contentful/experience-design-system-extraction';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentful/experience-design-system-cli",
3
- "version": "2.12.4-dev-build-ff57340.0",
3
+ "version": "2.13.1-dev-build-847dead.0",
4
4
  "description": "Contentful Experiences design system import CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,16 +28,13 @@
28
28
  "skills/"
29
29
  ],
30
30
  "dependencies": {
31
- "@vue/compiler-sfc": "^3.5.31",
32
31
  "commander": "^13.1.0",
33
32
  "ink": "^4.4.1",
34
33
  "react": "^18.3.1",
35
34
  "react-devtools-core": "^4.19.1",
36
35
  "react-dom": "^18.3.1",
37
- "svelte": "^5.56.4",
38
- "ts-morph": "^27.0.2",
39
- "typescript": "^5.9.3",
40
- "@contentful/experience-design-system-types": "2.12.4-dev-build-ff57340.0"
36
+ "@contentful/experience-design-system-extraction": "2.13.1-dev-build-847dead.0",
37
+ "@contentful/experience-design-system-types": "2.13.1-dev-build-847dead.0"
41
38
  },
42
39
  "devDependencies": {
43
40
  "@tsconfig/node24": "^24.0.3",
@@ -1,93 +0,0 @@
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[];
@@ -1,326 +0,0 @@
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,5 +0,0 @@
1
- import type { ComponentExtractionResult } from '../../types.js';
2
- export declare function extractAstroComponents(filePaths: string[], onProgress?: (p: {
3
- filesProcessed: number;
4
- componentsFound: number;
5
- }) => void): Promise<ComponentExtractionResult>;