feat(parser): . as a subject, ../field as an alias of ${field}, function forms on . (#14) - #24
Open
PrjShrestha wants to merge 3 commits into
Open
PrjShrestha wants to merge 3 commits into
PrjShrestha wants to merge 3 commits into
Conversation
…lected" reopen as clauses (#15) Two defects met at ruleToClause in conditionReducer.ts: (a) clauseToRule emitted `${f}` and `not(${f})` as kind 'raw', so on reopen the parser handed back a raw rule, hydrateColumn fell to rawFallback and every control was disabled. They now have a real parser kind, `truthy` { field, negated }, that serializes to exactly the two spellings the builder has always written. Spacing-divergent forms (`${ f }`, `not( ${f} )`) stay raw via the self-check. (b) `${f} != ''` / `${f} = ''` parsed cleanly as kind 'answered' and were dropped at the same spot. They now map to the ref / not clause and carry the author's spelling as Clause.source, which clauseToRule re-emits while it still describes the clause. An unedited reopen writes the same bytes; `${f} != ''` is never rewritten to `${f}`. Pinned separately: a bare `${f}` fixture exercises only the truthy path, `${f} != ''` only the answered path. All four reducer tests and the three positive parser round-trip tests fail on bff69cb and pass here. Three UI consumers that switch exhaustively on Rule['kind'] gained a branch for the new kind (modal row, decisions prose, calc prose). Verified: shared tests 792 pass / 0 fail; typecheck clean; corpus sweep output byte-identical to master; a cell-level serialize(parse(x)) check over 4034 relevant/constraint/choice_filter cells in seven configs shows the same 37 pre-existing drifts as master and none new; 21 relevant cells now open as clauses that were raw before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…uilder (#15) Two Playwright specs on a throwaway copy of the mini-config fixture: - write `${lmp_date}` and `not(${danger_signs})` through the strip, save, assert the bytes via the API, reload, and reopen both rows as clauses (no "hand-written" status, undo-last-clause present, insert enabled); - the fixture's existing `${lmp_date} != ''` relevant opens as a clause, re-inserts with zero edits as the same bytes, and survives a save. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… function forms on `.` (#14) Real validation rules are written against the answer itself and real relevants use relative paths; the shared parser only knew `${field}` subjects, so 774 of the 777 constraints in seven real configs opened as plain text. This slice is shared-only and additive. New in shared/src/xlsform/operand.ts: a closed operand grammar (`.`, `${f}` / `../f` with the spelling kept, literals, whitelisted ODK + CHT calls, `+ - * div mod`, parens). On top of it, four additive rule kinds: - expr-comparison `. >= 0`, `string-length(.) <= 100`, `int(format-date(today(),'%Y')) + 57 >= int(.)`, `. > max(coalesce(${a}, 0), ...)`, `. <= today() - 30` - predicate `regex(., '...')`, `selected(., 'none')`, `not(...)` of those - not-group `not(selected(., 'none') and count-selected(.) > 1)` - always-true `true`, `true()`, `1` (text carried, never rewritten) Each carries the clause verbatim as `source`; the serializer re-emits it while it still parses to the same rule, so `.<=100` and `. <= 100` both open AND save back byte-identical, and only a rule the author changed gets canonical spacing. `../field` on the existing comparison / selected / answered / truthy kinds is a `refSpelling: 'relative'` flag, re-emitted exactly as written in either direction. The reducer attaches `source` to any hydrated clause whose canonical emission would differ, so a `../field` rule opens in the inline strip and saves back unchanged. Also: the self-check now runs on all-raw chains too. Splitting on the combinator rejoined `a and b` with one space (six distinct real FCHV / LMP constraints); such a chain is now one raw rule, byte-identical. Consumers that switch exhaustively on Rule['kind'] show the new kinds as the text the author wrote (modal row, decisions / calc prose); a change in the modal turns the rule into a raw fragment, as before. Measured on the seven analysis configs (777 constraint cells): constraint 682 / 777 open fully structured (3 before; 51 are placeholders) relevant 1839 / 3050 drift 0 / 3919 cells (serialize(parse(x)) === x on every cell) `../` share: 410 relative refs vs 4433 ${} refs; 329 cells use `../` Tests: operand grammar; serializer-exercising round trips from non-canonical fixtures for every form in the ticket; `../` hostile fixtures live for both halves (byte identity and opens-as-clause); the all-raw self-check; an "additive" guard that every pre-T9a fixture still yields its old kind; Playwright: seeded `.` / `../` cells survive open-and-save byte-identical, the modal opens a `.` constraint as rows, a `../` relevant opens in the strip. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This was referenced Oct 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #14 (T9a). Parent epic #9. Plan:
docs/plans/9_complex_logic_calculation_relevant_constraint/README.md.Stacked on #23 (9b): both touch
parseSinglePart/ruleToString, so this branch is cut from the 9b tip. The first two commits are #23; the diff collapses to the last commit once #23 merges.What
Shared-only, additive. A closed operand grammar (
shared/src/xlsform/operand.ts) and four new rule kinds on top of it:expr-comparison. >= 0,.<=100,string-length(.) <= 100,int(format-date(today(),'%Y')) + 57 >= int(.),. > max(coalesce(${a}, 0), …),. <= today() - 30,. >= date('…'),difference-in-months(., today()) > 3,decimal-date-time(.) …predicateregex(., '…'),selected(., 'none'),contains(…), each withnot(…)not-groupnot(selected(., 'none') and count-selected(.) > 1)always-truetrue,true(),1(text carried, never rewritten)../fieldon the existing comparison / selected / answered / truthy kinds is arefSpelling: 'relative'flag, re-emitted exactly as written; a single segment only, so../inputs/contact/xkeeps its own kind and other multi-segment paths stay raw.No normalising. Each new rule carries the clause verbatim as
source; the serializer re-emits it while it still parses to the same rule (same pattern asrefSourceon contact-summary rules). A consumer that edits a rule gets canonical spacing for that rule only. The reducer attachessourceto any hydrated clause whose canonical emission would differ, soselected(../lmp_approx, 'approx_weeks')opens in the inline strip and saves back unchanged.Self-check on all-raw chains.
a and b(two spaces; six distinct real FCHV / LMP constraints) was rejoined with one space. Such a chain is now one raw rule, byte-identical. This is the pre-existing drift noted in #23.Three consumers that switch exhaustively on
Rule['kind']show the new kinds as the text the author wrote (modal row, decisions / calc prose). No other UI; the.kinds get presets in 9e and clauses in 9f.Measured on the seven analysis configs
constrainttrueplaceholders; 631 / 726 real rules)relevantchoice_filterserializeAnyParsed(parseRelevantGrouped(x)) === x)../vs${}share across the three columns: 410 relative references vs 4433${}(8.5%); 329 of 3919 cells carry a../reference, 9 mix both; none of them is../inputs/…. Recorded in the plan README.What still stays raw (23 distinct constraints): a newline before
and(eight rules), double spaces (six), curly quotes aroundnone(one), andand/ormixed insidenot(…)(three). All byte-identical.Tests
operand.test.ts: tree shape, canonical spacing, what is outside the grammar.relevantParser.selfSubject.roundtrip.test.ts: every round-trip calls the serializer and starts from a non-canonical fixture (.<=100,(.)>=1 and (.)<=7,.>= 36.5 and .<= 40,regex(.,'…')). The../fieldhostile fixtures are live for both halves (byte identity and opens-as-clause), since they pass. An "additive" guard asserts every pre-T9a fixture still yields its old kind and that${a}='x'stays raw.../rules hydrate as clauses and re-emit their spelling; a new clause beside them is canonical;.rules are structured in the parser but fall to the strip's raw text (9e owns them).today() - ${field} > 5*42is no longer raw but an operand comparison, still not adate_offset, still byte-identical.parser-dot-subject.spec.ts: seeds.constraints and../relevants through the API, opens the form, asserts the modal shows a.constraint as rows with no raw warning and the strip opens a../relevant as a clause, saves, and asserts every seeded cell byte-identical on disk.Validate
pnpm --filter @cht-ui/shared build && testpnpm typecheckparseRelevantchanged shapepnpm lintmasteralready (164 problems in untouched files); every file this PR touches lints cleannode scripts/smoke-parser.mjs …/pregnancy.xlsx(geriatric-workflow, lumbini)node scripts/corpus-sweep.mjsmaster🤖 Generated with Claude Code