@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,409 @@
|
|
|
1
|
+
// Local-state store for Prompt Enhancement runs (Epic #1307, Issue #1313; ADR-0044 §1/§5).
|
|
2
|
+
//
|
|
3
|
+
// Extends the existing `keiko-evidence` JSON-on-disk discipline (ADR-0023 D7 "extend, don't fork"),
|
|
4
|
+
// mirroring the Quality Intelligence store: each run is persisted as one schema-validated JSON file
|
|
5
|
+
// `<runId>.pe.json` under a `pe/` subdirectory of the evidence base dir, kept separate from the
|
|
6
|
+
// run-level evidence manifests and the QI sub-manifests.
|
|
7
|
+
//
|
|
8
|
+
// The record path is record → redact → hash → validate → write:
|
|
9
|
+
// 1. every free-text leaf is passed through the security redactor (redaction by construction);
|
|
10
|
+
// 2. per-group SHA-256 integrity hashes are computed over the redacted groups;
|
|
11
|
+
// 3. the (totals, collection-length) invariant is asserted;
|
|
12
|
+
// 4. the manifest is written atomically (O_EXCL temp + rename) inside a realpath-contained base dir.
|
|
13
|
+
// On read the schema gate, the totals invariant, and the integrity hashes are all re-checked, so a
|
|
14
|
+
// tampered or corrupt manifest fails closed.
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
import { chmodSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
17
|
+
import { join, resolve } from "node:path";
|
|
18
|
+
import { resolveWithinWorkspace } from "@oscharko-dev/keiko-workspace";
|
|
19
|
+
import { nodeWorkspaceFs } from "@oscharko-dev/keiko-workspace/internal/fs";
|
|
20
|
+
import { assertValidRunId, sha256Hex } from "@oscharko-dev/keiko-security";
|
|
21
|
+
import { EvidenceReadError, EvidenceWriteError } from "../errors.js";
|
|
22
|
+
import { PROMPT_ENHANCEMENT_EVIDENCE_SCHEMA_VERSION, validatePromptEnhancementEvidenceManifest, } from "./manifestSchema.js";
|
|
23
|
+
import { redactPromptEnhancementEvidence, } from "./redaction.js";
|
|
24
|
+
// `pe/` subdir of the evidence base; chosen so `listEvidence()` and the QI `list()` never see Prompt
|
|
25
|
+
// Enhancement manifests by accident — different layer, different shape.
|
|
26
|
+
export const PE_SUBDIR = "pe";
|
|
27
|
+
const PE_MANIFEST_SUFFIX = ".pe.json";
|
|
28
|
+
const PE_DIR_MODE = 0o700;
|
|
29
|
+
const DEFAULT_INPUT_EXCERPT_MAX_CHARS = 2_000;
|
|
30
|
+
function sha256OfJson(value) {
|
|
31
|
+
return sha256Hex(JSON.stringify(value));
|
|
32
|
+
}
|
|
33
|
+
function buildIntegrityHashes(enhancedOutput, appliedRules, candidateScores, record) {
|
|
34
|
+
return {
|
|
35
|
+
enhancedOutput: sha256OfJson(enhancedOutput),
|
|
36
|
+
appliedRules: sha256OfJson(appliedRules),
|
|
37
|
+
candidateScores: sha256OfJson(candidateScores),
|
|
38
|
+
record: sha256OfJson(record),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function buildManifestIntegrityHashes(manifest) {
|
|
42
|
+
return buildIntegrityHashes({
|
|
43
|
+
enhancedPromptId: manifest.enhancedPromptId,
|
|
44
|
+
enhancedPromptTextRedacted: manifest.enhancedPromptTextRedacted,
|
|
45
|
+
}, {
|
|
46
|
+
appliedSafetyRules: manifest.appliedSafetyRules,
|
|
47
|
+
appliedGroundingDirectives: manifest.appliedGroundingDirectives,
|
|
48
|
+
assumptions: manifest.assumptions,
|
|
49
|
+
}, manifest.candidateScores, manifest);
|
|
50
|
+
}
|
|
51
|
+
function withoutIntegrityHashes(manifest) {
|
|
52
|
+
const record = { ...manifest };
|
|
53
|
+
delete record.integrityHashes;
|
|
54
|
+
return record;
|
|
55
|
+
}
|
|
56
|
+
function withRecomputedIntegrityHashes(manifest) {
|
|
57
|
+
return { ...manifest, integrityHashes: buildManifestIntegrityHashes(manifest) };
|
|
58
|
+
}
|
|
59
|
+
function truncate(value, max) {
|
|
60
|
+
return value.length <= max ? value : value.slice(0, max);
|
|
61
|
+
}
|
|
62
|
+
function assembleManifest(input, redacted, summary, inputRedactedFingerprintSha256) {
|
|
63
|
+
const manifestWithoutHashes = {
|
|
64
|
+
peEvidenceSchemaVersion: PROMPT_ENHANCEMENT_EVIDENCE_SCHEMA_VERSION,
|
|
65
|
+
runId: input.runId,
|
|
66
|
+
recordedAt: input.recordedAt,
|
|
67
|
+
requestId: input.requestId,
|
|
68
|
+
status: input.status,
|
|
69
|
+
inputRedactedFingerprintSha256,
|
|
70
|
+
inputExcerptRedacted: redacted.inputExcerptRedacted,
|
|
71
|
+
enhancedPromptId: input.enhancedPromptId,
|
|
72
|
+
enhancedPromptTextRedacted: redacted.enhancedPromptTextRedacted,
|
|
73
|
+
appliedSafetyRules: redacted.appliedSafetyRules,
|
|
74
|
+
appliedGroundingDirectives: input.appliedGroundingDirectives,
|
|
75
|
+
assumptions: redacted.assumptions,
|
|
76
|
+
candidateScores: input.candidateScores,
|
|
77
|
+
safety: input.safety,
|
|
78
|
+
modelMetadata: redacted.modelMetadata,
|
|
79
|
+
redactionSummary: summary,
|
|
80
|
+
totals: {
|
|
81
|
+
candidateScores: input.candidateScores.length,
|
|
82
|
+
appliedSafetyRules: redacted.appliedSafetyRules.length,
|
|
83
|
+
assumptions: redacted.assumptions.length,
|
|
84
|
+
safetyFindings: input.safety.findingCodes.length,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
return withRecomputedIntegrityHashes(manifestWithoutHashes);
|
|
88
|
+
}
|
|
89
|
+
export function buildPromptEnhancementEvidenceManifest(input, redaction = {}) {
|
|
90
|
+
assertValidRunId(input.runId);
|
|
91
|
+
const excerptMax = input.inputExcerptMaxChars ?? DEFAULT_INPUT_EXCERPT_MAX_CHARS;
|
|
92
|
+
// Redact every free-text leaf BEFORE assembly or hashing (redaction by construction). Ids, enums,
|
|
93
|
+
// and numbers (runId, requestId, candidate ids/profiles, scores, finding codes) carry no free text
|
|
94
|
+
// and stay outside the redactor — matching the QI store's redaction scope.
|
|
95
|
+
const { redacted, summary } = redactPromptEnhancementEvidence({
|
|
96
|
+
originalInputRedacted: input.originalInput,
|
|
97
|
+
inputExcerptRedacted: "",
|
|
98
|
+
enhancedPromptTextRedacted: input.enhancedPromptText,
|
|
99
|
+
appliedSafetyRules: input.appliedSafetyRules,
|
|
100
|
+
assumptions: input.assumptions,
|
|
101
|
+
modelMetadata: input.modelMetadata,
|
|
102
|
+
}, redaction);
|
|
103
|
+
const inputExcerptRedacted = truncate(redacted.originalInputRedacted, excerptMax);
|
|
104
|
+
const inputRedactedFingerprintSha256 = sha256Hex(redacted.originalInputRedacted);
|
|
105
|
+
const manifest = assembleManifest(input, { ...redacted, inputExcerptRedacted }, summary, inputRedactedFingerprintSha256);
|
|
106
|
+
assertManifestWriteReady(manifest);
|
|
107
|
+
return { manifest };
|
|
108
|
+
}
|
|
109
|
+
function foldRedactionSummary(base, add) {
|
|
110
|
+
const patternsMatched = { ...base.patternsMatched };
|
|
111
|
+
for (const [key, count] of Object.entries(add.patternsMatched)) {
|
|
112
|
+
patternsMatched[key] = (patternsMatched[key] ?? 0) + count;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
totalStringsScanned: base.totalStringsScanned + add.totalStringsScanned,
|
|
116
|
+
stringsRedacted: base.stringsRedacted + add.stringsRedacted,
|
|
117
|
+
patternsMatched,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function assertManifestWriteReady(manifest) {
|
|
121
|
+
const validation = validatePromptEnhancementEvidenceManifest(manifest);
|
|
122
|
+
if (!validation.ok) {
|
|
123
|
+
throw new EvidenceWriteError(`PE manifest schema invalid: ${validation.reason ?? "unknown"}`);
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
assertManifestIntegrity(manifest);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
throw new EvidenceWriteError(error instanceof Error ? error.message : "PE manifest integrity invalid");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function sanitizeManifestForPersistence(manifest) {
|
|
133
|
+
assertValidRunId(manifest.runId);
|
|
134
|
+
const { redacted, summary } = redactPromptEnhancementEvidence(withoutIntegrityHashes(manifest));
|
|
135
|
+
const redactionSummary = summary.stringsRedacted > 0
|
|
136
|
+
? foldRedactionSummary(redacted.redactionSummary, summary)
|
|
137
|
+
: redacted.redactionSummary;
|
|
138
|
+
const sanitized = withRecomputedIntegrityHashes({ ...redacted, redactionSummary });
|
|
139
|
+
assertManifestWriteReady(sanitized);
|
|
140
|
+
return sanitized;
|
|
141
|
+
}
|
|
142
|
+
// ─── In-memory store (tests + future port-injected callers) ─────────────────────────
|
|
143
|
+
export function createInMemoryPromptEnhancementLocalStore() {
|
|
144
|
+
const data = new Map();
|
|
145
|
+
return {
|
|
146
|
+
record: (manifest) => {
|
|
147
|
+
const sanitized = sanitizeManifestForPersistence(manifest);
|
|
148
|
+
data.set(sanitized.runId, sanitized);
|
|
149
|
+
return `${sanitized.runId}${PE_MANIFEST_SUFFIX}`;
|
|
150
|
+
},
|
|
151
|
+
load: (runId) => {
|
|
152
|
+
assertValidRunId(runId);
|
|
153
|
+
const manifest = data.get(runId);
|
|
154
|
+
return manifest === undefined
|
|
155
|
+
? undefined
|
|
156
|
+
: parseAndValidateManifest(JSON.stringify(manifest));
|
|
157
|
+
},
|
|
158
|
+
list: () => [...data.keys()].sort(),
|
|
159
|
+
location: (runId) => {
|
|
160
|
+
assertValidRunId(runId);
|
|
161
|
+
return `${runId}${PE_MANIFEST_SUFFIX}`;
|
|
162
|
+
},
|
|
163
|
+
delete: (runId) => {
|
|
164
|
+
assertValidRunId(runId);
|
|
165
|
+
return data.delete(runId);
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
// ─── Integrity assertions (on read) ──────────────────────────────────────────────────
|
|
170
|
+
function assertHashMatches(label, expected, stored) {
|
|
171
|
+
if (expected !== stored) {
|
|
172
|
+
throw new EvidenceReadError(`PE manifest ${label} integrity hash mismatch`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function assertManifestIntegrity(manifest) {
|
|
176
|
+
const expectations = [
|
|
177
|
+
["candidateScores", manifest.totals.candidateScores, manifest.candidateScores.length],
|
|
178
|
+
["appliedSafetyRules", manifest.totals.appliedSafetyRules, manifest.appliedSafetyRules.length],
|
|
179
|
+
["assumptions", manifest.totals.assumptions, manifest.assumptions.length],
|
|
180
|
+
["safetyFindings", manifest.totals.safetyFindings, manifest.safety.findingCodes.length],
|
|
181
|
+
];
|
|
182
|
+
for (const [label, total, length] of expectations) {
|
|
183
|
+
if (total !== length) {
|
|
184
|
+
throw new EvidenceReadError(`PE manifest totals.${label} (${String(total)}) does not match collection length (${String(length)})`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const expected = buildManifestIntegrityHashes(withoutIntegrityHashes(manifest));
|
|
188
|
+
assertHashMatches("enhancedOutput", expected.enhancedOutput, manifest.integrityHashes.enhancedOutput);
|
|
189
|
+
assertHashMatches("appliedRules", expected.appliedRules, manifest.integrityHashes.appliedRules);
|
|
190
|
+
assertHashMatches("candidateScores", expected.candidateScores, manifest.integrityHashes.candidateScores);
|
|
191
|
+
assertHashMatches("record", expected.record, manifest.integrityHashes.record);
|
|
192
|
+
}
|
|
193
|
+
function parseAndValidateManifest(json) {
|
|
194
|
+
let parsed;
|
|
195
|
+
try {
|
|
196
|
+
parsed = JSON.parse(json);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
throw new EvidenceReadError(`PE manifest is not valid JSON: ${error instanceof Error ? error.message : "unknown"}`);
|
|
200
|
+
}
|
|
201
|
+
const validation = validatePromptEnhancementEvidenceManifest(parsed);
|
|
202
|
+
if (!validation.ok) {
|
|
203
|
+
throw new EvidenceReadError(`PE manifest schema invalid: ${validation.reason ?? "unknown"}`);
|
|
204
|
+
}
|
|
205
|
+
const manifest = parsed;
|
|
206
|
+
assertManifestIntegrity(manifest);
|
|
207
|
+
return manifest;
|
|
208
|
+
}
|
|
209
|
+
// ─── Node adapter ──────────────────────────────────────────────────────────────────
|
|
210
|
+
function prepareBaseDir(baseDir, fs) {
|
|
211
|
+
try {
|
|
212
|
+
mkdirSync(baseDir, { recursive: true, mode: PE_DIR_MODE });
|
|
213
|
+
return fs.realPath(baseDir);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
throw new EvidenceWriteError(`cannot create PE evidence directory: ${error instanceof Error ? error.message : "unknown"}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function existingBaseDir(baseDir, fs) {
|
|
220
|
+
if (!fs.exists(baseDir)) {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
return fs.realPath(baseDir);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
throw new EvidenceReadError(`cannot read PE evidence directory: ${error instanceof Error ? error.message : "unknown"}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function lexicalManifestPath(runId, realBase) {
|
|
231
|
+
assertValidRunId(runId);
|
|
232
|
+
return resolveWithinWorkspace(realBase, `${runId}${PE_MANIFEST_SUFFIX}`);
|
|
233
|
+
}
|
|
234
|
+
function isManifestName(name) {
|
|
235
|
+
if (!name.endsWith(PE_MANIFEST_SUFFIX)) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
const runId = name.slice(0, name.length - PE_MANIFEST_SUFFIX.length);
|
|
239
|
+
try {
|
|
240
|
+
assertValidRunId(runId);
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function isSingleLinkRegularFile(path, fs) {
|
|
248
|
+
try {
|
|
249
|
+
const stat = fs.stat(path);
|
|
250
|
+
return stat.isFile && (stat.hardLinkCount ?? 1) <= 1;
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
throw new EvidenceReadError(`cannot inspect PE manifest: ${error instanceof Error ? error.message : "unknown"}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function assertWritableManifestEntry(target, fs) {
|
|
257
|
+
const entry = lstatSync(target, { throwIfNoEntry: false });
|
|
258
|
+
if (entry === undefined)
|
|
259
|
+
return;
|
|
260
|
+
if (!entry.isFile() || !isSingleLinkRegularFile(target, fs)) {
|
|
261
|
+
throw new EvidenceWriteError("cannot overwrite a non-ledger PE manifest");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function listRunIds(realBase, fs) {
|
|
265
|
+
const runIds = [];
|
|
266
|
+
try {
|
|
267
|
+
for (const entry of readdirSync(realBase, { withFileTypes: true })) {
|
|
268
|
+
if (entry.isSymbolicLink() ||
|
|
269
|
+
!entry.isFile() ||
|
|
270
|
+
!isManifestName(entry.name) ||
|
|
271
|
+
!isSingleLinkRegularFile(join(realBase, entry.name), fs)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
runIds.push(entry.name.slice(0, entry.name.length - PE_MANIFEST_SUFFIX.length));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
throw new EvidenceReadError(`cannot list PE manifests: ${error instanceof Error ? error.message : "unknown"}`);
|
|
279
|
+
}
|
|
280
|
+
return runIds.sort();
|
|
281
|
+
}
|
|
282
|
+
function atomicWriteManifest(target, json, randomSuffix) {
|
|
283
|
+
const temp = `${target}.${randomSuffix()}.tmp`;
|
|
284
|
+
try {
|
|
285
|
+
writeFileSync(temp, json, { encoding: "utf8", flag: "wx" });
|
|
286
|
+
try {
|
|
287
|
+
chmodSync(temp, 0o600);
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// ignore; not all filesystems support chmod (e.g. Windows)
|
|
291
|
+
}
|
|
292
|
+
renameSync(temp, target);
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
rmSync(temp, { force: true });
|
|
296
|
+
throw new EvidenceWriteError(`PE manifest write failed: ${error instanceof Error ? error.message : "unknown"}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
function recordManifest(baseDir, fs, randomSuffix, manifest) {
|
|
300
|
+
const sanitized = sanitizeManifestForPersistence(manifest);
|
|
301
|
+
const realBase = prepareBaseDir(baseDir, fs);
|
|
302
|
+
const target = lexicalManifestPath(sanitized.runId, realBase);
|
|
303
|
+
assertWritableManifestEntry(target, fs);
|
|
304
|
+
atomicWriteManifest(target, JSON.stringify(sanitized), randomSuffix);
|
|
305
|
+
return target;
|
|
306
|
+
}
|
|
307
|
+
function loadManifest(baseDir, fs, runId) {
|
|
308
|
+
assertValidRunId(runId);
|
|
309
|
+
const realBase = existingBaseDir(baseDir, fs);
|
|
310
|
+
if (realBase === undefined) {
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
const target = join(realBase, `${runId}${PE_MANIFEST_SUFFIX}`);
|
|
314
|
+
try {
|
|
315
|
+
if (lstatSync(target, { throwIfNoEntry: false })?.isFile() !== true) {
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
318
|
+
if (!isSingleLinkRegularFile(target, fs)) {
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
return parseAndValidateManifest(readFileSync(target, "utf8"));
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
if (error instanceof EvidenceReadError) {
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
throw new EvidenceReadError(`cannot read PE manifest: ${error instanceof Error ? error.message : "unknown"}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function deleteManifest(baseDir, fs, runId) {
|
|
331
|
+
assertValidRunId(runId);
|
|
332
|
+
const realBase = existingBaseDir(baseDir, fs);
|
|
333
|
+
if (realBase === undefined) {
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
const target = lexicalManifestPath(runId, realBase);
|
|
337
|
+
if (lstatSync(target, { throwIfNoEntry: false })?.isFile() !== true) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
if (!isSingleLinkRegularFile(target, fs)) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
rmSync(target, { force: true });
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Build a Prompt Enhancement store that writes under `<evidenceDir>/pe/`. The caller passes the SAME
|
|
348
|
+
* evidence dir it would pass to `createNodeEvidenceStore`; the store layers the `pe/` subdir itself.
|
|
349
|
+
*/
|
|
350
|
+
export function createNodePromptEnhancementLocalStore(evidenceDir, options = {}) {
|
|
351
|
+
const baseDir = join(evidenceDir, PE_SUBDIR);
|
|
352
|
+
const fs = options.fs ?? nodeWorkspaceFs;
|
|
353
|
+
const randomSuffix = options.randomSuffix ?? randomUUID;
|
|
354
|
+
return {
|
|
355
|
+
record: (manifest) => recordManifest(baseDir, fs, randomSuffix, manifest),
|
|
356
|
+
load: (runId) => loadManifest(baseDir, fs, runId),
|
|
357
|
+
list: () => {
|
|
358
|
+
const realBase = existingBaseDir(baseDir, fs);
|
|
359
|
+
return realBase === undefined ? [] : listRunIds(realBase, fs);
|
|
360
|
+
},
|
|
361
|
+
location: (runId) => {
|
|
362
|
+
assertValidRunId(runId);
|
|
363
|
+
const realBase = existingBaseDir(baseDir, fs);
|
|
364
|
+
return realBase === undefined
|
|
365
|
+
? join(resolve(baseDir), `${runId}${PE_MANIFEST_SUFFIX}`)
|
|
366
|
+
: lexicalManifestPath(runId, realBase);
|
|
367
|
+
},
|
|
368
|
+
delete: (runId) => deleteManifest(baseDir, fs, runId),
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
// ─── Public CRUD API ───────────────────────────────────────────────────────────────
|
|
372
|
+
function resolveStore(options) {
|
|
373
|
+
if (options.store !== undefined) {
|
|
374
|
+
return options.store;
|
|
375
|
+
}
|
|
376
|
+
if (options.evidenceDir !== undefined) {
|
|
377
|
+
return createNodePromptEnhancementLocalStore(options.evidenceDir);
|
|
378
|
+
}
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Build and persist a Prompt Enhancement run record. Redacts every free-text leaf, hashes the redacted
|
|
383
|
+
* groups, validates the totals invariant, then writes the manifest through the resolved store. The
|
|
384
|
+
* store is supplied via `options.store` (explicit, e.g. in-memory for tests) or `options.evidenceDir`
|
|
385
|
+
* (resolve to a node adapter); one MUST be provided.
|
|
386
|
+
*/
|
|
387
|
+
export function recordPromptEnhancementRun(input, options = {}) {
|
|
388
|
+
const store = resolveStore(options);
|
|
389
|
+
if (store === undefined) {
|
|
390
|
+
throw new EvidenceWriteError("recordPromptEnhancementRun requires options.store or options.evidenceDir");
|
|
391
|
+
}
|
|
392
|
+
const { manifest } = buildPromptEnhancementEvidenceManifest(input, options.redaction ?? {});
|
|
393
|
+
return { manifest, location: store.record(manifest) };
|
|
394
|
+
}
|
|
395
|
+
function resolveLoadStore(options) {
|
|
396
|
+
if (options.store !== undefined) {
|
|
397
|
+
return options.store;
|
|
398
|
+
}
|
|
399
|
+
if (options.evidenceDir !== undefined) {
|
|
400
|
+
return createNodePromptEnhancementLocalStore(options.evidenceDir);
|
|
401
|
+
}
|
|
402
|
+
throw new EvidenceReadError("PE load/list requires options.store or options.evidenceDir");
|
|
403
|
+
}
|
|
404
|
+
export function loadPromptEnhancementRun(runId, options = {}) {
|
|
405
|
+
return resolveLoadStore(options).load(runId);
|
|
406
|
+
}
|
|
407
|
+
export function listPromptEnhancementRuns(options = {}) {
|
|
408
|
+
return resolveLoadStore(options).list();
|
|
409
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { QualityIntelligence } from "@oscharko-dev/keiko-contracts";
|
|
2
|
+
export declare const QUALITY_INTELLIGENCE_CANDIDATES_SCHEMA_VERSION: 1;
|
|
3
|
+
export declare const CANDIDATES_SUFFIX = ".candidates.json";
|
|
4
|
+
export interface QualityIntelligenceCandidateRow {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly title: string;
|
|
7
|
+
readonly preconditions: readonly string[];
|
|
8
|
+
readonly steps: readonly string[];
|
|
9
|
+
readonly expectedResults: readonly string[];
|
|
10
|
+
readonly priority: QualityIntelligence.QualityIntelligencePriority;
|
|
11
|
+
readonly riskClass: QualityIntelligence.QualityIntelligenceRiskClass;
|
|
12
|
+
readonly tags: readonly string[];
|
|
13
|
+
readonly status: QualityIntelligence.QualityIntelligenceTestCaseStatus;
|
|
14
|
+
readonly derivedFromAtomIds: readonly string[];
|
|
15
|
+
readonly qualityVerdict?: QualityIntelligenceCandidateQualityVerdict;
|
|
16
|
+
}
|
|
17
|
+
export interface QualityIntelligenceCandidateQualityVerdict {
|
|
18
|
+
readonly verdict: QualityIntelligence.TestQualityJudgeVerdict["verdict"];
|
|
19
|
+
readonly score: number;
|
|
20
|
+
readonly dimensions: readonly QualityIntelligence.TestQualityRubricDimension[];
|
|
21
|
+
readonly overallRationale: string;
|
|
22
|
+
}
|
|
23
|
+
export interface QualityIntelligenceCandidatesArtifact {
|
|
24
|
+
readonly qiCandidatesSchemaVersion: typeof QUALITY_INTELLIGENCE_CANDIDATES_SCHEMA_VERSION;
|
|
25
|
+
readonly runId: string;
|
|
26
|
+
readonly generatedAt: string;
|
|
27
|
+
readonly candidates: readonly QualityIntelligenceCandidateRow[];
|
|
28
|
+
readonly editedRevisions?: readonly QualityIntelligence.QualityIntelligenceCandidateEditedRevision[];
|
|
29
|
+
}
|
|
30
|
+
export interface QualityIntelligenceCandidatesStoreOptions {
|
|
31
|
+
readonly evidenceDir: string;
|
|
32
|
+
}
|
|
33
|
+
export interface RecordQualityIntelligenceCandidatesInput {
|
|
34
|
+
readonly runId: string;
|
|
35
|
+
readonly generatedAt: string;
|
|
36
|
+
readonly candidates: readonly QualityIntelligence.QualityIntelligenceTestCaseCandidate[];
|
|
37
|
+
readonly editedRevisions?: readonly QualityIntelligence.QualityIntelligenceCandidateEditedRevision[] | undefined;
|
|
38
|
+
readonly evidenceDir: string;
|
|
39
|
+
/**
|
|
40
|
+
* Required defence-in-depth redactor applied to every string leaf before persist. The server
|
|
41
|
+
* passes the live audit redactor (`deps.redactor`); tests pass an explicit identity. Making it
|
|
42
|
+
* mandatory keeps a forgetful caller from writing an unredacted candidate body to disk (#284).
|
|
43
|
+
*/
|
|
44
|
+
readonly redact: (value: unknown) => unknown;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Persist the generated candidate bodies for a run. Redacts every string leaf first, then writes
|
|
48
|
+
* the companion artifact atomically. Returns the on-disk location.
|
|
49
|
+
*/
|
|
50
|
+
export declare const recordQualityIntelligenceCandidates: (input: RecordQualityIntelligenceCandidatesInput) => string;
|
|
51
|
+
export declare const loadQualityIntelligenceCandidates: (runId: string, options: QualityIntelligenceCandidatesStoreOptions) => QualityIntelligenceCandidatesArtifact | undefined;
|
|
52
|
+
export declare const deleteQualityIntelligenceCandidates: (runId: string, options: QualityIntelligenceCandidatesStoreOptions) => boolean;
|
|
53
|
+
type EditableFields = QualityIntelligence.QualityIntelligenceCandidateEditableFields;
|
|
54
|
+
type EditProvenance = QualityIntelligence.QualityIntelligenceCandidateEditProvenance;
|
|
55
|
+
export type QualityIntelligenceCandidateEditErrorReason = "artifact-not-found" | "candidate-not-found" | "no-edited-fields";
|
|
56
|
+
export type ApplyQualityIntelligenceCandidateEditResult = {
|
|
57
|
+
readonly ok: true;
|
|
58
|
+
readonly candidate: QualityIntelligenceCandidateRow;
|
|
59
|
+
readonly changed: boolean;
|
|
60
|
+
} | {
|
|
61
|
+
readonly ok: false;
|
|
62
|
+
readonly reason: QualityIntelligenceCandidateEditErrorReason;
|
|
63
|
+
};
|
|
64
|
+
export interface ApplyQualityIntelligenceCandidateEditInput {
|
|
65
|
+
readonly runId: string;
|
|
66
|
+
readonly candidateId: string;
|
|
67
|
+
readonly editedFields: EditableFields;
|
|
68
|
+
readonly provenance: EditProvenance;
|
|
69
|
+
readonly evidenceDir: string;
|
|
70
|
+
readonly redact: (value: unknown) => unknown;
|
|
71
|
+
}
|
|
72
|
+
export declare const applyQualityIntelligenceCandidateEdit: (input: ApplyQualityIntelligenceCandidateEditInput) => ApplyQualityIntelligenceCandidateEditResult;
|
|
73
|
+
export {};
|
|
74
|
+
//# sourceMappingURL=candidatesArtifact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"candidatesArtifact.d.ts","sourceRoot":"","sources":["../../src/qualityIntelligence/candidatesArtifact.ts"],"names":[],"mappings":"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"}
|