From 0ab3b4daada6c68b154cba5e500dbb4c5645d2ea Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Mon, 31 Aug 2026 13:42:05 +0200 Subject: [PATCH 01/27] HF-359: Split the developer docs and add per-directory agent instructions Prepare the repository for the monorepo migration by moving from a single always-loaded DEV_DOCS.md to a three-layer documentation model: - AGENTS.md files carry the rules that must not be violated, plus a map of where to look next. They load automatically in their own subtree, so an agent working in src/parser/ never pays for the docs portal's conventions. Added at the repository root and in src/, src/parser/, src/interpreter/, src/interpreter/plugin/, src/interpreter/functionMetadata/, src/DependencyGraph/, src/i18n/, docs/, test/, and script/. In each directory CLAUDE.md is a symlink to the sibling AGENTS.md, so Claude Code and Cursor read one source. - dev-docs/ holds the deep reference, loaded on demand: structure, architecture, build, testing, definition of done, code style, doc standards, the function metadata catalogue, i18n, worktrees, and the target monorepo layout. DEV_DOCS.md becomes an index into it. - .claude/skills/ holds nine task workflows, scoped with the paths frontmatter field so each loads only where it applies. Also add .worktreeinclude, which documents why the branch-matched private test suite and the build outputs must never be copied into a worktree, and .claude/settings.json, which enables the typescript-lsp plugin repository-wide and keeps build output out of agent reads. Co-Authored-By: Claude Opus 5 --- .claude/settings.json | 19 ++ .claude/skills/changelog-creation/SKILL.md | 60 ++++++ .../skills/hyperformula-code-review/SKILL.md | 62 ++++++ .claude/skills/hyperformula-dev/SKILL.md | 67 +++++++ .../skills/hyperformula-function-dev/SKILL.md | 112 +++++++++++ .../skills/hyperformula-unit-testing/SKILL.md | 60 ++++++ .claude/skills/i18n-translations/SKILL.md | 40 ++++ .claude/skills/pr-creation/SKILL.md | 62 ++++++ .../skills/test-writing-discipline/SKILL.md | 58 ++++++ .claude/skills/writing-docs-pages/SKILL.md | 49 +++++ .worktreeinclude | 47 +++++ AGENTS.md | 67 ++++++- DEV_DOCS.md | 188 ++---------------- dev-docs/ARCHITECTURE.md | 85 ++++++++ dev-docs/BUILD.md | 62 ++++++ dev-docs/CODE-STYLE.md | 38 ++++ dev-docs/DEFINITION-OF-DONE.md | 34 ++++ dev-docs/DOC-STANDARDS.md | 29 +++ dev-docs/FUNCTION-CATALOGUE.md | 50 +++++ dev-docs/I18N.md | 22 ++ dev-docs/MONOREPO.md | 110 ++++++++++ dev-docs/README.md | 37 ++++ dev-docs/STRUCTURE.md | 69 +++++++ dev-docs/TESTING.md | 55 +++++ dev-docs/WORKTREES.md | 38 ++++ docs/AGENTS.md | 49 +++++ docs/CLAUDE.md | 1 + script/AGENTS.md | 20 ++ script/CLAUDE.md | 1 + src/AGENTS.md | 38 ++++ src/CLAUDE.md | 1 + src/DependencyGraph/AGENTS.md | 29 +++ src/DependencyGraph/CLAUDE.md | 1 + src/i18n/AGENTS.md | 22 ++ src/i18n/CLAUDE.md | 1 + src/interpreter/AGENTS.md | 26 +++ src/interpreter/CLAUDE.md | 1 + src/interpreter/functionMetadata/AGENTS.md | 19 ++ src/interpreter/functionMetadata/CLAUDE.md | 1 + src/interpreter/plugin/AGENTS.md | 73 +++++++ src/interpreter/plugin/CLAUDE.md | 1 + src/parser/AGENTS.md | 28 +++ src/parser/CLAUDE.md | 1 + test/AGENTS.md | 39 ++++ test/CLAUDE.md | 1 + 45 files changed, 1699 insertions(+), 174 deletions(-) create mode 100644 .claude/settings.json create mode 100644 .claude/skills/changelog-creation/SKILL.md create mode 100644 .claude/skills/hyperformula-code-review/SKILL.md create mode 100644 .claude/skills/hyperformula-dev/SKILL.md create mode 100644 .claude/skills/hyperformula-function-dev/SKILL.md create mode 100644 .claude/skills/hyperformula-unit-testing/SKILL.md create mode 100644 .claude/skills/i18n-translations/SKILL.md create mode 100644 .claude/skills/pr-creation/SKILL.md create mode 100644 .claude/skills/test-writing-discipline/SKILL.md create mode 100644 .claude/skills/writing-docs-pages/SKILL.md create mode 100644 .worktreeinclude create mode 100644 dev-docs/ARCHITECTURE.md create mode 100644 dev-docs/BUILD.md create mode 100644 dev-docs/CODE-STYLE.md create mode 100644 dev-docs/DEFINITION-OF-DONE.md create mode 100644 dev-docs/DOC-STANDARDS.md create mode 100644 dev-docs/FUNCTION-CATALOGUE.md create mode 100644 dev-docs/I18N.md create mode 100644 dev-docs/MONOREPO.md create mode 100644 dev-docs/README.md create mode 100644 dev-docs/STRUCTURE.md create mode 100644 dev-docs/TESTING.md create mode 100644 dev-docs/WORKTREES.md create mode 100644 docs/AGENTS.md create mode 120000 docs/CLAUDE.md create mode 100644 script/AGENTS.md create mode 120000 script/CLAUDE.md create mode 100644 src/AGENTS.md create mode 120000 src/CLAUDE.md create mode 100644 src/DependencyGraph/AGENTS.md create mode 120000 src/DependencyGraph/CLAUDE.md create mode 100644 src/i18n/AGENTS.md create mode 120000 src/i18n/CLAUDE.md create mode 100644 src/interpreter/AGENTS.md create mode 120000 src/interpreter/CLAUDE.md create mode 100644 src/interpreter/functionMetadata/AGENTS.md create mode 120000 src/interpreter/functionMetadata/CLAUDE.md create mode 100644 src/interpreter/plugin/AGENTS.md create mode 120000 src/interpreter/plugin/CLAUDE.md create mode 100644 src/parser/AGENTS.md create mode 120000 src/parser/CLAUDE.md create mode 100644 test/AGENTS.md create mode 120000 test/CLAUDE.md diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..29304b213f --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,19 @@ +{ + "enabledPlugins": ["typescript-lsp@claude-plugins-official"], + "permissions": { + "deny": [ + "Read(./dist/**)", + "Read(./lib/**)", + "Read(./es/**)", + "Read(./commonjs/**)", + "Read(./typings/**)", + "Read(./languages/**)", + "Read(./coverage/**)", + "Read(./docs/.vuepress/dist/**)", + "Read(./docs/api/**)" + ] + }, + "worktree": { + "symlinkDirectories": ["node_modules"] + } +} diff --git a/.claude/skills/changelog-creation/SKILL.md b/.claude/skills/changelog-creation/SKILL.md new file mode 100644 index 0000000000..793b5b249f --- /dev/null +++ b/.claude/skills/changelog-creation/SKILL.md @@ -0,0 +1,60 @@ +--- +name: changelog-creation +description: Use when a change to source code needs a changelog entry, and before pushing any bug fix, feature, or behaviour change. Covers when an entry is required, which section it belongs in, how to write the title, and the link format. +--- + +## When an entry is required + +Every pull request that changes source code needs one: bug fixes, new features, behaviour changes, deprecations, removals, security fixes, and new or changed language packs. + +**Not required for:** documentation-only changes (guides, JSDoc, README), test-only changes, and CI or tooling changes. + +Also not required when the bug being fixed was introduced **and never released** — the regression never reached users, so there is nothing to document. + +## Where it goes + +`CHANGELOG.md`, under `## [Unreleased]`, in the section matching the change. The format is [Keep a Changelog](https://keepachangelog.com/en/1.0.0/); the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +Create the `###
` heading under `## [Unreleased]` if it does not exist yet. Section order follows the released blocks above. + +| Section | Use for | +|---|---| +| `### Added` | A wholly new capability — a function, an option, a language pack, an API method | +| `### Changed` | Modified behaviour of something that already existed | +| `### Fixed` | A bug fix | +| `### Deprecated` | Scheduled for removal | +| `### Removed` | Already removed in this release | +| `### Security` | A vulnerability fix | + +## The format + +One bullet per change, ending with a link to the pull request or the issue it resolves: + +```markdown +- Fixed the `MOD` function returning a remainder with the sign of the dividend instead of the sign of the divisor, which made the results differ from Excel and Google Sheets for arguments with opposite signs (e.g. `=MOD(-3, 12)` now returns `9` instead of `-3`). [#1747](https://github.com/handsontable/hyperformula/issues/1747) +``` + +- Link the **public GitHub issue** when one exists; otherwise link the pull request. +- The link needs the PR number, so write the entry after `gh pr create` returns the URL. + +## Writing the title + +- **From the user's perspective.** Describe what changed for someone using HyperFormula, not what you changed in the code. +- **Past tense, starting with the verb**: "Added…", "Fixed…", "Changed…", "Removed…". +- **Be specific.** "Fixed a bug" tells nobody anything. Name the function, the option, or the operation, and say what it does now. +- **Show the difference when a value changed.** `=MOD(-3, 12)` now returns `9` instead of `-3` is worth more than a paragraph of prose. +- **No internal identifiers.** No class names, no file paths, no private ticket contents. An identifier such as `HF-123` is fine on its own; what is in that ticket is not. +- **No client, customer, or partner names**, and nothing that identifies them indirectly. +- End with a period, then the link. + +## Breaking changes + +Say what breaks and what to do instead, in the entry itself. A breaking change also needs a migration-guide section — the changelog entry is not a substitute. + +## Checklist + +1. Confirm the change needs an entry at all. +2. Pick the section; create it under `## [Unreleased]` if absent. +3. Write one user-facing bullet, past tense, specific. +4. Append the issue link, or the PR link when there is no issue. +5. Confirm no sensitive or internal information appears in it. diff --git a/.claude/skills/hyperformula-code-review/SKILL.md b/.claude/skills/hyperformula-code-review/SKILL.md new file mode 100644 index 0000000000..6e9ccae742 --- /dev/null +++ b/.claude/skills/hyperformula-code-review/SKILL.md @@ -0,0 +1,62 @@ +--- +name: hyperformula-code-review +description: Use when reviewing a diff, a branch, or a pull request in the HyperFormula repository. Covers correctness for a calculation engine, performance on the hot paths, the five places a function change must touch, API stability, and what the definition of done requires. +--- + +Review in this order. Stop at the first category that finds something serious and report it — do not bury a correctness bug under style notes. + +## 1. Correctness + +- **Does the test actually fail without the fix?** Ask it of every bug-fix PR. A test added alongside a fix, written from the implementation, proves nothing. +- **Errors returned, never thrown.** Any `throw` reachable from formula evaluation takes down the whole recalculation. It must be a `CellError` with a message from `src/error-message.ts`. +- **Coercion through `ArithmeticHelper`.** Hand-rolled string-to-number or value-to-boolean conversion inside a function is a bug waiting for a locale or an empty cell. +- **Empty cells and empty ranges.** The most common gap in a function change. So are error arguments — an error must propagate, not be coerced. +- **Round-tripping.** A parser change without a matching `Unparser` change means `getCellFormula` returns something the user never typed. +- **Structural changes.** Adding or removing rows or columns must leave the address mapping, range mapping, and array mapping consistent. Assert the formula **text** afterwards, not just the value. +- **Undo.** A new mutation needs `CrudOperations` (validate), `Operations` (mutate), and `UndoRedo` (record). Missing the third diverges silently. + +## 2. The five places a function change must touch + +Check all five; the failures are silent: + +1. the plugin implementation; +2. `implementedFunctions` metadata; +3. the catalogue entry in `src/interpreter/functionMetadata/categories/` — **parameter count must match**, or authored names and descriptions are discarded at run time with only a console warning; +4. **every** file in `src/i18n/languages/`; +5. tests. + +Also: a function that can return an array needs `sizeOfResultArrayMethod`. A function with a zero-argument or omitted-argument form needs an explicit `optionalArg: true` — nothing cross-checks optionality. + +## 3. Performance + +The engine's performance is a feature. Flag: + +- allocation inside a per-cell or per-vertex loop; +- work that could be hoisted out of the broadcast path; +- a range expanded into per-cell edges or per-cell iteration where the range vertex would do; +- anything that widens what a change invalidates, forcing a larger recalculation; +- a change to `ParserWithCaching` that makes the parse result depend on something outside the cache key. + +Ask for `npm run test:performance` on changes to evaluation or CRUD hot paths. + +## 4. Public API + +`src/HyperFormula.ts` and the types it exports are the contract. + +- A signature, return-type, or behaviour change is breaking. It needs a migration-guide section and an explicit note in the PR. +- JSDoc here is published output. Review it as documentation, not as a comment. +- A new config option needs a default, validation, and a guide entry. + +## 5. Definition of done + +Production change, tests, documentation, JSDoc, changelog entry, current PR description. See [`dev-docs/DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md). + +One pull request, one atomic change. Unrelated refactors and reformatting belong in a separate PR — say so rather than approving them through. + +## 6. Style + +Last, and briefly. ESLint owns formatting. Comment only on what it cannot check: a name that misleads, a function doing two things, duplicated logic that an existing helper already covers, a clever one-liner where an obvious three lines would read better. + +## Reporting + +One line per finding: what is wrong, where, and what to do instead. No praise, no summary of what the PR does — the author knows. Separate "this is a bug" from "I would have done this differently", and do not present the second as the first. diff --git a/.claude/skills/hyperformula-dev/SKILL.md b/.claude/skills/hyperformula-dev/SKILL.md new file mode 100644 index 0000000000..1b25b161f3 --- /dev/null +++ b/.claude/skills/hyperformula-dev/SKILL.md @@ -0,0 +1,67 @@ +--- +name: hyperformula-dev +paths: src/** +description: > + Use for ANY work touching the HyperFormula engine in `src/`: fixing bugs, adding features, + changing the public API, working on the parser, the interpreter, the dependency graph, + CRUD operations, configuration options, named expressions, or number and date formats. + Also use for how-to questions about engine internals (how recalculation works, why a formula + returns an error, where a value is coerced). Triggers on file paths under `src/`, or when the + user describes a symptom in a calculation without naming a file. This is the primary entry + point for engine development — when in doubt, load it. +--- + +## Orient first + +The engine is a pipeline. Locate the stage before changing anything: + +``` +setCellContents → CellContentParser → parser/ → GraphBuilder → DependencyGraph/ → Evaluator → interpreter/ → Serialization +``` + +| Symptom | Stage | Read | +|---|---|---| +| The formula does not parse, or parses wrongly | `src/parser/` | `src/parser/AGENTS.md` | +| `getCellFormula` returns something the user did not type | `src/parser/Unparser.ts` | `src/parser/AGENTS.md` | +| A function returns the wrong value or the wrong error | `src/interpreter/plugin/` | skill `hyperformula-function-dev` | +| The value is right but stale after an edit | `src/DependencyGraph/`, `src/Evaluator.ts` | `src/DependencyGraph/AGENTS.md` | +| Wrong after adding/removing rows or columns | `src/dependencyTransformers/`, `src/LazilyTransformingAstService.ts` | `dev-docs/ARCHITECTURE.md` | +| Wrong in one language only | `src/i18n/languages/` | skill `i18n-translations` | +| Coercion or comparison is wrong | `src/interpreter/ArithmeticHelper.ts` | `src/interpreter/AGENTS.md` | +| The public API behaves differently from its docs | `src/HyperFormula.ts` | `dev-docs/DOC-STANDARDS.md` | + +Use the `typescript-lsp` plugin to find a definition or its callers. Grep is for text, not for symbols. + +## Non-negotiables + +- **Headless.** No DOM, no network, no filesystem in `src/`. +- **Never throw from evaluation.** Return a `CellError` with a message from `src/error-message.ts`. A throw takes down the whole recalculation, not one cell. +- **Coerce through `ArithmeticHelper`.** Spreadsheet coercion is not JavaScript coercion, and it is already implemented once. +- **No English function names hard-coded** anywhere in the parser or interpreter. +- **Incremental recalculation.** Anything that forces a full recalculation is a regression. +- **JSDoc on `HyperFormula.ts` is published output.** Write it for the docs portal reader. + +## Workflow + +1. **Reproduce first.** Write the failing test before the fix — see skill `test-writing-discipline`. For a calculation bug, the smallest reproduction is a two-line `buildFromArray` plus one `getCellValue`. +2. **Fetch the private test suite** if it is not present: `npm run test:setup-private`. Do this after every branch switch. +3. **Change the narrowest stage** that owns the behaviour. A bug that looks like an interpreter problem is often a parser or graph problem; confirm which before editing. +4. **Run the fast loop**: `npm run test:jest`. Narrow it with `npm run test:jest -- `. +5. **Complete the change**: tests, docs, changelog, translations. See [`dev-docs/DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md). +6. **Lint**: `npm run lint`. ESLint is the source of truth for style. + +## Performance + +The engine is a calculation engine; production-code performance is a feature. Before changing anything under `src/interpreter/`, `src/DependencyGraph/`, `src/Evaluator.ts`, `src/parser/ParserWithCaching.ts`, or `src/LazilyTransformingAstService.ts`, ask what runs per cell and what runs once. + +Run `npm run test:performance` for changes that touch evaluation or CRUD hot paths. It requires the private suite. + +## Common traps + +- **The parser cache keys on the formula string.** If a parse result must depend on anything else, the key must include it — otherwise the cache returns the wrong AST. +- **Ranges are one vertex.** Expanding a range into per-cell edges destroys the performance the engine exists for. +- **`EmptyCellVertex` is not "no vertex".** An empty cell someone depends on still needs one. +- **A new mutation needs `CrudOperations` (validate), `Operations` (mutate), and `UndoRedo` (record).** Miss the third and undo silently diverges. +- **A new config option needs a default in `ConfigParams.ts`, validation in `Config.ts`, and a guide entry.** + +Deep reference: [`dev-docs/ARCHITECTURE.md`](../../../dev-docs/ARCHITECTURE.md). diff --git a/.claude/skills/hyperformula-function-dev/SKILL.md b/.claude/skills/hyperformula-function-dev/SKILL.md new file mode 100644 index 0000000000..dba0599a1a --- /dev/null +++ b/.claude/skills/hyperformula-function-dev/SKILL.md @@ -0,0 +1,112 @@ +--- +name: hyperformula-function-dev +paths: src/interpreter/** +description: Use when adding a new built-in spreadsheet function to HyperFormula, changing an existing one's signature, arguments, return type, or error behaviour, or when a function returns the wrong value or the wrong error. Covers the FunctionPlugin contract, runFunction and argument metadata, the function metadata catalogue, translations, and the full end-to-end checklist. +--- + +## The five places a function lives + +A function is not done until all five agree. Skipping one produces a silent, specific failure: + +| # | Place | Skipping it causes | +|---|---|---| +| 1 | `src/interpreter/plugin/Plugin.ts` — implementation | The function does not exist | +| 2 | `implementedFunctions` in the same file — engine metadata | The id does not resolve | +| 3 | `src/interpreter/functionMetadata/categories/.ts` — catalogue entry | `npm run docs:generate-function-docs` **fails the docs build** | +| 4 | `src/i18n/languages/*.ts` — every language | The function is unparseable in that language | +| 5 | `test/` — tests | The change is not done; see the definition of done | + +## 1–2. The plugin + +```ts +export class AbsPlugin extends FunctionPlugin implements FunctionPluginTypecheck { + public static implementedFunctions: ImplementedFunctions = { + 'ABS': { + method: 'abs', + parameters: [ + {argumentType: FunctionArgumentType.NUMBER} + ] + }, + } + + public abs(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('ABS'), Math.abs) + } +} +``` + +The key is the **canonical English id**. `method` names the class method; `FunctionPluginTypecheck` turns a mismatch into a compile error. Put a new function in the existing plugin for its family; create a new plugin file only for a genuinely new family, and register it where the other plugins are registered. + +### `runFunction` already did the work + +Pass raw args, state, `this.metadata('ID')`, and a plain implementation. `runFunction` handles argument evaluation, arity checking, coercion to the declared `argumentType`, range vectorization, and broadcasting. + +**Write the implementation as if it received already-coerced scalars.** Re-checking types inside it is a sign the metadata is wrong. + +`runFunctionWithReferenceArgument` is the variant for reference-taking functions (`ROW`, `COLUMN`, `SHEET`, `ISFORMULA`); it takes a no-argument, a reference, and a non-reference callback. + +### Argument metadata + +| Field | Meaning | +|---|---| +| `argumentType` | `NUMBER`, `STRING`, `BOOLEAN`, `SCALAR`, `NOERROR`, `RANGE`, `ANY`, `INTEGER`, `COMPLEX` | +| `optionalArg` | May be omitted | +| `defaultValue` | Used when omitted | +| `minValue`, `maxValue`, `lessThan`, `greaterThan` | Enforced during coercion — use these instead of checking in the implementation | +| `passSubtype` | Keep the extended number subtype rather than unwrapping to a raw number | + +### Function-level metadata + +| Field | Set it when | +|---|---| +| `repeatLastArgs` | The function is variadic — how many trailing arguments repeat | +| `expandRanges` | Range arguments should be inlined into scalar arguments | +| `isVolatile` | It must recalculate every time (`RAND`, `NOW`) | +| `isDependentOnSheetStructureChange` | It must recalculate when rows or columns are added or removed | +| `doesNotNeedArgumentsToBeComputed` | Reference and range arguments create no dependency | +| `vectorizationForbidden` | It returns an array, or is otherwise special | +| `sizeOfResultArrayMethod` | **Required** for any function that can return an array | +| `returnNumberType` | The result is a percent, currency, date, or time | + +## 3. The catalogue entry + +Add the entry to `src/interpreter/functionMetadata/categories/.ts`: `shortDescription`, `parameters` with `snake_case` names and descriptions, `examples`, `documentationUrl`, `category`. Every field is required. + +Three ways this bites: + +- **No entry** → described as `category: 'Custom'`, which has no docs section, so the docs build **fails**. +- **Parameter count disagrees with `implementedFunctions`** → the implementation wins. Authored names and descriptions are discarded, positional `Arg1`, `Arg2` are reported, and a console warning names the function. Availability is unaffected, so nothing red appears in the test run. +- **Optionality is never cross-checked.** The `optional` flag comes only from `optionalArg` / `defaultValue`. If the function accepts a call arity alone does not express, declare `optionalArg: true` explicitly. + +Writing rules: refer to another argument by its exact `snake_case` name, no docs-page-local markup in `shortDescription`, and describe **HyperFormula's** behaviour rather than Excel's. Full detail: [`dev-docs/FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md). + +## 4. Translations + +Add the name to **every** file in `src/i18n/languages/`. Sources and rules: skill `i18n-translations`. + +## 5. Tests + +In `test/`. Cover, at minimum: + +- the documented result for ordinary arguments; +- each boundary the metadata declares (`minValue`, `maxValue`, `lessThan`, `greaterThan`); +- wrong argument **count** — too few and too many; +- wrong argument **type**, asserting the specific `CellError`; +- error **propagation**: an argument that is itself an error; +- an empty cell and an empty range as arguments; +- if the function returns an array: the spilled shape, and the size predicted by `sizeOfResultArrayMethod`; +- if `optionalArg` is declared: the call with the argument omitted. + +One assertion per case, no loops, no conditionals. See skill `hyperformula-unit-testing`. + +## Verify + +```bash +npm run test:jest -- +npm run docs:generate-function-docs # fails loudly on a bad or missing catalogue entry +npm run lint +``` + +## Deviating from Excel + +HyperFormula deliberately deviates in places. If the implementation does not match Excel or Google Sheets, that is a decision — record it in [`docs/guide/list-of-differences.md`](../../../docs/guide/list-of-differences.md) and say so in the changelog entry. Never write a description that documents Excel's behaviour while the code does something else. diff --git a/.claude/skills/hyperformula-unit-testing/SKILL.md b/.claude/skills/hyperformula-unit-testing/SKILL.md new file mode 100644 index 0000000000..e9c863e2be --- /dev/null +++ b/.claude/skills/hyperformula-unit-testing/SKILL.md @@ -0,0 +1,60 @@ +--- +name: hyperformula-unit-testing +paths: test/** +description: Use when writing or modifying tests for HyperFormula, or when a change to `src/` needs test coverage. Covers the two suites, fetching the private suite, how to build an engine in a test, and what a function or CRUD change must cover. +--- + +## Two suites + +| Suite | Path | Availability | +|---|---|---| +| Smoke tests | `test/smoke.spec.ts` | In this repository | +| Full suite | `test/hyperformula-tests/` | Private repository, git-ignored | + +**The private suite is branch-matched.** `npm run test:setup-private` checks out the branch of the same name, creating it from `develop` if it does not exist. Run it after every branch switch — otherwise the previous branch's tests run against the current source and the results mean nothing. + +In a fresh git worktree the directory is absent entirely, and `npm run test:jest` covers only the smoke tests. It looks like a clean pass. + +## Running + +```bash +npm run test:jest # fast loop +npm run test:jest -- # one file or one describe +npm run test:watch +npm run test # lint + jest + browser — the full local gate +``` + +`test:performance` and `test:compatibility` resolve into `test/hyperformula-tests/`. Without it they fail on a missing path, not on an assertion. Read the error before concluding the code is broken. + +## Shape of a case + +```ts +it('returns the divisor sign for arguments with opposite signs', () => { + const engine = HyperFormula.buildFromArray([['=MOD(-3, 12)']]) + + expect(engine.getCellValue(adr('A1'))).toBe(9) +}) +``` + +- **One assertion per case.** Split rather than adding a second `expect`. +- **No loops, no conditionals.** A parameterised loop hides which input failed. Write the cases out. +- **Name the case after the behaviour**, not after the function: "returns `#VALUE!` when the range is empty", not "test SUMIFS". +- Build the smallest engine that exhibits the behaviour. A two-cell array beats a realistic sheet. + +## What to cover + +**A bug fix** ships a test that fails against the unfixed code. Write it first, run it, watch it fail, then fix. A test written after the fix proves nothing about the bug. + +**A function change** — see skill `hyperformula-function-dev` for the full list: ordinary arguments, each declared boundary, wrong argument count, wrong argument type with the specific `CellError`, error propagation, empty cell and empty range, array shape, omitted optional argument. + +**A CRUD or structural change** — add and remove rows and columns around a formula, move a range across a formula that references it, then assert **both** the recalculated value and the formula text afterwards. Structural bugs usually show up in the formula text first. + +**A parser change** — the parse, the round trip through `Unparser`, at least one non-English language, and malformed input that must produce a parsing error rather than a throw. + +**A config option** — the default, a valid non-default value, and an invalid value that must be rejected. + +## Never + +- Never relax an assertion, widen a matcher, add `toBeCloseTo` where an exact value is expected, or skip a case to turn a run green. When a test is red the default assumption is that the **code** is wrong. See skill `test-writing-discipline`. +- Never test `docs/`, `examples/`, or `script/`. +- Never assert on internals a public API already exposes — use `getCellValue`, `getCellFormula`, `getSheetValues`. diff --git a/.claude/skills/i18n-translations/SKILL.md b/.claude/skills/i18n-translations/SKILL.md new file mode 100644 index 0000000000..1042704e39 --- /dev/null +++ b/.claude/skills/i18n-translations/SKILL.md @@ -0,0 +1,40 @@ +--- +name: i18n-translations +paths: src/i18n/** +description: Use when adding a built-in function that needs translated names, adding a language pack, or fixing a function name that is wrong in one language. Covers the translation sources, the rules, and what breaks when a key is missing. +--- + +## Why it matters + +Parsing is language-dependent: the lexer builds its token set from the active translation package. A function with a missing translation is not "untranslated" — it is **unparseable** in that language. + +## Adding a function name + +Add the key to **every** file in `src/i18n/languages/`, in the same change as the function. There are 19 language packs; missing one is the common failure and nothing type-checks it in every path. + +Do not reorder existing entries while adding one — it turns a one-line diff into an unreviewable one. + +The key is the canonical English id, the same one used in `implementedFunctions` and in the metadata catalogue. Translations map onto that id; they never replace it. + +## Where to find a translation + +| Source | Use for | +|---|---| +| [Microsoft's Excel functions translator](https://support.microsoft.com/en-us/office/excel-functions-translator-f262d0c0-991c-485b-89b6-32cc8d326889) | Languages Excel supports | +| | Cross-check against the above | +| Google Sheets function list with `hl` set to the locale — e.g. for Indonesian | Languages Excel does not support | +| The English name | Functions Google Sheets does not list either — this matches what Excel does in unsupported locales | + +Never invent a translation, and never machine-translate a function name. A wrong name ships to every user of that language pack and cannot be changed without breaking their formulas. + +## Scope + +Translate the function **name** only. Argument separators, error literals, and boolean literals are also part of a language package; changing them is a language-pack decision, not a side effect of adding a function. + +## Adding a language pack + +A new pack needs: the file in `src/i18n/languages/`, its export in `src/i18n/languages/index.ts`, a complete key set matching the other packs, and a changelog entry. `npm run bundle:languages` produces the standalone UMD build. + +## Tests + +A translation change needs a test that parses a formula using the translated name and asserts the result — in that language, not in English. diff --git a/.claude/skills/pr-creation/SKILL.md b/.claude/skills/pr-creation/SKILL.md new file mode 100644 index 0000000000..02a6bd2e57 --- /dev/null +++ b/.claude/skills/pr-creation/SKILL.md @@ -0,0 +1,62 @@ +--- +name: pr-creation +description: Use before creating, pushing, opening, or updating a pull request in the HyperFormula repository — load this BEFORE running `gh pr create` or pushing a branch, not only when the user says "PR". Covers branch naming, the pre-flight gate, filling the PR template, and the changelog ordering. +--- + +## Order of operations + +1. Commit the source change on a feature branch. +2. Run the pre-flight gate (below). +3. Push and run `gh pr create`. +4. Read the PR number from the URL it prints. +5. Add the changelog entry, linking that PR (or the public issue it fixes) — see skill `changelog-creation`. +6. Commit and push the changelog entry to the same branch. + +The changelog entry comes **after** the PR exists, because it carries the link. Do not guess the next PR number. + +## Branch naming + +`/-`, lowercase, hyphen-separated: + +``` +feat/hf-305-overwrite-flag +fix/hf-357-mod-divisor-sign +docs/hf-282-counts-guide +spike/hf-270-null-to-zero +``` + +Never put a client name, a customer report's wording, or anything from a private ticket in the branch name. The ticket identifier alone is fine. + +Never force-push to `master`, `develop`, or a branch that already has an open pull request. + +## Pre-flight gate + +```bash +npm run test:setup-private # after any branch switch +npm run lint +npm run test:jest +``` + +Run `npm run test` (adds the browser run) when the change touches bundling, module format, or anything browser-specific. Run `npm run docs:generate-function-docs` when the change touches a function or its catalogue entry — it fails on a bad or missing entry. + +Read the output. Do not open a pull request on a red run and describe it as ready. + +## The PR template + +`.github/pull_request_template.md` is filled in, not deleted: + +- **Context** — why the change is needed and what problem it solves. Written for a reviewer who has not seen the ticket. +- **How did you test your changes?** — the actual commands run and what they showed, not "added tests". +- **Types of changes** — tick every box that applies. Breaking change is its own box; tick it honestly. +- **Related issues** — `Fixes #...` for a public issue. A private ticket identifier may be named, its contents may not. +- **Checklist** — go through it. The OpenDocument, Excel, and Google Sheets compatibility boxes are real questions about the change, not formalities; if the behaviour deliberately deviates, say so in Context and record it in `docs/guide/list-of-differences.md`. + +## Keep the description current + +The most common failure in this repository is a description written once and never revisited. When the branch changes scope, update the description in the same push. + +## Definition of done + +Before requesting review, confirm every item in [`dev-docs/DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md): production change, tests, documentation, JSDoc, changelog entry, and a current PR description. One pull request holds one atomic change — unrelated refactors and clean-ups belong in their own. + +Read your own diff end to end before asking anyone else to. diff --git a/.claude/skills/test-writing-discipline/SKILL.md b/.claude/skills/test-writing-discipline/SKILL.md new file mode 100644 index 0000000000..438e2baede --- /dev/null +++ b/.claude/skills/test-writing-discipline/SKILL.md @@ -0,0 +1,58 @@ +--- +name: test-writing-discipline +description: Use when writing, fixing, or reviewing any test for HyperFormula, and whenever a test is red during feature work. Enforces that tests prove intended behaviour rather than merely execute code, and never go "green for the sake of green". +--- + +## The rule + +A test exists to prove that the code does what it is supposed to do. A test that passes without proving anything is worse than no test: it occupies the space where the real test would have gone, and it makes the next person believe the behaviour is covered. + +## Intent first + +Write the test from the requirement, not from the implementation. For a bug fix that means: + +1. Write the test that describes the correct behaviour. +2. **Run it. Watch it fail.** A bug-fix test that has never failed proves nothing about the bug. +3. Fix the code. +4. Run it again. + +Reading the implementation first and then writing a test that mirrors it produces a test that passes for any implementation, including the wrong one. + +## When a test is red, the code is wrong + +That is the default assumption. Change the test only when you can state, in one sentence, why the test's expectation was wrong — and that sentence must be about the specification, not about the effort of fixing the code. + +## Banned ways of going green + +- Relaxing an assertion: exact value to `toBeCloseTo`, specific error to "some error", `toEqual` to `toContain`. +- Deleting the assertion that fails and keeping the ones that pass. +- Adding `.skip`, `.todo`, or a comment-out to a case that used to run. +- Widening a matcher until every implementation passes. +- Catching the error the code should not be throwing and asserting that it was caught. +- Mocking the unit under test, or mocking so deeply that only the mock is exercised. +- Asserting that a function "does not throw" when the requirement is a specific returned value. +- Changing the input until the current implementation happens to be right. + +If you find yourself doing any of these, stop and say so instead. "This test fails and I do not yet know why" is a useful report; a green run that hides it is not. + +## Hollow assertions + +These execute code and prove nothing: + +```ts +expect(engine.getCellValue(adr('A1'))).toBeDefined() // any value passes +expect(() => engine.setCellContents(...)).not.toThrow() // any non-throwing bug passes +expect(result).toBeTruthy() // 1, 'x', and [] all pass +``` + +Assert the value the specification names. + +## Verify with a real run + +Never claim a test passes without having run it. Never claim a fix works because the reasoning is sound. Run the command, read the output, and quote the decisive line. + +```bash +npm run test:jest -- +``` + +If the private suite is missing, `npm run test:jest` runs only the smoke tests and reports a clean pass over almost nothing. Confirm `test/hyperformula-tests/` exists before treating a green run as coverage — `npm run test:setup-private`. diff --git a/.claude/skills/writing-docs-pages/SKILL.md b/.claude/skills/writing-docs-pages/SKILL.md new file mode 100644 index 0000000000..59a679569c --- /dev/null +++ b/.claude/skills/writing-docs-pages/SKILL.md @@ -0,0 +1,49 @@ +--- +name: writing-docs-pages +paths: docs/** +description: Use when creating or editing a page in the HyperFormula documentation portal, adding a guide, or updating the API reference. Covers what is generated versus hand-written, sidebar registration, running the portal, and the writing rules. +--- + +## Generated versus hand-written + +| Path | Source | Editable | +|---|---|---| +| `docs/guide/*.md` | Hand-written | yes | +| `docs/guide/built-in-functions.tmpl.md` | Hand-written template | yes | +| `docs/guide/built-in-functions.md` | Generated from the template + `src/interpreter/functionMetadata/` | **no — git-ignored** | +| `docs/api/` | Generated from JSDoc by TypeDoc | **no — git-ignored** | +| `docs/.vuepress/` | Configuration, theme, components | yes | +| `docs/examples/` | Code examples embedded in guides | yes | + +To change what the functions page says about a function, edit its **catalogue entry** in `src/interpreter/functionMetadata/categories/`, then run `npm run docs:generate-function-docs`. + +To change the API reference, edit the **JSDoc** in `src/`, then run `npm run typedoc:build-api`. + +## Running the portal + +```bash +npm run bundle-all # the portal embeds the built engine +npm run docs:dev # http://localhost:8080/hyperformula/ +``` + +Both `docs:dev` and `docs:build` regenerate the functions page and the API reference first. + +## Adding a guide + +1. Create the file in `docs/guide/`. +2. **Register it in the sidebar** under `docs/.vuepress/`. A page without a sidebar entry builds successfully and is unreachable. +3. Link to the API reference for the detail rather than restating it. + +## Writing rules + +Follow [`DOCS_CONTENT_GUIDE.md`](../../../DOCS_CONTENT_GUIDE.md) for style, language, and structure. Beyond it: + +- **Do not duplicate the API reference.** It is the primary source of truth and holds the detail. A guide gives the overview and links. +- **Describe HyperFormula's behaviour, not Excel's.** HyperFormula deliberately deviates in places. Verify against the implementation, and record any deviation in [`docs/guide/list-of-differences.md`](../../../docs/guide/list-of-differences.md). +- Every code example must run against the current version. Examples live in `docs/examples/`; `docs/code-examples-generator.sh` derives the JavaScript variants from the TypeScript ones. + +## When docs are required + +Any public-API change updates the JSDoc **and** the affected guides in the same change. Any user-facing behaviour change is documented. Any breaking change adds a migration-guide section. Documentation-only changes need no changelog entry. + +See [`dev-docs/DOC-STANDARDS.md`](../../../dev-docs/DOC-STANDARDS.md). diff --git a/.worktreeinclude b/.worktreeinclude new file mode 100644 index 0000000000..c2c6c2f70d --- /dev/null +++ b/.worktreeinclude @@ -0,0 +1,47 @@ +# Gitignored files copied into each worktree Claude Code creates. +# +# Uses .gitignore syntax. Only files that match a pattern AND are gitignored are +# copied, so nothing tracked is ever duplicated. Applies to worktrees created by +# `--worktree`, by subagent isolation, and by the desktop app. +# +# This covers only the file half of setting up a worktree. It cannot install +# dependencies and it cannot fetch the private test suite. See dev-docs/WORKTREES.md. + +# Cloudflare Worker local secrets, used by the docs deploy/preview scripts. +# Absent from a fresh worktree, so `wrangler` commands fail there with no +# explanation that points at the worktree. +.dev.vars +.dev.vars.* + +# Deliberately NOT copied — listing these here would cause real bugs: +# +# node_modules A copy is not an install: `.bin` shims and native +# builds do not survive it. Run `npm ci`, or symlink it +# with `worktree.symlinkDirectories`. +# +# test/hyperformula-tests/ +# The private suite is BRANCH-MATCHED to this repository +# (test/fetch-tests.sh checks out the branch of the same +# name). A copy carries the source branch's tests and +# silently tests the wrong thing — worse than having no +# suite at all. Run `npm run test:setup-private` in the +# worktree instead. +# +# lib/ dist/ es/ commonjs/ typings/ languages/ +# Build outputs. A stale copy makes `npm run test:browser` +# and the bundle checks pass or fail against the other +# branch's build. Run `npm run bundle-all`. +# +# docs/api/ docs/guide/built-in-functions.md docs/.vuepress/dist/ +# Generated from source. A copy goes stale the moment the +# JSDoc or the metadata catalogue changes. +# +# coverage/ test-jasmine/ test-jest/ .wrangler/ +# Run artifacts and local tool state. Nothing reads them +# across branches. +# +# .claude/settings.local.json +# Not needed. Since Claude Code v2.1.211 a permission +# approval made in a worktree is saved to the MAIN +# checkout's file and applies in every worktree of the +# repository, so a copy here would only go stale. diff --git a/AGENTS.md b/AGENTS.md index c81e681c12..f297553114 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,10 +1,46 @@ # AGENTS.md -Instructions for AI coding agents (Cursor, Claude Code, Codex, Aider, and any other AI tool) working in this repository. +Instructions for AI coding agents (Claude Code, Cursor, Codex, Aider, and any other AI tool) working in this repository. -## Start here +This is the **repository-level** guide: rules that apply everywhere, plus a map of where to look next. Directory-specific rules live in that directory's own `AGENTS.md`, which loads automatically when you work there. -Whatever you do, start by reading entire [DEV_DOCS.md](DEV_DOCS.md). Only then proceed to your task. +In every directory, `CLAUDE.md` is a symlink to its sibling `AGENTS.md`. Edit `AGENTS.md` — the symlink keeps Claude Code and Cursor reading the same single source. + +## Overview + +HyperFormula is a headless spreadsheet calculation engine in TypeScript. No UI, no DOM, no server: it parses formulas, tracks cell dependencies, and recalculates incrementally, in the browser and in Node. It ships as ES, CommonJS, and UMD bundles plus standalone language packs. + +## Where to look + +Route to the lowest correct scope. `AGENTS.md` answers *"what must I never get wrong here, and where do I look next."* `dev-docs/` answers *"how does this work and why."* Skills answer *"how do I do task X."* `AGENTS.md` files load automatically in their subtree; `dev-docs/` files need an explicit read. + +| You are working on | Look here | +|---|---| +| Anything repository-wide (build, release, workspace) | This file; [`dev-docs/`](dev-docs/) | +| Engine source, any subsystem | [`src/AGENTS.md`](src/AGENTS.md); [`dev-docs/ARCHITECTURE.md`](dev-docs/ARCHITECTURE.md) | +| Formula parsing | [`src/parser/AGENTS.md`](src/parser/AGENTS.md) | +| Formula evaluation | [`src/interpreter/AGENTS.md`](src/interpreter/AGENTS.md) | +| A built-in spreadsheet function | [`src/interpreter/plugin/AGENTS.md`](src/interpreter/plugin/AGENTS.md); skill `hyperformula-function-dev` | +| Function descriptions shown in the API and the docs | [`src/interpreter/functionMetadata/AGENTS.md`](src/interpreter/functionMetadata/AGENTS.md); [`dev-docs/FUNCTION-CATALOGUE.md`](dev-docs/FUNCTION-CATALOGUE.md) | +| Dependency tracking and recalculation order | [`src/DependencyGraph/AGENTS.md`](src/DependencyGraph/AGENTS.md) | +| Function-name translations | [`src/i18n/AGENTS.md`](src/i18n/AGENTS.md); [`dev-docs/I18N.md`](dev-docs/I18N.md) | +| Tests | [`test/AGENTS.md`](test/AGENTS.md); [`dev-docs/TESTING.md`](dev-docs/TESTING.md) | +| The documentation portal | [`docs/AGENTS.md`](docs/AGENTS.md); [`DOCS_CONTENT_GUIDE.md`](DOCS_CONTENT_GUIDE.md) | +| Build, docs-generation, and release scripts | [`script/AGENTS.md`](script/AGENTS.md) | +| Working inside a linked git worktree | [`dev-docs/WORKTREES.md`](dev-docs/WORKTREES.md) | +| Step-by-step task workflows | [`.claude/skills/`](.claude/skills/) | + +`dev-docs/` reference index: [`dev-docs/README.md`](dev-docs/README.md). + +## Mandatory for every change + +1. **Tests.** Every change to `src/` ships tests in `test/`. A bug fix ships a test that fails against the unfixed code. See [`dev-docs/TESTING.md`](dev-docs/TESTING.md). +2. **Documentation.** A public-API, behaviour, or configuration change updates the JSDoc and the affected guides in the same change. A breaking change adds a migration-guide section. See [`dev-docs/DOC-STANDARDS.md`](dev-docs/DOC-STANDARDS.md). +3. **Changelog entry**, unless the change is documentation-only. +4. **Keep the pull request description current.** Update it as the branch evolves. +5. **Update `AGENTS.md`.** If the change introduces a convention, constraint, file location, or gotcha that future agents should know, record it in the `AGENTS.md` at the correct scope. + +The full list, and what "correct" means for each item, is in [`dev-docs/DEFINITION-OF-DONE.md`](dev-docs/DEFINITION-OF-DONE.md). ## Never publish sensitive information @@ -19,6 +55,26 @@ Describe the change on its own technical terms instead: write "fix an off-by-one If a change cannot be described without such information, stop and ask the user how to proceed. +## Build and test + +Node version is pinned in [`.nvmrc`](.nvmrc); install with `npm ci`. + +| Command | Runs | +|---|---| +| `npm run test:jest` | Jest — the fast loop | +| `npm run test` | Lint, Jest, and the browser run — the full local gate | +| `npm run test:setup-private` | Fetch the private test suite for the current branch | +| `npm run lint` | ESLint, the source of truth for formatting and code rules | +| `npm run bundle-all` | Every bundle, then verify them | + +The full command reference, with what each script leaves on disk, is in [`dev-docs/BUILD.md`](dev-docs/BUILD.md). + +**Run `npm run test:setup-private` after every branch switch.** The private suite in `test/hyperformula-tests/` is branch-matched; a stale checkout silently runs another branch's tests. + +## Never edit generated or built output + +`lib/`, `es/`, `commonjs/`, `dist/`, `languages/`, `typings/`, `docs/api/`, and `docs/guide/built-in-functions.md` are all produced by a build step and git-ignored. Never edit them, never commit them, and never read them to answer a question about behaviour — read `src/` instead. + ## Other important resources - the repository [README.md](README.md) — high-level project description and quick install/usage @@ -44,7 +100,7 @@ This section is maintained by the team. Whenever an AI agent makes a mistake wor - **Short title** — What the agent did wrong. What it should have done instead. --> -1. Often pull request descriptions becomes obsolete. Remember to update it as you work. +1. **Stale pull request descriptions** — The description was written once and never revisited. Update it as the branch evolves. ## Skills, MCPs, and other agent tools @@ -54,4 +110,5 @@ This section is maintained by the team. Skills, MCP servers, and other tools vet - **Name** — What it provides and when to use it. --> -_No items yet._ +- **`.claude/skills/`** — Repository skills, scoped by a `path` glob in their frontmatter. `hyperformula-dev` is the entry point for engine work; `hyperformula-function-dev` for adding or changing a built-in function; `pr-creation` before opening a pull request. +- **`typescript-lsp` plugin** — Language-server-backed go-to-definition and find-references. Use it instead of grepping for a symbol's definition or callers; grep stays right for text searches. Enabled repository-wide in `.claude/settings.json`. diff --git a/DEV_DOCS.md b/DEV_DOCS.md index 78fe3f49c0..61ec4c8044 100644 --- a/DEV_DOCS.md +++ b/DEV_DOCS.md @@ -1,171 +1,21 @@ # Developer documentation -Canonical reference for everyone working on the HyperFormula source code: maintainers, the internal team, and AI agents triggered by them. Everything a developer needs to know lives here or is linked from here. - -## Quick links - -- **[Building, testing, and linting](docs/guide/building.md)** — all `npm` commands and build outputs -- **[Test suite](test/README.md)** — smoke tests and how to attach the private test suite -- **[Public docs portal](https://hyperformula.handsontable.com/docs)** — main documentation -- **[Docs README](docs/README.md)** — how to run the docs portal locally -- **[Docs content guide](DOCS_CONTENT_GUIDE.md)** — how to create and edit docs content -- **[Changelog](CHANGELOG.md)** -- **[Pull request template](.github/pull_request_template.md)** - -## Repository layout - -``` -. -├── src/ # Source code -│ ├── HyperFormula.ts # Main engine class, public API entry point -│ ├── parser/ # Formula parsing (uses Chevrotain parser generator) -│ ├── interpreter/ # Formula evaluation engine -│ │ └── plugin/ # Built-in spreadsheet function plugins -│ ├── DependencyGraph/ # Cell dependency tracking and recalculation order -│ ├── CrudOperations.ts # Create/read/update/delete operations on sheets and cells -│ └── i18n/ # Function-name translations per language -├── test/ # Test suite -├── docs/ # Public documentation portal (VuePress) -│ ├── guide/ # Markdown guides (building, contributing, usage…) -│ ├── api/ # API reference (generated from JSDoc) -│ ├── .vuepress/ # VuePress configuration, theme, components -│ └── README.md # How to run the docs portal locally -├── script/ # Maintenance and release scripts -├── .github/ # CI workflows, issue and PR templates -├── DEV_DOCS.md # Canonical developer documentation (this file) -├── AGENTS.md # Guidance for AI agents -├── CONTRIBUTING.md # Guide for external contributors -├── README.md # Project overview -├── CHANGELOG.md -├── LICENSE.txt -├── package.json -└── tsconfig.json -``` - -## Architecture - -### Core modules - -- `src/HyperFormula.ts` — main engine class, public API entry point -- `src/parser/` — formula parsing (uses the [Chevrotain](https://chevrotain.io/) parser generator) -- `src/interpreter/` — formula evaluation engine -- `src/DependencyGraph/` — cell dependency tracking and recalculation order -- `src/CrudOperations.ts` — create/read/update/delete operations on sheets and cells - -### Function plugins (`src/interpreter/plugin/`) - -All spreadsheet functions are implemented as plugins extending `FunctionPlugin`. Each plugin: - -- declares an `implementedFunctions` static property mapping function names to metadata -- uses the `runFunction()` helper for argument validation, coercion, and array handling -- registers function translations in `src/i18n/languages/` - -## Definition of Done - -Each change to the production code (bugfix, new feature, or improvement) must include the following elements **before** requesting a code review: - -- Changes to the production code - - including changes to all supported language packs in `src/i18n/languages` (if applicable) -- Automatic tests - - for bug fixes: at least one test reproducing the bug - - for new features: a set of tests precisely describing the feature - - pull requests from external contributors should include tests in the `test/` directory (they will be moved to the private repository by the internal team) - - the internal team adds tests directly to the private repository (through a separate pull request) -- Updates to documentation related to the change - - for breaking changes: a section in the migration guide -- Technical documentation in the form of JSDoc comments (high-level description of the concepts used in more complex code fragments) -- Changelog entry (not required for documentation-only changes (guides, JSDoc, README, etc.) -- Pull request description - -Every element of the change must not only be present but also correct: the changelog entry must describe the change accurately, and the documentation updates must match the new behaviour. - -Read through your own diff before requesting a review, and ask yourself what could be done better. Fix what you find while the change is still yours. - -A single pull request should contain an atomic self-contained functional change (single bugfix, single feature, single improvement). If a pull request contains multiple features or bugfixes, it should be split. Every change in the pull request must be relevant to the issue it solves — unrelated refactors, reformatting, or clean-ups belong in a separate pull request. - -## Code style - -- Prefer a functional approach where possible (`filter`, `map`, `reduce`). -- Write self-documenting code: use meaningful names for classes, functions, and variables. Add code comments only when they explain intent the code itself cannot. -- Add JSDoc to all classes and functions. -- Choose readability over brevity. Explicit, obvious code is better than a clever one-liner. -- Keep the logic straightforward: avoid convoluted control flow, prefer early returns and a flat structure, and make sure every branch and condition has a reason to exist. -- Follow clean code principles and general programming best practices: small functions with a single responsibility, no hidden side effects, no magic values. -- Avoid duplication. Extract shared logic instead of copying it, and reuse the existing helpers and abstractions of the codebase. -- Match the style of the surrounding code and of the project as a whole. New code should not stand out from its neighbours. -- Optimize for long-term maintainability: someone else should be able to read, extend, and safely change the code months from now. -- ESLint is the source of truth for formatting and code rules. Run `npm run lint` before submitting changes (see [building](docs/guide/building.md#run-the-linter)). - -## Performance - -HyperFormula is a calculation engine, so the performance of the production code is a feature, not an afterthought. - -- Consider the computational complexity of every change, especially in code that runs per cell, per formula, or per dependency-graph node. Nested loops over ranges and repeated work that could be computed once or cached are the usual suspects. -- Pick the best complexity that still keeps the code readable. When a faster algorithm is harder to follow, explain the trade-off in a JSDoc comment. -- Run `npm run test:performance` for changes that may affect the evaluation or CRUD hot paths. - -## Automatic tests - -- All changes to the production code (the `src/` directory) must be covered by automatic tests kept in the `test/` directory. -- Each test case must be very simple and focused on a single assertion. Don't use loops, conditionals, or other control flow statements in test cases. -- Cover more than the happy path: boundary values, empty and invalid input, error results, and interactions with related features. -- Before requesting a review, ask yourself which further tests would be valuable and add the ones that protect against realistic regressions. -- Don't add tests for code in the `docs/`, `examples/`, and `script/` directories. - -## Documentation - -- Follow the [documentation content guide](DOCS_CONTENT_GUIDE.md) when creating or editing docs (writing style, language, and how to structure guides). -- We try not to duplicate information in the documentation. The API reference (generated from JSDocs) should contain all the details about each function and class (it is the primary source of truth). Guides should provide high-level overview. They may duplicate some of the information from the API reference if they are relevant to the context but, above all, they should link to the API reference for the detailed information. - -## How to add a new function - -Adding a built-in function is similar to adding a [custom function](docs/guide/custom-functions.md), so that guide is a useful reference for the function-implementation patterns (argument metadata, return types, array handling). The built-in flow on top of that is: - -1. Create or modify a plugin in `src/interpreter/plugin/`. -2. Add function metadata to `implementedFunctions`. -3. Implement the function method. -4. Add a catalogue entry to `src/interpreter/functionMetadata/categories/.ts` (see below). -5. Add translations to all language files in `src/i18n/languages/`. -6. Add tests in `test/unit/interpreter/`. - -### The function metadata catalogue - -`src/interpreter/functionMetadata/categories/` holds the human-readable metadata for every built-in function: `shortDescription`, `parameters` (`snake_case` names, each with a description), `examples`, `documentationUrl`, and the category. It is the single source of truth for two consumers: the public [`getAvailableFunctions`/`getFunctionDetails`](docs/api/classes/hyperformula.md) API, and the generated built-in functions guide page (see [docs/README.md](docs/README.md)). - -Every field is required, `documentationUrl` included. Each entry authors its own link rather than inheriting a shared default, so that the links can diverge per function without touching any code; they all happen to point at the same guide page today. - -An entry's `category` must be one of the categories in `FUNCTION_CATEGORIES` — the ones the generated guide page renders as `### ` sections. The separate `'Custom'` category is reserved for user-registered functions and must never appear in `FUNCTION_CATEGORIES` or in a catalogue file: it names no section, and the docs generator rejects an entry carrying it rather than silently dropping it from the page it is building. (That rejection is also what turns a missing catalogue entry into a failed docs build. Arity drift below needs no such guard: the function still reaches the generator, which renders its degraded syntax line.) - -The catalogue's key set decides which ids carry an authored **description**, not which ids the API lists. Both `getAvailableFunctions` and `getFunctionDetails` describe every registered function — custom ones included — and an entry is applied whenever the catalogue holds one for the id, whichever plugin currently provides it: the catalogue is keyed by id, not by implementation, so a custom plugin registered over a built-in id is described with that built-in's authored metadata. Nothing checks a key against a registered function either, so an entry left behind after a rename describes nothing and merely ships in the bundle. Remove or rename it in the same change as the function. - -Two ways to get this wrong: - -- **No catalogue entry.** A registered function with no entry is still listed and still resolves to details, but as a custom function: `category: 'Custom'`, no `shortDescription`, `documentationUrl` or `examples` (the API omits every authored field it has no source for, rather than reporting an empty one), and positional parameter names (`Arg1`, `Arg2`, …). `'Custom'` has no section on the generated docs page, so `npm run docs:generate-function-docs` fails rather than publishing a built-in with no description. -- **Arity drift.** If the entry's parameter **count** disagrees with the plugin's `implementedFunctions`, the implementation wins: `getFunctionDetails` reports one parameter per implemented argument under positional names, discarding the authored names and descriptions, and warns on the console naming the function. The entry's category, `shortDescription`, `examples` and `documentationUrl` are still used, and the function stays listed — the parameter prose degrades, not the availability. - -Keep the entry's parameters in step with `implementedFunctions` whenever you change a signature. - -When a description **refers to** a parameter, use that parameter's exact `snake_case` name, never a prose variant: write "shifts `start_date` by …", not "shifts the start date by …". The same strings are rendered next to the generated syntax line, where the `snake_case` name is what the reader sees, so a prose variant leaves the reader guessing which argument is meant. This applies to `shortDescription` and to every parameter description. - -It does **not** turn ordinary English into identifiers. A parameter's own description may open with a prose noun phrase for the thing it describes — `lower_bound` is fine as "The lower bound, rounded up to an integer" — and words that merely happen to match a name ("entries that appear exactly once") stay as they are. The rule is about naming a *different* argument, or naming one from the syntax line. - -`shortDescription` must not use docs-page-local markup (no relative links, no footnote references): the strings are rendered by API consumers as well as by the docs page. - -Note what the drift warning does **not** cover: **optionality is not cross-checked.** The catalogue authors no optionality of its own — a parameter's `optional` flag is derived entirely from `optionalArg`/`defaultValue` in `implementedFunctions` — so a description that calls an argument optional can sit next to `optional: false` with nothing failing. When a function accepts a call that arity alone does not express (`SHEET()`, `ROW()`, and anything else served by `runFunctionWithReferenceArgument`'s zero-argument path), the plugin must declare `optionalArg: true` explicitly, or the public API will advertise the argument as required. `ROW`, `COLUMN`, `SHEET` and `SHEETS` all declare it; `ISFORMULA` takes the same path and correctly does not, because its zero-argument call is an error rather than a shorthand. - -Descriptions must describe **HyperFormula's** behaviour, not Excel's. Much of the catalogue was seeded from a hand-written page that documented Excel, and HyperFormula deliberately deviates in places (`INT` truncates toward zero, `ISEVEN`/`ISODD` do not truncate, `CEILING.MATH`/`FLOOR.MATH` honour only `mode` = 1). Verify a claim against the implementation before authoring it, and record any deviation in [the list of differences](docs/guide/list-of-differences.md). - -## Internationalization and function translations - -HyperFormula supports internationalization and provides localized function names for all built-in languages. Translation files live in `src/i18n/languages/`. New functions must include translations for all built-in languages. - -When looking for the valid translations for new functions, try these sources: - -- https://support.microsoft.com/en-us/office/excel-functions-translator-f262d0c0-991c-485b-89b6-32cc8d326889 -- http://dolf.trieschnigg.nl/excel/index.php - -For languages not officially supported by Microsoft Excel, the two sources above do not apply. For these languages, use Google Sheets as the reference. Switch the `hl` query parameter to the target locale, for example: - -- https://support.google.com/docs/table/25273?hl=id (Indonesian) - -For functions that Google Sheets does not list either, fall back to the English name (matching the convention used by Excel in unsupported locales). +The developer documentation now lives in **[`dev-docs/`](dev-docs/)**, split by topic. Start with [`dev-docs/README.md`](dev-docs/README.md). + +| I want to know | Read | +|---|---| +| What lives where in this repository | [`dev-docs/STRUCTURE.md`](dev-docs/STRUCTURE.md) | +| How the engine works | [`dev-docs/ARCHITECTURE.md`](dev-docs/ARCHITECTURE.md) | +| How to build, bundle, and lint | [`dev-docs/BUILD.md`](dev-docs/BUILD.md) | +| How to run and write tests | [`dev-docs/TESTING.md`](dev-docs/TESTING.md) | +| What a change must include before review | [`dev-docs/DEFINITION-OF-DONE.md`](dev-docs/DEFINITION-OF-DONE.md) | +| Code style and performance rules | [`dev-docs/CODE-STYLE.md`](dev-docs/CODE-STYLE.md) | +| Documentation rules | [`dev-docs/DOC-STANDARDS.md`](dev-docs/DOC-STANDARDS.md) | +| How function descriptions reach the API and the docs | [`dev-docs/FUNCTION-CATALOGUE.md`](dev-docs/FUNCTION-CATALOGUE.md) | +| How function-name translations work | [`dev-docs/I18N.md`](dev-docs/I18N.md) | +| How to work in a linked git worktree | [`dev-docs/WORKTREES.md`](dev-docs/WORKTREES.md) | +| Where the monorepo migration is going | [`dev-docs/MONOREPO.md`](dev-docs/MONOREPO.md) | + +Rules that AI agents must follow are in [`AGENTS.md`](AGENTS.md), and in the `AGENTS.md` file of each directory. + +External contributors: start with [`CONTRIBUTING.md`](CONTRIBUTING.md). diff --git a/dev-docs/ARCHITECTURE.md b/dev-docs/ARCHITECTURE.md new file mode 100644 index 0000000000..8c117f9f1e --- /dev/null +++ b/dev-docs/ARCHITECTURE.md @@ -0,0 +1,85 @@ +# Engine architecture + +HyperFormula is a headless spreadsheet calculation engine. No UI, no DOM, no server. Everything below runs in the browser and in Node. + +## The pipeline + +``` + setCellContents / buildFromArray + │ + ▼ + CellContentParser classify raw input: formula, number, date, string, error + │ + ▼ + parser/ (Chevrotain) formula text ──► AST + relative dependencies + │ + ▼ + GraphBuilder AST ──► vertices and edges + │ + ▼ + DependencyGraph/ who depends on whom; topological order + │ + ▼ + Evaluator walk the order, evaluate each formula vertex + │ + ▼ + interpreter/ AST node ──► InterpreterValue + │ + ▼ + Serialization / Exporter engine values ──► values the caller asked for +``` + +## Core modules + +| Module | Role | +|---|---| +| `src/HyperFormula.ts` | The public API. Every documented method lives here; JSDoc on it is the API reference. | +| `src/BuildEngineFactory.ts` | Constructs an engine from sheets, data, and config. | +| `src/Config.ts`, `src/ConfigParams.ts` | Configuration options, defaults, and validation. | +| `src/CrudOperations.ts` | Create/read/update/delete on sheets and cells. Validates before mutating. | +| `src/Operations.ts` | The mutation primitives `CrudOperations` composes. | +| `src/UndoRedo.ts` | The undo/redo stack, expressed in terms of those primitives. | +| `src/parser/` | Formula text to AST, and back (`Unparser`). Caches parsed formulas. | +| `src/interpreter/` | AST to value. Owns the function registry and every built-in function. | +| `src/DependencyGraph/` | Vertices, edges, address mapping, range mapping, topological sort. | +| `src/dependencyTransformers/` | Rewrites ASTs when rows, columns, or sheets are added, removed, or moved. | +| `src/Evaluator.ts` | Drives recalculation over the graph. | +| `src/NamedExpressions.ts` | Named expression storage and scoping. | +| `src/Serialization.ts`, `src/Exporter.ts` | Read values, formulas, and serialized content back out. | +| `src/i18n/` | Function-name translations, one file per language. | +| `src/format/` | Number and date format parsing. | +| `src/statistics/` | Instrumentation counters used by the performance benchmarks. | + +## The parser + +`src/parser/` wraps the [Chevrotain](https://chevrotain.io/) parser generator. + +- `LexerConfig.ts` and `ParserConfig.ts` build the token set from the active language and config — the lexer is **language-dependent**, because function names are translated. +- `ParserWithCaching.ts` is the entry point; identical formula strings resolve from `Cache.ts` rather than being reparsed. +- `collectDependencies.ts` extracts the relative dependencies of an AST; `absolutizeDependencies.ts` resolves them against a concrete address. +- `Unparser.ts` is the inverse — AST back to text, in the target language. A change to parsing almost always needs a matching change here, or round-tripping breaks. + +## The interpreter + +`src/interpreter/Interpreter.ts` evaluates an AST node against an `InterpreterState`. + +- `FunctionRegistry.ts` maps a function id to the plugin that implements it. Custom functions register through the same registry, and may override a built-in id. +- Every built-in function lives in a plugin under `src/interpreter/plugin/` extending `FunctionPlugin`. See [`src/interpreter/plugin/AGENTS.md`](../src/interpreter/plugin/AGENTS.md). +- `src/interpreter/functionMetadata/` holds the human-readable description of each function — a separate, id-keyed source from the implementation. See [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md). +- `ArithmeticHelper.ts` centralises coercion and comparison. Reach for it rather than writing coercion inside a function. + +## The dependency graph + +`src/DependencyGraph/` is what makes recalculation incremental. + +- `Graph.ts` holds the vertices and edges; `TopSort.ts` produces the evaluation order and detects cycles. +- Vertex kinds: `ValueCellVertex`, `FormulaVertex`, `EmptyCellVertex`, `RangeVertex`, `ParsingErrorVertex`. +- `AddressMapping/` maps a `SimpleCellAddress` to its vertex; `RangeMapping.ts` does the same for ranges, so a range is a single vertex rather than one edge per cell. +- `LazilyTransformingAstService.ts` defers AST rewrites after structural changes until a formula is actually read — a change here affects both correctness and the CRUD hot path. + +## Invariants + +- **Headless.** No DOM, no network, no filesystem access in `src/`. +- **Incremental.** A change recalculates the affected subgraph, never the whole sheet. Anything that forces a full recalculation is a performance regression. +- **Language-dependent parsing.** Function names, argument separators, and error literals all vary by language. Never hard-code an English function name in the parser or interpreter. +- **Public API stability.** `src/HyperFormula.ts` is the contract. See [`DEFINITION-OF-DONE.md`](DEFINITION-OF-DONE.md) for what a breaking change requires. diff --git a/dev-docs/BUILD.md b/dev-docs/BUILD.md new file mode 100644 index 0000000000..67b9c95505 --- /dev/null +++ b/dev-docs/BUILD.md @@ -0,0 +1,62 @@ +# Building, bundling, and linting + +The user-facing version of this page is [`docs/guide/building.md`](../docs/guide/building.md). This file is the maintainer's view: what each script does and what it leaves on disk. + +## Prerequisites + +Node version is pinned in [`.nvmrc`](../.nvmrc). The repository uses npm and a committed `package-lock.json`. + +```bash +npm ci +``` + +## Everyday commands + +| Command | What it does | +|---|---| +| `npm run compile` | `tsc` into `lib/`. The input to every bundle. | +| `npm run bundle-all` | Clean, compile, produce every bundle, then verify them. | +| `npm run lint` | ESLint over `.js` and `.ts`. **The source of truth for formatting and code rules.** | +| `npm run lint:fix` | The same, with `--fix`. | +| `npm run test` | Lint, Jest, and the Karma browser run. | +| `npm run test:jest` | Jest only — the fast loop. | +| `npm run test:watch` | Jest in watch mode. | +| `npm run clean` | Remove every build output. | + +## Bundles + +`npm run bundle-all` runs `clean`, `compile`, every `bundle:*`, then `verify-bundles`. + +| Script | Output | +|---|---| +| `bundle:es` | `es/` — ES modules, `.mjs` extension | +| `bundle:cjs` | `commonjs/` — CommonJS modules | +| `bundle:development` | `dist/hyperformula.js`, `dist/hyperformula.full.js` | +| `bundle:production` | `dist/hyperformula.min.js`, `dist/hyperformula.full.min.js` | +| `bundle:languages` | `languages/` — standalone UMD language packs | +| `bundle:typings` | `typings/` — public `.d.ts` declarations | + +`verify-bundles` runs every `verify:*` check in parallel: each `dist/` artifact and the `commonjs/` output must exist and be non-trivial (`script/check-file.js`), and `verify:typings` type-checks with `tsc --noEmit`. + +Two bundle variants exist: the base build and the `.full` build. When a change can affect bundling, packaging, or module resolution, confirm both still work. + +`verify:publish-package` runs `npm pack` through `script/check-publish-package.js` — use it when a change touches `package.json`, `.npmignore`, or the `exports`/`typings` surface. + +## Documentation + +| Command | What it does | +|---|---| +| `npm run docs:dev` | Generate the function docs and API reference, then serve the portal locally | +| `npm run docs:build` | The full production build of the portal | +| `npm run docs:generate-function-docs` | Regenerate `docs/guide/built-in-functions.md` from the function metadata catalogue | +| `npm run typedoc:build-api` | Regenerate `docs/api/` from JSDoc | + +`docs/guide/built-in-functions.md` is generated and git-ignored — never commit it, and never edit it by hand. Its source is the metadata catalogue; see [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md). + +The portal is deployed as a Cloudflare Worker: `docs:build:cf` composes the assets through `script/prepare-cf-assets.js`, `docs:deploy:cf` and `docs:preview:cf` call `wrangler` against [`wrangler.jsonc`](../wrangler.jsonc). The Worker entry point is [`worker/index.js`](../worker/index.js). + +## Release + +`npm run release` runs `script/release/release.sh`. Releasing is maintainer-owned; do not invent steps around it. + +`npm run check:licenses` asserts that every production dependency carries a permissive licence. diff --git a/dev-docs/CODE-STYLE.md b/dev-docs/CODE-STYLE.md new file mode 100644 index 0000000000..85ee2231cd --- /dev/null +++ b/dev-docs/CODE-STYLE.md @@ -0,0 +1,38 @@ +# Code style + +ESLint is the source of truth for formatting and code rules. Run `npm run lint` before submitting changes. Everything below is what ESLint cannot check. + +## Style + +- Prefer a functional approach where possible (`filter`, `map`, `reduce`). +- Write self-documenting code: meaningful names for classes, functions, and variables. Add comments only where they explain intent the code itself cannot. +- Add JSDoc to all classes and functions. +- Choose readability over brevity. Explicit, obvious code beats a clever one-liner. +- Keep control flow straightforward: early returns, flat structure, and a reason for every branch and condition. +- Small functions with a single responsibility. No hidden side effects, no magic values. +- Avoid duplication. Extract shared logic instead of copying it, and reuse the codebase's existing helpers and abstractions. +- Match the style of the surrounding code and of the project as a whole. New code should not stand out from its neighbours. +- Optimize for long-term maintainability: someone else should be able to read, extend, and safely change the code months from now. + +## Performance + +HyperFormula is a calculation engine, so the performance of production code is a feature, not an afterthought. + +- Consider the computational complexity of every change, especially in code that runs **per cell, per formula, or per dependency-graph node**. Nested loops over ranges, and repeated work that could be computed once or cached, are the usual suspects. +- Pick the best complexity that still keeps the code readable. When a faster algorithm is harder to follow, explain the trade-off in a JSDoc comment. +- Run `npm run test:performance` for changes that may affect the evaluation or CRUD hot paths. + +Hot paths worth knowing about before touching them: + +| Path | Why it is hot | +|---|---| +| `src/interpreter/` function implementations | Runs once per formula, and once per cell for array-broadcast arguments | +| `src/DependencyGraph/` vertex and range mapping | Every read and every structural change goes through it | +| `src/Evaluator.ts` | Walks the whole recalculation order | +| `src/parser/ParserWithCaching.ts` | Cache misses reparse; a change that defeats the cache is a regression | +| `src/LazilyTransformingAstService.ts` | Deferred AST rewrites after row/column/sheet operations | + +## TypeScript + +- The public API surface is `src/HyperFormula.ts` and the types it exports; `npm run bundle:typings` emits them into `typings/`. +- `npm run verify:typings` (`tsc --noEmit`) must pass. A change that only compiles because of an `as` cast usually has a modelling problem behind it. diff --git a/dev-docs/DEFINITION-OF-DONE.md b/dev-docs/DEFINITION-OF-DONE.md new file mode 100644 index 0000000000..0be743a922 --- /dev/null +++ b/dev-docs/DEFINITION-OF-DONE.md @@ -0,0 +1,34 @@ +# Definition of done + +Every change to production code — bug fix, feature, or improvement — must include all of the following **before** a code review is requested. + +1. **The production change**, including every supported language pack in `src/i18n/languages/` when function names are involved. +2. **Automatic tests** in `test/`: + - bug fix — at least one test that reproduces the bug; + - new feature — a set of tests that precisely describe the feature; + - pull requests from external contributors put tests in `test/`; the internal team adds them to the private repository through a separate pull request. + See [`TESTING.md`](TESTING.md). +3. **Documentation updates** matching the change. A breaking change also needs a section in the migration guide. See [`DOC-STANDARDS.md`](DOC-STANDARDS.md). +4. **JSDoc** on classes and functions, plus a high-level description of the concepts used in any complex fragment. +5. **A changelog entry**, unless the change is documentation-only (guides, JSDoc, README). +6. **A pull request description** — kept current as the branch evolves, not written once and left to rot. + +Every element must be not only present but correct: the changelog entry must describe the change accurately, and the documentation must match the new behaviour. + +## Before requesting a review + +Read your own diff end to end and ask what could be done better. Fix what you find while the change is still yours. + +## One pull request, one change + +A pull request contains a single atomic, self-contained functional change: one bug fix, one feature, or one improvement. A pull request with several of those should be split. + +Every change in the pull request must be relevant to the issue it solves. Unrelated refactors, reformatting, and clean-ups belong in a separate pull request — including the ones that are obviously improvements. + +## Breaking changes + +The public API is `src/HyperFormula.ts` and the types it exports. Avoid breaking it. When a change genuinely requires a break: + +- state it explicitly in the pull request description and the changelog entry; +- add a migration-guide section describing what breaks and what to do instead; +- keep a test proving the old behaviour where the old behaviour is meant to keep working. diff --git a/dev-docs/DOC-STANDARDS.md b/dev-docs/DOC-STANDARDS.md new file mode 100644 index 0000000000..1debde80c7 --- /dev/null +++ b/dev-docs/DOC-STANDARDS.md @@ -0,0 +1,29 @@ +# Documentation standards + +These apply across the whole repository: guides, the API reference, JSDoc inside `src/`, the changelog, migration guides, and README files. + +Writing style, language, and guide structure live in [`DOCS_CONTENT_GUIDE.md`](../DOCS_CONTENT_GUIDE.md). Follow it when creating or editing docs content. + +## When documentation is required + +- Any public-API change updates the JSDoc **and** the affected guides. +- Any user-facing behaviour change is documented in the same pull request as the change. +- Any breaking change adds a migration-guide section. +- Documentation-only changes (guides, JSDoc, README) need no changelog entry. + +## Do not duplicate + +The API reference — generated from JSDoc — holds all the detail about each function and class, and is the primary source of truth. Guides give a high-level overview. A guide may repeat something from the API reference where it is relevant to the context, but above all it should **link** to the API reference for the detail. + +## Generated files + +| File | Generated by | Rule | +|---|---|---| +| `docs/api/` | `npm run typedoc:build-api` | Never edit. Change the JSDoc in `src/`. | +| `docs/guide/built-in-functions.md` | `npm run docs:generate-function-docs` | Never edit, never commit. Change the metadata catalogue — see [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md). | + +Both are git-ignored. A missing `docs/api/` folder means it has not been built yet; run `npm run docs:build`. + +## Describing behaviour + +Describe **HyperFormula's** behaviour, not Excel's. HyperFormula deliberately deviates in places, and much of the existing prose was seeded from Excel documentation. Verify a claim against the implementation before writing it down, and record any deviation in [the list of differences](../docs/guide/list-of-differences.md). diff --git a/dev-docs/FUNCTION-CATALOGUE.md b/dev-docs/FUNCTION-CATALOGUE.md new file mode 100644 index 0000000000..6071c497dd --- /dev/null +++ b/dev-docs/FUNCTION-CATALOGUE.md @@ -0,0 +1,50 @@ +# The function metadata catalogue + +`src/interpreter/functionMetadata/categories/` holds the human-readable metadata for every built-in function: `shortDescription`, `parameters` (`snake_case` names, each with a description), `examples`, `documentationUrl`, and the category. + +It is the single source of truth for two consumers: + +- the public [`getAvailableFunctions` / `getFunctionDetails`](../docs/api/classes/hyperformula.md) API; +- the generated built-in functions guide page (`docs/guide/built-in-functions.md`, produced by `npm run docs:generate-function-docs`). + +Every field is required, `documentationUrl` included. Each entry authors its own link rather than inheriting a shared default, so links can diverge per function without touching any code; they all happen to point at the same guide page today. + +## Categories + +An entry's `category` must be one of the categories in `FUNCTION_CATEGORIES` — the ones the generated guide page renders as `### ` sections. + +The separate `'Custom'` category is reserved for user-registered functions and must **never** appear in `FUNCTION_CATEGORIES` or in a catalogue file. It names no section, and the docs generator rejects an entry carrying it rather than silently dropping it from the page it is building. That rejection is also what turns a missing catalogue entry into a failed docs build. + +## Keyed by id, not by implementation + +The catalogue's key set decides which ids carry an authored **description**, not which ids the API lists. + +Both `getAvailableFunctions` and `getFunctionDetails` describe every registered function — custom ones included — and an entry is applied whenever the catalogue holds one for the id, whichever plugin currently provides it. A custom plugin registered over a built-in id is therefore described with that built-in's authored metadata. + +Nothing checks a key against a registered function either, so an entry left behind after a rename describes nothing and merely ships in the bundle. Remove or rename it in the same change as the function. + +## Two ways to get this wrong + +**No catalogue entry.** A registered function with no entry is still listed and still resolves to details, but as a custom function: `category: 'Custom'`, no `shortDescription`, `documentationUrl` or `examples` (the API omits every authored field it has no source for, rather than reporting an empty one), and positional parameter names (`Arg1`, `Arg2`, …). `'Custom'` has no section on the generated docs page, so `npm run docs:generate-function-docs` fails rather than publishing a built-in with no description. + +**Arity drift.** If the entry's parameter **count** disagrees with the plugin's `implementedFunctions`, the implementation wins: `getFunctionDetails` reports one parameter per implemented argument under positional names, discarding the authored names and descriptions, and warns on the console naming the function. The entry's `category`, `shortDescription`, `examples` and `documentationUrl` are still used, and the function stays listed — the parameter prose degrades, not the availability. Keep the entry's parameters in step with `implementedFunctions` whenever you change a signature. + +## Optionality is not cross-checked + +The catalogue authors no optionality of its own — a parameter's `optional` flag is derived entirely from `optionalArg` / `defaultValue` in `implementedFunctions` — so a description that calls an argument optional can sit next to `optional: false` with nothing failing. + +When a function accepts a call that arity alone does not express (`SHEET()`, `ROW()`, and anything else served by `runFunctionWithReferenceArgument`'s zero-argument path), the plugin must declare `optionalArg: true` explicitly, or the public API will advertise the argument as required. `ROW`, `COLUMN`, `SHEET` and `SHEETS` all declare it; `ISFORMULA` takes the same path and correctly does not, because its zero-argument call is an error rather than a shorthand. + +## Naming parameters in prose + +When a description **refers to** a parameter, use that parameter's exact `snake_case` name, never a prose variant: write "shifts `start_date` by …", not "shifts the start date by …". The same strings are rendered next to the generated syntax line, where the `snake_case` name is what the reader sees, so a prose variant leaves the reader guessing which argument is meant. This applies to `shortDescription` and to every parameter description. + +It does **not** turn ordinary English into identifiers. A parameter's own description may open with a prose noun phrase for the thing it describes — `lower_bound` is fine as "The lower bound, rounded up to an integer" — and words that merely happen to match a name ("entries that appear exactly once") stay as they are. The rule is about naming a *different* argument, or naming one from the syntax line. + +## Markup + +`shortDescription` must not use docs-page-local markup — no relative links, no footnote references. The strings are rendered by API consumers as well as by the docs page. + +## Behaviour, not Excel + +Descriptions must describe **HyperFormula's** behaviour, not Excel's. Much of the catalogue was seeded from a hand-written page that documented Excel, and HyperFormula deliberately deviates in places (`INT` truncates toward zero, `ISEVEN`/`ISODD` do not truncate, `CEILING.MATH`/`FLOOR.MATH` honour only `mode` = 1). Verify a claim against the implementation before authoring it, and record any deviation in [the list of differences](../docs/guide/list-of-differences.md). diff --git a/dev-docs/I18N.md b/dev-docs/I18N.md new file mode 100644 index 0000000000..efa3d98945 --- /dev/null +++ b/dev-docs/I18N.md @@ -0,0 +1,22 @@ +# Internationalization and function translations + +HyperFormula provides localized function names for every built-in language. Translation files live in [`src/i18n/languages/`](../src/i18n/languages/), one per language. + +A new function must ship translations for **all** built-in languages in the same change. A missing key is not caught by the type checker in every path, and it degrades silently to a formula that cannot be parsed in that language. + +Parsing is language-dependent: the lexer builds its token set from the active translation package (`src/parser/LexerConfig.ts`). Never hard-code an English function name in the parser or the interpreter. + +## Where to find a translation + +| Source | Use for | +|---|---| +| [Microsoft's Excel functions translator](https://support.microsoft.com/en-us/office/excel-functions-translator-f262d0c0-991c-485b-89b6-32cc8d326889) | Languages Excel supports | +| | The same, as a cross-check | +| Google Sheets function list, with `hl` set to the locale — for example for Indonesian | Languages Excel does not support | +| The English name | Functions Google Sheets does not list either — this matches what Excel does in unsupported locales | + +## Rules + +- Translate the function **name** only. Argument separators, error literals, and boolean literals are part of the language package too; do not invent values for them. +- Keep the key set identical across every language file. Adding a key to one file and not the rest is the common failure. +- Do not reorder existing entries while adding one — it turns a one-line diff into an unreviewable one. diff --git a/dev-docs/MONOREPO.md b/dev-docs/MONOREPO.md new file mode 100644 index 0000000000..0c3eb7d42d --- /dev/null +++ b/dev-docs/MONOREPO.md @@ -0,0 +1,110 @@ +# Target monorepo layout + +This repository is becoming a monorepo (HF-359). This file is the target: what the tree looks like, which directories are workspace members, and what the migration has to solve. It is a proposal until the migration lands — the authoritative layout of the repository as it stands today is [`STRUCTURE.md`](STRUCTURE.md). + +## Packages + +| Package | Directory | Purpose | Published | +|---|---|---|---| +| `hyperformula` | `hyperformula/` | The calculation engine. Everything in `src/` and `test/` today. | yes | +| `@hyperformula/ui-core` | `hyperformula-ui/packages/core/` | Formula editor UI: reference highlighting, inline editor, function help. | yes | +| `@hyperformula/handsontable-adapter` | `hyperformula-ui/packages/handsontable-adapter/` | Binds the editor to Handsontable. | yes | +| `@hyperformula/plain-table-adapter` | `hyperformula-ui/packages/plain-table-adapter/` | Binds the editor to a plain HTML table. | yes | +| `@hyperformula/tanstack-table-adapter` | `hyperformula-ui/packages/tanstack-table-adapter/` | Binds the editor to TanStack Table. | yes | +| `hyperformula-skill` | `hyperformula-skill/` | The agent skill that teaches coding agents to use HyperFormula. | as a plugin/zip | +| `hyperformula-docs` | `docs/` | The VuePress documentation portal. | no | +| — | `hyperformula-ui/demo/`, `hyperformula-ui/e2e/` | Demo app and end-to-end tests for the UI packages. | no | + +`docs/` is deliberately **not** a workspace member: the portal drags in a large, old dependency tree (VuePress, `--openssl-legacy-provider`) that must not reach an engine install. It keeps its own `package.json` and is installed separately. This mirrors how the Handsontable monorepo isolates its documentation site. + +## Tree + +``` +hyperformula/ # repository root — private, workspace root +├── AGENTS.md # monorepo-wide rules + routing map +├── CLAUDE.md -> AGENTS.md +├── DEV_DOCS.md # pointer into dev-docs/ +├── README.md CONTRIBUTING.md CHANGELOG.md LICENSE.txt +├── package.json # private: true, workspaces, fan-out scripts +├── pnpm-workspace.yaml +├── .worktreeinclude +├── .changelogs/ # one JSON fragment per PR (see below) +├── .claude/ +│ ├── settings.json # hooks, enabledPlugins, worktree settings +│ ├── skills/ # ALL skills, scoped by frontmatter path +│ └── agents/ +├── dev-docs/ # monorepo-scope reference +│ ├── README.md STRUCTURE.md BUILD.md TESTING.md +│ ├── DEFINITION-OF-DONE.md CODE-STYLE.md DOC-STANDARDS.md +│ ├── WORKTREES.md MONOREPO.md +│ +├── hyperformula/ # ── package: the engine +│ ├── AGENTS.md CLAUDE.md -> AGENTS.md +│ ├── package.json +│ ├── dev-docs/ # engine-scope reference +│ │ ├── ARCHITECTURE.md FUNCTION-CATALOGUE.md I18N.md +│ ├── src/ +│ │ ├── AGENTS.md CLAUDE.md -> AGENTS.md +│ │ ├── HyperFormula.ts Config.ts CrudOperations.ts Evaluator.ts … +│ │ ├── parser/ AGENTS.md +│ │ ├── interpreter/ AGENTS.md +│ │ │ ├── plugin/ AGENTS.md +│ │ │ └── functionMetadata/ AGENTS.md +│ │ ├── DependencyGraph/ AGENTS.md +│ │ ├── dependencyTransformers/ +│ │ ├── i18n/languages/ AGENTS.md +│ │ ├── format/ helpers/ Lookup/ statistics/ +│ └── test/ AGENTS.md +│ ├── smoke.spec.ts fetch-tests.sh +│ └── hyperformula-tests/ # private suite, git-ignored, branch-matched +│ +├── hyperformula-ui/ # ── formula editor UI +│ ├── AGENTS.md CLAUDE.md -> AGENTS.md +│ ├── dev-docs/ +│ ├── packages/ +│ │ ├── core/ AGENTS.md +│ │ ├── handsontable-adapter/ AGENTS.md +│ │ ├── plain-table-adapter/ AGENTS.md +│ │ └── tanstack-table-adapter/ AGENTS.md +│ ├── demo/ +│ └── e2e/ +│ +├── hyperformula-skill/ # ── agent skill +│ ├── AGENTS.md CLAUDE.md -> AGENTS.md +│ ├── skills/hyperformula/SKILL.md +│ └── scripts/ +│ +├── docs/ # ── documentation portal (NOT a workspace member) +│ ├── AGENTS.md CLAUDE.md -> AGENTS.md +│ ├── package.json +│ ├── guide/ api/ .vuepress/ +│ +├── worker/ # Cloudflare Worker serving the built docs +├── examples/ # images and CSV fixtures used by the docs +├── script/ AGENTS.md +└── .github/workflows/ # path-filtered per-package jobs +``` + +## Why this shape + +- **Flat, name-matched top-level directories.** A directory is named after the package it holds, so a path in a stack trace, a CI job name, and a changelog entry all say the same word. No `packages/` wrapper at the root — it adds a level that carries no information. +- **`hyperformula-ui/` keeps its own `packages/`** because it genuinely holds four published packages that version together. Its workspace glob is `hyperformula-ui/packages/*`. +- **`AGENTS.md` travels with the code.** Every file listed above stays with its directory through the move, so the agent instructions survive the migration unchanged. +- **One `.claude/skills/` at the root.** Skills are scoped by a `path` glob in their frontmatter rather than by placement, so there is one place to look and one place to keep them consistent. + +## Migration steps + +1. **Pick the package manager.** The engine uses npm with `package-lock.json` today; `hyperformula-ui` and the Handsontable monorepo both use pnpm. Converging on pnpm makes the incoming package a straight move and matches the sibling repository; it costs one lockfile migration and a CI update. +2. **Move `src/` and `test/` into `hyperformula/`.** Mechanical, but it invalidates every path in CI, in `tsconfig.json`, in `jest.config.js`, in `karma.conf.js`, in `.eslintignore`, and in the docs generator scripts. +3. **Give `docs/` its own `package.json`** and take it out of the root dependency tree. +4. **Move the root build scripts down into `hyperformula/package.json`**, leaving fan-out scripts at the root. +5. **Bring in `hyperformula-ui`** from the formula-builder repository, preserving its history. Decide the published scope (`@hfe/*` today) before the first release from here. +6. **Bring in `hyperformula-skill`** from the shared skills repository. Its marketplace entry has to keep resolving — either the plugin build publishes from here, or the old repository keeps pointing at this one. +7. **Split `CHANGELOG.md` into `.changelogs/*.json` fragments.** One `CHANGELOG.md` edited by every package's pull requests conflicts on every merge. A per-PR JSON fragment plus a `consume` step removes the conflict entirely. +8. **Path-filter CI.** Each package's jobs run only when its paths change; full runs on `develop`, `master`, and release branches. + +## Open questions + +- Does the private `hyperformula-tests` repository stay branch-matched to this repository once the engine sits one level down? Its `fetch-tests.sh` target path changes with the move. +- Do the UI packages ship on the engine's release cadence, or their own? +- Which scope do the UI packages publish under, and does renaming them break existing consumers? diff --git a/dev-docs/README.md b/dev-docs/README.md new file mode 100644 index 0000000000..e1ed7f2542 --- /dev/null +++ b/dev-docs/README.md @@ -0,0 +1,37 @@ +# `dev-docs/` — developer reference + +Deep reference documentation for everyone working on HyperFormula: maintainers, the internal team, and the AI agents they run. Written for humans first; agents read the same files. + +These files are **loaded on demand** — linked from the always-loaded `AGENTS.md` files and from skills, not read on every turn. + +## Three-layer model + +| Layer | Answers | Loaded | +|---|---|---| +| `AGENTS.md` / `CLAUDE.md` | *What must I never get wrong here, and where do I look next?* Lean rules plus a navigation map for the directory it lives in. | Always, within its subtree | +| `dev-docs/` | *How does this work and why?* Architecture, conventions, standards, deep detail. | On demand | +| `.claude/skills/` | *How do I do task X?* Step-by-step workflows. | On skill trigger | + +In every directory, `CLAUDE.md` is a symlink to its sibling `AGENTS.md`. Edit `AGENTS.md` — the symlink keeps Claude Code and Cursor reading the same single source. + +## Index + +| File | Contents | +|---|---| +| [`STRUCTURE.md`](STRUCTURE.md) | Repository tree and what lives where. | +| [`ARCHITECTURE.md`](ARCHITECTURE.md) | Engine architecture: parser, interpreter, dependency graph, and the data flow between them. | +| [`BUILD.md`](BUILD.md) | Every build, bundle, lint, and release command, with its output. | +| [`TESTING.md`](TESTING.md) | Test suites, the private test repository, and what a change must cover. | +| [`DEFINITION-OF-DONE.md`](DEFINITION-OF-DONE.md) | What every change must include before review. | +| [`CODE-STYLE.md`](CODE-STYLE.md) | Code style and the performance rules that apply to engine code. | +| [`DOC-STANDARDS.md`](DOC-STANDARDS.md) | Documentation rules across guides, API reference, JSDoc, and the changelog. | +| [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md) | The function metadata catalogue: what it feeds, and the two ways to get it wrong. | +| [`I18N.md`](I18N.md) | Function-name translations and where to source them. | +| [`WORKTREES.md`](WORKTREES.md) | Working in a linked git worktree: what is missing and how to bootstrap it. | +| [`MONOREPO.md`](MONOREPO.md) | The target monorepo layout and the migration it implies. | + +## Conventions + +- Cross-references use repository-root-relative paths (for example `dev-docs/TESTING.md`), not filesystem-relative `../` paths. +- Diagrams live only in `dev-docs/`, never in the always-loaded `AGENTS.md` files. +- Public, user-facing documentation belongs in [`docs/`](../docs/), not here. `dev-docs/` never ships. diff --git a/dev-docs/STRUCTURE.md b/dev-docs/STRUCTURE.md new file mode 100644 index 0000000000..75bcd18280 --- /dev/null +++ b/dev-docs/STRUCTURE.md @@ -0,0 +1,69 @@ +# Repository structure + +The layout as it stands today. For the monorepo layout this repository is moving to, see [`MONOREPO.md`](MONOREPO.md). + +``` +. +├── src/ # Source code +│ ├── HyperFormula.ts # Main engine class, public API entry point +│ ├── BuildEngineFactory.ts # Engine construction from sheets, data, and config +│ ├── Config.ts, ConfigParams.ts # Engine configuration and its defaults +│ ├── CrudOperations.ts # Create/read/update/delete on sheets and cells +│ ├── Operations.ts, UndoRedo.ts # Operation primitives and the undo/redo stack +│ ├── Evaluator.ts # Recalculation driver +│ ├── Serialization.ts, Exporter.ts # Reading values and formulas back out +│ ├── NamedExpressions.ts # Named expression store +│ ├── parser/ # Formula parsing (Chevrotain parser generator) +│ ├── interpreter/ # Formula evaluation +│ │ ├── plugin/ # Built-in spreadsheet function plugins +│ │ └── functionMetadata/ # Human-readable metadata for every built-in function +│ ├── DependencyGraph/ # Cell dependency tracking and recalculation order +│ ├── dependencyTransformers/ # AST rewrites when rows/columns/sheets move +│ ├── i18n/languages/ # Function-name translations, one file per language +│ ├── format/ # Number and date format parsing +│ ├── helpers/ # Shared utilities +│ ├── Lookup/ # Lookup/search strategies used by lookup functions +│ └── statistics/ # Instrumentation counters +├── test/ # Smoke tests; the full suite is fetched here +│ ├── smoke.spec.ts # Public smoke tests +│ ├── fetch-tests.sh # Clones/updates the private test repository +│ └── hyperformula-tests/ # Private suite (git-ignored, branch-matched) +├── docs/ # Public documentation portal (VuePress) +│ ├── guide/ # Markdown guides +│ ├── api/ # API reference (generated from JSDoc; git-ignored) +│ └── .vuepress/ # VuePress configuration, theme, components +├── script/ # Maintenance, docs-generation, and release scripts +├── worker/ # Cloudflare Worker that serves the built docs +├── examples/ # Images and CSV fixtures used by the docs +├── dev-docs/ # Developer reference (this directory) +├── .claude/ # Claude Code settings, skills, and agents +├── .github/ # CI workflows, issue and PR templates +├── AGENTS.md # Always-loaded agent rules and routing map +├── CLAUDE.md # Symlink to AGENTS.md +├── DEV_DOCS.md # Pointer to dev-docs/ +├── CONTRIBUTING.md # Guide for external contributors +├── DOCS_CONTENT_GUIDE.md # Writing style and structure for docs content +├── README.md # Project overview +└── CHANGELOG.md +``` + +## Build outputs + +All git-ignored, all produced by `npm run bundle-all` (see [`BUILD.md`](BUILD.md)): + +| Directory | Contents | +|---|---| +| `lib/` | `tsc` output, the input to every bundle | +| `es/` | ES modules (`.mjs`) | +| `commonjs/` | CommonJS modules | +| `dist/` | UMD bundles, minified and not, base and `.full` | +| `languages/` | Standalone UMD language packs | +| `typings/` | Public `.d.ts` declarations | + +Never edit these, and never read them to answer a question about behaviour — read `src/` instead. + +## Directories with their own `AGENTS.md` + +Each of these carries rules that load only when an agent works inside it: + +`src/`, `src/parser/`, `src/interpreter/`, `src/interpreter/plugin/`, `src/interpreter/functionMetadata/`, `src/DependencyGraph/`, `src/i18n/`, `docs/`, `test/`, `script/`. diff --git a/dev-docs/TESTING.md b/dev-docs/TESTING.md new file mode 100644 index 0000000000..99b3788664 --- /dev/null +++ b/dev-docs/TESTING.md @@ -0,0 +1,55 @@ +# Testing + +## The two suites + +| Suite | Where | Who has it | +|---|---|---| +| Smoke tests | [`test/smoke.spec.ts`](../test/smoke.spec.ts) | Everyone, in this repository | +| Full suite | `test/hyperformula-tests/` | Internal team and anyone granted access | + +The full suite is kept in a separate private repository and is **git-ignored** here. It carries the unit tests, the browser and compatibility runs, and the performance benchmarks. External contributors put their tests in `test/`; the internal team moves them into the private repository through a separate pull request. + +## Fetching the private suite + +`test/hyperformula-tests` is **branch-matched to this repository**. [`test/fetch-tests.sh`](../test/fetch-tests.sh) checks out the branch with the same name, creating it from `develop` when it does not exist yet. + +```bash +npm run test:setup-private +``` + +Run it after every branch switch. Skipping it runs the previous branch's tests against the current source, which passes or fails for reasons that have nothing to do with the change under test. See [`test/README.md`](../test/README.md) for the environment variables it honours. + +## Running tests + +| Command | Runs | +|---|---| +| `npm run test` | Lint, Jest, and the Karma browser run — the full local gate | +| `npm run test:jest` | Jest only; the fast loop | +| `npm run test:watch` | Jest in watch mode | +| `npm run test:coverage` | Jest with coverage | +| `npm run test:browser` | Karma, against the `dist` build | +| `npm run test:compatibility` | `test/compatibility/test-compatibility.sh` | +| `npm run test:performance` | The basic and CRUD benchmarks | + +`test:performance`, `test:compatibility`, and the benchmark scripts all live inside `test/hyperformula-tests/`. Without the private suite they fail with a missing path, not with a test failure — read the error before concluding that something is broken. + +## What a change must cover + +- Every change to `src/` needs tests in `test/`. This is part of the [definition of done](DEFINITION-OF-DONE.md), not a suggestion. +- **Bug fix**: at least one test that reproduces the bug — it must fail against the unfixed code. Write it first and watch it fail. +- **New feature**: a set of tests that describe the feature precisely enough to serve as its specification. +- Cover more than the happy path: boundary values, empty and invalid input, error results, and interaction with related features. +- `docs/`, `examples/`, and `script/` are not tested. + +## How to write a test case + +- **One assertion per test case.** Each case is very simple and focused. +- **No control flow in a test case.** No loops, no conditionals. A parameterised loop hides which input failed; write the cases out. +- Name the case after the behaviour it pins, not after the function under test. +- A test must prove intended behaviour. Never relax an assertion, widen a matcher, or skip a case to turn a run green — if a test is red, the default assumption is that the code is wrong. + +Before requesting a review, ask which further tests would be valuable and add the ones that protect against realistic regressions. + +## Performance + +HyperFormula is a calculation engine, so production-code performance is a feature. Run `npm run test:performance` for any change that can touch the evaluation or CRUD hot paths. See [`CODE-STYLE.md`](CODE-STYLE.md#performance). diff --git a/dev-docs/WORKTREES.md b/dev-docs/WORKTREES.md new file mode 100644 index 0000000000..1f38856d36 --- /dev/null +++ b/dev-docs/WORKTREES.md @@ -0,0 +1,38 @@ +# Working in a linked git worktree + +Claude Code can run a session — or an isolated subagent — in a `git worktree` so its changes stay off your main checkout. `git worktree` materialises **tracked files only**, so a fresh worktree is not a working checkout until you bootstrap it. + +## What is missing, and what to do about it + +| Missing | Why it matters | Fix | +|---|---|---| +| `node_modules/` | Nothing runs. | `npm ci` in the worktree, or symlink it — see below | +| `test/hyperformula-tests/` | The private suite is git-ignored, so every `npm run test:jest` run covers only the smoke tests | `npm run test:setup-private` | +| `lib/`, `dist/`, `es/`, `commonjs/`, `typings/`, `languages/` | `npm run test:browser` and the bundle checks have nothing to run against | `npm run bundle-all` | +| `docs/api/`, `docs/guide/built-in-functions.md` | The docs build fails, or serves nothing | `npm run docs:build` | +| `.dev.vars*` | `wrangler` commands fail | Copied automatically — see [`.worktreeinclude`](../.worktreeinclude) | + +## The branch-matched test suite is the trap + +`test/fetch-tests.sh` checks out the branch of the **same name** in the private test repository. Two consequences in a worktree: + +1. Copying `test/hyperformula-tests/` from the main checkout brings the *other* branch's tests. They will run, and they will report results that have nothing to do with the code in front of you. `.worktreeinclude` deliberately does not copy it. +2. Run `npm run test:setup-private` once per worktree, and again after any branch switch inside it. + +## Symlinking `node_modules` + +`worktree.symlinkDirectories` in `.claude/settings.json` points each worktree's `node_modules/` at the main checkout's copy instead of duplicating it: + +```json +{ + "worktree": { + "symlinkDirectories": ["node_modules"] + } +} +``` + +This is safe while HyperFormula is a single package: there is exactly one `node_modules/.bin`, and the symlink resolves it. **It stops being safe once the repository becomes a workspace** — package-local `node_modules/.bin` directories are not covered by a root-level symlink, and scripts then die mid-build with a bare `command not found`. Revisit this setting as part of the monorepo migration ([`MONOREPO.md`](MONOREPO.md)). + +## Sparse checkouts + +`worktree.sparsePaths` limits what git writes to disk. It buys little today — this repository is small and `src/` is needed by everything. It becomes worth setting once the packages in [`MONOREPO.md`](MONOREPO.md) exist and a task can be scoped to one of them. diff --git a/docs/AGENTS.md b/docs/AGENTS.md new file mode 100644 index 0000000000..ebc935ac93 --- /dev/null +++ b/docs/AGENTS.md @@ -0,0 +1,49 @@ +# `docs/` — the documentation portal + +A [VuePress](https://vuepress.vuejs.org/) site. Everything here is public, user-facing output. + +## Layout + +| Path | What it is | Editable | +|---|---|---| +| `guide/` | Hand-written guides | yes | +| `guide/built-in-functions.tmpl.md` | Template for the generated functions page | yes | +| `guide/built-in-functions.md` | **Generated** from the template plus the function metadata catalogue | **no — git-ignored** | +| `api/` | **Generated** from JSDoc by TypeDoc | **no — git-ignored** | +| `.vuepress/` | Configuration, theme, components, sidebar | yes | +| `examples/` | Code examples embedded in guides | yes | +| `code-examples-generator.sh` | Generates the JavaScript variants of TypeScript examples | yes | + +## Never edit generated output + +`docs/api/` comes from the JSDoc in `src/` — fix the JSDoc, then run `npm run typedoc:build-api`. + +`docs/guide/built-in-functions.md` comes from `built-in-functions.tmpl.md` and the metadata catalogue in `src/interpreter/functionMetadata/` — fix the catalogue entry, then run `npm run docs:generate-function-docs`. It is git-ignored; never commit it. + +A missing `docs/api/` folder means it has not been built yet. Run `npm run docs:build`. + +## Writing + +Follow [`DOCS_CONTENT_GUIDE.md`](../DOCS_CONTENT_GUIDE.md) for style, language, and guide structure. The rules that apply beyond the portal — JSDoc, changelog, migration guides — are in [`dev-docs/DOC-STANDARDS.md`](../dev-docs/DOC-STANDARDS.md). + +Two rules worth repeating here: + +- **Do not duplicate the API reference.** It holds the detail and is the primary source of truth. A guide gives the high-level overview and **links** to the reference. +- **Describe HyperFormula's behaviour, not Excel's.** Verify against the implementation, and record deviations in [`guide/list-of-differences.md`](guide/list-of-differences.md). + +## Running it + +```bash +npm run bundle-all # the portal embeds the built engine +npm run docs:dev # http://localhost:8080/hyperformula/ +``` + +`docs:dev` and `docs:build` both regenerate the functions page and the API reference first. Details in [`README.md`](README.md) and [`dev-docs/BUILD.md`](../dev-docs/BUILD.md). + +## Adding a guide + +A new page needs a sidebar entry in `.vuepress/` as well as the file — otherwise it builds and is unreachable. + +## Deployment + +The portal is served by a Cloudflare Worker: `npm run docs:build:cf` composes the assets through `script/prepare-cf-assets.js`, then `docs:deploy:cf` or `docs:preview:cf` calls `wrangler`. The Worker entry point is [`worker/index.js`](../worker/index.js). diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/docs/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/script/AGENTS.md b/script/AGENTS.md new file mode 100644 index 0000000000..e911632446 --- /dev/null +++ b/script/AGENTS.md @@ -0,0 +1,20 @@ +# `script/` — build, docs, and release scripts + +Node and shell scripts invoked from `package.json`. Not shipped, not tested. + +| Script | Invoked by | What it does | +|---|---|---| +| `check-file.js` | `verify:umd*`, `verify:cjs` | Asserts a build artifact exists and is not trivially small | +| `check-publish-package.js` | `verify:publish-package` | Reads `npm pack` output and checks what would ship | +| `generate-builtin-functions-doc.ts` | `docs:generate-function-docs` | Renders `docs/guide/built-in-functions.md` from the template and the metadata catalogue | +| `formatFunctionSyntax.ts`, `renderBuiltinFunctionsTable.ts` | the generator above | Formatting helpers for the generated page | +| `prepare-cf-assets.js` | `docs:build:cf` | Composes the Cloudflare Worker asset tree | +| `if-ne-env.js` | the `bundle:*` scripts | Skips a recompile when `HF_COMPILE` is already set | +| `release/` | `npm run release` | The release procedure | + +## Rules + +- **The docs generator is a gate, not a formatter.** It fails the build on a missing catalogue entry or a `'Custom'` category. Keep it failing loudly — a silent skip publishes a built-in function with no description. +- **Release is maintainer-owned.** Do not invent steps around `script/release/`, and do not run it as part of another task. +- These scripts run in CI as well as locally. Do not assume an interactive terminal, a specific working directory, or a developer's environment variables. +- Nothing here is covered by tests. A change to a script is verified by running it. diff --git a/script/CLAUDE.md b/script/CLAUDE.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/script/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/src/AGENTS.md b/src/AGENTS.md new file mode 100644 index 0000000000..836380d2eb --- /dev/null +++ b/src/AGENTS.md @@ -0,0 +1,38 @@ +# `src/` — the engine + +Everything here ships. It is TypeScript, it runs in the browser and in Node, and it is on the hot path of a calculation engine. + +## Never + +- **No DOM, no network, no filesystem.** The engine is headless. A dependency that reaches for `window`, `document`, or `fs` does not belong in `src/`. +- **No English function names hard-coded** in the parser or the interpreter. Function names are translated; the lexer builds its token set from the active language package. See [`i18n/AGENTS.md`](i18n/AGENTS.md). +- **No full recalculation.** A change recalculates the affected subgraph. Anything that forces the engine to walk every cell is a performance regression, not an implementation detail. +- **No breaking change to the public API** without a migration-guide section and an explicit note in the pull request. The public surface is `HyperFormula.ts` and the types it exports. + +## Always + +- JSDoc on every class and function. The JSDoc on `HyperFormula.ts` **is** the published API reference — write it for the reader of the docs portal, not for yourself. +- A test in [`test/`](../test/) for every change here. A bug fix ships a test that fails against the unfixed code. +- Coercion and comparison go through `interpreter/ArithmeticHelper.ts`, not hand-rolled inside a caller. +- Think about complexity in anything that runs per cell, per formula, or per dependency-graph node. See [`dev-docs/CODE-STYLE.md`](../dev-docs/CODE-STYLE.md#performance). + +## Where to look next + +| Subsystem | Directory | +|---|---| +| Formula text to AST, and back | [`parser/`](parser/AGENTS.md) | +| AST to value | [`interpreter/`](interpreter/AGENTS.md) | +| Built-in spreadsheet functions | [`interpreter/plugin/`](interpreter/plugin/AGENTS.md) | +| Function descriptions for the API and the docs | [`interpreter/functionMetadata/`](interpreter/functionMetadata/AGENTS.md) | +| Dependency tracking and recalculation order | [`DependencyGraph/`](DependencyGraph/AGENTS.md) | +| Function-name translations | [`i18n/`](i18n/AGENTS.md) | + +Deep reference: [`dev-docs/ARCHITECTURE.md`](../dev-docs/ARCHITECTURE.md). Skill: `hyperformula-dev`. + +## Files without their own `AGENTS.md` + +- `HyperFormula.ts` — the public API. Every method here is documented output. +- `CrudOperations.ts` / `Operations.ts` / `UndoRedo.ts` — CRUD validates, `Operations` mutates, `UndoRedo` records. A new mutation needs all three, or undo silently diverges. +- `Config.ts` / `ConfigParams.ts` — a new option needs a default, validation, and a guide entry. +- `dependencyTransformers/` — AST rewrites when rows, columns, or sheets move. Paired with `LazilyTransformingAstService.ts`, which defers them until a formula is read. +- `format/`, `helpers/`, `Lookup/`, `statistics/` — number and date formats, shared utilities, lookup strategies, instrumentation counters. diff --git a/src/CLAUDE.md b/src/CLAUDE.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/src/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/src/DependencyGraph/AGENTS.md b/src/DependencyGraph/AGENTS.md new file mode 100644 index 0000000000..82d3875c16 --- /dev/null +++ b/src/DependencyGraph/AGENTS.md @@ -0,0 +1,29 @@ +# `src/DependencyGraph/` — what depends on what + +The structure that makes recalculation incremental. Vertices are cells and ranges; edges are dependencies; a topological sort gives the evaluation order. + +## The pieces + +| File | Role | +|---|---| +| `DependencyGraph.ts` | The facade the rest of the engine talks to. | +| `Graph.ts` | Vertices, edges, and dirty tracking. | +| `TopSort.ts` | Evaluation order, and cycle detection. | +| `AddressMapping/` | `SimpleCellAddress` to vertex. Several strategies, chosen by sheet density. | +| `RangeMapping.ts`, `RangeVertex.ts` | A range is **one** vertex, not one edge per cell. | +| `ArrayMapping.ts` | Array formulas and the cells they spill into. | +| `SheetMapping.ts` | Sheet ids and names. | +| `CellVertex.ts`, `ValueCellVertex.ts`, `FormulaVertex.ts`, `EmptyCellVertex.ts`, `ParsingErrorVertex.ts` | The vertex kinds. | +| `collectAddressesDependentToRange.ts` | Which addresses a range change invalidates. | + +## Rules + +- **Ranges stay collapsed.** Expanding a range into per-cell edges turns a `SUM(A1:A100000)` into 100 000 edges and destroys the performance the engine exists for. If you need per-cell information, ask whether the range vertex can answer instead. +- **Every structural change must keep the mapping consistent.** Adding or removing a row moves addresses; the address mapping, the range mapping, and the array mapping all have to agree afterwards, or a later read resolves to the wrong vertex. Structural changes are paired with `dependencyTransformers/` and `LazilyTransformingAstService.ts`. +- **Cycles are a value, not an exception.** `TopSort` detects them and the affected cells get a `CYCLE` error. Never let a cycle throw or loop. +- **`EmptyCellVertex` is not "no vertex".** An empty cell that something depends on still needs a vertex, or the dependency is lost when it is later filled. +- This is the hottest code in the engine after the interpreter — every read and every structural change goes through it. + +## Testing + +Structural-change tests are the ones that catch real bugs here: add/remove rows and columns around a formula, move a range across the formula that references it, and assert both the recalculated value **and** the formula text afterwards. diff --git a/src/DependencyGraph/CLAUDE.md b/src/DependencyGraph/CLAUDE.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/src/DependencyGraph/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/src/i18n/AGENTS.md b/src/i18n/AGENTS.md new file mode 100644 index 0000000000..a432b1a158 --- /dev/null +++ b/src/i18n/AGENTS.md @@ -0,0 +1,22 @@ +# `src/i18n/` — function-name translations + +One file per language under `languages/`, plus `TranslationPackage.ts`. Parsing is language-dependent: the lexer builds its token set from the active package, so these files are engine input, not decoration. + +## Rules + +- **A new function ships translations for every language in the same change.** A missing key leaves that function unparseable in that language. +- **Keep the key set identical across all language files.** Adding a key to one file and not the rest is the usual failure. +- **Do not reorder existing entries** while adding one. It turns a one-line diff into an unreviewable one. +- **Translate the name only.** Separators, error literals, and boolean literals are part of the package too — do not invent values for them. +- The canonical English id is what `implementedFunctions` and the metadata catalogue use. Translations map onto it; they never replace it. + +## Where to find a translation + +| Source | Use for | +|---|---| +| [Microsoft's Excel functions translator](https://support.microsoft.com/en-us/office/excel-functions-translator-f262d0c0-991c-485b-89b6-32cc8d326889) | Languages Excel supports | +| | Cross-check | +| Google Sheets function list with `hl` set to the locale, e.g. | Languages Excel does not support | +| The English name | Functions Google Sheets does not list either | + +Skill: `i18n-translations`. Reference: [`dev-docs/I18N.md`](../../dev-docs/I18N.md). diff --git a/src/i18n/CLAUDE.md b/src/i18n/CLAUDE.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/src/i18n/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/src/interpreter/AGENTS.md b/src/interpreter/AGENTS.md new file mode 100644 index 0000000000..f4bda9ac50 --- /dev/null +++ b/src/interpreter/AGENTS.md @@ -0,0 +1,26 @@ +# `src/interpreter/` — AST to value + +Evaluates an AST node against an `InterpreterState` and returns an `InterpreterValue`. + +## The pieces + +| File | Role | +|---|---| +| `Interpreter.ts` | Dispatches on AST node type. | +| `FunctionRegistry.ts` | Maps a function id to the plugin that implements it. Custom functions register here too, and may override a built-in id. | +| `InterpreterValue.ts` | The value types the engine passes around, including `CellError` and the extended number subtypes. | +| `InterpreterState.ts` | Evaluation context: the address being evaluated and the array-arithmetic flag. | +| `ArithmeticHelper.ts` | Coercion, comparison, and the arithmetic operators. | +| `Criterion.ts`, `CriterionFunctionCompute.ts` | The `*IF`/`*IFS` criterion machinery. | +| `binarySearch.ts` | Shared search used by the lookup functions. | +| `plugin/` | Every built-in function. See [`plugin/AGENTS.md`](plugin/AGENTS.md). | +| `functionMetadata/` | Human-readable descriptions of those functions. See [`functionMetadata/AGENTS.md`](functionMetadata/AGENTS.md). | + +## Rules + +- **Coerce through `ArithmeticHelper`.** Never write ad-hoc string-to-number or value-to-boolean conversion inside a function; the coercion rules are spreadsheet semantics, not JavaScript semantics, and they are already implemented once. +- **Errors are values.** Return a `CellError` with a message from `error-message.ts`. Do not throw: a thrown error escapes the evaluation of one cell and takes the recalculation with it. +- **The registry is keyed by id, not by implementation.** A custom plugin can be registered over a built-in id. Do not assume the plugin you are reading is the one that will answer for that id at run time. +- **This is the hot path.** `Interpreter.evaluateAst` runs once per formula, and once per cell for array-broadcast arguments. Allocation inside a per-cell loop is measurable. + +Deep reference: [`dev-docs/ARCHITECTURE.md`](../../dev-docs/ARCHITECTURE.md). diff --git a/src/interpreter/CLAUDE.md b/src/interpreter/CLAUDE.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/src/interpreter/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/src/interpreter/functionMetadata/AGENTS.md b/src/interpreter/functionMetadata/AGENTS.md new file mode 100644 index 0000000000..6da48a0e62 --- /dev/null +++ b/src/interpreter/functionMetadata/AGENTS.md @@ -0,0 +1,19 @@ +# `src/interpreter/functionMetadata/` — what the docs and the API say about each function + +The human-readable description of every built-in function: `shortDescription`, `parameters` with `snake_case` names and descriptions, `examples`, `documentationUrl`, and `category`. One file per category under `categories/`. + +Two consumers read it: the public `getAvailableFunctions` / `getFunctionDetails` API, and the generated `docs/guide/built-in-functions.md` page. + +## Rules + +- **Keyed by function id, not by implementation.** An entry applies to whichever plugin currently provides that id — including a custom plugin registered over a built-in. An entry left behind after a rename describes nothing and still ships in the bundle; remove or rename it in the same change. +- **Every field is required**, `documentationUrl` included. +- **Parameter count must match `implementedFunctions`.** On a mismatch the implementation wins: authored names and descriptions are discarded, positional names (`Arg1`, `Arg2`) are reported, and a console warning names the function. The function stays listed. +- **A missing entry fails the docs build.** A registered function with no entry is described as `category: 'Custom'`, and `'Custom'` has no section on the generated page, so `npm run docs:generate-function-docs` fails rather than publishing a built-in with no description. +- **`'Custom'` must never appear in a catalogue file** or in `FUNCTION_CATEGORIES`. +- **Optionality is not cross-checked.** The `optional` flag comes entirely from `optionalArg` / `defaultValue` in the plugin. A description calling an argument optional next to `optional: false` fails nothing and misleads everyone. +- **Name parameters exactly.** When a description refers to a *different* argument, use its exact `snake_case` name — "shifts `start_date` by …", never "shifts the start date by …". A parameter's own description may still open with ordinary English for the thing it describes. +- **No docs-page-local markup in `shortDescription`** — no relative links, no footnote references. API consumers render these strings too. +- **Describe HyperFormula, not Excel.** Verify against the implementation, and record deviations in [`docs/guide/list-of-differences.md`](../../../docs/guide/list-of-differences.md). + +Full detail, including why each failure mode behaves the way it does: [`dev-docs/FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md). diff --git a/src/interpreter/functionMetadata/CLAUDE.md b/src/interpreter/functionMetadata/CLAUDE.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/src/interpreter/functionMetadata/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/src/interpreter/plugin/AGENTS.md b/src/interpreter/plugin/AGENTS.md new file mode 100644 index 0000000000..10ad981e4c --- /dev/null +++ b/src/interpreter/plugin/AGENTS.md @@ -0,0 +1,73 @@ +# `src/interpreter/plugin/` — built-in functions + +Every built-in spreadsheet function lives in a plugin class extending `FunctionPlugin`. One file per plugin; a plugin holds one function or a family of related ones. + +## The shape + +```ts +export class AbsPlugin extends FunctionPlugin implements FunctionPluginTypecheck { + public static implementedFunctions: ImplementedFunctions = { + 'ABS': { + method: 'abs', + parameters: [ + {argumentType: FunctionArgumentType.NUMBER} + ] + }, + } + + public abs(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('ABS'), Math.abs) + } +} +``` + +- `implementedFunctions` maps the **canonical English id** to its metadata. Translated names never appear here. +- `method` names the class method that implements it. `FunctionPluginTypecheck` makes a mismatch a compile error. +- The method takes `(ast: ProcedureAst, state: InterpreterState)` and delegates to `runFunction`. + +## `runFunction` does the work + +Pass the raw args, the state, `this.metadata('ID')`, and a plain implementation function. `runFunction` then handles argument evaluation, arity checking, coercion to the declared `argumentType`, range vectorization, and argument broadcasting. **Write the implementation as if it received already-coerced scalars** — do not re-check types inside it. + +`runFunctionWithReferenceArgument` is the variant for functions that take a reference rather than a value (`ROW`, `COLUMN`, `SHEET`, `ISFORMULA`). It takes three callbacks: no-argument, reference, and non-reference. + +## Argument metadata + +| Field | Meaning | +|---|---| +| `argumentType` | `NUMBER`, `STRING`, `BOOLEAN`, `SCALAR`, `NOERROR`, `RANGE`, `ANY`, `INTEGER`, `COMPLEX` | +| `optionalArg` | The argument may be omitted | +| `defaultValue` | Value used when omitted | +| `minValue`, `maxValue`, `lessThan`, `greaterThan` | Range constraints, enforced during coercion | +| `passSubtype` | Keep the extended number subtype instead of unwrapping to a raw number | + +Function-level metadata worth knowing: + +| Field | Meaning | +|---|---| +| `repeatLastArgs` | How many trailing arguments repeat indefinitely (variadic functions) | +| `expandRanges` | Inline range arguments into scalar arguments | +| `isVolatile` | Recalculate on every recalculation (`RAND`, `NOW`) | +| `isDependentOnSheetStructureChange` | Recalculate when rows or columns are added or removed | +| `doesNotNeedArgumentsToBeComputed` | Reference and range arguments create no dependency | +| `vectorizationForbidden` | Never vectorize — array-output and special functions | +| `sizeOfResultArrayMethod` | Names the method predicting the result array size; required for any function that can return an array | +| `returnNumberType` | Pack the returned number into this subtype (percent, currency, date, time) | + +## Never + +- **Never throw.** Return a `CellError` with a message from `error-message.ts`. A throw escapes one cell's evaluation and takes the whole recalculation with it. +- **Never hand-roll coercion.** Declare the `argumentType` and let `runFunction` coerce, or use `ArithmeticHelper`. +- **Never describe Excel's behaviour when HyperFormula deviates.** Implement what is specified, then record the deviation in [`docs/guide/list-of-differences.md`](../../../docs/guide/list-of-differences.md). +- **Never allocate inside a per-cell loop** when the value can be hoisted. These methods run once per cell for broadcast arguments. + +## Checklist for a new or changed function + +1. Implement or modify the plugin here, with `implementedFunctions` metadata. +2. Add or update the catalogue entry in [`../functionMetadata/categories/`](../functionMetadata/AGENTS.md) — parameter **count and names must match** `implementedFunctions`, or the authored descriptions are silently discarded. +3. Add translations for **every** language in [`../../i18n/languages/`](../../i18n/AGENTS.md). +4. Add tests in `test/`, including boundary values, wrong argument types, and error propagation. +5. If the function can return an array, declare `sizeOfResultArrayMethod` and implement it. +6. If it takes an optional or zero-argument form that arity alone does not express, declare `optionalArg: true` explicitly — nothing cross-checks this. + +Skill: `hyperformula-function-dev`. Reference: [`dev-docs/FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md). diff --git a/src/interpreter/plugin/CLAUDE.md b/src/interpreter/plugin/CLAUDE.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/src/interpreter/plugin/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/src/parser/AGENTS.md b/src/parser/AGENTS.md new file mode 100644 index 0000000000..d811e47477 --- /dev/null +++ b/src/parser/AGENTS.md @@ -0,0 +1,28 @@ +# `src/parser/` — formula text to AST + +Wraps the [Chevrotain](https://chevrotain.io/) parser generator. Turns a formula string into an AST plus its relative dependencies, and turns an AST back into a string. + +## The pieces + +| File | Role | +|---|---| +| `ParserWithCaching.ts` | The entry point. Identical formula strings resolve from `Cache.ts` rather than being reparsed. | +| `FormulaParser.ts` | The Chevrotain grammar. | +| `LexerConfig.ts`, `ParserConfig.ts` | Build the token set from the active language package and config. | +| `Ast.ts` | AST node types. | +| `Unparser.ts` | AST back to formula text, in the target language. | +| `collectDependencies.ts` | Relative dependencies of an AST. | +| `Address.ts`, `CellAddress.ts`, `ColumnAddress.ts`, `RowAddress.ts` | Address representations and their absolute/relative flags. | +| `addressRepresentationConverters.ts` | A1 notation to and from the internal representation. | + +## Rules + +- **The lexer is language-dependent.** Function names, the argument separator, the decimal separator, and error literals all come from the config and the translation package. Never hard-code an English name or an English-locale separator. +- **Parsing and unparsing move together.** A grammar change that `Unparser.ts` does not learn about breaks round-tripping: the formula parses, the engine calculates, and `getCellFormula` returns something the user never typed. Add a round-trip test for every grammar change. +- **The cache is load-bearing.** `ParserWithCaching` keys on the formula string. A change that makes the parse result depend on something outside that key — the address, the sheet, mutable config — silently returns the wrong AST from the cache. If the result must vary, the key must vary. +- Address arithmetic lives in the `*Address.ts` files. Do not reimplement relative-to-absolute conversion at a call site. +- A parse failure produces a `ParsingErrorVertex` in the graph, not a thrown exception. Keep it that way — one bad formula must not take down the engine. + +## Testing + +Grammar changes need cases for: the happy parse, the round trip through `Unparser`, at least one non-English language, and the malformed input that must yield a parsing error rather than a throw. diff --git a/src/parser/CLAUDE.md b/src/parser/CLAUDE.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/src/parser/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/test/AGENTS.md b/test/AGENTS.md new file mode 100644 index 0000000000..39092c1507 --- /dev/null +++ b/test/AGENTS.md @@ -0,0 +1,39 @@ +# `test/` — the test suite + +Two suites live here. Only one of them is in this repository. + +| Suite | Path | Availability | +|---|---|---| +| Smoke tests | `smoke.spec.ts` | Everyone | +| Full suite | `hyperformula-tests/` | Private repository, git-ignored, fetched on demand | + +## The private suite is branch-matched + +[`fetch-tests.sh`](fetch-tests.sh) checks out the branch of the **same name** in the private test repository, creating it from `develop` when it does not exist. + +```bash +npm run test:setup-private +``` + +**Run this after every branch switch.** Skipping it runs the previous branch's tests against the current source: the results are meaningless, and they look like ordinary passes and failures. In a fresh git worktree the directory is absent entirely — see [`dev-docs/WORKTREES.md`](../dev-docs/WORKTREES.md). + +`test:performance`, `test:compatibility`, and the benchmark scripts all resolve into `hyperformula-tests/`. Without it they fail with a missing path, not with a test failure. Read the error before concluding the code is broken. + +## What a change must cover + +Every change to `src/` ships tests here. A bug fix ships a test that **fails against the unfixed code** — write it first and watch it fail. A feature ships a set of tests precise enough to serve as its specification. + +Cover more than the happy path: boundary values, empty and invalid input, error results, and interaction with related features. + +`docs/`, `examples/`, and `script/` are not tested. + +## How to write a case + +- **One assertion per case.** Each case is very simple and focused. +- **No loops, no conditionals in a case.** A parameterised loop hides which input failed; write the cases out. +- Name the case after the behaviour it pins, not after the function under test. +- Never relax an assertion, widen a matcher, or skip a case to turn a run green. When a test is red the default assumption is that the **code** is wrong. + +External contributors put tests here; the internal team moves them into the private repository through a separate pull request. + +Commands and detail: [`dev-docs/TESTING.md`](../dev-docs/TESTING.md), [`README.md`](README.md). Skills: `hyperformula-unit-testing`, `test-writing-discipline`. diff --git a/test/CLAUDE.md b/test/CLAUDE.md new file mode 120000 index 0000000000..47dc3e3d86 --- /dev/null +++ b/test/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file From 5e3af053404d2293def2a79b410a64eca7472ac7 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Mon, 31 Aug 2026 14:08:32 +0200 Subject: [PATCH 02/27] HF-359: Deny agent reads of every generated and built path The build outputs are git-ignored, so content searches already skip them, but nothing stopped an agent from opening dist/hyperformula.js directly or from answering a question about behaviour from typings/ instead of src/. Extend the deny list to cover every generated path in .gitignore. node_modules/ and package-lock.json are deliberately left readable: reading a dependency's source is occasionally the right move when debugging, and a deny rule would also block a targeted grep for a dependency version. Co-Authored-By: Claude Opus 5 --- .claude/settings.json | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 29304b213f..d763fe2b41 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -2,15 +2,24 @@ "enabledPlugins": ["typescript-lsp@claude-plugins-official"], "permissions": { "deny": [ - "Read(./dist/**)", "Read(./lib/**)", "Read(./es/**)", "Read(./commonjs/**)", - "Read(./typings/**)", + "Read(./dist/**)", "Read(./languages/**)", - "Read(./coverage/**)", + "Read(./typings/**)", + "Read(./docs/api/**)", + "Read(./docs/functions/**)", + "Read(./docs/guide/built-in-functions.md)", "Read(./docs/.vuepress/dist/**)", - "Read(./docs/api/**)" + "Read(./docs/.vuepress/api-sidebar.json)", + "Read(./docs/.vuepress/api-sidebar-relative.json)", + "Read(./typedoc/**)", + "Read(./doc/**)", + "Read(./coverage/**)", + "Read(./test-jest/**)", + "Read(./test-jasmine/**)", + "Read(./storage/**)" ] }, "worktree": { From 5b7cbae960690f52f4c7d464a49185a10d702ff3 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Mon, 31 Aug 2026 14:11:25 +0200 Subject: [PATCH 03/27] HF-359: Lint edited files at authoring time with a PostToolUse hook Add script/claude/post-tool-use.mjs, wired to Edit|Write in .claude/settings.json. Claude Code passes the tool payload as JSON on stdin; the hook reads the edited path, lints that one file, and exits 2 with whatever errors remain, which Claude Code shows to the agent. ESLint rules are then applied while the change is still being written, instead of surfacing when someone runs `npm run lint` at the end. Three properties are deliberate: - It reports errors only. `npm run lint` produces tens of thousands of warnings across the repository, and reporting them per edit would bury the agent in noise unrelated to its own change. - It passes `--fix-type problem,layout`, never a plain `--fix`. The jsdoc/require-jsdoc rule autofixes by inserting an EMPTY JSDoc block above every undocumented declaration, so an unconstrained --fix quietly scatters those stubs through any file the agent touches. The restriction keeps the fixes that are unambiguously right (licence header, semicolons, quotes, spacing) and leaves the rest to a human. - It fails open. A missing binary, a spawn failure, or unparseable output exits 0 in silence, because a broken hook must never block work. Also record three decisions in the monorepo plan: the repository stays on npm workspaces, the private test suite stays branch-matched and only changes its checkout path, and every package versions and releases on its own cadence, which implies a changelog per package and package-namespaced tags. Co-Authored-By: Claude Opus 5 --- .claude/settings.json | 22 ++++- AGENTS.md | 3 +- dev-docs/MONOREPO.md | 18 ++-- script/AGENTS.md | 20 ++++ script/claude/post-tool-use.mjs | 156 ++++++++++++++++++++++++++++++++ 5 files changed, 207 insertions(+), 12 deletions(-) create mode 100755 script/claude/post-tool-use.mjs diff --git a/.claude/settings.json b/.claude/settings.json index d763fe2b41..53a19a46f5 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,5 +1,7 @@ { - "enabledPlugins": ["typescript-lsp@claude-plugins-official"], + "enabledPlugins": [ + "typescript-lsp@claude-plugins-official" + ], "permissions": { "deny": [ "Read(./lib/**)", @@ -23,6 +25,22 @@ ] }, "worktree": { - "symlinkDirectories": ["node_modules"] + "symlinkDirectories": [ + "node_modules" + ] + }, + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "node \"$CLAUDE_PROJECT_DIR/script/claude/post-tool-use.mjs\"", + "timeout": 60 + } + ] + } + ] } } diff --git a/AGENTS.md b/AGENTS.md index f297553114..4301e32b6f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -110,5 +110,6 @@ This section is maintained by the team. Skills, MCP servers, and other tools vet - **Name** — What it provides and when to use it. --> -- **`.claude/skills/`** — Repository skills, scoped by a `path` glob in their frontmatter. `hyperformula-dev` is the entry point for engine work; `hyperformula-function-dev` for adding or changing a built-in function; `pr-creation` before opening a pull request. +- **`.claude/skills/`** — Repository skills, scoped by a `paths` glob in their frontmatter. `hyperformula-dev` is the entry point for engine work; `hyperformula-function-dev` for adding or changing a built-in function; `pr-creation` before opening a pull request. - **`typescript-lsp` plugin** — Language-server-backed go-to-definition and find-references. Use it instead of grepping for a symbol's definition or callers; grep stays right for text searches. Enabled repository-wide in `.claude/settings.json`. +- **`PostToolUse` lint hook** — After every `Edit` or `Write`, [`script/claude/post-tool-use.mjs`](script/claude/post-tool-use.mjs) runs `eslint --fix` on that file and reports the remaining **errors** back to the agent. Fix them in the same turn. Warnings are deliberately not reported. See [`script/AGENTS.md`](script/AGENTS.md). diff --git a/dev-docs/MONOREPO.md b/dev-docs/MONOREPO.md index 0c3eb7d42d..569acc1098 100644 --- a/dev-docs/MONOREPO.md +++ b/dev-docs/MONOREPO.md @@ -25,8 +25,8 @@ hyperformula/ # repository root — private, worksp ├── CLAUDE.md -> AGENTS.md ├── DEV_DOCS.md # pointer into dev-docs/ ├── README.md CONTRIBUTING.md CHANGELOG.md LICENSE.txt -├── package.json # private: true, workspaces, fan-out scripts -├── pnpm-workspace.yaml +├── package.json # private: true, npm workspaces, fan-out scripts +├── package-lock.json ├── .worktreeinclude ├── .changelogs/ # one JSON fragment per PR (see below) ├── .claude/ @@ -88,23 +88,23 @@ hyperformula/ # repository root — private, worksp ## Why this shape - **Flat, name-matched top-level directories.** A directory is named after the package it holds, so a path in a stack trace, a CI job name, and a changelog entry all say the same word. No `packages/` wrapper at the root — it adds a level that carries no information. -- **`hyperformula-ui/` keeps its own `packages/`** because it genuinely holds four published packages that version together. Its workspace glob is `hyperformula-ui/packages/*`. +- **`hyperformula-ui/` keeps its own `packages/`** because it genuinely holds four published packages that share a source tree and a test setup. They still version independently, like every other package here. Its entry in the root `workspaces` array is `hyperformula-ui/packages/*`. - **`AGENTS.md` travels with the code.** Every file listed above stays with its directory through the move, so the agent instructions survive the migration unchanged. -- **One `.claude/skills/` at the root.** Skills are scoped by a `path` glob in their frontmatter rather than by placement, so there is one place to look and one place to keep them consistent. +- **Every package versions and releases on its own cadence.** No lockstep version, no shared release train. Consequences to build in from the start: one `CHANGELOG.md` per package rather than one at the root, git tags namespaced by package (`hyperformula@3.5.0`, `@hyperformula/ui-core@0.2.0`), a changelog fragment that names its package, and a release workflow parameterised by package instead of one that ships everything. Cross-package dependencies are declared as ordinary semver ranges, so the UI packages pin an engine range and are not forced to re-release when the engine does. +- **One `.claude/skills/` at the root.** Skills are scoped by a `paths` glob in their frontmatter rather than by placement, so there is one place to look and one place to keep them consistent. ## Migration steps -1. **Pick the package manager.** The engine uses npm with `package-lock.json` today; `hyperformula-ui` and the Handsontable monorepo both use pnpm. Converging on pnpm makes the incoming package a straight move and matches the sibling repository; it costs one lockfile migration and a CI update. +1. **Stay on npm.** The engine uses npm with a committed `package-lock.json`, and it keeps doing so: `workspaces` in the root `package.json` covers what this repository needs, and a package-manager migration is a risk the monorepo move does not need to carry at the same time. The cost lands on the way in — `hyperformula-ui` arrives as a pnpm workspace, so its `pnpm-workspace.yaml` and `pnpm-lock.yaml` are dropped and its package globs fold into the root `workspaces` array. Reconsider pnpm only if npm's hoisting turns out to break the wrapper packages. 2. **Move `src/` and `test/` into `hyperformula/`.** Mechanical, but it invalidates every path in CI, in `tsconfig.json`, in `jest.config.js`, in `karma.conf.js`, in `.eslintignore`, and in the docs generator scripts. 3. **Give `docs/` its own `package.json`** and take it out of the root dependency tree. 4. **Move the root build scripts down into `hyperformula/package.json`**, leaving fan-out scripts at the root. 5. **Bring in `hyperformula-ui`** from the formula-builder repository, preserving its history. Decide the published scope (`@hfe/*` today) before the first release from here. 6. **Bring in `hyperformula-skill`** from the shared skills repository. Its marketplace entry has to keep resolving — either the plugin build publishes from here, or the old repository keeps pointing at this one. -7. **Split `CHANGELOG.md` into `.changelogs/*.json` fragments.** One `CHANGELOG.md` edited by every package's pull requests conflicts on every merge. A per-PR JSON fragment plus a `consume` step removes the conflict entirely. -8. **Path-filter CI.** Each package's jobs run only when its paths change; full runs on `develop`, `master`, and release branches. +7. **Split `CHANGELOG.md` into `.changelogs/*.json` fragments.** One `CHANGELOG.md` edited by every package's pull requests conflicts on every merge. A per-PR JSON fragment plus a `consume` step removes the conflict entirely. Each fragment names the package it belongs to, so `consume` can compile one package's changelog without touching the others. +8. **Keep the private test suite branch-matched.** `hyperformula-tests` stays keyed to this repository's branch name; only its checkout path moves, from `test/hyperformula-tests/` to `hyperformula/test/hyperformula-tests/`. Update `fetch-tests.sh`, the `test:setup-private` script, and `.gitignore` together, and re-check `.worktreeinclude`, which names the old path. +9. **Path-filter CI.** Each package's jobs run only when its paths change; full runs on `develop`, `master`, and release branches. ## Open questions -- Does the private `hyperformula-tests` repository stay branch-matched to this repository once the engine sits one level down? Its `fetch-tests.sh` target path changes with the move. -- Do the UI packages ship on the engine's release cadence, or their own? - Which scope do the UI packages publish under, and does renaming them break existing consumers? diff --git a/script/AGENTS.md b/script/AGENTS.md index e911632446..7671dc16e3 100644 --- a/script/AGENTS.md +++ b/script/AGENTS.md @@ -11,6 +11,7 @@ Node and shell scripts invoked from `package.json`. Not shipped, not tested. | `prepare-cf-assets.js` | `docs:build:cf` | Composes the Cloudflare Worker asset tree | | `if-ne-env.js` | the `bundle:*` scripts | Skips a recompile when `HF_COMPILE` is already set | | `release/` | `npm run release` | The release procedure | +| `claude/post-tool-use.mjs` | the Claude Code `PostToolUse` hook | Lints the file an agent just edited and reports the remaining ESLint errors back to it | ## Rules @@ -18,3 +19,22 @@ Node and shell scripts invoked from `package.json`. Not shipped, not tested. - **Release is maintainer-owned.** Do not invent steps around `script/release/`, and do not run it as part of another task. - These scripts run in CI as well as locally. Do not assume an interactive terminal, a specific working directory, or a developer's environment variables. - Nothing here is covered by tests. A change to a script is verified by running it. + +## `claude/` — agent-time hooks + +`post-tool-use.mjs` is wired to `Edit|Write` in [`.claude/settings.json`](../.claude/settings.json). Claude Code passes the tool payload as JSON on stdin; the script reads `tool_input.file_path`, runs `eslint --fix` on that one file, and exits 2 with the remaining errors on stderr, which Claude Code shows to the agent. + +Two properties to preserve when changing it: + +- **Errors only, never warnings.** `npm run lint` reports tens of thousands of warnings across the repository. Reporting them per edit would bury the agent in noise unrelated to the change it just made. +- **`--fix-type problem,layout`.** Never plain `--fix`. `jsdoc/require-jsdoc` is a suggestion-type rule whose autofix inserts an **empty** JSDoc block above every undocumented declaration, so an unconstrained `--fix` quietly scatters those stubs through any file the agent edits. The restriction keeps the fixes that are unambiguously right — the licence header, semicolons, quotes, spacing — and drops the ones that need a human. +- **Fails open.** A missing ESLint binary, a spawn failure, or unparseable output exits 0 silently. A broken hook must never block work. + +Verify a change to it by piping a payload in by hand: + +```bash +printf '{"tool_name":"Edit","cwd":"'"$PWD"'","tool_input":{"file_path":"src/interpreter/plugin/AbsPlugin.ts"}}' \ + | node script/claude/post-tool-use.mjs; echo "exit=$?" +``` + +`script/` is listed in [`.eslintignore`](../.eslintignore), so these files are not linted themselves. diff --git a/script/claude/post-tool-use.mjs b/script/claude/post-tool-use.mjs new file mode 100755 index 0000000000..cb33f40666 --- /dev/null +++ b/script/claude/post-tool-use.mjs @@ -0,0 +1,156 @@ +#!/usr/bin/env node +/** + * Claude Code PostToolUse hook, matched on `Edit|Write`. + * + * Lints the file the agent just wrote and feeds the remaining ESLint *errors* + * back to the model (exit 2), so style and correctness rules are applied at + * authoring time instead of surfacing at the end of the session. + * + * Two deliberate choices: + * + * - Errors only. `npm run lint` currently reports tens of thousands of + * warnings across the repository; reporting them per edit would drown the + * model in noise unrelated to the change it just made. + * - `--fix-type problem,layout`. Plain `--fix` also applies suggestion-type + * fixes, and `jsdoc/require-jsdoc` autofixes by inserting an EMPTY JSDoc + * block above every undocumented declaration. Left unconstrained, the hook + * silently sprinkles those stubs through any source file it touches. + * - Fails open. Any problem with the hook itself (no ESLint binary, spawn + * failure, unparseable output) exits 0 and stays silent. A broken hook must + * never block work. + * + * The tool payload arrives as JSON on stdin; there is no environment variable + * carrying the edited path. + */ +import { spawnSync } from 'node:child_process' +import { readFileSync } from 'node:fs' +import path from 'node:path' + +// npx is a .cmd shim on Windows; spawnSync needs a shell there or it ENOENTs. +const WINDOWS = process.platform === 'win32' + +const LINTABLE_EXTENSIONS = new Set(['.ts', '.js']) + +/** Maximum number of errors reported back to the model for a single file. */ +const MAX_REPORTED_ERRORS = 20 + +/** + * Reads the whole of stdin synchronously. + * + * @returns {string} Raw stdin contents, or an empty string when there is none. + */ +function readStdin() { + try { + // Read fd 0 directly - cross-platform, unlike spawning `cat`. + return readFileSync(0, 'utf8') + } catch { + return '' + } +} + +/** + * Extracts the edited file path from a PostToolUse payload. + * + * @param {string} rawPayload Raw JSON received on stdin. + * @returns {string | undefined} Absolute path of the edited file, if there is one. + */ +function editedFilePath(rawPayload) { + try { + const payload = JSON.parse(rawPayload) + const filePath = payload?.tool_input?.file_path + + if (typeof filePath !== 'string' || filePath.length === 0) { + return undefined + } + + const projectDir = process.env.CLAUDE_PROJECT_DIR ?? payload?.cwd ?? process.cwd() + + return path.resolve(projectDir, filePath) + } catch { + return undefined + } +} + +/** + * Runs ESLint with `--fix` over a single file. + * + * @param {string} filePath Absolute path of the file to lint. + * @returns {object[] | undefined} ESLint JSON results, or `undefined` when the run itself failed. + */ +function lintAndFix(filePath) { + const result = spawnSync( + 'npx', + ['--no-install', 'eslint', '--fix', '--fix-type', 'problem,layout', '--format', 'json', filePath], + { encoding: 'utf8', shell: WINDOWS, maxBuffer: 10 * 1024 * 1024 } + ) + + // ESLint exits 1 when it reports errors, which is the case this hook exists + // for. Anything else - a missing binary, a crash - means the run failed and + // the hook has nothing trustworthy to say. + if (result.error || result.status === null || result.status > 1) { + return undefined + } + + try { + return JSON.parse(result.stdout) + } catch { + return undefined + } +} + +/** + * Collects the ESLint messages of severity `error` from a set of results. + * + * @param {object[]} results ESLint JSON results. + * @returns {object[]} The error-severity messages. + */ +function errorsOnly(results) { + return results.flatMap(result => (result.messages ?? []).filter(message => message.severity === 2)) +} + +/** + * Renders the errors as a message for the model. + * + * @param {string} filePath Absolute path of the linted file. + * @param {object[]} errors The error-severity messages. + * @returns {string} The message to write to stderr. + */ +function formatReport(filePath, errors) { + const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd() + const relativePath = path.relative(projectDir, filePath) || filePath + const shown = errors.slice(0, MAX_REPORTED_ERRORS) + const lines = shown.map( + ({ line, column, message, ruleId }) => ` ${relativePath}:${line}:${column} ${message}${ruleId ? ` (${ruleId})` : ''}` + ) + + if (errors.length > shown.length) { + lines.push(` ...and ${errors.length - shown.length} more.`) + } + + return [ + `ESLint reports ${errors.length} error${errors.length === 1 ? '' : 's'} in ${relativePath} that --fix could not resolve:`, + ...lines, + 'Fix them now. ESLint is the source of truth for formatting and code rules in this repository.', + ].join('\n') +} + +const filePath = editedFilePath(readStdin()) + +if (filePath === undefined || !LINTABLE_EXTENSIONS.has(path.extname(filePath))) { + process.exit(0) +} + +const results = lintAndFix(filePath) + +if (results === undefined) { + process.exit(0) +} + +const errors = errorsOnly(results) + +if (errors.length === 0) { + process.exit(0) +} + +process.stderr.write(`${formatReport(filePath, errors)}\n`) +process.exit(2) From 7cb969fc133598e8d8fbf0903cc0e6765012bc6c Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Mon, 31 Aug 2026 14:13:36 +0200 Subject: [PATCH 04/27] HF-359: Record the remaining monorepo decisions Every package versions and releases on its own cadence, so the plan now calls for a changelog per package, package-namespaced tags, and a release workflow parameterised by package. The UI packages keep the @hfe/* scope they publish under today: moving them between repositories is enough change for one migration, and a rename would break every existing consumer's imports for no benefit the move itself delivers. Co-Authored-By: Claude Opus 5 --- dev-docs/MONOREPO.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dev-docs/MONOREPO.md b/dev-docs/MONOREPO.md index 569acc1098..e62b490de4 100644 --- a/dev-docs/MONOREPO.md +++ b/dev-docs/MONOREPO.md @@ -7,10 +7,10 @@ This repository is becoming a monorepo (HF-359). This file is the target: what t | Package | Directory | Purpose | Published | |---|---|---|---| | `hyperformula` | `hyperformula/` | The calculation engine. Everything in `src/` and `test/` today. | yes | -| `@hyperformula/ui-core` | `hyperformula-ui/packages/core/` | Formula editor UI: reference highlighting, inline editor, function help. | yes | -| `@hyperformula/handsontable-adapter` | `hyperformula-ui/packages/handsontable-adapter/` | Binds the editor to Handsontable. | yes | -| `@hyperformula/plain-table-adapter` | `hyperformula-ui/packages/plain-table-adapter/` | Binds the editor to a plain HTML table. | yes | -| `@hyperformula/tanstack-table-adapter` | `hyperformula-ui/packages/tanstack-table-adapter/` | Binds the editor to TanStack Table. | yes | +| `@hfe/core` | `hyperformula-ui/packages/core/` | Formula editor UI: reference highlighting, inline editor, function help. | yes | +| `@hfe/handsontable-adapter` | `hyperformula-ui/packages/handsontable-adapter/` | Binds the editor to Handsontable. | yes | +| `@hfe/plain-table-adapter` | `hyperformula-ui/packages/plain-table-adapter/` | Binds the editor to a plain HTML table. | yes | +| `@hfe/tanstack-table-adapter` | `hyperformula-ui/packages/tanstack-table-adapter/` | Binds the editor to TanStack Table. | yes | | `hyperformula-skill` | `hyperformula-skill/` | The agent skill that teaches coding agents to use HyperFormula. | as a plugin/zip | | `hyperformula-docs` | `docs/` | The VuePress documentation portal. | no | | — | `hyperformula-ui/demo/`, `hyperformula-ui/e2e/` | Demo app and end-to-end tests for the UI packages. | no | @@ -90,7 +90,7 @@ hyperformula/ # repository root — private, worksp - **Flat, name-matched top-level directories.** A directory is named after the package it holds, so a path in a stack trace, a CI job name, and a changelog entry all say the same word. No `packages/` wrapper at the root — it adds a level that carries no information. - **`hyperformula-ui/` keeps its own `packages/`** because it genuinely holds four published packages that share a source tree and a test setup. They still version independently, like every other package here. Its entry in the root `workspaces` array is `hyperformula-ui/packages/*`. - **`AGENTS.md` travels with the code.** Every file listed above stays with its directory through the move, so the agent instructions survive the migration unchanged. -- **Every package versions and releases on its own cadence.** No lockstep version, no shared release train. Consequences to build in from the start: one `CHANGELOG.md` per package rather than one at the root, git tags namespaced by package (`hyperformula@3.5.0`, `@hyperformula/ui-core@0.2.0`), a changelog fragment that names its package, and a release workflow parameterised by package instead of one that ships everything. Cross-package dependencies are declared as ordinary semver ranges, so the UI packages pin an engine range and are not forced to re-release when the engine does. +- **Every package versions and releases on its own cadence.** No lockstep version, no shared release train. Consequences to build in from the start: one `CHANGELOG.md` per package rather than one at the root, git tags namespaced by package (`hyperformula@3.5.0`, `@hfe/core@0.2.0`), a changelog fragment that names its package, and a release workflow parameterised by package instead of one that ships everything. Cross-package dependencies are declared as ordinary semver ranges, so the UI packages pin an engine range and are not forced to re-release when the engine does. - **One `.claude/skills/` at the root.** Skills are scoped by a `paths` glob in their frontmatter rather than by placement, so there is one place to look and one place to keep them consistent. ## Migration steps @@ -99,7 +99,7 @@ hyperformula/ # repository root — private, worksp 2. **Move `src/` and `test/` into `hyperformula/`.** Mechanical, but it invalidates every path in CI, in `tsconfig.json`, in `jest.config.js`, in `karma.conf.js`, in `.eslintignore`, and in the docs generator scripts. 3. **Give `docs/` its own `package.json`** and take it out of the root dependency tree. 4. **Move the root build scripts down into `hyperformula/package.json`**, leaving fan-out scripts at the root. -5. **Bring in `hyperformula-ui`** from the formula-builder repository, preserving its history. Decide the published scope (`@hfe/*` today) before the first release from here. +5. **Bring in `hyperformula-ui`** from the formula-builder repository, preserving its history. It keeps the `@hfe/*` scope it publishes under today — moving the packages between repositories is already enough change for one migration, and a rename would break every existing consumer's imports for no benefit the move itself delivers. Revisit the scope as its own decision, not as a side effect of this one. 6. **Bring in `hyperformula-skill`** from the shared skills repository. Its marketplace entry has to keep resolving — either the plugin build publishes from here, or the old repository keeps pointing at this one. 7. **Split `CHANGELOG.md` into `.changelogs/*.json` fragments.** One `CHANGELOG.md` edited by every package's pull requests conflicts on every merge. A per-PR JSON fragment plus a `consume` step removes the conflict entirely. Each fragment names the package it belongs to, so `consume` can compile one package's changelog without touching the others. 8. **Keep the private test suite branch-matched.** `hyperformula-tests` stays keyed to this repository's branch name; only its checkout path moves, from `test/hyperformula-tests/` to `hyperformula/test/hyperformula-tests/`. Update `fetch-tests.sh`, the `test:setup-private` script, and `.gitignore` together, and re-check `.worktreeinclude`, which names the old path. @@ -107,4 +107,4 @@ hyperformula/ # repository root — private, worksp ## Open questions -- Which scope do the UI packages publish under, and does renaming them break existing consumers? +None outstanding. Decisions taken so far are recorded above; add new questions here as the migration turns them up. From 7c2ac83d65fb2e9dc07529476458d5abed4c91b5 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Mon, 31 Aug 2026 14:35:37 +0200 Subject: [PATCH 05/27] HF-359: Record the worker, changelog, and Node version decisions Four decisions, and one small correctness fix on the way through. - The Cloudflare Worker moves under docs/. wrangler.jsonc and worker/index.js exist only to serve the built portal, and docs/ is leaving the workspace anyway, so the whole deployment path ends up in one directory that installs and versions on its own. - CHANGELOG.md keeps its current Keep a Changelog form, one file per package. No JSON fragment mechanism: fragments solve merge conflicts this repository does not have yet, and introducing them now would cost a tool, a CI gate, and a habit change for a problem that has not arrived. - Every .nvmrc says 22, docs/ included once it has its own. - The HyperFormula agent skill stays in the shared skills repository for now. Bringing it here is a separate decision taken later; nothing in the target layout depends on it. The fix: test/fetch-tests.sh called the directory holding test/ REPO_ROOT. That is true today and stops being true once src/ and test/ move a level down, where it becomes the package root instead. Renamed to PACKAGE_ROOT, with a comment recording that all the script needs from it is a path inside the git working tree, which `git rev-parse` resolves from any depth. Verified by running `npm run test:setup-private`. Also correct docs/README.md, which still asked for Node.js 14+. Co-Authored-By: Claude Opus 5 --- dev-docs/MONOREPO.md | 33 ++++++++++++++++----------------- docs/README.md | 2 +- test/fetch-tests.sh | 10 +++++++--- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/dev-docs/MONOREPO.md b/dev-docs/MONOREPO.md index e62b490de4..d8a5a45350 100644 --- a/dev-docs/MONOREPO.md +++ b/dev-docs/MONOREPO.md @@ -11,10 +11,11 @@ This repository is becoming a monorepo (HF-359). This file is the target: what t | `@hfe/handsontable-adapter` | `hyperformula-ui/packages/handsontable-adapter/` | Binds the editor to Handsontable. | yes | | `@hfe/plain-table-adapter` | `hyperformula-ui/packages/plain-table-adapter/` | Binds the editor to a plain HTML table. | yes | | `@hfe/tanstack-table-adapter` | `hyperformula-ui/packages/tanstack-table-adapter/` | Binds the editor to TanStack Table. | yes | -| `hyperformula-skill` | `hyperformula-skill/` | The agent skill that teaches coding agents to use HyperFormula. | as a plugin/zip | | `hyperformula-docs` | `docs/` | The VuePress documentation portal. | no | | — | `hyperformula-ui/demo/`, `hyperformula-ui/e2e/` | Demo app and end-to-end tests for the UI packages. | no | +The HyperFormula agent skill stays in the shared `handsontable-skills` repository for now. Bringing it here is a separate decision, taken later and on its own terms; nothing in this layout depends on it. + `docs/` is deliberately **not** a workspace member: the portal drags in a large, old dependency tree (VuePress, `--openssl-legacy-provider`) that must not reach an engine install. It keeps its own `package.json` and is installed separately. This mirrors how the Handsontable monorepo isolates its documentation site. ## Tree @@ -28,11 +29,9 @@ hyperformula/ # repository root — private, worksp ├── package.json # private: true, npm workspaces, fan-out scripts ├── package-lock.json ├── .worktreeinclude -├── .changelogs/ # one JSON fragment per PR (see below) ├── .claude/ │ ├── settings.json # hooks, enabledPlugins, worktree settings -│ ├── skills/ # ALL skills, scoped by frontmatter path -│ └── agents/ +│ └── skills/ # ALL skills, scoped by the `paths` frontmatter field ├── dev-docs/ # monorepo-scope reference │ ├── README.md STRUCTURE.md BUILD.md TESTING.md │ ├── DEFINITION-OF-DONE.md CODE-STYLE.md DOC-STANDARDS.md @@ -69,17 +68,13 @@ hyperformula/ # repository root — private, worksp │ ├── demo/ │ └── e2e/ │ -├── hyperformula-skill/ # ── agent skill -│ ├── AGENTS.md CLAUDE.md -> AGENTS.md -│ ├── skills/hyperformula/SKILL.md -│ └── scripts/ -│ ├── docs/ # ── documentation portal (NOT a workspace member) │ ├── AGENTS.md CLAUDE.md -> AGENTS.md -│ ├── package.json +│ ├── package.json .nvmrc # .nvmrc says 22, like every other one here +│ ├── wrangler.jsonc # deploy config for the portal +│ ├── worker/index.js # Cloudflare Worker serving the built portal │ ├── guide/ api/ .vuepress/ │ -├── worker/ # Cloudflare Worker serving the built docs ├── examples/ # images and CSV fixtures used by the docs ├── script/ AGENTS.md └── .github/workflows/ # path-filtered per-package jobs @@ -90,7 +85,10 @@ hyperformula/ # repository root — private, worksp - **Flat, name-matched top-level directories.** A directory is named after the package it holds, so a path in a stack trace, a CI job name, and a changelog entry all say the same word. No `packages/` wrapper at the root — it adds a level that carries no information. - **`hyperformula-ui/` keeps its own `packages/`** because it genuinely holds four published packages that share a source tree and a test setup. They still version independently, like every other package here. Its entry in the root `workspaces` array is `hyperformula-ui/packages/*`. - **`AGENTS.md` travels with the code.** Every file listed above stays with its directory through the move, so the agent instructions survive the migration unchanged. -- **Every package versions and releases on its own cadence.** No lockstep version, no shared release train. Consequences to build in from the start: one `CHANGELOG.md` per package rather than one at the root, git tags namespaced by package (`hyperformula@3.5.0`, `@hfe/core@0.2.0`), a changelog fragment that names its package, and a release workflow parameterised by package instead of one that ships everything. Cross-package dependencies are declared as ordinary semver ranges, so the UI packages pin an engine range and are not forced to re-release when the engine does. +- **Every package versions and releases on its own cadence.** No lockstep version, no shared release train. Consequences to build in from the start: one `CHANGELOG.md` per package rather than one at the root, git tags namespaced by package (`hyperformula@3.5.0`, `@hfe/core@0.2.0`), and a release workflow parameterised by package instead of one that ships everything. Cross-package dependencies are declared as ordinary semver ranges, so the UI packages pin an engine range and are not forced to re-release when the engine does. +- **The Cloudflare Worker belongs to `docs/`.** `wrangler.jsonc` and `worker/index.js` exist only to serve the built portal, and `docs/` is leaving the workspace anyway. Moving them under `docs/` keeps the whole deployment path in one directory that installs and versions on its own, instead of leaving two root-level files whose only consumer lives elsewhere. `wrangler.jsonc`'s `main` and `script/prepare-cf-assets.js` change with them. +- **Every `.nvmrc` says `22`.** One Node version across the repository, including `docs/` once it has its own. A package that needs a different one is a problem to fix, not a version to pin around. +- **`CHANGELOG.md` keeps its current form** — Keep a Changelog, an `## [Unreleased]` block, one bullet per change with a link to the pull request or issue. One file per package rather than one at the root, and no fragment mechanism. Fragments solve merge conflicts this repository does not have yet; introducing them now would cost a tool, a CI gate, and a habit change for a problem that has not arrived. - **One `.claude/skills/` at the root.** Skills are scoped by a `paths` glob in their frontmatter rather than by placement, so there is one place to look and one place to keep them consistent. ## Migration steps @@ -100,11 +98,12 @@ hyperformula/ # repository root — private, worksp 3. **Give `docs/` its own `package.json`** and take it out of the root dependency tree. 4. **Move the root build scripts down into `hyperformula/package.json`**, leaving fan-out scripts at the root. 5. **Bring in `hyperformula-ui`** from the formula-builder repository, preserving its history. It keeps the `@hfe/*` scope it publishes under today — moving the packages between repositories is already enough change for one migration, and a rename would break every existing consumer's imports for no benefit the move itself delivers. Revisit the scope as its own decision, not as a side effect of this one. -6. **Bring in `hyperformula-skill`** from the shared skills repository. Its marketplace entry has to keep resolving — either the plugin build publishes from here, or the old repository keeps pointing at this one. -7. **Split `CHANGELOG.md` into `.changelogs/*.json` fragments.** One `CHANGELOG.md` edited by every package's pull requests conflicts on every merge. A per-PR JSON fragment plus a `consume` step removes the conflict entirely. Each fragment names the package it belongs to, so `consume` can compile one package's changelog without touching the others. -8. **Keep the private test suite branch-matched.** `hyperformula-tests` stays keyed to this repository's branch name; only its checkout path moves, from `test/hyperformula-tests/` to `hyperformula/test/hyperformula-tests/`. Update `fetch-tests.sh`, the `test:setup-private` script, and `.gitignore` together, and re-check `.worktreeinclude`, which names the old path. -9. **Path-filter CI.** Each package's jobs run only when its paths change; full runs on `develop`, `master`, and release branches. +6. **Move `wrangler.jsonc` and `worker/` under `docs/`.** Update `wrangler.jsonc`'s `main`, the `docs:*:cf` scripts in `package.json`, and `script/prepare-cf-assets.js` in the same change. Verify with `npm run docs:preview:cf` before merging — a broken `main` path fails only at deploy time. +7. **Split `CHANGELOG.md` per package**, each keeping the current Keep a Changelog form. No fragment mechanism. +8. **Give every package an `.nvmrc` saying `22`**, `docs/` included. +9. **Keep the private test suite branch-matched.** `hyperformula-tests` stays keyed to this repository's branch name; only its checkout path moves, from `test/hyperformula-tests/` to `hyperformula/test/hyperformula-tests/`. Update `fetch-tests.sh`, the `test:setup-private` script, and `.gitignore` together, and re-check `.worktreeinclude`, which names the old path. +10. **Path-filter CI.** Each package's jobs run only when its paths change; full runs on `develop`, `master`, and release branches. ## Open questions -None outstanding. Decisions taken so far are recorded above; add new questions here as the migration turns them up. +- Does `examples/` move under `docs/` as well? Every reference to it comes from a guide in `docs/guide/`, so it has no other consumer, but it is not part of the deployment path the way the Worker is. diff --git a/docs/README.md b/docs/README.md index ae2201be6d..924d37ea90 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,7 +14,7 @@ When editing the docs, you can use features described [here](https://vuepress.vu To start a local HyperFormula docs server: -1. Make sure you're running [Node.js](https://nodejs.org/en/) 14+. +1. Make sure you're running the [Node.js](https://nodejs.org/en/) version pinned in [`.nvmrc`](../.nvmrc) (22). 2. From the main `hyperformula` directory, install the docs dependencies: ```bash npm install diff --git a/test/fetch-tests.sh b/test/fetch-tests.sh index 9f585b1724..d232b0843a 100755 --- a/test/fetch-tests.sh +++ b/test/fetch-tests.sh @@ -2,7 +2,10 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +# The directory that holds test/. That is the repository root today, and will be +# the hyperformula package root once this repository becomes a monorepo. Either +# way it sits inside the git working tree, which is all `git rev-parse` needs. +PACKAGE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" HYPERFORMULA_TESTS_DIR="$SCRIPT_DIR/hyperformula-tests" # 1. Check if hyperformula-tests exists @@ -15,8 +18,9 @@ if [ ! -d "$HYPERFORMULA_TESTS_DIR" ]; then fi fi -# 2. Get current branch from root repo (GitHub Actions uses detached HEAD, so use env vars in CI) -cd "$REPO_ROOT" +# 2. Get the current branch of the hyperformula working tree (GitHub Actions uses a +# detached HEAD, so prefer the env vars there) +cd "$PACKAGE_ROOT" if [ -n "$GITHUB_HEAD_REF" ]; then CURRENT_BRANCH="$GITHUB_HEAD_REF" elif [ -n "$GITHUB_REF_NAME" ] && [[ "$GITHUB_REF_NAME" != *"/merge" ]]; then From c1fd06b4cc7ab3610aadc92c52fbdcae09f0e17c Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Mon, 31 Aug 2026 15:16:43 +0200 Subject: [PATCH 06/27] HF-359: Make dev-docs the single source of truth Review feedback. Every AGENTS.md, README.md, and SKILL.md now carries only what is so specific to its own context that it would be useless anywhere else; everything else is a link into dev-docs/. Two copies of a rule means one of them is wrong within a release, and the reader cannot tell which. The always-loaded surface drops from roughly 610 lines to 147. Each per-directory AGENTS.md is now six to ten lines: what the directory is, and where the detail lives. The nine skills drop from about 1100 lines to 273, keeping the steps and the ordering and linking out for the rules those steps enforce. The content moved rather than being deleted. dev-docs/ gains PARSER.md, INTERPRETER.md (including the whole built-in function contract), DEPENDENCY-GRAPH.md, PULL-REQUESTS.md, and AGENT-TOOLING.md; ARCHITECTURE.md absorbs the engine-wide invariants, TESTING.md the test-writing discipline, DOC-STANDARDS.md the changelog rules, FUNCTION-CATALOGUE.md and I18N.md their respective rule lists. script/README.md is new, and docs/README.md now describes the portal it lives in. BUILD.md no longer restates docs/guide/building.md. It points there for the command reference and keeps only what the public guide omits: the intermediate lib/ build, HF_COMPILE, the generated documentation and the rule against committing it, the Cloudflare deployment path, release, and packaging. MONOREPO.md drops the rationale and open-questions sections - the plan is worth keeping, the record of how it was decided is not - and hyperformula-ui is now one package rather than four. DEV_DOCS.md is removed; dev-docs/README.md is the front page. .ai/ exists with a single sentence pointing at dev-docs/, for agents that look there by habit. Co-Authored-By: Claude Opus 5 --- .ai/README.md | 3 + .claude/skills/changelog-creation/SKILL.md | 62 ++--------- .../skills/hyperformula-code-review/SKILL.md | 56 +++------- .claude/skills/hyperformula-dev/SKILL.md | 61 ++++------ .../skills/hyperformula-function-dev/SKILL.md | 104 +++--------------- .../skills/hyperformula-unit-testing/SKILL.md | 56 +++------- .claude/skills/i18n-translations/SKILL.md | 37 ++----- .claude/skills/pr-creation/SKILL.md | 71 +++--------- .../skills/test-writing-discipline/SKILL.md | 57 ++-------- .claude/skills/writing-docs-pages/SKILL.md | 45 +++----- AGENTS.md | 95 ++++------------ DEV_DOCS.md | 21 ---- dev-docs/AGENT-TOOLING.md | 65 +++++++++++ dev-docs/ARCHITECTURE.md | 31 +++++- dev-docs/BUILD.md | 72 +++++------- dev-docs/DEPENDENCY-GRAPH.md | 29 +++++ dev-docs/DOC-STANDARDS.md | 37 +++++++ dev-docs/FUNCTION-CATALOGUE.md | 12 ++ dev-docs/I18N.md | 8 ++ dev-docs/INTERPRETER.md | 95 ++++++++++++++++ dev-docs/MONOREPO.md | 78 ++++--------- dev-docs/PARSER.md | 28 +++++ dev-docs/PULL-REQUESTS.md | 56 ++++++++++ dev-docs/README.md | 58 +++++++--- dev-docs/STRUCTURE.md | 2 +- dev-docs/TESTING.md | 65 ++++++++++- docs/AGENTS.md | 53 ++------- docs/README.md | 14 +++ script/AGENTS.md | 43 +------- script/README.md | 22 ++++ src/AGENTS.md | 40 +------ src/DependencyGraph/AGENTS.md | 31 +----- src/i18n/AGENTS.md | 24 +--- src/interpreter/AGENTS.md | 28 +---- src/interpreter/functionMetadata/AGENTS.md | 21 +--- src/interpreter/plugin/AGENTS.md | 75 +------------ src/parser/AGENTS.md | 30 +---- test/AGENTS.md | 42 +------ 38 files changed, 752 insertions(+), 975 deletions(-) create mode 100644 .ai/README.md delete mode 100644 DEV_DOCS.md create mode 100644 dev-docs/AGENT-TOOLING.md create mode 100644 dev-docs/DEPENDENCY-GRAPH.md create mode 100644 dev-docs/INTERPRETER.md create mode 100644 dev-docs/PARSER.md create mode 100644 dev-docs/PULL-REQUESTS.md create mode 100644 script/README.md diff --git a/.ai/README.md b/.ai/README.md new file mode 100644 index 0000000000..a0f58df44e --- /dev/null +++ b/.ai/README.md @@ -0,0 +1,3 @@ +# `.ai/` + +Everything an AI agent needs is in [`dev-docs/`](../dev-docs/README.md). This directory holds nothing else. diff --git a/.claude/skills/changelog-creation/SKILL.md b/.claude/skills/changelog-creation/SKILL.md index 793b5b249f..3e7d6f349b 100644 --- a/.claude/skills/changelog-creation/SKILL.md +++ b/.claude/skills/changelog-creation/SKILL.md @@ -3,58 +3,14 @@ name: changelog-creation description: Use when a change to source code needs a changelog entry, and before pushing any bug fix, feature, or behaviour change. Covers when an entry is required, which section it belongs in, how to write the title, and the link format. --- -## When an entry is required +Which section to use, the bullet format, and how to write the text are in [`DOC-STANDARDS.md`](../../../dev-docs/DOC-STANDARDS.md#the-changelog). This skill is the sequence. -Every pull request that changes source code needs one: bug fixes, new features, behaviour changes, deprecations, removals, security fixes, and new or changed language packs. +## Sequence -**Not required for:** documentation-only changes (guides, JSDoc, README), test-only changes, and CI or tooling changes. - -Also not required when the bug being fixed was introduced **and never released** — the regression never reached users, so there is nothing to document. - -## Where it goes - -`CHANGELOG.md`, under `## [Unreleased]`, in the section matching the change. The format is [Keep a Changelog](https://keepachangelog.com/en/1.0.0/); the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -Create the `###
` heading under `## [Unreleased]` if it does not exist yet. Section order follows the released blocks above. - -| Section | Use for | -|---|---| -| `### Added` | A wholly new capability — a function, an option, a language pack, an API method | -| `### Changed` | Modified behaviour of something that already existed | -| `### Fixed` | A bug fix | -| `### Deprecated` | Scheduled for removal | -| `### Removed` | Already removed in this release | -| `### Security` | A vulnerability fix | - -## The format - -One bullet per change, ending with a link to the pull request or the issue it resolves: - -```markdown -- Fixed the `MOD` function returning a remainder with the sign of the dividend instead of the sign of the divisor, which made the results differ from Excel and Google Sheets for arguments with opposite signs (e.g. `=MOD(-3, 12)` now returns `9` instead of `-3`). [#1747](https://github.com/handsontable/hyperformula/issues/1747) -``` - -- Link the **public GitHub issue** when one exists; otherwise link the pull request. -- The link needs the PR number, so write the entry after `gh pr create` returns the URL. - -## Writing the title - -- **From the user's perspective.** Describe what changed for someone using HyperFormula, not what you changed in the code. -- **Past tense, starting with the verb**: "Added…", "Fixed…", "Changed…", "Removed…". -- **Be specific.** "Fixed a bug" tells nobody anything. Name the function, the option, or the operation, and say what it does now. -- **Show the difference when a value changed.** `=MOD(-3, 12)` now returns `9` instead of `-3` is worth more than a paragraph of prose. -- **No internal identifiers.** No class names, no file paths, no private ticket contents. An identifier such as `HF-123` is fine on its own; what is in that ticket is not. -- **No client, customer, or partner names**, and nothing that identifies them indirectly. -- End with a period, then the link. - -## Breaking changes - -Say what breaks and what to do instead, in the entry itself. A breaking change also needs a migration-guide section — the changelog entry is not a substitute. - -## Checklist - -1. Confirm the change needs an entry at all. -2. Pick the section; create it under `## [Unreleased]` if absent. -3. Write one user-facing bullet, past tense, specific. -4. Append the issue link, or the PR link when there is no issue. -5. Confirm no sensitive or internal information appears in it. +1. **Decide whether an entry is required at all.** Documentation-only, test-only, and CI or tooling changes take none, and neither does a bug that was introduced and never released. +2. **Open the pull request first.** The entry carries a link, so it needs the number. Do not guess it — read it from the URL. See skill `pr-creation`. +3. **Write the bullet** under `## [Unreleased]` in `CHANGELOG.md`, in the section that matches the change, creating the `### ` heading if it is absent. +4. **Link the public issue** where one exists; otherwise the pull request. +5. **Re-read it as a user would.** If it names a class, a file, or an internal identifier, rewrite it. +6. **Check it carries nothing sensitive** — no client, customer, or partner names, and nothing that identifies them indirectly. See [`AGENTS.md`](../../../AGENTS.md#never-publish-sensitive-information). +7. **Commit and push to the same branch**, so the open pull request picks it up. diff --git a/.claude/skills/hyperformula-code-review/SKILL.md b/.claude/skills/hyperformula-code-review/SKILL.md index 6e9ccae742..a780cf0069 100644 --- a/.claude/skills/hyperformula-code-review/SKILL.md +++ b/.claude/skills/hyperformula-code-review/SKILL.md @@ -3,60 +3,38 @@ name: hyperformula-code-review description: Use when reviewing a diff, a branch, or a pull request in the HyperFormula repository. Covers correctness for a calculation engine, performance on the hot paths, the five places a function change must touch, API stability, and what the definition of done requires. --- -Review in this order. Stop at the first category that finds something serious and report it — do not bury a correctness bug under style notes. +Review in this order, and stop to report the first serious finding rather than burying it under style notes. The rules each check enforces are in [`dev-docs/`](../../../dev-docs/README.md); this skill is what to look at, in what order. ## 1. Correctness -- **Does the test actually fail without the fix?** Ask it of every bug-fix PR. A test added alongside a fix, written from the implementation, proves nothing. -- **Errors returned, never thrown.** Any `throw` reachable from formula evaluation takes down the whole recalculation. It must be a `CellError` with a message from `src/error-message.ts`. -- **Coercion through `ArithmeticHelper`.** Hand-rolled string-to-number or value-to-boolean conversion inside a function is a bug waiting for a locale or an empty cell. -- **Empty cells and empty ranges.** The most common gap in a function change. So are error arguments — an error must propagate, not be coerced. -- **Round-tripping.** A parser change without a matching `Unparser` change means `getCellFormula` returns something the user never typed. -- **Structural changes.** Adding or removing rows or columns must leave the address mapping, range mapping, and array mapping consistent. Assert the formula **text** afterwards, not just the value. -- **Undo.** A new mutation needs `CrudOperations` (validate), `Operations` (mutate), and `UndoRedo` (record). Missing the third diverges silently. +- **Would the test fail without the fix?** Ask it of every bug-fix pull request. See [`TESTING.md`](../../../dev-docs/TESTING.md#a-test-must-prove-behaviour). +- **Any `throw` reachable from evaluation**, instead of a returned `CellError`. +- **Hand-rolled coercion** instead of `ArithmeticHelper`. +- **Empty cells, empty ranges, and error arguments** — the most common gap in a function change. +- **A parser change without a matching `Unparser` change** — see [`PARSER.md`](../../../dev-docs/PARSER.md). +- **A structural change that does not assert the formula text afterwards** — see [`DEPENDENCY-GRAPH.md`](../../../dev-docs/DEPENDENCY-GRAPH.md). +- **A new mutation missing one of `CrudOperations`, `Operations`, `UndoRedo`** — undo diverges silently. -## 2. The five places a function change must touch +## 2. Completeness of a function change -Check all five; the failures are silent: - -1. the plugin implementation; -2. `implementedFunctions` metadata; -3. the catalogue entry in `src/interpreter/functionMetadata/categories/` — **parameter count must match**, or authored names and descriptions are discarded at run time with only a console warning; -4. **every** file in `src/i18n/languages/`; -5. tests. - -Also: a function that can return an array needs `sizeOfResultArrayMethod`. A function with a zero-argument or omitted-argument form needs an explicit `optionalArg: true` — nothing cross-checks optionality. +All five places, and three of them fail silently: implementation, catalogue entry with a matching parameter count, every language file, tests, changelog. Plus `sizeOfResultArrayMethod` for anything array-returning, and an explicit `optionalArg` where arity does not express the valid call. See skill `hyperformula-function-dev`. ## 3. Performance -The engine's performance is a feature. Flag: - -- allocation inside a per-cell or per-vertex loop; -- work that could be hoisted out of the broadcast path; -- a range expanded into per-cell edges or per-cell iteration where the range vertex would do; -- anything that widens what a change invalidates, forcing a larger recalculation; -- a change to `ParserWithCaching` that makes the parse result depend on something outside the cache key. - -Ask for `npm run test:performance` on changes to evaluation or CRUD hot paths. +Allocation in a per-cell or per-vertex loop; work that could be hoisted out of the broadcast path; a range expanded into per-cell iteration; anything that widens what a change invalidates; a `ParserWithCaching` change that makes the result depend on something outside the cache key. Ask for `npm run test:performance` on hot-path changes. See [`CODE-STYLE.md`](../../../dev-docs/CODE-STYLE.md#performance). ## 4. Public API -`src/HyperFormula.ts` and the types it exports are the contract. - -- A signature, return-type, or behaviour change is breaking. It needs a migration-guide section and an explicit note in the PR. -- JSDoc here is published output. Review it as documentation, not as a comment. -- A new config option needs a default, validation, and a guide entry. - -## 5. Definition of done +`src/HyperFormula.ts` and its exported types are the contract. A signature, return-type, or behaviour change is breaking and needs a migration-guide section and an explicit note. JSDoc here is published output — review it as documentation. -Production change, tests, documentation, JSDoc, changelog entry, current PR description. See [`dev-docs/DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md). +## 5. Process -One pull request, one atomic change. Unrelated refactors and reformatting belong in a separate PR — say so rather than approving them through. +[`DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md), and one atomic change per pull request. Say so when unrelated refactors have been folded in, rather than approving them through. -## 6. Style +## 6. Style, last and briefly -Last, and briefly. ESLint owns formatting. Comment only on what it cannot check: a name that misleads, a function doing two things, duplicated logic that an existing helper already covers, a clever one-liner where an obvious three lines would read better. +ESLint owns formatting. Comment only on what it cannot check: a misleading name, a function doing two things, duplicated logic an existing helper already covers. ## Reporting -One line per finding: what is wrong, where, and what to do instead. No praise, no summary of what the PR does — the author knows. Separate "this is a bug" from "I would have done this differently", and do not present the second as the first. +One line per finding: what is wrong, where, and what to do instead. No praise, no summary of what the pull request does. Separate "this is a bug" from "I would have done it differently", and never present the second as the first. diff --git a/.claude/skills/hyperformula-dev/SKILL.md b/.claude/skills/hyperformula-dev/SKILL.md index 1b25b161f3..06feb53993 100644 --- a/.claude/skills/hyperformula-dev/SKILL.md +++ b/.claude/skills/hyperformula-dev/SKILL.md @@ -11,57 +11,42 @@ description: > point for engine development — when in doubt, load it. --- -## Orient first +The rules live in [`dev-docs/`](../../../dev-docs/README.md). This skill is the order of operations. -The engine is a pipeline. Locate the stage before changing anything: +## 1. Locate the stage before changing anything -``` -setCellContents → CellContentParser → parser/ → GraphBuilder → DependencyGraph/ → Evaluator → interpreter/ → Serialization -``` +The engine is a pipeline: `CellContentParser` → `parser/` → `GraphBuilder` → `DependencyGraph/` → `Evaluator` → `interpreter/` → `Serialization`. | Symptom | Stage | Read | |---|---|---| -| The formula does not parse, or parses wrongly | `src/parser/` | `src/parser/AGENTS.md` | -| `getCellFormula` returns something the user did not type | `src/parser/Unparser.ts` | `src/parser/AGENTS.md` | -| A function returns the wrong value or the wrong error | `src/interpreter/plugin/` | skill `hyperformula-function-dev` | -| The value is right but stale after an edit | `src/DependencyGraph/`, `src/Evaluator.ts` | `src/DependencyGraph/AGENTS.md` | -| Wrong after adding/removing rows or columns | `src/dependencyTransformers/`, `src/LazilyTransformingAstService.ts` | `dev-docs/ARCHITECTURE.md` | +| Does not parse, or parses wrongly | `src/parser/` | [`PARSER.md`](../../../dev-docs/PARSER.md) | +| `getCellFormula` returns something the user never typed | `src/parser/Unparser.ts` | [`PARSER.md`](../../../dev-docs/PARSER.md) | +| A function returns the wrong value or error | `src/interpreter/plugin/` | skill `hyperformula-function-dev` | +| Value right, but stale after an edit | `src/DependencyGraph/`, `src/Evaluator.ts` | [`DEPENDENCY-GRAPH.md`](../../../dev-docs/DEPENDENCY-GRAPH.md) | +| Wrong after adding or removing rows or columns | `src/dependencyTransformers/`, `LazilyTransformingAstService.ts` | [`DEPENDENCY-GRAPH.md`](../../../dev-docs/DEPENDENCY-GRAPH.md) | | Wrong in one language only | `src/i18n/languages/` | skill `i18n-translations` | -| Coercion or comparison is wrong | `src/interpreter/ArithmeticHelper.ts` | `src/interpreter/AGENTS.md` | -| The public API behaves differently from its docs | `src/HyperFormula.ts` | `dev-docs/DOC-STANDARDS.md` | +| Coercion or comparison is wrong | `src/interpreter/ArithmeticHelper.ts` | [`INTERPRETER.md`](../../../dev-docs/INTERPRETER.md) | +| The public API disagrees with its docs | `src/HyperFormula.ts` | [`DOC-STANDARDS.md`](../../../dev-docs/DOC-STANDARDS.md) | -Use the `typescript-lsp` plugin to find a definition or its callers. Grep is for text, not for symbols. +A bug that looks like an interpreter problem is often a parser or graph problem. Confirm which before editing. -## Non-negotiables +Use the `typescript-lsp` plugin to find a definition or its callers; grep is for text, not symbols. -- **Headless.** No DOM, no network, no filesystem in `src/`. -- **Never throw from evaluation.** Return a `CellError` with a message from `src/error-message.ts`. A throw takes down the whole recalculation, not one cell. -- **Coerce through `ArithmeticHelper`.** Spreadsheet coercion is not JavaScript coercion, and it is already implemented once. -- **No English function names hard-coded** anywhere in the parser or interpreter. -- **Incremental recalculation.** Anything that forces a full recalculation is a regression. -- **JSDoc on `HyperFormula.ts` is published output.** Write it for the docs portal reader. +## 2. Reproduce first -## Workflow +Write the failing test before the fix and watch it fail — skill `test-writing-discipline`. For a calculation bug the smallest reproduction is a two-line `buildFromArray` plus one `getCellValue`. -1. **Reproduce first.** Write the failing test before the fix — see skill `test-writing-discipline`. For a calculation bug, the smallest reproduction is a two-line `buildFromArray` plus one `getCellValue`. -2. **Fetch the private test suite** if it is not present: `npm run test:setup-private`. Do this after every branch switch. -3. **Change the narrowest stage** that owns the behaviour. A bug that looks like an interpreter problem is often a parser or graph problem; confirm which before editing. -4. **Run the fast loop**: `npm run test:jest`. Narrow it with `npm run test:jest -- `. -5. **Complete the change**: tests, docs, changelog, translations. See [`dev-docs/DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md). -6. **Lint**: `npm run lint`. ESLint is the source of truth for style. +If `test/hyperformula-tests/` is absent, `npm run test:jest` runs only the smoke tests and reports a clean pass over almost nothing. Run `npm run test:setup-private` first, and after every branch switch. -## Performance +## 3. Change, then run the fast loop -The engine is a calculation engine; production-code performance is a feature. Before changing anything under `src/interpreter/`, `src/DependencyGraph/`, `src/Evaluator.ts`, `src/parser/ParserWithCaching.ts`, or `src/LazilyTransformingAstService.ts`, ask what runs per cell and what runs once. - -Run `npm run test:performance` for changes that touch evaluation or CRUD hot paths. It requires the private suite. +```bash +npm run test:jest -- +npm run lint +``` -## Common traps +## 4. Finish the change -- **The parser cache keys on the formula string.** If a parse result must depend on anything else, the key must include it — otherwise the cache returns the wrong AST. -- **Ranges are one vertex.** Expanding a range into per-cell edges destroys the performance the engine exists for. -- **`EmptyCellVertex` is not "no vertex".** An empty cell someone depends on still needs one. -- **A new mutation needs `CrudOperations` (validate), `Operations` (mutate), and `UndoRedo` (record).** Miss the third and undo silently diverges. -- **A new config option needs a default in `ConfigParams.ts`, validation in `Config.ts`, and a guide entry.** +Tests, documentation, JSDoc, changelog, translations: [`DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md). -Deep reference: [`dev-docs/ARCHITECTURE.md`](../../../dev-docs/ARCHITECTURE.md). +Before touching anything under `src/interpreter/`, `src/DependencyGraph/`, `src/Evaluator.ts`, `src/parser/ParserWithCaching.ts`, or `src/LazilyTransformingAstService.ts`, ask what runs per cell and what runs once — [`CODE-STYLE.md`](../../../dev-docs/CODE-STYLE.md#performance). Run `npm run test:performance` for changes to the evaluation or CRUD hot paths. diff --git a/.claude/skills/hyperformula-function-dev/SKILL.md b/.claude/skills/hyperformula-function-dev/SKILL.md index dba0599a1a..53af19c41f 100644 --- a/.claude/skills/hyperformula-function-dev/SKILL.md +++ b/.claude/skills/hyperformula-function-dev/SKILL.md @@ -4,100 +4,22 @@ paths: src/interpreter/** description: Use when adding a new built-in spreadsheet function to HyperFormula, changing an existing one's signature, arguments, return type, or error behaviour, or when a function returns the wrong value or the wrong error. Covers the FunctionPlugin contract, runFunction and argument metadata, the function metadata catalogue, translations, and the full end-to-end checklist. --- -## The five places a function lives +The plugin contract, `runFunction`, and every metadata field are documented in [`INTERPRETER.md`](../../../dev-docs/INTERPRETER.md#built-in-functions). Read it before writing code. This skill is the checklist. -A function is not done until all five agree. Skipping one produces a silent, specific failure: +## The five places, in order -| # | Place | Skipping it causes | -|---|---|---| -| 1 | `src/interpreter/plugin/Plugin.ts` — implementation | The function does not exist | -| 2 | `implementedFunctions` in the same file — engine metadata | The id does not resolve | -| 3 | `src/interpreter/functionMetadata/categories/.ts` — catalogue entry | `npm run docs:generate-function-docs` **fails the docs build** | -| 4 | `src/i18n/languages/*.ts` — every language | The function is unparseable in that language | -| 5 | `test/` — tests | The change is not done; see the definition of done | +A function is not done until all five agree. Each omission fails differently, and three of them fail silently. -## 1–2. The plugin +1. **Implementation** — the plugin class in `src/interpreter/plugin/`, plus its `implementedFunctions` entry keyed by the canonical English id. See [`INTERPRETER.md`](../../../dev-docs/INTERPRETER.md#built-in-functions). +2. **Catalogue entry** — `src/interpreter/functionMetadata/categories/.ts`. Parameter **count** must match `implementedFunctions`, or the authored names and descriptions are discarded at run time with only a console warning. A missing entry fails the docs build. See [`FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md). +3. **Translations** — every file in `src/i18n/languages/`. See [`I18N.md`](../../../dev-docs/I18N.md). +4. **Tests** — in `test/`. The list of what a function change must cover is in [`TESTING.md`](../../../dev-docs/TESTING.md#what-each-kind-of-change-needs). +5. **Changelog** — skill `changelog-creation`. -```ts -export class AbsPlugin extends FunctionPlugin implements FunctionPluginTypecheck { - public static implementedFunctions: ImplementedFunctions = { - 'ABS': { - method: 'abs', - parameters: [ - {argumentType: FunctionArgumentType.NUMBER} - ] - }, - } +## Two declarations nothing cross-checks - public abs(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - return this.runFunction(ast.args, state, this.metadata('ABS'), Math.abs) - } -} -``` - -The key is the **canonical English id**. `method` names the class method; `FunctionPluginTypecheck` turns a mismatch into a compile error. Put a new function in the existing plugin for its family; create a new plugin file only for a genuinely new family, and register it where the other plugins are registered. - -### `runFunction` already did the work - -Pass raw args, state, `this.metadata('ID')`, and a plain implementation. `runFunction` handles argument evaluation, arity checking, coercion to the declared `argumentType`, range vectorization, and broadcasting. - -**Write the implementation as if it received already-coerced scalars.** Re-checking types inside it is a sign the metadata is wrong. - -`runFunctionWithReferenceArgument` is the variant for reference-taking functions (`ROW`, `COLUMN`, `SHEET`, `ISFORMULA`); it takes a no-argument, a reference, and a non-reference callback. - -### Argument metadata - -| Field | Meaning | -|---|---| -| `argumentType` | `NUMBER`, `STRING`, `BOOLEAN`, `SCALAR`, `NOERROR`, `RANGE`, `ANY`, `INTEGER`, `COMPLEX` | -| `optionalArg` | May be omitted | -| `defaultValue` | Used when omitted | -| `minValue`, `maxValue`, `lessThan`, `greaterThan` | Enforced during coercion — use these instead of checking in the implementation | -| `passSubtype` | Keep the extended number subtype rather than unwrapping to a raw number | - -### Function-level metadata - -| Field | Set it when | -|---|---| -| `repeatLastArgs` | The function is variadic — how many trailing arguments repeat | -| `expandRanges` | Range arguments should be inlined into scalar arguments | -| `isVolatile` | It must recalculate every time (`RAND`, `NOW`) | -| `isDependentOnSheetStructureChange` | It must recalculate when rows or columns are added or removed | -| `doesNotNeedArgumentsToBeComputed` | Reference and range arguments create no dependency | -| `vectorizationForbidden` | It returns an array, or is otherwise special | -| `sizeOfResultArrayMethod` | **Required** for any function that can return an array | -| `returnNumberType` | The result is a percent, currency, date, or time | - -## 3. The catalogue entry - -Add the entry to `src/interpreter/functionMetadata/categories/.ts`: `shortDescription`, `parameters` with `snake_case` names and descriptions, `examples`, `documentationUrl`, `category`. Every field is required. - -Three ways this bites: - -- **No entry** → described as `category: 'Custom'`, which has no docs section, so the docs build **fails**. -- **Parameter count disagrees with `implementedFunctions`** → the implementation wins. Authored names and descriptions are discarded, positional `Arg1`, `Arg2` are reported, and a console warning names the function. Availability is unaffected, so nothing red appears in the test run. -- **Optionality is never cross-checked.** The `optional` flag comes only from `optionalArg` / `defaultValue`. If the function accepts a call arity alone does not express, declare `optionalArg: true` explicitly. - -Writing rules: refer to another argument by its exact `snake_case` name, no docs-page-local markup in `shortDescription`, and describe **HyperFormula's** behaviour rather than Excel's. Full detail: [`dev-docs/FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md). - -## 4. Translations - -Add the name to **every** file in `src/i18n/languages/`. Sources and rules: skill `i18n-translations`. - -## 5. Tests - -In `test/`. Cover, at minimum: - -- the documented result for ordinary arguments; -- each boundary the metadata declares (`minValue`, `maxValue`, `lessThan`, `greaterThan`); -- wrong argument **count** — too few and too many; -- wrong argument **type**, asserting the specific `CellError`; -- error **propagation**: an argument that is itself an error; -- an empty cell and an empty range as arguments; -- if the function returns an array: the spilled shape, and the size predicted by `sizeOfResultArrayMethod`; -- if `optionalArg` is declared: the call with the argument omitted. - -One assertion per case, no loops, no conditionals. See skill `hyperformula-unit-testing`. +- A function that can return an array needs `sizeOfResultArrayMethod`. +- A function whose valid call arity alone does not express — a zero-argument form, an omitted trailing argument — needs `optionalArg: true` declared explicitly, or the public API advertises the argument as required. ## Verify @@ -107,6 +29,6 @@ npm run docs:generate-function-docs # fails loudly on a bad or missing catalog npm run lint ``` -## Deviating from Excel +## When the behaviour differs from Excel -HyperFormula deliberately deviates in places. If the implementation does not match Excel or Google Sheets, that is a decision — record it in [`docs/guide/list-of-differences.md`](../../../docs/guide/list-of-differences.md) and say so in the changelog entry. Never write a description that documents Excel's behaviour while the code does something else. +That is a decision, not an accident. Record it in [`docs/guide/list-of-differences.md`](../../../docs/guide/list-of-differences.md) and say so in the changelog entry. Never write a description that documents Excel while the code does something else. diff --git a/.claude/skills/hyperformula-unit-testing/SKILL.md b/.claude/skills/hyperformula-unit-testing/SKILL.md index e9c863e2be..9d8b78fbd6 100644 --- a/.claude/skills/hyperformula-unit-testing/SKILL.md +++ b/.claude/skills/hyperformula-unit-testing/SKILL.md @@ -4,57 +4,27 @@ paths: test/** description: Use when writing or modifying tests for HyperFormula, or when a change to `src/` needs test coverage. Covers the two suites, fetching the private suite, how to build an engine in a test, and what a function or CRUD change must cover. --- -## Two suites +How to write a case, and what each kind of change must cover, are in [`TESTING.md`](../../../dev-docs/TESTING.md). What the two suites are and how to attach the private one is in [`test/README.md`](../../../test/README.md). This skill is the loop. -| Suite | Path | Availability | -|---|---|---| -| Smoke tests | `test/smoke.spec.ts` | In this repository | -| Full suite | `test/hyperformula-tests/` | Private repository, git-ignored | +## Before you trust a green run -**The private suite is branch-matched.** `npm run test:setup-private` checks out the branch of the same name, creating it from `develop` if it does not exist. Run it after every branch switch — otherwise the previous branch's tests run against the current source and the results mean nothing. +```bash +npm run test:setup-private +``` -In a fresh git worktree the directory is absent entirely, and `npm run test:jest` covers only the smoke tests. It looks like a clean pass. +Run it after every branch switch. Without `test/hyperformula-tests/` the Jest run covers only the smoke tests and reports a clean pass over almost nothing — the most common false signal in this repository. `test:performance` and `test:compatibility` fail on a missing path rather than an assertion; read the error before concluding the code is broken. -## Running +## The loop ```bash -npm run test:jest # fast loop npm run test:jest -- # one file or one describe npm run test:watch -npm run test # lint + jest + browser — the full local gate -``` - -`test:performance` and `test:compatibility` resolve into `test/hyperformula-tests/`. Without it they fail on a missing path, not on an assertion. Read the error before concluding the code is broken. - -## Shape of a case - -```ts -it('returns the divisor sign for arguments with opposite signs', () => { - const engine = HyperFormula.buildFromArray([['=MOD(-3, 12)']]) - - expect(engine.getCellValue(adr('A1'))).toBe(9) -}) +npm run test # lint + jest + browser, the full local gate ``` -- **One assertion per case.** Split rather than adding a second `expect`. -- **No loops, no conditionals.** A parameterised loop hides which input failed. Write the cases out. -- **Name the case after the behaviour**, not after the function: "returns `#VALUE!` when the range is empty", not "test SUMIFS". -- Build the smallest engine that exhibits the behaviour. A two-cell array beats a realistic sheet. - -## What to cover - -**A bug fix** ships a test that fails against the unfixed code. Write it first, run it, watch it fail, then fix. A test written after the fix proves nothing about the bug. - -**A function change** — see skill `hyperformula-function-dev` for the full list: ordinary arguments, each declared boundary, wrong argument count, wrong argument type with the specific `CellError`, error propagation, empty cell and empty range, array shape, omitted optional argument. - -**A CRUD or structural change** — add and remove rows and columns around a formula, move a range across a formula that references it, then assert **both** the recalculated value and the formula text afterwards. Structural bugs usually show up in the formula text first. - -**A parser change** — the parse, the round trip through `Unparser`, at least one non-English language, and malformed input that must produce a parsing error rather than a throw. - -**A config option** — the default, a valid non-default value, and an invalid value that must be rejected. - -## Never +## Order -- Never relax an assertion, widen a matcher, add `toBeCloseTo` where an exact value is expected, or skip a case to turn a run green. When a test is red the default assumption is that the **code** is wrong. See skill `test-writing-discipline`. -- Never test `docs/`, `examples/`, or `script/`. -- Never assert on internals a public API already exposes — use `getCellValue`, `getCellFormula`, `getSheetValues`. +1. Write the case from the requirement, not from the implementation. +2. Run it and watch it fail. A bug-fix test that has never failed proves nothing — skill `test-writing-discipline`. +3. Fix the code. +4. Run it again, and read the output rather than assuming it. diff --git a/.claude/skills/i18n-translations/SKILL.md b/.claude/skills/i18n-translations/SKILL.md index 1042704e39..46c3d3178c 100644 --- a/.claude/skills/i18n-translations/SKILL.md +++ b/.claude/skills/i18n-translations/SKILL.md @@ -4,37 +4,22 @@ paths: src/i18n/** description: Use when adding a built-in function that needs translated names, adding a language pack, or fixing a function name that is wrong in one language. Covers the translation sources, the rules, and what breaks when a key is missing. --- -## Why it matters - -Parsing is language-dependent: the lexer builds its token set from the active translation package. A function with a missing translation is not "untranslated" — it is **unparseable** in that language. +Why translations are engine input rather than decoration, the rules, and the table of sources to translate from are in [`I18N.md`](../../../dev-docs/I18N.md). This skill is the procedure. ## Adding a function name -Add the key to **every** file in `src/i18n/languages/`, in the same change as the function. There are 19 language packs; missing one is the common failure and nothing type-checks it in every path. - -Do not reorder existing entries while adding one — it turns a one-line diff into an unreviewable one. - -The key is the canonical English id, the same one used in `implementedFunctions` and in the metadata catalogue. Translations map onto that id; they never replace it. - -## Where to find a translation - -| Source | Use for | -|---|---| -| [Microsoft's Excel functions translator](https://support.microsoft.com/en-us/office/excel-functions-translator-f262d0c0-991c-485b-89b6-32cc8d326889) | Languages Excel supports | -| | Cross-check against the above | -| Google Sheets function list with `hl` set to the locale — e.g. for Indonesian | Languages Excel does not support | -| The English name | Functions Google Sheets does not list either — this matches what Excel does in unsupported locales | - -Never invent a translation, and never machine-translate a function name. A wrong name ships to every user of that language pack and cannot be changed without breaking their formulas. - -## Scope - -Translate the function **name** only. Argument separators, error literals, and boolean literals are also part of a language package; changing them is a language-pack decision, not a side effect of adding a function. +1. Look the name up in the sources listed in [`I18N.md`](../../../dev-docs/I18N.md#where-to-find-a-translation), in that order. **Never invent or machine-translate a function name** — a wrong one ships to every user of that language pack and cannot be changed without breaking their formulas. +2. Add the key to **every** file in `src/i18n/languages/`, in the same change as the function. Missing one is the usual failure, and nothing type-checks it on every path. +3. Do not reorder existing entries while adding one; it turns a one-line diff into an unreviewable one. +4. Add a test that parses a formula using the translated name and asserts the result, in that language. ## Adding a language pack -A new pack needs: the file in `src/i18n/languages/`, its export in `src/i18n/languages/index.ts`, a complete key set matching the other packs, and a changelog entry. `npm run bundle:languages` produces the standalone UMD build. +The file, its export in `src/i18n/languages/index.ts`, a key set identical to the other packs, a changelog entry, and `npm run bundle:languages` to produce the standalone UMD build. -## Tests +## Verify -A translation change needs a test that parses a formula using the translated name and asserts the result — in that language, not in English. +```bash +npm run test:jest -- i18n +npm run lint +``` diff --git a/.claude/skills/pr-creation/SKILL.md b/.claude/skills/pr-creation/SKILL.md index 02a6bd2e57..2dd8964ff0 100644 --- a/.claude/skills/pr-creation/SKILL.md +++ b/.claude/skills/pr-creation/SKILL.md @@ -1,62 +1,25 @@ --- name: pr-creation -description: Use before creating, pushing, opening, or updating a pull request in the HyperFormula repository — load this BEFORE running `gh pr create` or pushing a branch, not only when the user says "PR". Covers branch naming, the pre-flight gate, filling the PR template, and the changelog ordering. +description: Use before creating, pushing, opening, or updating a pull request in the HyperFormula repository — load this BEFORE running `gh pr create` or pushing a feature/docs/fix branch, not only when the user says "PR". Covers branch naming, the pre-flight lint/tests, the PR-then-changelog flow, and filling the GitHub PR template. --- -## Order of operations +Branch naming, the pre-flight gate, the template, and the scope rule are in [`PULL-REQUESTS.md`](../../../dev-docs/PULL-REQUESTS.md). Read it before pushing. This skill is the run order. -1. Commit the source change on a feature branch. -2. Run the pre-flight gate (below). -3. Push and run `gh pr create`. -4. Read the PR number from the URL it prints. -5. Add the changelog entry, linking that PR (or the public issue it fixes) — see skill `changelog-creation`. -6. Commit and push the changelog entry to the same branch. +## Run order -The changelog entry comes **after** the PR exists, because it carries the link. Do not guess the next PR number. +1. Commit on a correctly named feature branch. +2. Run the gate and **read the output**: + ```bash + npm run test:setup-private + npm run lint + npm run test:jest + ``` + A green Jest run without `test/hyperformula-tests/` covers only the smoke tests. Confirm the suite is attached before calling it green. +3. Push and open the pull request. Fill in every section of the template. +4. Read the pull request number from the URL. +5. Add the changelog entry with that link — skill `changelog-creation` — and push it to the same branch. +6. Confirm every item of [`DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md). -## Branch naming +## While the branch is open -`/-`, lowercase, hyphen-separated: - -``` -feat/hf-305-overwrite-flag -fix/hf-357-mod-divisor-sign -docs/hf-282-counts-guide -spike/hf-270-null-to-zero -``` - -Never put a client name, a customer report's wording, or anything from a private ticket in the branch name. The ticket identifier alone is fine. - -Never force-push to `master`, `develop`, or a branch that already has an open pull request. - -## Pre-flight gate - -```bash -npm run test:setup-private # after any branch switch -npm run lint -npm run test:jest -``` - -Run `npm run test` (adds the browser run) when the change touches bundling, module format, or anything browser-specific. Run `npm run docs:generate-function-docs` when the change touches a function or its catalogue entry — it fails on a bad or missing entry. - -Read the output. Do not open a pull request on a red run and describe it as ready. - -## The PR template - -`.github/pull_request_template.md` is filled in, not deleted: - -- **Context** — why the change is needed and what problem it solves. Written for a reviewer who has not seen the ticket. -- **How did you test your changes?** — the actual commands run and what they showed, not "added tests". -- **Types of changes** — tick every box that applies. Breaking change is its own box; tick it honestly. -- **Related issues** — `Fixes #...` for a public issue. A private ticket identifier may be named, its contents may not. -- **Checklist** — go through it. The OpenDocument, Excel, and Google Sheets compatibility boxes are real questions about the change, not formalities; if the behaviour deliberately deviates, say so in Context and record it in `docs/guide/list-of-differences.md`. - -## Keep the description current - -The most common failure in this repository is a description written once and never revisited. When the branch changes scope, update the description in the same push. - -## Definition of done - -Before requesting review, confirm every item in [`dev-docs/DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md): production change, tests, documentation, JSDoc, changelog entry, and a current PR description. One pull request holds one atomic change — unrelated refactors and clean-ups belong in their own. - -Read your own diff end to end before asking anyone else to. +Update the description in the same push whenever the scope changes. Read your own diff end to end before asking anyone else to. diff --git a/.claude/skills/test-writing-discipline/SKILL.md b/.claude/skills/test-writing-discipline/SKILL.md index 438e2baede..b576bcb23e 100644 --- a/.claude/skills/test-writing-discipline/SKILL.md +++ b/.claude/skills/test-writing-discipline/SKILL.md @@ -3,56 +3,19 @@ name: test-writing-discipline description: Use when writing, fixing, or reviewing any test for HyperFormula, and whenever a test is red during feature work. Enforces that tests prove intended behaviour rather than merely execute code, and never go "green for the sake of green". --- -## The rule +The rule, the banned ways of going green, and what a hollow assertion looks like are in [`TESTING.md`](../../../dev-docs/TESTING.md#a-test-must-prove-behaviour). Read that section now — it is short. -A test exists to prove that the code does what it is supposed to do. A test that passes without proving anything is worse than no test: it occupies the space where the real test would have gone, and it makes the next person believe the behaviour is covered. +## Apply it in this order -## Intent first +1. **Write the test from the requirement**, before reading the implementation. A test written from the code passes for any implementation, including the wrong one. +2. **Run it and watch it fail.** For a bug fix this is not optional: a test that has never failed proves nothing about the bug. +3. **Fix the code**, not the test. When a test is red the default assumption is that the code is wrong. Changing the expectation requires a one-sentence reason about the *specification*. +4. **Run it again and read the output.** Never claim a test passes because the reasoning is sound. -Write the test from the requirement, not from the implementation. For a bug fix that means: +## Before treating green as coverage -1. Write the test that describes the correct behaviour. -2. **Run it. Watch it fail.** A bug-fix test that has never failed proves nothing about the bug. -3. Fix the code. -4. Run it again. +If `test/hyperformula-tests/` is missing, `npm run test:jest` runs only the smoke tests and reports a clean pass over almost nothing. Confirm it is there — `npm run test:setup-private`. -Reading the implementation first and then writing a test that mirrors it produces a test that passes for any implementation, including the wrong one. +## If you cannot make it pass honestly -## When a test is red, the code is wrong - -That is the default assumption. Change the test only when you can state, in one sentence, why the test's expectation was wrong — and that sentence must be about the specification, not about the effort of fixing the code. - -## Banned ways of going green - -- Relaxing an assertion: exact value to `toBeCloseTo`, specific error to "some error", `toEqual` to `toContain`. -- Deleting the assertion that fails and keeping the ones that pass. -- Adding `.skip`, `.todo`, or a comment-out to a case that used to run. -- Widening a matcher until every implementation passes. -- Catching the error the code should not be throwing and asserting that it was caught. -- Mocking the unit under test, or mocking so deeply that only the mock is exercised. -- Asserting that a function "does not throw" when the requirement is a specific returned value. -- Changing the input until the current implementation happens to be right. - -If you find yourself doing any of these, stop and say so instead. "This test fails and I do not yet know why" is a useful report; a green run that hides it is not. - -## Hollow assertions - -These execute code and prove nothing: - -```ts -expect(engine.getCellValue(adr('A1'))).toBeDefined() // any value passes -expect(() => engine.setCellContents(...)).not.toThrow() // any non-throwing bug passes -expect(result).toBeTruthy() // 1, 'x', and [] all pass -``` - -Assert the value the specification names. - -## Verify with a real run - -Never claim a test passes without having run it. Never claim a fix works because the reasoning is sound. Run the command, read the output, and quote the decisive line. - -```bash -npm run test:jest -- -``` - -If the private suite is missing, `npm run test:jest` runs only the smoke tests and reports a clean pass over almost nothing. Confirm `test/hyperformula-tests/` exists before treating a green run as coverage — `npm run test:setup-private`. +Say so. "This test fails and I do not yet know why" is a useful report; a green run that hides it is not. diff --git a/.claude/skills/writing-docs-pages/SKILL.md b/.claude/skills/writing-docs-pages/SKILL.md index 59a679569c..bab83de3c3 100644 --- a/.claude/skills/writing-docs-pages/SKILL.md +++ b/.claude/skills/writing-docs-pages/SKILL.md @@ -4,46 +4,27 @@ paths: docs/** description: Use when creating or editing a page in the HyperFormula documentation portal, adding a guide, or updating the API reference. Covers what is generated versus hand-written, sidebar registration, running the portal, and the writing rules. --- -## Generated versus hand-written +What the portal contains, what is generated, and how to add a page are in [`docs/README.md`](../../../docs/README.md). The rules that govern documentation anywhere in the repository are in [`DOC-STANDARDS.md`](../../../dev-docs/DOC-STANDARDS.md). Writing style is [`DOCS_CONTENT_GUIDE.md`](../../../DOCS_CONTENT_GUIDE.md). -| Path | Source | Editable | -|---|---|---| -| `docs/guide/*.md` | Hand-written | yes | -| `docs/guide/built-in-functions.tmpl.md` | Hand-written template | yes | -| `docs/guide/built-in-functions.md` | Generated from the template + `src/interpreter/functionMetadata/` | **no — git-ignored** | -| `docs/api/` | Generated from JSDoc by TypeDoc | **no — git-ignored** | -| `docs/.vuepress/` | Configuration, theme, components | yes | -| `docs/examples/` | Code examples embedded in guides | yes | +## Change the source, not the output -To change what the functions page says about a function, edit its **catalogue entry** in `src/interpreter/functionMetadata/categories/`, then run `npm run docs:generate-function-docs`. +| To change | Edit | Then run | +|---|---|---| +| What the functions page says about a function | its catalogue entry in `src/interpreter/functionMetadata/categories/` | `npm run docs:generate-function-docs` | +| The API reference | the JSDoc in `src/` | `npm run typedoc:build-api` | +| A guide | the file in `docs/guide/` | `npm run docs:dev` | -To change the API reference, edit the **JSDoc** in `src/`, then run `npm run typedoc:build-api`. +`docs/guide/built-in-functions.md` and `docs/api/` are git-ignored build output. Editing them is always wrong, and the edit disappears on the next build. -## Running the portal +## Run it ```bash npm run bundle-all # the portal embeds the built engine npm run docs:dev # http://localhost:8080/hyperformula/ ``` -Both `docs:dev` and `docs:build` regenerate the functions page and the API reference first. - -## Adding a guide - -1. Create the file in `docs/guide/`. -2. **Register it in the sidebar** under `docs/.vuepress/`. A page without a sidebar entry builds successfully and is unreachable. -3. Link to the API reference for the detail rather than restating it. - -## Writing rules - -Follow [`DOCS_CONTENT_GUIDE.md`](../../../DOCS_CONTENT_GUIDE.md) for style, language, and structure. Beyond it: - -- **Do not duplicate the API reference.** It is the primary source of truth and holds the detail. A guide gives the overview and links. -- **Describe HyperFormula's behaviour, not Excel's.** HyperFormula deliberately deviates in places. Verify against the implementation, and record any deviation in [`docs/guide/list-of-differences.md`](../../../docs/guide/list-of-differences.md). -- Every code example must run against the current version. Examples live in `docs/examples/`; `docs/code-examples-generator.sh` derives the JavaScript variants from the TypeScript ones. - -## When docs are required - -Any public-API change updates the JSDoc **and** the affected guides in the same change. Any user-facing behaviour change is documented. Any breaking change adds a migration-guide section. Documentation-only changes need no changelog entry. +## Before you finish -See [`dev-docs/DOC-STANDARDS.md`](../../../dev-docs/DOC-STANDARDS.md). +- A new page needs a sidebar entry under `docs/.vuepress/`, or it builds and is unreachable. +- Link to the API reference for detail rather than restating it. +- Verify any behavioural claim against the implementation. Where HyperFormula deviates from Excel, record it in [`docs/guide/list-of-differences.md`](../../../docs/guide/list-of-differences.md). diff --git a/AGENTS.md b/AGENTS.md index 4301e32b6f..e6affa7325 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,45 +2,33 @@ Instructions for AI coding agents (Claude Code, Cursor, Codex, Aider, and any other AI tool) working in this repository. -This is the **repository-level** guide: rules that apply everywhere, plus a map of where to look next. Directory-specific rules live in that directory's own `AGENTS.md`, which loads automatically when you work there. +HyperFormula is a headless spreadsheet calculation engine in TypeScript. No UI, no DOM, no server: it parses formulas, tracks cell dependencies, and recalculates incrementally, in the browser and in Node. -In every directory, `CLAUDE.md` is a symlink to its sibling `AGENTS.md`. Edit `AGENTS.md` — the symlink keeps Claude Code and Cursor reading the same single source. +## Start at `dev-docs/` -## Overview +**[`dev-docs/`](dev-docs/) is the single source of truth for everything internal to this project.** Architecture, conventions, build, testing, standards, and the monorepo plan all live there and nowhere else. This file routes; it does not explain. Neither does any other `AGENTS.md` or `README.md` — if one of them looks like it is explaining something, the explanation belongs in `dev-docs/` and the file should link to it. -HyperFormula is a headless spreadsheet calculation engine in TypeScript. No UI, no DOM, no server: it parses formulas, tracks cell dependencies, and recalculates incrementally, in the browser and in Node. It ships as ES, CommonJS, and UMD bundles plus standalone language packs. +Read [`dev-docs/README.md`](dev-docs/README.md) first. It indexes the rest. -## Where to look - -Route to the lowest correct scope. `AGENTS.md` answers *"what must I never get wrong here, and where do I look next."* `dev-docs/` answers *"how does this work and why."* Skills answer *"how do I do task X."* `AGENTS.md` files load automatically in their subtree; `dev-docs/` files need an explicit read. - -| You are working on | Look here | +| You are working on | Read | |---|---| -| Anything repository-wide (build, release, workspace) | This file; [`dev-docs/`](dev-docs/) | -| Engine source, any subsystem | [`src/AGENTS.md`](src/AGENTS.md); [`dev-docs/ARCHITECTURE.md`](dev-docs/ARCHITECTURE.md) | -| Formula parsing | [`src/parser/AGENTS.md`](src/parser/AGENTS.md) | -| Formula evaluation | [`src/interpreter/AGENTS.md`](src/interpreter/AGENTS.md) | -| A built-in spreadsheet function | [`src/interpreter/plugin/AGENTS.md`](src/interpreter/plugin/AGENTS.md); skill `hyperformula-function-dev` | -| Function descriptions shown in the API and the docs | [`src/interpreter/functionMetadata/AGENTS.md`](src/interpreter/functionMetadata/AGENTS.md); [`dev-docs/FUNCTION-CATALOGUE.md`](dev-docs/FUNCTION-CATALOGUE.md) | -| Dependency tracking and recalculation order | [`src/DependencyGraph/AGENTS.md`](src/DependencyGraph/AGENTS.md) | -| Function-name translations | [`src/i18n/AGENTS.md`](src/i18n/AGENTS.md); [`dev-docs/I18N.md`](dev-docs/I18N.md) | -| Tests | [`test/AGENTS.md`](test/AGENTS.md); [`dev-docs/TESTING.md`](dev-docs/TESTING.md) | -| The documentation portal | [`docs/AGENTS.md`](docs/AGENTS.md); [`DOCS_CONTENT_GUIDE.md`](DOCS_CONTENT_GUIDE.md) | -| Build, docs-generation, and release scripts | [`script/AGENTS.md`](script/AGENTS.md) | -| Working inside a linked git worktree | [`dev-docs/WORKTREES.md`](dev-docs/WORKTREES.md) | +| Anything in `src/` | [`dev-docs/ARCHITECTURE.md`](dev-docs/ARCHITECTURE.md) | +| Formula parsing | [`dev-docs/PARSER.md`](dev-docs/PARSER.md) | +| Formula evaluation, or a built-in function | [`dev-docs/INTERPRETER.md`](dev-docs/INTERPRETER.md) | +| Function descriptions in the API and the docs | [`dev-docs/FUNCTION-CATALOGUE.md`](dev-docs/FUNCTION-CATALOGUE.md) | +| Dependency tracking and recalculation order | [`dev-docs/DEPENDENCY-GRAPH.md`](dev-docs/DEPENDENCY-GRAPH.md) | +| Function-name translations | [`dev-docs/I18N.md`](dev-docs/I18N.md) | +| Tests | [`dev-docs/TESTING.md`](dev-docs/TESTING.md) | +| The documentation portal | [`dev-docs/DOC-STANDARDS.md`](dev-docs/DOC-STANDARDS.md) | +| Building, bundling, releasing | [`dev-docs/BUILD.md`](dev-docs/BUILD.md) | +| What a change must include before review | [`dev-docs/DEFINITION-OF-DONE.md`](dev-docs/DEFINITION-OF-DONE.md) | +| Style and performance | [`dev-docs/CODE-STYLE.md`](dev-docs/CODE-STYLE.md) | +| A linked git worktree | [`dev-docs/WORKTREES.md`](dev-docs/WORKTREES.md) | +| How this repository is set up for agents | [`dev-docs/AGENT-TOOLING.md`](dev-docs/AGENT-TOOLING.md) | +| The monorepo migration | [`dev-docs/MONOREPO.md`](dev-docs/MONOREPO.md) | | Step-by-step task workflows | [`.claude/skills/`](.claude/skills/) | -`dev-docs/` reference index: [`dev-docs/README.md`](dev-docs/README.md). - -## Mandatory for every change - -1. **Tests.** Every change to `src/` ships tests in `test/`. A bug fix ships a test that fails against the unfixed code. See [`dev-docs/TESTING.md`](dev-docs/TESTING.md). -2. **Documentation.** A public-API, behaviour, or configuration change updates the JSDoc and the affected guides in the same change. A breaking change adds a migration-guide section. See [`dev-docs/DOC-STANDARDS.md`](dev-docs/DOC-STANDARDS.md). -3. **Changelog entry**, unless the change is documentation-only. -4. **Keep the pull request description current.** Update it as the branch evolves. -5. **Update `AGENTS.md`.** If the change introduces a convention, constraint, file location, or gotcha that future agents should know, record it in the `AGENTS.md` at the correct scope. - -The full list, and what "correct" means for each item, is in [`dev-docs/DEFINITION-OF-DONE.md`](dev-docs/DEFINITION-OF-DONE.md). +Each directory's own `AGENTS.md` points at the page that covers it, and loads automatically when you work there. ## Never publish sensitive information @@ -55,33 +43,9 @@ Describe the change on its own technical terms instead: write "fix an off-by-one If a change cannot be described without such information, stop and ask the user how to proceed. -## Build and test - -Node version is pinned in [`.nvmrc`](.nvmrc); install with `npm ci`. - -| Command | Runs | -|---|---| -| `npm run test:jest` | Jest — the fast loop | -| `npm run test` | Lint, Jest, and the browser run — the full local gate | -| `npm run test:setup-private` | Fetch the private test suite for the current branch | -| `npm run lint` | ESLint, the source of truth for formatting and code rules | -| `npm run bundle-all` | Every bundle, then verify them | - -The full command reference, with what each script leaves on disk, is in [`dev-docs/BUILD.md`](dev-docs/BUILD.md). - -**Run `npm run test:setup-private` after every branch switch.** The private suite in `test/hyperformula-tests/` is branch-matched; a stale checkout silently runs another branch's tests. +## Keep the documentation single-sourced -## Never edit generated or built output - -`lib/`, `es/`, `commonjs/`, `dist/`, `languages/`, `typings/`, `docs/api/`, and `docs/guide/built-in-functions.md` are all produced by a build step and git-ignored. Never edit them, never commit them, and never read them to answer a question about behaviour — read `src/` instead. - -## Other important resources - -- the repository [README.md](README.md) — high-level project description and quick install/usage -- the markdown files in [`docs/guide/`](docs/guide/) — user-facing guides (installation, configuration, built-in functions, custom functions, integrations, etc.) -- the markdown files in [`docs/api/`](docs/api/) — API reference (generated from JSDoc; run `npm run docs:build` if the folder is missing) - -Prefer reading these local files over fetching the rendered documentation from the web. +When a change introduces a convention, constraint, file location, or gotcha that future agents should know, record it in `dev-docs/`, on the page that owns the topic — not in an `AGENTS.md`, not in a `README.md`, and not in a skill. Those three link to it. ## Response style @@ -100,16 +64,5 @@ This section is maintained by the team. Whenever an AI agent makes a mistake wor - **Short title** — What the agent did wrong. What it should have done instead. --> -1. **Stale pull request descriptions** — The description was written once and never revisited. Update it as the branch evolves. - -## Skills, MCPs, and other agent tools - -This section is maintained by the team. Skills, MCP servers, and other tools vetted as useful for AI agents working on this codebase are listed here. - - - -- **`.claude/skills/`** — Repository skills, scoped by a `paths` glob in their frontmatter. `hyperformula-dev` is the entry point for engine work; `hyperformula-function-dev` for adding or changing a built-in function; `pr-creation` before opening a pull request. -- **`typescript-lsp` plugin** — Language-server-backed go-to-definition and find-references. Use it instead of grepping for a symbol's definition or callers; grep stays right for text searches. Enabled repository-wide in `.claude/settings.json`. -- **`PostToolUse` lint hook** — After every `Edit` or `Write`, [`script/claude/post-tool-use.mjs`](script/claude/post-tool-use.mjs) runs `eslint --fix` on that file and reports the remaining **errors** back to the agent. Fix them in the same turn. Warnings are deliberately not reported. See [`script/AGENTS.md`](script/AGENTS.md). +1. **Duplicating `dev-docs/`** — The agent explained a rule inside an `AGENTS.md`, a `README.md`, or a skill instead of linking to the `dev-docs/` page that owns it. Two copies of a rule means one of them is wrong within a release, and the reader cannot tell which. +2. **Stale pull request descriptions** — The description was written once and never revisited. Update it as the branch evolves. diff --git a/DEV_DOCS.md b/DEV_DOCS.md deleted file mode 100644 index 61ec4c8044..0000000000 --- a/DEV_DOCS.md +++ /dev/null @@ -1,21 +0,0 @@ -# Developer documentation - -The developer documentation now lives in **[`dev-docs/`](dev-docs/)**, split by topic. Start with [`dev-docs/README.md`](dev-docs/README.md). - -| I want to know | Read | -|---|---| -| What lives where in this repository | [`dev-docs/STRUCTURE.md`](dev-docs/STRUCTURE.md) | -| How the engine works | [`dev-docs/ARCHITECTURE.md`](dev-docs/ARCHITECTURE.md) | -| How to build, bundle, and lint | [`dev-docs/BUILD.md`](dev-docs/BUILD.md) | -| How to run and write tests | [`dev-docs/TESTING.md`](dev-docs/TESTING.md) | -| What a change must include before review | [`dev-docs/DEFINITION-OF-DONE.md`](dev-docs/DEFINITION-OF-DONE.md) | -| Code style and performance rules | [`dev-docs/CODE-STYLE.md`](dev-docs/CODE-STYLE.md) | -| Documentation rules | [`dev-docs/DOC-STANDARDS.md`](dev-docs/DOC-STANDARDS.md) | -| How function descriptions reach the API and the docs | [`dev-docs/FUNCTION-CATALOGUE.md`](dev-docs/FUNCTION-CATALOGUE.md) | -| How function-name translations work | [`dev-docs/I18N.md`](dev-docs/I18N.md) | -| How to work in a linked git worktree | [`dev-docs/WORKTREES.md`](dev-docs/WORKTREES.md) | -| Where the monorepo migration is going | [`dev-docs/MONOREPO.md`](dev-docs/MONOREPO.md) | - -Rules that AI agents must follow are in [`AGENTS.md`](AGENTS.md), and in the `AGENTS.md` file of each directory. - -External contributors: start with [`CONTRIBUTING.md`](CONTRIBUTING.md). diff --git a/dev-docs/AGENT-TOOLING.md b/dev-docs/AGENT-TOOLING.md new file mode 100644 index 0000000000..238ad444c1 --- /dev/null +++ b/dev-docs/AGENT-TOOLING.md @@ -0,0 +1,65 @@ +# Agent tooling + +How this repository is configured for AI coding agents. The rules an agent must follow are elsewhere — this page is about the machinery. + +## The three layers + +| Layer | Answers | Loaded | Rule | +|---|---|---|---| +| `AGENTS.md` | *What is this directory, and where do I look next?* | Always, within its subtree | A pointer of a few lines. Never a place to explain anything. | +| `dev-docs/` | *How does this work and why?* | On demand | **The single source of truth for internal knowledge.** | +| `.claude/skills/` | *How do I do task X?* | On skill trigger | Steps and ordering. Links to `dev-docs/` for the rules. | + +`CLAUDE.md` is a symlink to the sibling `AGENTS.md` in every directory that has one, so Claude Code and Cursor read the same file. + +**Nothing outside `dev-docs/` restates what is in `dev-docs/`.** An `AGENTS.md`, a `README.md`, or a `SKILL.md` carries only what is so specific to its own context that it would be useless anywhere else; everything else is a link. Two copies of a rule means one of them is wrong within a release, and the reader cannot tell which. + +## `.claude/settings.json` + +Committed, so every developer gets the same setup. + +| Key | Why | +|---|---| +| `enabledPlugins` | `typescript-lsp` — language-server go-to-definition and find-references. Use it instead of grepping for a symbol's definition or callers; grep stays right for text searches. | +| `permissions.deny` | Blocks agent reads of every generated and built path. The build outputs are git-ignored, so content searches already skip them, but nothing otherwise stops an agent opening `dist/hyperformula.js` or answering a behaviour question from `typings/` instead of `src/`. | +| `worktree.symlinkDirectories` | Symlinks `node_modules` into each worktree rather than duplicating it. See [`WORKTREES.md`](WORKTREES.md). | +| `hooks` | The `PostToolUse` lint hook below. | + +`node_modules/` and `package-lock.json` are deliberately readable: reading a dependency's source is sometimes the right move when debugging, and a deny rule would also block a targeted grep for a dependency version. + +Relative deny patterns anchor at the session's working directory, and project settings are not inherited from parent directories — these rules apply to sessions started at the repository root. + +## The `PostToolUse` lint hook + +[`script/claude/post-tool-use.mjs`](../script/claude/post-tool-use.mjs), matched on `Edit|Write`. Claude Code passes the tool payload as JSON on stdin; the hook reads `tool_input.file_path`, lints that one file, and exits 2 with the remaining errors on stderr, which Claude Code shows to the agent. ESLint rules are applied while the change is being written rather than when someone runs `npm run lint` at the end. + +Three properties to preserve when changing it: + +- **Errors only, never warnings.** `npm run lint` reports tens of thousands of warnings across the repository. Reporting them per edit would bury the agent in noise unrelated to the change it just made. +- **`--fix-type problem,layout`.** Never plain `--fix`. `jsdoc/require-jsdoc` is a suggestion-type rule whose autofix inserts an **empty** JSDoc block above every undocumented declaration, so an unconstrained `--fix` quietly scatters those stubs through any file the agent edits. The restriction keeps the fixes that are unambiguously right — the licence header, semicolons, quotes, spacing — and drops the ones that need a human. +- **Fails open.** A missing ESLint binary, a spawn failure, or unparseable output exits 0 silently. A broken hook must never block work. + +`script/` is in [`.eslintignore`](../.eslintignore), so the hook is not linted by itself. Verify a change to it by piping a payload in by hand: + +```bash +printf '{"tool_name":"Edit","cwd":"'"$PWD"'","tool_input":{"file_path":"src/interpreter/plugin/AbsPlugin.ts"}}' \ + | node script/claude/post-tool-use.mjs; echo "exit=$?" +``` + +## Skills + +All skills live in `.claude/skills/`, at the repository root, and are scoped by the `paths` frontmatter field rather than by placement — one glob, or a comma-separated list. One directory to look in, one directory to keep consistent. + +A skill holds the **steps**: what to do, in what order, and what to check. It does not restate the rules those steps enforce — it links to the `dev-docs/` page that owns them. + +| Skill | For | +|---|---| +| `hyperformula-dev` | Any work in `src/` — the entry point | +| `hyperformula-function-dev` | Adding or changing a built-in function | +| `hyperformula-unit-testing` | Writing or modifying tests | +| `test-writing-discipline` | Any red test, and any test that might be going green for the wrong reason | +| `i18n-translations` | Function-name translations | +| `writing-docs-pages` | The documentation portal | +| `changelog-creation` | The changelog entry | +| `pr-creation` | Opening or updating a pull request | +| `hyperformula-code-review` | Reviewing a diff, a branch, or a pull request | diff --git a/dev-docs/ARCHITECTURE.md b/dev-docs/ARCHITECTURE.md index 8c117f9f1e..3bb617ce39 100644 --- a/dev-docs/ARCHITECTURE.md +++ b/dev-docs/ARCHITECTURE.md @@ -79,7 +79,30 @@ HyperFormula is a headless spreadsheet calculation engine. No UI, no DOM, no ser ## Invariants -- **Headless.** No DOM, no network, no filesystem access in `src/`. -- **Incremental.** A change recalculates the affected subgraph, never the whole sheet. Anything that forces a full recalculation is a performance regression. -- **Language-dependent parsing.** Function names, argument separators, and error literals all vary by language. Never hard-code an English function name in the parser or interpreter. -- **Public API stability.** `src/HyperFormula.ts` is the contract. See [`DEFINITION-OF-DONE.md`](DEFINITION-OF-DONE.md) for what a breaking change requires. +Everything in `src/` ships, runs in the browser and in Node, and sits on the hot path of a calculation engine. These hold everywhere in it. + +- **Headless.** No DOM, no network, no filesystem. A dependency that reaches for `window`, `document`, or `fs` does not belong in `src/`. +- **Incremental.** A change recalculates the affected subgraph, never the whole sheet. Anything that forces a full recalculation is a performance regression, not an implementation detail. +- **Language-dependent parsing.** Function names, argument separators, and error literals all vary by language. Never hard-code an English function name in the parser or the interpreter. +- **Errors are values.** Return a `CellError` with a message from `src/error-message.ts`. A throw reachable from evaluation escapes one cell and takes the whole recalculation with it. +- **Coercion goes through `ArithmeticHelper`.** Spreadsheet coercion is not JavaScript coercion, and it is already implemented once. +- **Public API stability.** `src/HyperFormula.ts` and the types it exports are the contract. The JSDoc on it **is** the published API reference — write it for the reader of the docs portal. See [`DEFINITION-OF-DONE.md`](DEFINITION-OF-DONE.md) for what a breaking change requires. +- **Tests, always.** Every change here ships a test in `test/`; a bug fix ships one that fails against the unfixed code. See [`TESTING.md`](TESTING.md). + +## Subsystem references + +| Subsystem | Page | +|---|---| +| `src/parser/` | [`PARSER.md`](PARSER.md) | +| `src/interpreter/`, including `plugin/` | [`INTERPRETER.md`](INTERPRETER.md) | +| `src/interpreter/functionMetadata/` | [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md) | +| `src/DependencyGraph/` | [`DEPENDENCY-GRAPH.md`](DEPENDENCY-GRAPH.md) | +| `src/i18n/` | [`I18N.md`](I18N.md) | + +## Everything else in `src/` + +- `HyperFormula.ts` — the public API. Every method here is documented output. +- `CrudOperations.ts` / `Operations.ts` / `UndoRedo.ts` — CRUD validates, `Operations` mutates, `UndoRedo` records. A new mutation needs all three, or undo silently diverges. +- `Config.ts` / `ConfigParams.ts` — a new option needs a default, validation, and a guide entry. +- `dependencyTransformers/` — AST rewrites when rows, columns, or sheets move. Paired with `LazilyTransformingAstService.ts`, which defers them until a formula is read. +- `format/`, `helpers/`, `Lookup/`, `statistics/` — number and date formats, shared utilities, lookup strategies, instrumentation counters. diff --git a/dev-docs/BUILD.md b/dev-docs/BUILD.md index 67b9c95505..d4645d0d5f 100644 --- a/dev-docs/BUILD.md +++ b/dev-docs/BUILD.md @@ -1,62 +1,50 @@ -# Building, bundling, and linting +# Building — the maintainer's view -The user-facing version of this page is [`docs/guide/building.md`](../docs/guide/building.md). This file is the maintainer's view: what each script does and what it leaves on disk. +The command reference lives in the public guide, [`docs/guide/building.md`](../docs/guide/building.md): output formats, every `bundle:*` and `verify:*` script, the test commands, and the linter. Read that first. -## Prerequisites +This page holds only what the guide does not cover. -Node version is pinned in [`.nvmrc`](../.nvmrc). The repository uses npm and a committed `package-lock.json`. +## Install -```bash -npm ci -``` +Node version is pinned in [`.nvmrc`](../.nvmrc) — 22, the same everywhere in the repository. The repository uses npm with a committed `package-lock.json`, so install with `npm ci`, not `npm install`. -## Everyday commands +## The intermediate build -| Command | What it does | -|---|---| -| `npm run compile` | `tsc` into `lib/`. The input to every bundle. | -| `npm run bundle-all` | Clean, compile, produce every bundle, then verify them. | -| `npm run lint` | ESLint over `.js` and `.ts`. **The source of truth for formatting and code rules.** | -| `npm run lint:fix` | The same, with `--fix`. | -| `npm run test` | Lint, Jest, and the Karma browser run. | -| `npm run test:jest` | Jest only — the fast loop. | -| `npm run test:watch` | Jest in watch mode. | -| `npm run clean` | Remove every build output. | +`npm run compile` runs `tsc` into `lib/`. Every bundle reads `lib/`, not `src/`, so a bundle built without recompiling ships the previous source. -## Bundles +`npm run bundle-all` chains `clean`, `compile`, every `bundle:*`, then `verify-bundles`. The individual `bundle:*` scripts recompile first unless `HF_COMPILE=1` is set — `script/if-ne-env.js` is what skips the redundant recompile inside `bundle-all`. -`npm run bundle-all` runs `clean`, `compile`, every `bundle:*`, then `verify-bundles`. +`npm run clean` removes `coverage/`, `commonjs/`, `dist/`, `es/`, `languages/`, `lib/`, `typings/`, and `test-jasmine/`. -| Script | Output | -|---|---| -| `bundle:es` | `es/` — ES modules, `.mjs` extension | -| `bundle:cjs` | `commonjs/` — CommonJS modules | -| `bundle:development` | `dist/hyperformula.js`, `dist/hyperformula.full.js` | -| `bundle:production` | `dist/hyperformula.min.js`, `dist/hyperformula.full.min.js` | -| `bundle:languages` | `languages/` — standalone UMD language packs | -| `bundle:typings` | `typings/` — public `.d.ts` declarations | +`languages/` — the standalone UMD language packs — is a build output like the rest, and is missing from the guide's list of output formats. -`verify-bundles` runs every `verify:*` check in parallel: each `dist/` artifact and the `commonjs/` output must exist and be non-trivial (`script/check-file.js`), and `verify:typings` type-checks with `tsc --noEmit`. +## Generated documentation -Two bundle variants exist: the base build and the `.full` build. When a change can affect bundling, packaging, or module resolution, confirm both still work. +| File | Generated by | Rule | +|---|---|---| +| `docs/api/` | `npm run typedoc:build-api` | Never edit. Change the JSDoc in `src/`. | +| `docs/guide/built-in-functions.md` | `npm run docs:generate-function-docs`, from `built-in-functions.tmpl.md` and the metadata catalogue | Never edit, never commit. | -`verify:publish-package` runs `npm pack` through `script/check-publish-package.js` — use it when a change touches `package.json`, `.npmignore`, or the `exports`/`typings` surface. +Both are git-ignored, and both are regenerated as the first step of `docs:dev` and `docs:build`. `docs:generate-function-docs` is a gate, not a formatter: it fails the build on a missing catalogue entry or a `'Custom'` category. See [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md). -## Documentation +## Deploying the portal -| Command | What it does | -|---|---| -| `npm run docs:dev` | Generate the function docs and API reference, then serve the portal locally | -| `npm run docs:build` | The full production build of the portal | -| `npm run docs:generate-function-docs` | Regenerate `docs/guide/built-in-functions.md` from the function metadata catalogue | -| `npm run typedoc:build-api` | Regenerate `docs/api/` from JSDoc | +The portal is served by a Cloudflare Worker. -`docs/guide/built-in-functions.md` is generated and git-ignored — never commit it, and never edit it by hand. Its source is the metadata catalogue; see [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md). +| Command | Does | +|---|---| +| `npm run docs:build:cf` | Full portal build, then composes the Worker asset tree through `script/prepare-cf-assets.js` | +| `npm run docs:deploy:cf` | `wrangler deploy` — production | +| `npm run docs:preview:cf` | `wrangler versions upload` — a per-branch preview URL | -The portal is deployed as a Cloudflare Worker: `docs:build:cf` composes the assets through `script/prepare-cf-assets.js`, `docs:deploy:cf` and `docs:preview:cf` call `wrangler` against [`wrangler.jsonc`](../wrangler.jsonc). The Worker entry point is [`worker/index.js`](../worker/index.js). +Config is [`wrangler.jsonc`](../wrangler.jsonc); the Worker entry point is [`worker/index.js`](../worker/index.js). -## Release +## Release and licences -`npm run release` runs `script/release/release.sh`. Releasing is maintainer-owned; do not invent steps around it. +`npm run release` runs `script/release/release.sh`. Releasing is maintainer-owned — do not invent steps around it, and do not run it as part of another task. `npm run check:licenses` asserts that every production dependency carries a permissive licence. + +## Packaging + +`npm run verify:publish-package` runs `npm pack` through `script/check-publish-package.js` and checks what would actually ship. Run it whenever a change touches `package.json`, `.npmignore`, or the `exports`/`typings` surface. diff --git a/dev-docs/DEPENDENCY-GRAPH.md b/dev-docs/DEPENDENCY-GRAPH.md new file mode 100644 index 0000000000..58871ab28b --- /dev/null +++ b/dev-docs/DEPENDENCY-GRAPH.md @@ -0,0 +1,29 @@ +# The dependency graph + +`src/DependencyGraph/` is what makes recalculation incremental. Vertices are cells and ranges, edges are dependencies, and a topological sort gives the evaluation order. + +## The pieces + +| File | Role | +|---|---| +| `DependencyGraph.ts` | The facade the rest of the engine talks to. | +| `Graph.ts` | Vertices, edges, and dirty tracking. | +| `TopSort.ts` | Evaluation order, and cycle detection. | +| `AddressMapping/` | `SimpleCellAddress` to vertex. Several strategies, chosen by sheet density. | +| `RangeMapping.ts`, `RangeVertex.ts` | A range is **one** vertex, not one edge per cell. | +| `ArrayMapping.ts` | Array formulas and the cells they spill into. | +| `SheetMapping.ts` | Sheet ids and names. | +| `CellVertex.ts`, `ValueCellVertex.ts`, `FormulaVertex.ts`, `EmptyCellVertex.ts`, `ParsingErrorVertex.ts` | The vertex kinds. | +| `collectAddressesDependentToRange.ts` | Which addresses a range change invalidates. | + +## Rules + +- **Ranges stay collapsed.** Expanding a range into per-cell edges turns `SUM(A1:A100000)` into 100 000 edges and destroys the performance the engine exists for. If you need per-cell information, ask whether the range vertex can answer instead. +- **Every structural change must keep the mappings consistent.** Adding or removing a row moves addresses; the address mapping, the range mapping, and the array mapping all have to agree afterwards, or a later read resolves to the wrong vertex. Structural changes are paired with `src/dependencyTransformers/` and `src/LazilyTransformingAstService.ts`, which defers the AST rewrites until a formula is actually read. +- **Cycles are a value, not an exception.** `TopSort` detects them and the affected cells get a `CYCLE` error. Never let a cycle throw or loop. +- **`EmptyCellVertex` is not "no vertex".** An empty cell that something depends on still needs a vertex, or the dependency is lost when it is later filled. +- This is the hottest code in the engine after the interpreter — every read and every structural change goes through it. + +## Testing + +Structural-change tests are the ones that catch real bugs here: add and remove rows and columns around a formula, move a range across a formula that references it, and assert **both** the recalculated value and the formula text afterwards. Structural bugs show up in the formula text first. diff --git a/dev-docs/DOC-STANDARDS.md b/dev-docs/DOC-STANDARDS.md index 1debde80c7..429cc39946 100644 --- a/dev-docs/DOC-STANDARDS.md +++ b/dev-docs/DOC-STANDARDS.md @@ -27,3 +27,40 @@ Both are git-ignored. A missing `docs/api/` folder means it has not been built y ## Describing behaviour Describe **HyperFormula's** behaviour, not Excel's. HyperFormula deliberately deviates in places, and much of the existing prose was seeded from Excel documentation. Verify a claim against the implementation before writing it down, and record any deviation in [the list of differences](../docs/guide/list-of-differences.md). + +## The changelog + +`CHANGELOG.md` follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/); the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Entries go under `## [Unreleased]`, in the section matching the change — create the `### ` heading if it is not there yet. + +| Section | For | +|---|---| +| `Added` | A wholly new capability — a function, an option, a language pack, an API method | +| `Changed` | Modified behaviour of something that already existed | +| `Fixed` | A bug fix | +| `Deprecated` | Scheduled for removal | +| `Removed` | Already removed in this release | +| `Security` | A vulnerability fix | + +One bullet per change, ending with a link to the public issue where one exists, otherwise to the pull request: + +```markdown +- Fixed the `MOD` function returning a remainder with the sign of the dividend instead of the sign of the divisor, which made the results differ from Excel and Google Sheets for arguments with opposite signs (e.g. `=MOD(-3, 12)` now returns `9` instead of `-3`). [#1747](https://github.com/handsontable/hyperformula/issues/1747) +``` + +Writing the entry: + +- **From the user's perspective.** What changed for someone using HyperFormula, not what changed in the code. No class names, no file paths. +- **Past tense, verb first**: "Added…", "Fixed…", "Changed…", "Removed…". +- **Specific.** Name the function, option, or operation, and say what it does now. Show the difference when a value changed. +- **Nothing sensitive.** No client, customer, or partner names, and nothing that identifies them indirectly. +- End with a period, then the link. + +A breaking change says what breaks and what to do instead, and still needs a migration-guide section — the entry is not a substitute. + +**No entry is needed** for documentation-only, test-only, or CI and tooling changes, or for a bug that was introduced and never released. + +## The documentation portal + +`docs/` is a VuePress site. How to run it, what is generated, and how to add a page are in [`docs/README.md`](../docs/README.md). + +Two rules from above bear repeating there, because they are the ones most often broken while writing a guide: do not restate the API reference, and describe HyperFormula rather than Excel. diff --git a/dev-docs/FUNCTION-CATALOGUE.md b/dev-docs/FUNCTION-CATALOGUE.md index 6071c497dd..1d230413dc 100644 --- a/dev-docs/FUNCTION-CATALOGUE.md +++ b/dev-docs/FUNCTION-CATALOGUE.md @@ -48,3 +48,15 @@ It does **not** turn ordinary English into identifiers. A parameter's own descri ## Behaviour, not Excel Descriptions must describe **HyperFormula's** behaviour, not Excel's. Much of the catalogue was seeded from a hand-written page that documented Excel, and HyperFormula deliberately deviates in places (`INT` truncates toward zero, `ISEVEN`/`ISODD` do not truncate, `CEILING.MATH`/`FLOOR.MATH` honour only `mode` = 1). Verify a claim against the implementation before authoring it, and record any deviation in [the list of differences](../docs/guide/list-of-differences.md). + +## The rules, in short + +- **Keyed by function id, not by implementation.** Remove or rename a stale entry in the same change as the function. +- **Every field is required**, `documentationUrl` included. +- **Parameter count must match `implementedFunctions`**, or the authored names and descriptions are discarded at run time with only a console warning. +- **A missing entry fails the docs build.** +- **`'Custom'` must never appear** in a catalogue file or in `FUNCTION_CATEGORIES`. +- **Optionality is not cross-checked** — declare `optionalArg` explicitly on the plugin. +- **Name a different argument by its exact `snake_case` name.** +- **No docs-page-local markup in `shortDescription`.** +- **Describe HyperFormula, not Excel.** diff --git a/dev-docs/I18N.md b/dev-docs/I18N.md index efa3d98945..767014dc88 100644 --- a/dev-docs/I18N.md +++ b/dev-docs/I18N.md @@ -20,3 +20,11 @@ Parsing is language-dependent: the lexer builds its token set from the active tr - Translate the function **name** only. Argument separators, error literals, and boolean literals are part of the language package too; do not invent values for them. - Keep the key set identical across every language file. Adding a key to one file and not the rest is the common failure. - Do not reorder existing entries while adding one — it turns a one-line diff into an unreviewable one. + +## Adding a language pack + +A new pack needs the file in `src/i18n/languages/`, its export in `src/i18n/languages/index.ts`, a key set identical to the other packs, and a changelog entry. `npm run bundle:languages` produces the standalone UMD build. + +## Testing + +A translation change needs a test that parses a formula using the translated name and asserts the result — in that language, not in English. diff --git a/dev-docs/INTERPRETER.md b/dev-docs/INTERPRETER.md new file mode 100644 index 0000000000..d858760880 --- /dev/null +++ b/dev-docs/INTERPRETER.md @@ -0,0 +1,95 @@ +# The interpreter + +`src/interpreter/` evaluates an AST node against an `InterpreterState` and returns an `InterpreterValue`. + +## The pieces + +| File | Role | +|---|---| +| `Interpreter.ts` | Dispatches on AST node type. | +| `FunctionRegistry.ts` | Maps a function id to the plugin that implements it. Custom functions register here too, and may override a built-in id. | +| `InterpreterValue.ts` | The value types the engine passes around, including `CellError` and the extended number subtypes. | +| `InterpreterState.ts` | Evaluation context: the address being evaluated and the array-arithmetic flag. | +| `ArithmeticHelper.ts` | Coercion, comparison, and the arithmetic operators. | +| `Criterion.ts`, `CriterionFunctionCompute.ts` | The `*IF` / `*IFS` criterion machinery. | +| `binarySearch.ts` | Shared search used by the lookup functions. | +| `plugin/` | Every built-in function. | +| `functionMetadata/` | Their human-readable descriptions. See [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md). | + +## Rules + +- **Coerce through `ArithmeticHelper`.** Never write ad-hoc string-to-number or value-to-boolean conversion inside a function; the coercion rules are spreadsheet semantics, not JavaScript semantics, and they are already implemented once. +- **Errors are values.** Return a `CellError` with a message from `src/error-message.ts`. Do not throw: a thrown error escapes the evaluation of one cell and takes the recalculation with it. +- **The registry is keyed by id, not by implementation.** A custom plugin can be registered over a built-in id. Do not assume the plugin you are reading is the one that will answer for that id at run time. +- **This is the hot path.** `Interpreter.evaluateAst` runs once per formula, and once per cell for array-broadcast arguments. Allocation inside a per-cell loop is measurable. + +## Built-in functions + +Every built-in function lives in a plugin class under `src/interpreter/plugin/` extending `FunctionPlugin`. One file per plugin; a plugin holds one function or a family of related ones. + +```ts +export class AbsPlugin extends FunctionPlugin implements FunctionPluginTypecheck { + public static implementedFunctions: ImplementedFunctions = { + 'ABS': { + method: 'abs', + parameters: [ + {argumentType: FunctionArgumentType.NUMBER} + ] + }, + } + + public abs(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('ABS'), Math.abs) + } +} +``` + +`implementedFunctions` maps the **canonical English id** to its metadata — translated names never appear here. `method` names the class method that implements it, and `FunctionPluginTypecheck` makes a mismatch a compile error. + +### `runFunction` does the work + +Pass the raw args, the state, `this.metadata('ID')`, and a plain implementation function. `runFunction` handles argument evaluation, arity checking, coercion to the declared `argumentType`, range vectorization, and argument broadcasting. **Write the implementation as if it received already-coerced scalars** — re-checking types inside it is a sign the metadata is wrong. + +`runFunctionWithReferenceArgument` is the variant for functions that take a reference rather than a value (`ROW`, `COLUMN`, `SHEET`, `ISFORMULA`). It takes a no-argument, a reference, and a non-reference callback. + +### Argument metadata + +| Field | Meaning | +|---|---| +| `argumentType` | `NUMBER`, `STRING`, `BOOLEAN`, `SCALAR`, `NOERROR`, `RANGE`, `ANY`, `INTEGER`, `COMPLEX` | +| `optionalArg` | The argument may be omitted | +| `defaultValue` | Value used when omitted | +| `minValue`, `maxValue`, `lessThan`, `greaterThan` | Range constraints, enforced during coercion | +| `passSubtype` | Keep the extended number subtype instead of unwrapping to a raw number | + +### Function-level metadata + +| Field | Set it when | +|---|---| +| `repeatLastArgs` | The function is variadic — how many trailing arguments repeat | +| `expandRanges` | Range arguments should be inlined into scalar arguments | +| `isVolatile` | It must recalculate every time (`RAND`, `NOW`) | +| `isDependentOnSheetStructureChange` | It must recalculate when rows or columns are added or removed | +| `doesNotNeedArgumentsToBeComputed` | Reference and range arguments create no dependency | +| `vectorizationForbidden` | It returns an array, or is otherwise special | +| `sizeOfResultArrayMethod` | **Required** for any function that can return an array | +| `returnNumberType` | The result is a percent, currency, date, or time | + +### Never + +- **Never throw.** Return a `CellError` with a message from `src/error-message.ts`. +- **Never hand-roll coercion.** Declare the `argumentType` and let `runFunction` coerce, or use `ArithmeticHelper`. +- **Never describe Excel's behaviour when HyperFormula deviates.** Implement what is specified, then record the deviation in [`docs/guide/list-of-differences.md`](../docs/guide/list-of-differences.md). +- **Never allocate inside a per-cell loop** when the value can be hoisted. + +### The five places a function change must touch + +All five, or the failure is silent: + +1. the plugin implementation in `src/interpreter/plugin/`; +2. its `implementedFunctions` metadata; +3. the catalogue entry in `src/interpreter/functionMetadata/categories/` — see [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md); +4. **every** language file in `src/i18n/languages/` — see [`I18N.md`](I18N.md); +5. tests — see [`TESTING.md`](TESTING.md). + +Skill: `hyperformula-function-dev`. diff --git a/dev-docs/MONOREPO.md b/dev-docs/MONOREPO.md index d8a5a45350..80137a1400 100644 --- a/dev-docs/MONOREPO.md +++ b/dev-docs/MONOREPO.md @@ -1,22 +1,16 @@ # Target monorepo layout -This repository is becoming a monorepo (HF-359). This file is the target: what the tree looks like, which directories are workspace members, and what the migration has to solve. It is a proposal until the migration lands — the authoritative layout of the repository as it stands today is [`STRUCTURE.md`](STRUCTURE.md). +This repository is becoming a monorepo (HF-359). The layout it stands at today is [`STRUCTURE.md`](STRUCTURE.md). ## Packages | Package | Directory | Purpose | Published | |---|---|---|---| | `hyperformula` | `hyperformula/` | The calculation engine. Everything in `src/` and `test/` today. | yes | -| `@hfe/core` | `hyperformula-ui/packages/core/` | Formula editor UI: reference highlighting, inline editor, function help. | yes | -| `@hfe/handsontable-adapter` | `hyperformula-ui/packages/handsontable-adapter/` | Binds the editor to Handsontable. | yes | -| `@hfe/plain-table-adapter` | `hyperformula-ui/packages/plain-table-adapter/` | Binds the editor to a plain HTML table. | yes | -| `@hfe/tanstack-table-adapter` | `hyperformula-ui/packages/tanstack-table-adapter/` | Binds the editor to TanStack Table. | yes | +| `hyperformula-ui` | `hyperformula-ui/` | UI components for working with HyperFormula: reference highlighting, inline formula editor, function help. | yes | | `hyperformula-docs` | `docs/` | The VuePress documentation portal. | no | -| — | `hyperformula-ui/demo/`, `hyperformula-ui/e2e/` | Demo app and end-to-end tests for the UI packages. | no | -The HyperFormula agent skill stays in the shared `handsontable-skills` repository for now. Bringing it here is a separate decision, taken later and on its own terms; nothing in this layout depends on it. - -`docs/` is deliberately **not** a workspace member: the portal drags in a large, old dependency tree (VuePress, `--openssl-legacy-provider`) that must not reach an engine install. It keeps its own `package.json` and is installed separately. This mirrors how the Handsontable monorepo isolates its documentation site. +`docs/` is not a workspace member: the portal drags in a large, old dependency tree (VuePress, `--openssl-legacy-provider`) that must not reach an engine install. It keeps its own `package.json` and is installed separately. ## Tree @@ -24,86 +18,58 @@ The HyperFormula agent skill stays in the shared `handsontable-skills` repositor hyperformula/ # repository root — private, workspace root ├── AGENTS.md # monorepo-wide rules + routing map ├── CLAUDE.md -> AGENTS.md -├── DEV_DOCS.md # pointer into dev-docs/ ├── README.md CONTRIBUTING.md CHANGELOG.md LICENSE.txt ├── package.json # private: true, npm workspaces, fan-out scripts ├── package-lock.json +├── .nvmrc # 22, like every other one here ├── .worktreeinclude ├── .claude/ │ ├── settings.json # hooks, enabledPlugins, worktree settings │ └── skills/ # ALL skills, scoped by the `paths` frontmatter field ├── dev-docs/ # monorepo-scope reference -│ ├── README.md STRUCTURE.md BUILD.md TESTING.md -│ ├── DEFINITION-OF-DONE.md CODE-STYLE.md DOC-STANDARDS.md -│ ├── WORKTREES.md MONOREPO.md │ ├── hyperformula/ # ── package: the engine │ ├── AGENTS.md CLAUDE.md -> AGENTS.md -│ ├── package.json +│ ├── package.json .nvmrc CHANGELOG.md │ ├── dev-docs/ # engine-scope reference -│ │ ├── ARCHITECTURE.md FUNCTION-CATALOGUE.md I18N.md │ ├── src/ │ │ ├── AGENTS.md CLAUDE.md -> AGENTS.md -│ │ ├── HyperFormula.ts Config.ts CrudOperations.ts Evaluator.ts … │ │ ├── parser/ AGENTS.md │ │ ├── interpreter/ AGENTS.md │ │ │ ├── plugin/ AGENTS.md │ │ │ └── functionMetadata/ AGENTS.md │ │ ├── DependencyGraph/ AGENTS.md -│ │ ├── dependencyTransformers/ │ │ ├── i18n/languages/ AGENTS.md -│ │ ├── format/ helpers/ Lookup/ statistics/ -│ └── test/ AGENTS.md -│ ├── smoke.spec.ts fetch-tests.sh +│ │ └── dependencyTransformers/ format/ helpers/ Lookup/ statistics/ +│ └── test/ AGENTS.md README.md │ └── hyperformula-tests/ # private suite, git-ignored, branch-matched │ -├── hyperformula-ui/ # ── formula editor UI +├── hyperformula-ui/ # ── package: the UI components │ ├── AGENTS.md CLAUDE.md -> AGENTS.md +│ ├── package.json .nvmrc CHANGELOG.md │ ├── dev-docs/ -│ ├── packages/ -│ │ ├── core/ AGENTS.md -│ │ ├── handsontable-adapter/ AGENTS.md -│ │ ├── plain-table-adapter/ AGENTS.md -│ │ └── tanstack-table-adapter/ AGENTS.md -│ ├── demo/ -│ └── e2e/ +│ └── src/ test/ │ ├── docs/ # ── documentation portal (NOT a workspace member) -│ ├── AGENTS.md CLAUDE.md -> AGENTS.md -│ ├── package.json .nvmrc # .nvmrc says 22, like every other one here +│ ├── AGENTS.md CLAUDE.md -> AGENTS.md README.md +│ ├── package.json .nvmrc │ ├── wrangler.jsonc # deploy config for the portal │ ├── worker/index.js # Cloudflare Worker serving the built portal -│ ├── guide/ api/ .vuepress/ +│ └── guide/ api/ .vuepress/ │ ├── examples/ # images and CSV fixtures used by the docs -├── script/ AGENTS.md +├── script/ AGENTS.md README.md └── .github/workflows/ # path-filtered per-package jobs ``` -## Why this shape - -- **Flat, name-matched top-level directories.** A directory is named after the package it holds, so a path in a stack trace, a CI job name, and a changelog entry all say the same word. No `packages/` wrapper at the root — it adds a level that carries no information. -- **`hyperformula-ui/` keeps its own `packages/`** because it genuinely holds four published packages that share a source tree and a test setup. They still version independently, like every other package here. Its entry in the root `workspaces` array is `hyperformula-ui/packages/*`. -- **`AGENTS.md` travels with the code.** Every file listed above stays with its directory through the move, so the agent instructions survive the migration unchanged. -- **Every package versions and releases on its own cadence.** No lockstep version, no shared release train. Consequences to build in from the start: one `CHANGELOG.md` per package rather than one at the root, git tags namespaced by package (`hyperformula@3.5.0`, `@hfe/core@0.2.0`), and a release workflow parameterised by package instead of one that ships everything. Cross-package dependencies are declared as ordinary semver ranges, so the UI packages pin an engine range and are not forced to re-release when the engine does. -- **The Cloudflare Worker belongs to `docs/`.** `wrangler.jsonc` and `worker/index.js` exist only to serve the built portal, and `docs/` is leaving the workspace anyway. Moving them under `docs/` keeps the whole deployment path in one directory that installs and versions on its own, instead of leaving two root-level files whose only consumer lives elsewhere. `wrangler.jsonc`'s `main` and `script/prepare-cf-assets.js` change with them. -- **Every `.nvmrc` says `22`.** One Node version across the repository, including `docs/` once it has its own. A package that needs a different one is a problem to fix, not a version to pin around. -- **`CHANGELOG.md` keeps its current form** — Keep a Changelog, an `## [Unreleased]` block, one bullet per change with a link to the pull request or issue. One file per package rather than one at the root, and no fragment mechanism. Fragments solve merge conflicts this repository does not have yet; introducing them now would cost a tool, a CI gate, and a habit change for a problem that has not arrived. -- **One `.claude/skills/` at the root.** Skills are scoped by a `paths` glob in their frontmatter rather than by placement, so there is one place to look and one place to keep them consistent. - ## Migration steps -1. **Stay on npm.** The engine uses npm with a committed `package-lock.json`, and it keeps doing so: `workspaces` in the root `package.json` covers what this repository needs, and a package-manager migration is a risk the monorepo move does not need to carry at the same time. The cost lands on the way in — `hyperformula-ui` arrives as a pnpm workspace, so its `pnpm-workspace.yaml` and `pnpm-lock.yaml` are dropped and its package globs fold into the root `workspaces` array. Reconsider pnpm only if npm's hoisting turns out to break the wrapper packages. -2. **Move `src/` and `test/` into `hyperformula/`.** Mechanical, but it invalidates every path in CI, in `tsconfig.json`, in `jest.config.js`, in `karma.conf.js`, in `.eslintignore`, and in the docs generator scripts. +1. **Move `src/` and `test/` into `hyperformula/`.** Mechanical, but it invalidates every path in CI, in `tsconfig.json`, `jest.config.js`, `karma.conf.js`, `.eslintignore`, and the docs generator scripts. The private suite's specs need no change: they import the engine relatively, and the depth from a spec to the package root is unchanged. +2. **Add `workspaces` to the root `package.json`** and make it `private: true`. Move the build scripts down into `hyperformula/package.json`, leaving fan-out scripts at the root. 3. **Give `docs/` its own `package.json`** and take it out of the root dependency tree. -4. **Move the root build scripts down into `hyperformula/package.json`**, leaving fan-out scripts at the root. -5. **Bring in `hyperformula-ui`** from the formula-builder repository, preserving its history. It keeps the `@hfe/*` scope it publishes under today — moving the packages between repositories is already enough change for one migration, and a rename would break every existing consumer's imports for no benefit the move itself delivers. Revisit the scope as its own decision, not as a side effect of this one. -6. **Move `wrangler.jsonc` and `worker/` under `docs/`.** Update `wrangler.jsonc`'s `main`, the `docs:*:cf` scripts in `package.json`, and `script/prepare-cf-assets.js` in the same change. Verify with `npm run docs:preview:cf` before merging — a broken `main` path fails only at deploy time. -7. **Split `CHANGELOG.md` per package**, each keeping the current Keep a Changelog form. No fragment mechanism. -8. **Give every package an `.nvmrc` saying `22`**, `docs/` included. -9. **Keep the private test suite branch-matched.** `hyperformula-tests` stays keyed to this repository's branch name; only its checkout path moves, from `test/hyperformula-tests/` to `hyperformula/test/hyperformula-tests/`. Update `fetch-tests.sh`, the `test:setup-private` script, and `.gitignore` together, and re-check `.worktreeinclude`, which names the old path. -10. **Path-filter CI.** Each package's jobs run only when its paths change; full runs on `develop`, `master`, and release branches. - -## Open questions - -- Does `examples/` move under `docs/` as well? Every reference to it comes from a guide in `docs/guide/`, so it has no other consumer, but it is not part of the deployment path the way the Worker is. +4. **Move `wrangler.jsonc` and `worker/` under `docs/`.** Update `wrangler.jsonc`'s `main`, the `docs:*:cf` scripts, and `script/prepare-cf-assets.js` in the same change. Verify with `npm run docs:preview:cf` — a broken `main` path fails only at deploy time. +5. **Bring in `hyperformula-ui`**, preserving its history. +6. **Split `CHANGELOG.md` per package**, each keeping the current Keep a Changelog form. +7. **Give every package an `.nvmrc` saying `22`.** +8. **Update the private test suite's checkout path**, from `test/hyperformula-tests/` to `hyperformula/test/hyperformula-tests/`, in `fetch-tests.sh`, `.gitignore`, the three workflows that check it out, and `.worktreeinclude`. It stays branch-matched. +9. **Path-filter CI.** Each package's jobs run only when its paths change; full runs on `develop`, `master`, and release branches. diff --git a/dev-docs/PARSER.md b/dev-docs/PARSER.md new file mode 100644 index 0000000000..8826357559 --- /dev/null +++ b/dev-docs/PARSER.md @@ -0,0 +1,28 @@ +# The parser + +`src/parser/` turns a formula string into an AST plus its relative dependencies, and turns an AST back into a string. It wraps the [Chevrotain](https://chevrotain.io/) parser generator. + +## The pieces + +| File | Role | +|---|---| +| `ParserWithCaching.ts` | The entry point. Identical formula strings resolve from `Cache.ts` rather than being reparsed. | +| `FormulaParser.ts` | The Chevrotain grammar. | +| `LexerConfig.ts`, `ParserConfig.ts` | Build the token set from the active language package and config. | +| `Ast.ts` | AST node types. | +| `Unparser.ts` | AST back to formula text, in the target language. | +| `collectDependencies.ts` | Relative dependencies of an AST. | +| `Address.ts`, `CellAddress.ts`, `ColumnAddress.ts`, `RowAddress.ts` | Address representations and their absolute/relative flags. | +| `addressRepresentationConverters.ts` | A1 notation to and from the internal representation. | + +## Rules + +- **The lexer is language-dependent.** Function names, the argument separator, the decimal separator, and error literals all come from the config and the translation package. Never hard-code an English name or an English-locale separator. See [`I18N.md`](I18N.md). +- **Parsing and unparsing move together.** A grammar change that `Unparser.ts` does not learn about breaks round-tripping: the formula parses, the engine calculates, and `getCellFormula` returns something the user never typed. Add a round-trip test for every grammar change. +- **The cache is load-bearing.** `ParserWithCaching` keys on the formula string. A change that makes the parse result depend on something outside that key — the address, the sheet, mutable config — silently returns the wrong AST from the cache. If the result must vary, the key must vary. +- **Address arithmetic lives in the `*Address.ts` files.** Do not reimplement relative-to-absolute conversion at a call site. +- **A parse failure is a value.** It produces a `ParsingErrorVertex` in the dependency graph, not a thrown exception. One bad formula must not take down the engine. + +## Testing + +A grammar change needs cases for the happy parse, the round trip through `Unparser`, at least one non-English language, and the malformed input that must yield a parsing error rather than a throw. diff --git a/dev-docs/PULL-REQUESTS.md b/dev-docs/PULL-REQUESTS.md new file mode 100644 index 0000000000..c32207eaa3 --- /dev/null +++ b/dev-docs/PULL-REQUESTS.md @@ -0,0 +1,56 @@ +# Pull requests + +## Branch naming + +`/-`, lowercase, hyphen-separated: + +``` +feat/hf-305-overwrite-flag +fix/hf-357-mod-divisor-sign +docs/hf-282-counts-guide +spike/hf-270-null-to-zero +``` + +Never put a client name, a customer report's wording, or anything from a private ticket in a branch name. The ticket identifier alone is fine. + +Never force-push to `master`, `develop`, or a branch that already has an open pull request. + +## Order of operations + +1. Commit the source change on a feature branch. +2. Run the pre-flight gate below. +3. Push, and open the pull request. +4. Add the changelog entry, linking that pull request or the public issue it fixes — see [`DOC-STANDARDS.md`](DOC-STANDARDS.md#the-changelog). +5. Commit and push the entry to the same branch. + +The entry comes after the pull request exists, because it carries the link. Do not guess the number. + +## Pre-flight gate + +```bash +npm run test:setup-private # after any branch switch +npm run lint +npm run test:jest +``` + +Add `npm run test` (which includes the browser run) when the change touches bundling, module format, or anything browser-specific. Add `npm run docs:generate-function-docs` when it touches a function or its catalogue entry. + +Read the output. Do not open a pull request on a red run and describe it as ready. + +## The template + +[`.github/pull_request_template.md`](../.github/pull_request_template.md) is filled in, not deleted. + +- **Context** — why the change is needed, written for a reviewer who has not seen the ticket. +- **How did you test your changes?** — the commands actually run and what they showed, not "added tests". +- **Types of changes** — tick every box that applies, breaking change included, honestly. +- **Related issues** — `Fixes #...` for a public issue. A private ticket identifier may be named; its contents may not. +- **Checklist** — the OpenDocument, Excel, and Google Sheets boxes are real questions about the change. If the behaviour deliberately deviates, say so in Context and record it in [`docs/guide/list-of-differences.md`](../docs/guide/list-of-differences.md). + +## Scope and upkeep + +One pull request holds one atomic, self-contained change. Unrelated refactors, reformatting, and clean-ups belong in their own — including the ones that are obviously improvements. + +**Keep the description current.** A description written once and never revisited is the most common failure in this repository. When the branch changes scope, update it in the same push. + +Read your own diff end to end before asking anyone else to. Everything a change must contain is in [`DEFINITION-OF-DONE.md`](DEFINITION-OF-DONE.md). diff --git a/dev-docs/README.md b/dev-docs/README.md index e1ed7f2542..5dbf818023 100644 --- a/dev-docs/README.md +++ b/dev-docs/README.md @@ -1,37 +1,67 @@ -# `dev-docs/` — developer reference +# Developer documentation -Deep reference documentation for everyone working on HyperFormula: maintainers, the internal team, and the AI agents they run. Written for humans first; agents read the same files. +The canonical reference for everyone working on the HyperFormula source: maintainers, the internal team, and the AI agents they run. Written for humans first; agents read the same files. -These files are **loaded on demand** — linked from the always-loaded `AGENTS.md` files and from skills, not read on every turn. +Everything a developer needs to know lives here or is linked from here. External contributors start with [`CONTRIBUTING.md`](../CONTRIBUTING.md); users start with the [documentation portal](https://hyperformula.handsontable.com/docs). -## Three-layer model +## How the documentation is organised | Layer | Answers | Loaded | |---|---|---| -| `AGENTS.md` / `CLAUDE.md` | *What must I never get wrong here, and where do I look next?* Lean rules plus a navigation map for the directory it lives in. | Always, within its subtree | -| `dev-docs/` | *How does this work and why?* Architecture, conventions, standards, deep detail. | On demand | -| `.claude/skills/` | *How do I do task X?* Step-by-step workflows. | On skill trigger | +| `AGENTS.md` | *What is this directory, and where do I look next?* A pointer, nothing more. | Always, within its subtree | +| `dev-docs/` | *How does this work and why?* | On demand | +| `.claude/skills/` | *How do I do task X?* | On skill trigger | In every directory, `CLAUDE.md` is a symlink to its sibling `AGENTS.md`. Edit `AGENTS.md` — the symlink keeps Claude Code and Cursor reading the same single source. ## Index +### Orientation + | File | Contents | |---|---| | [`STRUCTURE.md`](STRUCTURE.md) | Repository tree and what lives where. | -| [`ARCHITECTURE.md`](ARCHITECTURE.md) | Engine architecture: parser, interpreter, dependency graph, and the data flow between them. | -| [`BUILD.md`](BUILD.md) | Every build, bundle, lint, and release command, with its output. | -| [`TESTING.md`](TESTING.md) | Test suites, the private test repository, and what a change must cover. | +| [`ARCHITECTURE.md`](ARCHITECTURE.md) | How the engine works: the pipeline, the core modules, and the invariants that hold everywhere in `src/`. | +| [`MONOREPO.md`](MONOREPO.md) | The monorepo layout this repository is moving to. | + +### Subsystems + +| File | Contents | +|---|---| +| [`PARSER.md`](PARSER.md) | `src/parser/` — formula text to AST, and back. | +| [`INTERPRETER.md`](INTERPRETER.md) | `src/interpreter/` — AST to value, and how a built-in function is written. | +| [`DEPENDENCY-GRAPH.md`](DEPENDENCY-GRAPH.md) | `src/DependencyGraph/` — dependency tracking and recalculation order. | +| [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md) | `src/interpreter/functionMetadata/` — what the API and the docs say about each function. | +| [`I18N.md`](I18N.md) | `src/i18n/` — function-name translations and where to source them. | + +### Working on a change + +| File | Contents | +|---|---| | [`DEFINITION-OF-DONE.md`](DEFINITION-OF-DONE.md) | What every change must include before review. | -| [`CODE-STYLE.md`](CODE-STYLE.md) | Code style and the performance rules that apply to engine code. | +| [`CODE-STYLE.md`](CODE-STYLE.md) | Code style, and the performance rules that apply to engine code. | +| [`TESTING.md`](TESTING.md) | Test suites, the private test repository, and how to write a case. | | [`DOC-STANDARDS.md`](DOC-STANDARDS.md) | Documentation rules across guides, API reference, JSDoc, and the changelog. | -| [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md) | The function metadata catalogue: what it feeds, and the two ways to get it wrong. | -| [`I18N.md`](I18N.md) | Function-name translations and where to source them. | +| [`BUILD.md`](BUILD.md) | What the public [building guide](../docs/guide/building.md) does not cover: the intermediate build, generated docs, deployment, and release. | +| [`PULL-REQUESTS.md`](PULL-REQUESTS.md) | Branch naming, the pre-flight gate, and the pull request template. | | [`WORKTREES.md`](WORKTREES.md) | Working in a linked git worktree: what is missing and how to bootstrap it. | -| [`MONOREPO.md`](MONOREPO.md) | The target monorepo layout and the migration it implies. | +| [`AGENT-TOOLING.md`](AGENT-TOOLING.md) | How this repository is configured for AI agents: settings, hooks, and skills. | + +### Elsewhere + +| Where | For | +|---|---| +| [`docs/README.md`](../docs/README.md) | Running and extending the documentation portal | +| [`test/README.md`](../test/README.md) | Attaching the private test suite | +| [`script/README.md`](../script/README.md) | What each build and release script does | +| [`DOCS_CONTENT_GUIDE.md`](../DOCS_CONTENT_GUIDE.md) | Writing style and structure for docs content | +| [`CHANGELOG.md`](../CHANGELOG.md) | Release history | +| [`.github/pull_request_template.md`](../.github/pull_request_template.md) | The pull request template | ## Conventions - Cross-references use repository-root-relative paths (for example `dev-docs/TESTING.md`), not filesystem-relative `../` paths. - Diagrams live only in `dev-docs/`, never in the always-loaded `AGENTS.md` files. - Public, user-facing documentation belongs in [`docs/`](../docs/), not here. `dev-docs/` never ships. +- **Nothing outside this directory restates what is in it.** `AGENTS.md`, `README.md`, and `SKILL.md` files carry only what is so specific to their own context that it would be useless anywhere else; everything else is a link. Two copies of a rule means one of them is wrong within a release, and the reader cannot tell which. +- `.ai/` exists only because some agents look for it. It contains one sentence pointing here. diff --git a/dev-docs/STRUCTURE.md b/dev-docs/STRUCTURE.md index 75bcd18280..3b66c0b3bc 100644 --- a/dev-docs/STRUCTURE.md +++ b/dev-docs/STRUCTURE.md @@ -25,6 +25,7 @@ The layout as it stands today. For the monorepo layout this repository is moving │ ├── Lookup/ # Lookup/search strategies used by lookup functions │ └── statistics/ # Instrumentation counters ├── test/ # Smoke tests; the full suite is fetched here +│ ├── README.md # How to attach the private suite │ ├── smoke.spec.ts # Public smoke tests │ ├── fetch-tests.sh # Clones/updates the private test repository │ └── hyperformula-tests/ # Private suite (git-ignored, branch-matched) @@ -40,7 +41,6 @@ The layout as it stands today. For the monorepo layout this repository is moving ├── .github/ # CI workflows, issue and PR templates ├── AGENTS.md # Always-loaded agent rules and routing map ├── CLAUDE.md # Symlink to AGENTS.md -├── DEV_DOCS.md # Pointer to dev-docs/ ├── CONTRIBUTING.md # Guide for external contributors ├── DOCS_CONTENT_GUIDE.md # Writing style and structure for docs content ├── README.md # Project overview diff --git a/dev-docs/TESTING.md b/dev-docs/TESTING.md index 99b3788664..9d6b9c47ca 100644 --- a/dev-docs/TESTING.md +++ b/dev-docs/TESTING.md @@ -11,13 +11,11 @@ The full suite is kept in a separate private repository and is **git-ignored** h ## Fetching the private suite -`test/hyperformula-tests` is **branch-matched to this repository**. [`test/fetch-tests.sh`](../test/fetch-tests.sh) checks out the branch with the same name, creating it from `develop` when it does not exist yet. - ```bash npm run test:setup-private ``` -Run it after every branch switch. Skipping it runs the previous branch's tests against the current source, which passes or fails for reasons that have nothing to do with the change under test. See [`test/README.md`](../test/README.md) for the environment variables it honours. +**Run it after every branch switch.** The suite is branch-matched, so skipping it runs the previous branch's tests against the current source: the results are meaningless, and they look like ordinary passes and failures. How the fetch works, and the environment variables it honours, are in [`test/README.md`](../test/README.md). In a fresh git worktree the directory is absent entirely — see [`WORKTREES.md`](WORKTREES.md). ## Running tests @@ -43,13 +41,68 @@ Run it after every branch switch. Skipping it runs the previous branch's tests a ## How to write a test case -- **One assertion per test case.** Each case is very simple and focused. +```ts +it('returns the divisor sign for arguments with opposite signs', () => { + const engine = HyperFormula.buildFromArray([['=MOD(-3, 12)']]) + + expect(engine.getCellValue(adr('A1'))).toBe(9) +}) +``` + +- **One assertion per test case.** Each case is very simple and focused. Split rather than adding a second `expect`. - **No control flow in a test case.** No loops, no conditionals. A parameterised loop hides which input failed; write the cases out. -- Name the case after the behaviour it pins, not after the function under test. -- A test must prove intended behaviour. Never relax an assertion, widen a matcher, or skip a case to turn a run green — if a test is red, the default assumption is that the code is wrong. +- Name the case after the behaviour it pins, not after the function under test: "returns `#VALUE!` when the range is empty", not "test SUMIFS". +- Build the smallest engine that exhibits the behaviour. A two-cell array beats a realistic sheet. +- Assert through the public API — `getCellValue`, `getCellFormula`, `getSheetValues` — not through internals. +- A test must prove intended behaviour. Never relax an assertion, widen a matcher, or skip a case to turn a run green — if a test is red, the default assumption is that the **code** is wrong. Before requesting a review, ask which further tests would be valuable and add the ones that protect against realistic regressions. +## What each kind of change needs + +| Change | Cover | +|---|---| +| A built-in function | Ordinary arguments; each declared boundary (`minValue`, `maxValue`, `lessThan`, `greaterThan`); too few and too many arguments; wrong argument type, asserting the specific `CellError`; an argument that is itself an error; an empty cell and an empty range; the spilled shape if it returns an array; the call with an omitted optional argument | +| CRUD or a structural change | Add and remove rows and columns around a formula, move a range across a formula that references it, then assert **both** the recalculated value and the formula text afterwards — structural bugs show up in the formula text first | +| A parser change | The parse, the round trip through `Unparser`, at least one non-English language, and malformed input that must produce a parsing error rather than a throw | +| A config option | The default, a valid non-default value, and an invalid value that must be rejected | +| A translation | A formula parsed using the translated name, asserted in that language | + +Skills: `hyperformula-unit-testing`, `test-writing-discipline`. + ## Performance HyperFormula is a calculation engine, so production-code performance is a feature. Run `npm run test:performance` for any change that can touch the evaluation or CRUD hot paths. See [`CODE-STYLE.md`](CODE-STYLE.md#performance). + +## A test must prove behaviour + +A test that passes without proving anything is worse than no test: it occupies the space where the real test would have gone, and it makes the next reader believe the behaviour is covered. + +Write the case from the requirement, not from the implementation. Reading the implementation first and then writing a test that mirrors it produces a test that passes for any implementation, including the wrong one. + +**When a test is red, the default assumption is that the code is wrong.** Change the test only when you can state, in one sentence, why its expectation was wrong — and that sentence must be about the specification, not about the effort of fixing the code. + +### Banned ways of going green + +- Relaxing an assertion: an exact value to `toBeCloseTo`, a specific error to "some error", `toEqual` to `toContain`. +- Deleting the assertion that fails and keeping the ones that pass. +- Adding `.skip` or `.todo`, or commenting out a case that used to run. +- Widening a matcher until every implementation passes. +- Catching the error the code should not be throwing, and asserting that it was caught. +- Mocking the unit under test, or mocking so deeply that only the mock is exercised. +- Asserting that a call "does not throw" when the requirement is a specific returned value. +- Changing the input until the current implementation happens to be right. + +"This test fails and I do not yet know why" is a useful report. A green run that hides it is not. + +### Hollow assertions + +These execute code and prove nothing. Assert the value the specification names. + +```ts +expect(engine.getCellValue(adr('A1'))).toBeDefined() // any value passes +expect(() => engine.setCellContents(...)).not.toThrow() // any non-throwing bug passes +expect(result).toBeTruthy() // 1, 'x', and [] all pass +``` + +Never claim a test passes without having run it, and never claim a fix works because the reasoning is sound. diff --git a/docs/AGENTS.md b/docs/AGENTS.md index ebc935ac93..d67d7c7ce6 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -1,49 +1,10 @@ -# `docs/` — the documentation portal +# `docs/` -A [VuePress](https://vuepress.vuejs.org/) site. Everything here is public, user-facing output. +The public VuePress documentation portal. -## Layout +- What this directory contains, what is generated, and how to run and extend it: [`README.md`](README.md) +- Documentation rules across the repository: [`dev-docs/DOC-STANDARDS.md`](../dev-docs/DOC-STANDARDS.md) +- Writing style and guide structure: [`DOCS_CONTENT_GUIDE.md`](../DOCS_CONTENT_GUIDE.md) +- Everything else: [`dev-docs/README.md`](../dev-docs/README.md) -| Path | What it is | Editable | -|---|---|---| -| `guide/` | Hand-written guides | yes | -| `guide/built-in-functions.tmpl.md` | Template for the generated functions page | yes | -| `guide/built-in-functions.md` | **Generated** from the template plus the function metadata catalogue | **no — git-ignored** | -| `api/` | **Generated** from JSDoc by TypeDoc | **no — git-ignored** | -| `.vuepress/` | Configuration, theme, components, sidebar | yes | -| `examples/` | Code examples embedded in guides | yes | -| `code-examples-generator.sh` | Generates the JavaScript variants of TypeScript examples | yes | - -## Never edit generated output - -`docs/api/` comes from the JSDoc in `src/` — fix the JSDoc, then run `npm run typedoc:build-api`. - -`docs/guide/built-in-functions.md` comes from `built-in-functions.tmpl.md` and the metadata catalogue in `src/interpreter/functionMetadata/` — fix the catalogue entry, then run `npm run docs:generate-function-docs`. It is git-ignored; never commit it. - -A missing `docs/api/` folder means it has not been built yet. Run `npm run docs:build`. - -## Writing - -Follow [`DOCS_CONTENT_GUIDE.md`](../DOCS_CONTENT_GUIDE.md) for style, language, and guide structure. The rules that apply beyond the portal — JSDoc, changelog, migration guides — are in [`dev-docs/DOC-STANDARDS.md`](../dev-docs/DOC-STANDARDS.md). - -Two rules worth repeating here: - -- **Do not duplicate the API reference.** It holds the detail and is the primary source of truth. A guide gives the high-level overview and **links** to the reference. -- **Describe HyperFormula's behaviour, not Excel's.** Verify against the implementation, and record deviations in [`guide/list-of-differences.md`](guide/list-of-differences.md). - -## Running it - -```bash -npm run bundle-all # the portal embeds the built engine -npm run docs:dev # http://localhost:8080/hyperformula/ -``` - -`docs:dev` and `docs:build` both regenerate the functions page and the API reference first. Details in [`README.md`](README.md) and [`dev-docs/BUILD.md`](../dev-docs/BUILD.md). - -## Adding a guide - -A new page needs a sidebar entry in `.vuepress/` as well as the file — otherwise it builds and is unreachable. - -## Deployment - -The portal is served by a Cloudflare Worker: `npm run docs:build:cf` composes the assets through `script/prepare-cf-assets.js`, then `docs:deploy:cf` or `docs:preview:cf` calls `wrangler`. The Worker entry point is [`worker/index.js`](../worker/index.js). +Skill: `writing-docs-pages`. diff --git a/docs/README.md b/docs/README.md index 924d37ea90..3a32cb2362 100644 --- a/docs/README.md +++ b/docs/README.md @@ -29,6 +29,20 @@ To start a local HyperFormula docs server: ``` 5. In your browser, go to: http://localhost:8080/hyperformula/. +## What is generated + +`guide/built-in-functions.md` and the whole of `api/` are produced by a build step and git-ignored. Never edit or commit them: change the function metadata catalogue in `src/interpreter/functionMetadata/categories/` or the JSDoc in `src/`, and regenerate. Both are regenerated as the first step of `docs:dev` and `docs:build`, so a missing `api/` folder only means it has not been built yet. + +Everything else here is hand-written: `guide/`, `guide/built-in-functions.tmpl.md`, `.vuepress/` (configuration, theme, sidebar), and `examples/`. `code-examples-generator.sh` derives the JavaScript variants of the TypeScript examples. + +Which files are generated by what, and why the generator fails the build rather than skipping: [`dev-docs/BUILD.md`](../dev-docs/BUILD.md) and [`dev-docs/FUNCTION-CATALOGUE.md`](../dev-docs/FUNCTION-CATALOGUE.md). + +## Adding a guide + +Create the file in `guide/`, then **register it in the sidebar** under `.vuepress/` — a page without a sidebar entry builds successfully and is unreachable. + +Writing style and guide structure: [`DOCS_CONTENT_GUIDE.md`](../DOCS_CONTENT_GUIDE.md). When documentation is required, what must not be duplicated, and how to describe behaviour that deviates from Excel: [`dev-docs/DOC-STANDARDS.md`](../dev-docs/DOC-STANDARDS.md). + ## HyperFormula documentation npm scripts From the `hyperformula` directory, you can run the following npm scripts: diff --git a/script/AGENTS.md b/script/AGENTS.md index 7671dc16e3..53638a139f 100644 --- a/script/AGENTS.md +++ b/script/AGENTS.md @@ -1,40 +1,7 @@ -# `script/` — build, docs, and release scripts +# `script/` -Node and shell scripts invoked from `package.json`. Not shipped, not tested. +Build, docs-generation, release, and agent-hook scripts invoked from `package.json`. -| Script | Invoked by | What it does | -|---|---|---| -| `check-file.js` | `verify:umd*`, `verify:cjs` | Asserts a build artifact exists and is not trivially small | -| `check-publish-package.js` | `verify:publish-package` | Reads `npm pack` output and checks what would ship | -| `generate-builtin-functions-doc.ts` | `docs:generate-function-docs` | Renders `docs/guide/built-in-functions.md` from the template and the metadata catalogue | -| `formatFunctionSyntax.ts`, `renderBuiltinFunctionsTable.ts` | the generator above | Formatting helpers for the generated page | -| `prepare-cf-assets.js` | `docs:build:cf` | Composes the Cloudflare Worker asset tree | -| `if-ne-env.js` | the `bundle:*` scripts | Skips a recompile when `HF_COMPILE` is already set | -| `release/` | `npm run release` | The release procedure | -| `claude/post-tool-use.mjs` | the Claude Code `PostToolUse` hook | Lints the file an agent just edited and reports the remaining ESLint errors back to it | - -## Rules - -- **The docs generator is a gate, not a formatter.** It fails the build on a missing catalogue entry or a `'Custom'` category. Keep it failing loudly — a silent skip publishes a built-in function with no description. -- **Release is maintainer-owned.** Do not invent steps around `script/release/`, and do not run it as part of another task. -- These scripts run in CI as well as locally. Do not assume an interactive terminal, a specific working directory, or a developer's environment variables. -- Nothing here is covered by tests. A change to a script is verified by running it. - -## `claude/` — agent-time hooks - -`post-tool-use.mjs` is wired to `Edit|Write` in [`.claude/settings.json`](../.claude/settings.json). Claude Code passes the tool payload as JSON on stdin; the script reads `tool_input.file_path`, runs `eslint --fix` on that one file, and exits 2 with the remaining errors on stderr, which Claude Code shows to the agent. - -Two properties to preserve when changing it: - -- **Errors only, never warnings.** `npm run lint` reports tens of thousands of warnings across the repository. Reporting them per edit would bury the agent in noise unrelated to the change it just made. -- **`--fix-type problem,layout`.** Never plain `--fix`. `jsdoc/require-jsdoc` is a suggestion-type rule whose autofix inserts an **empty** JSDoc block above every undocumented declaration, so an unconstrained `--fix` quietly scatters those stubs through any file the agent edits. The restriction keeps the fixes that are unambiguously right — the licence header, semicolons, quotes, spacing — and drops the ones that need a human. -- **Fails open.** A missing ESLint binary, a spawn failure, or unparseable output exits 0 silently. A broken hook must never block work. - -Verify a change to it by piping a payload in by hand: - -```bash -printf '{"tool_name":"Edit","cwd":"'"$PWD"'","tool_input":{"file_path":"src/interpreter/plugin/AbsPlugin.ts"}}' \ - | node script/claude/post-tool-use.mjs; echo "exit=$?" -``` - -`script/` is listed in [`.eslintignore`](../.eslintignore), so these files are not linted themselves. +- What each script does and the rules for changing one: [`README.md`](README.md) +- What the build produces: [`dev-docs/BUILD.md`](../dev-docs/BUILD.md) +- Everything else: [`dev-docs/README.md`](../dev-docs/README.md) diff --git a/script/README.md b/script/README.md new file mode 100644 index 0000000000..de0c721769 --- /dev/null +++ b/script/README.md @@ -0,0 +1,22 @@ +# `script/` + +Node and shell scripts invoked from `package.json`. Not shipped, not tested, not linted (`script/` is in [`.eslintignore`](../.eslintignore)). + +| Script | Invoked by | What it does | +|---|---|---| +| `check-file.js` | `verify:umd*`, `verify:cjs` | Asserts a build artifact exists and is not trivially small | +| `check-publish-package.js` | `verify:publish-package` | Reads `npm pack` output and checks what would ship | +| `generate-builtin-functions-doc.ts` | `docs:generate-function-docs` | Renders `docs/guide/built-in-functions.md` from `built-in-functions.tmpl.md` and the function metadata catalogue | +| `formatFunctionSyntax.ts`, `renderBuiltinFunctionsTable.ts` | the generator above | Formatting helpers for the generated page | +| `prepare-cf-assets.js` | `docs:build:cf` | Composes the Cloudflare Worker asset tree | +| `if-ne-env.js` | the `bundle:*` scripts | Skips a recompile when `HF_COMPILE` is already set | +| `release/` | `npm run release` | The release procedure | +| `claude/post-tool-use.mjs` | the Claude Code `PostToolUse` hook | Lints the file an agent just edited and reports the remaining ESLint errors back to it | + +## Before changing one + +- What the build produces and how these scripts fit into it: [`dev-docs/BUILD.md`](../dev-docs/BUILD.md) +- Why the docs generator fails loudly rather than skipping: [`dev-docs/FUNCTION-CATALOGUE.md`](../dev-docs/FUNCTION-CATALOGUE.md) +- The `claude/` hook's design constraints, and how to verify a change to it: [`dev-docs/AGENT-TOOLING.md`](../dev-docs/AGENT-TOOLING.md) + +Two things that are true only here: these scripts run in CI as well as locally, so they must not assume an interactive terminal, a working directory, or a developer's environment variables; and nothing here is covered by tests, so a change is verified by running it. diff --git a/src/AGENTS.md b/src/AGENTS.md index 836380d2eb..2761b1d61e 100644 --- a/src/AGENTS.md +++ b/src/AGENTS.md @@ -1,38 +1,10 @@ # `src/` — the engine -Everything here ships. It is TypeScript, it runs in the browser and in Node, and it is on the hot path of a calculation engine. +TypeScript, headless, on the hot path of a calculation engine. Everything here ships. -## Never +- Architecture, invariants, and what every file group is for: [`dev-docs/ARCHITECTURE.md`](../dev-docs/ARCHITECTURE.md) +- Style and performance rules: [`dev-docs/CODE-STYLE.md`](../dev-docs/CODE-STYLE.md) +- What a change must include: [`dev-docs/DEFINITION-OF-DONE.md`](../dev-docs/DEFINITION-OF-DONE.md) +- Everything else: [`dev-docs/README.md`](../dev-docs/README.md) -- **No DOM, no network, no filesystem.** The engine is headless. A dependency that reaches for `window`, `document`, or `fs` does not belong in `src/`. -- **No English function names hard-coded** in the parser or the interpreter. Function names are translated; the lexer builds its token set from the active language package. See [`i18n/AGENTS.md`](i18n/AGENTS.md). -- **No full recalculation.** A change recalculates the affected subgraph. Anything that forces the engine to walk every cell is a performance regression, not an implementation detail. -- **No breaking change to the public API** without a migration-guide section and an explicit note in the pull request. The public surface is `HyperFormula.ts` and the types it exports. - -## Always - -- JSDoc on every class and function. The JSDoc on `HyperFormula.ts` **is** the published API reference — write it for the reader of the docs portal, not for yourself. -- A test in [`test/`](../test/) for every change here. A bug fix ships a test that fails against the unfixed code. -- Coercion and comparison go through `interpreter/ArithmeticHelper.ts`, not hand-rolled inside a caller. -- Think about complexity in anything that runs per cell, per formula, or per dependency-graph node. See [`dev-docs/CODE-STYLE.md`](../dev-docs/CODE-STYLE.md#performance). - -## Where to look next - -| Subsystem | Directory | -|---|---| -| Formula text to AST, and back | [`parser/`](parser/AGENTS.md) | -| AST to value | [`interpreter/`](interpreter/AGENTS.md) | -| Built-in spreadsheet functions | [`interpreter/plugin/`](interpreter/plugin/AGENTS.md) | -| Function descriptions for the API and the docs | [`interpreter/functionMetadata/`](interpreter/functionMetadata/AGENTS.md) | -| Dependency tracking and recalculation order | [`DependencyGraph/`](DependencyGraph/AGENTS.md) | -| Function-name translations | [`i18n/`](i18n/AGENTS.md) | - -Deep reference: [`dev-docs/ARCHITECTURE.md`](../dev-docs/ARCHITECTURE.md). Skill: `hyperformula-dev`. - -## Files without their own `AGENTS.md` - -- `HyperFormula.ts` — the public API. Every method here is documented output. -- `CrudOperations.ts` / `Operations.ts` / `UndoRedo.ts` — CRUD validates, `Operations` mutates, `UndoRedo` records. A new mutation needs all three, or undo silently diverges. -- `Config.ts` / `ConfigParams.ts` — a new option needs a default, validation, and a guide entry. -- `dependencyTransformers/` — AST rewrites when rows, columns, or sheets move. Paired with `LazilyTransformingAstService.ts`, which defers them until a formula is read. -- `format/`, `helpers/`, `Lookup/`, `statistics/` — number and date formats, shared utilities, lookup strategies, instrumentation counters. +Skill: `hyperformula-dev`. diff --git a/src/DependencyGraph/AGENTS.md b/src/DependencyGraph/AGENTS.md index 82d3875c16..8d7fa7c03e 100644 --- a/src/DependencyGraph/AGENTS.md +++ b/src/DependencyGraph/AGENTS.md @@ -1,29 +1,6 @@ -# `src/DependencyGraph/` — what depends on what +# `src/DependencyGraph/` -The structure that makes recalculation incremental. Vertices are cells and ranges; edges are dependencies; a topological sort gives the evaluation order. +Cell and range dependency tracking, and the recalculation order. -## The pieces - -| File | Role | -|---|---| -| `DependencyGraph.ts` | The facade the rest of the engine talks to. | -| `Graph.ts` | Vertices, edges, and dirty tracking. | -| `TopSort.ts` | Evaluation order, and cycle detection. | -| `AddressMapping/` | `SimpleCellAddress` to vertex. Several strategies, chosen by sheet density. | -| `RangeMapping.ts`, `RangeVertex.ts` | A range is **one** vertex, not one edge per cell. | -| `ArrayMapping.ts` | Array formulas and the cells they spill into. | -| `SheetMapping.ts` | Sheet ids and names. | -| `CellVertex.ts`, `ValueCellVertex.ts`, `FormulaVertex.ts`, `EmptyCellVertex.ts`, `ParsingErrorVertex.ts` | The vertex kinds. | -| `collectAddressesDependentToRange.ts` | Which addresses a range change invalidates. | - -## Rules - -- **Ranges stay collapsed.** Expanding a range into per-cell edges turns a `SUM(A1:A100000)` into 100 000 edges and destroys the performance the engine exists for. If you need per-cell information, ask whether the range vertex can answer instead. -- **Every structural change must keep the mapping consistent.** Adding or removing a row moves addresses; the address mapping, the range mapping, and the array mapping all have to agree afterwards, or a later read resolves to the wrong vertex. Structural changes are paired with `dependencyTransformers/` and `LazilyTransformingAstService.ts`. -- **Cycles are a value, not an exception.** `TopSort` detects them and the affected cells get a `CYCLE` error. Never let a cycle throw or loop. -- **`EmptyCellVertex` is not "no vertex".** An empty cell that something depends on still needs a vertex, or the dependency is lost when it is later filled. -- This is the hottest code in the engine after the interpreter — every read and every structural change goes through it. - -## Testing - -Structural-change tests are the ones that catch real bugs here: add/remove rows and columns around a formula, move a range across the formula that references it, and assert both the recalculated value **and** the formula text afterwards. +- Reference and rules: [`dev-docs/DEPENDENCY-GRAPH.md`](../../dev-docs/DEPENDENCY-GRAPH.md) +- Everything else: [`dev-docs/README.md`](../../dev-docs/README.md) diff --git a/src/i18n/AGENTS.md b/src/i18n/AGENTS.md index a432b1a158..65c835311d 100644 --- a/src/i18n/AGENTS.md +++ b/src/i18n/AGENTS.md @@ -1,22 +1,8 @@ -# `src/i18n/` — function-name translations +# `src/i18n/` -One file per language under `languages/`, plus `TranslationPackage.ts`. Parsing is language-dependent: the lexer builds its token set from the active package, so these files are engine input, not decoration. +Function-name translations, one file per language. Engine input: the lexer builds its token set from these. -## Rules +- Reference, rules, and translation sources: [`dev-docs/I18N.md`](../../dev-docs/I18N.md) +- Everything else: [`dev-docs/README.md`](../../dev-docs/README.md) -- **A new function ships translations for every language in the same change.** A missing key leaves that function unparseable in that language. -- **Keep the key set identical across all language files.** Adding a key to one file and not the rest is the usual failure. -- **Do not reorder existing entries** while adding one. It turns a one-line diff into an unreviewable one. -- **Translate the name only.** Separators, error literals, and boolean literals are part of the package too — do not invent values for them. -- The canonical English id is what `implementedFunctions` and the metadata catalogue use. Translations map onto it; they never replace it. - -## Where to find a translation - -| Source | Use for | -|---|---| -| [Microsoft's Excel functions translator](https://support.microsoft.com/en-us/office/excel-functions-translator-f262d0c0-991c-485b-89b6-32cc8d326889) | Languages Excel supports | -| | Cross-check | -| Google Sheets function list with `hl` set to the locale, e.g. | Languages Excel does not support | -| The English name | Functions Google Sheets does not list either | - -Skill: `i18n-translations`. Reference: [`dev-docs/I18N.md`](../../dev-docs/I18N.md). +Skill: `i18n-translations`. diff --git a/src/interpreter/AGENTS.md b/src/interpreter/AGENTS.md index f4bda9ac50..b75aa68f32 100644 --- a/src/interpreter/AGENTS.md +++ b/src/interpreter/AGENTS.md @@ -1,26 +1,6 @@ -# `src/interpreter/` — AST to value +# `src/interpreter/` -Evaluates an AST node against an `InterpreterState` and returns an `InterpreterValue`. +AST to value. Owns the function registry and every built-in function. -## The pieces - -| File | Role | -|---|---| -| `Interpreter.ts` | Dispatches on AST node type. | -| `FunctionRegistry.ts` | Maps a function id to the plugin that implements it. Custom functions register here too, and may override a built-in id. | -| `InterpreterValue.ts` | The value types the engine passes around, including `CellError` and the extended number subtypes. | -| `InterpreterState.ts` | Evaluation context: the address being evaluated and the array-arithmetic flag. | -| `ArithmeticHelper.ts` | Coercion, comparison, and the arithmetic operators. | -| `Criterion.ts`, `CriterionFunctionCompute.ts` | The `*IF`/`*IFS` criterion machinery. | -| `binarySearch.ts` | Shared search used by the lookup functions. | -| `plugin/` | Every built-in function. See [`plugin/AGENTS.md`](plugin/AGENTS.md). | -| `functionMetadata/` | Human-readable descriptions of those functions. See [`functionMetadata/AGENTS.md`](functionMetadata/AGENTS.md). | - -## Rules - -- **Coerce through `ArithmeticHelper`.** Never write ad-hoc string-to-number or value-to-boolean conversion inside a function; the coercion rules are spreadsheet semantics, not JavaScript semantics, and they are already implemented once. -- **Errors are values.** Return a `CellError` with a message from `error-message.ts`. Do not throw: a thrown error escapes the evaluation of one cell and takes the recalculation with it. -- **The registry is keyed by id, not by implementation.** A custom plugin can be registered over a built-in id. Do not assume the plugin you are reading is the one that will answer for that id at run time. -- **This is the hot path.** `Interpreter.evaluateAst` runs once per formula, and once per cell for array-broadcast arguments. Allocation inside a per-cell loop is measurable. - -Deep reference: [`dev-docs/ARCHITECTURE.md`](../../dev-docs/ARCHITECTURE.md). +- Reference and rules: [`dev-docs/INTERPRETER.md`](../../dev-docs/INTERPRETER.md) +- Everything else: [`dev-docs/README.md`](../../dev-docs/README.md) diff --git a/src/interpreter/functionMetadata/AGENTS.md b/src/interpreter/functionMetadata/AGENTS.md index 6da48a0e62..eaee95f029 100644 --- a/src/interpreter/functionMetadata/AGENTS.md +++ b/src/interpreter/functionMetadata/AGENTS.md @@ -1,19 +1,6 @@ -# `src/interpreter/functionMetadata/` — what the docs and the API say about each function +# `src/interpreter/functionMetadata/` -The human-readable description of every built-in function: `shortDescription`, `parameters` with `snake_case` names and descriptions, `examples`, `documentationUrl`, and `category`. One file per category under `categories/`. +The human-readable description of every built-in function, read by the public API and by the generated docs page. -Two consumers read it: the public `getAvailableFunctions` / `getFunctionDetails` API, and the generated `docs/guide/built-in-functions.md` page. - -## Rules - -- **Keyed by function id, not by implementation.** An entry applies to whichever plugin currently provides that id — including a custom plugin registered over a built-in. An entry left behind after a rename describes nothing and still ships in the bundle; remove or rename it in the same change. -- **Every field is required**, `documentationUrl` included. -- **Parameter count must match `implementedFunctions`.** On a mismatch the implementation wins: authored names and descriptions are discarded, positional names (`Arg1`, `Arg2`) are reported, and a console warning names the function. The function stays listed. -- **A missing entry fails the docs build.** A registered function with no entry is described as `category: 'Custom'`, and `'Custom'` has no section on the generated page, so `npm run docs:generate-function-docs` fails rather than publishing a built-in with no description. -- **`'Custom'` must never appear in a catalogue file** or in `FUNCTION_CATEGORIES`. -- **Optionality is not cross-checked.** The `optional` flag comes entirely from `optionalArg` / `defaultValue` in the plugin. A description calling an argument optional next to `optional: false` fails nothing and misleads everyone. -- **Name parameters exactly.** When a description refers to a *different* argument, use its exact `snake_case` name — "shifts `start_date` by …", never "shifts the start date by …". A parameter's own description may still open with ordinary English for the thing it describes. -- **No docs-page-local markup in `shortDescription`** — no relative links, no footnote references. API consumers render these strings too. -- **Describe HyperFormula, not Excel.** Verify against the implementation, and record deviations in [`docs/guide/list-of-differences.md`](../../../docs/guide/list-of-differences.md). - -Full detail, including why each failure mode behaves the way it does: [`dev-docs/FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md). +- Reference and rules: [`dev-docs/FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md) +- Everything else: [`dev-docs/README.md`](../../../dev-docs/README.md) diff --git a/src/interpreter/plugin/AGENTS.md b/src/interpreter/plugin/AGENTS.md index 10ad981e4c..7569f00911 100644 --- a/src/interpreter/plugin/AGENTS.md +++ b/src/interpreter/plugin/AGENTS.md @@ -1,73 +1,8 @@ -# `src/interpreter/plugin/` — built-in functions +# `src/interpreter/plugin/` -Every built-in spreadsheet function lives in a plugin class extending `FunctionPlugin`. One file per plugin; a plugin holds one function or a family of related ones. +Every built-in spreadsheet function, as a plugin class extending `FunctionPlugin`. -## The shape +- The plugin contract, `runFunction`, argument metadata, and the five places a function change must touch: [`dev-docs/INTERPRETER.md`](../../../dev-docs/INTERPRETER.md#built-in-functions) +- Everything else: [`dev-docs/README.md`](../../../dev-docs/README.md) -```ts -export class AbsPlugin extends FunctionPlugin implements FunctionPluginTypecheck { - public static implementedFunctions: ImplementedFunctions = { - 'ABS': { - method: 'abs', - parameters: [ - {argumentType: FunctionArgumentType.NUMBER} - ] - }, - } - - public abs(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - return this.runFunction(ast.args, state, this.metadata('ABS'), Math.abs) - } -} -``` - -- `implementedFunctions` maps the **canonical English id** to its metadata. Translated names never appear here. -- `method` names the class method that implements it. `FunctionPluginTypecheck` makes a mismatch a compile error. -- The method takes `(ast: ProcedureAst, state: InterpreterState)` and delegates to `runFunction`. - -## `runFunction` does the work - -Pass the raw args, the state, `this.metadata('ID')`, and a plain implementation function. `runFunction` then handles argument evaluation, arity checking, coercion to the declared `argumentType`, range vectorization, and argument broadcasting. **Write the implementation as if it received already-coerced scalars** — do not re-check types inside it. - -`runFunctionWithReferenceArgument` is the variant for functions that take a reference rather than a value (`ROW`, `COLUMN`, `SHEET`, `ISFORMULA`). It takes three callbacks: no-argument, reference, and non-reference. - -## Argument metadata - -| Field | Meaning | -|---|---| -| `argumentType` | `NUMBER`, `STRING`, `BOOLEAN`, `SCALAR`, `NOERROR`, `RANGE`, `ANY`, `INTEGER`, `COMPLEX` | -| `optionalArg` | The argument may be omitted | -| `defaultValue` | Value used when omitted | -| `minValue`, `maxValue`, `lessThan`, `greaterThan` | Range constraints, enforced during coercion | -| `passSubtype` | Keep the extended number subtype instead of unwrapping to a raw number | - -Function-level metadata worth knowing: - -| Field | Meaning | -|---|---| -| `repeatLastArgs` | How many trailing arguments repeat indefinitely (variadic functions) | -| `expandRanges` | Inline range arguments into scalar arguments | -| `isVolatile` | Recalculate on every recalculation (`RAND`, `NOW`) | -| `isDependentOnSheetStructureChange` | Recalculate when rows or columns are added or removed | -| `doesNotNeedArgumentsToBeComputed` | Reference and range arguments create no dependency | -| `vectorizationForbidden` | Never vectorize — array-output and special functions | -| `sizeOfResultArrayMethod` | Names the method predicting the result array size; required for any function that can return an array | -| `returnNumberType` | Pack the returned number into this subtype (percent, currency, date, time) | - -## Never - -- **Never throw.** Return a `CellError` with a message from `error-message.ts`. A throw escapes one cell's evaluation and takes the whole recalculation with it. -- **Never hand-roll coercion.** Declare the `argumentType` and let `runFunction` coerce, or use `ArithmeticHelper`. -- **Never describe Excel's behaviour when HyperFormula deviates.** Implement what is specified, then record the deviation in [`docs/guide/list-of-differences.md`](../../../docs/guide/list-of-differences.md). -- **Never allocate inside a per-cell loop** when the value can be hoisted. These methods run once per cell for broadcast arguments. - -## Checklist for a new or changed function - -1. Implement or modify the plugin here, with `implementedFunctions` metadata. -2. Add or update the catalogue entry in [`../functionMetadata/categories/`](../functionMetadata/AGENTS.md) — parameter **count and names must match** `implementedFunctions`, or the authored descriptions are silently discarded. -3. Add translations for **every** language in [`../../i18n/languages/`](../../i18n/AGENTS.md). -4. Add tests in `test/`, including boundary values, wrong argument types, and error propagation. -5. If the function can return an array, declare `sizeOfResultArrayMethod` and implement it. -6. If it takes an optional or zero-argument form that arity alone does not express, declare `optionalArg: true` explicitly — nothing cross-checks this. - -Skill: `hyperformula-function-dev`. Reference: [`dev-docs/FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md). +Skill: `hyperformula-function-dev`. diff --git a/src/parser/AGENTS.md b/src/parser/AGENTS.md index d811e47477..72e6eb62c5 100644 --- a/src/parser/AGENTS.md +++ b/src/parser/AGENTS.md @@ -1,28 +1,6 @@ -# `src/parser/` — formula text to AST +# `src/parser/` -Wraps the [Chevrotain](https://chevrotain.io/) parser generator. Turns a formula string into an AST plus its relative dependencies, and turns an AST back into a string. +Formula text to AST, and back. Wraps the Chevrotain parser generator. -## The pieces - -| File | Role | -|---|---| -| `ParserWithCaching.ts` | The entry point. Identical formula strings resolve from `Cache.ts` rather than being reparsed. | -| `FormulaParser.ts` | The Chevrotain grammar. | -| `LexerConfig.ts`, `ParserConfig.ts` | Build the token set from the active language package and config. | -| `Ast.ts` | AST node types. | -| `Unparser.ts` | AST back to formula text, in the target language. | -| `collectDependencies.ts` | Relative dependencies of an AST. | -| `Address.ts`, `CellAddress.ts`, `ColumnAddress.ts`, `RowAddress.ts` | Address representations and their absolute/relative flags. | -| `addressRepresentationConverters.ts` | A1 notation to and from the internal representation. | - -## Rules - -- **The lexer is language-dependent.** Function names, the argument separator, the decimal separator, and error literals all come from the config and the translation package. Never hard-code an English name or an English-locale separator. -- **Parsing and unparsing move together.** A grammar change that `Unparser.ts` does not learn about breaks round-tripping: the formula parses, the engine calculates, and `getCellFormula` returns something the user never typed. Add a round-trip test for every grammar change. -- **The cache is load-bearing.** `ParserWithCaching` keys on the formula string. A change that makes the parse result depend on something outside that key — the address, the sheet, mutable config — silently returns the wrong AST from the cache. If the result must vary, the key must vary. -- Address arithmetic lives in the `*Address.ts` files. Do not reimplement relative-to-absolute conversion at a call site. -- A parse failure produces a `ParsingErrorVertex` in the graph, not a thrown exception. Keep it that way — one bad formula must not take down the engine. - -## Testing - -Grammar changes need cases for: the happy parse, the round trip through `Unparser`, at least one non-English language, and the malformed input that must yield a parsing error rather than a throw. +- Reference and rules: [`dev-docs/PARSER.md`](../../dev-docs/PARSER.md) +- Everything else: [`dev-docs/README.md`](../../dev-docs/README.md) diff --git a/test/AGENTS.md b/test/AGENTS.md index 39092c1507..649b7de598 100644 --- a/test/AGENTS.md +++ b/test/AGENTS.md @@ -1,39 +1,9 @@ -# `test/` — the test suite +# `test/` -Two suites live here. Only one of them is in this repository. +The smoke tests, plus the mount point for the private test suite. -| Suite | Path | Availability | -|---|---|---| -| Smoke tests | `smoke.spec.ts` | Everyone | -| Full suite | `hyperformula-tests/` | Private repository, git-ignored, fetched on demand | +- What this directory contains and how to attach the private suite: [`README.md`](README.md) +- How to run tests, how to write a case, and what a change must cover: [`dev-docs/TESTING.md`](../dev-docs/TESTING.md) +- Everything else: [`dev-docs/README.md`](../dev-docs/README.md) -## The private suite is branch-matched - -[`fetch-tests.sh`](fetch-tests.sh) checks out the branch of the **same name** in the private test repository, creating it from `develop` when it does not exist. - -```bash -npm run test:setup-private -``` - -**Run this after every branch switch.** Skipping it runs the previous branch's tests against the current source: the results are meaningless, and they look like ordinary passes and failures. In a fresh git worktree the directory is absent entirely — see [`dev-docs/WORKTREES.md`](../dev-docs/WORKTREES.md). - -`test:performance`, `test:compatibility`, and the benchmark scripts all resolve into `hyperformula-tests/`. Without it they fail with a missing path, not with a test failure. Read the error before concluding the code is broken. - -## What a change must cover - -Every change to `src/` ships tests here. A bug fix ships a test that **fails against the unfixed code** — write it first and watch it fail. A feature ships a set of tests precise enough to serve as its specification. - -Cover more than the happy path: boundary values, empty and invalid input, error results, and interaction with related features. - -`docs/`, `examples/`, and `script/` are not tested. - -## How to write a case - -- **One assertion per case.** Each case is very simple and focused. -- **No loops, no conditionals in a case.** A parameterised loop hides which input failed; write the cases out. -- Name the case after the behaviour it pins, not after the function under test. -- Never relax an assertion, widen a matcher, or skip a case to turn a run green. When a test is red the default assumption is that the **code** is wrong. - -External contributors put tests here; the internal team moves them into the private repository through a separate pull request. - -Commands and detail: [`dev-docs/TESTING.md`](../dev-docs/TESTING.md), [`README.md`](README.md). Skills: `hyperformula-unit-testing`, `test-writing-discipline`. +Skills: `hyperformula-unit-testing`, `test-writing-discipline`. From a49b8693d5bd674bea6940cc2e08a6d64c562312 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Mon, 31 Aug 2026 15:28:32 +0200 Subject: [PATCH 07/27] HF-359: Open every skill by naming the dev-docs pages it needs Step 1 of all nine skills is now "Read the relevant files from dev-docs/", followed by a table of the specific pages with one line each saying why that page matters for this task. Not a general pointer at the directory: a skill that says "the rules live in dev-docs" invites an agent to skip the reading and work from the steps alone, which is exactly the failure the single-source rule exists to prevent. Where the reference depends on what the task touches, the step lists the always-read pages first and the conditional ones after: hyperformula-dev reads ARCHITECTURE, CODE-STYLE, and DEFINITION-OF-DONE every time, then the page for the pipeline stage being changed. hyperformula-code-review does the same with DEFINITION-OF-DONE, CODE-STYLE, and TESTING. The remaining steps are renumbered to follow, so every skill now reads as one ordered procedure rather than a set of sections. The convention is recorded in dev-docs/AGENT-TOOLING.md, so the next skill written here inherits it. Co-Authored-By: Claude Opus 5 --- .claude/skills/changelog-creation/SKILL.md | 33 ++++++++--- .../skills/hyperformula-code-review/SKILL.md | 38 ++++++++----- .claude/skills/hyperformula-dev/SKILL.md | 57 ++++++++++++------- .../skills/hyperformula-function-dev/SKILL.md | 28 +++++---- .../skills/hyperformula-unit-testing/SKILL.md | 27 ++++++--- .claude/skills/i18n-translations/SKILL.md | 28 ++++++--- .claude/skills/pr-creation/SKILL.md | 51 +++++++++++------ .../skills/test-writing-discipline/SKILL.md | 25 +++++--- .claude/skills/writing-docs-pages/SKILL.md | 16 ++++-- dev-docs/AGENT-TOOLING.md | 2 + 10 files changed, 206 insertions(+), 99 deletions(-) diff --git a/.claude/skills/changelog-creation/SKILL.md b/.claude/skills/changelog-creation/SKILL.md index 3e7d6f349b..0d434b0470 100644 --- a/.claude/skills/changelog-creation/SKILL.md +++ b/.claude/skills/changelog-creation/SKILL.md @@ -3,14 +3,29 @@ name: changelog-creation description: Use when a change to source code needs a changelog entry, and before pushing any bug fix, feature, or behaviour change. Covers when an entry is required, which section it belongs in, how to write the title, and the link format. --- -Which section to use, the bullet format, and how to write the text are in [`DOC-STANDARDS.md`](../../../dev-docs/DOC-STANDARDS.md#the-changelog). This skill is the sequence. +## 1. Read the relevant files from `dev-docs/` -## Sequence +| File | Why | +|---|---| +| [`DOC-STANDARDS.md`](../../../dev-docs/DOC-STANDARDS.md#the-changelog) | Which section to use, the bullet format, and how to write the text | +| [`PULL-REQUESTS.md`](../../../dev-docs/PULL-REQUESTS.md#order-of-operations) | Why the entry comes after the pull request, not before | -1. **Decide whether an entry is required at all.** Documentation-only, test-only, and CI or tooling changes take none, and neither does a bug that was introduced and never released. -2. **Open the pull request first.** The entry carries a link, so it needs the number. Do not guess it — read it from the URL. See skill `pr-creation`. -3. **Write the bullet** under `## [Unreleased]` in `CHANGELOG.md`, in the section that matches the change, creating the `### ` heading if it is absent. -4. **Link the public issue** where one exists; otherwise the pull request. -5. **Re-read it as a user would.** If it names a class, a file, or an internal identifier, rewrite it. -6. **Check it carries nothing sensitive** — no client, customer, or partner names, and nothing that identifies them indirectly. See [`AGENTS.md`](../../../AGENTS.md#never-publish-sensitive-information). -7. **Commit and push to the same branch**, so the open pull request picks it up. +## 2. Decide whether an entry is required at all + +Documentation-only, test-only, and CI or tooling changes take none, and neither does a bug that was introduced and never released. + +## 3. Open the pull request first + +The entry carries a link, so it needs the number. Do not guess it — read it from the URL. Skill `pr-creation`. + +## 4. Write the bullet + +Under `## [Unreleased]` in `CHANGELOG.md`, in the section that matches the change, creating the `### ` heading if it is absent. Link the public issue where one exists; otherwise the pull request. + +## 5. Re-read it as a user would + +If it names a class, a file, or an internal identifier, rewrite it. Check it carries nothing sensitive — no client, customer, or partner names, and nothing that identifies them indirectly. See [`AGENTS.md`](../../../AGENTS.md#never-publish-sensitive-information). + +## 6. Push it to the same branch + +So the open pull request picks it up. diff --git a/.claude/skills/hyperformula-code-review/SKILL.md b/.claude/skills/hyperformula-code-review/SKILL.md index a780cf0069..acffff47d1 100644 --- a/.claude/skills/hyperformula-code-review/SKILL.md +++ b/.claude/skills/hyperformula-code-review/SKILL.md @@ -3,35 +3,47 @@ name: hyperformula-code-review description: Use when reviewing a diff, a branch, or a pull request in the HyperFormula repository. Covers correctness for a calculation engine, performance on the hot paths, the five places a function change must touch, API stability, and what the definition of done requires. --- -Review in this order, and stop to report the first serious finding rather than burying it under style notes. The rules each check enforces are in [`dev-docs/`](../../../dev-docs/README.md); this skill is what to look at, in what order. +## 1. Read the relevant files from `dev-docs/` -## 1. Correctness +Always: -- **Would the test fail without the fix?** Ask it of every bug-fix pull request. See [`TESTING.md`](../../../dev-docs/TESTING.md#a-test-must-prove-behaviour). +| File | Why | +|---|---| +| [`DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md) | What the change was required to contain | +| [`CODE-STYLE.md`](../../../dev-docs/CODE-STYLE.md) | Style, and which paths are hot enough that complexity matters | +| [`TESTING.md`](../../../dev-docs/TESTING.md#a-test-must-prove-behaviour) | Whether the tests prove anything, or only execute code | + +Then the page covering what the diff touches: [`ARCHITECTURE.md`](../../../dev-docs/ARCHITECTURE.md), [`PARSER.md`](../../../dev-docs/PARSER.md), [`INTERPRETER.md`](../../../dev-docs/INTERPRETER.md), [`DEPENDENCY-GRAPH.md`](../../../dev-docs/DEPENDENCY-GRAPH.md), [`FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md), [`I18N.md`](../../../dev-docs/I18N.md). + +Review in the order below, and stop to report the first serious finding rather than burying it under style notes. + +## 2. Correctness + +- **Would the test fail without the fix?** Ask it of every bug-fix pull request. - **Any `throw` reachable from evaluation**, instead of a returned `CellError`. - **Hand-rolled coercion** instead of `ArithmeticHelper`. - **Empty cells, empty ranges, and error arguments** — the most common gap in a function change. -- **A parser change without a matching `Unparser` change** — see [`PARSER.md`](../../../dev-docs/PARSER.md). -- **A structural change that does not assert the formula text afterwards** — see [`DEPENDENCY-GRAPH.md`](../../../dev-docs/DEPENDENCY-GRAPH.md). +- **A parser change without a matching `Unparser` change.** +- **A structural change that does not assert the formula text afterwards.** - **A new mutation missing one of `CrudOperations`, `Operations`, `UndoRedo`** — undo diverges silently. -## 2. Completeness of a function change +## 3. Completeness of a function change -All five places, and three of them fail silently: implementation, catalogue entry with a matching parameter count, every language file, tests, changelog. Plus `sizeOfResultArrayMethod` for anything array-returning, and an explicit `optionalArg` where arity does not express the valid call. See skill `hyperformula-function-dev`. +All five places, three of which fail silently: implementation, catalogue entry with a matching parameter count, every language file, tests, changelog. Plus `sizeOfResultArrayMethod` for anything array-returning, and an explicit `optionalArg` where arity does not express the valid call. Skill `hyperformula-function-dev`. -## 3. Performance +## 4. Performance -Allocation in a per-cell or per-vertex loop; work that could be hoisted out of the broadcast path; a range expanded into per-cell iteration; anything that widens what a change invalidates; a `ParserWithCaching` change that makes the result depend on something outside the cache key. Ask for `npm run test:performance` on hot-path changes. See [`CODE-STYLE.md`](../../../dev-docs/CODE-STYLE.md#performance). +Allocation in a per-cell or per-vertex loop; work that could be hoisted out of the broadcast path; a range expanded into per-cell iteration; anything that widens what a change invalidates; a `ParserWithCaching` change that makes the result depend on something outside the cache key. Ask for `npm run test:performance` on hot-path changes. -## 4. Public API +## 5. Public API `src/HyperFormula.ts` and its exported types are the contract. A signature, return-type, or behaviour change is breaking and needs a migration-guide section and an explicit note. JSDoc here is published output — review it as documentation. -## 5. Process +## 6. Process -[`DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md), and one atomic change per pull request. Say so when unrelated refactors have been folded in, rather than approving them through. +One atomic change per pull request. Say so when unrelated refactors have been folded in, rather than approving them through. -## 6. Style, last and briefly +## 7. Style, last and briefly ESLint owns formatting. Comment only on what it cannot check: a misleading name, a function doing two things, duplicated logic an existing helper already covers. diff --git a/.claude/skills/hyperformula-dev/SKILL.md b/.claude/skills/hyperformula-dev/SKILL.md index 06feb53993..61df2081c0 100644 --- a/.claude/skills/hyperformula-dev/SKILL.md +++ b/.claude/skills/hyperformula-dev/SKILL.md @@ -11,42 +11,59 @@ description: > point for engine development — when in doubt, load it. --- -The rules live in [`dev-docs/`](../../../dev-docs/README.md). This skill is the order of operations. +## 1. Read the relevant files from `dev-docs/` -## 1. Locate the stage before changing anything +Always: -The engine is a pipeline: `CellContentParser` → `parser/` → `GraphBuilder` → `DependencyGraph/` → `Evaluator` → `interpreter/` → `Serialization`. +| File | Why | +|---|---| +| [`ARCHITECTURE.md`](../../../dev-docs/ARCHITECTURE.md) | The pipeline, the core modules, and the invariants that hold everywhere in `src/` | +| [`CODE-STYLE.md`](../../../dev-docs/CODE-STYLE.md) | Style, and which paths are hot enough that complexity matters | +| [`DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md) | What the change must contain before review | + +Then the page for the stage you are changing: + +| File | For | +|---|---| +| [`PARSER.md`](../../../dev-docs/PARSER.md) | `src/parser/` — formula text to AST, and back | +| [`INTERPRETER.md`](../../../dev-docs/INTERPRETER.md) | `src/interpreter/` — AST to value, and built-in functions | +| [`DEPENDENCY-GRAPH.md`](../../../dev-docs/DEPENDENCY-GRAPH.md) | `src/DependencyGraph/` — dependency tracking and recalculation order | +| [`FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md) | `src/interpreter/functionMetadata/` — function descriptions | +| [`I18N.md`](../../../dev-docs/I18N.md) | `src/i18n/` — function-name translations | +| [`TESTING.md`](../../../dev-docs/TESTING.md) | Writing the test the change needs | -| Symptom | Stage | Read | -|---|---|---| -| Does not parse, or parses wrongly | `src/parser/` | [`PARSER.md`](../../../dev-docs/PARSER.md) | -| `getCellFormula` returns something the user never typed | `src/parser/Unparser.ts` | [`PARSER.md`](../../../dev-docs/PARSER.md) | -| A function returns the wrong value or error | `src/interpreter/plugin/` | skill `hyperformula-function-dev` | -| Value right, but stale after an edit | `src/DependencyGraph/`, `src/Evaluator.ts` | [`DEPENDENCY-GRAPH.md`](../../../dev-docs/DEPENDENCY-GRAPH.md) | -| Wrong after adding or removing rows or columns | `src/dependencyTransformers/`, `LazilyTransformingAstService.ts` | [`DEPENDENCY-GRAPH.md`](../../../dev-docs/DEPENDENCY-GRAPH.md) | -| Wrong in one language only | `src/i18n/languages/` | skill `i18n-translations` | -| Coercion or comparison is wrong | `src/interpreter/ArithmeticHelper.ts` | [`INTERPRETER.md`](../../../dev-docs/INTERPRETER.md) | -| The public API disagrees with its docs | `src/HyperFormula.ts` | [`DOC-STANDARDS.md`](../../../dev-docs/DOC-STANDARDS.md) | +## 2. Locate the stage before changing anything + +The engine is a pipeline: `CellContentParser` → `parser/` → `GraphBuilder` → `DependencyGraph/` → `Evaluator` → `interpreter/` → `Serialization`. -A bug that looks like an interpreter problem is often a parser or graph problem. Confirm which before editing. +| Symptom | Stage | +|---|---| +| Does not parse, or parses wrongly | `src/parser/` | +| `getCellFormula` returns something the user never typed | `src/parser/Unparser.ts` | +| A function returns the wrong value or error | `src/interpreter/plugin/` — skill `hyperformula-function-dev` | +| Value right, but stale after an edit | `src/DependencyGraph/`, `src/Evaluator.ts` | +| Wrong after adding or removing rows or columns | `src/dependencyTransformers/`, `LazilyTransformingAstService.ts` | +| Wrong in one language only | `src/i18n/languages/` — skill `i18n-translations` | +| Coercion or comparison is wrong | `src/interpreter/ArithmeticHelper.ts` | +| The public API disagrees with its docs | `src/HyperFormula.ts` | -Use the `typescript-lsp` plugin to find a definition or its callers; grep is for text, not symbols. +A bug that looks like an interpreter problem is often a parser or graph problem. Confirm which before editing. Use the `typescript-lsp` plugin to find a definition or its callers; grep is for text, not symbols. -## 2. Reproduce first +## 3. Reproduce first Write the failing test before the fix and watch it fail — skill `test-writing-discipline`. For a calculation bug the smallest reproduction is a two-line `buildFromArray` plus one `getCellValue`. If `test/hyperformula-tests/` is absent, `npm run test:jest` runs only the smoke tests and reports a clean pass over almost nothing. Run `npm run test:setup-private` first, and after every branch switch. -## 3. Change, then run the fast loop +## 4. Change, then run the fast loop ```bash npm run test:jest -- npm run lint ``` -## 4. Finish the change +Run `npm run test:performance` for changes to the evaluation or CRUD hot paths. -Tests, documentation, JSDoc, changelog, translations: [`DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md). +## 5. Finish the change -Before touching anything under `src/interpreter/`, `src/DependencyGraph/`, `src/Evaluator.ts`, `src/parser/ParserWithCaching.ts`, or `src/LazilyTransformingAstService.ts`, ask what runs per cell and what runs once — [`CODE-STYLE.md`](../../../dev-docs/CODE-STYLE.md#performance). Run `npm run test:performance` for changes to the evaluation or CRUD hot paths. +Tests, documentation, JSDoc, changelog, translations — every item of `DEFINITION-OF-DONE.md`. diff --git a/.claude/skills/hyperformula-function-dev/SKILL.md b/.claude/skills/hyperformula-function-dev/SKILL.md index 53af19c41f..e5e50d48bc 100644 --- a/.claude/skills/hyperformula-function-dev/SKILL.md +++ b/.claude/skills/hyperformula-function-dev/SKILL.md @@ -4,24 +4,32 @@ paths: src/interpreter/** description: Use when adding a new built-in spreadsheet function to HyperFormula, changing an existing one's signature, arguments, return type, or error behaviour, or when a function returns the wrong value or the wrong error. Covers the FunctionPlugin contract, runFunction and argument metadata, the function metadata catalogue, translations, and the full end-to-end checklist. --- -The plugin contract, `runFunction`, and every metadata field are documented in [`INTERPRETER.md`](../../../dev-docs/INTERPRETER.md#built-in-functions). Read it before writing code. This skill is the checklist. +## 1. Read the relevant files from `dev-docs/` -## The five places, in order +| File | Why | +|---|---| +| [`INTERPRETER.md`](../../../dev-docs/INTERPRETER.md#built-in-functions) | The plugin contract, `runFunction`, and every argument and function metadata field. Read this before writing any code. | +| [`FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md) | What the catalogue entry must contain, and the two failure modes that are silent | +| [`I18N.md`](../../../dev-docs/I18N.md) | Where to source a translation, and why an invented one cannot be taken back | +| [`TESTING.md`](../../../dev-docs/TESTING.md#what-each-kind-of-change-needs) | The list of cases a function change must cover | +| [`DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md) | What the change must contain before review | + +## 2. Touch all five places A function is not done until all five agree. Each omission fails differently, and three of them fail silently. -1. **Implementation** — the plugin class in `src/interpreter/plugin/`, plus its `implementedFunctions` entry keyed by the canonical English id. See [`INTERPRETER.md`](../../../dev-docs/INTERPRETER.md#built-in-functions). -2. **Catalogue entry** — `src/interpreter/functionMetadata/categories/.ts`. Parameter **count** must match `implementedFunctions`, or the authored names and descriptions are discarded at run time with only a console warning. A missing entry fails the docs build. See [`FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md). -3. **Translations** — every file in `src/i18n/languages/`. See [`I18N.md`](../../../dev-docs/I18N.md). -4. **Tests** — in `test/`. The list of what a function change must cover is in [`TESTING.md`](../../../dev-docs/TESTING.md#what-each-kind-of-change-needs). +1. **Implementation** — the plugin class in `src/interpreter/plugin/`, plus its `implementedFunctions` entry keyed by the canonical English id. +2. **Catalogue entry** — `src/interpreter/functionMetadata/categories/.ts`. Parameter **count** must match `implementedFunctions`, or the authored names and descriptions are discarded at run time with only a console warning. A missing entry fails the docs build. +3. **Translations** — every file in `src/i18n/languages/`. +4. **Tests** — in `test/`. 5. **Changelog** — skill `changelog-creation`. -## Two declarations nothing cross-checks +## 3. Declare the two things nothing cross-checks - A function that can return an array needs `sizeOfResultArrayMethod`. - A function whose valid call arity alone does not express — a zero-argument form, an omitted trailing argument — needs `optionalArg: true` declared explicitly, or the public API advertises the argument as required. -## Verify +## 4. Verify ```bash npm run test:jest -- @@ -29,6 +37,6 @@ npm run docs:generate-function-docs # fails loudly on a bad or missing catalog npm run lint ``` -## When the behaviour differs from Excel +## 5. Record any deviation from Excel -That is a decision, not an accident. Record it in [`docs/guide/list-of-differences.md`](../../../docs/guide/list-of-differences.md) and say so in the changelog entry. Never write a description that documents Excel while the code does something else. +That is a decision, not an accident. Put it in [`docs/guide/list-of-differences.md`](../../../docs/guide/list-of-differences.md) and say so in the changelog entry. Never write a description that documents Excel while the code does something else. diff --git a/.claude/skills/hyperformula-unit-testing/SKILL.md b/.claude/skills/hyperformula-unit-testing/SKILL.md index 9d8b78fbd6..d6596ae850 100644 --- a/.claude/skills/hyperformula-unit-testing/SKILL.md +++ b/.claude/skills/hyperformula-unit-testing/SKILL.md @@ -4,9 +4,16 @@ paths: test/** description: Use when writing or modifying tests for HyperFormula, or when a change to `src/` needs test coverage. Covers the two suites, fetching the private suite, how to build an engine in a test, and what a function or CRUD change must cover. --- -How to write a case, and what each kind of change must cover, are in [`TESTING.md`](../../../dev-docs/TESTING.md). What the two suites are and how to attach the private one is in [`test/README.md`](../../../test/README.md). This skill is the loop. +## 1. Read the relevant files from `dev-docs/` -## Before you trust a green run +| File | Why | +|---|---| +| [`TESTING.md`](../../../dev-docs/TESTING.md) | The two suites, how to write a case, and what each kind of change must cover | +| [`WORKTREES.md`](../../../dev-docs/WORKTREES.md) | Only when working in a linked worktree, where the private suite is absent entirely | + +And [`test/README.md`](../../../test/README.md) for how the private suite is fetched and the environment variables it honours. + +## 2. Attach the private suite before trusting anything ```bash npm run test:setup-private @@ -14,17 +21,19 @@ npm run test:setup-private Run it after every branch switch. Without `test/hyperformula-tests/` the Jest run covers only the smoke tests and reports a clean pass over almost nothing — the most common false signal in this repository. `test:performance` and `test:compatibility` fail on a missing path rather than an assertion; read the error before concluding the code is broken. -## The loop +## 3. Write the case from the requirement + +Not from the implementation. A test written from the code passes for any implementation, including the wrong one. + +## 4. Run it and watch it fail + +For a bug fix this is not optional — skill `test-writing-discipline`. ```bash npm run test:jest -- # one file or one describe npm run test:watch -npm run test # lint + jest + browser, the full local gate ``` -## Order +## 5. Fix the code, then run again -1. Write the case from the requirement, not from the implementation. -2. Run it and watch it fail. A bug-fix test that has never failed proves nothing — skill `test-writing-discipline`. -3. Fix the code. -4. Run it again, and read the output rather than assuming it. +Read the output rather than assuming it. `npm run test` is the full local gate: lint, Jest, and the browser run. diff --git a/.claude/skills/i18n-translations/SKILL.md b/.claude/skills/i18n-translations/SKILL.md index 46c3d3178c..08bd5b49e4 100644 --- a/.claude/skills/i18n-translations/SKILL.md +++ b/.claude/skills/i18n-translations/SKILL.md @@ -4,22 +4,32 @@ paths: src/i18n/** description: Use when adding a built-in function that needs translated names, adding a language pack, or fixing a function name that is wrong in one language. Covers the translation sources, the rules, and what breaks when a key is missing. --- -Why translations are engine input rather than decoration, the rules, and the table of sources to translate from are in [`I18N.md`](../../../dev-docs/I18N.md). This skill is the procedure. +## 1. Read the relevant files from `dev-docs/` -## Adding a function name +| File | Why | +|---|---| +| [`I18N.md`](../../../dev-docs/I18N.md) | Why translations are engine input rather than decoration, the rules, and the table of sources to translate from | +| [`PARSER.md`](../../../dev-docs/PARSER.md) | Only when changing separators or error literals — the lexer builds its token set from the language package | -1. Look the name up in the sources listed in [`I18N.md`](../../../dev-docs/I18N.md#where-to-find-a-translation), in that order. **Never invent or machine-translate a function name** — a wrong one ships to every user of that language pack and cannot be changed without breaking their formulas. -2. Add the key to **every** file in `src/i18n/languages/`, in the same change as the function. Missing one is the usual failure, and nothing type-checks it on every path. -3. Do not reorder existing entries while adding one; it turns a one-line diff into an unreviewable one. -4. Add a test that parses a formula using the translated name and asserts the result, in that language. +## 2. Look the name up in a real source -## Adding a language pack +Use the sources in [`I18N.md`](../../../dev-docs/I18N.md#where-to-find-a-translation), in the order listed. **Never invent or machine-translate a function name.** A wrong one ships to every user of that language pack and cannot be changed without breaking their formulas. -The file, its export in `src/i18n/languages/index.ts`, a key set identical to the other packs, a changelog entry, and `npm run bundle:languages` to produce the standalone UMD build. +## 3. Add the key to every language file -## Verify +All of them, in the same change as the function. Missing one is the usual failure, and nothing type-checks it on every path. Do not reorder existing entries while adding one — it turns a one-line diff into an unreviewable one. + +## 4. Test it in that language + +A test that parses a formula using the translated name and asserts the result. Not in English. + +## 5. Verify ```bash npm run test:jest -- i18n npm run lint ``` + +## Adding a whole language pack + +The file, its export in `src/i18n/languages/index.ts`, a key set identical to the other packs, a changelog entry, and `npm run bundle:languages` for the standalone UMD build. diff --git a/.claude/skills/pr-creation/SKILL.md b/.claude/skills/pr-creation/SKILL.md index 2dd8964ff0..2837d631fe 100644 --- a/.claude/skills/pr-creation/SKILL.md +++ b/.claude/skills/pr-creation/SKILL.md @@ -3,23 +3,40 @@ name: pr-creation description: Use before creating, pushing, opening, or updating a pull request in the HyperFormula repository — load this BEFORE running `gh pr create` or pushing a feature/docs/fix branch, not only when the user says "PR". Covers branch naming, the pre-flight lint/tests, the PR-then-changelog flow, and filling the GitHub PR template. --- -Branch naming, the pre-flight gate, the template, and the scope rule are in [`PULL-REQUESTS.md`](../../../dev-docs/PULL-REQUESTS.md). Read it before pushing. This skill is the run order. - -## Run order - -1. Commit on a correctly named feature branch. -2. Run the gate and **read the output**: - ```bash - npm run test:setup-private - npm run lint - npm run test:jest - ``` - A green Jest run without `test/hyperformula-tests/` covers only the smoke tests. Confirm the suite is attached before calling it green. -3. Push and open the pull request. Fill in every section of the template. -4. Read the pull request number from the URL. -5. Add the changelog entry with that link — skill `changelog-creation` — and push it to the same branch. -6. Confirm every item of [`DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md). +## 1. Read the relevant files from `dev-docs/` + +| File | Why | +|---|---| +| [`PULL-REQUESTS.md`](../../../dev-docs/PULL-REQUESTS.md) | Branch naming, the pre-flight gate, the template, and the one-change-per-pull-request rule | +| [`DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md) | Every item the change must contain before review | +| [`DOC-STANDARDS.md`](../../../dev-docs/DOC-STANDARDS.md#the-changelog) | The changelog entry that follows the pull request | + +## 2. Commit on a correctly named branch + +`/-`. Nothing from a private ticket in the name — the identifier alone is fine. + +## 3. Run the gate and read the output + +```bash +npm run test:setup-private +npm run lint +npm run test:jest +``` + +A green Jest run without `test/hyperformula-tests/` covers only the smoke tests. Confirm the suite is attached before calling it green. Do not open a pull request on a red run and describe it as ready. + +## 4. Push and open the pull request + +Fill in every section of the template. Tick the Types of changes boxes honestly, breaking change included. + +## 5. Add the changelog entry + +Read the number from the pull request URL, then skill `changelog-creation`. Push it to the same branch. + +## 6. Confirm the definition of done + +Then read your own diff end to end before asking anyone else to. ## While the branch is open -Update the description in the same push whenever the scope changes. Read your own diff end to end before asking anyone else to. +Update the description in the same push whenever the scope changes. diff --git a/.claude/skills/test-writing-discipline/SKILL.md b/.claude/skills/test-writing-discipline/SKILL.md index b576bcb23e..35bf9d1126 100644 --- a/.claude/skills/test-writing-discipline/SKILL.md +++ b/.claude/skills/test-writing-discipline/SKILL.md @@ -3,18 +3,27 @@ name: test-writing-discipline description: Use when writing, fixing, or reviewing any test for HyperFormula, and whenever a test is red during feature work. Enforces that tests prove intended behaviour rather than merely execute code, and never go "green for the sake of green". --- -The rule, the banned ways of going green, and what a hollow assertion looks like are in [`TESTING.md`](../../../dev-docs/TESTING.md#a-test-must-prove-behaviour). Read that section now — it is short. +## 1. Read the relevant files from `dev-docs/` -## Apply it in this order +| File | Why | +|---|---| +| [`TESTING.md`](../../../dev-docs/TESTING.md#a-test-must-prove-behaviour) | The rule, the banned ways of going green, and what a hollow assertion looks like. It is short — read it now, before touching the test. | -1. **Write the test from the requirement**, before reading the implementation. A test written from the code passes for any implementation, including the wrong one. -2. **Run it and watch it fail.** For a bug fix this is not optional: a test that has never failed proves nothing about the bug. -3. **Fix the code**, not the test. When a test is red the default assumption is that the code is wrong. Changing the expectation requires a one-sentence reason about the *specification*. -4. **Run it again and read the output.** Never claim a test passes because the reasoning is sound. +## 2. Write the test from the requirement -## Before treating green as coverage +Before reading the implementation. A test written from the code passes for any implementation, including the wrong one. -If `test/hyperformula-tests/` is missing, `npm run test:jest` runs only the smoke tests and reports a clean pass over almost nothing. Confirm it is there — `npm run test:setup-private`. +## 3. Run it and watch it fail + +For a bug fix this is not optional: a test that has never failed proves nothing about the bug. + +## 4. Fix the code, not the test + +When a test is red the default assumption is that the code is wrong. Changing the expectation requires a one-sentence reason about the *specification*, not about the effort of fixing the code. + +## 5. Run it again and read the output + +Never claim a test passes because the reasoning is sound. And before treating green as coverage, confirm `test/hyperformula-tests/` is present — `npm run test:setup-private`. ## If you cannot make it pass honestly diff --git a/.claude/skills/writing-docs-pages/SKILL.md b/.claude/skills/writing-docs-pages/SKILL.md index bab83de3c3..5db667cf0c 100644 --- a/.claude/skills/writing-docs-pages/SKILL.md +++ b/.claude/skills/writing-docs-pages/SKILL.md @@ -4,9 +4,17 @@ paths: docs/** description: Use when creating or editing a page in the HyperFormula documentation portal, adding a guide, or updating the API reference. Covers what is generated versus hand-written, sidebar registration, running the portal, and the writing rules. --- -What the portal contains, what is generated, and how to add a page are in [`docs/README.md`](../../../docs/README.md). The rules that govern documentation anywhere in the repository are in [`DOC-STANDARDS.md`](../../../dev-docs/DOC-STANDARDS.md). Writing style is [`DOCS_CONTENT_GUIDE.md`](../../../DOCS_CONTENT_GUIDE.md). +## 1. Read the relevant files from `dev-docs/` -## Change the source, not the output +| File | Why | +|---|---| +| [`DOC-STANDARDS.md`](../../../dev-docs/DOC-STANDARDS.md) | When documentation is required, what must not duplicate the API reference, and describing HyperFormula rather than Excel | +| [`FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md) | Only when the change concerns the built-in functions page, which is generated from the catalogue | +| [`BUILD.md`](../../../dev-docs/BUILD.md) | Which documentation files are generated, by which command | + +And two files outside `dev-docs/`: [`docs/README.md`](../../../docs/README.md) for what the portal contains and how to run it, and [`DOCS_CONTENT_GUIDE.md`](../../../DOCS_CONTENT_GUIDE.md) for writing style and guide structure. + +## 2. Change the source, not the output | To change | Edit | Then run | |---|---|---| @@ -16,14 +24,14 @@ What the portal contains, what is generated, and how to add a page are in [`docs `docs/guide/built-in-functions.md` and `docs/api/` are git-ignored build output. Editing them is always wrong, and the edit disappears on the next build. -## Run it +## 3. Run the portal ```bash npm run bundle-all # the portal embeds the built engine npm run docs:dev # http://localhost:8080/hyperformula/ ``` -## Before you finish +## 4. Before you finish - A new page needs a sidebar entry under `docs/.vuepress/`, or it builds and is unreachable. - Link to the API reference for detail rather than restating it. diff --git a/dev-docs/AGENT-TOOLING.md b/dev-docs/AGENT-TOOLING.md index 238ad444c1..3c372dfbe5 100644 --- a/dev-docs/AGENT-TOOLING.md +++ b/dev-docs/AGENT-TOOLING.md @@ -52,6 +52,8 @@ All skills live in `.claude/skills/`, at the repository root, and are scoped by A skill holds the **steps**: what to do, in what order, and what to check. It does not restate the rules those steps enforce — it links to the `dev-docs/` page that owns them. +**Step 1 of every skill is "Read the relevant files from `dev-docs/`", and it names them.** Not a general pointer at the directory: a table of the specific pages, each with one line saying why that page matters for this task. Where a task's reference depends on what it touches, the step lists the always-read pages first and then the conditional ones. A skill whose first step is anything else is missing it. + | Skill | For | |---|---| | `hyperformula-dev` | Any work in `src/` — the entry point | From 2e86b5cc129333c33827a7f2e5be4093d83440c3 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Mon, 31 Aug 2026 15:35:11 +0200 Subject: [PATCH 08/27] HF-359: Move the docs content guide into dev-docs DOCS_CONTENT_GUIDE.md was the last piece of internal documentation sitting outside dev-docs/, which made it the one page an agent had to be told about separately. It is now dev-docs/DOCS-CONTENT-GUIDE.md, named like its neighbours, and reachable from the index like everything else. The move exposed a contradiction between it and DOC-STANDARDS.md, which is exactly what having two homes for documentation rules produces. DOC-STANDARDS said a guide should not duplicate the API reference; the content guide says self-containment beats DRY and a page must let the reader finish the task without leaving it. Both are right about different things, so the two pages now divide the work explicitly: DOC-STANDARDS covers WHEN documentation is required and what must be true of it, DOCS-CONTENT-GUIDE covers HOW to write the page, and DOC-STANDARDS defers to it on the trade-off rather than restating a competing rule. Also repair four checklist items whose continuation lines were wrapped in stray code fences, so half of each item rendered as a code block. References updated in docs/AGENTS.md, docs/README.md, dev-docs/README.md, dev-docs/STRUCTURE.md, and the writing-docs-pages skill, where the guide moves into the step-1 reading table and the self-review checklist becomes an explicit closing step. Co-Authored-By: Claude Opus 5 --- .claude/skills/writing-docs-pages/SKILL.md | 6 ++- dev-docs/DOC-STANDARDS.md | 8 ++-- .../DOCS-CONTENT-GUIDE.md | 39 ++----------------- dev-docs/README.md | 4 +- dev-docs/STRUCTURE.md | 1 - docs/AGENTS.md | 2 +- docs/README.md | 2 +- 7 files changed, 17 insertions(+), 45 deletions(-) rename DOCS_CONTENT_GUIDE.md => dev-docs/DOCS-CONTENT-GUIDE.md (97%) diff --git a/.claude/skills/writing-docs-pages/SKILL.md b/.claude/skills/writing-docs-pages/SKILL.md index 5db667cf0c..97654218d6 100644 --- a/.claude/skills/writing-docs-pages/SKILL.md +++ b/.claude/skills/writing-docs-pages/SKILL.md @@ -8,11 +8,12 @@ description: Use when creating or editing a page in the HyperFormula documentati | File | Why | |---|---| -| [`DOC-STANDARDS.md`](../../../dev-docs/DOC-STANDARDS.md) | When documentation is required, what must not duplicate the API reference, and describing HyperFormula rather than Excel | +| [`DOCS-CONTENT-GUIDE.md`](../../../dev-docs/DOCS-CONTENT-GUIDE.md) | How to write the page: structure, chunking, language, code examples, VuePress conventions, and the self-review checklist to run before finishing | +| [`DOC-STANDARDS.md`](../../../dev-docs/DOC-STANDARDS.md) | When documentation is required, and describing HyperFormula rather than Excel | | [`FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md) | Only when the change concerns the built-in functions page, which is generated from the catalogue | | [`BUILD.md`](../../../dev-docs/BUILD.md) | Which documentation files are generated, by which command | -And two files outside `dev-docs/`: [`docs/README.md`](../../../docs/README.md) for what the portal contains and how to run it, and [`DOCS_CONTENT_GUIDE.md`](../../../DOCS_CONTENT_GUIDE.md) for writing style and guide structure. +And [`docs/README.md`](../../../docs/README.md), for what the portal contains and how to run it. ## 2. Change the source, not the output @@ -36,3 +37,4 @@ npm run docs:dev # http://localhost:8080/hyperformula/ - A new page needs a sidebar entry under `docs/.vuepress/`, or it builds and is unreachable. - Link to the API reference for detail rather than restating it. - Verify any behavioural claim against the implementation. Where HyperFormula deviates from Excel, record it in [`docs/guide/list-of-differences.md`](../../../docs/guide/list-of-differences.md). +- Run the self-review checklist at the end of [`DOCS-CONTENT-GUIDE.md`](../../../dev-docs/DOCS-CONTENT-GUIDE.md#self-review-checklist-run-before-finishing-any-page). diff --git a/dev-docs/DOC-STANDARDS.md b/dev-docs/DOC-STANDARDS.md index 429cc39946..cceaa0a562 100644 --- a/dev-docs/DOC-STANDARDS.md +++ b/dev-docs/DOC-STANDARDS.md @@ -2,7 +2,7 @@ These apply across the whole repository: guides, the API reference, JSDoc inside `src/`, the changelog, migration guides, and README files. -Writing style, language, and guide structure live in [`DOCS_CONTENT_GUIDE.md`](../DOCS_CONTENT_GUIDE.md). Follow it when creating or editing docs content. +*How* to write a page — structure, chunking, language, code examples, and the self-review checklist — is [`DOCS-CONTENT-GUIDE.md`](DOCS-CONTENT-GUIDE.md). This page is *when* documentation is required and what it must be true about. ## When documentation is required @@ -11,9 +11,11 @@ Writing style, language, and guide structure live in [`DOCS_CONTENT_GUIDE.md`](. - Any breaking change adds a migration-guide section. - Documentation-only changes (guides, JSDoc, README) need no changelog entry. -## Do not duplicate +## The API reference is the source of truth -The API reference — generated from JSDoc — holds all the detail about each function and class, and is the primary source of truth. Guides give a high-level overview. A guide may repeat something from the API reference where it is relevant to the context, but above all it should **link** to the API reference for the detail. +The API reference, generated from JSDoc, holds all the detail about each function and class. A guide gives the overview and links to the reference for the detail. + +This is not a licence to make a guide unreadable on its own. A guide page repeats the small essential context a reader needs to finish the task in front of them, and links out only for depth. [`DOCS-CONTENT-GUIDE.md`](DOCS-CONTENT-GUIDE.md#how-to-structure-a-page) owns that trade-off and states the rule of thumb: if removing a link would make the current task impossible to finish, that information belongs on the page. ## Generated files diff --git a/DOCS_CONTENT_GUIDE.md b/dev-docs/DOCS-CONTENT-GUIDE.md similarity index 97% rename from DOCS_CONTENT_GUIDE.md rename to dev-docs/DOCS-CONTENT-GUIDE.md index 08d160f128..badbac09cb 100644 --- a/DOCS_CONTENT_GUIDE.md +++ b/dev-docs/DOCS-CONTENT-GUIDE.md @@ -16,8 +16,6 @@ helps humans and search. You are not gaming an algorithm — you are writing cle --- - - ## Project context - **Product:** HyperFormula — an open-source, headless spreadsheet and formula @@ -36,8 +34,6 @@ linked per version branch (e.g. `3.3.x`). --- - - ## What to write (coverage & gap prioritization) Optimization cannot recover a page that doesn't exist. Coverage comes first. @@ -69,8 +65,6 @@ Optimization cannot recover a page that doesn't exist. Coverage comes first. --- - - ## How to structure a page Treat **every page as page one.** @@ -102,8 +96,6 @@ Treat **every page as page one.** --- - - ## How to structure sections (chunking) 1. **One purpose per section.** Each `##`/`###` answers exactly one question. Don't @@ -132,8 +124,6 @@ Treat **every page as page one.** --- - - ## Language & terminology 1. **Write plainly.** Aim for a ~6th–7th-grade reading level: short sentences, one @@ -157,8 +147,6 @@ Treat **every page as page one.** --- - - ## Code examples Code is the primary content of these docs. Models reproduce complete examples well @@ -184,8 +172,6 @@ and hallucinate the parts you omit. --- - - ## Visuals, tables & the "why" 1. **Never put information only in an image, diagram, or video.** Assistants can't @@ -209,8 +195,6 @@ and hallucinate the parts you omit. --- - - ## VuePress conventions (so your output fits the site) - Start each page with frontmatter: @@ -237,33 +221,18 @@ VuePress — don't hand-write navigation. --- - - - ## Self-review checklist (run before finishing any page) -- [ ] **Stands alone:** a reader who lands here cold, seeing only this page, can - ``` - finish the task without opening another page. - ``` +- [ ] **Stands alone:** a reader who lands here cold, seeing only this page, can finish the task without opening another page. - [ ] **One question per section**, descriptive headings, hierarchy in order. -- [ ] **No walls of text:** long prose is broken into granular H2/H3 subsections, - ``` - each short enough that its heading covers everything under it. - ``` +- [ ] **No walls of text:** long prose is broken into granular H2/H3 subsections, each short enough that its heading covers everything under it. - [ ] **Product named** in the body; no bare "the library/the method/the grid." - [ ] **No backward references** ("as above," "now that you've…"). - [ ] **Prerequisites stated explicitly**; nothing assumed. - [ ] **Terminology consistent** with the canonical terms; acronyms expanded once. -- [ ] **Every code block** is language-tagged, complete, includes the `licenseKey`, - ``` - and would actually run; cell-address property order is `{ sheet, row, col }`. - ``` +- [ ] **Every code block** is language-tagged, complete, includes the `licenseKey`, and would actually run; cell-address property order is `{ sheet, row, col }`. - [ ] **No info trapped in images/tables**; visuals have text equivalents and captions. - [ ] **Frontmatter** has a specific `title` and a one-sentence `description`. - [ ] **No contradiction** with other pages; old facts updated everywhere. - [ ] **"Why" is covered:** intent, when-to-use, and known gotchas — not just syntax. -- [ ] Would an AI assistant quoting *only this page* give a correct, complete answer? - ``` - If not, fix the page. - ``` +- [ ] Would an AI assistant quoting *only this page* give a correct, complete answer? If not, fix the page. diff --git a/dev-docs/README.md b/dev-docs/README.md index 5dbf818023..dfc2cf9611 100644 --- a/dev-docs/README.md +++ b/dev-docs/README.md @@ -41,7 +41,8 @@ In every directory, `CLAUDE.md` is a symlink to its sibling `AGENTS.md`. Edit `A | [`DEFINITION-OF-DONE.md`](DEFINITION-OF-DONE.md) | What every change must include before review. | | [`CODE-STYLE.md`](CODE-STYLE.md) | Code style, and the performance rules that apply to engine code. | | [`TESTING.md`](TESTING.md) | Test suites, the private test repository, and how to write a case. | -| [`DOC-STANDARDS.md`](DOC-STANDARDS.md) | Documentation rules across guides, API reference, JSDoc, and the changelog. | +| [`DOC-STANDARDS.md`](DOC-STANDARDS.md) | When documentation is required, and the rules it must satisfy — guides, API reference, JSDoc, and the changelog. | +| [`DOCS-CONTENT-GUIDE.md`](DOCS-CONTENT-GUIDE.md) | How to write a documentation page: structure, chunking, language, code examples, VuePress conventions, and the self-review checklist. | | [`BUILD.md`](BUILD.md) | What the public [building guide](../docs/guide/building.md) does not cover: the intermediate build, generated docs, deployment, and release. | | [`PULL-REQUESTS.md`](PULL-REQUESTS.md) | Branch naming, the pre-flight gate, and the pull request template. | | [`WORKTREES.md`](WORKTREES.md) | Working in a linked git worktree: what is missing and how to bootstrap it. | @@ -54,7 +55,6 @@ In every directory, `CLAUDE.md` is a symlink to its sibling `AGENTS.md`. Edit `A | [`docs/README.md`](../docs/README.md) | Running and extending the documentation portal | | [`test/README.md`](../test/README.md) | Attaching the private test suite | | [`script/README.md`](../script/README.md) | What each build and release script does | -| [`DOCS_CONTENT_GUIDE.md`](../DOCS_CONTENT_GUIDE.md) | Writing style and structure for docs content | | [`CHANGELOG.md`](../CHANGELOG.md) | Release history | | [`.github/pull_request_template.md`](../.github/pull_request_template.md) | The pull request template | diff --git a/dev-docs/STRUCTURE.md b/dev-docs/STRUCTURE.md index 3b66c0b3bc..fa6cf6487a 100644 --- a/dev-docs/STRUCTURE.md +++ b/dev-docs/STRUCTURE.md @@ -42,7 +42,6 @@ The layout as it stands today. For the monorepo layout this repository is moving ├── AGENTS.md # Always-loaded agent rules and routing map ├── CLAUDE.md # Symlink to AGENTS.md ├── CONTRIBUTING.md # Guide for external contributors -├── DOCS_CONTENT_GUIDE.md # Writing style and structure for docs content ├── README.md # Project overview └── CHANGELOG.md ``` diff --git a/docs/AGENTS.md b/docs/AGENTS.md index d67d7c7ce6..b868ca3c15 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -4,7 +4,7 @@ The public VuePress documentation portal. - What this directory contains, what is generated, and how to run and extend it: [`README.md`](README.md) - Documentation rules across the repository: [`dev-docs/DOC-STANDARDS.md`](../dev-docs/DOC-STANDARDS.md) -- Writing style and guide structure: [`DOCS_CONTENT_GUIDE.md`](../DOCS_CONTENT_GUIDE.md) +- How to write a page: [`dev-docs/DOCS-CONTENT-GUIDE.md`](../dev-docs/DOCS-CONTENT-GUIDE.md) - Everything else: [`dev-docs/README.md`](../dev-docs/README.md) Skill: `writing-docs-pages`. diff --git a/docs/README.md b/docs/README.md index 3a32cb2362..98224192d4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -41,7 +41,7 @@ Which files are generated by what, and why the generator fails the build rather Create the file in `guide/`, then **register it in the sidebar** under `.vuepress/` — a page without a sidebar entry builds successfully and is unreachable. -Writing style and guide structure: [`DOCS_CONTENT_GUIDE.md`](../DOCS_CONTENT_GUIDE.md). When documentation is required, what must not be duplicated, and how to describe behaviour that deviates from Excel: [`dev-docs/DOC-STANDARDS.md`](../dev-docs/DOC-STANDARDS.md). +How to write the page — structure, chunking, language, code examples, and the self-review checklist: [`dev-docs/DOCS-CONTENT-GUIDE.md`](../dev-docs/DOCS-CONTENT-GUIDE.md). When documentation is required at all, and how to describe behaviour that deviates from Excel: [`dev-docs/DOC-STANDARDS.md`](../dev-docs/DOC-STANDARDS.md). ## HyperFormula documentation npm scripts From 80753c55bd0833af91d64100099490c952c1eb6b Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Tue, 1 Sep 2026 10:12:40 +0200 Subject: [PATCH 09/27] HF-359: Address the second review round Four threads. The routing table has moved out of AGENTS.md and into dev-docs/README.md, where it replaces the categorised file listing rather than sitting beside it. The two were the same information keyed differently, which is exactly the duplication the single-source rule exists to prevent. AGENTS.md now points at that page and says nothing about which page covers what. The line describing how directory AGENTS.md files load is folded into the same sentence rather than trailing the table it belonged to. STRUCTURE.md and MONOREPO.md are one file. Both carried a tree, and a reader had to know which of the two was current; STRUCTURE.md now covers where everything lives today and, under "Where it is going", the packages, the target tree and the migration steps. MONOREPO.md is deleted and its two inbound links repointed. The changelog skill states the link rule where it is acted on. It said only that "the entry carries a link" at the step where the number is obtained, and named the issue-or-pull-request rule two steps later; both steps now say it outright. Co-Authored-By: Claude Opus 5 --- .claude/skills/changelog-creation/SKILL.md | 4 +- AGENTS.md | 22 +----- dev-docs/MONOREPO.md | 75 --------------------- dev-docs/README.md | 62 ++++++----------- dev-docs/STRUCTURE.md | 78 +++++++++++++++++++++- dev-docs/WORKTREES.md | 4 +- 6 files changed, 103 insertions(+), 142 deletions(-) delete mode 100644 dev-docs/MONOREPO.md diff --git a/.claude/skills/changelog-creation/SKILL.md b/.claude/skills/changelog-creation/SKILL.md index 0d434b0470..b15d335e86 100644 --- a/.claude/skills/changelog-creation/SKILL.md +++ b/.claude/skills/changelog-creation/SKILL.md @@ -16,11 +16,11 @@ Documentation-only, test-only, and CI or tooling changes take none, and neither ## 3. Open the pull request first -The entry carries a link, so it needs the number. Do not guess it — read it from the URL. Skill `pr-creation`. +**Every entry ends with a GitHub link**: the public issue it fixes when one exists, otherwise the pull request. So the entry needs a number that only exists once the pull request is open. Do not guess it — read it from the URL `gh pr create` prints. Skill `pr-creation`. ## 4. Write the bullet -Under `## [Unreleased]` in `CHANGELOG.md`, in the section that matches the change, creating the `### ` heading if it is absent. Link the public issue where one exists; otherwise the pull request. +Under `## [Unreleased]` in `CHANGELOG.md`, in the section that matches the change, creating the `### ` heading if it is absent. End it with the link from step 3. ## 5. Re-read it as a user would diff --git a/AGENTS.md b/AGENTS.md index e6affa7325..d3a5fbcfde 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,27 +8,7 @@ HyperFormula is a headless spreadsheet calculation engine in TypeScript. No UI, **[`dev-docs/`](dev-docs/) is the single source of truth for everything internal to this project.** Architecture, conventions, build, testing, standards, and the monorepo plan all live there and nowhere else. This file routes; it does not explain. Neither does any other `AGENTS.md` or `README.md` — if one of them looks like it is explaining something, the explanation belongs in `dev-docs/` and the file should link to it. -Read [`dev-docs/README.md`](dev-docs/README.md) first. It indexes the rest. - -| You are working on | Read | -|---|---| -| Anything in `src/` | [`dev-docs/ARCHITECTURE.md`](dev-docs/ARCHITECTURE.md) | -| Formula parsing | [`dev-docs/PARSER.md`](dev-docs/PARSER.md) | -| Formula evaluation, or a built-in function | [`dev-docs/INTERPRETER.md`](dev-docs/INTERPRETER.md) | -| Function descriptions in the API and the docs | [`dev-docs/FUNCTION-CATALOGUE.md`](dev-docs/FUNCTION-CATALOGUE.md) | -| Dependency tracking and recalculation order | [`dev-docs/DEPENDENCY-GRAPH.md`](dev-docs/DEPENDENCY-GRAPH.md) | -| Function-name translations | [`dev-docs/I18N.md`](dev-docs/I18N.md) | -| Tests | [`dev-docs/TESTING.md`](dev-docs/TESTING.md) | -| The documentation portal | [`dev-docs/DOC-STANDARDS.md`](dev-docs/DOC-STANDARDS.md) | -| Building, bundling, releasing | [`dev-docs/BUILD.md`](dev-docs/BUILD.md) | -| What a change must include before review | [`dev-docs/DEFINITION-OF-DONE.md`](dev-docs/DEFINITION-OF-DONE.md) | -| Style and performance | [`dev-docs/CODE-STYLE.md`](dev-docs/CODE-STYLE.md) | -| A linked git worktree | [`dev-docs/WORKTREES.md`](dev-docs/WORKTREES.md) | -| How this repository is set up for agents | [`dev-docs/AGENT-TOOLING.md`](dev-docs/AGENT-TOOLING.md) | -| The monorepo migration | [`dev-docs/MONOREPO.md`](dev-docs/MONOREPO.md) | -| Step-by-step task workflows | [`.claude/skills/`](.claude/skills/) | - -Each directory's own `AGENTS.md` points at the page that covers it, and loads automatically when you work there. +Read [`dev-docs/README.md`](dev-docs/README.md) first. It says which page covers what, and every directory's own `AGENTS.md` points at the page that covers that directory. ## Never publish sensitive information diff --git a/dev-docs/MONOREPO.md b/dev-docs/MONOREPO.md deleted file mode 100644 index 80137a1400..0000000000 --- a/dev-docs/MONOREPO.md +++ /dev/null @@ -1,75 +0,0 @@ -# Target monorepo layout - -This repository is becoming a monorepo (HF-359). The layout it stands at today is [`STRUCTURE.md`](STRUCTURE.md). - -## Packages - -| Package | Directory | Purpose | Published | -|---|---|---|---| -| `hyperformula` | `hyperformula/` | The calculation engine. Everything in `src/` and `test/` today. | yes | -| `hyperformula-ui` | `hyperformula-ui/` | UI components for working with HyperFormula: reference highlighting, inline formula editor, function help. | yes | -| `hyperformula-docs` | `docs/` | The VuePress documentation portal. | no | - -`docs/` is not a workspace member: the portal drags in a large, old dependency tree (VuePress, `--openssl-legacy-provider`) that must not reach an engine install. It keeps its own `package.json` and is installed separately. - -## Tree - -``` -hyperformula/ # repository root — private, workspace root -├── AGENTS.md # monorepo-wide rules + routing map -├── CLAUDE.md -> AGENTS.md -├── README.md CONTRIBUTING.md CHANGELOG.md LICENSE.txt -├── package.json # private: true, npm workspaces, fan-out scripts -├── package-lock.json -├── .nvmrc # 22, like every other one here -├── .worktreeinclude -├── .claude/ -│ ├── settings.json # hooks, enabledPlugins, worktree settings -│ └── skills/ # ALL skills, scoped by the `paths` frontmatter field -├── dev-docs/ # monorepo-scope reference -│ -├── hyperformula/ # ── package: the engine -│ ├── AGENTS.md CLAUDE.md -> AGENTS.md -│ ├── package.json .nvmrc CHANGELOG.md -│ ├── dev-docs/ # engine-scope reference -│ ├── src/ -│ │ ├── AGENTS.md CLAUDE.md -> AGENTS.md -│ │ ├── parser/ AGENTS.md -│ │ ├── interpreter/ AGENTS.md -│ │ │ ├── plugin/ AGENTS.md -│ │ │ └── functionMetadata/ AGENTS.md -│ │ ├── DependencyGraph/ AGENTS.md -│ │ ├── i18n/languages/ AGENTS.md -│ │ └── dependencyTransformers/ format/ helpers/ Lookup/ statistics/ -│ └── test/ AGENTS.md README.md -│ └── hyperformula-tests/ # private suite, git-ignored, branch-matched -│ -├── hyperformula-ui/ # ── package: the UI components -│ ├── AGENTS.md CLAUDE.md -> AGENTS.md -│ ├── package.json .nvmrc CHANGELOG.md -│ ├── dev-docs/ -│ └── src/ test/ -│ -├── docs/ # ── documentation portal (NOT a workspace member) -│ ├── AGENTS.md CLAUDE.md -> AGENTS.md README.md -│ ├── package.json .nvmrc -│ ├── wrangler.jsonc # deploy config for the portal -│ ├── worker/index.js # Cloudflare Worker serving the built portal -│ └── guide/ api/ .vuepress/ -│ -├── examples/ # images and CSV fixtures used by the docs -├── script/ AGENTS.md README.md -└── .github/workflows/ # path-filtered per-package jobs -``` - -## Migration steps - -1. **Move `src/` and `test/` into `hyperformula/`.** Mechanical, but it invalidates every path in CI, in `tsconfig.json`, `jest.config.js`, `karma.conf.js`, `.eslintignore`, and the docs generator scripts. The private suite's specs need no change: they import the engine relatively, and the depth from a spec to the package root is unchanged. -2. **Add `workspaces` to the root `package.json`** and make it `private: true`. Move the build scripts down into `hyperformula/package.json`, leaving fan-out scripts at the root. -3. **Give `docs/` its own `package.json`** and take it out of the root dependency tree. -4. **Move `wrangler.jsonc` and `worker/` under `docs/`.** Update `wrangler.jsonc`'s `main`, the `docs:*:cf` scripts, and `script/prepare-cf-assets.js` in the same change. Verify with `npm run docs:preview:cf` — a broken `main` path fails only at deploy time. -5. **Bring in `hyperformula-ui`**, preserving its history. -6. **Split `CHANGELOG.md` per package**, each keeping the current Keep a Changelog form. -7. **Give every package an `.nvmrc` saying `22`.** -8. **Update the private test suite's checkout path**, from `test/hyperformula-tests/` to `hyperformula/test/hyperformula-tests/`, in `fetch-tests.sh`, `.gitignore`, the three workflows that check it out, and `.worktreeinclude`. It stays branch-matched. -9. **Path-filter CI.** Each package's jobs run only when its paths change; full runs on `develop`, `master`, and release branches. diff --git a/dev-docs/README.md b/dev-docs/README.md index dfc2cf9611..b6a20076fb 100644 --- a/dev-docs/README.md +++ b/dev-docs/README.md @@ -14,49 +14,29 @@ Everything a developer needs to know lives here or is linked from here. External In every directory, `CLAUDE.md` is a symlink to its sibling `AGENTS.md`. Edit `AGENTS.md` — the symlink keeps Claude Code and Cursor reading the same single source. -## Index +## Where to look -### Orientation - -| File | Contents | -|---|---| -| [`STRUCTURE.md`](STRUCTURE.md) | Repository tree and what lives where. | -| [`ARCHITECTURE.md`](ARCHITECTURE.md) | How the engine works: the pipeline, the core modules, and the invariants that hold everywhere in `src/`. | -| [`MONOREPO.md`](MONOREPO.md) | The monorepo layout this repository is moving to. | - -### Subsystems - -| File | Contents | -|---|---| -| [`PARSER.md`](PARSER.md) | `src/parser/` — formula text to AST, and back. | -| [`INTERPRETER.md`](INTERPRETER.md) | `src/interpreter/` — AST to value, and how a built-in function is written. | -| [`DEPENDENCY-GRAPH.md`](DEPENDENCY-GRAPH.md) | `src/DependencyGraph/` — dependency tracking and recalculation order. | -| [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md) | `src/interpreter/functionMetadata/` — what the API and the docs say about each function. | -| [`I18N.md`](I18N.md) | `src/i18n/` — function-name translations and where to source them. | - -### Working on a change - -| File | Contents | -|---|---| -| [`DEFINITION-OF-DONE.md`](DEFINITION-OF-DONE.md) | What every change must include before review. | -| [`CODE-STYLE.md`](CODE-STYLE.md) | Code style, and the performance rules that apply to engine code. | -| [`TESTING.md`](TESTING.md) | Test suites, the private test repository, and how to write a case. | -| [`DOC-STANDARDS.md`](DOC-STANDARDS.md) | When documentation is required, and the rules it must satisfy — guides, API reference, JSDoc, and the changelog. | -| [`DOCS-CONTENT-GUIDE.md`](DOCS-CONTENT-GUIDE.md) | How to write a documentation page: structure, chunking, language, code examples, VuePress conventions, and the self-review checklist. | -| [`BUILD.md`](BUILD.md) | What the public [building guide](../docs/guide/building.md) does not cover: the intermediate build, generated docs, deployment, and release. | -| [`PULL-REQUESTS.md`](PULL-REQUESTS.md) | Branch naming, the pre-flight gate, and the pull request template. | -| [`WORKTREES.md`](WORKTREES.md) | Working in a linked git worktree: what is missing and how to bootstrap it. | -| [`AGENT-TOOLING.md`](AGENT-TOOLING.md) | How this repository is configured for AI agents: settings, hooks, and skills. | - -### Elsewhere - -| Where | For | +| You are working on | Read | |---|---| -| [`docs/README.md`](../docs/README.md) | Running and extending the documentation portal | -| [`test/README.md`](../test/README.md) | Attaching the private test suite | -| [`script/README.md`](../script/README.md) | What each build and release script does | -| [`CHANGELOG.md`](../CHANGELOG.md) | Release history | -| [`.github/pull_request_template.md`](../.github/pull_request_template.md) | The pull request template | +| Anything in `src/` | [`ARCHITECTURE.md`](ARCHITECTURE.md) — the pipeline, the core modules, and the invariants that hold everywhere in `src/` | +| Formula parsing | [`PARSER.md`](PARSER.md) | +| Formula evaluation, or a built-in function | [`INTERPRETER.md`](INTERPRETER.md) | +| Function descriptions in the API and the docs | [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md) | +| Dependency tracking and recalculation order | [`DEPENDENCY-GRAPH.md`](DEPENDENCY-GRAPH.md) | +| Function-name translations | [`I18N.md`](I18N.md) | +| Tests | [`TESTING.md`](TESTING.md) | +| What a change must include before review | [`DEFINITION-OF-DONE.md`](DEFINITION-OF-DONE.md) | +| Style and performance | [`CODE-STYLE.md`](CODE-STYLE.md) | +| Documentation rules, and the changelog | [`DOC-STANDARDS.md`](DOC-STANDARDS.md) | +| Writing a documentation page | [`DOCS-CONTENT-GUIDE.md`](DOCS-CONTENT-GUIDE.md) | +| Building, bundling, releasing | [`BUILD.md`](BUILD.md) | +| Opening a pull request | [`PULL-REQUESTS.md`](PULL-REQUESTS.md) | +| Where things live, and where they are going | [`STRUCTURE.md`](STRUCTURE.md) | +| A linked git worktree | [`WORKTREES.md`](WORKTREES.md) | +| How this repository is set up for agents | [`AGENT-TOOLING.md`](AGENT-TOOLING.md) | +| Step-by-step task workflows | [`.claude/skills/`](../.claude/skills/) | + +Outside this directory: [`docs/README.md`](../docs/README.md) for running the documentation portal, [`test/README.md`](../test/README.md) for attaching the private test suite, [`script/README.md`](../script/README.md) for what each build and release script does, and [`CONTRIBUTING.md`](../CONTRIBUTING.md) for external contributors. ## Conventions diff --git a/dev-docs/STRUCTURE.md b/dev-docs/STRUCTURE.md index fa6cf6487a..7bebef5605 100644 --- a/dev-docs/STRUCTURE.md +++ b/dev-docs/STRUCTURE.md @@ -1,6 +1,6 @@ # Repository structure -The layout as it stands today. For the monorepo layout this repository is moving to, see [`MONOREPO.md`](MONOREPO.md). +Where everything lives today, and where it is going. The repository is becoming a monorepo (HF-359); the second half of this page is the target and the steps that get there. ``` . @@ -66,3 +66,79 @@ Never edit these, and never read them to answer a question about behaviour — r Each of these carries rules that load only when an agent works inside it: `src/`, `src/parser/`, `src/interpreter/`, `src/interpreter/plugin/`, `src/interpreter/functionMetadata/`, `src/DependencyGraph/`, `src/i18n/`, `docs/`, `test/`, `script/`. + +## Where it is going + +The repository is becoming a monorepo. The tree above is what a checkout looks like now; everything below is the target. + +### Packages + +| Package | Directory | Purpose | Published | +|---|---|---|---| +| `hyperformula` | `hyperformula/` | The calculation engine. Everything in `src/` and `test/` today. | yes | +| `hyperformula-ui` | `hyperformula-ui/` | UI components for working with HyperFormula: reference highlighting, inline formula editor, function help. | yes | +| `hyperformula-docs` | `docs/` | The VuePress documentation portal. | no | + +`docs/` is not a workspace member: the portal drags in a large, old dependency tree (VuePress, `--openssl-legacy-provider`) that must not reach an engine install. It keeps its own `package.json` and is installed separately. + +### Target tree + +``` +hyperformula/ # repository root — private, workspace root +├── AGENTS.md # monorepo-wide rules + routing map +├── CLAUDE.md -> AGENTS.md +├── README.md CONTRIBUTING.md CHANGELOG.md LICENSE.txt +├── package.json # private: true, npm workspaces, fan-out scripts +├── package-lock.json +├── .nvmrc # 22, like every other one here +├── .worktreeinclude +├── .claude/ +│ ├── settings.json # hooks, enabledPlugins, worktree settings +│ └── skills/ # ALL skills, scoped by the `paths` frontmatter field +├── dev-docs/ # monorepo-scope reference +│ +├── hyperformula/ # ── package: the engine +│ ├── AGENTS.md CLAUDE.md -> AGENTS.md +│ ├── package.json .nvmrc CHANGELOG.md +│ ├── dev-docs/ # engine-scope reference +│ ├── src/ +│ │ ├── AGENTS.md CLAUDE.md -> AGENTS.md +│ │ ├── parser/ AGENTS.md +│ │ ├── interpreter/ AGENTS.md +│ │ │ ├── plugin/ AGENTS.md +│ │ │ └── functionMetadata/ AGENTS.md +│ │ ├── DependencyGraph/ AGENTS.md +│ │ ├── i18n/languages/ AGENTS.md +│ │ └── dependencyTransformers/ format/ helpers/ Lookup/ statistics/ +│ └── test/ AGENTS.md README.md +│ └── hyperformula-tests/ # private suite, git-ignored, branch-matched +│ +├── hyperformula-ui/ # ── package: the UI components +│ ├── AGENTS.md CLAUDE.md -> AGENTS.md +│ ├── package.json .nvmrc CHANGELOG.md +│ ├── dev-docs/ +│ └── src/ test/ +│ +├── docs/ # ── documentation portal (NOT a workspace member) +│ ├── AGENTS.md CLAUDE.md -> AGENTS.md README.md +│ ├── package.json .nvmrc +│ ├── wrangler.jsonc # deploy config for the portal +│ ├── worker/index.js # Cloudflare Worker serving the built portal +│ └── guide/ api/ .vuepress/ +│ +├── examples/ # images and CSV fixtures used by the docs +├── script/ AGENTS.md README.md +└── .github/workflows/ # path-filtered per-package jobs +``` + +### Migration steps + +1. **Move `src/` and `test/` into `hyperformula/`.** Mechanical, but it invalidates every path in CI, in `tsconfig.json`, `jest.config.js`, `karma.conf.js`, `.eslintignore`, and the docs generator scripts. The private suite's specs need no change: they import the engine relatively, and the depth from a spec to the package root is unchanged. +2. **Add `workspaces` to the root `package.json`** and make it `private: true`. Move the build scripts down into `hyperformula/package.json`, leaving fan-out scripts at the root. +3. **Give `docs/` its own `package.json`** and take it out of the root dependency tree. +4. **Move `wrangler.jsonc` and `worker/` under `docs/`.** Update `wrangler.jsonc`'s `main`, the `docs:*:cf` scripts, and `script/prepare-cf-assets.js` in the same change. Verify with `npm run docs:preview:cf` — a broken `main` path fails only at deploy time. +5. **Bring in `hyperformula-ui`**, preserving its history. +6. **Split `CHANGELOG.md` per package**, each keeping the current Keep a Changelog form. +7. **Give every package an `.nvmrc` saying `22`.** +8. **Update the private test suite's checkout path**, from `test/hyperformula-tests/` to `hyperformula/test/hyperformula-tests/`, in `fetch-tests.sh`, `.gitignore`, the three workflows that check it out, and `.worktreeinclude`. It stays branch-matched. +9. **Path-filter CI.** Each package's jobs run only when its paths change; full runs on `develop`, `master`, and release branches. diff --git a/dev-docs/WORKTREES.md b/dev-docs/WORKTREES.md index 1f38856d36..13151d3d19 100644 --- a/dev-docs/WORKTREES.md +++ b/dev-docs/WORKTREES.md @@ -31,8 +31,8 @@ Claude Code can run a session — or an isolated subagent — in a `git worktree } ``` -This is safe while HyperFormula is a single package: there is exactly one `node_modules/.bin`, and the symlink resolves it. **It stops being safe once the repository becomes a workspace** — package-local `node_modules/.bin` directories are not covered by a root-level symlink, and scripts then die mid-build with a bare `command not found`. Revisit this setting as part of the monorepo migration ([`MONOREPO.md`](MONOREPO.md)). +This is safe while HyperFormula is a single package: there is exactly one `node_modules/.bin`, and the symlink resolves it. **It stops being safe once the repository becomes a workspace** — package-local `node_modules/.bin` directories are not covered by a root-level symlink, and scripts then die mid-build with a bare `command not found`. Revisit this setting as part of the monorepo migration ([`STRUCTURE.md`](STRUCTURE.md#where-it-is-going)). ## Sparse checkouts -`worktree.sparsePaths` limits what git writes to disk. It buys little today — this repository is small and `src/` is needed by everything. It becomes worth setting once the packages in [`MONOREPO.md`](MONOREPO.md) exist and a task can be scoped to one of them. +`worktree.sparsePaths` limits what git writes to disk. It buys little today — this repository is small and `src/` is needed by everything. It becomes worth setting once the packages in [`STRUCTURE.md`](STRUCTURE.md#where-it-is-going) exist and a task can be scoped to one of them. From 83eb116763dcd40bcdb7dfcb1dd25e75e75f1da4 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Tue, 1 Sep 2026 10:33:46 +0200 Subject: [PATCH 10/27] HF-359: Drop the PostToolUse lint hook Removes script/claude/post-tool-use.mjs, its registration in .claude/settings.json, and the documentation that described it. The hook linted every file an agent wrote and reported the remaining errors back. It worked, but it is not something this repository has to carry: lint still runs from the root on demand and in CI, which is where the gate belongs, and the hook added a per-edit cost and a file that rewrote the agent's work underneath it for a convenience nobody asked for. script/ now holds only build, docs-generation and release scripts, and dev-docs/AGENT-TOOLING.md covers settings and skills alone. Co-Authored-By: Claude Opus 5 --- .claude/settings.json | 14 --- dev-docs/AGENT-TOOLING.md | 18 ---- script/AGENTS.md | 2 +- script/README.md | 2 - script/claude/post-tool-use.mjs | 156 -------------------------------- 5 files changed, 1 insertion(+), 191 deletions(-) delete mode 100755 script/claude/post-tool-use.mjs diff --git a/.claude/settings.json b/.claude/settings.json index 53a19a46f5..297d8e0c03 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -28,19 +28,5 @@ "symlinkDirectories": [ "node_modules" ] - }, - "hooks": { - "PostToolUse": [ - { - "matcher": "Edit|Write", - "hooks": [ - { - "type": "command", - "command": "node \"$CLAUDE_PROJECT_DIR/script/claude/post-tool-use.mjs\"", - "timeout": 60 - } - ] - } - ] } } diff --git a/dev-docs/AGENT-TOOLING.md b/dev-docs/AGENT-TOOLING.md index 3c372dfbe5..b12d2471db 100644 --- a/dev-docs/AGENT-TOOLING.md +++ b/dev-docs/AGENT-TOOLING.md @@ -23,29 +23,11 @@ Committed, so every developer gets the same setup. | `enabledPlugins` | `typescript-lsp` — language-server go-to-definition and find-references. Use it instead of grepping for a symbol's definition or callers; grep stays right for text searches. | | `permissions.deny` | Blocks agent reads of every generated and built path. The build outputs are git-ignored, so content searches already skip them, but nothing otherwise stops an agent opening `dist/hyperformula.js` or answering a behaviour question from `typings/` instead of `src/`. | | `worktree.symlinkDirectories` | Symlinks `node_modules` into each worktree rather than duplicating it. See [`WORKTREES.md`](WORKTREES.md). | -| `hooks` | The `PostToolUse` lint hook below. | `node_modules/` and `package-lock.json` are deliberately readable: reading a dependency's source is sometimes the right move when debugging, and a deny rule would also block a targeted grep for a dependency version. Relative deny patterns anchor at the session's working directory, and project settings are not inherited from parent directories — these rules apply to sessions started at the repository root. -## The `PostToolUse` lint hook - -[`script/claude/post-tool-use.mjs`](../script/claude/post-tool-use.mjs), matched on `Edit|Write`. Claude Code passes the tool payload as JSON on stdin; the hook reads `tool_input.file_path`, lints that one file, and exits 2 with the remaining errors on stderr, which Claude Code shows to the agent. ESLint rules are applied while the change is being written rather than when someone runs `npm run lint` at the end. - -Three properties to preserve when changing it: - -- **Errors only, never warnings.** `npm run lint` reports tens of thousands of warnings across the repository. Reporting them per edit would bury the agent in noise unrelated to the change it just made. -- **`--fix-type problem,layout`.** Never plain `--fix`. `jsdoc/require-jsdoc` is a suggestion-type rule whose autofix inserts an **empty** JSDoc block above every undocumented declaration, so an unconstrained `--fix` quietly scatters those stubs through any file the agent edits. The restriction keeps the fixes that are unambiguously right — the licence header, semicolons, quotes, spacing — and drops the ones that need a human. -- **Fails open.** A missing ESLint binary, a spawn failure, or unparseable output exits 0 silently. A broken hook must never block work. - -`script/` is in [`.eslintignore`](../.eslintignore), so the hook is not linted by itself. Verify a change to it by piping a payload in by hand: - -```bash -printf '{"tool_name":"Edit","cwd":"'"$PWD"'","tool_input":{"file_path":"src/interpreter/plugin/AbsPlugin.ts"}}' \ - | node script/claude/post-tool-use.mjs; echo "exit=$?" -``` - ## Skills All skills live in `.claude/skills/`, at the repository root, and are scoped by the `paths` frontmatter field rather than by placement — one glob, or a comma-separated list. One directory to look in, one directory to keep consistent. diff --git a/script/AGENTS.md b/script/AGENTS.md index 53638a139f..5911c82f04 100644 --- a/script/AGENTS.md +++ b/script/AGENTS.md @@ -1,6 +1,6 @@ # `script/` -Build, docs-generation, release, and agent-hook scripts invoked from `package.json`. +Build, docs-generation, and release scripts invoked from `package.json`. - What each script does and the rules for changing one: [`README.md`](README.md) - What the build produces: [`dev-docs/BUILD.md`](../dev-docs/BUILD.md) diff --git a/script/README.md b/script/README.md index de0c721769..972e4e84d4 100644 --- a/script/README.md +++ b/script/README.md @@ -11,12 +11,10 @@ Node and shell scripts invoked from `package.json`. Not shipped, not tested, not | `prepare-cf-assets.js` | `docs:build:cf` | Composes the Cloudflare Worker asset tree | | `if-ne-env.js` | the `bundle:*` scripts | Skips a recompile when `HF_COMPILE` is already set | | `release/` | `npm run release` | The release procedure | -| `claude/post-tool-use.mjs` | the Claude Code `PostToolUse` hook | Lints the file an agent just edited and reports the remaining ESLint errors back to it | ## Before changing one - What the build produces and how these scripts fit into it: [`dev-docs/BUILD.md`](../dev-docs/BUILD.md) - Why the docs generator fails loudly rather than skipping: [`dev-docs/FUNCTION-CATALOGUE.md`](../dev-docs/FUNCTION-CATALOGUE.md) -- The `claude/` hook's design constraints, and how to verify a change to it: [`dev-docs/AGENT-TOOLING.md`](../dev-docs/AGENT-TOOLING.md) Two things that are true only here: these scripts run in CI as well as locally, so they must not assume an interactive terminal, a working directory, or a developer's environment variables; and nothing here is covered by tests, so a change is verified by running it. diff --git a/script/claude/post-tool-use.mjs b/script/claude/post-tool-use.mjs deleted file mode 100755 index cb33f40666..0000000000 --- a/script/claude/post-tool-use.mjs +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env node -/** - * Claude Code PostToolUse hook, matched on `Edit|Write`. - * - * Lints the file the agent just wrote and feeds the remaining ESLint *errors* - * back to the model (exit 2), so style and correctness rules are applied at - * authoring time instead of surfacing at the end of the session. - * - * Two deliberate choices: - * - * - Errors only. `npm run lint` currently reports tens of thousands of - * warnings across the repository; reporting them per edit would drown the - * model in noise unrelated to the change it just made. - * - `--fix-type problem,layout`. Plain `--fix` also applies suggestion-type - * fixes, and `jsdoc/require-jsdoc` autofixes by inserting an EMPTY JSDoc - * block above every undocumented declaration. Left unconstrained, the hook - * silently sprinkles those stubs through any source file it touches. - * - Fails open. Any problem with the hook itself (no ESLint binary, spawn - * failure, unparseable output) exits 0 and stays silent. A broken hook must - * never block work. - * - * The tool payload arrives as JSON on stdin; there is no environment variable - * carrying the edited path. - */ -import { spawnSync } from 'node:child_process' -import { readFileSync } from 'node:fs' -import path from 'node:path' - -// npx is a .cmd shim on Windows; spawnSync needs a shell there or it ENOENTs. -const WINDOWS = process.platform === 'win32' - -const LINTABLE_EXTENSIONS = new Set(['.ts', '.js']) - -/** Maximum number of errors reported back to the model for a single file. */ -const MAX_REPORTED_ERRORS = 20 - -/** - * Reads the whole of stdin synchronously. - * - * @returns {string} Raw stdin contents, or an empty string when there is none. - */ -function readStdin() { - try { - // Read fd 0 directly - cross-platform, unlike spawning `cat`. - return readFileSync(0, 'utf8') - } catch { - return '' - } -} - -/** - * Extracts the edited file path from a PostToolUse payload. - * - * @param {string} rawPayload Raw JSON received on stdin. - * @returns {string | undefined} Absolute path of the edited file, if there is one. - */ -function editedFilePath(rawPayload) { - try { - const payload = JSON.parse(rawPayload) - const filePath = payload?.tool_input?.file_path - - if (typeof filePath !== 'string' || filePath.length === 0) { - return undefined - } - - const projectDir = process.env.CLAUDE_PROJECT_DIR ?? payload?.cwd ?? process.cwd() - - return path.resolve(projectDir, filePath) - } catch { - return undefined - } -} - -/** - * Runs ESLint with `--fix` over a single file. - * - * @param {string} filePath Absolute path of the file to lint. - * @returns {object[] | undefined} ESLint JSON results, or `undefined` when the run itself failed. - */ -function lintAndFix(filePath) { - const result = spawnSync( - 'npx', - ['--no-install', 'eslint', '--fix', '--fix-type', 'problem,layout', '--format', 'json', filePath], - { encoding: 'utf8', shell: WINDOWS, maxBuffer: 10 * 1024 * 1024 } - ) - - // ESLint exits 1 when it reports errors, which is the case this hook exists - // for. Anything else - a missing binary, a crash - means the run failed and - // the hook has nothing trustworthy to say. - if (result.error || result.status === null || result.status > 1) { - return undefined - } - - try { - return JSON.parse(result.stdout) - } catch { - return undefined - } -} - -/** - * Collects the ESLint messages of severity `error` from a set of results. - * - * @param {object[]} results ESLint JSON results. - * @returns {object[]} The error-severity messages. - */ -function errorsOnly(results) { - return results.flatMap(result => (result.messages ?? []).filter(message => message.severity === 2)) -} - -/** - * Renders the errors as a message for the model. - * - * @param {string} filePath Absolute path of the linted file. - * @param {object[]} errors The error-severity messages. - * @returns {string} The message to write to stderr. - */ -function formatReport(filePath, errors) { - const projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd() - const relativePath = path.relative(projectDir, filePath) || filePath - const shown = errors.slice(0, MAX_REPORTED_ERRORS) - const lines = shown.map( - ({ line, column, message, ruleId }) => ` ${relativePath}:${line}:${column} ${message}${ruleId ? ` (${ruleId})` : ''}` - ) - - if (errors.length > shown.length) { - lines.push(` ...and ${errors.length - shown.length} more.`) - } - - return [ - `ESLint reports ${errors.length} error${errors.length === 1 ? '' : 's'} in ${relativePath} that --fix could not resolve:`, - ...lines, - 'Fix them now. ESLint is the source of truth for formatting and code rules in this repository.', - ].join('\n') -} - -const filePath = editedFilePath(readStdin()) - -if (filePath === undefined || !LINTABLE_EXTENSIONS.has(path.extname(filePath))) { - process.exit(0) -} - -const results = lintAndFix(filePath) - -if (results === undefined) { - process.exit(0) -} - -const errors = errorsOnly(results) - -if (errors.length === 0) { - process.exit(0) -} - -process.stderr.write(`${formatReport(filePath, errors)}\n`) -process.exit(2) From a000d8f80499c8d75e18d25765aa1b8e4f03b983 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Tue, 1 Sep 2026 12:18:06 +0200 Subject: [PATCH 11/27] HF-359: Correct what the dev-docs migration got wrong An audit traced the deleted DEV_DOCS.md and the rewritten AGENTS.md into the new tree. Almost nothing was lost; what it found instead were assertions the new prose invented, and rules that the split left in two places where they promptly disagreed. dev-docs/README.md told the reader to write repository-root-relative link targets. The directory it governs contains 33 filesystem-relative link targets and not one root-relative one, eight of them in that same file. An agent obeying the stated rule writes links that resolve nowhere. The convention now describes what the tree does, separating link targets from paths named in prose. "The five places a function change must touch" was a term of art naming two different sets: INTERPRETER.md omitted the changelog and counted implementedFunctions separately, while the review skill counted the changelog and folded implementedFunctions in. INTERPRETER.md is now the one list, and both skills link to it instead of keeping a copy. The same list claimed all five omissions fail silently, contradicting FUNCTION-CATALOGUE.md, which correctly says a missing catalogue entry fails the docs build; it now states the split. Two file locations were wrong in the same way, and last round only one of them was fixed: absolutizeDependencies.ts and LazilyTransformingAstService.ts both live at the source root, not in the parser and dependency-graph directories that listed them. I18N.md claimed argument separators are part of the language package. They are configuration - functionArgSeparator and its neighbours in ConfigParams - and nothing in the old documentation said otherwise. Also corrected: test:compatibility pointed at a path in neither repository, and the prose blamed its failure on a missing private suite; BUILD.md presented the portal deploy commands with no hint that deployment is automated on push and these exist for debugging; AGENT-TOOLING.md said every skill is scoped by a paths glob, when four of nine deliberately are not; STRUCTURE.md inventoried a .claude/agents/ that does not exist; the root AGENTS.md stated the single-source rule without the "only what is specific to its own context" exception its two other copies carry; CODE-STYLE.md claimed everything below its intro is what ESLint cannot check, while the jsdoc rules are enabled, and had lost the deep link to the linter section of the building guide; docs/README.md still said npm install where BUILD.md says npm ci; and DOC-STANDARDS.md stated the changelog exemption twice with two different scopes. The rule that agents should read the repository's own documentation rather than fetching the rendered portal was dropped in the rewrite and is restored. Two findings were not acted on: one describes the PostToolUse hook, which has since been deleted, and one is a wording quibble about which generator runs first. Co-Authored-By: Claude Opus 5 --- .claude/skills/hyperformula-code-review/SKILL.md | 2 +- .claude/skills/hyperformula-function-dev/SKILL.md | 10 +++------- .claude/skills/hyperformula-unit-testing/SKILL.md | 2 +- AGENTS.md | 2 +- dev-docs/AGENT-TOOLING.md | 6 +++++- dev-docs/ARCHITECTURE.md | 4 ++-- dev-docs/BUILD.md | 2 +- dev-docs/CODE-STYLE.md | 2 +- dev-docs/DOC-STANDARDS.md | 2 +- dev-docs/FUNCTION-CATALOGUE.md | 2 +- dev-docs/I18N.md | 2 +- dev-docs/INTERPRETER.md | 14 ++++++++------ dev-docs/README.md | 3 ++- dev-docs/STRUCTURE.md | 2 +- dev-docs/TESTING.md | 4 ++-- docs/README.md | 2 +- package.json | 2 +- 17 files changed, 33 insertions(+), 30 deletions(-) diff --git a/.claude/skills/hyperformula-code-review/SKILL.md b/.claude/skills/hyperformula-code-review/SKILL.md index acffff47d1..a63ab845cb 100644 --- a/.claude/skills/hyperformula-code-review/SKILL.md +++ b/.claude/skills/hyperformula-code-review/SKILL.md @@ -29,7 +29,7 @@ Review in the order below, and stop to report the first serious finding rather t ## 3. Completeness of a function change -All five places, three of which fail silently: implementation, catalogue entry with a matching parameter count, every language file, tests, changelog. Plus `sizeOfResultArrayMethod` for anything array-returning, and an explicit `optionalArg` where arity does not express the valid call. Skill `hyperformula-function-dev`. +Check every one of [the five places a function change must touch](../../../dev-docs/INTERPRETER.md#the-five-places-a-function-change-must-touch); most of them fail silently when missed. Plus `sizeOfResultArrayMethod` for anything array-returning, and an explicit `optionalArg` where arity does not express the valid call. Skill `hyperformula-function-dev`. ## 4. Performance diff --git a/.claude/skills/hyperformula-function-dev/SKILL.md b/.claude/skills/hyperformula-function-dev/SKILL.md index e5e50d48bc..d0cb5c39bd 100644 --- a/.claude/skills/hyperformula-function-dev/SKILL.md +++ b/.claude/skills/hyperformula-function-dev/SKILL.md @@ -9,20 +9,16 @@ description: Use when adding a new built-in spreadsheet function to HyperFormula | File | Why | |---|---| | [`INTERPRETER.md`](../../../dev-docs/INTERPRETER.md#built-in-functions) | The plugin contract, `runFunction`, and every argument and function metadata field. Read this before writing any code. | -| [`FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md) | What the catalogue entry must contain, and the two failure modes that are silent | +| [`FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md) | What the catalogue entry must contain, and the two ways to get it wrong | | [`I18N.md`](../../../dev-docs/I18N.md) | Where to source a translation, and why an invented one cannot be taken back | | [`TESTING.md`](../../../dev-docs/TESTING.md#what-each-kind-of-change-needs) | The list of cases a function change must cover | | [`DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md) | What the change must contain before review | ## 2. Touch all five places -A function is not done until all five agree. Each omission fails differently, and three of them fail silently. +A function is not done until all five agree, and they do not fail the same way — a missing catalogue entry fails the docs build, a parameter-count mismatch only warns on the console, and the rest fail silently. -1. **Implementation** — the plugin class in `src/interpreter/plugin/`, plus its `implementedFunctions` entry keyed by the canonical English id. -2. **Catalogue entry** — `src/interpreter/functionMetadata/categories/.ts`. Parameter **count** must match `implementedFunctions`, or the authored names and descriptions are discarded at run time with only a console warning. A missing entry fails the docs build. -3. **Translations** — every file in `src/i18n/languages/`. -4. **Tests** — in `test/`. -5. **Changelog** — skill `changelog-creation`. +The list is in [`INTERPRETER.md`](../../../dev-docs/INTERPRETER.md#the-five-places-a-function-change-must-touch). Work through it there rather than from a copy; two of the five have their own page, linked from it. ## 3. Declare the two things nothing cross-checks diff --git a/.claude/skills/hyperformula-unit-testing/SKILL.md b/.claude/skills/hyperformula-unit-testing/SKILL.md index d6596ae850..d3d6605efc 100644 --- a/.claude/skills/hyperformula-unit-testing/SKILL.md +++ b/.claude/skills/hyperformula-unit-testing/SKILL.md @@ -19,7 +19,7 @@ And [`test/README.md`](../../../test/README.md) for how the private suite is fet npm run test:setup-private ``` -Run it after every branch switch. Without `test/hyperformula-tests/` the Jest run covers only the smoke tests and reports a clean pass over almost nothing — the most common false signal in this repository. `test:performance` and `test:compatibility` fail on a missing path rather than an assertion; read the error before concluding the code is broken. +Run it after every branch switch. Without `test/hyperformula-tests/` the Jest run covers only the smoke tests and reports a clean pass over almost nothing — the most common false signal in this repository. `test:performance`, `test:compatibility` and the benchmarks all resolve into that directory, so without it they fail on a missing path rather than on an assertion; read the error before concluding the code is broken. ## 3. Write the case from the requirement diff --git a/AGENTS.md b/AGENTS.md index d3a5fbcfde..84ab063818 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ HyperFormula is a headless spreadsheet calculation engine in TypeScript. No UI, ## Start at `dev-docs/` -**[`dev-docs/`](dev-docs/) is the single source of truth for everything internal to this project.** Architecture, conventions, build, testing, standards, and the monorepo plan all live there and nowhere else. This file routes; it does not explain. Neither does any other `AGENTS.md` or `README.md` — if one of them looks like it is explaining something, the explanation belongs in `dev-docs/` and the file should link to it. +**[`dev-docs/`](dev-docs/) is the single source of truth for everything internal to this project.** Architecture, conventions, build, testing, standards, and the monorepo plan all live there and nowhere else. This file routes; it does not explain. Neither does any other `AGENTS.md` or `README.md`: each carries only what is so specific to its own directory that it would be useless anywhere else, and links for the rest. If one of them looks like it is explaining something general, the explanation belongs in `dev-docs/`. Read [`dev-docs/README.md`](dev-docs/README.md) first. It says which page covers what, and every directory's own `AGENTS.md` points at the page that covers that directory. diff --git a/dev-docs/AGENT-TOOLING.md b/dev-docs/AGENT-TOOLING.md index b12d2471db..ecdb32c016 100644 --- a/dev-docs/AGENT-TOOLING.md +++ b/dev-docs/AGENT-TOOLING.md @@ -28,9 +28,13 @@ Committed, so every developer gets the same setup. Relative deny patterns anchor at the session's working directory, and project settings are not inherited from parent directories — these rules apply to sessions started at the repository root. +## Read the repository, not the web + +The guides in `docs/guide/` and the generated API reference in `docs/api/` are the same content the documentation portal serves. Read the local files rather than fetching the rendered pages, and read `src/` rather than either when the question is what the code actually does. + ## Skills -All skills live in `.claude/skills/`, at the repository root, and are scoped by the `paths` frontmatter field rather than by placement — one glob, or a comma-separated list. One directory to look in, one directory to keep consistent. +All skills live in `.claude/skills/`, at the repository root — one directory to look in, one to keep consistent. A skill that belongs to part of the tree is scoped by the `paths` frontmatter field (one glob, or a comma-separated list) rather than by placement. A skill that applies anywhere — writing a changelog entry, opening a pull request, test discipline, reviewing a diff — carries no `paths` and is chosen from its description alone. A skill holds the **steps**: what to do, in what order, and what to check. It does not restate the rules those steps enforce — it links to the `dev-docs/` page that owns them. diff --git a/dev-docs/ARCHITECTURE.md b/dev-docs/ARCHITECTURE.md index 3bb617ce39..bf1bf45b39 100644 --- a/dev-docs/ARCHITECTURE.md +++ b/dev-docs/ARCHITECTURE.md @@ -56,7 +56,7 @@ HyperFormula is a headless spreadsheet calculation engine. No UI, no DOM, no ser - `LexerConfig.ts` and `ParserConfig.ts` build the token set from the active language and config — the lexer is **language-dependent**, because function names are translated. - `ParserWithCaching.ts` is the entry point; identical formula strings resolve from `Cache.ts` rather than being reparsed. -- `collectDependencies.ts` extracts the relative dependencies of an AST; `absolutizeDependencies.ts` resolves them against a concrete address. +- `collectDependencies.ts` extracts the relative dependencies of an AST. Resolving them against a concrete address is `src/absolutizeDependencies.ts`, which sits at the source root rather than in `src/parser/`. - `Unparser.ts` is the inverse — AST back to text, in the target language. A change to parsing almost always needs a matching change here, or round-tripping breaks. ## The interpreter @@ -75,7 +75,7 @@ HyperFormula is a headless spreadsheet calculation engine. No UI, no DOM, no ser - `Graph.ts` holds the vertices and edges; `TopSort.ts` produces the evaluation order and detects cycles. - Vertex kinds: `ValueCellVertex`, `FormulaVertex`, `EmptyCellVertex`, `RangeVertex`, `ParsingErrorVertex`. - `AddressMapping/` maps a `SimpleCellAddress` to its vertex; `RangeMapping.ts` does the same for ranges, so a range is a single vertex rather than one edge per cell. -- `LazilyTransformingAstService.ts` defers AST rewrites after structural changes until a formula is actually read — a change here affects both correctness and the CRUD hot path. +- `src/LazilyTransformingAstService.ts` — at the source root, not in this directory — defers AST rewrites after structural changes until a formula is actually read. A change there affects both correctness and the CRUD hot path. ## Invariants diff --git a/dev-docs/BUILD.md b/dev-docs/BUILD.md index d4645d0d5f..785fad27ca 100644 --- a/dev-docs/BUILD.md +++ b/dev-docs/BUILD.md @@ -29,7 +29,7 @@ Both are git-ignored, and both are regenerated as the first step of `docs:dev` a ## Deploying the portal -The portal is served by a Cloudflare Worker. +The portal is served by a Cloudflare Worker, and **deployment is driven by Workers Builds on push, not from a developer's machine**: `master` deploys to production, every other branch uploads a preview version. The commands below exist for debugging that pipeline. The triggers, the Worker name, and the dashboard build settings are in [`docs/README.md`](../docs/README.md#deployment). | Command | Does | |---|---| diff --git a/dev-docs/CODE-STYLE.md b/dev-docs/CODE-STYLE.md index 85ee2231cd..0edd7f670a 100644 --- a/dev-docs/CODE-STYLE.md +++ b/dev-docs/CODE-STYLE.md @@ -1,6 +1,6 @@ # Code style -ESLint is the source of truth for formatting and code rules. Run `npm run lint` before submitting changes. Everything below is what ESLint cannot check. +ESLint is the source of truth for formatting and code rules — run `npm run lint` before submitting changes (see [the linter section of the building guide](../docs/guide/building.md#run-the-linter)). Some of what follows it enforces, the `jsdoc` rules among them; the rest is what no linter can check for you. ## Style diff --git a/dev-docs/DOC-STANDARDS.md b/dev-docs/DOC-STANDARDS.md index cceaa0a562..1e34f7aff7 100644 --- a/dev-docs/DOC-STANDARDS.md +++ b/dev-docs/DOC-STANDARDS.md @@ -9,7 +9,7 @@ These apply across the whole repository: guides, the API reference, JSDoc inside - Any public-API change updates the JSDoc **and** the affected guides. - Any user-facing behaviour change is documented in the same pull request as the change. - Any breaking change adds a migration-guide section. -- Documentation-only changes (guides, JSDoc, README) need no changelog entry. +- Documentation-only changes need no changelog entry. The full exemption is under [The changelog](#the-changelog). ## The API reference is the source of truth diff --git a/dev-docs/FUNCTION-CATALOGUE.md b/dev-docs/FUNCTION-CATALOGUE.md index 1d230413dc..f114483e51 100644 --- a/dev-docs/FUNCTION-CATALOGUE.md +++ b/dev-docs/FUNCTION-CATALOGUE.md @@ -4,7 +4,7 @@ It is the single source of truth for two consumers: -- the public [`getAvailableFunctions` / `getFunctionDetails`](../docs/api/classes/hyperformula.md) API; +- the public `getAvailableFunctions` / `getFunctionDetails` API, documented in the generated API reference (`docs/api/`, built by `npm run typedoc:build-api`); - the generated built-in functions guide page (`docs/guide/built-in-functions.md`, produced by `npm run docs:generate-function-docs`). Every field is required, `documentationUrl` included. Each entry authors its own link rather than inheriting a shared default, so links can diverge per function without touching any code; they all happen to point at the same guide page today. diff --git a/dev-docs/I18N.md b/dev-docs/I18N.md index 767014dc88..488ecbf875 100644 --- a/dev-docs/I18N.md +++ b/dev-docs/I18N.md @@ -17,7 +17,7 @@ Parsing is language-dependent: the lexer builds its token set from the active tr ## Rules -- Translate the function **name** only. Argument separators, error literals, and boolean literals are part of the language package too; do not invent values for them. +- Translate the function **name** only. Error literals and boolean literals are part of the language package as well, and are not yours to invent. The argument, decimal and thousands separators are **not**: they are configuration (`functionArgSeparator` and friends in `ConfigParams`), independent of the language pack. - Keep the key set identical across every language file. Adding a key to one file and not the rest is the common failure. - Do not reorder existing entries while adding one — it turns a one-line diff into an unreviewable one. diff --git a/dev-docs/INTERPRETER.md b/dev-docs/INTERPRETER.md index d858760880..7119369f68 100644 --- a/dev-docs/INTERPRETER.md +++ b/dev-docs/INTERPRETER.md @@ -84,12 +84,14 @@ Pass the raw args, the state, `this.metadata('ID')`, and a plain implementation ### The five places a function change must touch -All five, or the failure is silent: +This list is the authority; the skills link here rather than repeating it. -1. the plugin implementation in `src/interpreter/plugin/`; -2. its `implementedFunctions` metadata; -3. the catalogue entry in `src/interpreter/functionMetadata/categories/` — see [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md); -4. **every** language file in `src/i18n/languages/` — see [`I18N.md`](I18N.md); -5. tests — see [`TESTING.md`](TESTING.md). +1. the plugin implementation in `src/interpreter/plugin/`, plus its `implementedFunctions` metadata; +2. the catalogue entry in `src/interpreter/functionMetadata/categories/` — see [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md); +3. **every** language file in `src/i18n/languages/` — see [`I18N.md`](I18N.md); +4. tests — see [`TESTING.md`](TESTING.md); +5. the changelog entry — see [`DEFINITION-OF-DONE.md`](DEFINITION-OF-DONE.md). + +They do not fail the same way. A missing catalogue entry **fails the docs build**; a parameter-count mismatch only warns on the console; a missing translation, a missing test, and a missing `optionalArg` fail silently. Skill: `hyperformula-function-dev`. diff --git a/dev-docs/README.md b/dev-docs/README.md index b6a20076fb..6ac11238b1 100644 --- a/dev-docs/README.md +++ b/dev-docs/README.md @@ -40,7 +40,8 @@ Outside this directory: [`docs/README.md`](../docs/README.md) for running the do ## Conventions -- Cross-references use repository-root-relative paths (for example `dev-docs/TESTING.md`), not filesystem-relative `../` paths. +- Markdown **link targets** are filesystem-relative — `../test/README.md` from here, `../../../dev-docs/TESTING.md` from a nested `AGENTS.md` — so they resolve on GitHub and in an editor. +- A path **named in prose** rather than linked is spelled from the repository root: `dev-docs/TESTING.md`, `src/interpreter/plugin/`. - Diagrams live only in `dev-docs/`, never in the always-loaded `AGENTS.md` files. - Public, user-facing documentation belongs in [`docs/`](../docs/), not here. `dev-docs/` never ships. - **Nothing outside this directory restates what is in it.** `AGENTS.md`, `README.md`, and `SKILL.md` files carry only what is so specific to their own context that it would be useless anywhere else; everything else is a link. Two copies of a rule means one of them is wrong within a release, and the reader cannot tell which. diff --git a/dev-docs/STRUCTURE.md b/dev-docs/STRUCTURE.md index 7bebef5605..5b00d10291 100644 --- a/dev-docs/STRUCTURE.md +++ b/dev-docs/STRUCTURE.md @@ -37,7 +37,7 @@ Where everything lives today, and where it is going. The repository is becoming ├── worker/ # Cloudflare Worker that serves the built docs ├── examples/ # Images and CSV fixtures used by the docs ├── dev-docs/ # Developer reference (this directory) -├── .claude/ # Claude Code settings, skills, and agents +├── .claude/ # Claude Code settings and skills ├── .github/ # CI workflows, issue and PR templates ├── AGENTS.md # Always-loaded agent rules and routing map ├── CLAUDE.md # Symlink to AGENTS.md diff --git a/dev-docs/TESTING.md b/dev-docs/TESTING.md index 9d6b9c47ca..a136dc5af4 100644 --- a/dev-docs/TESTING.md +++ b/dev-docs/TESTING.md @@ -26,10 +26,10 @@ npm run test:setup-private | `npm run test:watch` | Jest in watch mode | | `npm run test:coverage` | Jest with coverage | | `npm run test:browser` | Karma, against the `dist` build | -| `npm run test:compatibility` | `test/compatibility/test-compatibility.sh` | +| `npm run test:compatibility` | The compatibility suite, which ships with the private repository | | `npm run test:performance` | The basic and CRUD benchmarks | -`test:performance`, `test:compatibility`, and the benchmark scripts all live inside `test/hyperformula-tests/`. Without the private suite they fail with a missing path, not with a test failure — read the error before concluding that something is broken. +`test:performance`, `test:compatibility`, and the benchmark scripts all resolve into `test/hyperformula-tests/`, so they need the private suite attached. Without it they fail on a missing path rather than on an assertion — read the error before concluding the code is broken. ## What a change must cover diff --git a/docs/README.md b/docs/README.md index 98224192d4..58b6d0c2ee 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,7 +17,7 @@ To start a local HyperFormula docs server: 1. Make sure you're running the [Node.js](https://nodejs.org/en/) version pinned in [`.nvmrc`](../.nvmrc) (22). 2. From the main `hyperformula` directory, install the docs dependencies: ```bash - npm install + npm ci ``` 3. From the main `hyperformula` directory, build HyperFormula: ```bash diff --git a/package.json b/package.json index ca901db20b..26f08a8733 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "test:coverage": "npm run test:jest -- --coverage", "test:logMemory": "npm run test:jest -- --runInBand --logHeapUsage", "test:performance": "npm run benchmark:basic && npm run benchmark:cruds", - "test:compatibility": "bash test/compatibility/test-compatibility.sh", + "test:compatibility": "bash test/hyperformula-tests/compatibility/test-compatibility.sh", "test:ci": "cross-env NODE_ICU_DATA=node_modules/full-icu node --expose-gc ./node_modules/jest/bin/jest --forceExit", "test:browser": "cross-env-shell BABEL_ENV=dist env-cmd -f ht.config.js karma start", "test:browser.debug": "cross-env-shell BABEL_ENV=dist NODE_ENV=debug env-cmd -f ht.config.js karma start", From 0c2372b6faa2e60a12eb959e034a8ef98e9da461 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Tue, 1 Sep 2026 12:27:49 +0200 Subject: [PATCH 12/27] HF-359: Release the packages together, from one changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The monorepo plan had every package versioning and releasing on its own cadence, each with its own CHANGELOG.md. Reversed: the packages release together on one version, and CHANGELOG.md at the repository root stays the single history for all of them. One number to reason about — the version a user reports a bug against identifies the state of the whole repository — at the cost of publishing a package whose code did not move. Entries name the package they concern when the text does not make it obvious. The published package still needs a changelog inside its tarball, so the plan now says the release copies the root file in at pack time rather than keeping a second copy under version control. Co-Authored-By: Claude Opus 5 --- dev-docs/DOC-STANDARDS.md | 2 +- dev-docs/STRUCTURE.md | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/dev-docs/DOC-STANDARDS.md b/dev-docs/DOC-STANDARDS.md index 1e34f7aff7..c5df378a29 100644 --- a/dev-docs/DOC-STANDARDS.md +++ b/dev-docs/DOC-STANDARDS.md @@ -32,7 +32,7 @@ Describe **HyperFormula's** behaviour, not Excel's. HyperFormula deliberately de ## The changelog -`CHANGELOG.md` follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/); the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Entries go under `## [Unreleased]`, in the section matching the change — create the `### ` heading if it is not there yet. +`CHANGELOG.md` at the repository root is the single history for every package — they release together, on one version. It follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/); the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Entries go under `## [Unreleased]`, in the section matching the change — create the `### ` heading if it is not there yet. Name the package an entry concerns when it is not obvious from the text. | Section | For | |---|---| diff --git a/dev-docs/STRUCTURE.md b/dev-docs/STRUCTURE.md index 5b00d10291..072f718fd0 100644 --- a/dev-docs/STRUCTURE.md +++ b/dev-docs/STRUCTURE.md @@ -81,6 +81,8 @@ The repository is becoming a monorepo. The tree above is what a checkout looks l `docs/` is not a workspace member: the portal drags in a large, old dependency tree (VuePress, `--openssl-legacy-provider`) that must not reach an engine install. It keeps its own `package.json` and is installed separately. +**The packages release together, on one version, from one changelog.** A release cuts every published package at the same version, whether or not each one changed, and `CHANGELOG.md` at the repository root is the single history for all of them. That keeps one number to reason about — the version a user reports a bug against identifies the state of the whole repository — at the cost of publishing a package whose code did not move. Entries name the package they concern where it is not obvious. + ### Target tree ``` @@ -138,7 +140,7 @@ hyperformula/ # repository root — private, worksp 3. **Give `docs/` its own `package.json`** and take it out of the root dependency tree. 4. **Move `wrangler.jsonc` and `worker/` under `docs/`.** Update `wrangler.jsonc`'s `main`, the `docs:*:cf` scripts, and `script/prepare-cf-assets.js` in the same change. Verify with `npm run docs:preview:cf` — a broken `main` path fails only at deploy time. 5. **Bring in `hyperformula-ui`**, preserving its history. -6. **Split `CHANGELOG.md` per package**, each keeping the current Keep a Changelog form. -7. **Give every package an `.nvmrc` saying `22`.** +6. **Keep `CHANGELOG.md` at the repository root**, in the current Keep a Changelog form, as the single history for every package. The published package still needs one in its tarball, so the release copies it in at pack time rather than keeping a second file under version control. +7. **Give every package an `.nvmrc` saying `22`**, and keep their versions in step — a release bumps them together. 8. **Update the private test suite's checkout path**, from `test/hyperformula-tests/` to `hyperformula/test/hyperformula-tests/`, in `fetch-tests.sh`, `.gitignore`, the three workflows that check it out, and `.worktreeinclude`. It stays branch-matched. 9. **Path-filter CI.** Each package's jobs run only when its paths change; full runs on `develop`, `master`, and release branches. From 72001ee16912b4334b7ba43c3d9b8b8a94bc4048 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Tue, 1 Sep 2026 12:50:07 +0200 Subject: [PATCH 13/27] HF-359: Restructure the repository as a monorepo (#1758) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Context Turns the repository into a monorepo (HF-359). Stacked on #1757 — review that first; this branch is based on it. ``` hyperformula/ the engine (published) — src/, test/, its toolchain, dev-docs/, script/ hyperformula-ui/ placeholder — imported separately, preserving history docs/ the portal — own package, installs on its own script/ release/ and the licence gate — the only things that span the repo dev-docs/ repository-wide standards and process ``` **No changes under `hyperformula/src/`.** Every file there is byte-identical to its old location at `src/`; git records the moves as renames throughout, so history follows. The published package's name, version, entry points and `exports` map are unchanged, and the publish-package check confirms the tarball contents. #### The layout - **npm workspaces**, with a private root carrying fan-out scripts. **Linting is the deliberate exception** — it runs once from the root over the whole repository, so nothing between packages falls through the gap. - **`docs/` left the workspace.** VuePress 1.x and its `--openssl-legacy-provider` tree must never reach an engine install. `npm run docs:install` installs it; CI installs it before building the portal. - **The portal owns its deployment**: `wrangler.jsonc` and `worker/` moved under `docs/`, and the asset directory is now `./.vuepress/dist`. - **Per-package `.nvmrc` (all `22`) and `CHANGELOG.md`.** The root changelog is an index. - **The private test suite stays branch-matched**; only its checkout path moved. Its 501 spec files needed no edit — they import the engine relatively, and because `src/` and `test/` moved *together*, the depth from a spec to the package root is unchanged. #### Everything lives with what owns it Three directories are scoped by ownership rather than by convenience: | | Repository level | Package level | |---|---|---| | `dev-docs/` | definition of done, code style, testing standards, documentation rules, build and release, pull requests, worktrees, agent setup | `hyperformula/dev-docs/` — architecture, parser, interpreter, dependency graph, function catalogue, i18n, performance, its suites, its build | | `script/` | `release/`, `check-licenses.mjs` | `hyperformula/script/` — build checks; `docs/script/` — the generator and the Worker assets | Three `dev-docs` pages were genuinely mixed and were **split, not moved**: `TESTING` (what a change must prove stays; the suites go down), `BUILD` (workspace, deploy and release stay; the intermediate build and packaging go down), and `CODE-STYLE` (style stays; the hot-path table becomes `hyperformula/dev-docs/PERFORMANCE.md`). The `script/` split was decided by the callers, not by judgement: every script had exactly one caller and the caller named its owner. Twelve call sites stopped reaching across a package boundary, and two scripts stopped resolving paths through `../hyperformula/` and `../docs/`. `examples/` at the root is **deleted**: three of its PNGs were byte-identical duplicates of `docs/.vuepress/public/`, which is what the site actually serves through `$withBase`, and the remaining five files were referenced nowhere in the repository, in any config, workflow or package script. It was not moved under `docs/`, because `docs/examples/` already exists and is a different thing — the runnable examples embedded in the guides. #### Three things the move forced **The built-in-functions generator moved to `docs/script/`.** It uses `@vuepress/shared-utils` for slugs, and that package `require()`s `markdown-it-emoji` and an older `lru-cache` major without declaring either, relying on `vuepress` to hoist them. Outside a full VuePress tree it throws on load, so the generator has to live where that tree is. **Three portal paths were written from the old repository root** and broke once VuePress built from `docs/` as its own package. All three are now anchored explicitly rather than depending on cwd: | Path | Was | Symptom | |---|---|---| | code-snippet plugin | fell back to `process.cwd()` | looked in `docs/docs/examples/`, so every guide with an example failed to compile | | `graph.vue` | `../../../dist/hyperformula.full` | resolved to a `dist/` at the repository root that no longer exists | | `dest` | `docs/.vuepress/dist/docs` | wrote the built site to `docs/docs/` | **One decision was reversed.** The migration first kept a single `dev-docs/`, on the grounds that two directories would fragment the single source of truth. That conflated a *single source* with a *single directory* — splitting by ownership is not duplication, and keeping every package's internals at the root would have made the root a dumping ground the moment `hyperformula-ui` lands. ### How did you test your changes? Everything below was run on this branch, and re-run after each review round: | Command | Result | |---|---| | `npm ci` | Clean; the workspace resolves | | `npm run lint` | **0 errors**, 54011 warnings — the exact pre-move baseline | | `npm run test:jest` | **502 suites, 6232 passed**, 3 skipped, 0 failed | | `npm run test:ci -- --coverage` | **97.42%**, written to `hyperformula/coverage` | | `npm run test:browser` | **12464 assertions** across headless Chrome and Firefox, exit 0 | | `npm run verify:typings` | `tsc --noEmit` clean | | `npm run bundle-all` | Every bundle, and **`Publish package check: OK`** | | `npm run check:licenses` | 3 production dependencies, and it **fails** when the allowlist is tightened | | `npm run release code-freeze …` (dry run) | Correct version, dates and changelog section | | `npm run docs:generate-function-docs` | 564 lines, 13 category sections | | `npm --prefix docs run build:cf` | **217 pages**, sitemap, `_headers` and `_redirects` copied | Every relative Markdown link in `AGENTS.md`, both `dev-docs/` levels, the skills and the READMEs was resolved against the filesystem. All resolve. ### What the reviews found Two rounds, 36 findings. What they caught, and what I got wrong: **Flag arguments were being swallowed.** Every delegating root script dropped them, so `npm run test:ci -- --coverage` reached the workspace as bare `jest --forceExit` and CI had been uploading nothing to Codecov while staying green. Demonstrated with `npm run test:jest -- --listTests`, which ran all 6232 tests instead of listing them. **The release script was never adapted.** It read and wrote `./package.json` — now the private root manifest with no `version`. A dry run then surfaced a third path the review had not caught: the private test suite check still pointed at `test/hyperformula-tests`. **`check:licenses` was checking nothing.** `license-checker` reads `/node_modules`, and npm hoists, so from `hyperformula/` it saw an empty tree and from the root a manifest with no dependencies — zero packages either way, passing vacuously. Replaced with `script/check-licenses.mjs`, which takes the production set from `npm ls --omit=dev` and the licence data from a full `license-checker` run. **The portal was publishing internal agent instructions.** VuePress sweeps every markdown file under `docs/`, so `docs/AGENTS.md` and its `CLAUDE.md` symlink were being built into `/AGENTS.html` and a byte-identical `/CLAUDE.html`, sitemap and search index included. **`npm run test:browser` exited non-zero after passing.** All 6232 specs passed in both browsers and always did; Chrome then reported `Some of your tests did a full page reload!` during teardown. The cause was `client.clearContext: false` in the Karma base config, and it reproduced on the pre-move tree, so it predates this branch. `concurrency: 1` was the obvious first guess and is wrong — it does not remove the error, it doubles wall-clock time, and it turns an intermittent failure into a deterministic one. That determinism was what made the real fix testable, but it is not in the branch. Also fixed: the report path in `performance.yml`, `code-examples-generator.sh` silently processing zero files, `docs:build:cf` never installing the portal, `docs:install` using `npm install` against a committed lockfile, the licence-header rule resolving against the working directory, `hyperformula-ui` listed as a workspace with no `package.json`, a dead `.npmignore` rule, and a dozen documentation pages naming commands or paths this branch had moved. One finding was **rejected on measurement** — the claim that the type-aware ESLint program build risks the hook's 60 s timeout. Measured at **0.91 s** and **1.06 s**. That hook has since been removed anyway, at the author's request. ### Two pre-existing problems the move uncovered **The source language packs have never been linted.** The old ignore list carried a bare `languages` entry meant for the build output. An unanchored pattern matches a directory of that name at *any* depth, so it also excluded `src/i18n/languages/` — and the `sort-keys` override that targets exactly those files has therefore never run. Anchoring the build-output entry exposed **881 violations** across 19 translation files. Confirmed by linting `src/i18n/languages/enGB.ts` on the pre-move tree, where ESLint reports `File ignored because of a matching ignore pattern`. They are excluded again in `.eslintignore`, deliberately and with a comment explaining why, so this branch keeps the exact pre-move lint baseline. Sorting 19 translation files is a change of its own — and reordering translation entries is precisely what the i18n guidance tells contributors not to do casually. **`npm run test:compatibility` was broken for everyone.** It pointed at `test/compatibility/test-compatibility.sh`, a path that exists in neither repository; the script ships with the private suite. Fixed here, since the branch had to touch it anyway. ### Not done, on purpose **CI path filtering.** A naive `paths:` filter on a workflow that branch protection lists as a required check leaves the check permanently pending and makes pull requests unmergeable. Doing it safely needs the required-checks list, which lives in repository settings rather than in the tree, plus the `dorny/paths-filter`-and-single-gate shape the Handsontable monorepo uses. The CI *paths* are all updated; only the filtering is outstanding. **Importing `hyperformula-ui`** — left for the teammate who owns the formula-builder move. It is deliberately *not* in the `workspaces` array until then: npm silently ignores an entry with no `package.json`, and the lockfile has to be regenerated when the real package lands either way. Both are recorded in `dev-docs/STRUCTURE.md` under "What the move still owes". ### Types of changes - [ ] Breaking change (a fix or a feature because of which an existing functionality doesn't work as expected anymore) - [x] New feature or improvement (a non-breaking change that adds functionality) - [ ] Bug fix (a non-breaking change that fixes an issue) - [ ] Additional language file, or a change to an existing language file (translations) - [x] Change to the documentation Repository structure and tooling. The published package is unchanged. ### Related issues: 1. HF-359 ### Checklist: - [x] I have reviewed the guidelines about [Contributing to HyperFormula](https://hyperformula.handsontable.com/docs/guide/contributing.html) and I confirm that my code follows the code style of this project. - [x] I have signed the [Contributor License Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2). - [x] My change is compliant with the [OpenDocument](https://docs.oasis-open.org/office/OpenDocument/v1.3/os/part4-formula/OpenDocument-v1.3-os-part4-formula.html) standard. — not applicable; no engine behaviour changes. - [x] My change is compatible with Microsoft Excel. — not applicable; no engine behaviour changes. - [x] My change is compatible with Google Sheets. — not applicable; no engine behaviour changes. - [ ] I described my changes in the [CHANGELOG.md](https://github.com/handsontable/hyperformula/blob/master/CHANGELOG.md) file. — **intentionally not done.** Nothing user-facing changed; the published package is identical. - [x] My changes require a documentation update. — `dev-docs/` at both levels, `docs/README.md` and the public building guide are updated in this change. - [ ] My changes require a migration guide. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 --- .claude/settings.json | 38 +- .claude/skills/changelog-creation/SKILL.md | 2 +- .../skills/hyperformula-code-review/SKILL.md | 8 +- .claude/skills/hyperformula-dev/SKILL.md | 38 +- .../skills/hyperformula-function-dev/SKILL.md | 10 +- .../skills/hyperformula-unit-testing/SKILL.md | 10 +- .claude/skills/i18n-translations/SKILL.md | 10 +- .claude/skills/pr-creation/SKILL.md | 2 +- .../skills/test-writing-discipline/SKILL.md | 2 +- .claude/skills/writing-docs-pages/SKILL.md | 8 +- .eslintignore | 43 +- .eslintrc.js | 6 +- .github/workflows/build-docs.yml | 2 +- .github/workflows/lint.yml | 4 +- .github/workflows/performance.yml | 12 +- .github/workflows/publish.yml | 2 +- .github/workflows/test.yml | 8 +- .gitignore | 29 +- .worktreeinclude | 25 +- README.md | 125 +- dev-docs/AGENT-TOOLING.md | 9 +- dev-docs/BUILD.md | 35 +- dev-docs/CODE-STYLE.md | 24 +- dev-docs/DEFINITION-OF-DONE.md | 10 +- dev-docs/DOC-STANDARDS.md | 8 +- dev-docs/README.md | 26 +- dev-docs/STRUCTURE.md | 222 +- dev-docs/TESTING.md | 53 +- dev-docs/WORKTREES.md | 28 +- docs/.nvmrc | 1 + docs/.vuepress/build.config.js | 4 +- docs/.vuepress/components/graph.vue | 2 +- docs/.vuepress/config.js | 16 +- docs/AGENTS.md | 2 +- docs/README.md | 33 +- docs/code-examples-generator.sh | 7 +- docs/guide/building.md | 34 +- docs/package-lock.json | 21354 ++++++++++ docs/package.json | 43 + .../script}/formatFunctionSyntax.ts | 2 +- .../script}/generate-builtin-functions-doc.ts | 8 +- {script => docs/script}/prepare-cf-assets.js | 8 +- .../script}/renderBuiltinFunctionsTable.ts | 2 +- {worker => docs/worker}/index.js | 0 wrangler.jsonc => docs/wrangler.jsonc | 2 +- examples/ast.png | Bin 13678 -> 0 bytes examples/ranges.png | Bin 38360 -> 0 bytes examples/sample sheet.png | Bin 21682 -> 0 bytes examples/sample-sheet-1--formulas.csv | 4 - examples/sample-sheet-1--values.csv | 4 - examples/sample-sheet-2--formulas.csv | 8 - examples/sample-sheet-2--values.csv | 8 - examples/topsort.png | Bin 10431 -> 0 bytes hyperformula-ui/README.md | 7 + .../.config}/babel/add-import-extension.js | 0 .../.config}/karma/base.js | 9 +- .../.config}/karma/debug.js | 6 + .../.config}/source-license-header.js | 0 .../.config}/webpack/base.js | 0 .../.config}/webpack/development.js | 0 .../.config}/webpack/languages.js | 0 .../.config}/webpack/production.js | 0 .npmignore => hyperformula/.npmignore | 1 - hyperformula/.nvmrc | 1 + .typedoc.md.ts => hyperformula/.typedoc.md.ts | 6 +- .typedoc.ts => hyperformula/.typedoc.ts | 0 hyperformula/AGENTS.md | 10 + {src => hyperformula}/CLAUDE.md | 0 hyperformula/LICENSE.txt | 34 + hyperformula/README.md | 116 + .../babel.config.js | 0 .../dev-docs}/ARCHITECTURE.md | 66 +- hyperformula/dev-docs/BUILD.md | 19 + .../dev-docs}/DEPENDENCY-GRAPH.md | 4 +- .../dev-docs}/FUNCTION-CATALOGUE.md | 4 +- {dev-docs => hyperformula/dev-docs}/I18N.md | 6 +- .../dev-docs}/INTERPRETER.md | 18 +- {dev-docs => hyperformula/dev-docs}/PARSER.md | 4 +- hyperformula/dev-docs/PERFORMANCE.md | 19 + hyperformula/dev-docs/README.md | 19 + hyperformula/dev-docs/TESTING.md | 60 + ht.config.js => hyperformula/ht.config.js | 0 jasmine.json => hyperformula/jasmine.json | 0 jest.config.js => hyperformula/jest.config.js | 2 +- karma.conf.js => hyperformula/karma.conf.js | 0 .../karma.starter.ts | 0 hyperformula/package.json | 612 + {script => hyperformula/script}/check-file.js | 0 .../script}/check-publish-package.js | 0 {script => hyperformula/script}/if-ne-env.js | 0 {src => hyperformula/src}/AGENTS.md | 6 +- .../src}/AbsoluteCellRange.ts | 0 .../src}/ArgumentSanitization.ts | 0 {src => hyperformula/src}/ArraySize.ts | 0 {src => hyperformula/src}/ArrayValue.ts | 0 .../src}/BuildEngineFactory.ts | 0 .../src}/CLAUDE.md | 0 {src => hyperformula/src}/Cell.ts | 0 .../src}/CellContentParser.ts | 0 {src => hyperformula/src}/CellDependency.ts | 0 {src => hyperformula/src}/CellValue.ts | 0 .../src}/ClipboardOperations.ts | 0 {src => hyperformula/src}/Config.ts | 0 {src => hyperformula/src}/ConfigParams.ts | 0 {src => hyperformula/src}/ContentChanges.ts | 0 {src => hyperformula/src}/CrudOperations.ts | 0 {src => hyperformula/src}/DateTimeDefault.ts | 0 {src => hyperformula/src}/DateTimeHelper.ts | 0 .../src}/DependencyGraph/AGENTS.md | 2 +- .../AddressMapping/AddressMapping.ts | 0 .../AddressMapping/AddressMappingStrategy.ts | 0 .../ChooseAddressMappingPolicy.ts | 0 .../AddressMapping/DenseStrategy.ts | 0 .../AddressMapping/SparseStrategy.ts | 0 .../src}/DependencyGraph/ArrayMapping.ts | 0 .../src/DependencyGraph}/CLAUDE.md | 0 .../src}/DependencyGraph/CellVertex.ts | 0 .../src}/DependencyGraph/DependencyGraph.ts | 0 .../src}/DependencyGraph/EmptyCellVertex.ts | 0 .../src}/DependencyGraph/FormulaVertex.ts | 0 .../src}/DependencyGraph/Graph.ts | 0 .../DependencyGraph/ParsingErrorVertex.ts | 0 .../src}/DependencyGraph/ProcessableValue.ts | 0 .../src}/DependencyGraph/RangeMapping.ts | 0 .../src}/DependencyGraph/RangeVertex.ts | 0 .../src}/DependencyGraph/SheetMapping.ts | 0 .../SheetReferenceRegistrar.ts | 0 .../src}/DependencyGraph/TopSort.ts | 0 .../src}/DependencyGraph/ValueCellVertex.ts | 0 .../src}/DependencyGraph/Vertex.ts | 0 .../collectAddressesDependentToRange.ts | 0 .../src}/DependencyGraph/index.ts | 0 {src => hyperformula/src}/Destroy.ts | 0 {src => hyperformula/src}/Emitter.ts | 0 {src => hyperformula/src}/Evaluator.ts | 0 {src => hyperformula/src}/Exporter.ts | 0 {src => hyperformula/src}/GraphBuilder.ts | 0 {src => hyperformula/src}/HyperFormula.ts | 0 .../src}/LazilyTransformingAstService.ts | 0 .../src}/Lookup/AdvancedFind.ts | 0 .../src}/Lookup/ColumnBinarySearch.ts | 0 .../src}/Lookup/ColumnIndex.ts | 0 .../src}/Lookup/RowSearchStrategy.ts | 0 .../src}/Lookup/SearchStrategy.ts | 0 {src => hyperformula/src}/Maybe.ts | 0 {src => hyperformula/src}/NamedExpressions.ts | 0 .../src}/NumberLiteralHelper.ts | 0 {src => hyperformula/src}/Operations.ts | 0 {src => hyperformula/src}/Serialization.ts | 0 {src => hyperformula/src}/Sheet.ts | 0 {src => hyperformula/src}/SimpleRangeValue.ts | 0 {src => hyperformula/src}/Span.ts | 0 {src => hyperformula/src}/StringHelper.ts | 0 {src => hyperformula/src}/UndoRedo.ts | 0 .../src}/absolutizeDependencies.ts | 0 .../AddColumnsTransformer.ts | 0 .../AddRowsTransformer.ts | 0 .../CleanOutOfScopeDependenciesTransformer.ts | 0 .../CombinedTransformer.ts | 0 .../MoveCellsTransformer.ts | 0 .../RemoveColumnsTransformer.ts | 0 .../RemoveRowsTransformer.ts | 0 .../RenameSheetTransformer.ts | 0 .../dependencyTransformers/Transformer.ts | 0 {src => hyperformula/src}/error-message.ts | 0 {src => hyperformula/src}/errors.ts | 0 {src => hyperformula/src}/format/format.ts | 0 {src => hyperformula/src}/format/parser.ts | 0 {src => hyperformula/src}/generatorUtils.ts | 0 .../src}/helpers/licenseKeyHelper.ts | 0 .../src}/helpers/licenseKeyValidator.ts | 0 {src => hyperformula/src}/i18n/AGENTS.md | 2 +- .../src/i18n}/CLAUDE.md | 0 .../src}/i18n/TranslationPackage.ts | 0 {src => hyperformula/src}/i18n/index.ts | 0 .../src}/i18n/languages/csCZ.ts | 0 .../src}/i18n/languages/daDK.ts | 0 .../src}/i18n/languages/deDE.ts | 0 .../src}/i18n/languages/enGB.ts | 0 .../src}/i18n/languages/enUS.ts | 0 .../src}/i18n/languages/esES.ts | 0 .../src}/i18n/languages/fiFI.ts | 0 .../src}/i18n/languages/frFR.ts | 0 .../src}/i18n/languages/huHU.ts | 0 .../src}/i18n/languages/idID.ts | 0 .../src}/i18n/languages/index.ts | 0 .../src}/i18n/languages/itIT.ts | 0 .../src}/i18n/languages/nbNO.ts | 0 .../src}/i18n/languages/nlNL.ts | 0 .../src}/i18n/languages/plPL.ts | 0 .../src}/i18n/languages/ptPT.ts | 0 .../src}/i18n/languages/ruRU.ts | 0 .../src}/i18n/languages/svSE.ts | 0 .../src}/i18n/languages/trTR.ts | 0 {src => hyperformula/src}/index.ts | 0 .../src}/interpreter/AGENTS.md | 2 +- .../src}/interpreter/ArithmeticHelper.ts | 0 .../src/interpreter}/CLAUDE.md | 0 .../src}/interpreter/Criterion.ts | 0 .../interpreter/CriterionFunctionCompute.ts | 0 .../src}/interpreter/FunctionRegistry.ts | 0 .../src}/interpreter/Interpreter.ts | 0 .../src}/interpreter/InterpreterState.ts | 0 .../src}/interpreter/InterpreterValue.ts | 0 .../src}/interpreter/binarySearch.ts | 0 .../interpreter/functionMetadata/AGENTS.md | 2 +- .../interpreter/functionMetadata}/CLAUDE.md | 0 .../functionMetadata/FunctionDescription.ts | 0 .../buildFunctionDescriptions.ts | 0 .../categories/array-manipulation.ts | 0 .../functionMetadata/categories/database.ts | 0 .../categories/date-and-time.ts | 0 .../categories/engineering.ts | 0 .../functionMetadata/categories/financial.ts | 0 .../categories/information.ts | 0 .../functionMetadata/categories/logical.ts | 0 .../categories/lookup-and-reference.ts | 0 .../categories/math-and-trigonometry.ts | 0 .../categories/matrix-functions.ts | 0 .../functionMetadata/categories/operator.ts | 0 .../categories/statistical.ts | 0 .../functionMetadata/categories/text.ts | 0 .../interpreter/functionMetadata/index.ts | 0 .../protectedFunctionMetadata.ts | 0 .../src}/interpreter/index.ts | 0 .../plugin/3rdparty/bessel/bessel.ts | 0 .../plugin/3rdparty/jstat/jstat.ts | 0 .../src}/interpreter/plugin/AGENTS.md | 2 +- .../src}/interpreter/plugin/AbsPlugin.ts | 0 .../src}/interpreter/plugin/AddressPlugin.ts | 0 .../src}/interpreter/plugin/ArrayPlugin.ts | 0 .../src}/interpreter/plugin/BitShiftPlugin.ts | 0 .../plugin/BitwiseLogicOperationsPlugin.ts | 0 .../src}/interpreter/plugin/BooleanPlugin.ts | 0 .../src/interpreter/plugin}/CLAUDE.md | 0 .../src}/interpreter/plugin/CharPlugin.ts | 0 .../src}/interpreter/plugin/CodePlugin.ts | 0 .../src}/interpreter/plugin/ComplexPlugin.ts | 0 .../plugin/ConditionalAggregationPlugin.ts | 0 .../interpreter/plugin/CountBlankPlugin.ts | 0 .../interpreter/plugin/CountUniquePlugin.ts | 0 .../src}/interpreter/plugin/DatabasePlugin.ts | 0 .../src}/interpreter/plugin/DateTimePlugin.ts | 0 .../src}/interpreter/plugin/DegreesPlugin.ts | 0 .../src}/interpreter/plugin/DeltaPlugin.ts | 0 .../src}/interpreter/plugin/ExpPlugin.ts | 0 .../interpreter/plugin/FinancialPlugin.ts | 0 .../interpreter/plugin/FormulaTextPlugin.ts | 0 .../src}/interpreter/plugin/FunctionPlugin.ts | 0 .../interpreter/plugin/HyperlinkPlugin.ts | 0 .../interpreter/plugin/InformationPlugin.ts | 0 .../src}/interpreter/plugin/IsEvenPlugin.ts | 0 .../src}/interpreter/plugin/IsOddPlugin.ts | 0 .../interpreter/plugin/LogarithmPlugin.ts | 0 .../src}/interpreter/plugin/LookupPlugin.ts | 0 .../interpreter/plugin/MathConstantsPlugin.ts | 0 .../src}/interpreter/plugin/MathPlugin.ts | 0 .../src}/interpreter/plugin/MatrixPlugin.ts | 0 .../src}/interpreter/plugin/MedianPlugin.ts | 0 .../src}/interpreter/plugin/ModuloPlugin.ts | 0 .../plugin/NumericAggregationPlugin.ts | 0 .../interpreter/plugin/PercentilePlugin.ts | 0 .../src}/interpreter/plugin/PowerPlugin.ts | 0 .../src}/interpreter/plugin/RadiansPlugin.ts | 0 .../plugin/RadixConversionPlugin.ts | 0 .../src}/interpreter/plugin/RandomPlugin.ts | 0 .../src}/interpreter/plugin/RomanPlugin.ts | 0 .../src}/interpreter/plugin/RoundingPlugin.ts | 0 .../src}/interpreter/plugin/SequencePlugin.ts | 0 .../interpreter/plugin/SimpleArithmertic.ts | 0 .../src}/interpreter/plugin/SortPlugin.ts | 0 .../src}/interpreter/plugin/SqrtPlugin.ts | 0 .../plugin/StatisticalAggregationPlugin.ts | 0 .../interpreter/plugin/StatisticalPlugin.ts | 0 .../src}/interpreter/plugin/SumprodPlugin.ts | 0 .../src}/interpreter/plugin/TextPlugin.ts | 0 .../interpreter/plugin/TrigonometryPlugin.ts | 0 .../src}/interpreter/plugin/UniquePlugin.ts | 0 .../src}/interpreter/plugin/VersionPlugin.ts | 0 .../src}/interpreter/plugin/index.ts | 0 {src => hyperformula/src}/parser/AGENTS.md | 2 +- {src => hyperformula/src}/parser/Address.ts | 0 {src => hyperformula/src}/parser/Ast.ts | 0 {test => hyperformula/src/parser}/CLAUDE.md | 0 {src => hyperformula/src}/parser/Cache.ts | 0 .../src}/parser/CellAddress.ts | 0 .../src}/parser/CellReferenceMatcher.ts | 0 .../src}/parser/ColumnAddress.ts | 0 .../src}/parser/FormulaParser.ts | 0 .../src}/parser/LexerConfig.ts | 0 .../src}/parser/NamedExpressionMatcher.ts | 0 .../src}/parser/ParserConfig.ts | 0 .../src}/parser/ParserWithCaching.ts | 0 .../src}/parser/RelativeDependency.ts | 0 .../src}/parser/RowAddress.ts | 0 {src => hyperformula/src}/parser/Unparser.ts | 0 .../parser/addressRepresentationConverters.ts | 0 .../src}/parser/binaryOpTokenMap.ts | 0 .../src}/parser/collectDependencies.ts | 0 {src => hyperformula/src}/parser/index.ts | 0 .../src}/parser/parser-consts.ts | 0 .../src}/statistics/EmptyStatistics.ts | 0 .../src}/statistics/StatType.ts | 0 .../src}/statistics/Statistics.ts | 0 {src => hyperformula/src}/statistics/index.ts | 0 {test => hyperformula/test}/AGENTS.md | 4 +- hyperformula/test/CLAUDE.md | 1 + {test => hyperformula/test}/README.md | 0 .../test}/_setupFiles/babel.js | 0 .../test}/_setupFiles/bootstrap.ts | 0 .../test}/_setupFiles/globalSetup.ts | 0 .../test}/_setupFiles/jest/bootstrap.ts | 0 .../test}/_setupFiles/jest/toEqualError.ts | 0 .../test}/_setupFiles/jsdom.js | 0 .../test}/_setupFiles/matchers/index.ts | 0 .../_setupFiles/matchers/toContainEqual.ts | 0 .../_setupFiles/matchers/toEqualError.ts | 0 .../_setupFiles/matchers/toMatchObject.ts | 0 {test => hyperformula/test}/fetch-tests.sh | 0 {test => hyperformula/test}/smoke.spec.ts | 0 {test => hyperformula/test}/tsconfig.json | 0 tsconfig.json => hyperformula/tsconfig.json | 0 .../tsconfig.test.json | 0 .../webpack.config.js | 0 package-lock.json | 32105 +++++++--------- package.json | 631 +- script/AGENTS.md | 12 +- script/README.md | 20 - script/check-licenses.mjs | 112 + script/release/release.sh | 83 +- 330 files changed, 35997 insertions(+), 20353 deletions(-) create mode 100644 docs/.nvmrc create mode 100644 docs/package-lock.json create mode 100644 docs/package.json rename {script => docs/script}/formatFunctionSyntax.ts (96%) rename {script => docs/script}/generate-builtin-functions-doc.ts (92%) rename {script => docs/script}/prepare-cf-assets.js (73%) rename {script => docs/script}/renderBuiltinFunctionsTable.ts (99%) rename {worker => docs/worker}/index.js (100%) rename wrangler.jsonc => docs/wrangler.jsonc (97%) delete mode 100644 examples/ast.png delete mode 100644 examples/ranges.png delete mode 100644 examples/sample sheet.png delete mode 100644 examples/sample-sheet-1--formulas.csv delete mode 100644 examples/sample-sheet-1--values.csv delete mode 100644 examples/sample-sheet-2--formulas.csv delete mode 100644 examples/sample-sheet-2--values.csv delete mode 100644 examples/topsort.png create mode 100644 hyperformula-ui/README.md rename {.config => hyperformula/.config}/babel/add-import-extension.js (100%) rename {.config => hyperformula/.config}/karma/base.js (81%) rename {.config => hyperformula/.config}/karma/debug.js (54%) rename {.config => hyperformula/.config}/source-license-header.js (100%) rename {.config => hyperformula/.config}/webpack/base.js (100%) rename {.config => hyperformula/.config}/webpack/development.js (100%) rename {.config => hyperformula/.config}/webpack/languages.js (100%) rename {.config => hyperformula/.config}/webpack/production.js (100%) rename .npmignore => hyperformula/.npmignore (89%) create mode 100644 hyperformula/.nvmrc rename .typedoc.md.ts => hyperformula/.typedoc.md.ts (59%) rename .typedoc.ts => hyperformula/.typedoc.ts (100%) create mode 100644 hyperformula/AGENTS.md rename {src => hyperformula}/CLAUDE.md (100%) create mode 100644 hyperformula/LICENSE.txt create mode 100644 hyperformula/README.md rename babel.config.js => hyperformula/babel.config.js (100%) rename {dev-docs => hyperformula/dev-docs}/ARCHITECTURE.md (51%) create mode 100644 hyperformula/dev-docs/BUILD.md rename {dev-docs => hyperformula/dev-docs}/DEPENDENCY-GRAPH.md (84%) rename {dev-docs => hyperformula/dev-docs}/FUNCTION-CATALOGUE.md (94%) rename {dev-docs => hyperformula/dev-docs}/I18N.md (76%) rename {dev-docs => hyperformula/dev-docs}/INTERPRETER.md (81%) rename {dev-docs => hyperformula/dev-docs}/PARSER.md (85%) create mode 100644 hyperformula/dev-docs/PERFORMANCE.md create mode 100644 hyperformula/dev-docs/README.md create mode 100644 hyperformula/dev-docs/TESTING.md rename ht.config.js => hyperformula/ht.config.js (100%) rename jasmine.json => hyperformula/jasmine.json (100%) rename jest.config.js => hyperformula/jest.config.js (98%) rename karma.conf.js => hyperformula/karma.conf.js (100%) rename karma.starter.ts => hyperformula/karma.starter.ts (100%) create mode 100644 hyperformula/package.json rename {script => hyperformula/script}/check-file.js (100%) rename {script => hyperformula/script}/check-publish-package.js (100%) rename {script => hyperformula/script}/if-ne-env.js (100%) rename {src => hyperformula/src}/AGENTS.md (62%) rename {src => hyperformula/src}/AbsoluteCellRange.ts (100%) rename {src => hyperformula/src}/ArgumentSanitization.ts (100%) rename {src => hyperformula/src}/ArraySize.ts (100%) rename {src => hyperformula/src}/ArrayValue.ts (100%) rename {src => hyperformula/src}/BuildEngineFactory.ts (100%) rename {src/DependencyGraph => hyperformula/src}/CLAUDE.md (100%) rename {src => hyperformula/src}/Cell.ts (100%) rename {src => hyperformula/src}/CellContentParser.ts (100%) rename {src => hyperformula/src}/CellDependency.ts (100%) rename {src => hyperformula/src}/CellValue.ts (100%) rename {src => hyperformula/src}/ClipboardOperations.ts (100%) rename {src => hyperformula/src}/Config.ts (100%) rename {src => hyperformula/src}/ConfigParams.ts (100%) rename {src => hyperformula/src}/ContentChanges.ts (100%) rename {src => hyperformula/src}/CrudOperations.ts (100%) rename {src => hyperformula/src}/DateTimeDefault.ts (100%) rename {src => hyperformula/src}/DateTimeHelper.ts (100%) rename {src => hyperformula/src}/DependencyGraph/AGENTS.md (72%) rename {src => hyperformula/src}/DependencyGraph/AddressMapping/AddressMapping.ts (100%) rename {src => hyperformula/src}/DependencyGraph/AddressMapping/AddressMappingStrategy.ts (100%) rename {src => hyperformula/src}/DependencyGraph/AddressMapping/ChooseAddressMappingPolicy.ts (100%) rename {src => hyperformula/src}/DependencyGraph/AddressMapping/DenseStrategy.ts (100%) rename {src => hyperformula/src}/DependencyGraph/AddressMapping/SparseStrategy.ts (100%) rename {src => hyperformula/src}/DependencyGraph/ArrayMapping.ts (100%) rename {src/i18n => hyperformula/src/DependencyGraph}/CLAUDE.md (100%) rename {src => hyperformula/src}/DependencyGraph/CellVertex.ts (100%) rename {src => hyperformula/src}/DependencyGraph/DependencyGraph.ts (100%) rename {src => hyperformula/src}/DependencyGraph/EmptyCellVertex.ts (100%) rename {src => hyperformula/src}/DependencyGraph/FormulaVertex.ts (100%) rename {src => hyperformula/src}/DependencyGraph/Graph.ts (100%) rename {src => hyperformula/src}/DependencyGraph/ParsingErrorVertex.ts (100%) rename {src => hyperformula/src}/DependencyGraph/ProcessableValue.ts (100%) rename {src => hyperformula/src}/DependencyGraph/RangeMapping.ts (100%) rename {src => hyperformula/src}/DependencyGraph/RangeVertex.ts (100%) rename {src => hyperformula/src}/DependencyGraph/SheetMapping.ts (100%) rename {src => hyperformula/src}/DependencyGraph/SheetReferenceRegistrar.ts (100%) rename {src => hyperformula/src}/DependencyGraph/TopSort.ts (100%) rename {src => hyperformula/src}/DependencyGraph/ValueCellVertex.ts (100%) rename {src => hyperformula/src}/DependencyGraph/Vertex.ts (100%) rename {src => hyperformula/src}/DependencyGraph/collectAddressesDependentToRange.ts (100%) rename {src => hyperformula/src}/DependencyGraph/index.ts (100%) rename {src => hyperformula/src}/Destroy.ts (100%) rename {src => hyperformula/src}/Emitter.ts (100%) rename {src => hyperformula/src}/Evaluator.ts (100%) rename {src => hyperformula/src}/Exporter.ts (100%) rename {src => hyperformula/src}/GraphBuilder.ts (100%) rename {src => hyperformula/src}/HyperFormula.ts (100%) rename {src => hyperformula/src}/LazilyTransformingAstService.ts (100%) rename {src => hyperformula/src}/Lookup/AdvancedFind.ts (100%) rename {src => hyperformula/src}/Lookup/ColumnBinarySearch.ts (100%) rename {src => hyperformula/src}/Lookup/ColumnIndex.ts (100%) rename {src => hyperformula/src}/Lookup/RowSearchStrategy.ts (100%) rename {src => hyperformula/src}/Lookup/SearchStrategy.ts (100%) rename {src => hyperformula/src}/Maybe.ts (100%) rename {src => hyperformula/src}/NamedExpressions.ts (100%) rename {src => hyperformula/src}/NumberLiteralHelper.ts (100%) rename {src => hyperformula/src}/Operations.ts (100%) rename {src => hyperformula/src}/Serialization.ts (100%) rename {src => hyperformula/src}/Sheet.ts (100%) rename {src => hyperformula/src}/SimpleRangeValue.ts (100%) rename {src => hyperformula/src}/Span.ts (100%) rename {src => hyperformula/src}/StringHelper.ts (100%) rename {src => hyperformula/src}/UndoRedo.ts (100%) rename {src => hyperformula/src}/absolutizeDependencies.ts (100%) rename {src => hyperformula/src}/dependencyTransformers/AddColumnsTransformer.ts (100%) rename {src => hyperformula/src}/dependencyTransformers/AddRowsTransformer.ts (100%) rename {src => hyperformula/src}/dependencyTransformers/CleanOutOfScopeDependenciesTransformer.ts (100%) rename {src => hyperformula/src}/dependencyTransformers/CombinedTransformer.ts (100%) rename {src => hyperformula/src}/dependencyTransformers/MoveCellsTransformer.ts (100%) rename {src => hyperformula/src}/dependencyTransformers/RemoveColumnsTransformer.ts (100%) rename {src => hyperformula/src}/dependencyTransformers/RemoveRowsTransformer.ts (100%) rename {src => hyperformula/src}/dependencyTransformers/RenameSheetTransformer.ts (100%) rename {src => hyperformula/src}/dependencyTransformers/Transformer.ts (100%) rename {src => hyperformula/src}/error-message.ts (100%) rename {src => hyperformula/src}/errors.ts (100%) rename {src => hyperformula/src}/format/format.ts (100%) rename {src => hyperformula/src}/format/parser.ts (100%) rename {src => hyperformula/src}/generatorUtils.ts (100%) rename {src => hyperformula/src}/helpers/licenseKeyHelper.ts (100%) rename {src => hyperformula/src}/helpers/licenseKeyValidator.ts (100%) rename {src => hyperformula/src}/i18n/AGENTS.md (77%) rename {src/interpreter => hyperformula/src/i18n}/CLAUDE.md (100%) rename {src => hyperformula/src}/i18n/TranslationPackage.ts (100%) rename {src => hyperformula/src}/i18n/index.ts (100%) rename {src => hyperformula/src}/i18n/languages/csCZ.ts (100%) rename {src => hyperformula/src}/i18n/languages/daDK.ts (100%) rename {src => hyperformula/src}/i18n/languages/deDE.ts (100%) rename {src => hyperformula/src}/i18n/languages/enGB.ts (100%) rename {src => hyperformula/src}/i18n/languages/enUS.ts (100%) rename {src => hyperformula/src}/i18n/languages/esES.ts (100%) rename {src => hyperformula/src}/i18n/languages/fiFI.ts (100%) rename {src => hyperformula/src}/i18n/languages/frFR.ts (100%) rename {src => hyperformula/src}/i18n/languages/huHU.ts (100%) rename {src => hyperformula/src}/i18n/languages/idID.ts (100%) rename {src => hyperformula/src}/i18n/languages/index.ts (100%) rename {src => hyperformula/src}/i18n/languages/itIT.ts (100%) rename {src => hyperformula/src}/i18n/languages/nbNO.ts (100%) rename {src => hyperformula/src}/i18n/languages/nlNL.ts (100%) rename {src => hyperformula/src}/i18n/languages/plPL.ts (100%) rename {src => hyperformula/src}/i18n/languages/ptPT.ts (100%) rename {src => hyperformula/src}/i18n/languages/ruRU.ts (100%) rename {src => hyperformula/src}/i18n/languages/svSE.ts (100%) rename {src => hyperformula/src}/i18n/languages/trTR.ts (100%) rename {src => hyperformula/src}/index.ts (100%) rename {src => hyperformula/src}/interpreter/AGENTS.md (71%) rename {src => hyperformula/src}/interpreter/ArithmeticHelper.ts (100%) rename {src/interpreter/functionMetadata => hyperformula/src/interpreter}/CLAUDE.md (100%) rename {src => hyperformula/src}/interpreter/Criterion.ts (100%) rename {src => hyperformula/src}/interpreter/CriterionFunctionCompute.ts (100%) rename {src => hyperformula/src}/interpreter/FunctionRegistry.ts (100%) rename {src => hyperformula/src}/interpreter/Interpreter.ts (100%) rename {src => hyperformula/src}/interpreter/InterpreterState.ts (100%) rename {src => hyperformula/src}/interpreter/InterpreterValue.ts (100%) rename {src => hyperformula/src}/interpreter/binarySearch.ts (100%) rename {src => hyperformula/src}/interpreter/functionMetadata/AGENTS.md (77%) rename {src/interpreter/plugin => hyperformula/src/interpreter/functionMetadata}/CLAUDE.md (100%) rename {src => hyperformula/src}/interpreter/functionMetadata/FunctionDescription.ts (100%) rename {src => hyperformula/src}/interpreter/functionMetadata/buildFunctionDescriptions.ts (100%) rename {src => hyperformula/src}/interpreter/functionMetadata/categories/array-manipulation.ts (100%) rename {src => hyperformula/src}/interpreter/functionMetadata/categories/database.ts (100%) rename {src => hyperformula/src}/interpreter/functionMetadata/categories/date-and-time.ts (100%) rename {src => hyperformula/src}/interpreter/functionMetadata/categories/engineering.ts (100%) rename {src => hyperformula/src}/interpreter/functionMetadata/categories/financial.ts (100%) rename {src => hyperformula/src}/interpreter/functionMetadata/categories/information.ts (100%) rename {src => hyperformula/src}/interpreter/functionMetadata/categories/logical.ts (100%) rename {src => hyperformula/src}/interpreter/functionMetadata/categories/lookup-and-reference.ts (100%) rename {src => hyperformula/src}/interpreter/functionMetadata/categories/math-and-trigonometry.ts (100%) rename {src => hyperformula/src}/interpreter/functionMetadata/categories/matrix-functions.ts (100%) rename {src => hyperformula/src}/interpreter/functionMetadata/categories/operator.ts (100%) rename {src => hyperformula/src}/interpreter/functionMetadata/categories/statistical.ts (100%) rename {src => hyperformula/src}/interpreter/functionMetadata/categories/text.ts (100%) rename {src => hyperformula/src}/interpreter/functionMetadata/index.ts (100%) rename {src => hyperformula/src}/interpreter/functionMetadata/protectedFunctionMetadata.ts (100%) rename {src => hyperformula/src}/interpreter/index.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/3rdparty/bessel/bessel.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/3rdparty/jstat/jstat.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/AGENTS.md (82%) rename {src => hyperformula/src}/interpreter/plugin/AbsPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/AddressPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/ArrayPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/BitShiftPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/BitwiseLogicOperationsPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/BooleanPlugin.ts (100%) rename {src/parser => hyperformula/src/interpreter/plugin}/CLAUDE.md (100%) rename {src => hyperformula/src}/interpreter/plugin/CharPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/CodePlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/ComplexPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/ConditionalAggregationPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/CountBlankPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/CountUniquePlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/DatabasePlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/DateTimePlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/DegreesPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/DeltaPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/ExpPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/FinancialPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/FormulaTextPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/FunctionPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/HyperlinkPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/InformationPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/IsEvenPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/IsOddPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/LogarithmPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/LookupPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/MathConstantsPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/MathPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/MatrixPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/MedianPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/ModuloPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/NumericAggregationPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/PercentilePlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/PowerPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/RadiansPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/RadixConversionPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/RandomPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/RomanPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/RoundingPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/SequencePlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/SimpleArithmertic.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/SortPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/SqrtPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/StatisticalAggregationPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/StatisticalPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/SumprodPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/TextPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/TrigonometryPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/UniquePlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/VersionPlugin.ts (100%) rename {src => hyperformula/src}/interpreter/plugin/index.ts (100%) rename {src => hyperformula/src}/parser/AGENTS.md (69%) rename {src => hyperformula/src}/parser/Address.ts (100%) rename {src => hyperformula/src}/parser/Ast.ts (100%) rename {test => hyperformula/src/parser}/CLAUDE.md (100%) rename {src => hyperformula/src}/parser/Cache.ts (100%) rename {src => hyperformula/src}/parser/CellAddress.ts (100%) rename {src => hyperformula/src}/parser/CellReferenceMatcher.ts (100%) rename {src => hyperformula/src}/parser/ColumnAddress.ts (100%) rename {src => hyperformula/src}/parser/FormulaParser.ts (100%) rename {src => hyperformula/src}/parser/LexerConfig.ts (100%) rename {src => hyperformula/src}/parser/NamedExpressionMatcher.ts (100%) rename {src => hyperformula/src}/parser/ParserConfig.ts (100%) rename {src => hyperformula/src}/parser/ParserWithCaching.ts (100%) rename {src => hyperformula/src}/parser/RelativeDependency.ts (100%) rename {src => hyperformula/src}/parser/RowAddress.ts (100%) rename {src => hyperformula/src}/parser/Unparser.ts (100%) rename {src => hyperformula/src}/parser/addressRepresentationConverters.ts (100%) rename {src => hyperformula/src}/parser/binaryOpTokenMap.ts (100%) rename {src => hyperformula/src}/parser/collectDependencies.ts (100%) rename {src => hyperformula/src}/parser/index.ts (100%) rename {src => hyperformula/src}/parser/parser-consts.ts (100%) rename {src => hyperformula/src}/statistics/EmptyStatistics.ts (100%) rename {src => hyperformula/src}/statistics/StatType.ts (100%) rename {src => hyperformula/src}/statistics/Statistics.ts (100%) rename {src => hyperformula/src}/statistics/index.ts (100%) rename {test => hyperformula/test}/AGENTS.md (74%) create mode 120000 hyperformula/test/CLAUDE.md rename {test => hyperformula/test}/README.md (100%) rename {test => hyperformula/test}/_setupFiles/babel.js (100%) rename {test => hyperformula/test}/_setupFiles/bootstrap.ts (100%) rename {test => hyperformula/test}/_setupFiles/globalSetup.ts (100%) rename {test => hyperformula/test}/_setupFiles/jest/bootstrap.ts (100%) rename {test => hyperformula/test}/_setupFiles/jest/toEqualError.ts (100%) rename {test => hyperformula/test}/_setupFiles/jsdom.js (100%) rename {test => hyperformula/test}/_setupFiles/matchers/index.ts (100%) rename {test => hyperformula/test}/_setupFiles/matchers/toContainEqual.ts (100%) rename {test => hyperformula/test}/_setupFiles/matchers/toEqualError.ts (100%) rename {test => hyperformula/test}/_setupFiles/matchers/toMatchObject.ts (100%) rename {test => hyperformula/test}/fetch-tests.sh (100%) rename {test => hyperformula/test}/smoke.spec.ts (100%) rename {test => hyperformula/test}/tsconfig.json (100%) rename tsconfig.json => hyperformula/tsconfig.json (100%) rename tsconfig.test.json => hyperformula/tsconfig.test.json (100%) rename webpack.config.js => hyperformula/webpack.config.js (100%) delete mode 100644 script/README.md create mode 100644 script/check-licenses.mjs diff --git a/.claude/settings.json b/.claude/settings.json index 297d8e0c03..8fdee8bb0c 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -4,29 +4,21 @@ ], "permissions": { "deny": [ - "Read(./lib/**)", - "Read(./es/**)", - "Read(./commonjs/**)", - "Read(./dist/**)", - "Read(./languages/**)", - "Read(./typings/**)", - "Read(./docs/api/**)", - "Read(./docs/functions/**)", - "Read(./docs/guide/built-in-functions.md)", - "Read(./docs/.vuepress/dist/**)", - "Read(./docs/.vuepress/api-sidebar.json)", - "Read(./docs/.vuepress/api-sidebar-relative.json)", - "Read(./typedoc/**)", - "Read(./doc/**)", - "Read(./coverage/**)", - "Read(./test-jest/**)", - "Read(./test-jasmine/**)", - "Read(./storage/**)" - ] - }, - "worktree": { - "symlinkDirectories": [ - "node_modules" + "Read(/hyperformula/lib/**)", + "Read(/hyperformula/es/**)", + "Read(/hyperformula/commonjs/**)", + "Read(/hyperformula/dist/**)", + "Read(/hyperformula/languages/**)", + "Read(/hyperformula/typings/**)", + "Read(/hyperformula/coverage/**)", + "Read(/hyperformula/test-jest/**)", + "Read(/hyperformula/test-jasmine/**)", + "Read(/hyperformula/typedoc/**)", + "Read(/hyperformula/storage/**)", + "Read(/docs/functions/**)", + "Read(/docs/.vuepress/dist/**)", + "Read(/docs/.vuepress/api-sidebar.json)", + "Read(/docs/.vuepress/api-sidebar-relative.json)" ] } } diff --git a/.claude/skills/changelog-creation/SKILL.md b/.claude/skills/changelog-creation/SKILL.md index b15d335e86..4ccbc4ce26 100644 --- a/.claude/skills/changelog-creation/SKILL.md +++ b/.claude/skills/changelog-creation/SKILL.md @@ -20,7 +20,7 @@ Documentation-only, test-only, and CI or tooling changes take none, and neither ## 4. Write the bullet -Under `## [Unreleased]` in `CHANGELOG.md`, in the section that matches the change, creating the `### ` heading if it is absent. End it with the link from step 3. +Under `## [Unreleased]` in `CHANGELOG.md` at the repository root — one changelog for every package, because they release together on one version. Put it in the section that matches the change, creating the `### ` heading if it is absent. Name the package the entry concerns when the text does not make it obvious. End it with the link from step 3. ## 5. Re-read it as a user would diff --git a/.claude/skills/hyperformula-code-review/SKILL.md b/.claude/skills/hyperformula-code-review/SKILL.md index a63ab845cb..cb5e2248da 100644 --- a/.claude/skills/hyperformula-code-review/SKILL.md +++ b/.claude/skills/hyperformula-code-review/SKILL.md @@ -11,9 +11,9 @@ Always: |---|---| | [`DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md) | What the change was required to contain | | [`CODE-STYLE.md`](../../../dev-docs/CODE-STYLE.md) | Style, and which paths are hot enough that complexity matters | -| [`TESTING.md`](../../../dev-docs/TESTING.md#a-test-must-prove-behaviour) | Whether the tests prove anything, or only execute code | +| [`TESTING.md`](../../../hyperformula/dev-docs/TESTING.md#a-test-must-prove-behaviour) | Whether the tests prove anything, or only execute code | -Then the page covering what the diff touches: [`ARCHITECTURE.md`](../../../dev-docs/ARCHITECTURE.md), [`PARSER.md`](../../../dev-docs/PARSER.md), [`INTERPRETER.md`](../../../dev-docs/INTERPRETER.md), [`DEPENDENCY-GRAPH.md`](../../../dev-docs/DEPENDENCY-GRAPH.md), [`FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md), [`I18N.md`](../../../dev-docs/I18N.md). +Then the page covering what the diff touches: [`ARCHITECTURE.md`](../../../hyperformula/dev-docs/ARCHITECTURE.md), [`PARSER.md`](../../../hyperformula/dev-docs/PARSER.md), [`INTERPRETER.md`](../../../hyperformula/dev-docs/INTERPRETER.md), [`DEPENDENCY-GRAPH.md`](../../../hyperformula/dev-docs/DEPENDENCY-GRAPH.md), [`FUNCTION-CATALOGUE.md`](../../../hyperformula/dev-docs/FUNCTION-CATALOGUE.md), [`I18N.md`](../../../hyperformula/dev-docs/I18N.md). Review in the order below, and stop to report the first serious finding rather than burying it under style notes. @@ -29,7 +29,7 @@ Review in the order below, and stop to report the first serious finding rather t ## 3. Completeness of a function change -Check every one of [the five places a function change must touch](../../../dev-docs/INTERPRETER.md#the-five-places-a-function-change-must-touch); most of them fail silently when missed. Plus `sizeOfResultArrayMethod` for anything array-returning, and an explicit `optionalArg` where arity does not express the valid call. Skill `hyperformula-function-dev`. +Check every one of [the five places a function change must touch](../../../hyperformula/dev-docs/INTERPRETER.md#the-five-places-a-function-change-must-touch); most of them fail silently when missed. Plus `sizeOfResultArrayMethod` for anything array-returning, and an explicit `optionalArg` where arity does not express the valid call. Skill `hyperformula-function-dev`. ## 4. Performance @@ -37,7 +37,7 @@ Allocation in a per-cell or per-vertex loop; work that could be hoisted out of t ## 5. Public API -`src/HyperFormula.ts` and its exported types are the contract. A signature, return-type, or behaviour change is breaking and needs a migration-guide section and an explicit note. JSDoc here is published output — review it as documentation. +`hyperformula/src/HyperFormula.ts` and its exported types are the contract. A signature, return-type, or behaviour change is breaking and needs a migration-guide section and an explicit note. JSDoc here is published output — review it as documentation. ## 6. Process diff --git a/.claude/skills/hyperformula-dev/SKILL.md b/.claude/skills/hyperformula-dev/SKILL.md index 61df2081c0..587af24066 100644 --- a/.claude/skills/hyperformula-dev/SKILL.md +++ b/.claude/skills/hyperformula-dev/SKILL.md @@ -1,12 +1,12 @@ --- name: hyperformula-dev -paths: src/** +paths: hyperformula/src/** description: > - Use for ANY work touching the HyperFormula engine in `src/`: fixing bugs, adding features, + Use for ANY work touching the HyperFormula engine in `hyperformula/src/`: fixing bugs, adding features, changing the public API, working on the parser, the interpreter, the dependency graph, CRUD operations, configuration options, named expressions, or number and date formats. Also use for how-to questions about engine internals (how recalculation works, why a formula - returns an error, where a value is coerced). Triggers on file paths under `src/`, or when the + returns an error, where a value is coerced). Triggers on file paths under `hyperformula/src/`, or when the user describes a symptom in a calculation without naming a file. This is the primary entry point for engine development — when in doubt, load it. --- @@ -17,7 +17,7 @@ Always: | File | Why | |---|---| -| [`ARCHITECTURE.md`](../../../dev-docs/ARCHITECTURE.md) | The pipeline, the core modules, and the invariants that hold everywhere in `src/` | +| [`ARCHITECTURE.md`](../../../hyperformula/dev-docs/ARCHITECTURE.md) | The pipeline, the core modules, and the invariants that hold everywhere in `hyperformula/src/` | | [`CODE-STYLE.md`](../../../dev-docs/CODE-STYLE.md) | Style, and which paths are hot enough that complexity matters | | [`DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md) | What the change must contain before review | @@ -25,12 +25,12 @@ Then the page for the stage you are changing: | File | For | |---|---| -| [`PARSER.md`](../../../dev-docs/PARSER.md) | `src/parser/` — formula text to AST, and back | -| [`INTERPRETER.md`](../../../dev-docs/INTERPRETER.md) | `src/interpreter/` — AST to value, and built-in functions | -| [`DEPENDENCY-GRAPH.md`](../../../dev-docs/DEPENDENCY-GRAPH.md) | `src/DependencyGraph/` — dependency tracking and recalculation order | -| [`FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md) | `src/interpreter/functionMetadata/` — function descriptions | -| [`I18N.md`](../../../dev-docs/I18N.md) | `src/i18n/` — function-name translations | -| [`TESTING.md`](../../../dev-docs/TESTING.md) | Writing the test the change needs | +| [`PARSER.md`](../../../hyperformula/dev-docs/PARSER.md) | `hyperformula/src/parser/` — formula text to AST, and back | +| [`INTERPRETER.md`](../../../hyperformula/dev-docs/INTERPRETER.md) | `hyperformula/src/interpreter/` — AST to value, and built-in functions | +| [`DEPENDENCY-GRAPH.md`](../../../hyperformula/dev-docs/DEPENDENCY-GRAPH.md) | `hyperformula/src/DependencyGraph/` — dependency tracking and recalculation order | +| [`FUNCTION-CATALOGUE.md`](../../../hyperformula/dev-docs/FUNCTION-CATALOGUE.md) | `hyperformula/src/interpreter/functionMetadata/` — function descriptions | +| [`I18N.md`](../../../hyperformula/dev-docs/I18N.md) | `hyperformula/src/i18n/` — function-name translations | +| [`TESTING.md`](../../../hyperformula/dev-docs/TESTING.md) | Writing the test the change needs | ## 2. Locate the stage before changing anything @@ -38,14 +38,14 @@ The engine is a pipeline: `CellContentParser` → `parser/` → `GraphBuilder` | Symptom | Stage | |---|---| -| Does not parse, or parses wrongly | `src/parser/` | -| `getCellFormula` returns something the user never typed | `src/parser/Unparser.ts` | -| A function returns the wrong value or error | `src/interpreter/plugin/` — skill `hyperformula-function-dev` | -| Value right, but stale after an edit | `src/DependencyGraph/`, `src/Evaluator.ts` | -| Wrong after adding or removing rows or columns | `src/dependencyTransformers/`, `LazilyTransformingAstService.ts` | -| Wrong in one language only | `src/i18n/languages/` — skill `i18n-translations` | -| Coercion or comparison is wrong | `src/interpreter/ArithmeticHelper.ts` | -| The public API disagrees with its docs | `src/HyperFormula.ts` | +| Does not parse, or parses wrongly | `hyperformula/src/parser/` | +| `getCellFormula` returns something the user never typed | `hyperformula/src/parser/Unparser.ts` | +| A function returns the wrong value or error | `hyperformula/src/interpreter/plugin/` — skill `hyperformula-function-dev` | +| Value right, but stale after an edit | `hyperformula/src/DependencyGraph/`, `hyperformula/src/Evaluator.ts` | +| Wrong after adding or removing rows or columns | `hyperformula/src/dependencyTransformers/`, `LazilyTransformingAstService.ts` | +| Wrong in one language only | `hyperformula/src/i18n/languages/` — skill `i18n-translations` | +| Coercion or comparison is wrong | `hyperformula/src/interpreter/ArithmeticHelper.ts` | +| The public API disagrees with its docs | `hyperformula/src/HyperFormula.ts` | A bug that looks like an interpreter problem is often a parser or graph problem. Confirm which before editing. Use the `typescript-lsp` plugin to find a definition or its callers; grep is for text, not symbols. @@ -53,7 +53,7 @@ A bug that looks like an interpreter problem is often a parser or graph problem. Write the failing test before the fix and watch it fail — skill `test-writing-discipline`. For a calculation bug the smallest reproduction is a two-line `buildFromArray` plus one `getCellValue`. -If `test/hyperformula-tests/` is absent, `npm run test:jest` runs only the smoke tests and reports a clean pass over almost nothing. Run `npm run test:setup-private` first, and after every branch switch. +If `hyperformula/test/hyperformula-tests/` is absent, `npm run test:jest` runs only the smoke tests and reports a clean pass over almost nothing. Run `npm run test:setup-private` first, and after every branch switch. ## 4. Change, then run the fast loop diff --git a/.claude/skills/hyperformula-function-dev/SKILL.md b/.claude/skills/hyperformula-function-dev/SKILL.md index d0cb5c39bd..e99a778432 100644 --- a/.claude/skills/hyperformula-function-dev/SKILL.md +++ b/.claude/skills/hyperformula-function-dev/SKILL.md @@ -1,6 +1,6 @@ --- name: hyperformula-function-dev -paths: src/interpreter/** +paths: hyperformula/src/interpreter/** description: Use when adding a new built-in spreadsheet function to HyperFormula, changing an existing one's signature, arguments, return type, or error behaviour, or when a function returns the wrong value or the wrong error. Covers the FunctionPlugin contract, runFunction and argument metadata, the function metadata catalogue, translations, and the full end-to-end checklist. --- @@ -8,9 +8,9 @@ description: Use when adding a new built-in spreadsheet function to HyperFormula | File | Why | |---|---| -| [`INTERPRETER.md`](../../../dev-docs/INTERPRETER.md#built-in-functions) | The plugin contract, `runFunction`, and every argument and function metadata field. Read this before writing any code. | -| [`FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md) | What the catalogue entry must contain, and the two ways to get it wrong | -| [`I18N.md`](../../../dev-docs/I18N.md) | Where to source a translation, and why an invented one cannot be taken back | +| [`INTERPRETER.md`](../../../hyperformula/dev-docs/INTERPRETER.md#built-in-functions) | The plugin contract, `runFunction`, and every argument and function metadata field. Read this before writing any code. | +| [`FUNCTION-CATALOGUE.md`](../../../hyperformula/dev-docs/FUNCTION-CATALOGUE.md) | What the catalogue entry must contain, and the two ways to get it wrong | +| [`I18N.md`](../../../hyperformula/dev-docs/I18N.md) | Where to source a translation, and why an invented one cannot be taken back | | [`TESTING.md`](../../../dev-docs/TESTING.md#what-each-kind-of-change-needs) | The list of cases a function change must cover | | [`DEFINITION-OF-DONE.md`](../../../dev-docs/DEFINITION-OF-DONE.md) | What the change must contain before review | @@ -18,7 +18,7 @@ description: Use when adding a new built-in spreadsheet function to HyperFormula A function is not done until all five agree, and they do not fail the same way — a missing catalogue entry fails the docs build, a parameter-count mismatch only warns on the console, and the rest fail silently. -The list is in [`INTERPRETER.md`](../../../dev-docs/INTERPRETER.md#the-five-places-a-function-change-must-touch). Work through it there rather than from a copy; two of the five have their own page, linked from it. +The list is in [`INTERPRETER.md`](../../../hyperformula/dev-docs/INTERPRETER.md#the-five-places-a-function-change-must-touch). Work through it there rather than from a copy; two of the five have their own page, linked from it. ## 3. Declare the two things nothing cross-checks diff --git a/.claude/skills/hyperformula-unit-testing/SKILL.md b/.claude/skills/hyperformula-unit-testing/SKILL.md index d3d6605efc..3bcb498325 100644 --- a/.claude/skills/hyperformula-unit-testing/SKILL.md +++ b/.claude/skills/hyperformula-unit-testing/SKILL.md @@ -1,17 +1,17 @@ --- name: hyperformula-unit-testing -paths: test/** -description: Use when writing or modifying tests for HyperFormula, or when a change to `src/` needs test coverage. Covers the two suites, fetching the private suite, how to build an engine in a test, and what a function or CRUD change must cover. +paths: hyperformula/test/** +description: Use when writing or modifying tests for HyperFormula, or when a change to `hyperformula/src/` needs test coverage. Covers the two suites, fetching the private suite, how to build an engine in a test, and what a function or CRUD change must cover. --- ## 1. Read the relevant files from `dev-docs/` | File | Why | |---|---| -| [`TESTING.md`](../../../dev-docs/TESTING.md) | The two suites, how to write a case, and what each kind of change must cover | +| [`TESTING.md`](../../../hyperformula/dev-docs/TESTING.md) | The two suites, how to write a case, and what each kind of change must cover | | [`WORKTREES.md`](../../../dev-docs/WORKTREES.md) | Only when working in a linked worktree, where the private suite is absent entirely | -And [`test/README.md`](../../../test/README.md) for how the private suite is fetched and the environment variables it honours. +And [`hyperformula/test/README.md`](../../../dev-docs/README.md) for how the private suite is fetched and the environment variables it honours. ## 2. Attach the private suite before trusting anything @@ -19,7 +19,7 @@ And [`test/README.md`](../../../test/README.md) for how the private suite is fet npm run test:setup-private ``` -Run it after every branch switch. Without `test/hyperformula-tests/` the Jest run covers only the smoke tests and reports a clean pass over almost nothing — the most common false signal in this repository. `test:performance`, `test:compatibility` and the benchmarks all resolve into that directory, so without it they fail on a missing path rather than on an assertion; read the error before concluding the code is broken. +Run it after every branch switch. Without `hyperformula/test/hyperformula-tests/` the Jest run covers only the smoke tests and reports a clean pass over almost nothing — the most common false signal in this repository. `test:performance` and `test:compatibility` fail on a missing path rather than an assertion; read the error before concluding the code is broken. ## 3. Write the case from the requirement diff --git a/.claude/skills/i18n-translations/SKILL.md b/.claude/skills/i18n-translations/SKILL.md index 08bd5b49e4..84b729a067 100644 --- a/.claude/skills/i18n-translations/SKILL.md +++ b/.claude/skills/i18n-translations/SKILL.md @@ -1,6 +1,6 @@ --- name: i18n-translations -paths: src/i18n/** +paths: hyperformula/src/i18n/** description: Use when adding a built-in function that needs translated names, adding a language pack, or fixing a function name that is wrong in one language. Covers the translation sources, the rules, and what breaks when a key is missing. --- @@ -8,12 +8,12 @@ description: Use when adding a built-in function that needs translated names, ad | File | Why | |---|---| -| [`I18N.md`](../../../dev-docs/I18N.md) | Why translations are engine input rather than decoration, the rules, and the table of sources to translate from | -| [`PARSER.md`](../../../dev-docs/PARSER.md) | Only when changing separators or error literals — the lexer builds its token set from the language package | +| [`I18N.md`](../../../hyperformula/dev-docs/I18N.md) | Why translations are engine input rather than decoration, the rules, and the table of sources to translate from | +| [`PARSER.md`](../../../hyperformula/dev-docs/PARSER.md) | Only when changing separators or error literals — the lexer builds its token set from the language package | ## 2. Look the name up in a real source -Use the sources in [`I18N.md`](../../../dev-docs/I18N.md#where-to-find-a-translation), in the order listed. **Never invent or machine-translate a function name.** A wrong one ships to every user of that language pack and cannot be changed without breaking their formulas. +Use the sources in [`I18N.md`](../../../hyperformula/dev-docs/I18N.md#where-to-find-a-translation), in the order listed. **Never invent or machine-translate a function name.** A wrong one ships to every user of that language pack and cannot be changed without breaking their formulas. ## 3. Add the key to every language file @@ -32,4 +32,4 @@ npm run lint ## Adding a whole language pack -The file, its export in `src/i18n/languages/index.ts`, a key set identical to the other packs, a changelog entry, and `npm run bundle:languages` for the standalone UMD build. +The file, its export in `hyperformula/src/i18n/languages/index.ts`, a key set identical to the other packs, a changelog entry, and `npm run bundle:languages --workspace=hyperformula` for the standalone UMD build. diff --git a/.claude/skills/pr-creation/SKILL.md b/.claude/skills/pr-creation/SKILL.md index 2837d631fe..017a3a86d9 100644 --- a/.claude/skills/pr-creation/SKILL.md +++ b/.claude/skills/pr-creation/SKILL.md @@ -23,7 +23,7 @@ npm run lint npm run test:jest ``` -A green Jest run without `test/hyperformula-tests/` covers only the smoke tests. Confirm the suite is attached before calling it green. Do not open a pull request on a red run and describe it as ready. +A green Jest run without `hyperformula/test/hyperformula-tests/` covers only the smoke tests. Confirm the suite is attached before calling it green. Do not open a pull request on a red run and describe it as ready. ## 4. Push and open the pull request diff --git a/.claude/skills/test-writing-discipline/SKILL.md b/.claude/skills/test-writing-discipline/SKILL.md index 35bf9d1126..0a1506c7f6 100644 --- a/.claude/skills/test-writing-discipline/SKILL.md +++ b/.claude/skills/test-writing-discipline/SKILL.md @@ -23,7 +23,7 @@ When a test is red the default assumption is that the code is wrong. Changing th ## 5. Run it again and read the output -Never claim a test passes because the reasoning is sound. And before treating green as coverage, confirm `test/hyperformula-tests/` is present — `npm run test:setup-private`. +Never claim a test passes because the reasoning is sound. And before treating green as coverage, confirm `hyperformula/test/hyperformula-tests/` is present — `npm run test:setup-private`. ## If you cannot make it pass honestly diff --git a/.claude/skills/writing-docs-pages/SKILL.md b/.claude/skills/writing-docs-pages/SKILL.md index 97654218d6..774a956e97 100644 --- a/.claude/skills/writing-docs-pages/SKILL.md +++ b/.claude/skills/writing-docs-pages/SKILL.md @@ -10,17 +10,17 @@ description: Use when creating or editing a page in the HyperFormula documentati |---|---| | [`DOCS-CONTENT-GUIDE.md`](../../../dev-docs/DOCS-CONTENT-GUIDE.md) | How to write the page: structure, chunking, language, code examples, VuePress conventions, and the self-review checklist to run before finishing | | [`DOC-STANDARDS.md`](../../../dev-docs/DOC-STANDARDS.md) | When documentation is required, and describing HyperFormula rather than Excel | -| [`FUNCTION-CATALOGUE.md`](../../../dev-docs/FUNCTION-CATALOGUE.md) | Only when the change concerns the built-in functions page, which is generated from the catalogue | +| [`FUNCTION-CATALOGUE.md`](../../../hyperformula/dev-docs/FUNCTION-CATALOGUE.md) | Only when the change concerns the built-in functions page, which is generated from the catalogue | | [`BUILD.md`](../../../dev-docs/BUILD.md) | Which documentation files are generated, by which command | -And [`docs/README.md`](../../../docs/README.md), for what the portal contains and how to run it. +And [`docs/README.md`](../../../dev-docs/README.md), for what the portal contains and how to run it. ## 2. Change the source, not the output | To change | Edit | Then run | |---|---|---| -| What the functions page says about a function | its catalogue entry in `src/interpreter/functionMetadata/categories/` | `npm run docs:generate-function-docs` | -| The API reference | the JSDoc in `src/` | `npm run typedoc:build-api` | +| What the functions page says about a function | its catalogue entry in `hyperformula/src/interpreter/functionMetadata/categories/` | `npm run docs:generate-function-docs` | +| The API reference | the JSDoc in `hyperformula/src/` | `npm run typedoc:build-api` | | A guide | the file in `docs/guide/` | `npm run docs:dev` | `docs/guide/built-in-functions.md` and `docs/api/` are git-ignored build output. Editing them is always wrong, and the edit disappears on the next build. diff --git a/.eslintignore b/.eslintignore index 03546876e2..0ed40f6680 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,24 +5,41 @@ node_modules docs/examples/ # 3rd party -src/interpreter/plugin/3rdparty +hyperformula/src/interpreter/plugin/3rdparty # Configurations *.config.js -karma.* +hyperformula/.config/ +hyperformula/karma.* doc -test/_setupFiles/*.js +hyperformula/test/_setupFiles/*.js + +# Scripts, not linted +script + +# The source language packs, deliberately, and only for now. +# +# The old ignore list carried a bare `languages` entry, meant for the build +# output. An unanchored pattern matches a directory of that name at ANY depth, +# so it also excluded hyperformula/src/i18n/languages/ - and the `sort-keys` +# override that targets those files has therefore never run. Anchoring the +# build-output entry below exposed 881 pre-existing violations in them. +# +# Sorting 19 translation files is a change of its own, not a side effect of +# moving directories around. Delete this entry in that change. +hyperformula/src/i18n/languages # Auto-generated directories -commonjs -coverage -dist +hyperformula/commonjs +hyperformula/coverage +hyperformula/dist +hyperformula/es +hyperformula/languages +hyperformula/lib +hyperformula/test-jasmine +hyperformula/test-jest +hyperformula/typings +docs/.vuepress/dist doc -es -languages -lib -script -test-jasmine -test-jest typedoc -typings + diff --git a/.eslintrc.js b/.eslintrc.js index a5e9976e13..5eaf854bc6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,5 @@ +const path = require('path'); + module.exports = { root: true, ignorePatterns: ['.eslintrc.js'], @@ -15,7 +17,7 @@ module.exports = { }, parserOptions: { tsconfigRootDir: __dirname, - project: './tsconfig.json', + project: './hyperformula/tsconfig.json', createDefaultProgram: true, }, extends: [ @@ -134,7 +136,7 @@ module.exports = { { files: ['**/src/**/*.ts'], rules: { - 'license-header/header': [ 'error', './.config/source-license-header.js' ], + 'license-header/header': [ 'error', path.join(__dirname, 'hyperformula/.config/source-license-header.js') ], } }, { diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index 60c43dfc32..5ef21e7919 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -34,4 +34,4 @@ jobs: run: npm ci - name: Build docs - run: npm run docs:build + run: npm run docs:install && npm run docs:build diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 936ef53fa2..b745b1a0b4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -36,10 +36,10 @@ jobs: with: ssh-key: ${{ secrets.DEPLOY_TOKEN }} repository: handsontable/hyperformula-tests - path: test/hyperformula-tests + path: hyperformula/test/hyperformula-tests - name: Fetch hyperformula-tests and sync branches - run: cd test && ./fetch-tests.sh + run: cd hyperformula/test && ./fetch-tests.sh - name: Install dependencies run: npm ci diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 9ef3c8190c..80dd56bb05 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -34,16 +34,16 @@ jobs: with: ssh-key: ${{ secrets.DEPLOY_TOKEN }} repository: handsontable/hyperformula-tests - path: test/hyperformula-tests + path: hyperformula/test/hyperformula-tests - name: Fetch hyperformula-tests and sync branches - run: cd test && ./fetch-tests.sh + run: cd hyperformula/test && ./fetch-tests.sh - name: (base) Install dependencies run: npm ci - name: (base) Run performance tests - run: npm run benchmark:write-to-file base.json + run: npm run benchmark:write-to-file --workspace=hyperformula base.json - name: (head) Checkout main repository uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # https://github.com/actions/checkout/releases/tag/v2.3.4 @@ -55,11 +55,11 @@ jobs: - name: (head) Run performance tests run: | - npm run benchmark:write-to-file head.json + npm run benchmark:write-to-file --workspace=hyperformula head.json - name: Compare the results run: | - npm run benchmark:compare-benchmarks base.json head.json performance-report.md + npm run benchmark:compare-benchmarks --workspace=hyperformula base.json head.json performance-report.md - name: Publish a comment - header uses: marocchino/sticky-pull-request-comment@6804b5ad49d19c10c9ae7cf5057352f7ff333f31 # https://github.com/marocchino/sticky-pull-request-comment/tree/v1.6.0 @@ -73,4 +73,4 @@ jobs: with: append: true GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - path: performance-report.md + path: hyperformula/performance-report.md diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c87e1f36d1..8eebea9a1a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -28,7 +28,7 @@ jobs: run: npm ci - name: Build docs - run: npm run docs:build + run: npm run docs:install && npm run docs:build - name: Deploy to GH pages uses: peaceiris/actions-gh-pages@ba0b7df03e25ff29c924be8149041119e9421ea6 # https://github.com/peaceiris/actions-gh-pages/releases/tag/v3.5.6 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9486ae9250..264d67847b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,10 +36,10 @@ jobs: with: ssh-key: ${{ secrets.DEPLOY_TOKEN }} repository: handsontable/hyperformula-tests - path: test/hyperformula-tests + path: hyperformula/test/hyperformula-tests - name: Fetch hyperformula-tests and sync branches - run: cd test && ./fetch-tests.sh + run: cd hyperformula/test && ./fetch-tests.sh - name: Install dependencies run: npm ci @@ -74,10 +74,10 @@ jobs: with: ssh-key: ${{ secrets.DEPLOY_TOKEN }} repository: handsontable/hyperformula-tests - path: test/hyperformula-tests + path: hyperformula/test/hyperformula-tests - name: Fetch hyperformula-tests and sync branches - run: cd test && ./fetch-tests.sh + run: cd hyperformula/test && ./fetch-tests.sh - name: Install dependencies run: npm ci diff --git a/.gitignore b/.gitignore index 886d7289b9..4409ab3db0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,32 @@ .idea/ .vscode -/commonjs/ -/coverage/ -/dist/ -/doc/ +/hyperformula/commonjs/ +/hyperformula/coverage/ +/hyperformula/dist/ +/hyperformula/doc/ /docs/api/ /docs/functions/ /docs/.vuepress/dist/ /docs/.vuepress/api-sidebar-relative.json /docs/.vuepress/api-sidebar.json -/typedoc/ -/es/ -/languages/ -/lib/ -/test-jasmine/ -/test-jest/ +/hyperformula/typedoc/ +/hyperformula/es/ +/hyperformula/languages/ +/hyperformula/lib/ +/hyperformula/test-jasmine/ +/hyperformula/test-jest/ node_modules/ -/typings/ -/storage/ +/hyperformula/typings/ +/hyperformula/storage/ +# Copied in by the package's prepack so the tarball carries one; the source is the +# CHANGELOG.md at the repository root. +/hyperformula/CHANGELOG.md *.iml dev*.html .DS_Store -/test/hyperformula-tests/ +/hyperformula/test/hyperformula-tests/ # Generated at docs:build from built-in-functions.tmpl.md (HF-249 single-source); do not commit. docs/guide/built-in-functions.md diff --git a/.worktreeinclude b/.worktreeinclude index c2c6c2f70d..74111d6626 100644 --- a/.worktreeinclude +++ b/.worktreeinclude @@ -7,19 +7,26 @@ # This covers only the file half of setting up a worktree. It cannot install # dependencies and it cannot fetch the private test suite. See dev-docs/WORKTREES.md. -# Cloudflare Worker local secrets, used by the docs deploy/preview scripts. -# Absent from a fresh worktree, so `wrangler` commands fail there with no -# explanation that points at the worktree. -.dev.vars -.dev.vars.* +# Nothing is copied into a worktree. The list below records what was considered +# and rejected, so it does not get added back. # Deliberately NOT copied — listing these here would cause real bugs: # +# .dev.vars .dev.vars.* +# Cloudflare Worker deploy credentials. Copying them +# puts a deploy token on disk in every worktree, +# including the isolated ones subagents get, for the +# convenience of the one maintainer who actually runs +# `wrangler`. They can copy the file themselves. +# # node_modules A copy is not an install: `.bin` shims and native -# builds do not survive it. Run `npm ci`, or symlink it -# with `worktree.symlinkDirectories`. +# builds do not survive it. Run `npm ci` in the worktree. +# Do NOT symlink it back to the main checkout: in a +# workspace `npm ci` then installs THROUGH the symlink +# and rewrites the main checkout's dependency tree. See +# dev-docs/WORKTREES.md. # -# test/hyperformula-tests/ +# hyperformula/test/hyperformula-tests/ # The private suite is BRANCH-MATCHED to this repository # (test/fetch-tests.sh checks out the branch of the same # name). A copy carries the source branch's tests and @@ -27,7 +34,7 @@ # suite at all. Run `npm run test:setup-private` in the # worktree instead. # -# lib/ dist/ es/ commonjs/ typings/ languages/ +# hyperformula/{lib,dist,es,commonjs,typings,languages}/ # Build outputs. A stale copy makes `npm run test:browser` # and the bundle checks pass or fail against the other # branch's build. Run `npm run bundle-all`. diff --git a/README.md b/README.md index 8d3d14707b..5e46f88181 100644 --- a/README.md +++ b/README.md @@ -1,116 +1,41 @@ -
-

- - HyperFormula - A headless spreadsheet, a parser and evaluator of Excel formulas - -

+# HyperFormula monorepo -

- An open-source headless spreadsheet for business web apps -

+[HyperFormula](https://hyperformula.handsontable.com/) is a headless spreadsheet calculation engine in TypeScript. It parses formulas, tracks cell dependencies, and recalculates incrementally, in the browser and in Node. -

- npm total downloads - npm monthly downloads - GitHub contributors - Known Vulnerabilities -
- FOSSA Status - GitHub Workflow Status - codecov -

+This repository holds the engine and everything built around it. ---- +| Directory | What it is | Published | +|---|---|---| +| [`hyperformula/`](hyperformula/) | The calculation engine. **Start here** — its [README](dev-docs/README.md) is the product documentation. | yes | +| [`hyperformula-ui/`](hyperformula-ui/) | UI components for working with HyperFormula. Not imported yet. | yes | -HyperFormula is a headless spreadsheet built in TypeScript, serving as both a parser and evaluator of spreadsheet formulas. It can be integrated into your browser or utilized as a service with Node.js as your back-end technology. +The published packages release together, on one version, and share the single [`CHANGELOG.md`](CHANGELOG.md) at the root. +| [`docs/`](docs/) | The documentation portal. Installed separately; not a workspace member. | no | -## What HyperFormula can be used for? - -HyperFormula doesn't assume any existing user interface, making it a general-purpose library that can be used in various business applications. Here are some examples: - -- Deterministic compute layer for AI & LLMs -- Calculated fields in CRM and ERP software -- Custom spreadsheet-like app -- Business logic builder -- Forms and form builder -- Educational app -- Online calculator - -## Features - -- [Function syntax compatible with Microsoft Excel](https://hyperformula.handsontable.com/docs/guide/compatibility-with-microsoft-excel.html) and [Google Sheets](https://hyperformula.handsontable.com/docs/guide/compatibility-with-google-sheets.html) -- High-speed parsing and evaluation of spreadsheet formulas -- [A library of ~400 built-in functions](https://hyperformula.handsontable.com/docs/guide/built-in-functions.html) -- [Support for custom functions](https://hyperformula.handsontable.com/docs/guide/custom-functions.html) -- [Support for Node.js](https://hyperformula.handsontable.com/docs/guide/server-side-installation.html#install-with-npm-or-yarn) -- [Support for undo/redo](https://hyperformula.handsontable.com/docs/guide/undo-redo.html) -- [Support for CRUD operations](https://hyperformula.handsontable.com/docs/guide/basic-operations.html) -- [Support for clipboard](https://hyperformula.handsontable.com/docs/guide/clipboard-operations.html) -- [Support for named expressions](https://hyperformula.handsontable.com/docs/guide/named-expressions.html) -- [Support for data sorting](https://hyperformula.handsontable.com/docs/guide/sorting-data.html) -- [Support for formula localization with 17 built-in languages](https://hyperformula.handsontable.com/docs/guide/i18n-features.html) -- Easy integration with any front-end or back-end application -- GPLv3 or a [commercial license](https://handsontable.com/get-a-quote) -- Maintained by the team that stands behind the [Handsontable](https://handsontable.com/) data grid - -## Documentation - -- [Client-side installation](https://hyperformula.handsontable.com/docs/guide/client-side-installation.html) -- [Server-side installation](https://hyperformula.handsontable.com/docs/guide/server-side-installation.html) -- [Basic usage](https://hyperformula.handsontable.com/docs/guide/basic-usage.html) -- [Configuration options](https://hyperformula.handsontable.com/docs/guide/configuration-options.html) -- [List of built-in functions](https://hyperformula.handsontable.com/docs/guide/built-in-functions.html) -- [API Reference](https://hyperformula.handsontable.com/docs/api/) - -## Integrations - -- [Integration with React](https://hyperformula.handsontable.com/docs/guide/integration-with-react.html#demo) -- [Integration with Angular](https://hyperformula.handsontable.com/docs/guide/integration-with-angular.html#demo) -- [Integration with Vue](https://hyperformula.handsontable.com/docs/guide/integration-with-vue.html#demo) -- [Integration with Svelte](https://hyperformula.handsontable.com/docs/guide/integration-with-svelte.html#demo) - -## Installation and usage - -Install the library from [npm](https://www.npmjs.com/package/hyperformula) like so: +## Getting started ```bash -npm install hyperformula +npm ci # installs the workspace +npm run test:setup-private # attaches the private test suite, if you have access +npm run test:jest # the fast test loop +npm run bundle-all # every bundle for the engine ``` -Once installed, you can use it to develop applications tailored to your specific business needs. Here, we've used it to craft a form that calculates mortgage payments using the `PMT` formula. - -```js -import { HyperFormula } from 'hyperformula'; - -// Create a HyperFormula instance -const hf = HyperFormula.buildEmpty({ licenseKey: 'gpl-v3' }); - -// Add an empty sheet -const sheetName = hf.addSheet('Mortgage Calculator'); -const sheetId = hf.getSheetId(sheetName); - -// Enter the mortgage parameters -hf.addNamedExpression('AnnualInterestRate', '8%'); -hf.addNamedExpression('NumberOfMonths', 360); -hf.addNamedExpression('LoanAmount', 800000); +Root scripts fan out to the packages; run a package's own scripts from its directory, or with `--workspace=hyperformula`. -// Use the PMT function to calculate the monthly payment -hf.setCellContents({ sheet: sheetId, row: 0, col: 0 }, [['Monthly Payment', '=PMT(AnnualInterestRate/12, NumberOfMonths, -LoanAmount)']]); +The documentation portal installs on its own: -// Display the result -console.log(`${hf.getCellValue({ sheet: sheetId, row: 0, col: 0 })}: ${hf.getCellValue({ sheet: sheetId, row: 0, col: 1 })}`); +```bash +npm run docs:install +npm run docs:dev ``` -[Run this code in StackBlitz](https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.4.x/mortgage-calculator) - -HyperFormula ships an official Claude skill and machine-readable docs, so your AI coding agent can scaffold, configure, and debug HyperFormula correctly. To install the skill in Claude Code, or to point Cursor, GitHub Copilot, or another agent at the docs, see [Set up your coding agent](https://hyperformula.handsontable.com/docs/guide/setup-coding-agent.html). - -## Contributing - -Contributions are welcome, but before you make them, please read the [Contributing Guide](https://hyperformula.handsontable.com/docs/guide/contributing.html) and accept the [Contributor License Agreement](https://goo.gl/forms/yuutGuN0RjsikVpM2). +## Contributing and development -## License +- External contributors: [`CONTRIBUTING.md`](CONTRIBUTING.md) +- Everyone working on the source, including AI agents: [`dev-docs/README.md`](dev-docs/README.md) — architecture, build, testing, standards, and the definition of done +- AI coding agents: [`AGENTS.md`](AGENTS.md) -HyperFormula is available under two different licenses: GPLv3 and proprietary. The proprietary license can be purchased by [contacting our team](https://handsontable.com/get-a-quote) at Handsontable. +## Licence -Copyright (c) Handsoncode +GPL-3.0-only, plus a commercial licence. See [`LICENSE.txt`](LICENSE.txt). diff --git a/dev-docs/AGENT-TOOLING.md b/dev-docs/AGENT-TOOLING.md index ecdb32c016..e6654010c8 100644 --- a/dev-docs/AGENT-TOOLING.md +++ b/dev-docs/AGENT-TOOLING.md @@ -21,10 +21,11 @@ Committed, so every developer gets the same setup. | Key | Why | |---|---| | `enabledPlugins` | `typescript-lsp` — language-server go-to-definition and find-references. Use it instead of grepping for a symbol's definition or callers; grep stays right for text searches. | -| `permissions.deny` | Blocks agent reads of every generated and built path. The build outputs are git-ignored, so content searches already skip them, but nothing otherwise stops an agent opening `dist/hyperformula.js` or answering a behaviour question from `typings/` instead of `src/`. | -| `worktree.symlinkDirectories` | Symlinks `node_modules` into each worktree rather than duplicating it. See [`WORKTREES.md`](WORKTREES.md). | +| `permissions.deny` | Blocks agent reads of **build artifacts**. They are git-ignored, so content searches already skip them, but nothing otherwise stops an agent opening `dist/hyperformula.js` or answering a behaviour question from `typings/` instead of `hyperformula/src/`. | -`node_modules/` and `package-lock.json` are deliberately readable: reading a dependency's source is sometimes the right move when debugging, and a deny rule would also block a targeted grep for a dependency version. +**Generated documentation is not a build artifact, and is deliberately readable.** `docs/api/` and `docs/guide/built-in-functions.md` are produced by a build step, but they are the API reference and the function reference — reading them to answer a question is the right move, and `FUNCTION-CATALOGUE.md` links straight into them. The rule is about *artifacts*: bundles, declarations, coverage, and the compiled site. Editing either of those files is still always wrong; that is what the build regenerates. + +`node_modules/` and `package-lock.json` are deliberately readable too: reading a dependency's source is sometimes the right move when debugging, and a deny rule would also block a targeted grep for a dependency version. Relative deny patterns anchor at the session's working directory, and project settings are not inherited from parent directories — these rules apply to sessions started at the repository root. @@ -42,7 +43,7 @@ A skill holds the **steps**: what to do, in what order, and what to check. It do | Skill | For | |---|---| -| `hyperformula-dev` | Any work in `src/` — the entry point | +| `hyperformula-dev` | Any work in `hyperformula/src/` — the entry point | | `hyperformula-function-dev` | Adding or changing a built-in function | | `hyperformula-unit-testing` | Writing or modifying tests | | `test-writing-discipline` | Any red test, and any test that might be going green for the wrong reason | diff --git a/dev-docs/BUILD.md b/dev-docs/BUILD.md index 785fad27ca..268d86ab83 100644 --- a/dev-docs/BUILD.md +++ b/dev-docs/BUILD.md @@ -1,31 +1,30 @@ -# Building — the maintainer's view +# Building and releasing -The command reference lives in the public guide, [`docs/guide/building.md`](../docs/guide/building.md): output formats, every `bundle:*` and `verify:*` script, the test commands, and the linter. Read that first. +How the workspace installs, and the repository-level steps: generating the documentation, deploying the portal, and cutting a release. -This page holds only what the guide does not cover. +The engine's own build — the intermediate `lib/`, the bundles, and packaging — is in [`hyperformula/dev-docs/BUILD.md`](BUILD.md). ## Install -Node version is pinned in [`.nvmrc`](../.nvmrc) — 22, the same everywhere in the repository. The repository uses npm with a committed `package-lock.json`, so install with `npm ci`, not `npm install`. +Node version is pinned in [`.nvmrc`](../.nvmrc) — 22, the same in every package. The repository uses npm workspaces with a committed `package-lock.json`, so install with `npm ci`, not `npm install`. -## The intermediate build +```bash +npm ci # the workspace: hyperformula, hyperformula-ui +npm run docs:install # the portal, which is not a workspace member +``` -`npm run compile` runs `tsc` into `lib/`. Every bundle reads `lib/`, not `src/`, so a bundle built without recompiling ships the previous source. - -`npm run bundle-all` chains `clean`, `compile`, every `bundle:*`, then `verify-bundles`. The individual `bundle:*` scripts recompile first unless `HF_COMPILE=1` is set — `script/if-ne-env.js` is what skips the redundant recompile inside `bundle-all`. - -`npm run clean` removes `coverage/`, `commonjs/`, `dist/`, `es/`, `languages/`, `lib/`, `typings/`, and `test-jasmine/`. - -`languages/` — the standalone UMD language packs — is a build output like the rest, and is missing from the guide's list of output formats. +Root scripts fan out to the packages. `npm run lint` is the exception: it runs once, from the root, over the whole repository. To run a package's own scripts, work from its directory or pass `--workspace=hyperformula`. ## Generated documentation | File | Generated by | Rule | |---|---|---| -| `docs/api/` | `npm run typedoc:build-api` | Never edit. Change the JSDoc in `src/`. | -| `docs/guide/built-in-functions.md` | `npm run docs:generate-function-docs`, from `built-in-functions.tmpl.md` and the metadata catalogue | Never edit, never commit. | +| `docs/api/` | `npm run typedoc:build-api` — TypeDoc runs inside `hyperformula/`, where the `tsconfig.json` is, and writes across into the portal | Never edit. Change the JSDoc in `hyperformula/src/`. | +| `docs/guide/built-in-functions.md` | `npm run docs:generate-function-docs`, which runs `docs/script/generate-builtin-functions-doc.ts` against the metadata catalogue | Never edit, never commit. Needs the portal installed. | -Both are git-ignored, and both are regenerated as the first step of `docs:dev` and `docs:build`. `docs:generate-function-docs` is a gate, not a formatter: it fails the build on a missing catalogue entry or a `'Custom'` category. See [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md). +The generator lives in `docs/script/` rather than beside the engine build scripts because it uses `@vuepress/shared-utils` for slugs, and that package only resolves inside a full VuePress dependency tree. + +Both are git-ignored, and both are regenerated as the first step of `docs:dev` and `docs:build`. `docs:generate-function-docs` is a gate, not a formatter: it fails the build on a missing catalogue entry or a `'Custom'` category. See [`FUNCTION-CATALOGUE.md`](../hyperformula/dev-docs/FUNCTION-CATALOGUE.md). ## Deploying the portal @@ -37,14 +36,10 @@ The portal is served by a Cloudflare Worker, and **deployment is driven by Worke | `npm run docs:deploy:cf` | `wrangler deploy` — production | | `npm run docs:preview:cf` | `wrangler versions upload` — a per-branch preview URL | -Config is [`wrangler.jsonc`](../wrangler.jsonc); the Worker entry point is [`worker/index.js`](../worker/index.js). +Config is [`docs/wrangler.jsonc`](../docs/wrangler.jsonc); the Worker entry point is [`docs/worker/index.js`](../docs/worker/index.js). ## Release and licences `npm run release` runs `script/release/release.sh`. Releasing is maintainer-owned — do not invent steps around it, and do not run it as part of another task. `npm run check:licenses` asserts that every production dependency carries a permissive licence. - -## Packaging - -`npm run verify:publish-package` runs `npm pack` through `script/check-publish-package.js` and checks what would actually ship. Run it whenever a change touches `package.json`, `.npmignore`, or the `exports`/`typings` surface. diff --git a/dev-docs/CODE-STYLE.md b/dev-docs/CODE-STYLE.md index 0edd7f670a..9fe1eb71cc 100644 --- a/dev-docs/CODE-STYLE.md +++ b/dev-docs/CODE-STYLE.md @@ -14,25 +14,11 @@ ESLint is the source of truth for formatting and code rules — run `npm run lin - Match the style of the surrounding code and of the project as a whole. New code should not stand out from its neighbours. - Optimize for long-term maintainability: someone else should be able to read, extend, and safely change the code months from now. -## Performance - -HyperFormula is a calculation engine, so the performance of production code is a feature, not an afterthought. - -- Consider the computational complexity of every change, especially in code that runs **per cell, per formula, or per dependency-graph node**. Nested loops over ranges, and repeated work that could be computed once or cached, are the usual suspects. -- Pick the best complexity that still keeps the code readable. When a faster algorithm is harder to follow, explain the trade-off in a JSDoc comment. -- Run `npm run test:performance` for changes that may affect the evaluation or CRUD hot paths. - -Hot paths worth knowing about before touching them: - -| Path | Why it is hot | -|---|---| -| `src/interpreter/` function implementations | Runs once per formula, and once per cell for array-broadcast arguments | -| `src/DependencyGraph/` vertex and range mapping | Every read and every structural change goes through it | -| `src/Evaluator.ts` | Walks the whole recalculation order | -| `src/parser/ParserWithCaching.ts` | Cache misses reparse; a change that defeats the cache is a regression | -| `src/LazilyTransformingAstService.ts` | Deferred AST rewrites after row/column/sheet operations | - ## TypeScript -- The public API surface is `src/HyperFormula.ts` and the types it exports; `npm run bundle:typings` emits them into `typings/`. +- The public API surface is `hyperformula/src/HyperFormula.ts` and the types it exports; `npm run bundle:typings --workspace=hyperformula` emits them into `hyperformula/typings/`. - `npm run verify:typings` (`tsc --noEmit`) must pass. A change that only compiles because of an `as` cast usually has a modelling problem behind it. + +## Performance + +Where a package's code is on a hot path, its own reference says so and names the paths. For the engine — where performance is a feature rather than an afterthought — that is [`hyperformula/dev-docs/PERFORMANCE.md`](../hyperformula/dev-docs/PERFORMANCE.md). diff --git a/dev-docs/DEFINITION-OF-DONE.md b/dev-docs/DEFINITION-OF-DONE.md index 0be743a922..8611a4b6c1 100644 --- a/dev-docs/DEFINITION-OF-DONE.md +++ b/dev-docs/DEFINITION-OF-DONE.md @@ -2,15 +2,15 @@ Every change to production code — bug fix, feature, or improvement — must include all of the following **before** a code review is requested. -1. **The production change**, including every supported language pack in `src/i18n/languages/` when function names are involved. -2. **Automatic tests** in `test/`: +1. **The production change**, including every supported language pack in `hyperformula/src/i18n/languages/` when function names are involved. +2. **Automatic tests** in `hyperformula/test/`: - bug fix — at least one test that reproduces the bug; - new feature — a set of tests that precisely describe the feature; - - pull requests from external contributors put tests in `test/`; the internal team adds them to the private repository through a separate pull request. + - pull requests from external contributors put tests in `hyperformula/test/`; the internal team adds them to the private repository through a separate pull request. See [`TESTING.md`](TESTING.md). 3. **Documentation updates** matching the change. A breaking change also needs a section in the migration guide. See [`DOC-STANDARDS.md`](DOC-STANDARDS.md). 4. **JSDoc** on classes and functions, plus a high-level description of the concepts used in any complex fragment. -5. **A changelog entry**, unless the change is documentation-only (guides, JSDoc, README). +5. **A changelog entry**. Not needed for documentation-only, test-only, or CI and tooling changes, nor for a bug that was introduced and never released. The full rule, and how to write the entry, is in [`DOC-STANDARDS.md`](DOC-STANDARDS.md#the-changelog). 6. **A pull request description** — kept current as the branch evolves, not written once and left to rot. Every element must be not only present but correct: the changelog entry must describe the change accurately, and the documentation must match the new behaviour. @@ -27,7 +27,7 @@ Every change in the pull request must be relevant to the issue it solves. Unrela ## Breaking changes -The public API is `src/HyperFormula.ts` and the types it exports. Avoid breaking it. When a change genuinely requires a break: +The public API is `hyperformula/src/HyperFormula.ts` and the types it exports. Avoid breaking it. When a change genuinely requires a break: - state it explicitly in the pull request description and the changelog entry; - add a migration-guide section describing what breaks and what to do instead; diff --git a/dev-docs/DOC-STANDARDS.md b/dev-docs/DOC-STANDARDS.md index c5df378a29..4edba41885 100644 --- a/dev-docs/DOC-STANDARDS.md +++ b/dev-docs/DOC-STANDARDS.md @@ -1,6 +1,6 @@ # Documentation standards -These apply across the whole repository: guides, the API reference, JSDoc inside `src/`, the changelog, migration guides, and README files. +These apply across the whole repository: guides, the API reference, JSDoc inside `hyperformula/src/`, the changelog, migration guides, and README files. *How* to write a page — structure, chunking, language, code examples, and the self-review checklist — is [`DOCS-CONTENT-GUIDE.md`](DOCS-CONTENT-GUIDE.md). This page is *when* documentation is required and what it must be true about. @@ -21,8 +21,8 @@ This is not a licence to make a guide unreadable on its own. A guide page repeat | File | Generated by | Rule | |---|---|---| -| `docs/api/` | `npm run typedoc:build-api` | Never edit. Change the JSDoc in `src/`. | -| `docs/guide/built-in-functions.md` | `npm run docs:generate-function-docs` | Never edit, never commit. Change the metadata catalogue — see [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md). | +| `docs/api/` | `npm run typedoc:build-api` | Never edit. Change the JSDoc in `hyperformula/src/`. | +| `docs/guide/built-in-functions.md` | `npm run docs:generate-function-docs` | Never edit, never commit. Change the metadata catalogue — see [`FUNCTION-CATALOGUE.md`](../hyperformula/dev-docs/FUNCTION-CATALOGUE.md). | Both are git-ignored. A missing `docs/api/` folder means it has not been built yet; run `npm run docs:build`. @@ -63,6 +63,6 @@ A breaking change says what breaks and what to do instead, and still needs a mig ## The documentation portal -`docs/` is a VuePress site. How to run it, what is generated, and how to add a page are in [`docs/README.md`](../docs/README.md). +`docs/` is a VuePress site. How to run it, what is generated, and how to add a page are in [`docs/README.md`](README.md). Two rules from above bear repeating there, because they are the ones most often broken while writing a guide: do not restate the API reference, and describe HyperFormula rather than Excel. diff --git a/dev-docs/README.md b/dev-docs/README.md index 6ac11238b1..9cf49d6aa3 100644 --- a/dev-docs/README.md +++ b/dev-docs/README.md @@ -14,29 +14,33 @@ Everything a developer needs to know lives here or is linked from here. External In every directory, `CLAUDE.md` is a symlink to its sibling `AGENTS.md`. Edit `AGENTS.md` — the symlink keeps Claude Code and Cursor reading the same single source. +`dev-docs/` exists at two levels. This one holds what applies to the whole repository; each package holds its own internals. A fact has exactly one home in exactly one of them — the split is by ownership, never a copy. + ## Where to look +Repository-wide standards and process live here. Each package documents its own internals: the engine's are in [`hyperformula/dev-docs/`](../hyperformula/dev-docs/README.md). + | You are working on | Read | |---|---| -| Anything in `src/` | [`ARCHITECTURE.md`](ARCHITECTURE.md) — the pipeline, the core modules, and the invariants that hold everywhere in `src/` | -| Formula parsing | [`PARSER.md`](PARSER.md) | -| Formula evaluation, or a built-in function | [`INTERPRETER.md`](INTERPRETER.md) | -| Function descriptions in the API and the docs | [`FUNCTION-CATALOGUE.md`](FUNCTION-CATALOGUE.md) | -| Dependency tracking and recalculation order | [`DEPENDENCY-GRAPH.md`](DEPENDENCY-GRAPH.md) | -| Function-name translations | [`I18N.md`](I18N.md) | -| Tests | [`TESTING.md`](TESTING.md) | | What a change must include before review | [`DEFINITION-OF-DONE.md`](DEFINITION-OF-DONE.md) | -| Style and performance | [`CODE-STYLE.md`](CODE-STYLE.md) | +| Code style | [`CODE-STYLE.md`](CODE-STYLE.md) | +| What a test must prove, and how a case is written | [`TESTING.md`](TESTING.md) | | Documentation rules, and the changelog | [`DOC-STANDARDS.md`](DOC-STANDARDS.md) | | Writing a documentation page | [`DOCS-CONTENT-GUIDE.md`](DOCS-CONTENT-GUIDE.md) | -| Building, bundling, releasing | [`BUILD.md`](BUILD.md) | +| Installing the workspace, deploying the portal, cutting a release | [`BUILD.md`](BUILD.md) | | Opening a pull request | [`PULL-REQUESTS.md`](PULL-REQUESTS.md) | -| Where things live, and where they are going | [`STRUCTURE.md`](STRUCTURE.md) | +| Which package holds what, and what the monorepo move still owes | [`STRUCTURE.md`](STRUCTURE.md) | | A linked git worktree | [`WORKTREES.md`](WORKTREES.md) | | How this repository is set up for agents | [`AGENT-TOOLING.md`](AGENT-TOOLING.md) | | Step-by-step task workflows | [`.claude/skills/`](../.claude/skills/) | -Outside this directory: [`docs/README.md`](../docs/README.md) for running the documentation portal, [`test/README.md`](../test/README.md) for attaching the private test suite, [`script/README.md`](../script/README.md) for what each build and release script does, and [`CONTRIBUTING.md`](../CONTRIBUTING.md) for external contributors. +Inside the engine — architecture, the parser, the interpreter, the dependency graph, the function catalogue, translations, performance, its test suites and its build: + +| You are working on | Read | +|---|---| +| Anything in `hyperformula/src/` | [`hyperformula/dev-docs/README.md`](../hyperformula/dev-docs/README.md) | + +Outside both: [`docs/README.md`](../docs/README.md) for running the documentation portal, [`hyperformula/test/README.md`](../hyperformula/test/README.md) for attaching the private test suite, and [`CONTRIBUTING.md`](../CONTRIBUTING.md) for external contributors. ## Conventions diff --git a/dev-docs/STRUCTURE.md b/dev-docs/STRUCTURE.md index 072f718fd0..7aedde4b91 100644 --- a/dev-docs/STRUCTURE.md +++ b/dev-docs/STRUCTURE.md @@ -1,146 +1,126 @@ # Repository structure -Where everything lives today, and where it is going. The repository is becoming a monorepo (HF-359); the second half of this page is the target and the steps that get there. +A monorepo. Three top-level directories hold code; the rest is repository-wide. What the move still owes is at the bottom of this page. ``` . -├── src/ # Source code -│ ├── HyperFormula.ts # Main engine class, public API entry point -│ ├── BuildEngineFactory.ts # Engine construction from sheets, data, and config -│ ├── Config.ts, ConfigParams.ts # Engine configuration and its defaults -│ ├── CrudOperations.ts # Create/read/update/delete on sheets and cells -│ ├── Operations.ts, UndoRedo.ts # Operation primitives and the undo/redo stack -│ ├── Evaluator.ts # Recalculation driver -│ ├── Serialization.ts, Exporter.ts # Reading values and formulas back out -│ ├── NamedExpressions.ts # Named expression store -│ ├── parser/ # Formula parsing (Chevrotain parser generator) -│ ├── interpreter/ # Formula evaluation -│ │ ├── plugin/ # Built-in spreadsheet function plugins -│ │ └── functionMetadata/ # Human-readable metadata for every built-in function -│ ├── DependencyGraph/ # Cell dependency tracking and recalculation order -│ ├── dependencyTransformers/ # AST rewrites when rows/columns/sheets move -│ ├── i18n/languages/ # Function-name translations, one file per language -│ ├── format/ # Number and date format parsing -│ ├── helpers/ # Shared utilities -│ ├── Lookup/ # Lookup/search strategies used by lookup functions -│ └── statistics/ # Instrumentation counters -├── test/ # Smoke tests; the full suite is fetched here -│ ├── README.md # How to attach the private suite -│ ├── smoke.spec.ts # Public smoke tests -│ ├── fetch-tests.sh # Clones/updates the private test repository -│ └── hyperformula-tests/ # Private suite (git-ignored, branch-matched) -├── docs/ # Public documentation portal (VuePress) +├── hyperformula/ # ── package: the calculation engine (published) +│ ├── src/ # Source code +│ │ ├── HyperFormula.ts # Main engine class, public API entry point +│ │ ├── parser/ # Formula parsing (Chevrotain parser generator) +│ │ ├── interpreter/ # Formula evaluation +│ │ │ ├── plugin/ # Built-in spreadsheet function plugins +│ │ │ └── functionMetadata/ # Human-readable metadata for every built-in +│ │ ├── DependencyGraph/ # Cell dependency tracking and recalculation order +│ │ ├── dependencyTransformers/ # AST rewrites when rows/columns/sheets move +│ │ ├── i18n/languages/ # Function-name translations, one file per language +│ │ ├── format/ helpers/ Lookup/ statistics/ +│ ├── test/ # Smoke tests; the private suite mounts here +│ │ ├── smoke.spec.ts # Public smoke tests +│ │ ├── fetch-tests.sh # Clones/updates the private test repository +│ │ └── hyperformula-tests/ # Private suite (git-ignored, branch-matched) +│ ├── dev-docs/ # the engine's own internals reference +│ ├── script/ # its build checks: check-file, check-publish-package, if-ne-env +│ ├── .config/ # webpack, karma, and babel config factories +│ ├── tsconfig.json jest.config.js karma.conf.js webpack.config.js +│ ├── babel.config.js ht.config.js jasmine.json .npmignore +│ ├── .typedoc.ts .typedoc.md.ts # API reference generation, output into docs/api +│ ├── package.json .nvmrc README.md LICENSE.txt +│ └── AGENTS.md CLAUDE.md +│ +├── hyperformula-ui/ # ── package: UI components (not imported yet) +│ +├── docs/ # ── the documentation portal (NOT a workspace member) │ ├── guide/ # Markdown guides -│ ├── api/ # API reference (generated from JSDoc; git-ignored) -│ └── .vuepress/ # VuePress configuration, theme, components -├── script/ # Maintenance, docs-generation, and release scripts -├── worker/ # Cloudflare Worker that serves the built docs -├── examples/ # Images and CSV fixtures used by the docs -├── dev-docs/ # Developer reference (this directory) -├── .claude/ # Claude Code settings and skills +│ ├── api/ # API reference (generated; git-ignored) +│ ├── examples/ # Code examples embedded in guides +│ ├── .vuepress/ # VuePress configuration, theme, components, plugins +│ ├── script/ # Generates guide/built-in-functions.md; composes the Worker assets +│ ├── worker/index.js # Cloudflare Worker serving the built portal +│ ├── wrangler.jsonc # Its deploy configuration +│ ├── package.json .nvmrc +│ └── AGENTS.md CLAUDE.md README.md +│ +├── script/ # Repository-wide only: the release procedure and the licence gate +├── dev-docs/ # Repository-wide reference (this directory; start at README.md) +├── .ai/ # One sentence pointing at dev-docs/, for agents that look here +├── .claude/ # Claude Code settings, skills, and hooks ├── .github/ # CI workflows, issue and PR templates -├── AGENTS.md # Always-loaded agent rules and routing map -├── CLAUDE.md # Symlink to AGENTS.md -├── CONTRIBUTING.md # Guide for external contributors -├── README.md # Project overview -└── CHANGELOG.md +├── .eslintrc.js .eslintignore # Linting, run once from the root over everything +├── package.json # Private workspace root: fan-out scripts only +├── package-lock.json .nvmrc .worktreeinclude +├── AGENTS.md CLAUDE.md README.md CONTRIBUTING.md LICENSE.txt +├── CHANGELOG.md # one history for every package +└── CODE_OF_CONDUCT.md ``` -## Build outputs +## Workspaces + +`workspaces` in the root `package.json` lists `hyperformula`. `npm ci` at the root installs it into a shared `node_modules`. `hyperformula-ui/` is a placeholder and is deliberately not listed yet — see [What the move still owes](#what-the-move-still-owes). + +**`docs/` is deliberately outside the workspace.** The portal drags in a large, old dependency tree (VuePress 1.x, `--openssl-legacy-provider`) that must not reach an engine install. It has its own `package.json` and installs separately with `npm run docs:install`. -All git-ignored, all produced by `npm run bundle-all` (see [`BUILD.md`](BUILD.md)): +## Where a command runs -| Directory | Contents | +| Command | Runs in | |---|---| -| `lib/` | `tsc` output, the input to every bundle | -| `es/` | ES modules (`.mjs`) | -| `commonjs/` | CommonJS modules | -| `dist/` | UMD bundles, minified and not, base and `.full` | -| `languages/` | Standalone UMD language packs | -| `typings/` | Public `.d.ts` declarations | +| `npm run lint` | The root, over the whole repository | +| `npm run test:jest`, `bundle-all`, `compile` | Fanned out to `hyperformula` | +| `npm run docs:*` | Orchestrated from the root across both `hyperformula` and `docs` | -Never edit these, and never read them to answer a question about behaviour — read `src/` instead. +Run a package's own scripts from its directory, or with `--workspace=hyperformula`. See [`BUILD.md`](BUILD.md). -## Directories with their own `AGENTS.md` +## Build outputs -Each of these carries rules that load only when an agent works inside it: +All git-ignored, all under `hyperformula/`: `lib/` (`tsc` output, the input to every bundle), `es/`, `commonjs/`, `dist/`, `languages/`, `typings/`. The portal's output is `docs/.vuepress/dist/`, and `docs/api/` plus `docs/guide/built-in-functions.md` are generated. -`src/`, `src/parser/`, `src/interpreter/`, `src/interpreter/plugin/`, `src/interpreter/functionMetadata/`, `src/DependencyGraph/`, `src/i18n/`, `docs/`, `test/`, `script/`. +Never edit any of them. Never read the **build artifacts** — `lib/`, `es/`, `commonjs/`, `dist/`, `languages/`, `typings/`, `docs/.vuepress/dist/` — to answer a question about behaviour; read `hyperformula/src/` instead, and the agent deny list in `.claude/settings.json` enforces that. -## Where it is going +`docs/api/` and `docs/guide/built-in-functions.md` are the exception. They are generated, so editing them is pointless, but they *are* the API reference and the function reference and reading them is often exactly right. -The repository is becoming a monorepo. The tree above is what a checkout looks like now; everything below is the target. +## `dev-docs/` at two levels -### Packages +This directory holds what applies to the whole repository: the definition of done, code style, testing standards, documentation rules, the build and release process, pull requests, worktrees, and the agent setup. -| Package | Directory | Purpose | Published | -|---|---|---|---| -| `hyperformula` | `hyperformula/` | The calculation engine. Everything in `src/` and `test/` today. | yes | -| `hyperformula-ui` | `hyperformula-ui/` | UI components for working with HyperFormula: reference highlighting, inline formula editor, function help. | yes | -| `hyperformula-docs` | `docs/` | The VuePress documentation portal. | no | +[`hyperformula/dev-docs/`](../hyperformula/dev-docs/README.md) holds the engine's internals: architecture, the parser, the interpreter, the dependency graph, the function catalogue, translations, performance, its test suites, and its own build steps. -`docs/` is not a workspace member: the portal drags in a large, old dependency tree (VuePress, `--openssl-legacy-provider`) that must not reach an engine install. It keeps its own `package.json` and is installed separately. +The split is by ownership. A fact lives in exactly one of them, and `hyperformula-ui` gets its own when it lands. -**The packages release together, on one version, from one changelog.** A release cuts every published package at the same version, whether or not each one changed, and `CHANGELOG.md` at the repository root is the single history for all of them. That keeps one number to reason about — the version a user reports a bug against identifies the state of the whole repository — at the cost of publishing a package whose code did not move. Entries name the package they concern where it is not obvious. +## Scripts at three levels -### Target tree +Each script lives with whatever invokes it, and every one has exactly one caller: -``` -hyperformula/ # repository root — private, workspace root -├── AGENTS.md # monorepo-wide rules + routing map -├── CLAUDE.md -> AGENTS.md -├── README.md CONTRIBUTING.md CHANGELOG.md LICENSE.txt -├── package.json # private: true, npm workspaces, fan-out scripts -├── package-lock.json -├── .nvmrc # 22, like every other one here -├── .worktreeinclude -├── .claude/ -│ ├── settings.json # hooks, enabledPlugins, worktree settings -│ └── skills/ # ALL skills, scoped by the `paths` frontmatter field -├── dev-docs/ # monorepo-scope reference -│ -├── hyperformula/ # ── package: the engine -│ ├── AGENTS.md CLAUDE.md -> AGENTS.md -│ ├── package.json .nvmrc CHANGELOG.md -│ ├── dev-docs/ # engine-scope reference -│ ├── src/ -│ │ ├── AGENTS.md CLAUDE.md -> AGENTS.md -│ │ ├── parser/ AGENTS.md -│ │ ├── interpreter/ AGENTS.md -│ │ │ ├── plugin/ AGENTS.md -│ │ │ └── functionMetadata/ AGENTS.md -│ │ ├── DependencyGraph/ AGENTS.md -│ │ ├── i18n/languages/ AGENTS.md -│ │ └── dependencyTransformers/ format/ helpers/ Lookup/ statistics/ -│ └── test/ AGENTS.md README.md -│ └── hyperformula-tests/ # private suite, git-ignored, branch-matched -│ -├── hyperformula-ui/ # ── package: the UI components -│ ├── AGENTS.md CLAUDE.md -> AGENTS.md -│ ├── package.json .nvmrc CHANGELOG.md -│ ├── dev-docs/ -│ └── src/ test/ -│ -├── docs/ # ── documentation portal (NOT a workspace member) -│ ├── AGENTS.md CLAUDE.md -> AGENTS.md README.md -│ ├── package.json .nvmrc -│ ├── wrangler.jsonc # deploy config for the portal -│ ├── worker/index.js # Cloudflare Worker serving the built portal -│ └── guide/ api/ .vuepress/ -│ -├── examples/ # images and CSV fixtures used by the docs -├── script/ AGENTS.md README.md -└── .github/workflows/ # path-filtered per-package jobs -``` +| Directory | Holds | +|---|---| +| `script/` | `release/` and `check-licenses.mjs` — both span the whole repository | +| `hyperformula/script/` | `check-file.js`, `check-publish-package.js`, `if-ne-env.js` — called by the engine's build | +| `docs/script/` | the built-in-functions generator and `prepare-cf-assets.js` — called by the portal | + +## Directories with their own `AGENTS.md` + +`hyperformula/`, `hyperformula/src/`, `hyperformula/src/parser/`, `hyperformula/src/interpreter/`, `hyperformula/src/interpreter/plugin/`, `hyperformula/src/interpreter/functionMetadata/`, `hyperformula/src/DependencyGraph/`, `hyperformula/src/i18n/`, `hyperformula/test/`, `docs/`, and `script/`. + +Each is a pointer of a few lines — what the directory is, and which `dev-docs/` page or local `README.md` holds the detail. They load automatically when an agent reads a file in that subtree, so they stay small on purpose. + +## What the move still owes + +1. **Import `hyperformula-ui`.** The directory is a placeholder; the package is imported from the formula-builder repository in a separate change, preserving its history, and it keeps the scope it publishes under today. Add `hyperformula-ui` to the root `workspaces` array in that same change, not before: npm silently ignores an entry with no `package.json`, so listing it early buys nothing and the lockfile has to be regenerated when the package lands either way. When it arrives it also needs an `.nvmrc` saying `22` and an `AGENTS.md` with a `CLAUDE.md` symlink — but no changelog of its own, and its version moves in step with the engine's. The release script bumps one manifest today; give it the second one in the same change. +2. **Path-filter CI.** Each package's jobs should run only when its own paths change, with full runs on `develop`, `master`, and release branches. Not done here on purpose: a naive `paths:` filter on a workflow that branch protection lists as a required check leaves the check permanently pending, and pull requests become unmergeable. Doing it safely needs the required-checks list, which lives in repository settings rather than in the tree, and the `dorny/paths-filter`-plus-single-gate shape that the Handsontable monorepo uses. + +## What the move decided + +- **npm workspaces, not pnpm.** A package-manager migration is a risk the move did not need to carry at the same time. +- **`docs/` is not a workspace member.** VuePress 1.x and its `--openssl-legacy-provider` dependency tree must never reach an engine install. It installs on its own with `npm run docs:install`, and CI installs it before building the portal. +- **One `dev-docs/`, at the root.** The original plan put an engine-scope copy inside `hyperformula/`. That was dropped: two directories fragment the single source of truth, and every page would have to know which scope it was written from. The engine's subsystem pages live here alongside the repository-wide ones. +- **The packages release together, on one version, from one changelog.** A release cuts every published package at the same version, whether or not each one changed, and `CHANGELOG.md` at the repository root is the single history for all of them. That keeps one number to reason about — the version a user reports a bug against identifies the state of the whole repository — at the cost of publishing a package whose code did not move. Entries name the package they concern where it is not obvious. +- **The published tarball still carries a changelog.** `hyperformula`'s `prepack` copies the root `CHANGELOG.md` into the package and `postpack` removes it again, so there is one file under version control and npm consumers still get one. +- **Every `.nvmrc` says `22`.** +- **Linting stays at the root**, run once over the whole repository, so nothing between packages falls through the gap. +- **The private test suite stays branch-matched.** Only its checkout path moved, to `hyperformula/test/hyperformula-tests/`. Its specs needed no change: they import the engine relatively, and the depth from a spec to the package root is unchanged. + +## Two things the move uncovered + +Both were pre-existing, and both are recorded here because the next person will otherwise rediscover them the hard way. -### Migration steps - -1. **Move `src/` and `test/` into `hyperformula/`.** Mechanical, but it invalidates every path in CI, in `tsconfig.json`, `jest.config.js`, `karma.conf.js`, `.eslintignore`, and the docs generator scripts. The private suite's specs need no change: they import the engine relatively, and the depth from a spec to the package root is unchanged. -2. **Add `workspaces` to the root `package.json`** and make it `private: true`. Move the build scripts down into `hyperformula/package.json`, leaving fan-out scripts at the root. -3. **Give `docs/` its own `package.json`** and take it out of the root dependency tree. -4. **Move `wrangler.jsonc` and `worker/` under `docs/`.** Update `wrangler.jsonc`'s `main`, the `docs:*:cf` scripts, and `script/prepare-cf-assets.js` in the same change. Verify with `npm run docs:preview:cf` — a broken `main` path fails only at deploy time. -5. **Bring in `hyperformula-ui`**, preserving its history. -6. **Keep `CHANGELOG.md` at the repository root**, in the current Keep a Changelog form, as the single history for every package. The published package still needs one in its tarball, so the release copies it in at pack time rather than keeping a second file under version control. -7. **Give every package an `.nvmrc` saying `22`**, and keep their versions in step — a release bumps them together. -8. **Update the private test suite's checkout path**, from `test/hyperformula-tests/` to `hyperformula/test/hyperformula-tests/`, in `fetch-tests.sh`, `.gitignore`, the three workflows that check it out, and `.worktreeinclude`. It stays branch-matched. -9. **Path-filter CI.** Each package's jobs run only when its paths change; full runs on `develop`, `master`, and release branches. +- **The source language packs were never linted.** The old ignore list carried a bare `languages` entry meant for the build output. An unanchored pattern matches a directory of that name at any depth, so it also excluded `src/i18n/languages/`, and the `sort-keys` override targeting those files never ran. Anchoring the build-output entry exposed 881 violations. They are excluded again, deliberately and with a comment, in [`.eslintignore`](../.eslintignore); sorting 19 translation files is a change of its own. +- **`@vuepress/shared-utils` only works inside a full VuePress dependency tree.** It requires `markdown-it-emoji` and a `lru-cache` major it does not declare, and relied on `vuepress` hoisting them. That is why the built-in-functions generator moved into `docs/script/`, where that tree exists, rather than staying beside the engine build scripts. diff --git a/dev-docs/TESTING.md b/dev-docs/TESTING.md index a136dc5af4..1e2004cc19 100644 --- a/dev-docs/TESTING.md +++ b/dev-docs/TESTING.md @@ -1,43 +1,16 @@ -# Testing +# Testing standards -## The two suites +What a change must prove, and how a test case is written. These apply to every package in the repository. -| Suite | Where | Who has it | -|---|---|---| -| Smoke tests | [`test/smoke.spec.ts`](../test/smoke.spec.ts) | Everyone, in this repository | -| Full suite | `test/hyperformula-tests/` | Internal team and anyone granted access | - -The full suite is kept in a separate private repository and is **git-ignored** here. It carries the unit tests, the browser and compatibility runs, and the performance benchmarks. External contributors put their tests in `test/`; the internal team moves them into the private repository through a separate pull request. - -## Fetching the private suite - -```bash -npm run test:setup-private -``` - -**Run it after every branch switch.** The suite is branch-matched, so skipping it runs the previous branch's tests against the current source: the results are meaningless, and they look like ordinary passes and failures. How the fetch works, and the environment variables it honours, are in [`test/README.md`](../test/README.md). In a fresh git worktree the directory is absent entirely — see [`WORKTREES.md`](WORKTREES.md). - -## Running tests - -| Command | Runs | -|---|---| -| `npm run test` | Lint, Jest, and the Karma browser run — the full local gate | -| `npm run test:jest` | Jest only; the fast loop | -| `npm run test:watch` | Jest in watch mode | -| `npm run test:coverage` | Jest with coverage | -| `npm run test:browser` | Karma, against the `dist` build | -| `npm run test:compatibility` | The compatibility suite, which ships with the private repository | -| `npm run test:performance` | The basic and CRUD benchmarks | - -`test:performance`, `test:compatibility`, and the benchmark scripts all resolve into `test/hyperformula-tests/`, so they need the private suite attached. Without it they fail on a missing path rather than on an assertion — read the error before concluding the code is broken. +Each package documents its own suites and commands: the engine's are in [`hyperformula/dev-docs/TESTING.md`](../hyperformula/dev-docs/TESTING.md). ## What a change must cover -- Every change to `src/` needs tests in `test/`. This is part of the [definition of done](DEFINITION-OF-DONE.md), not a suggestion. +- Every change to `hyperformula/src/` needs tests in `hyperformula/test/`. This is part of the [definition of done](DEFINITION-OF-DONE.md), not a suggestion. - **Bug fix**: at least one test that reproduces the bug — it must fail against the unfixed code. Write it first and watch it fail. - **New feature**: a set of tests that describe the feature precisely enough to serve as its specification. - Cover more than the happy path: boundary values, empty and invalid input, error results, and interaction with related features. -- `docs/`, `examples/`, and `script/` are not tested. +- `docs/` and the `script/` directories are not tested. ## How to write a test case @@ -58,22 +31,6 @@ it('returns the divisor sign for arguments with opposite signs', () => { Before requesting a review, ask which further tests would be valuable and add the ones that protect against realistic regressions. -## What each kind of change needs - -| Change | Cover | -|---|---| -| A built-in function | Ordinary arguments; each declared boundary (`minValue`, `maxValue`, `lessThan`, `greaterThan`); too few and too many arguments; wrong argument type, asserting the specific `CellError`; an argument that is itself an error; an empty cell and an empty range; the spilled shape if it returns an array; the call with an omitted optional argument | -| CRUD or a structural change | Add and remove rows and columns around a formula, move a range across a formula that references it, then assert **both** the recalculated value and the formula text afterwards — structural bugs show up in the formula text first | -| A parser change | The parse, the round trip through `Unparser`, at least one non-English language, and malformed input that must produce a parsing error rather than a throw | -| A config option | The default, a valid non-default value, and an invalid value that must be rejected | -| A translation | A formula parsed using the translated name, asserted in that language | - -Skills: `hyperformula-unit-testing`, `test-writing-discipline`. - -## Performance - -HyperFormula is a calculation engine, so production-code performance is a feature. Run `npm run test:performance` for any change that can touch the evaluation or CRUD hot paths. See [`CODE-STYLE.md`](CODE-STYLE.md#performance). - ## A test must prove behaviour A test that passes without proving anything is worse than no test: it occupies the space where the real test would have gone, and it makes the next reader believe the behaviour is covered. diff --git a/dev-docs/WORKTREES.md b/dev-docs/WORKTREES.md index 13151d3d19..ff5e8b9c05 100644 --- a/dev-docs/WORKTREES.md +++ b/dev-docs/WORKTREES.md @@ -6,33 +6,27 @@ Claude Code can run a session — or an isolated subagent — in a `git worktree | Missing | Why it matters | Fix | |---|---|---| -| `node_modules/` | Nothing runs. | `npm ci` in the worktree, or symlink it — see below | -| `test/hyperformula-tests/` | The private suite is git-ignored, so every `npm run test:jest` run covers only the smoke tests | `npm run test:setup-private` | -| `lib/`, `dist/`, `es/`, `commonjs/`, `typings/`, `languages/` | `npm run test:browser` and the bundle checks have nothing to run against | `npm run bundle-all` | +| `node_modules/` | Nothing runs. | `npm ci` in the worktree | +| `hyperformula/test/hyperformula-tests/` | The private suite is git-ignored, so every `npm run test:jest` run covers only the smoke tests | `npm run test:setup-private` | +| `hyperformula/{lib,dist,es,commonjs,typings,languages}/` | `npm run test:browser` and the bundle checks have nothing to run against | `npm run bundle-all` | | `docs/api/`, `docs/guide/built-in-functions.md` | The docs build fails, or serves nothing | `npm run docs:build` | -| `.dev.vars*` | `wrangler` commands fail | Copied automatically — see [`.worktreeinclude`](../.worktreeinclude) | +| `.dev.vars*` | `wrangler` deploy and preview fail | Copy it yourself if you need it. Deploy credentials are deliberately **not** copied into worktrees — see [`.worktreeinclude`](../.worktreeinclude) | ## The branch-matched test suite is the trap -`test/fetch-tests.sh` checks out the branch of the **same name** in the private test repository. Two consequences in a worktree: +`hyperformula/test/fetch-tests.sh` checks out the branch of the **same name** in the private test repository. Two consequences in a worktree: -1. Copying `test/hyperformula-tests/` from the main checkout brings the *other* branch's tests. They will run, and they will report results that have nothing to do with the code in front of you. `.worktreeinclude` deliberately does not copy it. +1. Copying `hyperformula/test/hyperformula-tests/` from the main checkout brings the *other* branch's tests. They will run, and they will report results that have nothing to do with the code in front of you. `.worktreeinclude` copies nothing at all, for this reason among others. 2. Run `npm run test:setup-private` once per worktree, and again after any branch switch inside it. -## Symlinking `node_modules` +## Do not symlink `node_modules` -`worktree.symlinkDirectories` in `.claude/settings.json` points each worktree's `node_modules/` at the main checkout's copy instead of duplicating it: +`worktree.symlinkDirectories` used to point each worktree's `node_modules/` at the main checkout's copy. It was removed when the repository became a workspace, and should not come back. -```json -{ - "worktree": { - "symlinkDirectories": ["node_modules"] - } -} -``` +A root-level symlink was safe while there was exactly one `node_modules/.bin`. In a workspace it is not: npm may place a package-local `node_modules/.bin` under a package, which the symlink does not cover, and scripts then die mid-build with a bare `command not found`. Worse, `npm ci` inside a worktree whose `node_modules` is a symlink installs *through* it, so the main checkout and every other worktree silently get that branch's dependency tree. -This is safe while HyperFormula is a single package: there is exactly one `node_modules/.bin`, and the symlink resolves it. **It stops being safe once the repository becomes a workspace** — package-local `node_modules/.bin` directories are not covered by a root-level symlink, and scripts then die mid-build with a bare `command not found`. Revisit this setting as part of the monorepo migration ([`STRUCTURE.md`](STRUCTURE.md#where-it-is-going)). +Run `npm ci` in the worktree. The portal is a separate install either way — `npm run docs:install`. ## Sparse checkouts -`worktree.sparsePaths` limits what git writes to disk. It buys little today — this repository is small and `src/` is needed by everything. It becomes worth setting once the packages in [`STRUCTURE.md`](STRUCTURE.md#where-it-is-going) exist and a task can be scoped to one of them. +`worktree.sparsePaths` limits what git writes to disk. Worth setting once a task can be scoped to one package — `[".claude", "hyperformula"]` for engine work. It is not set by default, because a task that turns out to span packages then fails in a confusing way. diff --git a/docs/.nvmrc b/docs/.nvmrc new file mode 100644 index 0000000000..2bd5a0a98a --- /dev/null +++ b/docs/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/docs/.vuepress/build.config.js b/docs/.vuepress/build.config.js index 633ea41f32..85a777e5d8 100644 --- a/docs/.vuepress/build.config.js +++ b/docs/.vuepress/build.config.js @@ -2,11 +2,11 @@ * Docs build configuration. * Override any of these via environment variables: * DOCS_BASE — public base path (must start and end with `/`) - * DOCS_DEST — output directory (relative to repo root) + * DOCS_DEST — output directory (relative to the docs/ package root) * DOCS_HOSTNAME — absolute origin used for the sitemap */ module.exports = { base: '/docs/', - dest: 'docs/.vuepress/dist/docs', + dest: '.vuepress/dist/docs', hostname: 'https://hyperformula.handsontable.com', }; diff --git a/docs/.vuepress/components/graph.vue b/docs/.vuepress/components/graph.vue index 9c518f1968..edfccddffd 100644 --- a/docs/.vuepress/components/graph.vue +++ b/docs/.vuepress/components/graph.vue @@ -5,7 +5,7 @@