Skip to content

chore: repo-wide coding-standards cleanup pass - #18

Merged
nnkogift merged 11 commits into
mainfrom
refactor/coding-standards-cleanup
Aug 3, 2026
Merged

nnkogift merged 11 commits into
mainfrom
refactor/coding-standards-cleanup

Conversation

@nnkogift

@nnkogift nnkogift commented Aug 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

Repo-wide cleanup pass following a review against the coding-standards skill and fallow's whole-project static analysis (dead code, duplication, complexity, dependency hygiene). Split into four commits, one per phase:

  • chore: remove dead barrel, dedupe form types, and fix dependency hygiene — removes an orphaned barrel in utils/hooks, consolidates a DefaultFormValue type duplicated across useEventForm/useTrackerForm, extracts repeated magic numbers into named constants, and fixes package.json dependency declarations (missing @dhis2/d2-i18n/@testing-library/react in the playground, unused dayjs/mockdate in storybook, misdeclared @dhis2/api-types/@dhis2/data-engine in hooks).
  • refactor: split mui widgets into one-component-per-file — splits mui's 14-component Fields.tsx into per-domain files, matching the layout already used by dhis2-ui and mantine; resolves a CRITICAL complexity hotspot on the combined file.
  • refactor: dedupe rule-engine interop and simplify dispatch logic — extracts three functions duplicated identically between context.ts and enrollmentContext.ts (fallow's top-ranked duplication target) into a shared module, and replaces two switch-based dispatchers (valueTypeToZod, applyEffect) with declarative lookup tables, resolving fallow's two complexity hotspots.
  • chore: declare playground entry point for fallow and drop dead widget file — adds .fallowrc.json with an explicit entry point so fallow recognizes the DHIS2 App Platform's d2.config.js entry convention (previously cascaded into 55 files misreported as dead across the playground); also removes one genuinely orphaned widget file uncovered while verifying the corrected dead-file list.

Everything not touched here (playground's flat hooks//utils//types/ folders, cross-adapter D2Field duplication, further decomposition of context.ts) was deliberately left as a follow-up — see the review discussion for why.

Test plan

  • pnpm typecheck — all packages pass
  • pnpm lint — clean on all touched files (pre-existing unrelated warnings elsewhere untouched)
  • pnpm test — 162/162 unit tests, 80/80 Storybook browser tests pass
  • pnpm build — all packages + Storybook build successfully
  • Re-ran fallow: maintainability 86.9 → 90.9, both named complexity hotspots resolved, duplication clone groups down, dead-file false-positive cascade fixed
  • Local fallow-gate pre-commit hook passes (verdict: pass, 0 newly-introduced issues) on every commit

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added MUI form fields for text, numeric, percentage, choice, date, time, age, and unsupported widgets.
    • Added Fallow code-intelligence and Fallow Review skills, including guided reviews, CLI references, and workflow documentation.
    • Added automated repository health badge publishing.
  • Bug Fixes
    • Improved rule-effect handling, including option values, warnings, assignments, and legacy compatibility.
    • Standardized interaction wait times for more reliable validation behavior.
  • Refactor
    • Centralized form value types and rule interoperability utilities.
    • Organized field widgets into focused modules.

nnkogift and others added 6 commits August 3, 2026 08:51
Removes the unused utils/hooks fields barrel, consolidates the
DefaultFormValue type duplicated across useEventForm/useTrackerForm,
extracts repeated magic numbers (devtools trace maxEntries, storybook
interaction timeouts) into named constants, and corrects package.json
dependency declarations flagged by fallow (missing @dhis2/d2-i18n and
@testing-library/react in the playground app, unused dayjs/mockdate in
storybook, misdeclared @dhis2/api-types/@dhis2/data-engine in hooks).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
components/mui/src/fields/widgets/Fields.tsx bundled 14 field
components in a single file, unlike the dhis2-ui and mantine adapters
which already split by domain. Splits it into TextField, NumericFields,
ChoiceFields, and UnsupportedField to match that layout, and resolves
the CRITICAL cyclomatic-complexity hotspot fallow flagged on the
combined file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extracts toStringValue, toRuleAction, and normalizeEffect — identical
between context.ts and enrollmentContext.ts (fallow's top-ranked
duplication target: 4 clone groups, 128 duplicated lines) — into a
shared ruleInterop module.

Also replaces two switch-based dispatch functions with declarative
lookup tables to resolve fallow's complexity hotspots: buildSchema's
valueTypeToZod (28 cyclomatic -> grouped value-type-to-schema map) and
evaluate's applyEffect (34 cyclomatic -> a map of small per-action-type
appliers). Behavior is unchanged; existing fixture-based unit tests
cover both.

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

fallow's dead-code detector doesn't recognize the DHIS2 App Platform's
d2.config.js entryPoints convention, so it flagged AppWrapper.tsx (the
app's real entry, declared only in d2.config.js) as unused — cascading
into 55 files (96% of the whole-repo dead-code count) being misreported
as dead across the playground. Adding an explicit entry glob to
.fallowrc.json fixes the false-positive cascade (55 -> 13 remaining,
all legitimate config/test files not reached via imports).

While verifying the corrected dead-file list, found one file the
cascade had been masking: components/mantine/src/fields/widgets/DateFields.tsx
duplicates D2DateField/D2AgeField, but mantine's widgets/index.ts
already imports those from TextField.tsx — nothing references this
file. Removed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread .github/workflows/ci.yml Fixed
@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@nnkogift, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 43 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 90284146-a9d9-4311-ad89-e4fdf08876ba

📥 Commits

Reviewing files that changed from the base of the PR and between 98be014 and 95520fd.

📒 Files selected for processing (4)
  • .claude/hooks/fallow-gate.sh
  • .claude/settings.json
  • .github/workflows/ci.yml
  • AGENTS.md
📝 Walkthrough

Walkthrough

The PR adds Fallow skills, review hooks, references, configuration, and CI badge publication. It reorganizes MUI field widgets, centralizes form and rule conversions, updates metadata schemas, and applies supporting dependency, timeout, and devtools constant changes.

Changes

Fallow review and analysis tooling

Layer / File(s) Summary
Review contract and trade-off guidance
.agents/skills/fallow-review/SKILL.md, .agents/skills/fallow-review/agents/openai.yaml, .agents/skills/fallow-review/references/tradeoff-elicitation.md
Defines graph-grounded review judgments, snapshot validation, human feedback handling, and structured trade-off output.
Fallow analysis reference
.agents/skills/fallow/**
Documents Fallow commands, analysis modes, output formats, configuration, MCP tools, Node bindings, operational caveats, and workflow patterns.
Hook and repository integration
.agents/skills/fallow-review/hooks/*, .claude/skills/*, .fallowrc.json, .github/workflows/ci.yml, skills-lock.json, README.md, .gitignore, .prettierignore
Adds Claude session and feedback hooks, repository configuration, skill links, lock metadata, ignored paths, CI badge publication, and the README badge.

Form and rule utility changes

Layer / File(s) Summary
MUI widget modules and exports
components/mui/src/fields/widgets/*
Splits field widgets into specialized modules and adds text, numeric, choice, age, and unsupported-field implementations.
Schema and form type contracts
packages/metadata/src/buildSchema.ts, utils/hooks/src/formValue.ts, utils/hooks/src/index.ts, utils/hooks/src/useEventForm.ts, utils/hooks/src/useTrackerForm.ts
Uses lookup-based value-type schemas and exposes one shared DefaultFormValue type.
Rule conversion and effect application
utils/rules/src/*
Adds shared Kotlin/JS interop helpers, updates rule contexts to use them, and dispatches effects through registered appliers.

Supporting package and test maintenance

Layer / File(s) Summary
Shared interaction and trace defaults
apps/storybook/interactions/*, utils/devtools/src/*
Centralizes Storybook rule-effect wait durations and the default trace-store entry limit.
Package dependency updates
apps/playground/package.json, apps/storybook/package.json, utils/hooks/package.json
Adds playground dependencies and removes unused Storybook and hooks dependencies.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ClaudeCode
  participant SessionInitHook
  participant FeedFile
  participant FeedbackHook
  ClaudeCode->>SessionInitHook: start or resume session
  SessionInitHook->>FeedFile: create or register feed.jsonl
  ClaudeCode->>FeedbackHook: report feed.jsonl change
  FeedbackHook->>FeedFile: read new JSONL notes
  FeedbackHook-->>ClaudeCode: emit reviewer feedback context
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the repository-wide coding-standards and static-analysis cleanup described in the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/coding-standards-cleanup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Security review complete: 1 net-new, medium-severity finding after triage and deduplication.

Open in Web View Automation 

Sent by Cursor Security Agent: Security Reviewer

Comment thread .github/workflows/ci.yml
run: fallow health --format badge > badge.svg

- name: Publish badge
uses: peaceiris/actions-gh-pages@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Agentic Security Review
Severity: MEDIUM
Using peaceiris/actions-gh-pages@v4 pins to a mutable tag rather than an immutable commit SHA. If that upstream tag is retagged or compromised, the workflow could run attacker-controlled code in a job that has write-capable repository credentials.

Impact: A compromised action release could lead to unauthorized changes in repository-published artifacts/branches during CI.

Fix in Cursor Fix in Web

Reviewed by Cursor Security Reviewer for commit 98be014. Configure here.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale comment

Left a non-blocking comment (not approved): Cursor Security Agent completed with 1 unresolved medium-severity finding that needs human attention. Cursor Bugbot was not present; no reviewers were assigned because no eligible non-author reviewers are available.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

nnkogift and others added 3 commits August 3, 2026 09:54
…ntain permissions'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🧹 Nitpick comments (1)
components/mui/src/fields/widgets/TextField.tsx (1)

5-10: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep the public widget props as WidgetProps.

Line 10 exposes type and multiline as widget props. Keep D2TextField accepting only WidgetProps. Move these options into a private renderer that the specialized widgets call.

As per coding guidelines, “Widget props must use WidgetProps = { control: FieldControlReturn }.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/mui/src/fields/widgets/TextField.tsx` around lines 5 - 10, Update
D2TextField to accept only WidgetProps and remove type/multiline from its public
signature. Move these options into a private renderer used by the specialized
widgets, ensuring all public widget components continue to use the WidgetProps
contract.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.agents/skills/fallow-review/hooks/settings.snippet.json:
- Line 9: Update both hook commands in
.agents/skills/fallow-review/hooks/settings.snippet.json: lines 9-9 should
target .claude/skills/fallow-review/hooks/fallow-review-session-init.sh, and
lines 20-20 should target
.claude/skills/fallow-review/hooks/fallow-review-on-feedback.sh, preserving the
CLAUDE_PROJECT_DIR prefix.

In @.agents/skills/fallow-review/references/tradeoff-elicitation.md:
- Around line 18-25: The trade-off output schema must expose the
fallow-validated change-anchor fields instead of treating them as file-line
anchors. In .agents/skills/fallow-review/references/tradeoff-elicitation.md
lines 18-25, update the JSON shape and labels to use the actual change_anchor
field and distinguish change-region anchors from signal anchors; in
.agents/skills/fallow-review/SKILL.md lines 81-83, align the documented judgment
fields with that same contract.

In @.agents/skills/fallow-review/SKILL.md:
- Line 172: The “Act” step in SKILL.md must document how accepted judgments
become feed JSONL records. Define the exact mapping from each judgment’s
signal_id or change_anchor, framing, and concern into the hook-consumed
target.kind, target.value, and note fields, or reference a helper that performs
this conversion before appending to .fallow-review/feed.jsonl.

In @.agents/skills/fallow/references/cli-reference.md:
- Around line 540-566: Reconcile the Fallow version shown in the health JSON
example with the repository’s pinned `fallow@2.77.0` dependency. Update the
documented `"version"` value and any related version-specific examples or fields
in the reference so they match supported behavior, or explicitly document the
intentional delta if the example must remain on 3.11.0.

In @.agents/skills/fallow/references/gotchas.md:
- Around line 639-641: Update the fenced code block containing the fallow
license refresh message to specify the text language identifier, changing the
opening fence to use text while preserving the message content.

In @.fallowrc.json:
- Around line 17-26: Remove all // comments from .fallowrc.json, including the
explanatory comments around minOccurrences and the commented ignore
configuration, so the file contains valid JSON while preserving the active
minOccurrences setting.

In @.github/workflows/ci.yml:
- Around line 116-117: Update the “Generate health badge” job to check out the
repository, set up pnpm and Node with the workflow’s existing versions, and
install dependencies using pnpm install --frozen-lockfile before generating the
badge. Invoke the project-local CLI through pnpm exec fallow health --format
badge so it uses the repository configuration.
- Around line 117-126: Update the badge generation and Publish badge step in the
workflow to write badge.svg under the dedicated badge-output directory, set
peaceiris/actions-gh-pages@v4's publish_dir to badge-output, and remove the
unsupported include_files input.

In `@components/mui/src/fields/widgets/ChoiceFields.tsx`:
- Around line 12-31: D2BooleanField must resolve and display field validation
feedback. Update D2BooleanField to call resolveFieldValidation, pass the
resulting error state to an appropriate error-aware form control wrapping
ToggleButtonGroup, and render the validation message while preserving the
existing boolean toggle behavior.
- Around line 38-53: Update the checkbox rendering around FormControlLabel so it
is wrapped in FormControl, remove the unsupported helperText prop from
FormControlLabel, and render validationText conditionally with FormHelperText
when hasError is true. Preserve the existing checkbox behavior, label, required
state, and validation text.

In `@components/mui/src/fields/widgets/NumericFields.tsx`:
- Around line 6-12: Update D2NumberField to pass step="any" through
slotProps.htmlInput on D2TextField, allowing fractional values while retaining
type="number". Leave D2IntegerField unchanged so it continues enforcing integer
input.

In `@components/mui/src/fields/widgets/TextField.tsx`:
- Around line 56-65: Update computeAgeFromDob to parse the validated date
components as a local calendar date using the year, month minus one, and day
constructor arguments, replacing new Date(dob). Preserve the existing age
calculation and invalid/future-date handling.

In `@README.md`:
- Around line 7-8: Update the fallow-badge job in ci.yml so it independently
checks out the repository, sets up the required runtime and pnpm, and installs
dependencies before generating the badge. Replace the bare fallow health
invocation in Generate health badge with pnpm exec fallow health --format badge
redirected to badge.svg, then preserve the existing publishing flow.

---

Nitpick comments:
In `@components/mui/src/fields/widgets/TextField.tsx`:
- Around line 5-10: Update D2TextField to accept only WidgetProps and remove
type/multiline from its public signature. Move these options into a private
renderer used by the specialized widgets, ensuring all public widget components
continue to use the WidgetProps contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 93b6ef07-716b-48b5-9bde-7ccdbcc260fc

📥 Commits

Reviewing files that changed from the base of the PR and between 864c125 and 98be014.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (48)
  • .agents/skills/fallow-review/SKILL.md
  • .agents/skills/fallow-review/agents/openai.yaml
  • .agents/skills/fallow-review/hooks/fallow-review-on-feedback.sh
  • .agents/skills/fallow-review/hooks/fallow-review-session-init.sh
  • .agents/skills/fallow-review/hooks/settings.snippet.json
  • .agents/skills/fallow-review/references/tradeoff-elicitation.md
  • .agents/skills/fallow/SKILL.md
  • .agents/skills/fallow/agents/openai.yaml
  • .agents/skills/fallow/references/cli-reference.md
  • .agents/skills/fallow/references/gotchas.md
  • .agents/skills/fallow/references/mcp.md
  • .agents/skills/fallow/references/node-bindings.md
  • .agents/skills/fallow/references/patterns.md
  • .claude/skills/fallow
  • .claude/skills/fallow-review
  • .fallowrc.json
  • .github/workflows/ci.yml
  • .gitignore
  • .idea/vcs.xml
  • .prettierignore
  • README.md
  • apps/playground/package.json
  • apps/storybook/interactions/ancInteractions.ts
  • apps/storybook/interactions/childProgrammeInteractions.ts
  • apps/storybook/interactions/waitTimeouts.ts
  • apps/storybook/package.json
  • components/mantine/src/fields/widgets/DateFields.tsx
  • components/mui/src/fields/widgets/ChoiceFields.tsx
  • components/mui/src/fields/widgets/Fields.tsx
  • components/mui/src/fields/widgets/NumericFields.tsx
  • components/mui/src/fields/widgets/TextField.tsx
  • components/mui/src/fields/widgets/UnsupportedField.tsx
  • components/mui/src/fields/widgets/index.ts
  • packages/metadata/src/buildSchema.ts
  • skills-lock.json
  • utils/devtools/src/attach.ts
  • utils/devtools/src/constants.ts
  • utils/devtools/src/traceStore.ts
  • utils/hooks/package.json
  • utils/hooks/src/fields/index.ts
  • utils/hooks/src/formValue.ts
  • utils/hooks/src/index.ts
  • utils/hooks/src/useEventForm.ts
  • utils/hooks/src/useTrackerForm.ts
  • utils/rules/src/context.ts
  • utils/rules/src/enrollmentContext.ts
  • utils/rules/src/evaluate.ts
  • utils/rules/src/ruleInterop.ts
💤 Files with no reviewable changes (6)
  • components/mantine/src/fields/widgets/DateFields.tsx
  • components/mui/src/fields/widgets/Fields.tsx
  • utils/hooks/src/fields/index.ts
  • apps/storybook/package.json
  • utils/hooks/package.json
  • .idea/vcs.xml

"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/fallow-review-session-init.sh"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Point both hook commands at the installed skill hooks.

This change creates .claude/skills/fallow-review, but it does not create .claude/hooks/. Both commands will fail when Claude invokes the hooks.

  • .agents/skills/fallow-review/hooks/settings.snippet.json#L9-L9: change the command to ${CLAUDE_PROJECT_DIR}/.claude/skills/fallow-review/hooks/fallow-review-session-init.sh.
  • .agents/skills/fallow-review/hooks/settings.snippet.json#L20-L20: change the command to ${CLAUDE_PROJECT_DIR}/.claude/skills/fallow-review/hooks/fallow-review-on-feedback.sh.
📍 Affects 1 file
  • .agents/skills/fallow-review/hooks/settings.snippet.json#L9-L9 (this comment)
  • .agents/skills/fallow-review/hooks/settings.snippet.json#L20-L20
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.agents/skills/fallow-review/hooks/settings.snippet.json at line 9, Update
both hook commands in .agents/skills/fallow-review/hooks/settings.snippet.json:
lines 9-9 should target
.claude/skills/fallow-review/hooks/fallow-review-session-init.sh, and lines
20-20 should target
.claude/skills/fallow-review/hooks/fallow-review-on-feedback.sh, preserving the
CLAUDE_PROJECT_DIR prefix.

Comment on lines +18 to +25
by fallow. What fallow now DOES validate is the ANCHOR: a trade-off may cite a
`change_anchor` (a `chg:` id from the guide's `change_anchors`) and fallow
post-validates it on reentry the same way it validates a `signal_id`, rejecting an
anchor it never emitted (`unknown-change-anchor`). The accepted judgment carries
`anchor_kind: "change"` to mark it as the WEAKER, region-level anchor: it proves
only that the region changed, not that a graph finding exists there (that is
`anchor_kind: "signal"`). So the anchor is now fallow-grade; the framing prose
remains an agent-layer aid whose discipline is the prompt's.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant docs =="
for f in ".agents/skills/fallow-review/SKILL.md" ".agents/skills/fallow-review/references/tradeoff-elicitation.md"; do
  if [ -f "$f" ]; then
    echo "-- $f --"
    sed -n '70,90p;1,40p' "$f"
  else
    echo "MISSING $f"
  fi
done

echo
echo "== search walkthrough/judgment/tradeoff symbols =="
rg -n "tradeoff|change_anchor|signal_id|walkthrough|fallow-review|unknown-change-anchor|Determination|deterministic" .agents/skills/fallow-review . 2>/dev/null | head -n 200

Repository: nnkogift/dhis2-form-utils

Length of output: 23483


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== tradeoff prompt relevant sections =="
sed -n '120,170p;140,155p' .agents/skills/fallow-review/references/tradeoff-elicitation.md | cat -n

echo
echo "== exact references to tradeoffs.json and post-validate text in fallow-review docs =="
python3 - <<'PY'
from pathlib import Path
for path, start, end in [
    (".agents/skills/fallow-review/SKILL.md", 67, 128),
    (".agents/skills/fallow-review/references/tradeoff-elicitation.md", 1, 25),
]:
    p = Path(path)
    lines = p.read_text().splitlines()
    print(f"-- {path}:{start}-{end} --")
    for i in range(start, min(end+1, len(lines))+1):
        if "tradeoffs" in lines[i-1] or "walkthrough" in lines[i-1] or "change_anchor" in lines[i-1] or "signal_id" in lines[i-1] or "post-validates" in lines[i-1]:
            print(f"{i:04}: {lines[i-1]}")
PY

echo
echo "== programmatic check for stated invariant: file mentions tradeoffs.json excluded from --walkthrough-file; --walkthrough-file doc only lists signal_id? =="
python3 - <<'PY'
from pathlib import Path
skill = Path(".agents/skills/fallow-review/SKILL.md").read_text()
tradeoff = Path(".agents/skills/fallow-review/references/tradeoff-elicitation.md").read_text()
checks = {
    "tradeoff_18_25_mentions_change_anchor_as_post_validated": 'change_anchor' in "\n".join(tradeoff.splitlines()[17:25]) and 'post-validates' in "\n".join(tradeoff.splitlines()[17:25]),
    "tradeoff_18_25_mentions_signal_id_validation": 'signal_id' in "\n".join(tradeoff.splitlines()[17:25]) and 'validates' in "\n".join(tradeoff.splitlines()[17:25]),
    "skill_81_83_mentions_tradeoffs_json": 'tradeoffs.json' in "\n".join(skill.splitlines()[80:84]),
    "skill_81_83_says_step_not_part_of_post_validate": 'NOT part of the post-validate agent-contract loop' in "\n".join(skill.splitlines()[80:84]),
    "skill_81_83_says_tradeoffs_never_round_tripped": 'never round-tripped' in "\n".join(skill.splitlines()[80:84]),
    "skill_81_83_claims_walkthrough_file_validates_only_signal_ids": 'walkthrough-file' in "\n".join(skill.splitlines()[80:84]) and 'only validates emitted `signal_id`s' in "\n".join(skill.splitlines()[80:84]),
    "skill_92_128_describes_change_anchor_reentry_validation": 'change_anchor' in skill[skill.index('The ANCHOR is now fallow-validated'):skill.index('When a review surface')],
    "skill_92_128_describes_walkthrough_file_reentry_validation_labels": 'unanchored-signal-id' in skill[skill.index('The agent-contract loop above'):skill.index('## Human-in-the-loop walkthrough')],
}
for k, v in checks.items():
    print(f"{k}: {v}")
PY

Repository: nnkogift/dhis2-form-utils

Length of output: 7778


Make the trade-off output shape use the actual change_anchor fields.

The trade-off prompt schema only uses anchor as a file:line or cross-cutting, and SKILL.md#L67 already calls out that judgments may use change_anchor. If these items are meant to support fallow-validated change-region anchoring, add the matching JSON field(s) and labels here instead of mixing change_anchor prose into a file-line anchor contract.

📍 Affects 2 files
  • .agents/skills/fallow-review/references/tradeoff-elicitation.md#L18-L25 (this comment)
  • .agents/skills/fallow-review/SKILL.md#L81-L83
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.agents/skills/fallow-review/references/tradeoff-elicitation.md around lines
18 - 25, The trade-off output schema must expose the fallow-validated
change-anchor fields instead of treating them as file-line anchors. In
.agents/skills/fallow-review/references/tradeoff-elicitation.md lines 18-25,
update the JSON shape and labels to use the actual change_anchor field and
distinguish change-region anchors from signal anchors; in
.agents/skills/fallow-review/SKILL.md lines 81-83, align the documented judgment
fields with that same contract.

- `rejected` `unanchored-signal-id` / `unknown-change-anchor`: the human cited something fallow never emitted. Re-anchor to a real signal or region; do not invent one.
- `stale: true` (`stale-snapshot`): the tree moved since `guide.json` was fetched. Re-fetch the guide, re-capture, resubmit.

4. **Act:** relay the accepted human verdicts into the coding session in place, or append them to `.fallow-review/feed.jsonl` so the live-injection hooks (below) carry them to the session that wrote the code. Either way the note arrives anchored and fenced, never as a fallow-grade fact.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Document the feed JSONL conversion.

The hook in .agents/skills/fallow-review/hooks/fallow-review-on-feedback.sh reads target.kind, target.value, and note from each JSONL record. The walkthrough example produces judgments[] with signal_id or change_anchor, framing, and concern. Line 172 does not define the conversion. Appending the walkthrough envelope directly would produce invalid JSONL and empty injected notes.

Add the exact record format or provide a helper that maps each accepted judgment to one feed record.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.agents/skills/fallow-review/SKILL.md at line 172, The “Act” step in
SKILL.md must document how accepted judgments become feed JSONL records. Define
the exact mapping from each judgment’s signal_id or change_anchor, framing, and
concern into the hook-consumed target.kind, target.value, and note fields, or
reference a helper that performs this conversion before appending to
.fallow-review/feed.jsonl.

Comment on lines +540 to +566
```json
{
"kind": "health",
"schema_version": 7,
"version": "3.11.0",
"elapsed_ms": 32,
"summary": {
"files_analyzed": 482,
"functions_analyzed": 3200,
"functions_above_threshold": 3,
"max_cyclomatic_threshold": 20,
"max_cognitive_threshold": 15
},
"findings": [
{
"path": "src/parser.ts",
"name": "parseExpression",
"line": 42,
"col": 0,
"cyclomatic": 28,
"cognitive": 22,
"line_count": 95,
"exceeded": "both"
}
]
}
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Compare the pinned fallow version against the version literal used in the skill docs.
rg -n '"specifier".*fallow|^\s*fallow@' pnpm-lock.yaml | head -20
rg -n '"version": "3\.11\.0"' .agents/skills/fallow/references/cli-reference.md | head -5

Repository: nnkogift/dhis2-form-utils

Length of output: 354


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workspace/package references =="
grep -RIno '"fallow"|fallow@|fallow:' package.json pnpm-lock.yaml . || true

echo
echo "== fallow references in agent skill files (first matches per file) =="
fd . .agents/skills/fallow -t f 2>/dev/null | while read -r f; do
  if rg -n 'fallow|3\.11\.0|2\.77\.0|2\.76\.0' "$f" >/tmp/findings_match.$$ 2>/dev/null; then
    echo "--- $f"
    sed -n '1,40p' "$f"
  fi
done || true
rm -f /tmp/findings_match.$$

echo
echo "== cli-reference.md examples around version and schema/context =="
sed -n '510,580p' .agents/skills/fallow/references/cli-reference.md
echo
sed -n '920,970p' .agents/skills/fallow/references/cli-reference.md
echo
sed -n '1100,1170p' .agents/skills/fallow/references/cli-reference.md
echo
sed -n '1900,1955p' .agents/skills/fallow/references/cli-reference.md

Repository: nnkogift/dhis2-form-utils

Length of output: 50383


Reconcile the documented Fallow version with the installed dependency.

This repo pins and resolves fallow@2.77.0, but this reference repeatedly shows JSON examples with "version": "3.11.0". Update the example versions, pin an appropriate release, or document the intentional version delta so agents do not request unsupported command flags or JSON fields from 2.77.0.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.agents/skills/fallow/references/cli-reference.md around lines 540 - 566,
Reconcile the Fallow version shown in the health JSON example with the
repository’s pinned `fallow@2.77.0` dependency. Update the documented
`"version"` value and any related version-specific examples or fields in the
reference so they match supported behavior, or explicitly document the
intentional delta if the example must remain on 3.11.0.

Comment on lines +639 to +641
```
fallow license refresh: your stored license is too stale to refresh. Reactivate with: fallow license activate --trial --email <addr> (HTTP 401, code token_stale)
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language identifier to the fenced code block.

The fenced block at line 639 has no language tag, unlike the surrounding bash and jsonc blocks in this file. Add text so the block renders consistently.

📝 Proposed fix
-```
+```text
 fallow license refresh: your stored license is too stale to refresh. Reactivate with: fallow license activate --trial --email <addr> (HTTP 401, code token_stale)
</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 639-639: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.agents/skills/fallow/references/gotchas.md around lines 639 - 641, Update
the fenced code block containing the fallow license refresh message to specify
the text language identifier, changing the opening fence to use text while
preserving the message content.

Source: Linters/SAST tools

Comment on lines +12 to +31
export function D2BooleanField({ control }: WidgetProps) {
const { fieldConfig, field, isDisabled } = control;

return (
<ToggleButtonGroup
exclusive
value={field.value as string}
disabled={isDisabled}
onChange={(_event, value: string | null) => {
field.onChange(value ?? '');
}}
onBlur={field.onBlur}
aria-label={fieldConfig.label}
>
<ToggleButton value="true">Yes</ToggleButton>
<ToggleButton value="false">No</ToggleButton>
<ToggleButton value="">—</ToggleButton>
</ToggleButtonGroup>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Render validation feedback for D2BooleanField.

D2BooleanField does not call resolveFieldValidation. An invalid Boolean field has no error state or validation message. Resolve validation and render the message with an error-aware form control.

As per coding guidelines, “widgets must use resolveFieldValidation.”

🧰 Tools
🪛 ESLint

[error] 13-13: Unsafe assignment of an error typed value.

(@typescript-eslint/no-unsafe-assignment)


[error] 18-18: Unsafe member access .value on a type that cannot be resolved.

(@typescript-eslint/no-unsafe-member-access)


[error] 19-19: Unsafe assignment of an error typed value.

(@typescript-eslint/no-unsafe-assignment)


[error] 21-21: Unsafe call of a type that could not be resolved.

(@typescript-eslint/no-unsafe-call)


[error] 21-21: Unsafe member access .onChange on a type that cannot be resolved.

(@typescript-eslint/no-unsafe-member-access)


[error] 23-23: Unsafe assignment of an error typed value.

(@typescript-eslint/no-unsafe-assignment)


[error] 23-23: Unsafe member access .onBlur on a type that cannot be resolved.

(@typescript-eslint/no-unsafe-member-access)


[error] 24-24: Unsafe assignment of an error typed value.

(@typescript-eslint/no-unsafe-assignment)


[error] 24-24: Unsafe member access .label on a type that cannot be resolved.

(@typescript-eslint/no-unsafe-member-access)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/mui/src/fields/widgets/ChoiceFields.tsx` around lines 12 - 31,
D2BooleanField must resolve and display field validation feedback. Update
D2BooleanField to call resolveFieldValidation, pass the resulting error state to
an appropriate error-aware form control wrapping ToggleButtonGroup, and render
the validation message while preserving the existing boolean toggle behavior.

Source: Coding guidelines

Comment on lines +38 to +53
<FormControlLabel
control={
<Checkbox
name={field.name}
checked={field.value === 'true'}
disabled={isDisabled}
onChange={(event) => {
field.onChange(event.target.checked ? 'true' : '');
}}
onBlur={field.onBlur}
/>
}
label={fieldConfig.label}
required={isMandatory}
{...(hasError ? { helperText: validationText } : {})}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -t f '^package.json$' components -x rg -n -C 2 '"`@mui/material`"|"typecheck"' {}
rg -n -C 4 'FormControlLabel|helperText' components/mui/src/fields/widgets/ChoiceFields.tsx

Repository: nnkogift/dhis2-form-utils

Length of output: 2050


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const fs = require('fs');
const path = require('path');

const file = 'components/mui/src/fields/widgets/ChoiceFields.tsx';
const content = fs.readFileSync(file, 'utf8');
const idx = content.indexOf('<FormControlLabel');
const slice = content.slice(Math.max(0, idx - 120), Math.min(content.length, idx + 360));
console.log(slice);

const package = JSON.parse(fs.readFileSync('components/mui/package.json', 'utf8'));
console.log('mui dep:', package.dependencies?.['`@mui/material`'] || package.devDependencies?.['`@mui/material`']);
JS

npm view `@mui/material`@latest peerDependencies `@mui/types` version --json 2>/dev/null | jq -r '{version, peerDependencies}' || true
npm view `@mui/types`@latest version --json 2>/dev/null | jq -r 'if type=="array" then .[-1] else . end' || true

# Inspect installed MUI type declarations if the cache makes them available without repo runtime.
node - <<'JS'
console.log(JSON.stringify(process.env, null, 0).length);
JS

Repository: nnkogift/dhis2-form-utils

Length of output: 983


🌐 Web query:

@mui/material 9.0.1 FormControlLabel props helperText TypeScript

💡 Result:

In @mui/material 9.0.1, the FormControlLabel component does not have a helperText prop [1][2]. The FormControlLabel API is strictly defined to handle the label and control (such as Radio, Switch, or Checkbox) [1]. If you need to display helper text alongside a form control that uses FormControlLabel, the standard approach in Material UI is to wrap the component in a FormControl and use the FormHelperText component as a sibling [3][4]. Example pattern: <FormControlLabel control={} label="Accept terms" /> Please check this box to continue. Since FormControlLabel does not natively support helperText, there are no corresponding TypeScript types for such a prop on this component [1][2]. Any attempt to pass helperText to FormControlLabel will result in a TypeScript error (or be treated as an invalid/unknown prop).

Citations:


Move validation text to FormHelperText.

FormControlLabel does not accept helperText, so this JSX prop fails TypeScript strict mode and does not render the validation message. Wrap the checkbox input in FormControl and render validationText with FormHelperText.

Proposed fix
-import { Checkbox, FormControlLabel, MenuItem, TextField, ToggleButton, ToggleButtonGroup } from '`@mui/material`';
+import {
+    Checkbox,
+    FormControl,
+    FormControlLabel,
+    FormHelperText,
+    MenuItem,
+    TextField,
+    ToggleButton,
+    ToggleButtonGroup,
+} from '`@mui/material`';

-        <FormControlLabel
-            control={
+        <FormControl error={hasError}>
+            <FormControlLabel
+                control={
                 <Checkbox
                     name={field.name}
                     checked={field.value === 'true'}
                     disabled={isDisabled}
                     onChange={(event) => {
                         field.onChange(event.target.checked ? 'true' : '');
                     }}
                     onBlur={field.onBlur}
                 />
-            }
-            label={fieldConfig.label}
-            required={isMandatory}
-            {...(hasError ? { helperText: validationText } : {})}
-        />
+                }
+                label={fieldConfig.label}
+                required={isMandatory}
+            />
+            {hasError && <FormHelperText>{validationText}</FormHelperText>}
+        </FormControl>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<FormControlLabel
control={
<Checkbox
name={field.name}
checked={field.value === 'true'}
disabled={isDisabled}
onChange={(event) => {
field.onChange(event.target.checked ? 'true' : '');
}}
onBlur={field.onBlur}
/>
}
label={fieldConfig.label}
required={isMandatory}
{...(hasError ? { helperText: validationText } : {})}
/>
<FormControl error={hasError}>
<FormControlLabel
control={
<Checkbox
name={field.name}
checked={field.value === 'true'}
disabled={isDisabled}
onChange={(event) => {
field.onChange(event.target.checked ? 'true' : '');
}}
onBlur={field.onBlur}
/>
}
label={fieldConfig.label}
required={isMandatory}
/>
{hasError && <FormHelperText>{validationText}</FormHelperText>}
</FormControl>
🧰 Tools
🪛 ESLint

[error] 41-41: Unsafe assignment of an error typed value.

(@typescript-eslint/no-unsafe-assignment)


[error] 41-41: Unsafe member access .name on a type that cannot be resolved.

(@typescript-eslint/no-unsafe-member-access)


[error] 42-42: Unsafe member access .value on a type that cannot be resolved.

(@typescript-eslint/no-unsafe-member-access)


[error] 43-43: Unsafe assignment of an error typed value.

(@typescript-eslint/no-unsafe-assignment)


[error] 45-45: Unsafe call of a type that could not be resolved.

(@typescript-eslint/no-unsafe-call)


[error] 45-45: Unsafe member access .onChange on a type that cannot be resolved.

(@typescript-eslint/no-unsafe-member-access)


[error] 47-47: Unsafe assignment of an error typed value.

(@typescript-eslint/no-unsafe-assignment)


[error] 47-47: Unsafe member access .onBlur on a type that cannot be resolved.

(@typescript-eslint/no-unsafe-member-access)


[error] 50-50: Unsafe assignment of an error typed value.

(@typescript-eslint/no-unsafe-assignment)


[error] 50-50: Unsafe member access .label on a type that cannot be resolved.

(@typescript-eslint/no-unsafe-member-access)


[error] 51-51: Unsafe assignment of an error typed value.

(@typescript-eslint/no-unsafe-assignment)


[error] 52-52: Unsafe assignment of an error typed value.

(@typescript-eslint/no-unsafe-assignment)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/mui/src/fields/widgets/ChoiceFields.tsx` around lines 38 - 53,
Update the checkbox rendering around FormControlLabel so it is wrapped in
FormControl, remove the unsupported helperText prop from FormControlLabel, and
render validationText conditionally with FormHelperText when hasError is true.
Preserve the existing checkbox behavior, label, required state, and validation
text.

Comment on lines +6 to +12
export function D2NumberField(props: WidgetProps) {
return <D2TextField {...props} type="number" />;
}

export function D2IntegerField(props: WidgetProps) {
return <D2TextField {...props} type="number" />;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate metadata and tests that distinguish decimal NUMBER fields from INTEGER fields.
rg -n -C 3 \
  --glob '*.{ts,tsx}' \
  'D2NumberField|D2IntegerField|widgetKind.*number|widgetKind.*integer|valueType.*NUMBER|valueType.*INTEGER' \
  components packages utils

Repository: nnkogift/dhis2-form-utils

Length of output: 23520


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the numeric widgets and their parent text field props/type handling.
printf '\n--- components/mui/src/fields/widgets/NumericFields.tsx ---\n'
cat -n components/mui/src/fields/widgets/NumericFields.tsx

printf '\n--- components/mui/src/fields/widgets/TextField.tsx ---\n'
cat -n components/mui/src/fields/widgets/TextField.tsx

printf '\n--- Numeric field mappings/usages ---\n'
rg -n -C 4 \
  --glob '*.{ts,tsx}' \
  '(widgetKind|widgetKindValue|D2NumberField|D2IntegerField|INTEGER|NUMBER|Integer|Number)' \
  components packages utils | head -n 240

printf '\n--- package metadata for value types ---\n'
cat -n packages/metadata/src/buildSchema.ts | sed -n '1,120p'

Repository: nnkogift/dhis2-form-utils

Length of output: 26477


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect D2TextField type forwarding and locate any step-prop support for other number inputs.
printf '\n--- locate step props in components ---\n'
rg -n --glob '*.{ts,tsx}' -C 3 'step=|slotProps.*htmlInput|htmlInput|D2TextField|IntegerInput|NumericInput' components packages

printf '\n--- browser/JS numeric input step semantics probe ---\n'
node - <<'JS'
const inputs = [
  { attrs: { type: 'number' } },
  { attrs: { type: 'number', step: 'any' } },
  { attrs: { type: 'number', step: '1' } },
];
let skipped = false;
for (const input of inputs) {
  try {
    if (typeof document === 'undefined') {
      console.log(`jsdom unavailable, skipped input ${JSON.stringify(input.attrs)}`);
      skipped = true;
      continue;
    }
    const el = Document.prototype.createElement.call({}, 'input', input.attrs);
    el.value = '1.5';
    const result = el.checkValidity();
    const message = result ? 'valid' : 'invalid';
    console.log(`#${JSON.stringify(input.attrs)} checkValidity=${message} error=${el.validationMessage || ''}`);
  } catch (error) {
    if (!skipped) console.log(`probe failed: ${error.name}: ${error.message}`);
  }
}
JS

Repository: nnkogift/dhis2-form-utils

Length of output: 13728


🌐 Web query:

MDN HTML input[global] step attribute default value step="1" number field

💡 Result:

For an HTML element, the default value of the step attribute is 1 [1][2][3]. This default step of 1 means that, by default, the input only accepts integers (whole numbers) [1][3]. If a user attempts to enter a non-integer value, such as 4.2, it will be considered invalid and will match the:invalid CSS pseudo-class [3]. There are two primary ways to allow non-integer (floating-point) values: 1. Setting step="any": This removes the step constraint entirely, allowing any floating-point number [1][3]. 2. Setting a smaller step value: You can specify a decimal value for the step attribute (e.g., step="0.1" or step="0.01") to allow input with that specific precision [1][2][3]. Additionally, if the "step base" (determined by the min attribute, or the value attribute if min is not provided) is a non-integer, those non-integer values may remain valid despite the default step of 1 [1][4].

Citations:


Allow fractional values in D2NumberField.

D2NumberField and D2IntegerField both render the same type="number" input, and a number input defaults to step="1" so fractional entries fail native validation. Pass step="any" to the HTML input for D2NumberField; MUI forwards this through slotProps.htmlInput.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/mui/src/fields/widgets/NumericFields.tsx` around lines 6 - 12,
Update D2NumberField to pass step="any" through slotProps.htmlInput on
D2TextField, allowing fractional values while retaining type="number". Leave
D2IntegerField unchanged so it continues enforcing integer input.

Comment on lines +56 to +65
function computeAgeFromDob(dob: string): string {
if (!/^\d{4}-\d{2}-\d{2}$/.test(dob)) return '';
const birth = new Date(dob);
const today = new Date();
let age = today.getFullYear() - birth.getFullYear();
const monthDiff = today.getMonth() - birth.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
age -= 1;
}
return age >= 0 ? String(age) : '';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

TZ=America/Los_Angeles node - <<'NODE'
const dob = '2000-08-04';
const birth = new Date(dob);
const today = new Date(2026, 7, 3);

let age = today.getFullYear() - birth.getFullYear();
if (
  today.getMonth() < birth.getMonth() ||
  (today.getMonth() === birth.getMonth() && today.getDate() < birth.getDate())
) {
  age -= 1;
}

console.log({ parsedLocalDate: birth.toString(), computedAge: age, expectedAge: 25 });
NODE

Repository: nnkogift/dhis2-form-utils

Length of output: 284


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate TextField.tsx:"
git ls-files | grep -F 'components/mui/src/fields/widgets/TextField.tsx' || true

echo
echo "Relevant file excerpt:"
if [ -f components/mui/src/fields/widgets/TextField.tsx ]; then
  nl -ba components/mui/src/fields/widgets/TextField.tsx | sed -n '1,140p'
fi

echo
echo "Behavioral probe for calendar-date fix:"
TZ=America/Los_Angeles node - <<'NODE'
function computeAgeFromDob(dob) {
  if (!/^\d{4}-\d{2}-\d{2}$/.test(dob)) return '';
  const birth = new Date(dob);
  const today = new Date(2026, 7, 3);
  let age = today.getFullYear() - birth.getFullYear();
  if (today.getMonth() < birth.getMonth() || (today.getMonth() === birth.getMonth() && today.getDate() < birth.getDate())) age -= 1;
  return age >= 0 ? String(age) : '';
}

function computeAgeFromDobLocal(dob) {
  if (!/^\d{4}-\d{2}-\d{2}$/.test(dob)) return '';
  const year = parseInt(dob.slice(0, 4), 10);
  const month = parseInt(dob.slice(5, 7), 10);
  const day = parseInt(dob.slice(8, 10), 10);
  const birth = new Date(year, month - 1, day);
  const today = new Date(2026, 7, 3);
  let age = today.getFullYear() - birth.getFullYear();
  if (today.getMonth() < birth.getMonth() || (today.getMonth() === birth.getMonth() && today.getDate() < birth.getDate())) age -= 1;
  return age >= 0 ? String(age) : '';
}

for (const [dob, expected] of [
  ['2000-08-04', 25],
  ['2000-08-05', 25],
  ['2000-08-03', 26]
]) {
  console.log({ dob, ageCurrent: computeAgeFromDob(dob), ageLocalOnly: computeAgeFromDopLocal(dob), expectedAge: expected });
}
NODE

Repository: nnkogift/dhis2-form-utils

Length of output: 298


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Relevant file excerpt:"
head -n 140 components/mui/src/fields/widgets/TextField.tsx | cat -n

echo
echo "Behavioral probe for calendar-date fix:"
TZ=America/Los_Angeles node - <<'NODE'
function computeAgeFromDob(dob) {
  if (!/^\d{4}-\d{2}-\d{2}$/.test(dob)) return '';
  const birth = new Date(dob);
  const today = new Date(2026, 7, 3);
  let age = today.getFullYear() - birth.getFullYear();
  if (today.getMonth() < birth.getMonth() || (today.getMonth() === birth.getMonth() && today.getDate() < birth.getDate())) age -= 1;
  return age >= 0 ? String(age) : '';
}

function computeAgeFromDobLocal(dob) {
  if (!/^\d{4}-\d{2}-\d{2}$/.test(dob)) return '';
  const year = parseInt(dob.slice(0, 4), 10);
  const month = parseInt(dob.slice(5, 7), 10);
  const day = parseInt(dob.slice(8, 10), 10);
  const birth = new Date(year, month - 1, day);
  const today = new Date(2026, 7, 3);
  let age = today.getFullYear() - birth.getFullYear();
  if (today.getMonth() < birth.getMonth() || (today.getMonth() === birth.getMonth() && today.getDate() < birth.getDate())) age -= 1;
  return age >= 0 ? String(age) : '';
}

for (const dob of ['2000-08-04', '2000-08-05', '2000-08-03']) {
  console.log({ dob, ageCurrent: computeAgeFromDob(dob), ageLocalOnly: computeAgeFromDobLocal(dob), expectedAge: dob <= '2000-08-04' ? 25 : 24 });
}
NODE

Repository: nnkogift/dhis2-form-utils

Length of output: 3698


Parse the date as a local calendar date.

new Date(dob) treats the date-only string as UTC midnight, so local date getters can read the prior calendar day west of UTC. When the current day is before the birth date, this can report one extra year of age. Parse the yyyy-mm-dd fields into new Date(year, month - 1, day) instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/mui/src/fields/widgets/TextField.tsx` around lines 56 - 65, Update
computeAgeFromDob to parse the validated date components as a local calendar
date using the year, month minus one, and day constructor arguments, replacing
new Date(dob). Preserve the existing age calculation and invalid/future-date
handling.

Comment thread README.md

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Left a non-blocking comment (not approved): Cursor Security Agent completed successfully but still has 1 unresolved medium-severity finding that needs human attention. Cursor Bugbot was not present on this PR; no reviewers were assigned because no eligible non-author reviewers are available.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@nnkogift
nnkogift merged commit ece87d6 into main Aug 3, 2026
13 of 14 checks passed
@nnkogift
nnkogift deleted the refactor/coding-standards-cleanup branch August 3, 2026 07:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants