moving
This commit is contained in:
566
scripts/oss-drift-check.mjs
Normal file
566
scripts/oss-drift-check.mjs
Normal file
@@ -0,0 +1,566 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import {
|
||||
lstatSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
realpathSync,
|
||||
} from 'node:fs';
|
||||
import { dirname, isAbsolute, join, posix, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
||||
const BASELINE_PATH = join(SCRIPT_DIR, 'oss-drift-baseline.json');
|
||||
const MAPPING = Object.freeze({
|
||||
canonical: 'packages/hive-mind-core/src',
|
||||
oss: 'packages/core/src',
|
||||
ignoredDirectories: ['dist', 'node_modules'],
|
||||
ignoredFileSuffixes: ['.test.ts', '.tsbuildinfo'],
|
||||
});
|
||||
const FORBIDDEN_EXPORTS = Object.freeze({
|
||||
paths: [
|
||||
'mind/evolution-runs.ts',
|
||||
'mind/execution-traces.ts',
|
||||
'mind/improvement-signals.ts',
|
||||
],
|
||||
pathPrefixes: ['vault.ts', 'compliance/'],
|
||||
markers: [
|
||||
{ path: 'mind/db.ts', token: 'install_audit' },
|
||||
{ path: 'mind/schema.ts', token: 'install_audit' },
|
||||
],
|
||||
});
|
||||
const VALID_ADAPTATION_KINDS = new Set(['branding', 'import', 'layout', 'logger']);
|
||||
const VALID_STATES = new Set(['different', 'only-canonical', 'only-oss']);
|
||||
const VALID_DISPOSITIONS = new Set([
|
||||
'forward-port',
|
||||
'product-curation',
|
||||
'reconcile',
|
||||
'reverse-port',
|
||||
]);
|
||||
const HASH_PATTERN = /^[a-f0-9]{64}$/;
|
||||
const decoder = new TextDecoder('utf-8', { fatal: true });
|
||||
|
||||
class ConfigurationError extends Error {}
|
||||
|
||||
function compareOrdinal(left, right) {
|
||||
return left < right ? -1 : left > right ? 1 : 0;
|
||||
}
|
||||
|
||||
function assertRecord(value, label) {
|
||||
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw new ConfigurationError(`${label} must be an object`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertExactKeys(value, expected, label) {
|
||||
assertRecord(value, label);
|
||||
const actual = Object.keys(value).sort(compareOrdinal);
|
||||
const wanted = [...expected].sort(compareOrdinal);
|
||||
if (JSON.stringify(actual) !== JSON.stringify(wanted)) {
|
||||
throw new ConfigurationError(
|
||||
`${label} has unexpected keys: expected ${wanted.join(', ')}, got ${actual.join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assertExactArray(actual, expected, label) {
|
||||
if (!Array.isArray(actual) || JSON.stringify(actual) !== JSON.stringify(expected)) {
|
||||
throw new ConfigurationError(`${label} does not match the hardcoded publication policy`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertCanonicalPath(value, label) {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
value.length === 0 ||
|
||||
value.includes('\\') ||
|
||||
isAbsolute(value) ||
|
||||
value.startsWith('/') ||
|
||||
value.split('/').some((segment) => segment === '' || segment === '.' || segment === '..') ||
|
||||
posix.normalize(value) !== value
|
||||
) {
|
||||
throw new ConfigurationError(`${label} must be a canonical repository-relative path`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertSortedUnique(values, label, pathSelector = (value) => value) {
|
||||
if (!Array.isArray(values)) {
|
||||
throw new ConfigurationError(`${label} must be an array`);
|
||||
}
|
||||
const paths = values.map(pathSelector);
|
||||
const expected = [...new Set(paths)].sort(compareOrdinal);
|
||||
if (JSON.stringify(paths) !== JSON.stringify(expected)) {
|
||||
throw new ConfigurationError(`${label} must be sorted by path and contain no duplicates`);
|
||||
}
|
||||
}
|
||||
|
||||
function loadBaseline() {
|
||||
let baseline;
|
||||
try {
|
||||
baseline = JSON.parse(readFileSync(BASELINE_PATH, 'utf-8'));
|
||||
} catch (error) {
|
||||
throw new ConfigurationError(`cannot read baseline ${BASELINE_PATH}: ${error.message}`);
|
||||
}
|
||||
|
||||
assertExactKeys(
|
||||
baseline,
|
||||
[
|
||||
'schemaVersion',
|
||||
'mapping',
|
||||
'parityPaths',
|
||||
'intentionalAdaptations',
|
||||
'knownReviewedBlockers',
|
||||
'unreviewedDifferences',
|
||||
'forbiddenExports',
|
||||
],
|
||||
'baseline',
|
||||
);
|
||||
if (baseline.schemaVersion !== 1) {
|
||||
throw new ConfigurationError('baseline.schemaVersion must be 1');
|
||||
}
|
||||
|
||||
assertExactKeys(
|
||||
baseline.mapping,
|
||||
['canonical', 'oss', 'ignoredDirectories', 'ignoredFileSuffixes'],
|
||||
'baseline.mapping',
|
||||
);
|
||||
for (const key of Object.keys(MAPPING)) {
|
||||
const expected = MAPPING[key];
|
||||
if (Array.isArray(expected)) {
|
||||
assertExactArray(baseline.mapping[key], expected, `baseline.mapping.${key}`);
|
||||
} else if (baseline.mapping[key] !== expected) {
|
||||
throw new ConfigurationError(`baseline.mapping.${key} does not match the hardcoded mapping`);
|
||||
}
|
||||
}
|
||||
|
||||
assertExactKeys(
|
||||
baseline.forbiddenExports,
|
||||
['paths', 'pathPrefixes', 'markers'],
|
||||
'baseline.forbiddenExports',
|
||||
);
|
||||
assertExactArray(
|
||||
baseline.forbiddenExports.paths,
|
||||
FORBIDDEN_EXPORTS.paths,
|
||||
'baseline.forbiddenExports.paths',
|
||||
);
|
||||
assertExactArray(
|
||||
baseline.forbiddenExports.pathPrefixes,
|
||||
FORBIDDEN_EXPORTS.pathPrefixes,
|
||||
'baseline.forbiddenExports.pathPrefixes',
|
||||
);
|
||||
assertExactArray(
|
||||
baseline.forbiddenExports.markers,
|
||||
FORBIDDEN_EXPORTS.markers,
|
||||
'baseline.forbiddenExports.markers',
|
||||
);
|
||||
|
||||
assertSortedUnique(baseline.parityPaths, 'baseline.parityPaths', (entry) => entry?.path);
|
||||
for (const [index, entry] of baseline.parityPaths.entries()) {
|
||||
const label = `baseline.parityPaths[${index}]`;
|
||||
assertExactKeys(entry, ['path', 'sha256'], label);
|
||||
assertCanonicalPath(entry.path, `${label}.path`);
|
||||
if (!HASH_PATTERN.test(entry.sha256)) {
|
||||
throw new ConfigurationError(`${label}.sha256 must be a lowercase SHA-256 hash`);
|
||||
}
|
||||
}
|
||||
|
||||
assertSortedUnique(
|
||||
baseline.intentionalAdaptations,
|
||||
'baseline.intentionalAdaptations',
|
||||
(entry) => entry?.path,
|
||||
);
|
||||
for (const [index, entry] of baseline.intentionalAdaptations.entries()) {
|
||||
const label = `baseline.intentionalAdaptations[${index}]`;
|
||||
assertExactKeys(entry, ['path', 'kinds', 'canonicalSha256', 'ossSha256'], label);
|
||||
assertCanonicalPath(entry.path, `${label}.path`);
|
||||
assertSortedUnique(entry.kinds, `${label}.kinds`);
|
||||
if (entry.kinds.length === 0 || entry.kinds.some((kind) => !VALID_ADAPTATION_KINDS.has(kind))) {
|
||||
throw new ConfigurationError(`${label}.kinds contains an unsupported adaptation kind`);
|
||||
}
|
||||
if (!HASH_PATTERN.test(entry.canonicalSha256) || !HASH_PATTERN.test(entry.ossSha256)) {
|
||||
throw new ConfigurationError(`${label} must contain lowercase SHA-256 hashes`);
|
||||
}
|
||||
if (entry.canonicalSha256 === entry.ossSha256) {
|
||||
throw new ConfigurationError(`${label} must describe an actual adaptation, not parity`);
|
||||
}
|
||||
}
|
||||
|
||||
assertSortedUnique(
|
||||
baseline.knownReviewedBlockers,
|
||||
'baseline.knownReviewedBlockers',
|
||||
(entry) => entry?.path,
|
||||
);
|
||||
for (const [index, entry] of baseline.knownReviewedBlockers.entries()) {
|
||||
const label = `baseline.knownReviewedBlockers[${index}]`;
|
||||
assertExactKeys(entry, ['path', 'state', 'disposition'], label);
|
||||
assertCanonicalPath(entry.path, `${label}.path`);
|
||||
if (!VALID_STATES.has(entry.state)) {
|
||||
throw new ConfigurationError(`${label}.state is unsupported`);
|
||||
}
|
||||
if (!VALID_DISPOSITIONS.has(entry.disposition)) {
|
||||
throw new ConfigurationError(`${label}.disposition is unsupported`);
|
||||
}
|
||||
}
|
||||
|
||||
assertSortedUnique(
|
||||
baseline.unreviewedDifferences,
|
||||
'baseline.unreviewedDifferences',
|
||||
(entry) => entry?.path,
|
||||
);
|
||||
for (const [index, entry] of baseline.unreviewedDifferences.entries()) {
|
||||
const label = `baseline.unreviewedDifferences[${index}]`;
|
||||
assertExactKeys(entry, ['path', 'state'], label);
|
||||
assertCanonicalPath(entry.path, `${label}.path`);
|
||||
if (!VALID_STATES.has(entry.state)) {
|
||||
throw new ConfigurationError(`${label}.state is unsupported`);
|
||||
}
|
||||
}
|
||||
|
||||
const classified = [
|
||||
...baseline.parityPaths.map((entry) => entry.path),
|
||||
...baseline.intentionalAdaptations.map((entry) => entry.path),
|
||||
...baseline.knownReviewedBlockers.map((entry) => entry.path),
|
||||
...baseline.unreviewedDifferences.map((entry) => entry.path),
|
||||
];
|
||||
const duplicate = classified.find((path, index) => classified.indexOf(path) !== index);
|
||||
if (duplicate) {
|
||||
throw new ConfigurationError(`baseline classifies ${duplicate} more than once`);
|
||||
}
|
||||
const privateCategory = classified.find((path) => isPrivateExclusion(path));
|
||||
if (privateCategory) {
|
||||
throw new ConfigurationError(`baseline must not reclassify private exclusion ${privateCategory}`);
|
||||
}
|
||||
|
||||
return baseline;
|
||||
}
|
||||
|
||||
function resolveGitRoot(candidate, label) {
|
||||
let resolved;
|
||||
try {
|
||||
resolved = realpathSync(candidate);
|
||||
const root = execFileSync('git', ['-C', resolved, 'rev-parse', '--show-toplevel'], {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
}).trim();
|
||||
return realpathSync(root);
|
||||
} catch (error) {
|
||||
throw new ConfigurationError(`${label} is not a readable Git worktree: ${candidate}`);
|
||||
}
|
||||
}
|
||||
|
||||
function readUtf8Normalized(filePath) {
|
||||
let text;
|
||||
try {
|
||||
text = decoder.decode(readFileSync(filePath));
|
||||
} catch (error) {
|
||||
throw new ConfigurationError(`cannot read UTF-8 source file ${filePath}: ${error.message}`);
|
||||
}
|
||||
return text.replace(/^\uFEFF/, '').replace(/\r\n?/g, '\n');
|
||||
}
|
||||
|
||||
function inventory(root, label) {
|
||||
let rootStat;
|
||||
try {
|
||||
rootStat = lstatSync(root);
|
||||
} catch {
|
||||
throw new ConfigurationError(`${label} mapped source directory is missing: ${root}`);
|
||||
}
|
||||
if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
||||
throw new ConfigurationError(`${label} mapped source must be a real directory: ${root}`);
|
||||
}
|
||||
|
||||
const files = new Map();
|
||||
const casing = new Map();
|
||||
const walk = (directory, prefix = '') => {
|
||||
let entries;
|
||||
try {
|
||||
entries = readdirSync(directory, { withFileTypes: true }).sort((left, right) =>
|
||||
compareOrdinal(left.name, right.name),
|
||||
);
|
||||
} catch (error) {
|
||||
throw new ConfigurationError(`cannot inventory ${label} directory ${directory}: ${error.message}`);
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
||||
const absolutePath = join(directory, entry.name);
|
||||
const stat = lstatSync(absolutePath);
|
||||
if (stat.isSymbolicLink()) {
|
||||
throw new ConfigurationError(`${label} mapped inventory contains symlink: ${relativePath}`);
|
||||
}
|
||||
if (stat.isDirectory()) {
|
||||
if (!MAPPING.ignoredDirectories.includes(entry.name)) {
|
||||
walk(absolutePath, relativePath);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!stat.isFile()) {
|
||||
throw new ConfigurationError(`${label} mapped inventory contains special node: ${relativePath}`);
|
||||
}
|
||||
if (MAPPING.ignoredFileSuffixes.some((suffix) => relativePath.endsWith(suffix))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const folded = relativePath.toLowerCase();
|
||||
const previous = casing.get(folded);
|
||||
if (previous && previous !== relativePath) {
|
||||
throw new ConfigurationError(
|
||||
`${label} mapped inventory has case collision: ${previous} / ${relativePath}`,
|
||||
);
|
||||
}
|
||||
casing.set(folded, relativePath);
|
||||
const text = readUtf8Normalized(absolutePath);
|
||||
files.set(relativePath, {
|
||||
hash: createHash('sha256').update(text, 'utf-8').digest('hex'),
|
||||
text,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
walk(root);
|
||||
if (files.size === 0) {
|
||||
throw new ConfigurationError(`${label} mapped inventory is empty: ${root}`);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function resolveMappedRoot(repositoryRoot, relativePath, label) {
|
||||
let current = repositoryRoot;
|
||||
for (const segment of relativePath.split('/')) {
|
||||
current = join(current, segment);
|
||||
let stat;
|
||||
try {
|
||||
stat = lstatSync(current);
|
||||
} catch {
|
||||
throw new ConfigurationError(`${label} mapped path component is missing: ${current}`);
|
||||
}
|
||||
if (stat.isSymbolicLink()) {
|
||||
throw new ConfigurationError(`${label} mapped path contains ancestor symlink: ${current}`);
|
||||
}
|
||||
if (!stat.isDirectory()) {
|
||||
throw new ConfigurationError(`${label} mapped path component is not a directory: ${current}`);
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function isPrivateExclusion(path) {
|
||||
if (FORBIDDEN_EXPORTS.paths.includes(path)) return true;
|
||||
return FORBIDDEN_EXPORTS.pathPrefixes.some((prefix) =>
|
||||
prefix.endsWith('/') ? path.startsWith(prefix) : path === prefix || path.startsWith(`${prefix}/`),
|
||||
);
|
||||
}
|
||||
|
||||
function observedState(canonical, oss) {
|
||||
if (canonical && oss) return canonical.hash === oss.hash ? 'equal' : 'different';
|
||||
if (canonical) return 'only-canonical';
|
||||
if (oss) return 'only-oss';
|
||||
return 'absent';
|
||||
}
|
||||
|
||||
function hasForbiddenMarker(text, token) {
|
||||
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
return new RegExp(`\\b${escaped}\\b`, 'i').test(text);
|
||||
}
|
||||
|
||||
function gitReceipt(root, scopedPaths) {
|
||||
const head = execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], {
|
||||
encoding: 'utf-8',
|
||||
windowsHide: true,
|
||||
}).trim();
|
||||
const status = execFileSync(
|
||||
'git',
|
||||
['-C', root, 'status', '--porcelain=v1', '--untracked-files=all'],
|
||||
{ encoding: 'utf-8', windowsHide: true },
|
||||
).trim();
|
||||
const scopedStatus = execFileSync(
|
||||
'git',
|
||||
['-C', root, 'status', '--porcelain=v1', '--untracked-files=all', '--', ...scopedPaths],
|
||||
{ encoding: 'utf-8', windowsHide: true },
|
||||
).trim();
|
||||
return { head, dirty: status.length > 0, scopedStatus };
|
||||
}
|
||||
|
||||
function showSection(title, entries) {
|
||||
console.log(`${title}: ${entries.length}`);
|
||||
for (const entry of [...entries].sort(compareOrdinal)) {
|
||||
console.log(` ${entry}`);
|
||||
}
|
||||
}
|
||||
|
||||
function run() {
|
||||
if (process.argv.length > 3) {
|
||||
throw new ConfigurationError('usage: node scripts/oss-drift-check.mjs [path-to-oss-checkout]');
|
||||
}
|
||||
const baseline = loadBaseline();
|
||||
const canonicalRoot = resolveGitRoot(join(SCRIPT_DIR, '..'), 'canonical repository');
|
||||
const requestedOss =
|
||||
process.argv[2] ??
|
||||
(process.env.OSS_HIVE_MIND_DIR?.trim() || resolve(canonicalRoot, '..', 'hive-mind'));
|
||||
const ossRoot = resolveGitRoot(resolve(process.cwd(), requestedOss), 'OSS repository');
|
||||
if (canonicalRoot.toLowerCase() === ossRoot.toLowerCase()) {
|
||||
throw new ConfigurationError('canonical and OSS repositories must be different Git worktrees');
|
||||
}
|
||||
|
||||
const canonicalFiles = inventory(
|
||||
resolveMappedRoot(canonicalRoot, MAPPING.canonical, 'canonical'),
|
||||
'canonical',
|
||||
);
|
||||
const ossFiles = inventory(resolveMappedRoot(ossRoot, MAPPING.oss, 'OSS'), 'OSS');
|
||||
const crossRepoCasing = new Map();
|
||||
for (const path of [...canonicalFiles.keys(), ...ossFiles.keys()]) {
|
||||
const folded = path.toLowerCase();
|
||||
const previous = crossRepoCasing.get(folded);
|
||||
if (previous && previous !== path) {
|
||||
throw new ConfigurationError(`mapped inventories have case collision: ${previous} / ${path}`);
|
||||
}
|
||||
crossRepoCasing.set(folded, path);
|
||||
}
|
||||
|
||||
const parity = new Map(baseline.parityPaths.map((entry) => [entry.path, entry]));
|
||||
const adaptations = new Map(baseline.intentionalAdaptations.map((entry) => [entry.path, entry]));
|
||||
const blockers = new Map(baseline.knownReviewedBlockers.map((entry) => [entry.path, entry]));
|
||||
const knownUnreviewed = new Map(
|
||||
baseline.unreviewedDifferences.map((entry) => [entry.path, entry]),
|
||||
);
|
||||
const reviewedAdaptations = [];
|
||||
const intentionalExclusions = [];
|
||||
const knownBlockers = [];
|
||||
const unreviewed = [];
|
||||
const forbidden = [];
|
||||
const observedPaths = new Set();
|
||||
const union = [...new Set([...canonicalFiles.keys(), ...ossFiles.keys()])].sort(compareOrdinal);
|
||||
|
||||
for (const path of union) {
|
||||
observedPaths.add(path);
|
||||
const canonical = canonicalFiles.get(path);
|
||||
const oss = ossFiles.get(path);
|
||||
const state = observedState(canonical, oss);
|
||||
if (isPrivateExclusion(path)) {
|
||||
if (oss) {
|
||||
forbidden.push(`FORBIDDEN-OSS-CONTENT ${path}`);
|
||||
} else if (canonical) {
|
||||
intentionalExclusions.push(path);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const parityEntry = parity.get(path);
|
||||
if (parityEntry) {
|
||||
if (
|
||||
state === 'equal' &&
|
||||
canonical.hash === parityEntry.sha256 &&
|
||||
oss.hash === parityEntry.sha256
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
unreviewed.push(`BASELINE-DRIFT ${path} expected equal, observed ${state}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const adaptation = adaptations.get(path);
|
||||
if (adaptation) {
|
||||
if (
|
||||
state === 'different' &&
|
||||
canonical.hash === adaptation.canonicalSha256 &&
|
||||
oss.hash === adaptation.ossSha256
|
||||
) {
|
||||
reviewedAdaptations.push(`${path} [${adaptation.kinds.join(',')}]`);
|
||||
} else {
|
||||
unreviewed.push(`BASELINE-DRIFT ${path} reviewed adaptation bytes changed (${state})`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const blocker = blockers.get(path);
|
||||
if (blocker) {
|
||||
if (state === blocker.state) {
|
||||
knownBlockers.push(`${path} (${state}; ${blocker.disposition})`);
|
||||
} else {
|
||||
unreviewed.push(
|
||||
`BASELINE-DRIFT ${path} expected ${blocker.state}, observed ${state}`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const unreviewedEntry = knownUnreviewed.get(path);
|
||||
if (unreviewedEntry) {
|
||||
if (state === unreviewedEntry.state) {
|
||||
unreviewed.push(`${path} (${state})`);
|
||||
} else {
|
||||
unreviewed.push(
|
||||
`BASELINE-DRIFT ${path} expected ${unreviewedEntry.state}, observed ${state}`,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
unreviewed.push(`NEW ${path} (${state})`);
|
||||
}
|
||||
|
||||
for (const path of [
|
||||
...parity.keys(),
|
||||
...adaptations.keys(),
|
||||
...blockers.keys(),
|
||||
...knownUnreviewed.keys(),
|
||||
]) {
|
||||
if (!observedPaths.has(path)) {
|
||||
unreviewed.push(`STALE-BASELINE ${path} (absent)`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const marker of FORBIDDEN_EXPORTS.markers) {
|
||||
const file = ossFiles.get(marker.path);
|
||||
if (!file) continue;
|
||||
if (hasForbiddenMarker(file.text, marker.token)) {
|
||||
forbidden.push(`FORBIDDEN-OSS-MARKER ${marker.path}:${marker.token}`);
|
||||
}
|
||||
}
|
||||
|
||||
const canonicalReceipt = gitReceipt(canonicalRoot, [
|
||||
MAPPING.canonical,
|
||||
'scripts/oss-drift-check.mjs',
|
||||
'scripts/oss-drift-baseline.json',
|
||||
'scripts/oss-drift-check.sh',
|
||||
]);
|
||||
const ossReceipt = gitReceipt(ossRoot, [MAPPING.oss]);
|
||||
if (canonicalReceipt.scopedStatus) {
|
||||
unreviewed.push('SCOPED-DIRTY canonical checker, baseline, wrapper, or mapped source');
|
||||
}
|
||||
if (ossReceipt.scopedStatus) {
|
||||
unreviewed.push('SCOPED-DIRTY OSS mapped source');
|
||||
}
|
||||
console.log(
|
||||
`CANONICAL ${canonicalReceipt.head} ${canonicalReceipt.dirty ? 'dirty' : 'clean'} ${canonicalRoot}`,
|
||||
);
|
||||
console.log(`OSS ${ossReceipt.head} ${ossReceipt.dirty ? 'dirty' : 'clean'} ${ossRoot}`);
|
||||
console.log(`PARITY: ${parity.size}`);
|
||||
showSection('REVIEWED ADAPTATIONS', reviewedAdaptations);
|
||||
showSection('INTENTIONAL OSS EXCLUSIONS', intentionalExclusions);
|
||||
showSection('KNOWN REVIEWED BLOCKERS', knownBlockers);
|
||||
showSection('UNREVIEWED DIFFERENCES', unreviewed);
|
||||
showSection('FORBIDDEN EXPORTS', forbidden);
|
||||
|
||||
const blocked = knownBlockers.length || unreviewed.length || forbidden.length;
|
||||
if (blocked) {
|
||||
console.log(
|
||||
'[oss-drift-check] release blocked: reconcile in the canonical monorepo, then use a curated forward-port; never publish a raw split.',
|
||||
);
|
||||
}
|
||||
return blocked ? 1 : 0;
|
||||
}
|
||||
|
||||
try {
|
||||
process.exitCode = run();
|
||||
} catch (error) {
|
||||
if (error instanceof ConfigurationError) {
|
||||
console.error(`[oss-drift-check] CONFIGURATION ERROR: ${error.message}`);
|
||||
} else {
|
||||
console.error(`[oss-drift-check] UNEXPECTED ERROR: ${error?.stack ?? error}`);
|
||||
}
|
||||
process.exitCode = 2;
|
||||
}
|
||||
Reference in New Issue
Block a user