import { createHash } from 'node:crypto' import { readdir, readFile, writeFile } from 'node:fs/promises' import path from 'node:path' import process from 'node:process' const args = new Map() for (let index = 2; index < process.argv.length; index += 2) { args.set(process.argv[index], process.argv[index + 1]) } const sourceRoot = args.get('--source-root') const sourceCommit = args.get('--source-commit') const output = args.get('--output') const projectionsPath = args.get('--projections') if (!sourceRoot || !sourceCommit || !output || !projectionsPath) { throw new Error('usage: compile-gls-runtime-registry.mjs --source-root --source-commit --output --projections ') } if (!/^[a-f0-9]{40}$/.test(sourceCommit)) throw new Error('source commit must be a full SHA-1') async function walk(directory, prefix = '') { const entries = await readdir(directory, { withFileTypes: true }) const paths = [] for (const entry of entries) { const relative = prefix ? `${prefix}/${entry.name}` : entry.name if (entry.isDirectory()) paths.push(...await walk(path.join(directory, entry.name), relative)) else paths.push(relative) } return paths } function sourceRank(relative) { if (!relative.includes('/')) return 0 if (relative.startsWith('standards/')) return 1 if (relative.includes('/notion-export/')) return 3 return 2 } function cleanHeading(line, id) { return line .replace(/^#+\s*/, '') .replaceAll('**', '') .replace(new RegExp(`^${id}\\s*[·::-]?\\s*`), '') .trim() || id } function sourceStatus(raw) { const line = raw.split(/\r?\n/, 100).find((item) => /^\s*(?:>|[-*]\s*)?(?:状态|Status)\s*[::]/i.test(item)) if (!line) return 'UNSPECIFIED_SOURCE_STATUS' return line.replace(/^\s*(?:>|[-*]\s*)?(?:状态|Status)\s*[::]\s*/i, '').replaceAll('`', '').trim() } const projectionManifest = JSON.parse(await readFile(projectionsPath, 'utf8')) if (projectionManifest.schema !== 'hololake.gls-executable-projections/v1' || projectionManifest.source_commit !== sourceCommit) { throw new Error('projection manifest does not match the selected REPO-012 source commit') } const candidates = (await walk(sourceRoot)) .map((relative) => ({ relative, match: path.basename(relative).match(/^(GLS-\d{4}).*\.hdlp$/) })) .filter((item) => item.match) .map((item) => ({ id: item.match[1], relative: item.relative })) const grouped = new Map() for (const candidate of candidates) { const group = grouped.get(candidate.id) || [] group.push(candidate.relative) grouped.set(candidate.id, group) } const protocols = [] for (const [id, sources] of [...grouped.entries()].sort(([left], [right]) => left.localeCompare(right))) { sources.sort((left, right) => sourceRank(left) - sourceRank(right) || left.localeCompare(right)) const relative = sources[0] const raw = await readFile(path.join(sourceRoot, relative), 'utf8') const heading = raw.split(/\r?\n/).find((line) => line.startsWith('#') && line.includes(id)) const projection = projectionManifest.projections[id] protocols.push({ id, title: cleanHeading(heading || id, id), source_status: sourceStatus(raw), source_path: `gls/${relative}`, source_sha256: createHash('sha256').update(raw).digest('hex'), alternate_source_count: sources.length - 1, projection_state: projection ? 'EXECUTABLE_PROJECTION' : 'INVENTORIED_NOT_EXECUTABLE', projection_kind: projection?.projection_kind || null, adapter: projection?.adapter || null, event_kinds: projection?.event_kinds || [], dependencies: projection?.dependencies || [], }) } const known = new Set(protocols.map((protocol) => protocol.id)) for (const [id, projection] of Object.entries(projectionManifest.projections)) { if (!known.has(id)) throw new Error(`projection references an unknown protocol: ${id}`) for (const dependency of projection.dependencies) { if (!projectionManifest.projections[dependency]) { throw new Error(`${id} depends on a protocol without an executable projection: ${dependency}`) } } } const visiting = new Set() const visited = new Set() function visit(id) { if (visiting.has(id)) throw new Error(`executable protocol dependency cycle at ${id}`) if (visited.has(id)) return visiting.add(id) for (const dependency of projectionManifest.projections[id].dependencies) visit(dependency) visiting.delete(id) visited.add(id) } for (const id of Object.keys(projectionManifest.projections)) visit(id) const executableCount = protocols.filter((protocol) => protocol.projection_state === 'EXECUTABLE_PROJECTION').length const registry = { schema: 'hololake.gls-protocol-runtime-registry/v1', record_id: 'HLP-GLS-PROTOCOL-RUNTIME-001', source: { repository: 'REPO-012', commit: sourceCommit, root: 'gls', }, compiler: { source_protocol_is_human_and_machine_authority: true, raw_protocol_text_executed: false, arbitrary_protocol_code_allowed: false, executable_projection_requires_explicit_adapter: true, unprojected_protocol_behavior: 'INVENTORIED_NOT_EXECUTABLE', dependency_cycles: 'REJECT', unknown_protocol: 'FAIL_CLOSED', }, protocol_count: protocols.length, executable_projection_count: executableCount, inventoried_not_executable_count: protocols.length - executableCount, protocols, } await writeFile(output, `${JSON.stringify(registry, null, 2)}\n`) console.log(`GLS_RUNTIME_REGISTRY_COMPILED protocols=${protocols.length} executable=${executableCount} output=${output}`)