Skip to content

refactor(scripts): split UI contract canonicalizer into explicit phases - #204

Merged
matthewpeterkort merged 1 commit into
feature/postgres-backendfrom
ao/repo-simplify/200-inventory-canonicalizer
Nov 13, 2026
Merged

matthewpeterkort merged 1 commit into
feature/postgres-backendfrom
ao/repo-simplify/200-inventory-canonicalizer

Conversation

@matthewpeterkort

Copy link
Copy Markdown
Collaborator

What

Breaks the 497-line canonicalize() in scripts/normalize_ui_contract_inventory.py into a six-line orchestrator plus the phases that were already visible inside it (issue #200). No behavior, schema, fragment format, or Builder runtime change; normalization and independent validation stay separate modules.

Why this reduces reasoning burden (not line count)

Before, changing any one stage required holding all stages in view at once: a contributor touching the population-exclusion request override had to scroll past fragment folding, alias merges, dimension projection, seed policy, and disabled mapping to find it, and had to know which locals (refs, family, status) were mutated by which earlier stage.

After:

  • canonicalize() says the pipeline: fold → build each affordance → sort → attach aliases → disabled mappings. The ordering facts that matter (fragment read order, out.sort position, relational-fragment read at the end) are all visible in six lines.
  • Each phase is one private function with only the inputs it reads (_build_constraints(sources, refs), _availability_for(primary, status), …). The implicit shared-state trace shrinks: refs is now extended at exactly one boundary (_build_requests returns it), family is renamed in exactly one place, status never escapes _build_affordance.
  • Domain special cases sit beside the phase that owns them: the population-exclusion override and the combine input-catalog request live inside _build_requests; the group-field summary dimensions live inside _build_dimensions; the per-affordance flow-order/dependency table lives inside _build_request_flows. A change to one workflow no longer renders the other 400 lines as context.
  • No generic framework, registry, or data-driven transformation layer was added; the special cases stayed explicit Python, per the ticket.

Moved-body correspondence

The edit touches 901 lines, so it was produced by a rerunnable checked-extraction generator (reads the pinned base via git show 80e8877…:scripts/normalize_ui_contract_inventory.py, emits each body only from declared source ranges, asserts anchors on every boundary, classifies every original line 670–1166 exactly once as moved/edited/dropped/replaced, and fails otherwise; rerunning regenerates the committed file byte-identically).

New function Original lines
_fold_fragment_rows 671–672, 674–709, 711–714 verbatim; dropped 673 (aliases copy), blanks
_entry_paths 733–743 verbatim
_request_path 881–883, hoisted out of the inner loop (880 was the nested def); call site 884 renamed only
_build_requests 744–879, 885–939 verbatim
_build_request_flows 940–988 verbatim (incl. in-place order updates shared with requests)
_build_dimensions 989–1056 verbatim
_build_constraints 1057–1073 verbatim; dropped dead no-op loop 1074–1075 (for … : pass, no side effects)
_disabled_state_record / _build_disabled_states 1088–1099 verbatim; the two identical 6-line literals at 1091–1096 and 1100–1105 verified equal and merged into one projection
_availability_for 1079–1084 verbatim
_build_affordance 718–732, 1076–1078, 1085–1087, 1106–1141 verbatim, plus five phase-call lines
_attach_aliases 1143–1147; aliases→ALIASES (the dropped copy was read-only), out→affordances
_build_disabled_affordances 1148–1165 verbatim

One plumbing edit outside the region: seed_for's never-read lifecycle parameter removed (call site updated; no external callers or tests reference it).

Verification (all against the final committed source)

  • Byte stability (the ABI): python3 scripts/normalize_ui_contract_inventory.py --expand output is cmp-identical to the pinned pre-change output — 57 affordances, all 159 request rows, order, source paths, evidence, and the SHA-256 expansion digest re-checked by load_inventory.
  • Manifest checks: git diff --check clean; python3 -m unittest scripts.tests.test_normalize_ui_contract_inventory scripts.tests.test_verify_ui_contract_inventory → Ran 14, OK, 0 skipped, no test edited. This covers the named normalizer-drift, endpoint-inference-refusal, stable-output, cardinality, and verifier-mutation tests unchanged.
  • Verifier: python3 scripts/verify_ui_contract_inventory.py --source-only → PASS UI contract inventory: 57 affordances, source hashes and file set current, source bindings complete.
  • No-comment CI law: scripts/check_no_code_comments.py on the file → violations=0 (the one allowed line is the pre-existing shebang). scripts.tests.test_no_code_comments → OK with 11 skips, all pre-existing sandbox skips (typescript module not resolvable), unrelated to this file.
  • Node contract: scripts/lib/ui-contract-inventory.mjs untouched; readUiContractInventory() still returns 57 affordances / 159 requests.

Preserved, deliberately

Exact SystemExit messages and first-failure precedence (fragment-missing, method-mismatch, shape-drift, no-source-backed-method/endpoint/payload); unknown endpoints still refused, never guessed; population-exclusion flow keeps selection-revision → exact prior route lookup over pages → attach ordering and stop condition; request/SQL-irrelevant ordering semantics (dict insertion order feeding flow_order, requests.sort, stable flow ordering) unchanged; the checked-in compact manifest is untouched.

Not done here

No AO review triggered (coordinator-owned). No changes outside scripts/normalize_ui_contract_inventory.py; scripts/lib/ui-contract-inventory.mjs and scripts/tests/test_normalize_ui_contract_inventory.py were audited and needed no change.

Closes #200

canonicalize() mixed fragment folding, exceptional ID rewrites, request
and request-flow construction, capability/constraint/evidence projection,
flow sorting, and disabled-affordance mapping in one 497-line function.
Extract the already-visible phases into private functions
(_fold_fragment_rows, _entry_paths, _build_requests, _request_path,
_build_request_flows, _build_dimensions, _build_constraints,
_disabled_state_record, _build_disabled_states, _availability_for,
_build_affordance, _attach_aliases, _build_disabled_affordances) so each
domain special case sits beside the phase that owns it, and leave
canonicalize as a six-line orchestrator.

Bodies moved verbatim by a rerunnable checked-extraction generator with
exhaustive before/after line accounting. Deliberate edits beyond moves:
drop the aliases copy and a no-op invariants loop, merge two identical
disabled-state literals into one projection, hoist the request_path
closure out of the inner loop, and drop seed_for's unused lifecycle
parameter. Normalization and independent validation stay separate.

Verification: expanded inventory byte-for-byte identical to the pinned
pre-change output (57 affordances, 159 request rows, digest checked via
load_inventory); scripts.tests.test_normalize_ui_contract_inventory and
test_verify_ui_contract_inventory pass 14/14 unchanged with zero skips;
verify_ui_contract_inventory.py --source-only PASS; no-code-comments
checker 0 violations on the changed file; Node reader
scripts/lib/ui-contract-inventory.mjs returns 57 affordances / 159
requests unchanged.

Closes #200

@matthewpeterkort matthewpeterkort left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Review: PR #204 (f2003c3) — refactor(scripts): split UI contract canonicalizer into explicit phases

Scope: single commit, single file scripts/normalize_ui_contract_inventory.py (473+/428-), base feature/postgres-backend @ 80e8877.

Verdict: APPROVED — no changes required, ready to merge. (Posted as COMMENT because GitHub rejects APPROVE/REQUEST_CHANGES on own-account reviews; this is the approving verdict for AO.)

I treated this as a behavior-preserving refactor and verified equivalence by static line-range diff (git log -L) of every region against the base revision:

  • Lines 1-533 and 1212-1324: unchanged (no hunks). seed_for body byte-identical after dropping the unused lifecycle parameter; its sole call site was updated.
  • Every moved block is verbatim, including all per-fid special cases (exclude_unmapped_member sequence guard, combine catalog append, flow_specs synthesis with shared evidence refs), the flowId reassignment chain, by_flow reordering with kind_priority, requests.sort keys, finalize_request_record usage, request-flow sequenceOrder/dependency/alternative-group tables, dimensions/constraints/disabled-states/availability projection, and the affordance dict literal. The hoisted _request_path is the same closure body; _disabled_state_record unifies two identical literals.
  • The four deliberate non-move edits are behavior-neutral: the aliases copy was only iterated (never mutated) in the old code so _attach_aliases can read ALIASES directly; the removed invariants loop was a no-op pass; the lifecycle parameter was unused in seed_for; closure hoisting is semantically identical.
  • Caller compatibility holds: scripts/verify_ui_contract_inventory.py imports only load_inventory (unchanged), and scripts/tests/test_normalize_ui_contract_inventory.py only uses BASE, finalize_request_record, and load_inventory (all unchanged signatures).

I verified this statically; per reviewer constraints I did not execute the script or test suite. The commit message records the runtime evidence: expanded inventory byte-for-byte identical to the pinned output (57 affordances, 159 requests, digest via load_inventory), 14/14 tests, and --source-only PASS.

Optional follow-ups (not required for this PR):

  1. _build_request_flows still mutates request order for builder.table_route_and_occurrence_selection after finalize_request_record, so requests[] checkpointIds can go stale relative to flow steps. Pre-existing and faithfully preserved here; a future behavior-changing cleanup could recompute checkpointId or reorder before finalize.
  2. _build_constraints(sources, refs) parameter order is inconsistent with _build_disabled_states(fid, sources, mapping_rows); cosmetic.
  3. _build_requests is still ~194 lines; splitting flow-spec synthesis from ordering could continue the phase split later.

@matthewpeterkort
matthewpeterkort marked this pull request as ready for review November 13, 2026 21:39
@matthewpeterkort
matthewpeterkort merged commit afd454c into feature/postgres-backend Nov 13, 2026
15 checks passed
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.

1 participant