crewhaus 0.1.7 → 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/README.md +10 -3
- package/dist/advice-apply.d.ts +182 -0
- package/dist/advice-apply.js +286 -0
- package/dist/advise-rules.d.ts +348 -0
- package/dist/advise-rules.js +905 -0
- package/dist/alert-sink.d.ts +48 -0
- package/dist/alert-sink.js +86 -0
- package/dist/approval-gate.d.ts +127 -0
- package/dist/approval-gate.js +254 -0
- package/dist/audit-verify.d.ts +69 -0
- package/dist/audit-verify.js +97 -0
- package/dist/autodistill.d.ts +113 -0
- package/dist/autodistill.js +256 -0
- package/dist/channel-provision.d.ts +360 -0
- package/dist/channel-provision.js +881 -0
- package/dist/ci-scaffold.d.ts +31 -0
- package/dist/ci-scaffold.js +343 -0
- package/dist/compile-check.d.ts +90 -0
- package/dist/compile-check.js +285 -0
- package/dist/compliance-schedule.d.ts +35 -0
- package/dist/compliance-schedule.js +36 -0
- package/dist/context-pressure.d.ts +80 -0
- package/dist/context-pressure.js +166 -0
- package/dist/dataset-mine.d.ts +172 -0
- package/dist/dataset-mine.js +403 -0
- package/dist/datasets.d.ts +124 -0
- package/dist/datasets.js +260 -0
- package/dist/deploy-canary.d.ts +83 -0
- package/dist/deploy-canary.js +87 -0
- package/dist/doctor-checks.d.ts +33 -0
- package/dist/doctor-checks.js +92 -0
- package/dist/doctor-detect.d.ts +108 -0
- package/dist/doctor-detect.js +214 -0
- package/dist/doctor-fix.d.ts +81 -0
- package/dist/doctor-fix.js +164 -0
- package/dist/egress-triage.d.ts +121 -0
- package/dist/egress-triage.js +261 -0
- package/dist/eval-bridge.d.ts +114 -0
- package/dist/eval-bridge.js +158 -0
- package/dist/eval-coverage.d.ts +140 -0
- package/dist/eval-coverage.js +428 -0
- package/dist/eval-history.d.ts +48 -0
- package/dist/eval-history.js +157 -0
- package/dist/eval-matrix.d.ts +80 -0
- package/dist/eval-matrix.js +182 -0
- package/dist/eval-sentinel.d.ts +65 -0
- package/dist/eval-sentinel.js +132 -0
- package/dist/faq.d.ts +68 -0
- package/dist/faq.js +168 -0
- package/dist/feedback.d.ts +246 -0
- package/dist/feedback.js +612 -0
- package/dist/fewshot.d.ts +83 -0
- package/dist/fewshot.js +158 -0
- package/dist/fleet.d.ts +207 -0
- package/dist/fleet.js +488 -0
- package/dist/flywheel.d.ts +193 -0
- package/dist/flywheel.js +519 -0
- package/dist/graders-suggest.d.ts +186 -0
- package/dist/graders-suggest.js +658 -0
- package/dist/incident.d.ts +99 -0
- package/dist/incident.js +217 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +11727 -740
- package/dist/init-interactive.d.ts +105 -0
- package/dist/init-interactive.js +208 -0
- package/dist/intents.d.ts +105 -0
- package/dist/intents.js +292 -0
- package/dist/judge-calibrate.d.ts +137 -0
- package/dist/judge-calibrate.js +247 -0
- package/dist/justification-calibrate.d.ts +150 -0
- package/dist/justification-calibrate.js +262 -0
- package/dist/justification-gate.d.ts +27 -6
- package/dist/justification-gate.js +30 -6
- package/dist/knowledge-sync.d.ts +179 -0
- package/dist/knowledge-sync.js +551 -0
- package/dist/lessons.d.ts +87 -0
- package/dist/lessons.js +207 -0
- package/dist/lint.d.ts +127 -0
- package/dist/lint.js +226 -0
- package/dist/loadtest.d.ts +114 -0
- package/dist/loadtest.js +196 -0
- package/dist/marketplace-cli.d.ts +110 -0
- package/dist/marketplace-cli.js +250 -0
- package/dist/mcp-doctor.d.ts +121 -0
- package/dist/mcp-doctor.js +249 -0
- package/dist/model-scan.d.ts +116 -0
- package/dist/model-scan.js +226 -0
- package/dist/onchain-tune.d.ts +164 -0
- package/dist/onchain-tune.js +346 -0
- package/dist/permissions-suggest.d.ts +126 -0
- package/dist/permissions-suggest.js +333 -0
- package/dist/pii-tune.d.ts +107 -0
- package/dist/pii-tune.js +122 -0
- package/dist/propose.d.ts +117 -0
- package/dist/propose.js +184 -0
- package/dist/refresh-goldens.d.ts +82 -0
- package/dist/refresh-goldens.js +221 -0
- package/dist/regression-pin.d.ts +160 -0
- package/dist/regression-pin.js +281 -0
- package/dist/retention.d.ts +193 -0
- package/dist/retention.js +607 -0
- package/dist/retire.d.ts +118 -0
- package/dist/retire.js +291 -0
- package/dist/right-size.d.ts +100 -0
- package/dist/right-size.js +123 -0
- package/dist/scaffold-evals.d.ts +138 -0
- package/dist/scaffold-evals.js +410 -0
- package/dist/scope-audit-drift.d.ts +139 -0
- package/dist/scope-audit-drift.js +260 -0
- package/dist/security-corpus.d.ts +237 -0
- package/dist/security-corpus.js +516 -0
- package/dist/security-digest.d.ts +173 -0
- package/dist/security-digest.js +650 -0
- package/dist/sessions-index.d.ts +27 -0
- package/dist/sessions-index.js +51 -0
- package/dist/slo-doctor.d.ts +67 -0
- package/dist/slo-doctor.js +119 -0
- package/dist/slo-sink.d.ts +96 -0
- package/dist/slo-sink.js +107 -0
- package/dist/spec-changelog.d.ts +102 -0
- package/dist/spec-changelog.js +237 -0
- package/dist/state-backup.d.ts +223 -0
- package/dist/state-backup.js +648 -0
- package/dist/tools-cli.d.ts +170 -0
- package/dist/tools-cli.js +298 -0
- package/dist/triage.d.ts +202 -0
- package/dist/triage.js +403 -0
- package/dist/upgrade.d.ts +57 -0
- package/dist/upgrade.js +113 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.js +27 -0
- package/dist/voice-eval.d.ts +138 -0
- package/dist/voice-eval.js +309 -0
- package/dist/watch.d.ts +58 -0
- package/dist/watch.js +97 -0
- package/package.json +89 -64
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item 46 — auto-register spec versions with changelogs distilled from
|
|
3
|
+
* patch history. Shared by `crewhaus compile` (post-emission), `crewhaus
|
|
4
|
+
* optimize --write-back` (post-write-back) and `crewhaus spec put`:
|
|
5
|
+
*
|
|
6
|
+
* - `autoRegisterSpecVersion` content-hashes the spec and, when the
|
|
7
|
+
* local registry has no version with that content, `put`s the next
|
|
8
|
+
* `vN` — so registry state tracks working files without a manual
|
|
9
|
+
* `crewhaus spec put`. Recompiling an unchanged spec is a no-op.
|
|
10
|
+
* - every auto/manual put appends a distilled entry to a per-spec
|
|
11
|
+
* `CHANGELOG.md` stored BESIDE the registry manifest
|
|
12
|
+
* (`.crewhaus/specs/<name>/CHANGELOG.md` — a NEW file; the registry's
|
|
13
|
+
* own on-disk format is untouched): version, date, the field-level
|
|
14
|
+
* YAML diff vs the previous version, and — when the content carries a
|
|
15
|
+
* `formatWriteBackHeader` stamp — the optimizer's runId/mutator/score
|
|
16
|
+
* plus the SpecPatch `rationale` from that run's `patch.json`.
|
|
17
|
+
*
|
|
18
|
+
* Kept in a side-effect-free module (nothing runs on import) so the logic
|
|
19
|
+
* is unit-testable — the CLI entry file runs an argv switch on import.
|
|
20
|
+
*/
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
22
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { diffSpecYaml, parseWriteBackHeader, } from "@crewhaus/spec-patch";
|
|
25
|
+
/** The per-spec changelog file, beside the registry's `manifest.json`. */
|
|
26
|
+
export const CHANGELOG_FILE = "CHANGELOG.md";
|
|
27
|
+
// The registry's own name floor (spec-registry NAME_REGEX). Spec names may
|
|
28
|
+
// additionally contain spaces and ':' (the spec `safeName` grammar), so
|
|
29
|
+
// auto-registration maps a display name onto this grammar deterministically.
|
|
30
|
+
const REGISTRY_NAME_REGEX = /^[A-Za-z0-9_-][A-Za-z0-9_.-]*$/;
|
|
31
|
+
// A write-back runId is read from a COMMENT in the spec being registered;
|
|
32
|
+
// refuse to path-join anything that could escape the optimize root.
|
|
33
|
+
const SAFE_RUN_ID_REGEX = /^[A-Za-z0-9_-]+$/;
|
|
34
|
+
/**
|
|
35
|
+
* Map a spec's display name (spec `safeName`: letters, digits, spaces,
|
|
36
|
+
* `_ . - :`) onto the registry's stricter name grammar: runs of characters
|
|
37
|
+
* outside `[A-Za-z0-9_.-]` collapse to a single `-`, leading dots are
|
|
38
|
+
* stripped (the registry hides dot-dirs), and an empty result falls back to
|
|
39
|
+
* `"spec"`. Deterministic, so the same spec always lands in the same slot.
|
|
40
|
+
*/
|
|
41
|
+
export function registrySpecName(specName) {
|
|
42
|
+
const cleaned = specName.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^\.+/, "");
|
|
43
|
+
const name = cleaned.length > 0 ? cleaned : "spec";
|
|
44
|
+
// Defensive: the transform above always satisfies the registry grammar,
|
|
45
|
+
// but verify so a future grammar drift fails loudly here, not deep in fs.
|
|
46
|
+
if (!REGISTRY_NAME_REGEX.test(name))
|
|
47
|
+
return "spec";
|
|
48
|
+
return name;
|
|
49
|
+
}
|
|
50
|
+
/** SHA-256 hex of the exact YAML text (the registry stores bytes verbatim). */
|
|
51
|
+
export function contentHash(yaml) {
|
|
52
|
+
return createHash("sha256").update(yaml).digest("hex");
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Next auto-version: `v<max+1>` over the existing `v<N>` versions (manifest
|
|
56
|
+
* order, numeric — `v10` beats `v2`). Manually-put versions that don't match
|
|
57
|
+
* `v<N>` (e.g. `2.0`, `release-1`) are ignored for numbering.
|
|
58
|
+
*/
|
|
59
|
+
export function nextVersion(existing) {
|
|
60
|
+
let max = 0;
|
|
61
|
+
for (const v of existing) {
|
|
62
|
+
const m = /^v(\d+)$/.exec(v);
|
|
63
|
+
if (m?.[1] === undefined)
|
|
64
|
+
continue;
|
|
65
|
+
const n = Number.parseInt(m[1], 10);
|
|
66
|
+
if (n > max)
|
|
67
|
+
max = n;
|
|
68
|
+
}
|
|
69
|
+
return `v${max + 1}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* When the YAML carries a `formatWriteBackHeader` stamp, recover the run
|
|
73
|
+
* metadata and — if that run's `patch.json` is readable — its `rationale`.
|
|
74
|
+
* Returns `undefined` for content that was never written back. Never throws:
|
|
75
|
+
* provenance is best-effort garnish on the changelog, not a gate.
|
|
76
|
+
*/
|
|
77
|
+
export function extractOptimizeMetadata(opts) {
|
|
78
|
+
const header = parseWriteBackHeader(opts.yaml);
|
|
79
|
+
if (header === undefined)
|
|
80
|
+
return undefined;
|
|
81
|
+
let patchJsonPath = opts.patchJsonPath;
|
|
82
|
+
if (patchJsonPath === undefined &&
|
|
83
|
+
opts.optimizeRootDir !== undefined &&
|
|
84
|
+
SAFE_RUN_ID_REGEX.test(header.runId)) {
|
|
85
|
+
patchJsonPath = join(opts.optimizeRootDir, header.runId, "patch.json");
|
|
86
|
+
}
|
|
87
|
+
let rationale;
|
|
88
|
+
if (patchJsonPath !== undefined && existsSync(patchJsonPath)) {
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(readFileSync(patchJsonPath, "utf-8"));
|
|
91
|
+
if (typeof parsed.rationale === "string" && parsed.rationale.length > 0) {
|
|
92
|
+
rationale = parsed.rationale;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Unreadable/malformed patch.json — keep the header fields alone.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { ...header, ...(rationale !== undefined ? { rationale } : {}) };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Render one changelog entry (pure): `## <version> — <date>` followed by
|
|
103
|
+
* the field-level diff bullets vs the previous version (or `initial
|
|
104
|
+
* version`) and, when present, the optimizer provenance + rationale.
|
|
105
|
+
*/
|
|
106
|
+
export function renderChangelogEntry(opts) {
|
|
107
|
+
const date = (opts.now ?? new Date()).toISOString().slice(0, 10);
|
|
108
|
+
const lines = [`## ${opts.version} — ${date}`, ""];
|
|
109
|
+
if (opts.previousYaml === undefined) {
|
|
110
|
+
lines.push("- initial version");
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
let diff;
|
|
114
|
+
try {
|
|
115
|
+
diff = diffSpecYaml(opts.previousYaml, opts.yaml);
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
lines.push(`- diff unavailable (${err.message})`);
|
|
119
|
+
}
|
|
120
|
+
if (diff !== undefined) {
|
|
121
|
+
if (diff.length === 0) {
|
|
122
|
+
lines.push("- no structural changes (comments/formatting only)");
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
for (const d of diff)
|
|
126
|
+
lines.push(formatDiffLine(d));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const meta = extractOptimizeMetadata(opts);
|
|
131
|
+
if (meta !== undefined) {
|
|
132
|
+
const details = [];
|
|
133
|
+
if (meta.mutator !== undefined)
|
|
134
|
+
details.push(`mutator ${meta.mutator}`);
|
|
135
|
+
if (meta.iterations !== undefined)
|
|
136
|
+
details.push(`${meta.iterations} iteration(s)`);
|
|
137
|
+
if (meta.scoreBefore !== undefined && meta.scoreAfter !== undefined) {
|
|
138
|
+
details.push(`score ${meta.scoreBefore.toFixed(3)} → ${meta.scoreAfter.toFixed(3)}`);
|
|
139
|
+
}
|
|
140
|
+
const suffix = details.length > 0 ? ` (${details.join(", ")})` : "";
|
|
141
|
+
lines.push(`- optimizer: runId ${meta.runId}${suffix}`);
|
|
142
|
+
if (meta.rationale !== undefined)
|
|
143
|
+
lines.push(`- rationale: ${meta.rationale}`);
|
|
144
|
+
}
|
|
145
|
+
lines.push("");
|
|
146
|
+
return lines.join("\n");
|
|
147
|
+
}
|
|
148
|
+
function formatDiffLine(d) {
|
|
149
|
+
switch (d.kind) {
|
|
150
|
+
case "added":
|
|
151
|
+
return `- added \`${d.path}\`: ${d.after ?? ""}`;
|
|
152
|
+
case "removed":
|
|
153
|
+
return `- removed \`${d.path}\`: ${d.before ?? ""}`;
|
|
154
|
+
case "changed":
|
|
155
|
+
return `- changed \`${d.path}\`: ${d.before ?? ""} → ${d.after ?? ""}`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Insert an entry into a changelog text, NEWEST FIRST (pure): the new entry
|
|
160
|
+
* lands above the previous top `## ` heading, under the `# Changelog — …`
|
|
161
|
+
* title. `crewhaus spec log` can therefore print the file verbatim and read
|
|
162
|
+
* most-recent-first.
|
|
163
|
+
*/
|
|
164
|
+
export function insertNewestFirst(existing, name, entry) {
|
|
165
|
+
if (existing.trim() === "") {
|
|
166
|
+
return `# Changelog — ${name}\n\n${entry}`;
|
|
167
|
+
}
|
|
168
|
+
const firstHeading = /^## /m.exec(existing);
|
|
169
|
+
if (firstHeading === null) {
|
|
170
|
+
// A title-only (or hand-edited) file: append below whatever is there.
|
|
171
|
+
return `${existing.trimEnd()}\n\n${entry}`;
|
|
172
|
+
}
|
|
173
|
+
return `${existing.slice(0, firstHeading.index)}${entry}\n${existing.slice(firstHeading.index)}`;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Append (newest-first) one entry to `<registryRoot>/<name>/CHANGELOG.md`,
|
|
177
|
+
* creating the file with its title on first use. Returns the file path.
|
|
178
|
+
*/
|
|
179
|
+
export function appendChangelogEntry(opts) {
|
|
180
|
+
const dir = join(opts.registryRootDir, opts.name);
|
|
181
|
+
mkdirSync(dir, { recursive: true });
|
|
182
|
+
const path = join(dir, CHANGELOG_FILE);
|
|
183
|
+
const existing = existsSync(path) ? readFileSync(path, "utf-8") : "";
|
|
184
|
+
const entry = renderChangelogEntry(opts);
|
|
185
|
+
writeFileSync(path, insertNewestFirst(existing, opts.name, entry), { mode: 0o600 });
|
|
186
|
+
return path;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Content-hash gated auto-put: when some stored version already carries this
|
|
190
|
+
* exact content, report `unchanged` (with that version); otherwise `put` the
|
|
191
|
+
* next `vN` and append the distilled changelog entry. The previous-latest
|
|
192
|
+
* version (manifest order) is the diff baseline.
|
|
193
|
+
*/
|
|
194
|
+
export async function autoRegisterSpecVersion(opts) {
|
|
195
|
+
const name = registrySpecName(opts.specName);
|
|
196
|
+
const hash = contentHash(opts.yaml);
|
|
197
|
+
const manifest = await opts.registry.manifest(name);
|
|
198
|
+
// Scan ALL stored versions for this content — re-registering any past
|
|
199
|
+
// version (e.g. a rollback restored on disk) is also a no-op. Later
|
|
200
|
+
// matches win so the reported version is the most recent occurrence.
|
|
201
|
+
let matched;
|
|
202
|
+
for (const v of manifest.versions) {
|
|
203
|
+
try {
|
|
204
|
+
if (contentHash(await opts.registry.get(name, v)) === hash)
|
|
205
|
+
matched = v;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// A manifest-listed version whose file vanished: ignore for matching.
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (matched !== undefined) {
|
|
212
|
+
return { status: "unchanged", name, version: matched };
|
|
213
|
+
}
|
|
214
|
+
const previousVersion = manifest.versions[manifest.versions.length - 1];
|
|
215
|
+
let previousYaml;
|
|
216
|
+
if (previousVersion !== undefined) {
|
|
217
|
+
try {
|
|
218
|
+
previousYaml = await opts.registry.get(name, previousVersion);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// Baseline unavailable — the entry renders as diff-less.
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const version = nextVersion(manifest.versions);
|
|
225
|
+
await opts.registry.put(name, version, opts.yaml);
|
|
226
|
+
appendChangelogEntry({
|
|
227
|
+
registryRootDir: opts.registryRootDir,
|
|
228
|
+
name,
|
|
229
|
+
version,
|
|
230
|
+
yaml: opts.yaml,
|
|
231
|
+
...(previousYaml !== undefined ? { previousYaml } : {}),
|
|
232
|
+
...(opts.optimizeRootDir !== undefined ? { optimizeRootDir: opts.optimizeRootDir } : {}),
|
|
233
|
+
...(opts.patchJsonPath !== undefined ? { patchJsonPath: opts.patchJsonPath } : {}),
|
|
234
|
+
...(opts.now !== undefined ? { now: opts.now } : {}),
|
|
235
|
+
});
|
|
236
|
+
return { status: "registered", name, version };
|
|
237
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { type FeedbackRecord } from "./feedback";
|
|
2
|
+
/** Manifest file name at the archive root (NOT restored into `.crewhaus`). */
|
|
3
|
+
export declare const MANIFEST_FILENAME = "backup-manifest.json";
|
|
4
|
+
/** Thrown for every operational failure (missing dir, tar failure, bad
|
|
5
|
+
* archive). The CLI entry file catches it and routes the message through
|
|
6
|
+
* `die()`; tests assert on `.message` without the process exiting. The
|
|
7
|
+
* `name` stays a plain string so `StateRestoreRefusedError` can narrow it. */
|
|
8
|
+
export declare class StateBackupError extends Error {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
}
|
|
11
|
+
/** Thrown when `restore` (without --force/--merge) would overwrite an
|
|
12
|
+
* existing non-empty `.crewhaus`. Carries a preformatted message listing
|
|
13
|
+
* what is there and how to proceed. */
|
|
14
|
+
export declare class StateRestoreRefusedError extends StateBackupError {
|
|
15
|
+
readonly stateDir: string;
|
|
16
|
+
readonly name = "StateRestoreRefusedError";
|
|
17
|
+
constructor(stateDir: string, entries: ReadonlyArray<{
|
|
18
|
+
name: string;
|
|
19
|
+
kind: "dir" | "file";
|
|
20
|
+
files: number;
|
|
21
|
+
}>);
|
|
22
|
+
}
|
|
23
|
+
export type SubdirStat = {
|
|
24
|
+
files: number;
|
|
25
|
+
bytes: number;
|
|
26
|
+
};
|
|
27
|
+
export type BackupManifest = {
|
|
28
|
+
schemaVersion: 1;
|
|
29
|
+
/** ISO 8601 creation timestamp. */
|
|
30
|
+
createdAt: string;
|
|
31
|
+
/** Absolute path of the `.crewhaus` dir that was archived. */
|
|
32
|
+
sourceDir: string;
|
|
33
|
+
/** CLI version that produced the backup. */
|
|
34
|
+
crewhausVersion: string;
|
|
35
|
+
totals: SubdirStat;
|
|
36
|
+
/** Per-top-level-subdir file counts + byte sizes; "." = state-dir root files. */
|
|
37
|
+
subdirs: Record<string, SubdirStat>;
|
|
38
|
+
/** false when any sqlite file could not be snapshotted consistently and was
|
|
39
|
+
* byte-copied raw instead (a live writer may have torn the copy). */
|
|
40
|
+
sqliteConsistent: boolean;
|
|
41
|
+
sqlite: {
|
|
42
|
+
/** rel paths archived as a consistent read-only `serialize()` snapshot. */
|
|
43
|
+
snapshotted: string[];
|
|
44
|
+
/** rel paths byte-copied raw because the snapshot failed. */
|
|
45
|
+
copiedRaw: string[];
|
|
46
|
+
/** `-wal`/`-shm`/`-journal` sidecars omitted because their main db was
|
|
47
|
+
* snapshotted (the snapshot folds WAL content in). */
|
|
48
|
+
foldedSidecars: string[];
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
/** One file as it lands in the archive (post-snapshot sizes). */
|
|
52
|
+
export type ArchivedFile = {
|
|
53
|
+
relPath: string;
|
|
54
|
+
bytes: number;
|
|
55
|
+
};
|
|
56
|
+
/** Pure manifest assembly from the archived file list + backup metadata. */
|
|
57
|
+
export declare function buildManifest(files: ReadonlyArray<ArchivedFile>, meta: {
|
|
58
|
+
createdAt: string;
|
|
59
|
+
sourceDir: string;
|
|
60
|
+
crewhausVersion: string;
|
|
61
|
+
sqliteConsistent: boolean;
|
|
62
|
+
sqlite: BackupManifest["sqlite"];
|
|
63
|
+
}): BackupManifest;
|
|
64
|
+
/** Narrow a parsed JSON value to a BackupManifest (tolerant structural check —
|
|
65
|
+
* a hand-made or future-versioned manifest should degrade to a warning, not
|
|
66
|
+
* a crash). */
|
|
67
|
+
export declare function isBackupManifest(value: unknown): value is BackupManifest;
|
|
68
|
+
/** Collapse a harness name / dir basename to a filename-safe label. */
|
|
69
|
+
export declare function sanitizeBackupLabel(label: string): string;
|
|
70
|
+
/** Default tarball name: `crewhaus-state-<label>-<ISO date>.tar.gz`. */
|
|
71
|
+
export declare function defaultBackupFileName(label: string, now: Date): string;
|
|
72
|
+
/** Split a comma-separated `--exclude` value into trimmed non-empty globs. */
|
|
73
|
+
export declare function parseExcludeGlobs(raw: string | undefined): string[];
|
|
74
|
+
/**
|
|
75
|
+
* Whether a state-dir-relative path (always "/"-separated) matches any
|
|
76
|
+
* exclude glob. A glob is matched against the full relative path; a bare
|
|
77
|
+
* pattern (no "/") additionally matches any single path segment, mirroring
|
|
78
|
+
* tar's basename `--exclude` ergonomics — so `sessions` drops the whole
|
|
79
|
+
* subdir and `*.sqlite` drops sqlite files anywhere in the tree.
|
|
80
|
+
*/
|
|
81
|
+
export declare function isExcluded(relPath: string, globs: ReadonlyArray<string>): boolean;
|
|
82
|
+
/** Whether `bytes` starts with the 16-byte SQLite main-db magic header. */
|
|
83
|
+
export declare function isSqliteHeader(bytes: Uint8Array): boolean;
|
|
84
|
+
/** For a `-wal`/`-shm`/`-journal` sidecar path, the main db path it belongs
|
|
85
|
+
* to; undefined for everything else. */
|
|
86
|
+
export declare function sqliteSidecarBase(relPath: string): string | undefined;
|
|
87
|
+
export type AdditiveMergePlan = {
|
|
88
|
+
/** Archive files absent locally — safe to copy in. */
|
|
89
|
+
copy: string[];
|
|
90
|
+
/** Archive files that already exist locally — skipped, never overwritten. */
|
|
91
|
+
skip: string[];
|
|
92
|
+
};
|
|
93
|
+
/** Plan a `--merge all` additive copy: only files that don't exist locally
|
|
94
|
+
* are copied; existing local files always win. Pure via the injected
|
|
95
|
+
* existence predicate. */
|
|
96
|
+
export declare function planAdditiveMerge(archiveFiles: ReadonlyArray<string>, existsLocally: (relPath: string) => boolean): AdditiveMergePlan;
|
|
97
|
+
export type FeedbackMergePlan = {
|
|
98
|
+
/** Folded records to append to the local feedback store. */
|
|
99
|
+
toImport: FeedbackRecord[];
|
|
100
|
+
/** Archived (session, turn) keys with no local record at all. */
|
|
101
|
+
added: number;
|
|
102
|
+
/** Keys where folding the archive in changed the local fold. */
|
|
103
|
+
updated: number;
|
|
104
|
+
/** Archived keys already fully represented locally — deduped away. */
|
|
105
|
+
unchanged: number;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Plan a `--merge feedback` fold using the exact dedupe semantics `crewhaus
|
|
109
|
+
* distill` applies: `mergeFeedback` collapses records to one per
|
|
110
|
+
* (sessionId, turnNumber), newest field wins, and a later comment-only
|
|
111
|
+
* record never erases an earlier rating. Local and archived records are
|
|
112
|
+
* each folded first, then each archived fold is folded AGAINST the local
|
|
113
|
+
* fold for its key — imported only when the archive actually contributes
|
|
114
|
+
* something (new key, or a fold that differs from the local one). Running
|
|
115
|
+
* the same merge twice is therefore a no-op.
|
|
116
|
+
*/
|
|
117
|
+
export declare function planFeedbackMerge(local: ReadonlyArray<FeedbackRecord>, archived: ReadonlyArray<FeedbackRecord>): FeedbackMergePlan;
|
|
118
|
+
export type BackupOptions = {
|
|
119
|
+
/** The `.crewhaus` directory to archive. */
|
|
120
|
+
stateDir: string;
|
|
121
|
+
/** Absolute output tarball path. */
|
|
122
|
+
outFile: string;
|
|
123
|
+
excludeGlobs?: ReadonlyArray<string>;
|
|
124
|
+
/** Stamped into the manifest. */
|
|
125
|
+
crewhausVersion: string;
|
|
126
|
+
/** Test seam: clock. */
|
|
127
|
+
now?: () => Date;
|
|
128
|
+
};
|
|
129
|
+
export type BackupResult = {
|
|
130
|
+
manifest: BackupManifest;
|
|
131
|
+
outFile: string;
|
|
132
|
+
/** Files dropped by --exclude. */
|
|
133
|
+
excluded: string[];
|
|
134
|
+
/** Non-fatal warnings (skipped symlinks, raw-copied sqlite files). */
|
|
135
|
+
warnings: string[];
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Archive `stateDir` to a gzipped tarball. Every regular file is staged as
|
|
139
|
+
* an exact byte copy (sqlite files as a consistent read-only snapshot — see
|
|
140
|
+
* the file header), a manifest is written at the archive root, and the
|
|
141
|
+
* system `tar` packs the staging dir. Source files are never modified.
|
|
142
|
+
*/
|
|
143
|
+
export declare function createStateBackup(opts: BackupOptions): Promise<BackupResult>;
|
|
144
|
+
export type StateDirEntry = {
|
|
145
|
+
name: string;
|
|
146
|
+
kind: "dir" | "file";
|
|
147
|
+
files: number;
|
|
148
|
+
};
|
|
149
|
+
export type StateDirInspection = {
|
|
150
|
+
state: "absent";
|
|
151
|
+
} | {
|
|
152
|
+
state: "empty";
|
|
153
|
+
} | {
|
|
154
|
+
state: "occupied";
|
|
155
|
+
entries: StateDirEntry[];
|
|
156
|
+
};
|
|
157
|
+
/** What (if anything) currently occupies the target `.crewhaus` dir. */
|
|
158
|
+
export declare function inspectStateDir(stateDir: string): StateDirInspection;
|
|
159
|
+
export type RestoreOptions = {
|
|
160
|
+
archiveFile: string;
|
|
161
|
+
/** Directory that receives the `.crewhaus` dir (default: cwd — resolved by
|
|
162
|
+
* the caller). */
|
|
163
|
+
intoDir: string;
|
|
164
|
+
/** Full replace: move an existing non-empty `.crewhaus` aside to
|
|
165
|
+
* `.crewhaus.bak-<ts>` (never deleted) before restoring. */
|
|
166
|
+
force: boolean;
|
|
167
|
+
};
|
|
168
|
+
export type RestoreResult = {
|
|
169
|
+
stateDir: string;
|
|
170
|
+
/** Where the pre-existing state dir went (only set when one was moved). */
|
|
171
|
+
movedAsideTo?: string;
|
|
172
|
+
manifest?: BackupManifest;
|
|
173
|
+
filesRestored: number;
|
|
174
|
+
warnings: string[];
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Full restore of a backup into `<intoDir>/.crewhaus`. Refuses (throws
|
|
178
|
+
* `StateRestoreRefusedError`) when the target is non-empty and `force` is
|
|
179
|
+
* not set. Extraction happens in a sibling temp dir which is renamed into
|
|
180
|
+
* place, so a half-extracted archive can never corrupt existing state, and
|
|
181
|
+
* restored file bytes are exactly the archived bytes (tar preserves them;
|
|
182
|
+
* nothing is rewritten).
|
|
183
|
+
*/
|
|
184
|
+
export declare function restoreStateArchive(opts: RestoreOptions): Promise<RestoreResult>;
|
|
185
|
+
export type MergeFeedbackOptions = {
|
|
186
|
+
archiveFile: string;
|
|
187
|
+
intoDir: string;
|
|
188
|
+
/** Test seam: clock (names the imported jsonl file). */
|
|
189
|
+
now?: () => Date;
|
|
190
|
+
};
|
|
191
|
+
export type MergeFeedbackResult = FeedbackMergePlan & {
|
|
192
|
+
/** Distinct feedback records found in the archive (pre-fold). */
|
|
193
|
+
archivedRecords: number;
|
|
194
|
+
/** The `feedback/*.jsonl` file the imports were appended to (unset when
|
|
195
|
+
* the archive contributed nothing new). */
|
|
196
|
+
wroteFile?: string;
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* `--merge feedback`: fold ONLY the archive's feedback records into the
|
|
200
|
+
* local store. Imported folds land as bare records in a new
|
|
201
|
+
* `feedback/restored-<ts>.jsonl` — the exact sink `crewhaus distill`
|
|
202
|
+
* already reads — so local session transcripts are never rewritten and the
|
|
203
|
+
* next distill sees local + imported records merged by `mergeFeedback`.
|
|
204
|
+
*/
|
|
205
|
+
export declare function mergeFeedbackFromArchive(opts: MergeFeedbackOptions): Promise<MergeFeedbackResult>;
|
|
206
|
+
export type MergeAllOptions = {
|
|
207
|
+
archiveFile: string;
|
|
208
|
+
intoDir: string;
|
|
209
|
+
};
|
|
210
|
+
export type MergeAllResult = {
|
|
211
|
+
stateDir: string;
|
|
212
|
+
/** Archive files copied in (absent locally). */
|
|
213
|
+
copied: string[];
|
|
214
|
+
/** Archive files skipped (already exist locally — local always wins). */
|
|
215
|
+
skipped: string[];
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* `--merge all`: per-subdir additive copy. Only archive files that do NOT
|
|
219
|
+
* exist locally are written; an existing local file is never overwritten
|
|
220
|
+
* (the skips are reported). The archive manifest is metadata, not state,
|
|
221
|
+
* and is never copied.
|
|
222
|
+
*/
|
|
223
|
+
export declare function mergeAllFromArchive(opts: MergeAllOptions): Promise<MergeAllResult>;
|