Skip to content

feat(viewer): list an element's saved parameters in the info panel, read on selection (#35) (RE-207) - #1468

Merged
DrunkOnJava merged 18 commits into
mainfrom
feat/35-viewer-saved-parameters
Nov 6, 2026
Merged

DrunkOnJava merged 18 commits into
mainfrom
feat/35-viewer-saved-parameters

Conversation

@DrunkOnJava

@DrunkOnJava DrunkOnJava commented Nov 5, 2026 •

Copy link
Copy Markdown
Owner

Summary

The viewer's info panel lists a selected element's saved parameters (RE-207): instance, then type, named ones first, nameless ones behind a fold grouped by why they have no name (#255), each row saying where its parameter is defined. They are read for that element only, on selection, by a new savedParameters binding in a worker of its own; the open and every export are unchanged. File Status's Parameters row no longer says the rest are "still to be decoded".

Nothing is requested once the first file is open (Griffin, 2026-11-05, "Keep the claim, no request"): selecting an element, exporting IFC, plan or glTF, and opening the file again fetch nothing, and a page taken offline after it loaded does all of them.

Motivation

The last open acceptance item of #35 that is not blocked: "Viewer info panel displays them in readable form". The CLI, Python and IFC (--revit-property-sets) expose the saved parameters; the viewer showed a count and a sentence that RE-207 had already made wrong.

Changes

  • rvt-wasm/src/lib.rs, rvt-wasm/Cargo.toml, Cargo.lock: savedParameters(bytes, ids, limits) returns JSON text: each entry is serde_json::to_value of the parameter native_element_parameters::read gives, the call rvt-elements makes, with the release and how far it is read. rvt gets unstable-research here, which "adds paths and changes nothing else" (src/research.rs); no file under src/ changes, so no export can move, and arm.sh's src/ gate path does not match rvt-wasm/src/. The result's keys are a 1.0-stable surface (RELEASE.md): release, release_read, release_experimental, releases_read, releases_measured, elements, unresolved_family_slots, instance_parameters_without_value. I chose them to say what they mean (not the research module's "native"): the read range, the measured range (Revit 2015 to 2022 are read experimentally, RE-281), and what could not be listed. ids is a Float64Array.
  • viewer/src/parameters-worker.ts, viewer/src/saved-parameters.ts, viewer/src/wasm-failure.ts (new; the out-of-memory test both workers use), viewer/src/worker.ts, viewer/src/main.ts, viewer/src/styles.css, viewer/index.html, viewer/src/env.d.ts: the panel group and its states (loading, ready, empty, release not read, experimental release, no ElementId, error with Try again, read-when-asked), the page's map of answers, focus and a live region, the status row text, and the no-request mechanics below. The "Not recovered" note for an element with no IFC property set now says "IFC property sets".
  • viewer/tests/parameters-row.spec.ts, viewer/tests/no-network.spec.ts, viewer/tests/worker-failures.spec.ts (now patches the inline worker's blob where it routed a worker URL), viewer/tests/folded-pieces.spec.ts (prints what a read does and what the page keeps on the BIM4LCA office, and fails if the read errors).
  • .github/workflows/viewer-test.yml, .github/workflows/deploy-viewer.yml: after ci(viewer): build the WASM once per run, and test the file Deploy viewer publishes (#1457) #1458, --bin rvt-elements is built beside gen-fixture in the build job, copied into the artifact and checked to run in each leg; Deploy viewer builds and uploads its own artifact for tests and measure, so it carries the binary the same way. A local pnpm test:browser needs rvt-elements on PATH (viewer/README.md); the parameters-row specs skip without it locally and fail in CI.
  • Docs: docs/viewer-api.md, docs/viewer-privacy-posture.md (the CSP change, below), README.md (13 bindings), docs/status.md, docs/support-matrix.json (it also drops the "wall's or floor's type parameters are not joined" caveat RE-256 made stale), docs/evidence.md, docs/export-diagnostics.md, docs/user-guide.md, docs/viewer-build-pipeline.md, docs/maintaining.md, viewer/README.md, and changelog.d/added/35-viewer-saved-parameters.md.

Design

A worker of its own, nothing resident. extract is a pass over the file, so the read is lazy, per selected element. The open's worker keeps no RevitFile and no bytes: a held handle would keep the file's bytes and every partition it inflates for the session, and its reads would hold up the panel (Core Interior's take seconds). Instead the page opens a worker for one element, which reads the File the page already holds, calls the binding, answers and is terminated, or is terminated at once when another element, a storey or a scaffold panel replaces the group. wasm linear memory only grows, so ending the worker gives a read's memory back. A worker the browser ends with no word (no event) is found by a Web Lock it holds, so "Reading" cannot show for ever.

No request once the file is open; the CSP change. The page's worker-src was 'self'; it is now 'self' blob:, and nothing else in the policy moves (docs/viewer-privacy-posture.md says so and why). Both workers (the open's and the reader) are bundled into the page (?worker&inline) and start from blob URLs; the decoder's glue is a static import (the main thread's exports used to import() it, and fetch the wasm, at the first export click). The first open's worker fetches the wasm once, compiles it and hands the module to the page, which keeps it: the next open (another phase, the saved-mesh toggle, the large-model retry), the readers (initSync({ module })) and the main thread's exports (default({ module_or_path }); the main thread does not allow initSync of a large module) start from it. The lazy loads in viewer/src that could run after the open were: import('../pkg/rvt.js') (static now), new Worker(new URL('./worker.ts', ...)) (inline now), the worker's fetch(new URL('../pkg/rvt_bg.wasm', ...)) (the page resolves the URL once, and fetches only at the first open). Opening a gallery demo still fetches the demo, which is an open.

First page load. The page's one script is 999.56 kB (271.47 kB gzipped) against 959.29 kB (262.82 kB) in the build of d1de8adf: 40.27 kB, 8.65 kB gzipped, more, mostly the glue and the two workers it used to fetch later; the built site holds one script, one stylesheet and the wasm.

When a selection is read on its own. A read has taken 0.74 to 1.04 times the open's wasm memory, and the ratio rises with the file (Einhoven 23 MiB, Core Interior 326, the office 910). Nothing above the office has been measured, so a selection is read on its own only while the wasm memory the page holds (the open's, plus the main thread's instance if an export made one) is at most 1 GiB (a quarter of ?memoryCeilingBytes= where that lowers the 4 GiB ceiling), and above that the panel asks first with a button whose copy says clicking may close the tab: a renderer out of memory takes the tab and the model with it, with no error. After a read that ran out of memory, or a worker that went without a word, that file is read only when asked. This replaces the rule that two instances stay inside the ceiling, which extrapolated past what is measured.

What the page keeps. Each element's answer, in a map on the page: at most 64 (?parametersRemembered= lowers it for the test), the least recently selected going first, emptied when a file is opened. Selecting an element again starts no worker. On the office after the test's selections it held 2 answers, 4,654 characters of JSON (the entries are small; the answers there have 8 entries).

Testing

Unit tests are banned in this repository: none added. Measured in Viewer tests run 42125048710, dispatched on head 3146f14e6fcf221adc4f632ac1a5cfa31a3b885d (the merge of main with #1457's changes included), release_wasm=false, on a CI runner: success in all three legs, check-wasm-in-site.sh finding the one wasm sha256 in the site in each. Deploy viewer run 42125109395, dispatched on the same head, on the release (LTO) wasm: success. Its tests job passed all 61 tests of the suite (the whole of it, on the wasm the site publishes), its measure job 6, and check-wasm-in-site.sh found the artifact's one sha256 (d2b60a7e…) in the built site in each of the three jobs that build it; the page's one script is the same size as in the viewer tests' build (999.56 kB, 271.47 kB gzipped). On the office with that wasm a read took 3.3 s in 942 MiB beside the open's 910 MiB, on selection, and the page kept 3 answers, 4,929 characters; the real capped read ran out of memory as in the other run. (deploy is skipped on a branch, by design.) This PR's head is 3146f14e plus documentation (the figures, in the changelog, docs/status.md and docs/viewer-api.md) plus a merge of main as it stood at #1470, whose export changes (#1456, #1470, src/ifc) were not part of either run; the full CI of the ready PR runs the viewer tests on this head. Earlier runs on earlier code are not counted; Deploy viewer 42122627698 and viewer tests 42122623374, on 7663a4f3, each failed once, in the test of a worker ended with no word, which ended a worker that had already answered (a read of Einhoven takes a fraction of a second); the test now holds the file back first.

viewer/tests/parameters-row.spec.ts, in the viewer's leg 1/3 (no new spec file). The oracle is the command line: the test runs rvt-elements on the same file.

Einhoven (2023) Core Interior (2024) BIM4LCA office as built
Compared with rvt-elements, whole entries 37 of 37 tree elements: 28 with parameters (369 entries: 332 instance, 37 type, 8 named) equal, 7 with none show the empty state, 2 have no ElementId in the export (shown as such) 4 elements, IFCSLAB 22756 (4 entries), IFCWALL 22771 (11), IFCCOLUMN 22798 (17), IFCDOOR 80750 (21), equal not compared
A read, time (a CI runner's) median 153 ms, most 210 ms 1.5 to 1.9 s 3.9 s (8 entries, IFCCOVERING 2097832)
That read's wasm memory 17 MiB 275 MiB 943 MiB
The open's worker 23 MiB, 0.7 s in the open call 326 MiB, 17.3 s 910 MiB, 66.6 s
Read over open (memory) 0.74 0.84 1.04
How it was read on selection on selection on selection (no button)
Kept by the page 2 answers, 4,654 characters of JSON

Also in the spec (each passed in 42125048710):

  • A real out-of-memory read. A Chromium whose V8 caps wasm memory at 927 MiB (--wasm-max-mem-pages, the pattern of memory-ceiling-capped.spec.ts) opens the office (910 MiB) and the read (943 MiB) really runs out: the page says "The tab ran out of memory reading the file a second time (unreachable)... read only when asked from now on", and the next element asks. A read of Einhoven cannot fail after its open succeeds (17 MiB against 23), so the office is the only file in CI where a cap separates the two.
  • No request, offline. After the load the page is taken offline and every request is recorded (workers included): three elements are read and equal rvt-elements', an IFC, a plan and a glTF export are made, the file is opened again (the saved-mesh toggle), an element is read again, and no http(s) request was made. no-network.spec.ts runs the same exports and re-open, and a selection, in phases that allow nothing, and fails if the sample has no element to select, if the wasm is fetched to boot the page or more than once.
  • The gate at its edge (a limit 8 MiB over and 8 MiB under Einhoven's measured open memory: read on selection, then asked); the bare-unreachable branch and its copy, and the file read only when asked afterwards; a worker ended with no word, with the file held back four seconds so it is still reading; a read for an element no longer shown ended (another element, a storey), and the shown element's read not started over by selecting it again; an element selected again starting no worker; a copy of the file opened under another name emptying the map; the bound; a read that fails with a panic, with Try again, and focus on the result; the empty state; the Parameters row text; Revit 2013 (the worker's answer replaced) and Revit 2019 experimental (replaced).

Not verified:

  • The release-not-read and experimental-release states. No file of a release outside Revit 2015 to 2027, or of 2015 to 2022, is in the viewer's job, so those two tests replace the worker's answer to the page with the binding's shape for one (release_read false; release_experimental true). They test the page's rendering only. The Rust branches (NATIVE_RELEASES.contains, the measured range) are not exercised by any file.
  • tools/check-local.sh, clippy and tools/ci/check-workflows.py (no local compute; PyYAML is not installed here); CI's fast jobs on this PR are the check. The wasm network-import audit passed in the viewer legs.
  • An ElementId above u32::MAX: the binding reads it, the command line lists such elements as id: null, so nothing compares them.
  • The office is not compared with rvt-elements; only that its read does not error. The gate is held at its edge on Einhoven's open memory; no file between 910 MiB and the 4 GiB ceiling is in the viewer's job, which is why the limit is the measured 1 GiB.
  • The CDP session of no-network.spec.ts may not see worker requests; the offline test, which records every request and fails any, is what covers them.
  • Screen-reader announcements were not heard: the live region's text and the focus move are asserted, not a screen reader's output.

Breaking changes

None. A binding is added (tools/ci/wasm-api.py compat has nothing to flag); no existing binding or output changes. The worker-src of the page's Content-Security-Policy gains blob:.

Related issues

Refs #35 (#255 and the family built-in slots stay open).

…ead on selection (#35) (RE-207)

Selecting an element shows its saved parameters under "Saved parameters": instance, then type, named ones first, built-ins by id behind a fold that says why they have no name (#255). A new binding, savedParameters, reads them for the selected element in a worker of its own that ends with the answer; the open, and so every export, is unchanged. States: loading, ready, empty, release not read, no ElementId, error with retry, and a button for a file that may not fit twice. File Status's Parameters row no longer says the rest are still to be decoded.
…pare entries by content, cover a selection in the no-network spec (#35)

The group was built and never appended, which typecheck caught. savedParameters now makes each entry the way rvt-elements does (serde_json::to_value), and the spec compares entries with their keys in one order. The empty state has a test of its own that looks for an element to show it on, and no-network.spec.ts selects an element and allows only the same-origin assets the parameters worker loads.
viewer-test.yml: main moved the native build into the build job and the legs now take an artifact, so rvt-elements is built beside gen-fixture there, copied into the artifact, and checked to run in each leg. deploy-viewer.yml builds and uploads the artifact its tests and measure jobs download, so it carries rvt-elements the same way.
…office, and list rvt-elements in the build artifact docs (#35)

folded-pieces.spec.ts selects the office's first owner again at the end and prints whether the page read its saved parameters on its own or asked first, the entries, the read's time and wasm memory, and the open worker's. It fails on a read that errors. The artifact descriptions in viewer-build-pipeline.md and maintaining.md now name rvt-elements.
…er tests run 42103334445 (#35)

Einhoven, Core Interior and the BIM4LCA office: read time and wasm memory against the open's, and the office's selection read on its own. States that the ten-times-size estimate understates a read on the office and that the gate is not corrected for it.
@DrunkOnJava DrunkOnJava added ready-when-green Draft whose code is complete: marked ready for review once its fast CI passes and removed ready-when-green Draft whose code is complete: marked ready for review once its fast CI passes labels Nov 5, 2026
… keep each element's answer (#35)

The page read automatically while the ten-times-size estimate plus the open's memory fit the ceiling. On the BIM4LCA office a read took 943 MiB against 616 MiB estimated, 1.04 times the open's worker, so a file of about 166 MiB would have been read with about 2.4 GiB open and 2.5 GiB read at once. The page now reads on selection only while twice the open's wasm memory fits the ceiling, and asks above it. Each element's answer is kept, up to 64 and the least recently selected going first, until a file is opened again, so an element selected twice is read once. tests: the gate at its edge with the ceiling set either side of twice the open's measured memory, an element selected again, a file opened again, and the bound.
…e starts a new map (#35)

Run 42106122918 failed only here: the browser raises no change event when a file input is given the file it already holds, so the page never reopened. The gate test, the bound and the office read passed.
…he page keeps, from viewer tests run 42109657349 (#35)

Einhoven, Core Interior and the BIM4LCA office against rvt-elements and the open's worker, the gate held at its edge, and what the map holds on the office. The gate follows the open's wasm memory, not the ten-times-size estimate, which the office's read overran.
@DrunkOnJava DrunkOnJava added the ready-when-green Draft whose code is complete: marked ready for review once its fast CI passes label Nov 5, 2026
@DrunkOnJava
DrunkOnJava marked this pull request as ready for review November 5, 2026 21:29
@DrunkOnJava DrunkOnJava removed the ready-when-green Draft whose code is complete: marked ready for review once its fast CI passes label Nov 5, 2026
…n only within what is measured, and the panel's states are tested and accessible (#35)

The parameters worker is bundled into the page (?worker&inline, a blob URL; worker-src gains blob:) and starts from the compiled wasm the open's worker hands over with initSync, so a read fetches nothing and an offline page reads. A selection is read on its own only while the page holds at most 1 GiB of wasm memory (the open's and the main thread's), asks first above it with a copy that says clicking may close the tab, and after a read that ran out of memory, or a worker that went without a word (found by a Web Lock it holds), the file is read only when asked. A read for an element no longer shown is ended, and the element being read is not started over. Focus moves to the result, a live region announces, a row says where its parameter is defined, nameless rows are grouped by reason, and Revit 2015 to 2022 are said to be read experimentally. The binding's keys are renamed release_read, release_experimental, releases_read and releases_measured. outOfMemory is shared. tests: no request and offline, the bare unreachable, a worker ended with no word, stray workers, and a real wasm cap on the office.
…s static, the open's worker is inline, and every use starts from the module the first open compiled (#35)

The main thread's exports imported ../pkg/rvt.js and fetched the wasm at the first export click, and opening the file again (another phase, the saved-mesh toggle, the large-model retry) fetched the worker script and the wasm. The glue is now a static import, the open's worker is bundled and starts from a blob URL like the parameters worker, the page resolves the wasm's URL and keeps the module the first open compiled, and the main thread instantiates it with default({module_or_path}), which the main thread allows at any size. tests: no-network.spec.ts clicks an IFC, a plan and a glTF export and reopens the file, and fails on any request; the offline test does the same; worker-failures.spec.ts patches the blob's script instead of routing a worker URL.
… reading (#35)

Run 42122623374 passed every other test of the saved-parameters work, the real wasm cap on the office included, and failed only here: a read of Einhoven takes about a quarter of a second, so the worker had answered before the test ended it.
…capped read and the first page load, from viewer tests run 42125048710 (#35)

Figures from run 42125048710 on head 3146f14: Einhoven, Core Interior and the BIM4LCA office against rvt-elements and the open's worker, a read really running out of memory under a 927 MiB wasm cap on the office, and the page's one script 40.27 kB (8.65 kB gzipped) larger than in the build of d1de8ad, with the glue and both workers inside it.
@DrunkOnJava DrunkOnJava added the ready-when-green Draft whose code is complete: marked ready for review once its fast CI passes label Nov 5, 2026
DrunkOnJava added a commit that referenced this pull request Nov 5, 2026
…its model (#1192) (#1484)

## Summary

W10 of the 1.0 criteria (#1192): `tools/ci/measured_numbers.py`, the
last steps of the PII guard job, fails a number on the README or the
supported profile that has no model of a new "Model index" table in
`docs/reference-models.md` within 300 characters in its own clause, or a
number on a `local only` or `not shared` model without those words
beside that model's name, or an index row that does not say who can
fetch its model. The pages are edited until they pass, and the figures
the second read found stale are updated. One Measure dispatch is
documented as the command that re-runs the numbers on the models anyone
can fetch. The criteria's W10 row records both and stays **not
measured**.

## Motivation

W10 was not measured because nothing checked that a number names its
model (#1192). Several figures rest on models nobody else can fetch
(Snowdon Towers' exports, the MIT house, the 255ribeiro projects,
flowbim's house, the pre-2015 files, the client models), and a reader
could not tell which.

## Changes

- `tools/ci/measured_numbers.py` and
`tools/ci/test_measured_numbers.py`; three steps at the end of
`ci.yml`'s `pii-guard` job (the check, `--refusals`, the failure-first
tests).
- `docs/reference-models.md`: the Model index (19 models, four
fetchability words) and "Re-run the numbers" (the Measure dispatch, what
it covers, what it does not).
- `README.md`, `docs/supported-profile.md`, `docs/evidence.md`,
`docs/support-matrix.json`, `docs/release-1.0-criteria.md`: models
named, `local only` and `not shared` said, eight clauses marked `<!--
w10: reason -->`, the stale saved-mesh figures replaced, and a sentence
on how to read the figures.
- `docs/maintaining.md`: "Numbers on the README and the supported
profile".
- `changelog.d/changed/1192-numbers-name-their-model.md`.

### The rule

Every numeral on the two pages, in a heading as in a cell, is a number
except what its spelling marks as an identifier (an issue, a report, a
hash, a date, a Revit release or other year, a Measure run id, a layout
width, a tool's version, the 1.0 release). It needs a name from the
index within 300 characters in its own clause (between semicolons and
sentence ends, never split inside parentheses; each cell, item and
heading its own unit). `local only` / `not shared` must stand beside
that model's name (within 60 characters and nearer to it than to another
model's name; "both" and "all" reach back). A number that is not a
measurement carries `<!-- w10: reason -->`, which fails without a reason
or with nothing to excuse, and the failure message says so. No
allowlist. A pattern for measurements would miss the next form someone
writes; this fails any new form until it names a model.

What it cannot catch (stated in `docs/maintaining.md`): a number placed
near the wrong model's name, a number that is not true or not current, a
measured count inside a code span that is more than a number, a figure
spelled out in words, and the pages other than these two.

### Second read, and what I changed

- **Stale figures updated, not relabelled.** The BIM4LCA office's 1,294
of 1,709 and RE1's 207 and 148 of 214 (Measure 40192112285, October) are
now run 41850769925's at `e024f03c`, which RE-304 and RE-331 section 4.4
agree on, and which I re-totalled from that run's scorer files: the
office 1,554 of 1,650 exact as built and 1,638 of 1,738 at permit (265
of 269 doors and all 269 windows exact as built, run 41850758987 for the
default export, RE-331 section 4.1), RE1 148 of 228 exact (Architecture
45 of 53, Electrical 31 of 47, Mechanical 65 of 68, Plumbing 7 of 60)
and 228 of 228 within 0.01 ft of Revit's box. The "221 of 269 doors keep
their box" and "1,294 of the 1,352 bodies then drawn" sentences are
gone. **Core Interior: your note said 158 of 158 stands; run 41850769925
scores 138 saved-mesh bodies there (132 doors, 6 windows) and no shading
device, and RE-331 says 138, so the pages now say 138 of 138.** RE-78's
158 counted 20 shading devices; I did not find why they are no longer
scored. Snowdon Towers' 3,332 of 3,736 stays, labelled as RE-78's local
run not re-run since.
- `profile` 8 files' ElementIds: now the one Revit-side check from #421
(2026-11-04), with its counts; the other 7 are not checked.
- 1,030 and 724: cited to #446, not RE-70 to RE-74 (also on
`docs/evidence.md`).
- `--large-model`: the source's presets (2 GiB per stream, 8 GiB inflate
on 64-bit), marked.
- Beams and landings hold on 28 Revit 2025 models, arcs on 25 (README,
profile). `docs/compatibility.md`, `docs/evidence.md` line 15 and
`docs/support-matrix.json` lines 395, 422 and 440 still say 25 for the
beam and landing claims; outside the two pages, not changed.
- 923 of 942 Snowdon beams: the fetched samples (run 41787230413, RE-328
section 3), no longer local only.
- Index rows: the MIT house (the course page serves it, no script
fetches it), 255ribeiro (GitHub repository, no script fetches it),
Vitoria (`ALD26 Before` is spent and measured in full, the rest held
out); W10 row "none was cut"; fork dispatch and `-f ifc_flags=` figures
stated as untried and not covered; the "nobody has re-run and compared"
caveat is on the README and the profile.
- A pull request that moves a figure edits the page:
`docs/maintaining.md`.
- Check: parentheses, headings, proximity, local-only nearness, the
marker way out in the message, steps last.

### Numbers that could not be traced to a run, or only weakly

- **Snowdon Towers' 1,030 of 1,078 walls and 724 of 1,078 ends:** only
in #446's description (a local run, 2026-09-27) and `docs/evidence.md`;
kept, labelled "not re-run since". If you want them gone it is one
phrase in the Walls row.
- **Client-model counts:** #421 (comments of 2026-10-28, 2026-11-02,
2026-11-03, 2026-11-04) and #1075 (2026-11-02); the reporter's counts on
files nobody else has.
- **Snowdon Towers' 3,332 of 3,736 within 0.01 ft:** RE-78 (a local run,
not re-run since); CI cannot re-measure it.
- Spelled-out counts are not read; I named the models behind those in
the By-input table and the README by hand.

## Testing

Unit tests are banned here; the one exception is a pure helper, and
`test_measured_numbers.py` is that (35 cases, failure-first, no file or
network I/O; the first 28 are unchanged).

- [x] No Rust changed; `tools/check-local.sh` N/A (CI is the build)
- [x] End to end on the real pages: `python3
tools/ci/measured_numbers.py` passes (255 of 271 numbers name a model, 8
clauses marked). On the pages as they were at `f009976e`, the first form
of the check failed 51 clauses.
- [x] `python3 tools/ci/measured_numbers.py --refusals` copies the pages
and the index to a scratch directory, appends or breaks one thing each
time, and the check refuses it for the stated reason: a number with no
model, a local-only model that does not say so, a local-only word beside
another model's name, a number far from its model in a long clause, a
number in a heading, a marker with no reason, a marker with nothing to
excuse, an index row with no fetchability. It runs in CI. A scratch copy
with ", and 4,321 more beams" added to the 3,592-character Properties
clause also fails.
- [x] 22 mutations of the script (longest name, flag surviving its unit,
stale marker, no reason required, years, thousands comma, link targets,
hashes, heading rule, headings skipped, name boundary, any word
flagging, flag to every nearby name, unbounded flag gap, "both" and
"all" ignored, unbounded proximity, split inside parentheses, glued
unit, number-only code span, `2D`, numberless clause needing a flag,
message without the marker way out), each run against the tests in a
scratch copy: all fail them.
- [x] `changelog.py check`, `api-doc-ids.py`, `readme-matrix.py
--check`, `wasm-api.py docs`, `render-mcp-tools.py --check`,
`check-workflows.py` (PyYAML in a scratch venv, 0 errors) pass; the
matrix is valid JSON; the script and tests also pass on Python 3.11 and
3.12.
- Not run: `actionlint` (the environment's command filter refused it)
and CI itself, the first run of the new steps on a runner.
- Not measured: that the re-run command reproduces the pages' figures.
Nobody has compared; the dispatch is the one the cited runs were made
with. I did not dispatch Measure.

## Audit-honesty checklist

- [x] README / docs reflect the current state after this PR
- [x] Changelog fragment under `changelog.d/changed/`
- [x] No new limitation beyond those stated above
- [x] No emojis; no private paths or personal email addresses in
committed files
- N/A: no reverse-engineering finding

## Legal and contribution hygiene

- [x] Apache-2.0; no Autodesk source; no fixtures added

## Breaking changes

None for users. A pull request that adds a number to the README or the
supported profile without a model fails the PII guard job
(`docs/maintaining.md`). Open pull requests that touch these pages pass
their own CI until their next run after this merges, and #1468 (12
bindings becomes 13) will meet the marker at README line 85.

## Related issues

Refs #1192. Not `Closes`: the third done-when item is the maintainer
setting W10's bar, and the row's status records the result without
moving *proposed*.
@DrunkOnJava DrunkOnJava removed the ready-when-green Draft whose code is complete: marked ready for review once its fast CI passes label Nov 6, 2026
@DrunkOnJava
DrunkOnJava enabled auto-merge (squash) November 6, 2026 00:14
@DrunkOnJava
DrunkOnJava merged commit 0dd3ad9 into main Nov 6, 2026
38 checks passed
@DrunkOnJava
DrunkOnJava deleted the feat/35-viewer-saved-parameters branch November 6, 2026 00:54
DrunkOnJava added a commit to puzzbobb/rvt-rs that referenced this pull request Nov 6, 2026
DrunkOnJava added a commit that referenced this pull request Nov 6, 2026
#1499) (#1504)

## Summary

CodeQL's `analyze` job retries a failed SARIF upload once, in a job of
its own, with the same shape #1461 gave Scorecard and zizmor. A real
rejection of the file still fails the run.

## Motivation

#1499: `Analyze (rust)` failed on #1468 (CodeQL run 42128776536) at
`github/codeql-action/analyze` with the log ending at "Uploading
results" and no finding. `arm.sh` refuses a head with a red check, so
the landing waited for a hand rerun.

## Changes

`.github/workflows/codeql.yml` and `docs/maintaining.md`; the matrix
stays the four languages and the check names `Analyze (<language>)` do
not change.

- `analyze` gets `upload: failure-only` and `output: ${{ runner.temp
}}/codeql-sarif`: it writes the SARIF, with the category already in it
(analyze passes `category` to the CLI as the SARIF category), and does
not upload it. At the pinned commit (v4.39.1, `analyze-action.ts`)
analyze post-processes and writes the files for every value of `upload`,
and sends them only for `always`; the one-category-per-job sentinel is
set only by an upload (`validateUniqueCategory`, inside
`uploadPostProcessedFiles`), so `failure-only` leaves the later upload
step free of it. It is `failure-only` and not `never` because `never`
makes `init-action-post-helper.ts` (`prepareFailedSarif`) skip the
failed-run SARIF when the analysis itself dies, and analyze's
`action.yml` calls `never` not recommended. It still saves the overlay
base database on `main`.
- A step of its own, `Upload the results to code scanning`
(`upload-sarif`, same category, `continue-on-error: true`, waits for
processing as analyze did), uploads that directory. Its failure does not
fail the leg.
- `Keep the SARIF for the retry` runs only when that step failed and
uploads the directory as the artifact `codeql-sarif-<language>` (7 days,
`overwrite: true` so that "Re-run all jobs" can replace the one an
earlier attempt left; an artifact name is taken once per run). A healthy
leg leaves no artifact.
- `analyze-retry` (new job, `needs: analyze`, runs when `analyze`
succeeded; skipped on a draft and when a leg really failed) downloads
`codeql-sarif-*` into one directory (`merge-multiple`, since a single
match is otherwise not put in a directory of its own) and, for each
language whose SARIF is there, uploads it once more with that language's
category, the matrix input the leg has (`{"language": "<language>"}`, as
`toJson(matrix)` writes it) and `CODEQL_ACTION_ANALYSIS_KEY:
.github/workflows/codeql.yml:analyze`, so the retry is the same
configuration in code scanning and leaves no second one holding alerts.
The steps are one per language, each `if: ${{ !cancelled() &&
hashFiles('retry/<codeql language>.sarif') != '' }}`
(javascript-typescript's file is `javascript.sarif`): a rejected file
fails its step and the job, and the next language is still tried.
- The job's last step, `Fail on a SARIF no step above uploads`, fails on
a kept `retry/<name>.sarif` that no `sarif_file:` of the workflow names,
so a language added to `analyze`'s matrix and not to the retry cannot
pass with its kept results unsent. It reads the steps from the job's own
checkout, not from a second list of names.
- The retry job holds `contents: read` and `security-events: write`. It
does not hold `actions: read`: with the analysis key set, the only call
`upload-sarif` makes about the run (`GET
/repos/:owner/:repo/actions/runs/:run_id`, `getWorkflowRelativePath` in
`api-client.ts`) is not made, and the other `actions` API call in the
action's source is the cache listing `analyze` uses.
- Why a job of its own: codeql-action's `upload-lib.ts` exports a
sentinel into the job's `GITHUB_ENV` before it sends anything and
refuses a second upload of one category in a job ("only one run of the
codeql/analyze or codeql/upload-sarif actions is allowed per job per
tool/category"), so a second step would fail at once whatever the first
died of. Shown by #1461 (run 42096477400).
- Why the artifacts say which leg failed: a matrix job's outputs are one
set shared by its legs (the last to finish sets them), so a leg cannot
report its own failure through them. The cost is one job on each
non-draft run that does a checkout and an empty download when nothing
failed.
- `docs/maintaining.md`'s CodeQL bullet names the retry job.
- The SHA pins are the ones the file and #1461 already use
(`upload-sarif` at the pinned v4.39.1, `upload-artifact` v7.0.2,
`download-artifact` v8, as in `scorecard.yml`).

## Left alone

- The retry covers a short outage, as #1461's do: a new job starts
seconds after the first fails. It does not cover a long one, or a file
code scanning rejects (a 422), which fail both and fail the run. The
cause of #1468's failure was not recorded in the log (it ended at
"Uploading results"), so that it was a transport failure is an
inference, as it was for #1461.
- On a pull request the retried upload skips the post-filter that drops
alerts outside the diff: `filterAlertsByDiffRange` (in
`postProcessSarifFiles`, `upload-lib.ts`) reads the diff-ranges file
from the leg's own `RUNNER_TEMP`, which the retry job does not have,
while the first attempt, in the leg's job, applies it. The query-time
`restrictAlertsTo` of the diff-informed analysis still narrows the
results, so the retried file is the one the analysis wrote, not the one
the post-filter would have trimmed further.
- A re-run of the run (all jobs) can find an artifact an earlier attempt
left and upload that file again; the same commit and category replace
it.

## Testing

Unit tests are banned in this repository: do not add them. Show the
change working end to end on real files instead.

- [ ] `tools/check-local.sh` passes. N/A: CI-only change; no local
compute on this lane.
- [ ] Verified end to end on real Revit files. N/A: no Rust or reader
change.
- [ ] If this touches the IFC exporter. N/A.
- [ ] If this is a performance change. N/A.

On this head (`4837ed23`):

- `actionlint` exits 0. `python3 tools/ci/check-workflows.py`: 0 errors,
0 warnings (66 jobs run steps in 17 workflows, the new job has its
timeout). `zizmor` 1.32.0 offline over `.github`: 45 findings (2
ignored, 37 suppressed, 6 medium: the Dependabot 3-day cooldown), the
same count as `origin/main`; at the pedantic persona the retry job has
no finding. The first version of the retry steps passed the matrix input
as `toJson(fromJSON('{...}'))`, which zizmor reports as obfuscation, so
it is written out.
- First proof, on the first version (`upload: never`, `actions: read`
held, a list of file names in the last step; run 42130620963, scratch
branch, a copy of this workflow with the trigger `push` on that branch,
the matrix cut to `actions` and `python`, and in the first upload of
both legs `CODEQL_ACTION_ANALYSIS_KEY` set, so that the action needs no
API call before it sends the file, and `token` set to a refused one for
the `actions` leg only): the `actions` leg's upload logged
`Post-processing sarif files`, `Uploading results`, then `Bad
credentials`, failed, and the job stayed green; `codeql-sarif-actions`
was kept; `python`'s upload succeeded and left no artifact; the retry
job warned, uploaded `retry/actions.sarif` (`Successfully uploaded
results`, `Analysis upload status is complete`) and skipped the other
languages; code scanning listed the retried analysis with category
`/language:actions`, analysis key `.github/workflows/codeql.yml:analyze`
and environment `{"language":"actions"}`, the same three values `main`'s
`actions` analysis has.
- Second proof, on the changes of the second read (`upload:
failure-only`, no `actions: read`, `!cancelled()`; run 42133787781,
scratch branch, the same copy with the matrix `actions`,
`javascript-typescript` and `python`, every first upload given the
refused token, the retry of `actions` given a refused token as well, and
the python retry step removed): all three legs' uploads failed with `Bad
credentials` after `Uploading results` (so `analyze` under
`failure-only` had not uploaded, and set no sentinel) and the jobs
stayed green; the three artifacts were kept; the retry job (permissions
`contents: read` and `security-events: write` only) downloaded the
three, warned for each, failed the `actions` retry step on the refused
token, still ran the `javascript-typescript` retry step, which uploaded
(`Successfully uploaded results`, `Analysis upload status is complete`),
skipped rust, and the job ended `failure`, so the run did. That is a
real rejection staying red with the next language still tried, and the
upload working without `actions: read`.
- That run's last step was the first version of it, which listed the
file names and so passed for the python SARIF whose upload step I had
removed; it now reads the steps from the workflow. Its script, taken
from the workflow, run with bash on fake kept files: no file exits 0;
`actions`, `javascript`, `python` and `rust` exits 0; a kept `go.sarif`
exits 1 with the error naming it; the workflow with the python step's
`sarif_file` renamed and a kept `python.sarif` exits 1. Not run on a
runner.
- Both scratch runs' analyses on their refs, their artifacts and the two
scratch branches were deleted afterwards (their commits were never on
this branch).
- Not exercised: the real workflow's own run. CodeQL runs on this pull
request once it is ready, so the first run of `upload: failure-only`
followed by the upload step, the retry job finding nothing, and all four
languages on the real file is the CI run of this head; a retry of `rust`
(the shape is the others', with `rust.sarif`'s name taken from a run's
log); `overwrite` on a re-run.

## Audit-honesty checklist

- [x] README / docs reflect the current state after this PR
(`docs/maintaining.md`, and the workflow's header comment says what the
retry is and why)
- [ ] A user-visible change has its entry as a fragment under
`changelog.d/<section>/`. N/A: CI only, as agreed for #1457.
- [ ] If a limitation was resolved, the "Known limitations" section is
updated. N/A.
- [x] If a new limitation was introduced, it is documented in the PR
body (Left alone)
- [x] Commit messages describe WHAT changed and WHY
- [x] No emojis in code, docs, or commit messages
- [x] No private paths, personal email addresses, or internal jargon in
committed files
- [ ] Reverse-engineering finding. N/A.

## Legal and contribution hygiene

- [x] I agree this work is licensable under Apache-2.0
- [x] This PR contains no Autodesk proprietary source, decompiled
internals, or NDA'd SDK content
- [x] Any new test fixtures use synthetic values

## Breaking changes

None.

## Screenshots or output samples

See Testing.

## Related issues

Closes #1499
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