feat: add deployment profile generation slice

This commit is contained in:
Eve
2026-05-08 12:23:02 +08:00
parent 36bc00cc3f
commit de2d5d97b8
6 changed files with 409 additions and 2 deletions

View File

@@ -1 +1,10 @@
export { loadDeploymentProfileArtifact, createDeploymentBindingContract } from './profile-artifact.mjs';
export {
loadDeploymentProfileArtifact,
createDeploymentBindingContract,
} from './profile-artifact.mjs';
export {
parseDeploymentProfileYaml,
validateDeploymentProfileSchema,
generateDeploymentProfileArtifact,
generateDeploymentProfileArtifactFromFile,
} from './profile-generator.mjs';

View File

@@ -0,0 +1,227 @@
import fs from 'node:fs';
import path from 'node:path';
const packageRoot = path.resolve(import.meta.dirname, '..', '..');
const repoRoot = path.resolve(packageRoot, '..', '..');
const schemaPath = '../../../schemas/reporting-governance/deployment-profile.schema.json';
function readText(filePath) {
return fs.readFileSync(filePath, 'utf8');
}
function countIndent(line) {
let count = 0;
while (count < line.length && line[count] === ' ') count += 1;
return count;
}
function parseScalar(raw) {
const value = raw.trim();
if (value === 'true') return true;
if (value === 'false') return false;
if (/^-?\d+$/.test(value)) return Number.parseInt(value, 10);
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
return value.slice(1, -1);
}
return value;
}
function parseBlock(lines, startIndex = 0, baseIndent = 0) {
const firstMeaningful = lines.slice(startIndex).find((line) => line.trim() !== '');
const container = firstMeaningful?.trim().startsWith('- ') ? [] : {};
let index = startIndex;
while (index < lines.length) {
const line = lines[index];
const trimmed = line.trim();
if (trimmed === '' || trimmed.startsWith('#')) {
index += 1;
continue;
}
const indent = countIndent(line);
if (indent < baseIndent) break;
if (indent > baseIndent) {
throw new Error(`unexpected indentation at line ${index + 1}`);
}
if (Array.isArray(container)) {
if (!trimmed.startsWith('- ')) {
throw new Error(`expected array item at line ${index + 1}`);
}
const itemBody = trimmed.slice(2);
if (itemBody === '') {
const nested = parseBlock(lines, index + 1, baseIndent + 2);
container.push(nested.value);
index = nested.nextIndex;
continue;
}
if (itemBody.includes(':')) {
const separator = itemBody.indexOf(':');
const key = itemBody.slice(0, separator).trim();
const remainder = itemBody.slice(separator + 1).trim();
const objectItem = {};
if (remainder === '') {
const nested = parseBlock(lines, index + 1, baseIndent + 4);
objectItem[key] = nested.value;
container.push(objectItem);
index = nested.nextIndex;
continue;
}
objectItem[key] = parseScalar(remainder);
container.push(objectItem);
index += 1;
continue;
}
container.push(parseScalar(itemBody));
index += 1;
continue;
}
const separator = trimmed.indexOf(':');
if (separator === -1) {
throw new Error(`expected key/value pair at line ${index + 1}`);
}
const key = trimmed.slice(0, separator).trim();
let remainder = trimmed.slice(separator + 1).trim();
if (remainder === '>-') {
const blockLines = [];
index += 1;
while (index < lines.length) {
const nestedLine = lines[index];
const nestedTrimmed = nestedLine.trim();
if (nestedTrimmed === '') {
blockLines.push('');
index += 1;
continue;
}
const nestedIndent = countIndent(nestedLine);
if (nestedIndent <= baseIndent) break;
blockLines.push(nestedLine.slice(baseIndent + 2).trim());
index += 1;
}
container[key] = blockLines.join(' ').replace(/\s+/g, ' ').trim();
continue;
}
if (remainder === '') {
const nested = parseBlock(lines, index + 1, baseIndent + 2);
container[key] = nested.value;
index = nested.nextIndex;
continue;
}
container[key] = parseScalar(remainder);
index += 1;
}
return { value: container, nextIndex: index };
}
export function parseDeploymentProfileYaml(yamlText) {
const lines = yamlText.replace(/\r\n/g, '\n').split('\n');
return parseBlock(lines, 0, 0).value;
}
export function validateDeploymentProfileSchema(profile) {
if (!profile || typeof profile !== 'object' || Array.isArray(profile)) {
throw new Error('deployment profile must be an object');
}
if (profile.apiVersion !== 'reporting-governance/v1alpha1') {
throw new Error('deployment profile apiVersion must be reporting-governance/v1alpha1');
}
if (profile.kind !== 'DeploymentProfile') {
throw new Error('deployment profile kind must be DeploymentProfile');
}
if (typeof profile?.metadata?.id !== 'string' || profile.metadata.id.trim() === '') {
throw new Error('deployment profile metadata.id must be a non-empty string');
}
if (typeof profile?.metadata?.version !== 'string' || profile.metadata.version.trim() === '') {
throw new Error('deployment profile metadata.version must be a non-empty string');
}
if (typeof profile?.metadata?.runtime !== 'string' || profile.metadata.runtime.trim() === '') {
throw new Error('deployment profile metadata.runtime must be a non-empty string');
}
if (typeof profile?.spec?.package?.pluginVersion !== 'string' || profile.spec.package.pluginVersion.trim() === '') {
throw new Error('deployment profile spec.package.pluginVersion must be a non-empty string');
}
if (!profile?.spec?.adapters || typeof profile.spec.adapters !== 'object' || Array.isArray(profile.spec.adapters)) {
throw new Error('deployment profile spec.adapters must be an object');
}
if (!profile?.spec?.notifications || typeof profile.spec.notifications !== 'object' || Array.isArray(profile.spec.notifications)) {
throw new Error('deployment profile spec.notifications must be an object');
}
if (!profile?.spec?.audit || typeof profile.spec.audit !== 'object' || Array.isArray(profile.spec.audit)) {
throw new Error('deployment profile spec.audit must be an object');
}
if (!Array.isArray(profile?.spec?.audit?.requiredArtifacts) || profile.spec.audit.requiredArtifacts.length === 0) {
throw new Error('deployment profile spec.audit.requiredArtifacts must be a non-empty array');
}
if (!profile?.spec?.capability_expectations || typeof profile.spec.capability_expectations !== 'object' || Array.isArray(profile.spec.capability_expectations)) {
throw new Error('deployment profile spec.capability_expectations must be an object');
}
if (!Array.isArray(profile.spec.capability_expectations.required)) {
throw new Error('deployment profile spec.capability_expectations.required must be an array');
}
return profile;
}
export function generateDeploymentProfileArtifact(profile, { sourceProfile } = {}) {
const validated = validateDeploymentProfileSchema(profile);
const id = validated.metadata.id;
const sourceProfilePath = sourceProfile ?? path.join('profiles', `${id}.yaml`);
return {
$schema: schemaPath,
apiVersion: validated.apiVersion,
kind: 'DeploymentProfileArtifact',
metadata: {
id,
version: validated.metadata.version,
runtime: validated.metadata.runtime,
source_profile: sourceProfilePath,
compatibility_mode: 'strict_envelope',
},
spec: {
package: {
pluginVersion: validated.spec.package.pluginVersion,
},
bindings: {
runtime: validated.metadata.runtime,
entrypoint: 'scripts/watchdog_auto_notify_orchestrator.mjs',
scripts: {
watchdog: 'scripts/long_task_watchdog.mjs',
dispatcher: 'scripts/operator_notify_dispatcher.mjs',
bridgeSupervisor: 'scripts/operator_notify_bridge_supervisor.mjs',
senderBinding: 'scripts/operator_notify_sender_binding.mjs',
orchestrator: 'scripts/watchdog_auto_notify_orchestrator.mjs',
},
artifact_roots: {
watchdogEvidence: 'state/long-task-watchdog',
canonicalEvents: 'state/long-task-watchdog-events',
queueItems: 'state/operator-notify-queue',
spoolArtifacts: 'state/operator-notify-dispatch-spool',
bridgeReceipts: 'state/operator-notify-bridge-receipts',
senderAttempts: 'state/operator-notify-sender-attempts',
},
},
},
};
}
export function generateDeploymentProfileArtifactFromFile(profilePath) {
const absoluteProfilePath = path.resolve(profilePath);
const profile = parseDeploymentProfileYaml(readText(absoluteProfilePath));
return generateDeploymentProfileArtifact(profile, {
sourceProfile: path.relative(repoRoot, absoluteProfilePath),
});
}
export default {
parseDeploymentProfileYaml,
validateDeploymentProfileSchema,
generateDeploymentProfileArtifact,
generateDeploymentProfileArtifactFromFile,
};