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

370 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createHash } from 'node:crypto'
import { readFile, readdir, 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 explicitSourceRoot = args.get('--source-root')
const repoRoot = args.get('--repo-root') || (explicitSourceRoot ? path.dirname(explicitSourceRoot) : null)
const sourceRoot = explicitSourceRoot || (repoRoot ? path.join(repoRoot, 'gls') : null)
const sourceCommit = args.get('--source-commit')
const output = args.get('--output')
const projectionsPath = args.get('--projections')
if (!repoRoot || !sourceRoot || !sourceCommit || !output || !projectionsPath) {
throw new Error('usage: compile-gls-runtime-registry.mjs --repo-root <REPO-012> --source-commit <sha> --output <manifest.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
}
async function readRequired(relative) {
return readFile(path.join(repoRoot, relative), 'utf8')
}
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()
}
function sourceDependencies(raw) {
const match = raw.match(/^\s*depends:\s*\[([^\]]*)\]/m)
return [...new Set(match?.[1].match(/GLS-\d{4}/g) || [])].sort()
}
function contractKind(family, projectionKind) {
if (projectionKind) return projectionKind
const kinds = {
GLS_ENGINEERING: 'COMPILER_OR_INTERMEDIATE_REPRESENTATION',
GLP: 'MESSAGE_SCHEMA_OR_SYNC_CONTRACT',
GLP_CONTROL_PLANE: 'CONTROL_PLANE_STATE_MACHINE',
GLP_WITNESS: 'APPEND_ONLY_EVIDENCE_LEDGER',
HLDP: 'LANGUAGE_PROGRAM_PROFILE',
GLS_IMPLEMENTATION: 'ADAPTER_MODEL_OR_MODULE_CONTRACT',
AGE: 'EXECUTION_BODY_LIFECYCLE_STATE_MACHINE',
AGE_OS: 'RESOURCE_SCHEDULER',
AGE_AUTONOMOUS_RUNTIME: 'TIME_OR_CAPABILITY_EXTENSION_RUNTIME',
GUANGHU_WORLD: 'WORLD_BOOT_AND_RECOVERY_STATE_MACHINE',
GUANGHU_NATIVE_ENGINEERING: 'NATIVE_QUALITY_GATE',
GUANGHU_NATIVE_OS: 'NATIVE_NODE_RUNTIME_CONTRACT',
GUANGHU_NATIVE_STORAGE: 'NATIVE_STORAGE_CONTRACT',
GUANGHU_PERSONA_GESTATION: 'GESTATIONAL_INGRESS_OR_REVIEW_PIPELINE',
GUANGHU_LANGUAGE_WORLD: 'LANGUAGE_WORLD_BOUNDARY_GUARD',
}
return kinds[family] || 'UNCLASSIFIED_PROTOCOL_SOURCE'
}
function parseTableAuthorities(raw, authorityKind) {
const results = []
for (const line of raw.split(/\r?\n/)) {
const id = line.match(/^\|\s*(GLS-\d{4})\s*\|/)?.[1]
if (!id) continue
results.push({
id,
authority_kind: authorityKind,
declared_source_path: line.match(/`(gls\/[^`]+\.hdlp)`/)?.[1] || null,
})
}
return results
}
function parseSourceManifest(raw) {
const lines = raw.split(/\r?\n/)
const results = []
for (let index = 0; index < lines.length; index += 1) {
const match = lines[index].match(/^\s*-\s+id:\s*["']?(GLS-\d{4})["']?\s*$/)
if (!match) continue
let sourcePath = null
for (let cursor = index + 1; cursor < lines.length && cursor <= index + 12; cursor += 1) {
if (/^\s*-\s+id:/.test(lines[cursor])) break
sourcePath ||= lines[cursor].match(/^\s*source_path:\s*["']?(gls\/[^"']+\.hdlp)["']?\s*$/)?.[1] || null
}
results.push({ id: match[1], authority_kind: 'SOURCE_MANIFEST', declared_source_path: sourcePath })
}
return results
}
function stronglyConnectedComponents(graph) {
let nextIndex = 0
const indices = new Map()
const lowLinks = new Map()
const stack = []
const onStack = new Set()
const components = []
function visit(id) {
indices.set(id, nextIndex)
lowLinks.set(id, nextIndex)
nextIndex += 1
stack.push(id)
onStack.add(id)
for (const target of graph.get(id) || []) {
if (!graph.has(target)) continue
if (!indices.has(target)) {
visit(target)
lowLinks.set(id, Math.min(lowLinks.get(id), lowLinks.get(target)))
} else if (onStack.has(target)) lowLinks.set(id, Math.min(lowLinks.get(id), indices.get(target)))
}
if (lowLinks.get(id) !== indices.get(id)) return
const component = []
let current
do {
current = stack.pop()
onStack.delete(current)
component.push(current)
} while (current !== id)
if (component.length > 1 || (graph.get(id) || []).includes(id)) components.push(component.sort())
}
for (const id of [...graph.keys()].sort()) if (!indices.has(id)) visit(id)
return components.sort((left, right) => left[0].localeCompare(right[0]))
}
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 protocolRegistryPath = 'gls/GLS-PROTOCOL-REGISTRY.json'
const glsEntryPath = 'gls/GLS-ENTRY.hdlp'
const sourceManifestPath = 'gls/SOURCE-MANIFEST.yml'
const architectureCatalogPath = 'gls/GLS-ARCHITECTURE-CATALOG.hdlp'
const protocolRegistryRaw = await readRequired(protocolRegistryPath)
const glsEntryRaw = await readRequired(glsEntryPath)
const sourceManifestRaw = await readRequired(sourceManifestPath)
const architectureCatalogRaw = await readRequired(architectureCatalogPath)
const protocolRegistry = JSON.parse(protocolRegistryRaw)
const authorityFiles = [
[protocolRegistryPath, protocolRegistryRaw, 'PROTOCOL_REGISTRY'],
[glsEntryPath, glsEntryRaw, 'GLS_ENTRY'],
[sourceManifestPath, sourceManifestRaw, 'SOURCE_MANIFEST'],
[architectureCatalogPath, architectureCatalogRaw, 'ARCHITECTURE_CATALOG'],
].map(([source_path, raw, authority_kind]) => ({
authority_kind,
source_path,
source_sha256: createHash('sha256').update(raw).digest('hex'),
}))
const registryMetadata = new Map()
const authorityDeclarations = new Map()
function addAuthority(declaration) {
const declarations = authorityDeclarations.get(declaration.id) || []
if (!declarations.some((existing) => existing.authority_kind === declaration.authority_kind)) declarations.push(declaration)
authorityDeclarations.set(declaration.id, declarations)
}
for (const entry of protocolRegistry.existing_registered || []) {
registryMetadata.set(entry.id, { ...entry, registry_section: 'existing_registered' })
addAuthority({ id: entry.id, authority_kind: 'PROTOCOL_REGISTRY', declared_source_path: entry.source || null })
}
for (const entry of protocolRegistry.registered_draft_protocols || []) {
registryMetadata.set(entry.id, { ...entry, registry_section: 'registered_draft_protocols' })
addAuthority({ id: entry.id, authority_kind: 'PROTOCOL_REGISTRY', declared_source_path: entry.source || null })
}
for (const declaration of parseTableAuthorities(glsEntryRaw, 'GLS_ENTRY')) addAuthority(declaration)
for (const declaration of parseSourceManifest(sourceManifestRaw)) addAuthority(declaration)
for (const declaration of parseTableAuthorities(architectureCatalogRaw, 'ARCHITECTURE_CATALOG')) addAuthority(declaration)
const routingReferences = new Map()
const routingRoot = path.join(repoRoot, 'routing')
for (const relative of await walk(routingRoot)) {
if (!/\.(?:json|hdlp|md|yml|yaml)$/.test(relative)) continue
const raw = await readFile(path.join(routingRoot, relative), 'utf8')
for (const id of new Set(raw.match(/GLS-\d{4}/g) || [])) {
const refs = routingReferences.get(id) || []
refs.push(`routing/${relative}`)
routingReferences.set(id, refs)
}
}
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 selectedSource = new Map()
for (const [id, sources] of grouped) {
sources.sort((left, right) => sourceRank(left) - sourceRank(right) || left.localeCompare(right))
const relative = sources[0]
selectedSource.set(id, {
relative,
raw: await readFile(path.join(sourceRoot, relative), 'utf8'),
alternate_source_count: sources.length - 1,
})
}
const knownSourceIds = new Set(selectedSource.keys())
const protocolRegistryIds = new Set(registryMetadata.keys())
const draftIds = new Set((protocolRegistry.registered_draft_protocols || []).map((entry) => entry.id))
const legacyDependencyTargets = new Set()
const draftGraph = new Map()
for (const id of draftIds) {
const dependencies = sourceDependencies(selectedSource.get(id)?.raw || '')
draftGraph.set(id, dependencies.filter((target) => draftIds.has(target)))
for (const target of dependencies) legacyDependencyTargets.add(target)
}
const legacyDependencyCycles = stronglyConnectedComponents(draftGraph)
const protocols = []
for (const [id, source] of [...selectedSource.entries()].sort(([left], [right]) => left.localeCompare(right))) {
const heading = source.raw.split(/\r?\n/).find((line) => line.startsWith('#') && line.includes(id))
const projection = projectionManifest.projections[id]
const metadata = registryMetadata.get(id) || null
const authorities = (authorityDeclarations.get(id) || []).sort((left, right) => left.authority_kind.localeCompare(right.authority_kind))
const declaredPaths = [...new Set(authorities.map((entry) => entry.declared_source_path).filter(Boolean))]
const authorityConflict = declaredPaths.length > 1
const registrationState = authorityConflict
? 'REGISTRATION_CONFLICT'
: authorities.some((entry) => entry.authority_kind === 'PROTOCOL_REGISTRY')
? 'REGISTERED_PROTOCOL_REGISTRY'
: authorities.length > 0
? 'REGISTERED_OTHER_CANONICAL_INDEX'
: 'DISCOVERED_UNRECONCILED'
const legacyDependencies = sourceDependencies(source.raw)
const dependencyEdges = [
...legacyDependencies.map((target) => ({
target,
edge_kind: 'LEGACY_UNTYPED_REFERENCE',
declared_by: `gls/${source.relative}`,
enters_runtime_graph: false,
target_registered: protocolRegistryIds.has(target),
target_numbered_source_available: knownSourceIds.has(target),
})),
...(projection?.dependencies || []).map((target) => ({
target,
edge_kind: 'RUNTIME_REQUIRES',
declared_by: 'contracts/gls-executable-projections.json',
enters_runtime_graph: true,
target_registered: protocolRegistryIds.has(target),
target_numbered_source_available: knownSourceIds.has(target),
})),
]
const activationBlockers = []
if (!projection) activationBlockers.push('NO_EXECUTABLE_ADAPTER')
if (registrationState === 'DISCOVERED_UNRECONCILED') activationBlockers.push('REGISTRATION_NOT_RECONCILED')
if (authorityConflict) activationBlockers.push('REGISTRATION_SOURCE_CONFLICT')
if (legacyDependencies.length > 0 && !projection) activationBlockers.push('LEGACY_DEPENDENCIES_REQUIRE_TYPED_REVIEW')
if (legacyDependencies.some((target) => !knownSourceIds.has(target)) && !projection) activationBlockers.push('DEPENDENCY_NUMBERED_SOURCE_MISSING')
protocols.push({
id,
title: cleanHeading(heading || id, id),
source_status: sourceStatus(source.raw),
source_path: `gls/${source.relative}`,
source_sha256: createHash('sha256').update(source.raw).digest('hex'),
alternate_source_count: source.alternate_source_count,
registration: {
state: registrationState,
authorities,
declared_source_paths: declaredPaths,
routing_reference_count: (routingReferences.get(id) || []).length,
},
maturity: {
registry_section: metadata?.registry_section || null,
registry_status: metadata?.status || null,
family: metadata?.family || null,
implementation_evidence: metadata?.implementation || null,
},
contract_kind: contractKind(metadata?.family, projection?.projection_kind),
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 || [],
dependency_edges: dependencyEdges,
activation_blockers: activationBlockers,
})
}
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 dependenciesNotInRegistry = [...legacyDependencyTargets].filter((id) => !protocolRegistryIds.has(id)).sort()
const dependenciesWithoutNumberedSource = [...legacyDependencyTargets].filter((id) => !knownSourceIds.has(id)).sort()
const numberedSourcesNotInProtocolRegistry = [...knownSourceIds].filter((id) => !protocolRegistryIds.has(id)).sort()
const registry = {
schema: 'hololake.gls-runtime-manifest/v2',
record_id: 'HLP-GLS-RUNTIME-MANIFEST-002',
source: {
repository: 'REPO-012',
commit: sourceCommit,
root: 'gls',
authority_files: authorityFiles,
},
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',
legacy_untyped_dependency_behavior: 'AUDIT_ONLY_BLOCKS_NEW_ACTIVATION',
runtime_graph_source: 'EXPLICIT_EXECUTABLE_PROJECTIONS_ONLY',
runtime_dependency_cycles: 'REJECT',
unknown_protocol: 'FAIL_CLOSED',
},
reconciliation: {
numbered_protocol_count: protocols.length,
protocol_registry_id_count: protocolRegistryIds.size,
existing_registered_count: (protocolRegistry.existing_registered || []).length,
registered_draft_count: (protocolRegistry.registered_draft_protocols || []).length,
registered_draft_not_started_count: (protocolRegistry.registered_draft_protocols || []).filter((entry) => entry.implementation === 'NOT_STARTED').length,
legacy_dependency_target_count: legacyDependencyTargets.size,
dependencies_not_in_protocol_registry: dependenciesNotInRegistry,
dependencies_without_numbered_source: dependenciesWithoutNumberedSource,
numbered_sources_not_in_protocol_registry: numberedSourcesNotInProtocolRegistry,
legacy_dependency_cycles: legacyDependencyCycles,
discovered_unreconciled_count: protocols.filter((protocol) => protocol.registration.state === 'DISCOVERED_UNRECONCILED').length,
authority_conflict_count: protocols.filter((protocol) => protocol.registration.state === 'REGISTRATION_CONFLICT').length,
},
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_MANIFEST_COMPILED protocols=${protocols.length} registered=${protocolRegistryIds.size} executable=${executableCount} legacy_cycles=${legacyDependencyCycles.length} output=${output}`)