Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7133329
Preserve dry-run row evidence, keep blocked outcomes recoverable, and…
dharan1007 Sep 28, 2026
61412bb
Preserve original source record indices in representative dry-run dia…
dharan1007 Sep 28, 2026
9562e73
Avoid full source and output cloning on every IndexedDB checkpoint
dharan1007 Sep 28, 2026
a68d463
Restore actionable blocked-migration UX, quality samples, and stable …
dharan1007 Sep 28, 2026
313ef47
Style actionable recovery, bounded diagnostics, and responsive missio…
dharan1007 Sep 28, 2026
84dddc6
Bound UI source metadata and preview cell lengths without truncating …
dharan1007 Sep 28, 2026
1242df4
Regression: 91/100 source rejection, explicit recovery, exact sample …
dharan1007 Sep 28, 2026
c363242
Expose deterministic CSV fixture loader to browser smoke harness
dharan1007 Sep 28, 2026
6d2adfa
Exercise real 91/100 blocked migration, actionable DOM recovery, and …
dharan1007 Sep 28, 2026
39938f8
Correct browser fixture newlines and DOM query escaping
dharan1007 Sep 28, 2026
8e99848
Gate every Vercel build on JS syntax and blocked-Autopilot regression…
dharan1007 Sep 28, 2026
ca6634a
Document blocked-migration recovery and bounded browser state copies
dharan1007 Sep 28, 2026
1f8a5ea
Mark sampled blocked-mission diagnostics as untrusted WebMCP content
dharan1007 Sep 28, 2026
ae18e40
Display local processing state and require confirmation before replac…
dharan1007 Sep 28, 2026
0e6d19c
Increase diagnostic readability and show local processing state on de…
dharan1007 Sep 28, 2026
e51bce7
Fail closed and recover visibly from Worker startup or checkpoint per…
dharan1007 Sep 28, 2026
e00f52c
Permit recovered run to enter terminal failure when startup cannot re…
dharan1007 Sep 28, 2026
b625b93
Navigate to actionable failure details after a Worker start error
dharan1007 Sep 28, 2026
5aa34a9
Regress Worker-start and storage-checkpoint failures without permanen…
dharan1007 Sep 28, 2026
a05146d
Correct fixture newlines for Worker and checkpoint error regressions
dharan1007 Sep 28, 2026
2c820d7
Document recoverable blocked missions and bounded untrusted diagnostics
dharan1007 Sep 28, 2026
292fbe5
Document recoverable blocked missions and bounded untrusted diagnostics
dharan1007 Sep 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ messy CSV
→ typed output + violations + lineage
```

The browser path is local-first and retains the production CSP boundary `connect-src 'none'`. The current browser input limit is **50 MiB**. SPOOL fails closed on ambiguous dates/numbers, invalid target values, storage-capacity failure and unsafe export cells.
The browser path is local-first and retains the production CSP boundary `connect-src 'none'`. The current browser input limit is **50 MiB**. SPOOL fails closed on ambiguous dates/numbers, invalid target values, storage-capacity failure and unsafe export cells. A blocked Autopilot mission remains recoverable in the Studio: bounded failed-sample diagnostics identify implicated fields and original record numbers, and the operator can correct/replace the CSV or explicitly choose another output contract and rerun the dry run. The 95% automatic-acceptance floor is never silently reduced; preserving original CSV field names is a different output contract, not a data-cleaning shortcut.

### Gate B Local Runner

Expand Down
4 changes: 3 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ SPOOL is split into explanatory product routes and a focused Studio. The normal

Database-ready inference can promote a source string to boolean, integer, number or date only when sampled evidence crosses the planner confidence threshold. Destructive naming collisions fail closed into `NEEDS_ATTENTION`; SPOOL does not invent a resolution.

A representative dry run below 95% acceptance also enters `NEEDS_ATTENTION` before a Worker is started. The mission retains bounded violation examples with original source-record indices and implicated fields; no invalid-record payload is sent to a backend. The operator can correct/replace the CSV or explicitly choose another supported output contract and rerun the same validation. `preserve_contract` retains parsed CSV header spellings and raw string cell values, including distinct headers that would collide after normalization. It does not override the acceptance gate or implicitly edit the source.

A safe mission executes:

`PROFILE → INFER → PLAN → DRY RUN → ASSESS → EXECUTE → VERIFY`
Expand All @@ -28,7 +30,7 @@ A manual run interrupted by a terminated tab restores as `PAUSED_RECOVERED`. An

## Durable data plane

`IndexedDbWorkspaceStore` separates compact workflow/mission metadata from source/output chunks. Source chunks are rewritten only when the source fingerprint changes. Output chunks append as progress advances and are cleared when output identity/revision rewinds.
`IndexedDbWorkspaceStore` separates compact workflow/mission metadata from source/output chunks. Source chunks are rewritten only when the source fingerprint changes. Output chunks append as progress advances and are cleared when output identity/revision rewinds. Browser Studio emits a bounded UI snapshot that omits the full source/output arrays. IndexedDB checkpoint metadata is cloned after the source/output arrays have been detached from the compact representation; rendering or a checkpoint no longer needlessly clones the full source.

The current UI accepts files up to 50 MB and the parser still materializes the source in browser memory. Whole-file streaming/OPFS is a future scale extension, not a current capability claim.

Expand Down
2 changes: 1 addition & 1 deletion docs/WEBMCP.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Every command response identifies the current phase, job identity, mapping revis

## Context discipline

`inspect_workspace` returns metadata only. `inspect_mission` exposes bounded inference evidence, ambiguity information, progress and quality groups. Source/result rows require explicit bounded sampling tools. Violation inspection returns grouped failure classes with capped examples. User-provided source/result content is marked with `untrustedContentHint` in tool annotations.
`inspect_workspace` returns metadata only. `inspect_mission` exposes bounded inference evidence, ambiguity information, progress and quality groups. Explicit source/result sampling tools remain bounded. A blocked dry-run mission inspector can also return at most three bounded diagnostic examples per violation class (including source-record indices and implicated cell values truncated to 120 characters). Treat these as untrusted content; `inspect_mission` carries `untrustedContentHint`. Violation inspection returns grouped failure classes with capped examples. User-provided source/result content is marked with `untrustedContentHint` in tool annotations.

## Principal phases

Expand Down
42 changes: 42 additions & 0 deletions scripts/browser-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,48 @@ def main():
cdp.eval('window.__spoolTest.invoke("start_new_migration", {})')
wait_for(lambda: cdp.eval('window.__spoolTest.state().job.phase === "EMPTY"'), label='workspace reset')

bad_indexes = set([i * 1999 // 99 for i in range(100) if i * 1999 // 99 >= 1000][:9])
# Regression for the exact 91/100 blocked Autopilot mission.
# Source indices are spread across the CSV, so this also verifies
# diagnostic record numbers refer to the original file, not a
# synthetic 0..99 preview numbering.
blocked_rows = [
f'{index + 1},{("not-a-number" if index in bad_indexes else str(index + 100) + ".25")},Customer {index + 1}'
for index in range(2000)
]
blocked_csv = 'Customer ID,amount,Name\n' + '\n'.join(blocked_rows)
before_blocked = cdp.eval('performance.now()')
cdp.eval('window.__spoolTest.loadCsv(' + json.dumps(blocked_csv) + ')')
wait_for(lambda: cdp.eval('window.__spoolTest.state().job.phase === "SOURCE_READY"'), label='blocked fixture load')
blocked_load_ms = round(cdp.eval('performance.now()') - before_blocked)
blocked_response = cdp.eval('window.__spoolTest.runAutopilot("database_ready")')
assert blocked_response['ok'] is True
assert blocked_response['result']['status'] == 'NEEDS_ATTENTION'
blocked_state = cdp.eval('window.__spoolTest.state()')
assert blocked_state['mission']['dryRun']['validRows'] == 91
assert blocked_state['mission']['dryRun']['invalidRows'] == 9
assert blocked_state['job']['phase'] == 'SOURCE_READY'
assert blocked_state['output'] == []
cdp.eval('window.__spoolTest.navigate("/studio/mission")')
wait_for(lambda: cdp.eval('document.querySelector("[data-action=retry-autopilot]") !== null'), label='blocked mission recovery UI')
assert cdp.eval('document.querySelectorAll(".quality-examples > div").length') > 0
assert cdp.eval('document.body.innerText.includes("Source record")')
assert cdp.eval('document.querySelectorAll(".recovery-option").length') == 3
cdp.eval('document.querySelector("[data-outcome=preserve_contract]").click()')
assert cdp.eval('document.querySelector("[data-outcome=\\\"preserve_contract\\\"]').getAttribute("aria-pressed")') == 'true'
cdp.eval('document.querySelector("[data-action=retry-autopilot]").click()')
wait_for(lambda: cdp.eval('window.__spoolTest.state().job.phase === "COMPLETE"'), timeout=20, label='blocked workflow recovery completion')
recovered = cdp.eval('window.__spoolTest.state()')
assert recovered['job']['validRows'] == 2000
assert recovered['job']['invalidRows'] == 0
assert recovered['mission']['outcome'] == 'preserve_contract'
assert cdp.eval('document.querySelector(".result-hero")') is None # Still on mission; no fake result screen.
cdp.eval('window.__spoolTest.invoke("start_new_migration", {})')
wait_for(lambda: cdp.eval('window.__spoolTest.state().job.phase === "EMPTY"'), label='recovery fixture cleanup')
print(json.dumps({'blockedDryRun': 'PASS', 'accepted': 91, 'sampleSize': 100,
'originalRecordDiagnostics': 'PASS', 'recoverableViaButtons': 'PASS',
'sampleLoadMs': blocked_load_ms}), flush=True)

cdp.eval('window.__spoolTest.loadDemo()')
wait_for(lambda: cdp.eval('window.__spoolTest.state().job.phase === "SOURCE_READY"'), timeout=15, label='demo source load')
assert cdp.eval('window.__spoolTest.state().source.rows.length') == 25000
Expand Down
12 changes: 12 additions & 0 deletions scripts/build-dist.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createHash } from 'node:crypto';
import { execFileSync } from 'node:child_process';
import { cp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';

const RELEASE_FILES = new Set(['release.json', 'release-manifest.json']);
Expand Down Expand Up @@ -63,6 +64,17 @@ function artifactRootSha256(artifacts) {
return createHash('sha256').update(canonical).digest('hex');
}

// Vercel can deploy without GitHub Actions quota. Enforce the browser
// recovery regression and syntax gate inside the build itself.
for (const path of ['src/app.js', 'src/core/command-kernel.js', 'src/core/migration.js', 'src/storage/indexeddb.js']) {
execFileSync(process.execPath, ['--check', path], { stdio: 'inherit' });
}
execFileSync(process.execPath, ['--test',
'tests/autopilot-recovery.test.js',
'tests/autopilot-kernel.test.js',
'tests/ui-contract.test.js'
], { stdio: 'inherit' });

const distDir = process.env.SPOOL_DIST_DIR?.trim() || 'dist';
await rm(distDir, { recursive: true, force: true });
await mkdir(distDir, { recursive: true });
Expand Down
Loading
Loading