@plasius/gpu-demo-viewer 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -18,6 +18,36 @@ The format is based on Keep a Changelog and this project adheres to SemVer.
18
18
  - **Security**
19
19
  - (placeholder)
20
20
 
21
+ ## [0.5.0] - 2026-10-27
22
+
23
+ - Keep quality controls available after resource/device failures and add explicit scene retry with the latest settings.
24
+
25
+ - **Added**
26
+ - Static 50/100-astronaut grove studies using shared geometry, trees and complete shrub groups; typed dense layout definitions and a separate integrity-pinned, attributed benchmark source.
27
+
28
+ - **Changed**
29
+ - Report requested GPU resource storage and distinct instance/source counts; deduplicate repeated source credits while preserving placement counts and the original catalogue rollout.
30
+
31
+ - **Fixed**
32
+ - (placeholder)
33
+
34
+ - **Security**
35
+ - (placeholder)
36
+
37
+ ## [0.4.1] - 2026-10-23
38
+
39
+ - **Added**
40
+ - Approved unsigned legal templates and security/contribution policies in the npm artifact, with fail-closed compliance checks, pinned template hashes and regression tests.
41
+
42
+ - **Changed**
43
+ - (placeholder)
44
+
45
+ - **Fixed**
46
+ - (placeholder)
47
+
48
+ - **Security**
49
+ - (placeholder)
50
+
21
51
  ## [0.4.0] - 2026-10-21
22
52
 
23
53
  - **Added**
@@ -368,3 +398,5 @@ The format is based on Keep a Changelog and this project adheres to SemVer.
368
398
  [0.2.3]: https://github.com/Plasius-LTD/gpu-demo-viewer/releases/tag/v0.2.3
369
399
  [0.3.0]: https://github.com/Plasius-LTD/gpu-demo-viewer/releases/tag/v0.3.0
370
400
  [0.4.0]: https://github.com/Plasius-LTD/gpu-demo-viewer/releases/tag/v0.4.0
401
+ [0.4.1]: https://github.com/Plasius-LTD/gpu-demo-viewer/releases/tag/v0.4.1
402
+ [0.5.0]: https://github.com/Plasius-LTD/gpu-demo-viewer/releases/tag/v0.5.0
@@ -0,0 +1,32 @@
1
+ # Contributing to @plasius/gpu-demo-viewer
2
+
3
+ This workspace utility exists to accelerate validation across the Plasius
4
+ `gpu-*` packages.
5
+
6
+ ## Local Development
7
+
8
+ ### Prerequisites
9
+
10
+ - Node.js 24 (`nvm use`)
11
+ - npm
12
+
13
+ ### Validate
14
+
15
+ ```bash
16
+ npm run lint
17
+ npm test
18
+ ```
19
+
20
+ ### Demo
21
+
22
+ ```bash
23
+ npm run demo
24
+ ```
25
+
26
+ Then open `http://localhost:8000/gpu-demo-viewer/`.
27
+
28
+ ## Expectations
29
+
30
+ - Keep the viewer aligned with the real package demos; do not let the manifest drift.
31
+ - Add or update tests when browser target paths or shared showcase assets change.
32
+ - Update `README.md` and `CHANGELOG.md` whenever demo behavior or structure changes.
package/README.md CHANGED
@@ -56,6 +56,31 @@ See [the design and validation contract](docs/consented-diagnostics.md) and
56
56
  The npm package includes the viewer source, manifest and build-time acquisition
57
57
  helper and typed composite layout definitions. Model binaries are downloaded separately; they are not bundled in npm.
58
58
 
59
+ ## Package compliance documents
60
+
61
+ The npm artifact includes `LICENSE`, `SECURITY.md`, `CONTRIBUTING.md` and the
62
+ unsigned [CLA guide](legal/CLA.md), [Individual CLA](legal/INDIVIDUAL_CLA.md)
63
+ and [Corporate CLA](legal/CORPORATE_CLA.md) templates from the approved schema
64
+ baseline. Only package-name references are adapted. These documents describe
65
+ contribution and reporting policies; model-specific credits and licences remain
66
+ with their existing catalogue records.
67
+
68
+ Run `npm ci`, `npm run build`, `npm test`, `npm run test:coverage` and
69
+ `npm run pack:check` to validate the contract. The pack check requires non-empty
70
+ regular compliance files and a real `legal/` directory, then verifies their
71
+ presence in the actual npm dry-run manifest. SHA-256 checks pin the exact bytes
72
+ of all three templates to schema commit `d8f8eb430c77b8a98da496a3d8382d73d6297394`
73
+ after package-name substitution. Future legal changes require explicit review
74
+ of the approved baseline, pinned digests and regression expectations together.
75
+ CI and the approved CD path run this check. It fails on missing documents,
76
+ omitted package entries, symlinks, altered template terms or unexpected legal
77
+ files in the artifact. The package allowlist names each legal template individually.
78
+
79
+ Signed agreements and contributor acceptance records belong in the approved
80
+ access-controlled system outside source control. Never add them to this
81
+ repository, package or public build artifacts. See [ADR 0008](docs/adrs/adr-0008-public-compliance-artifact-contract.md)
82
+ for the packaging decision and release checks.
83
+
59
84
  ## Run the model workshop
60
85
 
61
86
  Use Node.js 24 or later, npm, and a WebGPU-capable browser on localhost or HTTPS.
@@ -245,6 +270,16 @@ short-lived npm OIDC with provenance, and has no long-lived npm write-token
245
270
  fallback. Rollback disables CD; it never rewrites published package history.
246
271
  <!-- END PLASIUS RELEASE INTEGRITY -->
247
272
 
273
+ ## Dense scene studies
274
+
275
+ Use `npm run models:download -- crowd` to acquire the separately tracked Poly
276
+ Astronaut benchmark and the two existing vegetation sources. The workshop adds
277
+ 50- and 100-astronaut groves using shared geometry, six trees and twelve full shrub
278
+ groups. Credits, unique geometry counts and requested GPU allocation bytes remain
279
+ visible. These static studies do not alter the original model-release target.
280
+ See the [dense-scene guide](docs/dense-scene-studies.md) for controls, instancing
281
+ options, limits and qualification requirements.
282
+
248
283
  ## Reuse composite layouts
249
284
 
250
285
  Import the small data-only subpath to offer the workshop's four arrangements in
package/SECURITY.md ADDED
@@ -0,0 +1,17 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ We currently support the latest major version of this project. Older versions may not receive security updates.
6
+
7
+ ## Reporting a Vulnerability
8
+
9
+ If you discover a security vulnerability, please report it privately by emailing us at [security@plasius.co.uk](mailto:security@plasius.co.uk). Please do not create a public issue for security-related matters.
10
+
11
+ ## Response Timeline
12
+
13
+ We aim to acknowledge your report within 2 business days and to provide a more detailed response (including next steps and, if applicable, a timeline for a fix) within 7 business days.
14
+
15
+ ## Disclosure Policy
16
+
17
+ We request that you give us the opportunity to address the vulnerability before publicly disclosing it. We will coordinate with you on public disclosure once a fix is available and deployed.
@@ -40,3 +40,7 @@ cover explicit user flow, mobile/keyboard access and network/cache isolation.
40
40
  Release uses PR CI followed by exact-main `cd.yml` in `production`; verify the
41
41
  npm artifact before downstream site#1677 adopts it. No backend or secret mapping
42
42
  changes occur in this static package release.
43
+
44
+ The 27 October 2026 dependency update in ADR 0009 preserves all four asset hashes
45
+ against published shared 1.4.0. The source-admission and consent decisions above
46
+ remain unchanged; model-library stress scenes are still excluded.
@@ -0,0 +1,60 @@
1
+ # ADR 0008: Public compliance artifact contract
2
+
3
+ Status: Accepted 2026-10-23
4
+
5
+ ## Context
6
+
7
+ Project Task gpu-demo-viewer#35 under operational task site#847 requires the
8
+ schema-template legal baseline and package-level guarantees. The previous
9
+ verifier allowed publication without security/contribution policies or legal
10
+ templates. This is repository-operational compliance maintenance; it changes no
11
+ product Feature, remotely controlled behaviour or entitlement. Flags and
12
+ capabilities are N/A for this packaging-only task.
13
+
14
+ ## Decision
15
+
16
+ Reuse the three unsigned templates from @plasius/schema main
17
+ `d8f8eb430c77b8a98da496a3d8382d73d6297394`, substituting only the package name.
18
+ Preserve Apache-2.0 package licensing and existing model-specific licences.
19
+ Include SECURITY.md and CONTRIBUTING.md and explicitly list each legal template
20
+ in package.json. Do not include a broad legal directory glob. Signed agreements
21
+ and acceptance records stay outside source control in the approved restricted
22
+ system; they are never package inputs.
23
+
24
+ The Node-built-in verifier checks metadata for non-empty regular compliance
25
+ files and a real legal directory. It pins SHA-256 digests of all three adapted
26
+ templates to exact approved bytes before invoking a bounded npm dry-run. Future
27
+ legal changes require reviewed baseline/digest and test updates together;
28
+ non-empty truncation, rewording or added terms must fail. It then
29
+ requires all six documents in the manifest and rejects any unapproved legal
30
+ entry. It does not read private contributor records. Its root argument permits
31
+ isolated test fixtures without mutating the working checkout. CI and CD invoke
32
+ the same pack check; no new runtime dependency or public runtime API is needed.
33
+
34
+ ## Alternatives
35
+
36
+ Repository-only documents would not satisfy the requested artifact baseline.
37
+ Including all of legal/ would make accidental document additions package inputs.
38
+ Checking source presence alone would miss package allowlist regressions. A new
39
+ shared package is unnecessary for these local filesystem checks.
40
+
41
+ ## Tests and NFR implications
42
+
43
+ Requirements-derived regressions remove, empty, symlink or omit each required
44
+ document and exercise actual npm dry-runs. Tampering regressions truncate,
45
+ reword and append to every template, and independently pin their approved hashes.
46
+ Positive fixtures prove the complete contract passes, while synthetic unlisted records stay excluded or fail packing.
47
+ Tests never create actual agreements or personal information. Source/helper
48
+ coverage, frozen install, audit, build and package checks establish reproducible
49
+ and maintainable validation. Viewer runtime, browser/GPU behaviour, accessibility,
50
+ SEO and performance are unchanged; no browser qualification or Azure secretref
51
+ change applies. Existing NFR.md remains the runtime baseline.
52
+
53
+ ## Release and rollback
54
+
55
+ Deliver through PR, successful CI, protected main and the existing cd.yml
56
+ production/OIDC prepare-publish route. Verify a fresh package version and compare
57
+ all required document bytes with the released source and sealed tarball. Do not
58
+ manually promote changelog sections or publish locally. Correct packaging defects
59
+ through an ordinary forward release; never roll back by publishing private
60
+ records or restoring fail-open validation. No site consumer deployment is needed.
@@ -0,0 +1,31 @@
1
+ # ADR 0009: Instanced crowd studies
2
+
3
+ Status: Accepted. Date: 2026-10-27. Task #90; Story site#2336; Feature site#1170.
4
+
5
+ Add two explicit static stress studies: 50 and 100 astronauts, each with six
6
+ Quiver Tree placements and twelve complete Searsia Lucida shrub groups. Each
7
+ shrub source includes seven authored variants; preserve them and label groups
8
+ accurately. Layout definitions are fresh data returned by `denseStudyDefinitions`
9
+ in the existing typed studies export. Existing four composite definitions remain
10
+ stable. Hosts resolve only their admitted sources and own rollout/access decisions.
11
+
12
+ Use gpu-shared's explicit instancing option and renderer allocation reports. Do not
13
+ expand copies, duplicate shader logic or substitute lower-detail vegetation. Show
14
+ unique geometry, source/placement/mesh-instance counts and requested GPU resource
15
+ storage distinctly. The displayed byte figure is not physical VRAM or total browser
16
+ memory. Retain source credits once per unique asset with placement counts.
17
+
18
+ The Poly Astronaut GLB is a separately pinned, integrity-checked CC-BY-2.0 benchmark
19
+ source. Keep it in `benchmarkAssets` so the original 75 research identities and
20
+ 74-model production target are not changed. `models:download -- crowd` acquires only
21
+ this asset plus the two existing vegetation sources through the bounded acquisition
22
+ helper. The original batch downloader and Gem exclusion retain their semantics.
23
+
24
+ No public access is enabled locally. The site continues to use
25
+ `gpu-demo.scene-fidelity.enabled` and `site.studio-coming-soon.view`. Complete
26
+ registry-package GPU qualification and approved production release before marking
27
+ this delivery complete. Static instances do not imply animation or voxel traversal.
28
+
29
+ Consume released shared 1.4.0 and renderer 0.2.57. The existing consented diagnostics
30
+ asset hashes are unchanged and verified against the new shared package; no wider
31
+ scene admission or diagnostics collection is introduced.
@@ -10,3 +10,7 @@
10
10
  - [ADR 0005: Build-time model acquisition subpath](adr-0005-build-time-model-acquisition-subpath.md)
11
11
  - [ADR 0006: Composite study consumer](adr-0006-composite-study-consumer.md)
12
12
  - [ADR 0007: Consented curated showcase diagnostics](adr-0007-consented-curated-showcase-diagnostics.md)
13
+
14
+ - [ADR 0008: Public compliance artifact contract](adr-0008-public-compliance-artifact-contract.md)
15
+
16
+ - [ADR 0009: Instanced crowd studies](adr-0009-instanced-crowd-studies.md)
@@ -225,3 +225,11 @@ raise resource limits to make qualification pass.
225
225
 
226
226
  The candidate contains 61 models. Production remains at the last independently
227
227
  verified release until package publication, site CD and public checks complete.
228
+
229
+ ## Instanced scene admission — 27 October 2026
230
+
231
+ Shared 1.4.0 replaces the historical 500,000-triangle guard with requested GPU
232
+ resource accounting and device limits. The four large tree sources above remain
233
+ unqualified: removing that guard does not establish their performance or memory
234
+ suitability. Dense studies reuse already reviewed vegetation and a separate pinned
235
+ Astronaut fixture; see [the dense-scene guide](dense-scene-studies.md).
@@ -50,6 +50,15 @@ signature/attestation verification cover the new locked graph. A future shared
50
50
  version requires an explicit asset/hash compatibility review rather than
51
51
  silently accepting changed assets.
52
52
 
53
+ ## Instancing dependency qualification — 27 October 2026
54
+
55
+ The dense-scene release advances the exact shared dependency to 1.4.0 and the
56
+ renderer range/lock to ^0.2.57/0.2.57. The existing four SHA-384 asset pins match
57
+ the published 1.4.0 bytes without changes; tests also confirm their resources
58
+ remain self-contained. The diagnostics public exports, access/consent boundaries
59
+ and source-owned mount are unchanged. Instanced model studies do not become
60
+ eligible diagnostics sources. See [ADR 0009](adrs/adr-0009-instanced-crowd-studies.md).
61
+
53
62
  ## Browser qualification — 21 October 2026
54
63
 
55
64
  An isolated in-app Chromium tab mounted the actual published shared/native
@@ -0,0 +1,55 @@
1
+ # Dense scene studies
2
+
3
+ The astronaut grove studies place 50 or 100 astronauts among six trees and twelve
4
+ shrub groups. Each group retains the complete seven-variant Searsia source. This
5
+ is a static instancing test: source meshes and acceleration structures are shared,
6
+ with distinct transforms per placement. Original geometry and textures are kept.
7
+
8
+ Acquire the three integrity-pinned sources with:
9
+
10
+ ```sh
11
+ npm run models:download -- crowd
12
+ npm run models:dev
13
+ ```
14
+
15
+ Open the local workshop with `?review=first-twelve&model=study-astronaut-grove-100`
16
+ and choose the requested sample count, bounces, image size and denoising. Pause
17
+ when hidden, move/reset the camera to invalidate history, and compare the 50- and
18
+ 100-placement study. An allocation or GPU failure must remain visible; choose a
19
+ smaller quality target or retry explicitly rather than silently changing the scene.
20
+
21
+ The `model-library/studies` export provides `denseStudyDefinitions()` alongside
22
+ the original `compositeStudyDefinitions()`. Dense definitions carry `instancing`
23
+ and `camera` as well as placement data. Resolve each source through your reviewed
24
+ catalogue, pass those options to `createSceneInspection`, and display the returned
25
+ source/instance counts and allocation report. Repeated models need one source
26
+ attribution, with a placement count, rather than a hundred duplicate credits.
27
+
28
+ The 100-astronaut scene has 118 model placements, 365 mesh instances including its
29
+ ground, 25 unique mesh primitives and 528,922 unique triangles. The expanded
30
+ geometry equivalent is 5,587,450 triangles. More instances still require TLAS and
31
+ transform storage and add ray traversal/shading work. Requested GPU storage also
32
+ depends on image size and denoising. It excludes driver overhead, other tabs and
33
+ physical-memory residency; it is not a measure of available VRAM.
34
+
35
+ Astronaut is credited to Poly under CC-BY-2.0, from the pinned Google model-viewer
36
+ source and attribution record in `catalogue.json`. Quiver Tree and Searsia Lucida
37
+ retain their existing Poly Haven CC0 source records and authors. Benchmark assets
38
+ are additional fixtures, separate from the original 74-model production target.
39
+
40
+ Qualification requirements: deterministic counts and fresh layout data; complete
41
+ integrity manifests; visible attribution; unchanged original composites; allocation
42
+ and device-loss errors; source cleanup; keyboard/touch/mouse and mobile layout;
43
+ registry-based 50/100 GPU review; approved release and public verification. Local
44
+ source-injected results alone do not establish production readiness. Track evidence
45
+ under Task #90 and Story site#2336.
46
+
47
+ After a resource allocation or device failure, adjust the quality settings and
48
+ choose **Retry selected scene**. Changes while failed do not submit further GPU
49
+ work; retry disposes the previous scene and uses the latest settings.
50
+
51
+ Registry consumer qualification on 27 October 2026 used renderer 0.2.57 and shared
52
+ 1.4.0 with no source overrides. Both 50/100 studies completed 64 samples/six
53
+ bounces/denoising at the workshop's 800x283 viewport. The existing diagnostics
54
+ asset pins also match shared 1.4.0. This establishes local released-dependency
55
+ behavior; public deployment remains a separate gate.
package/legal/CLA.md ADDED
@@ -0,0 +1,25 @@
1
+ # Contributor License Agreements (CLA)
2
+
3
+ To protect the intellectual property of this project and ensure clarity of rights, all contributors must sign a Contributor License Agreement (CLA) before their first contribution.
4
+
5
+ ## Which CLA should I sign?
6
+
7
+ - **Individual CLA**: If you are contributing personally and not on behalf of an employer, sign the [Individual CLA](INDIVIDUAL_CLA.md).
8
+ - **Corporate CLA**: If you are contributing as part of your work for a company, the company should sign the [Corporate CLA](CORPORATE_CLA.md).
9
+
10
+ ## How to sign
11
+
12
+ 1. Download the appropriate CLA file (Individual or Corporate).
13
+ 2. Fill in the required details, sign, and date it.
14
+ 3. Email a PDF copy of the signed document to **[contributors@plasius.co.uk](mailto:contributors@plasius.co.uk)** with subject: `CLA – Individual` or `CLA – Corporate`.
15
+
16
+ ## Registry
17
+
18
+ Signed agreements and acceptance records are stored in an access-controlled
19
+ system outside source control. They must never be committed to a repository,
20
+ included in a package, or copied into issues, pull requests, build logs, or
21
+ other public artifacts.
22
+
23
+ ## Questions?
24
+
25
+ If you have any questions about which CLA to sign or how the process works, please email **[contributors@plasius.co.uk](mailto:contributors@plasius.co.uk)**.
@@ -0,0 +1,57 @@
1
+ # Corporate Contributor License Agreement (CLA)
2
+
3
+ ## Purpose
4
+
5
+ This Corporate Contributor License Agreement ("Agreement") is intended to protect the intellectual property rights of the contributors and the project, ensure clear licensing terms for contributions, and maintain trust within the community. By signing this Agreement, the corporation agrees to the terms that facilitate the use, distribution, and modification of contributions under the project's licensing framework.
6
+
7
+ ## Agreement
8
+
9
+ 1. **Representation of Authority**
10
+ The undersigned individual represents and warrants that they have the full legal authority to enter into this Agreement on behalf of the corporation named below ("Corporation") and to grant the rights contained herein.
11
+
12
+ 2. **Grant of Copyright License**
13
+ The Corporation hereby grants to the project maintainers and users a perpetual, worldwide, non-exclusive, royalty-free, irrevocable copyright license to use, reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute the contributions submitted to the project.
14
+
15
+ 3. **Grant of Patent License**
16
+ The Corporation hereby grants to the project maintainers and users a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license under any patent claims that are necessarily infringed by the contributions to make, use, sell, offer for sale, import, and otherwise dispose of the contributions or derivative works thereof.
17
+
18
+ 4. **Warranties and Representations**
19
+ The Corporation represents and warrants that:
20
+
21
+ - The contributions are the original work of the Corporation or that the Corporation has sufficient rights to grant the licenses herein.
22
+ - The submission of the contributions does not violate any agreements or rights of third parties.
23
+
24
+ 5. **No Revocation**
25
+ This license is granted on a perpetual basis and cannot be revoked, provided that the terms of this Agreement are met.
26
+
27
+ 6. **Governing Law**
28
+ This Agreement shall be governed by and construed in accordance with the laws of the United Kingdom, without regard to its conflict of laws principles.
29
+
30
+ 7. **Execution**
31
+
32
+ This Agreement is effective upon signature by the authorized representative of the Corporation. Please sign and date this document, then email a scanned PDF copy to [contributors@plasius.co.uk](mailto:contributors@plasius.co.uk).
33
+
34
+ ---
35
+
36
+ ### **@plasius/gpu-demo-viewer**
37
+
38
+ **Corporation Legal Name:** \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
39
+
40
+ **Authorized Representative:** \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
41
+
42
+ **Title:** \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
43
+
44
+ **Email:** \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
45
+
46
+ **Date:** \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
47
+
48
+ **Signature:** \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
49
+
50
+ ---
51
+
52
+ ## How to Sign
53
+
54
+ - Download this file as a template.
55
+ - Fill in the Corporation’s legal name, authorized representative, title, email, date, and provide a signature.
56
+ - Sign and date the document.
57
+ - Send a scanned copy of the signed Agreement to [contributors@plasius.co.uk](mailto:contributors@plasius.co.uk).
@@ -0,0 +1,91 @@
1
+ # Individual Contributor License Agreement (CLA)
2
+
3
+ **Project:** @plasius/gpu-demo-viewer (Plasius LTD)
4
+ **Version:** 1.0 — 2025‑09‑12
5
+ **Contact:** [contributors@plasius.co.uk](mailto:contributors@plasius.co.uk)
6
+
7
+ ---
8
+
9
+ ## 1. Definitions
10
+
11
+ - **"You"** (or **"Contributor"**) means the individual signing this CLA and submitting Contributions to the Project.
12
+ - **"Contribution"** means any original work of authorship, including code, documentation, data, designs, or feedback that You submit to the Project in any form (e.g., pull request, issue comment, email, file upload).
13
+ - **"Project"** means the @plasius/gpu-demo-viewer repositories and related materials owned or managed by **Plasius LTD**.
14
+
15
+ ## 2. Copyright License Grant
16
+
17
+ You hereby grant to Plasius LTD a **perpetual, worldwide, non‑exclusive, transferable, sublicensable, royalty‑free, irrevocable** copyright license to:
18
+
19
+ - use, reproduce, publicly display, publicly perform, modify, create derivative works of, and
20
+ - distribute Contributions in source and object form,
21
+ - and to **sublicense** these rights under any terms Plasius LTD chooses, including proprietary or open‑source licenses.
22
+
23
+ ## 3. Patent License Grant
24
+
25
+ You hereby grant to Plasius LTD and its sublicensees a **perpetual, worldwide, non‑exclusive, transferable, royalty‑free, irrevocable** patent license to **make, have made, use, offer to sell, sell, import, and otherwise transfer** the Contribution and derivative works thereof, where such license applies only to patent claims that You **own or control** and that would be infringed by Your Contribution or its combination with the Project.
26
+
27
+ ## 4. Moral Rights & Attribution
28
+
29
+ To the maximum extent permitted by applicable law, You **waive** and agree not to assert any moral rights (e.g., rights of attribution or integrity) in or to the Contribution against Plasius LTD. Plasius LTD may, but is not required to, credit You.
30
+
31
+ ## 5. Representations & Warranties
32
+
33
+ You represent that:
34
+
35
+ 1. **Originality / Rights:** Each Contribution is Your original creation, or You have sufficient rights to submit it and grant the licenses above.
36
+ 2. **No Confidential Info:** Contributions **do not** include confidential information or trade secrets of any third party.
37
+ 3. **No Infringement:** To the best of Your knowledge, Contributions do not infringe any third‑party IP rights.
38
+ 4. **Employment / Contractor Status:** If Your employer or a third party might claim rights in Your Contribution, You have obtained **written permission** to make the Contribution and grant these licenses (attach or reference below), or Your Contribution is made **outside the scope** of your employment and without using your employer’s confidential information or resources.
39
+ 5. **Compliance:** You will follow the Project’s policies (e.g., Code of Conduct, Security Policy) and applicable laws.
40
+
41
+ ## 6. Third‑Party Code
42
+
43
+ If Your Contribution includes code, data, or other material from a third party, You will **identify the material and its license** in the pull request or submission, and ensure it is **compatible** with the Project’s licensing model. You will not submit material subject to terms that require the Project to disclose proprietary source code (e.g., certain copyleft obligations) unless the Project has **pre‑approved** such inclusion in writing.
44
+
45
+ ## 7. Scope & Duration
46
+
47
+ - This CLA covers **all past and future** Contributions You submit to the Project, unless and until You provide written notice to **revoke** it.
48
+ - Revocation is **not retroactive**: rights granted for prior Contributions remain in effect.
49
+
50
+ ## 8. Disclaimer
51
+
52
+ THE CONTRIBUTION IS PROVIDED “AS IS” WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON‑INFRINGEMENT.
53
+
54
+ ## 9. Governing Law & Jurisdiction
55
+
56
+ This CLA is governed by the **laws of England and Wales**, and the courts of England and Wales shall have **exclusive jurisdiction** over any dispute arising out of or relating to it.
57
+
58
+ ## 10. Entire Agreement
59
+
60
+ This CLA is the entire agreement between You and Plasius LTD regarding Contributions. It supersedes any prior discussions relating to Contributions. If any provision is held unenforceable, the remaining provisions remain in full force.
61
+
62
+ ---
63
+
64
+ ## 11. Contributor Information & Signature
65
+
66
+ By signing below, You agree to the terms of this CLA for Your Contributions to the Project.
67
+
68
+ **Full Name:** \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
69
+
70
+ **Email:** \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
71
+
72
+ **GitHub Handle:** \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
73
+
74
+ **Address (optional):** \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
75
+
76
+ **Employer (if applicable):** \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
77
+
78
+ **If employed:** ☐ I confirm Contributions are made outside the scope of employment **or** ☐ I have attached my employer’s written permission.
79
+
80
+ **Signature:** \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
81
+
82
+ **Date (YYYY‑MM‑DD):** \_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
83
+
84
+ _Electronic signatures are accepted. You may type your name in the Signature field and email a PDF copy._
85
+
86
+ **Submission:** Please email the signed CLA to **[contributors@plasius.co.uk](mailto:contributors@plasius.co.uk)** with subject line: `CLA – Individual – <GitHubHandle>`.
87
+
88
+ **(Optional) Attachments / Notes:**
89
+
90
+ - Employer permission letter (if required)
91
+ - Third‑party license disclosures (if any)
@@ -6088,5 +6088,45 @@
6088
6088
  ]
6089
6089
  }
6090
6090
  }
6091
+ ],
6092
+ "benchmarkAssets": [
6093
+ {
6094
+ "key": "Astronaut",
6095
+ "name": "Astronaut",
6096
+ "category": "Benchmark source",
6097
+ "priority": "Dense scene benchmark",
6098
+ "provider": "Poly / Google model-viewer",
6099
+ "source_url": "https://github.com/google/model-viewer/blob/626f7498f3b8bf1130d0ca0c7b7bb756d6fb89ee/packages/modelviewer.dev/ATTRIBUTIONS.md",
6100
+ "authors": "Poly",
6101
+ "licence": "CC-BY-2.0",
6102
+ "licence_url": "https://creativecommons.org/licenses/by/2.0/",
6103
+ "attribution_required": true,
6104
+ "original_source_url": "https://poly.google.com/view/dLHpzNdygsg",
6105
+ "source_revision": "626f7498f3b8bf1130d0ca0c7b7bb756d6fb89ee",
6106
+ "renderer_target": "Shared geometry, suit material and silhouette in dense scenes.",
6107
+ "preparation": "Static original model; source animation is not present.",
6108
+ "review_status": "local-review",
6109
+ "entry": "Astronaut.glb",
6110
+ "files": [
6111
+ {
6112
+ "path": "Astronaut.glb",
6113
+ "url": "https://raw.githubusercontent.com/google/model-viewer/626f7498f3b8bf1130d0ca0c7b7bb756d6fb89ee/packages/shared-assets/models/Astronaut.glb",
6114
+ "bytes": 2869044,
6115
+ "sha256": "313d48882e2ba7bb25a340dbfd3d6f9bbc7f012372d54f518dc52e4299e3a24e",
6116
+ "integrity": "sha256-MT1IiC4rp7slo0Db/T1vm7x/ASNy1U9RjcUuQpnjok4="
6117
+ }
6118
+ ],
6119
+ "inspection": {
6120
+ "triangles": 1604,
6121
+ "primitives": 1,
6122
+ "materials": 1,
6123
+ "extensions": [],
6124
+ "animations": [],
6125
+ "alphaModes": [
6126
+ "OPAQUE"
6127
+ ]
6128
+ },
6129
+ "checked": "2026-10-27"
6130
+ }
6091
6131
  ]
6092
6132
  }
@@ -29,7 +29,7 @@
29
29
  <p id="quality-help">Hold the camera still to refine. Higher targets reduce noise; more bounces reveal indirect light. HD, QHD and 4K trace at the selected size, even in a smaller window. Higher resolution costs more GPU time; use one sample for a quick inspection.</p>
30
30
  </fieldset>
31
31
  <div class="refinement"><label id="refinement" for="sample-progress">Waiting for a model…</label><progress id="sample-progress" max="64" value="0"></progress></div>
32
- <div class="canvas-wrap"><canvas id="canvas" width="800" height="600" tabindex="0" aria-label="Interactive Plasius model view" aria-describedby="help"></canvas><p id="hint" hidden>Drag to orbit · Shift-drag to move · Scroll to approach<br>Touch: drag or pinch · Keyboard: arrows, + / −, Home</p></div><p id="status" role="status" aria-live="polite">Preparing the review…</p></section></div>
32
+ <div class="canvas-wrap"><canvas id="canvas" width="800" height="600" tabindex="0" aria-label="Interactive Plasius model view" aria-describedby="help"></canvas><p id="hint" hidden>Drag to orbit · Shift-drag to move · Scroll to approach<br>Touch: drag or pinch · Keyboard: arrows, + / −, Home</p></div><p id="status" role="status" aria-live="polite">Preparing the review…</p><button id="retry" type="button" hidden>Retry selected scene</button></section></div>
33
33
  <p class="footnote">Source models join this workshop in review batches. Gem is excluded from this rollout; no substitute is shown.</p></main>
34
34
  <script type="module">import { mountLibrary } from './main.js'; mountLibrary().catch(error => { document.querySelector('#status').textContent = `Unable to open review: ${error.message}`; });</script>
35
35
  </body></html>
@@ -1,5 +1,5 @@
1
1
  import { createReviewController, fixtureRequest, reviewEnabled } from './controller.js';
2
- import { compositeStudies, compositeRequest, renderCompositeCredits } from './scenes.js';
2
+ import { compositeStudies, denseStudies, compositeRequest, renderCompositeCredits } from './scenes.js';
3
3
 
4
4
  export async function mountLibrary({document:doc=globalThis.document,window:win=globalThis.window,
5
5
  fetch:request=globalThis.fetch,importRuntime=()=>import('@plasius/gpu-shared/model-inspection')}={}) {
@@ -10,8 +10,8 @@ export async function mountLibrary({document:doc=globalThis.document,window:win=
10
10
  }
11
11
  const response=await request(new URL('./catalogue.json',win.location.href));
12
12
  if (!response.ok) throw new Error('Model catalogue could not be loaded.');
13
- const {assets}=await response.json();
14
- const studies=compositeStudies(assets), selections=[...assets,...studies];
13
+ const catalogue=await response.json(),assets=[...catalogue.assets,...(catalogue.benchmarkAssets??[])];
14
+ const studies=[...compositeStudies(assets),...denseStudies(assets)], selections=[...assets,...studies];
15
15
  for(const asset of selections) {
16
16
  const option=doc.createElement('option');option.value=asset.key;
17
17
  option.textContent=asset.name+(asset.models?' — composite study':asset.entry?'':' — deferred');el('model').append(option);
@@ -26,7 +26,13 @@ export async function mountLibrary({document:doc=globalThis.document,window:win=
26
26
  const refinement=p=>{
27
27
  el('sample-progress').max=p.target;el('sample-progress').value=p.samples;
28
28
  const phase={complete:'Complete',paused:'Paused',rendering:'Rendering',refining:'Refining'}[p.phase];
29
- el('refinement').textContent=`${phase} · ${p.samples} / ${p.target} samples · ${p.width} × ${p.height}`;
29
+ el('refinement').textContent=`${phase} · ${p.samples} / ${p.target} samples · ${p.width} × ${p.height}${p.allocations?` · ${(p.allocations.allocatedBytes/1048576).toFixed(1)} MiB requested GPU storage${p.allocations.budgetBytes?` / ${(p.allocations.budgetBytes/1048576).toFixed(0)} MiB budget`:""}`:""}`;
30
+ };
31
+ let stateKind='loading';
32
+ const failed=error=>{
33
+ stateKind='error';status.textContent=`Render failed: ${error.message}`;
34
+ el('refinement').textContent='Refinement stopped. Adjust quality, then retry.';
35
+ el('quality').disabled=false;el('retry').hidden=false;
30
36
  };
31
37
  const controller=createReviewController({
32
38
  load:async(asset,signal)=>{
@@ -37,16 +43,17 @@ export async function mountLibrary({document:doc=globalThis.document,window:win=
37
43
  return inspect({canvas:el('canvas'),...request,signal,enabled:true,
38
44
  ...quality(),quality:quality(),
39
45
  onProgress:p=>{if(!signal.aborted)refinement(p);},
40
- onHintChange:visible=>{el('hint').hidden=!visible;},
41
- onError:error=>{if(signal.aborted)return;status.textContent=`Render failed: ${error.message}`;el('refinement').textContent='Refinement stopped';el('quality').disabled=true;}});
46
+ onHintChange:visible=>{if(!signal.aborted)el('hint').hidden=!visible;},
47
+ onError:error=>{if(!signal.aborted)failed(error);}});
42
48
  },
43
49
  onState:state=>{
44
- el('hint').hidden=true;el('quality').disabled=state.kind!=='ready';
50
+ stateKind=state.kind;el('retry').hidden=state.kind!=='error';
51
+ el('hint').hidden=true;el('quality').disabled=!['ready','error'].includes(state.kind);
45
52
  if(state.kind==='loading'){el('sample-progress').value=0;el('refinement').textContent='Waiting for the first sample…';}
46
53
  if(state.kind==='loading')status.textContent=`Loading ${state.asset.name} and checking its files…`;
47
54
  else if(state.kind==='ready') {
48
55
  status.textContent=`${state.asset.review_status==='needs-fix'?'Rendered — visual defect recorded':'Ready'} · ${state.asset.name} · ${state.runtime.triangleCount.toLocaleString()} triangles · ${state.runtime.primitiveCount} mesh primitives`;
49
- el('metrics').textContent=`${state.runtime.triangleCount.toLocaleString()} triangles · ${state.asset.inspection.materials} source materials${state.asset.models?` · ${state.asset.models.length} models + shared ground`:''}`;
56
+ el('metrics').textContent=`${state.runtime.triangleCount.toLocaleString()} triangles · ${state.asset.inspection.materials} source materials${state.asset.models?` · ${state.asset.models.length} models + shared ground${state.runtime.geometryMode==='instanced'?` · ${state.runtime.sourceCount} unique sources · ${state.runtime.instanceCount} mesh instances · shared geometry`:""}`:''}`;
50
57
  } else {status.textContent=state.message;el('canvas').hidden=true;el('refinement').textContent='Refinement unavailable';}
51
58
  },
52
59
  });
@@ -60,7 +67,7 @@ export async function mountLibrary({document:doc=globalThis.document,window:win=
60
67
  if(!asset.models){el('source').href=asset.source_url;el('licence').href=asset.licence_url;el('licence').textContent=asset.licence;}
61
68
  el('metrics').textContent=asset.entry?`${(asset.files.reduce((n,f)=>n+f.bytes,0)/1048576).toFixed(1)} MiB on disk`:'';
62
69
  const clips=asset.inspection?.animations??[];
63
- el('note').textContent=asset.models?`${asset.models.length} original models share one camera, ground and lighting. Geometry counts include the ground.`:
70
+ el('note').textContent=asset.models?(asset.instancing?`${asset.models.length} model placements share three sources, one camera, ground and lighting. Geometry counts are unique; requested GPU storage excludes driver overhead and other tabs.`:`${asset.models.length} original models share one camera, ground and lighting. Geometry counts include the ground.`):
64
71
  clips.length?`Static pose only. ${clips.length} source clips are present: ${clips.join(', ')}. Animation playback needs importer work.`:
65
72
  asset.inspection?.extensions.length?`Material features to inspect: ${asset.inspection.extensions.join(', ')}.`:'Source materials under the same inspection lighting.';
66
73
  return controller.select(asset);
@@ -68,9 +75,11 @@ export async function mountLibrary({document:doc=globalThis.document,window:win=
68
75
  const requested=new URL(win.location.href).searchParams.get('model');
69
76
  el('model').value=selections.find(a=>a.key===requested&&(a.entry||a.models))?.key??assets.find(a=>a.entry).key;
70
77
  el('model').addEventListener('change',change);
78
+ el('retry').addEventListener('click',change);
71
79
  el('model').disabled=false;
72
80
  const qualityChange=async()=>{
73
- try{await controller.setQuality(quality());}catch(error){status.textContent=`Render failed: ${error.message}`;}
81
+ if(stateKind!=='ready')return;
82
+ try{await controller.setQuality(quality());}catch(error){failed(error);}
74
83
  };
75
84
  for(const id of ['samples','bounces','resolution','denoise'])el(id).addEventListener('change',qualityChange);
76
85
  const visibility=()=>controller.setPaused(doc.hidden===true);
@@ -78,7 +87,7 @@ export async function mountLibrary({document:doc=globalThis.document,window:win=
78
87
  let resizeTimer;
79
88
  const resize=()=>{clearTimeout(resizeTimer);resizeTimer=setTimeout(qualityChange,250);};
80
89
  win.addEventListener('resize',resize);
81
- const destroy=()=>{for(const id of ['samples','bounces','resolution','denoise'])el(id).removeEventListener('change',qualityChange);doc.removeEventListener('visibilitychange',visibility);clearTimeout(resizeTimer);controller.destroy();el('model').removeEventListener('change',change);win.removeEventListener('pagehide',destroy);win.removeEventListener('resize',resize);};
90
+ const destroy=()=>{for(const id of ['samples','bounces','resolution','denoise'])el(id).removeEventListener('change',qualityChange);doc.removeEventListener('visibilitychange',visibility);clearTimeout(resizeTimer);controller.destroy();el('model').removeEventListener('change',change);el('retry').removeEventListener('click',change);win.removeEventListener('pagehide',destroy);win.removeEventListener('resize',resize);};
82
91
  win.addEventListener('pagehide',destroy);
83
92
  await change();return {destroy};
84
93
  }
@@ -1,31 +1,44 @@
1
1
  import { fixtureRequest } from './controller.js';
2
- import { compositeStudyDefinitions } from './studies.js';
2
+ import { compositeStudyDefinitions, denseStudyDefinitions } from './studies.js';
3
3
 
4
4
  export function compositeStudies(assets) {
5
- return compositeStudyDefinitions().flatMap(({ placements, ...definition }) => {
5
+ return resolveStudies(compositeStudyDefinitions(),assets);
6
+ }
7
+
8
+ export function denseStudies(assets) {
9
+ return resolveStudies(denseStudyDefinitions(),assets);
10
+ }
11
+
12
+ function resolveStudies(definitions,assets) {
13
+ return definitions.flatMap(({ placements, ...definition }) => {
6
14
  const models = placements.map(([key, anchor, size, yaw]) => ({
7
15
  asset: assets.find(a => a.key === key), anchor: [...anchor], size, yaw,
8
16
  }));
9
17
  if (models.some(model => !model.asset?.entry)) return [];
10
18
  return [{ ...definition, category: 'Composite study', models,
11
- inspection: { materials: models.reduce((sum, model) => sum + model.asset.inspection.materials, 0) } }];
19
+ inspection: { materials: [...new Map(models.map(model=>[model.asset.key,model.asset])).values()].reduce((sum, asset) => sum + asset.inspection.materials, 0) } }];
12
20
  });
13
21
  }
14
22
 
15
23
  export function compositeRequest(study, base) {
16
- return { models: study.models.map(({ asset, anchor, size, yaw }) => ({
24
+ return { ...(study.instancing?{instancing:true,camera:study.camera}:{}), models: study.models.map(({ asset, anchor, size, yaw }) => ({
17
25
  ...fixtureRequest(asset, base), anchor, size, yaw,
18
26
  })) };
19
27
  }
20
28
 
21
29
  export function renderCompositeCredits(doc, container, models) {
22
30
  container.replaceChildren();
23
- for (const { asset, size } of models) {
31
+ const sources=new Map();
32
+ for(const {asset,size} of models) {
33
+ const source=sources.get(asset.key)??{asset,sizes:new Set(),count:0};
34
+ source.sizes.add(size);source.count++;sources.set(asset.key,source);
35
+ }
36
+ for (const { asset, sizes, count } of sources.values()) {
24
37
  const item = doc.createElement('li'), source = doc.createElement('a'), licence = doc.createElement('a');
25
38
  source.textContent = `${asset.name} — ${asset.authors}`; source.href = asset.source_url;
26
39
  licence.textContent = asset.licence; licence.href = asset.licence_url;
27
40
  for (const link of [source, licence]) { link.target = '_blank'; link.rel = 'noopener'; }
28
- item.append(source, ' · ', licence, ` · longest dimension ${size} scene units`);
41
+ item.append(source, ' · ', licence, ` · ${count} placement${count===1?'':'s'} · longest dimension ${[...sizes].join(', ')} scene units`);
29
42
  container.append(item);
30
43
  }
31
44
  }
@@ -4,7 +4,11 @@ export interface CompositeStudyDefinition {
4
4
  name: string;
5
5
  renderer_target: string;
6
6
  preparation: string;
7
+ instancing?: boolean;
8
+ camera?: { eye: [number, number, number]; target: [number, number, number]; fov: number };
7
9
  /** Source key, bottom-centre anchor, longest dimension in scene units, Y rotation in radians. */
8
10
  placements: [string, [number, number, number], number, number][];
9
11
  }
10
12
  export function compositeStudyDefinitions(): CompositeStudyDefinition[];
13
+
14
+ export function denseStudyDefinitions(): CompositeStudyDefinition[];
@@ -49,3 +49,18 @@ export function compositeStudyDefinitions() {
49
49
  placements: placements.map(([key, anchor, size, yaw]) => [key, [...anchor], size, yaw]),
50
50
  }));
51
51
  }
52
+
53
+ /** Dense static scenes share three source models; shrub placements retain all authored variants. */
54
+ export function denseStudyDefinitions() {
55
+ return [50,100].map(count => ({
56
+ key: `study-astronaut-grove-${count}`, name: `Astronaut grove — ${count} astronauts`,
57
+ renderer_target: `Trace ${count} astronauts, six trees and twelve shrub groups with shared geometry.`,
58
+ preparation: 'Inspect overlapping silhouettes, vegetation transparency and contact shadows. Each shrub group retains seven authored variants. Static instances; dimensions are chosen scene units.',
59
+ instancing: true, camera: {eye:[25,22,33],target:[0,1,0],fov:42},
60
+ placements: [
61
+ ...Array.from({length:count},(_,i)=>['Astronaut',[(i%10-4.5)*1.75,0,(Math.floor(i/10)-(count/10-1)/2)*1.75],1.8,(i*.618)%(Math.PI*2)]),
62
+ ...Array.from({length:6},(_,i)=>['quiver_tree_01',[i%2?12:-12,0,Math.floor(i/2)*9-9],7,(i*.618)%(Math.PI*2)]),
63
+ ...Array.from({length:12},(_,i)=>['searsia_lucida',[i%2?10.5:-10.5,0,Math.floor(i/2)*4-10],3,(i*.618)%(Math.PI*2)]),
64
+ ],
65
+ }));
66
+ }
@@ -7,3 +7,5 @@
7
7
 
8
8
  /* Preserve the traced camera aspect when fixed-size frames fit the viewport. */
9
9
  canvas{object-fit:contain}
10
+
11
+ #retry{min-height:44px;padding:.7rem 1rem;background:#222d37;color:#f2eee5;border:1px solid #dfc88d;border-radius:.35rem;font:inherit;cursor:pointer}#retry:focus-visible{outline:3px solid #edd7a1;outline-offset:3px}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasius/gpu-demo-viewer",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Root validation viewer for Plasius gpu-* demos.",
6
6
  "license": "Apache-2.0",
@@ -41,7 +41,12 @@
41
41
  "styles.css",
42
42
  "viewer-manifest.js",
43
43
  "model-library/",
44
- "diagnostics/"
44
+ "diagnostics/",
45
+ "SECURITY.md",
46
+ "CONTRIBUTING.md",
47
+ "legal/CLA.md",
48
+ "legal/INDIVIDUAL_CLA.md",
49
+ "legal/CORPORATE_CLA.md"
45
50
  ],
46
51
  "engines": {
47
52
  "node": ">=24"
@@ -60,8 +65,8 @@
60
65
  "test:coverage": "c8 --reporter=lcov --reporter=text node --test"
61
66
  },
62
67
  "dependencies": {
63
- "@plasius/gpu-renderer": "^0.2.54",
64
- "@plasius/gpu-shared": "1.3.0"
68
+ "@plasius/gpu-renderer": "^0.2.57",
69
+ "@plasius/gpu-shared": "1.4.0"
65
70
  },
66
71
  "devDependencies": {
67
72
  "c8": "^11.0.0",
@@ -3,9 +3,10 @@ import { fileURLToPath } from 'node:url';
3
3
  import { acquireFixtures } from '../model-library/acquire.js';
4
4
  import { selectBatch } from '../model-library/rollout.js';
5
5
  export async function run(acquire=acquireFixtures,batch=0) {
6
- const {assets}=JSON.parse(await readFile(new URL('../model-library/catalogue.json',import.meta.url),'utf8'));
6
+ const {assets,benchmarkAssets=[]}=JSON.parse(await readFile(new URL('../model-library/catalogue.json',import.meta.url),'utf8'));
7
+ if(batch==='crowd')return acquire({assets:[...benchmarkAssets,...assets.filter(a=>['quiver_tree_01','searsia_lucida'].includes(a.key))],root:fileURLToPath(new URL('../model-library/assets/',import.meta.url))});
7
8
  const {entries}=JSON.parse(await readFile(new URL('../model-library/rollout.json',import.meta.url),'utf8'));
8
9
  const keys=new Set(selectBatch(entries,batch).map(entry=>entry.key));
9
10
  return acquire({assets:assets.filter(asset=>keys.has(asset.key)),root:fileURLToPath(new URL('../model-library/assets/',import.meta.url))});
10
11
  }
11
- if(process.argv[1]===fileURLToPath(import.meta.url))console.log(await run(undefined,process.argv[2]===undefined?0:Number(process.argv[2])));
12
+ if(process.argv[1]===fileURLToPath(import.meta.url))console.log(await run(undefined,process.argv[2]===undefined?0:process.argv[2]==='crowd'?'crowd':Number(process.argv[2])));
@@ -1,9 +1,25 @@
1
1
  const fs = require("node:fs");
2
2
  const os = require("node:os");
3
+ const { createHash } = require("node:crypto");
3
4
  const path = require("node:path");
4
5
  const { spawnSync } = require("node:child_process");
5
6
 
6
- const repoRoot = path.resolve(__dirname, "..");
7
+ const complianceFiles = [
8
+ "LICENSE",
9
+ "SECURITY.md",
10
+ "CONTRIBUTING.md",
11
+ "legal/CLA.md",
12
+ "legal/INDIVIDUAL_CLA.md",
13
+ "legal/CORPORATE_CLA.md"
14
+ ];
15
+
16
+ // Approved schema d8f8eb430c77b8a98da496a3d8382d73d6297394, with only
17
+ // @plasius/schema replaced by @plasius/gpu-demo-viewer. Hash exact bytes.
18
+ const approvedTemplates = {
19
+ "legal/CLA.md": "8e7de8348013f65a551d9a3179d4927cdce4ae15497ba209a0cf2c1098ba489c",
20
+ "legal/INDIVIDUAL_CLA.md": "6ce945a06ee88d3c3103a6094123465440d8e9890a84956f257555cc9994fb39",
21
+ "legal/CORPORATE_CLA.md": "f56606b1a8a563ca1bae67a9335f32ced1ce3827dec500bc28617de604c66608"
22
+ };
7
23
 
8
24
  function assert(condition, message) {
9
25
  if (!condition) {
@@ -11,14 +27,15 @@ function assert(condition, message) {
11
27
  }
12
28
  }
13
29
 
14
- function readJson(relativePath) {
30
+ function readJson(repoRoot, relativePath) {
15
31
  return JSON.parse(fs.readFileSync(path.join(repoRoot, relativePath), "utf8"));
16
32
  }
17
33
 
18
- function packDryRunFiles() {
34
+ function packDryRunFiles(repoRoot) {
19
35
  const result = spawnSync("npm", ["pack", "--dry-run", "--json"], {
20
36
  cwd: repoRoot,
21
37
  encoding: "utf8",
38
+ timeout: 30000,
22
39
  env: {
23
40
  ...process.env,
24
41
  npm_config_cache:
@@ -38,8 +55,8 @@ function packDryRunFiles() {
38
55
  return packInfo.files.map((file) => file.path).sort();
39
56
  }
40
57
 
41
- function main() {
42
- const packageJson = readJson("package.json");
58
+ function verifyPublicPackage(repoRoot = path.resolve(__dirname, "..")) {
59
+ const packageJson = readJson(repoRoot, "package.json");
43
60
 
44
61
  assert(packageJson.name === "@plasius/gpu-demo-viewer", "package name must remain scoped");
45
62
  assert(packageJson.private !== true, "public package must not set private=true");
@@ -51,11 +68,24 @@ function main() {
51
68
  assert(packageJson.repository?.url?.includes("Plasius-LTD/gpu-demo-viewer"), "repository metadata is required");
52
69
  assert(packageJson.exports?.["."] === "./viewer-manifest.js", "viewer manifest must be the package root export");
53
70
 
54
- const files = packDryRunFiles();
71
+ // Inspect metadata before packing; never read contributor acceptance records.
72
+ const legalDirectory = fs.lstatSync(path.join(repoRoot, "legal"));
73
+ assert(legalDirectory.isDirectory(), "legal must be a real directory");
74
+ for (const file of complianceFiles) {
75
+ const stat = fs.lstatSync(path.join(repoRoot, file));
76
+ assert(stat.isFile() && stat.size > 0, `${file} must be a non-empty regular file`);
77
+ }
78
+
79
+ for (const [file, expected] of Object.entries(approvedTemplates)) {
80
+ const digest = createHash("sha256").update(fs.readFileSync(path.join(repoRoot, file))).digest("hex");
81
+ assert(digest === expected, `${file} must match approved schema template`);
82
+ }
83
+
84
+ const files = packDryRunFiles(repoRoot);
55
85
  const fileSet = new Set(files);
56
86
  const requiredFiles = [
57
87
  "CHANGELOG.md",
58
- "LICENSE",
88
+ ...complianceFiles,
59
89
  "README.md",
60
90
  "assets/brigantine.gltf",
61
91
  "docs/adrs/index.md",
@@ -73,6 +103,10 @@ function main() {
73
103
  }
74
104
 
75
105
  for (const file of files) {
106
+ assert(
107
+ !file.startsWith("legal/") || complianceFiles.includes(file),
108
+ `package tarball contains unapproved legal document: ${file}`
109
+ );
76
110
  assert(!file.startsWith("node_modules/"), `package tarball must not include ${file}`);
77
111
  assert(!file.startsWith("tests/"), `package tarball must not include ${file}`);
78
112
  assert(!file.startsWith(".git/"), `package tarball must not include ${file}`);
@@ -82,9 +116,13 @@ function main() {
82
116
  process.stdout.write("Public package contract verified.\n");
83
117
  }
84
118
 
85
- try {
86
- main();
87
- } catch (error) {
88
- process.stderr.write(`${error.message}\n`);
89
- process.exitCode = 1;
119
+ module.exports = { verifyPublicPackage };
120
+
121
+ if (require.main === module) {
122
+ try {
123
+ verifyPublicPackage();
124
+ } catch (error) {
125
+ process.stderr.write(`${error.message}\n`);
126
+ process.exitCode = 1;
127
+ }
90
128
  }