@plasius/gpu-demo-viewer 0.2.3 → 0.4.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,34 @@ The format is based on Keep a Changelog and this project adheres to SemVer.
18
18
  - **Security**
19
19
  - (placeholder)
20
20
 
21
+ ## [0.4.0] - 2026-10-21
22
+
23
+ - **Added**
24
+ - Source-owned curated showcase diagnostics with integrity-pinned public assets, remote access checks, explicit consent, accessible preview/copy, and fail-closed provenance; no client-pixel capture or diagnostic transport.
25
+
26
+ - **Changed**
27
+ - (placeholder)
28
+
29
+ - **Fixed**
30
+ - (placeholder)
31
+
32
+ - **Security**
33
+ - (placeholder)
34
+
35
+ ## [0.3.0] - 2026-10-21
36
+
37
+ - **Added**
38
+ - Typed, data-only composite study definitions for host applications, sharing the four workshop arrangements without copying placements.
39
+
40
+ - **Changed**
41
+ - (placeholder)
42
+
43
+ - **Fixed**
44
+ - (placeholder)
45
+
46
+ - **Security**
47
+ - (placeholder)
48
+
21
49
  ## [0.2.3] - 2026-10-21
22
50
 
23
51
  - **Added**
@@ -338,3 +366,5 @@ The format is based on Keep a Changelog and this project adheres to SemVer.
338
366
  [0.2.1]: https://github.com/Plasius-LTD/gpu-demo-viewer/releases/tag/v0.2.1
339
367
  [0.2.2]: https://github.com/Plasius-LTD/gpu-demo-viewer/releases/tag/v0.2.2
340
368
  [0.2.3]: https://github.com/Plasius-LTD/gpu-demo-viewer/releases/tag/v0.2.3
369
+ [0.3.0]: https://github.com/Plasius-LTD/gpu-demo-viewer/releases/tag/v0.3.0
370
+ [0.4.0]: https://github.com/Plasius-LTD/gpu-demo-viewer/releases/tag/v0.4.0
package/NFR.md CHANGED
@@ -34,3 +34,17 @@ entry surfaces.
34
34
  ## Model library review
35
35
 
36
36
  Inherits `gpu-demo.scene-fidelity.enabled` with a localhost-only fixture override. Model changes abort prior work and release its GPU resources. Downloads use trusted reviewed hosts, no redirects, strict byte/hash limits and bounded concurrency. Preserve source licences. Keep missing downloads and renderer defects visible. Keyboard, touch, reduced-motion and 320px layouts must remain usable.
37
+
38
+ ## Consented diagnostics
39
+
40
+ Task #42 inherits `feedback.game-diagnostics.enabled` and the host-evaluated
41
+ `feedback.game-diagnostics.attach` capability. The separate diagnostics mount
42
+ has no local/query override. Source-owned native mounts use four SRI-pinned
43
+ public baseline assets from published shared 1.3.0; fallback or unqualified
44
+ surfaces are ineligible. No scene/canvas/DOM/debug snapshot crosses the
45
+ diagnostics boundary. Timing is unknown rather than synthetic precision.
46
+ No automatic collection, diagnostic request, cache, storage or analytics is
47
+ permitted. Explicit preview/copy must remain keyboard accessible at 320px;
48
+ consent/access changes and disposal cancel pending work. Preserve the ordinary
49
+ structured-only reporting path on denial or rollback. Requirements and evidence
50
+ are recorded in [the design](docs/consented-diagnostics.md) and ADR 0007.
package/README.md CHANGED
@@ -11,8 +11,50 @@ A browser workshop for inspecting source models with the Plasius GPU renderer,
11
11
  plus a workspace launcher for the `gpu-*` package demos. The workshop provides
12
12
  camera gestures, progressive rendering, quality controls and model credits.
13
13
 
14
+ ## Optional consented renderer diagnostics
15
+
16
+ Hosts can mount the fixed public baseline harbour and an accessible optional
17
+ diagnostics panel through the focused subpath:
18
+
19
+ ```js
20
+ import { mountDiagnosticShowcase } from '@plasius/gpu-demo-viewer/feedback-diagnostics';
21
+
22
+ const viewer = await mountDiagnosticShowcase({
23
+ root: document.getElementById('scene'),
24
+ feedbackRoot: document.getElementById('feedback'),
25
+ resolveAccess: async () => {
26
+ const decisions = await readCurrentRemoteAccess(); // host source-of-truth evaluator
27
+ return {
28
+ featureEnabled: decisions.flags['feedback.game-diagnostics.enabled'] === true,
29
+ capabilityGranted: decisions.capabilities['feedback.game-diagnostics.attach'] === true,
30
+ sceneEnabled: decisions.flags['gpu-demo.scene-fidelity.enabled'] === true,
31
+ };
32
+ },
33
+ });
34
+ // On a pushed access/flag change: viewer.refreshAccess();
35
+ // On route exit: viewer.destroy(); remount after changing the scene rollout.
36
+ ```
37
+
38
+ Approved routes are exactly `/gpu-demo`, `/gpu-demo/`, and the workspace
39
+ `/gpu-demo-viewer/showcase/` (including its `index.html`). The API accepts no
40
+ surface ID, existing runtime, model, renderer loader or provenance override.
41
+ Only the fixed four integrity-pinned procedural public assets and a live native
42
+ WebGPU frame are eligible. Open model-library studies, generator and
43
+ `/player-system` are excluded. This does not add controls to the existing demo
44
+ launcher or grant a product capability; downstream site integration is tracked
45
+ in site#1677.
46
+
47
+ Consent starts unchecked. Preview and copy both verify current remote access;
48
+ copy also verifies that the surface still matches the reviewed packet. Only
49
+ coarse renderer/backend/viewport fields and closed codes leave this module;
50
+ synthetic timing stays unknown. Nothing is automatically captured, sent,
51
+ logged or stored. Clipboard denial leaves a selectable read-only preview.
52
+ Disabling `feedback.game-diagnostics.enabled` restores structured-only reporting.
53
+ See [the design and validation contract](docs/consented-diagnostics.md) and
54
+ [ADR 0007](docs/adrs/adr-0007-consented-curated-showcase-diagnostics.md).
55
+
14
56
  The npm package includes the viewer source, manifest and build-time acquisition
15
- helper. Model binaries are downloaded separately; they are not bundled in npm.
57
+ helper and typed composite layout definitions. Model binaries are downloaded separately; they are not bundled in npm.
16
58
 
17
59
  ## Run the model workshop
18
60
 
@@ -202,3 +244,26 @@ npm 11.5.1 or newer, uses the protected `production` environment and
202
244
  short-lived npm OIDC with provenance, and has no long-lived npm write-token
203
245
  fallback. Rollback disables CD; it never rewrites published package history.
204
246
  <!-- END PLASIUS RELEASE INTEGRITY -->
247
+
248
+ ## Reuse composite layouts
249
+
250
+ Import the small data-only subpath to offer the workshop's four arrangements in
251
+ another application:
252
+
253
+ ```ts
254
+ import { compositeStudyDefinitions } from '@plasius/gpu-demo-viewer/model-library/studies';
255
+
256
+ const layouts = compositeStudyDefinitions();
257
+ const interior = layouts.find(layout => layout.key === 'study-furnished-interior');
258
+ // Each placement is [sourceKey, bottomCentreAnchor, longestDimension, yawRadians].
259
+ console.log(interior?.placements);
260
+ ```
261
+
262
+ Every call returns independent arrays. The import performs no asset requests,
263
+ DOM work or GPU allocation. Resolve each source key against your admitted asset
264
+ catalogue and pass its URL, complete integrity map and placement to
265
+ `createSceneInspection` from `@plasius/gpu-shared/model-inspection`. Omit a layout
266
+ if any source is unavailable. Keep source licences/credits, translate the supplied
267
+ review copy as needed, and enforce the host application's access controls before
268
+ loading models. Dimensions are illustrative scene units. See the
269
+ [host integration contract](https://github.com/Plasius-LTD/gpu-demo-viewer/blob/main/docs/public-composite-definitions.md).
@@ -0,0 +1,7 @@
1
+ // Reviewed procedural public baseline from @plasius/gpu-shared@1.3.0.
2
+ export const showcaseAssetIntegrity = Object.freeze({
3
+ "brigantine": "sha384-iK6Q3rfU7L0mtjE/pgOJWrAaS0X56xrnmomRuGQkHaA24TvKra4BffwwDjhOxaNG",
4
+ "lighthouse": "sha384-rm8vGSiRziQ4V3IAFGJNiZqEzyvEpJDg/FvZNa2MxOrThSQ9HYIEVGzz9dN0S8Qb",
5
+ "harbor-dock": "sha384-4FJzP1Rk0Ll0IokVbzBWpY06m6zfWAUeALPC7Lh7refibUKF3Am+JWStJSlcC7cV",
6
+ "shoreline": "sha384-/hcbf3/xM+bdGaDnQliQ2r45WoCmZ769s1fpKneZ6QhIZGTDPGH2YNDQjKKP3MMO"
7
+ });
@@ -0,0 +1,20 @@
1
+ import { createCuratedShowcaseSource } from './source.js';
2
+ import { createFeedbackPanel } from './panel.js';
3
+
4
+ export async function mountDiagnosticShowcase(options) {
5
+ const keys=['root','feedbackRoot','resolveAccess'];
6
+ if(!options || Object.getPrototypeOf(options)!==Object.prototype ||
7
+ Reflect.ownKeys(options).length!==keys.length || keys.some(key=>!Object.hasOwn(options,key)) ||
8
+ typeof options.resolveAccess!=='function')throw new TypeError('Invalid diagnostics mount options.');
9
+ const pathname=globalThis.location?.pathname;
10
+ if(!['/gpu-demo','/gpu-demo/','/gpu-demo-viewer/showcase/','/gpu-demo-viewer/showcase/index.html'].includes(pathname)) {
11
+ throw new TypeError('This route is not registered for diagnostics.');
12
+ }
13
+ let access;
14
+ try {access=await options.resolveAccess();}catch {access=undefined;}
15
+ const source=await createCuratedShowcaseSource({root:options.root,sceneEnabled:access?.sceneEnabled===true});
16
+ try {
17
+ const panel=createFeedbackPanel({root:options.feedbackRoot,source,resolveAccess:options.resolveAccess,initialAccess:access});
18
+ return Object.freeze({refreshAccess:panel.refreshAccess,destroy(){panel.destroy();source.destroy();}});
19
+ } catch {source.destroy();throw new Error('Diagnostics controls could not be mounted.');}
20
+ }
@@ -0,0 +1,88 @@
1
+ import { parseFeedbackGameDiagnostics } from '@plasius/gpu-shared/feedback-diagnostics';
2
+
3
+ const available = value => value?.featureEnabled===true && value?.capabilityGranted===true && value?.sceneEnabled===true;
4
+
5
+ // Source is private to the public mount; this internal seam permits hostile-data
6
+ // and race tests without making arbitrary registrations a supported public API.
7
+ export function createFeedbackPanel({root,source,resolveAccess,initialAccess,document=globalThis.document,
8
+ copy=text=>globalThis.navigator.clipboard.writeText(text)}) {
9
+ const make=(tag,text)=>{const e=document.createElement(tag);if(text)e.textContent=text;return e;};
10
+ const section=make('section'),heading=make('h2','Optional renderer diagnostics');
11
+ const help=make('p','Share coarse renderer health only. No image, page content, names, locations or device identity is collected. Nothing is sent automatically.');
12
+ const label=make('label'),consent=make('input');consent.type='checkbox';consent.checked=false;
13
+ label.append(consent,make('span','I consent to preview and copy renderer diagnostics.'));
14
+ const prepare=make('button','Preview diagnostics'),copyButton=make('button','Copy diagnostics');
15
+ prepare.type=copyButton.type='button';copyButton.disabled=true;
16
+ const previewLabel=make('label','Diagnostics preview'),preview=make('textarea');
17
+ preview.readOnly=true;preview.value='';preview.rows=12;previewLabel.append(preview);
18
+ const status=make('p','Diagnostics are optional. Select consent to prepare a preview.');
19
+ status.setAttribute('role','status');status.setAttribute('aria-live','polite');
20
+ const style=make('style',`[data-feedback-diagnostics] {box-sizing:border-box;max-width:42rem;padding:1rem;color:#f4f7fb;background:#152234;font:1rem/1.5 system-ui;overflow-wrap:anywhere}
21
+ [data-feedback-diagnostics] label {display:block;min-height:44px;margin:.75rem 0}
22
+ [data-feedback-diagnostics] textarea {box-sizing:border-box;display:block;width:100%;max-width:100%;color:#f4f7fb;background:#101925;font:0.85rem/1.5 monospace}
23
+ [data-feedback-diagnostics] input {margin-right:.5rem}
24
+ [data-feedback-diagnostics] button {min-height:44px;margin:.25rem;padding:.5rem;color:#101925;background:#d7e7fa;border:2px solid #a3c6ed;border-radius:.25rem}
25
+ [data-feedback-diagnostics] :focus-visible {outline:3px solid #ffce64;outline-offset:3px}
26
+ [data-feedback-diagnostics] button:disabled {color:#526071;background:#d7e7fa}`);
27
+ section.setAttribute('data-feedback-diagnostics','');
28
+ section.hidden=!available(initialAccess);
29
+ section.append(style,heading,help,label,prepare,copyButton,previewLabel,status);root.append(section);
30
+ let revision=0,destroyed=false,text='';
31
+ function clear(message) {
32
+ revision++;text='';preview.value='';copyButton.disabled=true;
33
+ if(message)status.textContent=message;
34
+ }
35
+ function revoke(message) {consent.checked=false;section.hidden=true;clear(message);}
36
+ const consentChanged=()=>clear('Consent changed. Prepare a new preview before copying.');
37
+ async function permitted(token) {
38
+ try {
39
+ const decision=await resolveAccess();
40
+ if(destroyed || token!==revision || !consent.checked)return false;
41
+ if(!available(decision)){revoke('Diagnostics are unavailable. Ordinary bug reporting remains available.');return false;}
42
+ return true;
43
+ } catch {
44
+ if(!destroyed && token===revision)revoke('Diagnostics access could not be verified. Try again later.');
45
+ return false;
46
+ }
47
+ }
48
+ async function preparePreview() {
49
+ clear();const token=revision;
50
+ if(destroyed || !consent.checked){status.textContent='Select consent before preparing diagnostics.';return;}
51
+ if(!await permitted(token))return;
52
+ try {
53
+ const packet=await source.observe(true,()=>!destroyed && token===revision && consent.checked);
54
+ if(destroyed || token!==revision || !consent.checked)return;
55
+ if(!packet){revoke('This surface is not eligible for diagnostics.');return;}
56
+ text=JSON.stringify(parseFeedbackGameDiagnostics(packet),null,2);preview.value=text;
57
+ copyButton.disabled=false;status.textContent='Preview ready. Review these exact fields before copying.';
58
+ } catch {if(!destroyed && token===revision)revoke('Diagnostics could not be prepared safely.');}
59
+ }
60
+ async function copyPreview() {
61
+ if(destroyed || !text || !consent.checked)return;
62
+ const token=++revision;
63
+ if(!await permitted(token))return;
64
+ try {
65
+ const packet=await source.observe(true,()=>!destroyed && token===revision && consent.checked);
66
+ if(destroyed || token!==revision || !consent.checked)return;
67
+ if(!packet || JSON.stringify(parseFeedbackGameDiagnostics(packet),null,2)!==text){
68
+ clear('The surface changed. Prepare and review a new preview.');return;
69
+ }
70
+ // No await between the last consent check and invoking the explicit copy.
71
+ await copy(text);
72
+ if(!destroyed && token===revision)status.textContent='Diagnostics copied. Paste them only where you choose.';
73
+ } catch {if(!destroyed && token===revision)status.textContent='Copy unavailable. Select the preview text and copy it manually.';}
74
+ }
75
+ consent.addEventListener('change',consentChanged);
76
+ prepare.addEventListener('click',preparePreview);copyButton.addEventListener('click',copyPreview);
77
+ return Object.freeze({
78
+ async refreshAccess() {
79
+ revoke('Access changed. Select consent and prepare a new preview.');const token=revision;
80
+ try {
81
+ const decision=await resolveAccess();
82
+ if(!destroyed && token===revision)section.hidden=!available(decision);
83
+ } catch { /* Missing remote decisions leave the controls hidden. */ }
84
+ },
85
+ destroy() {if(destroyed)return;destroyed=true;revoke();consent.removeEventListener('change',consentChanged);
86
+ prepare.removeEventListener('click',preparePreview);copyButton.removeEventListener('click',copyPreview);section.remove();},
87
+ });
88
+ }
@@ -0,0 +1,39 @@
1
+ import { showcaseAssetIntegrity } from './assets.js';
2
+
3
+ // Internal mount seam for tests. The public entrypoint never accepts loaders or
4
+ // an existing runtime. Registration is a consequence of this fixed source mount.
5
+ export async function createCuratedShowcaseSource({root,sceneEnabled,
6
+ loadShared=()=>import('@plasius/gpu-shared'),
7
+ loadRenderer=()=>import('@plasius/gpu-renderer')}) {
8
+ let destroyed=false, runtime;
9
+ if(sceneEnabled===true) {
10
+ const shared=await loadShared();
11
+ const integrity=Object.fromEntries(Object.entries(showcaseAssetIntegrity).map(
12
+ ([key,digest])=>[String(shared.resolveShowcaseAssetUrl(key)),digest]));
13
+ runtime=await shared.mountGpuShowcase({root,focus:'integrated',
14
+ packageName:'@plasius/gpu-demo-viewer',
15
+ __featureFlags:{'gpu-demo.scene-fidelity.enabled':true,gpu_showcase_realistic_models_v1:false,
16
+ 'gpu.model.conversion.enabled':false},gltfLoadOptions:{integrity}});
17
+ }
18
+ function eligible() {
19
+ const state=runtime?.state;
20
+ return !destroyed && state?.assetCatalog?.mode==='modeled-baseline' &&
21
+ state.assetCatalog.fallbackReason===null && state.rendererUnavailable!==true &&
22
+ state.nativeRenderer?.backend==='webgpu-raster' && state.nativeRenderer.submittedFrames>0;
23
+ }
24
+ return Object.freeze({
25
+ async observe(consent,isCurrent=()=>true) {
26
+ if(consent!==true || !isCurrent() || !eligible())return null;
27
+ const {createFeedbackGameDiagnosticSnapshot}=await loadRenderer();
28
+ if(!isCurrent() || !eligible())return null;
29
+ const frame=runtime.state.nativeRenderer;
30
+ // Never pass the frame, canvas, telemetry, raw errors or other scene state
31
+ // across the renderer diagnostics boundary. Synthetic timing stays unknown.
32
+ return createFeedbackGameDiagnosticSnapshot({featureEnabled:true,capabilityGranted:true,
33
+ consentConfirmed:true,surfaceId:'site.gpu-demo',renderer:'webgpu',backend:'browser',
34
+ viewportWidth:frame.width,viewportHeight:frame.height,frameRate:undefined,frameTimeMs:undefined,
35
+ featureIds:['renderer.frame-loop'],counters:[],errorCodes:[]});
36
+ },
37
+ destroy() { if(destroyed)return;destroyed=true;runtime?.destroy();runtime=undefined; },
38
+ });
39
+ }
@@ -19,3 +19,12 @@ browser review with the registry package before release.
19
19
  The existing scene-fidelity feature flag and route capability continue to govern
20
20
  hosted product access. This local review does not add public-site navigation or
21
21
  change the existing localhost review gate.
22
+
23
+ ## Host-consumer contract — 21 October 2026
24
+
25
+ Story site#2321 / viewer Task #83 adds the typed `model-library/studies` subpath.
26
+ A pure function returns fresh copies of the same four layout definitions used by
27
+ the workshop. This keeps placement ownership here without coupling host apps to
28
+ the workshop UI, catalogue loading or its localhost gate. Hosts resolve their
29
+ own admitted sources and retain capability/rollout ownership. Shared inspection
30
+ still owns all rendering and resource limits. The local gate remains unchanged.
@@ -0,0 +1,42 @@
1
+ # ADR 0007: Consented diagnostics from a source-owned curated showcase
2
+
3
+ Status: accepted for Task gpu-demo-viewer#42, Story site#1671, Feature site#1665.
4
+
5
+ The shared registration contract and renderer bucketing API do not establish
6
+ that a caller's arbitrary canvas or scene is curated. The open model library
7
+ can contain private or unqualified assets. Existing debug snapshots contain
8
+ coordinates, names and broad runtime state.
9
+
10
+ The new `feedback-diagnostics` package subpath owns a fixed baseline showcase
11
+ mount. Its public API accepts only render/control roots and a remote access
12
+ resolver. Source registration is fixed to `site.gpu-demo` on exact approved
13
+ routes; generator, `/player-system`, model-library and arbitrary runtime
14
+ injection are excluded. Four procedural public shared-package assets are
15
+ integrity-pinned to the published 1.3.0 release. Missing assets, fallback,
16
+ unsubmitted frames, unavailable renderers and destroyed mounts fail closed.
17
+ The raw runtime stays in a closure. Only explicit native backend and viewport
18
+ numbers reach the released renderer bucketing helper. Synthetic timing is
19
+ reported as unknown. The resulting packet is validated by the shared parser.
20
+
21
+ The host resolves `feedback.game-diagnostics.enabled`, capability
22
+ `feedback.game-diagnostics.attach`, and `gpu-demo.scene-fidelity.enabled`.
23
+ Publishing does not grant access. No environment or query override exists.
24
+ Explicit consent and preview precede observation. Copy rechecks access and
25
+ surface eligibility, and requires a packet identical to the reviewed preview.
26
+ Revocation, access refresh and disposal invalidate pending asynchronous work.
27
+ Disable the diagnostics flag to roll back; ordinary structured reports remain.
28
+ Hosts must call `refreshAccess()` on pushed control changes and destroy on route
29
+ exit. Remount after changing the scene rollout decision.
30
+
31
+ Accepting caller-supplied IDs/provenance or filtering generic debug snapshots
32
+ was rejected because neither proves a curated source. Adding capture or
33
+ transport was rejected because the Feature prohibits client pixels. The new
34
+ boundary exposes an accessible optional preview/copy UI and performs no upload,
35
+ storage, diagnostic logging or automatic collection.
36
+
37
+ Tests cover denial/allow/rollback, exact routes, source provenance, immutable
38
+ schema output, forbidden fields, race cancellation and lifetime. Browser checks
39
+ cover explicit user flow, mobile/keyboard access and network/cache isolation.
40
+ Release uses PR CI followed by exact-main `cd.yml` in `production`; verify the
41
+ npm artifact before downstream site#1677 adopts it. No backend or secret mapping
42
+ changes occur in this static package release.
@@ -9,3 +9,4 @@
9
9
 
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
+ - [ADR 0007: Consented curated showcase diagnostics](adr-0007-consented-curated-showcase-diagnostics.md)
@@ -0,0 +1,82 @@
1
+ # Consented showcase diagnostics
2
+
3
+ Design for gpu-demo-viewer#42, Story site#1671, Feature site#1665.
4
+
5
+ Only a mount owned by this package can register `site.gpu-demo`. A new focused
6
+ entrypoint will mount the fixed baseline harbour using the published shared
7
+ runtime, four integrity-pinned procedural public assets, and the native renderer.
8
+ It will retain the runtime privately. It will accept no caller-provided surface
9
+ ID, provenance, scene, model, renderer loader, snapshot, warning or URL. Open
10
+ model-library studies, generator, arbitrary canvases and `/player-system` remain
11
+ ineligible. Asset failure, fallback rendering, no submitted frame, disposal or
12
+ unknown provenance disables diagnostics.
13
+
14
+ The host supplies a fresh remote decision for `feedback.game-diagnostics.enabled`,
15
+ `feedback.game-diagnostics.attach`, and `gpu-demo.scene-fidelity.enabled`.
16
+ Missing decisions deny access. This package adds no service endpoint or local
17
+ production override. The panel requires explicit unchecked consent and a preview
18
+ action before reading observations. Copy rechecks remote access; consent changes,
19
+ refresh, disposal and races clear or invalidate pending work. Normal structured
20
+ reporting is independent. Rollback disables the diagnostics flag.
21
+
22
+ Only native renderer/backend and coarse viewport buckets are observed. Timing is
23
+ unknown because existing showcase telemetry includes synthetic stress values.
24
+ No raw scene/debug snapshot is passed to the renderer helper. That released helper
25
+ constructs the closed packet, then the shared parser validates it. Preview is
26
+ read-only text; copying is an explicit clipboard operation. No automatic capture,
27
+ upload, download, browser storage, cache entry, analytics or log is introduced.
28
+
29
+ Alternatives: accepting any source-tagged object would permit forged provenance;
30
+ using `render_game_to_text` would disclose scene details; collecting images would
31
+ violate the Feature. This narrow mount adds a supported validation surface rather
32
+ than assuming every existing demo is eligible. Site integration/reconstruction is
33
+ the separate downstream site#1677.
34
+
35
+ Validation: tests derive from deny/allow/rollback/provenance/consent/lifetime
36
+ requirements, with changed-source LCOV, static package checks and real browser
37
+ keyboard/320px/no-pixel network/storage/cache checks. Release only via PR CI and
38
+ exact-main `cd.yml` in `production`, then verify npm artifact and provenance.
39
+
40
+ ## Dependency and compatibility decision
41
+
42
+ The direct shared dependency changes from `^1.2.0` (locked 1.2.0) to exact 1.3.0.
43
+ This adopts the published diagnostics contract verified under shared#101 and
44
+ fixes the bytes used by the four SRI entries. The renderer remains locked to
45
+ 0.2.54, with its existing compatible manifest range. Shared 1.3.0 brings its
46
+ declared model-conversion dependency graph; this mount explicitly disables
47
+ conversion and lazily loads the shared runtime only for a positive scene
48
+ decision. No unrelated direct dependency is updated. Package audits and
49
+ signature/attestation verification cover the new locked graph. A future shared
50
+ version requires an explicit asset/hash compatibility review rather than
51
+ silently accepting changed assets.
52
+
53
+ ## Browser qualification — 21 October 2026
54
+
55
+ An isolated in-app Chromium tab mounted the actual published shared/native
56
+ WebGPU renderer, with the four SRI-pinned assets. The local harness supplied
57
+ test access decisions only; no production controls changed. Keyboard activation
58
+ prepared the closed packet; explicit Copy reported success. No-consent preview
59
+ was denied. Revoking access immediately before Copy cleared preview and consent;
60
+ unchecking consent and disposal also removed retained diagnostics. A 320px-wide
61
+ panel had no horizontal overflow. This is actual narrow-container validation,
62
+ not an emulated 320px device viewport or a full multi-browser qualification.
63
+
64
+ The harness trapped canvas export/readback and display capture. It recorded
65
+ four public-asset fetches at mount, zero later fetches, zero non-GET requests,
66
+ zero request bodies and zero pixel reads. Resource entries stayed at 36 across
67
+ preview/copy/revocation; localStorage, sessionStorage, Cache Storage and IndexedDB
68
+ stayed empty. This combines browser observations with source/invariant tests;
69
+ it does not claim arbitrary third-party host code cannot perform network I/O.
70
+ No Chrome/Firefox/Safari cross-browser pass is inferred. Clipboard denial has a
71
+ tested selectable-preview fallback.
72
+
73
+ NFR disposition: security/privacy checked by integrity, closed packet, no-pixel,
74
+ rollback and race tests plus browser isolation evidence; accessibility checked
75
+ for labels, keyboard activation, live status, visible focus, contrast and 44px
76
+ action targets; performance checked through lazy loading and absence of new
77
+ sampling loops; reliability/reproducibility checked by clean install, build,
78
+ asset hashes and lifetime tests; maintainability/testability checked by ADR,
79
+ focused public entrypoint and changed-source LCOV; observability uses fixed
80
+ user-visible status messages without raw diagnostics logging. SEO is N/A for a
81
+ package subpath that creates no public route. Backend sensitive-environment
82
+ mapping is N/A because no backend or Azure resource changes are made.
@@ -0,0 +1,30 @@
1
+ # Composite definitions for host applications
2
+
3
+ Tracked work: site Story #2321 and viewer Task #83.
4
+
5
+ Expose the four existing qualified arrangements through the typed package
6
+ subpath `@plasius/gpu-demo-viewer/model-library/studies`. A pure
7
+ `compositeStudyDefinitions()` function returns fresh definitions containing the
8
+ stable key, source keys, bottom-centre anchors, longest dimensions, yaw and review
9
+ copy. Extract the existing definitions without changing any value. The workshop
10
+ and website then share one owner for placement.
11
+
12
+ The module must import no renderer, DOM, acquisition helper or local-review
13
+ bootstrap, and perform no I/O. Return independent arrays so consumer edits do not
14
+ alter another call or the workshop. Hosts resolve keys against their own admitted
15
+ catalogue, translate display copy, retain every credit/integrity entry, and apply
16
+ their capability/feature controls before calling released shared scene inspection.
17
+ The definitions are layout data, not entitlement decisions or additional models.
18
+
19
+ Parent Feature site#1170 / Epic site#2271 and inherited flag
20
+ `gpu-demo.scene-fidelity.enabled` apply. The local review gate stays unchanged;
21
+ public access is governed by the website's existing studio capability and GPU
22
+ runtime. Rollback uses those stored rules or reverts the consuming package version.
23
+
24
+ Before implementation, require tests for the supported export, its type/file
25
+ contract, exact unchanged scene membership and placement, isolated return values,
26
+ and continued workshop composition. Keep existing rendering/credit tests. Update
27
+ consumer README, Unreleased changelog and ADR0006. Run all package validation and
28
+ coverage with changed-source LCOV, publish only through approved main/production
29
+ CD after CI, and compare npm contents to the attested release artifact before
30
+ the site consumes the new export.
@@ -1,51 +1,8 @@
1
1
  import { fixtureRequest } from './controller.js';
2
-
3
- const definitions = [
4
- { key: 'study-furnished-interior', name: 'Furnished interior',
5
- renderer_target: 'Compare woven fabric, silk, leather, wicker and aged wood in one arrangement.',
6
- preparation: 'Inspect relative scale, fabric response and contact shadows. Sizes are chosen scene units, not verified physical dimensions.',
7
- placements: [
8
- ['SheenWoodLeatherSofa', [0, 0, -.8], 2.4, 0],
9
- ['vintage_cabinet_01', [-2.15, 0, -.8], 1.7, 0],
10
- ['SpecularSilkPouf', [.55, 0, .85], .7, 0],
11
- ['wicker_basket_01', [1.6, 0, -.3], .6, 0],
12
- ] },
13
- { key: 'study-workshop', name: 'Workshop still life',
14
- renderer_target: 'Compare worked metal, rough timber and worn surfaces under shared light.',
15
- preparation: 'Inspect object contact and the contrast between brass, painted metal and timber. These are static review arrangements.',
16
- placements: [
17
- ['spinning_wheel_01', [-1.1, 0, -.65], 1.8, 0],
18
- ['wine_barrel_01', [1.15, 0, -.7], 1.25, 0],
19
- ['wooden_crate_02', [0, 0, .7], 1.2, 0],
20
- ['bench_vice_01', [-1.3, 0, 1.05], .55, .3],
21
- // Crate lid height from the original accessor bounds and lid translation,
22
- // uniformly scaled to its chosen 1.2-unit longest dimension.
23
- ['brass_blowtorch', [0, .47735, .7], .55, 0],
24
- ] },
25
- { key: 'study-relic-collection', name: 'Relic collection',
26
- renderer_target: 'Compare carved stone, chess pieces and aged metal under shared light.',
27
- preparation: 'Inspect silhouette, surface detail and contact shadows across five originals. Dimensions are illustrative scene units.',
28
- placements: [
29
- ['gothic_statue', [-1.2, 0, -.7], 1.8, .15],
30
- ['marble_bust_01', [.65, 0, -.7], .85, -.25],
31
- ['chess_set', [0, 0, .8], 1.15, .1],
32
- ['kite_shield', [1.5, 0, .15], 1.15, -.4],
33
- ['antique_estoc', [-1.5, 0, .8], 1.25, .25],
34
- ] },
35
- { key: 'study-tool-chest', name: 'Tool-chest arrangement',
36
- renderer_target: 'Compare painted metal, rust and wooden handles in one working collection.',
37
- preparation: 'Inspect relative scale, worn surfaces and small contact shadows. These are static originals placed on a shared inspection floor.',
38
- placements: [
39
- ['metal_tool_chest', [0, 0, -.8], 1.7, 0],
40
- ['hand_plane_no4', [-.65, 0, .8], .5, -.25],
41
- ['vintage_hand_drill', [.25, 0, .9], .55, .3],
42
- ['rusted_spade_01', [1.25, 0, .05], 1.5, -.3],
43
- ['can_rusted', [-1.25, 0, .2], .4, 0],
44
- ] },
45
- ];
2
+ import { compositeStudyDefinitions } from './studies.js';
46
3
 
47
4
  export function compositeStudies(assets) {
48
- return definitions.flatMap(({ placements, ...definition }) => {
5
+ return compositeStudyDefinitions().flatMap(({ placements, ...definition }) => {
49
6
  const models = placements.map(([key, anchor, size, yaw]) => ({
50
7
  asset: assets.find(a => a.key === key), anchor: [...anchor], size, yaw,
51
8
  }));
@@ -0,0 +1,10 @@
1
+ /** Layout data only. Host applications own assets, credits and access decisions. */
2
+ export interface CompositeStudyDefinition {
3
+ key: string;
4
+ name: string;
5
+ renderer_target: string;
6
+ preparation: string;
7
+ /** Source key, bottom-centre anchor, longest dimension in scene units, Y rotation in radians. */
8
+ placements: [string, [number, number, number], number, number][];
9
+ }
10
+ export function compositeStudyDefinitions(): CompositeStudyDefinition[];
@@ -0,0 +1,51 @@
1
+ const definitions = [
2
+ { key: 'study-furnished-interior', name: 'Furnished interior',
3
+ renderer_target: 'Compare woven fabric, silk, leather, wicker and aged wood in one arrangement.',
4
+ preparation: 'Inspect relative scale, fabric response and contact shadows. Sizes are chosen scene units, not verified physical dimensions.',
5
+ placements: [
6
+ ['SheenWoodLeatherSofa', [0, 0, -.8], 2.4, 0],
7
+ ['vintage_cabinet_01', [-2.15, 0, -.8], 1.7, 0],
8
+ ['SpecularSilkPouf', [.55, 0, .85], .7, 0],
9
+ ['wicker_basket_01', [1.6, 0, -.3], .6, 0],
10
+ ] },
11
+ { key: 'study-workshop', name: 'Workshop still life',
12
+ renderer_target: 'Compare worked metal, rough timber and worn surfaces under shared light.',
13
+ preparation: 'Inspect object contact and the contrast between brass, painted metal and timber. These are static review arrangements.',
14
+ placements: [
15
+ ['spinning_wheel_01', [-1.1, 0, -.65], 1.8, 0],
16
+ ['wine_barrel_01', [1.15, 0, -.7], 1.25, 0],
17
+ ['wooden_crate_02', [0, 0, .7], 1.2, 0],
18
+ ['bench_vice_01', [-1.3, 0, 1.05], .55, .3],
19
+ // Crate lid height from the original accessor bounds and lid translation,
20
+ // uniformly scaled to its chosen 1.2-unit longest dimension.
21
+ ['brass_blowtorch', [0, .47735, .7], .55, 0],
22
+ ] },
23
+ { key: 'study-relic-collection', name: 'Relic collection',
24
+ renderer_target: 'Compare carved stone, chess pieces and aged metal under shared light.',
25
+ preparation: 'Inspect silhouette, surface detail and contact shadows across five originals. Dimensions are illustrative scene units.',
26
+ placements: [
27
+ ['gothic_statue', [-1.2, 0, -.7], 1.8, .15],
28
+ ['marble_bust_01', [.65, 0, -.7], .85, -.25],
29
+ ['chess_set', [0, 0, .8], 1.15, .1],
30
+ ['kite_shield', [1.5, 0, .15], 1.15, -.4],
31
+ ['antique_estoc', [-1.5, 0, .8], 1.25, .25],
32
+ ] },
33
+ { key: 'study-tool-chest', name: 'Tool-chest arrangement',
34
+ renderer_target: 'Compare painted metal, rust and wooden handles in one working collection.',
35
+ preparation: 'Inspect relative scale, worn surfaces and small contact shadows. These are static originals placed on a shared inspection floor.',
36
+ placements: [
37
+ ['metal_tool_chest', [0, 0, -.8], 1.7, 0],
38
+ ['hand_plane_no4', [-.65, 0, .8], .5, -.25],
39
+ ['vintage_hand_drill', [.25, 0, .9], .55, .3],
40
+ ['rusted_spade_01', [1.25, 0, .05], 1.5, -.3],
41
+ ['can_rusted', [-1.25, 0, .2], .4, 0],
42
+ ] },
43
+ ];
44
+
45
+ /** Independent layout data; the host resolves sources and controls access. */
46
+ export function compositeStudyDefinitions() {
47
+ return definitions.map(({ placements, ...definition }) => ({
48
+ ...definition,
49
+ placements: placements.map(([key, anchor, size, yaw]) => [key, [...anchor], size, yaw]),
50
+ }));
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plasius/gpu-demo-viewer",
3
- "version": "0.2.3",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Root validation viewer for Plasius gpu-* demos.",
6
6
  "license": "Apache-2.0",
@@ -19,7 +19,12 @@
19
19
  ".": "./viewer-manifest.js",
20
20
  "./viewer-manifest": "./viewer-manifest.js",
21
21
  "./package.json": "./package.json",
22
- "./model-library/acquire": "./model-library/acquire.js"
22
+ "./model-library/acquire": "./model-library/acquire.js",
23
+ "./model-library/studies": {
24
+ "types": "./model-library/studies.d.ts",
25
+ "default": "./model-library/studies.js"
26
+ },
27
+ "./feedback-diagnostics": "./diagnostics/index.js"
23
28
  },
24
29
  "files": [
25
30
  "assets/",
@@ -35,7 +40,8 @@
35
40
  "main.js",
36
41
  "styles.css",
37
42
  "viewer-manifest.js",
38
- "model-library/"
43
+ "model-library/",
44
+ "diagnostics/"
39
45
  ],
40
46
  "engines": {
41
47
  "node": ">=24"
@@ -47,7 +53,7 @@
47
53
  "lint": "npm run typecheck",
48
54
  "pack:check": "node scripts/verify-public-package.cjs",
49
55
  "prepublishOnly": "npm run build && npm run pack:check",
50
- "typecheck": "node --check index.js && node --check main.js && node --check viewer-manifest.js && node --check showcase/index.js && node --check showcase/main.js && node --check wavefront/main.js && node --check model-library/main.js && node --check model-library/controller.js && node --check model-library/scenes.js && node --check model-library/acquire.js && node --check model-library/rollout.js",
56
+ "typecheck": "node --check index.js && node --check main.js && node --check viewer-manifest.js && node --check showcase/index.js && node --check showcase/main.js && node --check wavefront/main.js && node --check model-library/main.js && node --check model-library/controller.js && node --check model-library/scenes.js && node --check model-library/acquire.js && node --check model-library/rollout.js && node --check model-library/studies.js && node --check diagnostics/index.js && node --check diagnostics/source.js && node --check diagnostics/panel.js && node --check diagnostics/assets.js",
51
57
  "test": "node --test",
52
58
  "models:download": "node scripts/acquire-model-library.mjs",
53
59
  "models:dev": "vite --config model-library/vite.config.js",
@@ -55,7 +61,7 @@
55
61
  },
56
62
  "dependencies": {
57
63
  "@plasius/gpu-renderer": "^0.2.54",
58
- "@plasius/gpu-shared": "^1.2.0"
64
+ "@plasius/gpu-shared": "1.3.0"
59
65
  },
60
66
  "devDependencies": {
61
67
  "c8": "^11.0.0",