diff --git a/.github/workflows/architecture-canvas-tests.yml b/.github/workflows/architecture-canvas-tests.yml new file mode 100644 index 000000000..db3a2e29f --- /dev/null +++ b/.github/workflows/architecture-canvas-tests.yml @@ -0,0 +1,905 @@ +name: Architecture Canvas model tests +# Retained source-only review: graph_reader_design_review REQUEST_CHANGES on draft +# 245d7feddba7a0ba58a900ab461f2526f4405bfdcd7341e94fca1b951be49826 (865L/55552B): +# mountpoint probe errors could be consumed as ordinary absence. Corrected below; +# no hosted cleanup failure was observed. Runtime NOT_RUN, organizational independence 0. +'on': + pull_request: + paths: + - .github/workflows/architecture-canvas-tests.yml + - docs/plans/2026-10-09-canvas-model-contract.md + - interface/tests/cubic.test.mjs + - interface/tests/elder.test.mjs + - interface/src/scripts/cubic-model.mjs + - interface/src/scripts/elder-model.mjs + workflow_dispatch: +permissions: + contents: read +concurrency: + group: architecture-canvas-tests-${{ github.run_id }}-${{ github.run_attempt }} + cancel-in-progress: false +jobs: + canvas: + name: architecture-canvas-tests + runs-on: ubuntu-24.04 + timeout-minutes: 15 + env: + IMAGE: node:24.21.0-bookworm-slim@sha256:51b1100cc2a83d370c6a60952e3f2989c8a43159d0e38586e090f3b3326efefd + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + persist-credentials: false + - name: Establish native identity and bounded fixture filesystems + run: | + set -euo pipefail + base="$RUNNER_TEMP/canvas-tests" + mkdir -m 0700 "$base" + printf '%s\n' "$GITHUB_REPOSITORY" > "$base/repository.txt" + printf '%s\n' "$GITHUB_SHA" > "$base/checked-commit.txt" + printf '%s\n' "$GITHUB_RUN_ID" > "$base/run-id.txt" + printf '%s\n' "$GITHUB_RUN_ATTEMPT" > "$base/run-attempt.txt" + test "$(git rev-parse HEAD)" = "$GITHUB_SHA" + git show -s --format='%T%n%P' HEAD > "$base/checked-tree-and-parents.txt" + ruby -rpsych -e 'Psych.safe_load(File.read(ARGV.fetch(0)), aliases: false)' .github/workflows/architecture-canvas-tests.yml + for label in inputs work publish; do + mkdir "$base/$label" + truncate -s 64M "$base/$label.img" + mkfs.ext4 -F "$base/$label.img" > "$base/$label-filesystem.log" 2>&1 + sudo mount -o loop,nodev,nosuid,noexec "$base/$label.img" "$base/$label" + sudo mkdir "$base/$label/payload" + sudo chown "$(id -u):$(id -g)" "$base/$label/payload" + chmod 0700 "$base/$label/payload" + done + mkdir "$base/inputs/payload/source" "$base/inputs/payload/control" + sudo chown 65532:65532 "$base/work/payload" + sudo chmod 0755 "$base/work/payload" + python3 -I -B -S - <<'PY' + import hashlib, json, os, pathlib, stat + base = pathlib.Path(os.environ['RUNNER_TEMP']) / 'canvas-tests' + source = base / 'inputs/payload/source' + def require(value, message): + if not value: + raise ValueError(message) + def original(root, relative, limit, optional=False): + parts = relative.split('/') + root_fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + descriptor = None + try: + for part in parts[:-1]: + try: + next_fd = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=root_fd) + except FileNotFoundError: + if optional: + return None + raise + os.close(root_fd) + root_fd = next_fd + try: + descriptor = os.open(parts[-1], os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=root_fd) + except FileNotFoundError: + if optional: + return None + raise + before = os.fstat(descriptor) + require(stat.S_ISREG(before.st_mode) and before.st_nlink == 1 + and (0 if optional else 1) <= before.st_size <= limit, + 'checked source regular-file or size refusal') + pieces, count = [], 0 + while True: + chunk = os.read(descriptor, min(65536, limit + 1 - count)) + if not chunk: + break + pieces.append(chunk) + count += len(chunk) + require(count <= limit, 'checked source growth limit') + after = os.fstat(descriptor) + require(stat.S_ISREG(after.st_mode) and after.st_nlink == 1 + and (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns) + == (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns) + and count == before.st_size, 'checked source changed while read') + return b''.join(pieces) + finally: + if descriptor is not None: + os.close(descriptor) + os.close(root_fd) + pins = { + 'docs/plans/2026-10-09-canvas-model-contract.md': (13354, '3473a664be9126b67006527f5257a7eeb4c2082c836d7ff1b38e21f0d40eecbc'), + 'interface/tests/cubic.test.mjs': (5697, '7205e700bd4028aacb953663526c3563f835cbfea4e68566ce25f6cdf6ee3ac0'), + 'interface/tests/elder.test.mjs': (18595, '7db412a9b64d8ffbea8d9364eef5206e36acd06e5e36040ddb792420fd81e854'), + } + bindings = [] + for path in [*pins, 'interface/src/scripts/cubic-model.mjs', 'interface/src/scripts/elder-model.mjs']: + raw = original(os.environ['GITHUB_WORKSPACE'], path, 2**20, optional=path not in pins) + record = {'path': path, 'presence': 'ABSENT' if raw is None else 'PRESENT', + 'bytes': None if raw is None else len(raw), + 'sha256': None if raw is None else hashlib.sha256(raw).hexdigest()} + if path in pins: + require((record['bytes'], record['sha256']) == pins[path], 'external Canvas freeze mismatch') + if raw is not None: + output = source / path + output.parent.mkdir(parents=True, exist_ok=True) + with output.open('xb') as stream: + stream.write(raw) + output.chmod(0o444) + bindings.append(record) + workflow = original(os.environ['GITHUB_WORKSPACE'], '.github/workflows/architecture-canvas-tests.yml', 262144) + with (base / 'publish/payload/checked-workflow.yml').open('xb') as stream: + stream.write(workflow) + context = {key: os.environ[variable] for key, variable in ( + ('repository', 'GITHUB_REPOSITORY'), ('checked_commit', 'GITHUB_SHA'), + ('run_id', 'GITHUB_RUN_ID'), ('run_attempt', 'GITHUB_RUN_ATTEMPT'))} + context['workflow_sha256'] = hashlib.sha256(workflow).hexdigest() + context['sources'] = bindings + with (base / 'inputs/payload/control/source-bindings.json').open('x') as stream: + stream.write(json.dumps(context, sort_keys=True, indent=2) + '\n') + filesystems = {} + for label in ('inputs', 'work', 'publish'): + mount = base / label + require(os.path.ismount(mount), 'fixture filesystem mount not observed') + info, capacity = os.stat(mount, follow_symlinks=False), os.statvfs(mount) + size = capacity.f_frsize * capacity.f_blocks + require(stat.S_ISDIR(info.st_mode) and 0 < size <= 64 * 2**20, 'actual fixture filesystem capacity refusal') + filesystems[label] = {'host_device': info.st_dev, 'reported_total_bytes': size, + 'configured_capacity_bytes': 64 * 2**20, 'mount_observed': True} + with (base / 'fixture-filesystems.json').open('x') as stream: + stream.write(json.dumps(filesystems, sort_keys=True, indent=2) + '\n') + PY + - name: Prepare the fixed bundled-Node execution control + run: | + set -euo pipefail + control="$RUNNER_TEMP/canvas-tests/inputs/payload/control" + cat > "$control/run.mjs" <<'JS' + import fs from 'node:fs'; + import crypto from 'node:crypto'; + import assert from 'node:assert/strict'; + import { spawn } from 'node:child_process'; + const node = '/usr/local/bin/node'; + const output = '/output/'; + const digest = raw => crypto.createHash('sha256').update(raw).digest('hex'); + const write = (name, value) => fs.writeFileSync(output + name, + typeof value === 'string' ? value : JSON.stringify(value, null, 2) + '\n', + { flag: 'wx', mode: 0o644 }); + const regular = (path, limit, allowEmpty = false, hashOnly = false) => { + const fd = fs.openSync(path, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK); + try { + const before = fs.fstatSync(fd); + assert.ok(before.isFile() && before.nlink === 1 && before.size >= (allowEmpty ? 0 : 1) + && before.size <= limit, 'regular byte admission'); + const pieces = []; + const hasher = hashOnly ? crypto.createHash('sha256') : null; + let count = 0; + for (;;) { + const part = Buffer.allocUnsafe(Math.min(65536, limit + 1 - count)); + const got = fs.readSync(fd, part, 0, part.length, null); + if (got === 0) break; + if (hashOnly) hasher.update(part.subarray(0, got)); + else pieces.push(part.subarray(0, got)); + count += got; + assert.ok(count <= limit, 'regular file growth admission'); + } + const after = fs.fstatSync(fd); + assert.ok(after.isFile() && after.nlink === 1); + for (const key of ['dev', 'ino', 'size', 'mtimeMs', 'ctimeMs']) assert.equal(after[key], before[key]); + assert.equal(count, before.size); + return hashOnly ? { bytes: count, sha256: hasher.digest('hex') } : Buffer.concat(pieces); + } finally { fs.closeSync(fd); } + }; + const bindings = JSON.parse(regular('/control/source-bindings.json', 65536)); + const sourceObservation = () => bindings.sources.map(record => { + const path = '/source/' + record.path; + if (record.presence === 'ABSENT') { + assert.ok(!fs.existsSync(path), 'absent candidate was introduced'); + return record; + } + const raw = regular(path, 2**20, true); + assert.equal(raw.length, record.bytes); + assert.equal(digest(raw), record.sha256); + return { ...record }; + }); + const runtime = () => { + assert.equal(process.version, 'v24.21.0'); + assert.equal(process.versions.node, '24.21.0'); + assert.equal(process.platform, 'linux'); + assert.equal(process.arch, 'x64'); + assert.equal(process.execPath, node); + assert.equal(fs.realpathSync(node), node); + assert.equal(process.getuid(), 65532); + assert.equal(process.getgid(), 65532); + const binary = regular(node, 256 * 2**20, false, true); + return { version: process.version, versions: process.versions, platform: process.platform, + arch: process.arch, uid: process.getuid(), gid: process.getgid(), + executable: process.execPath, executable_bytes: binary.bytes, + executable_sha256: binary.sha256, runtime_origin: 'PINNED_IMAGE_BUNDLED_BINARY', + earlier_extracted_binary_credit: 'NOT_REUSED' }; + }; + const inventory = [ + ['cubic', [ + 'PREFLIGHT cubic candidate imports and exports cubicModel', + 'BEHAVIOR cubic literal binary fixture retains all scales and affine teaching coordinates', + 'BEHAVIOR cubic halving radius divides the height gap by eight without rescaling k', + 'BEHAVIOR cubic nonbinary arithmetic and negative origin preserve nominal normalization', + 'BEHAVIOR cubic rejects malformed records and coercible or missing numeric fields', + 'BEHAVIOR cubic rejects invalid ranges overflow underflow and collapsed height windows', + 'BEHAVIOR cubic does not mutate inputs or share mutable results across calls', + ]], + ['elder', [ + 'PREFLIGHT elder candidate imports and exports finiteElderTrace', + 'BEHAVIOR elder unequal peaks retain the elder and longest finite bar', + 'BEHAVIOR elder full tie blocks and neutral edges have distinct meanings', + 'BEHAVIOR elder periodic seams join peaks before a spurious death', + 'BEHAVIOR elder axis graph does not connect diagonal square corners', + 'BEHAVIOR elder equal peaks prefer the smaller plateau ID', + 'BEHAVIOR elder negative constant plateau has one essential class and eight zero merges', + 'BEHAVIOR elder large integer translation and positive scaling preserve exact endpoints', + 'BEHAVIOR elder all 512 binary grids satisfy every threshold rank and exact trace', + 'BEHAVIOR elder deterministic tied multilevel fields satisfy complete birth and rank oracles', + 'BEHAVIOR elder rejects malformed record dimension scale shape and sparse integer samples', + 'BEHAVIOR elder accepts the teaching cap with linear event storage', + 'BEHAVIOR elder does not mutate input or share returned state across deterministic calls', + 'ORACLE sensitivity rejects omitted shifted mislabelled and malformed literal barcodes', + ]], + ]; + const execute = (name) => new Promise(resolve => { + const argv = ['--test', '--test-reporter=tap', '--test-concurrency=1', + '/source/interface/tests/' + name + '.test.mjs']; + const stdout = fs.openSync(output + name + '.tap', 'wx', 0o644); + const stderr = fs.openSync(output + name + '.stderr', 'wx', 0o644); + const started = process.hrtime.bigint(); + let timedOut = false, error = null, killer = null; + const child = spawn(node, argv, { cwd: '/source', stdio: ['ignore', stdout, stderr], + env: { PATH: '/usr/local/bin:/usr/bin:/bin', LANG: 'C.UTF-8', TZ: 'UTC', + NODE_DISABLE_COLORS: '1', NODE_OPTIONS: '' } }); + const timer = setTimeout(() => { + timedOut = true; + child.kill('SIGTERM'); + killer = setTimeout(() => child.kill('SIGKILL'), 5000); + }, 60000); + child.on('error', value => { error = String(value.message).slice(0, 2048); }); + child.on('close', (code, signal) => { + clearTimeout(timer); + if (killer !== null) clearTimeout(killer); + fs.closeSync(stdout); + fs.closeSync(stderr); + resolve({ file: name, executable: node, argv, attempts: 1, code, signal, error, + timed_out: timedOut, duration_ns: String(process.hrtime.bigint() - started), + stdout: name + '.tap', stderr: name + '.stderr' }); + }); + }); + const parse = (attempt, names) => { + const raw = regular(output + attempt.stdout, 4 * 2**20); + const text = raw.toString('utf8'); + assert.equal(Buffer.from(text).compare(raw), 0, 'TAP UTF-8 refusal'); + const lines = text.split('\n'); + const records = []; + const starts = []; + for (let index = 0; index < lines.length; index++) { + const match = /^(not ok|ok) ([1-9][0-9]*) - (.*?)(?: # (SKIP|TODO)(?: (.*))?)?$/.exec(lines[index]); + if (!match) { + assert.ok(!/^(?:not ok|ok)(?:\s|$)|^Bail out!/.test(lines[index]), 'malformed result or TAP bailout'); + continue; + } + assert.ok(starts.length < names.length, 'extra TAP result'); + starts.push({ line: index, match }); + } + for (let index = 0; index < starts.length; index++) { + const { line, match } = starts[index]; + const end = index + 1 < starts.length ? starts[index + 1].line : lines.length; + const diagnostic = lines.slice(line + 1, end).join('\n'); + const cancelled = /failureType: ['"]cancelledByParent['"]/.test(diagnostic); + records.push({ local_id: Number(match[2]), id: match[3], status: cancelled ? 'CANCELLED' : + match[4] || (match[1] === 'ok' ? 'PASS' : 'FAIL'), reason: match[5] || null, + assertion_diagnostic: match[1] === 'not ok' && /code: ['"]ERR_ASSERTION['"]/.test(diagnostic), + diagnostic_sha256: digest(Buffer.from(diagnostic)) }); + } + const one = pattern => { + const matches = lines.map(line => pattern.exec(line)).filter(Boolean); + assert.equal(matches.length, 1, 'missing or duplicate TAP summary'); + return Number(matches[0][1]); + }; + const summary = Object.fromEntries(['tests', 'suites', 'pass', 'fail', 'cancelled', 'skipped', 'todo'] + .map(key => [key, one(new RegExp('^# ' + key + ' ([0-9]+)$'))])); + assert.equal(lines.filter(line => line === 'TAP version 13').length, 1); + assert.equal(one(/^1\.\.([0-9]+)$/), names.length); + assert.equal(records.length, names.length); + assert.equal(summary.tests, names.length); + assert.equal(summary.suites, 0); + for (let index = 0; index < names.length; index++) { + assert.equal(records[index].local_id, index + 1); + assert.equal(records[index].id, names[index], 'unexpected or duplicate TAP identity'); + } + const counts = { PASS: 0, FAIL: 0, CANCELLED: 0, SKIP: 0, TODO: 0 }; + for (const record of records) counts[record.status]++; + assert.equal(summary.pass, counts.PASS); + assert.equal(summary.fail, counts.FAIL); + assert.equal(summary.cancelled, counts.CANCELLED); + assert.equal(summary.skipped, counts.SKIP); + assert.equal(summary.todo, counts.TODO); + assert.ok(attempt.code === 0 || attempt.code === 1, 'test runner terminal refusal'); + assert.equal(attempt.signal, null); + assert.equal(attempt.error, null); + assert.equal(attempt.timed_out, false); + const preflight = records[0]; + const behavior = records.filter(record => record.id.startsWith('BEHAVIOR ')); + const oracle = records.filter(record => record.id.startsWith('ORACLE ')); + const setupBlocked = preflight.status === 'FAIL'; + if (setupBlocked) assert.ok(behavior.every(record => record.status === 'SKIP'), 'blocked candidate executed behavior'); + else assert.equal(preflight.status, 'PASS', 'preflight skip or cancellation refusal'); + return { attempt, raw_sha256: digest(raw), raw_bytes: raw.length, summary, counts, records, + preflight: setupBlocked ? 'SETUP_BLOCKED' : 'IMPORTED_EXPECTED_FUNCTION', + behavior_result: setupBlocked ? 'NOT_RUN' : + behavior.every(record => record.status === 'PASS') ? 'PASS' : 'FAIL_OR_INCOMPLETE', + oracle_result: oracle.length === 0 ? 'NOT_APPLICABLE' : + oracle.every(record => record.status === 'PASS') ? 'PASS' : 'CHECKER_FAILURE' }; + }; + const report = { schema_version: 1, context: bindings, source_declared_controls: 21, + source_declared_preflight: 2, source_declared_behavior: 18, source_declared_oracle: 1, + status: 'RUNTIME_SETUP_BLOCKED', behavior_result: 'NOT_RUN', actual_controls: [], files: [], + scientific_effect: 'NONE', scientific_status_authority: false, organizational_independence: 0, + custody_authentication: 'UNKNOWN', genuine_behavioral_red: 'NOT_DECLARED_BY_WORKFLOW', + declared_control_inventory: inventory.flatMap(([file, names], fileIndex) => names.map((id, index) => ({ file, + local_id: index + 1, global_id: index + 1 + (fileIndex === 0 ? 0 : inventory[0][1].length), + id, declaration_status: 'SOURCE_DECLARED_RUNTIME_NOT_PREDICTED' }))) }; + let finalExit = 1; + try { + write('runtime-before.json', runtime()); + write('source-before.json', sourceObservation()); + write('control-source.json', { path: '/control/run.mjs', + sha256: digest(regular('/control/run.mjs', 262144)) }); + const attempts = []; + for (const [name] of inventory) attempts.push(await execute(name)); + write('attempts.json', attempts); + report.status = 'DISCOVERY_FAILED'; + for (let index = 0; index < inventory.length; index++) { + const [name, names] = inventory[index]; + try { report.files.push(parse(attempts[index], names)); } + catch (error) { report.files.push({ file: name, status: 'DISCOVERY_FAILED', + error: String(error.message).slice(0, 2048), attempt: attempts[index] }); } + } + report.actual_controls = report.files.flatMap((file, index) => (file.records || []).map(record => ({ ...record, + file: inventory[index][0], global_id: record.local_id + (index === 0 ? 0 : inventory[0][1].length) }))); + const complete = report.files.every(file => file.records !== undefined); + if (complete) { + const blocked = report.files.some(file => file.preflight === 'SETUP_BLOCKED'); + const checkerFailed = report.files.some(file => file.oracle_result === 'CHECKER_FAILURE'); + const allPass = report.actual_controls.every(record => record.status === 'PASS') + && report.files.every(file => file.attempt.code === 0); + report.status = blocked ? 'SETUP_BLOCKED' : checkerFailed ? 'CHECKER_FAILURE' : + allPass ? 'SCOPED_GREEN' : 'BEHAVIOR_FAILURE_OR_INCOMPLETE'; + report.behavior_result = blocked ? 'PER_FILE_NOT_RUN_WHERE_SETUP_BLOCKED' : + allPass ? 'PASS' : 'SEE_ACTUAL_CONTROL_STATUS'; + report.behavior_assertion_failures = report.actual_controls.filter(record => + record.id.startsWith('BEHAVIOR ') && record.status === 'FAIL' && record.assertion_diagnostic).map(record => record.id); + report.red_candidate_requires_source_and_native_custody_review = !blocked && !checkerFailed + && report.behavior_assertion_failures.length > 0; + if (allPass && !blocked && !checkerFailed) finalExit = 0; + } + write('source-after.json', sourceObservation()); + const after = runtime(); + const before = JSON.parse(regular(output + 'runtime-before.json', 65536)); + assert.equal(after.executable_sha256, before.executable_sha256); + assert.equal(after.executable_bytes, before.executable_bytes); + write('runtime-after.json', after); + } catch (error) { + report.status = 'RUNTIME_OR_SOURCE_CONTROL_FAILED'; + report.control_error = String(error.message).slice(0, 2048); + finalExit = 1; + } + report.control_exit_code = finalExit; + write('results.json', report); + write('control-exit-status.txt', String(finalExit) + '\n'); + console.log(JSON.stringify({ status: report.status, actual_discovered: report.actual_controls.length, + counts: report.actual_controls.reduce((counts, item) => ({ ...counts, + [item.status]: (counts[item.status] || 0) + 1 }), {}), control_exit_code: finalExit })); + process.exitCode = finalExit; + JS + chmod -R a-w "$RUNNER_TEMP/canvas-tests/inputs/payload" + find "$RUNNER_TEMP/canvas-tests/inputs/payload" -type d -exec chmod 0555 {} + + chmod 0444 "$control/run.mjs" "$control/source-bindings.json" + - name: Pull the immutable bundled runtime + run: | + set -euo pipefail + base="$RUNNER_TEMP/canvas-tests" + (ulimit -f 4096; timeout --signal=TERM --kill-after=10s 180s docker pull --platform linux/amd64 "$IMAGE") > "$base/image-pull.log" 2>&1 + docker image inspect "$IMAGE" > "$base/runtime-image.json" + - name: Run the two external test files once in the isolated container + run: | + set -euo pipefail + base="$RUNNER_TEMP/canvas-tests" + container="canvas-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" + printf '%s\n' "$container" > "$base/container-name.txt" + docker create --name "$container" --platform linux/amd64 --network none --user 65532:65532 \ + --ipc private --cgroupns private \ + --read-only --cap-drop ALL --security-opt no-new-privileges --log-driver none \ + --cpus 2 --memory 1g --memory-swap 1g --pids-limit 128 \ + --ulimit fsize=4194304:4194304 --ulimit nofile=256:256 \ + --tmpfs /tmp:rw,noexec,nosuid,nodev,size=64m,mode=1777 \ + --mount type=bind,src="$base/inputs/payload/source",dst=/source,readonly \ + --mount type=bind,src="$base/inputs/payload/control",dst=/control,readonly \ + --mount type=bind,src="$base/work/payload",dst=/output \ + --workdir /source --env LANG=C.UTF-8 --env TZ=UTC --env NODE_DISABLE_COLORS=1 --env NODE_OPTIONS= \ + --entrypoint /usr/local/bin/node "$IMAGE" /control/run.mjs > "$base/container-id.txt" + docker inspect "$container" > "$base/container-before.json" + set +e + (ulimit -f 4096; timeout --signal=TERM --kill-after=10s 180s docker start --attach "$container") > "$base/container-console.log" 2>&1 + attach=$? + set -e + printf '%s\n' "$attach" > "$base/attach-exit-status.txt" + docker inspect "$container" > "$base/container-after.json" + test "$attach" = 0 || test "$attach" = 1 + - name: Confirm strict container disposal + if: always() + run: | + set -euo pipefail + base="$RUNNER_TEMP/canvas-tests" + mkdir -p "$base" + container="canvas-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" + present="$(docker ps -a --filter "name=^/$container$" --format '{{.Names}}')" + if test -n "$present"; then + test "$present" = "$container" + docker rm -f "$container" > "$base/disposal.log" 2>&1 + else + printf '%s\n' 'No named container remained to remove.' > "$base/disposal.log" + fi + remaining="$(docker ps -a --filter "name=^/$container$" --format '{{.Names}}')" + test -z "$remaining" + printf '%s\n' 'CONFIRMED_ABSENT' > "$base/disposal-status.txt" + - name: Collect bounded original records only after confirmed disposal + if: always() + run: | + set -euo pipefail + python3 -I -B -S - <<'PY' + import hashlib, json, os, pathlib, stat + base = pathlib.Path(os.environ['RUNNER_TEMP']) / 'canvas-tests' + publish_root = base / 'publish' + destination = publish_root / 'payload' + destination.mkdir(parents=True, exist_ok=True) + def require(condition, message): + if not condition: + raise ValueError(message) + def original(root, relative, limit, allow_empty=True): + parts = relative.split('/') + current = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + descriptor = None + try: + for part in parts[:-1]: + nxt = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=current) + os.close(current) + current = nxt + descriptor = os.open(parts[-1], os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=current) + before = os.fstat(descriptor) + require(stat.S_ISREG(before.st_mode) and before.st_nlink == 1 + and (0 if allow_empty else 1) <= before.st_size <= limit, 'original regular byte admission') + chunks, count = [], 0 + while True: + part = os.read(descriptor, min(65536, limit + 1 - count)) + if not part: + break + chunks.append(part) + count += len(part) + require(count <= limit, 'original growth limit') + after = os.fstat(descriptor) + require(stat.S_ISREG(after.st_mode) and after.st_nlink == 1 + and (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns) + == (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns) + and count == before.st_size, 'original changed while read') + return b''.join(chunks) + finally: + if descriptor is not None: + os.close(descriptor) + os.close(current) + require(original(base, 'disposal-status.txt', 64, False) == b'CONFIRMED_ABSENT\n', + 'collection precedes confirmed disposal') + missing, refused, copied, total = [], [], [], 0 + specifications = [(base, name, name, limit) for name, limit in ( + ('repository.txt', 512), ('checked-commit.txt', 128), ('run-id.txt', 128), ('run-attempt.txt', 128), + ('checked-tree-and-parents.txt', 512), ('inputs-filesystem.log', 65536), + ('work-filesystem.log', 65536), ('publish-filesystem.log', 65536), ('image-pull.log', 4 * 2**20), + ('runtime-image.json', 2**20), ('fixture-filesystems.json', 65536), ('container-name.txt', 256), ('container-id.txt', 128), + ('container-before.json', 2**20), ('container-after.json', 2**20), ('container-console.log', 4 * 2**20), + ('attach-exit-status.txt', 64), ('disposal.log', 65536), ('disposal-status.txt', 64))] + specifications += [(base / 'inputs/payload/control', name, name, 262144) + for name in ('run.mjs', 'source-bindings.json')] + specifications += [(base / 'work/payload', name, name, 4 * 2**20) + for name in ('cubic.tap', 'cubic.stderr', 'elder.tap', 'elder.stderr')] + specifications += [(base / 'work/payload', name, name, 2**20) for name in + ('runtime-before.json', 'runtime-after.json', 'source-before.json', 'source-after.json', + 'control-source.json', 'attempts.json', 'results.json', 'control-exit-status.txt')] + source_names = [ + 'docs/plans/2026-10-09-canvas-model-contract.md', 'interface/tests/cubic.test.mjs', + 'interface/tests/elder.test.mjs', 'interface/src/scripts/cubic-model.mjs', + 'interface/src/scripts/elder-model.mjs'] + expected_absent = set() + try: + bindings = json.loads(original(base / 'inputs/payload/control', 'source-bindings.json', 262144, False)) + expected_absent = {record['path'] for record in bindings['sources'] if record['presence'] == 'ABSENT'} + except (OSError, ValueError, KeyError, TypeError): + pass + specifications += [(base / 'inputs/payload/source', name, 'checked-sources/' + name, 2**20) + for name in source_names if name not in expected_absent] + for root, name, output, limit in specifications: + try: + raw = original(root, name, limit) + total += len(raw) + require(total <= 32 * 2**20, 'collector attempted-copy subtotal limit') + target = destination / output + target.parent.mkdir(parents=True, exist_ok=True) + with target.open('xb') as stream: + stream.write(raw) + copied.append({'path': output, 'bytes': len(raw), 'sha256': hashlib.sha256(raw).hexdigest()}) + except FileNotFoundError: + missing.append(output) + except (OSError, ValueError): + refused.append(output) + mounted = os.path.ismount(publish_root) + actual = None + if mounted: + info, capacity = os.stat(publish_root, follow_symlinks=False), os.statvfs(publish_root) + require(stat.S_ISDIR(info.st_mode), 'publication root type refusal') + actual = {'host_device': info.st_dev, 'reported_total_bytes': capacity.f_frsize * capacity.f_blocks} + record = {'schema_version': 1, 'missing': sorted(missing), 'refused': sorted(refused), 'copied': copied, + 'expected_absent_candidate_paths': sorted(expected_absent), 'collector_counted_copy_input_bytes': total, + 'collector_copy_limit_bytes': 32 * 2**20, 'collector_counter_may_include_refused_copy_attempts': True, + 'collector_subtotal_excludes_prior_originals_and_its_metadata': True, + 'configured_publication_filesystem_capacity_bytes': 64 * 2**20, + 'publication_mount_observed': mounted, 'actual_publication_filesystem': actual, + 'image_layers': 'NOT_ARCHIVED', 'bundled_node_binary': 'NOT_ARCHIVED', + 'private_inputs': 'NOT_USED', 'scientific_effect': 'NONE', 'scientific_status_authority': False} + with (destination / 'collection.json').open('x') as stream: + stream.write(json.dumps(record, sort_keys=True, indent=2) + '\n') + PY + - name: Bind original terminal results to native runtime and source identity + if: always() + run: | + set -euo pipefail + python3 -I -B -S - <<'PY' + import datetime, hashlib, json, os, pathlib, stat + base = pathlib.Path(os.environ['RUNNER_TEMP']) / 'canvas-tests' + root = base / 'publish/payload' + def require(value, message): + if not value: + raise ValueError(message) + def raw(name, limit=2**20): + current = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + descriptor = None + try: + parts = name.split('/') + for part in parts[:-1]: + nxt = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=current) + os.close(current) + current = nxt + descriptor = os.open(parts[-1], os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=current) + before = os.fstat(descriptor) + require(stat.S_ISREG(before.st_mode) and before.st_nlink == 1 and before.st_size <= limit, + 'retained original regular byte admission') + chunks, count = [], 0 + while True: + part = os.read(descriptor, min(65536, limit + 1 - count)) + if not part: + break + count += len(part) + require(count <= limit, 'retained original growth admission') + chunks.append(part) + after = os.fstat(descriptor) + require(stat.S_ISREG(after.st_mode) and after.st_nlink == 1 + and (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns) + == (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns) + and count == before.st_size, 'retained original changed while read') + return b''.join(chunks) + finally: + if descriptor is not None: + os.close(descriptor) + os.close(current) + load = lambda name: json.loads(raw(name)) + collection = load('collection.json') + require(collection['missing'] == [] and collection['refused'] == [], 'original observation incomplete') + actual_fs = collection['actual_publication_filesystem'] + require(collection['publication_mount_observed'] is True + and collection['configured_publication_filesystem_capacity_bytes'] == 64 * 2**20 + and type(actual_fs) is dict and type(actual_fs['host_device']) is int + and type(actual_fs['reported_total_bytes']) is int + and 0 < actual_fs['reported_total_bytes'] <= 64 * 2**20, 'publication physical capacity unobserved') + bindings = load('source-bindings.json') + for key, variable, name in ( + ('repository', 'GITHUB_REPOSITORY', 'repository.txt'), ('checked_commit', 'GITHUB_SHA', 'checked-commit.txt'), + ('run_id', 'GITHUB_RUN_ID', 'run-id.txt'), ('run_attempt', 'GITHUB_RUN_ATTEMPT', 'run-attempt.txt')): + require(bindings[key] == os.environ[variable] and raw(name) == (os.environ[variable] + '\n').encode(), + 'native execution context mismatch') + require(hashlib.sha256(raw('checked-workflow.yml', 262144)).hexdigest() == bindings['workflow_sha256'], + 'checked workflow source mismatch') + for source in bindings['sources']: + if source['presence'] == 'ABSENT': + require(source['path'] in collection['expected_absent_candidate_paths'], 'absent candidate mismatch') + else: + value = raw('checked-sources/' + source['path']) + require(len(value) == source['bytes'] and hashlib.sha256(value).hexdigest() == source['sha256'], + 'retained original source mismatch') + require(load('source-before.json') == bindings['sources'] == load('source-after.json'), + 'actual runtime source observation mismatch') + require(load('control-source.json')['sha256'] == hashlib.sha256(raw('run.mjs', 262144)).hexdigest(), + 'actual control source mismatch') + runtime_before, runtime_after = load('runtime-before.json'), load('runtime-after.json') + require(runtime_before == runtime_after and runtime_before['version'] == 'v24.21.0' + and runtime_before['platform'] == 'linux' and runtime_before['arch'] == 'x64' + and runtime_before['uid'] == runtime_before['gid'] == 65532 + and runtime_before['executable'] == '/usr/local/bin/node' + and runtime_before['runtime_origin'] == 'PINNED_IMAGE_BUNDLED_BINARY' + and type(runtime_before['executable_bytes']) is int and runtime_before['executable_bytes'] > 0 + and len(runtime_before['executable_sha256']) == 64, 'actual bundled runtime preflight mismatch') + images, before, after = load('runtime-image.json'), load('container-before.json'), load('container-after.json') + require(len(images) == len(before) == len(after) == 1, 'native record cardinality refusal') + image, first, last = images[0], before[0], after[0] + expected_image = os.environ['IMAGE'] + require('node@sha256:51b1100cc2a83d370c6a60952e3f2989c8a43159d0e38586e090f3b3326efefd' in image['RepoDigests'] + and image['Os'] == 'linux' and image['Architecture'] == 'amd64', + 'actual immutable runtime image mismatch') + require(first['Id'] == last['Id'] == raw('container-id.txt').decode().strip() + and first['Image'] == last['Image'] == image['Id'] + and first['Config'] == last['Config'] and first['Config']['Image'] == expected_image, + 'native container/image/config identity mismatch') + config, host = first['Config'], first['HostConfig'] + require(config['Entrypoint'] == ['/usr/local/bin/node'] and config['Cmd'] == ['/control/run.mjs'] + and config['User'] == '65532:65532' and config['WorkingDir'] == '/source' + and config.get('ExposedPorts') in (None, {}) and config.get('Volumes') is None, + 'fixed native command mismatch') + expected_environment = list(image['Config']['Env']) + for value in ('LANG=C.UTF-8', 'TZ=UTC', 'NODE_DISABLE_COLORS=1', 'NODE_OPTIONS='): + key = value.split('=', 1)[0] + '=' + expected_environment = [entry for entry in expected_environment if not entry.startswith(key)] + expected_environment.append(value) + require(sorted(config['Env']) == sorted(expected_environment), 'native container environment mismatch') + later_host = dict(last['HostConfig']) + earlier_host = dict(host) + for record in (earlier_host, later_host): + require('OomKillDisable' in record and (record['OomKillDisable'] is False or record['OomKillDisable'] is None), + 'unexpected native OOM-disable value') + del record['OomKillDisable'] + require(earlier_host == later_host, 'native host configuration changed') + require(host['ReadonlyRootfs'] is True and host['NetworkMode'] == 'none' + and host['NanoCpus'] == 2000000000 and host['Memory'] == host['MemorySwap'] == 1073741824 + and host['PidsLimit'] == 128 and host['CapDrop'] == ['ALL'] and host['CapAdd'] is None + and host['SecurityOpt'] == ['no-new-privileges'] and host['Privileged'] is False + and host['LogConfig'] == {'Type': 'none', 'Config': {}} + and host['PortBindings'] == {} and host['PublishAllPorts'] is False + and host['Devices'] == [] and host['DeviceRequests'] is None and host['VolumesFrom'] is None + and host['PidMode'] == '' and host['IpcMode'] == 'private' and host['CgroupnsMode'] == 'private' + and host['Tmpfs'] == {'/tmp': 'rw,noexec,nosuid,nodev,size=64m,mode=1777'}, + 'native containment limits mismatch') + require(sorted(host['Ulimits'], key=lambda item: item['Name']) == [ + {'Name': 'fsize', 'Hard': 4194304, 'Soft': 4194304}, + {'Name': 'nofile', 'Hard': 256, 'Soft': 256}], 'native process limits mismatch') + expected_mounts = sorted([ + ('/source', str(base / 'inputs/payload/source'), False), + ('/control', str(base / 'inputs/payload/control'), False), + ('/output', str(base / 'work/payload'), True)]) + for record in (first, last): + mounts = record['Mounts'] + require(len(mounts) == 3 and all(item['Type'] == 'bind' and item['Propagation'] == 'rprivate' for item in mounts) + and sorted((item['Destination'], item['Source'], item['RW']) for item in mounts) == expected_mounts, + 'actual container mount tuple mismatch') + require(record['NetworkSettings']['Networks'] == {'none': record['NetworkSettings']['Networks']['none']}, + 'unexpected native network attachment') + filesystems = load('fixture-filesystems.json') + require(set(filesystems) == {'inputs', 'work', 'publish'}, 'fixture filesystem inventory mismatch') + for label, observed in filesystems.items(): + mount = base / label + require(os.path.ismount(mount) and observed['mount_observed'] is True + and observed['configured_capacity_bytes'] == 64 * 2**20 + and type(observed['host_device']) is int and type(observed['reported_total_bytes']) is int + and 0 < observed['reported_total_bytes'] <= 64 * 2**20 + and os.stat(mount, follow_symlinks=False).st_dev == observed['host_device'] + and os.statvfs(mount).f_frsize * os.statvfs(mount).f_blocks == observed['reported_total_bytes'], + 'actual fixture filesystem identity or capacity changed') + require(actual_fs == {key: filesystems['publish'][key] for key in ('host_device', 'reported_total_bytes')}, + 'actual publication filesystem observation join mismatch') + require(first['State']['Status'] == 'created' and last['State']['Status'] == 'exited' + and last['State']['Running'] is False and last['State']['OOMKilled'] is False + and last['State']['Error'] == '' and last['State']['FinishedAt'] != '0001-01-01T00:00:00Z', + 'actual terminal native execution missing') + started = datetime.datetime.fromisoformat(last['State']['StartedAt'].replace('Z', '+00:00')) + finished = datetime.datetime.fromisoformat(last['State']['FinishedAt'].replace('Z', '+00:00')) + duration = (finished - started).total_seconds() + require(0 < duration <= 190, 'actual terminal container wall bound refusal') + require(raw('disposal-status.txt') == b'CONFIRMED_ABSENT\n', 'actual disposal missing') + results = load('results.json') + require(results['context'] == bindings and results['control_exit_code'] in (0, 1) + and raw('control-exit-status.txt') == (str(results['control_exit_code']) + '\n').encode() + and last['State']['ExitCode'] == results['control_exit_code'] + and raw('attach-exit-status.txt') == (str(results['control_exit_code']) + '\n').encode(), + 'actual control/native terminal mismatch') + attempts = load('attempts.json') + require(len(attempts) == 2 and [item['file'] for item in attempts] == ['cubic', 'elder'] + and all(item['attempts'] == 1 for item in attempts), 'explicit test-file attempt inventory mismatch') + for attempt in attempts: + require(attempt['executable'] == '/usr/local/bin/node' + and attempt['argv'] == ['--test', '--test-reporter=tap', '--test-concurrency=1', + '/source/interface/tests/' + attempt['file'] + '.test.mjs'], + 'actual child command identity mismatch') + require(type(attempt['duration_ns']) is str and attempt['duration_ns'].isdigit() + and 0 < int(attempt['duration_ns']) <= 65 * 10**9, 'actual child wall bound refusal') + for file in results['files']: + require(file['attempt'] == next(item for item in attempts if item['file'] == file['attempt']['file']), + 'actual result/child terminal mismatch') + if 'records' in file: + tap = raw(file['attempt']['stdout'], 4 * 2**20) + require(file['raw_sha256'] == hashlib.sha256(tap).hexdigest() and file['raw_bytes'] == len(tap), + 'original TAP result join mismatch') + controls = results['actual_controls'] + counts = {} + for control in controls: + counts[control['status']] = counts.get(control['status'], 0) + 1 + if results['status'] == 'SCOPED_GREEN': + require(len(controls) == 21 and counts == {'PASS': 21} and results['control_exit_code'] == 0 + and len(results['files']) == 2 and all(file['attempt']['code'] == 0 for file in results['files']), + 'skipped or incomplete controls cannot produce GREEN') + receipt = {'schema_version': 1, 'status': results['status'], 'behavior_result': results['behavior_result'], + 'source_declared_controls': results['source_declared_controls'], 'actual_discovered_controls': len(controls), + 'actual_status_counts': counts, 'actual_control_records': controls, 'context': bindings, + 'runtime': runtime_before, 'image_id': image['Id'], 'image_reference': expected_image, + 'fixture_filesystems': filesystems, 'declared_control_inventory': results['declared_control_inventory'], + 'container_id': first['Id'], 'terminal_exit_code': last['State']['ExitCode'], + 'container_started_at': last['State']['StartedAt'], 'container_finished_at': last['State']['FinishedAt'], + 'actual_container_wall_seconds': duration, 'configured_container_timeout_seconds': 180, + 'configured_container_kill_grace_seconds': 10, 'configured_child_timeout_seconds': 60, + 'configured_child_kill_grace_seconds': 5, 'test_file_attempts': attempts, + 'disposal': 'CONFIRMED_ABSENT_BEFORE_COLLECTION', + 'actual_publication_filesystem': actual_fs, 'publication_mount_observed': True, + 'configured_publication_filesystem_capacity_bytes': 64 * 2**20, + 'collector_copy_limit_bytes': 32 * 2**20, 'collector_subtotal_is_not_whole_publication_bytes': True, + 'original_native_job_and_archive_readback': 'REQUIRED_AFTER_JOB_COMPLETION', + 'genuine_behavioral_red': 'NOT_DECLARED_BY_WORKFLOW', 'scientific_effect': 'NONE', + 'scientific_status_authority': False, 'organizational_independence': 0, + 'custody_authentication': 'UNKNOWN', 'canvas_browser_deployment': 'NOT_RUN', + 'continuum_mathematical_claims': 'NOT_PROMOTED'} + with (root / 'receipt.json').open('x') as stream: + stream.write(json.dumps(receipt, sort_keys=True, indent=2) + '\n') + PY + - name: Require every declared Canvas control to pass + if: always() + run: | + set -euo pipefail + python3 -I -B -S - <<'PY' + import json, os, pathlib, stat + root = pathlib.Path(os.environ['RUNNER_TEMP']) / 'canvas-tests/publish/payload' + root_fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + fd = None + try: + fd = os.open('receipt.json', os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=root_fd) + before = os.fstat(fd) + if not stat.S_ISREG(before.st_mode) or before.st_nlink != 1 or not 0 < before.st_size <= 2**20: + raise ValueError('final receipt admission refusal') + with os.fdopen(fd, 'rb', closefd=False) as stream: + raw = stream.read(2**20 + 1) + after = os.fstat(fd) + if (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns) != ( + after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns) or after.st_nlink != 1: + raise ValueError('final receipt changed') + receipt = json.loads(raw) + if receipt['status'] != 'SCOPED_GREEN' or receipt['actual_status_counts'] != {'PASS': 21}: + raise ValueError('Canvas setup, discovery, checker or behavior controls did not all pass') + finally: + if fd is not None: + os.close(fd) + os.close(root_fd) + PY + - name: Hash the bounded original artifact inventory + if: always() + run: | + set -euo pipefail + python3 -I -B -S - <<'PY' + import hashlib, os, pathlib, stat + root = pathlib.Path(os.environ['RUNNER_TEMP']) / 'canvas-tests/publish/payload' + root.mkdir(parents=True, exist_ok=True) + root_fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + rows, count, total = [], 0, 0 + def visit(directory, prefix=''): + global count, total + for name in sorted(os.listdir(directory)): + info = os.stat(name, dir_fd=directory, follow_symlinks=False) + relative = prefix + name + if stat.S_ISDIR(info.st_mode): + child = os.open(name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=directory) + try: + visit(child, relative + '/') + finally: + os.close(child) + continue + if relative == 'ARTIFACT_SHA256SUMS': + raise ValueError('pre-existing artifact manifest') + if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or info.st_size > 4 * 2**20: + raise ValueError('artifact member admission refusal') + descriptor = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=directory) + try: + before = os.fstat(descriptor) + if (before.st_dev, before.st_ino) != (info.st_dev, info.st_ino) or before.st_nlink != 1: + raise ValueError('artifact member identity changed') + digest, read_count = hashlib.sha256(), 0 + while True: + chunk = os.read(descriptor, 65536) + if not chunk: + break + read_count += len(chunk) + if read_count > 4 * 2**20: + raise ValueError('artifact growth limit') + digest.update(chunk) + after = os.fstat(descriptor) + if not stat.S_ISREG(after.st_mode) or after.st_nlink != 1 or read_count != before.st_size or ( + before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns) != ( + after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns): + raise ValueError('artifact member changed during hashing') + finally: + os.close(descriptor) + count += 1 + total += read_count + if count > 64 or total > 64 * 2**20: + raise ValueError('artifact inventory bound') + rows.append(digest.hexdigest() + ' ' + relative + '\n') + try: + visit(root_fd) + fd = os.open('ARTIFACT_SHA256SUMS', os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, dir_fd=root_fd) + with os.fdopen(fd, 'wb') as stream: + stream.write(''.join(rows).encode()) + finally: + os.close(root_fd) + PY + - name: Retain originals on every conclusion + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: canvas-originals-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/canvas-tests/publish/payload/ + if-no-files-found: error + retention-days: 90 + compression-level: 6 + - name: Strictly unmount every disposable fixture filesystem + if: always() + run: | + set -euo pipefail + base="$RUNNER_TEMP/canvas-tests" + failed=0 + for label in publish work inputs; do + mount="$base/$label" + set +e + python3 -I -B -S - "$mount" <<'PY' + import os, stat, sys + try: + info = os.lstat(sys.argv[1]) + except FileNotFoundError: + sys.exit(32) + if not stat.S_ISDIR(info.st_mode): + raise ValueError('fixture mount path is not an original directory') + PY + presence=$? + set -e + if test "$presence" = 32; then + continue + elif test "$presence" != 0; then + failed=1 + continue + fi + # util-linux mountpoint: 0 is mounted; 32 is an ordinary nonmount; + # invocation, permission and other probe errors must fail cleanup. + set +e + mountpoint -q "$mount" + probe=$? + set -e + if test "$probe" = 0; then + if ! sudo umount "$mount"; then + failed=1 + continue + fi + set +e + mountpoint -q "$mount" + after=$? + set -e + if test "$after" != 32; then + failed=1 + fi + elif test "$probe" != 32; then + failed=1 + fi + done + test "$failed" = 0 + diff --git a/docs/plans/2026-10-09-canvas-model-contract.md b/docs/plans/2026-10-09-canvas-model-contract.md new file mode 100644 index 000000000..ab5315ab4 --- /dev/null +++ b/docs/plans/2026-10-09-canvas-model-contract.md @@ -0,0 +1,223 @@ +# Finite Canvas model contract — 9 October 2026 + +Status: contract and external tests prepared; execution **NOT_RUN**. This is +Task 3's pure-model increment, not the Canvas/DOM, browser or deployment task. +Scientific effect is `NONE`; scientific-status authority is boolean `false`. + +## Scope, ownership and execution sequence + +Architecture coordinator OpenAI/Codex root01a11d37 owns workflow, publication, +hosted execution and integration. [Native scope 6086148751](https://github.com/d6g8k5htny-coder/main/issues/307#issuecomment-6086148751) +was created 2026-10-09T17:43:52Z. Actual author of this contract and the two tests +is OpenAI/Codex root01a0bbb5, delegated for Dylan Roy, source-exposed, +organizational independence 0. This is not Dylan's personal human review. +R17 claim `c2230001-89da-4a2a-af29-610091748001` reserves these three new files; +timestamp correction `c2230002-89da-4a2a-af29-610091746002` retains the original +entry and records observed-at 17:46:13Z; expiry remains 18:30Z. + +The six-path scope is this contract, `interface/tests/{cubic,elder}.test.mjs`, +`interface/src/scripts/{cubic,elder}-model.mjs`, and coordinator-owned +`.github/workflows/architecture-canvas-tests.yml`. A distinct later author +receives the two product modules only after these tests freeze. Existing +site, release, package, lockfile, Pages and required-check DAG paths are excluded. + +The coordinator's explicit sequencing amendment governs the earlier plan's +unachievable “behavioral RED before any product authoring” requirement: + +1. Freeze and source-review contract/tests first. Top-level imports use only + Node built-ins; candidate dynamic import errors are captured by preflight. +2. Missing/syntax-invalid/import-failing/wrong-export candidates are + `SETUP_BLOCKED`, behavioral result `NOT_RUN`. Zero discovery or runner failure + is `DISCOVERY_FAILED`. Neither is genuine behavioral RED. +3. A distinct author may supply a faithful predecessor-behavior port under the + new named APIs, preserving real functionality and honestly identifying absent + teaching-coordinate/trace extensions. Preserve the actual first candidate. +4. A hosted assertion mismatch after real candidate import and function execution + can establish behavioral RED for that assertion. No intentionally broken + stub or synthetic sensitivity control counts as candidate RED. If the first + real candidate is complete and passes, record GREEN honestly instead. +5. Fix actual findings; run all applicable final controls once on stable inputs. + GREEN requires all declared controls passing, no skipped/blocked cases and + complete runtime/custody/disposal evidence. It does not finish Task 3 or the + full project. Source review is not execution. + +No project, Node, Python or YAML execution/installation occurs on the laptop. +Host the explicit two test files with Node built-ins only, network denied in a +disposable sandbox. No npm/Astro installation, private capture or credentials +enter that sandbox. The coordinator must freshly identify the actual bundled +Node binary/version; prior runtime preparation executed an extracted binary +and does not establish bundled-binary execution. Hosted logs bind repository, +checked commit, source/test/contract/workflow bytes, runtime, run/attempt, +discovery, preflight, assertion results and confirmed disposal. Retain original +failures and exact native artifact bytes. No green claim from inner digests. + +## Frozen scientific and engineering sources + +All source paths below belong to `d6g8k5htny-coder/main` commit +`032267c259162728229e3ddd14d08fe1c3f9a392`. Native content/blob reads were compared +with exact local Git objects; these are source pins, not claims of current HEAD. + +| Path | Git blob | SHA-256 | +| --- | --- | --- | +| `docs/site/explore-models.mjs` | `edb0709b8efde17aed61235452930515ec35884b` | `fdbf01dd7235f0b35e1f7dad30fbd70f07d30a690b83ff9a87b3506390a7159b` | +| `tests/test_explore_models.mjs` | `8004df102215d454fef1e8c49e8f9426d98e6d4c` | `9ff140148fbeb088c664f482a754661819c4f609f8180b2ad34df5f9a696a2d7` | +| `experiments/periodic_h0/EXACT_H0.md` | `026d61fefcc422aca80a12d7b2501a5c2d080ed2` | `a788b46b1ea5640661e2692b84816bb3acfd1bae5a1fd3083dd18d5d8ba19fff` | +| `experiments/periodic_h0/exact_h0.py` | `ffbfe6a3cd5ed39465e804ecfec0d9f0c6855da2` | `48ee16f2316cb1a233594ed10466340aa321d553c2b888198fe86b4bd9052970` | +| `experiments/periodic_h0/test_exact_h0.py` | `d5ab63cad6e3a0e082342c77eb29bb73c375209a` | `c22930ef4fd0007ab8fd78079f4560cdad3043b210f3b074f7b0220d99039610` | + +Parent plan `docs/plans/2026-10-08-architecture-interface.md` at PR327 head +`765b6a473b0652858dcd9724bd31c944b3f5ca04`: blob +`aad7b7d2f562be0b05c65d0cabe35b42306f5a1f`, SHA-256 +`0b754e6fda9c32e0a67cd865f8f3759590937d8faf81cc1e90f5a8f3fa3a9c97`. +Its Task 3 supplies the API names and teaching intent. The choices below are +explicit new JavaScript/teaching definitions where the original sources do not +define a trace, normalization schema or teaching resource cap. + +## Cubic API and new teaching coordinates + +`cubicModel({r, k, b})` is synchronous and pure, with no defaults or coercion. +Input is a non-null non-array object with own `r`, `k`, `b` fields. Extra fields +are ignored. Each field must be a primitive finite Number, `r > 0`, `k > 0`. +Malformed record/field types throw `TypeError`; nonfinite values, invalid ranges +and failed representability checks throw `RangeError`. Exact error text is not +an API. Accessor/proxy objects are not a security boundary; hosted isolation is. + +The exact output key set is: + +```text +{r, k, b, rCubed, pins, gap, heightWindow, normalized} +normalized = {xDivisor, yOrigin, yDivisor, pins, heightWindow} +``` + +Define Number operations `rCubed = r ** 3`, `gap = k * rCubed`, +`pins = [-r/2, r/2]`, `heightWindow = [b-gap, b]`. Reject unless `rCubed` and +`gap` are finite and positive, pins are finite and distinct, and `b-gap` is +finite and strictly less than `b`. This deliberately rejects underflow, +overflow and subtraction rounded to the upper endpoint. Do not import the old +module's unrelated annulus/remote geometry or its release-query dependency. + +The **new teaching normalization** is `X=x/r`, `Y=(y-b)/gap`, retaining all +normalizers: `xDivisor=r`, `yOrigin=b`, `yDivisor=gap`. Its nominal pins are +`[-0.5,0.5]`, nominal height window `[-1,0]`. These fixed coordinates describe +the symbolic affine definition; they do not assert that subtracting rounded +Number endpoints and dividing reproduces them exactly. The open source height +window and this illustrative coordinate range are not persistence intervals. +The cubic model uses floating-point teaching arithmetic, not exact rationals or +a proved continuum sampling law. Nonbinary controls use a stated numerical +tolerance; exact golden values use powers of two. + +The input must remain unchanged. Calls return fresh arrays/objects; mutating +one result must not affect another. Arrays are tuples in the declared order. + +## Exact finite elder API + +`finiteElderTrace({values, n, scale})` is synchronous and pure. The argument is +a non-null non-array object with own `values`, `n`, `scale`; extra fields are +ignored. `n` is a primitive safe integer Number with `3 <= n <= 32`. This upper +limit is a new teaching cap, not the Python theorem's domain limitation. +`values` is an Array of length `n*n`, with an own element at every index and +only primitive BigInt numerators. No sparse/inherited slots, typed arrays, +numbers, booleans, strings or nested arrays. `scale` is required positive +primitive BigInt. Invalid record/types/dense-element conditions throw +`TypeError`; invalid dimension/range/shape or nonpositive scale throw +`RangeError`. A noninteger Number dimension is a range error. No message-text +requirements. No mutation of inputs; returned `values` is a copy. + +The exact output keys are `{n, scale, values, events, levels, barcode}`. Integer +sample values, levels, births and deaths remain BigInt. IDs, offsets and counts +are safe integer Numbers. Never convert sample/level/birth/death BigInts to +Number or divide them in the exact engine; safe Number arithmetic for IDs and +indexes (including row/column division) is allowed. A future JSON adapter must separately specify decimal strings; it is +not part of this unit. Approximate drawing coordinates belong outside it. + +The graph has row-major ID `x*n+y`, four axis neighbors with periodic seams, +no diagonal edges and no duplicated boundary vertices. Sweep descending value, +then increasing vertex ID. For each newly active vertex, inspect active +neighbors in order negative first coordinate, positive first coordinate, +negative second coordinate, positive second coordinate. Every undirected edge +is inspected once, when its later endpoint is activated. The edge tuple stores +`[newlyActiveVertex, alreadyActiveNeighbor]` in that order. + +Mathematical elder priority is larger birth, then smaller vertex ID. Storage +roots, union size and path compression cannot change that priority. + +`barcode` has exactly the source's five keys: + +```text +{intervals: [[b,d],...], birth_vertices: [id,...], + essential: [b], essential_vertices: [id], zero_count: Number} +``` + +Finite records satisfy `b>d` and sort ascending `(b,d,birthVertex)`, with arrays +aligned. Physical superlevel interval is **`(d/scale, b/scale]`**, lifetime +`(b-d)/scale`; the class is dead at the death threshold. There is one separate +essential class `(-infinity,b/scale]`, represented by the smallest maximum ID. +Never remove the longest finite bar. Successful equal-level merges count as +zero pairs, omitted from `intervals`. Redundant edges are not zero pairs. +`intervals.length + zero_count + 1 = n*n`. + +## New explanatory trace, exact schema + +The original Python source does not expose this stream or use “neutral” as an +event name. Each record below has exactly the shown keys: + +```text +{kind:'activate', vertex, level} +{kind:'merge', edge:[u,v], level, survivor, dying, birth, death, zero} +{kind:'neutral', edge:[u,v], level, elder} +``` + +Activation precedes that vertex's edges. `merge` joins two distinct current +components; `survivor`/`dying` are their mathematical elder IDs before joining, +`birth=values[dying]`, `death=level`, `zero=(birth===death)` is boolean. +`neutral` means the endpoints were already in the same current component; +`elder` is its elder. A merge is never called neutral just because its lifetime +is zero. The exact stream contains `N` activations, `N-1` merges, `N+1` neutral +edges and `3N` records for `N=n*n`. This follows from the connected torus graph's +`2N` unique undirected edges; it is not a fresh execution result. + +`levels` is descending, one record after each **complete equal-value block**: + +```text +{level, eventEnd, activeCount, componentCount, positiveCount, zeroCount} +``` + +`eventEnd` is the exclusive stream offset, including every edge processed at +that level. `activeCount` is the current number of active vertices and +`componentCount` the current number of connected components; only +`positiveCount` and `zeroCount` are cumulative merge tallies. Only these full blocks represent the actual +superlevel sets `{v:values[v]>=level}`. Intermediate event playback illustrates +the chosen insertion order and is not a distinct filtration threshold. Do not +store a full grid snapshot in every event; the specified stream is linear-size. + +## External controls and completion limits + +The test files classify candidate preflight separately and skip behavioral +controls explicitly if preflight fails. These skips block GREEN. Oracle +sensitivity controls use literal good/corrupt barcodes; they validate the +checker, not a mocked model or genuine candidate RED. No product helper is +imported into expected-value calculations. + +Cubic controls cover literal outputs, halving radius/eighth gap, both retained +normalizers, nonbinary arithmetic, invalid type/range/representability and +input/result independence. Elder controls cover source literal unequal-peak, +seam, no-diagonal, tie and negative fixtures; `2^96` translation, scaling and +fractional physical scale; strict input rejection; and maximum teaching size. + +An independently constructed undirected graph supplies two stronger checks: +(1) complete regional-maximum birth labels plus BFS elder reachability at death +and death+1, and (2) all high/low threshold ranks on all 512 binary 3x3 fields +and deterministic explicitly generated tied fields. A separate partial-edge +BFS replays trace connectivity without union-find; it verifies exact schemas, +event order/coverage, mathematical elder identities, zero-versus-neutral, +barcode joins and full-block state counts. The six-column golden fixture also +has hand-derived component counts `[1,2,3,2,1,1]` at levels `[5,4,3,2,1,0]`. + +Finite exact tests, model agreement and source review do not prove the missing +bridge from weighted critical pairs to actual ordinary persistence bars. They +do not equate branch adjacency with elder pairing, contact integrals with elder +events, or global factorial bounds with regional shrinking multiple-witness +collisions. No field-first sampling, all-small-bars converse, once-counted +continuum-bar law, source-node mapping or formal certificate is supplied here. +Canvas, equivalent DOM tables, keyboard/reduced-motion controls, real browser +checks, SSG, privacy admission and exact-artifact deployment remain later work. diff --git a/interface/src/scripts/cubic-model.mjs b/interface/src/scripts/cubic-model.mjs new file mode 100644 index 000000000..0cfd5acf6 --- /dev/null +++ b/interface/src/scripts/cubic-model.mjs @@ -0,0 +1,40 @@ +// NON-CERTIFYING cubic arithmetic with explicit affine teaching coordinates. +// Source: d6g8k5htny-coder/main@032267c259162728229e3ddd14d08fe1c3f9a392, +// docs/site/explore-models.mjs, pinModel; source SHA-256: +// fdbf01dd7235f0b35e1f7dad30fbd70f07d30a690b83ff9a87b3506390a7159b. +// Unrelated annulus/remote geometry is excluded from this pure cubic module. +// Strict admission and nominal normalization are new teaching definitions from +// docs/plans/2026-10-09-canvas-model-contract.md, not a continuum sampling law. + +export function cubicModel(input) { + if (input === null || typeof input !== 'object' || Array.isArray(input)) { + throw new TypeError('A cubic-parameter record is required'); + } + for (const name of ['r', 'k', 'b']) { + if (!Object.hasOwn(input, name)) throw new TypeError(`Own ${name} is required`); + } + const {r, k, b} = input; + for (const [name, value] of Object.entries({r, k, b})) { + if (typeof value !== 'number') throw new TypeError(`${name} must be a primitive Number`); + if (!Number.isFinite(value)) throw new RangeError(`${name} must be finite`); + } + if (r <= 0 || k <= 0) throw new RangeError('r and k must be positive'); + const rCubed = r ** 3; + const gap = k * rCubed; + const pins = [-r / 2, r / 2]; + const lower = b - gap; + if (!Number.isFinite(rCubed) || rCubed <= 0 || !Number.isFinite(gap) || gap <= 0 + || !pins.every(Number.isFinite) || pins[0] === pins[1] + || !Number.isFinite(lower) || lower >= b) { + throw new RangeError('The derived scales must remain representable'); + } + return { + r, k, b, rCubed, pins, gap, heightWindow: [lower, b], + // These nominal coordinates express X=x/r and Y=(y-b)/gap. They do not + // assert exact recovery by dividing already-rounded Number endpoints. + normalized: { + xDivisor: r, yOrigin: b, yDivisor: gap, + pins: [-0.5, 0.5], heightWindow: [-1, 0], + }, + }; +} diff --git a/interface/src/scripts/elder-model.mjs b/interface/src/scripts/elder-model.mjs new file mode 100644 index 000000000..aab4a06a5 --- /dev/null +++ b/interface/src/scripts/elder-model.mjs @@ -0,0 +1,125 @@ +// Exact finite periodic H0 with an explanatory insertion-order trace. +// Source: d6g8k5htny-coder/main@032267c259162728229e3ddd14d08fe1c3f9a392, +// experiments/periodic_h0/exact_h0.py, compute; source SHA-256: +// 48ee16f2316cb1a233594ed10466340aa321d553c2b888198fe86b4bd9052970. +// The JavaScript wrapper supplies strict BigInt samples, a positive BigInt +// scale and the frozen teaching cap. These admission choices are new here. +// The five-key barcode preserves the predecessor computation. The event and +// complete-level schemas are new teaching definitions from the frozen contract. +// Integer endpoints stay in numerator units; this engine never divides them. +// Finite computation alone has no continuum or scientific acceptance effect. + +function admittedInput(input) { + if (input === null || typeof input !== 'object' || Array.isArray(input)) { + throw new TypeError('A finite-grid record is required'); + } + for (const name of ['values', 'n', 'scale']) { + if (!Object.hasOwn(input, name)) throw new TypeError(`Own ${name} is required`); + } + const {values, n, scale} = input; + if (typeof n !== 'number') throw new TypeError('Grid side must be a Number'); + if (!Number.isSafeInteger(n) || n < 3 || n > 32) { + throw new RangeError('Grid side must be an integer from three through thirty-two'); + } + if (typeof scale !== 'bigint') throw new TypeError('Scale must be a BigInt'); + if (scale <= 0n) throw new RangeError('Scale must be positive'); + if (!Array.isArray(values)) throw new TypeError('Flat sample array is required'); + if (values.length !== n * n) throw new RangeError('Square sample array is required'); + const samples = []; + for (let vertex = 0; vertex < values.length; vertex++) { + if (!Object.hasOwn(values, vertex)) throw new TypeError('Own dense samples are required'); + const value = values[vertex]; + if (typeof value !== 'bigint') throw new TypeError('Primitive BigInt samples are required'); + samples.push(value); + } + return {values: samples, n, scale}; +} + +function compareInteger(a, b) { + return a < b ? -1 : a > b ? 1 : 0; +} + +function computeTrace(values, n) { + const count = values.length; + const parent = new Int32Array(count).fill(-1); + const sizes = new Uint32Array(count); + const eldest = new Uint32Array(count); + // Explicit increasing-ID ties preserve the Python source's stable sort. + const order = Array.from({length: count}, (_, vertex) => vertex) + .sort((a, b) => compareInteger(values[b], values[a]) || a - b); + const records = []; + const events = []; + const levels = []; + let zero = 0; + let activeCount = 0; + let componentCount = 0; + + function root(vertex) { + while (parent[vertex] !== vertex) { + parent[vertex] = parent[parent[vertex]]; + vertex = parent[vertex]; + } + return vertex; + } + + for (let index = 0; index < order.length; index++) { + const vertex = order[index], level = values[vertex]; + parent[vertex] = vertex; + sizes[vertex] = 1; + eldest[vertex] = vertex; + activeCount++; + componentCount++; + events.push({kind: 'activate', vertex, level}); + const x = Math.floor(vertex / n), y = vertex % n; + // Adding n before subtraction preserves Python's nonnegative modulo. + const neighbors = [((x + n - 1) % n) * n + y, ((x + 1) % n) * n + y, + x * n + (y + n - 1) % n, x * n + (y + 1) % n]; + for (const neighbor of neighbors) { + if (parent[neighbor] < 0) continue; + let u = root(vertex), v = root(neighbor); + if (u === v) { + events.push({kind: 'neutral', edge: [vertex, neighbor], level, elder: eldest[u]}); + continue; + } + let survivor = eldest[u], dying = eldest[v]; + if (values[survivor] < values[dying] + || (values[survivor] === values[dying] && survivor > dying)) { + [survivor, dying] = [dying, survivor]; + } + const birth = values[dying], death = level; + events.push({kind: 'merge', edge: [vertex, neighbor], level, + survivor, dying, birth, death, zero: birth === death}); + if (birth > death) records.push([birth, death, dying]); + else zero++; + // Storage balancing is independent of the mathematical elder priority. + if (sizes[u] < sizes[v]) [u, v] = [v, u]; + parent[v] = u; + sizes[u] += sizes[v]; + eldest[u] = survivor; + componentCount--; + } + // Only the state after every vertex and edge of an equal-value block is + // a complete superlevel set. No full-grid snapshots are stored per event. + if (index + 1 === order.length || values[order[index + 1]] !== level) { + levels.push({level, eventEnd: events.length, activeCount, componentCount, + positiveCount: records.length, zeroCount: zero}); + } + } + records.sort((a, b) => compareInteger(a[0], b[0]) + || compareInteger(a[1], b[1]) || a[2] - b[2]); + const essential = eldest[root(0)]; + const barcode = { + intervals: records.map(([birth, death]) => [birth, death]), + birth_vertices: records.map(([, , vertex]) => vertex), + essential: [values[essential]], + essential_vertices: [essential], + zero_count: zero, + }; + return {events, levels, barcode}; +} + +export function finiteElderTrace(input) { + const {values, n, scale} = admittedInput(input); + const {events, levels, barcode} = computeTrace(values, n); + return {n, scale, values, events, levels, barcode}; +} diff --git a/interface/tests/cubic.test.mjs b/interface/tests/cubic.test.mjs new file mode 100644 index 000000000..a34f3d10b --- /dev/null +++ b/interface/tests/cubic.test.mjs @@ -0,0 +1,133 @@ +// External contract controls; no implementation helper is used for expectations. +// Contract: docs/plans/2026-10-09-canvas-model-contract.md. +// Candidate setup failure is never behavioral RED. Run only in the hosted lane. +import test from 'node:test'; +import assert from 'node:assert/strict'; + +let candidate; +let setup; +try { + const module = await import('../src/scripts/cubic-model.mjs'); + if (typeof module.cubicModel !== 'function') throw new TypeError('Missing cubicModel export'); + candidate = module.cubicModel; +} catch (error) { + setup = 'Candidate import/export rejected with an unprintable value'; + try { + setup = error instanceof Error ? `${error.name}: ${error.message}` : `Non-Error rejection: ${String(error)}`; + } catch { /* Preserve SETUP_BLOCKED even for a hostile rejection value. */ } +} + +test('PREFLIGHT cubic candidate imports and exports cubicModel', () => { + assert.equal(setup, undefined, `SETUP_BLOCKED; behavior NOT_RUN: ${setup}`); +}); + +function behavior(name, check) { + test(`BEHAVIOR cubic ${name}`, (t) => { + if (!candidate) { t.skip('SETUP_BLOCKED; behavior NOT_RUN'); return; } + check(candidate); + }); +} + +function exactKeys(object, names) { + assert.ok(object !== null && typeof object === 'object' && !Array.isArray(object)); + assert.deepEqual(Object.keys(object).sort(), [...names].sort()); +} + +function shape(result) { + exactKeys(result, ['r', 'k', 'b', 'rCubed', 'pins', 'gap', 'heightWindow', 'normalized']); + exactKeys(result.normalized, ['xDivisor', 'yOrigin', 'yDivisor', 'pins', 'heightWindow']); + for (const key of ['r', 'k', 'b', 'rCubed', 'gap']) { + assert.equal(typeof result[key], 'number'); + assert.ok(Number.isFinite(result[key])); + } + for (const tuple of [result.pins, result.heightWindow, result.normalized.pins, + result.normalized.heightWindow]) { + assert.ok(Array.isArray(tuple)); + assert.equal(tuple.length, 2); + for (const value of tuple) assert.ok(typeof value === 'number' && Number.isFinite(value)); + } +} + +behavior('literal binary fixture retains all scales and affine teaching coordinates', (model) => { + const result = model({r: 0.5, k: 2, b: 3}); + shape(result); + assert.deepEqual(result, { + r: 0.5, k: 2, b: 3, rCubed: 0.125, pins: [-0.25, 0.25], gap: 0.25, + heightWindow: [2.75, 3], + normalized: {xDivisor: 0.5, yOrigin: 3, yDivisor: 0.25, + pins: [-0.5, 0.5], heightWindow: [-1, 0]}, + }); +}); + +behavior('halving radius divides the height gap by eight without rescaling k', (model) => { + const large = model({r: 0.5, k: 2, b: 3}); + const small = model({r: 0.25, k: 2, b: 3}); + shape(small); + assert.equal(small.gap, 0.03125); + assert.equal(large.gap / small.gap, 8); + assert.equal(small.rCubed, 0.015625); + assert.deepEqual(small.pins, [-0.125, 0.125]); + assert.deepEqual(small.heightWindow, [2.96875, 3]); + assert.deepEqual(small.normalized, {xDivisor: 0.25, yOrigin: 3, yDivisor: 0.03125, + pins: [-0.5, 0.5], heightWindow: [-1, 0]}); +}); + +behavior('nonbinary arithmetic and negative origin preserve nominal normalization', (model) => { + const result = model({r: 0.3, k: 5, b: -2}); + shape(result); + // Hand-derived decimal quantities, tolerance only for Number arithmetic. + assert.ok(Math.abs(result.rCubed - 0.027) <= 1e-16); + assert.ok(Math.abs(result.gap - 0.135) <= 1e-15); + assert.ok(Math.abs(result.heightWindow[0] - (-2.135)) <= 1e-14); + assert.deepEqual(result.pins, [-0.15, 0.15]); + assert.equal(result.normalized.xDivisor, 0.3); + assert.equal(result.normalized.yOrigin, -2); + assert.equal(result.normalized.yDivisor, result.gap); + assert.deepEqual(result.normalized.pins, [-0.5, 0.5]); + assert.deepEqual(result.normalized.heightWindow, [-1, 0]); +}); + +behavior('rejects malformed records and coercible or missing numeric fields', (model) => { + for (const input of [undefined, null, [], true, 4, 'record', {}, {r: 1, k: 1}, + Object.create({r: 1, k: 1, b: 1})]) assert.throws(() => model(input), TypeError); + for (const key of ['r', 'k', 'b']) { + for (const value of [undefined, null, true, '1', 1n, {}, [], new Number(1)]) { + assert.throws(() => model({...{r: 1, k: 1, b: 1}, [key]: value}), TypeError); + } + } +}); + +behavior('rejects invalid ranges overflow underflow and collapsed height windows', (model) => { + for (const key of ['r', 'k', 'b']) { + for (const value of [NaN, Infinity, -Infinity]) { + assert.throws(() => model({...{r: 1, k: 1, b: 1}, [key]: value}), RangeError); + } + } + for (const key of ['r', 'k']) for (const value of [0, -0, -1]) { + assert.throws(() => model({...{r: 1, k: 1, b: 1}, [key]: value}), RangeError); + } + for (const input of [ + {r: 1e200, k: 1, b: 1}, // rCubed overflow + {r: 1e-200, k: 1e300, b: 0}, // rCubed underflow before multiplication + {r: 2, k: Number.MAX_VALUE, b: 0}, // gap overflow + {r: 0.5, k: Number.MIN_VALUE, b: 0}, // gap underflow + {r: 1, k: 1, b: 1e30}, // subtraction rounds back to b + {r: 1, k: Number.MAX_VALUE, b: -Number.MAX_VALUE}, // lower overflow + ]) assert.throws(() => model(input), RangeError); +}); + +behavior('does not mutate inputs or share mutable results across calls', (model) => { + const input = Object.freeze({r: 0.5, k: 2, b: 3, unused: 'allowed'}); + const first = model(input); + const snapshot = structuredClone(first); + const second = model(input); + assert.deepEqual(first, second); + first.pins[0] = 99; + first.heightWindow[0] = 99; + first.normalized.pins[0] = 99; + first.normalized.heightWindow[0] = 99; + first.normalized.yDivisor = 99; + assert.deepEqual(second, snapshot); + assert.deepEqual(model(input), snapshot); + assert.deepEqual(input, {r: 0.5, k: 2, b: 3, unused: 'allowed'}); +}); diff --git a/interface/tests/elder.test.mjs b/interface/tests/elder.test.mjs new file mode 100644 index 000000000..b685c4e2d --- /dev/null +++ b/interface/tests/elder.test.mjs @@ -0,0 +1,379 @@ +// Source-informed external controls with independent graph-search oracles. +// Contract: docs/plans/2026-10-09-canvas-model-contract.md. Hosted execution only. +// Expected barcodes do not call the implementation or its helpers. +import test from 'node:test'; +import assert from 'node:assert/strict'; + +let candidate; +let setup; +try { + const module = await import('../src/scripts/elder-model.mjs'); + if (typeof module.finiteElderTrace !== 'function') throw new TypeError('Missing finiteElderTrace export'); + candidate = module.finiteElderTrace; +} catch (error) { + setup = 'Candidate import/export rejected with an unprintable value'; + try { + setup = error instanceof Error ? `${error.name}: ${error.message}` : `Non-Error rejection: ${String(error)}`; + } catch { /* Preserve SETUP_BLOCKED even for a hostile rejection value. */ } +} + +test('PREFLIGHT elder candidate imports and exports finiteElderTrace', () => { + assert.equal(setup, undefined, `SETUP_BLOCKED; behavior NOT_RUN: ${setup}`); +}); + +function behavior(name, check) { + test(`BEHAVIOR elder ${name}`, (t) => { + if (!candidate) { t.skip('SETUP_BLOCKED; behavior NOT_RUN'); return; } + check(candidate); + }); +} + +function keys(object, names) { + assert.ok(object !== null && typeof object === 'object' && !Array.isArray(object)); + assert.deepEqual(Object.keys(object).sort(), [...names].sort()); +} + +function integer(value, low = 0) { + assert.ok(typeof value === 'number' && Number.isSafeInteger(value) && value >= low); +} + +function dense(array, length) { + assert.ok(Array.isArray(array)); + assert.equal(array.length, length); + for (let i = 0; i < length; i++) assert.ok(Object.hasOwn(array, i)); +} + +function compareBig(a, b) { return a < b ? -1 : a > b ? 1 : 0; } +function older(values, u, v) { + return values[u] > values[v] || (values[u] === values[v] && u < v); +} + +// Independent graph: add positive-direction edges and symmetrize, no DSU. +function graphFor(n) { + const graph = Array.from({length: n * n}, () => new Set()); + for (let row = 0; row < n; row++) for (let col = 0; col < n; col++) { + const u = row * n + col; + for (const v of [((row + 1) % n) * n + col, row * n + (col + 1) % n]) { + graph[u].add(v); graph[v].add(u); + } + } + return graph; +} + +function connected(graph, start, admitted) { + assert.ok(admitted(start), 'BFS start must be admitted'); + const seen = new Set([start]); + const queue = [start]; + for (let i = 0; i < queue.length; i++) for (const v of graph[queue[i]]) { + if (admitted(v) && !seen.has(v)) { seen.add(v); queue.push(v); } + } + return seen; +} + +function labelsAt(values, graph, threshold) { + const labels = Array(values.length).fill(-1); + let count = 0; + for (let v = 0; v < values.length; v++) { + if (values[v] < threshold || labels[v] !== -1) continue; + for (const u of connected(graph, v, (u) => values[u] >= threshold)) labels[u] = count; + count++; + } + return {labels, count}; +} + +function recordsFor(barcode) { + return barcode.intervals.map(([b, d], i) => [b, d, barcode.birth_vertices[i]]); +} + +function sortRecords(records) { + return [...records].sort((a, b) => compareBig(a[0], b[0]) || compareBig(a[1], b[1]) || a[2] - b[2]); +} + +function verifyBarcode(values, n, barcode) { + const size = values.length; + keys(barcode, ['intervals', 'birth_vertices', 'essential', 'essential_vertices', 'zero_count']); + assert.ok(Array.isArray(barcode.intervals)); + dense(barcode.intervals, barcode.intervals.length); + dense(barcode.birth_vertices, barcode.intervals.length); + dense(barcode.essential, 1); dense(barcode.essential_vertices, 1); + integer(barcode.zero_count); + assert.equal(barcode.intervals.length + barcode.zero_count + 1, size); + for (const [i, pair] of barcode.intervals.entries()) { + dense(pair, 2); + assert.equal(typeof pair[0], 'bigint'); assert.equal(typeof pair[1], 'bigint'); + assert.ok(pair[0] > pair[1]); + integer(barcode.birth_vertices[i]); assert.ok(barcode.birth_vertices[i] < size); + } + assert.equal(typeof barcode.essential[0], 'bigint'); + integer(barcode.essential_vertices[0]); assert.ok(barcode.essential_vertices[0] < size); + assert.equal(new Set(barcode.birth_vertices).size, barcode.birth_vertices.length); + assert.deepEqual(recordsFor(barcode), sortRecords(recordsFor(barcode))); + + const graph = graphFor(n); + const visited = new Set(); + const maxima = []; + // Complete birth coverage via regional plateaus, independent of the sweep. + for (let v = 0; v < size; v++) { + if (visited.has(v)) continue; + const plateau = connected(graph, v, (u) => values[u] === values[v]); + let hasHigher = false; + for (const u of plateau) { + visited.add(u); + for (const w of graph[u]) if (values[w] > values[v]) hasHigher = true; + } + if (!hasHigher) maxima.push(v); // increasing scan gives minimum plateau ID + } + const essential = maxima.reduce((best, v) => older(values, v, best) ? v : best); + assert.deepEqual(barcode.essential, [values[essential]]); + assert.deepEqual(barcode.essential_vertices, [essential]); + assert.deepEqual([...barcode.birth_vertices].sort((a, b) => a - b), + maxima.filter((v) => v !== essential).sort((a, b) => a - b)); + for (const [birth, death, vertex] of recordsFor(barcode)) { + assert.equal(birth, values[vertex]); + for (const [threshold, expected] of [[death, true], [death + 1n, false]]) { + const region = connected(graph, vertex, (u) => values[u] >= threshold); + assert.equal([...region].some((u) => older(values, u, vertex)), expected, + `Exact elder death mismatch at vertex ${vertex}, threshold ${threshold}`); + } + } +} + +function verifyRanks(values, n, barcode) { + const graph = graphFor(n); + const actualLevels = [...new Set(values)].sort((a, b) => compareBig(b, a)); + const thresholds = [actualLevels[0] + 1n, ...actualLevels, actualLevels.at(-1) - 1n]; + const labels = new Map(thresholds.map((level) => [level, labelsAt(values, graph, level).labels])); + for (let i = 0; i < thresholds.length; i++) for (const low of thresholds.slice(i)) { + const high = thresholds[i]; + const touched = new Set(); + for (let v = 0; v < values.length; v++) if (values[v] >= high) touched.add(labels.get(low)[v]); + const finite = barcode.intervals.filter(([birth, death]) => birth >= high && death < low).length; + const essential = barcode.essential.filter((birth) => birth >= high).length; + assert.equal(finite + essential, touched.size, `Rank mismatch high=${high}, low=${low}`); + } +} + +// Trace verification uses a partial-edge BFS, not union-find or candidate data. +function verifyTrace(values, n, scale, result) { + keys(result, ['n', 'scale', 'values', 'events', 'levels', 'barcode']); + assert.equal(result.n, n); assert.equal(result.scale, scale); + dense(result.values, values.length); assert.deepEqual(result.values, values); + assert.notEqual(result.values, values); + verifyBarcode(values, n, result.barcode); + assert.ok(Array.isArray(result.events) && Array.isArray(result.levels)); + dense(result.events, 3 * values.length); + dense(result.levels, new Set(values).size); + const partial = Array.from({length: values.length}, () => new Set()); + const active = new Set(); + const seenEdges = new Set(); + const order = values.map((_, v) => v).sort((a, b) => compareBig(values[b], values[a]) || a - b); + const records = []; + let cursor = 0, block = 0, zeros = 0, neutrals = 0, merges = 0; + const elderOf = (region) => [...region].reduce((best, v) => older(values, v, best) ? v : best); + for (let index = 0; index < order.length; index++) { + const vertex = order[index], level = values[vertex]; + assert.deepEqual(result.events[cursor++], {kind: 'activate', vertex, level}); + active.add(vertex); + const row = Math.floor(vertex / n), col = vertex % n; + // Branch-based boundaries differ from the source's modular neighbor formula. + const neighbors = [row ? vertex - n : vertex + n * (n - 1), + row + 1 < n ? vertex + n : vertex - n * (n - 1), + col ? vertex - 1 : vertex + n - 1, + col + 1 < n ? vertex + 1 : vertex - n + 1]; + for (const neighbor of neighbors) { + if (!active.has(neighbor)) continue; + const edgeKey = [vertex, neighbor].sort((a, b) => a - b).join(':'); + assert.ok(!seenEdges.has(edgeKey)); seenEdges.add(edgeKey); + const first = connected(partial, vertex, (u) => active.has(u)); + const firstElder = elderOf(first); + let expected; + if (first.has(neighbor)) { + expected = {kind: 'neutral', edge: [vertex, neighbor], level, elder: firstElder}; + neutrals++; + } else { + const secondElder = elderOf(connected(partial, neighbor, (u) => active.has(u))); + const survivor = older(values, firstElder, secondElder) ? firstElder : secondElder; + const dying = survivor === firstElder ? secondElder : firstElder; + const birth = values[dying], death = level, zero = birth === death; + expected = {kind: 'merge', edge: [vertex, neighbor], level, survivor, dying, birth, death, zero}; + merges++; + if (zero) zeros++; else records.push([birth, death, dying]); + } + assert.deepEqual(result.events[cursor++], expected); + partial[vertex].add(neighbor); partial[neighbor].add(vertex); + } + if (index + 1 === order.length || values[order[index + 1]] !== level) { + const complete = labelsAt(values, graphFor(n), level); + assert.deepEqual(result.levels[block++], {level, eventEnd: cursor, + activeCount: active.size, componentCount: complete.count, + positiveCount: records.length, zeroCount: zeros}); + assert.equal(active.size - merges, complete.count); + } + } + assert.equal(cursor, result.events.length); + assert.equal(block, result.levels.length); + assert.equal(seenEdges.size, 2 * values.length); + assert.equal(merges, values.length - 1); + assert.equal(neutrals, values.length + 1); + assert.equal(zeros, result.barcode.zero_count); + assert.deepEqual(sortRecords(records), recordsFor(result.barcode)); +} + +const peaks = () => Array.from({length: 36}, (_, i) => [5n, 1n, 4n, 2n, 3n, 0n][i % 6]); +const golden = () => ({intervals: [[3n, 2n], [4n, 1n]], birth_vertices: [4, 2], + essential: [5n], essential_vertices: [0], zero_count: 33}); + +behavior('unequal peaks retain the elder and longest finite bar', (model) => { + const values = peaks(), result = model({values, n: 6, scale: 1n}); + assert.deepEqual(result.barcode, golden()); + verifyTrace(values, 6, 1n, result); verifyRanks(values, 6, result.barcode); +}); + +behavior('full tie blocks and neutral edges have distinct meanings', (model) => { + const values = peaks(), result = model({values, n: 6, scale: 1n}); + verifyTrace(values, 6, 1n, result); + assert.deepEqual(result.levels.map((x) => [x.level, x.activeCount, x.componentCount]), + [[5n, 6, 1], [4n, 12, 2], [3n, 18, 3], [2n, 24, 2], [1n, 30, 1], [0n, 36, 1]]); + assert.equal(result.events.filter((x) => x.kind === 'neutral').length, 37); + assert.equal(result.events.filter((x) => x.kind === 'merge' && x.zero).length, 33); + const positive = result.events.filter((x) => x.kind === 'merge' && !x.zero); + assert.deepEqual(positive.map((x) => [x.birth, x.death, x.dying, x.survivor]), + [[3n, 2n, 4, 2], [4n, 1n, 2, 0]]); +}); + +behavior('periodic seams join peaks before a spurious death', (model) => { + const values = Array.from({length: 16}, (_, i) => [5n, 0n, 1n, 4n][i % 4]); + const result = model({values, n: 4, scale: 1n}); + assert.deepEqual(result.barcode, {intervals: [], birth_vertices: [], essential: [5n], + essential_vertices: [0], zero_count: 15}); + verifyTrace(values, 4, 1n, result); verifyRanks(values, 4, result.barcode); +}); + +behavior('axis graph does not connect diagonal square corners', (model) => { + const values = Array(16).fill(0n); values[0] = values[5] = 5n; + const result = model({values, n: 4, scale: 1n}); + assert.deepEqual(result.barcode, {intervals: [[5n, 0n]], birth_vertices: [5], + essential: [5n], essential_vertices: [0], zero_count: 14}); + verifyTrace(values, 4, 1n, result); verifyRanks(values, 4, result.barcode); +}); + +behavior('equal peaks prefer the smaller plateau ID', (model) => { + const values = Array.from({length: 16}, (_, i) => [5n, 1n, 5n, 1n][i % 4]); + const result = model({values, n: 4, scale: 1n}); + assert.deepEqual(result.barcode, {intervals: [[5n, 1n]], birth_vertices: [2], + essential: [5n], essential_vertices: [0], zero_count: 14}); + verifyTrace(values, 4, 1n, result); verifyRanks(values, 4, result.barcode); +}); + +behavior('negative constant plateau has one essential class and eight zero merges', (model) => { + const values = Array(9).fill(-7n), result = model({values, n: 3, scale: 2n}); + assert.deepEqual(result.barcode, {intervals: [], birth_vertices: [], essential: [-7n], + essential_vertices: [0], zero_count: 8}); + verifyTrace(values, 3, 2n, result); verifyRanks(values, 3, result.barcode); +}); + +behavior('large integer translation and positive scaling preserve exact endpoints', (model) => { + const offset = 1n << 96n, values = peaks().map((v) => v + offset); + const result = model({values, n: 6, scale: 7n}); + assert.deepEqual(result.barcode.intervals, [[offset + 3n, offset + 2n], [offset + 4n, offset + 1n]]); + assert.deepEqual(result.barcode.intervals.map(([b, d]) => b - d), [1n, 3n]); + verifyTrace(values, 6, 7n, result); verifyRanks(values, 6, result.barcode); + const scaledValues = values.map((v) => 11n * v); + const scaled = model({values: scaledValues, n: 6, scale: 77n}); + assert.deepEqual(scaled.barcode.intervals.map(([b, d]) => b - d), [11n, 33n]); + verifyTrace(scaledValues, 6, 77n, scaled); verifyRanks(scaledValues, 6, scaled.barcode); + // Keep rational endpoints as numerator/scale; no truncating integer division. + assert.equal(result.scale, 7n); assert.equal(scaled.scale, 77n); +}); + +behavior('all 512 binary grids satisfy every threshold rank and exact trace', (model) => { + for (let mask = 0; mask < 512; mask++) { + const values = Array.from({length: 9}, (_, i) => BigInt((mask >> i) & 1)); + const result = model({values, n: 3, scale: 1n}); + verifyTrace(values, 3, 1n, result); verifyRanks(values, 3, result.barcode); + } +}); + +behavior('deterministic tied multilevel fields satisfy complete birth and rank oracles', (model) => { + // Explicit reproducible synthetic fields; no sampling-law or randomness claim. + let state = 3934000; + for (const n of [3, 4, 5]) for (let sample = 0; sample < 25; sample++) { + const values = Array.from({length: n * n}, () => { + state = (Math.imul(state, 1664525) + 1013904223) >>> 0; + return BigInt((state % 7) - 3); + }); + const result = model({values, n, scale: 3n}); + verifyTrace(values, n, 3n, result); verifyRanks(values, n, result.barcode); + } +}); + +behavior('rejects malformed record dimension scale shape and sparse integer samples', (model) => { + const valid = {values: Array(9).fill(0n), n: 3, scale: 1n}; + for (const input of [undefined, null, [], true, 3, 'record', {}, + {values: valid.values, n: 3}, Object.create(valid)]) assert.throws(() => model(input), TypeError); + for (const n of [true, '3', 3n, new Number(3), null]) { + assert.throws(() => model({...valid, n}), TypeError); + } + for (const n of [2, 33, 3.5, NaN, Infinity, -1, 0]) { + assert.throws(() => model({...valid, n}), RangeError); + } + for (const scale of [1, '1', true, null, undefined, Object(1n)]) { + assert.throws(() => model({...valid, scale}), TypeError); + } + for (const scale of [0n, -1n]) assert.throws(() => model({...valid, scale}), RangeError); + for (const values of [null, '000000000', new BigInt64Array(9), {length: 9}]) { + assert.throws(() => model({...valid, values}), TypeError); + } + for (const values of [[], Array(8).fill(0n), Array(10).fill(0n)]) { + assert.throws(() => model({...valid, values}), RangeError); + } + const sparse = Array(9).fill(0n); delete sparse[4]; + const inherited = Array(9).fill(0n); delete inherited[4]; + const prototype = Object.create(Array.prototype); prototype[4] = 0n; + Object.setPrototypeOf(inherited, prototype); + for (const values of [sparse, inherited]) assert.throws(() => model({...valid, values}), TypeError); + for (const bad of [0, 0.5, NaN, Infinity, false, '0', null, undefined, {}, [], Object(0n)]) { + const values = Array(9).fill(0n); values[8] = bad; + assert.throws(() => model({...valid, values}), TypeError); + } +}); + +behavior('accepts the teaching cap with linear event storage', (model) => { + const values = Array(1024).fill(0n), result = model({values, n: 32, scale: 1n}); + assert.equal(result.events.length, 3072); + assert.deepEqual(result.barcode, {intervals: [], birth_vertices: [], essential: [0n], + essential_vertices: [0], zero_count: 1023}); + verifyTrace(values, 32, 1n, result); +}); + +behavior('does not mutate input or share returned state across deterministic calls', (model) => { + const values = Object.freeze(peaks()); + const input = Object.freeze({values, n: 6, scale: 7n, unused: 'allowed'}); + const first = model(input), second = model(input), snapshot = structuredClone(second); + assert.deepEqual(first, second); verifyTrace(values, 6, 7n, second); + first.values[0] = -99n; first.events[0].level = -99n; + first.events.find((x) => x.kind !== 'activate').edge[0] = -1; + first.levels[0].componentCount = -1; + first.barcode.intervals[0][0] = -99n; first.barcode.birth_vertices[0] = -1; + first.barcode.essential[0] = -99n; first.barcode.essential_vertices[0] = -1; + assert.deepEqual(second, snapshot); assert.deepEqual(model(input), snapshot); + assert.deepEqual(values, peaks()); assert.equal(input.scale, 7n); +}); + +test('ORACLE sensitivity rejects omitted shifted mislabelled and malformed literal barcodes', () => { + // Checker controls only: not a product substitute and never candidate RED. + verifyBarcode(peaks(), 6, golden()); verifyRanks(peaks(), 6, golden()); + const mutations = []; + for (const death of [0n, 2n]) { + const changed = golden(); changed.intervals[1][1] = death; mutations.push(changed); + } + const omitted = golden(); omitted.intervals.pop(); omitted.birth_vertices.pop(); omitted.zero_count++; + mutations.push(omitted); + for (const [key, value] of [['birth_vertices', [2, 4]], ['essential', [4n]], + ['essential_vertices', [2]], ['zero_count', 0], ['zero_count', true]]) { + mutations.push({...golden(), [key]: value}); + } + const inexact = golden(); inexact.intervals[0][0] = 3; mutations.push(inexact); + mutations.push({...golden(), extra: true}); + for (const corrupted of mutations) assert.throws(() => verifyBarcode(peaks(), 6, corrupted)); +});