hololake-system-architecture/product-source/hololake-native-desktop/scripts/compile-gls-runtime-registry.mjs

139 lines
5.4 KiB
JavaScript
Raw Normal View History

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 <REPO-012/gls> --source-commit <sha> --output <registry.json> --projections <projections.json>')
}
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}`)