@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.
Files changed (92) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/aggregate.d.ts +4 -0
  3. package/dist/aggregate.d.ts.map +1 -0
  4. package/dist/aggregate.js +21 -0
  5. package/dist/build.d.ts +3 -0
  6. package/dist/build.d.ts.map +1 -0
  7. package/dist/build.js +227 -0
  8. package/dist/connected-context-evidence.d.ts +47 -0
  9. package/dist/connected-context-evidence.d.ts.map +1 -0
  10. package/dist/connected-context-evidence.js +197 -0
  11. package/dist/errors.d.ts +3 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +4 -0
  14. package/dist/index-api.d.ts +15 -0
  15. package/dist/index-api.d.ts.map +1 -0
  16. package/dist/index-api.js +136 -0
  17. package/dist/index.d.ts +20 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +34 -0
  20. package/dist/persist.d.ts +9 -0
  21. package/dist/persist.d.ts.map +1 -0
  22. package/dist/persist.js +40 -0
  23. package/dist/promptEnhancement/index.d.ts +7 -0
  24. package/dist/promptEnhancement/index.d.ts.map +1 -0
  25. package/dist/promptEnhancement/index.js +10 -0
  26. package/dist/promptEnhancement/manifestSchema.d.ts +71 -0
  27. package/dist/promptEnhancement/manifestSchema.d.ts.map +1 -0
  28. package/dist/promptEnhancement/manifestSchema.js +307 -0
  29. package/dist/promptEnhancement/redaction.d.ts +17 -0
  30. package/dist/promptEnhancement/redaction.d.ts.map +1 -0
  31. package/dist/promptEnhancement/redaction.js +66 -0
  32. package/dist/promptEnhancement/store.d.ts +64 -0
  33. package/dist/promptEnhancement/store.d.ts.map +1 -0
  34. package/dist/promptEnhancement/store.js +409 -0
  35. package/dist/qualityIntelligence/candidatesArtifact.d.ts +74 -0
  36. package/dist/qualityIntelligence/candidatesArtifact.d.ts.map +1 -0
  37. package/dist/qualityIntelligence/candidatesArtifact.js +258 -0
  38. package/dist/qualityIntelligence/companionStore.d.ts +37 -0
  39. package/dist/qualityIntelligence/companionStore.d.ts.map +1 -0
  40. package/dist/qualityIntelligence/companionStore.js +158 -0
  41. package/dist/qualityIntelligence/figmaSnapshot/schema.d.ts +123 -0
  42. package/dist/qualityIntelligence/figmaSnapshot/schema.d.ts.map +1 -0
  43. package/dist/qualityIntelligence/figmaSnapshot/schema.js +163 -0
  44. package/dist/qualityIntelligence/figmaSnapshot/store.d.ts +144 -0
  45. package/dist/qualityIntelligence/figmaSnapshot/store.d.ts.map +1 -0
  46. package/dist/qualityIntelligence/figmaSnapshot/store.js +898 -0
  47. package/dist/qualityIntelligence/index.d.ts +18 -0
  48. package/dist/qualityIntelligence/index.d.ts.map +1 -0
  49. package/dist/qualityIntelligence/index.js +21 -0
  50. package/dist/qualityIntelligence/manifestSchema.d.ts +154 -0
  51. package/dist/qualityIntelligence/manifestSchema.d.ts.map +1 -0
  52. package/dist/qualityIntelligence/manifestSchema.js +70 -0
  53. package/dist/qualityIntelligence/redaction.d.ts +10 -0
  54. package/dist/qualityIntelligence/redaction.d.ts.map +1 -0
  55. package/dist/qualityIntelligence/redaction.js +103 -0
  56. package/dist/qualityIntelligence/retention.d.ts +71 -0
  57. package/dist/qualityIntelligence/retention.d.ts.map +1 -0
  58. package/dist/qualityIntelligence/retention.js +287 -0
  59. package/dist/qualityIntelligence/retentionPolicy.d.ts +10 -0
  60. package/dist/qualityIntelligence/retentionPolicy.d.ts.map +1 -0
  61. package/dist/qualityIntelligence/retentionPolicy.js +38 -0
  62. package/dist/qualityIntelligence/store.d.ts +95 -0
  63. package/dist/qualityIntelligence/store.d.ts.map +1 -0
  64. package/dist/qualityIntelligence/store.js +483 -0
  65. package/dist/redaction.d.ts +2 -0
  66. package/dist/redaction.d.ts.map +1 -0
  67. package/dist/redaction.js +4 -0
  68. package/dist/report.d.ts +17 -0
  69. package/dist/report.d.ts.map +1 -0
  70. package/dist/report.js +50 -0
  71. package/dist/retention.d.ts +4 -0
  72. package/dist/retention.d.ts.map +1 -0
  73. package/dist/retention.js +95 -0
  74. package/dist/runid.d.ts +2 -0
  75. package/dist/runid.d.ts.map +1 -0
  76. package/dist/runid.js +4 -0
  77. package/dist/side-file.d.ts +9 -0
  78. package/dist/side-file.d.ts.map +1 -0
  79. package/dist/side-file.js +102 -0
  80. package/dist/store.d.ts +8 -0
  81. package/dist/store.d.ts.map +1 -0
  82. package/dist/store.js +332 -0
  83. package/dist/types.d.ts +3 -0
  84. package/dist/types.d.ts.map +1 -0
  85. package/dist/types.js +5 -0
  86. package/dist/version.d.ts +2 -0
  87. package/dist/version.d.ts.map +1 -0
  88. package/dist/version.js +1 -0
  89. package/dist/workflow-evidence.d.ts +36 -0
  90. package/dist/workflow-evidence.d.ts.map +1 -0
  91. package/dist/workflow-evidence.js +158 -0
  92. 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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"}