fix(P01): migrate audit trail to git-native and replace audit_file with commit_hash
---ci---
project: ci
phase: 1
milestone: v0.8
status: in_progress
decisions:
- id: D-024
decision: Audit trail reads from git log instead of .ciagent/audit/*.json
rationale: Git-native context means audit data should come from commit history, not files
confidence: 0.88
- id: D-025
decision: Replace audit_file with commit_hash in Escalation type
rationale: Escalations are committed to git; reference by hash instead of deprecated file path
confidence: 0.90
requirements:
covered: [FIX-04, FIX-05]
---/ci---
FIX-04: audit.ts logDecision/logEscalation now emit deprecation warnings
and are no-ops (decisions/escalations live in ---ci--- blocks). readAudit()
and getAuditSummary() parse git log for ---ci--- blocks instead of reading
.ciagent/audit/*.json files. ArtifactManager no longer creates audit dir.
FIX-05: Escalation type replaces audit_file: string with commit_hash: string.
All consumers updated (escalation.ts, ollama-base.ts, opencode.ts).
Audit tests rewritten for git-native approach.
This commit is contained in:
@@ -328,7 +328,7 @@ export abstract class OllamaBaseBackend implements IntelligenceBackend {
|
|||||||
options: Array.isArray(e.options) ? e.options : [],
|
options: Array.isArray(e.options) ? e.options : [],
|
||||||
default_option_id: String(e.default_option_id || ""),
|
default_option_id: String(e.default_option_id || ""),
|
||||||
resolution: (e.resolution as Escalation["resolution"]) || "pending",
|
resolution: (e.resolution as Escalation["resolution"]) || "pending",
|
||||||
audit_file: String(e.audit_file || ""),
|
commit_hash: String(e.commit_hash || ""),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ describe("ArtifactManager", () => {
|
|||||||
it("creates .ciagent directory structure", () => {
|
it("creates .ciagent directory structure", () => {
|
||||||
manager.ensureStructure();
|
manager.ensureStructure();
|
||||||
expect(fs.existsSync(path.join(tempDir, ".ciagent"))).toBe(true);
|
expect(fs.existsSync(path.join(tempDir, ".ciagent"))).toBe(true);
|
||||||
expect(fs.existsSync(path.join(tempDir, ".ciagent", "audit"))).toBe(true);
|
expect(fs.existsSync(path.join(tempDir, ".ciagent", "phases"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is idempotent", () => {
|
it("is idempotent", () => {
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ export class ArtifactManager {
|
|||||||
ensureStructure(): void {
|
ensureStructure(): void {
|
||||||
ensureDir(this.ciDir);
|
ensureDir(this.ciDir);
|
||||||
ensureDir(path.join(this.ciDir, "phases"));
|
ensureDir(path.join(this.ciDir, "phases"));
|
||||||
ensureDir(path.join(this.ciDir, "audit"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isInitialized(): boolean {
|
isInitialized(): boolean {
|
||||||
|
|||||||
+128
-64
@@ -1,16 +1,23 @@
|
|||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import * as os from "node:os";
|
import * as os from "node:os";
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
import { logDecision, logEscalation, readAudit, getAuditSummary } from "../core/audit.js";
|
import { logDecision, logEscalation, readAudit, getAuditSummary } from "../core/audit.js";
|
||||||
import { Decision } from "../types/decisions.js";
|
import { Decision } from "../types/decisions.js";
|
||||||
import { Escalation } from "../types/escalation.js";
|
import { Escalation } from "../types/escalation.js";
|
||||||
|
|
||||||
describe("Audit", () => {
|
describe("Audit (git-native)", () => {
|
||||||
let tempDir: string;
|
let tempDir: string;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-audit-test-"));
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ciagent-audit-test-"));
|
||||||
fs.mkdirSync(path.join(tempDir, ".ciagent", "audit"), { recursive: true });
|
fs.mkdirSync(path.join(tempDir, ".ciagent"), { recursive: true });
|
||||||
|
execSync("git init", { cwd: tempDir, stdio: "pipe" });
|
||||||
|
execSync('git config user.email "test@test.com"', { cwd: tempDir, stdio: "pipe" });
|
||||||
|
execSync('git config user.name "Test"', { cwd: tempDir, stdio: "pipe" });
|
||||||
|
const placeholder = path.join(tempDir, "README.md");
|
||||||
|
fs.writeFileSync(placeholder, "# test\n");
|
||||||
|
execSync("git add -A && git commit -m 'initial'", { cwd: tempDir, stdio: "pipe" });
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -40,12 +47,48 @@ describe("Audit", () => {
|
|||||||
],
|
],
|
||||||
default_option_id: "A",
|
default_option_id: "A",
|
||||||
resolution: "pending",
|
resolution: "pending",
|
||||||
audit_file: ".ciagent/audit/test.json",
|
commit_hash: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("logDecision", () => {
|
describe("deprecated log functions", () => {
|
||||||
it("logs a decision to the audit trail", () => {
|
it("logDecision is a no-op that warns", () => {
|
||||||
logDecision(tempDir, 1, sampleDecision);
|
logDecision(tempDir, 1, sampleDecision);
|
||||||
|
const audit = readAudit(tempDir);
|
||||||
|
expect(audit).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("logEscalation is a no-op that warns", () => {
|
||||||
|
logEscalation(tempDir, 1, sampleEscalation);
|
||||||
|
const audit = readAudit(tempDir);
|
||||||
|
expect(audit).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("readAudit from git log", () => {
|
||||||
|
it("returns empty array when no ci blocks exist", () => {
|
||||||
|
const audit = readAudit(tempDir);
|
||||||
|
expect(audit).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads decisions from ---ci--- blocks in git log", () => {
|
||||||
|
const ciBlock = `docs(P01): test commit
|
||||||
|
|
||||||
|
---ci---
|
||||||
|
project: ci
|
||||||
|
phase: 1
|
||||||
|
milestone: v0.8
|
||||||
|
status: in_progress
|
||||||
|
decisions:
|
||||||
|
- id: D-001
|
||||||
|
decision: Use PostgreSQL
|
||||||
|
rationale: ACID compliance needed
|
||||||
|
confidence: 0.92
|
||||||
|
---/ci---`;
|
||||||
|
execSync(`git add -A && git commit -m "${ciBlock.replace(/"/g, '\\"')}" --allow-empty`, {
|
||||||
|
cwd: tempDir,
|
||||||
|
stdio: "pipe",
|
||||||
|
});
|
||||||
|
|
||||||
const audit = readAudit(tempDir);
|
const audit = readAudit(tempDir);
|
||||||
expect(audit).toHaveLength(1);
|
expect(audit).toHaveLength(1);
|
||||||
expect(audit[0].phase).toBe(1);
|
expect(audit[0].phase).toBe(1);
|
||||||
@@ -53,47 +96,35 @@ describe("Audit", () => {
|
|||||||
expect(audit[0].decisions[0].id).toBe("D-001");
|
expect(audit[0].decisions[0].id).toBe("D-001");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("appends multiple decisions to same phase file", () => {
|
|
||||||
logDecision(tempDir, 1, { ...sampleDecision, id: "D-001" });
|
|
||||||
logDecision(tempDir, 1, { ...sampleDecision, id: "D-002" });
|
|
||||||
const audit = readAudit(tempDir);
|
|
||||||
expect(audit[0].decisions).toHaveLength(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("separates decisions into different phase files", () => {
|
|
||||||
logDecision(tempDir, 1, sampleDecision);
|
|
||||||
logDecision(tempDir, 2, { ...sampleDecision, id: "D-002" });
|
|
||||||
const audit = readAudit(tempDir);
|
|
||||||
expect(audit).toHaveLength(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("logEscalation", () => {
|
|
||||||
it("logs an escalation to the audit trail", () => {
|
|
||||||
logEscalation(tempDir, 1, sampleEscalation);
|
|
||||||
const audit = readAudit(tempDir);
|
|
||||||
expect(audit).toHaveLength(1);
|
|
||||||
expect(audit[0].escalations).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("can mix decisions and escalations in same phase", () => {
|
|
||||||
logDecision(tempDir, 1, sampleDecision);
|
|
||||||
logEscalation(tempDir, 1, sampleEscalation);
|
|
||||||
const audit = readAudit(tempDir);
|
|
||||||
expect(audit[0].decisions).toHaveLength(1);
|
|
||||||
expect(audit[0].escalations).toHaveLength(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("readAudit", () => {
|
|
||||||
it("returns empty array when no audit files exist", () => {
|
|
||||||
const audit = readAudit(tempDir);
|
|
||||||
expect(audit).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("filters by phase number", () => {
|
it("filters by phase number", () => {
|
||||||
logDecision(tempDir, 1, sampleDecision);
|
const ciBlock1 = `docs(P01): phase 1 commit
|
||||||
logDecision(tempDir, 2, { ...sampleDecision, id: "D-002" });
|
|
||||||
|
---ci---
|
||||||
|
project: ci
|
||||||
|
phase: 1
|
||||||
|
milestone: v0.8
|
||||||
|
status: complete
|
||||||
|
decisions:
|
||||||
|
- id: D-001
|
||||||
|
decision: Phase 1 decision
|
||||||
|
rationale: reason
|
||||||
|
confidence: 0.90
|
||||||
|
---/ci---`;
|
||||||
|
const ciBlock2 = `docs(P02): phase 2 commit
|
||||||
|
|
||||||
|
---ci---
|
||||||
|
project: ci
|
||||||
|
phase: 2
|
||||||
|
milestone: v0.8
|
||||||
|
status: in_progress
|
||||||
|
decisions:
|
||||||
|
- id: D-002
|
||||||
|
decision: Phase 2 decision
|
||||||
|
rationale: reason
|
||||||
|
confidence: 0.80
|
||||||
|
---/ci---`;
|
||||||
|
execSync(`git commit --allow-empty -m "${ciBlock1.replace(/"/g, '\\"')}"`, { cwd: tempDir, stdio: "pipe" });
|
||||||
|
execSync(`git commit --allow-empty -m "${ciBlock2.replace(/"/g, '\\"')}"`, { cwd: tempDir, stdio: "pipe" });
|
||||||
|
|
||||||
const phase1 = readAudit(tempDir, 1);
|
const phase1 = readAudit(tempDir, 1);
|
||||||
expect(phase1).toHaveLength(1);
|
expect(phase1).toHaveLength(1);
|
||||||
@@ -101,29 +132,62 @@ describe("Audit", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getAuditSummary", () => {
|
describe("getAuditSummary from git log", () => {
|
||||||
it("returns summary with counts", () => {
|
it("returns zeros for empty git log with no ci blocks", () => {
|
||||||
logDecision(tempDir, 1, { ...sampleDecision, confidence: 0.95 });
|
|
||||||
logDecision(tempDir, 1, { ...sampleDecision, id: "D-002", confidence: 0.7 });
|
|
||||||
logDecision(tempDir, 2, { ...sampleDecision, id: "D-003", confidence: 0.4 });
|
|
||||||
logEscalation(tempDir, 1, sampleEscalation);
|
|
||||||
|
|
||||||
const summary = getAuditSummary(tempDir);
|
|
||||||
expect(summary.total_decisions).toBe(3);
|
|
||||||
expect(summary.total_escalations).toBe(1);
|
|
||||||
expect(summary.phases).toContain(1);
|
|
||||||
expect(summary.phases).toContain(2);
|
|
||||||
expect(summary.decisions_by_confidence.high).toBe(1);
|
|
||||||
expect(summary.decisions_by_confidence.medium).toBe(1);
|
|
||||||
expect(summary.decisions_by_confidence.low).toBe(1);
|
|
||||||
expect(summary.escalations_by_type.irreversible_action).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns zeros for empty audit", () => {
|
|
||||||
const summary = getAuditSummary(tempDir);
|
const summary = getAuditSummary(tempDir);
|
||||||
expect(summary.total_decisions).toBe(0);
|
expect(summary.total_decisions).toBe(0);
|
||||||
expect(summary.total_escalations).toBe(0);
|
expect(summary.total_escalations).toBe(0);
|
||||||
expect(summary.phases).toHaveLength(0);
|
expect(summary.phases).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns summary with decision counts and confidence breakdown", () => {
|
||||||
|
const ciBlock = `docs(P01): multi-decision commit
|
||||||
|
|
||||||
|
---ci---
|
||||||
|
project: ci
|
||||||
|
phase: 1
|
||||||
|
milestone: v0.8
|
||||||
|
status: complete
|
||||||
|
decisions:
|
||||||
|
- id: D-001
|
||||||
|
decision: High confidence decision
|
||||||
|
rationale: reason
|
||||||
|
confidence: 0.95
|
||||||
|
- id: D-002
|
||||||
|
decision: Medium confidence decision
|
||||||
|
rationale: reason
|
||||||
|
confidence: 0.70
|
||||||
|
- id: D-003
|
||||||
|
decision: Low confidence decision
|
||||||
|
rationale: reason
|
||||||
|
confidence: 0.40
|
||||||
|
---/ci---`;
|
||||||
|
execSync(`git commit --allow-empty -m "${ciBlock.replace(/"/g, '\\"')}"`, { cwd: tempDir, stdio: "pipe" });
|
||||||
|
|
||||||
|
const summary = getAuditSummary(tempDir);
|
||||||
|
expect(summary.total_decisions).toBe(3);
|
||||||
|
expect(summary.decisions_by_confidence.high).toBe(1);
|
||||||
|
expect(summary.decisions_by_confidence.medium).toBe(1);
|
||||||
|
expect(summary.decisions_by_confidence.low).toBe(1);
|
||||||
|
expect(summary.phases).toContain(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reads escalations from ci blocks", () => {
|
||||||
|
const ciBlock = `escalation(P01): test escalation
|
||||||
|
|
||||||
|
---ci---
|
||||||
|
project: ci
|
||||||
|
phase: 1
|
||||||
|
milestone: v0.8
|
||||||
|
escalations:
|
||||||
|
- type: irreversible_action
|
||||||
|
description: Deploy to production
|
||||||
|
---/ci---`;
|
||||||
|
execSync(`git commit --allow-empty -m "${ciBlock.replace(/"/g, '\\"')}"`, { cwd: tempDir, stdio: "pipe" });
|
||||||
|
|
||||||
|
const summary = getAuditSummary(tempDir);
|
||||||
|
expect(summary.total_escalations).toBe(1);
|
||||||
|
expect(summary.escalations_by_type.irreversible_action).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+90
-63
@@ -1,7 +1,7 @@
|
|||||||
import * as fs from "node:fs";
|
import { execSync } from "node:child_process";
|
||||||
import * as path from "node:path";
|
|
||||||
import { Decision } from "../types/decisions.js";
|
import { Decision } from "../types/decisions.js";
|
||||||
import { Escalation } from "../types/escalation.js";
|
import { Escalation } from "../types/escalation.js";
|
||||||
|
import { confidenceToLevel } from "../types/decisions.js";
|
||||||
|
|
||||||
export interface AuditEntry {
|
export interface AuditEntry {
|
||||||
phase: number;
|
phase: number;
|
||||||
@@ -9,41 +9,15 @@ export interface AuditEntry {
|
|||||||
escalations: Escalation[];
|
escalations: Escalation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const AUDIT_DIR = "audit";
|
|
||||||
|
|
||||||
function getAuditDir(projectPath: string): string {
|
|
||||||
return path.join(projectPath, ".ciagent", AUDIT_DIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAuditFilePath(projectPath: string, phase: number): string {
|
|
||||||
const date = new Date().toISOString().split("T")[0];
|
|
||||||
return path.join(getAuditDir(projectPath), `${date}-phase${phase}-decisions.json`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureAuditDir(projectPath: string): void {
|
|
||||||
const dir = getAuditDir(projectPath);
|
|
||||||
if (!fs.existsSync(dir)) {
|
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logDecision(
|
export function logDecision(
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
phase: number,
|
phase: number,
|
||||||
decision: Decision
|
decision: Decision
|
||||||
): void {
|
): void {
|
||||||
ensureAuditDir(projectPath);
|
console.warn(
|
||||||
const filePath = getAuditFilePath(projectPath, phase);
|
`[DEPRECATED] logDecision() is a no-op. Decisions are now committed to git via ---ci--- blocks. ` +
|
||||||
let entry: AuditEntry;
|
`Read audit data with readAudit() or getAuditSummary() which derive from git log.`
|
||||||
|
);
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
entry = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
||||||
} else {
|
|
||||||
entry = { phase, decisions: [], escalations: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.decisions.push(decision);
|
|
||||||
fs.writeFileSync(filePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logEscalation(
|
export function logEscalation(
|
||||||
@@ -51,41 +25,20 @@ export function logEscalation(
|
|||||||
phase: number,
|
phase: number,
|
||||||
escalation: Escalation
|
escalation: Escalation
|
||||||
): void {
|
): void {
|
||||||
ensureAuditDir(projectPath);
|
console.warn(
|
||||||
const filePath = getAuditFilePath(projectPath, phase);
|
`[DEPRECATED] logEscalation() is a no-op. Escalations are now committed to git via ---ci--- blocks. ` +
|
||||||
let entry: AuditEntry;
|
`Read audit data with readAudit() or getAuditSummary() which derive from git log.`
|
||||||
|
);
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
entry = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
||||||
} else {
|
|
||||||
entry = { phase, decisions: [], escalations: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.escalations.push(escalation);
|
|
||||||
fs.writeFileSync(filePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readAudit(
|
export function readAudit(
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
phase?: number
|
phase?: number
|
||||||
): AuditEntry[] {
|
): AuditEntry[] {
|
||||||
const auditDir = getAuditDir(projectPath);
|
const entries = readAuditFromGit(projectPath);
|
||||||
if (!fs.existsSync(auditDir)) return [];
|
if (phase !== undefined) {
|
||||||
|
return entries.filter((e) => e.phase === phase);
|
||||||
const files = fs
|
|
||||||
.readdirSync(auditDir)
|
|
||||||
.filter((f) => f.endsWith("-decisions.json"))
|
|
||||||
.sort();
|
|
||||||
|
|
||||||
const entries: AuditEntry[] = [];
|
|
||||||
for (const file of files) {
|
|
||||||
const content = fs.readFileSync(path.join(auditDir, file), "utf-8");
|
|
||||||
const entry: AuditEntry = JSON.parse(content);
|
|
||||||
if (phase === undefined || entry.phase === phase) {
|
|
||||||
entries.push(entry);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +49,7 @@ export function getAuditSummary(projectPath: string): {
|
|||||||
decisions_by_confidence: Record<string, number>;
|
decisions_by_confidence: Record<string, number>;
|
||||||
escalations_by_type: Record<string, number>;
|
escalations_by_type: Record<string, number>;
|
||||||
} {
|
} {
|
||||||
const entries = readAudit(projectPath);
|
const entries = readAuditFromGit(projectPath);
|
||||||
let total_decisions = 0;
|
let total_decisions = 0;
|
||||||
let total_escalations = 0;
|
let total_escalations = 0;
|
||||||
const phases = new Set<number>();
|
const phases = new Set<number>();
|
||||||
@@ -113,8 +66,7 @@ export function getAuditSummary(projectPath: string): {
|
|||||||
total_escalations += entry.escalations.length;
|
total_escalations += entry.escalations.length;
|
||||||
|
|
||||||
for (const d of entry.decisions) {
|
for (const d of entry.decisions) {
|
||||||
const level =
|
const level = confidenceToLevel(d.confidence);
|
||||||
d.confidence > 0.85 ? "high" : d.confidence >= 0.6 ? "medium" : "low";
|
|
||||||
decisions_by_confidence[level]++;
|
decisions_by_confidence[level]++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,3 +84,78 @@ export function getAuditSummary(projectPath: string): {
|
|||||||
escalations_by_type,
|
escalations_by_type,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readAuditFromGit(projectPath: string): AuditEntry[] {
|
||||||
|
try {
|
||||||
|
const raw = execSync(
|
||||||
|
`git log --all --max-count=200 --format="%B%x01"`,
|
||||||
|
{ cwd: projectPath, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 10000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const phaseMap = new Map<number, AuditEntry>();
|
||||||
|
const entries = raw.split("\x01").filter(Boolean);
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const ciBlockMatch = entry.match(/---ci---[\s\S]*?---\/ci---/);
|
||||||
|
if (!ciBlockMatch) continue;
|
||||||
|
|
||||||
|
const phaseMatch = ciBlockMatch[0].match(/phase:\s*(\d+)/);
|
||||||
|
if (!phaseMatch) continue;
|
||||||
|
const phase = parseInt(phaseMatch[1]);
|
||||||
|
|
||||||
|
if (!phaseMap.has(phase)) {
|
||||||
|
phaseMap.set(phase, { phase, decisions: [], escalations: [] });
|
||||||
|
}
|
||||||
|
const auditEntry = phaseMap.get(phase)!;
|
||||||
|
|
||||||
|
const decisionsMatch = ciBlockMatch[0].match(/decisions:\s*\n([\s\S]*?)(?=\n[a-z]|---\/ci---)/);
|
||||||
|
if (decisionsMatch) {
|
||||||
|
const idMatches = [...decisionsMatch[1].matchAll(/id:\s*(D-\d+)/g)];
|
||||||
|
const decMatches = [...decisionsMatch[1].matchAll(/decision:\s*(.+)/g)];
|
||||||
|
const ratMatches = [...decisionsMatch[1].matchAll(/rationale:\s*(.+)/g)];
|
||||||
|
const confMatches = [...decisionsMatch[1].matchAll(/confidence:\s*([0-9.]+)/g)];
|
||||||
|
const catMatches = [...decisionsMatch[1].matchAll(/category:\s*(.+)/g)];
|
||||||
|
|
||||||
|
for (let i = 0; i < idMatches.length; i++) {
|
||||||
|
auditEntry.decisions.push({
|
||||||
|
id: idMatches[i]?.[1] || "D-000",
|
||||||
|
decision: decMatches[i]?.[1]?.trim() || "",
|
||||||
|
rationale: ratMatches[i]?.[1]?.trim() || "",
|
||||||
|
confidence: parseFloat(confMatches[i]?.[1] || "0.5"),
|
||||||
|
category: (catMatches[i]?.[1]?.trim() as Decision["category"]) || "general",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
alternatives_considered: [],
|
||||||
|
human_override: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const escMatch = ciBlockMatch[0].match(/escalations:\s*\n([\s\S]*?)(?=\n[a-z]|---\/ci---)/);
|
||||||
|
if (escMatch) {
|
||||||
|
const escEntries = escMatch[1].split(/-\s*/).filter(Boolean);
|
||||||
|
for (const escLine of escEntries) {
|
||||||
|
const typeMatch = escLine.match(/type:\s*(\S+)/);
|
||||||
|
const descMatch = escLine.match(/description:\s*(.+)/);
|
||||||
|
if (typeMatch) {
|
||||||
|
auditEntry.escalations.push({
|
||||||
|
id: "E-000",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
type: typeMatch[1] as Escalation["type"],
|
||||||
|
phase: String(phase),
|
||||||
|
description: descMatch?.[1]?.trim() || "",
|
||||||
|
context: "",
|
||||||
|
options: [],
|
||||||
|
default_option_id: "",
|
||||||
|
resolution: "pending",
|
||||||
|
commit_hash: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...phaseMap.values()];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,7 +66,7 @@ export class EscalationProtocol {
|
|||||||
options: input.options,
|
options: input.options,
|
||||||
default_option_id: input.default_option_id,
|
default_option_id: input.default_option_id,
|
||||||
resolution: "pending",
|
resolution: "pending",
|
||||||
audit_file: `.ciagent/audit/deprecated`,
|
commit_hash: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
this.pendingEscalations.set(id, escalation);
|
this.pendingEscalations.set(id, escalation);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export interface Escalation {
|
|||||||
resolution: EscalationResolution;
|
resolution: EscalationResolution;
|
||||||
resolved_at?: string;
|
resolved_at?: string;
|
||||||
resolution_detail?: string;
|
resolution_detail?: string;
|
||||||
audit_file: string;
|
commit_hash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EscalationResult {
|
export interface EscalationResult {
|
||||||
|
|||||||
Reference in New Issue
Block a user