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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/README.md +64 -2
  2. package/dist/package.json +3 -2
  3. package/dist/src/analyze/command.d.ts +11 -0
  4. package/dist/src/analyze/command.js +298 -3
  5. package/dist/src/analyze/composite-closure.d.ts +25 -0
  6. package/dist/src/analyze/composite-closure.js +0 -0
  7. package/dist/src/analyze/composition/agent-parser/author-prompt.d.ts +4 -0
  8. package/dist/src/analyze/composition/agent-parser/author-prompt.js +22 -0
  9. package/dist/src/analyze/composition/agent-parser/extract-parser.d.ts +8 -0
  10. package/dist/src/analyze/composition/agent-parser/extract-parser.js +44 -0
  11. package/dist/src/analyze/composition/agent-parser/load-prompt.d.ts +7 -0
  12. package/dist/src/analyze/composition/agent-parser/load-prompt.js +31 -0
  13. package/dist/src/analyze/composition/agent-parser/resolve-via-parser.d.ts +46 -0
  14. package/dist/src/analyze/composition/agent-parser/resolve-via-parser.js +88 -0
  15. package/dist/src/analyze/composition/agent-parser/sandbox.d.ts +45 -0
  16. package/dist/src/analyze/composition/agent-parser/sandbox.js +187 -0
  17. package/dist/src/analyze/composition/apply-mapping.d.ts +22 -0
  18. package/dist/src/analyze/composition/apply-mapping.js +67 -0
  19. package/dist/src/analyze/composition/candidate-critic-agent.d.ts +7 -0
  20. package/dist/src/analyze/composition/candidate-critic-agent.js +39 -0
  21. package/dist/src/analyze/composition/candidate-critic.d.ts +32 -0
  22. package/dist/src/analyze/composition/candidate-critic.js +63 -0
  23. package/dist/src/analyze/composition/candidate-files.d.ts +34 -0
  24. package/dist/src/analyze/composition/candidate-files.js +131 -0
  25. package/dist/src/analyze/composition/composition-cache-key.d.ts +19 -0
  26. package/dist/src/analyze/composition/composition-cache-key.js +19 -0
  27. package/dist/src/analyze/composition/interchange-schema.d.ts +37 -0
  28. package/dist/src/analyze/composition/interchange-schema.js +61 -0
  29. package/dist/src/analyze/composition/merge-edges.d.ts +19 -0
  30. package/dist/src/analyze/composition/merge-edges.js +50 -0
  31. package/dist/src/analyze/composition/parse-map-edges.d.ts +16 -0
  32. package/dist/src/analyze/composition/parse-map-edges.js +55 -0
  33. package/dist/src/analyze/composition/resolve-mapping-cli.d.ts +23 -0
  34. package/dist/src/analyze/composition/resolve-mapping-cli.js +29 -0
  35. package/dist/src/analyze/composition/resolve-mapping.d.ts +59 -0
  36. package/dist/src/analyze/composition/resolve-mapping.js +94 -0
  37. package/dist/src/analyze/cycle-detection.d.ts +27 -0
  38. package/dist/src/analyze/cycle-detection.js +234 -0
  39. package/dist/src/analyze/cycle-view.d.ts +7 -0
  40. package/dist/src/analyze/cycle-view.js +28 -0
  41. package/dist/src/analyze/fuzzy-search.d.ts +3 -0
  42. package/dist/src/analyze/fuzzy-search.js +55 -0
  43. package/dist/src/analyze/issue-inheritance.d.ts +12 -0
  44. package/dist/src/analyze/issue-inheritance.js +108 -0
  45. package/dist/src/analyze/lineage.d.ts +29 -0
  46. package/dist/src/analyze/lineage.js +0 -0
  47. package/dist/src/analyze/scope-gate-cascade.d.ts +11 -0
  48. package/dist/src/analyze/scope-gate-cascade.js +170 -0
  49. package/dist/src/analyze/search-neighborhood.d.ts +3 -0
  50. package/dist/src/analyze/search-neighborhood.js +56 -0
  51. package/dist/src/analyze/select/tui/App.js +5 -16
  52. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +0 -4
  53. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +3 -15
  54. package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +29 -38
  55. package/dist/src/analyze/select/tui/components/FieldEditor.js +309 -185
  56. package/dist/src/analyze/select/tui/components/FinalizeDialog.d.ts +13 -1
  57. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +23 -2
  58. package/dist/src/analyze/select/tui/components/GotoBanner.d.ts +31 -0
  59. package/dist/src/analyze/select/tui/components/GotoBanner.js +30 -0
  60. package/dist/src/analyze/select/tui/components/GroupedSidebar.d.ts +91 -0
  61. package/dist/src/analyze/select/tui/components/GroupedSidebar.js +497 -0
  62. package/dist/src/analyze/select/tui/components/HelpOverlay.d.ts +13 -1
  63. package/dist/src/analyze/select/tui/components/HelpOverlay.js +9 -3
  64. package/dist/src/analyze/select/tui/components/JsonEditor.js +3 -3
  65. package/dist/src/analyze/select/tui/components/LineagePanel.d.ts +14 -0
  66. package/dist/src/analyze/select/tui/components/LineagePanel.js +22 -0
  67. package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +0 -7
  68. package/dist/src/analyze/select/tui/components/RationalePanel.js +3 -15
  69. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +0 -6
  70. package/dist/src/analyze/select/tui/components/Sidebar.js +11 -27
  71. package/dist/src/analyze/select/tui/components/StatusBar.js +2 -1
  72. package/dist/src/analyze/select/tui/components/removed-components-text.d.ts +7 -0
  73. package/dist/src/analyze/select/tui/components/removed-components-text.js +11 -0
  74. package/dist/src/analyze/select/tui/hooks/useImmediateInput.d.ts +1 -5
  75. package/dist/src/analyze/select/tui/hooks/useImmediateInput.js +8 -10
  76. package/dist/src/analyze/select/tui/theme.d.ts +15 -0
  77. package/dist/src/analyze/select/tui/theme.js +28 -0
  78. package/dist/src/analyze/selection-cascade.d.ts +3 -0
  79. package/dist/src/analyze/selection-cascade.js +19 -0
  80. package/dist/src/analyze/slot-graph.d.ts +11 -0
  81. package/dist/src/analyze/slot-graph.js +17 -0
  82. package/dist/src/apply/api-client.d.ts +2 -1
  83. package/dist/src/apply/api-client.js +40 -6
  84. package/dist/src/apply/command.d.ts +17 -1
  85. package/dist/src/apply/command.js +69 -11
  86. package/dist/src/apply/error-parser.d.ts +16 -0
  87. package/dist/src/apply/error-parser.js +117 -0
  88. package/dist/src/apply/tui/ServerPreviewView.js +3 -1
  89. package/dist/src/credentials-store.d.ts +6 -0
  90. package/dist/src/credentials-store.js +5 -1
  91. package/dist/src/generate/agent-runner.d.ts +6 -0
  92. package/dist/src/generate/agent-runner.js +24 -8
  93. package/dist/src/import/command.js +69 -31
  94. package/dist/src/import/cycle-auto-reject.d.ts +4 -0
  95. package/dist/src/import/cycle-auto-reject.js +14 -0
  96. package/dist/src/import/orchestrator.d.ts +20 -7
  97. package/dist/src/import/orchestrator.js +139 -35
  98. package/dist/src/import/strip-allowed-components.d.ts +18 -0
  99. package/dist/src/import/strip-allowed-components.js +22 -0
  100. package/dist/src/import/tui/CustomPromptBanner.d.ts +0 -5
  101. package/dist/src/import/tui/CustomPromptBanner.js +2 -1
  102. package/dist/src/import/tui/WizardApp.d.ts +10 -74
  103. package/dist/src/import/tui/WizardApp.js +234 -246
  104. package/dist/src/import/tui/ai-flag.d.ts +5 -0
  105. package/dist/src/import/tui/ai-flag.js +6 -0
  106. package/dist/src/import/tui/autocomplete.d.ts +5 -0
  107. package/dist/src/import/tui/autocomplete.js +32 -0
  108. package/dist/src/import/tui/components/AutoFilterBanner.d.ts +10 -0
  109. package/dist/src/import/tui/components/AutoFilterBanner.js +19 -0
  110. package/dist/src/import/tui/components/CounterStrip.d.ts +12 -0
  111. package/dist/src/import/tui/components/CounterStrip.js +12 -0
  112. package/dist/src/import/tui/components/LegendEntry.d.ts +2 -0
  113. package/dist/src/import/tui/components/LegendEntry.js +6 -0
  114. package/dist/src/import/tui/cycle-panel-scroll.d.ts +8 -0
  115. package/dist/src/import/tui/cycle-panel-scroll.js +33 -0
  116. package/dist/src/import/tui/final-review-host.d.ts +3 -7
  117. package/dist/src/import/tui/final-review-host.js +9 -4
  118. package/dist/src/import/tui/group-collapse.d.ts +2 -0
  119. package/dist/src/import/tui/group-collapse.js +11 -0
  120. package/dist/src/import/tui/history.d.ts +22 -0
  121. package/dist/src/import/tui/history.js +53 -0
  122. package/dist/src/import/tui/hooks/useLineage.d.ts +32 -0
  123. package/dist/src/import/tui/hooks/useLineage.js +50 -0
  124. package/dist/src/import/tui/hooks/useOverlayPanel.d.ts +13 -0
  125. package/dist/src/import/tui/hooks/useOverlayPanel.js +27 -0
  126. package/dist/src/import/tui/lineage-layout.d.ts +75 -0
  127. package/dist/src/import/tui/lineage-layout.js +82 -0
  128. package/dist/src/import/tui/runLivePreview.d.ts +11 -0
  129. package/dist/src/import/tui/runLivePreview.js +7 -1
  130. package/dist/src/import/tui/scope-gate-columns.d.ts +31 -0
  131. package/dist/src/import/tui/scope-gate-columns.js +94 -0
  132. package/dist/src/import/tui/scope-gate-host.d.ts +3 -1
  133. package/dist/src/import/tui/scope-gate-host.js +11 -3
  134. package/dist/src/import/tui/sidebar-width.d.ts +1 -0
  135. package/dist/src/import/tui/sidebar-width.js +3 -0
  136. package/dist/src/import/tui/step-filters.d.ts +19 -0
  137. package/dist/src/import/tui/step-filters.js +42 -0
  138. package/dist/src/import/tui/steps/AtomicGenerateReviewStep.d.ts +42 -0
  139. package/dist/src/import/tui/steps/AtomicGenerateReviewStep.js +726 -0
  140. package/dist/src/import/tui/steps/AtomicScopeGateStep.d.ts +3 -0
  141. package/dist/src/import/tui/steps/AtomicScopeGateStep.js +211 -0
  142. package/dist/src/import/tui/steps/CredentialsStep.js +5 -18
  143. package/dist/src/import/tui/steps/DoneStep.d.ts +0 -2
  144. package/dist/src/import/tui/steps/DoneStep.js +3 -2
  145. package/dist/src/import/tui/steps/ErrorStep.js +2 -1
  146. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +19 -25
  147. package/dist/src/import/tui/steps/GenerateReviewStep.js +1258 -229
  148. package/dist/src/import/tui/steps/PathValidationStep.js +3 -2
  149. package/dist/src/import/tui/steps/PreviewStep.js +2 -1
  150. package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +0 -7
  151. package/dist/src/import/tui/steps/PushDecisionGateStep.js +3 -6
  152. package/dist/src/import/tui/steps/PushingStep.js +2 -1
  153. package/dist/src/import/tui/steps/RunningStep.d.ts +4 -1
  154. package/dist/src/import/tui/steps/RunningStep.js +3 -2
  155. package/dist/src/import/tui/steps/ScopeGateStep.d.ts +24 -0
  156. package/dist/src/import/tui/steps/ScopeGateStep.js +850 -193
  157. package/dist/src/import/tui/steps/TokenInputStep.js +2 -1
  158. package/dist/src/import/tui/steps/WelcomeStep.js +2 -1
  159. package/dist/src/import/tui/steps/WizardPreviewStep.d.ts +6 -0
  160. package/dist/src/import/tui/steps/WizardPreviewStep.js +95 -56
  161. package/dist/src/import/tui/steps/auto-reject-decision.d.ts +7 -0
  162. package/dist/src/import/tui/steps/auto-reject-decision.js +9 -0
  163. package/dist/src/import/tui/steps/breaking-change-format.d.ts +2 -0
  164. package/dist/src/import/tui/steps/breaking-change-format.js +24 -0
  165. package/dist/src/import/tui/steps/enumerate-cycle-breaks.d.ts +14 -0
  166. package/dist/src/import/tui/steps/enumerate-cycle-breaks.js +0 -0
  167. package/dist/src/import/tui/steps/preview-diff.js +48 -5
  168. package/dist/src/import/tui/useFinalizePreview.d.ts +34 -0
  169. package/dist/src/import/tui/useFinalizePreview.js +0 -0
  170. package/dist/src/import/tui/useLivePreview.d.ts +3 -0
  171. package/dist/src/import/tui/useLivePreview.js +1 -0
  172. package/dist/src/import/tui/wizard-state-transitions.d.ts +5 -59
  173. package/dist/src/import/tui/wizard-state-transitions.js +5 -60
  174. package/dist/src/lib/composition-mode.d.ts +28 -0
  175. package/dist/src/lib/composition-mode.js +37 -0
  176. package/dist/src/lib/prompt-overrides.d.ts +38 -0
  177. package/dist/src/lib/prompt-overrides.js +69 -0
  178. package/dist/src/print/command.js +19 -1
  179. package/dist/src/runs/modify-launcher.d.ts +3 -0
  180. package/dist/src/runs/modify-launcher.js +2 -0
  181. package/dist/src/runs/replay-helpers.js +1 -0
  182. package/dist/src/runs/run-picker.js +2 -18
  183. package/dist/src/runs/store.d.ts +4 -0
  184. package/dist/src/session/db.d.ts +23 -40
  185. package/dist/src/session/db.js +110 -138
  186. package/dist/src/setup/composition-mode-prompt.d.ts +14 -0
  187. package/dist/src/setup/composition-mode-prompt.js +37 -0
  188. package/package.json +5 -4
  189. package/prompts/composition-dir-critic.md +7 -0
  190. package/prompts/composition-edges.md +8 -0
  191. package/prompts/composition-parser-repair-empty.md +1 -0
  192. package/prompts/composition-parser-repair-error.md +2 -0
  193. package/prompts/composition-parser.md +42 -0
@@ -0,0 +1,46 @@
1
+ import type { CompositionEdge } from '../interchange-schema.js';
2
+ export type ResolveViaParserResult = {
3
+ edges: CompositionEdge[];
4
+ warnings: string[];
5
+ /** True when authoring/execution failed and the caller should fall back to
6
+ * direct edge-emission (or no composition). */
7
+ usedFallback: boolean;
8
+ /** The authored parser source, when one ran successfully (for caching). */
9
+ parserSource?: string;
10
+ };
11
+ /**
12
+ * Agent-authored parser path (spec: agent-authored-parser, Phase 2).
13
+ *
14
+ * Ask the agent to WRITE a pure `(ctx) => Edge[]` parser, run it in the
15
+ * sandbox, and verify its edges against the component name set. One repair
16
+ * round if the authored parser fails to run; otherwise signal fallback. The
17
+ * agent call is injected so this is testable without spawning a subprocess.
18
+ */
19
+ export declare function resolveViaAgentParser(input: {
20
+ /** Files inlined into the authoring prompt — a bounded candidate sample so
21
+ * the agent sees the convention without ingesting the whole repo. */
22
+ files: Array<{
23
+ path: string;
24
+ content: string;
25
+ }>;
26
+ /** Files the authored parser actually RUNS over in the sandbox. Defaults to
27
+ * `files`; pass the full scanned set here so the parser is never starved by
28
+ * the prompt-side candidate filter (the filter stops being load-bearing). */
29
+ runtimeFiles?: Array<{
30
+ path: string;
31
+ content: string;
32
+ }>;
33
+ componentNames: Set<string>;
34
+ runAgentFn: (opts: {
35
+ prompt: string;
36
+ }) => Promise<string>;
37
+ instructionOverride?: string;
38
+ onPhase?: (phase: string) => void;
39
+ timeoutMs?: number;
40
+ /** When true, a parser that runs cleanly but emits ZERO verified edges is
41
+ * treated as suspicious (the candidate files contain composition markers, so
42
+ * 0 edges usually means a broken parser, not a genuinely flat repo) and gets
43
+ * one repair round. Off by default so genuinely-empty repos don't pay a
44
+ * wasted retry. The caller sets it from marker presence in the candidates. */
45
+ retryOnEmpty?: boolean;
46
+ }): Promise<ResolveViaParserResult>;
@@ -0,0 +1,88 @@
1
+ import { buildAuthorPrompt } from './author-prompt.js';
2
+ import { extractParserSource } from './extract-parser.js';
3
+ import { runParserInSandbox } from './sandbox.js';
4
+ import { loadPrompt } from './load-prompt.js';
5
+ /**
6
+ * Agent-authored parser path (spec: agent-authored-parser, Phase 2).
7
+ *
8
+ * Ask the agent to WRITE a pure `(ctx) => Edge[]` parser, run it in the
9
+ * sandbox, and verify its edges against the component name set. One repair
10
+ * round if the authored parser fails to run; otherwise signal fallback. The
11
+ * agent call is injected so this is testable without spawning a subprocess.
12
+ */
13
+ export async function resolveViaAgentParser(input) {
14
+ const warnings = [];
15
+ const componentNamesArr = [...input.componentNames];
16
+ const ctx = { files: input.runtimeFiles ?? input.files, componentNames: componentNamesArr };
17
+ const verify = (edges) => {
18
+ const out = [];
19
+ for (const e of edges) {
20
+ // A component cannot be its own composite parent. Self-edges are a common
21
+ // parser bug (matching a component's own name in its own source) and, left
22
+ // in, masquerade as one-node cycles downstream.
23
+ if (e.parent === e.child) {
24
+ warnings.push(`parser edge dropped: self-edge "${e.parent}→${e.child}"`);
25
+ continue;
26
+ }
27
+ if (!input.componentNames.has(e.parent)) {
28
+ warnings.push(`parser edge dropped: unknown parent "${e.parent}" (${e.parent}→${e.child})`);
29
+ continue;
30
+ }
31
+ if (!input.componentNames.has(e.child)) {
32
+ warnings.push(`parser edge dropped: unknown child "${e.child}" (${e.parent}→${e.child})`);
33
+ continue;
34
+ }
35
+ out.push(e);
36
+ }
37
+ return out;
38
+ };
39
+ // Attempt: author → extract → sandbox. Returns null on a fallback-worthy miss.
40
+ const attempt = async (prompt) => {
41
+ input.onPhase?.('authoring');
42
+ const raw = await input.runAgentFn({ prompt });
43
+ const source = extractParserSource(raw);
44
+ if (source === null)
45
+ return { error: 'agent output had no parser-shaped code block (expected `export default function (ctx) {…}`)' };
46
+ input.onPhase?.('parsing');
47
+ const res = await runParserInSandbox(source, ctx, input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {});
48
+ if (res.error)
49
+ return { error: res.error };
50
+ return { edges: res.edges, source };
51
+ };
52
+ const basePrompt = buildAuthorPrompt(input.files, componentNamesArr, input.instructionOverride);
53
+ const first = await attempt(basePrompt);
54
+ if ('edges' in first) {
55
+ const verified = verify(first.edges);
56
+ // Suspiciously-empty guard: a parser that ran cleanly but emitted no usable
57
+ // edges, when the candidates DO contain composition markers, is almost
58
+ // always broken (wrong parent attribution, wrong pattern). Give it one
59
+ // repair round rather than silently returning nothing.
60
+ if (verified.length > 0 || !input.retryOnEmpty) {
61
+ return { edges: verified, warnings, usedFallback: false, parserSource: first.source };
62
+ }
63
+ warnings.push('parser produced 0 usable edges despite composition markers — retrying once');
64
+ const emptyRepairPrompt = `${basePrompt}\n\n${loadPrompt('composition-parser-repair-empty.md').trim()}`;
65
+ const retry = await attempt(emptyRepairPrompt);
66
+ if ('edges' in retry) {
67
+ const retried = verify(retry.edges);
68
+ // Prefer the retry when it found something; otherwise keep the first
69
+ // parser (it at least ran) rather than discarding a working artifact.
70
+ if (retried.length > 0) {
71
+ return { edges: retried, warnings, usedFallback: false, parserSource: retry.source };
72
+ }
73
+ warnings.push('repair still produced 0 edges — keeping the original parser');
74
+ return { edges: verified, warnings, usedFallback: false, parserSource: first.source };
75
+ }
76
+ warnings.push(`repair attempt failed: ${retry.error} — keeping the original (empty) parser`);
77
+ return { edges: verified, warnings, usedFallback: false, parserSource: first.source };
78
+ }
79
+ // One repair round — tell the agent what went wrong and try again.
80
+ warnings.push(`parser attempt failed: ${first.error} — retrying once`);
81
+ const repairPrompt = `${basePrompt}\n\n${loadPrompt('composition-parser-repair-error.md').trim().replace('{{error}}', first.error)}`;
82
+ const second = await attempt(repairPrompt);
83
+ if ('edges' in second) {
84
+ return { edges: verify(second.edges), warnings, usedFallback: false, parserSource: second.source };
85
+ }
86
+ warnings.push(`parser attempt failed again: ${second.error} — falling back`);
87
+ return { edges: [], warnings, usedFallback: true };
88
+ }
@@ -0,0 +1,45 @@
1
+ import type { CompositionEdge } from '../interchange-schema.js';
2
+ export type SandboxCtx = {
3
+ files: Array<{
4
+ path: string;
5
+ content: string;
6
+ }>;
7
+ componentNames: string[];
8
+ };
9
+ export type SandboxResult = {
10
+ edges: CompositionEdge[];
11
+ error?: string;
12
+ };
13
+ /**
14
+ * Execute agent-authored parser source under three nested jails (spec:
15
+ * dsi-agent-authored-parser-design, Phase 1 — the security core).
16
+ *
17
+ * OUTER — a throwaway child `node` process: `env: {}` (no secrets/tokens leak
18
+ * even on a full escape), a heap cap (`--max-old-space-size`, OOM self-kills),
19
+ * and a wall-clock `SIGKILL` the parent enforces for any hang (sync or async).
20
+ *
21
+ * PERMISSION — the child runs with `--permission` and NO `--allow-fs-*`, so
22
+ * the Node runtime denies filesystem access, child_process spawning, worker
23
+ * creation, and native addons (ERR_ACCESS_DENIED). The parser needs none —
24
+ * its files arrive via stdin as ctx — so even a vm escape reaching
25
+ * `require('node:fs')`/`child_process` is stopped at the runtime layer.
26
+ *
27
+ * INNER — a `node:vm` context created from a null-prototype object, an
28
+ * ALLOW-LIST: it has only ECMAScript intrinsics (Object/Array/JSON/Math/RegExp
29
+ * …). There is no `process`, `require`, `Buffer`, `fetch`, timers, or module
30
+ * loader in scope, so the parser cannot reach them — and only STRINGS cross
31
+ * the boundary (source + JSON-encoded ctx), so there is no host-object handle
32
+ * for the classic `this.constructor.constructor('return process')()` escape to
33
+ * climb. `vm.runInContext(..., { timeout })` kills sync infinite loops.
34
+ *
35
+ * The parser must be SYNCHRONOUS — an async/thenable return is rejected, which
36
+ * closes the async-timeout gap (vm `timeout` covers sync execution only).
37
+ *
38
+ * Any failure — throw, timeout, OOM, denied access, bad/async return, compile
39
+ * error — resolves to `{ edges: [], error }` and is NEVER thrown. Returned
40
+ * edges are shape-validated here; the caller still re-verifies parent/child
41
+ * against the component name set.
42
+ */
43
+ export declare function runParserInSandbox(source: string, ctx: SandboxCtx, opts?: {
44
+ timeoutMs?: number;
45
+ }): Promise<SandboxResult>;
@@ -0,0 +1,187 @@
1
+ import { spawn } from 'node:child_process';
2
+ const DEFAULT_TIMEOUT_MS = 5000;
3
+ const HEAP_CAP_MB = 128;
4
+ const MAX_INPUT_BYTES = 8 * 1024 * 1024; // refuse absurd ctx up front
5
+ /**
6
+ * Execute agent-authored parser source under three nested jails (spec:
7
+ * dsi-agent-authored-parser-design, Phase 1 — the security core).
8
+ *
9
+ * OUTER — a throwaway child `node` process: `env: {}` (no secrets/tokens leak
10
+ * even on a full escape), a heap cap (`--max-old-space-size`, OOM self-kills),
11
+ * and a wall-clock `SIGKILL` the parent enforces for any hang (sync or async).
12
+ *
13
+ * PERMISSION — the child runs with `--permission` and NO `--allow-fs-*`, so
14
+ * the Node runtime denies filesystem access, child_process spawning, worker
15
+ * creation, and native addons (ERR_ACCESS_DENIED). The parser needs none —
16
+ * its files arrive via stdin as ctx — so even a vm escape reaching
17
+ * `require('node:fs')`/`child_process` is stopped at the runtime layer.
18
+ *
19
+ * INNER — a `node:vm` context created from a null-prototype object, an
20
+ * ALLOW-LIST: it has only ECMAScript intrinsics (Object/Array/JSON/Math/RegExp
21
+ * …). There is no `process`, `require`, `Buffer`, `fetch`, timers, or module
22
+ * loader in scope, so the parser cannot reach them — and only STRINGS cross
23
+ * the boundary (source + JSON-encoded ctx), so there is no host-object handle
24
+ * for the classic `this.constructor.constructor('return process')()` escape to
25
+ * climb. `vm.runInContext(..., { timeout })` kills sync infinite loops.
26
+ *
27
+ * The parser must be SYNCHRONOUS — an async/thenable return is rejected, which
28
+ * closes the async-timeout gap (vm `timeout` covers sync execution only).
29
+ *
30
+ * Any failure — throw, timeout, OOM, denied access, bad/async return, compile
31
+ * error — resolves to `{ edges: [], error }` and is NEVER thrown. Returned
32
+ * edges are shape-validated here; the caller still re-verifies parent/child
33
+ * against the component name set.
34
+ */
35
+ export function runParserInSandbox(source, ctx, opts = {}) {
36
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
37
+ return new Promise((resolve) => {
38
+ let input;
39
+ try {
40
+ input = JSON.stringify({ source, ctx, timeoutMs });
41
+ }
42
+ catch {
43
+ resolve({ edges: [], error: 'ctx is not serializable' });
44
+ return;
45
+ }
46
+ if (input.length > MAX_INPUT_BYTES) {
47
+ resolve({ edges: [], error: 'parser input exceeds size limit' });
48
+ return;
49
+ }
50
+ let settled = false;
51
+ let stdout = '';
52
+ let stderr = '';
53
+ const child = spawn(process.execPath, [
54
+ // Node Permission Model (Node ≥20/24): deny fs, child_process, worker,
55
+ // and native addons at the RUNTIME level. We pass NO --allow-fs-* — the
56
+ // parser needs no disk access (its files arrive via stdin as ctx), so
57
+ // even if the vm allow-list were escaped and `require('node:fs')` were
58
+ // reached, reads are denied (ERR_ACCESS_DENIED) and spawning is denied
59
+ // by default. Third jail layer beneath process-isolation + vm.
60
+ '--permission',
61
+ `--max-old-space-size=${HEAP_CAP_MB}`,
62
+ '--input-type=module',
63
+ '-e',
64
+ RUNNER,
65
+ ],
66
+ // No inherited env (secrets stay out of the jail), neutral cwd, no argv.
67
+ { env: {}, cwd: process.cwd(), stdio: ['pipe', 'pipe', 'pipe'] });
68
+ const done = (r) => {
69
+ if (settled)
70
+ return;
71
+ settled = true;
72
+ clearTimeout(timer);
73
+ try {
74
+ child.kill('SIGKILL');
75
+ }
76
+ catch {
77
+ /* already gone */
78
+ }
79
+ resolve(r);
80
+ };
81
+ // Wall-clock backstop (covers async hangs the vm timeout can't). Grace over
82
+ // the inner timeout so the vm-level error is preferred when it fires first.
83
+ const timer = setTimeout(() => done({ edges: [], error: `parser timed out after ${timeoutMs}ms` }), timeoutMs + 1000);
84
+ child.stdout.on('data', (c) => {
85
+ stdout += String(c);
86
+ });
87
+ child.stderr.on('data', (c) => {
88
+ stderr += String(c);
89
+ });
90
+ child.on('error', (e) => done({ edges: [], error: `sandbox spawn failed: ${e.message}` }));
91
+ child.on('close', (code, signal) => {
92
+ if (settled)
93
+ return;
94
+ if (signal === 'SIGKILL') {
95
+ done({ edges: [], error: 'parser killed (timeout or memory limit)' });
96
+ return;
97
+ }
98
+ let msg;
99
+ try {
100
+ msg = JSON.parse(stdout);
101
+ }
102
+ catch {
103
+ done({
104
+ edges: [],
105
+ error: `sandbox produced no result (code ${code})${stderr ? `: ${stderr.slice(0, 200)}` : ''}`,
106
+ });
107
+ return;
108
+ }
109
+ if (msg.ok)
110
+ done({ edges: sanitizeEdges(msg.edges) });
111
+ else
112
+ done({ edges: [], error: msg.error ?? 'parser failed' });
113
+ });
114
+ try {
115
+ child.stdin.on('error', () => { });
116
+ child.stdin.write(input);
117
+ child.stdin.end();
118
+ }
119
+ catch {
120
+ done({ edges: [], error: 'failed to send input to sandbox' });
121
+ }
122
+ });
123
+ }
124
+ /** Keep only well-formed edges: parent+child non-empty strings. */
125
+ function sanitizeEdges(raw) {
126
+ if (!Array.isArray(raw))
127
+ return [];
128
+ const out = [];
129
+ for (const e of raw) {
130
+ if (!e || typeof e !== 'object')
131
+ continue;
132
+ const rec = e;
133
+ if (typeof rec.parent !== 'string' || rec.parent === '')
134
+ continue;
135
+ if (typeof rec.child !== 'string' || rec.child === '')
136
+ continue;
137
+ const edge = { parent: rec.parent, child: rec.child, provenance: 'adapter:agent-parser' };
138
+ if (typeof rec.slot === 'string' && rec.slot !== '')
139
+ edge.slot = rec.slot;
140
+ if (typeof rec.confidence === 'number' && rec.confidence >= 1 && rec.confidence <= 5)
141
+ edge.confidence = rec.confidence;
142
+ out.push(edge);
143
+ }
144
+ return out;
145
+ }
146
+ /**
147
+ * Child-process runner (trusted — OUR code). Reads {source, ctx, timeoutMs}
148
+ * from stdin, runs the parser inside a vm allow-list context, writes a single
149
+ * JSON result to stdout. Only strings enter the vm; only a JSON string leaves.
150
+ */
151
+ const RUNNER = `
152
+ import vm from 'node:vm';
153
+ let input = '';
154
+ process.stdin.on('data', (c) => (input += c));
155
+ process.stdin.on('end', () => {
156
+ let out;
157
+ try {
158
+ const { source, ctx, timeoutMs } = JSON.parse(input);
159
+ // Null-prototype global: no inherited host props. The context still gets
160
+ // ECMAScript intrinsics (Object/Array/JSON/Function/Math/RegExp), but NOT
161
+ // process/require/Buffer/timers/module.
162
+ const context = vm.createContext(Object.create(null));
163
+ // Cross the boundary as PRIMITIVES only — no host object/function handle
164
+ // for a prototype-chain escape to grab.
165
+ context.__source = String(source);
166
+ context.__ctxJson = JSON.stringify(ctx);
167
+ const bootstrap = [
168
+ '(function () {',
169
+ ' var ctx = JSON.parse(__ctxJson);',
170
+ " var body = String(__source).replace(/export\\\\s+default\\\\s+/, 'return ');",
171
+ ' var factory = Function(body);', // the vm context's own Function
172
+ ' var parser = factory();',
173
+ " if (typeof parser !== 'function') return JSON.stringify({ ok: false, error: 'parser source did not resolve to a function' });",
174
+ ' var result = parser(ctx);',
175
+ " if (result && typeof result.then === 'function') return JSON.stringify({ ok: false, error: 'parser must be synchronous' });",
176
+ " if (!Array.isArray(result)) return JSON.stringify({ ok: false, error: 'parser did not return an array' });",
177
+ ' return JSON.stringify({ ok: true, edges: result });',
178
+ '})()',
179
+ ].join('\\n');
180
+ out = vm.runInContext(bootstrap, context, { timeout: timeoutMs });
181
+ } catch (e) {
182
+ out = JSON.stringify({ ok: false, error: String(e && e.message ? e.message : e) });
183
+ }
184
+ try { process.stdout.write(typeof out === 'string' ? out : JSON.stringify({ ok: false, error: 'no result' })); }
185
+ catch { process.stdout.write(JSON.stringify({ ok: false, error: 'result not serializable' })); }
186
+ });
187
+ `;
@@ -0,0 +1,22 @@
1
+ import type { RawComponentDefinition } from '../../types.js';
2
+ import type { CompositionEdge } from './interchange-schema.js';
3
+ export type ApplyMappingResult = {
4
+ components: RawComponentDefinition[];
5
+ warnings: string[];
6
+ };
7
+ /**
8
+ * Enrichment pass (spec T7): merge a resolved composition edge list into the
9
+ * extracted components' `allowedComponents`. Runs AFTER extraction; it is the
10
+ * mapping resolver's entire job — populating the one field every downstream
11
+ * graph consumer reads.
12
+ *
13
+ * Slot targeting:
14
+ * - edge.slot present + slot exists → write there.
15
+ * - edge.slot present + slot missing → synthesize for high-trust
16
+ * (typed-slot / adapter) edges; drop-and-warn for `agent` edges.
17
+ * - edge.slot absent → default slot (isDefault), synthesized if none exists.
18
+ *
19
+ * Edges naming unknown parents/children are dropped-and-warned (§1.3). Inputs
20
+ * are not mutated; a deep-enough clone of touched slots is returned.
21
+ */
22
+ export declare function applyMapping(components: RawComponentDefinition[], edges: CompositionEdge[]): ApplyMappingResult;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Enrichment pass (spec T7): merge a resolved composition edge list into the
3
+ * extracted components' `allowedComponents`. Runs AFTER extraction; it is the
4
+ * mapping resolver's entire job — populating the one field every downstream
5
+ * graph consumer reads.
6
+ *
7
+ * Slot targeting:
8
+ * - edge.slot present + slot exists → write there.
9
+ * - edge.slot present + slot missing → synthesize for high-trust
10
+ * (typed-slot / adapter) edges; drop-and-warn for `agent` edges.
11
+ * - edge.slot absent → default slot (isDefault), synthesized if none exists.
12
+ *
13
+ * Edges naming unknown parents/children are dropped-and-warned (§1.3). Inputs
14
+ * are not mutated; a deep-enough clone of touched slots is returned.
15
+ */
16
+ export function applyMapping(components, edges) {
17
+ const warnings = [];
18
+ const names = new Set(components.map((c) => c.name));
19
+ // Clone components (and their slots) so inputs stay untouched.
20
+ const cloned = components.map((c) => ({
21
+ ...c,
22
+ slots: c.slots.map((s) => ({
23
+ ...s,
24
+ ...(s.allowedComponents ? { allowedComponents: [...s.allowedComponents] } : {}),
25
+ })),
26
+ }));
27
+ const byName = new Map(cloned.map((c) => [c.name, c]));
28
+ const isHighTrust = (p) => p === 'user' || p === 'typed-slot' || p.startsWith('adapter:');
29
+ const addAllowed = (slot, child) => {
30
+ const set = new Set(slot.allowedComponents ?? []);
31
+ set.add(child);
32
+ slot.allowedComponents = [...set];
33
+ };
34
+ for (const edge of edges) {
35
+ if (!names.has(edge.parent)) {
36
+ warnings.push(`dropped edge: unknown parent component "${edge.parent}" (${edge.parent}→${edge.child})`);
37
+ continue;
38
+ }
39
+ if (!names.has(edge.child)) {
40
+ warnings.push(`dropped edge: unknown child component "${edge.child}" (${edge.parent}→${edge.child})`);
41
+ continue;
42
+ }
43
+ const parent = byName.get(edge.parent);
44
+ if (edge.slot) {
45
+ const named = parent.slots.find((s) => s.name === edge.slot);
46
+ if (named) {
47
+ addAllowed(named, edge.child);
48
+ continue;
49
+ }
50
+ if (isHighTrust(edge.provenance)) {
51
+ const synthesized = { name: edge.slot, isDefault: false, allowedComponents: [edge.child] };
52
+ parent.slots.push(synthesized);
53
+ continue;
54
+ }
55
+ warnings.push(`dropped edge: slot "${edge.slot}" not found on "${edge.parent}" (agent-provenance; ${edge.parent}→${edge.child})`);
56
+ continue;
57
+ }
58
+ // Default slot.
59
+ let def = parent.slots.find((s) => s.isDefault);
60
+ if (!def) {
61
+ def = { name: 'children', isDefault: true, allowedComponents: [] };
62
+ parent.slots.push(def);
63
+ }
64
+ addAllowed(def, edge.child);
65
+ }
66
+ return { components: cloned, warnings };
67
+ }
@@ -0,0 +1,7 @@
1
+ export declare function buildDirCriticPrompt(dirs: string[]): string;
2
+ /**
3
+ * Parse the agent's reply into the chosen directories, keeping only ones that
4
+ * were offered (no injection). Lenient: extracts the first JSON array from
5
+ * surrounding prose; malformed / absent → [].
6
+ */
7
+ export declare function parseDirCriticReply(reply: string, offered: string[]): string[];
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Prompt + reply parser for the directory completeness critic. Cheap: the
3
+ * agent sees only directory NAMES (no file contents) and returns which look
4
+ * like they might hold composition/mapping declarations.
5
+ */
6
+ import { loadPrompt } from './agent-parser/load-prompt.js';
7
+ export function buildDirCriticPrompt(dirs) {
8
+ return [loadPrompt('composition-dir-critic.md').trim(), '', 'Directories:', ...dirs.map((d) => `- ${d}`)].join('\n');
9
+ }
10
+ /**
11
+ * Parse the agent's reply into the chosen directories, keeping only ones that
12
+ * were offered (no injection). Lenient: extracts the first JSON array from
13
+ * surrounding prose; malformed / absent → [].
14
+ */
15
+ export function parseDirCriticReply(reply, offered) {
16
+ const start = reply.indexOf('[');
17
+ const end = reply.indexOf(']', start);
18
+ if (start === -1 || end === -1)
19
+ return [];
20
+ let arr;
21
+ try {
22
+ arr = JSON.parse(reply.slice(start, end + 1));
23
+ }
24
+ catch {
25
+ return [];
26
+ }
27
+ if (!Array.isArray(arr))
28
+ return [];
29
+ const offeredSet = new Set(offered);
30
+ const out = [];
31
+ const seen = new Set();
32
+ for (const v of arr) {
33
+ if (typeof v === 'string' && offeredSet.has(v) && !seen.has(v)) {
34
+ seen.add(v);
35
+ out.push(v);
36
+ }
37
+ }
38
+ return out;
39
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Completeness critic for the candidate-file filter (design: the heuristic can
3
+ * miss a composition-layer directory whose name we didn't anticipate). We show
4
+ * the agent the DIRECTORIES it did NOT pick — names only, no file contents, so
5
+ * it's cheap — and let it flag ones that look promising from the path alone.
6
+ * Flagged dirs' files are folded into the prompt sample.
7
+ *
8
+ * This only widens the AUTHORING-PROMPT sample; the parser already runs over
9
+ * every file at runtime, so this is purely "help the agent see the convention",
10
+ * never a correctness dependency.
11
+ */
12
+ type FileLike = {
13
+ path: string;
14
+ content: string;
15
+ };
16
+ /** Directories present in `all` but with no file in `selected`, sorted+deduped. */
17
+ export declare function uncoveredDirectories(all: FileLike[], selected: FileLike[]): string[];
18
+ /** Union `selected` with every `all` file whose directory is in `chosenDirs`. */
19
+ export declare function expandCandidatesByDirs(all: FileLike[], selected: FileLike[], chosenDirs: string[]): FileLike[];
20
+ export type CritiqueResult = {
21
+ files: FileLike[];
22
+ addedDirs: string[];
23
+ };
24
+ /**
25
+ * Run the completeness critic. `askDirs` receives the uncovered directory names
26
+ * and returns the subset the agent judges composition-relevant. We only honor
27
+ * dirs that were actually offered (no injection), and any error from the agent
28
+ * falls back to the original selection — the critic can only ever ADD, never
29
+ * break resolution.
30
+ */
31
+ export declare function critiqueCandidates(all: FileLike[], selected: FileLike[], askDirs: (dirs: string[]) => Promise<string[]>): Promise<CritiqueResult>;
32
+ export {};
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Completeness critic for the candidate-file filter (design: the heuristic can
3
+ * miss a composition-layer directory whose name we didn't anticipate). We show
4
+ * the agent the DIRECTORIES it did NOT pick — names only, no file contents, so
5
+ * it's cheap — and let it flag ones that look promising from the path alone.
6
+ * Flagged dirs' files are folded into the prompt sample.
7
+ *
8
+ * This only widens the AUTHORING-PROMPT sample; the parser already runs over
9
+ * every file at runtime, so this is purely "help the agent see the convention",
10
+ * never a correctness dependency.
11
+ */
12
+ function dirOf(path) {
13
+ const i = path.lastIndexOf('/');
14
+ return i === -1 ? '' : path.slice(0, i);
15
+ }
16
+ /** Directories present in `all` but with no file in `selected`, sorted+deduped. */
17
+ export function uncoveredDirectories(all, selected) {
18
+ const coveredDirs = new Set(selected.map((f) => dirOf(f.path)));
19
+ const out = new Set();
20
+ for (const f of all) {
21
+ const d = dirOf(f.path);
22
+ if (d !== '' && !coveredDirs.has(d))
23
+ out.add(d);
24
+ }
25
+ return [...out].sort();
26
+ }
27
+ /** Union `selected` with every `all` file whose directory is in `chosenDirs`. */
28
+ export function expandCandidatesByDirs(all, selected, chosenDirs) {
29
+ const chosen = new Set(chosenDirs);
30
+ const seen = new Set(selected.map((f) => f.path));
31
+ const out = [...selected];
32
+ for (const f of all) {
33
+ if (chosen.has(dirOf(f.path)) && !seen.has(f.path)) {
34
+ seen.add(f.path);
35
+ out.push(f);
36
+ }
37
+ }
38
+ return out;
39
+ }
40
+ /**
41
+ * Run the completeness critic. `askDirs` receives the uncovered directory names
42
+ * and returns the subset the agent judges composition-relevant. We only honor
43
+ * dirs that were actually offered (no injection), and any error from the agent
44
+ * falls back to the original selection — the critic can only ever ADD, never
45
+ * break resolution.
46
+ */
47
+ export async function critiqueCandidates(all, selected, askDirs) {
48
+ const uncovered = uncoveredDirectories(all, selected);
49
+ if (uncovered.length === 0)
50
+ return { files: selected, addedDirs: [] };
51
+ let chosen;
52
+ try {
53
+ chosen = await askDirs(uncovered);
54
+ }
55
+ catch {
56
+ return { files: selected, addedDirs: [] };
57
+ }
58
+ const offered = new Set(uncovered);
59
+ const addedDirs = [...new Set(chosen)].filter((d) => offered.has(d)).sort();
60
+ if (addedDirs.length === 0)
61
+ return { files: selected, addedDirs: [] };
62
+ return { files: expandCandidatesByDirs(all, selected, addedDirs), addedDirs };
63
+ }
@@ -0,0 +1,34 @@
1
+ export declare const CANDIDATE_NAME_PATTERNS: RegExp[];
2
+ export declare const CANDIDATE_CONTENT_MARKERS: string[];
3
+ export declare const CANDIDATE_TOKEN_BUDGET = 6000;
4
+ /**
5
+ * Token ceiling for the candidate set INLINED into a single agent prompt.
6
+ * Sized to stay under a 200k-context model once the agent's own system prompt
7
+ * + tool definitions (~105k observed) are added, so a large design system
8
+ * (hundreds of components → many matched files) can't overflow the request and
9
+ * fail resolution. Files beyond the budget are dropped (with a warning), not
10
+ * silently truncated. Distinct from `CANDIDATE_TOKEN_BUDGET`, which sizes the
11
+ * unused per-batch chunking.
12
+ */
13
+ export declare const PROMPT_CANDIDATE_TOKEN_BUDGET = 80000;
14
+ export type CandidateFile = {
15
+ path: string;
16
+ content: string;
17
+ };
18
+ export type SelectedCandidate = CandidateFile & {
19
+ reason: string;
20
+ };
21
+ export declare function selectCandidateFiles(files: CandidateFile[]): SelectedCandidate[];
22
+ /**
23
+ * Cap a candidate set to what fits in a single agent prompt (see
24
+ * `PROMPT_CANDIDATE_TOKEN_BUDGET`). Files are kept smallest-first so the budget
25
+ * admits the most declarations; deterministic tie-break by path. Returns the
26
+ * kept files plus the paths dropped so the caller can warn (silent truncation
27
+ * would read as "resolved everything" when it didn't).
28
+ */
29
+ export declare function capCandidatesToPromptBudget<T extends CandidateFile>(files: T[], budget?: number): {
30
+ kept: T[];
31
+ dropped: T[];
32
+ };
33
+ export declare function sliceDeclarationRegions(content: string, markers?: string[], window?: number): string[];
34
+ export declare function batchCandidates(files: CandidateFile[], budget?: number): CandidateFile[][];