#!/usr/bin/env nu # Validate that reflection/forms/config.ncl and reflection/forms/config.ncl.j2 # are in sync: every form field with a nickel_path must have a {{ name }} reference # in the template, and every {{ var }} in the template must map to a known form field. # # Run: nu install/check-config-sync.nu # CI: just ci-check-config-sync let form_path = "reflection/forms/config.ncl" let template_path = "reflection/forms/config.ncl.j2" # ── Extract field names that have nickel_path ───────────────────────────────── # Strategy: scan the form for blocks containing both `name = "..."` and `nickel_path`. # We do a two-pass: first collect all (line_number, name) pairs, then check which # have a nickel_path within the same element block (lines up to the next `}`). let form_lines = open --raw $form_path | lines | enumerate # Collect all element start positions and their names let elements = $form_lines | reduce --fold [] {|row, acc| let line = $row.item if ($line | str contains "name = \"") { let name = try { $line | parse --regex 'name\s*=\s*"(?P[^"]+)"' | get n | first } catch { "" } if ($name | is-not-empty) { $acc | append { idx: $row.index, name: $name } } else { $acc } } else { $acc } } # For each element, check if its block (from its line until the next element or EOF) # contains a nickel_path. Collect names that have nickel_path. let form_text = open --raw $form_path let ncl_path_fields = $elements | reduce --fold [] {|el, acc| # Find the substring from this element's line forward to end, check for nickel_path # before the next `name =` (other than within nested structures) let after = $form_lines | skip $el.idx | take while {|r| not ($r.item | str contains "name = \"") or $r.index == $el.idx } | get item | str join "\n" if ($after | str contains "nickel_path") { $acc | append $el.name } else { $acc } } # ── Extract {{ var }} references from template ──────────────────────────────── let template_vars = open --raw $template_path | parse --regex '\{\{\s*(?P[a-zA-Z_][a-zA-Z0-9_]*)\s*[\|}\s]' | get var | uniq | where {|v| $v != "if" and $v != "else" and $v != "endif" and $v != "for" and $v != "endfor"} # ── Cross-check ─────────────────────────────────────────────────────────────── mut errors = [] # 1. Every nickel_path field must appear in template for field in $ncl_path_fields { if not ($template_vars | any {|v| $v == $field}) { $errors = ($errors | append $"MISSING in template: form field '($field)' has nickel_path but no \{\{ ($field) \}\} in config.ncl.j2") } } # 2. Every template var must exist as a form field name (excluding Tera filters/functions) let all_form_names = $elements | get name let tera_builtins = ["json", "trim", "split", "map", "attribute", "pat"] for var in $template_vars { if not ($all_form_names | any {|n| $n == $var}) and not ($tera_builtins | any {|b| $b == $var}) { $errors = ($errors | append $"ORPHAN in template: \{\{ ($var) \}\} has no matching form field in config.ncl") } } # ── Report ──────────────────────────────────────────────────────────────────── if ($errors | is-empty) { print $"(ansi green)✓ config form/template in sync(ansi reset) ($ncl_path_fields | length) fields, ($template_vars | length) template vars" } else { print $"(ansi red)✗ config form/template DRIFT DETECTED(ansi reset)" for e in $errors { print $" (ansi yellow)→(ansi reset) ($e)" } print "" print "Fix: update reflection/forms/config.ncl AND reflection/forms/config.ncl.j2 together." exit 1 }