Skip to content

HF-131 (2/7): the origin/propagation mechanism, plus the same guarantee outside the interpreter - #1762

Open
marcin-kordas-hoc wants to merge 6 commits into
feat/hf-131-error-messagesfrom
feat/hf-131-error-messages-outside-interpreter
Open

HF-131 (2/7): the origin/propagation mechanism, plus the same guarantee outside the interpreter#1762
marcin-kordas-hoc wants to merge 6 commits into
feat/hf-131-error-messagesfrom
feat/hf-131-error-messages-outside-interpreter

Conversation

@marcin-kordas-hoc

@marcin-kordas-hoc marcin-kordas-hoc commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

What and why

Stacked on #1761.

Revision note: an earlier version of this description covered only the second half below.
The first half — the origin/propagation mechanism — was always in this PR's actual diff; the
description just didn't say so. #1764's description has been corrected to match (it does not
build the mechanism, only exposes two of its fields publicly).

1. The origin/propagation mechanism. CellError needs to know, for any error value anywhere
in a sheet, which function or operator originally produced it, whether it's since been merely
propagated by a cell that read it, and (for static errors with no formula root) which address it
originated at. The hard problem isn't computing that — it's stamping it exactly once, at the
moment of truth, regardless of which of the ~200 call sites across the interpreter and dependency
graph produced or merely relayed the error.

Two choke points, first-wins immutability (the same shape attachRootVertex already used for
root):

  • Cell.ts gains withOrigin/withArgumentIndex/asPropagated/withOriginAddress — each a
    no-op once the relevant field (or propagated) is already set, otherwise a fresh immutable copy.
  • Interpreter.ts's evaluateAst stamps originFunction in a single postprocessing tail
    (stampOriginForAstNode) that runs after every AST node evaluates — including the NaN-overflow
    guard's freshly-minted error, and including the fix that a propagated error no longer gets
    attachRootVertex'd to whichever cell happened to read it.
  • DependencyGraph.ts's getCellValue — the one place every cross-cell read funnels through —
    marks every error read from another cell as propagated, and additionally stamps the address
    for a vertex holding a static error (no FormulaVertex to serve as a root).
  • Exporter.ts falls back to the stamped originAddress when there is no root.

Measured: A1='=1+' reports Sheet1!A1 where it reported no address; B1='=A1' reports
Sheet1!A1 where it reported its own address; the same holds two hops out, through a range, and
through a named expression; it follows the cell across addRows; =1/0 with a propagating SUM
still reports A1.

Also consolidates the toEqualError matcher: it existed twice, once per test runner, with
hand-copied logic and two strip lists that could drift — and had drifted in a way that made a
change appear inert under Jest. The decision now lives in one shared module both wrappers call
with their own equality function.

Not yet public: originFunction/argumentIndex are carried on CellError and stamped here,
but DetailedCellError does not expose them yet — that's #1764.

2. The same guarantee outside the interpreter. Extends #1761's guarantee to the 17
message-less sites (+2 with an explicit undefined) outside src/interpreter/: the parser's
#REF!/#NAME? branches, dependency transformers' 4 reference-removal sites, DependencyGraph's
array-source-removed site, FormulaVertex's outside-array-result site, and Evaluator's two
#CYCLE! sites.

Kuba's scope ruling was all places throwing a cell error, not only src/interpreter/ — these 19
sites are outside the spec's original 78-site counter but within that ruling.

Widens the ESLint rule from #1761 to all of src/, and documents the one call site it structurally
cannot catch: CellContentParser.ts's CellContent.Error constructor keeps message optional on
purpose (its one no-message caller — a user typing e.g. #REF! literally into a cell — has no
engine-side cause to state), and new CellError(errorType, message) is syntactically a two-argument
call that passes the rule regardless of what the caller actually passes.

Two new ErrorMessage constants for user-supplied/literal error values are deliberately honest
rather than invented: an error literal typed into a formula or a value typed directly into a cell
round-trips exactly what the user wrote, so the message says that rather than fabricating an
engine-side cause.

Verified

  • npx tsc --noEmit — clean
  • npx eslint src/ — 0 errors (same pre-existing warning count)
  • The widened rule was proven to reach files outside src/interpreter/: a #CYCLE! site in
    src/Evaluator.ts was temporarily reverted to a bare new CellError(ErrorType.CYCLE), confirmed
    to produce the expected lint error, then restored (git diff clean afterward)
  • Full private test suite reproduced against this branch's tip — no regressions beyond the same
    pre-existing branch-pinning mismatch as HF-131 (1/7): every interpreter cell error carries a message #1761
  • Nested-attribution correctness re-confirmed by re-running the test suite live: =SUM(SQRT(-1))
    reports SQRT not SUM; =1/0 then =SUM(A1) reports divide, propagated: true; no code
    path exists to overwrite a propagated error's address; zero surviving message-less CellError
    construction sites in src/ (338 call sites individually audited)

Stack

2 of 7 — stacked on #1761. Next: feat/hf-131-has-message.

🤖 Generated with Claude Code

marcin-kordas-hoc and others added 4 commits September 8, 2026 02:25
Reading another cell's value funnels through DependencyGraph.getCellValue, so
that is where an error now gets marked as propagated -- one choke point instead
of a marking at each interpreter case, which is what let three read paths
(named expressions, range reads via SimpleRangeValue, and getScalarValue's
aggregation callers) slip through an earlier attempt. getScalarValue delegates
to getCellValue rather than reading the address mapping itself, so its callers
inherit the mark instead of bypassing it.

wrapperForRootVertex then skips a propagated error, so the reading cell is no
longer attached as its root. A vertex holding a STATIC error has no
FormulaVertex to serve as a lazily-resolved root, so getCellValue stamps the
address it already has; CellError is immutable, so this copies rather than
mutating what is stored, which is what keeps the address correct across row and
column changes.

evaluateAst's overflow guard now assigns instead of returning early, so a
NaN/Infinity result reaches the same postprocessing as every other value and
gets a root like anything else -- previously =SQRT(-1) had neither.

Measured on this branch: A1='=1+' reports Sheet1!A1 where it reported no
address; B1='=A1' reports Sheet1!A1 where it reported its own address; the same
holds two hops out, through a range, and through a named expression; it follows
the cell across addRows; and =1/0 with a propagating SUM still reports A1.
Full private suite: 5 failed / 6157 passed, byte-identical to this worktree's
baseline (those 5 are a pre-existing branch-pinning mismatch in the test repo).
tsc and eslint clean.

Also consolidates the toEqualError matcher. It existed twice, once per runner,
with hand-copied logic and two strip lists that could drift -- and had drifted
in a way that made a change appear inert under Jest. The decision now lives in
one shared module both wrappers call with their own equality function, so there
is a single strip list to extend. Verified by short-circuiting the shared
module: 30 tests went red and came back green, proving Jest really reads it.
The Karma half could not be executed here (no browser in this environment).

NOT yet observable through the public API: originFunction and argumentIndex are
carried on CellError and stamped in evaluateAst, but DetailedCellError does not
expose them yet. Exposing them is one line plus a test migration -- 10
assertions in error-address-preservation.spec.ts, arrays.spec.ts and
matrix-plugin.spec.ts use plain toEqual, which no matcher edit reaches, and
they must NOT be moved to toEqualError because it also strips address, which is
exactly what those tests assert. That split is the natural PR boundary here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Task 2 of the HF-131 explainability plan: the 19 CellError construction
sites outside src/interpreter/ (17) plus src/Evaluator.ts's two
explicit-undefined CYCLE sites that passed no message at all now do,
mirroring what Task 1 already did for src/interpreter/.

Seven new catalogue constants cover semantically distinct groups rather than
one generic REF/NAME sentence: a removed-reference in an existing formula
(Transformer.ts, 4 sites), an array's source cell being overwritten
(DependencyGraph.ts), a position outside an array's own computed bounds
(FormulaVertex.ts), an unresolved parser reference (FormulaParser.ts, 6
REF sites), a reference exceeding sheet size limits (FormulaParser.ts, 4
NAME sites), an error value typed directly into a formula (FormulaParser.ts,
1 site), and a circular reference (Evaluator.ts, 2 CYCLE sites).

No error type changed, no function signature changed, i18n/Cell.ts/
CellValue.ts/Exporter.ts/the DependencyGraph choke point untouched.

Originally attempted via prep-ship's autonomous pipeline; the run crashed
twice with an empty error at its own 'authoring AC specs' step (unrelated
to this diff — see the private-test patch README for the diagnosis). The
production src/ implementation it left behind was verified correct against
every constraint in the task file and kept; the pre-existing private-test
breakage it hadn't yet reached was fixed by hand.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Enforces the guarantee this PR's message-filling work establishes: a
message-less new CellError(...) construction under src/interpreter/ is now
a lint error. Two selectors — a bare one-argument call, and an explicit
literal undefined as the second argument (the exact shape the two CYCLE
sites in src/Evaluator.ts still use; Evaluator.ts is outside src/interpreter/
so this rule doesn't reach them yet, and is unaffected by this commit).

Verified: 0 lint errors on the full src/ tree (same pre-existing warning
count as before this commit — no new warnings). The rule was proven to
actually fire, not just parse, by temporarily reverting one src/interpreter/
plugin site to a bare 'new CellError(ErrorType.NUM)', observing the expected
lint error, then restoring it (git diff empty afterward).

Widened to all of src/ in the next PR in this stack
(feat/hf-131-error-messages-outside-interpreter).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Changes the override's files glob from src/interpreter/ to all of src/, so
the guard this PR's own message-filling work (and the earlier commit's
interpreter-only rule) establishes covers the whole engine, not just the
interpreter plugins.

Documents the one call site the rule structurally cannot catch:
CellContentParser.ts's CellContent.Error constructor keeps message optional
on purpose (its one no-message caller — a user typing e.g. #REF! literally
into a cell — has no engine-side cause to state), and the call
'new CellError(errorType, message)' is syntactically a two-argument
construction that passes the rule cleanly regardless of what the caller
actually passes.

Verified: 0 lint errors on the full src/ tree (same pre-existing warning
count as before this commit). The widened rule was proven to actually reach
files outside src/interpreter/ by temporarily reverting one CYCLE site in
src/Evaluator.ts to a bare 'new CellError(ErrorType.CYCLE)', observing the
expected lint error, then restoring it (git diff empty afterward).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@qunabu

qunabu commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread CHANGELOG.md

### Fixed

- A cell error read from another cell no longer reports the reading cell as its origin. Errors that come from a cell holding a static error value — a formula with a syntax error, or an error value entered directly — now report that cell's address instead of the address of whichever cell happened to read them first. [#131](https://github.com/handsontable/hyperformula/issues/131)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changelog bullets lack PR links

Low Severity

The new Unreleased bullets end with an issue link to #131 rather than a pull-request URL. Engine changelog entries need a [#NNNN](https://github.com/handsontable/hyperformula/pull/NNNN) link; an issue-only reference is not a substitute.

Fix in Cursor Fix in Web

Triggered by learned rule: CHANGELOG bullets need a PR link

Reviewed by Cursor Bugbot for commit 9c4015e. Configure here.

Comment thread .eslintrc.js
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 10, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
hyperformula-docs 749044d Commit Preview URL

Branch Preview URL
Sep 11 2026, 05:36 AM

…at/hf-131-error-messages-outside-interpreter

# Conflicts:
#	.eslintrc.js

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/Cell.ts
if (this.originAddress !== undefined) {
return this
}
return new CellError(this.type, this.message, this.root, this.originFunction, this.argumentIndex, this.propagated, address)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static error origin can go stale

Medium Severity

withOriginAddress snapshots a raw SimpleCellAddress onto the copy stored in the reading formula. After rows or columns move that cell, a cached formula value can keep the old coordinates. root.getAddress() stays current; this snapshot does not, so the exported origin can point at the wrong cell.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f667a23. Configure here.

address: undefined,
originFunction: undefined,
argumentIndex: undefined,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error matcher omits new fields

Low Severity

IGNORED_IN_STRUCTURAL_COMPARE lists originFunction and argumentIndex but not propagated or originAddress. The helper’s own comment says every new CellError field belongs here once; leaving these two in the structural compare will make toEqualError fail on otherwise matching errors.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f667a23. Configure here.

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

Performance comparison of head (749044d) vs base (0ea5c64)

                                     testName |    base |    head |  change
---------------------------------------------------------------------------
                                      Sheet A |  341.49 |  335.61 |  -1.72%
                                      Sheet B |  112.33 |  114.27 |  +1.73%
                                      Sheet T |      98 |   99.17 |  +1.19%
                                Column ranges |  491.99 |  493.99 |  +0.41%
                                Sorted lookup | 16512.4 | 16361.8 |  -0.91%
Sheet A:  change value, add/remove row/column |   10.99 |   10.57 |  -3.82%
 Sheet B: change value, add/remove row/column |  104.16 |   86.46 | -16.99%
                   Column ranges - add column |  131.75 |  114.91 | -12.78%
                Column ranges - without batch |  400.17 |  372.66 |  -6.87%
                        Column ranges - batch |   99.14 |   93.37 |  -5.82%

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 4 total unresolved issues (including 3 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 749044d. Configure here.

const holdsAStaticError = vertex instanceof ParsingErrorVertex || vertex instanceof ValueCellVertex
return holdsAStaticError
? value.withOriginAddress(address).asPropagated()
: value.asPropagated()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale origin address after structural edits

Medium Severity

withOriginAddress snapshots a concrete address, and the dependent formula then caches that stamped CellError. Row and column inserts only lazily rewrite ASTs and do not recompute those values, so DetailedCellError.address for a static error (typed value or parse error) can keep pointing at the pre-shift cell.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 749044d. Configure here.

@marcin-kordas-hoc marcin-kordas-hoc changed the title HF-131 (2/7): the same guarantee outside the interpreter HF-131 (2/7): the origin/propagation mechanism, plus the same guarantee outside the interpreter Sep 11, 2026
@codecov

codecov Bot commented Sep 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.05825% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.38%. Comparing base (0ea5c64) to head (749044d).

Files with missing lines Patch % Lines
src/parser/FormulaParser.ts 81.81% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                      Coverage Diff                       @@
##           feat/hf-131-error-messages    #1762      +/-   ##
==============================================================
+ Coverage                       97.36%   97.38%   +0.01%     
==============================================================
  Files                             195      195              
  Lines                           15750    15825      +75     
  Branches                         3461     3491      +30     
==============================================================
+ Hits                            15335    15411      +76     
+ Misses                            407      406       -1     
  Partials                            8        8              
Files with missing lines Coverage Δ
src/Cell.ts 95.27% <100.00%> (+0.88%) ⬆️
src/CellContentParser.ts 100.00% <ø> (ø)
src/DependencyGraph/DependencyGraph.ts 98.82% <100.00%> (+<0.01%) ⬆️
src/DependencyGraph/FormulaVertex.ts 84.25% <100.00%> (ø)
src/Evaluator.ts 100.00% <100.00%> (ø)
src/Exporter.ts 86.66% <100.00%> (ø)
src/dependencyTransformers/Transformer.ts 97.40% <100.00%> (+0.03%) ⬆️
src/error-message.ts 100.00% <100.00%> (ø)
src/interpreter/Interpreter.ts 95.91% <100.00%> (+0.55%) ⬆️
src/parser/FormulaParser.ts 97.61% <81.81%> (+0.23%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.

2 participants