pi-crew 0.9.67 → 0.10.1
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/CHANGELOG.md +172 -0
- package/agents/analyst.md +1 -1
- package/agents/cold-verifier.md +3 -1
- package/agents/critic.md +1 -1
- package/agents/executor.md +1 -1
- package/agents/explorer.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/agents/writer.md +1 -1
- package/dist/index.mjs +65847 -60331
- package/docs/README.md +2 -0
- package/docs/actions-reference.md +31 -0
- package/docs/commands-reference.md +17 -6
- package/docs/resource-formats.md +13 -0
- package/package.json +9 -6
- package/schema.json +15 -1
- package/scripts/resource-sampler.mjs +36 -2
- package/skills/requirements-to-task-packet/SKILL.md +26 -0
- package/skills/widget-rendering/SKILL.md +7 -7
- package/src/agents/agent-config.ts +36 -14
- package/src/agents/discover-agents.ts +23 -14
- package/src/config/config-merge.ts +183 -0
- package/src/config/config-validation.ts +681 -0
- package/src/config/config.ts +22 -864
- package/src/config/defaults.ts +35 -1
- package/src/config/drift-detector.ts +1 -1
- package/src/config/env-vars.ts +691 -0
- package/src/config/role-tools.ts +8 -8
- package/src/config/sanitize-project-config.ts +172 -0
- package/src/config/types.ts +30 -0
- package/src/extension/async-notifier.ts +25 -2
- package/src/extension/crew-vibes/config.ts +2 -1
- package/src/extension/plan-orchestrate.ts +132 -0
- package/src/extension/registration/commands/dashboard.ts +158 -0
- package/src/extension/registration/commands/index.ts +35 -0
- package/src/extension/registration/commands/manage.ts +303 -0
- package/src/extension/registration/commands/run.ts +214 -0
- package/src/extension/registration/commands/shared.ts +631 -0
- package/src/extension/registration/commands/status.ts +60 -0
- package/src/extension/registration/commands.ts +13 -1224
- package/src/extension/registration/lifecycle-handlers.ts +126 -15
- package/src/extension/registration/subagent-tools.ts +218 -9
- package/src/extension/rpc-hmac.ts +5 -3
- package/src/extension/team-tool/api/heartbeat.ts +47 -10
- package/src/extension/team-tool/api/plan-approval.ts +9 -0
- package/src/extension/team-tool/api/task-claims.ts +109 -40
- package/src/extension/team-tool/cancel.ts +84 -50
- package/src/extension/team-tool/dispatch/index.ts +1 -0
- package/src/extension/team-tool/dispatch/run.ts +4 -1
- package/src/extension/team-tool/doctor.ts +103 -1
- package/src/extension/team-tool/orchestrate.ts +66 -1
- package/src/extension/team-tool/plans.ts +192 -0
- package/src/extension/team-tool/respond.ts +197 -65
- package/src/extension/team-tool/run-deadline.ts +16 -1
- package/src/extension/team-tool/run-intent.ts +63 -0
- package/src/extension/team-tool/run.ts +48 -18
- package/src/extension/team-tool/status.ts +84 -26
- package/src/extension/team-tool.ts +11 -2
- package/src/hooks/registry.ts +1 -6
- package/src/i18n.ts +9 -0
- package/src/prompt/prompt-runtime.ts +521 -2
- package/src/prompt/worker-events-channel.ts +173 -0
- package/src/runtime/README.md +8 -8
- package/src/runtime/async-runner.ts +7 -3
- package/src/runtime/background-runner.ts +42 -14
- package/src/runtime/broker/broker-issuer.ts +9 -2
- package/src/runtime/broker/crew-broker-tokens.ts +43 -6
- package/src/runtime/broker/crew-broker.ts +838 -10
- package/src/runtime/broker/wait-status-cache.ts +157 -0
- package/src/runtime/budget-enforcement.ts +281 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
- package/src/runtime/child-pi/child-pi-timers.ts +324 -0
- package/src/runtime/child-pi/child-pi.ts +97 -201
- package/src/runtime/child-pi/mock-fixtures.ts +16 -2
- package/src/runtime/crew-agent-records.ts +259 -14
- package/src/runtime/delegate-spawn.ts +148 -0
- package/src/runtime/deterministic-ast.ts +2 -1
- package/src/runtime/dispatch-batch.ts +945 -0
- package/src/runtime/finalize-run.ts +557 -0
- package/src/runtime/goal-workflow/adaptive-plan.ts +87 -13
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +3 -2
- package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
- package/src/runtime/group-join.ts +11 -125
- package/src/runtime/live-session/live-session-runtime.ts +26 -1
- package/src/runtime/merge-gate.ts +7 -1
- package/src/runtime/merge-loop.ts +130 -0
- package/src/runtime/model/model-budget-summary.ts +53 -0
- package/src/runtime/model/model-fallback.ts +32 -2
- package/src/runtime/model/pi-args.ts +10 -0
- package/src/runtime/model/provider-extensions.ts +10 -0
- package/src/runtime/orphan-worker-registry.ts +1 -1
- package/src/runtime/output/output-validator.ts +45 -0
- package/src/runtime/parent-guard.ts +3 -1
- package/src/runtime/peer-dep.ts +2 -1
- package/src/runtime/per-write-validator.ts +0 -5
- package/src/runtime/pi-spawn.ts +61 -15
- package/src/runtime/plan-approval.ts +125 -0
- package/src/runtime/plan-replan.ts +151 -0
- package/src/runtime/recovery/checkpoint.ts +0 -18
- package/src/runtime/recovery/crash-recovery.ts +95 -46
- package/src/runtime/scheduler-context.ts +98 -0
- package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
- package/src/runtime/scheduling/global-worker-cap.ts +2 -1
- package/src/runtime/scheduling/nested-slots.ts +70 -0
- package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
- package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
- package/src/runtime/scratchpad/README.md +10 -5
- package/src/runtime/scratchpad/guest.ts +103 -2
- package/src/runtime/scratchpad/protocol.ts +13 -1
- package/src/runtime/scratchpad/transform.ts +206 -12
- package/src/runtime/settings-store.ts +219 -0
- package/src/runtime/spawn-policy.ts +217 -0
- package/src/runtime/stale-reconciler.ts +87 -6
- package/src/runtime/subagent-manager.ts +25 -1
- package/src/runtime/task-output-context.ts +230 -9
- package/src/runtime/task-packet.ts +23 -1
- package/src/runtime/task-runner/child-executor.ts +106 -7
- package/src/runtime/task-runner/post-execution.ts +125 -1
- package/src/runtime/task-runner/pre-execution.ts +39 -1
- package/src/runtime/task-runner/prompt-builder.ts +51 -1
- package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
- package/src/runtime/task-runner/spec-evidence.ts +403 -0
- package/src/runtime/task-runner/state-helpers.ts +26 -24
- package/src/runtime/task-runner.ts +11 -0
- package/src/runtime/team-runner.ts +129 -1671
- package/src/runtime/verification/spec-sandbox.ts +255 -0
- package/src/runtime/verification/verification-gates.ts +3 -2
- package/src/runtime/verification/verification-worktree.ts +2 -1
- package/src/runtime/workflow-phase-advance.ts +100 -0
- package/src/runtime/workspace-tree.ts +9 -0
- package/src/schema/config-schema.ts +65 -25
- package/src/schema/sensitive-config-paths.ts +64 -0
- package/src/schema/team-tool-schema.ts +13 -3
- package/src/state/README.md +4 -10
- package/src/state/atomic-write.ts +20 -3
- package/src/state/contracts.ts +38 -0
- package/src/state/coordination/mailbox.ts +12 -2
- package/src/state/event-log/cursor.ts +223 -0
- package/src/state/event-log/event-log-rotation.ts +12 -4
- package/src/state/event-log/event-log.ts +152 -369
- package/src/state/event-log/sequence-cache.ts +373 -0
- package/src/state/event-log/worker-atomic-writer.ts +2 -1
- package/src/state/stores/active-run-registry.ts +3 -2
- package/src/state/stores/manifest-io.ts +237 -0
- package/src/state/stores/ownership-map.ts +162 -0
- package/src/state/stores/plan-store.ts +241 -0
- package/src/state/stores/run-cache.ts +0 -90
- package/src/state/stores/spec-store.ts +189 -0
- package/src/state/stores/state-store.ts +66 -229
- package/src/state/types.ts +197 -0
- package/src/ui/dashboard-panes/plan-pane.ts +136 -0
- package/src/ui/dashboard-panes/progress-pane.ts +6 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
- package/src/ui/heartbeat-aggregator.ts +9 -1
- package/src/ui/keybinding-map.ts +54 -13
- package/src/ui/powerbar-publisher.ts +52 -1
- package/src/ui/run-dashboard.ts +31 -5
- package/src/ui/run-snapshot-cache.ts +57 -30
- package/src/ui/snapshot-types.ts +6 -1
- package/src/ui/widget/widget-renderer.ts +9 -1
- package/src/utils/file-coalescer.ts +0 -4
- package/src/utils/fs-errno.ts +66 -0
- package/src/utils/fs-watch.ts +1 -1
- package/src/utils/internal-error.ts +3 -1
- package/src/utils/paths.ts +11 -3
- package/src/utils/task-name-generator.ts +1 -8
- package/src/workflows/discover-workflows.ts +19 -2
- package/src/workflows/validate-workflow.ts +7 -1
- package/src/workflows/workflow-config.ts +10 -0
- package/src/workflows/workflow-serializer.ts +3 -0
- package/src/worktree/worktree-manager.ts +22 -0
- package/src/agents/agent-search.ts +0 -98
- package/src/benchmark/benchmark-runner.ts +0 -313
- package/src/benchmark/feedback-loop.ts +0 -73
- package/src/config/resilient-parser.ts +0 -117
- package/src/extension/crew-vibes/cat-frames.ts +0 -18
- package/src/extension/result-watcher.ts +0 -139
- package/src/observability/exporters/prometheus-exporter.ts +0 -54
- package/src/observability/metric-retention.ts +0 -64
- package/src/runtime/compaction/compaction-summary.ts +0 -278
- package/src/runtime/errors/crew-errors.ts +0 -162
- package/src/runtime/live-session/intercom-bridge.ts +0 -187
- package/src/runtime/loop-gates.ts +0 -128
- package/src/runtime/metric-parser.ts +0 -36
- package/src/runtime/output/stream-preview.ts +0 -184
- package/src/runtime/output/tool-progress.ts +0 -278
- package/src/runtime/phase-tracker.ts +0 -385
- package/src/runtime/pipeline-runner.ts +0 -523
- package/src/runtime/process/process-lifecycle.ts +0 -491
- package/src/runtime/recovery/retry-runner.ts +0 -330
- package/src/runtime/run-drift.ts +0 -219
- package/src/runtime/scratchpad/snapshot-hmac.ts +0 -167
- package/src/runtime/task-quality.ts +0 -199
- package/src/runtime/task-runner/run-projection.ts +0 -128
- package/src/runtime/verification/post-checks.ts +0 -142
- package/src/state/coordination/schedule.ts +0 -166
- package/src/state/event-log/jsonl-writer.ts +0 -115
- package/src/state/hook-instinct-bridge.ts +0 -94
- package/src/state/hook-integrations.ts +0 -51
- package/src/state/session-state-map.ts +0 -51
- package/src/state/stores/blob-store.ts +0 -308
- package/src/state/stores/instinct-store.ts +0 -275
- package/src/state/stores/observation-store.ts +0 -176
- package/src/state/tiered-eval.ts +0 -480
- package/src/state/types-eval.ts +0 -58
- package/src/tools/safe-bash-extension.ts +0 -54
- package/src/tools/safe-bash.ts +0 -505
- package/src/ui/agent-management-overlay.ts +0 -160
- package/src/ui/crew-footer.ts +0 -102
- package/src/ui/crew-select-list.ts +0 -114
- package/src/ui/dashboard-panes/capability-pane.ts +0 -77
- package/src/ui/transcript-entries.ts +0 -256
- package/src/utils/conflict-detect.ts +0 -721
- package/src/utils/fingerprint.ts +0 -180
- package/src/utils/gh-protocol.ts +0 -556
- package/src/utils/project-detector.ts +0 -160
- package/src/utils/sse-parser.ts +0 -131
- package/src/workflows/cost-estimator.ts +0 -34
- package/src/workflows/intermediate-store.ts +0 -166
|
@@ -2,7 +2,7 @@ import type { AgentConfig } from "../../agents/agent-config.ts";
|
|
|
2
2
|
import type { CrewReliabilityConfig, CrewRuntimeConfig } from "../../config/types.ts";
|
|
3
3
|
import { appendEventAsync } from "../../state/event-log/event-log.ts";
|
|
4
4
|
import { writeArtifact } from "../../state/stores/artifact-store.ts";
|
|
5
|
-
import { saveRunTasksAsync, updateRunStatus } from "../../state/stores/state-store.ts";
|
|
5
|
+
import { loadRunManifestById, saveRunTasksAsync, updateRunStatus } from "../../state/stores/state-store.ts";
|
|
6
6
|
import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
7
7
|
import type { WorkflowStep } from "../../workflows/workflow-config.ts";
|
|
8
8
|
import { createWorkerHeartbeat, touchWorkerHeartbeat } from "../heartbeat/worker-heartbeat.ts";
|
|
@@ -115,17 +115,12 @@ export async function runCoalescedTaskGroup(input: CoalescedTaskGroupInput): Pro
|
|
|
115
115
|
heartbeatInFlight = true;
|
|
116
116
|
heartbeatPromise = (async () => {
|
|
117
117
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
...t,
|
|
125
|
-
heartbeat: touchWorkerHeartbeat(t.heartbeat ?? createWorkerHeartbeat(t.id), { alive: true }),
|
|
126
|
-
};
|
|
127
|
-
});
|
|
128
|
-
await saveRunTasksAsync(manifest, updatedTasks);
|
|
118
|
+
// R15-3: persist heartbeats for ONLY this group's tasks via a fresh
|
|
119
|
+
// disk read. The old closure `updatedTasks` is a dispatch-time
|
|
120
|
+
// snapshot of the FULL task array — a sibling cancelled on disk after
|
|
121
|
+
// dispatch could be un-cancelled by a late heartbeat save (the map
|
|
122
|
+
// only mutated group tasks, but the SAVE wrote the whole stale array).
|
|
123
|
+
await persistGroupHeartbeats(manifest, taskIds);
|
|
129
124
|
} catch {
|
|
130
125
|
// Run may have been pruned mid-dispatch — best-effort only.
|
|
131
126
|
} finally {
|
|
@@ -134,9 +129,12 @@ export async function runCoalescedTaskGroup(input: CoalescedTaskGroupInput): Pro
|
|
|
134
129
|
// save was in flight, re-write the (now-terminal) updatedTasks so
|
|
135
130
|
// our late snapshot cannot leave stale pre-terminal state on disk.
|
|
136
131
|
// Runs after our own save resolves, so it always lands last.
|
|
132
|
+
// R15-3: the repair now fresh-reads disk and re-applies ONLY the
|
|
133
|
+
// group's terminal state — the old full-array write could resurrect
|
|
134
|
+
// a sibling cancelled on disk (same flaw as the heartbeat save).
|
|
137
135
|
if (finalWriteStarted) {
|
|
138
136
|
try {
|
|
139
|
-
await
|
|
137
|
+
await repairGroupTerminalWrite(manifest, taskIds, updatedTasks);
|
|
140
138
|
} catch {
|
|
141
139
|
// best-effort repair — same swallow policy as the heartbeat.
|
|
142
140
|
}
|
|
@@ -298,6 +296,59 @@ export async function runCoalescedTaskGroup(input: CoalescedTaskGroupInput): Pro
|
|
|
298
296
|
return { manifest: updatedManifest, tasks: updatedTasks, taskIds, rawOutput, success };
|
|
299
297
|
}
|
|
300
298
|
|
|
299
|
+
/**
|
|
300
|
+
* R15-3: heartbeat save scoped to this coalesced group only.
|
|
301
|
+
*
|
|
302
|
+
* Previously the heartbeat wrote the dispatch-time closure `updatedTasks` —
|
|
303
|
+
* the FULL task array including sibling tasks outside this group. A sibling
|
|
304
|
+
* cancelled on disk after dispatch (external cancel / reconciler) could be
|
|
305
|
+
* un-cancelled ("resurrected") by a late heartbeat save, because the map only
|
|
306
|
+
* mutated group tasks but the SAVE wrote the whole stale array. Fix: fresh
|
|
307
|
+
* re-read tasks from disk (loadRunManifestById — the established fresh-read
|
|
308
|
+
* pattern used by team-runner) and write back ONLY the group's own task ids
|
|
309
|
+
* with heartbeats touched; sibling tasks are preserved untouched from the
|
|
310
|
+
* fresh disk read. Keeps the FIND-06 'never replace terminal state' guard.
|
|
311
|
+
*/
|
|
312
|
+
async function persistGroupHeartbeats(manifest: TeamRunManifest, taskIds: string[]): Promise<void> {
|
|
313
|
+
const fresh = loadRunManifestById(manifest.cwd, manifest.runId);
|
|
314
|
+
if (!fresh) return; // run pruned mid-dispatch — best-effort only.
|
|
315
|
+
const merged = fresh.tasks.map((t) => {
|
|
316
|
+
if (!taskIds.includes(t.id)) return t; // sibling tasks: preserved untouched.
|
|
317
|
+
// FIND-06 belt-and-suspenders: never replace terminal state or its
|
|
318
|
+
// resultArtifact with a heartbeat-only snapshot.
|
|
319
|
+
if (t.status === "completed" || t.status === "failed" || t.status === "cancelled") return t;
|
|
320
|
+
return {
|
|
321
|
+
...t,
|
|
322
|
+
heartbeat: touchWorkerHeartbeat(t.heartbeat ?? createWorkerHeartbeat(t.id), { alive: true }),
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
await saveRunTasksAsync(manifest, merged);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* R15-3: FIND-06 P1 repair — re-apply the group's terminal state after a late
|
|
330
|
+
* heartbeat save may have landed last. Fresh-reads disk so sibling tasks are
|
|
331
|
+
* preserved untouched; only the group's own terminal state from `updatedTasks`
|
|
332
|
+
* is re-applied, and a disk-terminal group task is never flipped (same
|
|
333
|
+
* belt-and-suspenders guard as the heartbeat).
|
|
334
|
+
*/
|
|
335
|
+
async function repairGroupTerminalWrite(manifest: TeamRunManifest, taskIds: string[], updatedTasks: TeamTaskState[]): Promise<void> {
|
|
336
|
+
const fresh = loadRunManifestById(manifest.cwd, manifest.runId);
|
|
337
|
+
if (!fresh) return; // run pruned mid-dispatch — best-effort only.
|
|
338
|
+
const groupById = new Map(updatedTasks.map((t) => [t.id, t] as const));
|
|
339
|
+
const merged = fresh.tasks.map((t) => {
|
|
340
|
+
if (!taskIds.includes(t.id)) return t; // sibling tasks: preserved untouched.
|
|
341
|
+
if (t.status === "completed" || t.status === "failed" || t.status === "cancelled") return t;
|
|
342
|
+
return groupById.get(t.id) ?? t;
|
|
343
|
+
});
|
|
344
|
+
await saveRunTasksAsync(manifest, merged);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** @internal 3.5 R15-3 test export — exercise the heartbeat/repair writes directly. */
|
|
348
|
+
export const __test__persistGroupHeartbeats = persistGroupHeartbeats;
|
|
349
|
+
/** @internal 3.5 R15-3 test export — exercise the terminal repair write directly. */
|
|
350
|
+
export const __test__repairGroupTerminalWrite = repairGroupTerminalWrite;
|
|
351
|
+
|
|
301
352
|
async function buildCoalescedPrompt(
|
|
302
353
|
manifest: TeamRunManifest,
|
|
303
354
|
step: WorkflowStep,
|
|
@@ -48,16 +48,6 @@ export function buildTaskGraphIndex(tasks: TeamTaskState[]): TaskGraphIndex {
|
|
|
48
48
|
return fresh;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/** Test/diagnostic helper — invalidate the WeakMap-backed index cache. Not
|
|
52
|
-
* used by production code paths; the WeakMap self-invalidates when a tasks
|
|
53
|
-
* array goes out of scope. Provided for parity with `clearStablePrefixCache`. */
|
|
54
|
-
export function clearTaskGraphIndexCache(): void {
|
|
55
|
-
// WeakMap has no `.clear()`; rely on GC. Exposed as a no-op stub so callers
|
|
56
|
-
// that import `clearStablePrefixCache` (also a no-op stub for symmetry) can
|
|
57
|
-
// adopt a parallel API if needed in the future. Documented as no-op rather
|
|
58
|
-
// than removed so the API stays discoverable.
|
|
59
|
-
}
|
|
60
|
-
|
|
61
51
|
function taskById(tasks: TeamTaskState[]): Map<string, TeamTaskState> {
|
|
62
52
|
return new Map(tasks.map((task) => [task.id, task]));
|
|
63
53
|
}
|
|
@@ -124,11 +124,16 @@ attempt N+1 worker (scratchpad-lifecycle.ts)
|
|
|
124
124
|
token), so this does not cross the existing boundary. An HMAC over the payload
|
|
125
125
|
is a Phase 2.5/3 hardening if artifacts ever land in a shared location.
|
|
126
126
|
|
|
127
|
-
>
|
|
128
|
-
>
|
|
129
|
-
>
|
|
130
|
-
>
|
|
131
|
-
>
|
|
127
|
+
> **REMOVED (2026-08-12):** `src/runtime/scratchpad/snapshot-hmac.ts` and its
|
|
128
|
+
> test were deleted. Decision (P3 of `docs/rlm-fixes-implementation-plan.md`):
|
|
129
|
+
> the HMAC threat is double-conditional — it only materializes when scratchpad
|
|
130
|
+
> has adoption (>0 cells; today: 0/83 runs) AND snapshots move to a
|
|
131
|
+
> shared/networked store (today: same-uid dev-machine). Hardening a
|
|
132
|
+
> 0-adoption surface that is itself at risk of removal (decision gate §3.3)
|
|
133
|
+
> is premature. The design is fully recorded in ADR
|
|
134
|
+
> `docs/decisions/2026-08-10-scratchpad-snapshot-hmac.md` (Superseded), so it
|
|
135
|
+
> can be re-added cleanly if/when both conditions hold. Until then the note
|
|
136
|
+
> below (v8.deserialize unauthenticated) remains exactly how the code behaves.
|
|
132
137
|
- **Secret-at-rest under a benign key name** persists as base64 for the run's
|
|
133
138
|
retention (structural redaction is key-name based, best-effort). A sanitized-
|
|
134
139
|
namespace policy is a Phase 2.5 concern.
|
|
@@ -188,6 +188,49 @@ console.trace = consoleErr;
|
|
|
188
188
|
|
|
189
189
|
const INTERNAL_BINDINGS = new Map<string, unknown>();
|
|
190
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Protected Node globals — must never be persistently shadowed by a cell.
|
|
193
|
+
*
|
|
194
|
+
* Because the scope proxy's `get` trap checks the namespace BEFORE globalThis,
|
|
195
|
+
* a cell writing `const process = 'poisoned'` (transformed to a namespace
|
|
196
|
+
* assignment) would otherwise poison EVERY later cell in the same engine
|
|
197
|
+
* (the namespace is a module-level singleton) AND survive snapshot→restore
|
|
198
|
+
* (a serializable shadow revives). Result: silent corruption — e.g.
|
|
199
|
+
* `typeof process.env` becomes 'undefined' with no error.
|
|
200
|
+
*
|
|
201
|
+
* We register the LIVE global for each protected name in INTERNAL_BINDINGS +
|
|
202
|
+
* the namespace, so (a) restore's re-install overwrites any revived shadow,
|
|
203
|
+
* and (b) each cell starts with the real global. Within-cell shadowing still
|
|
204
|
+
* works (the cell's own write wins for that cell only); it just cannot leak
|
|
205
|
+
* to other cells or across restarts. Verified: probe reproduced the bug
|
|
206
|
+
* pre-fix; see test/unit/runtime/scratchpad/guest-global-shadow.test.ts.
|
|
207
|
+
*/
|
|
208
|
+
const PROTECTED_GLOBALS: ReadonlyArray<string> = [
|
|
209
|
+
"process",
|
|
210
|
+
"Buffer",
|
|
211
|
+
"console",
|
|
212
|
+
"setTimeout",
|
|
213
|
+
"clearTimeout",
|
|
214
|
+
"setInterval",
|
|
215
|
+
"clearInterval",
|
|
216
|
+
"setImmediate",
|
|
217
|
+
"clearImmediate",
|
|
218
|
+
"queueMicrotask",
|
|
219
|
+
"structuredClone",
|
|
220
|
+
"AbortController",
|
|
221
|
+
"globalThis",
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
function resetProtectedGlobals(): void {
|
|
225
|
+
const g = globalThis as Record<string, unknown>;
|
|
226
|
+
for (const name of PROTECTED_GLOBALS) {
|
|
227
|
+
const live = g[name];
|
|
228
|
+
if (live === undefined) continue;
|
|
229
|
+
INTERNAL_BINDINGS.set(name, live);
|
|
230
|
+
namespace[name] = live;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
191
234
|
/** Pattern-12 nullish guard: refuse undefined/null BEFORE spawning. */
|
|
192
235
|
function assertShellArgNotNullish(name: string, value: unknown): void {
|
|
193
236
|
if (value === undefined || value === null) {
|
|
@@ -235,6 +278,9 @@ function installBootstrapBindings(): void {
|
|
|
235
278
|
// serialization, and so cells can call `sh(...)` directly.
|
|
236
279
|
INTERNAL_BINDINGS.set("sh", sh);
|
|
237
280
|
namespace.sh = sh;
|
|
281
|
+
// Overwrite any protected-global shadow (e.g. a revived `process='x'` from
|
|
282
|
+
// restore) with the live global — fixes the global shadow poisoning bug.
|
|
283
|
+
resetProtectedGlobals();
|
|
238
284
|
}
|
|
239
285
|
|
|
240
286
|
installBootstrapBindings();
|
|
@@ -246,14 +292,69 @@ const AsyncFunction = (async () => {}).constructor as new (...args: string[]) =>
|
|
|
246
292
|
|
|
247
293
|
const liveCells = new Map<string, CellContext>();
|
|
248
294
|
|
|
295
|
+
// ── P6: remap transformed error-stack lines back to the cell's source ───────
|
|
296
|
+
// V8 reports a thrown line as `N = bodyLine + 2` (1-based) — the AsyncFunction
|
|
297
|
+
// wrapper adds 2 prefix lines (the anonymous function line + the `with (...)`
|
|
298
|
+
// line) before the cell body. `lineMap` (from transformCell) maps body lines
|
|
299
|
+
// that RECEIVED A REPLACEMENT back to the source line the model wrote. Lines
|
|
300
|
+
// between replacements (unreplaced spans — e.g. a bare `throw`) are identity:
|
|
301
|
+
// sourceLine = entry.sourceLine + (bodyLine - entry.bodyLine), using the
|
|
302
|
+
// nearest replacement entry at or before the body line.
|
|
303
|
+
//
|
|
304
|
+
// SECURITY/robustness (P6 review finding): only lines that look like a real
|
|
305
|
+
// V8 frame (` at ... <anonymous>:N:C)`) are remapped. The `Error: <message>`
|
|
306
|
+
// line and any cell text containing `<anonymous>:N:C)` (e.g. an error message
|
|
307
|
+
// that echoes code) must be left byte-identical — a crafted message must never
|
|
308
|
+
// get its numbers rewritten.
|
|
309
|
+
function remapStackLines(stack: string, lineMap: { sourceLine: number; bodyLine: number }[]): string[] {
|
|
310
|
+
const lines = stack.split("\n");
|
|
311
|
+
const sorted = [...lineMap].sort((a, b) => a.bodyLine - b.bodyLine);
|
|
312
|
+
return lines.map((line) => {
|
|
313
|
+
// Real V8 frame only: ` at <anything> <anonymous>:N:C)` — anchored to
|
|
314
|
+
// the leading frame prefix and a parenthesized trailing position. The
|
|
315
|
+
// message line (`Error: ...`) does not start with ` at `, so it is
|
|
316
|
+
// never matched; a crafted message containing `<anonymous>:N:C)` is
|
|
317
|
+
// likewise safe because it lacks the frame prefix.
|
|
318
|
+
const m = line.match(/^\s+at .*<anonymous>:(\d+):(\d+)\)/);
|
|
319
|
+
if (!m) return line;
|
|
320
|
+
const reported = Number(m[1]);
|
|
321
|
+
const bodyLine = reported - 2; // wrapper prefix offset (verified by probe)
|
|
322
|
+
// nearest replacement entry at or before bodyLine (lower bound)
|
|
323
|
+
let lo = 0;
|
|
324
|
+
let hi = sorted.length - 1;
|
|
325
|
+
let entry: { sourceLine: number; bodyLine: number } | undefined;
|
|
326
|
+
while (lo <= hi) {
|
|
327
|
+
const mid = (lo + hi) >> 1;
|
|
328
|
+
if (sorted[mid].bodyLine <= bodyLine) {
|
|
329
|
+
entry = sorted[mid];
|
|
330
|
+
lo = mid + 1;
|
|
331
|
+
} else {
|
|
332
|
+
hi = mid - 1;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (!entry) return line; // before any replacement — leave as-is
|
|
336
|
+
const sourceLine = entry.sourceLine + (bodyLine - entry.bodyLine);
|
|
337
|
+
const col = m[2];
|
|
338
|
+
return line.replace(/<anonymous>:\d+:\d+/, `<anonymous>:${sourceLine}:${col}`);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
249
342
|
async function runCell(cellId: string, code: string): Promise<void> {
|
|
250
343
|
const ctx = makeCellContext(cellId);
|
|
251
344
|
activeCell = ctx;
|
|
252
345
|
liveCells.set(cellId, ctx);
|
|
346
|
+
// Reset protected globals so a prior cell's shadow (e.g. `const process='x'`)
|
|
347
|
+
// cannot poison this cell — shadowing stays local to the cell that did it
|
|
348
|
+
// (the namespace is a module-level singleton shared across cells).
|
|
349
|
+
resetProtectedGlobals();
|
|
253
350
|
|
|
254
351
|
let done: GuestToHostMessage;
|
|
352
|
+
// P6: lineMap from transformCell is needed in BOTH try (to build the
|
|
353
|
+
// wrapper) and catch (to remap the error stack), so hoist it here.
|
|
354
|
+
let lineMap: { sourceLine: number; bodyLine: number }[] = [];
|
|
255
355
|
try {
|
|
256
|
-
const { body } = transformCell(code, { ctxName: CTX_NAME });
|
|
356
|
+
const { body, lineMap: lm } = transformCell(code, { ctxName: CTX_NAME });
|
|
357
|
+
lineMap = lm;
|
|
257
358
|
// Sloppy-mode wrapper so `with` is legal (AsyncFunction bodies are always
|
|
258
359
|
// sloppy, even inside a strict ESM strip-types module); async for
|
|
259
360
|
// top-level await.
|
|
@@ -271,7 +372,7 @@ async function runCell(cellId: string, code: string): Promise<void> {
|
|
|
271
372
|
type: "done",
|
|
272
373
|
cellId,
|
|
273
374
|
status: ctx.aborted ? "aborted" : "error",
|
|
274
|
-
error: { name: err.name, message: err.message, stack: (err.stack ?? "")
|
|
375
|
+
error: { name: err.name, message: err.message, stack: remapStackLines(err.stack ?? "", lineMap) },
|
|
275
376
|
};
|
|
276
377
|
} finally {
|
|
277
378
|
if (activeCell === ctx) activeCell = undefined;
|
|
@@ -45,7 +45,19 @@ export interface GuestToHost {
|
|
|
45
45
|
error?: { name: string; message: string; stack: string[] };
|
|
46
46
|
};
|
|
47
47
|
pong: { type: "pong"; id: string };
|
|
48
|
-
host_request: {
|
|
48
|
+
host_request: {
|
|
49
|
+
// RESERVED for future host bridge (rlm-deep-review-2026-08-12.md §5.2F /
|
|
50
|
+
// J2): guest-side cells would request host services (tools.read,
|
|
51
|
+
// tools.grep) so data enters the namespace WITHOUT crossing the
|
|
52
|
+
// transcript — pi-rlm's core token-saving value proposition. Declared
|
|
53
|
+
// here so the protocol type is stable, but NOT YET WIRED: engine.ts and
|
|
54
|
+
// guest.ts have no host_request dispatcher/handler. Blocked on scratchpad
|
|
55
|
+
// adoption > 0 (do not add a host execution surface nobody calls).
|
|
56
|
+
type: "host_request";
|
|
57
|
+
id: string;
|
|
58
|
+
requestType: string;
|
|
59
|
+
payload: Record<string, unknown>;
|
|
60
|
+
};
|
|
49
61
|
snapshot_result: {
|
|
50
62
|
type: "snapshot_result";
|
|
51
63
|
id: string;
|
|
@@ -31,6 +31,15 @@ export interface TransformedCell {
|
|
|
31
31
|
body: string;
|
|
32
32
|
/** Top-level names this cell binds into the namespace. */
|
|
33
33
|
declaredNames: string[];
|
|
34
|
+
/**
|
|
35
|
+
* P6: line-based position map — for each body line (1-based), the
|
|
36
|
+
* corresponding SOURCE line (1-based) the model wrote. Used to remap the
|
|
37
|
+
* V8-reported error line (relative to `body`) back to the cell's original
|
|
38
|
+
* line so stack traces point at the source the user wrote, not the
|
|
39
|
+
* transformed body. Built WITHOUT a source-map dependency by tracking
|
|
40
|
+
* replacement newline shifts against the source (acorn `locations`).
|
|
41
|
+
*/
|
|
42
|
+
lineMap: { sourceLine: number; bodyLine: number }[];
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
export interface TransformOptions {
|
|
@@ -41,8 +50,16 @@ export interface TransformOptions {
|
|
|
41
50
|
// esbuild's transform strips types but never drops side-effect-free trailing
|
|
42
51
|
// expressions (no DCE in transform mode) — the exact thing we capture as the
|
|
43
52
|
// cell result.
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
// P6: `sourcemap: 'inline'` so we can map the stripped `js` lines back to the
|
|
54
|
+
// `rewritten` (pre-strip) lines — esbuild does NOT preserve line numbers
|
|
55
|
+
// across type stripping (probe: 7 input lines → 5 output lines). The inline
|
|
56
|
+
// sourcemap is stripped from the output (it would otherwise leak into the cell
|
|
57
|
+
// body as a trailing comment); we only keep the decoded line map.
|
|
58
|
+
function stripTypes(code: string): { code: string; lineMap: number[] } {
|
|
59
|
+
const out = transformSync(code, { loader: "ts", sourcemap: "inline" });
|
|
60
|
+
const lineMap = esbuildLineMap(out.code);
|
|
61
|
+
const codeWithoutMap = out.code.replace(/\/\/# sourceMappingURL=data:application\/json;base64,[A-Za-z0-9+/=]+\s*$/, "");
|
|
62
|
+
return { code: codeWithoutMap, lineMap };
|
|
46
63
|
}
|
|
47
64
|
|
|
48
65
|
// ── top-level import extraction ──────────────────────────────────────────────
|
|
@@ -271,14 +288,147 @@ function variableReplacement(decl: VariableDeclaration, source: string): string
|
|
|
271
288
|
return statements.join(" ");
|
|
272
289
|
}
|
|
273
290
|
|
|
291
|
+
// ── P6: line-based position map ─────────────────────────────────────────────
|
|
292
|
+
// V8 reports a cell error's line as 1-based relative to the TRANSFORMED `body`.
|
|
293
|
+
// The model wrote `code`; between them sit 3 transforms:
|
|
294
|
+
// 1. import pre-rewrite (acorn) — collapses multi-line imports to 1 line
|
|
295
|
+
// 2. esbuild strip-types — collapses type annotations/signatures
|
|
296
|
+
// 3. declaration → assignment + trailing-expr capture — may add/remove lines
|
|
297
|
+
// We build a map { sourceLine (1-based, in `code`) → bodyLine (1-based) } by
|
|
298
|
+
// tracking line deltas across each stage with acorn `locations` + newline
|
|
299
|
+
// counts. No source-map dependency (probe: esbuild strip-types does NOT
|
|
300
|
+
// preserve lines — 7→5 — so a naive single-layer sourcemap would be wrong).
|
|
301
|
+
|
|
302
|
+
function countNewlines(s: string): number {
|
|
303
|
+
let n = 0;
|
|
304
|
+
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) === 10) n++;
|
|
305
|
+
return n;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** 1-based line of `pos` (char offset) in `text`. */
|
|
309
|
+
function lineAt(text: string, pos: number): number {
|
|
310
|
+
let line = 1;
|
|
311
|
+
for (let i = 0; i < pos && i < text.length; i++) if (text.charCodeAt(i) === 10) line++;
|
|
312
|
+
return line;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── P6: esbuild inline sourcemap → line map (js → rewritten) ───────────────
|
|
316
|
+
// esbuild `sourcemap: 'inline'` emits a base64 VLQ sourcemap whose `mappings`
|
|
317
|
+
// field encodes, per generated line, segments mapping back to source
|
|
318
|
+
// positions. We only need LINE fidelity (column is overkill for the bug), so
|
|
319
|
+
// we decode each generated line's first segment's source line. VLQ is the
|
|
320
|
+
// standard base64-variable-length encoding (no dependency needed).
|
|
321
|
+
|
|
322
|
+
const BASE64: string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
323
|
+
const VLQ: Map<string, number> = new Map([...BASE64].map((c, i) => [c, i]));
|
|
324
|
+
|
|
325
|
+
function decodeVlqSegment(segment: string): number[] {
|
|
326
|
+
const values: number[] = [];
|
|
327
|
+
let shift = 0;
|
|
328
|
+
let value = 0;
|
|
329
|
+
for (let i = 0; i < segment.length; i++) {
|
|
330
|
+
const digit = VLQ.get(segment[i]);
|
|
331
|
+
if (digit === undefined) break;
|
|
332
|
+
value |= (digit & 31) << shift;
|
|
333
|
+
if (digit & 32) {
|
|
334
|
+
shift += 5;
|
|
335
|
+
} else {
|
|
336
|
+
values.push(value & 1 ? -(value >>> 1) : value >>> 1);
|
|
337
|
+
value = 0;
|
|
338
|
+
shift = 0;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return values;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Decode an esbuild inline sourcemap's `mappings` into an array where
|
|
346
|
+
* `lineMapGenerated[generatedLine-1]` = the source LINE (1-based) that
|
|
347
|
+
* generated line originates from (first segment per generated line; falls back
|
|
348
|
+
* to the previous mapped line for continuation lines).
|
|
349
|
+
*/
|
|
350
|
+
function esbuildLineMap(codeWithInlineMap: string): number[] {
|
|
351
|
+
const m = codeWithInlineMap.match(/\/\/# sourceMappingURL=data:application\/json;base64,([A-Za-z0-9+/=]+)/);
|
|
352
|
+
if (!m) return [];
|
|
353
|
+
let sm: { mappings?: string };
|
|
354
|
+
try {
|
|
355
|
+
sm = JSON.parse(Buffer.from(m[1], "base64").toString("utf8")) as { mappings?: string };
|
|
356
|
+
} catch {
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
if (!sm.mappings) return [];
|
|
360
|
+
const result: number[] = [];
|
|
361
|
+
let srcLine = 1;
|
|
362
|
+
let lastSrcLine = 1;
|
|
363
|
+
for (const genLine of sm.mappings.split(";")) {
|
|
364
|
+
const segs = genLine.split(",").filter(Boolean);
|
|
365
|
+
if (segs.length > 0) {
|
|
366
|
+
const vals = decodeVlqSegment(segs[0]);
|
|
367
|
+
// values: [genColDelta, srcIdxDelta, srcLineDelta, srcColDelta, ...]
|
|
368
|
+
if (vals.length >= 3) {
|
|
369
|
+
srcLine += vals[2];
|
|
370
|
+
lastSrcLine = srcLine;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
result.push(lastSrcLine);
|
|
374
|
+
}
|
|
375
|
+
return result;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Build a `code`-line → `js`-line map across the import pre-rewrite.
|
|
380
|
+
* For each import replacement, its `code` span [codeStart, codeEnd] collapses
|
|
381
|
+
* to `js` lines [jsStart, jsEnd] (usually 1). Lines outside any replacement
|
|
382
|
+
* are identity-shifted by the cumulative delta.
|
|
383
|
+
*/
|
|
384
|
+
function buildImportLineMap(
|
|
385
|
+
code: string,
|
|
386
|
+
rewritten: string,
|
|
387
|
+
importSpans: { codeStart: number; codeEnd: number; jsStart: number; jsEnd: number }[],
|
|
388
|
+
): (src: number) => number {
|
|
389
|
+
if (importSpans.length === 0) return (src) => src;
|
|
390
|
+
// P6 (final): map a `rewritten` (post-import) LINE to the original `code`
|
|
391
|
+
// line. `rewritten` = code with each import statement replaced by its
|
|
392
|
+
// (usually 1-line) dynamic-import fragment. We walk both strings tracking
|
|
393
|
+
// newline counts: between imports the line numbers advance identically;
|
|
394
|
+
// inside an import span, all its code lines map to the replacement's first
|
|
395
|
+
// rewritten line (collapsed).
|
|
396
|
+
const rewrittenLines: string[] = rewritten.split("\n");
|
|
397
|
+
const codeLines: string[] = code.split("\n");
|
|
398
|
+
const map = new Map<number, number>(); // rewrittenLine → codeLine (1-based)
|
|
399
|
+
let codeIdx = 1;
|
|
400
|
+
let rewIdx = 1;
|
|
401
|
+
for (const span of importSpans) {
|
|
402
|
+
// Advance both to the span start, emitting identity mapping for the
|
|
403
|
+
// untouched lines between the previous span end and this span start.
|
|
404
|
+
const spanStartCodeLine = lineAt(code, span.codeStart);
|
|
405
|
+
const spanStartRewLine = lineAt(rewritten, span.jsStart);
|
|
406
|
+
for (; codeIdx < spanStartCodeLine && rewIdx < spanStartRewLine; codeIdx++, rewIdx++) {
|
|
407
|
+
map.set(rewIdx, codeIdx);
|
|
408
|
+
}
|
|
409
|
+
// Lines inside the import span (in code) collapse to the replacement's
|
|
410
|
+
// single starting rewritten line.
|
|
411
|
+
const spanEndCodeLine = lineAt(code, span.codeEnd);
|
|
412
|
+
for (; codeIdx <= spanEndCodeLine; codeIdx++) {
|
|
413
|
+
map.set(spanStartRewLine, spanStartCodeLine);
|
|
414
|
+
}
|
|
415
|
+
rewIdx = spanStartRewLine + 1;
|
|
416
|
+
}
|
|
417
|
+
// Tail: identity from wherever we stopped.
|
|
418
|
+
for (; rewIdx <= rewrittenLines.length; rewIdx++, codeIdx++) {
|
|
419
|
+
map.set(rewIdx, Math.min(codeIdx, codeLines.length));
|
|
420
|
+
}
|
|
421
|
+
return (rewLine: number) => {
|
|
422
|
+
const mapped = map.get(rewLine);
|
|
423
|
+
return mapped ?? Math.min(rewLine, codeLines.length);
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
274
427
|
export function transformCell(code: string, options: TransformOptions = {}): TransformedCell {
|
|
275
428
|
const ctxName = options.ctxName ?? "__ctx";
|
|
276
|
-
|
|
277
|
-
// Top-level imports are rewritten into awaited dynamic imports BEFORE type
|
|
278
|
-
// stripping: esbuild elides unused imports, and the reference behaviour
|
|
279
|
-
// binds every imported name into the namespace so it persists across cells.
|
|
280
429
|
let importDeclared: string[] = [];
|
|
281
430
|
let rewritten = code;
|
|
431
|
+
const importSpans: { codeStart: number; codeEnd: number; jsStart: number; jsEnd: number }[] = [];
|
|
282
432
|
try {
|
|
283
433
|
const tokens = lexTopLevel(code);
|
|
284
434
|
const imports = findImportStatements(tokens);
|
|
@@ -286,11 +436,21 @@ export function transformCell(code: string, options: TransformOptions = {}): Tra
|
|
|
286
436
|
importDeclared = [];
|
|
287
437
|
const pieces: string[] = [];
|
|
288
438
|
let cursor = 0;
|
|
439
|
+
let jsCursor = 0;
|
|
289
440
|
for (const stmt of imports) {
|
|
290
|
-
|
|
441
|
+
const pre = rewritten.slice(cursor, stmt.start);
|
|
442
|
+
pieces.push(pre);
|
|
443
|
+
jsCursor += pre.length;
|
|
291
444
|
const rewrite = rewriteImport(code, stmt, tokens);
|
|
292
445
|
importDeclared.push(...rewrite.declaredNames);
|
|
293
446
|
pieces.push(rewrite.replacement);
|
|
447
|
+
importSpans.push({
|
|
448
|
+
codeStart: stmt.start,
|
|
449
|
+
codeEnd: stmt.end,
|
|
450
|
+
jsStart: jsCursor,
|
|
451
|
+
jsEnd: jsCursor + rewrite.replacement.length - 1,
|
|
452
|
+
});
|
|
453
|
+
jsCursor += rewrite.replacement.length;
|
|
294
454
|
cursor = stmt.end;
|
|
295
455
|
}
|
|
296
456
|
pieces.push(rewritten.slice(cursor));
|
|
@@ -301,8 +461,15 @@ export function transformCell(code: string, options: TransformOptions = {}): Tra
|
|
|
301
461
|
// the pre-rewrite; type stripping below surfaces the error.
|
|
302
462
|
}
|
|
303
463
|
|
|
304
|
-
const js = stripTypes(rewritten);
|
|
305
|
-
const
|
|
464
|
+
const { code: js, lineMap: esbLineMap } = stripTypes(rewritten);
|
|
465
|
+
const rewToCode = buildImportLineMap(code, rewritten, importSpans);
|
|
466
|
+
const program: Program = parse(js, {
|
|
467
|
+
ecmaVersion: "latest",
|
|
468
|
+
sourceType: "module",
|
|
469
|
+
allowAwaitOutsideFunction: true,
|
|
470
|
+
// P6: locations needed to know each top-level node's source lines.
|
|
471
|
+
locations: true,
|
|
472
|
+
});
|
|
306
473
|
|
|
307
474
|
const declaredNames: string[] = [];
|
|
308
475
|
const replacements: { start: number; end: number; text: string }[] = [];
|
|
@@ -353,11 +520,38 @@ export function transformCell(code: string, options: TransformOptions = {}): Tra
|
|
|
353
520
|
replacements.sort((a, b) => a.start - b.start);
|
|
354
521
|
let body = "";
|
|
355
522
|
let cursor = 0;
|
|
523
|
+
// P6: build bodyLine → sourceLine map while splicing. `body` is what V8
|
|
524
|
+
// reports error lines against; `js` is the esbuild-stripped source; the map
|
|
525
|
+
// composes: bodyLine → jsLine (via newline deltas) → rewrittenLine (via
|
|
526
|
+
// esbuild inline sourcemap) → codeLine (via import pre-rewrite).
|
|
527
|
+
const lineMap: { sourceLine: number; bodyLine: number }[] = [];
|
|
528
|
+
let bodyLine = 1;
|
|
529
|
+
let jsLine = 1;
|
|
530
|
+
// jsLineOfBodyStart maps the js line at each body boundary; we track the
|
|
531
|
+
// cumulative js-line delta as body lines are emitted.
|
|
356
532
|
for (const replacement of replacements) {
|
|
357
|
-
|
|
533
|
+
const pre = js.slice(cursor, replacement.start);
|
|
534
|
+
body += pre;
|
|
535
|
+
bodyLine += countNewlines(pre);
|
|
536
|
+
jsLine += countNewlines(pre);
|
|
537
|
+
// The replacement's first body line maps from the js line at its start.
|
|
538
|
+
const repJsStartLine = lineAt(js, replacement.start);
|
|
539
|
+
// Source line = compose: js → rewritten (esbuild) → code (imports).
|
|
540
|
+
const rewLine = esbLineMap.length > 0 ? esbLineMap[Math.min(repJsStartLine - 1, esbLineMap.length - 1)] : repJsStartLine;
|
|
541
|
+
const srcLine = rewToCode(rewLine);
|
|
542
|
+
lineMap.push({ sourceLine: srcLine, bodyLine: bodyLine });
|
|
543
|
+
body += replacement.text;
|
|
544
|
+
bodyLine += countNewlines(replacement.text);
|
|
545
|
+
// js line after the replacement: replacements are in js coordinate, so
|
|
546
|
+
// jsLine advances by the replacement's text newlines too.
|
|
547
|
+
jsLine += countNewlines(replacement.text);
|
|
358
548
|
cursor = replacement.end;
|
|
359
549
|
}
|
|
360
|
-
|
|
550
|
+
const tail = js.slice(cursor);
|
|
551
|
+
body += tail;
|
|
552
|
+
bodyLine += countNewlines(tail);
|
|
553
|
+
// Any body line not covered by a replacement maps identity.
|
|
554
|
+
lineMap.sort((a, b) => a.bodyLine - b.bodyLine);
|
|
361
555
|
|
|
362
|
-
return { body, declaredNames: [...new Set([...importDeclared, ...declaredNames])] };
|
|
556
|
+
return { body, declaredNames: [...new Set([...importDeclared, ...declaredNames])], lineMap };
|
|
363
557
|
}
|