Description
Design: a measurement is a plain decimal — one number grammar for every measurement string
Design doc: files/designs/code-measurement-grammar.md in the keep-the-four-repos-healthy goal workspace (design architect pass 25, 2026-10-31; audited against origin/main bd137984).
Problem
The PM stack turns strings into measurements two ways, and neither states its grammar, so each parser inherits the grammar of its primitive by accident:
- Coercion gates run field values through bare
Number()— a language-literal parser that accepts""(→ 0), whitespace, hex/binary/octal ("0x10"→ 16) and scientific ("1e2"→ 100). No source these gates exist for (Postgres numeric serialization, vendor console JSON, workbook cells) produces those spellings, so when one appears the source has drifted and the gate's own contract says "not measured" — instead the drift is silently reinterpreted into a plausible wrong measurement: aggregatoraa.tsnum()(aggregator(aa): num() reinterprets hex/binary/octal strings as measurements — a drifted '0x10' raw_score renders as intelligence 16 instead of 'not measured' #2985), cloudflare-export (cloudflare-export: num() reinterprets empty, hex and scientific strings as real measurements — a drifted '' request count exports as a measured 0 #2791), supabase-exportparseDbSize/parseRequestCount(supabase-export: parseDbSize/parseRequestCount accept empty, hex, scientific and fractional strings as real measurements — Number("") reads a drifted db-size row as a measured 0-byte database #2743). - Prose extraction pulls counts from human-written usage bullets with bare
\d+, which cannot cross a comma: grouped counts truncate to their last digit group (1,121 secrets→ 121,1,000 projects→ 0) or drop the whole metric line — and the wrong figures persist as snapshot history (aggregator(bitwarden): the new bws count extractor truncates comma-grouped counts — '1,121 secrets' records 121 and '1,000 projects' records 0, and the wrong figures flow into snapshot records #2997, aggregator(saas-extract): Cloudflare, Turso and GitHub count patterns still use bare \d+ — grouped counts truncate (1,234 → 234) or drop the whole metric #3000).
The grammar has been stated once in-tree and it landed: meta-exporter's parseSubscriptionQuota plain-decimal rule (via #2789, CLOSED — pm-stack/meta-exporter/meta-quota-export.mjs:69-80). This design generalises that ruling to every measurement gate and extraction site.
The grammar (recorded)
- Field form (a value that IS the measurement): plain decimal only — optional sign where the gate's domain already admits negatives, digits, optional single fraction. Empty / padded / radix-prefixed / scientific / separator spellings are not a measurement: the gate returns its existing not-measured outcome (
null; the exporters' fail-closed error), never a reinterpreted number. - Prose form (a count in a bullet): capture the grouped form (
[\d,]+, strip commas — the house idiom indns.ts parsePriceUsd,snapshot-records.ts:177,allowance()), in every position; a grouped figure that cannot match makes the metric absent, never truncated. - Pins for the origins' open questions: scientific spellings REFUSED in both forms (no named source emits them; the landed meta-exporter gate refuses them); whitespace padding REFUSED (drift, per the same precedent — aggregator(aa): num() reinterprets hex/binary/octal strings as measurements — a drifted '0x10' raw_score renders as intelligence 16 instead of 'not measured' #2985's padded case returns
null); grouping well-formedness is stripped, not validated (all three correct in-tree embodiments strip; strict grouping considered and rejected in the doc). - Per-app copies with one shared probe matrix (independent deploys — the design(path-decode): one decode for path segments — a malformed percent-encoding is a 400, never a 500 #2747/Design: env numbers are arguments — one strict parse for boot-time configuration #2835 rule). Ranges/kind rules stay with their gates; already-typed numbers keep their existing finite checks.
Phases (each independently landable)
- Phase 1 (ADOPTED origin): aggregator(aa): num() reinterprets hex/binary/octal strings as measurements — a drifted '0x10' raw_score renders as intelligence 16 instead of 'not measured' #2985 —
aa.tsnum()enforces the field form + direct tests. - Phase 2 (ADOPTED origins): aggregator(bitwarden): the new bws count extractor truncates comma-grouped counts — '1,121 secrets' records 121 and '1,000 projects' records 0, and the wrong figures flow into snapshot records #2997 + aggregator(saas-extract): Cloudflare, Turso and GitHub count patterns still use bare \d+ — grouped counts truncate (1,234 → 234) or drop the whole metric #3000 — every
saas-extract.tscount pattern captures the grouped form; includes the Modal comma-dollar sibling aggregator(bitwarden): the new bws count extractor truncates comma-grouped counts — '1,121 secrets' records 121 and '1,000 projects' records 0, and the wrong figures flow into snapshot records #2997 names (fix here, not a follow-up — same file, same defect). - Phase 3 (ADOPTED origins cloudflare-export: num() reinterprets empty, hex and scientific strings as real measurements — a drifted '' request count exports as a measured 0 #2791 + supabase-export: parseDbSize/parseRequestCount accept empty, hex, scientific and fractional strings as real measurements — Number("") reads a drifted db-size row as a measured 0-byte database #2743 + a new closing audit issue) — exporter gates enforce the field form; the audit greps the whole measurement surface for remaining bare
Number(string coercions and bare\d+extractions, dispositions each, verifies meta-exporter: parseSubscriptionQuota's num() reinterprets empty, hex and scientific strings as real measurements — a drifted '' used value exports as a measured 0% window #2789's embodiment against the grammar, and writes the grammar into the aggregator README.
Boundaries
- design(aggregator): ingest, fail-closed — one validation contract for every document the PM stack consumes #2576 is the layer above (validation outcomes at document boundaries); its
validate.ts(aggregator: Phase 1 of #2576 — validate.ts primitives, the ingest contract in the README, and the malformed-input probe matrix #2577) has no string→number grammar — this design is that missing layer, and composes rather than re-scoping design(aggregator): ingest, fail-closed — one validation contract for every document the PM stack consumes #2576's phases. Ifvalidate.tshas landed when the aggregator phases start, the grammar helper belongs in it; this design does not gate on design(aggregator): ingest, fail-closed — one validation contract for every document the PM stack consumes #2576 either way. - Design: env numbers are arguments — one strict parse for boot-time configuration #2835 owns boot-time env numbers (arguments: fail at boot). This design owns measurement strings (data: read as not-measured). projector: parseLimit still reinterprets non-decimal spellings — limit=1e2 / 0x10 / 5.0 / +5 are honoured instead of falling back to the default (#1547 residual) #2971 (projector
parseLimit, request-time) is named in the doc as an open ownership question between the two — not claimed here. - meta-exporter: parseBillingBanner accepts fractional minor units — 1050.5 exports as $10.505 of free credits instead of failing closed on shape drift #2510 (fractional minor units) stays design(aggregator): ingest, fail-closed — one validation contract for every document the PM stack consumes #2576's recorded exporter question — a kind rule, explicitly out.
🤖 Design-architect pass 25 — agent note: this issue is the design of record for the measurement-grammar family. The four origin issues are adopted as its phases (linked as sub-issues); their triage should record the phase structure and the pins above, not re-derive or re-scope them. Flag genuine blockers here.
Acceptance criteria
- The measurement grammar recorded in this issue (field form and prose form, with the scientific/padded-spelling pins) is the design of record for the family: each adopted phase issue records the phase structure and pins in its triage instead of re-deriving or re-scoping them.
- Phase 1 lands via aggregator(aa): num() reinterprets hex/binary/octal strings as measurements — a drifted '0x10' raw_score renders as intelligence 16 instead of 'not measured' #2985 (
aa.tsnum()enforces the field form, with direct tests for empty/padded/radix/scientific spellings returning the not-measured outcome). - Phase 2 lands via aggregator(bitwarden): the new bws count extractor truncates comma-grouped counts — '1,121 secrets' records 121 and '1,000 projects' records 0, and the wrong figures flow into snapshot records #2997 and aggregator(saas-extract): Cloudflare, Turso and GitHub count patterns still use bare \d+ — grouped counts truncate (1,234 → 234) or drop the whole metric #3000 (every
saas-extract.tscount pattern captures the grouped form; the Modal comma-dollar sibling named by aggregator(bitwarden): the new bws count extractor truncates comma-grouped counts — '1,121 secrets' records 121 and '1,000 projects' records 0, and the wrong figures flow into snapshot records #2997 is fixed in the same pass). - Phase 3 lands via cloudflare-export: num() reinterprets empty, hex and scientific strings as real measurements — a drifted '' request count exports as a measured 0 #2791, supabase-export: parseDbSize/parseRequestCount accept empty, hex, scientific and fractional strings as real measurements — Number("") reads a drifted db-size row as a measured 0-byte database #2743, and the closing audit tracked by Phase 3 (audit) of #3005 — closing inventory audit for the measurement grammar + contract statement #3006 (exporter gates enforce the field form; the audit dispositions every remaining bare
Number(string coercion and bare\d+extraction on the measurement surface, verifies the meta-exporter: parseSubscriptionQuota's num() reinterprets empty, hex and scientific strings as real measurements — a drifted '' used value exports as a measured 0% window #2789 embodiment against the grammar, and writes the grammar into the aggregator README). - No phase reopens the boundaries recorded above: design(aggregator): ingest, fail-closed — one validation contract for every document the PM stack consumes #2576/aggregator: Phase 1 of #2576 — validate.ts primitives, the ingest contract in the README, and the malformed-input probe matrix #2577 own validation outcomes at document boundaries, Design: env numbers are arguments — one strict parse for boot-time configuration #2835 owns boot-time env numbers, meta-exporter: parseBillingBanner accepts fractional minor units — 1050.5 exports as $10.505 of free credits instead of failing closed on shape drift #2510 stays an exporter kind-rule question, and projector: parseLimit still reinterprets non-decimal spellings — limit=1e2 / 0x10 / 5.0 / +5 are honoured instead of falling back to the default (#1547 residual) #2971 ownership is decided explicitly rather than claimed by default.
- This epic closes when all three phases have landed and the Phase 3 audit reports no undispositioned measurement-string coercions or extractions.
Activity
dezren39 commentedon Oct 31, 2026
🤖 Design-architect pass 25 (2026-10-31) — agent note: this design was originated this pass from the measurement-grammar family filed across 2026-10-30/31 (#2985, #2997, #3000, #2791, #2743), grounded in the one landed embodiment (meta-exporter's plain-decimal gate via #2789, CLOSED). Design doc:
files/designs/code-measurement-grammar.mdin the keep-the-four-repos-healthy goal workspace. The four origins are adopted as phases and linked as sub-issues; a new closing-audit issue (#3006) completes Phase 3. Register entry added infiles/potential-projects.md(status: proposed). Nouser/review: every origin carries explicit acceptance, and the open questions they left (scientific spellings, whitespace padding, grouping strictness, the Modal sibling) are pinned in the doc as the architect's recorded decisions.5 remaining items
developing-today commentedon Nov 3, 2026
agent-note:v1
state: triaged (coverage pass)
Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 40 (priority/normal). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
Found via: coverage pass 2026-11-03
dezren39 commentedon Nov 3, 2026
🤖 Agent note · handed off
opencodedezren39 commentedon Nov 3, 2026
🤖 Agent note · handed off
dezren39 commentedon Nov 4, 2026
🤖 Agent note · picked up
dezren39 commentedon Nov 4, 2026
Coverage triage — 2026-11-04
Verified against current origin/main e81e202: pm-stack/supabase-export/supabase-quota-export.mjs still converts string measurements with Number() in parseDbSize and parseRequestCount. The defect is still live and already scoped to Phase 3 issue #2743.
Current family state: #2985, #2997, #3000 and #2791 are closed; #2743 remains open with implementation PR #5183 open; #3006 remains open and parked, with its audit gated on #2743 closing. No direct PR or superseding work for this parent was found.
Verdict: keep #3005 open as the design-of-record. No further split or follow-up task is needed: let #2743 land, then resume #3006 to finish the inventory/contract audit; close #3005 only after that audit is complete. This lane made no changes to PR #5183.
🤖 Agent note — triaged (task 8175, lane aea12c82-c5af-4e5a-9b66-f8e360f87177)