@oscharko-dev/keiko-evidence 0.2.0
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/dist/.tsbuildinfo +1 -0
- package/dist/aggregate.d.ts +4 -0
- package/dist/aggregate.d.ts.map +1 -0
- package/dist/aggregate.js +21 -0
- package/dist/build.d.ts +3 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +227 -0
- package/dist/connected-context-evidence.d.ts +47 -0
- package/dist/connected-context-evidence.d.ts.map +1 -0
- package/dist/connected-context-evidence.js +197 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +4 -0
- package/dist/index-api.d.ts +15 -0
- package/dist/index-api.d.ts.map +1 -0
- package/dist/index-api.js +136 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/persist.d.ts +9 -0
- package/dist/persist.d.ts.map +1 -0
- package/dist/persist.js +40 -0
- package/dist/promptEnhancement/index.d.ts +7 -0
- package/dist/promptEnhancement/index.d.ts.map +1 -0
- package/dist/promptEnhancement/index.js +10 -0
- package/dist/promptEnhancement/manifestSchema.d.ts +71 -0
- package/dist/promptEnhancement/manifestSchema.d.ts.map +1 -0
- package/dist/promptEnhancement/manifestSchema.js +307 -0
- package/dist/promptEnhancement/redaction.d.ts +17 -0
- package/dist/promptEnhancement/redaction.d.ts.map +1 -0
- package/dist/promptEnhancement/redaction.js +66 -0
- package/dist/promptEnhancement/store.d.ts +64 -0
- package/dist/promptEnhancement/store.d.ts.map +1 -0
- package/dist/promptEnhancement/store.js +409 -0
- package/dist/qualityIntelligence/candidatesArtifact.d.ts +74 -0
- package/dist/qualityIntelligence/candidatesArtifact.d.ts.map +1 -0
- package/dist/qualityIntelligence/candidatesArtifact.js +258 -0
- package/dist/qualityIntelligence/companionStore.d.ts +37 -0
- package/dist/qualityIntelligence/companionStore.d.ts.map +1 -0
- package/dist/qualityIntelligence/companionStore.js +158 -0
- package/dist/qualityIntelligence/figmaSnapshot/schema.d.ts +123 -0
- package/dist/qualityIntelligence/figmaSnapshot/schema.d.ts.map +1 -0
- package/dist/qualityIntelligence/figmaSnapshot/schema.js +163 -0
- package/dist/qualityIntelligence/figmaSnapshot/store.d.ts +144 -0
- package/dist/qualityIntelligence/figmaSnapshot/store.d.ts.map +1 -0
- package/dist/qualityIntelligence/figmaSnapshot/store.js +898 -0
- package/dist/qualityIntelligence/index.d.ts +18 -0
- package/dist/qualityIntelligence/index.d.ts.map +1 -0
- package/dist/qualityIntelligence/index.js +21 -0
- package/dist/qualityIntelligence/manifestSchema.d.ts +154 -0
- package/dist/qualityIntelligence/manifestSchema.d.ts.map +1 -0
- package/dist/qualityIntelligence/manifestSchema.js +70 -0
- package/dist/qualityIntelligence/redaction.d.ts +10 -0
- package/dist/qualityIntelligence/redaction.d.ts.map +1 -0
- package/dist/qualityIntelligence/redaction.js +103 -0
- package/dist/qualityIntelligence/retention.d.ts +71 -0
- package/dist/qualityIntelligence/retention.d.ts.map +1 -0
- package/dist/qualityIntelligence/retention.js +287 -0
- package/dist/qualityIntelligence/retentionPolicy.d.ts +10 -0
- package/dist/qualityIntelligence/retentionPolicy.d.ts.map +1 -0
- package/dist/qualityIntelligence/retentionPolicy.js +38 -0
- package/dist/qualityIntelligence/store.d.ts +95 -0
- package/dist/qualityIntelligence/store.d.ts.map +1 -0
- package/dist/qualityIntelligence/store.js +483 -0
- package/dist/redaction.d.ts +2 -0
- package/dist/redaction.d.ts.map +1 -0
- package/dist/redaction.js +4 -0
- package/dist/report.d.ts +17 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +50 -0
- package/dist/retention.d.ts +4 -0
- package/dist/retention.d.ts.map +1 -0
- package/dist/retention.js +95 -0
- package/dist/runid.d.ts +2 -0
- package/dist/runid.d.ts.map +1 -0
- package/dist/runid.js +4 -0
- package/dist/side-file.d.ts +9 -0
- package/dist/side-file.d.ts.map +1 -0
- package/dist/side-file.js +102 -0
- package/dist/store.d.ts +8 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +332 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +1 -0
- package/dist/workflow-evidence.d.ts +36 -0
- package/dist/workflow-evidence.d.ts.map +1 -0
- package/dist/workflow-evidence.js +158 -0
- package/package.json +32 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// QI retention + deletion + restart-recovery semantics (Issue #274, ADR-0023 D8).
|
|
2
|
+
//
|
|
3
|
+
// Pure decision function (`applyQualityIntelligenceRetention`) consumed by the orchestrator with
|
|
4
|
+
// the current store snapshot; the orchestrator does the side effects. Idempotent deletion
|
|
5
|
+
// (`deleteQualityIntelligenceRun`) removes the manifest AND its companion artifacts
|
|
6
|
+
// (`.candidates.json` plus any caller-supplied server-owned suffixes) so a deleted run leaves no
|
|
7
|
+
// orphaned customer-derived content, and emits a typed deletion-receipt audit event without
|
|
8
|
+
// throwing on a missing run.
|
|
9
|
+
//
|
|
10
|
+
// Restart recovery is enforced at the WRITE seam: every persist is atomic O_EXCL temp + rename,
|
|
11
|
+
// so an unclean shutdown leaves at most one `.tmp` (or nothing). The list/load surfaces filter on
|
|
12
|
+
// the `.qi.json` suffix, so a half-written run is never surfaced. There is no recovery procedure
|
|
13
|
+
// to call — recovery IS the absence of a code path that surfaces partials.
|
|
14
|
+
import { lstatSync, realpathSync, renameSync, rmSync } from "node:fs";
|
|
15
|
+
import { join, resolve, sep } from "node:path";
|
|
16
|
+
import { assertValidRunId } from "@oscharko-dev/keiko-security";
|
|
17
|
+
import { CANDIDATES_SUFFIX } from "./candidatesArtifact.js";
|
|
18
|
+
import { deleteQualityIntelligenceCompanionArtifact } from "./companionStore.js";
|
|
19
|
+
import { getQualityIntelligenceRetentionProfile, } from "./retentionPolicy.js";
|
|
20
|
+
import { createNodeQualityIntelligenceLocalStore, QI_SUBDIR, } from "./store.js";
|
|
21
|
+
// Companion artifacts that `keiko-evidence` OWNS and writes alongside the run manifest under
|
|
22
|
+
// `qi/`, keyed by `<runId>`. They are swept by default on deletion so a "deleted" run never leaves
|
|
23
|
+
// customer-derived generated content (`.candidates.json`) orphaned on disk (Issue #274 AC4).
|
|
24
|
+
// Higher layers that own their own companions (e.g. keiko-server's `.review.json`, the figma
|
|
25
|
+
// route companions) pass their suffixes via `QualityIntelligenceDeleteOptions.companionSuffixes`;
|
|
26
|
+
// keiko-evidence MUST NOT hard-code suffixes it does not own (layering). The figma-snapshot
|
|
27
|
+
// companion + its `figma-snapshots/<runId>/` side-files are deliberately EXCLUDED here — they have
|
|
28
|
+
// their own retention seam (`enforceFigmaSnapshotRetention`) and are cleaned by that subsystem.
|
|
29
|
+
const QI_EVIDENCE_OWNED_COMPANION_SUFFIXES = Object.freeze([CANDIDATES_SUFFIX]);
|
|
30
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
31
|
+
function profileForEntry(entry) {
|
|
32
|
+
return { entry, profile: getQualityIntelligenceRetentionProfile(entry.retentionPolicyId) };
|
|
33
|
+
}
|
|
34
|
+
// Bucket snapshot entries by their retention-policy id so each profile is enforced
|
|
35
|
+
// independently. Entries with an unknown profile id are retained (forward-compat: a future schema
|
|
36
|
+
// migration may introduce a profile a current binary does not know).
|
|
37
|
+
function bucketByProfile(snapshot) {
|
|
38
|
+
const buckets = new Map();
|
|
39
|
+
for (const entry of snapshot) {
|
|
40
|
+
const profiled = profileForEntry(entry);
|
|
41
|
+
const list = buckets.get(entry.retentionPolicyId) ?? [];
|
|
42
|
+
list.push(profiled);
|
|
43
|
+
buckets.set(entry.retentionPolicyId, list);
|
|
44
|
+
}
|
|
45
|
+
return buckets;
|
|
46
|
+
}
|
|
47
|
+
function decideOneBucket(bucket, now) {
|
|
48
|
+
// The first bucket entry's profile is the bucket's profile (all entries share the same
|
|
49
|
+
// retentionPolicyId by construction). Empty bucket → no decisions.
|
|
50
|
+
const head = bucket[0];
|
|
51
|
+
if (head === undefined) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const profile = head.profile;
|
|
55
|
+
if (profile === undefined) {
|
|
56
|
+
// Unknown profile id: retain everything (forward-compat).
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
// Newest-first ordering so the "always keep newest N" guarantee is the bucket head.
|
|
60
|
+
const sorted = [...bucket].sort((a, b) => b.entry.recordedAt - a.entry.recordedAt);
|
|
61
|
+
const decisions = [];
|
|
62
|
+
const maxAgeMs = profile.retainedDays * MS_PER_DAY;
|
|
63
|
+
for (let i = 0; i < sorted.length; i += 1) {
|
|
64
|
+
const slot = sorted[i];
|
|
65
|
+
if (slot === undefined) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const { entry } = slot;
|
|
69
|
+
if (i >= profile.maxRunArtifacts) {
|
|
70
|
+
decisions.push({ runId: entry.runId, reason: "count-exceeded" });
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (now - entry.recordedAt > maxAgeMs) {
|
|
74
|
+
decisions.push({ runId: entry.runId, reason: "age-exceeded" });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return decisions;
|
|
78
|
+
}
|
|
79
|
+
// Pure retention decision. Returns the set of run ids to expire under each entry's configured
|
|
80
|
+
// policy. Always keeps the newest N runs per policy (count-exceeded only fires for runs at index
|
|
81
|
+
// >= N when sorted newest-first); age decisions are evaluated against the entry's own profile.
|
|
82
|
+
export function applyQualityIntelligenceRetention(input) {
|
|
83
|
+
const buckets = bucketByProfile(input.snapshot);
|
|
84
|
+
const decisions = [];
|
|
85
|
+
for (const bucket of buckets.values()) {
|
|
86
|
+
decisions.push(...decideOneBucket(bucket, input.now));
|
|
87
|
+
}
|
|
88
|
+
const expired = new Set(decisions.map((d) => d.runId));
|
|
89
|
+
const expiredRunIds = [...expired].sort();
|
|
90
|
+
const retainedRunIds = input.snapshot
|
|
91
|
+
.map((entry) => entry.runId)
|
|
92
|
+
.filter((runId) => !expired.has(runId))
|
|
93
|
+
.sort();
|
|
94
|
+
return { expiredRunIds, retainedRunIds, decisions };
|
|
95
|
+
}
|
|
96
|
+
function resolveDeleteStore(options) {
|
|
97
|
+
if (options.store !== undefined) {
|
|
98
|
+
return options.store;
|
|
99
|
+
}
|
|
100
|
+
if (options.evidenceDir !== undefined) {
|
|
101
|
+
return createNodeQualityIntelligenceLocalStore(options.evidenceDir);
|
|
102
|
+
}
|
|
103
|
+
throw new Error("deleteQualityIntelligenceRun requires options.store or options.evidenceDir");
|
|
104
|
+
}
|
|
105
|
+
function containedPath(path, root) {
|
|
106
|
+
return path === root || path.startsWith(root + sep);
|
|
107
|
+
}
|
|
108
|
+
function realDirectoryForDeletion(path, label) {
|
|
109
|
+
const lexical = resolve(path);
|
|
110
|
+
const stat = lstatSync(lexical, { throwIfNoEntry: false });
|
|
111
|
+
if (stat === undefined) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
115
|
+
throw new Error(`${label} is not a real directory, refusing to delete: ${lexical}`);
|
|
116
|
+
}
|
|
117
|
+
return realpathSync(lexical);
|
|
118
|
+
}
|
|
119
|
+
function realEvidenceRootForContainment(evidenceDir) {
|
|
120
|
+
try {
|
|
121
|
+
return realpathSync(resolve(evidenceDir));
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Recursively removes a per-run side-file directory if it exists. Missing dir → no-op. Symlinked
|
|
128
|
+
// side-file roots or run dirs are refused before the manifest is deleted so retention cannot rm
|
|
129
|
+
// outside the evidence tree or emit a false successful deletion receipt. The evidence root itself
|
|
130
|
+
// may be a workspace-approved symlink; canonicalize it for containment instead of rejecting it.
|
|
131
|
+
function removeSideFileDirIfPresent(runId, sideFileRoot, evidenceDir) {
|
|
132
|
+
const root = realDirectoryForDeletion(sideFileRoot, "QI side-file root");
|
|
133
|
+
if (root === undefined) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (evidenceDir !== undefined) {
|
|
137
|
+
const evidenceRoot = realEvidenceRootForContainment(evidenceDir);
|
|
138
|
+
if (evidenceRoot === undefined || !containedPath(root, evidenceRoot)) {
|
|
139
|
+
throw new Error(`QI side-file root escapes the evidence directory, refusing to delete: ${root}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const runDir = join(root, runId);
|
|
143
|
+
// Defence-in-depth: runId is already `assertValidRunId`-checked (no separators, no `..`, no
|
|
144
|
+
// leading dot) before this is reached. Assert containment against the canonical side-file root so
|
|
145
|
+
// symlinked roots or future validation drift cannot redirect recursive deletion outside evidence.
|
|
146
|
+
if (!containedPath(runDir, root)) {
|
|
147
|
+
throw new Error(`QI side-file dir escapes the side-file root, refusing to delete: ${runDir}`);
|
|
148
|
+
}
|
|
149
|
+
const stat = lstatSync(runDir, { throwIfNoEntry: false });
|
|
150
|
+
if (stat === undefined) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (stat.isSymbolicLink()) {
|
|
154
|
+
throw new Error(`QI side-file dir is a symlink, refusing to delete: ${runDir}`);
|
|
155
|
+
}
|
|
156
|
+
if (!stat.isDirectory()) {
|
|
157
|
+
throw new Error(`QI side-file dir is not a real directory, refusing to delete: ${runDir}`);
|
|
158
|
+
}
|
|
159
|
+
rmSync(runDir, { recursive: true, force: true });
|
|
160
|
+
}
|
|
161
|
+
// Sweep the run's companion artifacts (evidence-owned + caller-supplied) from the contained `qi/`
|
|
162
|
+
// dir. Exact-suffix, idempotent: a companion that does not exist is skipped. Returns the suffixes
|
|
163
|
+
// that were actually removed so the deletion receipt/audit event can attest completeness.
|
|
164
|
+
function removeRunCompanions(evidenceDir, runId, extraSuffixes) {
|
|
165
|
+
const suffixes = [
|
|
166
|
+
...new Set([...QI_EVIDENCE_OWNED_COMPANION_SUFFIXES, ...(extraSuffixes ?? [])]),
|
|
167
|
+
];
|
|
168
|
+
const removed = [];
|
|
169
|
+
for (const suffix of suffixes) {
|
|
170
|
+
if (deleteQualityIntelligenceCompanionArtifact(evidenceDir, runId, suffix)) {
|
|
171
|
+
removed.push(suffix);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return removed.sort();
|
|
175
|
+
}
|
|
176
|
+
// Idempotent removal of a single QI run's local state. Returns a structured receipt rather than
|
|
177
|
+
// throwing on a missing run — callers (UI, retention orchestrator) need to distinguish "deleted"
|
|
178
|
+
// from "absent" without try/catch noise. When options.sideFileRoot is set, also removes
|
|
179
|
+
// `<sideFileRoot>/<runId>/` so binary side-files written by future export adapters are cleaned
|
|
180
|
+
// up alongside the manifest.
|
|
181
|
+
export function deleteQualityIntelligenceRun(runId, options = {}) {
|
|
182
|
+
assertValidRunId(runId);
|
|
183
|
+
const store = resolveDeleteStore(options);
|
|
184
|
+
if (options.sideFileRoot !== undefined) {
|
|
185
|
+
removeSideFileDirIfPresent(runId, options.sideFileRoot, options.evidenceDir);
|
|
186
|
+
}
|
|
187
|
+
const removed = store.delete(runId);
|
|
188
|
+
// Sweep companion artifacts so a deleted run leaves no orphaned customer-derived content on disk
|
|
189
|
+
// (Issue #274 AC4). On-disk only — skipped when just an in-memory store is supplied.
|
|
190
|
+
const removedCompanionSuffixes = options.evidenceDir === undefined
|
|
191
|
+
? []
|
|
192
|
+
: removeRunCompanions(options.evidenceDir, runId, options.companionSuffixes);
|
|
193
|
+
const at = new Date(options.now?.() ?? Date.now()).toISOString();
|
|
194
|
+
const status = removed ? "deleted" : "absent";
|
|
195
|
+
return {
|
|
196
|
+
runId,
|
|
197
|
+
status,
|
|
198
|
+
removedCompanionSuffixes,
|
|
199
|
+
auditEvent: { type: "qi:run:deleted", runId, status, removedCompanionSuffixes, at },
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// Load one run's manifest and project it onto a retention snapshot entry. Returns undefined when the
|
|
203
|
+
// run MUST be skipped: load returned absent/corrupt (undefined or EvidenceReadError) or the
|
|
204
|
+
// manifest's `completedAt ?? planAt` is not a parseable timestamp. Skipping means the run never
|
|
205
|
+
// reaches the decision and is therefore never purged — the destructive-path fail-safe.
|
|
206
|
+
function snapshotEntryForRun(store, runId) {
|
|
207
|
+
let manifest;
|
|
208
|
+
try {
|
|
209
|
+
manifest = store.load(runId);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// A corrupt / tampered / unreadable manifest fails closed: skip, never delete.
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
if (manifest === undefined) {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
const recordedAt = Date.parse(manifest.completedAt ?? manifest.planAt);
|
|
219
|
+
if (Number.isNaN(recordedAt)) {
|
|
220
|
+
// No usable timestamp → cannot age the run out safely → retain.
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
return { runId, recordedAt, retentionPolicyId: manifest.retentionPolicyId };
|
|
224
|
+
}
|
|
225
|
+
function buildRetentionSnapshot(store) {
|
|
226
|
+
const snapshot = [];
|
|
227
|
+
for (const runId of store.list()) {
|
|
228
|
+
const entry = snapshotEntryForRun(store, runId);
|
|
229
|
+
if (entry !== undefined) {
|
|
230
|
+
snapshot.push(entry);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return snapshot;
|
|
234
|
+
}
|
|
235
|
+
export function enforceQualityIntelligenceRetentionPolicy(options) {
|
|
236
|
+
const store = createNodeQualityIntelligenceLocalStore(options.evidenceDir);
|
|
237
|
+
const snapshot = buildRetentionSnapshot(store);
|
|
238
|
+
const result = applyQualityIntelligenceRetention({
|
|
239
|
+
snapshot,
|
|
240
|
+
now: options.now?.() ?? Date.now(),
|
|
241
|
+
});
|
|
242
|
+
const receipts = [];
|
|
243
|
+
const failures = [];
|
|
244
|
+
for (const runId of result.expiredRunIds) {
|
|
245
|
+
try {
|
|
246
|
+
receipts.push(deleteQualityIntelligenceRun(runId, {
|
|
247
|
+
evidenceDir: options.evidenceDir,
|
|
248
|
+
now: options.now,
|
|
249
|
+
companionSuffixes: options.companionSuffixes,
|
|
250
|
+
sideFileRoot: options.sideFileRoot,
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
failures.push({
|
|
255
|
+
runId,
|
|
256
|
+
message: error instanceof Error ? error.message : "unknown deletion failure",
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return { receipts, failures, result };
|
|
261
|
+
}
|
|
262
|
+
export function quarantineCorruptQualityIntelligenceManifest(evidenceDir, runId, options = {}) {
|
|
263
|
+
assertValidRunId(runId);
|
|
264
|
+
const baseDir = join(evidenceDir, QI_SUBDIR);
|
|
265
|
+
const originalPath = join(baseDir, `${runId}.qi.json`);
|
|
266
|
+
const stat = lstatSync(originalPath, { throwIfNoEntry: false });
|
|
267
|
+
if (!stat?.isFile()) {
|
|
268
|
+
return { originalPath, quarantinedPath: originalPath, status: "absent" };
|
|
269
|
+
}
|
|
270
|
+
const ts = new Date(options.now?.() ?? Date.now()).toISOString();
|
|
271
|
+
const quarantinedPath = `${originalPath}.corrupt.${ts}`;
|
|
272
|
+
renameSync(originalPath, quarantinedPath);
|
|
273
|
+
return { originalPath, quarantinedPath, status: "quarantined" };
|
|
274
|
+
}
|
|
275
|
+
export function snapshotQualityIntelligenceRunsForRecovery(store) {
|
|
276
|
+
const loaded = [];
|
|
277
|
+
const skipped = [];
|
|
278
|
+
for (const runId of store.list()) {
|
|
279
|
+
const manifest = store.load(runId);
|
|
280
|
+
if (manifest === undefined) {
|
|
281
|
+
skipped.push(runId);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
loaded.push(runId);
|
|
285
|
+
}
|
|
286
|
+
return { loadedRunIds: loaded.sort(), skippedRunIds: skipped.sort() };
|
|
287
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface QualityIntelligenceRetentionProfile {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly description: string;
|
|
4
|
+
readonly retainedDays: number;
|
|
5
|
+
readonly maxRunArtifacts: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const QUALITY_INTELLIGENCE_RETENTION_PROFILES: Readonly<Record<string, QualityIntelligenceRetentionProfile>>;
|
|
8
|
+
export declare const QUALITY_INTELLIGENCE_DEFAULT_RETENTION_PROFILE_ID: "qi:short-30d";
|
|
9
|
+
export declare function getQualityIntelligenceRetentionProfile(profileId: string): QualityIntelligenceRetentionProfile | undefined;
|
|
10
|
+
//# sourceMappingURL=retentionPolicy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retentionPolicy.d.ts","sourceRoot":"","sources":["../../src/qualityIntelligence/retentionPolicy.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,mCAAmC;IAClD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AA4BD,eAAO,MAAM,uCAAuC,EAAE,QAAQ,CAC5D,MAAM,CAAC,MAAM,EAAE,mCAAmC,CAAC,CACL,CAAC;AAEjD,eAAO,MAAM,iDAAiD,EAAG,cAAuB,CAAC;AAKzF,wBAAgB,sCAAsC,CACpD,SAAS,EAAE,MAAM,GAChB,mCAAmC,GAAG,SAAS,CAEjD"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// QI retention profiles (Issue #274, ADR-0023 D8).
|
|
2
|
+
//
|
|
3
|
+
// Frozen, typed constants describing how long a QI run's evidence record persists locally before
|
|
4
|
+
// it becomes a candidate for `applyQualityIntelligenceRetention`. The profile is a pure
|
|
5
|
+
// description; the decision function is in `./retention.ts`.
|
|
6
|
+
// Three pre-shipped profiles. New profiles are added by extending this map; the IDs are
|
|
7
|
+
// referenced by string on the manifest, so renaming a profile is a manifest-schema breaking
|
|
8
|
+
// change (bump `QUALITY_INTELLIGENCE_EVIDENCE_SCHEMA_VERSION`).
|
|
9
|
+
const QI_RETENTION_PROFILES_MUTABLE = {
|
|
10
|
+
"qi:short-30d": Object.freeze({
|
|
11
|
+
id: "qi:short-30d",
|
|
12
|
+
description: "Short retention: 30 days, up to 100 runs.",
|
|
13
|
+
retainedDays: 30,
|
|
14
|
+
maxRunArtifacts: 100,
|
|
15
|
+
}),
|
|
16
|
+
"qi:standard-90d": Object.freeze({
|
|
17
|
+
id: "qi:standard-90d",
|
|
18
|
+
description: "Standard retention: 90 days, up to 500 runs.",
|
|
19
|
+
retainedDays: 90,
|
|
20
|
+
maxRunArtifacts: 500,
|
|
21
|
+
}),
|
|
22
|
+
"qi:long-365d": Object.freeze({
|
|
23
|
+
id: "qi:long-365d",
|
|
24
|
+
description: "Long retention: 365 days, up to 2000 runs.",
|
|
25
|
+
retainedDays: 365,
|
|
26
|
+
maxRunArtifacts: 2000,
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
// The exposed table is a frozen view; mutating it (or any contained profile) throws in strict
|
|
30
|
+
// mode and is a no-op otherwise. Tests assert frozenness as a regression guard.
|
|
31
|
+
export const QUALITY_INTELLIGENCE_RETENTION_PROFILES = Object.freeze(QI_RETENTION_PROFILES_MUTABLE);
|
|
32
|
+
export const QUALITY_INTELLIGENCE_DEFAULT_RETENTION_PROFILE_ID = "qi:short-30d";
|
|
33
|
+
// Looks up a profile by id, returning undefined for unknown ids. Callers MUST not throw on
|
|
34
|
+
// unknown — a future schema migration may introduce a profile a current binary does not know,
|
|
35
|
+
// and the local-state contract requires graceful read-back.
|
|
36
|
+
export function getQualityIntelligenceRetentionProfile(profileId) {
|
|
37
|
+
return QUALITY_INTELLIGENCE_RETENTION_PROFILES[profileId];
|
|
38
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { type WorkspaceFs } from "@oscharko-dev/keiko-workspace";
|
|
2
|
+
import { type QualityIntelligenceEvidenceManifest, type QualityIntelligenceAtomFingerprintRow, type QualityIntelligenceSourceFingerprintRow } from "./manifestSchema.js";
|
|
3
|
+
import { type QualityIntelligenceRedactionOptions } from "./redaction.js";
|
|
4
|
+
export declare const QI_SUBDIR = "qi";
|
|
5
|
+
export interface QualityIntelligenceLocalStore {
|
|
6
|
+
readonly record: (manifest: QualityIntelligenceEvidenceManifest) => string;
|
|
7
|
+
readonly load: (runId: string) => QualityIntelligenceEvidenceManifest | undefined;
|
|
8
|
+
readonly list: () => readonly string[];
|
|
9
|
+
readonly location: (runId: string) => string;
|
|
10
|
+
readonly delete: (runId: string) => boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function createInMemoryQualityIntelligenceLocalStore(): QualityIntelligenceLocalStore;
|
|
13
|
+
export interface QualityIntelligenceNodeStoreOptions {
|
|
14
|
+
readonly fs?: WorkspaceFs;
|
|
15
|
+
readonly randomSuffix?: () => string;
|
|
16
|
+
}
|
|
17
|
+
export declare function createNodeQualityIntelligenceLocalStore(evidenceDir: string, options?: QualityIntelligenceNodeStoreOptions): QualityIntelligenceLocalStore;
|
|
18
|
+
export interface QualityIntelligenceRecordInput {
|
|
19
|
+
readonly runId: string;
|
|
20
|
+
readonly planAt: string;
|
|
21
|
+
readonly completedAt: string | undefined;
|
|
22
|
+
readonly status: QualityIntelligenceEvidenceManifest["status"];
|
|
23
|
+
readonly policyProfileIds: readonly string[];
|
|
24
|
+
readonly retentionPolicyId: string;
|
|
25
|
+
readonly modelGatewayCallCount: number;
|
|
26
|
+
readonly totals: QualityIntelligenceEvidenceManifest["totals"];
|
|
27
|
+
readonly findings: QualityIntelligenceEvidenceManifest["findings"];
|
|
28
|
+
readonly exports: QualityIntelligenceEvidenceManifest["exports"];
|
|
29
|
+
readonly evidenceRefs: QualityIntelligenceEvidenceManifest["evidenceRefs"];
|
|
30
|
+
readonly provenanceRefs: QualityIntelligenceEvidenceManifest["provenanceRefs"];
|
|
31
|
+
/** Optional coverage matrix (per-atom status, refs only). Added in #738. */
|
|
32
|
+
readonly coverageMatrix?: QualityIntelligenceEvidenceManifest["coverageMatrix"];
|
|
33
|
+
/** Optional run quality score — percent of candidates with a strong judge outcome [0-100]; null when judge was skipped. Added in #736. */
|
|
34
|
+
readonly qualityScore?: QualityIntelligenceEvidenceManifest["qualityScore"];
|
|
35
|
+
/** Optional per-envelope content fingerprints for drift detection (Epic #735). */
|
|
36
|
+
readonly sourceFingerprints?: readonly QualityIntelligenceSourceFingerprintRow[];
|
|
37
|
+
/** Optional per-atom content fingerprints for atom-aware drift detection (#798/#799). */
|
|
38
|
+
readonly atomFingerprints?: readonly QualityIntelligenceAtomFingerprintRow[];
|
|
39
|
+
/** Optional model id that generated the candidates (Epic #761). */
|
|
40
|
+
readonly modelId?: string;
|
|
41
|
+
/** Optional redaction-safe request parameter scalars (Epic #761). */
|
|
42
|
+
readonly modelParameters?: Record<string, unknown>;
|
|
43
|
+
/** Optional seed used for deterministic sampling (Epic #761). */
|
|
44
|
+
readonly seedUsed?: number | null;
|
|
45
|
+
}
|
|
46
|
+
export interface QualityIntelligenceRecordOptions {
|
|
47
|
+
readonly store?: QualityIntelligenceLocalStore | undefined;
|
|
48
|
+
readonly evidenceDir?: string | undefined;
|
|
49
|
+
readonly redaction?: QualityIntelligenceRedactionOptions | undefined;
|
|
50
|
+
}
|
|
51
|
+
export interface QualityIntelligenceRecordResult {
|
|
52
|
+
readonly manifest: QualityIntelligenceEvidenceManifest;
|
|
53
|
+
readonly location: string;
|
|
54
|
+
}
|
|
55
|
+
export declare function recordQualityIntelligenceRun(input: QualityIntelligenceRecordInput, options?: QualityIntelligenceRecordOptions): QualityIntelligenceRecordResult;
|
|
56
|
+
export interface QualityIntelligenceExportEvidenceInput {
|
|
57
|
+
readonly runId: string;
|
|
58
|
+
/** The export row to append: target adapter, artifact id, integrity hash, attestation, mode. */
|
|
59
|
+
readonly export: QualityIntelligenceEvidenceManifest["exports"][number];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Append one export-evidence row to an already-recorded run manifest (Issue #283, AC4 — "export
|
|
63
|
+
* evidence records target type, artifact IDs, mapping profile, and result without leaking secrets";
|
|
64
|
+
* Audit Addendum — "audit evidence for every export action").
|
|
65
|
+
*
|
|
66
|
+
* Every QI export action — a local serialisation download, a binary PDF/ZIP bundle, or a dry-run
|
|
67
|
+
* preview — emits one audit row recording WHAT was exported (`targetAdapter`), the artifact id, its
|
|
68
|
+
* integrity hash, the redaction attestation, and whether it was a dry-run. The disabled external-TMS
|
|
69
|
+
* write path produces no artifact and therefore records nothing.
|
|
70
|
+
*
|
|
71
|
+
* Rows are deduplicated by `(id, dryRun)` so re-exporting the same adapter in the same mode is
|
|
72
|
+
* idempotent — the audit captures each distinct (artifact, mode) once rather than once per click,
|
|
73
|
+
* which also bounds manifest growth.
|
|
74
|
+
*
|
|
75
|
+
* Invariants preserved (mirrors {@link recordQualityIntelligenceRun}):
|
|
76
|
+
* - the new row's string leaves pass the persist redactor before assembly (the row carries only
|
|
77
|
+
* ids / an enum / a sha-256 hash / booleans, so this is a no-op in practice, but the persist
|
|
78
|
+
* redactor is applied unconditionally to keep the fail-closed contract uniform);
|
|
79
|
+
* - `integrityHashes.exports` is recomputed over the full new collection; the other hash groups are
|
|
80
|
+
* carried over unchanged because their collections did not change;
|
|
81
|
+
* - `totals.exports` stays equal to `exports.length` (asserted on read);
|
|
82
|
+
* - the counts-only `redactionSummary` folds in the new row's scan;
|
|
83
|
+
* - the manifest is rewritten through the same atomic O_EXCL temp + rename path.
|
|
84
|
+
*
|
|
85
|
+
* Throws `EvidenceReadError` when the run manifest does not exist (an export cannot precede its run)
|
|
86
|
+
* and `EvidenceWriteError` when neither `store` nor `evidenceDir` is supplied or the write fails.
|
|
87
|
+
*/
|
|
88
|
+
export declare function appendQualityIntelligenceExportRow(input: QualityIntelligenceExportEvidenceInput, options?: QualityIntelligenceRecordOptions): QualityIntelligenceRecordResult;
|
|
89
|
+
export interface QualityIntelligenceLoadOptions {
|
|
90
|
+
readonly store?: QualityIntelligenceLocalStore | undefined;
|
|
91
|
+
readonly evidenceDir?: string | undefined;
|
|
92
|
+
}
|
|
93
|
+
export declare function loadQualityIntelligenceRun(runId: string, options?: QualityIntelligenceLoadOptions): QualityIntelligenceEvidenceManifest | undefined;
|
|
94
|
+
export declare function listQualityIntelligenceRuns(options?: QualityIntelligenceLoadOptions): readonly string[];
|
|
95
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/qualityIntelligence/store.ts"],"names":[],"mappings":"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"}
|