crewhaus 0.1.8 → 0.2.1

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 (138) hide show
  1. package/README.md +10 -3
  2. package/dist/advice-apply.d.ts +182 -0
  3. package/dist/advice-apply.js +286 -0
  4. package/dist/advise-rules.d.ts +348 -0
  5. package/dist/advise-rules.js +905 -0
  6. package/dist/alert-sink.d.ts +48 -0
  7. package/dist/alert-sink.js +86 -0
  8. package/dist/approval-gate.d.ts +127 -0
  9. package/dist/approval-gate.js +254 -0
  10. package/dist/audit-verify.d.ts +69 -0
  11. package/dist/audit-verify.js +97 -0
  12. package/dist/autodistill.d.ts +113 -0
  13. package/dist/autodistill.js +256 -0
  14. package/dist/channel-provision.d.ts +360 -0
  15. package/dist/channel-provision.js +881 -0
  16. package/dist/ci-scaffold.d.ts +31 -0
  17. package/dist/ci-scaffold.js +343 -0
  18. package/dist/compile-check.d.ts +90 -0
  19. package/dist/compile-check.js +285 -0
  20. package/dist/compliance-schedule.d.ts +35 -0
  21. package/dist/compliance-schedule.js +36 -0
  22. package/dist/context-pressure.d.ts +80 -0
  23. package/dist/context-pressure.js +166 -0
  24. package/dist/dataset-mine.d.ts +173 -0
  25. package/dist/dataset-mine.js +404 -0
  26. package/dist/datasets.d.ts +124 -0
  27. package/dist/datasets.js +260 -0
  28. package/dist/deploy-canary.d.ts +83 -0
  29. package/dist/deploy-canary.js +87 -0
  30. package/dist/doctor-checks.d.ts +33 -0
  31. package/dist/doctor-checks.js +92 -0
  32. package/dist/doctor-detect.d.ts +108 -0
  33. package/dist/doctor-detect.js +214 -0
  34. package/dist/doctor-fix.d.ts +81 -0
  35. package/dist/doctor-fix.js +164 -0
  36. package/dist/egress-triage.d.ts +121 -0
  37. package/dist/egress-triage.js +261 -0
  38. package/dist/eval-bridge.d.ts +114 -0
  39. package/dist/eval-bridge.js +158 -0
  40. package/dist/eval-coverage.d.ts +140 -0
  41. package/dist/eval-coverage.js +428 -0
  42. package/dist/eval-history.d.ts +48 -0
  43. package/dist/eval-history.js +157 -0
  44. package/dist/eval-matrix.d.ts +80 -0
  45. package/dist/eval-matrix.js +182 -0
  46. package/dist/eval-sentinel.d.ts +65 -0
  47. package/dist/eval-sentinel.js +132 -0
  48. package/dist/faq.d.ts +68 -0
  49. package/dist/faq.js +168 -0
  50. package/dist/feedback.d.ts +9 -2
  51. package/dist/feedback.js +17 -7
  52. package/dist/fewshot.d.ts +83 -0
  53. package/dist/fewshot.js +158 -0
  54. package/dist/fleet.d.ts +207 -0
  55. package/dist/fleet.js +488 -0
  56. package/dist/flywheel.d.ts +193 -0
  57. package/dist/flywheel.js +519 -0
  58. package/dist/graders-suggest.d.ts +186 -0
  59. package/dist/graders-suggest.js +658 -0
  60. package/dist/incident.d.ts +99 -0
  61. package/dist/incident.js +217 -0
  62. package/dist/index.d.ts +9 -1
  63. package/dist/index.js +11664 -963
  64. package/dist/init-interactive.d.ts +105 -0
  65. package/dist/init-interactive.js +208 -0
  66. package/dist/intents.d.ts +105 -0
  67. package/dist/intents.js +292 -0
  68. package/dist/judge-calibrate.d.ts +137 -0
  69. package/dist/judge-calibrate.js +247 -0
  70. package/dist/justification-calibrate.d.ts +150 -0
  71. package/dist/justification-calibrate.js +262 -0
  72. package/dist/justification-gate.d.ts +27 -6
  73. package/dist/justification-gate.js +30 -6
  74. package/dist/knowledge-sync.d.ts +179 -0
  75. package/dist/knowledge-sync.js +551 -0
  76. package/dist/lessons.d.ts +87 -0
  77. package/dist/lessons.js +207 -0
  78. package/dist/lint.d.ts +127 -0
  79. package/dist/lint.js +226 -0
  80. package/dist/loadtest.d.ts +114 -0
  81. package/dist/loadtest.js +196 -0
  82. package/dist/marketplace-cli.d.ts +110 -0
  83. package/dist/marketplace-cli.js +250 -0
  84. package/dist/mcp-doctor.d.ts +121 -0
  85. package/dist/mcp-doctor.js +249 -0
  86. package/dist/model-scan.d.ts +116 -0
  87. package/dist/model-scan.js +226 -0
  88. package/dist/onchain-tune.d.ts +164 -0
  89. package/dist/onchain-tune.js +346 -0
  90. package/dist/permissions-suggest.d.ts +126 -0
  91. package/dist/permissions-suggest.js +333 -0
  92. package/dist/pii-tune.d.ts +107 -0
  93. package/dist/pii-tune.js +122 -0
  94. package/dist/propose.d.ts +117 -0
  95. package/dist/propose.js +184 -0
  96. package/dist/refresh-goldens.d.ts +82 -0
  97. package/dist/refresh-goldens.js +221 -0
  98. package/dist/regression-pin.d.ts +160 -0
  99. package/dist/regression-pin.js +281 -0
  100. package/dist/retention.d.ts +193 -0
  101. package/dist/retention.js +607 -0
  102. package/dist/retire.d.ts +118 -0
  103. package/dist/retire.js +291 -0
  104. package/dist/right-size.d.ts +100 -0
  105. package/dist/right-size.js +123 -0
  106. package/dist/route.d.ts +19 -0
  107. package/dist/route.js +90 -0
  108. package/dist/scaffold-evals.d.ts +138 -0
  109. package/dist/scaffold-evals.js +410 -0
  110. package/dist/scope-audit-drift.d.ts +139 -0
  111. package/dist/scope-audit-drift.js +260 -0
  112. package/dist/security-corpus.d.ts +237 -0
  113. package/dist/security-corpus.js +516 -0
  114. package/dist/security-digest.d.ts +174 -0
  115. package/dist/security-digest.js +656 -0
  116. package/dist/sessions-index.d.ts +27 -0
  117. package/dist/sessions-index.js +51 -0
  118. package/dist/slo-doctor.d.ts +67 -0
  119. package/dist/slo-doctor.js +119 -0
  120. package/dist/slo-sink.d.ts +96 -0
  121. package/dist/slo-sink.js +107 -0
  122. package/dist/spec-changelog.d.ts +102 -0
  123. package/dist/spec-changelog.js +237 -0
  124. package/dist/state-backup.d.ts +223 -0
  125. package/dist/state-backup.js +648 -0
  126. package/dist/tools-cli.d.ts +170 -0
  127. package/dist/tools-cli.js +300 -0
  128. package/dist/triage.d.ts +202 -0
  129. package/dist/triage.js +403 -0
  130. package/dist/upgrade.d.ts +57 -0
  131. package/dist/upgrade.js +113 -0
  132. package/dist/version.d.ts +6 -0
  133. package/dist/version.js +27 -0
  134. package/dist/voice-eval.d.ts +138 -0
  135. package/dist/voice-eval.js +309 -0
  136. package/dist/watch.d.ts +58 -0
  137. package/dist/watch.js +97 -0
  138. package/package.json +90 -65
@@ -0,0 +1,281 @@
1
+ /**
2
+ * Item 9 — pin every recovered failure into a permanent regression suite.
3
+ *
4
+ * When an optimize run is accepted, the samples that flipped fail→pass
5
+ * between the baseline eval run and the winning candidate's eval run are
6
+ * exactly the behaviors the accepted patch fixed. If the training dataset
7
+ * later churns, those regressions can silently return — so we pin them
8
+ * into a per-spec `<specName>-regressions` dataset in the Section 29
9
+ * registry (single `train` split; each pin unions the previous latest
10
+ * version's samples with the new recoveries, deduped by sample id), and
11
+ * `crewhaus eval` unions that suite into the loaded dataset by default.
12
+ *
13
+ * Kept in a side-effect-free module (the CLI entry file runs an argv
14
+ * switch on import) mirroring `eval-history.ts` / `datasets.ts`: all
15
+ * filesystem access goes through an injected `DatasetRegistry` or through
16
+ * eval-report's `loadRun` on caller-supplied run dirs. The failure-arbiter
17
+ * wiring (item 7, `triage.ts`) is the other consumer of
18
+ * {@link pinRecoveredSamples}: post-eval triage promotes the samples whose
19
+ * `ArbiterAction.promoteRegression` flag is set (bug-class failures,
20
+ * eval-optimizer-orchestrator/src/failure-arbiter.ts) with
21
+ * `source: "failure-arbiter"` provenance.
22
+ */
23
+ import { createHash } from "node:crypto";
24
+ import { latestVersion } from "@crewhaus/dataset-registry";
25
+ import { diffReports, loadRun } from "@crewhaus/eval-report";
26
+ import { nextVersion, overallDatasetHash } from "./datasets";
27
+ /** The per-spec regression suite's registry name. */
28
+ export function regressionSuiteName(specName) {
29
+ return `${specName}-regressions`;
30
+ }
31
+ /**
32
+ * dataset-registry's name grammar (spec `safeName` additionally allows
33
+ * spaces/colons, which the registry rejects). A spec whose name can't form
34
+ * a valid registry dataset name simply has no regression suite — both the
35
+ * pin and the union become no-ops instead of surfacing a registry error
36
+ * (with a one-line warning per invocation, so the no-op is never silent).
37
+ */
38
+ const REGISTRY_NAME_REGEX = /^[A-Za-z0-9_-][A-Za-z0-9_.-]*$/;
39
+ export function isRegistrySafeName(name) {
40
+ return REGISTRY_NAME_REGEX.test(name);
41
+ }
42
+ /** Default warning sink for the registry-unsafe-name no-ops. */
43
+ const warnToStderr = (line) => {
44
+ process.stderr.write(`${line}\n`);
45
+ };
46
+ /** The one-line "this degraded to a no-op" warning for a spec whose name
47
+ * can't map to the registry grammar (see {@link isRegistrySafeName}). */
48
+ function unsafeNameWarning(what, specName, suiteName) {
49
+ return (`[eval] ${what} skipped: spec name "${specName}" can't form a registry dataset name ` +
50
+ `("${suiteName}") — rename the spec to match ${REGISTRY_NAME_REGEX} to enable the regression suite`);
51
+ }
52
+ /**
53
+ * Append recovered samples to the per-spec regression suite as a new
54
+ * auto-bumped version: union of the previous latest version's samples and
55
+ * the new ones, deduped by sample id (existing pins win, so re-pinning the
56
+ * same recovery is idempotent and keeps its original provenance). All
57
+ * samples live in the `train` split — the suite is a single-split dataset.
58
+ * Provenance (optimize runId, pin date, source dataset) is recorded in each
59
+ * newly pinned sample's `metadata.regression_pin` (SampleSchema carries an
60
+ * open `metadata` record). No new samples → no-op: no version is written.
61
+ */
62
+ export async function pinRecoveredSamples(opts) {
63
+ const suiteName = regressionSuiteName(opts.specName);
64
+ if (opts.samples.length === 0)
65
+ return { suiteName, pinned: 0 };
66
+ if (!isRegistrySafeName(suiteName)) {
67
+ // There WERE samples to pin — surface the degradation instead of
68
+ // silently dropping them (one warning per invocation).
69
+ (opts.warn ?? warnToStderr)(unsafeNameWarning("regression pin", opts.specName, suiteName));
70
+ return { suiteName, pinned: 0 };
71
+ }
72
+ const prevVersion = await latestVersion(opts.registry, suiteName);
73
+ const prevTrain = prevVersion !== undefined
74
+ ? (await opts.registry.getRecord(suiteName, prevVersion)).splits.train
75
+ : [];
76
+ const byId = new Map(prevTrain.map((s) => [s.id, s]));
77
+ const pinnedAt = (opts.now?.() ?? new Date()).toISOString();
78
+ let pinned = 0;
79
+ for (const s of opts.samples) {
80
+ if (byId.has(s.id))
81
+ continue;
82
+ byId.set(s.id, {
83
+ ...s,
84
+ metadata: {
85
+ ...(s.metadata ?? {}),
86
+ regression_pin: {
87
+ optimizeRunId: opts.optimizeRunId,
88
+ pinnedAt,
89
+ sourceDataset: opts.sourceDataset,
90
+ ...(opts.source !== undefined ? { source: opts.source } : {}),
91
+ },
92
+ },
93
+ });
94
+ pinned += 1;
95
+ }
96
+ if (pinned === 0)
97
+ return { suiteName, pinned: 0 };
98
+ const version = nextVersion(await opts.registry.list(suiteName));
99
+ await opts.registry.put({
100
+ name: suiteName,
101
+ version,
102
+ splits: { train: [...byId.values()], dev: [] },
103
+ });
104
+ return { suiteName, pinned, version };
105
+ }
106
+ /**
107
+ * The optimize success path: diff the baseline eval run against the winning
108
+ * candidate's, extract `recoveries` (fail→pass sampleIds), look those up in
109
+ * the dataset that was used, and pin them via {@link pinRecoveredSamples}.
110
+ * Returns undefined when pinning is disabled or either run dir is missing
111
+ * (e.g. a fitness fn that doesn't report `runDir`).
112
+ */
113
+ export async function pinRecoveriesAfterOptimize(opts) {
114
+ if (!opts.pin)
115
+ return undefined;
116
+ if (opts.baselineRunDir === undefined || opts.candidateRunDir === undefined)
117
+ return undefined;
118
+ const prev = await loadRun(opts.baselineRunDir);
119
+ const next = await loadRun(opts.candidateRunDir);
120
+ const { diff } = diffReports(prev, next);
121
+ const recovered = [];
122
+ for (const r of diff.recoveries) {
123
+ const s = opts.samplesById.get(r.sampleId);
124
+ if (s !== undefined)
125
+ recovered.push(s);
126
+ }
127
+ return pinRecoveredSamples({
128
+ registry: opts.registry,
129
+ specName: opts.specName,
130
+ samples: recovered,
131
+ sourceDataset: opts.sourceDataset,
132
+ optimizeRunId: opts.optimizeRunId,
133
+ ...(opts.now !== undefined ? { now: opts.now } : {}),
134
+ });
135
+ }
136
+ /**
137
+ * Union the per-spec regression suite into an eval's dataset (dedupe by
138
+ * sample id; the primary dataset wins on collision). Returns undefined when
139
+ * disabled, when no suite exists, or when the primary IS the suite (a
140
+ * `registry:` ref to it, or a file whose basename matches it — an exported
141
+ * copy must not union the suite into itself).
142
+ *
143
+ * Item-3 interaction, by design: a union that ADDS samples changes the
144
+ * run's sample keyset, so the returned `datasetName` is suffixed
145
+ * `+regressions@vX` and the returned `datasetHash` folds the suite's
146
+ * content hash into the primary's. Run-index entries and (spec, dataset)
147
+ * baselines then key on the honest union lineage — the first ADDING union
148
+ * starts a new baseline lineage instead of tripping diffReports'
149
+ * keyset-mismatch guard against the pre-union baseline. A union that adds
150
+ * NOTHING (every suite sample already in the primary) keeps the primary
151
+ * identity untouched: the keyset didn't change, so rewriting the key would
152
+ * orphan the existing baseline and silently disarm the gate.
153
+ */
154
+ export async function applyRegressionUnion(opts) {
155
+ if (!opts.includeRegressions)
156
+ return undefined;
157
+ const suiteName = regressionSuiteName(opts.specName);
158
+ if (!isRegistrySafeName(suiteName)) {
159
+ (opts.warn ?? warnToStderr)(unsafeNameWarning("regression suite union", opts.specName, suiteName));
160
+ return undefined;
161
+ }
162
+ if (opts.primaryRegistryName === suiteName)
163
+ return undefined;
164
+ // File-dataset self-union guard: a file literally named after the suite
165
+ // (`<specName>-regressions.jsonl` — loadDataset names it by basename) is
166
+ // treated as an export of the suite, same as the registry ref above.
167
+ if (opts.datasetName === suiteName || opts.datasetName.replace(/\.[^.]+$/, "") === suiteName) {
168
+ return undefined;
169
+ }
170
+ const suiteVersion = await latestVersion(opts.registry, suiteName);
171
+ if (suiteVersion === undefined)
172
+ return undefined;
173
+ const record = await opts.registry.getRecord(suiteName, suiteVersion);
174
+ // Validate the record shape BEFORE materializing the primary stream — a
175
+ // corrupt suite record must fail here, while the caller's fallback can
176
+ // still hand the untouched primary stream to the runner.
177
+ if (!Array.isArray(record?.splits?.train)) {
178
+ throw new Error(`regression suite ${suiteName}@${suiteVersion} is corrupt (missing splits.train) — re-pin it or delete the record`);
179
+ }
180
+ const primary = await opts.loadPrimarySamples();
181
+ const seen = new Set(primary.map((s) => s.id));
182
+ const samples = [...primary];
183
+ let added = 0;
184
+ for (const s of record.splits.train) {
185
+ if (seen.has(s.id))
186
+ continue;
187
+ seen.add(s.id);
188
+ samples.push(s);
189
+ added += 1;
190
+ }
191
+ if (added === 0) {
192
+ // Keyset unchanged — keep the primary identity so the lineage stays
193
+ // comparable (see the doc comment above).
194
+ return {
195
+ samples,
196
+ datasetName: opts.datasetName,
197
+ datasetHash: opts.datasetHash,
198
+ added,
199
+ suiteName,
200
+ suiteVersion,
201
+ };
202
+ }
203
+ return {
204
+ samples,
205
+ datasetName: `${opts.datasetName}+regressions@${suiteVersion}`,
206
+ datasetHash: foldDatasetHash(opts.datasetHash, overallDatasetHash(record, ["train"])),
207
+ added,
208
+ suiteName,
209
+ suiteVersion,
210
+ };
211
+ }
212
+ /**
213
+ * `applyRegressionUnion` with the failure semantics `crewhaus eval` needs:
214
+ * best-effort (a broken suite record warns and falls back to the primary
215
+ * dataset) AND stream-loss-proof. The primary samples are captured as they
216
+ * are materialized for the union attempt, so a throw AFTER the one-shot
217
+ * primary stream was consumed (e.g. a suite record that passes the shape
218
+ * check but breaks the hash fold) falls back to the captured array instead
219
+ * of handing the runner an exhausted iterable — which would silently run a
220
+ * 0-sample eval.
221
+ */
222
+ export async function applyRegressionUnionGuarded(opts) {
223
+ const warn = opts.warn ?? warnToStderr;
224
+ let materialized;
225
+ try {
226
+ const union = await applyRegressionUnion({
227
+ registry: opts.registry,
228
+ specName: opts.specName,
229
+ includeRegressions: opts.includeRegressions,
230
+ ...(opts.primaryRegistryName !== undefined
231
+ ? { primaryRegistryName: opts.primaryRegistryName }
232
+ : {}),
233
+ loadPrimarySamples: async () => {
234
+ materialized = [];
235
+ for await (const s of opts.primary.samples)
236
+ materialized.push(s);
237
+ return materialized;
238
+ },
239
+ datasetName: opts.primary.name,
240
+ datasetHash: opts.datasetHash,
241
+ warn,
242
+ });
243
+ if (union !== undefined) {
244
+ return {
245
+ datasetName: union.datasetName,
246
+ datasetHash: union.datasetHash,
247
+ samples: reiterable(union.samples),
248
+ union,
249
+ };
250
+ }
251
+ }
252
+ catch (err) {
253
+ warn(`[eval] regression suite union skipped: ${err instanceof Error ? err.message : String(err)}`);
254
+ }
255
+ return {
256
+ datasetName: opts.primary.name,
257
+ datasetHash: opts.datasetHash,
258
+ samples: materialized !== undefined ? reiterable(materialized) : opts.primary.samples,
259
+ };
260
+ }
261
+ /** Re-iterable view over an already-materialized sample array. */
262
+ function reiterable(samples) {
263
+ return {
264
+ async *[Symbol.asyncIterator]() {
265
+ for (const s of samples)
266
+ yield s;
267
+ },
268
+ };
269
+ }
270
+ /**
271
+ * One stable sha256 folding the regression suite's content hash into the
272
+ * primary dataset's, with domain separators (mirrors `overallDatasetHash`'s
273
+ * style). Keeps RunIndexEntry.datasetHash honest for unioned runs: it
274
+ * changes when EITHER the primary bytes or the suite content change.
275
+ */
276
+ export function foldDatasetHash(primaryHash, suiteHash) {
277
+ return createHash("sha256")
278
+ .update(`primary:${primaryHash}\n`)
279
+ .update(`regressions:${suiteHash}\n`)
280
+ .digest("hex");
281
+ }
@@ -0,0 +1,193 @@
1
+ import { type PurgeResult, type RecordStore, type RetentionConfig, type RetentionRecord } from "@crewhaus/data-retention-engine";
2
+ export { DEFAULT_SESSION_MAX_AGE_DAYS, RETENTION_CONFIG_RELPATH, type RetentionAuditWindowConfig, type RetentionConfig, RetentionConfigError, loadRetentionConfig, } from "@crewhaus/data-retention-engine";
3
+ /** Single-tenant placeholder: the on-disk stores carry no tenant ids. */
4
+ export declare const LOCAL_TENANT = "local";
5
+ /**
6
+ * Why NO audit data — not individual records, not whole day files — is ever
7
+ * deleted by retention. Kept as an exported constant so the report, the
8
+ * adapter refusal, and the tests all cite the same rationale.
9
+ */
10
+ export declare const AUDIT_CHAIN_EXCLUSION_REASON: string;
11
+ /** Thrown by `parseRetentionDate` on an unparseable `--since`/`--before`. */
12
+ export declare class InvalidRetentionDateError extends Error {
13
+ readonly name = "InvalidRetentionDateError";
14
+ constructor(flag: string, value: string);
15
+ }
16
+ /** Parse a `--since`/`--before` flag value to epoch ms (UTC for bare dates). */
17
+ export declare function parseRetentionDate(flag: string, value: string): number;
18
+ export type SkippedEntry = {
19
+ readonly path: string;
20
+ readonly reason: string;
21
+ };
22
+ export type DeletionEntry = {
23
+ readonly id: string;
24
+ readonly paths: ReadonlyArray<string>;
25
+ };
26
+ export type RefusalEntry = {
27
+ readonly id: string;
28
+ readonly reason: string;
29
+ };
30
+ type SessionInventoryEntry = {
31
+ readonly record: RetentionRecord;
32
+ readonly sessionId: string;
33
+ readonly jsonPath: string;
34
+ /** Present when the sibling `<id>.jsonl` event log exists. */
35
+ readonly eventLogPath?: string;
36
+ };
37
+ type AuditInventoryEntry = {
38
+ readonly record: RetentionRecord;
39
+ readonly day: string;
40
+ readonly path: string;
41
+ };
42
+ /**
43
+ * RecordStore adapter over `<rootDir>/.crewhaus/sessions` +
44
+ * `<rootDir>/.crewhaus/audit`. The inventory is a snapshot taken by
45
+ * `openHarnessRecordStore` (a sweep operates on the state it observed at
46
+ * start). Deletions are tracked for reporting; in `dryRun` mode `delete()`
47
+ * makes the same decision (and records it) without touching the disk, so the
48
+ * engine's counters mirror a real run exactly.
49
+ */
50
+ export declare class HarnessRecordStore implements RecordStore {
51
+ readonly sessions: ReadonlyArray<SessionInventoryEntry>;
52
+ readonly auditDays: ReadonlyArray<AuditInventoryEntry>;
53
+ readonly skipped: ReadonlyArray<SkippedEntry>;
54
+ readonly deletions: DeletionEntry[];
55
+ readonly refusals: RefusalEntry[];
56
+ private readonly byId;
57
+ private readonly pins;
58
+ private readonly dryRun;
59
+ private readonly sessionsDir;
60
+ constructor(opts: {
61
+ readonly sessionsDir: string;
62
+ readonly sessions: ReadonlyArray<SessionInventoryEntry>;
63
+ readonly auditDays: ReadonlyArray<AuditInventoryEntry>;
64
+ readonly skipped: ReadonlyArray<SkippedEntry>;
65
+ readonly pins: ReadonlyArray<string>;
66
+ readonly dryRun: boolean;
67
+ });
68
+ listAll(): AsyncIterable<RetentionRecord>;
69
+ listByTenant(tenantId: string): AsyncIterable<RetentionRecord>;
70
+ delete(id: string): Promise<boolean>;
71
+ }
72
+ export type OpenHarnessRecordStoreOptions = {
73
+ readonly rootDir: string;
74
+ readonly pins?: ReadonlyArray<string>;
75
+ readonly dryRun?: boolean;
76
+ };
77
+ /**
78
+ * Snapshot the two stores under `rootDir` into a `HarnessRecordStore`.
79
+ * Anything unrecognised is SKIPPED with a reason, never deleted:
80
+ * `.tmp` leftovers, non-`sess_` filenames, orphaned event logs (a
81
+ * `<id>.jsonl` without its `<id>.json`), and non-day files in the audit dir.
82
+ */
83
+ export declare function openHarnessRecordStore(opts: OpenHarnessRecordStoreOptions): Promise<HarnessRecordStore>;
84
+ export type EvidenceRef = {
85
+ readonly seq: number;
86
+ readonly hash: string;
87
+ };
88
+ export type RetentionEnforcementReport = {
89
+ readonly action: "sweep" | "purge";
90
+ readonly rootDir: string;
91
+ readonly dryRun: boolean;
92
+ readonly config: RetentionConfig;
93
+ readonly before?: number;
94
+ /** Sessions deleted (or, dry-run, that WOULD be) with their on-disk paths. */
95
+ readonly deleted: ReadonlyArray<DeletionEntry>;
96
+ /** Sessions kept because they are pinned (only reached once expired). */
97
+ readonly keptPinned: ReadonlyArray<RefusalEntry>;
98
+ /** Audit day files — always kept; see AUDIT_CHAIN_EXCLUSION_REASON. */
99
+ readonly keptAuditChain: ReadonlyArray<string>;
100
+ /** Session ids still inside the retention window (age < maxAgeDays). */
101
+ readonly keptWithinRetention: ReadonlyArray<string>;
102
+ /** Record ids deferred because an audit window is active. */
103
+ readonly keptAuditWindow: ReadonlyArray<string>;
104
+ /** Session ids outside the `--before` cutoff (purge only). */
105
+ readonly keptOutsideCutoff: ReadonlyArray<string>;
106
+ readonly activeAuditWindows: PurgeResult["auditWindowDeferred"];
107
+ readonly skipped: ReadonlyArray<SkippedEntry>;
108
+ /** The appended `retention_enforcement` record (real runs only). */
109
+ readonly evidence?: EvidenceRef;
110
+ };
111
+ export type RunRetentionSweepOptions = {
112
+ readonly rootDir: string;
113
+ readonly dryRun?: boolean;
114
+ readonly now?: () => number;
115
+ };
116
+ /**
117
+ * `crewhaus retention sweep` — the scheduled GDPR/TTL enforcement pass.
118
+ * Deletes expired sessions (mtime older than `sessions.maxAgeDays`), honors
119
+ * pins + audit windows, never touches audit data, and appends a
120
+ * `retention_enforcement` evidence record on real runs. Exported as a plain
121
+ * library function so a daemon janitor can call it without the CLI.
122
+ */
123
+ export declare function runRetentionSweep(opts: RunRetentionSweepOptions): Promise<RetentionEnforcementReport>;
124
+ export type RunRetentionPurgeOptions = {
125
+ readonly rootDir: string;
126
+ /** Only records with createdAt strictly BEFORE this epoch-ms cutoff are
127
+ * candidates. Omitted = every expired record (same coverage as sweep). */
128
+ readonly before?: number;
129
+ /** Report what WOULD be purged; delete nothing, append no evidence.
130
+ * (Ops-review F1: `--dry-run` used to be accepted-and-ignored here — a
131
+ * dry run that really deletes is the worst possible failure mode.) */
132
+ readonly dryRun?: boolean;
133
+ readonly now?: () => number;
134
+ };
135
+ /**
136
+ * `crewhaus retention purge` — the explicit right-to-delete verb. Identical
137
+ * rules to the sweep (pins, audit windows, retention TTLs, audit exclusion)
138
+ * restricted to records older than `--before` when given. Not a bypass:
139
+ * a record still inside its retention window is deferred, not deleted.
140
+ */
141
+ export declare function runRetentionPurge(opts: RunRetentionPurgeOptions): Promise<RetentionEnforcementReport>;
142
+ export type RetentionExportReport = {
143
+ readonly rootDir: string;
144
+ readonly outDir: string;
145
+ readonly dryRun: boolean;
146
+ readonly since?: number;
147
+ /** Session ids whose files were copied (dry run: WOULD be copied). */
148
+ readonly sessions: ReadonlyArray<string>;
149
+ /** Audit day labels whose files were copied (dry run: WOULD be copied). */
150
+ readonly auditDays: ReadonlyArray<string>;
151
+ /**
152
+ * True when the export covered EVERY audit day file and the on-host
153
+ * `_chain-tail.json` anchor was copied too — the exported audit directory
154
+ * is then independently verifiable with `crewhaus audit verify --dir
155
+ * <outDir>/audit`. A `--since`-filtered (partial) export omits the anchor:
156
+ * its chain starts mid-stream and would fail verification by construction.
157
+ * (Dry run: whether the anchor WOULD be copied.)
158
+ */
159
+ readonly chainTailCopied: boolean;
160
+ readonly skipped: ReadonlyArray<SkippedEntry>;
161
+ /** The appended `retention_enforcement` record. Absent on a dry run, and
162
+ * on a real no-op export against a harness with no audit store (F8b). */
163
+ readonly evidence?: EvidenceRef;
164
+ };
165
+ export type RunRetentionExportOptions = {
166
+ readonly rootDir: string;
167
+ readonly outDir: string;
168
+ /** Only records at/after this epoch-ms timestamp are exported (sessions by
169
+ * mtime; audit at day granularity — a day is included when any instant of
170
+ * it is >= since, an inclusive over-approximation). */
171
+ readonly since?: number;
172
+ /** Report what WOULD be exported; write nothing (no outDir, no manifest),
173
+ * append no audit evidence. */
174
+ readonly dryRun?: boolean;
175
+ readonly now?: () => number;
176
+ };
177
+ /**
178
+ * `crewhaus retention export` — the right-to-export verb. Copies matching
179
+ * records OUT of the harness stores (originals untouched) as raw files, so
180
+ * an exported audit set keeps its verbatim hash-chain lines:
181
+ *
182
+ * <outDir>/sessions/<id>.json (+ <id>.jsonl event log when present)
183
+ * <outDir>/audit/<day>.jsonl (+ _chain-tail.json on a complete export)
184
+ * <outDir>/manifest.json
185
+ *
186
+ * Non-destructive, so pins and audit windows do not gate it; it IS
187
+ * audit-logged (a `retention_enforcement` record with action "export").
188
+ */
189
+ export declare function runRetentionExport(opts: RunRetentionExportOptions): Promise<RetentionExportReport>;
190
+ /** Render a sweep/purge report as the CLI's indented summary lines. */
191
+ export declare function formatEnforcementReport(report: RetentionEnforcementReport): ReadonlyArray<string>;
192
+ /** Render an export report as the CLI's indented summary lines. */
193
+ export declare function formatExportReport(report: RetentionExportReport): ReadonlyArray<string>;