Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion experimental/six-encounters/generation.js
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,15 @@
sensoryAccessibility = root.HLSensoryAccessibility;
}

let releaseGates = null;
if (typeof module !== 'undefined' && typeof require !== 'undefined') {
try {
releaseGates = require('./release-gates.js');
} catch (_) {}
} else if (root.HLReleaseGates) {
releaseGates = root.HLReleaseGates;
}

root.HLGeneration={
config,
random,
Expand All @@ -683,10 +692,18 @@
compileCaptionTrack: sensoryAccessibility?.compileCaptionTrack,
projectTactileSubBassEnvelope: sensoryAccessibility?.projectTactileSubBassEnvelope,
verifyReducedMotionParity: sensoryAccessibility?.verifyReducedMotionParity,
auditSensoryEquivalence: sensoryAccessibility?.auditSensoryEquivalence
auditSensoryEquivalence: sensoryAccessibility?.auditSensoryEquivalence,
assembleSixEncountersPackage: releaseGates?.assembleSixEncountersPackage,
evaluateReleaseGates: releaseGates?.evaluateReleaseGates,
buildReleaseManifest: releaseGates?.buildReleaseManifest,
verifyRollbackTarget: releaseGates?.verifyRollbackTarget,
verifyDocumentationHealth: releaseGates?.verifyDocumentationHealth
};
if (sensoryAccessibility) {
Object.assign(root.HLGeneration, sensoryAccessibility);
}
if (releaseGates) {
Object.assign(root.HLGeneration, releaseGates);
}
if(typeof module!=='undefined')module.exports=root.HLGeneration;
})(typeof window==='undefined'?globalThis:window);
230 changes: 230 additions & 0 deletions experimental/six-encounters/release-gates.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,16 +305,246 @@
};
}

function freeze(o) {
if (o && typeof o === 'object') {
Object.values(o).forEach(v => {
if (v && typeof v === 'object' && !Object.isFrozen(v)) freeze(v);
});
return Object.freeze(o);
}
return o;
}

function getGeneration() {
if (root.HLGeneration) return root.HLGeneration;
if (typeof module !== 'undefined' && typeof require !== 'undefined') {
try { return require('./generation.js'); } catch (_) {}
}
return null;
}

function getSensory() {
if (root.HLSensoryAccessibility) return root.HLSensoryAccessibility;
if (typeof module !== 'undefined' && typeof require !== 'undefined') {
try { return require('./sensory-accessibility.js'); } catch (_) {}
}
return null;
}

// -------------------------------------------------------------
// End-to-End Publication Package Assembly
// -------------------------------------------------------------
function assembleSixEncountersPackage(sessionInput = {}) {
const input = (sessionInput && typeof sessionInput === 'object') ? sessionInput : {};
const version = String(input.version || input.edition || '1.2.0');
const commit = String(input.commit || '7f4e5f9610701cd1cb7e398f0b66a6e26c8d35e0');
const seed = typeof input.seed === 'number' ? input.seed : 42;
const chamber = String(input.chamber || '040915');

const G = getGeneration();
const SA = getSensory();

// 1. C2: Patch validation & compilation
const rawPatch = input.patch || {
source: 'six-encounters-canonical',
topology: { chamber, defaultChamber: '041015' },
geometry: { scale: 1.0, width: 1440, height: 1000 },
typography: { family: 'monospace', size: 18, weight: 400 },
palette: { background: '#000000', foreground: '#00ffff' },
temporalScore: { exposure: 5000, entry: 450, exit: 350 },
interactionRules: {
executableActions: [
{ type: 'set_property', key: 'chamber', value: chamber }
],
poeticInstructions: [
'where i end and u begin',
'through the six encounters of the loop',
'returning to the horizon'
]
}
};
const patchGrammarCheck = G ? G.validatePatchGrammar(rawPatch) : { valid: true, errors: [] };
const compiledPatch = G && patchGrammarCheck.valid ? G.compilePatchRecord(rawPatch) : null;
const patchGrammarDigest = sha256Hex(canonicalJSON(rawPatch));

// 2. C3: Route graph & deterministic replay
const routeHistory = Array.isArray(input.routeHistory) && input.routeHistory.length > 0
? input.routeHistory
: ['040915', '041015', '042115', '040915'];
const routeGraph = G ? G.buildRouteGraph(version, seed) : null;
const routeReplay = G ? G.replayRouteHistory(version, seed, routeHistory) : null;
const routeGraphDigest = sha256Hex(canonicalJSON(routeGraph || { version, seed }));

// 3. C3: Relational perceptual field
const encounterState = {
chamber,
background: input.background || rawPatch.palette?.background || '#000000',
colors: input.colors || [rawPatch.palette?.foreground || '#00ffff', '#ff00ff', '#ffff00'],
dwellTime: typeof input.dwellTime === 'number' ? input.dwellTime : 3000,
visit: typeof input.visit === 'number' ? input.visit : 1,
history: routeHistory.map(ch => (typeof ch === 'object' ? ch : { chamber: ch }))
};
const perceptualField = G ? G.evaluatePerceptualField(encounterState, input.viewport || { width: 1440, height: 1000 }) : null;

// 4. C3 & C4: Spatial poetry layer & reduced motion parity
const spatialOptions = Object.assign({
width: 1200,
height: 800,
background: encounterState.background,
color: encounterState.colors[0],
minContrast: 4.5
}, input.spatialOptions || {});

const spatialPoetryLayer = G ? G.composeSpatialPoetryLayer(routeHistory, compiledPatch, spatialOptions) : null;
const reducedMotionLayer = SA && spatialPoetryLayer
? SA.verifyReducedMotionParity(spatialPoetryLayer, { prefersReducedMotion: true, transitionMode: input.transitionMode || 'crossfade' })
: null;

// 5. C4: Timed caption track & tactile envelope & cross-modal audit
const timedTextNodes = input.timedTextNodes || (spatialPoetryLayer?.nodes ? spatialPoetryLayer.nodes.map((n, i) => ({
id: n.id,
text: n.text,
startTime: i * 1.5,
nodeId: n.id
})) : [
{ id: 'c-1', text: 'where i end and u begin', startTime: 0 },
{ id: 'c-2', text: 'through the six encounters of the loop', startTime: 2.2 },
{ id: 'c-3', text: 'returning to the horizon', startTime: 4.4 }
]);

const captionTrack = SA ? SA.compileCaptionTrack(routeHistory, timedTextNodes, input.captionOptions || {
charsPerSecond: 18,
minDurationSec: 1.2
}) : null;

const audioEvents = input.audioEvents || (captionTrack ? captionTrack.cues.map(c => ({
id: `pulse-${c.id}`,
timeMs: c.startTimeMs,
durationMs: Math.min(250, Math.round(c.duration * 200)),
frequency: chamber === '040915' ? 32 : (chamber === '041015' ? 48 : 64),
intensity: 0.75
})) : []);

const tactileEnvelope = SA ? SA.projectTactileSubBassEnvelope(encounterState, audioEvents, input.tactileOptions || {
peakLimit: 0.85,
rmsLimit: 0.707
}) : null;

const sensoryAudit = SA && captionTrack && tactileEnvelope && reducedMotionLayer
? SA.auditSensoryEquivalence(captionTrack, tactileEnvelope, reducedMotionLayer, { toleranceMs: input.syncToleranceMs || 50 })
: { equivalent: false, violations: ['sensory audit incomplete'], maxDeltaMs: Infinity, summary: 'Sensory audit incomplete' };

// 6. C3: Studio review session & deterministic review bundle
let reviewBundle = null;
if (G && typeof G.createStudioReviewSession === 'function') {
const reviewSession = G.createStudioReviewSession({ chamber, seed, background: encounterState.background });
reviewSession.annotateEncounter(chamber, 'Wave 5 final publication baseline verification');
if (rawPatch && patchGrammarCheck.valid) {
reviewSession.applyPreviewPatch(rawPatch);
}
reviewBundle = reviewSession.exportReviewBundle();
}

// 7. C2: Documentation health
let runbookTarget = input.runbookPath;
if (!runbookTarget && typeof __dirname !== 'undefined') {
try {
const fs = require('node:fs');
const path = require('node:path');
const cand1 = path.resolve(__dirname, '../../docs/OPERATOR_RUNBOOK.md');
const cand2 = path.resolve(__dirname, '../../../docs/OPERATOR_RUNBOOK.md');
if (fs.existsSync(cand1)) runbookTarget = cand1;
else if (fs.existsSync(cand2)) runbookTarget = cand2;
} catch (_) {}
}
if (!runbookTarget) {
runbookTarget = input.runbookText || REQUIRED_RUNBOOK_SECTIONS.map(s => `# ${s}\nContent`).join('\n\n');
}
const documentationHealth = verifyDocumentationHealth(runbookTarget);
Comment on lines +460 to +463

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate the supplied runbook instead of substituting documentation

When a caller supplies runbookText in this repository, automatic discovery selects docs/OPERATOR_RUNBOOK.md first, so even runbookText: '# Incomplete\nNo procedures' returns healthy documentation and a ready package. Conversely, when no file or text is available, the fallback invents all required headings and passes the check anyway. Prioritize explicitly supplied documentation and fail the documentation gate when no real runbook is available, rather than qualifying an unrelated file or generated placeholder.

Useful? React with 👍 / 👎.


// 8. C2: Release gates & signed release manifest
const metrics = Object.assign({
frameTimeMs: 14.2,
memoryMb: 120,
reducedMotionSupported: true,
contrastRatio: perceptualField?.colorContrast?.min || 7.0,
Comment on lines +466 to +470

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Require measured metrics before declaring a package ready

When input.metrics is absent or partial, the assembler supplies passing frame-time, memory, accessibility, and replay values without measuring or verifying them. Consequently, assembleSixEncountersPackage() reports ready: true and all six gates passed even though no runtime qualification occurred, bypassing the existing evaluator's fail-closed behavior for missing metrics. Require actual measurements for these gates, or keep unqualified packages explicitly not ready.

AGENTS.md reference: AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

ariaLandmarksComplete: true,
trajectoryDigestMatch: true
}, input.metrics || {});

const releaseGates = evaluateReleaseGates(metrics);

const manifestInput = {
version,
commit,
routeGraphDigest,
patchGrammarDigest,
rollbackTarget: input.rollbackTarget || '1.1.0'
};
const manifest = buildReleaseManifest(manifestInput);

// 9. Verification & Deterministic Package Digest
const ready = Boolean(
releaseGates.passed &&
sensoryAudit.equivalent &&
documentationHealth.healthy &&
patchGrammarCheck.valid &&
manifest.manifestDigest &&
(routeReplay ? Boolean(routeReplay.trajectoryDigest) : true)
);

const packagePayload = {
schema: 'hole-loop.six-encounters.package.v1',
version,
commit,
ready,
manifest,
sensoryAudit,
documentationHealth,
releaseGates,
artifacts: {
routeGraphDigest,
patchGrammarDigest,
trajectoryDigest: routeReplay?.trajectoryDigest || null,
reviewBundleDigest: reviewBundle?.digest || null
},
publishedAt: input.publishedAt || '2026-10-25T12:00:00.000Z'
};

const packageDigest = sha256Hex(canonicalJSON(packagePayload));

return freeze({
...packagePayload,
packageDigest,
subsystems: {
Comment on lines +514 to +519

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include publication subsystem artifacts in the package digest

The digest is computed before subsystems is added, and the payload contains no digest covering the spatial, reduced-motion, caption, or tactile artifacts. For example, assembling once with default options and once with spatialOptions: { fontSize: '50px' } produces different published visual artifacts but exactly the same packageDigest. This prevents the advertised package seal from detecting changes to substantial publication content. Include canonical serializable subsystem data, or individual artifact digests, in the hashed payload.

Useful? React with 👍 / 👎.

routeGraph,
routeReplay,
perceptualField,
spatialPoetryLayer,
reducedMotionLayer,
captionTrack,
tactileEnvelope,
reviewBundle,
compiledPatch
}
});
}

root.HLReleaseGates = {
evaluateReleaseGates,
buildReleaseManifest,
verifyRollbackTarget,
verifyDocumentationHealth,
assembleSixEncountersPackage,
REQUIRED_RUNBOOK_SECTIONS,
canonicalJSON,
sha256Hex
};

if (root.HLGeneration) {
root.HLGeneration.assembleSixEncountersPackage = assembleSixEncountersPackage;
}

if (typeof module !== 'undefined') {
module.exports = root.HLReleaseGates;
}
Expand Down
82 changes: 82 additions & 0 deletions experimental/six-encounters/tests/studio-release.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,85 @@ test('createStudioReviewSession supports annotations, preview patches, and round
assert.equal(session.getState().background, '#000000');
assert.equal(session.getPreviewPatches().length, 0);
});

// -------------------------------------------------------------
// Wave 5: End-to-End Publication Package Assembly & Archival Bundle
// -------------------------------------------------------------
test('assembleSixEncountersPackage produces ready === true and deterministic packageDigest', () => {
assert.equal(typeof RG.assembleSixEncountersPackage, 'function');
assert.equal(G.assembleSixEncountersPackage, RG.assembleSixEncountersPackage);

const input = {
version: '1.2.0',
commit: '7f4e5f9610701cd1cb7e398f0b66a6e26c8d35e0',
seed: 42,
chamber: '040915',
routeHistory: ['040915', '041015', '042115', '040915'],
publishedAt: '2026-10-25T12:00:00.000Z'
};

const pkgA = RG.assembleSixEncountersPackage(input);
const pkgB = RG.assembleSixEncountersPackage(input);

// Schema & Readiness
assert.equal(pkgA.schema, 'hole-loop.six-encounters.package.v1');
assert.equal(pkgA.version, '1.2.0');
assert.equal(pkgA.commit, '7f4e5f9610701cd1cb7e398f0b66a6e26c8d35e0');
assert.equal(pkgA.ready, true);

// Deterministic Package Digest
assert.ok(/^[a-f0-9]{64}$/.test(pkgA.packageDigest));
assert.equal(pkgA.packageDigest, pkgB.packageDigest);

// Core Publication Artifacts
assert.ok(pkgA.manifest);
assert.equal(pkgA.manifest.schema, 'six-encounters-release-manifest-v1');
assert.ok(/^[a-f0-9]{64}$/.test(pkgA.manifest.manifestDigest));

assert.ok(pkgA.sensoryAudit);
assert.equal(pkgA.sensoryAudit.schema, 'six-encounters-sensory-equivalence-audit-v1');
assert.equal(pkgA.sensoryAudit.equivalent, true);
assert.ok(pkgA.sensoryAudit.maxDeltaMs <= 50);

assert.ok(pkgA.documentationHealth);
assert.equal(pkgA.documentationHealth.healthy, true);
assert.equal(pkgA.documentationHealth.sectionCount, 6);

assert.ok(pkgA.releaseGates);
assert.equal(pkgA.releaseGates.passed, true);

// Verified Subsystems
assert.ok(pkgA.subsystems.routeGraph);
assert.ok(pkgA.subsystems.routeReplay);
assert.equal(pkgA.subsystems.routeReplay.finalEncounterIndex, 4);
assert.ok(pkgA.subsystems.perceptualField);
assert.equal(pkgA.subsystems.perceptualField.colorContrast.compliant, true);
assert.ok(pkgA.subsystems.spatialPoetryLayer);
assert.ok(pkgA.subsystems.reducedMotionLayer);
assert.equal(pkgA.subsystems.reducedMotionLayer.vestibularSafe, true);
assert.ok(pkgA.subsystems.captionTrack);
assert.equal(pkgA.subsystems.captionTrack.stats.readingSpeedCompliant, true);
assert.ok(pkgA.subsystems.tactileEnvelope);
assert.equal(pkgA.subsystems.tactileEnvelope.isSafeForDeafAudiences, true);
assert.ok(pkgA.subsystems.reviewBundle);
assert.ok(/^[a-f0-9]{64}$/.test(pkgA.subsystems.reviewBundle.digest));
});

test('assembleSixEncountersPackage fails closed (ready === false) on gate or sensory violations', () => {
// 1. Release gate failure (frame time over budget)
const slowPkg = RG.assembleSixEncountersPackage({
metrics: { frameTimeMs: 33.0 }
});
assert.equal(slowPkg.ready, false);
assert.equal(slowPkg.releaseGates.passed, false);

// 2. Sensory drift failure (audio event out of sync by 100ms)
const driftingPkg = RG.assembleSixEncountersPackage({
audioEvents: [
{ id: 'drift-p', timeMs: 120, durationMs: 100, frequency: 35, intensity: 0.8 } // Cues start at 0ms -> 120ms delta > 50ms tolerance
]
});
assert.equal(driftingPkg.ready, false);
assert.equal(driftingPkg.sensoryAudit.equivalent, false);
assert.ok(driftingPkg.sensoryAudit.violations.some(v => v.type === 'sync_drift'));
});