Skip to content
Open
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
59 changes: 59 additions & 0 deletions core/context/providers/ActionGateDebtProvider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, it, expect } from "vitest";
import { ActionGateDebtContextProvider, GENESIS_HASH } from "./ActionGateDebtProvider";

describe("ActionGateDebtContextProvider", () => {
it("should approve clean refactoring with high readiness score", () => {
const provider = new ActionGateDebtContextProvider(80);
const report = provider.evaluateRefactor(
"src/server.ts",
"const a = 1;\nconst b = 2;\n",
"const a = 1;\nconst b = 2;\nconst c = 3;\n",
{
unboundedLoops: 0,
unhandledExceptions: 0,
unGatedMutations: 0,
},
);

expect(report.isProductionReady).toBe(true);
expect(report.readinessScore).toBe(100);
expect(report.criticalSmells.length).toBe(0);
expect(report.receipt.currHash).toBeDefined();
});

it("should reject refactoring with unbounded loops and unhandled exceptions", () => {
const provider = new ActionGateDebtContextProvider(80);
const report = provider.evaluateRefactor(
"src/agent.ts",
"function run() {}\n",
"function run() { while(true) {} }\n",
{
unboundedLoops: 1,
unhandledExceptions: 2,
unGatedMutations: 1,
},
);

expect(report.isProductionReady).toBe(false);
expect(report.readinessScore).toBeLessThan(50);
expect(report.criticalSmells).toContain("DETECTED_1_UNBOUNDED_LOOPS");
expect(report.criticalSmells).toContain(
"DETECTED_2_UNHANDLED_EXCEPTION_PATHS",
);
expect(report.criticalSmells).toContain("DETECTED_1_UNGATED_MUTATIONS");
});

it("should maintain cryptographic hash-chain integrity", () => {
const provider = new ActionGateDebtContextProvider();
provider.evaluateRefactor("file1.ts", "a", "b");
provider.evaluateRefactor("file2.ts", "c", "d");
provider.evaluateRefactor("file3.ts", "e", "f");

const entries = provider.getLedger().getEntries();
expect(entries.length).toBe(3);
expect(entries[0].prevHash).toBe(GENESIS_HASH);
expect(entries[1].prevHash).toBe(entries[0].currHash);
expect(entries[2].prevHash).toBe(entries[1].currHash);
expect(provider.getLedger().verifyIntegrity()).toBe(true);
});
});
181 changes: 181 additions & 0 deletions core/context/providers/ActionGateDebtProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { createHash } from "crypto";

export const GENESIS_HASH =
"0000000000000000000000000000000000000000000000000000000000000000";

export interface IDEDebtReceipt {
index: number;
timestamp: string;
filename: string;
eventType: string;
readinessScore: number;
criticalSmells: string[];
prevHash: string;
currHash: string;
metadata: Record<string, unknown>;
}

export interface InIDEDebtReport {
filename: string;
readinessScore: number;
isProductionReady: boolean;
debtDelta: number;
criticalSmells: string[];
receipt: IDEDebtReceipt;
}

export class ActionGateIDELedger {
private entries: IDEDebtReceipt[] = [];
private lastHash: string = GENESIS_HASH;

public recordRefactor(
filename: string,
eventType: string,
readinessScore: number,
criticalSmells: string[],
metadata: Record<string, unknown> = {},
): IDEDebtReceipt {
const timestamp = new Date().toISOString();
const index = this.entries.length;

const metaString = JSON.stringify(metadata);
const metaHash = createHash("sha256").update(metaString).digest("hex");

const canonical = `${index}|${this.lastHash}|${filename}|${eventType}|${readinessScore}|${timestamp}|${metaHash}`;
const currHash = createHash("sha256").update(canonical).digest("hex");

const receipt: IDEDebtReceipt = {
index,
timestamp,
filename,
eventType,
readinessScore,
criticalSmells,
prevHash: this.lastHash,
currHash,
metadata,
};

this.entries.push(receipt);
this.lastHash = currHash;
return receipt;
}

public getEntries(): IDEDebtReceipt[] {
return [...this.entries];
}

public verifyIntegrity(): boolean {
let prev = GENESIS_HASH;
for (const entry of this.entries) {
if (entry.prevHash !== prev) {
return false;
}
prev = entry.currHash;
}
return true;
}
}

export class ActionGateDebtContextProvider {
public static readonly title = "ActionGate Production Debt";
public static readonly description =
"In-IDE real-time technical debt, refactor safety, and cryptographic due diligence scoring.";

private ledger: ActionGateIDELedger;
public readonly neverEquateIntentToApproval: boolean;
public readonly minimumReadinessScore: number;

constructor(
minimumReadinessScore: number = 80,
neverEquateIntentToApproval: boolean = true,
) {
this.ledger = new ActionGateIDELedger();
this.minimumReadinessScore = minimumReadinessScore;
this.neverEquateIntentToApproval = neverEquateIntentToApproval;
}

public getLedger(): ActionGateIDELedger {
return this.ledger;
}

public checkKillSwitch(): boolean {
const envVal = (process.env.AAG_KILL_SWITCH || "").toLowerCase();
return envVal === "true" || envVal === "1" || envVal === "yes";
}

public evaluateRefactor(
filename: string,
originalCode: string,
refactoredCode: string,
options: {
unboundedLoops?: number;
unhandledExceptions?: number;
unGatedMutations?: number;
} = {},
): InIDEDebtReport {
if (this.checkKillSwitch()) {
this.ledger.recordRefactor(
filename,
"refactor_blocked_kill_switch",
0,
["EMERGENCY_KILL_SWITCH_ACTIVE"],
{ reason: "AAG_KILL_SWITCH is set" },
);
throw new Error(
"A2Z SOC ActionGate: Emergency kill switch is engaged. In-IDE refactoring halted.",
);
}

const unboundedLoops = options.unboundedLoops || 0;
const unhandledExceptions = options.unhandledExceptions || 0;
const unGatedMutations = options.unGatedMutations || 0;

const criticalSmells: string[] = [];

if (unboundedLoops > 0) {
criticalSmells.push(`DETECTED_${unboundedLoops}_UNBOUNDED_LOOPS`);
}
if (unhandledExceptions > 0) {
criticalSmells.push(`DETECTED_${unhandledExceptions}_UNHANDLED_EXCEPTION_PATHS`);
}
if (unGatedMutations > 0) {
criticalSmells.push(`DETECTED_${unGatedMutations}_UNGATED_MUTATIONS`);
}

const originalLines = originalCode.split("\n").length;
const refactoredLines = refactoredCode.split("\n").length;
const lineDelta = refactoredLines - originalLines;

// Debt Penalty Calculation
const penalty =
unboundedLoops * 30 +
unhandledExceptions * 20 +
unGatedMutations * 25 +
(lineDelta > 50 ? 15 : 0);

const readinessScore = Math.max(0, 100 - penalty);
const isProductionReady =
readinessScore >= this.minimumReadinessScore && criticalSmells.length === 0;

const receipt = this.ledger.recordRefactor(
filename,
isProductionReady ? "refactor_approved" : "refactor_rejected_debt",
readinessScore,
criticalSmells,
{
lineDelta,
neverEquateIntentToApproval: this.neverEquateIntentToApproval,
},
);

return {
filename,
readinessScore,
isProductionReady,
debtDelta: penalty,
criticalSmells,
receipt,
};
}
}
Loading