feat(lighthouse): add canonical three-host skill navigation
This commit is contained in:
parent
8423a96776
commit
fb5f931e78
14 changed files with 891 additions and 22 deletions
|
|
@ -11,13 +11,19 @@ const DEFAULT_NODE_MAP = path.resolve(__dirname, "../../routing/server-node-map.
|
|||
const DEFAULT_SUBJECT_REGISTRY = path.resolve(__dirname, "../../identity/fifth-domain-subject-registry.json");
|
||||
const DEFAULT_SUBJECT_ALIAS_MAP = path.resolve(__dirname, "../../identity/subject-id-alias-map.json");
|
||||
const DEFAULT_NAVIGATION_MAP = path.resolve(__dirname, "../../routing/ai-machine-navigation-map.json");
|
||||
const SNAPSHOT_KEYS = Object.freeze(["repository", "nodes", "subjects", "aliases", "navigation"]);
|
||||
const DEFAULT_LIGHTHOUSE_PATHS = path.resolve(__dirname, "../../routing/lighthouse-path-registry.json");
|
||||
const DEFAULT_HOST_SKILLS = path.resolve(__dirname, "../../routing/host-skill-navigation-map.json");
|
||||
const SNAPSHOT_KEYS = Object.freeze([
|
||||
"repository", "nodes", "subjects", "aliases", "navigation", "lighthouse_paths", "host_skills",
|
||||
]);
|
||||
const SNAPSHOT_PATHS = Object.freeze({
|
||||
repository: "routing/repository-route-map.json",
|
||||
nodes: "routing/server-node-map.json",
|
||||
subjects: "identity/fifth-domain-subject-registry.json",
|
||||
aliases: "identity/subject-id-alias-map.json",
|
||||
navigation: "routing/ai-machine-navigation-map.json",
|
||||
lighthouse_paths: "routing/lighthouse-path-registry.json",
|
||||
host_skills: "routing/host-skill-navigation-map.json",
|
||||
});
|
||||
|
||||
function loadAnchor(filename = process.env.GUANGHU_NAVIGATION_ANCHOR || DEFAULT_ANCHOR) {
|
||||
|
|
@ -44,6 +50,14 @@ function loadNavigationMap(filename = process.env.GUANGHU_NAVIGATION_MAP || DEFA
|
|||
return JSON.parse(fs.readFileSync(filename, "utf8"));
|
||||
}
|
||||
|
||||
function loadLighthousePaths(filename = process.env.GUANGHU_LIGHTHOUSE_PATHS || DEFAULT_LIGHTHOUSE_PATHS) {
|
||||
return JSON.parse(fs.readFileSync(filename, "utf8"));
|
||||
}
|
||||
|
||||
function loadHostSkills(filename = process.env.GUANGHU_HOST_SKILLS || DEFAULT_HOST_SKILLS) {
|
||||
return JSON.parse(fs.readFileSync(filename, "utf8"));
|
||||
}
|
||||
|
||||
function validateSnapshot(anchor, maps) {
|
||||
if (anchor.schema !== "guanghu.public-navigation-anchor/v1") throw new Error("invalid_anchor_schema");
|
||||
if (anchor.anchor_id !== "GLW-PUBLIC-NAV-ANCHOR-001") throw new Error("invalid_anchor_id");
|
||||
|
|
@ -76,6 +90,8 @@ function loadFileSnapshot(options = {}) {
|
|||
subjects: loadSubjectRegistry(options.subjectRegistryFile),
|
||||
aliases: loadSubjectAliasMap(options.subjectAliasMapFile),
|
||||
navigation: loadNavigationMap(options.navigationMapFile),
|
||||
lighthouse_paths: loadLighthousePaths(options.lighthousePathsFile),
|
||||
host_skills: loadHostSkills(options.hostSkillsFile),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -351,12 +367,100 @@ function compileNavigation(navigationMap, requestedSubject, requestedIntent, sig
|
|||
};
|
||||
}
|
||||
|
||||
function resolveLighthousePath(registry, requestedId) {
|
||||
const id = String(requestedId || "").trim().toUpperCase();
|
||||
const route = (registry.paths || []).find(item => String(item.id).toUpperCase() === id);
|
||||
if (route) {
|
||||
const current = ["CURRENT", "ACTIVE", "ACTIVE_CANDIDATE", "ACTIVE_LOCAL_PRIVATE"].includes(route.state);
|
||||
return {
|
||||
status: current ? 200 : 410,
|
||||
body: {
|
||||
schema: "guanghu.lighthouse-path-resolution/v1",
|
||||
decision: current ? "VALID_REGISTERED" : "INVALID_STATE",
|
||||
lighthouse_id: registry.lighthouse_id,
|
||||
registry_id: registry.registry_id,
|
||||
registry_version: registry.version,
|
||||
route,
|
||||
authority: "NONE_NAVIGATION_ONLY",
|
||||
},
|
||||
};
|
||||
}
|
||||
const redirect = (registry.redirects || []).find(item => String(item.id).toUpperCase() === id);
|
||||
if (redirect) {
|
||||
return {
|
||||
status: 308,
|
||||
body: {
|
||||
schema: "guanghu.lighthouse-path-resolution/v1",
|
||||
decision: "REDIRECT_ONLY_NOT_CURRENT",
|
||||
lighthouse_id: registry.lighthouse_id,
|
||||
registry_id: registry.registry_id,
|
||||
registry_version: registry.version,
|
||||
redirect,
|
||||
authority: "NONE_NAVIGATION_ONLY",
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
error: "lighthouse_path_not_registered_no_guess",
|
||||
requested_id: requestedId,
|
||||
lighthouse_id: registry.lighthouse_id,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function compileHostNavigation(hostMap, lighthouseRegistry, requestedHost, requestedIntent) {
|
||||
const hostKey = normalize(requestedHost);
|
||||
const host = (hostMap.hosts || []).find(item =>
|
||||
[item.id, ...(item.aliases || [])].some(id => normalize(id) === hostKey)
|
||||
);
|
||||
if (!host) return { status: 404, body: { error: "host_unknown_no_guess", requested_host: requestedHost } };
|
||||
const intentText = normalize(requestedIntent);
|
||||
const ranked = (hostMap.intents || [])
|
||||
.map(intent => ({
|
||||
intent,
|
||||
score: (intent.phrases || []).reduce(
|
||||
(score, phrase) => score + (intentText.includes(normalize(phrase)) ? normalize(phrase).length : 0),
|
||||
0,
|
||||
),
|
||||
}))
|
||||
.filter(item => item.score > 0)
|
||||
.sort((a, b) => b.score - a.score || a.intent.id.localeCompare(b.intent.id));
|
||||
if (!ranked.length) {
|
||||
return { status: 404, body: { error: "host_intent_unknown_no_guess", requested_intent: requestedIntent } };
|
||||
}
|
||||
const intent = ranked[0].intent;
|
||||
const target = resolveLighthousePath(lighthouseRegistry, intent.target_id);
|
||||
if (target.status !== 200) {
|
||||
return {
|
||||
status: 503,
|
||||
body: { error: "lighthouse_target_not_current", target_resolution: target.body },
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
schema: "guanghu.host-skill-navigation-bundle/v1",
|
||||
status: "COMPILED_EXACT_NO_GUESS",
|
||||
lighthouse_id: lighthouseRegistry.lighthouse_id,
|
||||
registry_id: lighthouseRegistry.registry_id,
|
||||
host,
|
||||
intent,
|
||||
target: target.body.route,
|
||||
authority: "NONE_NAVIGATION_ONLY",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createServer(options = {}) {
|
||||
const mapFile = options.mapFile || process.env.GUANGHU_REPOSITORY_MAP || DEFAULT_MAP;
|
||||
const nodeMapFile = options.nodeMapFile || process.env.GUANGHU_NODE_MAP || DEFAULT_NODE_MAP;
|
||||
const subjectRegistryFile = options.subjectRegistryFile || process.env.GUANGHU_SUBJECT_REGISTRY || DEFAULT_SUBJECT_REGISTRY;
|
||||
const subjectAliasMapFile = options.subjectAliasMapFile || process.env.GUANGHU_SUBJECT_ALIAS_MAP || DEFAULT_SUBJECT_ALIAS_MAP;
|
||||
const navigationMapFile = options.navigationMapFile || process.env.GUANGHU_NAVIGATION_MAP || DEFAULT_NAVIGATION_MAP;
|
||||
const lighthousePathsFile = options.lighthousePathsFile || process.env.GUANGHU_LIGHTHOUSE_PATHS || DEFAULT_LIGHTHOUSE_PATHS;
|
||||
const hostSkillsFile = options.hostSkillsFile || process.env.GUANGHU_HOST_SKILLS || DEFAULT_HOST_SKILLS;
|
||||
const anchorFile = options.anchorFile || process.env.GUANGHU_NAVIGATION_ANCHOR || DEFAULT_ANCHOR;
|
||||
const gitDir = options.gitDir || process.env.GUANGHU_REPOSITORY_GIT_DIR;
|
||||
const snapshotStore = options.snapshotStore || (gitDir ? new GitSnapshotStore(gitDir) : null);
|
||||
|
|
@ -371,6 +475,7 @@ function createServer(options = {}) {
|
|||
: {
|
||||
...loadFileSnapshot({
|
||||
anchorFile, mapFile, nodeMapFile, subjectRegistryFile, subjectAliasMapFile, navigationMapFile,
|
||||
lighthousePathsFile, hostSkillsFile,
|
||||
}),
|
||||
source_commit: null,
|
||||
source_mode: "FILESYSTEM_SNAPSHOT",
|
||||
|
|
@ -384,6 +489,8 @@ function createServer(options = {}) {
|
|||
const subjectRegistry = snapshot.subjects;
|
||||
const aliasMap = snapshot.aliases;
|
||||
const navigationMap = snapshot.navigation;
|
||||
const lighthousePaths = snapshot.lighthouse_paths;
|
||||
const hostSkills = snapshot.host_skills;
|
||||
if (url.pathname === "/health") {
|
||||
return json(res, snapshot.source_degraded ? 503 : 200, {
|
||||
ok: !snapshot.source_degraded,
|
||||
|
|
@ -399,6 +506,8 @@ function createServer(options = {}) {
|
|||
if (url.pathname === "/v1/nodes") return json(res, 200, withSource(nodeMap, snapshot), 60);
|
||||
if (url.pathname === "/v1/subjects") return json(res, 200, withSource(subjectRegistry, snapshot), 60);
|
||||
if (url.pathname === "/v1/navigation") return json(res, 200, withSource(navigationMap, snapshot), 60);
|
||||
if (url.pathname === "/v1/lighthouse") return json(res, 200, withSource(lighthousePaths, snapshot), 60);
|
||||
if (url.pathname === "/v1/host-skills") return json(res, 200, withSource(hostSkills, snapshot), 60);
|
||||
if (url.pathname === "/v1/entry") {
|
||||
const subject = String(url.searchParams.get("subject") || "").toUpperCase();
|
||||
if (!["ICE-P-ZY001", "ICE-GL-ZY001", "ICE-PZY-001"].includes(subject)) {
|
||||
|
|
@ -427,6 +536,15 @@ function createServer(options = {}) {
|
|||
);
|
||||
return json(res, compiled.status, withSource(compiled.body, snapshot), compiled.status === 200 ? 60 : 0);
|
||||
}
|
||||
if (url.pathname === "/v1/host-navigate") {
|
||||
const compiled = compileHostNavigation(
|
||||
hostSkills,
|
||||
lighthousePaths,
|
||||
String(url.searchParams.get("host") || "").slice(0, 100),
|
||||
String(url.searchParams.get("intent") || "").slice(0, 500),
|
||||
);
|
||||
return json(res, compiled.status, withSource(compiled.body, snapshot), compiled.status === 200 ? 60 : 0);
|
||||
}
|
||||
if (url.pathname === "/v1/search") {
|
||||
const query = String(url.searchParams.get("q") || "").slice(0, 200);
|
||||
const results = searchAll(map, nodeMap, query, subjectRegistry);
|
||||
|
|
@ -493,6 +611,10 @@ function createServer(options = {}) {
|
|||
},
|
||||
}, snapshot), 60);
|
||||
}
|
||||
const lighthouseResolution = resolveLighthousePath(lighthousePaths, id);
|
||||
if (lighthouseResolution.status !== 404) {
|
||||
return json(res, lighthouseResolution.status, withSource(lighthouseResolution.body, snapshot), 60);
|
||||
}
|
||||
return json(res, 404, { error: "route_not_found", id });
|
||||
}
|
||||
if (url.pathname === "/openapi.json") return json(res, 200, openApi(), 3600);
|
||||
|
|
@ -506,6 +628,9 @@ function createServer(options = {}) {
|
|||
subject_registry: "https://guanghulab.com/api/ai/v1/subjects",
|
||||
subject_alias_map: "https://guanghulab.com/code/bingshuo/guanghu-ice-heart/raw/branch/main/identity/subject-id-alias-map.json",
|
||||
machine_navigation_map: "https://guanghulab.com/api/ai/v1/navigation",
|
||||
lighthouse_path_registry: "https://guanghulab.com/api/ai/v1/lighthouse",
|
||||
host_skill_navigation_map: "https://guanghulab.com/api/ai/v1/host-skills",
|
||||
host_navigate_api: "https://guanghulab.com/api/ai/v1/host-navigate?host={HOST}&intent={NATURAL_LANGUAGE_INTENT}",
|
||||
warm_persona_entry: "https://guanghulab.com/api/ai/v1/entry?subject=ICE-P-ZY001",
|
||||
navigate_api: "https://guanghulab.com/api/ai/v1/navigate?subject={SUBJECT}&intent={INTENT}&signals={EXPLICIT_SIGNALS}",
|
||||
search_api: "https://guanghulab.com/api/ai/v1/search?q={query}",
|
||||
|
|
@ -562,6 +687,9 @@ function openApi() {
|
|||
"/v1/nodes": { get: { summary: "读取最新服务器节点与人格路径编号映射", responses: { "200": { description: "Server node map" } } } },
|
||||
"/v1/subjects": { get: { summary: "读取人类、人格体与公共系统的分型身份注册表", responses: { "200": { description: "Subject registry" } } } },
|
||||
"/v1/navigation": { get: { summary: "读取主体与意图驱动的机器导航地图", responses: { "200": { description: "Machine navigation map" } } } },
|
||||
"/v1/lighthouse": { get: { summary: "读取光湖灯塔唯一有效路径注册表", responses: { "200": { description: "Lighthouse path registry" } } } },
|
||||
"/v1/host-skills": { get: { summary: "读取 Codex、Qoder CN 与 QoderWork CN 宿主技能导航表", responses: { "200": { description: "Host skill navigation map" } } } },
|
||||
"/v1/host-navigate": { get: { summary: "按宿主和自然语言意图编译确定性技能路径", responses: { "200": { description: "Compiled host skill route" }, "404": { description: "Unknown host or intent" } } } },
|
||||
"/v1/entry": { get: { summary: "读取常驻人格运行体的快速恢复门;只返回脱敏状态,不授予执行权限", responses: { "200": { description: "Warm resume ready" }, "409": { description: "Full cycle required" }, "503": { description: "Resident runtime unavailable" } } } },
|
||||
"/v1/navigate": { get: { summary: "按明确主体、意图与信号生成最小运行包", parameters: [{ name: "subject", in: "query", required: true, schema: { type: "string", example: "ICE-P-ZY001" } }, { name: "intent", in: "query", schema: { type: "string", example: "persona_restore" } }, { name: "signals", in: "query", schema: { type: "string" } }], responses: { "200": { description: "Compiled exact navigation bundle" }, "404": { description: "Unknown subject or intent; no guessing" } } } },
|
||||
"/v1/search": { get: { summary: "按中文、编号或项目名检索", parameters: [{ name: "q", in: "query", schema: { type: "string" } }], responses: { "200": { description: "Search results" } } } },
|
||||
|
|
@ -578,7 +706,8 @@ if (require.main === module) {
|
|||
|
||||
module.exports = {
|
||||
createServer, loadAnchor, loadMap, loadNodeMap, loadSubjectRegistry, loadSubjectAliasMap, loadNavigationMap,
|
||||
loadLighthousePaths, loadHostSkills,
|
||||
loadFileSnapshot, validateSnapshot, GitSnapshotStore, sourceReceipt,
|
||||
readResidentRuntimeStatus, compileWarmEntry,
|
||||
resolveSubjectId, navigationRoute, compileNavigation, search, searchAll,
|
||||
resolveSubjectId, navigationRoute, compileNavigation, resolveLighthousePath, compileHostNavigation, search, searchAll,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ const { execFileSync } = require("node:child_process");
|
|||
const test = require("node:test");
|
||||
const {
|
||||
createServer, loadAnchor, loadMap, loadNodeMap, loadSubjectRegistry, loadSubjectAliasMap, loadNavigationMap,
|
||||
loadLighthousePaths, loadHostSkills,
|
||||
loadFileSnapshot, GitSnapshotStore, compileWarmEntry,
|
||||
resolveSubjectId, compileNavigation, search, searchAll,
|
||||
resolveSubjectId, compileNavigation, resolveLighthousePath, compileHostNavigation, search, searchAll,
|
||||
} = require("./server");
|
||||
|
||||
function warmRuntimeStatus(overrides = {}) {
|
||||
|
|
@ -65,6 +66,8 @@ test("Git snapshot store follows main atomically and retains the last known-good
|
|||
subjects: ["identity/fifth-domain-subject-registry.json", "FD-SUBJECT-REGISTRY-001"],
|
||||
aliases: ["identity/subject-id-alias-map.json", "FD-SUBJECT-ID-ALIAS-MAP-001"],
|
||||
navigation: ["routing/ai-machine-navigation-map.json", "AI-MACHINE-NAV-001"],
|
||||
lighthouse_paths: ["routing/lighthouse-path-registry.json", "GLW-LIGHTHOUSE-PATH-REGISTRY-001"],
|
||||
host_skills: ["routing/host-skill-navigation-map.json", "GLW-HOST-SKILL-NAV-001"],
|
||||
};
|
||||
fs.mkdirSync(path.join(root, "routing"), { recursive: true });
|
||||
fs.mkdirSync(path.join(root, "identity"), { recursive: true });
|
||||
|
|
@ -79,7 +82,7 @@ test("Git snapshot store follows main atomically and retains the last known-good
|
|||
};
|
||||
for (const [key, [relative, id]] of Object.entries(declarations)) {
|
||||
anchor.maps[key] = { path: relative, id, version: "1" };
|
||||
const idKey = key === "subjects" ? "registry_id" : "map_id";
|
||||
const idKey = ["subjects", "lighthouse_paths"].includes(key) ? "registry_id" : "map_id";
|
||||
fs.writeFileSync(path.join(root, relative), JSON.stringify({ [idKey]: id, version: "1" }));
|
||||
}
|
||||
fs.writeFileSync(path.join(root, "routing/public-navigation-anchor.json"), JSON.stringify(anchor));
|
||||
|
|
@ -211,6 +214,31 @@ test("machine navigator fails closed instead of guessing", () => {
|
|||
assert.equal(compileNavigation(loadNavigationMap(), "ICE-P-ZY001", "please_guess").body.error, "unknown_intent_no_guess");
|
||||
});
|
||||
|
||||
test("lighthouse is the numbered path validity gate", () => {
|
||||
const registry = loadLighthousePaths();
|
||||
assert.equal(resolveLighthousePath(registry, "REPO-012").status, 200);
|
||||
const old = resolveLighthousePath(registry, "REPO-001");
|
||||
assert.equal(old.status, 308);
|
||||
assert.equal(old.body.redirect.redirect_to, "REPO-012");
|
||||
assert.equal(resolveLighthousePath(registry, "RANDOM-PATH").status, 404);
|
||||
});
|
||||
|
||||
test("host navigator compiles local keychain publication without granting authority", () => {
|
||||
const compiled = compileHostNavigation(
|
||||
loadHostSkills(),
|
||||
loadLighthousePaths(),
|
||||
"codex",
|
||||
"你推一下线上仓库",
|
||||
);
|
||||
assert.equal(compiled.status, 200);
|
||||
assert.equal(compiled.body.host.id, "HOST-CODEX-MACOS-001");
|
||||
assert.equal(compiled.body.intent.id, "INTENT-REPOSITORY-PUBLISH-001");
|
||||
assert.equal(compiled.body.target.id, "REPO-012");
|
||||
assert.equal(compiled.body.intent.transport, "LOCAL_GIT_OSXKEYCHAIN");
|
||||
assert.equal(compiled.body.authority, "NONE_NAVIGATION_ONLY");
|
||||
assert.equal(compileHostNavigation(loadHostSkills(), loadLighthousePaths(), "unknown", "推一下线上仓库").status, 404);
|
||||
});
|
||||
|
||||
test("public endpoints are read-only and expose CORS", async () => {
|
||||
const server = createServer({ runtimeStatusProvider: async () => warmRuntimeStatus() });
|
||||
await new Promise(resolve => server.listen(0, "127.0.0.1", resolve));
|
||||
|
|
@ -248,6 +276,14 @@ test("public endpoints are read-only and expose CORS", async () => {
|
|||
);
|
||||
const navigationResponse = await fetch(`${base}/v1/navigation`);
|
||||
assert.equal((await navigationResponse.json()).map_id, "AI-MACHINE-NAV-001");
|
||||
const lighthouseResponse = await fetch(`${base}/v1/lighthouse`);
|
||||
assert.equal((await lighthouseResponse.json()).registry_id, "GLW-LIGHTHOUSE-PATH-REGISTRY-001");
|
||||
const hostNavigateResponse = await fetch(
|
||||
`${base}/v1/host-navigate?host=codex&intent=${encodeURIComponent("推一下线上仓库")}`,
|
||||
);
|
||||
const hostBundle = await hostNavigateResponse.json();
|
||||
assert.equal(hostNavigateResponse.status, 200);
|
||||
assert.equal(hostBundle.intent.id, "INTENT-REPOSITORY-PUBLISH-001");
|
||||
const navigateResponse = await fetch(`${base}/v1/navigate?subject=ICE-P-ZY001&intent=persona_restore&signals=context_compacted`);
|
||||
const bundle = await navigateResponse.json();
|
||||
assert.equal(navigateResponse.status, 200);
|
||||
|
|
|
|||
Loading…
Reference in a new issue