harnery 0.37.0 → 0.38.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/commander.d.ts +9 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +7 -2
- package/dist/commands/admission.d.ts +21 -0
- package/dist/commands/admission.d.ts.map +1 -0
- package/dist/commands/admission.js +565 -0
- package/dist/commands/agents.d.ts +7 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +61 -1
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +48 -2
- package/dist/commands/browse-ai.d.ts +2 -2
- package/dist/commands/browse-ai.d.ts.map +1 -1
- package/dist/commands/browse-ai.js +6 -4
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +349 -21
- package/dist/commands/fetch.js +1 -0
- package/dist/commands/qa-record.d.ts +144 -0
- package/dist/commands/qa-record.d.ts.map +1 -0
- package/dist/commands/qa-record.js +0 -0
- package/dist/commands/qa-run.d.ts +7 -3
- package/dist/commands/qa-run.d.ts.map +1 -1
- package/dist/commands/qa-run.js +268 -13
- package/dist/commands/qa-status.d.ts +71 -0
- package/dist/commands/qa-status.d.ts.map +1 -0
- package/dist/commands/qa-status.js +490 -0
- package/dist/commands/qa-verify.d.ts +40 -0
- package/dist/commands/qa-verify.d.ts.map +1 -0
- package/dist/commands/qa-verify.js +180 -0
- package/dist/commands/review-pack.d.ts +4 -0
- package/dist/commands/review-pack.d.ts.map +1 -0
- package/dist/commands/review-pack.js +1001 -0
- package/dist/core/agents/qa-signal.d.ts +111 -0
- package/dist/core/agents/qa-signal.d.ts.map +1 -0
- package/dist/core/agents/qa-signal.js +231 -0
- package/dist/core/agents/session-name-display.d.ts +20 -5
- package/dist/core/agents/session-name-display.d.ts.map +1 -1
- package/dist/core/agents/session-name-display.js +67 -7
- package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +11 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +17 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +1 -0
- package/dist/core/agents/state/live-coordination-writer.js +5 -0
- package/dist/core/artifacts/constants.d.ts +1 -1
- package/dist/core/artifacts/constants.js +1 -1
- package/dist/core/artifacts/index.d.ts +57 -6
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +265 -10
- package/dist/core/config.d.ts +8 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +16 -0
- package/dist/core/diagnostics/bundle.d.ts +16 -0
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +101 -7
- package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
- package/dist/core/events/v3/bootstrap.js +10 -0
- package/dist/core/events/v3/coordination-view.d.ts +3 -0
- package/dist/core/events/v3/coordination-view.d.ts.map +1 -1
- package/dist/core/events/v3/coordination-view.js +69 -8
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +9 -2
- package/dist/core/events/v3/producers/recorder.d.ts +23 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +264 -18
- package/dist/core/hooks/cli.js +120 -27
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +10 -3
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
- package/dist/core/hooks/session-name-presence.js +4 -1
- package/dist/core/qa-artifacts.d.ts +20 -0
- package/dist/core/qa-artifacts.d.ts.map +1 -0
- package/dist/core/qa-artifacts.js +110 -0
- package/dist/core/resources/contract.d.ts +6 -0
- package/dist/core/resources/contract.d.ts.map +1 -1
- package/dist/core/resources/sampler.d.ts +7 -0
- package/dist/core/resources/sampler.d.ts.map +1 -1
- package/dist/core/resources/sampler.js +106 -5
- package/dist/lib/admission.d.ts +71 -0
- package/dist/lib/admission.d.ts.map +1 -0
- package/dist/lib/admission.js +264 -0
- package/dist/lib/agent-browser/client.d.ts +1 -1
- package/dist/lib/agent-browser/client.d.ts.map +1 -1
- package/dist/lib/agent-browser/client.js +1 -5
- package/dist/lib/browser/capture-fidelity.d.ts +39 -0
- package/dist/lib/browser/capture-fidelity.d.ts.map +1 -0
- package/dist/lib/browser/capture-fidelity.js +84 -0
- package/dist/lib/browser/client.d.ts +41 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +167 -9
- package/dist/lib/browser/critique.d.ts +38 -1
- package/dist/lib/browser/critique.d.ts.map +1 -1
- package/dist/lib/browser/critique.js +34 -6
- package/dist/lib/browser/index.d.ts +4 -2
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +2 -0
- package/dist/lib/browser/page-review-judge.d.ts +64 -0
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -0
- package/dist/lib/browser/page-review-judge.js +270 -0
- package/dist/lib/browser/page-review-pack.d.ts +613 -0
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -0
- package/dist/lib/browser/page-review-pack.js +1751 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +214 -10
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run-contracts.js +136 -1
- package/dist/lib/browser/qa-run.d.ts +100 -10
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +768 -169
- package/dist/lib/browser/request-diagnostics.d.ts +13 -0
- package/dist/lib/browser/request-diagnostics.d.ts.map +1 -0
- package/dist/lib/browser/request-diagnostics.js +18 -0
- package/dist/lib/browser/tiling.d.ts +19 -0
- package/dist/lib/browser/tiling.d.ts.map +1 -1
- package/dist/lib/browser/tiling.js +28 -0
- package/dist/lib/cookies/client.d.ts +9 -0
- package/dist/lib/cookies/client.d.ts.map +1 -1
- package/dist/lib/cookies/client.js +197 -44
- package/dist/lib/cookies/extra.d.ts +18 -0
- package/dist/lib/cookies/extra.d.ts.map +1 -0
- package/dist/lib/cookies/extra.js +14 -0
- package/dist/lib/cookies/index.d.ts +2 -1
- package/dist/lib/cookies/index.d.ts.map +1 -1
- package/dist/lib/cookies/index.js +2 -1
- package/dist/lib/durable-job.d.ts +124 -0
- package/dist/lib/durable-job.d.ts.map +1 -0
- package/dist/lib/durable-job.js +296 -0
- package/dist/lib/http/client.d.ts +7 -1
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +2 -0
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +5 -2
- package/package.json +8 -2
- package/src/commander.ts +50 -2
- package/src/commands/admission.ts +699 -0
- package/src/commands/agents.ts +87 -1
- package/src/commands/artifacts.ts +97 -21
- package/src/commands/browse-ai.ts +10 -5
- package/src/commands/browse.ts +481 -20
- package/src/commands/fetch.ts +1 -0
- package/src/commands/qa-record.ts +682 -0
- package/src/commands/qa-run.ts +335 -16
- package/src/commands/qa-status.ts +608 -0
- package/src/commands/qa-verify.ts +238 -0
- package/src/commands/review-pack.ts +1281 -0
- package/src/core/agents/qa-signal.ts +261 -0
- package/src/core/agents/session-name-display.ts +78 -7
- package/src/core/agents/state/heartbeat-reader.ts +7 -0
- package/src/core/agents/state/heartbeat-writer.ts +23 -0
- package/src/core/agents/state/live-coordination-view.ts +1 -0
- package/src/core/agents/state/live-coordination-writer.ts +5 -0
- package/src/core/artifacts/constants.ts +1 -1
- package/src/core/artifacts/index.ts +370 -21
- package/src/core/config.ts +23 -0
- package/src/core/diagnostics/bundle.ts +119 -11
- package/src/core/events/v3/bootstrap.ts +10 -0
- package/src/core/events/v3/coordination-view.ts +100 -11
- package/src/core/events/v3/producers/intake.ts +9 -2
- package/src/core/events/v3/producers/recorder.ts +312 -18
- package/src/core/hooks/cli.ts +140 -32
- package/src/core/hooks/resolve/transcript.ts +10 -3
- package/src/core/hooks/session-name-presence.ts +6 -1
- package/src/core/qa-artifacts.ts +126 -0
- package/src/core/resources/contract.ts +7 -0
- package/src/core/resources/sampler.ts +138 -6
- package/src/lib/admission.ts +347 -0
- package/src/lib/agent-browser/client.ts +2 -10
- package/src/lib/browser/capture-fidelity.ts +98 -0
- package/src/lib/browser/client.ts +206 -10
- package/src/lib/browser/critique.ts +62 -7
- package/src/lib/browser/index.ts +36 -0
- package/src/lib/browser/page-review-judge.ts +360 -0
- package/src/lib/browser/page-review-pack.ts +2384 -0
- package/src/lib/browser/qa-run-contracts.ts +366 -3
- package/src/lib/browser/qa-run.ts +868 -190
- package/src/lib/browser/request-diagnostics.ts +27 -0
- package/src/lib/browser/tiling.ts +32 -0
- package/src/lib/cookies/client.ts +228 -42
- package/src/lib/cookies/extra.ts +28 -0
- package/src/lib/cookies/index.ts +2 -0
- package/src/lib/durable-job.ts +407 -0
- package/src/lib/http/client.ts +13 -1
- package/src/lib/instructions/templates.ts +5 -2
|
@@ -14,8 +14,9 @@ export interface ArtifactActor {
|
|
|
14
14
|
session_id?: string;
|
|
15
15
|
name?: string;
|
|
16
16
|
}
|
|
17
|
-
export interface
|
|
17
|
+
export interface ArtifactManifestV2 {
|
|
18
18
|
schema_version: typeof ARTIFACT_SCHEMA_VERSION;
|
|
19
|
+
holds: ArtifactHold[];
|
|
19
20
|
artifact_id: string;
|
|
20
21
|
slug: string;
|
|
21
22
|
purpose: string;
|
|
@@ -30,7 +31,24 @@ export interface ArtifactManifestV1 {
|
|
|
30
31
|
released_by?: ArtifactActor;
|
|
31
32
|
oversize_acknowledged?: boolean;
|
|
32
33
|
}
|
|
33
|
-
export
|
|
34
|
+
export interface ArtifactHold {
|
|
35
|
+
id: string;
|
|
36
|
+
reason: string;
|
|
37
|
+
set_by: ArtifactActor;
|
|
38
|
+
set_at: string;
|
|
39
|
+
}
|
|
40
|
+
export interface ArtifactHoldInput {
|
|
41
|
+
id: string;
|
|
42
|
+
reason: string;
|
|
43
|
+
}
|
|
44
|
+
export declare function artifactCapabilities(): {
|
|
45
|
+
readonly schema_version: 2;
|
|
46
|
+
readonly holds: true;
|
|
47
|
+
readonly atomic_create_holds: true;
|
|
48
|
+
readonly owner_scoped_unhold: true;
|
|
49
|
+
readonly explicit_v1_migration: true;
|
|
50
|
+
};
|
|
51
|
+
export type ArtifactClassification = "managed-held" | "managed-active" | "managed-current" | "managed-expired" | "managed-oversize" | "managed-over-budget" | "managed-tracked" | "invalid-manifest" | "unmanaged" | "symlink" | "unknown";
|
|
34
52
|
export interface ArtifactInventoryEntry {
|
|
35
53
|
name: string;
|
|
36
54
|
path: string;
|
|
@@ -51,7 +69,12 @@ export interface ArtifactCreateInput {
|
|
|
51
69
|
slug: string;
|
|
52
70
|
purpose: string;
|
|
53
71
|
retentionDays: number;
|
|
72
|
+
/** Sub-day retention. When set, it replaces `retentionDays` for this unit
|
|
73
|
+
* (a page review pack expires in minutes, not days). 1 to 5,256,000. */
|
|
74
|
+
retentionMinutes?: number;
|
|
54
75
|
actor?: ArtifactActor;
|
|
76
|
+
/** Holds are persisted with the first manifest; a valid actor is required. */
|
|
77
|
+
holds?: ArtifactHoldInput[];
|
|
55
78
|
now?: Date;
|
|
56
79
|
id?: string;
|
|
57
80
|
big?: boolean;
|
|
@@ -73,12 +96,20 @@ export interface ArtifactAdoptionResult {
|
|
|
73
96
|
adopted_path: string | null;
|
|
74
97
|
adopted_directories: number;
|
|
75
98
|
}
|
|
99
|
+
interface ParsedManifest {
|
|
100
|
+
ok: true;
|
|
101
|
+
manifest: ArtifactManifestV2;
|
|
102
|
+
}
|
|
103
|
+
interface ManifestError {
|
|
104
|
+
ok: false;
|
|
105
|
+
reason: string;
|
|
106
|
+
}
|
|
76
107
|
export declare function artifactsRoot(repoRoot: string): string;
|
|
77
108
|
/** Public config-aware default for embedding hosts that create artifact units. */
|
|
78
109
|
export declare function configuredArtifactRetentionDays(repoRoot: string): number;
|
|
79
110
|
export declare function createArtifact(repoRoot: string, input: ArtifactCreateInput): {
|
|
80
111
|
path: string;
|
|
81
|
-
manifest:
|
|
112
|
+
manifest: ArtifactManifestV2;
|
|
82
113
|
};
|
|
83
114
|
export declare function inventoryArtifacts(repoRoot: string, opts?: {
|
|
84
115
|
now?: Date;
|
|
@@ -89,10 +120,18 @@ export declare function showArtifact(repoRoot: string, ref: string, opts?: {
|
|
|
89
120
|
freshnessSeconds?: number;
|
|
90
121
|
}): {
|
|
91
122
|
entry: ArtifactInventoryEntry;
|
|
92
|
-
manifest:
|
|
123
|
+
manifest: ArtifactManifestV2;
|
|
93
124
|
};
|
|
94
|
-
export declare function renewArtifact(repoRoot: string, ref: string, days: number, reason: string, input?: ArtifactMutationInput):
|
|
95
|
-
export declare function releaseArtifact(repoRoot: string, ref: string, input?: ArtifactMutationInput):
|
|
125
|
+
export declare function renewArtifact(repoRoot: string, ref: string, days: number, reason: string, input?: ArtifactMutationInput): ArtifactManifestV2;
|
|
126
|
+
export declare function releaseArtifact(repoRoot: string, ref: string, input?: ArtifactMutationInput): ArtifactManifestV2;
|
|
127
|
+
export declare function holdArtifact(repoRoot: string, ref: string, input: ArtifactHoldInput & {
|
|
128
|
+
actor: ArtifactActor;
|
|
129
|
+
now?: Date;
|
|
130
|
+
}): ArtifactManifestV2;
|
|
131
|
+
export declare function unholdArtifact(repoRoot: string, ref: string, id: string, input: {
|
|
132
|
+
actor: ArtifactActor;
|
|
133
|
+
now?: Date;
|
|
134
|
+
}): ArtifactManifestV2;
|
|
96
135
|
/** Adopt untracked loose files and legacy directories without changing directory paths. */
|
|
97
136
|
export declare function adoptUnmanagedArtifactFiles(repoRoot: string, input: {
|
|
98
137
|
yes?: boolean;
|
|
@@ -129,4 +168,16 @@ export declare function autoCleanArtifacts(repoRoot: string, opts?: {
|
|
|
129
168
|
now?: Date;
|
|
130
169
|
}): ArtifactAutoCleanResult;
|
|
131
170
|
export declare function resolveArtifactRef(repoRoot: string, ref: string): string;
|
|
171
|
+
/** Validate only the current schema. Legacy conversion belongs to migrateArtifacts. */
|
|
172
|
+
export declare function parseArtifactManifest(value: unknown): ParsedManifest | ManifestError;
|
|
173
|
+
export interface ArtifactMigrationEntry {
|
|
174
|
+
path: string;
|
|
175
|
+
action: "keep" | "would-migrate" | "migrated";
|
|
176
|
+
reason: string;
|
|
177
|
+
preimage_path?: string;
|
|
178
|
+
}
|
|
179
|
+
/** Explicit, bounded v1 cutover. Preview never writes; every applied unit keeps its exact preimage. */
|
|
180
|
+
export declare function migrateArtifacts(repoRoot: string, opts?: {
|
|
181
|
+
yes?: boolean;
|
|
182
|
+
}): ArtifactMigrationEntry[];
|
|
132
183
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/artifacts/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/artifacts/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,13 +8,22 @@
|
|
|
8
8
|
* expired, inactive, untracked managed unit is deletable.
|
|
9
9
|
*/
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
|
-
import { randomUUID } from "node:crypto";
|
|
12
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
11
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
12
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmdirSync, rmSync, writeFileSync, } from "node:fs";
|
|
13
13
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
14
14
|
import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.js";
|
|
15
15
|
import { artifactAutoCleanEnabled, artifactAutoCleanIntervalHours, artifactDefaultRetentionDays, artifactMaxBytes, artifactMaxUnitBytes, } from "../config.js";
|
|
16
16
|
import { ARTIFACT_MANIFEST, ARTIFACT_SCHEMA_VERSION, ARTIFACTS_DIR } from "./constants.js";
|
|
17
17
|
export { ARTIFACT_MANIFEST, ARTIFACT_SCHEMA_VERSION, ARTIFACTS_DIR } from "./constants.js";
|
|
18
|
+
export function artifactCapabilities() {
|
|
19
|
+
return {
|
|
20
|
+
schema_version: ARTIFACT_SCHEMA_VERSION,
|
|
21
|
+
holds: true,
|
|
22
|
+
atomic_create_holds: true,
|
|
23
|
+
owner_scoped_unhold: true,
|
|
24
|
+
explicit_v1_migration: true,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
18
27
|
export function artifactsRoot(repoRoot) {
|
|
19
28
|
return join(resolve(repoRoot), ARTIFACTS_DIR);
|
|
20
29
|
}
|
|
@@ -23,6 +32,9 @@ export function configuredArtifactRetentionDays(repoRoot) {
|
|
|
23
32
|
return artifactDefaultRetentionDays(repoRoot);
|
|
24
33
|
}
|
|
25
34
|
export function createArtifact(repoRoot, input) {
|
|
35
|
+
return withArtifactLock(repoRoot, () => createArtifactUnlocked(repoRoot, input));
|
|
36
|
+
}
|
|
37
|
+
function createArtifactUnlocked(repoRoot, input) {
|
|
26
38
|
const now = input.now ?? new Date();
|
|
27
39
|
assertValidDate(now, "now");
|
|
28
40
|
const slug = normalizeSlug(input.slug);
|
|
@@ -31,11 +43,17 @@ export function createArtifact(repoRoot, input) {
|
|
|
31
43
|
const purpose = input.purpose.trim();
|
|
32
44
|
if (!purpose)
|
|
33
45
|
throw new Error("purpose must not be empty");
|
|
34
|
-
const
|
|
46
|
+
const expiresAt = input.retentionMinutes !== undefined
|
|
47
|
+
? addMinutes(now, positiveMinutes(input.retentionMinutes))
|
|
48
|
+
: addDays(now, positiveDays(input.retentionDays));
|
|
35
49
|
const artifactId = input.id ?? randomUUID();
|
|
36
50
|
if (!isSafeId(artifactId)) {
|
|
37
51
|
throw new Error("artifact id must use ASCII letters, digits, hyphens, or underscores");
|
|
38
52
|
}
|
|
53
|
+
const holds = (input.holds ?? []).map((hold) => makeHold(hold, input.actor, now));
|
|
54
|
+
if (new Set(holds.map((hold) => hold.id)).size !== holds.length) {
|
|
55
|
+
throw new Error("duplicate hold id");
|
|
56
|
+
}
|
|
39
57
|
const root = artifactsRoot(repoRoot);
|
|
40
58
|
mkdirSync(root, { recursive: true });
|
|
41
59
|
const date = now.toISOString().slice(0, 10);
|
|
@@ -43,13 +61,14 @@ export function createArtifact(repoRoot, input) {
|
|
|
43
61
|
mkdirSync(path);
|
|
44
62
|
const manifest = {
|
|
45
63
|
schema_version: ARTIFACT_SCHEMA_VERSION,
|
|
64
|
+
holds,
|
|
46
65
|
artifact_id: artifactId,
|
|
47
66
|
slug,
|
|
48
67
|
purpose,
|
|
49
68
|
created_at: now.toISOString(),
|
|
50
69
|
created_by: input.actor,
|
|
51
70
|
retention: {
|
|
52
|
-
expires_at:
|
|
71
|
+
expires_at: expiresAt.toISOString(),
|
|
53
72
|
},
|
|
54
73
|
...(input.big ? { oversize_acknowledged: true } : {}),
|
|
55
74
|
};
|
|
@@ -89,6 +108,9 @@ export function showArtifact(repoRoot, ref, opts = {}) {
|
|
|
89
108
|
return { entry, manifest: parsed.manifest };
|
|
90
109
|
}
|
|
91
110
|
export function renewArtifact(repoRoot, ref, days, reason, input = {}) {
|
|
111
|
+
return withArtifactLock(repoRoot, () => renewArtifactUnlocked(repoRoot, ref, days, reason, input));
|
|
112
|
+
}
|
|
113
|
+
function renewArtifactUnlocked(repoRoot, ref, days, reason, input) {
|
|
92
114
|
const now = input.now ?? new Date();
|
|
93
115
|
assertValidDate(now, "now");
|
|
94
116
|
const retentionDays = positiveDays(days);
|
|
@@ -111,6 +133,9 @@ export function renewArtifact(repoRoot, ref, days, reason, input = {}) {
|
|
|
111
133
|
return manifest;
|
|
112
134
|
}
|
|
113
135
|
export function releaseArtifact(repoRoot, ref, input = {}) {
|
|
136
|
+
return withArtifactLock(repoRoot, () => releaseArtifactUnlocked(repoRoot, ref, input));
|
|
137
|
+
}
|
|
138
|
+
function releaseArtifactUnlocked(repoRoot, ref, input) {
|
|
114
139
|
const now = input.now ?? new Date();
|
|
115
140
|
assertValidDate(now, "now");
|
|
116
141
|
const path = resolveArtifactRef(repoRoot, ref);
|
|
@@ -125,8 +150,57 @@ export function releaseArtifact(repoRoot, ref, input = {}) {
|
|
|
125
150
|
atomicWriteManifest(path, manifest);
|
|
126
151
|
return manifest;
|
|
127
152
|
}
|
|
153
|
+
export function holdArtifact(repoRoot, ref, input) {
|
|
154
|
+
const hold = makeHold(input, input.actor, input.now ?? new Date());
|
|
155
|
+
return withArtifactLock(repoRoot, () => {
|
|
156
|
+
const path = resolveArtifactRef(repoRoot, ref);
|
|
157
|
+
const parsed = readManifest(path);
|
|
158
|
+
if (!parsed.ok)
|
|
159
|
+
throw new Error(parsed.reason);
|
|
160
|
+
const previous = parsed.manifest.holds.find((item) => item.id === hold.id);
|
|
161
|
+
if (previous) {
|
|
162
|
+
if (previous.set_by.instance_id !== input.actor.instance_id ||
|
|
163
|
+
previous.reason !== hold.reason) {
|
|
164
|
+
throw new Error("hold id already exists with a different owner or reason");
|
|
165
|
+
}
|
|
166
|
+
return parsed.manifest;
|
|
167
|
+
}
|
|
168
|
+
const manifest = { ...parsed.manifest, holds: [...parsed.manifest.holds, hold] };
|
|
169
|
+
atomicWriteManifest(path, manifest);
|
|
170
|
+
return manifest;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
export function unholdArtifact(repoRoot, ref, id, input) {
|
|
174
|
+
if (!validHoldId(id))
|
|
175
|
+
throw new Error("invalid hold id");
|
|
176
|
+
if (!validActor(input.actor))
|
|
177
|
+
throw new Error("a valid hold actor is required");
|
|
178
|
+
return withArtifactLock(repoRoot, () => {
|
|
179
|
+
const path = resolveArtifactRef(repoRoot, ref);
|
|
180
|
+
const parsed = readManifest(path);
|
|
181
|
+
if (!parsed.ok)
|
|
182
|
+
throw new Error(parsed.reason);
|
|
183
|
+
const hold = parsed.manifest.holds.find((item) => item.id === id);
|
|
184
|
+
if (!hold)
|
|
185
|
+
return parsed.manifest;
|
|
186
|
+
if (hold.set_by.instance_id !== input.actor.instance_id) {
|
|
187
|
+
throw new Error("only the hold owner may remove this hold");
|
|
188
|
+
}
|
|
189
|
+
const manifest = {
|
|
190
|
+
...parsed.manifest,
|
|
191
|
+
holds: parsed.manifest.holds.filter((item) => item.id !== id),
|
|
192
|
+
};
|
|
193
|
+
atomicWriteManifest(path, manifest);
|
|
194
|
+
return manifest;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
128
197
|
/** Adopt untracked loose files and legacy directories without changing directory paths. */
|
|
129
198
|
export function adoptUnmanagedArtifactFiles(repoRoot, input) {
|
|
199
|
+
if (input.yes)
|
|
200
|
+
return withArtifactLock(repoRoot, () => adoptUnmanagedArtifactFilesUnlocked(repoRoot, input));
|
|
201
|
+
return adoptUnmanagedArtifactFilesUnlocked(repoRoot, input);
|
|
202
|
+
}
|
|
203
|
+
function adoptUnmanagedArtifactFilesUnlocked(repoRoot, input) {
|
|
130
204
|
const now = input.now ?? new Date();
|
|
131
205
|
const candidates = [];
|
|
132
206
|
for (const row of inventoryArtifacts(repoRoot, { now })) {
|
|
@@ -185,7 +259,7 @@ export function adoptUnmanagedArtifactFiles(repoRoot, input) {
|
|
|
185
259
|
}
|
|
186
260
|
const files = candidates.filter((candidate) => candidate.kind === "file");
|
|
187
261
|
const created = files.length
|
|
188
|
-
?
|
|
262
|
+
? createArtifactUnlocked(repoRoot, {
|
|
189
263
|
slug: "adopted-unmanaged",
|
|
190
264
|
purpose: input.purpose,
|
|
191
265
|
retentionDays: input.retentionDays,
|
|
@@ -201,6 +275,7 @@ export function adoptUnmanagedArtifactFiles(repoRoot, input) {
|
|
|
201
275
|
for (const candidate of directories) {
|
|
202
276
|
atomicWriteManifest(candidate.path, {
|
|
203
277
|
schema_version: ARTIFACT_SCHEMA_VERSION,
|
|
278
|
+
holds: [],
|
|
204
279
|
artifact_id: randomUUID(),
|
|
205
280
|
slug: normalizeSlug(candidate.name),
|
|
206
281
|
purpose: `${input.purpose}: ${candidate.name}`,
|
|
@@ -218,6 +293,23 @@ export function adoptUnmanagedArtifactFiles(repoRoot, input) {
|
|
|
218
293
|
};
|
|
219
294
|
}
|
|
220
295
|
export function cleanArtifacts(repoRoot, opts = {}) {
|
|
296
|
+
if (!opts.yes)
|
|
297
|
+
return inventoryArtifacts(repoRoot, opts);
|
|
298
|
+
try {
|
|
299
|
+
return withArtifactLock(repoRoot, () => cleanArtifactsUnlocked(repoRoot, opts));
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
return inventoryArtifacts(repoRoot, opts).map((row) => row.action === "would-delete"
|
|
303
|
+
? {
|
|
304
|
+
...row,
|
|
305
|
+
classification: "unknown",
|
|
306
|
+
action: "keep",
|
|
307
|
+
reason: errorMessage("cleanup refused", error),
|
|
308
|
+
}
|
|
309
|
+
: row);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function cleanArtifactsUnlocked(repoRoot, opts) {
|
|
221
313
|
const now = opts.now ?? new Date();
|
|
222
314
|
const freshnessSeconds = opts.freshnessSeconds ?? 600;
|
|
223
315
|
const rows = inventoryArtifacts(repoRoot, { now, freshnessSeconds });
|
|
@@ -369,6 +461,14 @@ function classifyArtifactPath(repoRoot, path, now, freshnessSeconds) {
|
|
|
369
461
|
owner_instance_id: manifest.created_by?.instance_id ?? null,
|
|
370
462
|
oversize_acknowledged: manifest.oversize_acknowledged === true,
|
|
371
463
|
});
|
|
464
|
+
if (manifest.holds.length > 0) {
|
|
465
|
+
return {
|
|
466
|
+
...base,
|
|
467
|
+
classification: "managed-held",
|
|
468
|
+
reason: `held: ${manifest.holds.map((hold) => hold.id).join(", ")}`,
|
|
469
|
+
action: "keep",
|
|
470
|
+
};
|
|
471
|
+
}
|
|
372
472
|
if (base.bytes === null || lastModifiedMs === null) {
|
|
373
473
|
return {
|
|
374
474
|
...base,
|
|
@@ -452,11 +552,24 @@ function readManifest(path) {
|
|
|
452
552
|
}
|
|
453
553
|
let value;
|
|
454
554
|
try {
|
|
555
|
+
const unit = lstatSync(path);
|
|
556
|
+
const stat = lstatSync(manifestPath);
|
|
557
|
+
if (!unit.isDirectory() ||
|
|
558
|
+
unit.isSymbolicLink() ||
|
|
559
|
+
!stat.isFile() ||
|
|
560
|
+
stat.isSymbolicLink() ||
|
|
561
|
+
stat.size > 1024 * 1024) {
|
|
562
|
+
return { ok: false, reason: "manifest must be a bounded regular file in a direct directory" };
|
|
563
|
+
}
|
|
455
564
|
value = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
456
565
|
}
|
|
457
566
|
catch (error) {
|
|
458
567
|
return { ok: false, reason: errorMessage("manifest is unreadable", error) };
|
|
459
568
|
}
|
|
569
|
+
return parseArtifactManifest(value);
|
|
570
|
+
}
|
|
571
|
+
/** Validate only the current schema. Legacy conversion belongs to migrateArtifacts. */
|
|
572
|
+
export function parseArtifactManifest(value) {
|
|
460
573
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
461
574
|
return { ok: false, reason: "manifest must be a JSON object" };
|
|
462
575
|
}
|
|
@@ -464,6 +577,11 @@ function readManifest(path) {
|
|
|
464
577
|
if (m.schema_version !== ARTIFACT_SCHEMA_VERSION) {
|
|
465
578
|
return { ok: false, reason: `unsupported schema_version ${String(m.schema_version)}` };
|
|
466
579
|
}
|
|
580
|
+
if (!Array.isArray(m.holds) ||
|
|
581
|
+
m.holds.some((hold) => !validHold(hold)) ||
|
|
582
|
+
new Set(m.holds.map((hold) => hold.id)).size !== m.holds.length) {
|
|
583
|
+
return { ok: false, reason: "invalid holds" };
|
|
584
|
+
}
|
|
467
585
|
if (!isSafeId(m.artifact_id))
|
|
468
586
|
return { ok: false, reason: "invalid artifact_id" };
|
|
469
587
|
if (typeof m.slug !== "string" || !m.slug || normalizeSlug(m.slug) !== m.slug) {
|
|
@@ -501,11 +619,139 @@ function readManifest(path) {
|
|
|
501
619
|
function atomicWriteManifest(path, manifest) {
|
|
502
620
|
const target = join(path, ARTIFACT_MANIFEST);
|
|
503
621
|
const tmp = `${target}.tmp.${process.pid}.${randomUUID().slice(0, 8)}`;
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
622
|
+
try {
|
|
623
|
+
writeFileSync(tmp, `${JSON.stringify(manifest, null, 2)}\n`, {
|
|
624
|
+
encoding: "utf8",
|
|
625
|
+
mode: 0o600,
|
|
626
|
+
flag: "wx",
|
|
627
|
+
});
|
|
628
|
+
renameSync(tmp, target);
|
|
629
|
+
}
|
|
630
|
+
finally {
|
|
631
|
+
rmSync(tmp, { force: true });
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
/** Explicit, bounded v1 cutover. Preview never writes; every applied unit keeps its exact preimage. */
|
|
635
|
+
export function migrateArtifacts(repoRoot, opts = {}) {
|
|
636
|
+
const migrate = () => {
|
|
637
|
+
const root = artifactsRoot(repoRoot);
|
|
638
|
+
if (!existsSync(root))
|
|
639
|
+
return [];
|
|
640
|
+
return readdirSync(root)
|
|
641
|
+
.sort()
|
|
642
|
+
.map((name) => {
|
|
643
|
+
const path = join(root, name);
|
|
644
|
+
try {
|
|
645
|
+
const stat = lstatSync(path);
|
|
646
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
647
|
+
throw new Error("not a direct directory");
|
|
648
|
+
if (containsTrackedPath(repoRoot, path))
|
|
649
|
+
throw new Error("artifact contains tracked files");
|
|
650
|
+
const target = join(path, ARTIFACT_MANIFEST);
|
|
651
|
+
const manifestStat = lstatSync(target);
|
|
652
|
+
if (!manifestStat.isFile() ||
|
|
653
|
+
manifestStat.isSymbolicLink() ||
|
|
654
|
+
manifestStat.size > 1024 * 1024) {
|
|
655
|
+
throw new Error("manifest is not a bounded regular file");
|
|
656
|
+
}
|
|
657
|
+
const preimage = readFileSync(target, "utf8");
|
|
658
|
+
const legacy = JSON.parse(preimage);
|
|
659
|
+
if (legacy?.schema_version !== 1) {
|
|
660
|
+
return {
|
|
661
|
+
path,
|
|
662
|
+
action: "keep",
|
|
663
|
+
reason: `schema_version ${String(legacy?.schema_version)} is not a migration source`,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
if (Object.hasOwn(legacy, "holds"))
|
|
667
|
+
throw new Error("v1 manifest unexpectedly contains holds");
|
|
668
|
+
const parsed = parseArtifactManifest({
|
|
669
|
+
...legacy,
|
|
670
|
+
schema_version: ARTIFACT_SCHEMA_VERSION,
|
|
671
|
+
holds: [],
|
|
672
|
+
});
|
|
673
|
+
if (!parsed.ok)
|
|
674
|
+
throw new Error(parsed.reason);
|
|
675
|
+
const digest = createHash("sha256").update(preimage).digest("hex");
|
|
676
|
+
const preimagePath = join(resolve(repoRoot), ".harnery/artifact-migrations", `${digest}.v1.json`);
|
|
677
|
+
if (!opts.yes)
|
|
678
|
+
return {
|
|
679
|
+
path,
|
|
680
|
+
action: "would-migrate",
|
|
681
|
+
reason: "valid v1 manifest",
|
|
682
|
+
preimage_path: preimagePath,
|
|
683
|
+
};
|
|
684
|
+
mkdirSync(dirname(preimagePath), { recursive: true });
|
|
685
|
+
try {
|
|
686
|
+
writeFileSync(preimagePath, preimage, { flag: "wx", mode: 0o600 });
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
if (error.code !== "EEXIST" ||
|
|
690
|
+
lstatSync(preimagePath).isSymbolicLink() ||
|
|
691
|
+
readFileSync(preimagePath, "utf8") !== preimage)
|
|
692
|
+
throw error;
|
|
693
|
+
}
|
|
694
|
+
if (readFileSync(target, "utf8") !== preimage)
|
|
695
|
+
throw new Error("manifest changed before migration");
|
|
696
|
+
atomicWriteManifest(path, parsed.manifest);
|
|
697
|
+
return {
|
|
698
|
+
path,
|
|
699
|
+
action: "migrated",
|
|
700
|
+
reason: "v1 preimage preserved; identity and retention unchanged",
|
|
701
|
+
preimage_path: preimagePath,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
catch (error) {
|
|
705
|
+
return { path, action: "keep", reason: errorMessage("migration refused", error) };
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
};
|
|
709
|
+
return opts.yes ? withArtifactLock(repoRoot, migrate) : migrate();
|
|
710
|
+
}
|
|
711
|
+
/** A persistent directory lock fails closed on contention or a crashed owner. */
|
|
712
|
+
function withArtifactLock(repoRoot, action) {
|
|
713
|
+
const lock = join(resolve(repoRoot), ".harnery/artifacts-mutation.lock");
|
|
714
|
+
mkdirSync(dirname(lock), { recursive: true });
|
|
715
|
+
try {
|
|
716
|
+
mkdirSync(lock);
|
|
717
|
+
}
|
|
718
|
+
catch (error) {
|
|
719
|
+
throw new Error(errorMessage("artifact store mutation lock unavailable; retry after its owner finishes", error));
|
|
720
|
+
}
|
|
721
|
+
try {
|
|
722
|
+
return action();
|
|
723
|
+
}
|
|
724
|
+
finally {
|
|
725
|
+
rmdirSync(lock);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function validHoldId(value) {
|
|
729
|
+
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/.test(value);
|
|
730
|
+
}
|
|
731
|
+
function validHold(value) {
|
|
732
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
733
|
+
return false;
|
|
734
|
+
const hold = value;
|
|
735
|
+
return (validHoldId(hold.id) &&
|
|
736
|
+
typeof hold.reason === "string" &&
|
|
737
|
+
!!hold.reason.trim() &&
|
|
738
|
+
validActor(hold.set_by) &&
|
|
739
|
+
validIso(hold.set_at));
|
|
740
|
+
}
|
|
741
|
+
function makeHold(input, actor, now) {
|
|
742
|
+
assertValidDate(now, "now");
|
|
743
|
+
if (!validActor(actor))
|
|
744
|
+
throw new Error("a valid hold actor is required");
|
|
745
|
+
if (!validHoldId(input.id))
|
|
746
|
+
throw new Error("invalid hold id");
|
|
747
|
+
if (typeof input.reason !== "string" || !input.reason.trim())
|
|
748
|
+
throw new Error("hold reason must not be empty");
|
|
749
|
+
return {
|
|
750
|
+
id: input.id,
|
|
751
|
+
reason: input.reason.trim(),
|
|
752
|
+
set_by: { ...actor },
|
|
753
|
+
set_at: now.toISOString(),
|
|
754
|
+
};
|
|
509
755
|
}
|
|
510
756
|
function ownerLiveness(repoRoot, instanceId, now, freshnessSeconds) {
|
|
511
757
|
try {
|
|
@@ -621,6 +867,15 @@ function positiveDays(value) {
|
|
|
621
867
|
function addDays(date, days) {
|
|
622
868
|
return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
|
|
623
869
|
}
|
|
870
|
+
function positiveMinutes(value) {
|
|
871
|
+
if (!Number.isInteger(value) || value <= 0 || value > 3650 * 24 * 60) {
|
|
872
|
+
throw new Error("retention minutes must be between 1 and 5256000");
|
|
873
|
+
}
|
|
874
|
+
return value;
|
|
875
|
+
}
|
|
876
|
+
function addMinutes(date, minutes) {
|
|
877
|
+
return new Date(date.getTime() + minutes * 60 * 1000);
|
|
878
|
+
}
|
|
624
879
|
function isSafeId(value) {
|
|
625
880
|
return (typeof value === "string" &&
|
|
626
881
|
value.length >= 8 &&
|
package/dist/core/config.d.ts
CHANGED
|
@@ -220,6 +220,14 @@ export declare function artifactMaxUnitBytes(coordRoot?: string | null): number;
|
|
|
220
220
|
* `artifacts clean --yes`.
|
|
221
221
|
*/
|
|
222
222
|
export declare function artifactAutoCleanEnabled(coordRoot?: string | null): boolean;
|
|
223
|
+
/**
|
|
224
|
+
* Whether `qa-run` and `review-pack create` sweep expired page review packs
|
|
225
|
+
* out of the artifact store before starting. Precedence:
|
|
226
|
+
* `HARNERY_REVIEW_PACK_AUTO_CLEAN` (1/true enables, 0/false disables) ->
|
|
227
|
+
* `review_pack.auto_clean` -> disabled. The sweep deletes only packs whose
|
|
228
|
+
* manifest says `managed: true` with `retention.expires_at` in the past.
|
|
229
|
+
*/
|
|
230
|
+
export declare function reviewPackAutoCleanEnabled(coordRoot?: string | null): boolean;
|
|
223
231
|
/**
|
|
224
232
|
* Sweep cadence in hours. Env-only (`HARNERY_ARTIFACT_AUTO_CLEAN_INTERVAL_HOURS`,
|
|
225
233
|
* mainly for tests): the cadence is an implementation detail, not policy, and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAUH,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,wCAAwC,EAAE,MAAM,gCAAgC,CAAC;AAE1F,oFAAoF;AACpF,eAAO,MAAM,gBAAgB,SAAS,CAAC;AAEvC,yFAAyF;AACzF,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,2EAA2E;AAC3E,eAAO,MAAM,8BAA8B,MAAM,CAAC;AAElD,MAAM,WAAW,yBAAyB;IACxC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,wBAAwB,EAAE,MAAM,CAAC;CAClC;AAED,eAAO,MAAM,mCAAmC,EAAE,yBAMjD,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG,KAAK,GAAG,QAAQ,CAAC;AAE5D,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,4BAA4B,CAAC;CAC3C;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"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"}
|
package/dist/core/config.js
CHANGED
|
@@ -582,6 +582,22 @@ export function artifactAutoCleanEnabled(coordRoot) {
|
|
|
582
582
|
return true;
|
|
583
583
|
return readConfig(root).artifacts?.auto_clean !== false;
|
|
584
584
|
}
|
|
585
|
+
/**
|
|
586
|
+
* Whether `qa-run` and `review-pack create` sweep expired page review packs
|
|
587
|
+
* out of the artifact store before starting. Precedence:
|
|
588
|
+
* `HARNERY_REVIEW_PACK_AUTO_CLEAN` (1/true enables, 0/false disables) ->
|
|
589
|
+
* `review_pack.auto_clean` -> disabled. The sweep deletes only packs whose
|
|
590
|
+
* manifest says `managed: true` with `retention.expires_at` in the past.
|
|
591
|
+
*/
|
|
592
|
+
export function reviewPackAutoCleanEnabled(coordRoot) {
|
|
593
|
+
const env = coordEnv("REVIEW_PACK_AUTO_CLEAN");
|
|
594
|
+
if (env !== undefined)
|
|
595
|
+
return env === "1" || env.toLowerCase() === "true";
|
|
596
|
+
const root = coordRoot ?? findCoordRoot();
|
|
597
|
+
if (!root)
|
|
598
|
+
return false;
|
|
599
|
+
return readConfig(root).review_pack?.auto_clean === true;
|
|
600
|
+
}
|
|
585
601
|
/**
|
|
586
602
|
* Sweep cadence in hours. Env-only (`HARNERY_ARTIFACT_AUTO_CLEAN_INTERVAL_HOURS`,
|
|
587
603
|
* mainly for tests): the cadence is an implementation detail, not policy, and
|
|
@@ -25,11 +25,27 @@ export interface DiagnosticBundleListRow {
|
|
|
25
25
|
finding_id?: string;
|
|
26
26
|
error?: string;
|
|
27
27
|
}
|
|
28
|
+
export interface DiagnosticBundleCandidateRow {
|
|
29
|
+
artifact_id: string;
|
|
30
|
+
created_at: string;
|
|
31
|
+
expires_at: string | null;
|
|
32
|
+
classification: ArtifactInventoryEntry["classification"];
|
|
33
|
+
selectable: boolean;
|
|
34
|
+
captured_at?: string;
|
|
35
|
+
finding_id?: string;
|
|
36
|
+
error?: string;
|
|
37
|
+
}
|
|
28
38
|
export declare function captureDiagnosticBundle(repoRootRaw: string, input?: CaptureDiagnosticBundleInput): CapturedDiagnosticBundle;
|
|
29
39
|
export declare function validateDiagnosticBundle(repoRootRaw: string, ref: string): ValidatedDiagnosticBundle;
|
|
30
40
|
/** Frozen web readers accept an opaque artifact id, never a filesystem path. */
|
|
31
41
|
export declare function readFrozenDiagnosticBundle(repoRoot: string, artifactId: string): ValidatedDiagnosticBundle;
|
|
32
42
|
export declare function showDiagnosticBundle(repoRoot: string, ref: string): ValidatedDiagnosticBundle;
|
|
33
43
|
export declare function listDiagnosticBundles(repoRoot: string): DiagnosticBundleListRow[];
|
|
44
|
+
/**
|
|
45
|
+
* Lists managed bundle candidates without opening their bounded payloads.
|
|
46
|
+
* Selection is not authority: every show, replay, and comparison operation
|
|
47
|
+
* still performs complete digest and schema validation before returning data.
|
|
48
|
+
*/
|
|
49
|
+
export declare function listDiagnosticBundleCandidates(repoRoot: string): DiagnosticBundleCandidateRow[];
|
|
34
50
|
export declare function replayDiagnosticBundle(repoRoot: string, ref: string, now?: Date): DiagnosticReplayResult;
|
|
35
51
|
//# sourceMappingURL=bundle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../../src/core/diagnostics/bundle.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../../src/core/diagnostics/bundle.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EACV,aAAa,EACb,sBAAsB,EAEvB,MAAM,uBAAuB,CAAC;AAwB/B,OAAO,EAUL,KAAK,wBAAwB,EAG7B,KAAK,sBAAsB,EAE3B,KAAK,iBAAiB,EAEtB,KAAK,yBAAyB,EAC/B,MAAM,eAAe,CAAC;AA0BvB,MAAM,WAAW,4BAA4B;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,wBAAwB,CAAC;IACnC,OAAO,EAAE,iBAAiB,CAAC;CAC5B;AAED,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,sBAAsB,CAAC,gBAAgB,CAAC,CAAC;IACzD,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,4BAA4B;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,cAAc,EAAE,sBAAsB,CAAC,gBAAgB,CAAC,CAAC;IACzD,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,EACnB,KAAK,GAAE,4BAAiC,GACvC,wBAAwB,CAqG1B;AAkCD,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,MAAM,GACV,yBAAyB,CAsB3B;AAED,gFAAgF;AAChF,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,yBAAyB,CAK3B;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,yBAAyB,CAE7F;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,GAAG,uBAAuB,EAAE,CA0BjF;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAAC,QAAQ,EAAE,MAAM,GAAG,4BAA4B,EAAE,CA4C/F;AAqCD,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,GAAG,OAAa,GACf,sBAAsB,CAgBxB"}
|