Skip to content

fix(drawing-2d): resolve override subtypes from the schema, not a drifted table - #3262

Merged
louistrue merged 4 commits into
mainfrom
fix/graphic-override-subtype-table
Aug 25, 2026
Merged

fix(drawing-2d): resolve override subtypes from the schema, not a drifted table#3262
louistrue merged 4 commits into
mainfrom
fix/graphic-override-subtype-table

Conversation

@BIMvoice

Copy link
Copy Markdown
Collaborator

Refs #3260

ifcTypeCriterion defaults includeSubtypes to true, so a graphic-override rule naming a supertype is meant to style everything beneath it. The expansion ran off IFC_TYPE_HIERARCHY, a hand-written table in rule-engine.ts. Derived against the IFC4 (ADD2 TC1) SCHEMA_REGISTRY parent chain:

supertype table resolved schema says missing
IfcBuildingElement 21 31 10
IfcDistributionElement 2 76 74

IfcDistributionElement resolved IfcDistributionFlowElement and IfcDistributionControlElement and stopped, because neither was itself a key — so no duct, pipe, cable, terminal, valve or sensor was ever styled. The elements still drew; they drew without the override the rule asked for, with no warning.

Two further defects in the same table:

  • IfcFlowElement is not an IFC entity. It appeared in exactly one file in the repo — this table. Absent from IFC2X3 (ENTITIES_IFC2X3), IFC4 and IFC4X3, so it was never a legacy alias.
  • Two edges the schema does not have: IfcStairFlight under IfcStair, IfcRampFlight under IfcRamp. IFC4 makes both flights siblings under IfcBuildingElement.

Is the table curated policy, or drift?

Checked before widening. It is drift:

  • The doc comment says only "IFC TYPE HIERARCHY (for subtype matching)" — it states a general purpose, no narrowing rationale.
  • The built-in presets never rely on it. Every preset enumerates leaf types explicitly (ifcTypeCriterion(['IfcDuctSegment', 'IfcDuctFitting'])), including MEP, which would be the obvious beneficiary of a supertype. So the table's only consumer is user-authored rules and the public drawing.ifcTypeCriterion SDK surface — where breadth is the whole point.
  • A curation policy does not invent IfcFlowElement, a name in none of the three bundled schemas.
  • The partial rows settle it: IfcBuildingElement lists IfcWall, IfcSlab, IfcBeam, IfcColumn, IfcDoor, IfcWindow but not IfcCurtainWall or IfcPlate. There is no performance or authoring story under which those eleven are in and IfcCurtainWall is out.

The fix

The table moves to ifc-type-hierarchy.ts as the direct-children map of every entity under IfcElement and IfcSpatialElement, derived from IFC4 ADD2 TC1 — 31 rows covering 146 entities. It had to move: rule-engine.ts sat at exactly its recorded module-size budget (561), so the table could not grow in place. Its row drops 561 → 523; ALLOWLIST_DIGEST is re-pinned in the same commit. No new allowlist row — the new module is 158 lines.

Rather than silently delete the dead key, IfcFlowElement is kept in an explicit AUTHORING_ALIASES map pointing at IfcDistributionFlowElement, the real supertype of the four names it used to list; a rule already naming it keeps working and now reaches the whole flow subtree. The two non-schema stair/ramp edges join it there — narrowing those rules would be its own silent regression. Keeping aliases in a separate map is what lets the parity test hold the derived map to the schema exactly.

getIfcSubtypes now de-duplicates and tracks visited nodes, so an alias pointing back into the table cannot spin. The old version recursed unguarded.

Deriving from SCHEMA_REGISTRY at module load was the obvious alternative and is what several packages do, but drawing-2d depends only on @ifc-lite/geometry; making a published drawing package pull the 63k-line parser registry at runtime to answer a subtype question is a poor trade. The literal table stays, and a test — not a comment — holds it to the schema.

Tests

The module had none. Two suites:

  • rule-engine.test.ts drives rules through the real path (createOverrideEngine().applyOverrides) against named required entities per supertype, not a count floor — a count reds on benign schema growth and stays silent when a name drops out. Includes a positive anti-vacuity control and negative controls (IfcBuildingElement must not reach IfcSpace; IfcWall must not reach IfcSlab) so the matcher cannot pass by matching everything.
  • ifc-type-hierarchy.test.ts re-derives the hierarchy from @ifc-lite/data's ENTITIES_IFC4 — deliberately a different authority from the parser registry the table was generated from, so the two cross-check — and fails if the table omits a schema subtype, lists a name that is not an IFC4 entity, claims an edge the schema lacks, or grows a key outside the covered roots. Guarded against vacuity by asserting the derived universe is non-trivial before any subset check.

@ifc-lite/data is a devDependency; the published bundle is unchanged.

RED on upstream/main: Tests 33 failed | 467 passed (500).
GREEN: Tests 511 passed (511); @ifc-lite/sdk also green (188 passed).

The parity guard was verified to fire rather than assumed: deleting IfcCurtainWall from the table reds with "IfcBuildingElement does not reach: IfcCurtainWall" and "IfcElement does not reach: IfcCurtainWall". The table was then restored by the inverse edit and proved byte-identical with diff.

🤖 Generated with Claude Code

…fted table (#3260)

`ifcTypeCriterion` defaults `includeSubtypes` to true, so a rule naming a
supertype should style everything beneath it. The expansion ran off a
hand-written `IFC_TYPE_HIERARCHY` that had fallen behind the schema:
`IfcBuildingElement` reached 21 of IFC4's 31 subtypes, and
`IfcDistributionElement` reached 2 of 76 because neither of the two names it
listed was itself a key. Elements still drew, just without the override the
rule asked for, and nothing warned.

The table moves to `ifc-type-hierarchy.ts` as the direct-children map of
everything under `IfcElement` and `IfcSpatialElement`, derived from IFC4
ADD2 TC1. `rule-engine.ts` was at its exact module-size budget, so the table
could not grow in place; its row drops 561 -> 523.

`IfcFlowElement` was a key and is not an IFC entity in IFC2X3, IFC4 or
IFC4X3, so it was never a legacy alias. It is kept in an explicit
`AUTHORING_ALIASES` map pointing at the real supertype
`IfcDistributionFlowElement` rather than deleted, since a rule may already
name it. `IfcStair` -> `IfcStairFlight` and `IfcRamp` -> `IfcRampFlight` join
it there: IFC4 makes the flights siblings, and narrowing those rules would be
its own regression.

`getIfcSubtypes` de-duplicates and tracks visited nodes so an alias pointing
back into the table cannot spin.

The module had no tests. Adds a behavioural suite driving rules through
`applyOverrides` against named required entities, and a parity suite that
re-derives the hierarchy from `@ifc-lite/data`'s `ENTITIES_IFC4` — an
authority independent of the parser registry the table was generated from —
failing if the table omits a subtype, invents an entity, or claims an edge
the schema lacks. Verified the guard fires: deleting `IfcCurtainWall` from
the table reds it with "IfcBuildingElement does not reach: IfcCurtainWall".
`@ifc-lite/data` is a devDependency; the published bundle is unchanged.
@BIMvoice
BIMvoice requested a review from louistrue as a code owner August 25, 2026 20:12
@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

This review includes 8 billable files and costs up to $2.00.

Or wait 59 minutes for your next included review.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0e0d6a47-93f7-4ab6-b502-3f7f758196c3

📥 Commits

Reviewing files that changed from the base of the PR and between 38460bd and fa52de9.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • .changeset/graphic-override-subtype-hierarchy.md
  • packages/drawing-2d/package.json
  • packages/drawing-2d/src/graphic-overrides/ifc-type-hierarchy.test.ts
  • packages/drawing-2d/src/graphic-overrides/ifc-type-hierarchy.ts
  • packages/drawing-2d/src/graphic-overrides/rule-engine.test.ts
  • packages/drawing-2d/src/graphic-overrides/rule-engine.ts
  • scripts/check-module-size.mjs
  • scripts/module-size-allowlist.txt

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.

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Viewer benchmark

✅ No threshold regressions detected.

01_Snowdon_Towers_Sample_Structural(1).ifc

Baseline recorded 2026-07-01T20:31:05.538Z on github-actions ubuntu-latest, viewer-benchmark-ci (headless Chrome, SwiftShader ANGLE), production build.

Metric Current Baseline Delta Threshold Status
firstBatchWaitMs 1123ms 2905ms -61.3% +50%
firstVisibleGeometryMs 1489ms 3652ms -59.2% +50%
streamCompleteMs 1987ms 3598ms -44.8% +50%
spatialReadyMs 908ms 1032ms -12.0% +50%
metadataCompleteMs 1210ms 3063ms -60.5% +50%
totalWallClockMs 2100ms 3700ms -43.2% +50%

AC20-FZK-Haus.ifc

Baseline recorded 2026-07-01T20:30:59.972Z on github-actions ubuntu-latest, viewer-benchmark-ci (headless Chrome, SwiftShader ANGLE), production build.

Metric Current Baseline Delta Threshold Status
firstBatchWaitMs 287ms 1075ms -73.3% +50%
firstVisibleGeometryMs 648ms 1572ms -58.8% +50%
streamCompleteMs 927ms 1980ms -53.2% +50%
spatialReadyMs 776ms 915ms -15.2% +50%
metadataCompleteMs 855ms 1392ms -38.6% +50%
totalWallClockMs 1400ms 3300ms -57.6% +50%

Refresh the baseline from a CI run: dispatch the Benchmark workflow with record_baseline, download the benchmark-baseline artifact, and commit baseline.json (see tests/benchmark/README.md).

Conflicted only on scripts/module-size-allowlist.txt and check-module-size.mjs.
Every open PR in this batch re-pinned the digest from the same base value, so
whichever landed first conflicted the rest; #3196 landing on main is what did it
here.

Resolved to main's structure, then re-derived the budgets and the digest from
`node scripts/check-module-size.mjs` rather than hand-picking them, so the
allowlist states measured counts rather than a merge artefact.
@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
ifc-lite-dev Ignored Ignored Preview Aug 25, 2026 11:12pm
ifc-lite-viewer-embed Ignored Ignored Aug 25, 2026 11:12pm

@louistrue

Copy link
Copy Markdown
Collaborator

Merged main in to clear the conflict. No change to this PR's own work — the only conflicting files were scripts/module-size-allowlist.txt and scripts/check-module-size.mjs.

Why it conflicted. Eight PRs in tonight's batch re-pin ALLOWLIST_DIGEST from the same base value 674100036802546405, each to a different result, so they were all cut expecting to be first. #3196 (the STEP export split) landing on main is what conflicted the set: it took three step-* modules under 400, so their allowlist rows are gone on main.

How I resolved it. Took main's structure, then re-derived every number from node scripts/check-module-size.mjs rather than hand-picking, so the file states measured counts rather than a merge artefact. The gate reports the budgets to lower and the digest to pin, and I applied what it said. Verified check-module-size: OK before pushing.

The digest re-pin is now correct for main as of ffe80a76. If another of the eight lands before this one it will conflict again on the same two lines — that is inherent to a single pinned file, not to your change.

Conflicted only on scripts/module-size-allowlist.txt and check-module-size.mjs.
Every open PR in this batch re-pinned the digest from the same base value, so
whichever landed first conflicted the rest; #3196 landing on main is what did it
here.

Resolved to main's structure, then re-derived the budgets and the digest from
`node scripts/check-module-size.mjs` rather than hand-picking them, so the
allowlist states measured counts rather than a merge artefact.
Conflicted only on scripts/module-size-allowlist.txt and check-module-size.mjs.
Every open PR in this batch re-pinned the digest from the same base value, so
whichever landed first conflicted the rest; #3196 landing on main is what did it
here.

Resolved to main's structure, then re-derived the budgets and the digest from
`node scripts/check-module-size.mjs` rather than hand-picking them, so the
allowlist states measured counts rather than a merge artefact.
@louistrue

Copy link
Copy Markdown
Collaborator

CodeRabbit CLI at head: one finding, severity major, and I am rejecting it with evidence.

The finding: remove the non-schema authoring aliases from the exported API — IfcFlowElement is not an IFC EXPRESS entity, and IfcStair/IfcRamp must not match siblings that are not their schema subtypes. Cites AGENTS.md's "never invent aliases".

The guideline is real. The premise is not. The concern is that aliases contaminate the schema-derived table. They cannot, because the two are separate exports and BOTH are pinned:

:56   derives a non-trivial universe from the schema        (anti-vacuity)
:66   resolves every schema subtype of every covered supertype
:80   SUBTYPES_BY_SUPERTYPE lists only real IFC4 entities
:91   ...and claims only edges the schema actually has
:102  ...and stays within the covered roots
:111  AUTHORING_ALIASES are exactly the three documented non-schema conveniences

So the schema half is held to the schema in both directions, and the alias half cannot grow silently — a fourth alias fails :111. That is a stronger arrangement than deleting the aliases would give, because it makes the non-schema names enumerable and asserted rather than absent-and-therefore-untracked.

And the remedy would cause a silent narrowing, which is the failure mode this repo has spent tonight removing. IfcStairFlight and IfcRampFlight are siblings of IfcStair/IfcRamp under IfcBuildingElement, not subtypes. The table has always expanded them that way, so an existing rule written on IfcStair currently covers its flights. Removing the alias would stop it covering them — with no error, no warning, and no test failure. A drawing that used to style stair flights would quietly stop.

IfcFlowElement is the same shape: it is not an entity in IFC2X3, IFC4 or IFC4X3 (the real supertype is IfcDistributionFlowElement), but it shipped as a table key, so rules may already name it. Mapping it keeps those working, and now reaches the whole flow subtree rather than the four names the drifted table had.

No change made. The aliases are deliberate backward compatibility, isolated from the derived table, individually documented with why, and pinned to exactly three.

On the change itself: this is the ninth instance tonight of a hand-kept table standing in for a schema relation, and it is fixed the right way — derive SUBTYPES_BY_SUPERTYPE from the schema, keep the genuinely-non-schema names in a separate named map, and pin both. The drifted table reached four flow names; the derived one reaches the whole subtree.

Verified on the merged tree against current main: @ifc-lite/drawing-2d 39 files, 511 tests, 0 failed. Checked for descendants before merging: none.

@BIMvoice

Copy link
Copy Markdown
Collaborator Author

Adversarial review of #3262 — RED verified and unusually clean; one scope gap worth naming

What I ran (own worktree, branch head, after pnpm install --frozen-lockfile):

pnpm exec turbo run test --filter=@ifc-lite/drawing-2d --force
Test Files  39 passed (39)      Tests  511 passed (511)

(The lockfile change is correct — --frozen-lockfile installs clean and links @ifc-lite/data into packages/drawing-2d/node_modules. My first run failed on a stale install predating the checkout, not on anything in the PR.)

RED verified by inverse edit. Rather than trust the changeset's account of the drift, I reproduced it: deleted the IfcDistributionFlowElement and IfcDistributionControlElement keys (so the walk resolves those two names and stops, exactly as described) and removed the ten missing names from IfcBuildingElement. Tests untouched. The failures read as a precise inventory of the defect:

× a rule on IfcBuildingElement reaches IfcCurtainWall
× a rule on IfcBuildingElement reaches IfcPlate / IfcPlateStandardCase
× a rule on IfcBuildingElement reaches IfcMember / IfcMemberStandardCase
× a rule on IfcBuildingElement reaches IfcFooting / IfcPile
× a rule on IfcBuildingElement reaches IfcBuildingElementProxy / IfcChimney / IfcShadingDevice
× a rule on IfcDistributionElement reaches IfcDuctSegment / IfcDuctFitting
× a rule on IfcDistributionElement reaches IfcPipeSegment / IfcPipeFitting
× a rule on IfcDistributionElement reaches IfcAirTerminal / IfcSanitaryTerminal / IfcOutlet
× a rule on IfcDistributionElement reaches IfcSwitchingDevice / IfcSensor / IfcActuator / IfcAlarm
× a rule on IfcDistributionElement reaches IfcPump / IfcValve / IfcCableCarrierSegment
  ... (25 shown)

Restored, diff byte-identical, tree clean.

This is the best-shaped test suite I have reviewed tonight. One assertion per entity means the failure output is the bug report; the changeset's numbers ("21 of 31", "2 of 76") are checkable rather than asserted; and both controls are present and meaningful — overrideReaches('IfcWall','IfcWall') guards the helper, and IfcBuildingElementIfcSpace proves the matcher is not matching everything. The AUTHORING_ALIASES split is the right call: it keeps SUBTYPES_BY_SUPERTYPE exactly schema-shaped so the parity test can be strict, instead of loosening the test to accommodate three convenience names.


Finding — the guarantee holds for IFC4 only, and the module doc claims more (medium)

 * `ifc-type-hierarchy.test.ts` re-derives the same map from `@ifc-lite/data`'s
 * `ENTITIES_IFC4` and fails if the two disagree, so a schema bump cannot
 * quietly reintroduce the gap.

ENTITIES_IFC4X3 and ENTITIES_IFC2X3 are exported from the same package, right beside ENTITIES_IFC4, and neither is consulted. ifcTypeCriterion matches on element.ifcType, a raw class name from whatever schema the model was authored in. I measured what that costs:

SCRATCH| 4X3 IfcBuiltElement              -> 0 subtypes
SCRATCH| 4X3 IfcEarthworksElement         -> 0 subtypes
SCRATCH| 4X3 IfcFacilityPart              -> 0 subtypes

SCRATCH| IFC4X3 IfcBuiltElement            schema= 35 table=  0 MISSING=35
SCRATCH| IFC4X3 IfcElement                 schema=158 table=136 MISSING=33
SCRATCH| IFC2X3 IfcBuildingElement         schema= 27 table= 31 MISSING=7
SCRATCH|      missing: IfcBuildingElementComponent, IfcBuildingElementPart,
SCRATCH|               IfcReinforcingBar, IfcReinforcingElement, IfcReinforcingMesh,
SCRATCH|               IfcTendon, IfcTendonAnchor
SCRATCH| IFC2X3 IfcDistributionElement     schema= 12 table= 76 MISSING=1
SCRATCH|      missing: IfcElectricDistributionPoint

(scratch file removed; tree clean.)

IfcBuiltElement is IFC4X3's rename of IfcBuildingElement and the canonical name in that schema. A rule written on it today styles nothing, with no warning — the same silent no-op this PR exists to remove, one schema over. (IfcBuildingElement still resolves 31 in IFC4X3, so an IFC4X3 file using the deprecated spelling is fine; it is the canonical spelling that is dead.)

The IFC2X3 gaps are smaller but real, and one of them is live in this repo right now: IfcElectricDistributionPoint is the subject of #3185. The IFC2X3 reinforcing gap is a genuine hierarchy difference, not an omission — IFC2X3 puts those under IfcBuildingElementComponent where IFC4 uses IfcElementComponent — which is precisely why one schema's parent map cannot stand in for another's.

I am not asking for this in the PR; it is a clear improvement as it stands and the table's IFC4 provenance is stated honestly in the changeset. What I would change is the module doc sentence above, which promises a guarantee one schema wide as though it were general, and the changeset, which never mentions that IFC4X3 and IFC2X3 rules still silently under-match. If the intent is IFC4-only for now, saying so costs a line and stops the next reader assuming the drift problem is solved. If you want it closed, COVERED_ROOTS plus a per-schema loop over the three exported tables is a small change to the same test, and AUTHORING_ALIASES already gives the escape hatch for the cross-schema renames.

Question — AUTHORING_ALIASES is pinned to exactly three names

    expect(Object.keys(AUTHORING_ALIASES).sort()).toEqual([
      'IfcFlowElement', 'IfcRamp', 'IfcStair',
    ]);

I like this — it forces a new alias to be deliberate. But it is also the natural place IfcBuiltElement would land if the scope above is ever widened, and this assertion will red when it does. That is the intended behaviour rather than a defect; I mention it only so the failure is recognised as "someone added an alias on purpose" rather than treated as drift.

Cleared

  • Changeset starts with ---, no licence header, and minor is the right bump for a widening that visibly changes existing drawings. The changeset says so explicitly ("it is a visible change to any drawing that used a supertype rule") — that is exactly the honesty this needs.
  • @ifc-lite/data really is devDependency-only; nothing reaches the published bundle. Confirmed in package.json and by the fact that only the test imports it.
  • Both directions plus a third are asserted against the schema: table ⊆ schema entities, table edges ⊆ schema edges, and schema descendants ⊆ table resolution. The "stays within the covered roots" check is the one I would have asked for and it is already there.
  • The anti-vacuity guard is real (IFC4_NAMES.size > 500, COVERED.size > 100, and each root's descendant count > 0), so the four toEqual([]) assertions below it cannot pass over an empty universe.
  • getIfcSubtypes de-duplicates and tracks seen, and getIfcSubtypes('IfcWallStandardCase')[] pins the leaf case. The alias merge cannot cycle: walk skips anything already in seen.
  • IfcFlowElement genuinely is not an entity in any bundled schema — the test asserts IFC4_NAMES.has('IfcFlowElement') === false rather than taking the changeset's word for it. Keeping it as an alias rather than deleting it is the right call for a name users may already have saved in a rule.
  • Cross-PR: this PR re-pins ALLOWLIST_DIGEST in scripts/check-module-size.mjs and edits scripts/module-size-allowlist.txt. fix(viewer): one schema-derived MATERIAL_DEF_TYPES; bare material definitions were dead clicks #3264, fix(data): IfcQuantityNumber was relabelled as a count, silently #3266 and fix(parser): read the schema from FILE_SCHEMA, not from header free text (#3278) #3279 do the same, from different pre-images — four PRs, one digest. Only the first to merge stays consistent; the rest need theirs re-derived or check-module-size reds on main.

louistrue added a commit that referenced this pull request Aug 26, 2026
…lves (#3291) (#3306)

* wip: sharded digest (probe base)

* fix(scripts,tests): shard the module-size digest by scope, both halves

One repo-wide ALLOWLIST_DIGEST made every open PR touching ANY budget conflict
with every other one: they all rewrote the same pinned line, whatever they
changed. The pin is now one entry per packages/<name> / apps/<name> /
rust/<crate>, so PRs in different scopes edit different lines and git merges
them (#3291).

MEASURED BOTH WAYS, with the same two-branch probe. Two branches off one base,
each raising one budget in a different scope and re-pinning:

    old single digest   merge exit 1, CONFLICT (content) in check-module-size.mjs
    sharded             merge exit 0, 0 conflicts

The first run of that probe was wrong and I nearly kept it: the second branch's
sed had not matched, so it committed nothing and the merge succeeded trivially.
A clean merge of an empty branch proves nothing. The numbers above are from the
redo, where both branches change 2 files each.

HOW MUCH IT ACTUALLY BUYS, on the incident the issue measured. The four PRs
touched: #3239 {apps/viewer, packages/export}, #3243 {packages/parser},
#3262 {packages/drawing-2d, packages/parser}, #3264 {apps/viewer}. Of the six
pairs, four become independent and two still collide -- #3239/#3264 share
apps/viewer, #3243/#3262 share packages/parser. So this removes CROSS-scope
coupling, not within-scope, and both allowlists are concentrated: 48% of TS rows
are apps/viewer, 61% of Rust rows are rust/geometry. Worth stating plainly
rather than claiming the class is dead.

BOTH HALVES, and the Rust one is the bigger problem. I assumed TS was the
painful side because that is where last night's batch landed. Counting commits
in the last 200 on main: rust/processing/tests/module_size_allowlist.txt 69,
scripts/module-size-allowlist.txt 8. Deferring the Rust half would have deferred
8.6x the contention, and leaving one twin sharded and the other not is the
drift I flagged on vercel-install.sh earlier today.

The TS/Rust parity test now compares the two SCOPE TABLES rather than two
numbers, with an anti-vacuity guard: a regex that matched nothing would have
given two empty maps and a passing deepEqual.

Verified: 796 JS tests, 5 Rust ratchet tests, clippy -D warnings clean.
Mutations, each reddening only what it should -- collapsing every scope to one
bucket (4 JS tests), disabling the drift check (2), perturbing one Rust scope
pin (names that scope alone), drifting a Rust pin against the JS side (parity).
An orphaned pin -- a scope whose rows all vanished -- is drift too, on both
sides, or deleting a scope leaves a pin describing nothing and the gate stays
silent.

`cargo fmt` on the Rust file also reformatted two hunks I never touched; those
are reverted, per the repo's no-blanket-fmt rule.

* fix(scripts,tests): close the scope-rule gap /simplify found, and retract a bad measurement

THE MEASUREMENT I PUT IN A CODE COMMENT WAS WRONG, and the review caught it.

I wrote that this Rust allowlist is touched "69 of the last 200 commits on main
against 8" for the TS one, and used that to argue the Rust half was the busier
side. `git log -200 -- <path>` limits to 200 commits TOUCHING that path, so it
returned each file's WHOLE history -- and the TS file was three days old while
the Rust one was seven weeks. Counting properly, of the last 200 commits on
main: 6 touch the Rust allowlist, 18 touch the TS one. The TS side is busier by
3x, the opposite of what I claimed.

Sharding both halves is still right, for the reason that always held: two twin
gates where one is sharded and the other is not is the drift I flagged on
vercel-install.sh this morning. The comment now says that instead, and records
the miscount so the next reader does not repeat it.

THE SCOPE RULES COULD DIVERGE SILENTLY. `allowlistScope` (JS) and
`allowlist_scope` (Rust) are the same rule in two languages, and the digest
parity test could not see a divergence: it runs the shared rule only over the
RUST allowlist, which holds ZERO packages/ rows, so that branch is dead on its
only input. Measured -- deleting `"packages"` from the Rust rule left every
digest byte-identical and every gate green.

Closed with the pattern this repo already uses for csv_cell_vectors.json and
unit_scale_vectors.json: a shared JSON fixture both sides read, carrying the
paths production data does not contain. That mutation now fails on the Rust
side, and the mirror mutation fails on the JS side.

The fixture also surfaced a divergence that already existed: for an empty or
leading-slash path JS returned "other" and Rust returned "". Rust's fallback was
unreachable (`str::split` never yields an empty iterator). Reconciled on "other".

SEVEN MORE, each verified by running it:

- the failure's own remedy was impossible: "empty it and run this script to read
  the true values" lands on the guard that rejects an empty object and prints
  nothing else. Now names a placeholder entry, which does print all 37.
- an orphan-only failure said "0 scope(s) disagree" above a list of orphans.
  Counted, and now tested -- that branch cites #3200 and nothing checked it fired.
- `selfText.replace(PIN_RE, nextBlock)` treated `$&` in a scope name as a
  substitution pattern. Replacer function instead.
- the Rust doc omitted `packages/<name>` while the code matched it -- exactly the
  edit that produces the silent divergence above.
- `parse_allowlist()` ran three times per Rust check; once now.
- the depth-3 justification was wrong. Three levels is not "too fine", it is a
  NO-OP: segment 3 is `src` for 307 of 309 TS rows and 65 of 65 Rust rows.
- an unverifiable session anecdote ("eight PRs, 18 conflict resolutions") was
  repeated in four files. Replaced with the checkable numbers: of the batch's
  six PR pairs, four become independent and two still collide on shared scopes.

798 JS tests, 6 Rust ratchet tests, clippy clean, and my own added lines are
rustfmt-clean (the two remaining diffs at :239 and :303 are pre-existing on main,
confirmed by content match, and left alone).

* fix(scripts,tests): state the real residual — adjacent pin lines still conflict

/code-review found the justification claiming more than was measured, for the
second time on this PR. The first was the contention miscount; this is the
merge-conflict claim.

I wrote that the only remaining collision is two PRs in the SAME scope, and
called that "the honest limit of this change". It is not the limit. Git cannot
auto-merge two edits on ADJACENT lines, so two PRs in DIFFERENT scopes whose pin
entries happen to sit next to each other still conflict. Verified with the same
two-branch probe, one budget raised per side, each side confirmed to have
changed exactly one line before merging:

    old scheme, any two scopes      CONFLICT
    same scope                      CONFLICT
    adjacent pin lines              CONFLICT   <- the case I missed
    two or more lines apart         clean

Measured: 36 of the 666 cross-scope pairs in the 37-entry TS table (5.4%), and
5 of 15 (33%) in the 6-entry Rust table. The Rust half is worse for a reason
unrelated to how often it changes -- a smaller table makes adjacency likelier --
which belongs next to the twin-drift argument rather than leaving the impression
that sharding is uniformly effective.

Sharding still takes both from 100%, and the remedy is one line either way. But
"PRs in other scopes are unaffected", which the allowlist header claimed, is
false, and all three copies now say what was measured.

Also dropped the now-dead `allowlistDigest` import, which this diff orphaned
when it replaced both call sites -- `check-lint-ran` was reporting it as a new
no-unused-vars warning.

Everything else in that review came back clean, including the five ways the
ratchet could have been loosened silently, the group/hash injectivity, the
include_str!/readFileSync pair reading the same bytes, every degenerate
--digests input failing closed, and --update reproducing both committed files
byte-for-byte.

798 JS tests, 6 Rust tests, clippy clean, gates green.
@louistrue
louistrue deleted the fix/graphic-override-subtype-table branch August 27, 2026 14:50
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