generated from Labyricorn/labyricorn-project-template
Steps 6.4-6.7B — Local surfaces, reference-exhibit validation, SciFi Observation surface
One commit for the work accumulated in the working tree since Step 6.3, which had never been split into per-step commits: - src/local-surfaces.js + src/surface-url.js (new); src/ui.js, src/validation.js, src/connection.js and public/index.html updated for local-surface hosting and generic surface rendering - tests: local-surfaces (20), scifi-surfaces (24) and postmessage-interop (7) new; connection/museum-gallery/surface-validation suites updated - reference exhibits: shared/contract-core.js defaults to Contract 5.3 (major 5, minor 3, xzbt 5.3); museum-gallery advertises its surface catalog; aquarium/haunted-house/planetarium adapters updated - SciFi-XZBT (Step 6.7A/6.7B): surface-mode.js + surface-bus.js, Observation-surface boot branch, local-change hooks, view.pillars / view.warp-flight targets; fixture byte-identical to G:/.vibe/SciFi-XZBT - SciFi-XZBT contract adapter handshake fix: the inbound bridge filter no longer gates on an exact advisory xzbt value (Contract 5.3 §6.5), only on its presence/type, matching the host's own envelope validation; the adapter now advertises contract minor 3 / version 5.3.0, which it already implemented via the 5.3 surfaces field. Root cause of the five failing postmessage-interop tests (host hello was silently dropped). - docs: architecture 6.4 and 6.7A, reference 6.6 and 6.7; evidence logs; test-fixtures/PROVENANCE.md resync record Test results: NGN 154/154 (was 149/154); postmessage-interop 7/7 (was 2/7); SciFi contract harness 21/21, real-adapter suite 32/32. git diff --check clean for changed files; two pre-existing trailing-whitespace lines remain in test-fixtures/reference-exhibits/scifi/index.html, copied verbatim from the authoritative SciFi source. Step 6.7 live verification (browser Observation, packaged standalone) is still pending and is not claimed here.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* XZBT Contract Adapter — Specification v5.2
|
||||
* XZBT Contract Adapter — Specification v5.3
|
||||
*
|
||||
* Implements the normalized contract interface for SciFi-XZBT:
|
||||
* - Canonical external target catalog & descriptors
|
||||
@@ -15,10 +15,10 @@
|
||||
class XZBTContractAdapter {
|
||||
constructor(options = {}) {
|
||||
this.product = options.product || 'SciFi-XZBT';
|
||||
this.version = options.version || '5.2.0';
|
||||
this.version = options.version || '5.3.0';
|
||||
this.build = options.build || 'production';
|
||||
this.contractMajor = 5;
|
||||
this.contractMinor = 2;
|
||||
this.contractMinor = 3;
|
||||
|
||||
this.sessionId = null;
|
||||
this.sessionActive = false;
|
||||
@@ -28,6 +28,17 @@
|
||||
this.operationQueue = Promise.resolve();
|
||||
this.eventSink = null;
|
||||
|
||||
// Surface catalog (Contract 5.3 §31.2/§31.3). Advertised in describe()
|
||||
// only when non-empty, so absence stays §31.3 form 1 ("absent").
|
||||
this.surfaces = Array.isArray(options.surfaces) ? options.surfaces : null;
|
||||
this.instanceId = options.instanceId || null;
|
||||
|
||||
// Session-independent local-change hooks (Step 6.7B). Unlike
|
||||
// _emitEvent, these fire regardless of sessionActive so an
|
||||
// exhibit-internal surface mirror keeps working with no NGN attached.
|
||||
this.onLocalChange = typeof options.onLocalChange === 'function' ? options.onLocalChange : null;
|
||||
this.onLocalAction = typeof options.onLocalAction === 'function' ? options.onLocalAction : null;
|
||||
|
||||
// Transport hooks
|
||||
this.onMessage = options.onMessage || (() => {});
|
||||
this.hostOrigin = options.hostOrigin || null; // optional origin filter
|
||||
@@ -355,6 +366,31 @@
|
||||
requires: ['observation']
|
||||
});
|
||||
|
||||
reg({
|
||||
id: 'view.pillars',
|
||||
legacyId: 'observation-pillars',
|
||||
label: 'Window Pillars / Mullions',
|
||||
kind: 'state',
|
||||
valueType: 'boolean',
|
||||
readable: true,
|
||||
writable: true,
|
||||
restorable: true,
|
||||
category: 'observation',
|
||||
requires: ['observation']
|
||||
});
|
||||
|
||||
reg({
|
||||
id: 'view.warp-flight',
|
||||
label: 'Warp Flight Mode',
|
||||
kind: 'state',
|
||||
valueType: 'boolean',
|
||||
readable: true,
|
||||
writable: true,
|
||||
restorable: true,
|
||||
category: 'observation',
|
||||
requires: ['observation']
|
||||
});
|
||||
|
||||
reg({
|
||||
id: 'alert.active',
|
||||
label: 'Active Tactical Alert Status',
|
||||
@@ -416,9 +452,9 @@
|
||||
restorable: false,
|
||||
category: 'speech',
|
||||
requires: ['speech'],
|
||||
args: {
|
||||
text: { type: 'string', required: true, maxLength: 2000 }
|
||||
}
|
||||
arguments: [
|
||||
{ name: 'text', type: 'string', required: true, maxLength: 2000 }
|
||||
]
|
||||
});
|
||||
|
||||
reg({
|
||||
@@ -442,9 +478,9 @@
|
||||
restorable: false,
|
||||
category: 'display',
|
||||
requires: ['display-text'],
|
||||
args: {
|
||||
text: { type: 'string', required: true, maxLength: 512 }
|
||||
}
|
||||
arguments: [
|
||||
{ name: 'text', type: 'string', required: true, maxLength: 512 }
|
||||
]
|
||||
});
|
||||
|
||||
// --- 4. Canonical Universe Event Impulses ---
|
||||
@@ -555,7 +591,7 @@
|
||||
return;
|
||||
}
|
||||
// Ignore non-XZBT messages
|
||||
if (!e.data || typeof e.data !== 'object' || e.data.xzbt !== '5.2') {
|
||||
if (!e.data || typeof e.data !== 'object' || typeof e.data.xzbt !== 'string') {
|
||||
return;
|
||||
}
|
||||
if (e.data.sequence || e.data.type === 'error' || /\.result$/.test(e.data.type)) return;
|
||||
@@ -616,6 +652,15 @@
|
||||
stateRevision: this.stateRevision, target: target.id, value: after[target.id]
|
||||
}, correlationId, source);
|
||||
}
|
||||
// Session-independent mirror feed (Step 6.7B). _emitEvent above is
|
||||
// gated on sessionActive and therefore silent with no NGN attached;
|
||||
// onLocalChange is not, so an exhibit-internal surface mirror keeps
|
||||
// working standalone. Fires for every changed target, same as above.
|
||||
if (this.onLocalChange) {
|
||||
for (const target of changes) {
|
||||
this.onLocalChange(target.id, after[target.id], this.stateRevision);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -755,11 +800,13 @@
|
||||
|
||||
// Validate args schema & text length limits if defined
|
||||
const sanitizedArgs = (args && typeof args === 'object' && !Array.isArray(args)) ? { ...args } : {};
|
||||
if (Object.keys(sanitizedArgs).some(key => !target.args || !target.args[key])) {
|
||||
const argumentSpecs = target.arguments || [];
|
||||
if (Object.keys(sanitizedArgs).some(key => !argumentSpecs.some(spec => spec.name === key))) {
|
||||
return { ok: false, code: 'INVALID_VALUE', message: 'Unexpected argument key' };
|
||||
}
|
||||
if (target.args) {
|
||||
for (const [argKey, argSpec] of Object.entries(target.args)) {
|
||||
if (argumentSpecs.length) {
|
||||
for (const argSpec of argumentSpecs) {
|
||||
const argKey = argSpec.name;
|
||||
if (argSpec.required && (sanitizedArgs[argKey] === undefined || sanitizedArgs[argKey] === null)) {
|
||||
return { ok: false, code: 'INVALID_VALUE', message: `Missing required argument '${argKey}'` };
|
||||
}
|
||||
@@ -823,6 +870,11 @@
|
||||
args: sanitizedArgs
|
||||
}, correlationId, source);
|
||||
|
||||
// Session-independent mirror feed (Step 6.7B); see _commitChanges.
|
||||
if (this.onLocalAction) {
|
||||
this.onLocalAction(canonicalId, sanitizedArgs);
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@@ -836,7 +888,7 @@
|
||||
|
||||
// Public API: Protocol Description
|
||||
describe() {
|
||||
return {
|
||||
const description = {
|
||||
exhibit: {
|
||||
product: this.product,
|
||||
version: this.version,
|
||||
@@ -851,6 +903,10 @@
|
||||
capabilities: Array.from(this.capabilities.values()),
|
||||
targets: Array.from(this.targets.values())
|
||||
};
|
||||
if (this.surfaces && this.surfaces.length) {
|
||||
description.surfaces = this.surfaces;
|
||||
}
|
||||
return description;
|
||||
}
|
||||
|
||||
// Update Capability state and emit capability.changed
|
||||
|
||||
Reference in New Issue
Block a user