04c4489e70
---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.
161 lines
5.0 KiB
TypeScript
161 lines
5.0 KiB
TypeScript
import { execSync } from "node:child_process";
|
|
import { Decision } from "../types/decisions.js";
|
|
import { Escalation } from "../types/escalation.js";
|
|
import { confidenceToLevel } from "../types/decisions.js";
|
|
|
|
export interface AuditEntry {
|
|
phase: number;
|
|
decisions: Decision[];
|
|
escalations: Escalation[];
|
|
}
|
|
|
|
export function logDecision(
|
|
projectPath: string,
|
|
phase: number,
|
|
decision: Decision
|
|
): void {
|
|
console.warn(
|
|
`[DEPRECATED] logDecision() is a no-op. Decisions are now committed to git via ---ci--- blocks. ` +
|
|
`Read audit data with readAudit() or getAuditSummary() which derive from git log.`
|
|
);
|
|
}
|
|
|
|
export function logEscalation(
|
|
projectPath: string,
|
|
phase: number,
|
|
escalation: Escalation
|
|
): void {
|
|
console.warn(
|
|
`[DEPRECATED] logEscalation() is a no-op. Escalations are now committed to git via ---ci--- blocks. ` +
|
|
`Read audit data with readAudit() or getAuditSummary() which derive from git log.`
|
|
);
|
|
}
|
|
|
|
export function readAudit(
|
|
projectPath: string,
|
|
phase?: number
|
|
): AuditEntry[] {
|
|
const entries = readAuditFromGit(projectPath);
|
|
if (phase !== undefined) {
|
|
return entries.filter((e) => e.phase === phase);
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
export function getAuditSummary(projectPath: string): {
|
|
total_decisions: number;
|
|
total_escalations: number;
|
|
phases: number[];
|
|
decisions_by_confidence: Record<string, number>;
|
|
escalations_by_type: Record<string, number>;
|
|
} {
|
|
const entries = readAuditFromGit(projectPath);
|
|
let total_decisions = 0;
|
|
let total_escalations = 0;
|
|
const phases = new Set<number>();
|
|
const decisions_by_confidence: Record<string, number> = {
|
|
high: 0,
|
|
medium: 0,
|
|
low: 0,
|
|
};
|
|
const escalations_by_type: Record<string, number> = {};
|
|
|
|
for (const entry of entries) {
|
|
phases.add(entry.phase);
|
|
total_decisions += entry.decisions.length;
|
|
total_escalations += entry.escalations.length;
|
|
|
|
for (const d of entry.decisions) {
|
|
const level = confidenceToLevel(d.confidence);
|
|
decisions_by_confidence[level]++;
|
|
}
|
|
|
|
for (const e of entry.escalations) {
|
|
escalations_by_type[e.type] =
|
|
(escalations_by_type[e.type] || 0) + 1;
|
|
}
|
|
}
|
|
|
|
return {
|
|
total_decisions,
|
|
total_escalations,
|
|
phases: [...phases],
|
|
decisions_by_confidence,
|
|
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 [];
|
|
}
|
|
} |