From 5f8611630335f8848161a93bb54715e3d31691fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Tue, 11 Aug 2026 21:37:49 +0800 Subject: [PATCH] feat: register deterministic HoloLake stage relay --- .../HLP-STAGE-RELAY-001-20260811.json | 32 + routing/hololake-stage-relay-map.json | 55 ++ routing/host-skill-navigation-map.json | 34 +- routing/lighthouse-path-registry.json | 11 +- routing/public-navigation-anchor.json | 9 +- .../lake-lamp-continuity-guard/README.md | 11 + .../stage-relay-state-machine.md | 66 ++ .../stage-relay-state.schema.json | 46 ++ .../stage-relay.mjs | 752 ++++++++++++++++++ .../stage-relay.test.mjs | 19 + tests/stage-relay-navigation.test.mjs | 34 + 11 files changed, 1065 insertions(+), 4 deletions(-) create mode 100644 deployment/receipts/HLP-STAGE-RELAY-001-20260811.json create mode 100644 routing/hololake-stage-relay-map.json create mode 100644 server-tools/lake-lamp-continuity-guard/stage-relay-state-machine.md create mode 100644 server-tools/lake-lamp-continuity-guard/stage-relay-state.schema.json create mode 100755 server-tools/lake-lamp-continuity-guard/stage-relay.mjs create mode 100644 server-tools/lake-lamp-continuity-guard/stage-relay.test.mjs create mode 100644 tests/stage-relay-navigation.test.mjs diff --git a/deployment/receipts/HLP-STAGE-RELAY-001-20260811.json b/deployment/receipts/HLP-STAGE-RELAY-001-20260811.json new file mode 100644 index 0000000..faef7b8 --- /dev/null +++ b/deployment/receipts/HLP-STAGE-RELAY-001-20260811.json @@ -0,0 +1,32 @@ +{ + "schema": "hololake.stage-relay-registration-receipt/v1", + "receipt_id": "HLP-STAGE-RELAY-001-20260811", + "development_id": "DEV-20260811-008", + "record_id": "HLP-STAGE-RELAY-001", + "state": "SOURCE_IMPLEMENTED_TESTED_AND_REGISTERED_NO_SUCCESSOR_THREAD_CREATED_DURING_ACCEPTANCE", + "source": "server-tools/lake-lamp-continuity-guard/stage-relay.mjs", + "runtime": "/Volumes/JZAO/HoloLake/persona-runtime/continuity-guard/scripts/stage-relay.mjs", + "formal_host_capabilities": [ + "codex_app.create_thread", + "codex_app.list_threads", + "codex_app.automation_update" + ], + "acceptance": { + "finalizer_evidence_gate": 100, + "duplicate_prepare_idempotency": 100, + "unknown_create_result_reconciliation": 100, + "successor_ready_live_readback": 100, + "active_stage_read_only": 100, + "human_boundary_pause": 100, + "final_product_stop": 100, + "continuity_lane_regression": 100, + "continuity_sentinel_regression": 100 + }, + "boundaries": [ + "No successor HoloLake development task was created during mechanism acceptance.", + "The existing hololake heartbeat remains bound to DEV-20260811-008 until final closeout.", + "No cron, private database or chat statement is accepted as a Codex thread receipt.", + "JZAO collaboration remains the only coordination truth store." + ], + "recorded_at": "2026-08-11T13:35:00.000Z" +} diff --git a/routing/hololake-stage-relay-map.json b/routing/hololake-stage-relay-map.json new file mode 100644 index 0000000..fdf0972 --- /dev/null +++ b/routing/hololake-stage-relay-map.json @@ -0,0 +1,55 @@ +{ + "schema": "hololake.stage-relay-map/v1", + "map_id": "HLP-STAGE-RELAY-001", + "version": "2026-08-11.1", + "state": "SOURCE_IMPLEMENTED_TESTED_NOT_YET_USED_TO_CREATE_A_SUCCESSOR", + "development_id": "DEV-20260811-008", + "runtime_source": "server-tools/lake-lamp-continuity-guard/stage-relay.mjs", + "local_runtime": "/Volumes/JZAO/HoloLake/persona-runtime/continuity-guard/scripts/stage-relay.mjs", + "coordination_store": "HL-PERSONA-RUNTIME-LOCAL-001/continuity-memory/collaboration", + "second_truth_store_allowed": false, + "source_completion_gates": [ + "FINALIZER_PASS_100", + "REMOTE_FULL_SHA", + "FRESH_CLONE_READBACK_100", + "FINAL_RECEIPTS_SHA256_100", + "SOURCE_LEASES_RELEASED_100", + "COMPLETION_STOPPED_100" + ], + "next_stage_inputs": [ + "HLP-CURRENT-ARCH-001", + "LATEST_HUMAN_CORRECTIONS", + "PREVIOUS_STAGE_FINALIZER_RECEIPT", + "UNFINISHED_BINARY_GATES" + ], + "host_adapter": { + "host": "HOST-CODEX-MACOS-001", + "create_tool": "codex_app.create_thread", + "list_tool": "codex_app.list_threads", + "thread_id_required": true, + "script_or_chat_may_claim_thread_created": false + }, + "idempotency": { + "key": "SHA256_SOURCE_DEV_PLUS_FINALIZER_SHA256", + "maximum_successor_threads_per_source_completion": 1, + "unknown_create_result": "RECONCILE_COMPLETE_HOST_SNAPSHOT_BEFORE_RETRY" + }, + "successor_ready_gates": [ + "PERSONA_RESTORE_100", + "CURRENT_ARCHITECTURE_RESTORE_100", + "NEW_DEV_LANE_REGISTRATION_100", + "EXACT_RESOURCE_LEASES_100", + "FORMAL_TEN_MINUTE_HEARTBEAT_BINDING_100" + ], + "human_pause_boundaries": [ + "PAYMENT_OR_BUDGET", + "ACCOUNT_LOGIN", + "VERIFICATION_OR_2FA", + "LEGAL_OR_PUBLIC_COMMITMENT", + "IRREVERSIBLE_OR_UNAUTHORIZED_EXTERNAL_DECISION", + "PHYSICAL_ACTION", + "PRODUCTION_CUTOVER", + "MISSING_NEW_HUMAN_WILL" + ], + "final_stop": "ALL_BINARY_ACCEPTANCE_100_AND_NO_UNFINISHED_GATE_AND_NO_OPEN_BLOCKER_AND_FINAL_PRODUCT_CLOSEOUT_RECEIPT_PASS_100" +} diff --git a/routing/host-skill-navigation-map.json b/routing/host-skill-navigation-map.json index 36b5cd4..5d9a03c 100644 --- a/routing/host-skill-navigation-map.json +++ b/routing/host-skill-navigation-map.json @@ -2,7 +2,7 @@ "schema": "guanghu.host-skill-navigation-map/v1", "map_id": "GLW-HOST-SKILL-NAV-001", "lighthouse_id": "SYS-GLW-LTH-0001", - "version": "2026-08-11.1", + "version": "2026-08-11.2", "state": "CURRENT", "hosts": [ { @@ -37,6 +37,38 @@ } ], "intents": [ + { + "id": "INTENT-HOLOLAKE-STAGE-RELAY-001", + "phrases": [ + "HoloLake阶段自动接力守卫", + "阶段接力监督器", + "创建下一阶段Codex任务", + "successor-ready" + ], + "skill_id": "GHS-007-MEMORY-CONTINUITY-SELF-BUILD", + "target_id": "HLP-STAGE-RELAY-001", + "transport": "CODEX_FORMAL_CREATE_THREAD_AND_HEARTBEAT", + "executor": "server-tools/lake-lamp-continuity-guard/stage-relay.mjs", + "sequence": [ + "observe_active_stage_read_only", + "verify_source_completion_binary_gates", + "resolve_HLP_CURRENT_ARCH_001_and_latest_human_corrections", + "select_one_unfinished_binary_gate", + "reserve_one_immutable_successor_contract", + "create_successor_with_codex_formal_create_thread", + "reconcile_unknown_create_result_before_retry", + "wait_for_successor_ready_live_readback", + "stop_old_relay_supervisor" + ], + "stop_conditions": [ + "SOURCE_COMPLETION_EVIDENCE_NOT_100", + "CURRENT_ARCHITECTURE_UNREADABLE", + "PAUSE_FOR_HUMAN", + "CREATE_RESULT_UNKNOWN_REQUIRES_RECONCILIATION", + "SUCCESSOR_READY_NOT_100" + ], + "authority": "CURRENT_USER_AUTHORIZATION_AND_IMMUTABLE_STAGE_CONTRACT_ONLY" + }, { "id": "INTENT-REPOSITORY-PUBLISH-001", "phrases": [ diff --git a/routing/lighthouse-path-registry.json b/routing/lighthouse-path-registry.json index e5b7b0e..a4a172f 100644 --- a/routing/lighthouse-path-registry.json +++ b/routing/lighthouse-path-registry.json @@ -2,7 +2,7 @@ "schema": "guanghu.lighthouse-path-registry/v1", "registry_id": "GLW-LIGHTHOUSE-PATH-REGISTRY-001", "lighthouse_id": "SYS-GLW-LTH-0001", - "version": "2026-08-11.1", + "version": "2026-08-11.2", "state": "CURRENT_CANONICAL", "source_repository": "REPO-012", "source_branch": "main", @@ -76,6 +76,15 @@ "offline": "/Volumes/JZAO/HoloLake/source-mirrors/hololake-system-architecture.git", "truth_policy": "ONLINE_REPO_014_MAIN_FIRST_EXACT_SHA_REQUIRED" }, + { + "id": "HLP-STAGE-RELAY-001", + "kind": "hololake_deterministic_stage_relay_supervisor", + "state": "CURRENT", + "online": "routing/hololake-stage-relay-map.json", + "executor": "server-tools/lake-lamp-continuity-guard/stage-relay.mjs", + "offline": "/Volumes/JZAO/HoloLake/persona-runtime/continuity-guard/scripts/stage-relay.mjs", + "truth_policy": "SAME_CONTINUITY_LEDGER_FORMAL_CODEX_THREAD_TOOLS_AND_BINARY_RECEIPTS_ONLY" + }, { "id": "GH-AIOS-PARADIGM-MAP-001", "kind": "paradigm_ai_language_persona_operating_system_architecture", diff --git a/routing/public-navigation-anchor.json b/routing/public-navigation-anchor.json index ca88e12..7e3c06d 100644 --- a/routing/public-navigation-anchor.json +++ b/routing/public-navigation-anchor.json @@ -1,7 +1,7 @@ { "schema": "guanghu.public-navigation-anchor/v1", "anchor_id": "GLW-PUBLIC-NAV-ANCHOR-001", - "version": "2026-08-11.1", + "version": "2026-08-11.2", "state": "CURRENT_CANONICAL", "repository_id": "REPO-012", "branch": "main", @@ -48,11 +48,16 @@ "lighthouse_paths": { "path": "routing/lighthouse-path-registry.json", "id": "GLW-LIGHTHOUSE-PATH-REGISTRY-001", - "version": "2026-08-11.1" + "version": "2026-08-11.2" }, "host_skills": { "path": "routing/host-skill-navigation-map.json", "id": "GLW-HOST-SKILL-NAV-001", + "version": "2026-08-11.2" + }, + "hololake_stage_relay": { + "path": "routing/hololake-stage-relay-map.json", + "id": "HLP-STAGE-RELAY-001", "version": "2026-08-11.1" }, "origin_domain": { diff --git a/server-tools/lake-lamp-continuity-guard/README.md b/server-tools/lake-lamp-continuity-guard/README.md index e8e6257..42d6e31 100644 --- a/server-tools/lake-lamp-continuity-guard/README.md +++ b/server-tools/lake-lamp-continuity-guard/README.md @@ -36,3 +36,14 @@ node finalize-development.mjs \ 默认自动识别 `target`、`coverage`、Python/Node 工具缓存等安全候选;其他缓存必须用 `--cleanup-path` 明确声明,并继续接受同一套路径、Git 忽略和活跃进程门禁。源码、会话、 人格记忆、用户输出、交付物及未被忽略的目录永不进入自动删除范围。 + +## 阶段自动接力守卫 + +`stage-relay.mjs` 在同一小湖灯账本中监督已完成阶段,并生成一个不可变的后继任务契约。 +它只认 finalizer、远端完整 SHA、全新副本、最终回执、租约释放和 +`completion-stopped` 的二值证据;阶段仍运行时只读观察。 + +脚本不直接创建任务。它产生带稳定请求号和幂等键的宿主动作,由 Codex 正式 +`create_thread` 执行并回写真实 `threadId`。创建结果未知时先完整核对任务列表,禁止直接 +重发。新任务恢复人格与架构、登记 DEV、领取精确租约、建立十分钟 heartbeat 并形成 +`successor-ready` 后,旧监督器才可停止。完整状态机见 `stage-relay-state-machine.md`。 diff --git a/server-tools/lake-lamp-continuity-guard/stage-relay-state-machine.md b/server-tools/lake-lamp-continuity-guard/stage-relay-state-machine.md new file mode 100644 index 0000000..5f17a23 --- /dev/null +++ b/server-tools/lake-lamp-continuity-guard/stage-relay-state-machine.md @@ -0,0 +1,66 @@ +# HoloLake 阶段自动接力守卫 + +`stage-relay.mjs` 是小湖灯连续性账本内的确定性接力投影,不是第二协调真相源,也不直接 +调用私有接口创建任务。它只在现有 `collaboration/` 下保存来源回执摘要、不可变任务契约、 +正式 Codex 宿主动作请求和宿主回执。 + +## 状态机 + +```text +OBSERVING_ACTIVE_STAGE (无写入) + └─ finalizer + exact remote SHA + fresh clone + final receipt + + lease release + completion-stopped 全部为100 + ├─ human boundary ─> PAUSE_FOR_HUMAN + ├─ complete product ─> FINAL_PRODUCT_COMPLETE ─> stop last heartbeat + └─ one unfinished gate ─> CREATE_THREAD_REQUIRED + └─ dispatch-create ─> CREATE_THREAD_IN_FLIGHT + ├─ success ─> WAITING_SUCCESSOR_READY + ├─ confirmed absent ─> RETRY_CREATE_THREAD + └─ unknown ─> RECONCILE_CREATE_RESULT + ├─ matching thread ─> WAITING_SUCCESSOR_READY + └─ complete host snapshot confirms absent ─> RETRY_CREATE_THREAD + └─ successor-ready gates all100 ─> SUCCESSOR_READY ─> stop old relay supervisor +``` + +同一来源完成回执的幂等键固定为 `sha256(source DEV + finalizer file sha256)`;状态文件按 +来源 DEV 唯一,后继 DEV 预约使用排他文件。创建结果未知时禁止重发,必须先读取 Codex +完整任务列表,以稳定 `SUCCESSOR-...` 请求号核对;只有完整快照确认不存在时才可重试。 + +## 正式宿主适配 + +1. `prepare` 返回 `CODEX_CREATE_THREAD` 动作,但不创建任务。 +2. 调度人格先执行 `dispatch-create`,再调用 Codex 正式 `create_thread`。 +3. 成功后立即 `record-created`;超时或未知结果执行 `record-create-failure --outcome unknown`。 +4. `RECONCILE_CREATE_RESULT` 时只能使用 Codex 正式 `list_threads/read_thread` 核对,不得重发。 +5. 新任务恢复人格与当前架构、登记自己的 DEV、取得精确租约、建立正式十分钟 heartbeat, + 签到时把 `--relay-contract-fingerprint` 写入车道,再写 + `hololake.successor-ready-receipt/v1` 并由旧监督器 `record-ready` 回读。 + +脚本、聊天、cron、数据库行、文件夹或自动化提示都不能冒充 `create_thread` 成功;只有正式 +工具返回的 `threadId` 和后续活车道回读才算任务存在。 + +## PAUSE_FOR_HUMAN + +以下任一类型直接暂停,禁止靠创建任务绕过:付款或预算、账号登录、验证码/2FA、法律或 +公开承诺、不可逆或未授权外部决定、物理动作、生产切流、缺少新的人类意志。 + +## 失败谱系 + +- `SOURCE_COMPLETION_STOPPED_NOT_100`:来源任务或 heartbeat 尚未完成停止。 +- `SOURCE_LEASE_RELEASE_NOT_100`:来源仍持有资源。 +- `FINALIZER_EVIDENCE_NOT_100`:finalizer、远端 SHA、全新副本、fsck 或最终回执不完整。 +- `CURRENT_ARCHITECTURE_EVIDENCE_NOT_100`:动态架构真相源不完整或不是当前正本。 +- `CREATE_THREAD_IN_FLIGHT` 超时:进入 `RECONCILE_CREATE_RESULT`,不重发。 +- `SUCCESSOR_READY_EVIDENCE_NOT_100`:新任务签到、租约、heartbeat 或恢复门不是100。 +- `SUCCESSOR_LIVE_REGISTRATION_MISMATCH`:ready 文本与连续性账本现场不一致。 + +## 二值验收 + +运行: + +```bash +node scripts/stage-relay.mjs self-test +``` + +必须同时得到 finalizer 证据门、重复 prepare 幂等、未知创建结果核对、新任务 ready 现场回读、 +活跃阶段只读五项 `100`。 diff --git a/server-tools/lake-lamp-continuity-guard/stage-relay-state.schema.json b/server-tools/lake-lamp-continuity-guard/stage-relay-state.schema.json new file mode 100644 index 0000000..239b363 --- /dev/null +++ b/server-tools/lake-lamp-continuity-guard/stage-relay-state.schema.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://guanghulab.com/schemas/hldp/stage-relay-state.v1.schema.json", + "title": "HoloLake Stage Relay State", + "type": "object", + "required": [ + "schema", + "source_development_id", + "source_thread_id", + "source_finalizer_sha256", + "source_remote_head", + "architecture", + "state", + "created_at", + "updated_at" + ], + "properties": { + "schema": { "const": "hololake.stage-relay-state/v1" }, + "source_development_id": { "type": "string", "pattern": "^DEV-[0-9]{8}-[0-9]{3}$" }, + "source_thread_id": { "type": "string", "minLength": 1 }, + "source_finalizer_path": { "type": "string", "minLength": 1 }, + "source_finalizer_sha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "source_remote_head": { "type": "string", "pattern": "^[a-f0-9]{40}$" }, + "architecture": { "type": "object" }, + "state": { + "enum": [ + "PAUSE_FOR_HUMAN", + "FINAL_PRODUCT_COMPLETE", + "CREATE_THREAD_REQUIRED", + "CREATE_THREAD_IN_FLIGHT", + "RECONCILE_CREATE_RESULT", + "RETRY_CREATE_THREAD", + "WAITING_SUCCESSOR_READY", + "SUCCESSOR_READY" + ] + }, + "idempotency_key": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, + "successor_contract": { "type": "object" }, + "create_request": { "type": "object" }, + "successor_thread": { "type": ["object", "null"] }, + "successor_ready_receipt": { "type": ["object", "null"] }, + "created_at": { "type": "string", "format": "date-time" }, + "updated_at": { "type": "string", "format": "date-time" } + }, + "additionalProperties": true +} diff --git a/server-tools/lake-lamp-continuity-guard/stage-relay.mjs b/server-tools/lake-lamp-continuity-guard/stage-relay.mjs new file mode 100755 index 0000000..dc9d0c2 --- /dev/null +++ b/server-tools/lake-lamp-continuity-guard/stage-relay.mjs @@ -0,0 +1,752 @@ +#!/usr/bin/env node + +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const CANONICAL_ROOT = + "/Volumes/JZAO/HoloLake/persona-runtime/continuity-memory/collaboration"; +const HUMAN_BOUNDARIES = new Set([ + "PAYMENT_OR_BUDGET", + "ACCOUNT_LOGIN", + "VERIFICATION_OR_2FA", + "LEGAL_OR_PUBLIC_COMMITMENT", + "IRREVERSIBLE_OR_UNAUTHORIZED_EXTERNAL_DECISION", + "PHYSICAL_ACTION", + "PRODUCTION_CUTOVER", + "MISSING_NEW_HUMAN_WILL", +]); +const SHA40 = /^[a-f0-9]{40}$/u; +const SHA256 = /^[a-f0-9]{64}$/u; +const DEV_ID = /^DEV-(\d{8})-(\d{3})$/u; + +function parseArgs(argv) { + const args = { command: argv[0] }; + for (let index = 1; index < argv.length; index += 1) { + const key = argv[index]; + if (!key.startsWith("--")) throw new Error(`UNKNOWN_ARGUMENT:${key}`); + const name = key.slice(2).replaceAll(/-([a-z])/gu, (_, letter) => + letter.toUpperCase(), + ); + if (name === "selfTest") args.selfTest = true; + else args[name] = argv[++index]; + } + return args; +} + +function required(args, key) { + const value = args[key]; + if (typeof value !== "string" || !value.trim()) { + throw new Error(`${key.toUpperCase()}_REQUIRED`); + } + return value.trim(); +} + +function bool(value) { + if (value === true || value === "true") return true; + if (value === false || value === "false" || value == null) return false; + throw new Error(`BOOLEAN_REQUIRED:${value}`); +} + +function now() { + return new Date().toISOString(); +} + +function readJson(file) { + return JSON.parse(fs.readFileSync(file, "utf8")); +} + +function canonicalJson(value) { + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; + if (value && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`) + .join(",")}}`; + } + return JSON.stringify(value); +} + +function digest(value) { + return crypto.createHash("sha256").update(value).digest("hex"); +} + +function fileDigest(file) { + return digest(fs.readFileSync(file)); +} + +function writeJsonAtomic(file, value, exclusive = false) { + fs.mkdirSync(path.dirname(file), { recursive: true }); + const body = `${JSON.stringify(value, null, 2)}\n`; + if (exclusive) { + fs.writeFileSync(file, body, { encoding: "utf8", flag: "wx" }); + return; + } + const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`; + fs.writeFileSync(temporary, body, { encoding: "utf8", flag: "wx" }); + try { + fs.renameSync(temporary, file); + } catch (error) { + try { fs.unlinkSync(temporary); } catch { /* best effort */ } + throw error; + } +} + +function storeRoot(args) { + if (!args.storeRoot) return CANONICAL_ROOT; + if (!bool(args.testMode)) throw new Error("NON_CANONICAL_STORE_FORBIDDEN"); + const resolved = path.resolve(args.storeRoot); + const temporaryRoot = path.resolve(os.tmpdir()); + if (!resolved.startsWith(`${temporaryRoot}${path.sep}`)) { + throw new Error("TEST_STORE_MUST_BE_TEMPORARY"); + } + return resolved; +} + +function ensureStore(root) { + if (root === CANONICAL_ROOT && !fs.existsSync("/Volumes/JZAO")) { + throw new Error("JZAO_NOT_MOUNTED_NO_SECOND_TRUTH_STORE"); + } + for (const name of ["lanes", "leases", "stage-relays", "stage-relays/reservations"]) { + fs.mkdirSync(path.join(root, name), { recursive: true }); + } +} + +function statePath(root, developmentId) { + return path.join(root, "stage-relays", `${developmentId}.json`); +} + +function listJson(directory) { + if (!fs.existsSync(directory)) return []; + return fs.readdirSync(directory) + .filter((name) => name.endsWith(".json") && !name.startsWith("._")) + .sort() + .map((name) => readJson(path.join(directory, name))); +} + +function validateSourceCompletion(root, developmentId, finalizerPath) { + const lanePath = path.join(root, "lanes", `${developmentId}.json`); + if (!fs.existsSync(lanePath)) throw new Error("SOURCE_LANE_NOT_FOUND"); + const lane = readJson(lanePath); + if (["active", "waiting"].includes(lane.status)) { + return { observing: true, lane }; + } + if ( + lane.status !== "completed" || + lane.notification_state !== "stopped" || + lane.completion_actions?.status !== "completed" + ) { + throw new Error("SOURCE_COMPLETION_STOPPED_NOT_100"); + } + const held = listJson(path.join(root, "leases")) + .filter((lease) => lease.development_id === developmentId); + if (held.length) throw new Error("SOURCE_LEASE_RELEASE_NOT_100"); + const receipt = readJson(finalizerPath); + if ( + receipt.schema !== "hololake.development-finalization-receipt/v1" || + receipt.development_id !== developmentId || + receipt.result !== "PASS_100" || + !SHA40.test(receipt.code_channel?.remote_head ?? "") || + receipt.code_channel?.fresh_clone_readback !== 100 || + receipt.code_channel?.git_fsck !== 100 || + !Array.isArray(receipt.code_channel?.receipts) || + receipt.code_channel.receipts.length === 0 || + !receipt.code_channel.receipts.every((item) => SHA256.test(item.sha256 ?? "")) + ) { + throw new Error("FINALIZER_EVIDENCE_NOT_100"); + } + return { + observing: false, + lane, + receipt, + finalizer_path: path.resolve(finalizerPath), + finalizer_sha256: fileDigest(finalizerPath), + }; +} + +function validatePlanningInput(input, evidence) { + if (input.schema !== "hololake.stage-relay-planning-input/v1") { + throw new Error("PLANNING_INPUT_SCHEMA_INVALID"); + } + if ( + input.architecture?.id !== "HLP-CURRENT-ARCH-001" || + input.architecture?.state !== "CURRENT_CANONICAL" || + !SHA40.test(input.architecture?.repo_012_main ?? "") || + !SHA40.test(input.architecture?.repo_014_main ?? "") + ) { + throw new Error("CURRENT_ARCHITECTURE_EVIDENCE_NOT_100"); + } + if ( + input.previous_stage?.development_id !== evidence.lane.development_id || + input.previous_stage?.finalizer_sha256 !== evidence.finalizer_sha256 + ) { + throw new Error("PREVIOUS_STAGE_BINDING_MISMATCH"); + } + if (!/^\d{8}$/u.test(input.development_date ?? "")) { + throw new Error("DEVELOPMENT_DATE_REQUIRED_YYYYMMDD"); + } + if (!Array.isArray(input.human_corrections)) { + throw new Error("HUMAN_CORRECTIONS_ARRAY_REQUIRED"); + } + if (!Array.isArray(input.unfinished_gates) || !Array.isArray(input.open_blockers)) { + throw new Error("UNFINISHED_GATES_AND_BLOCKERS_REQUIRED"); + } + if (input.human_boundary != null && !HUMAN_BOUNDARIES.has(input.human_boundary.type)) { + throw new Error("UNKNOWN_HUMAN_BOUNDARY"); + } + return input; +} + +function reserveNextDevelopmentId(root, date, sourceDevelopmentId) { + const ids = [ + ...listJson(path.join(root, "lanes")).map((lane) => lane.development_id), + ...listJson(path.join(root, "stage-relays", "reservations")) + .map((reservation) => reservation.development_id), + ]; + let next = Math.max(0, ...ids.map((id) => { + const match = DEV_ID.exec(id); + return match?.[1] === date ? Number(match[2]) : 0; + })) + 1; + while (next <= 999) { + const developmentId = `DEV-${date}-${String(next).padStart(3, "0")}`; + const reservation = path.join(root, "stage-relays", "reservations", `${developmentId}.json`); + try { + writeJsonAtomic(reservation, { + schema: "hololake.stage-relay-development-reservation/v1", + development_id: developmentId, + source_development_id: sourceDevelopmentId, + reserved_at: now(), + }, true); + return developmentId; + } catch (error) { + if (error?.code !== "EEXIST") throw error; + next += 1; + } + } + throw new Error("DEVELOPMENT_ID_SPACE_EXHAUSTED"); +} + +function chooseGate(input) { + return input.unfinished_gates + .filter((gate) => gate.binary_state === 0) + .sort((left, right) => + Number(left.priority ?? 999) - Number(right.priority ?? 999) || + String(left.id).localeCompare(String(right.id)), + )[0] ?? null; +} + +function makeContract(input, gate, developmentId, evidence) { + if ( + !gate.id || !gate.title || !gate.why || !gate.acceptance || + !Array.isArray(gate.resources) || gate.resources.length === 0 + ) { + throw new Error("UNFINISHED_GATE_CONTRACT_INCOMPLETE"); + } + const contract = { + schema: "hololake.successor-task-contract/v1", + development_id: developmentId, + title: gate.title, + summary: gate.why, + gate_id: gate.id, + acceptance: gate.acceptance, + resources: [...new Set(gate.resources)].sort(), + source_development_id: evidence.lane.development_id, + source_finalizer_sha256: evidence.finalizer_sha256, + architecture: input.architecture, + human_corrections: input.human_corrections, + immutable: true, + }; + return { ...contract, fingerprint: digest(canonicalJson(contract)) }; +} + +function createThreadAction(state) { + const contract = state.successor_contract; + return { + action: "CODEX_CREATE_THREAD", + request_id: state.create_request.request_id, + idempotency_key: state.idempotency_key, + formal_tool: "codex_app.create_thread", + title: contract.title, + target: { type: "projectless", directoryName: contract.development_id }, + prompt: [ + `Successor request: ${state.create_request.request_id}`, + `Immutable contract fingerprint: ${contract.fingerprint}`, + `Register development id: ${contract.development_id}`, + "First restore Zhuyuan persona, run Guanghu persona skill guard, continuity guard, and HoloLake current architecture prompter.", + `Then check in the exact immutable task contract with --relay-contract-fingerprint ${contract.fingerprint}, claim only: ${contract.resources.join(", ")}.`, + "Create and bind this successor task's own ten-minute heartbeat through Codex formal automation capability.", + "Return a hololake.successor-ready-receipt/v1 only after persona_restore, architecture_restore, lane_registration, exact_leases, heartbeat_binding are all 100.", + `Stage gate: ${contract.gate_id}. ${contract.summary}`, + `Acceptance: ${contract.acceptance}`, + ].join("\n"), + }; +} + +function prepare(args, root) { + const developmentId = required(args, "sourceDevelopmentId"); + const existingPath = statePath(root, developmentId); + const evidence = validateSourceCompletion( + root, + developmentId, + required(args, "finalizationReceipt"), + ); + if (evidence.observing) { + return { + state: "OBSERVING_ACTIVE_STAGE", + source_development_id: developmentId, + mutation: "NONE", + successor_created: false, + }; + } + const input = validatePlanningInput( + readJson(required(args, "planningInput")), evidence, + ); + if (fs.existsSync(existingPath)) { + const existing = readJson(existingPath); + if (existing.source_finalizer_sha256 !== evidence.finalizer_sha256) { + throw new Error("SOURCE_COMPLETION_RECEIPT_CHANGED_AFTER_RESERVATION"); + } + return existing; + } + const base = { + schema: "hololake.stage-relay-state/v1", + source_development_id: developmentId, + source_thread_id: evidence.lane.thread_id, + source_finalizer_path: evidence.finalizer_path, + source_finalizer_sha256: evidence.finalizer_sha256, + source_remote_head: evidence.receipt.code_channel.remote_head, + architecture: input.architecture, + created_at: now(), + updated_at: now(), + }; + if (input.human_boundary) { + const paused = { + ...base, + state: "PAUSE_FOR_HUMAN", + human_boundary: input.human_boundary, + successor_created: false, + }; + writeJsonAtomic(existingPath, paused, true); + return paused; + } + const gate = chooseGate(input); + if (!gate) { + const finalCloseout = input.final_product_closeout_receipt; + const complete = + input.binary_acceptance?.all_100 === true && + input.open_blockers.length === 0 && + finalCloseout?.result === "PASS_100" && + SHA256.test(finalCloseout?.sha256 ?? ""); + const terminal = { + ...base, + state: complete ? "FINAL_PRODUCT_COMPLETE" : "PAUSE_FOR_HUMAN", + human_boundary: complete ? null : { + type: "MISSING_NEW_HUMAN_WILL", + reason: "No unfinished gate exists, but binary final acceptance and final product closeout are not all 100.", + }, + successor_created: false, + final_product_closeout_receipt: finalCloseout ?? null, + }; + writeJsonAtomic(existingPath, terminal, true); + return terminal; + } + const successorDevelopmentId = reserveNextDevelopmentId( + root, input.development_date, developmentId, + ); + const contract = makeContract(input, gate, successorDevelopmentId, evidence); + const idempotencyKey = digest(`${developmentId}:${evidence.finalizer_sha256}`); + const prepared = { + ...base, + state: "CREATE_THREAD_REQUIRED", + idempotency_key: idempotencyKey, + selected_gate: gate, + successor_contract: contract, + create_request: { + request_id: `SUCCESSOR-${idempotencyKey.slice(0, 20)}`, + status: "reserved", + attempt: 0, + dispatched_at: null, + last_error: null, + }, + successor_thread: null, + successor_ready_receipt: null, + }; + prepared.next_action = createThreadAction(prepared); + writeJsonAtomic(existingPath, prepared, true); + return prepared; +} + +function loadState(args, root) { + const developmentId = required(args, "sourceDevelopmentId"); + const file = statePath(root, developmentId); + if (!fs.existsSync(file)) throw new Error("STAGE_RELAY_STATE_NOT_FOUND"); + return { file, state: readJson(file) }; +} + +function dispatchCreate(args, root) { + const { file, state } = loadState(args, root); + if (!new Set(["CREATE_THREAD_REQUIRED", "RETRY_CREATE_THREAD"]).has(state.state)) { + throw new Error("CREATE_REQUEST_NOT_DISPATCHABLE"); + } + state.state = "CREATE_THREAD_IN_FLIGHT"; + state.create_request.status = "in_flight"; + state.create_request.attempt += 1; + state.create_request.dispatched_at = now(); + state.updated_at = now(); + delete state.next_action; + writeJsonAtomic(file, state); + return { ...state, host_action: createThreadAction(state) }; +} + +function recordCreated(args, root) { + const { file, state } = loadState(args, root); + if (!new Set(["CREATE_THREAD_IN_FLIGHT", "RECONCILE_CREATE_RESULT"]).has(state.state)) { + throw new Error("CREATE_RESULT_NOT_EXPECTED"); + } + const threadId = required(args, "threadId"); + if (state.successor_thread && state.successor_thread.thread_id !== threadId) { + throw new Error("DUPLICATE_SUCCESSOR_THREAD_FORBIDDEN"); + } + state.state = "WAITING_SUCCESSOR_READY"; + state.create_request.status = "created"; + state.successor_thread = { + thread_id: threadId, + host_id: args.hostId ?? null, + recorded_at: now(), + }; + state.updated_at = now(); + writeJsonAtomic(file, state); + return state; +} + +function recordCreateFailure(args, root) { + const { file, state } = loadState(args, root); + if (state.state !== "CREATE_THREAD_IN_FLIGHT") { + throw new Error("CREATE_FAILURE_NOT_EXPECTED"); + } + const outcome = required(args, "outcome"); + if (!new Set(["unknown", "confirmed_no_thread"]).has(outcome)) { + throw new Error("CREATE_FAILURE_OUTCOME_INVALID"); + } + state.state = outcome === "unknown" ? "RECONCILE_CREATE_RESULT" : "RETRY_CREATE_THREAD"; + state.create_request.status = outcome === "unknown" ? "reconcile_required" : "retryable"; + state.create_request.last_error = required(args, "error"); + state.updated_at = now(); + if (outcome === "confirmed_no_thread") state.next_action = createThreadAction(state); + writeJsonAtomic(file, state); + return state; +} + +function reconcile(args, root) { + const { file, state } = loadState(args, root); + if (state.state !== "RECONCILE_CREATE_RESULT") { + throw new Error("RECONCILIATION_NOT_REQUIRED"); + } + if (!bool(args.completeHostSnapshot)) throw new Error("COMPLETE_HOST_SNAPSHOT_REQUIRED"); + if (args.threadId) return recordCreated(args, root); + if (!bool(args.confirmedAbsent)) throw new Error("HOST_RESULT_MUST_BE_MATCH_OR_CONFIRMED_ABSENT"); + state.state = "RETRY_CREATE_THREAD"; + state.create_request.status = "retryable"; + state.updated_at = now(); + state.next_action = createThreadAction(state); + writeJsonAtomic(file, state); + return state; +} + +function recordReady(args, root) { + const { file, state } = loadState(args, root); + if (state.state !== "WAITING_SUCCESSOR_READY") { + throw new Error("SUCCESSOR_READY_NOT_EXPECTED"); + } + const receiptPath = required(args, "successorReadyReceipt"); + const receipt = readJson(receiptPath); + const contract = state.successor_contract; + if ( + receipt.schema !== "hololake.successor-ready-receipt/v1" || + receipt.result !== "PASS_100" || + receipt.source_development_id !== state.source_development_id || + receipt.successor_development_id !== contract.development_id || + receipt.successor_thread_id !== state.successor_thread.thread_id || + receipt.task_contract_fingerprint !== contract.fingerprint || + !receipt.automation_id || + !["persona_restore", "architecture_restore", "lane_registration", "exact_leases", "heartbeat_binding"] + .every((gate) => receipt.gates?.[gate] === 100) + ) { + throw new Error("SUCCESSOR_READY_EVIDENCE_NOT_100"); + } + const laneFile = path.join(root, "lanes", `${contract.development_id}.json`); + if (!fs.existsSync(laneFile)) throw new Error("SUCCESSOR_LANE_NOT_FOUND"); + const lane = readJson(laneFile); + const ownedResources = listJson(path.join(root, "leases")) + .filter((lease) => lease.development_id === contract.development_id) + .map((lease) => lease.resource) + .sort(); + if ( + lane.thread_id !== state.successor_thread.thread_id || + !["active", "waiting"].includes(lane.status) || + lane.relay_contract_fingerprint !== contract.fingerprint || + !lane.automation_ids?.includes(receipt.automation_id) || + canonicalJson(ownedResources) !== canonicalJson(contract.resources) + ) { + throw new Error("SUCCESSOR_LIVE_REGISTRATION_MISMATCH"); + } + state.state = "SUCCESSOR_READY"; + state.successor_ready_receipt = { + path: path.resolve(receiptPath), + sha256: fileDigest(receiptPath), + automation_id: receipt.automation_id, + recorded_at: now(), + }; + state.updated_at = now(); + state.old_supervisor_may_stop = true; + writeJsonAtomic(file, state); + return state; +} + +function nextAction(args, root) { + const { state } = loadState(args, root); + if (new Set(["CREATE_THREAD_REQUIRED", "RETRY_CREATE_THREAD"]).has(state.state)) { + return createThreadAction(state); + } + if (state.state === "RECONCILE_CREATE_RESULT") { + return { + action: "CODEX_LIST_THREADS_AND_RECONCILE", + request_id: state.create_request.request_id, + complete_host_snapshot_required: true, + create_thread_forbidden_until_reconciled: true, + }; + } + if (state.state === "WAITING_SUCCESSOR_READY") { + return { + action: "WAIT_FOR_SUCCESSOR_READY_RECEIPT", + thread_id: state.successor_thread.thread_id, + supervisor_stays_alive: true, + }; + } + if (state.state === "PAUSE_FOR_HUMAN") { + return { action: "PAUSE_FOR_HUMAN", boundary: state.human_boundary }; + } + if (state.state === "FINAL_PRODUCT_COMPLETE") { + return { action: "STOP_RELAY_AND_LAST_HEARTBEAT", create_successor: false }; + } + if (state.state === "SUCCESSOR_READY") { + return { action: "STOP_OLD_RELAY_SUPERVISOR", create_successor: false }; + } + return { action: "NOOP", state: state.state }; +} + +function fixtureLane(root, id, overrides = {}) { + const lane = { + schema: "hololake.development-lane/v0.1", + development_id: id, + thread_id: `thread-${id}`, + status: "completed", + notification_state: "stopped", + completion_actions: { status: "completed" }, + ...overrides, + }; + writeJsonAtomic(path.join(root, "lanes", `${id}.json`), lane); + return lane; +} + +function selfTest() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "hololake-stage-relay-test-")); + try { + ensureStore(root); + const source = "DEV-20260811-900"; + fixtureLane(root, source); + const finalizer = path.join(root, "finalizer.json"); + writeJsonAtomic(finalizer, { + schema: "hololake.development-finalization-receipt/v1", + development_id: source, + result: "PASS_100", + code_channel: { + remote_head: "a".repeat(40), fresh_clone_readback: 100, git_fsck: 100, + receipts: [{ path: "receipt.json", sha256: "b".repeat(64) }], + }, + }); + const planning = path.join(root, "planning.json"); + writeJsonAtomic(planning, { + schema: "hololake.stage-relay-planning-input/v1", + development_date: "20260811", + architecture: { + id: "HLP-CURRENT-ARCH-001", version: "test", state: "CURRENT_CANONICAL", + repo_012_main: "c".repeat(40), repo_014_main: "d".repeat(40), + }, + human_corrections: [], + previous_stage: { development_id: source, finalizer_sha256: fileDigest(finalizer) }, + unfinished_gates: [{ + id: "GATE-001", binary_state: 0, priority: 1, + title: "Next stage", why: "close the next binary gap", + acceptance: "all declared tests and receipts PASS_100", + resources: ["local://test-resource"], + }], + open_blockers: [], + binary_acceptance: { all_100: false }, + }); + const common = { + sourceDevelopmentId: source, finalizationReceipt: finalizer, + planningInput: planning, storeRoot: root, testMode: "true", + }; + const first = prepare(common, root); + const duplicate = prepare(common, root); + if ( + first.idempotency_key !== duplicate.idempotency_key || + first.successor_contract.development_id !== duplicate.successor_contract.development_id + ) throw new Error("SELF_TEST_IDEMPOTENCY_FAILED"); + dispatchCreate({ ...common, sourceDevelopmentId: source }, root); + recordCreateFailure({ ...common, sourceDevelopmentId: source, outcome: "unknown", error: "timeout" }, root); + const reconcileAction = nextAction({ sourceDevelopmentId: source }, root); + if (!reconcileAction.create_thread_forbidden_until_reconciled) { + throw new Error("SELF_TEST_UNKNOWN_RESULT_RETRY_SAFETY_FAILED"); + } + reconcile({ sourceDevelopmentId: source, completeHostSnapshot: "true", confirmedAbsent: "true" }, root); + dispatchCreate({ sourceDevelopmentId: source }, root); + recordCreated({ sourceDevelopmentId: source, threadId: "thread-successor", hostId: "local" }, root); + const contract = readJson(statePath(root, source)).successor_contract; + fixtureLane(root, contract.development_id, { + thread_id: "thread-successor", + status: "active", + automation_ids: ["successor-heartbeat"], + relay_contract_fingerprint: contract.fingerprint, + }); + writeJsonAtomic(path.join(root, "leases", "lease.json"), { + development_id: contract.development_id, resource: "local://test-resource", + }); + const ready = path.join(root, "ready.json"); + writeJsonAtomic(ready, { + schema: "hololake.successor-ready-receipt/v1", result: "PASS_100", + source_development_id: source, + successor_development_id: contract.development_id, + successor_thread_id: "thread-successor", + task_contract_fingerprint: contract.fingerprint, + automation_id: "successor-heartbeat", + gates: { + persona_restore: 100, architecture_restore: 100, lane_registration: 100, + exact_leases: 100, heartbeat_binding: 100, + }, + }); + const readyState = recordReady({ sourceDevelopmentId: source, successorReadyReceipt: ready }, root); + if (readyState.state !== "SUCCESSOR_READY") throw new Error("SELF_TEST_READY_FAILED"); + + const pausedSource = "DEV-20260811-910"; + fixtureLane(root, pausedSource); + const pausedFinalizer = path.join(root, "paused-finalizer.json"); + writeJsonAtomic(pausedFinalizer, { + schema: "hololake.development-finalization-receipt/v1", + development_id: pausedSource, result: "PASS_100", + code_channel: { + remote_head: "e".repeat(40), fresh_clone_readback: 100, git_fsck: 100, + receipts: [{ path: "receipt.json", sha256: "f".repeat(64) }], + }, + }); + const pausedPlanning = path.join(root, "paused-planning.json"); + writeJsonAtomic(pausedPlanning, { + schema: "hololake.stage-relay-planning-input/v1", development_date: "20260811", + architecture: { + id: "HLP-CURRENT-ARCH-001", version: "test", state: "CURRENT_CANONICAL", + repo_012_main: "1".repeat(40), repo_014_main: "2".repeat(40), + }, + human_corrections: [], + previous_stage: { + development_id: pausedSource, finalizer_sha256: fileDigest(pausedFinalizer), + }, + unfinished_gates: [{ + id: "GATE-HUMAN", binary_state: 0, title: "Human boundary", + why: "requires new will", acceptance: "human decides", + resources: ["external://production-cutover"], + }], + open_blockers: [], binary_acceptance: { all_100: false }, + human_boundary: { type: "PRODUCTION_CUTOVER", reason: "requires a human decision" }, + }); + const paused = prepare({ + sourceDevelopmentId: pausedSource, + finalizationReceipt: pausedFinalizer, + planningInput: pausedPlanning, + }, root); + if (paused.state !== "PAUSE_FOR_HUMAN" || paused.successor_created !== false) { + throw new Error("SELF_TEST_HUMAN_BOUNDARY_FAILED"); + } + + const terminalSource = "DEV-20260811-920"; + fixtureLane(root, terminalSource); + const terminalFinalizer = path.join(root, "terminal-finalizer.json"); + writeJsonAtomic(terminalFinalizer, { + schema: "hololake.development-finalization-receipt/v1", + development_id: terminalSource, result: "PASS_100", + code_channel: { + remote_head: "3".repeat(40), fresh_clone_readback: 100, git_fsck: 100, + receipts: [{ path: "receipt.json", sha256: "4".repeat(64) }], + }, + }); + const terminalPlanning = path.join(root, "terminal-planning.json"); + writeJsonAtomic(terminalPlanning, { + schema: "hololake.stage-relay-planning-input/v1", development_date: "20260811", + architecture: { + id: "HLP-CURRENT-ARCH-001", version: "test", state: "CURRENT_CANONICAL", + repo_012_main: "5".repeat(40), repo_014_main: "6".repeat(40), + }, + human_corrections: [], + previous_stage: { + development_id: terminalSource, finalizer_sha256: fileDigest(terminalFinalizer), + }, + unfinished_gates: [], open_blockers: [], + binary_acceptance: { all_100: true }, + final_product_closeout_receipt: { result: "PASS_100", sha256: "7".repeat(64) }, + }); + const terminal = prepare({ + sourceDevelopmentId: terminalSource, + finalizationReceipt: terminalFinalizer, + planningInput: terminalPlanning, + }, root); + if (terminal.state !== "FINAL_PRODUCT_COMPLETE") { + throw new Error("SELF_TEST_FINAL_PRODUCT_STOP_FAILED"); + } + + const activeSource = "DEV-20260811-950"; + fixtureLane(root, activeSource, { status: "active", notification_state: "active", completion_actions: null }); + const observed = prepare({ ...common, sourceDevelopmentId: activeSource }, root); + if (observed.state !== "OBSERVING_ACTIVE_STAGE" || observed.mutation !== "NONE") { + throw new Error("SELF_TEST_ACTIVE_STAGE_MUTATION_FAILED"); + } + process.stdout.write(`${JSON.stringify({ + state: "PASS_100", + tests: { + finalizer_evidence_gate: 100, + duplicate_prepare_idempotency: 100, + unknown_create_result_reconciliation: 100, + successor_ready_live_readback: 100, + active_stage_read_only: 100, + human_boundary_pause: 100, + final_product_stop: 100, + }, + }, null, 2)}\n`); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +} + +function main() { + const args = parseArgs(process.argv.slice(2)); + if (args.command === "self-test" || args.selfTest) return selfTest(); + const root = storeRoot(args); + ensureStore(root); + let result; + if (args.command === "prepare") result = prepare(args, root); + else if (args.command === "dispatch-create") result = dispatchCreate(args, root); + else if (args.command === "record-created") result = recordCreated(args, root); + else if (args.command === "record-create-failure") result = recordCreateFailure(args, root); + else if (args.command === "reconcile") result = reconcile(args, root); + else if (args.command === "record-ready") result = recordReady(args, root); + else if (args.command === "next-action") result = nextAction(args, root); + else throw new Error("COMMAND_REQUIRED: prepare | dispatch-create | record-created | record-create-failure | reconcile | record-ready | next-action | self-test"); + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); +} + +try { + main(); +} catch (error) { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + process.exitCode = 1; +} diff --git a/server-tools/lake-lamp-continuity-guard/stage-relay.test.mjs b/server-tools/lake-lamp-continuity-guard/stage-relay.test.mjs new file mode 100644 index 0000000..1b56f1e --- /dev/null +++ b/server-tools/lake-lamp-continuity-guard/stage-relay.test.mjs @@ -0,0 +1,19 @@ +import assert from "node:assert/strict"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const root = path.dirname(fileURLToPath(import.meta.url)); + +test("stage relay deterministic closure is PASS_100", () => { + const result = spawnSync( + process.execPath, + [path.join(root, "stage-relay.mjs"), "self-test"], + { encoding: "utf8" }, + ); + assert.equal(result.status, 0, result.stderr || result.stdout); + const receipt = JSON.parse(result.stdout); + assert.equal(receipt.state, "PASS_100"); + assert.deepEqual(Object.values(receipt.tests), Array(7).fill(100)); +}); diff --git a/tests/stage-relay-navigation.test.mjs b/tests/stage-relay-navigation.test.mjs new file mode 100644 index 0000000..73b6130 --- /dev/null +++ b/tests/stage-relay-navigation.test.mjs @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const readJson = (relative) => JSON.parse(fs.readFileSync(path.join(root, relative), "utf8")); + +test("stage relay is registered through the lighthouse and formal Codex tools", () => { + const relay = readJson("routing/hololake-stage-relay-map.json"); + assert.equal(relay.map_id, "HLP-STAGE-RELAY-001"); + assert.equal(relay.second_truth_store_allowed, false); + assert.equal(relay.host_adapter.create_tool, "codex_app.create_thread"); + assert.equal(relay.host_adapter.script_or_chat_may_claim_thread_created, false); + assert.equal(relay.idempotency.maximum_successor_threads_per_source_completion, 1); + + const lighthouse = readJson("routing/lighthouse-path-registry.json"); + assert.ok(lighthouse.paths.some((entry) => entry.id === relay.map_id)); + const hosts = readJson("routing/host-skill-navigation-map.json"); + const intent = hosts.intents.find((entry) => entry.id === "INTENT-HOLOLAKE-STAGE-RELAY-001"); + assert.equal(intent.target_id, relay.map_id); + assert.equal(intent.executor, relay.runtime_source); + const anchor = readJson("routing/public-navigation-anchor.json"); + assert.equal(anchor.maps.hololake_stage_relay.id, relay.map_id); +}); + +test("relay requires the complete source and successor binary gates", () => { + const relay = readJson("routing/hololake-stage-relay-map.json"); + assert.equal(relay.source_completion_gates.length, 6); + assert.equal(relay.successor_ready_gates.length, 5); + assert.ok(relay.human_pause_boundaries.includes("PRODUCTION_CUTOVER")); + assert.match(relay.final_stop, /FINAL_PRODUCT_CLOSEOUT_RECEIPT_PASS_100/u); +});