Import sheets (history-preserving, merge-commit) - #13
Merged
Conversation
Headless, framework-agnostic workbook model with an MIT formula engine (fast-formula-parser; no HyperFormula/GPL): A1 addressing, sheets CRUD, cell get/set by ref, ranges, and a dependency-graph recalc engine with fixed-point evaluation and circular-reference detection. CSV/XLSX/JSON import-export (exceljs optional, lazy-loaded). Ships a react-spreadsheet-backed Spreadsheet editor from ./react bound to the model, a headless sheets CLI, a Vite+React+Tailwind demo dashboard, docs, MIT LICENSE, hasna.contract.json, and CI/publish workflows. 52 unit tests, tsc --noEmit and build green.
…x exports map (#1) * fix(sheets): bound recalc/read/write against untrusted-input DoS + fix exports map Every write and load routes through recalc(), whose dependency graph, range materialization, formula parsing, and string output were all unbounded. Raw client workbooks (saveSheetWorkbookAction) and agent tool calls (sheets_set_cells) could hang or exhaust memory: V1 range amplification (getRangeValues / SUM over an oversized declared range) V2 O(N^2) dependency graph (N cells each SUM a wide range) V3 bulk CSV import of many formula cells V4 REPT/CONCAT output bomb (tiny input, huge output) V5 oversized formula parse cost (single giant body and many max-length bodies) New src/lib/limits.ts adds SheetsLimits + DEFAULT_LIMITS, a typed dash-free SheetsLimitError (code/limit/actual, instanceof-safe), and a RecalcBudget (op counter + wall-clock deadline) threaded through recalc(). Source caps reject oversized input before the expensive work runs (the only defense that can bound a single synchronous parser.parse call, which a counter cannot interrupt): formula length 8192, cell content 32767, sheet dims 1048576 x 16384, populated cells 1e6, range cells 65536, batch 1e5. The string-amplifier functions (REPT/CONCAT/CONCATENATE/TEXTJOIN) are overridden to refuse output larger than a cell can hold, with a post-eval coerceResult guard for the raw & operator. maxRangeCells is 65536, below Excel's row ceiling: fast-formula-parser evaluates a single range super-linearly (a 200k-cell SUM costs ~66s in one uninterruptible call, with a sharp cliff past ~65k), so the per-range cap must sit under that cliff. Limit breaches thrown inside onCell/onRange (which the parser rewraps as FormulaError) are stashed and re-thrown after parse(). All limits are optional overrides over DEFAULT_LIMITS, so existing callers are unchanged; a realistic ~10k-cell workbook recalcs in well under the budgets. Also fixes the package.json exports map: add a default/require condition to "." and "./react" and add "./package.json", which unblocks webpack/next build in consuming apps (previously worked around with a next.config alias). Adds src/lib/recalc.dos.test.ts reproducing V1..V5 with the real payloads plus a false-positive guard. Bumps 0.1.0 -> 0.2.0. * fix(limits): tighten recalc deadline to 1800ms so within-cap DoS vectors trip in ~1.5-2s The round-4 op-budget + Excel-spec caps bound every DoS vector, but two residual within-cap payloads only terminated at the 10s wall-clock: an O(N^2) dep graph (thousands of =SUM(A1:A20000) cells) and a parse-aggregate flood (thousands of ~8000-char =1+1+... bodies). A 10s single-thread block is still an availability hit. Lower recalcWallClockMs 10000->1800 and scale recalcOpBudget 500M->90M to the same time envelope. Both vectors now trip in ~1.5-1.8s (V2 on the op budget, V5-aggregate on the wall clock) instead of ~10s. A realistic ~10k-cell workbook recalcs in ~70ms, so 1800ms keeps a ~25x false-positive margin. No architecture change; only the default constants (and the test pinning them) are tuned. * test(limits): make V5-aggregate parse-budget test deterministic The V5 aggregate DoS test tightened only totalFormulaCharsBudget and left recalcWallClockMs at the 1800ms default, so on a slow CI runner the ~250 expensive 8000-char parses crossed the wall-clock deadline first and the test flaked with recalc_time_budget_exceeded instead of the asserted formula_parse_budget_exceeded. Lower the char budget so it trips after a few dozen parses and lift the wall clock, making the parse-char budget the deterministic binding constraint. Production code is unchanged.
Agent: agent-ea
Merge capsule 9a7588d (rewritten history of hasna/sheets, 12 commits) into the monorepo. Absorption: no fixes required. Root bun.lock registers the workspace member. Agent: agent-ea
Agent: agent-ea
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Import sheets (history-preserving, merge-commit)
Imports
hasna/sheets(@hasna/sheets0.2.1) into the monorepo underapps/sheetsvia the ratified filter-repo + history-capsule mechanism (import-mechanics-codex.md). DO NOT MERGE with squash/rebase — merge-commit only (squash/rebase breaks imported-history reachability).Mechanism evidence
gh pr list -R hasna/sheets --state open.git filter-repo --to-subdirectory-filter apps/sheets --preserve-commit-hashes --preserve-commit-encodingon a fresh mirror (git-filter-repo a40bce548d2c, venv /tmp/opencode/import-proof/venv). 12 commits parsed.9a7588dae89b686abb5ed7d3879ba1615f4490be— tree = rewritten main tree; 1 ref peeled (main); 0 skipped; 1 parent.apps/sheetssubtreef0253c38== mono subtreef0253c38, rc=0; blob multiset 48/48 identical, 0 diffs.git diff --cached --checkrc=0./tmp/opencode/wave1a-sheets/tag-commit-manifest.md.Absorption notes
"bun-types": "latest"directly.bun installrc=0;bun run checkrc=0 (publish guard: sheets — 0 tarball entries, 0 internal-infra strings).TURBO_SCM_BASE=origin/main turbo run build/test/lint --affected— build rc=0, test rc=0 (72 pass / 0 fail), lint rc=0 (member has no lint script).bun.lockregisters the workspace member (sheet deps: exceljs, react-spreadsheet, scheduler).Notes
Agent:trailers preserved (12 commits).hasna/sheetsis NOT archived/deleted (owner gate: backup-first).Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.