@contentful/experience-design-system-cli 2.14.2 → 2.14.3-dev-build-5487975.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/README.md +64 -2
- package/dist/package.json +3 -2
- package/dist/src/analyze/command.d.ts +11 -0
- package/dist/src/analyze/command.js +298 -3
- package/dist/src/analyze/composite-closure.d.ts +25 -0
- package/dist/src/analyze/composite-closure.js +0 -0
- package/dist/src/analyze/composition/agent-parser/author-prompt.d.ts +4 -0
- package/dist/src/analyze/composition/agent-parser/author-prompt.js +22 -0
- package/dist/src/analyze/composition/agent-parser/extract-parser.d.ts +8 -0
- package/dist/src/analyze/composition/agent-parser/extract-parser.js +44 -0
- package/dist/src/analyze/composition/agent-parser/load-prompt.d.ts +7 -0
- package/dist/src/analyze/composition/agent-parser/load-prompt.js +31 -0
- package/dist/src/analyze/composition/agent-parser/resolve-via-parser.d.ts +46 -0
- package/dist/src/analyze/composition/agent-parser/resolve-via-parser.js +88 -0
- package/dist/src/analyze/composition/agent-parser/sandbox.d.ts +45 -0
- package/dist/src/analyze/composition/agent-parser/sandbox.js +187 -0
- package/dist/src/analyze/composition/apply-mapping.d.ts +22 -0
- package/dist/src/analyze/composition/apply-mapping.js +67 -0
- package/dist/src/analyze/composition/candidate-critic-agent.d.ts +7 -0
- package/dist/src/analyze/composition/candidate-critic-agent.js +39 -0
- package/dist/src/analyze/composition/candidate-critic.d.ts +32 -0
- package/dist/src/analyze/composition/candidate-critic.js +63 -0
- package/dist/src/analyze/composition/candidate-files.d.ts +34 -0
- package/dist/src/analyze/composition/candidate-files.js +131 -0
- package/dist/src/analyze/composition/composition-cache-key.d.ts +19 -0
- package/dist/src/analyze/composition/composition-cache-key.js +19 -0
- package/dist/src/analyze/composition/interchange-schema.d.ts +37 -0
- package/dist/src/analyze/composition/interchange-schema.js +61 -0
- package/dist/src/analyze/composition/merge-edges.d.ts +19 -0
- package/dist/src/analyze/composition/merge-edges.js +50 -0
- package/dist/src/analyze/composition/parse-map-edges.d.ts +16 -0
- package/dist/src/analyze/composition/parse-map-edges.js +55 -0
- package/dist/src/analyze/composition/resolve-mapping-cli.d.ts +23 -0
- package/dist/src/analyze/composition/resolve-mapping-cli.js +29 -0
- package/dist/src/analyze/composition/resolve-mapping.d.ts +59 -0
- package/dist/src/analyze/composition/resolve-mapping.js +94 -0
- package/dist/src/analyze/cycle-detection.d.ts +27 -0
- package/dist/src/analyze/cycle-detection.js +234 -0
- package/dist/src/analyze/cycle-view.d.ts +7 -0
- package/dist/src/analyze/cycle-view.js +28 -0
- package/dist/src/analyze/fuzzy-search.d.ts +3 -0
- package/dist/src/analyze/fuzzy-search.js +55 -0
- package/dist/src/analyze/issue-inheritance.d.ts +12 -0
- package/dist/src/analyze/issue-inheritance.js +108 -0
- package/dist/src/analyze/lineage.d.ts +29 -0
- package/dist/src/analyze/lineage.js +0 -0
- package/dist/src/analyze/scope-gate-cascade.d.ts +11 -0
- package/dist/src/analyze/scope-gate-cascade.js +170 -0
- package/dist/src/analyze/search-neighborhood.d.ts +3 -0
- package/dist/src/analyze/search-neighborhood.js +56 -0
- package/dist/src/analyze/select/tui/App.js +5 -16
- package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +0 -4
- package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +3 -15
- package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +29 -38
- package/dist/src/analyze/select/tui/components/FieldEditor.js +309 -185
- package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +13 -1
- package/dist/src/analyze/select/tui/components/FinalizeDialog.js +23 -2
- package/dist/src/analyze/select/tui/components/GotoBanner.d.ts +31 -0
- package/dist/src/analyze/select/tui/components/GotoBanner.js +30 -0
- package/dist/src/analyze/select/tui/components/GroupedSidebar.d.ts +91 -0
- package/dist/src/analyze/select/tui/components/GroupedSidebar.js +497 -0
- package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +13 -1
- package/dist/src/analyze/select/tui/components/HelpOverlay.js +9 -3
- package/dist/src/analyze/select/tui/components/JsonEditor.js +3 -3
- package/dist/src/analyze/select/tui/components/LineagePanel.d.ts +14 -0
- package/dist/src/analyze/select/tui/components/LineagePanel.js +22 -0
- package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +0 -7
- package/dist/src/analyze/select/tui/components/RationalePanel.js +3 -15
- package/dist/src/analyze/select/tui/components/Sidebar.d.ts +0 -6
- package/dist/src/analyze/select/tui/components/Sidebar.js +11 -27
- package/dist/src/analyze/select/tui/components/StatusBar.js +2 -1
- package/dist/src/analyze/select/tui/components/removed-components-text.d.ts +7 -0
- package/dist/src/analyze/select/tui/components/removed-components-text.js +11 -0
- package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +1 -5
- package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +8 -10
- package/dist/src/analyze/select/tui/theme.d.ts +15 -0
- package/dist/src/analyze/select/tui/theme.js +28 -0
- package/dist/src/analyze/selection-cascade.d.ts +3 -0
- package/dist/src/analyze/selection-cascade.js +19 -0
- package/dist/src/analyze/slot-graph.d.ts +11 -0
- package/dist/src/analyze/slot-graph.js +17 -0
- package/dist/src/apply/api-client.d.ts +2 -1
- package/dist/src/apply/api-client.js +40 -6
- package/dist/src/apply/command.d.ts +17 -1
- package/dist/src/apply/command.js +69 -11
- package/dist/src/apply/error-parser.d.ts +16 -0
- package/dist/src/apply/error-parser.js +117 -0
- package/dist/src/apply/tui/ServerPreviewView.js +3 -1
- package/dist/src/credentials-store.d.ts +6 -0
- package/dist/src/credentials-store.js +5 -1
- package/dist/src/generate/agent-runner.d.ts +6 -0
- package/dist/src/generate/agent-runner.js +24 -8
- package/dist/src/import/command.js +69 -31
- package/dist/src/import/cycle-auto-reject.d.ts +4 -0
- package/dist/src/import/cycle-auto-reject.js +14 -0
- package/dist/src/import/orchestrator.d.ts +20 -7
- package/dist/src/import/orchestrator.js +139 -35
- package/dist/src/import/strip-allowed-components.d.ts +18 -0
- package/dist/src/import/strip-allowed-components.js +22 -0
- package/dist/src/import/tui/CustomPromptBanner.d.ts +0 -5
- package/dist/src/import/tui/CustomPromptBanner.js +2 -1
- package/dist/src/import/tui/WizardApp.d.ts +10 -74
- package/dist/src/import/tui/WizardApp.js +234 -246
- package/dist/src/import/tui/ai-flag.d.ts +5 -0
- package/dist/src/import/tui/ai-flag.js +6 -0
- package/dist/src/import/tui/autocomplete.d.ts +5 -0
- package/dist/src/import/tui/autocomplete.js +32 -0
- package/dist/src/import/tui/components/AutoFilterBanner.d.ts +10 -0
- package/dist/src/import/tui/components/AutoFilterBanner.js +19 -0
- package/dist/src/import/tui/components/CounterStrip.d.ts +12 -0
- package/dist/src/import/tui/components/CounterStrip.js +12 -0
- package/dist/src/import/tui/components/LegendEntry.d.ts +2 -0
- package/dist/src/import/tui/components/LegendEntry.js +6 -0
- package/dist/src/import/tui/cycle-panel-scroll.d.ts +8 -0
- package/dist/src/import/tui/cycle-panel-scroll.js +33 -0
- package/dist/src/import/tui/final-review-host.d.ts +3 -7
- package/dist/src/import/tui/final-review-host.js +9 -4
- package/dist/src/import/tui/group-collapse.d.ts +2 -0
- package/dist/src/import/tui/group-collapse.js +11 -0
- package/dist/src/import/tui/history.d.ts +22 -0
- package/dist/src/import/tui/history.js +53 -0
- package/dist/src/import/tui/hooks/useLineage.d.ts +32 -0
- package/dist/src/import/tui/hooks/useLineage.js +50 -0
- package/dist/src/import/tui/hooks/useOverlayPanel.d.ts +13 -0
- package/dist/src/import/tui/hooks/useOverlayPanel.js +27 -0
- package/dist/src/import/tui/lineage-layout.d.ts +75 -0
- package/dist/src/import/tui/lineage-layout.js +82 -0
- package/dist/src/import/tui/runLivePreview.d.ts +11 -0
- package/dist/src/import/tui/runLivePreview.js +7 -1
- package/dist/src/import/tui/scope-gate-columns.d.ts +31 -0
- package/dist/src/import/tui/scope-gate-columns.js +94 -0
- package/dist/src/import/tui/scope-gate-host.d.ts +3 -1
- package/dist/src/import/tui/scope-gate-host.js +11 -3
- package/dist/src/import/tui/sidebar-width.d.ts +1 -0
- package/dist/src/import/tui/sidebar-width.js +3 -0
- package/dist/src/import/tui/step-filters.d.ts +19 -0
- package/dist/src/import/tui/step-filters.js +42 -0
- package/dist/src/import/tui/steps/AtomicGenerateReviewStep.d.ts +42 -0
- package/dist/src/import/tui/steps/AtomicGenerateReviewStep.js +726 -0
- package/dist/src/import/tui/steps/AtomicScopeGateStep.d.ts +3 -0
- package/dist/src/import/tui/steps/AtomicScopeGateStep.js +211 -0
- package/dist/src/import/tui/steps/CredentialsStep.js +5 -18
- package/dist/src/import/tui/steps/DoneStep.d.ts +0 -2
- package/dist/src/import/tui/steps/DoneStep.js +3 -2
- package/dist/src/import/tui/steps/ErrorStep.js +2 -1
- package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +19 -25
- package/dist/src/import/tui/steps/GenerateReviewStep.js +1258 -229
- package/dist/src/import/tui/steps/PathValidationStep.js +3 -2
- package/dist/src/import/tui/steps/PreviewStep.js +2 -1
- package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +0 -7
- package/dist/src/import/tui/steps/PushDecisionGateStep.js +3 -6
- package/dist/src/import/tui/steps/PushingStep.js +2 -1
- package/dist/src/import/tui/steps/RunningStep.d.ts +4 -1
- package/dist/src/import/tui/steps/RunningStep.js +3 -2
- package/dist/src/import/tui/steps/ScopeGateStep.d.ts +24 -0
- package/dist/src/import/tui/steps/ScopeGateStep.js +850 -193
- package/dist/src/import/tui/steps/TokenInputStep.js +2 -1
- package/dist/src/import/tui/steps/WelcomeStep.js +2 -1
- package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +6 -0
- package/dist/src/import/tui/steps/WizardPreviewStep.js +95 -56
- package/dist/src/import/tui/steps/auto-reject-decision.d.ts +7 -0
- package/dist/src/import/tui/steps/auto-reject-decision.js +9 -0
- package/dist/src/import/tui/steps/breaking-change-format.d.ts +2 -0
- package/dist/src/import/tui/steps/breaking-change-format.js +24 -0
- package/dist/src/import/tui/steps/enumerate-cycle-breaks.d.ts +14 -0
- package/dist/src/import/tui/steps/enumerate-cycle-breaks.js +0 -0
- package/dist/src/import/tui/steps/preview-diff.js +48 -5
- package/dist/src/import/tui/useFinalizePreview.d.ts +34 -0
- package/dist/src/import/tui/useFinalizePreview.js +0 -0
- package/dist/src/import/tui/useLivePreview.d.ts +3 -0
- package/dist/src/import/tui/useLivePreview.js +1 -0
- package/dist/src/import/tui/wizard-state-transitions.d.ts +5 -59
- package/dist/src/import/tui/wizard-state-transitions.js +5 -60
- package/dist/src/lib/composition-mode.d.ts +28 -0
- package/dist/src/lib/composition-mode.js +37 -0
- package/dist/src/lib/prompt-overrides.d.ts +38 -0
- package/dist/src/lib/prompt-overrides.js +69 -0
- package/dist/src/print/command.js +19 -1
- package/dist/src/runs/modify-launcher.d.ts +3 -0
- package/dist/src/runs/modify-launcher.js +2 -0
- package/dist/src/runs/replay-helpers.js +1 -0
- package/dist/src/runs/run-picker.js +2 -18
- package/dist/src/runs/store.d.ts +4 -0
- package/dist/src/session/db.d.ts +23 -40
- package/dist/src/session/db.js +110 -138
- package/dist/src/setup/composition-mode-prompt.d.ts +14 -0
- package/dist/src/setup/composition-mode-prompt.js +37 -0
- package/package.json +5 -4
- package/prompts/composition-dir-critic.md +7 -0
- package/prompts/composition-edges.md +8 -0
- package/prompts/composition-parser-repair-empty.md +1 -0
- package/prompts/composition-parser-repair-error.md +2 -0
- package/prompts/composition-parser.md +42 -0
package/README.md
CHANGED
|
@@ -34,6 +34,46 @@ All intermediate data flows through a local SQLite session database (`~/.content
|
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
37
|
+
## Composite components & composition
|
|
38
|
+
|
|
39
|
+
A **composite component** is one that renders other components inside it — a `Card` that slots a `Button` and an `Icon`, a `Tabs` that slots `Tab` panels. When you import composite components, the CLI can populate each slot's `$allowedComponents` so the parent→child relationships survive into Contentful.
|
|
40
|
+
|
|
41
|
+
### Atomic vs. composite
|
|
42
|
+
|
|
43
|
+
Imports are **atomic by default** — flat components, no embedded hierarchy. This is the right choice when you just want each component registered on its own. Opt into hierarchy resolution with `--composite` (or any composition flag, which implies it).
|
|
44
|
+
|
|
45
|
+
| Mode | Flag | Behavior |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| Atomic | `--atomic` (default) | Flat import; composition stripped before push |
|
|
48
|
+
| Composite | `--composite` | Resolve and import the parent→child hierarchy |
|
|
49
|
+
|
|
50
|
+
### How composition is resolved
|
|
51
|
+
|
|
52
|
+
Under `--composite`, relationships are resolved from the highest-confidence source available, in this precedence order:
|
|
53
|
+
|
|
54
|
+
1. **Typed slots (code)** — slots the source already declares, e.g. React `ReactElement<XProps>` / `children`, Svelte `Snippet<[XProps]>`, or an explicit `@allowedComponents` JSDoc tag. Fully deterministic; picked up automatically.
|
|
55
|
+
2. **Mapping map** — a hand-authored parent→children interchange map you feed with `--composition-map <path>`. Use `--generate-map <path>` to emit a skeleton from whatever was resolved, then hand-edit it and feed it back.
|
|
56
|
+
3. **Agent (`--composition-agent`)** — for codebases that encode composition in *code patterns* rather than typed slots (common in real-world design systems). Only runs when the deterministic sources above find nothing.
|
|
57
|
+
|
|
58
|
+
When more than one source speaks to the same relationship, the higher-precedence one wins (**code slots > map > agent**).
|
|
59
|
+
|
|
60
|
+
### The composition agent
|
|
61
|
+
|
|
62
|
+
`--composition-agent` doesn't ask the model to *list* relationships (which would be non-deterministic and unauditable). Instead the agent **writes a small parser** — a pure `(ctx) => Edge[]` function — which the CLI runs in a locked-down sandbox (separate process, no filesystem/network, memory + wall-clock limits), caches by the parser's own source, and replays. Repeated runs over the same code are stable and inspectable.
|
|
63
|
+
|
|
64
|
+
- `--composition-agent-mode <parser|edges>` — `parser` (default; the sandboxed-parser design above) or `edges` (agent lists relationships directly; less robust).
|
|
65
|
+
- `--composition-refresh` — ignore the cache and re-resolve from scratch, forcing the agent to run.
|
|
66
|
+
- `--agent <name>` — which coding agent authors the parser (`claude`, `codex`, `opencode`, `cursor`).
|
|
67
|
+
- `--prompt composition=<file-or-text>` — override the composition stage's prompt.
|
|
68
|
+
|
|
69
|
+
Because the agent path spawns a coding agent, it adds latency and cost and is best-effort (parser quality can vary run to run). For reproducible results, prefer `--composition-map`.
|
|
70
|
+
|
|
71
|
+
### Slot cycles
|
|
72
|
+
|
|
73
|
+
If the resolved graph contains a circular slot dependency (A slots B, B slots A), it is detected before push. `apply push` and the wizard's push path **refuse to send a manifest with cycles**; the cycle path is reported so you can break it. In headless `import`, pass `--auto-reject-cycles` to auto-reject the components in a cycle and retry instead of failing.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
37
77
|
## Prerequisites
|
|
38
78
|
|
|
39
79
|
### Coding agent
|
|
@@ -80,7 +120,8 @@ experiences import [flags]
|
|
|
80
120
|
```
|
|
81
121
|
welcome
|
|
82
122
|
↓
|
|
83
|
-
extracting — runs analyze extract
|
|
123
|
+
extracting — runs analyze extract (atomic by default; resolves composition
|
|
124
|
+
under --composite, see below); spawns generate in parallel (prefetch)
|
|
84
125
|
↓
|
|
85
126
|
[auto-filter] — analyze select-agent runs automatically (skip with --no-auto-filter)
|
|
86
127
|
↓
|
|
@@ -139,6 +180,15 @@ Pass `--select-prompt-path <path>` and/or `--generate-prompt-path <path>` to swa
|
|
|
139
180
|
| `--model <name>` | agent default | Model name |
|
|
140
181
|
| `--tokens <path>` | — | DTCG `tokens.json` to push alongside generated components |
|
|
141
182
|
| `--auto-accept-scope` | off | Accept all extracted components without prompting (required for non-TTY without other headless flags) |
|
|
183
|
+
| `--atomic` | **default** | Flat import, no embedded-component hierarchy (composition stripped on push) |
|
|
184
|
+
| `--composite` | — | Import the embedded-component hierarchy (any composition flag implies this) |
|
|
185
|
+
| `--composition-map <path>` | — | Consume a hand-authored parent→children interchange map (implies `--composite`) |
|
|
186
|
+
| `--generate-map <path>` | — | Also write a composition-map skeleton from the resolved composition (implies `--composite`) |
|
|
187
|
+
| `--composition-agent` | — | Opt into agentic resolution when deterministic sources find no groups (implies `--composite`) |
|
|
188
|
+
| `--composition-refresh` | — | Bypass the composition cache and re-resolve from scratch, forcing the agent to run (implies `--composite`) |
|
|
189
|
+
| `--composition-agent-mode <mode>` | `parser` | `parser` (agent writes a sandboxed parser) or `edges` (agent lists edges directly) |
|
|
190
|
+
| `--prompt <stage=value>` | — | Override a stage prompt (repeatable); value is a file path or literal text, e.g. `--prompt composition=./p.md` |
|
|
191
|
+
| `--auto-reject-cycles` | off (fail loud) | Auto-reject components in slot cycles and retry, instead of stopping with the cycle path |
|
|
142
192
|
| `--auto-filter` / `--no-auto-filter` | persisted in `credentials.json` | Force AI auto-filter on or off; overrides saved preference |
|
|
143
193
|
| `--no-live-preview` | live preview on | Skip the automatic preview re-run after each FieldEditor save |
|
|
144
194
|
| `--no-push` | push on | Run extract → scope-gate → generate → final-review and exit without pushing |
|
|
@@ -224,13 +274,23 @@ The standalone subcommands below are pinned by snapshot test (`test/analyze/sele
|
|
|
224
274
|
Extract component definitions from a project source tree.
|
|
225
275
|
|
|
226
276
|
```bash
|
|
227
|
-
experiences analyze extract --project <path> [--dir <src-dir>]
|
|
277
|
+
experiences analyze extract --project <path> [--dir <src-dir>] [composition flags]
|
|
228
278
|
```
|
|
229
279
|
|
|
230
280
|
| Option | Default | Description |
|
|
231
281
|
|---|---|---|
|
|
232
282
|
| `--project <path>` | _(required)_ | Path to the project root |
|
|
233
283
|
| `--dir <path>` | `src` (falls back to project root) | Source directory relative to project root |
|
|
284
|
+
| `--resolve-unreachable <mode>` | `auto` | Retry pass for unresolved Svelte `Props` types: `auto`, `always`, or `never` |
|
|
285
|
+
| `--atomic` | **default** | Skip composition resolution — flat components only |
|
|
286
|
+
| `--composite` | — | Resolve embedded-component composition (any composition flag implies this) |
|
|
287
|
+
| `--composition-map <path>` | — | Consume a hand-authored parent→children interchange map (implies `--composite`) |
|
|
288
|
+
| `--generate-map <path>` | — | Write a skeleton interchange map from the resolved composition (implies `--composite`) |
|
|
289
|
+
| `--composition-agent` | — | Opt into agentic resolution when deterministic sources find no groups (implies `--composite`) |
|
|
290
|
+
| `--composition-refresh` | — | Bypass the composition cache and re-resolve from scratch, forcing the agent to run (implies `--composite`) |
|
|
291
|
+
| `--composition-agent-mode <mode>` | `parser` | `parser` (agent writes a sandboxed parser — deterministic) or `edges` (agent lists edges directly) |
|
|
292
|
+
| `--agent <name>` | saved by setup | Coding agent for composition resolution: `claude`, `codex`, `opencode`, `cursor` |
|
|
293
|
+
| `--prompt <stage=value>` | — | Override a stage prompt (repeatable); value is a file path or literal text, e.g. `--prompt composition=./p.md` |
|
|
234
294
|
|
|
235
295
|
Scans `.tsx`, `.ts`, `.jsx`, `.js`, `.vue`, and `.astro` files. Ignores `node_modules`, `dist`, `build`, `.next`, `.nuxt`, `coverage`, `storybook-static`, `out`, `demo(s)`, and `example(s)` directories. Also ignores `*.stories.*`, `*.story.*`, `*.spec.*`, and `*.test.*` files.
|
|
236
296
|
|
|
@@ -238,6 +298,8 @@ Writes extracted components to the session database and prints `session=<id>` to
|
|
|
238
298
|
|
|
239
299
|
The deterministic non-authorable filter drops infrastructure components with no authoring surface (Context providers, refs-only wrappers, etc.); each drop is reported as a warning so the operator can audit.
|
|
240
300
|
|
|
301
|
+
Extraction is **atomic by default** — flat components, no embedded hierarchy. See [Composite components & composition](#composite-components--composition) for how `--composite` and the composition flags resolve parent→child relationships.
|
|
302
|
+
|
|
241
303
|
---
|
|
242
304
|
|
|
243
305
|
### `analyze select`
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentful/experience-design-system-cli",
|
|
3
|
-
"version": "2.14.
|
|
3
|
+
"version": "2.14.3-dev-build-5487975.0",
|
|
4
4
|
"description": "Contentful Experiences design system import CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"files": [
|
|
26
26
|
"bin/",
|
|
27
27
|
"dist/",
|
|
28
|
-
"skills/"
|
|
28
|
+
"skills/",
|
|
29
|
+
"prompts/"
|
|
29
30
|
],
|
|
30
31
|
"scripts": {
|
|
31
32
|
"build": "nx build experience-design-system-cli && BINDIR=$(dirname $(which node)) && for cmd in exo experiences experience-design-system-cli; do ln -sf \"$(pwd)/bin/cli.js\" \"$BINDIR/$cmd\"; done",
|
|
@@ -1,3 +1,14 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
+
import type { InterchangeMap } from './composition/interchange-schema.js';
|
|
3
|
+
import type { RawSlotDefinition } from '../types.js';
|
|
2
4
|
export declare function collectSourceFiles(directory: string, onProgress?: (scannedCount: number) => void): Promise<string[]>;
|
|
5
|
+
/**
|
|
6
|
+
* Build a { version, groups } interchange skeleton (spec T1) from the resolved
|
|
7
|
+
* components' slot allowedComponents — reflecting BOTH typed-slot edges the
|
|
8
|
+
* extractor found and anything the mapping resolver added.
|
|
9
|
+
*/
|
|
10
|
+
export declare function componentsToInterchangeMap(components: Array<{
|
|
11
|
+
name: string;
|
|
12
|
+
slots: RawSlotDefinition[];
|
|
13
|
+
}>): InterchangeMap;
|
|
3
14
|
export declare function registerAnalyzeCommand(program: Command): void;
|
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
import { createElement } from 'react';
|
|
2
2
|
import { render } from 'ink';
|
|
3
|
-
import { mkdir, readdir, stat } from 'node:fs/promises';
|
|
3
|
+
import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
5
|
import { extractComponents, preClassifyComponent, isNonAuthorableComponent, computeExtractionScore, deriveNeedsReview, describeReviewReasons, inspectComponentSource, validateExtractedComponents, } from '@contentful/experience-design-system-extraction';
|
|
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, getCliCacheVersion, lookupCompositionCache, storeCompositionCache, } from '../session/db.js';
|
|
10
|
+
import { findSlotCycles, suggestCycleBreakEdge } from './cycle-detection.js';
|
|
11
|
+
import { resolveCompositionMode } from '../lib/composition-mode.js';
|
|
12
|
+
import { resolveMapping } from './composition/resolve-mapping.js';
|
|
13
|
+
import { loadUserMap, resolveCompositionSources } from './composition/resolve-mapping-cli.js';
|
|
14
|
+
import { selectCandidateFiles, capCandidatesToPromptBudget } from './composition/candidate-files.js';
|
|
15
|
+
import { critiqueCandidates } from './composition/candidate-critic.js';
|
|
16
|
+
import { buildDirCriticPrompt, parseDirCriticReply } from './composition/candidate-critic-agent.js';
|
|
17
|
+
import { buildCompositionInputHash } from './composition/composition-cache-key.js';
|
|
18
|
+
import { runParserInSandbox } from './composition/agent-parser/sandbox.js';
|
|
19
|
+
import { resolveViaAgentParser } from './composition/agent-parser/resolve-via-parser.js';
|
|
20
|
+
import { parsePromptOverrides, resolvePromptOverride } from '../lib/prompt-overrides.js';
|
|
21
|
+
import { runAgent } from '../generate/agent-runner.js';
|
|
22
|
+
import { readExperiencesCredentials } from '../credentials-store.js';
|
|
10
23
|
import { buildAnalyzeViewRows, partitionGlobalWarnings } from './build-analyze-view-rows.js';
|
|
11
24
|
const SCANNED_FILE_EXTENSIONS = new Set(['.astro', '.js', '.jsx', '.svelte', '.ts', '.tsx', '.vue']);
|
|
12
25
|
const IGNORED_DIRECTORY_NAMES = new Set([
|
|
@@ -85,6 +98,69 @@ export async function collectSourceFiles(directory, onProgress) {
|
|
|
85
98
|
await visit(directory);
|
|
86
99
|
return files.sort();
|
|
87
100
|
}
|
|
101
|
+
/** Read the persisted default composition mode; missing config is fine. */
|
|
102
|
+
async function safeReadCompositionMode() {
|
|
103
|
+
try {
|
|
104
|
+
return (await readExperiencesCredentials()).compositionMode;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Build a { version, groups } interchange skeleton (spec T1) from the resolved
|
|
112
|
+
* components' slot allowedComponents — reflecting BOTH typed-slot edges the
|
|
113
|
+
* extractor found and anything the mapping resolver added.
|
|
114
|
+
*/
|
|
115
|
+
export function componentsToInterchangeMap(components) {
|
|
116
|
+
const groups = {};
|
|
117
|
+
for (const c of components) {
|
|
118
|
+
const children = new Set();
|
|
119
|
+
for (const slot of c.slots) {
|
|
120
|
+
for (const child of slot.allowedComponents ?? [])
|
|
121
|
+
children.add(child);
|
|
122
|
+
}
|
|
123
|
+
if (children.size > 0)
|
|
124
|
+
groups[c.name] = [...children].sort();
|
|
125
|
+
}
|
|
126
|
+
const sorted = {};
|
|
127
|
+
for (const parent of Object.keys(groups).sort())
|
|
128
|
+
sorted[parent] = groups[parent];
|
|
129
|
+
return { version: 1, groups: sorted };
|
|
130
|
+
}
|
|
131
|
+
/** Resolve which coding-agent runs mapping resolution: `--agent` flag > env > default. */
|
|
132
|
+
function resolveCompositionAgentName(flagValue) {
|
|
133
|
+
const valid = ['claude', 'codex', 'opencode', 'cursor'];
|
|
134
|
+
if (flagValue && valid.includes(flagValue))
|
|
135
|
+
return flagValue;
|
|
136
|
+
const env = process.env['EDS_COMPOSITION_AGENT'];
|
|
137
|
+
if (env && valid.includes(env))
|
|
138
|
+
return env;
|
|
139
|
+
return 'claude';
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Read the source files of the extracted components plus nearby mapping/meta
|
|
143
|
+
* files, so the candidate pre-filter (T3) can pick the relevant ones. Reads
|
|
144
|
+
* each unique `sourcePath` once; missing files are skipped.
|
|
145
|
+
*/
|
|
146
|
+
async function readCandidateFiles(components, extraFiles = []) {
|
|
147
|
+
const paths = new Set(extraFiles);
|
|
148
|
+
for (const c of components) {
|
|
149
|
+
if (c.sourcePath)
|
|
150
|
+
paths.add(c.sourcePath);
|
|
151
|
+
}
|
|
152
|
+
const out = [];
|
|
153
|
+
await Promise.all([...paths].map(async (p) => {
|
|
154
|
+
try {
|
|
155
|
+
const content = await readFile(p, 'utf8');
|
|
156
|
+
out.push({ path: p, content });
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
void 0;
|
|
160
|
+
}
|
|
161
|
+
}));
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
88
164
|
export function registerAnalyzeCommand(program) {
|
|
89
165
|
const analyze = program
|
|
90
166
|
.command('analyze')
|
|
@@ -95,6 +171,15 @@ export function registerAnalyzeCommand(program) {
|
|
|
95
171
|
.requiredOption('--project <path>', 'Path to the project root')
|
|
96
172
|
.option('--dir <path>', 'Path to the component source directory relative to the project root')
|
|
97
173
|
.option('--resolve-unreachable <mode>', "Retry pass for unresolved Svelte Props types: 'auto' (default), 'always', or 'never'", 'auto')
|
|
174
|
+
.option('--composite', 'Resolve embedded-component composition (opt in; default is atomic)')
|
|
175
|
+
.option('--atomic', 'Skip composition resolution — flat components only (default)')
|
|
176
|
+
.option('--composition-map <path>', 'Consume a hand-authored parent→children interchange map (implies --composite)')
|
|
177
|
+
.option('--composition-agent', 'Opt into agentic mapping resolution when deterministic sources find no groups (implies --composite)')
|
|
178
|
+
.option('--composition-refresh', 'Force the mapping agent to run even where deterministic sources answered (implies --composite)')
|
|
179
|
+
.option('--generate-map <path>', 'Write a skeleton interchange map from resolved composition (implies --composite)')
|
|
180
|
+
.option('--prompt <stage=value>', 'Override a stage prompt (repeatable). value is a file path or literal text, e.g. --prompt composition=./p.md', (v, acc) => [...acc, v], [])
|
|
181
|
+
.option('--agent <name>', 'Coding agent for composition mapping resolution (claude|codex|opencode|cursor)')
|
|
182
|
+
.option('--composition-agent-mode <mode>', "Agent resolution mode: 'parser' (agent writes a sandboxed parser — deterministic, default) or 'edges' (agent lists edges directly)", 'parser')
|
|
98
183
|
.action(async (opts) => {
|
|
99
184
|
const resolveUnreachable = (() => {
|
|
100
185
|
const v = opts.resolveUnreachable ?? 'auto';
|
|
@@ -184,8 +269,218 @@ export function registerAnalyzeCommand(program) {
|
|
|
184
269
|
(component.needsReview ?? false),
|
|
185
270
|
});
|
|
186
271
|
}
|
|
187
|
-
|
|
272
|
+
let validatedComponents = validateExtractedComponents(filteredComponents);
|
|
273
|
+
// Composition mapping resolution (spec U2). Only in composite mode and
|
|
274
|
+
// only when a source is provided (user map / agent opt-in).
|
|
275
|
+
// Atomic (default) never resolves — it would only be stripped later.
|
|
276
|
+
const compositionMode = resolveCompositionMode(opts, (await safeReadCompositionMode()) ?? undefined);
|
|
277
|
+
if (compositionMode === 'composite') {
|
|
278
|
+
const sources = resolveCompositionSources(opts);
|
|
279
|
+
const { overrides: promptOverrides, errors: promptErrors } = parsePromptOverrides(opts.prompt ?? []);
|
|
280
|
+
for (const err of promptErrors) {
|
|
281
|
+
process.stderr.write(`Error: ${err}\n`);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
let compositionPrompt;
|
|
285
|
+
const compositionOverride = promptOverrides.get('composition');
|
|
286
|
+
if (compositionOverride) {
|
|
287
|
+
try {
|
|
288
|
+
compositionPrompt = await resolvePromptOverride(compositionOverride);
|
|
289
|
+
}
|
|
290
|
+
catch (e) {
|
|
291
|
+
process.stderr.write(`Error: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
let userMap;
|
|
296
|
+
if (opts.compositionMap) {
|
|
297
|
+
const loaded = await loadUserMap(opts.compositionMap);
|
|
298
|
+
if (!loaded.ok) {
|
|
299
|
+
process.stderr.write(`Error: ${loaded.error}\n`);
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
userMap = loaded.map;
|
|
303
|
+
}
|
|
304
|
+
const hasSource = !!userMap || sources.useAgent || sources.forceAgent;
|
|
305
|
+
if (hasSource || opts.generateMap) {
|
|
306
|
+
// Composition progress mirrors the scan/extract progress convention:
|
|
307
|
+
// emit `progress=composition:<phase>` on stderr so the wizard can
|
|
308
|
+
// render a second progress line during the (potentially slow, agent-
|
|
309
|
+
// backed) resolution instead of appearing frozen.
|
|
310
|
+
const emitCompositionProgress = (phase) => {
|
|
311
|
+
if (!process.stdout.isTTY)
|
|
312
|
+
process.stderr.write(`progress=composition:${phase}\n`);
|
|
313
|
+
};
|
|
314
|
+
emitCompositionProgress('resolving');
|
|
315
|
+
const allFiles = await readCandidateFiles(validatedComponents, sourceFiles);
|
|
316
|
+
// Decouple the two file sets (design: candidate-heuristic fragility):
|
|
317
|
+
// - `promptFiles`: a bounded candidate SAMPLE inlined into the agent
|
|
318
|
+
// prompt so it sees the convention without ingesting the whole repo.
|
|
319
|
+
// - `allFiles`: EVERY scanned file, handed to the authored parser at
|
|
320
|
+
// runtime. The parser is deterministic code — give it everything so
|
|
321
|
+
// a candidate-filter miss can never starve it of a definition file.
|
|
322
|
+
const selectedCandidates = selectCandidateFiles(allFiles).map((c) => ({ path: c.path, content: c.content }));
|
|
323
|
+
// Cap the inlined set so a large design system can't overflow the
|
|
324
|
+
// agent's context window and fail resolution outright (see the budget
|
|
325
|
+
// constant). The runtime parser still sees EVERY file.
|
|
326
|
+
const capped = capCandidatesToPromptBudget(selectedCandidates);
|
|
327
|
+
let promptFiles = capped.kept;
|
|
328
|
+
if (capped.dropped.length > 0) {
|
|
329
|
+
process.stderr.write(`Warning: composition — ${capped.dropped.length} candidate file(s) omitted from the agent prompt to fit the context budget; resolution runs on the ${promptFiles.length} highest-value files.\n`);
|
|
330
|
+
}
|
|
331
|
+
const runtimeFiles = allFiles.map((c) => ({ path: c.path, content: c.content }));
|
|
332
|
+
const resolverAgent = resolveCompositionAgentName(opts.agent);
|
|
333
|
+
const parserMode = (opts.compositionAgentMode ?? 'parser') !== 'edges';
|
|
334
|
+
const componentNameSet = new Set(validatedComponents.map((c) => c.name));
|
|
335
|
+
const cacheVersion = await getCliCacheVersion();
|
|
336
|
+
// Tracks the exit code of the most recent spawnAgent call so the
|
|
337
|
+
// caching sites can refuse to persist a failed run (a non-zero exit —
|
|
338
|
+
// e.g. a context-window overflow — otherwise poisons the cache and
|
|
339
|
+
// replays the error on every subsequent run).
|
|
340
|
+
let lastAgentExitCode = 0;
|
|
341
|
+
const spawnAgent = async (prompt) => {
|
|
342
|
+
const res = await runAgent({
|
|
343
|
+
agent: resolverAgent,
|
|
344
|
+
prompt,
|
|
345
|
+
interactive: false,
|
|
346
|
+
timeoutMs: 120_000,
|
|
347
|
+
promptViaStdin: true,
|
|
348
|
+
});
|
|
349
|
+
lastAgentExitCode = res.exitCode;
|
|
350
|
+
if (res.exitCode !== 0 && res.stderr.trim()) {
|
|
351
|
+
process.stderr.write(`Warning: composition — agent exited ${res.exitCode}: ${res.stderr.trim()}\n`);
|
|
352
|
+
}
|
|
353
|
+
return res.stdout;
|
|
354
|
+
};
|
|
355
|
+
// Completeness critic: let the agent flag composition-relevant dirs
|
|
356
|
+
// the keyword filter missed (by path/name alone — cheap). It can only
|
|
357
|
+
// ADD to the prompt sample. Runs ONLY on a genuine authoring pass (see
|
|
358
|
+
// the cache-miss branch below) so a parser cache hit pays nothing.
|
|
359
|
+
const runCandidateCritic = async () => {
|
|
360
|
+
const critic = await critiqueCandidates(runtimeFiles, promptFiles, async (dirs) => parseDirCriticReply(await spawnAgent(buildDirCriticPrompt(dirs)), dirs));
|
|
361
|
+
if (critic.addedDirs.length > 0) {
|
|
362
|
+
promptFiles = critic.files;
|
|
363
|
+
if (process.env['EDS_DEBUG']) {
|
|
364
|
+
process.stderr.write(`[composition-debug] critic added dirs: ${critic.addedDirs.join(', ')}\n`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
// Agent-authored parser path (default): the agent writes a sandboxed
|
|
369
|
+
// (ctx) => Edge[] parser we run deterministically, cached by parser
|
|
370
|
+
// SOURCE. Falls back to direct edge-emission if authoring fails.
|
|
371
|
+
let parserEdges;
|
|
372
|
+
if (sources.useAgent && parserMode) {
|
|
373
|
+
const parserCacheKey = buildCompositionInputHash({
|
|
374
|
+
files: runtimeFiles,
|
|
375
|
+
agent: resolverAgent,
|
|
376
|
+
kind: 'parser',
|
|
377
|
+
});
|
|
378
|
+
const cachedSource = opts.compositionRefresh
|
|
379
|
+
? null
|
|
380
|
+
: lookupCompositionCache(db, parserCacheKey, cacheVersion);
|
|
381
|
+
if (cachedSource !== null) {
|
|
382
|
+
emitCompositionProgress('cache-hit');
|
|
383
|
+
const ran = await runParserInSandbox(cachedSource, {
|
|
384
|
+
files: runtimeFiles,
|
|
385
|
+
componentNames: [...componentNameSet],
|
|
386
|
+
});
|
|
387
|
+
if (!ran.error) {
|
|
388
|
+
parserEdges = ran.edges.filter((e) => e.parent !== e.child && componentNameSet.has(e.parent) && componentNameSet.has(e.child));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (parserEdges === undefined) {
|
|
392
|
+
await runCandidateCritic();
|
|
393
|
+
const pr = await resolveViaAgentParser({
|
|
394
|
+
files: promptFiles,
|
|
395
|
+
runtimeFiles,
|
|
396
|
+
componentNames: componentNameSet,
|
|
397
|
+
runAgentFn: ({ prompt }) => spawnAgent(prompt),
|
|
398
|
+
...(compositionPrompt ? { instructionOverride: compositionPrompt } : {}),
|
|
399
|
+
onPhase: (phase) => emitCompositionProgress(phase),
|
|
400
|
+
// The candidate filter selected these files because they carry
|
|
401
|
+
// composition markers, so a clean 0-edge parse is suspicious —
|
|
402
|
+
// let the resolver spend its one repair round on it.
|
|
403
|
+
retryOnEmpty: promptFiles.length > 0,
|
|
404
|
+
});
|
|
405
|
+
for (const w of pr.warnings)
|
|
406
|
+
process.stderr.write(`Warning: composition — ${w}\n`);
|
|
407
|
+
if (!pr.usedFallback) {
|
|
408
|
+
parserEdges = pr.edges;
|
|
409
|
+
if (pr.parserSource && lastAgentExitCode === 0) {
|
|
410
|
+
storeCompositionCache(db, parserCacheKey, cacheVersion, pr.parserSource);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
process.stderr.write('Warning: composition — parser mode failed; falling back to edge emission\n');
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (process.env['EDS_DEBUG'] && parserEdges) {
|
|
419
|
+
process.stderr.write(`[composition-debug] prompt files: ${promptFiles.length}; runtime files: ${runtimeFiles.length}; componentNames: ${componentNameSet.size}; parser edges: ${parserEdges.length}\n`);
|
|
420
|
+
for (const e of parserEdges)
|
|
421
|
+
process.stderr.write(`[composition-debug] edge ${e.parent} -> ${e.child}\n`);
|
|
422
|
+
}
|
|
423
|
+
// Edge-emission cache (used for both explicit edges-mode and the
|
|
424
|
+
// parser-mode fallback). Keyed on prompt files + agent identity — the
|
|
425
|
+
// agent emits edges directly from what it reads in the prompt.
|
|
426
|
+
const agentCacheKey = buildCompositionInputHash({
|
|
427
|
+
files: promptFiles,
|
|
428
|
+
agent: resolverAgent,
|
|
429
|
+
kind: 'edges',
|
|
430
|
+
});
|
|
431
|
+
const useEdgeEmission = sources.useAgent && parserEdges === undefined;
|
|
432
|
+
const result = await resolveMapping({
|
|
433
|
+
components: validatedComponents,
|
|
434
|
+
...(userMap ? { userMap } : {}),
|
|
435
|
+
...(parserEdges ? { extraEdges: parserEdges } : {}),
|
|
436
|
+
useAgent: useEdgeEmission,
|
|
437
|
+
forceAgent: sources.forceAgent && useEdgeEmission,
|
|
438
|
+
files: promptFiles,
|
|
439
|
+
...(compositionPrompt ? { promptOverride: compositionPrompt } : {}),
|
|
440
|
+
runAgentFn: async ({ prompt }) => {
|
|
441
|
+
if (!opts.compositionRefresh) {
|
|
442
|
+
const cached = lookupCompositionCache(db, agentCacheKey, cacheVersion);
|
|
443
|
+
if (cached !== null) {
|
|
444
|
+
emitCompositionProgress('cache-hit');
|
|
445
|
+
return cached;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
emitCompositionProgress(`agent:${resolverAgent}`);
|
|
449
|
+
const stdout = await spawnAgent(prompt);
|
|
450
|
+
if (lastAgentExitCode === 0) {
|
|
451
|
+
storeCompositionCache(db, agentCacheKey, cacheVersion, stdout);
|
|
452
|
+
}
|
|
453
|
+
return stdout;
|
|
454
|
+
},
|
|
455
|
+
});
|
|
456
|
+
emitCompositionProgress('done');
|
|
457
|
+
for (const w of result.warnings)
|
|
458
|
+
process.stderr.write(`Warning: composition — ${w}\n`);
|
|
459
|
+
for (const c of result.conflicts) {
|
|
460
|
+
process.stderr.write(`Warning: composition conflict on ${c.parent}→${c.child}: kept ${c.winner}, dropped ${c.loser}\n`);
|
|
461
|
+
}
|
|
462
|
+
validatedComponents = result.components;
|
|
463
|
+
if (opts.generateMap) {
|
|
464
|
+
// Reflect the FULL resolved composition — typed-slot edges already
|
|
465
|
+
// on the extracted components PLUS anything the resolver added — not
|
|
466
|
+
// just the resolver's own contributed edges.
|
|
467
|
+
const skeleton = componentsToInterchangeMap(validatedComponents);
|
|
468
|
+
await writeFile(opts.generateMap, JSON.stringify(skeleton, null, 2) + '\n');
|
|
469
|
+
process.stderr.write(`Wrote composition map skeleton to ${opts.generateMap}\n`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
188
473
|
storeRawComponents(db, sessionId, validatedComponents);
|
|
474
|
+
const cycleInput = validatedComponents.map((c) => ({
|
|
475
|
+
name: c.name,
|
|
476
|
+
slots: c.slots.map((s) => ({ name: s.name, allowedComponents: s.allowedComponents })),
|
|
477
|
+
}));
|
|
478
|
+
const cycles = findSlotCycles(cycleInput);
|
|
479
|
+
const withBreaks = cycles.map((cycle) => ({
|
|
480
|
+
...cycle,
|
|
481
|
+
suggestedBreak: suggestCycleBreakEdge(cycle, cycles),
|
|
482
|
+
}));
|
|
483
|
+
storeSlotCycles(db, sessionId, withBreaks);
|
|
189
484
|
storeScannedFiles(db, sessionId, sourceFiles.map((f) => relative(projectRoot, f)));
|
|
190
485
|
updateStep(db, stepId, 'complete', { sessionId });
|
|
191
486
|
db.close();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ComponentGraphNode {
|
|
2
|
+
name: string;
|
|
3
|
+
slots: Array<{
|
|
4
|
+
name: string;
|
|
5
|
+
allowedComponents?: string[];
|
|
6
|
+
}>;
|
|
7
|
+
}
|
|
8
|
+
export interface ClosureNode {
|
|
9
|
+
name: string;
|
|
10
|
+
depth: number;
|
|
11
|
+
path: string[];
|
|
12
|
+
parents: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface Closure {
|
|
15
|
+
root: string;
|
|
16
|
+
nodes: ClosureNode[];
|
|
17
|
+
containsCycle: boolean;
|
|
18
|
+
cyclePath?: string[];
|
|
19
|
+
}
|
|
20
|
+
export type NodeStatus = 'ok' | 'warning' | 'error';
|
|
21
|
+
export declare function findRoots(components: ComponentGraphNode[], selected?: Set<string>): string[];
|
|
22
|
+
export declare function computeClosure(root: string, components: ComponentGraphNode[]): Closure;
|
|
23
|
+
export declare function computeAllClosures(components: ComponentGraphNode[], selected?: Set<string>): Map<string, Closure>;
|
|
24
|
+
export declare function aggregateStatus(closure: Closure, perNodeStatus: Map<string, NodeStatus>): NodeStatus;
|
|
25
|
+
export declare function findSharedDeps(closures: Map<string, Closure>): Map<string, string[]>;
|
|
Binary file
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { loadPrompt } from './load-prompt.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build the prompt that asks the agent to WRITE a pure parser function (spec:
|
|
4
|
+
* agent-authored-parser, Phase 2) rather than list edges. The static
|
|
5
|
+
* instruction + contract live in `prompts/composition-parser.md`; this
|
|
6
|
+
* appends the dynamic candidate files + component names. The authored parser
|
|
7
|
+
* runs in the sandbox (sandbox.ts), so the contract forbids all I/O.
|
|
8
|
+
*/
|
|
9
|
+
const PROMPT_FILE = 'composition-parser.md';
|
|
10
|
+
export function buildAuthorPrompt(files, componentNames, instructionOverride) {
|
|
11
|
+
const fileBlocks = files.map((f) => `--- ${f.path} ---\n${f.content}`).join('\n\n');
|
|
12
|
+
const instruction = instructionOverride?.trim() ? instructionOverride.trim() : loadPrompt(PROMPT_FILE).trim();
|
|
13
|
+
return [
|
|
14
|
+
instruction,
|
|
15
|
+
'',
|
|
16
|
+
'Component names (use ONLY these):',
|
|
17
|
+
componentNames.join(', '),
|
|
18
|
+
'',
|
|
19
|
+
'Candidate files:',
|
|
20
|
+
fileBlocks,
|
|
21
|
+
].join('\n');
|
|
22
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cheap structural check that source matches the parser contract:
|
|
3
|
+
* `export default` of a function (declaration or arrow) that takes at least
|
|
4
|
+
* one parameter. Not a full parse — just enough to reject JSON samples, prose,
|
|
5
|
+
* and no-arg functions before we spend a sandbox spawn on them.
|
|
6
|
+
*/
|
|
7
|
+
export declare function looksLikeParser(source: string): boolean;
|
|
8
|
+
export declare function extractParserSource(stdout: string): string | null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pull the parser function source out of the agent's stdout (spec:
|
|
3
|
+
* agent-authored-parser, Phase 2). Agents typically wrap code in a fenced
|
|
4
|
+
* block, but often emit a non-parser block first (a JSON sample, prose). We
|
|
5
|
+
* return the first block that is actually SHAPED like the contract — a
|
|
6
|
+
* default-exported function taking one param — so a stray sample never reaches
|
|
7
|
+
* the sandbox. Lenient by design: no parser-shaped source → null → caller
|
|
8
|
+
* falls back.
|
|
9
|
+
*/
|
|
10
|
+
const FENCE = /```(?:[a-zA-Z]+)?\n([\s\S]*?)```/g;
|
|
11
|
+
/**
|
|
12
|
+
* Cheap structural check that source matches the parser contract:
|
|
13
|
+
* `export default` of a function (declaration or arrow) that takes at least
|
|
14
|
+
* one parameter. Not a full parse — just enough to reject JSON samples, prose,
|
|
15
|
+
* and no-arg functions before we spend a sandbox spawn on them.
|
|
16
|
+
*/
|
|
17
|
+
export function looksLikeParser(source) {
|
|
18
|
+
const s = source.trim();
|
|
19
|
+
if (!/export\s+default\b/.test(s))
|
|
20
|
+
return false;
|
|
21
|
+
// export default function [name] (param...) { ... }
|
|
22
|
+
const fnDecl = /export\s+default\s+(?:async\s+)?function\b[^(]*\(\s*[A-Za-z_$][\w$]*/;
|
|
23
|
+
// export default (param...) => ... OR export default param => ...
|
|
24
|
+
const arrow = /export\s+default\s+(?:async\s+)?(?:\(\s*[A-Za-z_$][\w$]*|[A-Za-z_$][\w$]*\s*=>)/;
|
|
25
|
+
return fnDecl.test(s) || arrow.test(s);
|
|
26
|
+
}
|
|
27
|
+
export function extractParserSource(stdout) {
|
|
28
|
+
FENCE.lastIndex = 0;
|
|
29
|
+
let m;
|
|
30
|
+
while ((m = FENCE.exec(stdout)) !== null) {
|
|
31
|
+
const candidate = m[1].trim();
|
|
32
|
+
if (candidate !== '' && looksLikeParser(candidate))
|
|
33
|
+
return candidate;
|
|
34
|
+
}
|
|
35
|
+
// No parser-shaped fenced block — accept bare source only if it, too, is
|
|
36
|
+
// shaped like the contract.
|
|
37
|
+
const idx = stdout.indexOf('export default');
|
|
38
|
+
if (idx !== -1) {
|
|
39
|
+
const bare = stdout.slice(idx).trim();
|
|
40
|
+
if (looksLikeParser(bare))
|
|
41
|
+
return bare;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a bundled prompt file from the package-root `prompts/` directory.
|
|
3
|
+
* Walks up from this module so it works from both `src/` and `dist/src/`,
|
|
4
|
+
* mirroring `resolveSkillPath` for the skills/ directory.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolvePromptPath(fileName: string): string;
|
|
7
|
+
export declare function loadPrompt(fileName: string): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a bundled prompt file from the package-root `prompts/` directory.
|
|
6
|
+
* Walks up from this module so it works from both `src/` and `dist/src/`,
|
|
7
|
+
* mirroring `resolveSkillPath` for the skills/ directory.
|
|
8
|
+
*/
|
|
9
|
+
export function resolvePromptPath(fileName) {
|
|
10
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
let dir = thisDir;
|
|
12
|
+
for (;;) {
|
|
13
|
+
const candidate = join(dir, 'prompts');
|
|
14
|
+
if (existsSync(candidate))
|
|
15
|
+
return join(candidate, fileName);
|
|
16
|
+
const parent = resolve(dir, '..');
|
|
17
|
+
if (parent === dir) {
|
|
18
|
+
throw new Error(`prompt file missing from CLI installation (could not locate prompts/ directory from: ${thisDir})`);
|
|
19
|
+
}
|
|
20
|
+
dir = parent;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function loadPrompt(fileName) {
|
|
24
|
+
const path = resolvePromptPath(fileName);
|
|
25
|
+
try {
|
|
26
|
+
return readFileSync(path, 'utf8');
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new Error(`prompt file missing from CLI installation — try reinstalling the CLI (looked for: ${path})`);
|
|
30
|
+
}
|
|
31
|
+
}
|