Skip to content

Import sheets (history-preserving, merge-commit) - #13

Merged
andrei-hasna merged 8 commits into
mainfrom
import/sheets
Aug 13, 2026
Merged

Import sheets (history-preserving, merge-commit)#13
andrei-hasna merged 8 commits into
mainfrom
import/sheets

Conversation

@andrei-hasna

@andrei-hasna andrei-hasna commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Import sheets (history-preserving, merge-commit)

Imports hasna/sheets (@hasna/sheets 0.2.1) into the monorepo under apps/sheets via 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

  • PR freeze: hasna/sheets had 0 open PRs at import time — verified gh pr list -R hasna/sheets --state open.
  • Rewrite: git filter-repo --to-subdirectory-filter apps/sheets --preserve-commit-hashes --preserve-commit-encoding on a fresh mirror (git-filter-repo a40bce548d2c, venv /tmp/opencode/import-proof/venv). 12 commits parsed.
  • Capsule: 9a7588dae89b686abb5ed7d3879ba1615f4490be — tree = rewritten main tree; 1 ref peeled (main); 0 skipped; 1 parent.
  • Parent-set equality: GAPS=0 (1/1).
  • Tree gate: rewritten apps/sheets subtree f0253c38 == mono subtree f0253c38, rc=0; blob multiset 48/48 identical, 0 diffs.
  • Hygiene: no .gitattributes/.gitmodules/LFS in the member tree; staged secrets scan 0 findings (49 files); git diff --cached --check rc=0.
  • Tag manifest: hasna/sheets has zero tags. Manifest: /tmp/opencode/wave1a-sheets/tag-commit-manifest.md.

Absorption notes

  • No fixes required — member already carries "bun-types": "latest" directly.
  • bun install rc=0; bun run check rc=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).
  • Root bun.lock registers the workspace member (sheet deps: exceljs, react-spreadsheet, scheduler).

Notes

  • Commits are rewritten (shas changed, signatures stripped — documented filter-repo behavior); authorship, dates, messages and Agent: trailers preserved (12 commits).
  • Old repo hasna/sheets is NOT archived/deleted (owner gate: backup-first).

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

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.
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
@andrei-hasna
andrei-hasna merged commit b42a3ed into main Aug 13, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant