Skip to content

fix: "has an answer" / "is not selected" rules reopen in the inline builder (#15) - #23

Open
PrjShrestha wants to merge 2 commits into
masterfrom
9b_reopen_truthy_answered
Open

PrjShrestha wants to merge 2 commits into
masterfrom
9b_reopen_truthy_answered

Conversation

@PrjShrestha

@PrjShrestha PrjShrestha commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #15 (T9b). Parent epic #9. Plan: docs/plans/9_complex_logic_calculation_relevant_constraint/README.md.

What: "has an answer" and "is not selected" rules written by the inline builder reopen in the inline builder as picked clauses; a hand-typed ${f} != '' opens as a clause and saves back unchanged.

Why this was necessary: the builder wrote ${f} and not(${f}) correctly but could not read its own output. An author who came back the next day found the rule frozen as hand-written text with every control greyed out, which is the fastest way to lose trust in the tool.

What it solves: what you build today is editable tomorrow, and the no-normalise promise holds for the simplest rules too.

Demo video and the "what an author can do" table: #15 (comment)


What

The inline builder could write "has an answer" (${f}) and "is not selected" (not(${f})) but could not reopen them. Two causes met at ruleToClause in conditionReducer.ts:

  • Case (a) clauseToRule emitted both as kind: 'raw', so a reopen parsed them as raw and fell to rawFallback with every control disabled. They now have a real parser kind, truthy { field, negated }, serialized to exactly the two spellings the builder always wrote. ${ f } and not( ${f} ) stay raw through the self-check.
  • Case (b) ${f} != '' / ${f} = '' parse cleanly as answered and were dropped at the same line. They now map to the ref / not clause and carry the author's spelling as Clause.source, re-emitted while it still describes the clause. An unedited reopen writes the same bytes. ${f} != '' is never rewritten to ${f}.

Three consumers that switch exhaustively on Rule['kind'] gained a branch for truthy (modal row in RelevantRuleBuilder, prose in DecisionsView and CalculationBuilder). No other UI change.

Tests

Pinned separately, as the ticket asks: a bare ${f} fixture exercises only the truthy path, ${f} != '' only the answered path; each test asserts the parser kind so the two cannot be confused. All seven new tests fail on bff69cb and pass here. Every round-trip assertion calls the serializer.

Validate

Check Result
pnpm --filter @cht-ui/shared build && test 829 tests, 792 pass, 0 fail, 37 todo (unchanged)
pnpm typecheck clean, no consumer of parseRelevant changed shape
pnpm lint fails on master already (164 problems in untouched files); the seven files this PR touches add none
node scripts/smoke-parser.mjs geriatric-workflow/forms/app/pregnancy.xlsx Round-trip stable: YES
node scripts/corpus-sweep.mjs output byte-identical to master (the extraSheets / contact-summary drift it reports is pre-existing)
Cell-level serializeAnyParsed(parseRelevantGrouped(x)) === x over 4034 relevant / constraint / choice_filter cells in seven configs same 37 pre-existing drifts as master, none new; 21 relevant cells now open as clauses

Found on the way, not fixed here

The 37 pre-existing drifts are all-raw chains with a double space around and (gandaki / lumbini FCHV training constraints, one nssd relevant). parseRelevant skips the self-check when every split part is raw and rejoins with single spaces. The inline builder keeps the original text on its raw path, so the bytes are safe through the UI today, but the self-check is not authoritative on that path. Will be handled in 9a (#14), where the self-check rule lives.

PrjShrestha and others added 2 commits October 1, 2026 17:19
…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>

@witash witash left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

code looks good
not part of this issue exactly, but I seems to be doing parsing with regex. I wonder if that's the best way to do this or if it would be more maintainable long term to do the parsing differently.

Not for this PR though.

wasn't able to reproduce the issue locally on master or the fix on this branch with the default config because of other issues, I don't think that needs to block this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

T9b: Rules the builder writes can be reopened (ruleToClause defects)

2 participants