Commit Graph
149 Commits
Author SHA1 Message Date
aiongg 9705e7a6c1 Merge track r3-t03 2026-10-24 06:39:33 +00:00
aiongg 761353775b Merge PR #4317: M12: round XFA check buttons draw a round background and border 2026-10-24 06:23:48 +00:00
aiongg 8a0319a24f Merge PR #4368: M6: check boxes, radio buttons and push buttons without an appearance are drawn, and comb fields show their cells
crates/forms/src/tests.rs: append conflict, kept both tests.
parity/acrobat-features.toml: form.fill-text-field evidence is the union; notes keep HEAD's text plus the comb-cells sentence.
2026-10-24 06:22:30 +00:00
aiongg 2a1ceebb4f Merge PR #4339: M6: fields that don't scroll stop taking text when full
parity/acrobat-features.toml: form.fill-text-field evidence is the union of both sides; notes keep both sides' sentences (DoNotScroll clause and the Password sentence).
2026-10-24 06:22:02 +00:00
aiongg c0687edbbe Merge PR #3552: M12: a date typed by another of an XFA field's edit patterns is kept in its first 2026-10-24 06:22:01 +00:00
aiongg 339d8f411f Merge PR #4307: M6: Field Properties > Actions: Edit opens an existing script (#4006) 2026-10-24 06:21:45 +00:00
aiongg 4eb7cf4a4b Merge PR #4295: Check boxes and radio buttons draw their mark in the field's text colour 2026-10-24 06:21:42 +00:00
aiongg 01dc49c13c Merge PR #4053: fix: Issue #4018, form_fill warns when a value does not fit its field (#4018)
crates/forms/src/tests.rs: append conflict, kept both tests.
2026-10-24 06:21:41 +00:00
aiongg b6b6e6eca3 Merge PR #3943: M6: values typed into Password fields are written to no file (#3926)
crates/forms/src/tests.rs: append conflict, kept both tests.
2026-10-24 06:21:33 +00:00
aiongg 0cfde22386 Merge PR #3488: fix(forms): write each /DR font as its own object, so Acrobat keeps typed text (#3441)
crates/forms/src/tests.rs: append conflict, kept both tests.
2026-10-24 06:21:22 +00:00
aiongg ba85d2af66 Merge PR #2957: M6: dotted field names make parent fields (#2696) 2026-10-24 06:21:05 +00:00
mtlcl410andClaude Opus 5.5 dd1a894b2d M6: leave the combo box drop-down arrow to #3567
The on-screen drop-down button for combo boxes drawn display-only, the
narrowed text area beside it, and the Print as Image copy without it
(with_printed_field_appearances, printed_display_bytes) are removed.
Open PR #3567 draws the arrow for every combo box in the page view.
Check boxes, radio buttons, push buttons and comb lines are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
2026-10-24 12:01:55 +08:00
mtlcl410andClaude Opus 5.5 5233e12281 M6: check boxes, radio buttons and push buttons without an appearance are drawn, and comb fields show their cells (ISO 32000-2 Table 192, 12.7.5)
A form that leaves its appearances to the viewer had its text and choice
fields drawn for display (#3475), but not its buttons. Now, also for
display only, without changing the file:

- Check boxes and radio buttons with no normal appearance for their
  state show their /MK background and border (Table 192), and when on,
  the /MK /CA caption as a standard-14 ZapfDingbats character
  (12.7.5.2.3), 4 or l by default, in the /DA colour, auto-sized for
  0 Tf.
- Push buttons with none show their /MK background, their /BS border
  (beveled per Table 168) and their centred caption.
- Comb fields with a border colour draw the MaxLen - 1 lines between
  their positions (Table 228), up to 500 cells. This also applies to the
  appearances PdfCraft saves for comb fields.
- A combo box drawn for display shows a drop-down button on screen, with
  its text kept clear of it. Saved appearances and Print as Image leave
  it out (with_printed_field_appearances, printed_display_bytes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
2026-10-24 11:51:00 +08:00
a047c6ba09 chore: resolve SonarCloud maintainability issues (#4356)
* chore: resolve SonarCloud maintainability issues

* fix(packaging): use verify_sha256 helper function

* style: cargo fmt

* fix: restore wildcard imports and fix method-reference type errors

Revert the glob-import expansions (they missed SomIndices and left unused
imports), revert char::is_ascii_digit where the iterator yields char by
value, and pass the content slice to prune_doomed_xobjects as &[Vec<u8>].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep XObject decodability check without binding content in interp

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
2026-10-23 23:28:18 -04:00
Eric Kaden 6d7e887efe M6: a paste into a full field keeps the longest part that fits, across a change of font 2026-10-23 22:04:52 -04:00
Eric Kaden 64bdea4e61 M6: fields that don't scroll stop taking text when full 2026-10-23 19:25:32 -04:00
Eric Kaden ac181479ef M12: a round check button's underline border stays an underline 2026-10-23 17:03:53 -04:00
Eric Kaden 21f3241e6d M12: a round border as wide as its check button fills it as a disc 2026-10-23 16:45:40 -04:00
Eric Kaden 1c4a481ea3 M12: round XFA check buttons draw a round background and border 2026-10-23 16:19:43 -04:00
Marco Arnulfo c5b0e6aa61 M6: Actions tab: replacing a trigger's first action keeps its chain, and the row shows it (#4006)
The actions chained after a trigger's first one (/Next) are kept
whatever replaces it, not only another script, and the row reads
"…, then N more actions". Edit is disabled on every row while a
script is open, so it can't be overwritten.
2026-10-23 21:02:47 +02:00
Marco Arnulfo 61fe4b56a7 M6: Field Properties ▸ Actions: Edit opens an existing script (#4006)
Edit on a Run a JavaScript action loads its script into the editor;
Update replaces it on the same trigger, Discard leaves it. Saving the
tab now keeps actions that did not change as they are, and an edited
script keeps the actions chained after it (/Next).
2026-10-23 20:28:13 +02:00
Marco Arnulfo 6062535e0e Test: the check strokes and the circle fills in the field's colour (#4261) 2026-10-23 20:06:20 +02:00
Marco Arnulfo 8b9d5017fe Check boxes and radio buttons draw their mark in the field's text colour (fixes #4261) 2026-10-23 19:59:13 +02:00
PiohrunandClaude Opus 5.5 ba01244987 M6: calculations read their notation only from a comment, and a regex after a condition as a regex (#3993)
* M6: calculations read their notation only from a comment, and a regex after a condition as a regex

Two findings from CodeRabbit's review of #3550, both reproduced first:

- parse_calculate and calculation_sites found BVCALC and EVCALC anywhere
  in the script, so `var s = "BVCALC a EVCALC";` read as the notation `a`:
  renaming `a` rewrote the string, and the form calculated `a` instead of
  running the script. The lexer now keeps the text of each comment, and
  the notation is read from the first comment that holds BVCALC and the
  EVCALC after it (notation_range), by both the Calculate tab and a
  rename. A line comment counts, and a comment that never ends runs to
  the end of the script, as before.
- A `/` after any `)` divided, so after the condition of `if`, `while`,
  `for` or `with` a regular expression was read as code: renaming `a`
  rewrote `if (flag) /getField("a")/.test(x)`, and in
  `if (flag) /"/.test(x)` the quote started a string that hid the
  script's real getField calls. The lexer now notes which `(` opened
  such a condition (not a method of that name, `o.if(x)`), and a `/`
  after its `)` starts a regular expression.

af::tests::notation_is_read_only_from_a_comment,
af::tests::a_regex_after_a_condition_is_not_read_as_code and
tests::a_script_that_spells_the_markers_in_a_string_runs_as_a_script
fail without the fix. Parity, ROADMAP.md and docs/roadmap.md updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z

* M6: form.props-calculate notes that a regex right after a brace is still read as code

From CodeRabbit's review of this PR. The lexer still divides after `}`,
which ends a block as often as an object literal, so in
`if (x) { } /"/.test(s); event.value = getField("a").value;` the quote
starts a string and the getField is hidden from a rename and from the
calculation order. Checked with a scratch test before writing it down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z

* M6: a line comment ends at any line terminator, so code after `\r` is still read

From CodeRabbit's review of this PR. A `//` comment ran to the next `\n`,
so with `\r` line endings it hid the rest of the script: a rename missed
the getField calls after it (since #3550), and a string after it that
spells BVCALC ... EVCALC read as the notation. A `/` taken for the start
of a regular expression likewise looked past `\r` for its end, and could
swallow the next line's code.

Both now stop at any of JavaScript's line terminators (`\n`, `\r`,
U+2028, U+2029; line_ends_at), and U+2028 and U+2029 between tokens are
skipped rather than read as the start of a word.

af::tests::a_line_comment_ends_at_any_line_terminator fails without each
of the three changes: with `\r`, the string read as notation `a`; with
the comment fixed but not the regex scan, `i++ / 2;\r` swallowed the
getField after it; without skipping U+2028, the getField after it was
not a call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z

* M6: a word, number or regex's flags end at U+2028 and U+2029

From CodeRabbit's review of this PR. The scans for an identifier, a
number and a regular expression's flags took every byte past ASCII as
part of a word, U+2028 and U+2029 included, so in `x\u{2028}getField("a")`
the getField was part of one word and not a call: a rename missed it, and
the calculation order didn't read it.

`word` now takes a position and is false where line_ends_at is true, so
the three scans share one rule. The arm that skips U+2028 and U+2029
between tokens stays: without it, the punctuation arm would step one byte
into the character.

af::tests::a_token_ends_at_a_line_separator fails without the fix, for a
word, a number and a regex's flags alike; it and
a_line_comment_ends_at_any_line_terminator fail without the skipping arm.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z

* M6: the `while` that ends a `do` loop counts as a condition, said and tested

A `/` after the `)` of a do-while starts the next statement, so it starts a
regular expression there too. The comment on BEFORE_CONDITION says so, and
a_regex_after_a_condition_is_not_read_as_code covers it (from review).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:42:48 -04:00
Dylan M. Taylor dad86bf9f5 fix(redact,sign,cos): close high-severity findings from the agent scan
Redact: refuse Redact marks written in place; remove replies and pop-ups listed on
other pages; delete form fields by object, not name; resolve Do by its last operand
like the renderer; sanitize drops PCAdded and flattens widgets before hidden-content
cleanup. Sign: freed signed objects and edits to an already-signed widget are
modifications. Cos: only reading trusts /Type /XRef to skip encryption.
2026-10-23 10:11:10 -04:00
mtlcl410andClaude Opus 5.5 9efc1727a1 M6: default names skip empty parents; a full name under the same parents is a plain rename (#2696)
Review follow-up on #2957:
- free_name picked a default name (Text1) that a parent field left empty
  by a move still held at the top of /Fields, and attach then refused it
  for every new field of that kind. The own names (/T) of the top-level
  /Fields nodes are now skipped too.
- the editor sends nested fields' full names, so renaming address.city to
  address.town took the move path: it copied the inherited keys onto the
  field and moved it to the end of the parent's /Kids. A full name whose
  parents are the field's current parents now renames only its own /T.

Field tree per ISO 32000-2 12.7.4 (Arlington Field*.tsv: /Parent, /Kids,
/T; FT, Ff, V, DV, DA, MaxLen inheritable).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
2026-10-23 20:48:22 +08:00
mtlcl410andClaude Opus 5.5 eed2afa165 fix(forms): an inherited signature value and a direct /AcroForm are handled when filling (#3441)
Review follow-ups on filling a field converting inline /DR fonts:
- A signature field whose /V sits on its parent counted as unsigned, so
  filling another field rewrote /DR in a signed form. The check now
  follows the /Parent chain (bounded at 64, like the field walk).
- A catalog holding /AcroForm as a direct dictionary was skipped; its
  inline fonts are now converted and written back into the catalog.

Both tests failed before the change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
2026-10-23 18:00:42 +08:00
mtlcl410andClaude Opus 5.5 9ebc237ecf M6: skip forms without a Password field quickly, and find ancestors once (#3926)
Review on #3943, point 1. has_password_values ran a full fields() walk
on every save and recovery snapshot, and clear_password_values walked
every field's ancestors again for each ancestor of each target.

has_password_flag now answers first, reading only /Ff, /Kids and /Parent:
the field tree from /Fields (seen set, 32 levels) and, as fields() does,
the widgets on the pages with the fields above them. A form without such
a field returns before any Field is built. clear_password_values finds
each field's ancestors once.

The test puts a page-only widget, a flag inherited from an unlisted
parent and a nested field whose widget is on no page each alone in a
document. Mutants that drop the page scan, the parent chain, the tree
walk or the descent into kids each fail it, as does treating every
ancestor as exclusive. Dropping the seen set or the early-out changes
only speed (equivalent).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:38:29 +08:00
mtlcl410andClaude Opus 5.5 adc8307c0b M6: a saved Password field returns to its default value and is redrawn from it (#3926)
Review on #3943. A black-box test of Acrobat (synthetic form, Save As and
Ctrl+S, every decoded object searched) showed how it saves a Password
field: the typed value is nowhere in the file; a field with /DV is saved
holding /DV, one without is left empty; the asterisks are drawn from the
saved value (5 for a 5-letter default, not 13 for the typed password);
/DV and /AA are kept.

clear_password_values now does the same: it restores the (inherited)
/DV as the value, or leaves the field empty, and redraws the widgets, so
the appearance no longer shows how long the typed password was. A field
already at its default no longer counts as holding a password, so such
a form needs no cleaned copy. The parity note for form.fill-text says so
and lists the two limits Acrobat shares (a script copying the password
into an ordinary field, and an earlier revision kept by an incremental
save).

Mutants, each failing a test: no /DV restore, no redraw, the default
ignored in password_fields, emptiness ignored (an empty string in an
array). Dropping the empty-default filter was equivalent, so the filter
is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:38:29 +08:00
mtlcl410andClaude Opus 5.5 509b5d5cf3 M6: clear Password values inherited from an ancestor or held by a widget (#3926)
Review follow-up: the helpers checked only the field dictionary's own /V. A value
inherited through /Parent or carried by a separate widget was missed, so the
document could be written as it was. Detection now uses the resolved value and
the widgets; clearing removes /V from the field, its widgets and every ancestor
only password fields inherit from, and gives the field an empty value of its own
where a shared ancestor value has to stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:38:28 +08:00
mtlcl410andClaude Opus 5.5 0e883c44a0 M6: values typed into Password fields are written to no file (#3926)
A text field with the Password flag kept its typed value in /V, so Save, a full
rewrite, Reduce File Size and the recovery snapshots all wrote it in plain text.
ISO 32000-2 §12.7.5.3, Table 231 (note to the Password flag): PDF processors
should never store the value in the PDF file. Each of those paths now writes a
copy without the values; the open document keeps them in memory, and the masked
appearance is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 14:38:16 +08:00
mtlcl410andClaude Opus 5.5 fa146bbecd fix(forms): filling a field also stores inline /DR fonts as objects (#3441)
Review follow-up (CodeRabbit on #3488): a form an earlier PdfCraft made
should work in Acrobat once it has been filled here too, not only once a
field is added or edited. set_value now runs the same conversion (shared
with ensure_form through inline_fonts_as_objects and store_form_fonts);
it adds nothing, only changes where the existing fonts are stored.

Not in a form a signature covers (a signature field with /V): there the
file stays as the signer saw it apart from the value. PdfCraft's own
change check counts a /DR change after a form-fill certification as form
fill, but another validator may not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:38:36 +08:00
mtlcl410andClaude Opus 5.5 941077715a fix(forms): write each /DR font as its own object, so Acrobat keeps typed text (#3441)
A text field PdfCraft added showed its typed text in Acrobat only while
the field had focus: as soon as the user clicked elsewhere the text
vanished (it was still stored, and printed). PDF-XChange Editor and
PDFgear were fine, and the same field made by MuPDF was fine in Acrobat.

Bisected in Acrobat with one-change variants of the same file: only the
variants whose /DR fonts are objects (or that have no /DR) keep the text.
The appearance stream's own fonts, /P, the appearance layout and /DA 0
made no difference. PdfCraft wrote Helv, TiRo and Cour inline in /DR.

ensure_form now adds them as objects, and turns fonts an earlier save
wrote inline into objects too (same dictionaries). An indirect /DR or
/Font stays the object it is. The XFA-to-AcroForm writer does the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:38:36 +08:00
Eric Kaden cf2f4ec766 Merge remote-tracking branch 'upstream/main' into fix/xfa-edit-picture-alternatives
# Conflicts:
#	crates/engine/src/tests.rs
2026-10-22 22:55:12 -04:00
mtlcl410andClaude Opus 5.5 1f4b49ebd7 M6: a typed field name loses its ends but keeps spaces beside periods (#2696)
Review on #2957, point 1. Both entry points already trim the whole name
before splitting it, and a space beside a period stays in its part,
which matches black-box observation of Acrobat and ISO 32000-2 12.7.4.2
(only a period is barred from a partial name). The test pins that down;
three mutants (no trim in add_field, no trim in set_props, untrimmed
empty-part check) each fail it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:45:14 +08:00
mtlcl410andClaude Opus 5.5 5e4ca789b8 M6: note why a dotted move that fails half way never reaches the document; the parity note says the table tool still refuses periods (#2696)
Review on #2957 (benandez, points 2 and 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:45:14 +08:00
mtlcl410andClaude Opus 5.5 601d7afe60 M6: a dotted rename's calculations follow the field (#2696)
main now renames a field in the calculations that read it. After rebasing,
that runs for a dotted name too, which moves the field under new parents: the
sum and the script that read `a` read `Group.a`, and still compute after a
save and reopen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:45:14 +08:00
mtlcl410andClaude Opus 5.5 5f8e573e04 M6: a refused move doesn't copy inherited values onto the field first (#2696)
Review on #2957: own_inherited wrote what the field inherits onto it before
check_place could refuse the move, so a refused move still changed the field.
What it inherits is now read first, counted as its own by the check, and
written only once the move is allowed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:45:14 +08:00
mtlcl410andClaude Opus 5.5 a77c47bea3 M6: a move the destination refuses leaves the field in place (#2696)
Review on #2957: set_props took the field out of the tree before attach
checked the destination, so a refused move (a parent holding /MaxLen, say)
left it out of /Fields for a direct caller of the forms API. The read-only
check is now its own function, run before the field is detached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:45:14 +08:00
mtlcl410andClaude Opus 5.5 224a64e3d0 M6: a dotted name checks the place it goes: an empty parent's name, a parent's value (#2696)
Review on #2957. The field list holds only terminal fields, so a parent
left empty by a move wasn't seen and a second field of its name could be
made beside it, by adding or by renaming. And a new field joining a parent
that holds /V, /DV or /MaxLen inherited them, with no neutral value of its
own to turn them off: such a parent now takes no field that doesn't hold
its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:45:14 +08:00
mtlcl410andClaude Opus 5.5 f41abf921f M6: dotted field names make parent fields (#2696)
A field's own (partial) name can't contain a period (ISO 32000-2
§12.7.4.2), so Prepare a form refused MASTER.first. Acrobat reads such a
name as a full name: the field `first` under a parent field `MASTER`.

- add_field: a dotted name makes or joins the parent fields it names.
- set_props: a dotted name is the full name and moves the field there,
  copying what it inherited onto it first; a plain name still renames
  the field within its parent. Either way the new full name may not be
  taken by, or hold or sit inside, another field.
- A field joining an existing parent doesn't take its Ff or Q.
- Field Properties and the Prepare a form name tags show the full name
  instead of the last part.
- Names are capped at 32 parts; empty parts are refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:45:13 +08:00
Eric Kaden dd6d7feadb M12: a date is tried in the field's first pattern again only when its format refused it 2026-10-22 22:18:11 -04:00
Dylan M. Taylor 07a9109fed Merge remote-tracking branch 'origin/main' into HEAD 2026-10-22 22:07:20 -04:00
Dylan M. Taylor 93b25f67a6 Merge main into fix/coderabbit-c5-forms-misc
Keep the Ops iterator from this branch and carry main's long-number skip
(MAX_RETRIED_TOKEN) into its operand error path.
2026-10-22 22:07:19 -04:00
aiongg 5473d354a9 fix(forms): skip a field of an unread kind before it claims shared widgets 2026-10-22 23:36:31 +00:00
aiongg 6384381127 fix(forms): read each widget once when enumerating fields
A /Kids array listing one widget many times, or shared by several fields,
built a Widget for every listing, so a small file expanded into fields times
listings widgets. A widget now belongs to the first field that lists it, and a
page widget already owned is not adopted again.
2026-10-22 21:20:10 +00:00
aiongg 923e499597 Merge upstream/main into fix/coderabbit-c5-forms-misc
Conflict in crates/content/src/lib.rs: main ends inline images where the
renderer's tokenizer ends them and counts the images whose end is a guess
(Parsed::ambiguous_images); this branch reads operators through the
streaming Ops iterator that parse collects. The iterator's BI branch takes
main's inline_image result and keeps the ambiguous-image count, which parse
copies into Parsed and Ops::ambiguous_images reports.
2026-10-22 20:51:14 +00:00
aiongg a72845ad80 Merge upstream/main into fix/coderabbit-b-shared
Redaction reads page content and form XObjects strictly (upstream) and
within one page's decode budget (this branch): both now go through
DecodeBudget::decode_strict.
2026-10-22 19:44:33 +00:00
aiongg f42fb0ad49 fix(forms): a deleted field leaves the calculation order
Deleting a field took it off its pages and out of the field tree but left
its entry in the form's /CO array. That reference kept the field
dictionary, its value and its widgets reachable, so a full save after
applying redactions over the field still wrote the redacted value.
2026-10-22 15:17:44 -04:00
aiongg 2d5c5054e2 fix(forms): merge a calculation's read names with a set
calculation_order merged the names a Calculate script reads into the
field's other reads by searching the list so far for each name, so a
script naming many fields took quadratic time before the link limits
applied. The merge looks names up in a set.
2026-10-22 18:02:33 +00:00