MOD-TABLE — a register field can hold line items, not just a value - #111
MOD-TABLE — a register field can hold line items, not just a value#111ibuilder wants to merge 1 commit into
Conversation
Strix Security ReviewNo security issues found. Updated for Reviewed by Strix |
…just a value
The field sweep found 22 places a LIST had been flattened into one textarea
(`bid_submission.unit_prices`, `daily_report.crew_by_trade`, `incident.witnesses`), and the
deeper version of the same gap: `sov` is one record PER LINE with no parent document, and
`estimate` was a single `amount`. A schedule of values, a bid, an estimate and a daily manpower
log are all line-item documents. The config had no way to say so — so `bid_leveling.py`, whose
whole job is a row-per-scope-item comparison, was parsing prose for numbers a structured grid
would have handed it clean.
`table` fields declare `columns` and an optional `total_column`.
THE COLUMN VOCABULARY IS A DELIBERATE SUBSET, and the exclusions are the design:
nested table a grid inside a grid has no sane form rendering and no sane CSV export
rollup computed from other RECORDS, which a row is not
file/signature each needs its own upload or signing flow; a repeating row multiplies it
reference a picker per row is a real feature, not a column type. Shipping it half-done
would put unresolvable ids in rows — the exact defect `refCell` was written
to stop, reintroduced one layer down
A `total_column` must name a column that exists AND is numeric: a footer summing a text column
would render a number nobody can account for, which is worse than no total.
THE PROPERTY THAT MAKES THIS SAFE TO SHIP IS THE LEGACY STRING. Those 22 fields were
textareas, so live records hold prose where rows are now expected. Rejecting it would make
every one of those records un-saveable the moment somebody edited an unrelated field on them —
a config change breaking data it never touched. Accepting it and dropping it would be worse:
it is the only copy. So `validate_record` accepts a string, and the form renders it above the
grid, verbatim and read-only, until the lines are entered by hand. Mutation-checked: removing
that tolerance fails the test with exactly that message.
Numeric cells store as NUMBERS, not the input's string. That is the third place one root cause
has now surfaced: a number stored as text is what makes SQL comparison lexicographic
('9' >= 10 is TRUE), which is the bug MOD-FILTER's cast exists to survive and the bug the
record form had for `percent` fields.
A HOLE CLOSED AT THE POINT IT WAS CREATED: `isEmpty` returns false when a field has no entry in
`inputs`, and a table is deliberately not registered there (its value is an array of objects,
not an `el.value` string, so every consumer of `inputs` would read it wrongly). A `required`
table with zero rows would therefore have passed the client-side check. None of the eight
shipped tables is required, so this is a hole rather than a live bug — and it is the same
"plausible answer for the missing case" shape that keeps costing this codebase, so it is
handled before the table branch rather than after it.
Applied to 8 fields across 5 registers, each where the artifact genuinely IS a line-item
document: bid_submission.unit_prices + .alternates (bid leveling is DEFINED as a row per scope
item), daily_report.crew_by_trade + .equipment_on_site, incident.witnesses (OSHA 301 asks who
witnessed, with employer and statement), project_charter.key_milestones + .key_deliverables,
and a new estimate.line_items with a G703-shaped column set (code, description, qty, unit,
unit cost, amount).
The register table shows "3 lines · $118,260" rather than "[object Object]" per row — a missing
branch that renders as a plausible-looking bug.
MERGE DEPENDENCY: this adds `table` to `FIELD_TYPES`, and `fieldTypeCoverage.test.ts` (in the
MOD-FILTER PR, not yet merged) asserts every legal type is either editable or deliberately
read-only. `table` belongs in READ_ONLY_FIELD_TYPES — a grid has no cell-sized inline editor.
That gate will correctly fail this branch until the two are sequenced, which is the gate doing
exactly what it was written for one PR earlier.
No version files or CHANGELOG — the release lane batches those.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Rebased onto The gate did its job, and I want to record that it did
Mutation-checked: removing that one entry — the exact pre-classification state — fails 2 of the 6 assertions ( The rebase, and the hunk where "keep both" would have been wrongTwo conflicts in
And the JSDoc-boundary break happened again, exactly as expected: every marker grep passed ( ManifestTook main's side and inserted inside Verification after the rebase
Ready for your merge order (#111 → #112 → #113). #112 and #113 will need the same manifest treatment; both are mine and I'll rebase them as they come up. |
Base
42f73677,git merge-treeCLEAN as of that SHA. Item 2 of the field sweep's "not done" list.The gap
The sweep found 22 places a list had been flattened into one textarea (
bid_submission.unit_prices,daily_report.crew_by_trade,incident.witnesses), and the deeper version of the same thing:sovis one record per line with no parent document, andestimatewas a singleamount.A schedule of values, a bid, an estimate and a daily manpower log are all line-item documents. The config had no way to say so — so
bid_leveling.py, whose entire job is a row-per-scope-item comparison, was parsing prose for numbers a structured grid would have handed it clean.The column vocabulary is a deliberate subset
tablerollupfile/signaturereferencerefCellwas written to stop, one layer downA
total_columnmust name a column that exists and is numeric: a footer summing a text column renders a number nobody can account for, which is worse than no total.What makes it safe to ship: the legacy string
Those 22 fields were textareas, so live records hold prose where rows are now expected.
So
validate_recordaccepts a string, and the form renders it above the grid, verbatim and read-only, until the lines are entered. Mutation-checked — removing the tolerance fails the test with exactly that message.Numeric cells store as numbers
Third place one root cause has surfaced. A number stored as text is what makes SQL comparison lexicographic (
'9' >= 10is TRUE) — the bugMOD-FILTER's cast exists to survive, and the bug the record form had forpercent.A hole closed at the point it was created
isEmptyreturnsfalsewhen a field has no entry ininputs, and a table is deliberately not registered there — its value is an array of objects, not anel.valuestring, so every consumer ofinputswould read it wrongly. Arequiredtable with zero rows would have passed the client check. None of the eight shipped tables is required, so it's a hole rather than a live bug — and it's the same "plausible answer for the missing case" shape, so it's handled before the branch rather than after.Applied to 8 fields across 5 registers
bid_submission.unit_prices+.alternates·daily_report.crew_by_trade+.equipment_on_site·incident.witnesses(OSHA 301 asks who witnessed, with employer and statement) ·project_charter.key_milestones+.key_deliverables· newestimate.line_itemswith a G703-shaped column set.The register table shows
3 lines · $118,260rather than[object Object]per row — a missing branch that renders as a plausible-looking bug.Verification
test_bim_columnspasses with the correctPYTHONPATH(src;../data/src),test_scan_cachepasses solo — DB contention from a concurrent web-suite run.test_module_tablesPASS inside the full suite, not only alone.tsc --noEmitclean, re-run after the rebase — a rebase can break what a marker check cannot see, which is how MOD-FILTER — per-field filters + server-side sort · MOD-PERCENT — the form layer didn't knowpercentexisted #109's resolution passed every grep and failed to compile.This adds
tabletoFIELD_TYPES. #109'sfieldTypeCoverage.test.tswill correctly fail this branch untiltableis added toREAD_ONLY_FIELD_TYPES— a grid has no cell-sized inline editor. I own both branches and will sequence them.That gate was written one PR ago to catch a new type added without a renderer classification, and it caught the very next one. Worth merging #109 first, then this.
🤖 Generated with Claude Code