ai-foreman 1.7.11 → 1.7.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/activity.d.ts +5 -1
- package/dist/activity.js +1 -1
- package/dist/adapters/claude.d.ts +7 -0
- package/dist/adapters/claude.js +133 -7
- package/dist/adapters/codex.d.ts +5 -0
- package/dist/adapters/codex.js +121 -11
- package/dist/adapters/recovering.d.ts +2 -0
- package/dist/adapters/recovering.js +11 -1
- package/dist/adapters/types.d.ts +47 -1
- package/dist/agentRun.d.ts +2 -0
- package/dist/agentRun.js +8 -2
- package/dist/branch/resume.js +11 -1
- package/dist/branch/runner.d.ts +12 -3
- package/dist/branch/runner.js +131 -67
- package/dist/buildRuns.d.ts +6 -2
- package/dist/buildRuns.js +76 -32
- package/dist/cli/doctor.d.ts +7 -0
- package/dist/cli/doctor.js +84 -1
- package/dist/cli/manager.d.ts +17 -0
- package/dist/cli/manager.js +135 -0
- package/dist/cli/recovery.d.ts +4 -0
- package/dist/cli/recovery.js +80 -0
- package/dist/cli/start.js +446 -142
- package/dist/cli/status.js +13 -2
- package/dist/cli/tickets.js +27 -9
- package/dist/config.d.ts +4 -0
- package/dist/config.js +28 -1
- package/dist/continuity.js +14 -9
- package/dist/diagnostics.d.ts +25 -0
- package/dist/diagnostics.js +428 -0
- package/dist/foreman.d.ts +26 -6
- package/dist/foreman.js +147 -104
- package/dist/handoffs.d.ts +35 -3
- package/dist/handoffs.js +155 -17
- package/dist/index.js +6 -0
- package/dist/log.d.ts +3 -3
- package/dist/log.js +4 -1
- package/dist/managerPacket.d.ts +25 -0
- package/dist/managerPacket.js +83 -0
- package/dist/markers.d.ts +1 -1
- package/dist/markers.js +4 -1
- package/dist/notify.d.ts +4 -0
- package/dist/notify.js +15 -0
- package/dist/observability.d.ts +248 -0
- package/dist/observability.js +1018 -0
- package/dist/planChanges.d.ts +4 -0
- package/dist/planChanges.js +16 -0
- package/dist/projectDiagnostics.d.ts +27 -0
- package/dist/projectDiagnostics.js +407 -0
- package/dist/qaRecovery.d.ts +140 -0
- package/dist/qaRecovery.js +595 -0
- package/dist/qaReview.d.ts +133 -14
- package/dist/qaReview.js +984 -59
- package/dist/qaSnapshot.d.ts +31 -5
- package/dist/qaSnapshot.js +209 -134
- package/dist/recoveryPolicy.d.ts +37 -0
- package/dist/recoveryPolicy.js +251 -0
- package/dist/roles.js +1 -0
- package/dist/supervisor.d.ts +40 -0
- package/dist/supervisor.js +102 -0
- package/dist/ticketHistoryReader.d.ts +19 -0
- package/dist/ticketHistoryReader.js +58 -0
- package/dist/tickets/details.d.ts +8 -0
- package/dist/tickets/details.js +59 -11
- package/dist/tickets/output.d.ts +5 -0
- package/dist/tickets/output.js +73 -0
- package/dist/tickets/stateDb.d.ts +1 -0
- package/dist/tickets/stateDb.js +5 -0
- package/dist/workflowDb.d.ts +31 -2
- package/dist/workflowDb.js +192 -23
- package/dist/workflowReader.d.ts +42 -0
- package/dist/workflowReader.js +171 -0
- package/foreman.yaml +13 -0
- package/package.json +51 -3
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type PlanChangeClassification = "bookkeeping" | "material";
|
|
2
|
+
/** Structural classification; agent prose is intentionally ignored. */
|
|
3
|
+
export declare function classifyPlanChange(before: unknown, after: unknown): PlanChangeClassification;
|
|
4
|
+
export declare function changedPaths(before: unknown, after: unknown, prefix?: string[]): string[][];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const MATERIAL_KEYS = new Set(["scope", "summary", "acceptance", "acceptance_criteria", "depends_on", "dependencies", "delivery", "delivery_strategy", "branch", "branch_mode", "completion", "completion_mode"]);
|
|
2
|
+
const BOOKKEEPING_KEYS = new Set(["status", "evidence", "updated_at", "updatedAt", "timestamp", "notes"]);
|
|
3
|
+
/** Structural classification; agent prose is intentionally ignored. */
|
|
4
|
+
export function classifyPlanChange(before, after) {
|
|
5
|
+
return changedPaths(before, after).some((path) => path.some((segment) => MATERIAL_KEYS.has(segment))) ? "material" : "bookkeeping";
|
|
6
|
+
}
|
|
7
|
+
export function changedPaths(before, after, prefix = []) {
|
|
8
|
+
if (Object.is(before, after))
|
|
9
|
+
return [];
|
|
10
|
+
if (!isRecord(before) || !isRecord(after))
|
|
11
|
+
return [prefix];
|
|
12
|
+
const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
13
|
+
const paths = [...keys].flatMap((key) => changedPaths(before[key], after[key], [...prefix, key]));
|
|
14
|
+
return paths.filter((path) => !path.length || !path.every((segment) => BOOKKEEPING_KEYS.has(segment)));
|
|
15
|
+
}
|
|
16
|
+
function isRecord(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); }
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ManagerAggregateQueryV1, ManagerAggregateResultV1, ManagerEvidenceRequestV1, ManagerEvidenceResponseV1, ManagerProjectDiagnosticReportV1, ManagerRunSummaryV1 } from "rafi-spec";
|
|
2
|
+
export declare const MANAGER_CATALOG_LIMIT = 30;
|
|
3
|
+
export declare const MANAGER_LOOKUP_MAX_ROUNDS = 2;
|
|
4
|
+
export declare const MANAGER_LOOKUP_MAX_OPERATIONS = 6;
|
|
5
|
+
export declare const MANAGER_LOOKUP_MAX_DETAIL_RUNS = 5;
|
|
6
|
+
export declare const MANAGER_LOOKUP_MAX_LIST_ROWS = 50;
|
|
7
|
+
export interface CollectManagerProjectOptions {
|
|
8
|
+
initialFocusRunId?: string;
|
|
9
|
+
currentFocusRunId?: string;
|
|
10
|
+
referencedRunIds?: string[];
|
|
11
|
+
question?: string;
|
|
12
|
+
now?: Date;
|
|
13
|
+
maxCatalog?: number;
|
|
14
|
+
external?: "auto" | "on" | "off";
|
|
15
|
+
}
|
|
16
|
+
export interface ManagerProjectCollection {
|
|
17
|
+
report: ManagerProjectDiagnosticReportV1;
|
|
18
|
+
allSummaries: ManagerRunSummaryV1[];
|
|
19
|
+
}
|
|
20
|
+
export declare function collectManagerProjectDiagnostics(projectDir: string, options?: CollectManagerProjectOptions): ManagerProjectCollection;
|
|
21
|
+
export declare function aggregateManagerRuns(summaries: readonly ManagerRunSummaryV1[], query: ManagerAggregateQueryV1): ManagerAggregateResultV1;
|
|
22
|
+
export declare function executeManagerEvidenceRequest(projectDir: string, request: ManagerEvidenceRequestV1, collection: ManagerProjectCollection, lookupRound: number): ManagerEvidenceResponseV1;
|
|
23
|
+
export declare function parseManagerEvidenceRequest(text: string): ManagerEvidenceRequestV1 | undefined;
|
|
24
|
+
export declare function resolveManagerQuestionRuns(summaries: readonly ManagerRunSummaryV1[], question: string, currentFocus: string, priorReferences?: readonly string[]): {
|
|
25
|
+
focusRunId: string;
|
|
26
|
+
referencedRunIds: string[];
|
|
27
|
+
};
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { hostname } from "node:os";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { BUILD_LEASE_STALE_MS, readBuildRuns } from "./buildRuns.js";
|
|
5
|
+
import { collectManagerDiagnostics } from "./diagnostics.js";
|
|
6
|
+
import { diagnosticDigest, ObservabilityReader } from "./observability.js";
|
|
7
|
+
import { WorkflowReader } from "./workflowReader.js";
|
|
8
|
+
export const MANAGER_CATALOG_LIMIT = 30;
|
|
9
|
+
export const MANAGER_LOOKUP_MAX_ROUNDS = 2;
|
|
10
|
+
export const MANAGER_LOOKUP_MAX_OPERATIONS = 6;
|
|
11
|
+
export const MANAGER_LOOKUP_MAX_DETAIL_RUNS = 5;
|
|
12
|
+
export const MANAGER_LOOKUP_MAX_LIST_ROWS = 50;
|
|
13
|
+
const AGGREGATE_METRICS = [
|
|
14
|
+
"calendarMs", "activeExecutionMs", "pausedOfflineMs", "explicitWaitMs", "attributedMs", "unattributedMs",
|
|
15
|
+
"observedRetryMs", "reportedRetryDelayMs", "qaAttempts", "qaFailures", "fixes", "retries", "tools",
|
|
16
|
+
"providerTurns", "waits", "executions", "inputTokens", "outputTokens", "totalTokens", "authoritativeCostUsd",
|
|
17
|
+
];
|
|
18
|
+
const AGGREGATE_OPERATIONS = ["sum", "count", "minimum", "maximum", "average", "median", "p75", "p90"];
|
|
19
|
+
export function collectManagerProjectDiagnostics(projectDir, options = {}) {
|
|
20
|
+
const root = resolve(projectDir);
|
|
21
|
+
const now = options.now ?? new Date();
|
|
22
|
+
const buildRuns = readBuildRuns(root);
|
|
23
|
+
const observability = new ObservabilityReader(root);
|
|
24
|
+
const workflow = new WorkflowReader(root);
|
|
25
|
+
try {
|
|
26
|
+
const stored = observability.runSummaries({ limit: 10_000 });
|
|
27
|
+
const rollups = observability.rollups();
|
|
28
|
+
const workflowRuns = workflow.buildRuns();
|
|
29
|
+
const lease = workflow.currentLease();
|
|
30
|
+
const activeRunId = verifiedActiveRunId(lease, now) ?? buildRuns.find(run => verifiedSnapshotLease(run, now))?.runId;
|
|
31
|
+
const buildById = new Map(buildRuns.map(run => [run.runId, run]));
|
|
32
|
+
const storedById = new Map(stored.map(summary => [summary.runId, summary]));
|
|
33
|
+
const rollupById = new Map(rollups.map(rollup => [rollup.runId, rollup]));
|
|
34
|
+
const workflowById = new Map(workflowRuns.map(run => [run.runId, run]));
|
|
35
|
+
const ids = new Set([...buildById.keys(), ...storedById.keys(), ...rollupById.keys(), ...workflowById.keys()]);
|
|
36
|
+
if (!ids.size)
|
|
37
|
+
throw new Error("no build runs found for this project; start one with `rafi start` before opening Manager");
|
|
38
|
+
const defaultFocus = activeRunId && ids.has(activeRunId)
|
|
39
|
+
? activeRunId
|
|
40
|
+
: [...ids].sort((a, b) => runUpdatedAt(b, buildById, storedById, workflowById, rollupById).localeCompare(runUpdatedAt(a, buildById, storedById, workflowById, rollupById)))[0];
|
|
41
|
+
const initialFocus = options.initialFocusRunId ?? defaultFocus;
|
|
42
|
+
if (!ids.has(initialFocus))
|
|
43
|
+
throw new Error(`build run not found: ${initialFocus}`);
|
|
44
|
+
const currentFocus = options.currentFocusRunId ?? initialFocus;
|
|
45
|
+
if (!ids.has(currentFocus))
|
|
46
|
+
throw new Error(`build run not found: ${currentFocus}`);
|
|
47
|
+
const summaries = [...ids].map(runId => mergeSummary({ runId, build: buildById.get(runId), stored: storedById.get(runId), rollup: rollupById.get(runId), workflow: workflowById.get(runId), activeRunId, leaseRunId: lease?.runId, now, schemaVersion: observability.schemaVersion() }))
|
|
48
|
+
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || b.createdAt.localeCompare(a.createdAt) || a.runId.localeCompare(b.runId));
|
|
49
|
+
const detailIds = [...new Set([currentFocus, activeRunId, ...(options.referencedRunIds ?? [])].filter((id) => Boolean(id && buildById.has(id))))].slice(0, 5);
|
|
50
|
+
const focusedReports = detailIds.map(runId => collectManagerDiagnostics(root, { runId, question: options.question, external: runId === currentFocus ? options.external ?? "off" : "off", now, maxDetailSpans: 50 }));
|
|
51
|
+
for (const report of focusedReports) {
|
|
52
|
+
const index = summaries.findIndex(item => item.runId === report.runId);
|
|
53
|
+
if (index >= 0)
|
|
54
|
+
summaries[index] = mergeDetailedReport(summaries[index], report);
|
|
55
|
+
}
|
|
56
|
+
const allRunsQuery = { version: 1, metrics: AGGREGATE_METRICS, operations: AGGREGATE_OPERATIONS };
|
|
57
|
+
const successfulQuery = { version: 1, filters: { statuses: ["completed"] }, metrics: AGGREGATE_METRICS, operations: AGGREGATE_OPERATIONS };
|
|
58
|
+
const allRuns = aggregateManagerRuns(summaries, allRunsQuery);
|
|
59
|
+
const successfulCompletedRuns = aggregateManagerRuns(summaries, successfulQuery);
|
|
60
|
+
const topRuns = {
|
|
61
|
+
activeTime: rank(summaries, "activeExecutionMs"), waitTime: rank(summaries, "explicitWaitMs"),
|
|
62
|
+
qaTime: rankDerived(summaries, item => item.metricCoverage.timing === "unavailable" && !Object.keys(item.timing.inclusiveByKind).length ? undefined : sumKinds(item.timing.inclusiveByKind, ["qa_attempt", "qa_fix", "fix"])),
|
|
63
|
+
retryTime: rank(summaries, "observedRetryMs"), unattributedTime: rank(summaries, "unattributedMs"),
|
|
64
|
+
};
|
|
65
|
+
const recent = summaries.slice(0, 20);
|
|
66
|
+
const relevant = [...topRuns.activeTime, ...topRuns.waitTime, ...topRuns.qaTime, ...topRuns.retryTime, ...topRuns.unattributedTime]
|
|
67
|
+
.map(item => summaries.find(summary => summary.runId === item.runId)).filter((item) => Boolean(item)).slice(0, 10);
|
|
68
|
+
const preferred = [summaries.find(item => item.runId === activeRunId), summaries.find(item => item.runId === currentFocus), ...recent, ...relevant]
|
|
69
|
+
.filter((item) => Boolean(item));
|
|
70
|
+
const runCatalog = dedupe(preferred).slice(0, Math.max(1, options.maxCatalog ?? MANAGER_CATALOG_LIMIT));
|
|
71
|
+
const statusDistribution = distribution(summaries.map(item => item.status));
|
|
72
|
+
const capabilityDistribution = distribution(summaries.flatMap(item => [`detail:${item.detailLevel}`, ...Object.values(item.capabilities.sources).map(source => `${source.source}:${source.state}`)]));
|
|
73
|
+
const sourceCoverage = coverageForAllMetrics(summaries);
|
|
74
|
+
const staleRecoveryRunIds = summaries.filter(item => item.activeState === "stale_recovery").map(item => item.runId);
|
|
75
|
+
const completedDates = summaries.filter(item => item.status === "completed").map(item => item.completedAt).filter((item) => Boolean(item)).sort();
|
|
76
|
+
const createdDates = summaries.map(item => item.createdAt).sort();
|
|
77
|
+
const projectBasis = { ids: summaries.map(item => [item.runId, item.digest]), activeRunId, initialFocus, currentFocus };
|
|
78
|
+
const withoutDigest = {
|
|
79
|
+
version: 1, generatedAt: now.toISOString(), projectDigest: diagnosticDigest(projectBasis), totalRunCount: summaries.length,
|
|
80
|
+
dateRange: { earliestCreatedAt: createdDates[0], latestCreatedAt: createdDates.at(-1), ...(completedDates.length ? { latestCompletedAt: completedDates.at(-1) } : {}) },
|
|
81
|
+
statusDistribution, capabilityDistribution, ...(activeRunId ? { verifiedActiveRunId: activeRunId } : {}), staleRecoveryRunIds,
|
|
82
|
+
initialFocusRunId: initialFocus, currentFocusRunId: currentFocus, allRuns, successfulCompletedRuns, topRuns,
|
|
83
|
+
runCatalog, omittedRunCount: Math.max(0, summaries.length - runCatalog.length), focusedReports,
|
|
84
|
+
findings: crossRunFindings(summaries, successfulCompletedRuns), sourceCoverage,
|
|
85
|
+
};
|
|
86
|
+
return { report: { ...withoutDigest, digest: diagnosticDigest(withoutDigest) }, allSummaries: summaries };
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
observability.close();
|
|
90
|
+
workflow.close();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function aggregateManagerRuns(summaries, query) {
|
|
94
|
+
const filtered = summaries.filter(summary => matches(summary, query.filters));
|
|
95
|
+
const dimensions = [...new Set(query.groupBy ?? [])];
|
|
96
|
+
const groups = new Map();
|
|
97
|
+
for (const summary of filtered) {
|
|
98
|
+
const keys = groupKeys(summary, dimensions);
|
|
99
|
+
for (const key of keys) {
|
|
100
|
+
const digest = JSON.stringify(key);
|
|
101
|
+
groups.set(digest, [...(groups.get(digest) ?? []), summary]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!groups.size && !dimensions.length)
|
|
105
|
+
groups.set("{}", []);
|
|
106
|
+
const rows = [...groups.entries()].map(([encoded, runs]) => {
|
|
107
|
+
const values = {};
|
|
108
|
+
const coverage = {};
|
|
109
|
+
for (const metric of [...new Set(query.metrics)]) {
|
|
110
|
+
const measured = runs.map(item => metricValue(item, metric)).filter((value) => value !== undefined && Number.isFinite(value));
|
|
111
|
+
coverage[metric] = metricCoverage(runs.length, measured.length);
|
|
112
|
+
const sorted = measured.sort((a, b) => a - b);
|
|
113
|
+
const operations = {};
|
|
114
|
+
if (sorted.length)
|
|
115
|
+
for (const operation of [...new Set(query.operations)])
|
|
116
|
+
operations[operation] = aggregateOperation(sorted, operation);
|
|
117
|
+
values[metric] = operations;
|
|
118
|
+
}
|
|
119
|
+
const evidenceIds = runs.slice(0, 100).map(item => `run:${item.runId}`);
|
|
120
|
+
return { key: JSON.parse(encoded), runCount: runs.length, values, coverage, evidenceIds, ...(runs.length > evidenceIds.length ? { omittedEvidenceCount: runs.length - evidenceIds.length } : {}) };
|
|
121
|
+
}).sort((a, b) => JSON.stringify(a.key).localeCompare(JSON.stringify(b.key)));
|
|
122
|
+
const bounded = rows.slice(0, 200);
|
|
123
|
+
const basis = { version: 1, query, matchedRunCount: filtered.length, groups: bounded, omittedGroupCount: Math.max(0, rows.length - bounded.length) };
|
|
124
|
+
return { ...basis, digest: diagnosticDigest(basis) };
|
|
125
|
+
}
|
|
126
|
+
export function executeManagerEvidenceRequest(projectDir, request, collection, lookupRound) {
|
|
127
|
+
const results = [];
|
|
128
|
+
const operations = request.operations.slice(0, MANAGER_LOOKUP_MAX_OPERATIONS);
|
|
129
|
+
for (const operation of operations) {
|
|
130
|
+
if (operation.kind === "list_runs") {
|
|
131
|
+
const filtered = collection.allSummaries.filter(item => matches(item, operation.filters));
|
|
132
|
+
const offset = decodeCursor(operation.cursor);
|
|
133
|
+
const limit = Math.max(1, Math.min(MANAGER_LOOKUP_MAX_LIST_ROWS, operation.limit ?? 20));
|
|
134
|
+
const rows = filtered.slice(offset, offset + limit);
|
|
135
|
+
results.push({ kind: operation.kind, status: operation.limit && operation.limit > limit ? "limited" : "ok", data: { runs: rows, total: filtered.length, omittedCount: Math.max(0, filtered.length - offset - rows.length), nextCursor: offset + rows.length < filtered.length ? String(offset + rows.length) : undefined }, ...(operation.limit && operation.limit > limit ? { limitation: `list limit reduced to ${limit}` } : {}) });
|
|
136
|
+
}
|
|
137
|
+
else if (operation.kind === "aggregate_runs") {
|
|
138
|
+
results.push({ kind: operation.kind, status: "ok", data: aggregateManagerRuns(collection.allSummaries, operation.query) });
|
|
139
|
+
}
|
|
140
|
+
else if (operation.kind === "compare_runs") {
|
|
141
|
+
const ids = [...new Set(operation.runIds)].slice(0, MANAGER_LOOKUP_MAX_DETAIL_RUNS);
|
|
142
|
+
const runs = ids.map(id => collection.allSummaries.find(item => item.runId === id)).filter((item) => Boolean(item));
|
|
143
|
+
results.push({ kind: operation.kind, status: runs.length === operation.runIds.length ? "ok" : "limited", data: { runs: runs.map(run => ({ runId: run.runId, status: run.status, values: Object.fromEntries(operation.metrics.map(metric => [metric, metricValue(run, metric)])), coverage: Object.fromEntries(operation.metrics.map(metric => [metric, metricValue(run, metric) === undefined ? "unavailable" : run.metricCoverage[metric] ?? "available"])), evidenceIds: [`run:${run.runId}`] })) }, ...(runs.length !== operation.runIds.length ? { limitation: "some requested runs were unavailable or over the five-run limit" } : {}) });
|
|
144
|
+
}
|
|
145
|
+
else if (operation.kind === "get_run_details") {
|
|
146
|
+
const ids = [...new Set(operation.runIds)].slice(0, MANAGER_LOOKUP_MAX_DETAIL_RUNS);
|
|
147
|
+
const reports = [];
|
|
148
|
+
const unavailable = [];
|
|
149
|
+
for (const runId of ids) {
|
|
150
|
+
try {
|
|
151
|
+
reports.push(collectManagerDiagnostics(projectDir, { runId, external: "off", maxDetailSpans: Math.max(1, Math.min(100, operation.maxSpans ?? 50)) }));
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
unavailable.push(runId);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const limited = operation.runIds.length > ids.length || unavailable.length > 0;
|
|
158
|
+
results.push({ kind: operation.kind, status: limited ? "limited" : "ok", data: { reports, unavailableRunIds: unavailable }, ...(limited ? { limitation: "details are limited to five retained build snapshots and available evidence" } : {}) });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (request.operations.length > operations.length)
|
|
162
|
+
results.push({ kind: request.operations[operations.length].kind, status: "limited", limitation: `only ${MANAGER_LOOKUP_MAX_OPERATIONS} operations are allowed per lookup round` });
|
|
163
|
+
const withoutDigest = { version: 1, requestId: request.requestId, results, lookupRound, remainingRounds: Math.max(0, MANAGER_LOOKUP_MAX_ROUNDS - lookupRound) };
|
|
164
|
+
return { ...withoutDigest, digest: diagnosticDigest(withoutDigest) };
|
|
165
|
+
}
|
|
166
|
+
export function parseManagerEvidenceRequest(text) {
|
|
167
|
+
const candidate = text.trim().match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i)?.[1] ?? text.trim();
|
|
168
|
+
if (!candidate.startsWith("{"))
|
|
169
|
+
return undefined;
|
|
170
|
+
let value;
|
|
171
|
+
try {
|
|
172
|
+
value = JSON.parse(candidate);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
if (!isRecord(value) || value.version !== 1 || typeof value.requestId !== "string" || !Array.isArray(value.operations) || value.operations.length < 1)
|
|
178
|
+
return undefined;
|
|
179
|
+
if (Object.keys(value).some(key => !["version", "requestId", "operations"].includes(key)))
|
|
180
|
+
return undefined;
|
|
181
|
+
if (!value.operations.every(validateOperation))
|
|
182
|
+
return undefined;
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
export function resolveManagerQuestionRuns(summaries, question, currentFocus, priorReferences = []) {
|
|
186
|
+
const lower = question.toLowerCase();
|
|
187
|
+
const ordered = [...summaries].sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.runId.localeCompare(b.runId));
|
|
188
|
+
const references = [];
|
|
189
|
+
for (const summary of summaries)
|
|
190
|
+
if (question.includes(summary.runId))
|
|
191
|
+
references.push(summary.runId);
|
|
192
|
+
if (/\bactive run\b/i.test(question))
|
|
193
|
+
references.push(...summaries.filter(item => item.activeState === "verified_active").map(item => item.runId));
|
|
194
|
+
if (/\blast completed\b/i.test(question)) {
|
|
195
|
+
const last = summaries.filter(item => item.status === "completed" && item.completedAt).sort((a, b) => b.completedAt.localeCompare(a.completedAt))[0];
|
|
196
|
+
if (last)
|
|
197
|
+
references.push(last.runId);
|
|
198
|
+
}
|
|
199
|
+
if (/\bprevious run\b/i.test(question)) {
|
|
200
|
+
const index = ordered.findIndex(item => item.runId === currentFocus);
|
|
201
|
+
if (index > 0)
|
|
202
|
+
references.push(ordered[index - 1].runId);
|
|
203
|
+
}
|
|
204
|
+
if (/\bthat run\b/i.test(question))
|
|
205
|
+
references.push(priorReferences.at(-1) ?? currentFocus);
|
|
206
|
+
const lastSuccessful = lower.match(/\blast\s+(\d+|one|two|three|four|five|six|seven|eight|nine|ten)\s+successful runs?\b/);
|
|
207
|
+
if (lastSuccessful)
|
|
208
|
+
references.push(...summaries.filter(item => item.status === "completed").sort((a, b) => (b.completedAt ?? b.updatedAt).localeCompare(a.completedAt ?? a.updatedAt)).slice(0, Math.min(50, numberWord(lastSuccessful[1]))).map(item => item.runId));
|
|
209
|
+
const unique = [...new Set(references.filter(Boolean))];
|
|
210
|
+
return { focusRunId: unique.length === 1 ? unique[0] : currentFocus, referencedRunIds: unique };
|
|
211
|
+
}
|
|
212
|
+
function mergeSummary(input) {
|
|
213
|
+
const { runId, build, stored, rollup, workflow, activeRunId, leaseRunId, now } = input;
|
|
214
|
+
const status = workflow?.status ?? build?.status ?? stored?.status ?? rollup?.status ?? "unknown";
|
|
215
|
+
const createdAt = build?.createdAt ?? workflow?.createdAt ?? stored?.createdAt ?? rollup?.createdAt ?? now.toISOString();
|
|
216
|
+
const updatedAt = build?.updatedAt ?? workflow?.updatedAt ?? stored?.updatedAt ?? rollup?.completedAt ?? rollup?.createdAt ?? createdAt;
|
|
217
|
+
const completedAt = build?.completedAt ?? stored?.completedAt ?? rollup?.completedAt;
|
|
218
|
+
const activeState = activeRunId === runId ? "verified_active" : leaseRunId === runId || ["running", "paused", "blocked"].includes(status) ? "stale_recovery" : ["recoverable", "interrupted"].includes(status) ? "recoverable" : "inactive";
|
|
219
|
+
const timing = stored?.timing ?? { calendarMs: rollup?.calendarMs ?? Math.max(0, Date.parse(completedAt ?? updatedAt) - Date.parse(createdAt)), activeExecutionMs: rollup?.activeExecutionMs, pausedOfflineMs: rollup ? Math.max(0, rollup.calendarMs - rollup.activeExecutionMs) : undefined, explicitWaitMs: rollup?.explicitWaitMs, attributedMs: rollup?.attributedMs, unattributedMs: rollup?.unattributedMs, inclusiveByKind: rollup?.totals ?? {}, exclusiveByKind: rollup?.totals ?? {} };
|
|
220
|
+
const sourceState = stored ? "available" : rollup ? "partial" : "unavailable";
|
|
221
|
+
const source = { source: stored ? "run_summary_v2" : rollup ? "run_rollup_v1" : "build_snapshot", state: sourceState, observedAt: now.toISOString(), ...(sourceState === "partial" ? { detail: "legacy rollup lacks some project summary fields" } : sourceState === "unavailable" ? { detail: "only build or recovery snapshot facts are available" } : {}) };
|
|
222
|
+
const conflicts = [
|
|
223
|
+
build && stored && build.status !== stored.status ? `status snapshot=${build.status},summary=${stored.status}` : undefined,
|
|
224
|
+
build?.builder?.settings.make && stored?.provider && build.builder.settings.make !== stored.provider ? `provider snapshot=${build.builder.settings.make},summary=${stored.provider}` : undefined,
|
|
225
|
+
build?.builder?.settings.model && stored?.model && build.builder.settings.model !== stored.model ? `model snapshot=${build.builder.settings.model},summary=${stored.model}` : undefined,
|
|
226
|
+
].filter((item) => Boolean(item));
|
|
227
|
+
const capabilities = stored?.capabilities ?? { version: 1, sources: { summary: source } };
|
|
228
|
+
if (conflicts.length)
|
|
229
|
+
capabilities.sources = { ...capabilities.sources, source_conflict: { source: "source_conflict", state: "partial", observedAt: now.toISOString(), detail: conflicts.join("; ") } };
|
|
230
|
+
const withoutDigest = {
|
|
231
|
+
version: 1, runId, status, createdAt, updatedAt, ...(completedAt ? { completedAt } : {}), ...(build?.checkpoint ?? workflow?.checkpoint ?? stored?.checkpoint ? { checkpoint: build?.checkpoint ?? workflow?.checkpoint ?? stored?.checkpoint } : {}), activeState,
|
|
232
|
+
ticketIds: build?.tickets ?? stored?.ticketIds ?? ticketsFromWorkflow(workflow), ...(build?.deliveryUnit ?? stored?.deliveryUnit ? { deliveryUnit: build?.deliveryUnit ?? stored?.deliveryUnit } : {}),
|
|
233
|
+
...(build?.branchMode ?? stored?.branchMode ?? rollup?.branchMode ? { branchMode: build?.branchMode ?? stored?.branchMode ?? rollup?.branchMode } : {}),
|
|
234
|
+
qaEnabled: build ? Boolean(build.qa) : stored?.qaEnabled ?? rollup?.qaEnabled,
|
|
235
|
+
...(build?.builder?.settings.make ?? stored?.provider ?? rollup?.primaryProvider ? { provider: build?.builder?.settings.make ?? stored?.provider ?? rollup?.primaryProvider } : {}),
|
|
236
|
+
...(build?.builder?.settings.model ?? stored?.model ? { model: build?.builder?.settings.model ?? stored?.model } : {}), timing,
|
|
237
|
+
counts: stored?.counts ?? { byKind: {}, byOutcome: {} }, usage: stored?.usage ?? { scope: "unavailable" }, retry: stored?.retry ?? {},
|
|
238
|
+
...(build?.failure?.category ?? build?.interruption?.category ?? stored?.failureCategory ? { failureCategory: build?.failure?.category ?? build?.interruption?.category ?? stored?.failureCategory } : {}),
|
|
239
|
+
topOperations: stored?.topOperations ?? [], ...(stored?.git ? { git: stored.git } : build ? { git: { branch: build.repository.git.branch ?? build.repository.branch, changedPathCount: build.repository.git.runOwnedPaths.length, historical: true } } : {}),
|
|
240
|
+
detailLevel: stored?.detailLevel ?? (rollup ? "rollup" : "legacy"),
|
|
241
|
+
metricCoverage: stored?.metricCoverage ?? { timing: rollup ? "partial" : "unavailable", counts: "unavailable", usage: "unavailable", git: build ? "partial" : "unavailable" },
|
|
242
|
+
capabilities, evidenceIds: stored?.evidenceIds ?? [`run:${runId}`],
|
|
243
|
+
};
|
|
244
|
+
return { ...withoutDigest, digest: diagnosticDigest(withoutDigest) };
|
|
245
|
+
}
|
|
246
|
+
function mergeDetailedReport(summary, report) {
|
|
247
|
+
const byKind = {};
|
|
248
|
+
const byOutcome = {};
|
|
249
|
+
for (const span of report.detail.spans) {
|
|
250
|
+
byKind[span.kind] = (byKind[span.kind] ?? 0) + 1;
|
|
251
|
+
const outcome = span.outcome ?? (span.endedAt ? "unknown" : "open");
|
|
252
|
+
byOutcome[outcome] = (byOutcome[outcome] ?? 0) + 1;
|
|
253
|
+
}
|
|
254
|
+
const measuredTiming = { ...summary.timing, calendarMs: report.timing.calendarAgeMs, activeExecutionMs: report.timing.activeExecutionMs, pausedOfflineMs: report.timing.pausedOfflineMs, explicitWaitMs: report.timing.explicitWaitMs, attributedMs: report.timing.attributedMs, unattributedMs: report.timing.unattributedMs, inclusiveByKind: report.timing.inclusiveByKind, exclusiveByKind: report.timing.exclusiveByKind };
|
|
255
|
+
const measuredCounts = { ...summary.counts, byKind: report.detail.omittedSpans ? summary.counts.byKind : byKind, byOutcome: report.detail.omittedSpans ? summary.counts.byOutcome : byOutcome, qaAttempts: report.counts.qaAttempts, qaFailures: report.counts.qaFailures, fixes: report.counts.fixes, retries: report.counts.retries, waits: report.counts.waits };
|
|
256
|
+
const withoutDigest = { ...summary, timing: report.legacy ? summary.timing : measuredTiming, counts: report.legacy ? summary.counts : measuredCounts, detailLevel: report.legacy ? summary.detailLevel : "detailed", capabilities: report.capabilities, evidenceIds: [...new Set([...summary.evidenceIds, ...report.evidence.map(item => item.evidenceId)])], metricCoverage: { ...summary.metricCoverage, timing: report.legacy ? summary.metricCoverage.timing ?? "partial" : "available", counts: report.legacy ? summary.metricCoverage.counts ?? "partial" : report.detail.omittedSpans ? "partial" : "available" } };
|
|
257
|
+
return { ...withoutDigest, digest: diagnosticDigest(withoutDigest) };
|
|
258
|
+
}
|
|
259
|
+
function verifiedActiveRunId(lease, now) {
|
|
260
|
+
if (!lease || now.getTime() - Date.parse(lease.heartbeatAt) > BUILD_LEASE_STALE_MS)
|
|
261
|
+
return undefined;
|
|
262
|
+
if (lease.host !== hostname())
|
|
263
|
+
return undefined;
|
|
264
|
+
try {
|
|
265
|
+
process.kill(lease.pid, 0);
|
|
266
|
+
const start = readFileSync(`/proc/${lease.pid}/stat`, "utf8").split(" ")[21];
|
|
267
|
+
return start === lease.processStart ? lease.runId : undefined;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function verifiedSnapshotLease(run, now) {
|
|
274
|
+
const lease = run.lease;
|
|
275
|
+
if (!lease || now.getTime() - Date.parse(lease.heartbeatAt) > BUILD_LEASE_STALE_MS || lease.hostname !== hostname())
|
|
276
|
+
return false;
|
|
277
|
+
try {
|
|
278
|
+
process.kill(lease.pid, 0);
|
|
279
|
+
return readFileSync(`/proc/${lease.pid}/stat`, "utf8").split(" ")[21] === lease.processStart;
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function runUpdatedAt(runId, builds, stored, workflows, rollups) { return builds.get(runId)?.updatedAt ?? workflows.get(runId)?.updatedAt ?? stored.get(runId)?.updatedAt ?? rollups.get(runId)?.completedAt ?? rollups.get(runId)?.createdAt ?? ""; }
|
|
286
|
+
function ticketsFromWorkflow(run) { const value = run?.originalWork; return Array.isArray(value?.tickets) ? value.tickets.filter((item) => typeof item === "string") : []; }
|
|
287
|
+
function rank(summaries, metric) { return rankDerived(summaries, item => metric === "observedRetryMs" ? item.retry.observedMs : item.timing[metric]); }
|
|
288
|
+
function rankDerived(summaries, get) { return summaries.map(item => ({ runId: item.runId, value: get(item), evidenceIds: [`run:${item.runId}`] })).filter((item) => item.value !== undefined).sort((a, b) => b.value - a.value || a.runId.localeCompare(b.runId)).slice(0, 5); }
|
|
289
|
+
function dedupe(summaries) { const seen = new Set(); return summaries.filter(item => !seen.has(item.runId) && Boolean(seen.add(item.runId))); }
|
|
290
|
+
function distribution(values) { const result = {}; for (const value of values)
|
|
291
|
+
result[value] = (result[value] ?? 0) + 1; return result; }
|
|
292
|
+
function sumKinds(values, kinds) { return kinds.reduce((sum, kind) => sum + (values[kind] ?? 0), 0); }
|
|
293
|
+
function metricCoverage(eligibleRuns, coveredRuns) { return { eligibleRuns, coveredRuns, missingRuns: eligibleRuns - coveredRuns, state: coveredRuns === 0 ? "unavailable" : coveredRuns === eligibleRuns ? "available" : "partial" }; }
|
|
294
|
+
function coverageForAllMetrics(summaries) { return Object.fromEntries(AGGREGATE_METRICS.map(metric => { const count = summaries.filter(item => metricValue(item, metric) !== undefined).length; return [metric, metricCoverage(summaries.length, count)]; })); }
|
|
295
|
+
function metricValue(summary, metric) {
|
|
296
|
+
if (["calendarMs", "activeExecutionMs", "pausedOfflineMs", "explicitWaitMs", "attributedMs", "unattributedMs"].includes(metric))
|
|
297
|
+
return summary.timing[metric];
|
|
298
|
+
if (metric === "observedRetryMs")
|
|
299
|
+
return summary.retry.observedMs;
|
|
300
|
+
if (metric === "reportedRetryDelayMs")
|
|
301
|
+
return summary.retry.reportedDelayMs;
|
|
302
|
+
if (["qaAttempts", "qaFailures", "fixes", "retries", "tools", "providerTurns", "waits", "executions"].includes(metric))
|
|
303
|
+
return summary.counts[metric];
|
|
304
|
+
return summary.usage[metric];
|
|
305
|
+
}
|
|
306
|
+
function aggregateOperation(sorted, operation) {
|
|
307
|
+
if (operation === "count")
|
|
308
|
+
return sorted.length;
|
|
309
|
+
if (!sorted.length)
|
|
310
|
+
return 0;
|
|
311
|
+
if (operation === "sum")
|
|
312
|
+
return sorted.reduce((a, b) => a + b, 0);
|
|
313
|
+
if (operation === "minimum")
|
|
314
|
+
return sorted[0];
|
|
315
|
+
if (operation === "maximum")
|
|
316
|
+
return sorted.at(-1);
|
|
317
|
+
if (operation === "average")
|
|
318
|
+
return sorted.reduce((a, b) => a + b, 0) / sorted.length;
|
|
319
|
+
return percentile(sorted, operation === "median" ? 0.5 : operation === "p75" ? 0.75 : 0.9);
|
|
320
|
+
}
|
|
321
|
+
function percentile(sorted, p) { if (!sorted.length)
|
|
322
|
+
return 0; const index = (sorted.length - 1) * p; const lower = Math.floor(index); const upper = Math.ceil(index); return sorted[lower] + (sorted[upper] - sorted[lower]) * (index - lower); }
|
|
323
|
+
function matches(summary, filters) {
|
|
324
|
+
if (!filters)
|
|
325
|
+
return true;
|
|
326
|
+
return (!filters.runIds?.length || filters.runIds.includes(summary.runId)) && (!filters.statuses?.length || filters.statuses.includes(summary.status))
|
|
327
|
+
&& (!filters.providers?.length || Boolean(summary.provider && filters.providers.includes(summary.provider))) && (!filters.models?.length || Boolean(summary.model && filters.models.includes(summary.model)))
|
|
328
|
+
&& (!filters.branchModes?.length || Boolean(summary.branchMode && filters.branchModes.includes(summary.branchMode))) && (filters.qaEnabled === undefined || summary.qaEnabled === filters.qaEnabled)
|
|
329
|
+
&& (!filters.ticketIds?.length || filters.ticketIds.some(ticket => summary.ticketIds.includes(ticket))) && (!filters.detailLevels?.length || filters.detailLevels.includes(summary.detailLevel))
|
|
330
|
+
&& (!filters.createdFrom || summary.createdAt >= filters.createdFrom) && (!filters.createdTo || summary.createdAt <= filters.createdTo)
|
|
331
|
+
&& (!filters.completedFrom || Boolean(summary.completedAt && summary.completedAt >= filters.completedFrom)) && (!filters.completedTo || Boolean(summary.completedAt && summary.completedAt <= filters.completedTo));
|
|
332
|
+
}
|
|
333
|
+
function groupKeys(summary, dimensions) {
|
|
334
|
+
if (!dimensions.length)
|
|
335
|
+
return [{}];
|
|
336
|
+
let keys = [{}];
|
|
337
|
+
for (const dimension of dimensions) {
|
|
338
|
+
const values = dimension === "ticket" ? (summary.ticketIds.length ? summary.ticketIds : ["unknown"]) : [dimension === "status" ? summary.status : dimension === "provider" ? summary.provider ?? "unknown" : dimension === "model" ? summary.model ?? "unknown" : dimension === "branchMode" ? summary.branchMode ?? "unknown" : dimension === "qaEnabled" ? summary.qaEnabled ?? "unknown" : summary.detailLevel];
|
|
339
|
+
keys = keys.flatMap(key => values.map(value => ({ ...key, [dimension]: value })));
|
|
340
|
+
}
|
|
341
|
+
return keys;
|
|
342
|
+
}
|
|
343
|
+
function crossRunFindings(summaries, baseline) {
|
|
344
|
+
const successful = summaries.filter(item => item.status === "completed");
|
|
345
|
+
const cohort = successful.filter(item => item.timing.activeExecutionMs !== undefined);
|
|
346
|
+
if (cohort.length < 5)
|
|
347
|
+
return [];
|
|
348
|
+
const p90 = baseline.groups[0]?.values.activeExecutionMs?.p90;
|
|
349
|
+
if (p90 === undefined)
|
|
350
|
+
return [];
|
|
351
|
+
const missingMetric = successful.length - cohort.length;
|
|
352
|
+
return cohort.filter(item => item.timing.activeExecutionMs > p90).map(item => ({ code: "project_duration_outlier", title: "Active time exceeds the successful-run p90", summary: `Run ${item.runId} has ${item.timing.activeExecutionMs}ms active time versus a ${p90}ms p90 across ${cohort.length} successful completed runs; ${missingMetric} successful run(s) lacked this metric and ${summaries.length - successful.length} non-successful/incomplete run(s) were outside the baseline.`, confidence: "derived", evidenceIds: [`run:${item.runId}`, ...baseline.groups[0].evidenceIds], runIds: [item.runId], excludedRunCount: missingMetric }));
|
|
353
|
+
}
|
|
354
|
+
function decodeCursor(cursor) { const value = Number(cursor); return Number.isSafeInteger(value) && value >= 0 ? value : 0; }
|
|
355
|
+
function numberWord(value) { return Number(value) || ({ one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7, eight: 8, nine: 9, ten: 10 }[value] ?? 1); }
|
|
356
|
+
function isRecord(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); }
|
|
357
|
+
function validateOperation(operation) {
|
|
358
|
+
if (!isRecord(operation) || typeof operation.kind !== "string" || !["list_runs", "get_run_details", "aggregate_runs", "compare_runs"].includes(operation.kind))
|
|
359
|
+
return false;
|
|
360
|
+
const allowed = operation.kind === "list_runs" ? ["kind", "filters", "limit", "cursor"] : operation.kind === "get_run_details" ? ["kind", "runIds", "maxSpans"] : operation.kind === "aggregate_runs" ? ["kind", "query"] : ["kind", "runIds", "metrics"];
|
|
361
|
+
if (Object.keys(operation).some(key => !allowed.includes(key)))
|
|
362
|
+
return false;
|
|
363
|
+
if (operation.kind === "list_runs" && operation.limit !== undefined && (typeof operation.limit !== "number" || !Number.isSafeInteger(operation.limit) || operation.limit < 1))
|
|
364
|
+
return false;
|
|
365
|
+
if (operation.kind === "list_runs" && operation.cursor !== undefined && typeof operation.cursor !== "string")
|
|
366
|
+
return false;
|
|
367
|
+
if (operation.kind === "get_run_details" && operation.maxSpans !== undefined && (typeof operation.maxSpans !== "number" || !Number.isSafeInteger(operation.maxSpans) || operation.maxSpans < 1))
|
|
368
|
+
return false;
|
|
369
|
+
if ((operation.kind === "get_run_details" || operation.kind === "compare_runs") && (!Array.isArray(operation.runIds) || !operation.runIds.every(item => typeof item === "string")))
|
|
370
|
+
return false;
|
|
371
|
+
if (operation.kind === "list_runs" && operation.filters !== undefined && !validateFilters(operation.filters))
|
|
372
|
+
return false;
|
|
373
|
+
if (operation.kind === "aggregate_runs" && !validateAggregateQuery(operation.query))
|
|
374
|
+
return false;
|
|
375
|
+
if (operation.kind === "compare_runs" && (!Array.isArray(operation.metrics) || !operation.metrics.every(item => AGGREGATE_METRICS.includes(item))))
|
|
376
|
+
return false;
|
|
377
|
+
return !containsForbiddenKey(operation);
|
|
378
|
+
}
|
|
379
|
+
function containsForbiddenKey(value) { if (Array.isArray(value))
|
|
380
|
+
return value.some(containsForbiddenKey); if (!isRecord(value))
|
|
381
|
+
return false; return Object.entries(value).some(([key, item]) => /sql|command|path|tool/i.test(key) || containsForbiddenKey(item)); }
|
|
382
|
+
function validateAggregateQuery(value) {
|
|
383
|
+
if (!isRecord(value) || value.version !== 1 || Object.keys(value).some(key => !["version", "filters", "groupBy", "metrics", "operations"].includes(key)))
|
|
384
|
+
return false;
|
|
385
|
+
if (value.filters !== undefined && !validateFilters(value.filters))
|
|
386
|
+
return false;
|
|
387
|
+
if (value.groupBy !== undefined && (!Array.isArray(value.groupBy) || !value.groupBy.every(item => ["status", "provider", "model", "branchMode", "qaEnabled", "ticket", "detailLevel"].includes(String(item)))))
|
|
388
|
+
return false;
|
|
389
|
+
return Array.isArray(value.metrics) && value.metrics.every(item => AGGREGATE_METRICS.includes(item))
|
|
390
|
+
&& Array.isArray(value.operations) && value.operations.every(item => AGGREGATE_OPERATIONS.includes(item));
|
|
391
|
+
}
|
|
392
|
+
function validateFilters(value) {
|
|
393
|
+
if (!isRecord(value) || Object.keys(value).some(key => !["runIds", "statuses", "createdFrom", "createdTo", "completedFrom", "completedTo", "providers", "models", "branchModes", "qaEnabled", "ticketIds", "detailLevels"].includes(key)))
|
|
394
|
+
return false;
|
|
395
|
+
const arrays = ["runIds", "statuses", "providers", "models", "branchModes", "ticketIds", "detailLevels"];
|
|
396
|
+
if (arrays.some(key => value[key] !== undefined && (!Array.isArray(value[key]) || !value[key].every(item => typeof item === "string"))))
|
|
397
|
+
return false;
|
|
398
|
+
if (["createdFrom", "createdTo", "completedFrom", "completedTo"].some(key => value[key] !== undefined && typeof value[key] !== "string"))
|
|
399
|
+
return false;
|
|
400
|
+
if (value.qaEnabled !== undefined && typeof value.qaEnabled !== "boolean")
|
|
401
|
+
return false;
|
|
402
|
+
if (Array.isArray(value.providers) && !value.providers.every(item => item === "claude" || item === "codex"))
|
|
403
|
+
return false;
|
|
404
|
+
if (Array.isArray(value.detailLevels) && !value.detailLevels.every(item => ["detailed", "rollup", "legacy"].includes(String(item))))
|
|
405
|
+
return false;
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { type QaFailureReportV1 } from "rafi-spec";
|
|
2
|
+
import { type FrozenQaSourceState } from "./qaSnapshot.js";
|
|
3
|
+
export declare const QA_RECOVERY_ROOT = ".foreman/qa-report-recovery";
|
|
4
|
+
export declare const QA_CONTEXT_ROOT = ".foreman/qa-recovery-context";
|
|
5
|
+
export type QaRecoveryIntegrityPolicy = "sealed" | "operator_editable";
|
|
6
|
+
export interface QaRecoveryResourceV2 {
|
|
7
|
+
path: string;
|
|
8
|
+
purpose: string;
|
|
9
|
+
mediaType: string;
|
|
10
|
+
bytes: number;
|
|
11
|
+
digest: string;
|
|
12
|
+
requiredForRecovery: boolean;
|
|
13
|
+
integrityPolicy: QaRecoveryIntegrityPolicy;
|
|
14
|
+
objectPath?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface QaRecoveryManifestV2 {
|
|
17
|
+
version: 2;
|
|
18
|
+
packetId: string;
|
|
19
|
+
packetDigest: string;
|
|
20
|
+
revision: number;
|
|
21
|
+
parentPacketDigest?: string;
|
|
22
|
+
runId: string;
|
|
23
|
+
ticketId: string;
|
|
24
|
+
cycle: number;
|
|
25
|
+
reviewAttempt: number;
|
|
26
|
+
reviewAttemptId: string;
|
|
27
|
+
/** @deprecated numeric compatibility alias for reviewAttempt. */
|
|
28
|
+
attempt: number;
|
|
29
|
+
recoveryStage: string;
|
|
30
|
+
correctionTurns: number;
|
|
31
|
+
pendingAction: string;
|
|
32
|
+
originalReviewedStateDigest: string;
|
|
33
|
+
currentReviewedStateDigest: string;
|
|
34
|
+
/** Current digest compatibility alias used by handoff APIs. */
|
|
35
|
+
reviewedStateDigest: string;
|
|
36
|
+
createdAt: string;
|
|
37
|
+
updatedAt: string;
|
|
38
|
+
resources: QaRecoveryResourceV2[];
|
|
39
|
+
}
|
|
40
|
+
/** Only for detecting and routing old packets; V1 is never authoritative. */
|
|
41
|
+
export interface QaRecoveryManifestV1 {
|
|
42
|
+
version: 1;
|
|
43
|
+
packetId?: string;
|
|
44
|
+
packetDigest?: string;
|
|
45
|
+
runId?: string;
|
|
46
|
+
ticketId?: string;
|
|
47
|
+
resources?: Array<{
|
|
48
|
+
path?: string;
|
|
49
|
+
purpose?: string;
|
|
50
|
+
mediaType?: string;
|
|
51
|
+
bytes?: number;
|
|
52
|
+
digest?: string;
|
|
53
|
+
}>;
|
|
54
|
+
}
|
|
55
|
+
export declare class LegacyQaRecoveryPacketError extends Error {
|
|
56
|
+
readonly directory: string;
|
|
57
|
+
readonly manifest: QaRecoveryManifestV1;
|
|
58
|
+
constructor(directory: string, manifest: QaRecoveryManifestV1);
|
|
59
|
+
}
|
|
60
|
+
export interface QaRecoveryPacket {
|
|
61
|
+
directory: string;
|
|
62
|
+
projectDir: string;
|
|
63
|
+
manifest: QaRecoveryManifestV2;
|
|
64
|
+
}
|
|
65
|
+
export interface QaRecoveryPacketInput {
|
|
66
|
+
projectDir: string;
|
|
67
|
+
frozenState?: FrozenQaSourceState;
|
|
68
|
+
/** Compatibility input; captured immediately when frozenState is unavailable. */
|
|
69
|
+
reviewedWorktree?: string;
|
|
70
|
+
runId: string;
|
|
71
|
+
ticketId: string;
|
|
72
|
+
cycle: number;
|
|
73
|
+
reviewAttempt: number;
|
|
74
|
+
reviewAttemptId?: string;
|
|
75
|
+
attempt?: number;
|
|
76
|
+
recoveryStage: string;
|
|
77
|
+
correctionTurns?: number;
|
|
78
|
+
pendingAction?: string;
|
|
79
|
+
resources: Record<string, {
|
|
80
|
+
value: unknown;
|
|
81
|
+
purpose: string;
|
|
82
|
+
mediaType?: string;
|
|
83
|
+
requiredForRecovery?: boolean;
|
|
84
|
+
exactText?: boolean;
|
|
85
|
+
}>;
|
|
86
|
+
reportJson?: string;
|
|
87
|
+
}
|
|
88
|
+
export declare function loadQaRecoveryPacket(directory: string): QaRecoveryPacket;
|
|
89
|
+
export declare function inspectLegacyQaRecoveryPacket(directory: string): {
|
|
90
|
+
directory: string;
|
|
91
|
+
manifest: QaRecoveryManifestV1;
|
|
92
|
+
} | undefined;
|
|
93
|
+
export declare function materializeLegacyQaHistoricalContext(projectDir: string, legacy: {
|
|
94
|
+
directory: string;
|
|
95
|
+
manifest: QaRecoveryManifestV1;
|
|
96
|
+
}): {
|
|
97
|
+
directory: string;
|
|
98
|
+
readable: string[];
|
|
99
|
+
rejected: Array<{
|
|
100
|
+
path: string;
|
|
101
|
+
reason: string;
|
|
102
|
+
}>;
|
|
103
|
+
};
|
|
104
|
+
export declare function compareQaRecoveryReviewedState(packet: QaRecoveryPacket, source: string | FrozenQaSourceState): {
|
|
105
|
+
matches: boolean;
|
|
106
|
+
originalDigest: string;
|
|
107
|
+
currentDigest: string;
|
|
108
|
+
drift: string[];
|
|
109
|
+
frozenState: FrozenQaSourceState;
|
|
110
|
+
};
|
|
111
|
+
/** Create a unique owner-only V2 packet from the immutable state QA reviewed. */
|
|
112
|
+
export declare function createQaRecoveryPacket(input: QaRecoveryPacketInput): QaRecoveryPacket;
|
|
113
|
+
export declare function appendQaRecoveryResource(packet: QaRecoveryPacket, relativePath: string, value: string | Buffer | unknown, options: {
|
|
114
|
+
purpose: string;
|
|
115
|
+
mediaType?: string;
|
|
116
|
+
requiredForRecovery?: boolean;
|
|
117
|
+
exact?: boolean;
|
|
118
|
+
}): QaRecoveryPacket;
|
|
119
|
+
export declare function appendQaRecoveryReviewedState(packet: QaRecoveryPacket, source: string | FrozenQaSourceState, prefix?: string): QaRecoveryPacket;
|
|
120
|
+
export declare function updateQaRecoveryPosition(packet: QaRecoveryPacket, stage: string, correctionTurns: number, pendingAction?: string): QaRecoveryPacket;
|
|
121
|
+
export declare function readManualQaReport(packetDirectory: string): unknown;
|
|
122
|
+
export declare function validateManualQaReport(packet: QaRecoveryPacket): {
|
|
123
|
+
packet: QaRecoveryPacket;
|
|
124
|
+
report?: QaFailureReportV1;
|
|
125
|
+
errors: string[];
|
|
126
|
+
};
|
|
127
|
+
/** Copy one exact sealed revision into a fresh snapshot and return a mutation seal. */
|
|
128
|
+
export declare function materializeQaRecoveryContext(packet: QaRecoveryPacket, qaWorktree: string): {
|
|
129
|
+
path: string;
|
|
130
|
+
relativePath: string;
|
|
131
|
+
digest: string;
|
|
132
|
+
verify(): void;
|
|
133
|
+
};
|
|
134
|
+
export declare function renderQaRecoveryAcknowledgementInstruction(packet: QaRecoveryPacket, relativePath: string): string;
|
|
135
|
+
export declare function validateQaRecoveryAcknowledgement(text: string, packet: QaRecoveryPacket, options?: {
|
|
136
|
+
continuityAlreadyValidated?: boolean;
|
|
137
|
+
}): string[];
|
|
138
|
+
export declare function qaRecoveryInventory(packet: QaRecoveryPacket): string;
|
|
139
|
+
export declare function qaReportDigest(report: QaFailureReportV1): string;
|
|
140
|
+
export declare function ensureQaRecoveryExcluded(projectDir: string): void;
|