Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/testing/main-capability-coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"version": 1,
"audit": {
"base": "2b34c653119bdf480f2af0330ee3809b51441807",
"head": "0accf78e1919556c57dd08a4d055bc5bfa6419ae",
"head": "66fbdbcf0e6b9d26768ce432fe0e0a49dad2e8db",
"ignoredDocumentationCommits": [
"dd86a325e3db5aa013ffa18a647e67e9fa279c10",
"0b0e12b4d97ec7d77a8a93076459fdaad2e52070",
Expand Down Expand Up @@ -478,7 +478,8 @@
"4cf025e589a2f7426ae1f68d40c774429b6b9855",
"8aeef7b953132be14677977d21d41ffe1e051dd0",
"6cc7fca077b4929411d6dbaede79e8e560346ece",
"2cc5b47de6e8fe51b4998293b0dc224ea908fdf3"
"2cc5b47de6e8fe51b4998293b0dc224ea908fdf3",
"37027f5c598f916c0656343248fb059be7ab8daf"
]
},
"capabilities": [
Expand Down Expand Up @@ -2356,7 +2357,8 @@
"630595db25f3f3723c85aa863de001982a1f6aaa",
"86a597dbe65633fd3328534788890febeb1644fd",
"4b3d7e25531865cbdd203830e71c964a57207de8",
"0accf78e1919556c57dd08a4d055bc5bfa6419ae"
"0accf78e1919556c57dd08a4d055bc5bfa6419ae",
"66fbdbcf0e6b9d26768ce432fe0e0a49dad2e8db"
],
"behaviors": [
"ARCH-POLICY-SCHEMA-001",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,7 @@
"test:conversation-performance": "node scripts/run-conversation-performance-checks.js",
"test:conversation-sources:remote": "node --test tests/platform/remote/conversationSources.test.js",
"test:extension-host": "npm run package:release && node scripts/run-extension-host-tests.js",
"test:ci:linux": "npm run test-compile && npm run brand:verify && npm run brand:check && npm run test:behavior-contracts && npm run lint:ci && npm run test:coverage:run && npm run test:conversation-sources:remote && npm run test:conversation-performance && npm run test:browser:run && npm run test:safety:run && npm run test:dashboard:run && npm run test:architecture-baseline && npm run test:architecture-guards && npm run test:architecture-policy && node scripts/run-guard-mutation-parity.js && npm run test:release-notes && npm run test:release-packaging && node scripts/check-coverage-baseline.js && node scripts/check-changed-coverage.js",
"test:ci:linux": "npm run test-compile && npm run brand:verify && npm run brand:check && npm run test:behavior-contracts && npm run lint:ci && npm run test:coverage:run && npm run test:conversation-sources:remote && npm run test:conversation-performance && npm run test:browser:run && npm run test:safety:run && npm run test:dashboard:run && npm run test:architecture-baseline && npm run test:architecture-guards && npm run test:architecture-policy && npm run test:release-notes && npm run test:release-packaging && node scripts/check-coverage-baseline.js && node scripts/check-changed-coverage.js",
"brand:check": "node scripts/check-brand-identity.js",
"brand:generate": "node scripts/generate-brand-assets.js",
"brand:verify": "node scripts/generate-brand-assets.js --check",
Expand Down
3 changes: 1 addition & 2 deletions scripts/architecture/architectureChangeRecords.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const BLOCK_PATTERN = /```arch-change\s*\r?\n([\s\S]*?)```/;

const DELTA_MAP_KEYS = ['mayDependOnGrown', 'entrypointsGrown'];
const DELTA_LIST_KEYS = ['baselineGrown', 'waiversAdded', 'ledgerRegressions'];
const DELTA_FLAG_KEYS = ['rePartition', 'harnessWeakening', 'guardSemantics'];
const DELTA_FLAG_KEYS = ['rePartition', 'harnessWeakening'];
const DELTA_KEYS = [...DELTA_MAP_KEYS, ...DELTA_LIST_KEYS, ...DELTA_FLAG_KEYS,
'invariantChanges', 'fileMoves'];

Expand Down Expand Up @@ -213,7 +213,6 @@ function parseArchitectureChangeRecord({ path: recordPath, text }) {
fileMoves: rawDelta.fileMoves || [],
rePartition: rawDelta.rePartition === true,
harnessWeakening: rawDelta.harnessWeakening === true,
guardSemantics: rawDelta.guardSemantics === true,
};
return {
record: {
Expand Down
179 changes: 22 additions & 157 deletions scripts/architecture/checkArchitectureChange.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,23 @@
'use strict';

/**
* Anti-self-amendment gate (Harness v0, program Stage 2 PR 4; charter 8.9).
* Anti-self-amendment gate (Harness Simplification PR #296).
*
* Classifies a change by its impact on protected architecture policy and the
* harness surface (review R2):
* - product-only: neither policy files nor the harness surface touched;
* - tightening: policy only narrows, or the harness surface changes without
* weakening;
* - relaxing or registry re-partition: baseline grew, waivers added,
* mayDependOn broadened, writer sets grew, module structure changed, or
* the harness surface weakened (a guard file deleted, a guard id removed,
* a lane or workflow invocation removed, mutation tests shrank).
* harness surface. Relaxing or re-partition changes require owner architecture
* approval (approve-architecture <sha>), enforced by the trusted kernel.
*
* A relaxing or re-partition change is authorized in one of two ways:
*
* 1. Owner architecture approval (Harness Simplification decision,
* docs/architecture/harness-simplification-decision.md): the repository
* owner comments `approve-architecture <full-head-sha>` on the PR. The
* approval binds the exact head and expires when the head moves. This is
* the transitional replacement for machine authorization and the only
* path once Architecture Change records become historical ADRs.
* 2. An Architecture Change record that already exists in the PR base
* (review R3; charter 8.9: the record lands in its own earlier PR before
* product work consumes it). The record must carry a valid ```arch-change
* machine-summary block whose declared delta covers the actual policy
* delta; an empty markdown file, a bare filename match, or a record added
* in the same PR never authorizes. Because every merge requires the
* merge-approval status (owner comment newer than the PR head), a record
* present in the base was necessarily approved after its final commit —
* approval timing holds transitively.
*
* An agent cannot legalize its own violation: the classification is computed
* from the diff, not declared, and both authorization paths live outside the
* PR head (owner comment on the PR, record in the base).
* Architecture Change records are historical ADRs only — no machine
* authorization, no record consumption, no parity exemption.
*/

const path = require('path');
const {
collectArchitectureDiff,
defaultGit,
} = require('./reportArchitectureDiff');
const {
ARCH_CHANGE_RECORD_PATTERN,
coversPolicyDelta,
fingerprintFields,
parseArchitectureChangeRecord,
} = require('./architectureChangeRecords');
const { ARCH_CHANGE_RECORD_PATTERN } = require('./architectureChangeRecords');

// Kept for backward compatibility with existing imports.
const ARCH_CHANGE_PATTERN = ARCH_CHANGE_RECORD_PATTERN;

function harnessWeakenedOf(harness) {
Expand All @@ -58,118 +27,6 @@ function harnessWeakenedOf(harness) {
|| harness.shrunkMutationTests.length > 0;
}

/**
* The exact delta a consuming change realizes — the shape a record must
* declare verbatim (round-2 review Blocker 3). Shared by the gate (coverage
* matching) and scripts/architecture/describeArchitectureChange.js
* (record authoring).
*/
function computeActualDelta(report, classification, harnessWeakened) {
const delta = report.policyDelta;
const invariantChanges = Object.entries(delta.invariantChanges || {})
.map(([id, change]) => ({ id, ...change }));
const isTighteningOnly = change =>
change.fields.length === 1 && change.fields[0] === 'writers'
&& (change.writersAdded || []).length === 0;
const relaxingInvariantChanges = invariantChanges.filter(change => !isTighteningOnly(change));
// Removed invariants join the coverage surface with a removal marker:
// the record must declare the exact removed record's fingerprint.
const removedEntries = (delta.invariantsRemoved || []).map(id => ({
id,
fields: ['removed'],
before: fingerprintFields({ record: (report.removedInvariantRecords || {})[id] }),
after: fingerprintFields({}),
}));
// Round-2 review Blocker 3: the record's module scope must cover every
// module the change actually touches.
const touchedModules = new Set(Object.keys(report.touchedModules || {}));
for (const key of Object.keys(delta.mayDependOnGrown || {})) { touchedModules.add(key); }
for (const key of Object.keys(delta.entrypointsGrown || {})) { touchedModules.add(key); }
for (const move of report.moduleMoves || []) {
touchedModules.add(move.from);
touchedModules.add(move.to);
}
for (const regression of delta.ledgerRegressions || []) {
touchedModules.add(regression.split(':')[0]);
}
for (const moduleId of report.changedInvariantModules || []) { touchedModules.add(moduleId); }
return {
policyDelta: delta,
harnessWeakened,
rePartition: classification === 're-partition',
invariantChanges: [...relaxingInvariantChanges, ...removedEntries].map(
({ id, fields, before, after }) => ({ id, fields, before, after })),
fileMoves: report.moduleMoves || [],
touchedModules: [...touchedModules].sort(),
};
}

/**
* Authorize a relaxing/re-partition classification against the Architecture
* Change records present in the base. Returns the error list (empty when
* authorized).
*/
function authorizeWithBaseRecords(report, classification, harnessWeakened, options) {
const baseRecords = report.baseRecords || [];
const architectureApproved = Boolean(options && options.architectureApproved);
const candidates = [];
const invalid = [];
for (const { path: recordPath, text } of baseRecords) {
const { record, errors } = parseArchitectureChangeRecord({ path: recordPath, text });
if (record) { candidates.push(record); }
if (errors.length > 0) { invalid.push(...errors); }
}
const actual = computeActualDelta(report, classification, harnessWeakened);
let bestMissing = null;
for (const record of candidates) {
const { covered, missing } = coversPolicyDelta(record, actual);
if (covered) { return []; }
if (bestMissing === null || missing.length < bestMissing.length) {
bestMissing = missing;
}
}

const verb = classification === 'relaxing' ? 'relaxes' : 're-partitions';
const errors = [];
if (architectureApproved) {
// Owner architecture approval (approve-architecture <full-head-sha>)
// authorizes the relaxation; the approval is verified by the caller
// against the exact head SHA and never by PR-head content.
return errors;
}
if (candidates.length === 0) {
errors.push(`anti-self-amendment: this change ${verb} architecture policy or weakens the `
+ 'harness, and no valid approved Architecture Change record exists in the PR base '
+ `(found ${baseRecords.length} record file(s), ${invalid.length} invalid). Owner `
+ 'architecture approval required — comment '
+ '\'approve-architecture <full-head-sha>\' on the pull request; alternatively land a '
+ 'docs-only PR adding docs/architecture/changes/ARCH-CHANGE-<seq>.md with an '
+ '```arch-change machine-summary block (id, status "approved", modules, declared '
+ 'delta) first; a record added in the same PR never authorizes consumption.');
} else {
errors.push(`anti-self-amendment: this change ${verb} architecture policy or weakens the `
+ `harness beyond every approved Architecture Change record in the PR base. Uncovered `
+ `delta: ${bestMissing.join('; ')}. Owner architecture approval required — comment `
+ '\'approve-architecture <full-head-sha>\' on the pull request; alternatively land a '
+ 'docs-only record PR declaring this delta first, or narrow the change to what an '
+ 'existing record declares.');
}
if (report.newFiles.some(file => ARCH_CHANGE_RECORD_PATTERN.test(file))) {
errors.push('anti-self-amendment: this PR adds an Architecture Change record and consumes '
+ 'a relaxation in the same diff — the record must land in an earlier PR (charter 8.9).');
}
return errors;
}

/**
* classifyArchitectureChange(report, options) -> {
* classification: 'product-only' | 'tightening' | 'relaxing' | 're-partition',
* errors: string[]
* }
* options.architectureApproved: the owner has bound an architecture approval
* comment to the exact head SHA (verified by the caller, never by PR-head
* content).
*/
function classifyArchitectureChange(report, options) {
const errors = [];
if (report.errors && report.errors.length > 0) {
Expand All @@ -188,9 +45,6 @@ function classifyArchitectureChange(report, options) {
const grownMayDependOn = Object.keys(delta.mayDependOnGrown).length > 0;
const grownEntrypoints = Object.keys(delta.entrypointsGrown || {}).length > 0;
const ledgerRegressions = (delta.ledgerRegressions || []);
// Review R9 (Important 4): only a pure writer removal with an unchanged
// authority is tightening — an invariant change whose only edited field is
// `writers` with removals alone. Everything else is relaxing.
const isTighteningOnly = change =>
change.fields.length === 1 && change.fields[0] === 'writers'
&& (change.writersAdded || []).length === 0;
Expand All @@ -213,9 +67,21 @@ function classifyArchitectureChange(report, options) {
if (!relaxing && !rePartition) {
return { classification: 'tightening', errors };
}
const classification = relaxing ? 'relaxing' : 're-partition';
errors.push(...authorizeWithBaseRecords(report, classification, harnessWeakened, options));
return { classification, errors };

// Owner architecture approval (approve-architecture <full-head-sha>) is
// the only authorization left after record machine authorization was
// deleted. The caller verifies the comment binds the exact head SHA;
// the classifier only receives the verdict.
if (!(options && options.architectureApproved)) {
const verb = relaxing ? 'relaxes' : 're-partitions';
errors.push(
`anti-self-amendment: this change ${verb} architecture policy. `
+ 'Owner architecture approval required — comment '
+ '\'approve-architecture <full-head-sha>\' on the pull request.'
);
}

return { classification: relaxing ? 'relaxing' : 're-partition', errors };
}

function runArchitectureChangeCheck(rootDirectory, baseRef, options) {
Expand All @@ -239,7 +105,7 @@ function main() {
path.resolve(__dirname, '..', '..'), undefined, { architectureApproved });
if (errors.length > 0) {
console.error(`Architecture change gate FAILED (classification: ${classification}):`);
for (const error of errors) { console.error(` ✗ ${error}`); }
for (const error of errors) console.error(` ✗ ${error}`);
process.exitCode = 1;
return;
}
Expand All @@ -251,6 +117,5 @@ if (require.main === module) { main(); }
module.exports = {
ARCH_CHANGE_PATTERN,
classifyArchitectureChange,
computeActualDelta,
runArchitectureChangeCheck,
};
47 changes: 13 additions & 34 deletions scripts/architecture/describeArchitectureChange.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@
'use strict';

/**
* Record-authoring helper (round-2 review Blocker 3): prints the exact
* record-ready `delta` object for the current branch diff, including
* before/after fingerprints for invariant changes and per-file module moves.
* Architecture diff reporter (Harness Simplification PR #296).
*
* Authoring flow (charter 8.9): make the policy edit locally, run this
* script, paste the printed delta into the ARCH-CHANGE record's
* machine-summary block, revert the edit, land the docs-only record PR, then
* re-apply the edit in the consuming PR — the gate requires exact equality.
* Prints the architecture diff for the current branch. No longer generates
* record-ready deltas — Architecture Change records are historical ADRs.
*
* Usage: node scripts/architecture/describeArchitectureChange.js [base-ref]
*/
Expand All @@ -19,10 +15,7 @@ const {
collectArchitectureDiff,
defaultGit,
} = require('./reportArchitectureDiff');
const {
classifyArchitectureChange,
computeActualDelta,
} = require('./checkArchitectureChange');
const { classifyArchitectureChange } = require('./checkArchitectureChange');

function describeArchitectureChange(rootDirectory, baseRef) {
const report = collectArchitectureDiff({
Expand All @@ -33,33 +26,19 @@ function describeArchitectureChange(rootDirectory, baseRef) {
git: defaultGit(rootDirectory),
});
const { classification } = classifyArchitectureChange(report);
const harness = report.harnessDelta || {};
const harnessWeakened = (harness.deletedFiles || []).length > 0
|| (harness.removedGuardIds || []).length > 0
|| (harness.removedInvocations || []).length > 0
|| (harness.shrunkMutationTests || []).length > 0;
const actual = computeActualDelta(report, classification, harnessWeakened);
const delta = {
mayDependOnGrown: actual.policyDelta.mayDependOnGrown,
entrypointsGrown: actual.policyDelta.entrypointsGrown || {},
baselineGrown: actual.policyDelta.baselineGrown,
waiversAdded: actual.policyDelta.waiversAdded,
ledgerRegressions: actual.policyDelta.ledgerRegressions || [],
invariantChanges: actual.invariantChanges,
fileMoves: actual.fileMoves,
rePartition: actual.rePartition,
harnessWeakening: actual.harnessWeakened,
return {
classification,
policyDelta: report.policyDelta,
protectedTouched: report.protectedTouched,
harnessTouched: (report.harnessDelta || {}).touched || [],
touchedModules: Object.keys(report.touchedModules || {}),
};
return { classification, delta, touchedModules: actual.touchedModules };
}

function main() {
const { classification, delta, touchedModules } = describeArchitectureChange(
path.resolve(__dirname, '..', '..'), process.argv[2]);
console.log(`classification: ${classification}`);
console.log(`modules: ${JSON.stringify(touchedModules)}`);
console.log('delta for the record machine-summary block:');
console.log(JSON.stringify(delta, null, 2));
const root = path.resolve(__dirname, '..', '..');
const result = describeArchitectureChange(root, process.argv[2]);
console.log(JSON.stringify(result, null, 2));
}

if (require.main === module) { main(); }
Expand Down
1 change: 0 additions & 1 deletion scripts/architecture/trustedKernel.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,6 @@ const PROTECTED_PATHS = [
'tests/unit/architecture-parity/',
'.github/workflows/',
'scripts/run-architecture-guards.js',
'scripts/run-guard-mutation-parity.js',
'scripts/run-merge-approval-gate.js',
'scripts/lib/mergeApprovals.js',
'scripts/lib/ciContracts.js',
Expand Down
1 change: 0 additions & 1 deletion scripts/run-architecture-approval-gate.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ const PROTECTED_PATHS = [
'tests/unit/architecture-parity/',
'.github/workflows/',
'scripts/run-architecture-guards.js',
'scripts/run-guard-mutation-parity.js',
'scripts/run-merge-approval-gate.js',
'scripts/lib/mergeApprovals.js',
'scripts/lib/ciContracts.js',
Expand Down
Loading
Loading