opencode-codeops 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +39 -0
  3. package/_shared/spec-first-ordering.md +6 -2
  4. package/_shared/test-selection.md +130 -0
  5. package/bin/codeops-test.mjs +855 -0
  6. package/bin/lib/test-branch-policy.d.mts +51 -0
  7. package/bin/lib/test-branch-policy.mjs +185 -0
  8. package/bin/lib/test-map-validate.d.mts +42 -0
  9. package/bin/lib/test-map-validate.mjs +618 -0
  10. package/bin/lib/test-map.d.mts +114 -0
  11. package/bin/lib/test-map.mjs +384 -0
  12. package/bin/lib/test-provenance.d.mts +73 -0
  13. package/bin/lib/test-provenance.mjs +188 -0
  14. package/bin/lib/test-runner.d.mts +72 -0
  15. package/bin/lib/test-runner.mjs +350 -0
  16. package/bin/lib/test-selection-changes.d.mts +76 -0
  17. package/bin/lib/test-selection-changes.mjs +272 -0
  18. package/bin/lib/test-selection-explain.mjs +75 -0
  19. package/bin/lib/test-selection-glob.d.mts +32 -0
  20. package/bin/lib/test-selection-glob.mjs +206 -0
  21. package/bin/lib/test-selection.d.mts +80 -0
  22. package/bin/lib/test-selection.mjs +418 -0
  23. package/package.json +4 -2
  24. package/schemas/test-map.schema.json +164 -0
  25. package/scripts/__pycache__/codeops_plan.cpython-314.pyc +0 -0
  26. package/scripts/__pycache__/install_agents.cpython-314.pyc +0 -0
  27. package/skills/analyze-project/SKILL.md +8 -1
  28. package/skills/exec-plan/SKILL.md +16 -8
  29. package/skills/exec-plan/execution-protocol.md +15 -2
  30. package/skills/git-commit/SKILL.md +1 -1
  31. package/skills/make-plan/SKILL.md +10 -3
  32. package/skills/make-plan/quality-checklist.md +4 -0
  33. package/skills/make-plan/templates.md +19 -1
  34. package/skills/organize-tests/SKILL.md +169 -0
  35. package/skills/organize-tests/references/agents-testing-block.md +80 -0
  36. package/skills/organize-tests/references/analysis-protocol.md +94 -0
  37. package/skills/organize-tests/references/ci-github-actions.md +178 -0
  38. package/skills/organize-tests/references/language-adapters.md +54 -0
  39. package/skills/organize-tests/references/map-health.md +47 -0
  40. package/skills/organize-tests/references/project-wrapper.md +72 -0
  41. package/skills/organize-tests/references/shadow-mode.md +60 -0
  42. package/standards/coding-standards-full.md +28 -0
  43. package/standards/coding-standards.md +29 -0
  44. package/scripts/__pycache__/codeops_plan.cpython-312.pyc +0 -0
  45. package/scripts/__pycache__/install_agents.cpython-312.pyc +0 -0
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Type declarations for the test-map loader and canonical hash.
3
+ *
4
+ * The implementation is plain JavaScript so Node's test runner can import it
5
+ * directly; these declarations describe the public data shape for typed
6
+ * consumers.
7
+ */
8
+
9
+ /** One feature-slice test group. */
10
+ export interface TestMapGroup {
11
+ id: string
12
+ slice: string
13
+ tier: "task" | "phase" | "release" | "acceptance"
14
+ sources: string[]
15
+ tests?: string[]
16
+ dependsOn?: string[]
17
+ alwaysRun?: boolean
18
+ durationSec?: number
19
+ parallelSafe?: boolean
20
+ requires?: string[]
21
+ owner?: string
22
+ description?: string
23
+ acceptance?: { mode: "manual" | "external" | "na"; reason: string }
24
+ run: { command: string[]; cwd?: string }
25
+ }
26
+
27
+ /** Normalized map policy. */
28
+ export interface TestMapPolicy {
29
+ schema: number
30
+ full: string[] | string[][] | null
31
+ budgets: { taskTargetSeconds: number; taskAcceptableSeconds: number }
32
+ branches: Record<string, string | string[]>
33
+ ignorePaths: Array<{ glob: string; reason: string }>
34
+ fullOnPaths: string[]
35
+ acceptance: { mode: "automated" | "manual" | "external"; groups: string[]; checklist: string | null }
36
+ testRoots: string[] | null
37
+ fullThreshold: number
38
+ prSmoke: string[]
39
+ requirements: Record<string, { check: string[] }>
40
+ caseSensitive: boolean
41
+ [key: string]: unknown
42
+ }
43
+
44
+ /** A loaded map. */
45
+ export interface TestMap {
46
+ schema: number
47
+ policy: TestMapPolicy
48
+ groups: TestMapGroup[]
49
+ layout: "single" | "directory"
50
+ path: string
51
+ fragments: string[]
52
+ warnings: string[]
53
+ }
54
+
55
+ /** A map load result. */
56
+ export type LoadMapResult =
57
+ | { mode: "no-map" }
58
+ | { mode: "map"; map: TestMap; mapHash: string; path: string; fragments: string[]; warnings: string[] }
59
+
60
+ /** Error raised for an unreadable or unsafe map. */
61
+ export declare class MapLoadError extends Error {
62
+ code: string
63
+ path?: string
64
+ constructor(code: string, message: string, path?: string)
65
+ }
66
+
67
+ /** The only supported map schema. */
68
+ export declare const MAP_SCHEMA: number
69
+
70
+ /** Layout marker for one file. */
71
+ export declare const SINGLE_LAYOUT: "single"
72
+
73
+ /** Layout marker for a directory. */
74
+ export declare const DIRECTORY_LAYOUT: "directory"
75
+
76
+ /** Default branch classification. */
77
+ export declare const DEFAULT_BRANCHES: Readonly<Record<string, string | readonly string[]>>
78
+
79
+ /** Default task budgets. */
80
+ export declare const DEFAULT_BUDGETS: Readonly<{ taskTargetSeconds: number; taskAcceptableSeconds: number }>
81
+
82
+ /** Deep-copy a JSON-compatible value. */
83
+ export declare function cloneValue<T>(value: T): T
84
+
85
+ /** Check whether a repository path stays inside the root. */
86
+ export declare function isSafeRepoPath(value: unknown): boolean
87
+
88
+ /** Normalize a path to forward slashes. */
89
+ export declare function normalizeRepoPath(value: string): string
90
+
91
+ /** Resolve the map layout for a project. */
92
+ export declare function resolveMapPath(input: { root: string; mapPath?: string }): {
93
+ kind: "none" | "file" | "directory"
94
+ path: string | null
95
+ warning: string | null
96
+ }
97
+
98
+ /** Apply policy defaults. */
99
+ export declare function normalizePolicy(raw: unknown): TestMapPolicy
100
+
101
+ /** Load the project map. */
102
+ export declare function loadMap(input: { root: string; mapPath?: string }): LoadMapResult
103
+
104
+ /** Produce the canonical JSON used for hashing. */
105
+ export declare function canonicalMap(map: TestMap): string
106
+
107
+ /** Compute the stable map hash. */
108
+ export declare function hashMap(map: TestMap): string
109
+
110
+ /** Check whether a group relies on target discovery instead of `{tests}`. */
111
+ export declare function isTargetBasedGroup(group: TestMapGroup): boolean
112
+
113
+ /** Resolve a repo-relative path inside the root, or return null. */
114
+ export declare function resolveInsideRoot(root: string, relativePath: string): string | null
@@ -0,0 +1,384 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Test-map loading, policy normalization, and canonical hashing.
4
+ *
5
+ * A test map is plain data: a policy plus a list of feature-slice groups. This
6
+ * module owns only the file-level concerns — finding the map, merging fragments,
7
+ * applying defaults, and producing a stable hash. Validation and selection live
8
+ * in separate modules so each can be read and tested on its own.
9
+ *
10
+ * The loader is deliberately strict about layout and lenient about optional
11
+ * policy fields. A missing `full` command is left for the validator to report;
12
+ * inventing one here would hide a project configuration error.
13
+ *
14
+ * @module lib/test-map
15
+ */
16
+
17
+ import { createHash } from "node:crypto"
18
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"
19
+ import { isAbsolute, join, relative, resolve } from "node:path"
20
+
21
+ /** Layout marker used when one file holds the whole map. */
22
+ export const SINGLE_LAYOUT = "single"
23
+
24
+ /** Layout marker used when `codeops/test-map/` holds policy and fragments. */
25
+ export const DIRECTORY_LAYOUT = "directory"
26
+
27
+ /** The only map schema this version understands. */
28
+ export const MAP_SCHEMA = 1
29
+
30
+ /** Default branch classification, applied when policy does not override it. */
31
+ export const DEFAULT_BRANCHES = Object.freeze({
32
+ integration: Object.freeze(["main", "master"]),
33
+ feature: Object.freeze(["feature/**"]),
34
+ release: Object.freeze(["release/**", "release*", "hotfix/**", "hotfix*"]),
35
+ acceptance: Object.freeze(["acceptance/**", "acceptance*"]),
36
+ releaseTags: Object.freeze(["v*"]),
37
+ unknown: "integration",
38
+ })
39
+
40
+ /** Default soft task budget in seconds. */
41
+ export const DEFAULT_BUDGETS = Object.freeze({
42
+ taskTargetSeconds: 120,
43
+ taskAcceptableSeconds: 180,
44
+ })
45
+
46
+ /**
47
+ * Error raised when a map cannot be read or addressed safely.
48
+ *
49
+ * The stable `code` lets the CLI map the failure to an exit code without
50
+ * parsing an error message.
51
+ */
52
+ export class MapLoadError extends Error {
53
+ /**
54
+ * @param {string} code Stable machine code such as `map.policy`.
55
+ * @param {string} message Human-readable explanation.
56
+ * @param {string} [path] File or directory involved.
57
+ */
58
+ constructor(code, message, path) {
59
+ super(message)
60
+ this.name = "MapLoadError"
61
+ this.code = code
62
+ this.path = path
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Create one independent copy of a JSON-compatible value.
68
+ *
69
+ * @template T
70
+ * @param {T} value Value to copy.
71
+ * @returns {T} A deep copy that shares no mutable structure with the input.
72
+ */
73
+ export function cloneValue(value) {
74
+ if (Array.isArray(value)) return value.map((item) => cloneValue(item))
75
+ if (value && typeof value === "object") {
76
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneValue(item)]))
77
+ }
78
+ return value
79
+ }
80
+
81
+ /**
82
+ * Check whether a repository-relative path is safe to match or execute.
83
+ *
84
+ * Rejects absolute paths, Windows drive paths, NUL bytes, empty values, and any
85
+ * parent-directory segment. Backslashes are treated as separators so a Windows
86
+ * style traversal cannot slip through.
87
+ *
88
+ * @param {unknown} value Candidate path or glob.
89
+ * @returns {boolean} `true` when the value stays inside the repository.
90
+ * @example
91
+ * isSafeRepoPath("web/billing/**") // true
92
+ * isSafeRepoPath("../outside/**") // false
93
+ */
94
+ export function isSafeRepoPath(value) {
95
+ if (typeof value !== "string" || value.length === 0) return false
96
+ if (value.includes("\0")) return false
97
+ if (value.startsWith("/") || value.startsWith("\\")) return false
98
+ if (/^[A-Za-z]:[\\/]/.test(value)) return false
99
+ const normalized = value.replace(/\\/g, "/")
100
+ return !normalized.split("/").includes("..")
101
+ }
102
+
103
+ /**
104
+ * Normalize a path or glob to POSIX separators without changing its meaning.
105
+ *
106
+ * @param {string} value Path from a map or a Git change set.
107
+ * @returns {string} The same path with forward slashes.
108
+ */
109
+ export function normalizeRepoPath(value) {
110
+ return value.replace(/\\/g, "/")
111
+ }
112
+
113
+ /**
114
+ * Resolve which map layout a project uses.
115
+ *
116
+ * A single file wins when both shapes exist; the caller receives a warning so
117
+ * `check --strict` can surface the ambiguity without guessing.
118
+ *
119
+ * @param {object} input Resolution input.
120
+ * @param {string} input.root Repository root.
121
+ * @param {string} [input.mapPath] Explicit file or directory override.
122
+ * @returns {{kind: "none"|"file"|"directory", path: string|null, warning: string|null}}
123
+ */
124
+ export function resolveMapPath({ root, mapPath }) {
125
+ if (mapPath) {
126
+ const absolute = resolve(root, mapPath)
127
+ if (!existsSync(absolute)) return { kind: "none", path: absolute, warning: null }
128
+ const info = statSync(absolute)
129
+ return {
130
+ kind: info.isDirectory() ? "directory" : "file",
131
+ path: absolute,
132
+ warning: null,
133
+ }
134
+ }
135
+
136
+ const single = join(root, "codeops", "test-map.json")
137
+ const directory = join(root, "codeops", "test-map")
138
+ const hasSingle = existsSync(single)
139
+ const hasDirectory = existsSync(directory)
140
+
141
+ if (hasSingle && hasDirectory) {
142
+ return {
143
+ kind: "file",
144
+ path: single,
145
+ warning: "both codeops/test-map.json and codeops/test-map/ exist; the single file wins",
146
+ }
147
+ }
148
+ if (hasSingle) return { kind: "file", path: single, warning: null }
149
+ if (hasDirectory) return { kind: "directory", path: directory, warning: null }
150
+ return { kind: "none", path: null, warning: null }
151
+ }
152
+
153
+ /**
154
+ * Read and parse one JSON file with a stable error code.
155
+ *
156
+ * @param {string} absolutePath File to read.
157
+ * @returns {unknown} Parsed JSON.
158
+ * @throws {MapLoadError} When the file is missing or is not valid JSON.
159
+ */
160
+ function readJson(absolutePath) {
161
+ let text
162
+ try {
163
+ text = readFileSync(absolutePath, "utf8")
164
+ } catch (error) {
165
+ throw new MapLoadError("map.policy", `cannot read ${absolutePath}: ${error.message}`, absolutePath)
166
+ }
167
+ try {
168
+ return JSON.parse(text)
169
+ } catch (error) {
170
+ throw new MapLoadError("map.parse", `invalid JSON in ${absolutePath}: ${error.message}`, absolutePath)
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Apply defaults and copy the known policy fields into one normalized object.
176
+ *
177
+ * Unknown fields are preserved so a future schema can report them rather than
178
+ * silently dropping project intent.
179
+ *
180
+ * @param {object} raw Policy object from the map.
181
+ * @returns {object} Normalized policy with defaults applied.
182
+ */
183
+ export function normalizePolicy(raw) {
184
+ const source = raw && typeof raw === "object" ? raw : {}
185
+ const branches = source.branches && typeof source.branches === "object" ? source.branches : {}
186
+ const budgets = source.budgets && typeof source.budgets === "object" ? source.budgets : {}
187
+ return {
188
+ ...cloneValue(source),
189
+ schema: source.schema ?? MAP_SCHEMA,
190
+ full: source.full === undefined ? null : cloneValue(source.full),
191
+ budgets: {
192
+ taskTargetSeconds: budgets.taskTargetSeconds ?? DEFAULT_BUDGETS.taskTargetSeconds,
193
+ taskAcceptableSeconds: budgets.taskAcceptableSeconds ?? DEFAULT_BUDGETS.taskAcceptableSeconds,
194
+ },
195
+ branches: {
196
+ ...cloneValue(DEFAULT_BRANCHES),
197
+ ...cloneValue(branches),
198
+ },
199
+ ignorePaths: cloneValue(source.ignorePaths ?? []),
200
+ fullOnPaths: cloneValue(source.fullOnPaths ?? []),
201
+ acceptance: cloneValue(source.acceptance ?? { mode: "automated", groups: [], checklist: null }),
202
+ testRoots: source.testRoots === undefined ? null : cloneValue(source.testRoots),
203
+ fullThreshold: source.fullThreshold ?? 0.6,
204
+ prSmoke: cloneValue(source.prSmoke ?? []),
205
+ requirements: cloneValue(source.requirements ?? {}),
206
+ caseSensitive: source.caseSensitive ?? true,
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Load the project test map.
212
+ *
213
+ * @param {object} input Load input.
214
+ * @param {string} input.root Repository root.
215
+ * @param {string} [input.mapPath] Explicit map file or directory.
216
+ * @returns {{mode: "no-map"} | {mode: "map", map: object, mapHash: string, path: string, fragments: string[], warnings: string[]}}
217
+ * @throws {MapLoadError} When a present map cannot be read or parsed.
218
+ * @example
219
+ * const loaded = loadMap({ root: process.cwd() })
220
+ * if (loaded.mode === "map") console.log(loaded.mapHash)
221
+ */
222
+ export function loadMap({ root, mapPath }) {
223
+ const resolvedRoot = resolve(root)
224
+ const located = resolveMapPath({ root: resolvedRoot, mapPath: mapPath === undefined ? undefined : mapPath })
225
+ if (located.kind === "none") return { mode: "no-map" }
226
+
227
+ const warnings = located.warning ? [located.warning] : []
228
+ let map
229
+
230
+ if (located.kind === "file") {
231
+ const data = readJson(located.path)
232
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
233
+ throw new MapLoadError("map.schema", "the map file must contain a JSON object", located.path)
234
+ }
235
+ map = {
236
+ schema: data.schema ?? data.policy?.schema ?? MAP_SCHEMA,
237
+ policy: normalizePolicy({ schema: data.schema, ...(data.policy ?? {}) }),
238
+ groups: Array.isArray(data.groups) ? cloneValue(data.groups) : [],
239
+ layout: SINGLE_LAYOUT,
240
+ path: located.path,
241
+ fragments: [],
242
+ warnings,
243
+ }
244
+ } else {
245
+ const policyPath = join(located.path, "policy.json")
246
+ if (!existsSync(policyPath)) {
247
+ throw new MapLoadError("map.policy", "codeops/test-map/policy.json is required", policyPath)
248
+ }
249
+ const policyData = readJson(policyPath)
250
+ const names = listFragmentNames(located.path)
251
+ const groups = []
252
+ for (const name of names) {
253
+ const fragment = readJson(join(located.path, name))
254
+ if (!fragment || typeof fragment !== "object") {
255
+ throw new MapLoadError("map.schema", `fragment ${name} must contain an object`, join(located.path, name))
256
+ }
257
+ if (Array.isArray(fragment.groups)) groups.push(...cloneValue(fragment.groups))
258
+ }
259
+ map = {
260
+ schema: policyData.schema ?? MAP_SCHEMA,
261
+ policy: normalizePolicy(policyData),
262
+ groups,
263
+ layout: DIRECTORY_LAYOUT,
264
+ path: located.path,
265
+ fragments: names,
266
+ warnings,
267
+ }
268
+ }
269
+
270
+ return {
271
+ mode: "map",
272
+ map,
273
+ mapHash: hashMap(map),
274
+ path: map.path,
275
+ fragments: map.fragments,
276
+ warnings: map.warnings,
277
+ }
278
+ }
279
+
280
+ /**
281
+ * List fragment file names for a directory map.
282
+ *
283
+ * `shared.json` loads first so cross-cutting groups are available to every
284
+ * domain fragment's dependency references. Remaining fragments load in sorted
285
+ * order to keep diagnostics stable across machines.
286
+ *
287
+ * @param {string} directory Map directory.
288
+ * @returns {string[]} Fragment file names.
289
+ */
290
+ function listFragmentNames(directory) {
291
+ const names = readdirSync(directory)
292
+ .filter((name) => name.endsWith(".json"))
293
+ .filter((name) => name !== "policy.json")
294
+ .sort()
295
+ const sharedIndex = names.indexOf("shared.json")
296
+ if (sharedIndex > 0) {
297
+ names.splice(sharedIndex, 1)
298
+ names.unshift("shared.json")
299
+ }
300
+ return names
301
+ }
302
+
303
+ /**
304
+ * Sort object keys recursively so equal content serializes identically.
305
+ *
306
+ * @param {unknown} value JSON-compatible value.
307
+ * @returns {unknown} A key-sorted copy.
308
+ */
309
+ function sortKeys(value) {
310
+ if (Array.isArray(value)) return value.map(sortKeys)
311
+ if (value && typeof value === "object") {
312
+ return Object.fromEntries(
313
+ Object.keys(value)
314
+ .sort()
315
+ .map((key) => [key, sortKeys(value[key])]),
316
+ )
317
+ }
318
+ return value
319
+ }
320
+
321
+ /**
322
+ * Produce the canonical JSON used for hashing.
323
+ *
324
+ * Groups are sorted by ID and object keys are sorted recursively. Load-only
325
+ * metadata such as the layout, file path, fragments, and warnings is excluded,
326
+ * so reordering fragments never changes the hash.
327
+ *
328
+ * @param {object} map Loaded map.
329
+ * @returns {string} Canonical JSON string.
330
+ */
331
+ export function canonicalMap(map) {
332
+ const groups = [...(map.groups ?? [])].sort((left, right) =>
333
+ String(left.id ?? "").localeCompare(String(right.id ?? "")),
334
+ )
335
+ return JSON.stringify(
336
+ sortKeys({
337
+ schema: map.schema,
338
+ policy: map.policy,
339
+ groups,
340
+ }),
341
+ )
342
+ }
343
+
344
+ /**
345
+ * Compute the stable SHA-256 hash of a map.
346
+ *
347
+ * @param {object} map Loaded map.
348
+ * @returns {string} Lowercase hexadecimal SHA-256 digest.
349
+ * @example
350
+ * const hash = hashMap(loaded.map)
351
+ */
352
+ export function hashMap(map) {
353
+ return createHash("sha256").update(canonicalMap(map)).digest("hex")
354
+ }
355
+
356
+ /**
357
+ * Check whether a group relies on its runner's own target discovery.
358
+ *
359
+ * A target-based group has no `{tests}` token, so it may omit `tests` and is
360
+ * exempt from file-ownership checks.
361
+ *
362
+ * @param {object} group Map group.
363
+ * @returns {boolean} `true` when the command contains no `{tests}` placeholder.
364
+ */
365
+ export function isTargetBasedGroup(group) {
366
+ const command = group?.run?.command
367
+ if (!Array.isArray(command)) return false
368
+ return !command.some((token) => typeof token === "string" && token.includes("{tests}"))
369
+ }
370
+
371
+ /**
372
+ * Convert a repository-relative path to an absolute path inside the root.
373
+ *
374
+ * @param {string} root Repository root.
375
+ * @param {string} relativePath Safe repository-relative path.
376
+ * @returns {string|null} Absolute path, or `null` when the path would escape.
377
+ */
378
+ export function resolveInsideRoot(root, relativePath) {
379
+ if (!isSafeRepoPath(relativePath)) return null
380
+ const absolute = resolve(root, relativePath)
381
+ const relation = relative(resolve(root), absolute)
382
+ if (relation.startsWith("..") || isAbsolute(relation)) return null
383
+ return absolute
384
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Type declarations for release provenance.
3
+ */
4
+
5
+ /** Artifact evidence in a provenance record. */
6
+ export interface ProvenanceArtifact {
7
+ path: string | null
8
+ digest: string
9
+ sourceCommit: string
10
+ tested: "source" | "artifact"
11
+ reproducible: boolean
12
+ }
13
+
14
+ /** One provenance record. */
15
+ export interface ProvenanceRecord {
16
+ schema: number
17
+ commit: string
18
+ ref: string | null
19
+ gate: string
20
+ mapHash: string | null
21
+ selection: "map" | "no-map"
22
+ full: boolean
23
+ groups: string[]
24
+ fullRunId: string | null
25
+ artifact: ProvenanceArtifact | null
26
+ coverage: object | null
27
+ coverageStatus: "provided" | "not-provided"
28
+ acceptance: object | null
29
+ acceptanceStatus: "provided" | "not-provided"
30
+ startedAt: string
31
+ finishedAt: string
32
+ result: string
33
+ }
34
+
35
+ /** Verification result. */
36
+ export interface ProvenanceVerification {
37
+ ok: boolean
38
+ errors: Array<{ field: string; message: string }>
39
+ }
40
+
41
+ /** Compute a file digest. */
42
+ export declare function sha256File(path: string): string
43
+
44
+ /** Build a provenance record. */
45
+ export declare function createProvenanceRecord(input: {
46
+ commit: string
47
+ ref?: string | null
48
+ gate: string
49
+ mapHash: string | null
50
+ groups?: string[]
51
+ fullRunId?: string | null
52
+ artifactPath?: string | null
53
+ artifactDigest?: string | null
54
+ sourceCommit?: string | null
55
+ artifactTested?: "source" | "artifact"
56
+ reproducible?: boolean
57
+ coverage?: object | null
58
+ acceptance?: object | null
59
+ result?: string
60
+ startedAt?: string
61
+ finishedAt?: string
62
+ }): ProvenanceRecord
63
+
64
+ /** Verify a record against expected release facts. */
65
+ export declare function verifyProvenance(
66
+ record: ProvenanceRecord,
67
+ expected?: {
68
+ commit?: string
69
+ mapHash?: string
70
+ artifactPath?: string
71
+ artifactDigest?: string
72
+ }
73
+ ): ProvenanceVerification