aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
239 lines (207 loc) • 7.06 kB
JavaScript
/**
* Framework Guard — Pre-commit hook that blocks commits to L1/L2 protected paths.
*
* Reads ALL configuration from core-config.yaml (single source of truth):
* - boundary.frameworkProtection: toggle (true/false)
* - boundary.protected: L1/L2 blocked glob patterns
* - boundary.exceptions: L3 allowed glob patterns
*
* When frameworkProtection is true (default), blocks staged changes to protected paths.
* When false, acts as a no-op (contributor mode).
*
* Story: BM-3 (Epic: Boundary Mapping & Framework-Project Separation)
*/
;
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
// Hardcoded fallbacks — used ONLY if core-config.yaml is missing or malformed.
// The canonical source is core-config.yaml boundary.protected/exceptions.
const FALLBACK_PROTECTED = [
'.aios-core/core/**',
'.aios-core/development/tasks/**',
'.aios-core/development/templates/**',
'.aios-core/development/checklists/**',
'.aios-core/development/workflows/**',
'.aios-core/infrastructure/**',
'.aios-core/constitution.md',
'bin/aios.js',
'bin/aios-init.js',
];
const FALLBACK_EXCEPTIONS = [
'.aios-core/data/**',
'.aios-core/development/agents/*/MEMORY.md',
];
/**
* Convert a glob pattern to a RegExp.
* Supports: ** (any depth), * (single segment), literal dots.
* @param {string} glob
* @returns {RegExp}
*/
function globToRegex(glob) {
let pattern = glob
.replace(/\./g, '\\.') // escape dots
.replace(/\*\*/g, '\u0000') // placeholder for **
.replace(/\*/g, '[^/]+') // * = single segment
.replace(/\u0000/g, '.+'); // ** = any depth
// If pattern ends with .+ (was **), match prefix
if (glob.endsWith('**')) {
return new RegExp('^' + pattern);
}
// Exact file match
return new RegExp('^' + pattern + '$');
}
/**
* Read the raw content of core-config.yaml.
* @returns {string|null}
*/
function readConfigContent() {
const configPath = path.resolve(__dirname, '../../.aios-core/core-config.yaml');
if (!fs.existsSync(configPath)) {
return null;
}
return fs.readFileSync(configPath, 'utf8');
}
/**
* Parse a YAML list under a given key using simple line-based parsing.
* Avoids js-yaml dependency for speed.
* @param {string} content - YAML file content
* @param {string} parentKey - Parent key (e.g., 'boundary')
* @param {string} listKey - List key (e.g., 'protected')
* @returns {string[]}
*/
function parseYamlList(content, parentKey, listKey) {
const lines = content.split('\n');
const items = [];
let inParent = false;
let inList = false;
let parentIndent = -1;
let listIndent = -1;
for (const line of lines) {
const trimmed = line.trimStart();
const indent = line.length - trimmed.length;
// Find parent key (e.g., "boundary:")
if (trimmed === parentKey + ':' || trimmed.startsWith(parentKey + ':')) {
inParent = true;
parentIndent = indent;
continue;
}
// If we're past the parent section (back to same or lower indent)
if (inParent && indent <= parentIndent && trimmed.length > 0 && !trimmed.startsWith('#')) {
inParent = false;
inList = false;
}
if (!inParent) continue;
// Find list key within parent (e.g., "protected:")
if (trimmed === listKey + ':' || trimmed.startsWith(listKey + ':')) {
inList = true;
listIndent = indent;
continue;
}
// If we're past the list (back to same or lower indent within parent)
if (inList && indent <= listIndent && trimmed.length > 0 && !trimmed.startsWith('#') && !trimmed.startsWith('-')) {
inList = false;
}
// Collect list items
if (inList && trimmed.startsWith('- ')) {
items.push(trimmed.slice(2).trim());
}
}
return items;
}
/**
* Read boundary config from core-config.yaml.
* @returns {{ enabled: boolean, protected: string[], exceptions: string[] }}
*/
function readBoundaryConfig() {
const content = readConfigContent();
if (!content) {
return { enabled: true, protected: FALLBACK_PROTECTED, exceptions: FALLBACK_EXCEPTIONS };
}
// Read toggle
const toggleMatch = content.match(/frameworkProtection:\s*(true|false)/);
const enabled = toggleMatch ? toggleMatch[1] === 'true' : true;
// Read lists
const protectedPaths = parseYamlList(content, 'boundary', 'protected');
const exceptionPaths = parseYamlList(content, 'boundary', 'exceptions');
return {
enabled,
protected: protectedPaths.length > 0 ? protectedPaths : FALLBACK_PROTECTED,
exceptions: exceptionPaths.length > 0 ? exceptionPaths : FALLBACK_EXCEPTIONS,
};
}
/**
* Check if a file path matches any pattern in the list.
* @param {string} filePath
* @param {RegExp[]} patterns
* @returns {boolean}
*/
function matchesAny(filePath, patterns) {
return patterns.some((pattern) => pattern.test(filePath));
}
/**
* Get list of staged files from git.
* @returns {string[]}
*/
function getStagedFiles() {
try {
const output = execSync('git diff --cached --name-only', { encoding: 'utf8' });
return output
.split('\n')
.map((f) => f.trim())
.filter(Boolean);
} catch {
return [];
}
}
function main() {
// Step 1: Read config (single source of truth)
const config = readBoundaryConfig();
if (!config.enabled) {
process.exit(0);
}
// Step 2: Compile glob patterns to regex
const blockedPatterns = config.protected.map(globToRegex);
const allowedPatterns = config.exceptions.map(globToRegex);
// Step 3: Get staged files
const stagedFiles = getStagedFiles();
if (stagedFiles.length === 0) {
process.exit(0);
}
// Step 4: Check each staged file
const blockedFiles = [];
for (const file of stagedFiles) {
// Normalize to forward slashes (Windows compat)
const normalized = file.replace(/\\/g, '/');
if (matchesAny(normalized, blockedPatterns) && !matchesAny(normalized, allowedPatterns)) {
blockedFiles.push(normalized);
}
}
// Step 5: Report
if (blockedFiles.length > 0) {
console.error('');
console.error('Framework Guard: Commit blocked!');
console.error('');
console.error('The following framework files are protected (L1/L2):');
for (const file of blockedFiles) {
console.error(` - ${file}`);
}
console.error('');
console.error('These files are read-only in project mode (boundary.frameworkProtection: true).');
console.error('');
console.error('To bypass (framework contributors only):');
console.error(' git commit --no-verify');
console.error('');
console.error('To disable permanently (contributors):');
console.error(' Set boundary.frameworkProtection: false in core-config.yaml');
console.error('');
process.exit(1);
}
process.exit(0);
}
// Export for testing
module.exports = { readBoundaryConfig, globToRegex, matchesAny, getStagedFiles, FALLBACK_PROTECTED, FALLBACK_EXCEPTIONS };
// Run when executed directly
if (require.main === module) {
main();
}