@bjoernrochel/sevro 0.1.0-rc.7 → 1.0.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 (230) hide show
  1. package/CONTRIBUTING.md +22 -6
  2. package/README.md +5 -1
  3. package/docs/README.md +19 -15
  4. package/docs/architecture.md +138 -17
  5. package/docs/assets/README.md +5 -4
  6. package/docs/builtin-graders-v1.md +17 -3
  7. package/docs/codex-host.md +155 -49
  8. package/docs/contracts.md +2 -1
  9. package/docs/creating-cases.md +9 -2
  10. package/docs/development-cli.md +187 -19
  11. package/docs/documentation-quality.md +12 -2
  12. package/docs/evaluation-workflow.md +110 -0
  13. package/docs/extension-protocol-v1.md +26 -3
  14. package/docs/getting-started.md +23 -2
  15. package/docs/guide-evaluation.md +33 -1
  16. package/docs/identity-v1.md +25 -2
  17. package/docs/installing.md +11 -11
  18. package/docs/native-controls.md +76 -5
  19. package/docs/native-hosts.md +196 -7
  20. package/docs/reading-results.md +102 -0
  21. package/docs/recipes.md +489 -0
  22. package/docs/releases.md +31 -12
  23. package/docs/report-v1.md +29 -1
  24. package/docs/results-v1.md +141 -4
  25. package/docs/run-settings-v1.md +147 -0
  26. package/docs/runtime-v1.md +307 -18
  27. package/docs/troubleshooting.md +12 -12
  28. package/docs/typescript-quality.md +89 -26
  29. package/examples/recipes/candidate.ts +26 -0
  30. package/examples/recipes/cases/coding-change.json +21 -0
  31. package/examples/recipes/cases/host-tools.json +21 -0
  32. package/examples/recipes/cases/mise.json +21 -0
  33. package/examples/recipes/cases/node.json +21 -0
  34. package/examples/recipes/cases/python-uv.json +21 -0
  35. package/examples/recipes/cases/semantic.json +17 -0
  36. package/examples/recipes/coding-change/check.ts +23 -0
  37. package/examples/recipes/extension-artifacts.ts +65 -0
  38. package/examples/recipes/extension-methods.ts +174 -0
  39. package/examples/recipes/extension.ts +37 -0
  40. package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
  41. package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
  42. package/examples/recipes/repository-skill/README.md +3 -0
  43. package/examples/recipes/run-inputs.ts +188 -0
  44. package/examples/recipes/run-options.ts +131 -0
  45. package/examples/recipes/run.ts +63 -0
  46. package/examples/recipes/runtime/empty.json +1 -0
  47. package/examples/recipes/runtime/goals.json +1 -0
  48. package/examples/recipes/runtime/mise.json +6 -0
  49. package/examples/recipes/runtime/path.json +1 -0
  50. package/examples/recipes/runtime/plugins.json +4 -0
  51. package/examples/recipes/runtime/python-uv.json +10 -0
  52. package/examples/recipes/semantic-judge.ts +29 -0
  53. package/examples/templates/coding/adapter.ts +17 -0
  54. package/examples/templates/coding/case.json +15 -0
  55. package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
  56. package/examples/templates/repository-skill/extension-data.ts +64 -0
  57. package/examples/templates/repository-skill/extension.ts +31 -0
  58. package/examples/templates/response/adapter.ts +12 -0
  59. package/examples/templates/response/case.json +13 -0
  60. package/package.json +11 -5
  61. package/schemas/report-v1.schema.json +9 -0
  62. package/schemas/run-evidence-v1.schema.json +2 -1
  63. package/schemas/runtime-v1.schema.json +63 -7
  64. package/src/advisory.ts +2 -2
  65. package/src/candidate-artifacts.ts +206 -0
  66. package/src/candidate-codex-state.ts +185 -0
  67. package/src/case-validation.ts +73 -50
  68. package/src/cli/cancellation.ts +20 -0
  69. package/src/cli/command.ts +87 -0
  70. package/src/cli/completion.ts +15 -0
  71. package/src/cli/demo.ts +60 -0
  72. package/src/cli/evaluation.ts +55 -0
  73. package/src/cli/extension.ts +79 -0
  74. package/src/cli/help.ts +198 -0
  75. package/src/cli/hosts.ts +55 -0
  76. package/src/cli/init-settings.ts +30 -0
  77. package/src/cli/init.ts +111 -0
  78. package/src/{cli-invocation.ts → cli/invocation.ts} +214 -218
  79. package/src/cli/loading.ts +151 -0
  80. package/src/cli/preparation.ts +154 -0
  81. package/src/cli/presentation.ts +93 -0
  82. package/src/cli/progress.ts +6 -0
  83. package/src/cli/run-manifest.ts +128 -0
  84. package/src/cli/run-options.ts +403 -0
  85. package/src/cli/run-overrides.ts +69 -0
  86. package/src/cli/settings.ts +154 -0
  87. package/src/cli/summary.ts +43 -0
  88. package/src/cli.ts +13 -591
  89. package/src/contract-types.ts +94 -0
  90. package/src/engine.ts +13 -5
  91. package/src/evaluation/advisory.ts +150 -0
  92. package/src/{evaluation-check-plan.ts → evaluation/check-plan.ts} +103 -67
  93. package/src/evaluation/collection.ts +88 -0
  94. package/src/evaluation/context.ts +10 -0
  95. package/src/{evaluation-grading-host.ts → evaluation/grading-host.ts} +5 -5
  96. package/src/evaluation/grading.ts +235 -0
  97. package/src/{evaluation-invocation-plan.ts → evaluation/invocation-plan.ts} +147 -58
  98. package/src/{evaluation-plan.ts → evaluation/plan.ts} +208 -141
  99. package/src/evaluation/progress.ts +19 -0
  100. package/src/evaluation/resources.ts +241 -0
  101. package/src/evaluation/run.ts +206 -0
  102. package/src/{evaluation-semantic.ts → evaluation/semantic.ts} +133 -115
  103. package/src/evaluation/trial.ts +800 -0
  104. package/src/evaluation/types.ts +207 -0
  105. package/src/evidence/host-results.ts +85 -0
  106. package/src/evidence/retained-artifacts.ts +18 -0
  107. package/src/evidence/trial-writer.ts +203 -0
  108. package/src/evidence/usage.ts +17 -0
  109. package/src/extension-client.ts +34 -96
  110. package/src/extension-session.ts +24 -48
  111. package/src/{advisory-fixture.ts → fixtures/advisory.ts} +15 -76
  112. package/src/{fixture-bin.ts → fixtures/bin.ts} +1 -1
  113. package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
  114. package/src/fixtures/create.ts +183 -0
  115. package/src/{generated-fixture.ts → fixtures/generated.ts} +10 -53
  116. package/src/{git-hooks.ts → fixtures/git-hooks.ts} +1 -1
  117. package/src/{preparation.ts → fixtures/preparation.ts} +6 -16
  118. package/src/{repository-fixture.ts → fixtures/repository.ts} +30 -64
  119. package/src/{fixture-setup.ts → fixtures/setup.ts} +33 -51
  120. package/src/{fixture-tools.ts → fixtures/tools.ts} +1 -1
  121. package/src/fixtures/workspace-state.ts +212 -0
  122. package/src/generated/cli-result.d.cts +2 -1
  123. package/src/generated/extension.d.cts +2 -1
  124. package/src/generated/report.cjs +1 -1
  125. package/src/generated/report.d.cts +2 -1
  126. package/src/generated/run-evidence.cjs +1 -1
  127. package/src/generated/run-evidence.d.cts +2 -1
  128. package/src/generated/runtime.cjs +1 -1
  129. package/src/generated/runtime.d.cts +2 -1
  130. package/src/git.ts +93 -0
  131. package/src/graders/git-head.ts +29 -84
  132. package/src/graders/shell.ts +57 -68
  133. package/src/host-diagnostic-redaction.ts +52 -0
  134. package/src/host-diagnostics.ts +331 -0
  135. package/src/hosts/claude-continuation.ts +57 -40
  136. package/src/hosts/claude-credential.ts +160 -63
  137. package/src/hosts/claude-events.ts +34 -18
  138. package/src/hosts/claude-keychain.ts +78 -21
  139. package/src/hosts/claude-login-recovery.ts +85 -0
  140. package/src/hosts/claude-login-refresh.ts +178 -0
  141. package/src/hosts/claude-login-store.ts +162 -0
  142. package/src/hosts/claude-login.ts +107 -0
  143. package/src/hosts/claude-native-goal.ts +15 -12
  144. package/src/hosts/claude-process.ts +10 -50
  145. package/src/hosts/claude-refresh-lock.ts +81 -0
  146. package/src/hosts/claude-repository-invocation.ts +9 -34
  147. package/src/hosts/claude-settings.ts +91 -14
  148. package/src/hosts/claude-skill-activations.ts +169 -0
  149. package/src/hosts/claude-state.ts +46 -16
  150. package/src/hosts/claude-tool-calls.ts +13 -10
  151. package/src/hosts/claude.ts +122 -73
  152. package/src/hosts/codex-app-server.ts +78 -52
  153. package/src/hosts/codex-configuration.ts +5 -10
  154. package/src/hosts/codex-exec-controls.ts +516 -0
  155. package/src/hosts/codex-marketplace.ts +12 -10
  156. package/src/hosts/codex-native-calls.ts +15 -122
  157. package/src/hosts/codex-process.ts +10 -54
  158. package/src/hosts/codex-profile.ts +50 -21
  159. package/src/hosts/codex-session-files.ts +1 -9
  160. package/src/hosts/codex-state.ts +141 -19
  161. package/src/hosts/codex.ts +221 -179
  162. package/src/hosts/isolation-common.ts +15 -11
  163. package/src/hosts/isolation-roots.ts +13 -20
  164. package/src/hosts/linux-sandbox.ts +27 -3
  165. package/src/hosts/mac-sandbox.ts +111 -18
  166. package/src/hosts/native-controls.ts +72 -22
  167. package/src/hosts/native-request.ts +55 -0
  168. package/src/hosts/process-lifecycle.ts +221 -45
  169. package/src/hosts/protected-worktrees.ts +1 -1
  170. package/src/hosts/skill-hook-catalog.ts +131 -0
  171. package/src/hosts/skill-hook-handler.ts +374 -0
  172. package/src/hosts/skill-hooks.ts +577 -0
  173. package/src/instrumentation.ts +7 -8
  174. package/src/native-transcript-state.ts +22 -3
  175. package/src/native-transcript-view.ts +52 -13
  176. package/src/native-transcripts.ts +34 -15
  177. package/src/paths.ts +22 -0
  178. package/src/process-lifecycle.ts +150 -0
  179. package/src/provenance.ts +27 -7
  180. package/src/prune.ts +300 -0
  181. package/src/report.ts +63 -36
  182. package/src/resolved-case.ts +1 -1
  183. package/src/results-lock.ts +42 -0
  184. package/src/results.ts +14 -18
  185. package/src/run-owner.ts +4 -4
  186. package/src/{runtime-config.ts → runtime/config.ts} +182 -44
  187. package/src/{runtime-discovery-apple.ts → runtime/discovery-apple.ts} +3 -3
  188. package/src/{runtime-discovery-providers.ts → runtime/discovery-providers.ts} +3 -9
  189. package/src/{runtime-discovery.ts → runtime/discovery.ts} +4 -4
  190. package/src/{runtime-hook-processes.ts → runtime/hook-processes.ts} +1 -1
  191. package/src/{runtime-hooks.ts → runtime/hooks.ts} +33 -22
  192. package/src/{runtime-paths.ts → runtime/paths.ts} +6 -18
  193. package/src/{runtime-query.ts → runtime/query.ts} +5 -5
  194. package/src/{runtime-seeds.ts → runtime/seeds.ts} +3 -3
  195. package/src/runtime/shared-cache.ts +130 -0
  196. package/src/{runtime-state.ts → runtime/state.ts} +28 -11
  197. package/src/schema-types.ts +73 -161
  198. package/src/shell.ts +4 -0
  199. package/src/transcript-retention.ts +146 -0
  200. package/src/value-guards.ts +22 -0
  201. package/docs/assets/sevro-logo.png +0 -0
  202. package/docs/assets/sevro-symbol.svg +0 -1
  203. package/docs/assets/sevro-wordmark.svg +0 -1
  204. package/docs/cache-ownership-validation.md +0 -49
  205. package/docs/claude-continuation-validation.md +0 -47
  206. package/docs/concurrency-validation.md +0 -86
  207. package/docs/configuration-root-validation.md +0 -70
  208. package/docs/engine-slice.md +0 -114
  209. package/docs/evidence/runtime-startup-30/README.md +0 -185
  210. package/docs/evidence/runtime-startup-30/after.json +0 -319
  211. package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +0 -2
  212. package/docs/evidence/runtime-startup-30/before.json +0 -403
  213. package/docs/evidence/runtime-startup-30/consumer-smoke.json +0 -63
  214. package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +0 -3
  215. package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +0 -3
  216. package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +0 -20
  217. package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +0 -2
  218. package/docs/evidence.md +0 -17
  219. package/docs/fixture-cleanup-validation.md +0 -70
  220. package/docs/guide-verification.json +0 -239
  221. package/src/evaluation-context.ts +0 -257
  222. package/src/evaluation-fixture.ts +0 -313
  223. package/src/evaluation-run.ts +0 -198
  224. package/src/evaluation-trial.ts +0 -946
  225. package/src/evaluation-types.ts +0 -149
  226. package/src/hosts/claude-nested-skills.ts +0 -272
  227. package/src/hosts/codex-command-output.ts +0 -510
  228. package/src/hosts/codex-executor-command.ts +0 -291
  229. package/src/hosts/codex-skill-reads.ts +0 -609
  230. /package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +0 -0
@@ -0,0 +1,130 @@
1
+ import { access, lstat, realpath, symlink } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { userInfo } from "node:os";
4
+ import { isAbsolute, join } from "node:path";
5
+ import type { RuntimePolicy, RuntimeRole } from "./config";
6
+ import { declaredRuntimeRoots, requireRuntimeReadRoots } from "./paths";
7
+
8
+ export interface SharedCacheDirectory {
9
+ source: string;
10
+ target: string;
11
+ roles: RuntimeRole[];
12
+ }
13
+
14
+ export async function resolveSharedCaches(
15
+ caches: SharedCacheDirectory[],
16
+ environment: Record<string, string>,
17
+ base: string,
18
+ protectedRoots: string[],
19
+ ): Promise<SharedCacheDirectory[]> {
20
+ return Promise.all(
21
+ caches.map(async (cache) => {
22
+ const roots = await declaredRuntimeRoots(
23
+ [cache.source],
24
+ environment,
25
+ base,
26
+ );
27
+ requireRuntimeReadRoots(roots, protectedRoots);
28
+ const [declared] = roots;
29
+ if (!declared) throw new Error("runtime shared cache source is missing");
30
+ const source = await realpath(declared);
31
+ await requireSharedCache(source);
32
+ return { ...cache, source };
33
+ }),
34
+ );
35
+ }
36
+
37
+ export function selectedSharedCaches(
38
+ policy: RuntimePolicy | undefined,
39
+ role: RuntimeRole = "candidate",
40
+ ): SharedCacheDirectory[] {
41
+ return (policy?.sharedCacheDirectories ?? []).filter((cache) =>
42
+ cache.roles.includes(role),
43
+ );
44
+ }
45
+
46
+ export async function sharedCacheRoots(
47
+ policy: RuntimePolicy | undefined,
48
+ role: RuntimeRole = "candidate",
49
+ protectedRoots: string[],
50
+ ): Promise<string[]> {
51
+ const roots = selectedSharedCaches(policy, role).map((cache) => cache.source);
52
+ return requireWritableCacheRoots(roots, protectedRoots);
53
+ }
54
+
55
+ export function requireSharedCachePolicy(
56
+ policy: RuntimePolicy | undefined,
57
+ ): void {
58
+ if (!policy) return;
59
+ const caches = policy.sharedCacheDirectories ?? [];
60
+ const protectedRoots = [
61
+ ...policy.readOnlyRoots,
62
+ ...(policy.seeds ?? []).map((seed) => seed.source),
63
+ ];
64
+ for (const cache of caches) {
65
+ requireRuntimeReadRoots(
66
+ [cache.source],
67
+ [
68
+ ...protectedRoots,
69
+ ...caches
70
+ .filter((other) => other !== cache)
71
+ .map((other) => other.source),
72
+ ],
73
+ );
74
+ }
75
+ }
76
+
77
+ export function sharedCacheDenyRoots(
78
+ policy: RuntimePolicy | undefined,
79
+ role: RuntimeRole = "candidate",
80
+ ): string[] {
81
+ return (policy?.sharedCacheDirectories ?? [])
82
+ .filter((cache) => !cache.roles.includes(role))
83
+ .map((cache) => cache.source);
84
+ }
85
+
86
+ export async function requireWritableCacheRoots(
87
+ roots: string[],
88
+ protectedRoots: string[],
89
+ ): Promise<string[]> {
90
+ requireRuntimeReadRoots(roots, protectedRoots);
91
+ for (const root of roots) await requireSharedCache(root);
92
+ return roots;
93
+ }
94
+
95
+ async function requireSharedCache(source: string): Promise<void> {
96
+ if (!isAbsolute(source) || (await realpath(source)) !== source)
97
+ throw new Error("runtime shared cache must be a canonical directory");
98
+ const info = await lstat(source);
99
+ if (!info.isDirectory() || info.uid !== userInfo().uid)
100
+ throw new Error("runtime shared cache must be an owned directory");
101
+ await access(source, constants.R_OK | constants.W_OK | constants.X_OK);
102
+ }
103
+
104
+ /** A slot aliases the operator-selected cache; its contents are never copied. */
105
+ export async function linkSharedCaches(
106
+ policy: RuntimePolicy,
107
+ role: RuntimeRole,
108
+ root: string,
109
+ ): Promise<void> {
110
+ for (const cache of selectedSharedCaches(policy, role)) {
111
+ const target = join(root, cache.target);
112
+ try {
113
+ await symlink(cache.source, target, "dir");
114
+ } catch (cause) {
115
+ if ((cause as NodeJS.ErrnoException).code !== "EEXIST") throw cause;
116
+ await requireCacheSlot(target, cache.source, cause);
117
+ }
118
+ }
119
+ }
120
+
121
+ async function requireCacheSlot(
122
+ target: string,
123
+ source: string,
124
+ cause: unknown,
125
+ ): Promise<void> {
126
+ if (!(await lstat(target)).isSymbolicLink())
127
+ throw new Error("runtime shared cache slot was replaced", { cause });
128
+ if ((await realpath(target)) !== source)
129
+ throw new Error("runtime shared cache slot changed its source", { cause });
130
+ }
@@ -1,17 +1,21 @@
1
1
  import { mkdir, realpath, stat } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import {
4
- requireRuntimeEnvironment,
4
+ requireRuntimePolicy,
5
5
  runtimeRoleSeeds,
6
6
  type RuntimeRole,
7
- requireRuntimeSeedTargets,
8
7
  type RuntimePolicy,
9
- } from "./runtime-config";
10
- import { materializeRuntimeSeed } from "./runtime-seeds";
11
- import { requireRuntimeReadRoots } from "./runtime-paths";
12
- import { hashJson } from "./identity";
8
+ } from "./config";
9
+ import { materializeRuntimeSeed } from "./seeds";
10
+ import { requireRuntimeReadRoots } from "./paths";
11
+ import { hashJson } from "../identity";
12
+ import {
13
+ linkSharedCaches,
14
+ sharedCacheRoots,
15
+ selectedSharedCaches,
16
+ } from "./shared-cache";
13
17
 
14
- export type { RuntimeRole } from "./runtime-config";
18
+ export type { RuntimeRole } from "./config";
15
19
 
16
20
  export function runtimeObservations(
17
21
  policy: RuntimePolicy | undefined,
@@ -29,6 +33,9 @@ export function runtimeObservations(
29
33
  environmentNames: Object.keys(policy.environment).sort(),
30
34
  readOnlyRoots: policy.readOnlyRoots,
31
35
  ...discoveryObservations(policy),
36
+ ...(policy.sharedCacheDirectories?.length
37
+ ? { sharedCacheDirectories: selectedSharedCaches(policy, role) }
38
+ : {}),
32
39
  seeds: runtimeRoleSeeds(policy, role).map((seed) => ({
33
40
  target: seed.target,
34
41
  sha256: seed.sha256,
@@ -57,16 +64,19 @@ function runtimeDigest(policy: RuntimePolicy): string {
57
64
  });
58
65
  }
59
66
 
60
- /** Each role gets private writable copies while the host inputs stay protected. */
67
+ /** Private role state contains copied seeds and aliases to explicitly shared caches. */
61
68
  export async function prepareRuntimeState(
62
69
  workspace: string,
63
70
  policy: RuntimePolicy,
64
71
  role: RuntimeRole,
65
72
  privateRoot?: string,
66
73
  ) {
67
- requireRuntimeEnvironment(policy.environment);
68
- requireRuntimeSeedTargets(policy.seeds ?? []);
74
+ requireRuntimePolicy(policy);
69
75
  const root = await runtimeRoot(workspace, role, privateRoot);
76
+ const writableCacheRoots = await sharedCacheRoots(policy, role, [
77
+ workspace,
78
+ root,
79
+ ]);
70
80
  const home = join(root, "home"),
71
81
  temp = join(root, "tmp");
72
82
  await Promise.all(
@@ -74,6 +84,7 @@ export async function prepareRuntimeState(
74
84
  );
75
85
  for (const seed of runtimeRoleSeeds(policy, role))
76
86
  await copyRuntimeSeed(seed, join(root, seed.target));
87
+ await linkSharedCaches(policy, role, root);
77
88
  const environment = Object.fromEntries(
78
89
  Object.entries(policy.environment).map(([name, value]) => [
79
90
  name,
@@ -87,7 +98,13 @@ export async function prepareRuntimeState(
87
98
  UV_OFFLINE: "1",
88
99
  PYTHONDONTWRITEBYTECODE: "1",
89
100
  };
90
- return { root, home, temp, environment: isolatedEnvironment };
101
+ return {
102
+ root,
103
+ home,
104
+ temp,
105
+ environment: isolatedEnvironment,
106
+ writableCacheRoots,
107
+ };
91
108
  }
92
109
 
93
110
  async function runtimeRoot(
@@ -1,83 +1,65 @@
1
- /** Structural contracts for data validated by the versioned JSON schemas. */
2
- export type CliResultData = {
1
+ import type { AdvisoryAssessment } from "./advisory";
2
+ import type {
3
+ ArtifactReference,
4
+ Assessment,
5
+ CheckOutcome,
6
+ Diagnostic,
7
+ DomainOutcome,
8
+ EvidenceRoute,
9
+ ExecutionStatus,
10
+ ExitCode,
11
+ GradingStatus,
12
+ MetricEvidence,
13
+ ObservationCompleteness,
14
+ ObservationEvidence,
15
+ OutcomeStatus,
16
+ RawResultReference,
17
+ TaskVerdict,
18
+ UsageEvidence,
19
+ } from "./contract-types";
20
+ import type { IdentityDimensions } from "./identity";
21
+ import type { InstrumentationRequest } from "./instrumentation";
22
+
23
+ /** Wire assessments additionally allow optional serialized error codes. */
24
+ export type WireAssessment = {
25
+ execution: Assessment["execution"] & { errorCode?: string };
26
+ grading: Assessment["grading"] & { errorCode?: string };
27
+ task: Assessment["task"];
28
+ };
29
+
30
+ /** Validated v1 wire data also represents failures before run allocation. */
31
+ export type CliResultData = WireAssessment & {
3
32
  format: "sevro.cli-result.v1";
4
33
  runId: string | null;
5
- execution: CliResultDataExecution;
6
- grading: CliResultDataGrading;
7
- task: CliResultDataTask;
8
- exitCode: 0 | 1 | 2 | 3 | 4 | 64 | 70 | 130 | 143;
34
+ exitCode: ExitCode;
9
35
  evidencePath: string | null;
10
36
  cases: Array<CliResultDataCase>;
11
- diagnostic?: CliResultDataDiagnostic;
12
- };
13
- type CliResultDataExecution = {
14
- status: "completed" | "failed" | "cancelled" | "not_run";
15
- errorCode?: string;
16
- };
17
- type CliResultDataGrading = {
18
- status: "completed" | "error" | "unavailable" | "not_requested";
19
- errorCode?: string;
20
- };
21
- type CliResultDataTask = { verdict: "passed" | "failed" | "not_assessed" };
22
- type CliResultDataCheck = {
23
- id: string;
24
- grader: string;
25
- status: "passed" | "failed" | "unavailable";
26
- detail?: string;
27
- evidenceRefs?: Array<string>;
37
+ diagnostic?: Diagnostic;
28
38
  };
29
- type CliResultDataDomainOutcome = {
30
- id: string;
31
- status: "passed" | "failed" | "unavailable";
32
- detail?: string;
33
- evidenceRefs: Array<string>;
34
- data?: Record<string, unknown>;
35
- };
36
- type CliResultDataTrial = {
39
+ export type CliResultDataTrial = WireAssessment & {
37
40
  trial: number;
38
- execution: CliResultDataExecution;
39
- grading: CliResultDataGrading;
40
- task: CliResultDataTask;
41
- checks: Array<CliResultDataCheck>;
42
- domainOutcomes?: Array<CliResultDataDomainOutcome>;
41
+ checks: Array<CheckOutcome>;
42
+ domainOutcomes?: Array<DomainOutcome>;
43
43
  artifactPath: string | null;
44
44
  };
45
- type CliResultDataCase = {
45
+ export type CliResultDataCase = WireAssessment & {
46
46
  caseId: string;
47
- execution: CliResultDataExecution;
48
- grading: CliResultDataGrading;
49
- task: CliResultDataTask;
50
47
  trials: Array<CliResultDataTrial>;
51
48
  };
52
- type CliResultDataDiagnostic = { code: string; message: string };
49
+
50
+ /** Structural contracts for data validated by the versioned JSON schemas. */
53
51
  export type RunEvidenceData = {
54
52
  format: "sevro.run-evidence.v1";
55
53
  runId: string;
56
54
  evaluationIdentity: {
57
55
  algorithm: "sevro.identity.v1";
58
56
  digest: RunEvidenceDataDigest;
59
- dimensions: {
60
- runnerBuildDigest: RunEvidenceDataDigest;
61
- projectDigest: RunEvidenceDataDigest;
62
- configurationDigest: RunEvidenceDataDigest;
63
- extensionDigest: RunEvidenceDataDigest | null;
64
- extensionProtocol: "sevro.extension.v1" | null;
65
- caseDigest: RunEvidenceDataDigest;
66
- fixtureDigest: RunEvidenceDataDigest;
67
- checksDigest: RunEvidenceDataDigest;
68
- requiredEvidenceDigest: RunEvidenceDataDigest;
69
- evaluatorDigest: RunEvidenceDataDigest;
70
- graderDigest: RunEvidenceDataDigest;
71
- instrumentationDigest: RunEvidenceDataDigest;
72
- routeDigest: RunEvidenceDataDigest;
73
- condition: "passive" | "enforced";
74
- trialCount: number;
75
- passThreshold: number;
76
- };
57
+ dimensions: IdentityDimensions;
77
58
  };
78
59
  configuration: {
79
60
  digest: RunEvidenceDataDigest;
80
61
  redacted: Record<string, unknown>;
62
+ resolved?: Record<string, unknown>;
81
63
  };
82
64
  runner: RunEvidenceDataRunner;
83
65
  project: {
@@ -89,17 +71,17 @@ export type RunEvidenceData = {
89
71
  condition: {
90
72
  requested: "passive" | "enforced";
91
73
  actual: "passive" | "enforced" | "unknown";
92
- requestedInstrumentation: RunEvidenceDataInstrumentationList;
93
- appliedInstrumentation: RunEvidenceDataInstrumentationList;
74
+ requestedInstrumentation: InstrumentationRequest[];
75
+ appliedInstrumentation: InstrumentationRequest[];
94
76
  };
95
77
  graders: {
96
78
  active: Array<RunEvidenceDataGrader>;
97
79
  replacedDefaults: Array<string>;
98
80
  };
99
- routes: Array<RunEvidenceDataRoute>;
81
+ routes: Array<EvidenceRoute>;
100
82
  result: CliResultData;
101
83
  trials: Array<RunEvidenceDataTrialEvidence>;
102
- diagnostic?: RunEvidenceDataDiagnostic;
84
+ diagnostic?: Diagnostic;
103
85
  };
104
86
  type RunEvidenceDataDigest = string;
105
87
  type RunEvidenceDataFileUrl = string;
@@ -126,49 +108,18 @@ type RunEvidenceDataExtension = {
126
108
  capabilities: Array<string>;
127
109
  replacements: { graders: Array<string>; taskVerdictPolicy: string | null };
128
110
  };
129
- type RunEvidenceDataInstrumentationList = Array<{
130
- id: string;
131
- configuration: Record<string, unknown>;
132
- }>;
133
111
  type RunEvidenceDataGrader = {
134
112
  id: string;
135
113
  source: "builtin" | "extension";
136
114
  version: string;
137
115
  };
138
- type RunEvidenceDataRoute = {
139
- role: "candidate" | "semantic" | "advisory";
140
- host: string;
141
- model: string;
142
- effort: string;
143
- };
144
- type RunEvidenceDataAdvisoryReview = null | {
116
+ export type AdvisoryReviewData = {
145
117
  status: "completed" | "failed" | "not_run";
146
- assessment: null | {
147
- verdict: "pass" | "fail";
148
- overallScore: number;
149
- dimensions: {
150
- correctness: number;
151
- maintainability: number;
152
- testQuality: number;
153
- scopeDiscipline: number;
154
- };
155
- strengths: Array<string>;
156
- weaknesses: Array<string>;
157
- summary: string;
158
- };
159
- usage: {
160
- inputTokens: number | null;
161
- outputTokens: number | null;
162
- costUsd: number | null;
163
- complete: boolean;
164
- };
165
- rawResult: {
166
- source: string;
167
- path: RunEvidenceDataFileUrl | null;
168
- sha256: RunEvidenceDataDigest | null;
169
- };
118
+ assessment: AdvisoryAssessment | null;
119
+ usage: UsageEvidence;
120
+ rawResult: RawResultReference;
170
121
  };
171
- type RunEvidenceDataTrialEvidence = {
122
+ export type RunEvidenceDataTrialEvidence = {
172
123
  caseId: string;
173
124
  trial: number;
174
125
  executionMode: "executed" | "dry" | "unknown";
@@ -176,50 +127,22 @@ type RunEvidenceDataTrialEvidence = {
176
127
  condition: {
177
128
  requested: "passive" | "enforced";
178
129
  actual: "passive" | "enforced" | "unknown";
179
- appliedInstrumentation: RunEvidenceDataInstrumentationList;
130
+ appliedInstrumentation: InstrumentationRequest[];
180
131
  };
181
- observationCompleteness: "complete" | "partial" | "unavailable";
182
- observations: Array<{
183
- id: string;
184
- source: string;
185
- completeness: "complete" | "partial" | "unavailable";
186
- data: Record<string, unknown>;
187
- }>;
188
- metrics: Array<RunEvidenceDataMetric>;
189
- domainOutcomes?: Array<{
190
- id: string;
191
- status: "passed" | "failed" | "unavailable";
192
- detail?: string;
193
- evidenceRefs: Array<string>;
194
- data?: Record<string, unknown>;
195
- }>;
132
+ observationCompleteness: ObservationCompleteness;
133
+ observations: Array<ObservationEvidence>;
134
+ metrics: Array<MetricEvidence>;
135
+ domainOutcomes?: Array<DomainOutcome>;
196
136
  taskVerdictPolicy: null | {
197
137
  id: string;
198
- recommendation: "passed" | "failed" | "not_assessed" | null;
199
- };
200
- advisoryReview?: RunEvidenceDataAdvisoryReview;
201
- routes: Array<RunEvidenceDataRoute>;
202
- usage: {
203
- inputTokens: number | null;
204
- outputTokens: number | null;
205
- costUsd: number | null;
206
- complete: boolean;
138
+ recommendation: TaskVerdict | null;
207
139
  };
208
- rawResult: {
209
- source: string;
210
- path: RunEvidenceDataFileUrl | null;
211
- sha256: RunEvidenceDataDigest | null;
212
- };
213
- artifactRefs: Array<{
214
- id: string;
215
- path: RunEvidenceDataFileUrl;
216
- sha256: RunEvidenceDataDigest;
217
- gitExclude?: boolean;
218
- executable?: boolean;
219
- }>;
140
+ advisoryReview?: AdvisoryReviewData | null;
141
+ routes: Array<EvidenceRoute>;
142
+ usage: UsageEvidence;
143
+ rawResult: RawResultReference;
144
+ artifactRefs: Array<ArtifactReference>;
220
145
  };
221
- type RunEvidenceDataDiagnostic = { code: string; message: string };
222
- type RunEvidenceDataMetric = { id: string; value: number | null; unit: string };
223
146
  export type ReportData = {
224
147
  format: "sevro.report.v1";
225
148
  inputs: Array<ReportDataInput>;
@@ -233,38 +156,32 @@ export type ReportData = {
233
156
  };
234
157
  type ReportDataPath = string;
235
158
  type ReportDataDigest = string;
236
- type ReportDataState =
237
- | "completed"
238
- | "failed"
239
- | "cancelled"
240
- | "not_run"
241
- | "error"
242
- | "unavailable"
243
- | "not_requested";
244
- type ReportDataVerdict = "passed" | "failed" | "not_assessed";
159
+ /** The report wire schema deliberately allows either state family in rows. */
160
+ type ReportDataState = ExecutionStatus | GradingStatus;
245
161
  type ReportDataInput = {
246
162
  resultFile: ReportDataPath;
247
- execution: "completed" | "failed" | "cancelled" | "not_run";
248
- grading: "completed" | "error" | "unavailable" | "not_requested";
249
- task: ReportDataVerdict;
250
- exitCode: 0 | 1 | 2 | 3 | 4 | 64 | 70 | 130 | 143;
163
+ execution: ExecutionStatus;
164
+ grading: GradingStatus;
165
+ task: TaskVerdict;
166
+ exitCode: ExitCode;
251
167
  caseCount: number;
252
168
  };
253
- type ReportDataRoute = { host: string; model: string; effort: string };
169
+ type ReportDataRoute = Omit<EvidenceRoute, "role">;
254
170
  type ReportDataOutcome = {
255
171
  trial: number;
256
172
  id: string;
257
- status: "passed" | "failed" | "unavailable";
173
+ status: OutcomeStatus;
258
174
  };
259
175
  type ReportDataRow = {
176
+ nativeTranscripts?: { retained: number; pruned: number };
260
177
  resultFile: ReportDataPath;
261
178
  evidencePath: string | null;
262
179
  runId: string | null;
263
180
  caseId: string;
264
181
  execution: ReportDataState;
265
182
  grading: ReportDataState;
266
- task: ReportDataVerdict;
267
- exitCode: 0 | 1 | 2 | 3 | 4 | 64 | 70 | 130 | 143;
183
+ task: TaskVerdict;
184
+ exitCode: ExitCode;
268
185
  trialCount: number;
269
186
  taskPassRate: number | null;
270
187
  candidateDurationMs: number | null;
@@ -276,11 +193,6 @@ type ReportDataRow = {
276
193
  requestedCondition: "passive" | "enforced" | null;
277
194
  actualCondition: "passive" | "enforced" | "unknown" | null;
278
195
  candidateRoute: ReportDataRoute | null;
279
- routes: Array<{
280
- role: "candidate" | "semantic" | "advisory";
281
- host: string;
282
- model: string;
283
- effort: string;
284
- }>;
196
+ routes: Array<EvidenceRoute>;
285
197
  domainOutcomes: Array<ReportDataOutcome>;
286
198
  };
package/src/shell.ts ADDED
@@ -0,0 +1,4 @@
1
+ /** Encode one literal argument for a POSIX shell, including empty strings. */
2
+ export function shellQuote(value: string): string {
3
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
4
+ }
@@ -0,0 +1,146 @@
1
+ import { readFile, rm } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import type { RunEvidenceData } from "./schema-types";
5
+ import { canonicalJson, sha256 } from "./identity";
6
+ import type { ArtifactReference } from "./contract-types";
7
+ import { retainedFile } from "./results-lock";
8
+ import { atomicWriteJson } from "./storage";
9
+
10
+ function isNativeTranscript(artifact: ArtifactReference) {
11
+ return /^(?:sevro\.(?:semantic(?:\.[a-f0-9]{64})?|advisory)\.)?sevro\.native-transcripts\.bundle$/.test(
12
+ artifact.id,
13
+ );
14
+ }
15
+
16
+ function transcriptArtifacts(evidence: RunEvidenceData) {
17
+ return evidence.trials.flatMap((trial) =>
18
+ trial.artifactRefs.filter(isNativeTranscript).map((artifact) => ({
19
+ caseId: trial.caseId,
20
+ trial: trial.trial,
21
+ ...artifact,
22
+ })),
23
+ );
24
+ }
25
+
26
+ function pruningRecord(evidence: RunEvidenceData) {
27
+ return {
28
+ format: "sevro.pruned-native-transcripts.v1",
29
+ runId: evidence.runId,
30
+ artifacts: transcriptArtifacts(evidence),
31
+ };
32
+ }
33
+
34
+ function pruningPath(evidence: RunEvidenceData) {
35
+ if (evidence.result.evidencePath === null)
36
+ throw new Error(
37
+ "native transcript retention requires a retained evidence path",
38
+ );
39
+ return join(
40
+ dirname(evidence.result.evidencePath),
41
+ "native-transcripts-pruned.json",
42
+ );
43
+ }
44
+
45
+ function transcriptPath(
46
+ evidence: RunEvidenceData,
47
+ artifact: ArtifactReference,
48
+ ) {
49
+ const path = fileURLToPath(artifact.path);
50
+ if (dirname(path) !== dirname(pruningPath(evidence)))
51
+ throw new Error(
52
+ "native transcript artifact is outside its retained result directory",
53
+ );
54
+ if (metadataPaths(evidence).includes(path))
55
+ throw new Error(
56
+ "native transcript artifact aliases retained result metadata",
57
+ );
58
+ if (otherArtifactPaths(evidence).includes(path))
59
+ throw new Error(
60
+ "native transcript artifact aliases other retained evidence",
61
+ );
62
+ return path;
63
+ }
64
+
65
+ function otherArtifactPaths(evidence: RunEvidenceData) {
66
+ return evidence.trials
67
+ .flatMap((trial) => [
68
+ trial.rawResult.path,
69
+ trial.advisoryReview?.rawResult.path,
70
+ ...trial.artifactRefs
71
+ .filter((ref) => !isNativeTranscript(ref))
72
+ .map((ref) => ref.path),
73
+ ])
74
+ .filter((path): path is string => path != null)
75
+ .map((path) => fileURLToPath(path));
76
+ }
77
+
78
+ function metadataPaths(evidence: RunEvidenceData) {
79
+ const receipt = pruningPath(evidence);
80
+ return [
81
+ receipt,
82
+ evidence.result.evidencePath,
83
+ join(dirname(receipt), "result.json"),
84
+ join(dirname(receipt), "latest.json"),
85
+ ...evidence.result.cases.flatMap((selected) =>
86
+ selected.trials.map((trial) => trial.artifactPath),
87
+ ),
88
+ ];
89
+ }
90
+
91
+ async function transcriptPresent(
92
+ evidence: RunEvidenceData,
93
+ artifact: ArtifactReference,
94
+ ) {
95
+ const path = transcriptPath(evidence, artifact);
96
+ if (!(await retainedFile(path))) return false;
97
+ if (sha256(await readFile(path)) !== artifact.sha256)
98
+ throw new Error("retained native transcript evidence hash differs");
99
+ return true;
100
+ }
101
+
102
+ /** Keep capture evidence immutable; a separate receipt records intentional removal. */
103
+ export async function pruneNativeTranscripts(evidence: RunEvidenceData) {
104
+ const artifacts = transcriptArtifacts(evidence);
105
+ if (!artifacts.length) return 0;
106
+ await atomicWriteJson(pruningPath(evidence), pruningRecord(evidence));
107
+ let removed = 0;
108
+ for (const artifact of artifacts) {
109
+ const path = transcriptPath(evidence, artifact);
110
+ if (!(await retainedFile(path))) continue;
111
+ await rm(path);
112
+ removed++;
113
+ }
114
+ return removed;
115
+ }
116
+
117
+ /** Missing native bytes require an exact receipt bound to their original references. */
118
+ async function hasPruningRecord(evidence: RunEvidenceData) {
119
+ const path = pruningPath(evidence);
120
+ const marked = await retainedFile(path);
121
+ if (marked) {
122
+ const receipt: unknown = JSON.parse(await readFile(path, "utf8"));
123
+ if (canonicalJson(receipt) !== canonicalJson(pruningRecord(evidence)))
124
+ throw new Error(
125
+ "native transcript pruning record differs from retained evidence",
126
+ );
127
+ }
128
+ return marked !== null;
129
+ }
130
+
131
+ export async function nativeTranscriptRetention(
132
+ evidence: RunEvidenceData | null,
133
+ ) {
134
+ if (!evidence) return [];
135
+ const marked = await hasPruningRecord(evidence);
136
+ const states = [];
137
+ for (const artifact of transcriptArtifacts(evidence)) {
138
+ const present = await transcriptPresent(evidence, artifact);
139
+ if (!present && !marked)
140
+ throw new Error(
141
+ "retained native transcript evidence is missing without a pruning record",
142
+ );
143
+ states.push({ caseId: artifact.caseId, pruned: !present });
144
+ }
145
+ return states;
146
+ }