139 lines
5.4 KiB
JavaScript
139 lines
5.4 KiB
JavaScript
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}`)
|