@agentskit/doc-bridge 1.7.44 → 1.9.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 (223) hide show
  1. package/CHANGELOG.md +471 -0
  2. package/CONTRIBUTING.md +29 -4
  3. package/README.md +87 -40
  4. package/SECURITY.md +7 -0
  5. package/action.yml +1 -1
  6. package/bin/ak-docs.js +2 -2
  7. package/bin/ak-verify.js +13 -7
  8. package/dist/cli/program.d.ts +3 -1
  9. package/dist/cli/program.js +15888 -6061
  10. package/dist/cli/program.js.map +1 -1
  11. package/dist/config/index.d.ts +1 -1
  12. package/dist/config/index.js +91 -9
  13. package/dist/config/index.js.map +1 -1
  14. package/dist/index-Beor6Yhi.d.ts +792 -0
  15. package/dist/index.d.ts +9979 -3257
  16. package/dist/index.js +15954 -5774
  17. package/dist/index.js.map +1 -1
  18. package/docs/MARKETPLACE.md +1 -1
  19. package/docs/PRD-documentation-efficiency-study.md +406 -0
  20. package/docs/PRD-knowledge-retrieval-and-enrichment.md +466 -0
  21. package/docs/RELEASE.md +22 -8
  22. package/docs/adr/0002-documentation-audit-boundary.md +22 -0
  23. package/docs/adr/0003-study-protocol-and-historical-evidence.md +40 -0
  24. package/docs/adr/0004-controlled-study-runner.md +25 -0
  25. package/docs/adr/0005-documentation-quality-and-criticality.md +20 -0
  26. package/docs/adr/0006-registry-semantic-grounding.md +20 -0
  27. package/docs/adr/0007-longitudinal-study-metrics.md +21 -0
  28. package/docs/adr/0008-study-verification-boundary.md +21 -0
  29. package/docs/adr/0009-study-provider-cli-adapter.md +25 -0
  30. package/docs/agent-corpus/INDEX.md +14 -3
  31. package/docs/agent-corpus/OVERVIEW.md +25 -0
  32. package/docs/agent-corpus/chat.md +7 -3
  33. package/docs/agent-corpus/cli.md +18 -2
  34. package/docs/agent-corpus/conformance.md +14 -2
  35. package/docs/agent-corpus/doc-bridge.md +48 -1
  36. package/docs/agent-corpus/doctor.md +10 -2
  37. package/docs/agent-corpus/gates.md +6 -2
  38. package/docs/agent-corpus/mcp.md +15 -2
  39. package/docs/agent-corpus/memory.md +6 -2
  40. package/docs/agent-corpus/query.md +35 -2
  41. package/docs/bench/README.md +122 -0
  42. package/docs/bench/retrieval-baseline-v1.json +28 -0
  43. package/docs/bench/retrieval-suite-v1.json +1033 -0
  44. package/docs/chat-and-rag.md +3 -2
  45. package/docs/for-agents.md +9 -1
  46. package/docs/getting-started.md +4 -11
  47. package/docs/guides/gate-ci.md +11 -1
  48. package/docs/guides/install-and-run.md +9 -65
  49. package/docs/index.md +22 -1
  50. package/docs/knowledge-engine-runbook.md +51 -4
  51. package/docs/landing/assets/context-payload-reduction.svg +21 -0
  52. package/docs/landing/assets/controlled-ab-comparison.svg +30 -0
  53. package/docs/landing/index.html +119 -5
  54. package/docs/loop-workflow.md +117 -0
  55. package/docs/mcp.md +6 -1
  56. package/docs/parity/public-claims-v1.json +145 -0
  57. package/docs/playbook/doc-bridge-pattern.md +1 -1
  58. package/docs/query.md +90 -2
  59. package/docs/recipes/index-pipeline.md +1 -1
  60. package/docs/schemas/agent-handoff-v1.md +15 -0
  61. package/docs/schemas/doc-bridge-index-v1.md +65 -0
  62. package/docs/spec/benchmark-v1.md +39 -1
  63. package/docs/spec/cli.md +30 -10
  64. package/docs/spec/config-v1.md +192 -8
  65. package/docs/spec/documentation-audit-v1.md +61 -0
  66. package/docs/spec/enrichment-overlay-v1.md +241 -0
  67. package/docs/spec/graph-signals-v1.md +92 -0
  68. package/docs/spec/incremental-scan-v1.md +102 -0
  69. package/docs/spec/markdown-analyzer-v1.md +73 -0
  70. package/docs/spec/mcp-knowledge-tools-v1.md +147 -0
  71. package/docs/spec/measured-enrichment-v1.md +229 -0
  72. package/docs/spec/public-parity-v1.md +119 -0
  73. package/docs/spec/registry-agents.md +6 -0
  74. package/docs/spec/render-v1.md +122 -0
  75. package/docs/spec/retrieval-index-v1.md +164 -0
  76. package/docs/spec/study-metrics-v1.md +58 -0
  77. package/docs/spec/study-protocol-v1.md +46 -0
  78. package/docs/spec/study-provider-cli-v1.md +116 -0
  79. package/docs/spec/study-runner-v1.md +35 -0
  80. package/docs/spec/study-task-suite-v1.md +41 -0
  81. package/docs/spec/study-verification-v1.md +40 -0
  82. package/docs/study/README.md +84 -0
  83. package/docs/study/ab-adjudicated-cost-analysis-v1.md +29 -0
  84. package/docs/study/ab-adjudicated-cost-plan-v1.json +33 -0
  85. package/docs/study/ab-adjudicated-cost-plan-v2-v1.json +33 -0
  86. package/docs/study/ab-adjudicated-cost-result-v1.json +80 -0
  87. package/docs/study/ab-baseline-analysis-v1.md +21 -0
  88. package/docs/study/ab-baseline-plan-v1.json +33 -0
  89. package/docs/study/ab-baseline-recovery-plan-v1.json +33 -0
  90. package/docs/study/ab-baseline-result-v1.json +79 -0
  91. package/docs/study/documentation-audit-round-2026-08-31.json +183 -0
  92. package/docs/study/historical-evidence-v1.json +252 -0
  93. package/docs/study/observation-ledger-v1.json +30632 -0
  94. package/docs/study/phase3-task-coverage-v1.json +34 -0
  95. package/docs/study/phase4-public-pilot-ledger-v1.json +1344 -0
  96. package/docs/study/phase4-public-pilot-result-v1.json +52 -0
  97. package/docs/study/phase4-public-pilot-run-plan-v1.json +26 -0
  98. package/docs/study/phase4-public-pilot-task-suite-v1.json +71 -0
  99. package/docs/study/pilot-round-2026-08-31.json +46 -0
  100. package/docs/study/protocol-v1.json +90 -0
  101. package/docs/study/publication-gate-v1.md +45 -0
  102. package/docs/study/quality-scorecard-cycle-plan.md +545 -0
  103. package/docs/study/quality-scorecard-v1.json +38 -0
  104. package/docs/study/round-1-adjudicated-smoke-v1.json +30642 -0
  105. package/docs/study/round-1-instrumentation-plan-v1.md +39 -0
  106. package/docs/study/round-2-expanded-adjudication-v1.json +91 -0
  107. package/docs/study/round-2-expanded-validation-v1.md +58 -0
  108. package/docs/study/round-3-evidence-contract-v1.json +75 -0
  109. package/docs/study/round-3-evidence-contract-v1.md +57 -0
  110. package/docs/study/round-4-confirmation-v1.json +75 -0
  111. package/docs/study/round-4-confirmation-v1.md +55 -0
  112. package/docs/study/run-plan-v1.json +33 -0
  113. package/docs/study/semantic-adjudication-cycle-8.md +20 -0
  114. package/docs/study/task-suite-v1.json +96 -0
  115. package/docs/study/token-efficiency-plan-v1.md +337 -0
  116. package/docs/study/token-efficiency-protocol-v2.json +62 -0
  117. package/docs/study/verification-binding-v1.json +27 -0
  118. package/docs/validation-cycle-plan.md +33 -0
  119. package/docs/verification-harness.md +15 -6
  120. package/ecosystem-claims.json +2 -2
  121. package/ecosystem-upstream.json +2 -2
  122. package/ecosystem.json +4 -4
  123. package/mcpb/manifest.json +9 -1
  124. package/package.json +89 -72
  125. package/scripts/check-ecosystem-upstream.mjs +36 -7
  126. package/scripts/report-visual-check.mjs +20 -3
  127. package/skills/doc-bridge-handoff/fixtures/synthetic-repo/docs/for-agents/packages/payments.md +7 -0
  128. package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
  129. package/src/agents/registry-adapter.ts +192 -24
  130. package/src/audit/documentation.ts +513 -0
  131. package/src/bench/baseline.ts +198 -0
  132. package/src/bench/overlay-delta.ts +139 -0
  133. package/src/bench/retrieval.ts +319 -0
  134. package/src/budget/compile.ts +91 -0
  135. package/src/budget/sections.ts +70 -0
  136. package/src/cli/demo.ts +2 -2
  137. package/src/cli/program.ts +699 -79
  138. package/src/cli/usage.ts +71 -0
  139. package/src/config/defaults.ts +1 -0
  140. package/src/config/index.ts +4 -0
  141. package/src/config/load-config.ts +7 -1
  142. package/src/config/schema.ts +121 -4
  143. package/src/conformance/documentation-standard-v1.ts +22 -14
  144. package/src/discovery/areas.ts +182 -0
  145. package/src/discovery/documentation.ts +255 -23
  146. package/src/discovery/identity.ts +24 -0
  147. package/src/discovery/incremental.ts +314 -0
  148. package/src/discovery/inputs.ts +110 -0
  149. package/src/discovery/markdown.ts +481 -0
  150. package/src/discovery/repository.ts +557 -125
  151. package/src/doctor/run-doctor.ts +246 -27
  152. package/src/enrich/approvals.ts +190 -0
  153. package/src/enrich/cache.ts +93 -0
  154. package/src/enrich/context-pack.ts +272 -0
  155. package/src/enrich/overlay.ts +255 -0
  156. package/src/enrich/review.ts +106 -0
  157. package/src/enrich/stage.ts +374 -0
  158. package/src/enrich/stats.ts +100 -0
  159. package/src/enrich/validate.ts +410 -0
  160. package/src/federation/llms.ts +74 -24
  161. package/src/findings/report.ts +103 -0
  162. package/src/fixes/proposals.ts +4 -3
  163. package/src/graph/build.ts +356 -0
  164. package/src/graph/memory.ts +208 -0
  165. package/src/index-builder/build-handoffs.ts +22 -11
  166. package/src/index-builder/build-index.ts +132 -3
  167. package/src/index-builder/human-adapters/fumadocs.ts +1 -1
  168. package/src/index-builder/llms-txt.ts +48 -8
  169. package/src/index-builder/project-corpus.ts +111 -0
  170. package/src/index-builder/watch-index.ts +1 -1
  171. package/src/index.ts +630 -2
  172. package/src/lib/bounded-text.ts +15 -10
  173. package/src/lib/fuzzy-match.ts +235 -0
  174. package/src/mcp/knowledge.ts +554 -0
  175. package/src/mcp/server.ts +113 -18
  176. package/src/metrics/benchmark.ts +21 -0
  177. package/src/parity/check.ts +309 -0
  178. package/src/parity/claims.ts +259 -0
  179. package/src/parity/resolve.ts +160 -0
  180. package/src/query/handoff.ts +326 -0
  181. package/src/query/load-index.ts +53 -1
  182. package/src/query/query.ts +92 -59
  183. package/src/query/search.ts +289 -92
  184. package/src/query/text.ts +155 -0
  185. package/src/reconciliation/reconcile.ts +148 -15
  186. package/src/render/data.ts +356 -0
  187. package/src/render/engine.ts +398 -0
  188. package/src/render/generated.ts +77 -0
  189. package/src/render/render.ts +209 -0
  190. package/src/render/template-source.ts +52 -0
  191. package/src/render/templates.ts +289 -0
  192. package/src/report/html.ts +23 -17
  193. package/src/retrieval/bm25.ts +161 -0
  194. package/src/retrieval/project.ts +495 -0
  195. package/src/retrieval/rank.ts +383 -0
  196. package/src/retrieval/weights.ts +39 -0
  197. package/src/retriever/doc-bridge-retriever.ts +100 -15
  198. package/src/rules/engine.ts +45 -12
  199. package/src/safety/repository.ts +1 -1
  200. package/src/schemas/agent-handoff.ts +56 -0
  201. package/src/schemas/budget.ts +37 -0
  202. package/src/schemas/doc-bridge-index.ts +53 -2
  203. package/src/schemas/enrichment.ts +369 -0
  204. package/src/schemas/json-schemas.ts +39 -2
  205. package/src/schemas/knowledge.ts +19 -3
  206. package/src/schemas/retrieval-index.ts +152 -0
  207. package/src/shims/graphology.d.ts +91 -0
  208. package/src/study/adjudication.ts +196 -0
  209. package/src/study/execution.ts +350 -0
  210. package/src/study/expectations.ts +219 -0
  211. package/src/study/metrics.ts +467 -0
  212. package/src/study/protocol.ts +271 -0
  213. package/src/study/provider-cli.ts +115 -0
  214. package/src/study/provider-telemetry.ts +47 -0
  215. package/src/study/quality-scorecard.ts +164 -0
  216. package/src/study/runner.ts +461 -0
  217. package/src/study/task-suite.ts +321 -0
  218. package/src/study/verification.ts +134 -0
  219. package/src/validate.ts +8 -5
  220. package/src/version.ts +1 -1
  221. package/src/workflow/engine.ts +36 -11
  222. package/dist/index-C2PCQSrB.d.ts +0 -2251
  223. package/scripts/verification-harness.mjs +0 -483
@@ -0,0 +1,461 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { createHash } from 'node:crypto'
3
+ import { mkdirSync, writeFileSync } from 'node:fs'
4
+ import { dirname, resolve } from 'node:path'
5
+ import { z } from 'zod'
6
+
7
+ import { contentHashForArtifactV1, sha256NormalizedV1 } from '../index-builder/content-hash.js'
8
+ import { isSafeStudyText } from './protocol.js'
9
+ import { calculateStudyCostUsd, type StudyProviderCostPricing } from './provider-cli.js'
10
+
11
+ export const STUDY_RUNNER_SCHEMA_VERSION = 1 as const
12
+ export const STUDY_RUNNER_CONTENT_HASH_ALGO = 'sha256-normalized-v1' as const
13
+
14
+ const hash = z.string().regex(/^[a-f0-9]{64}$/)
15
+ const identifier = z.string().regex(/^[a-z][a-z0-9-]{0,127}$/)
16
+ const evidenceId = z.string().min(1).max(256).refine((value) => !/[\u0000\r\n]/.test(value), 'Evidence IDs cannot contain control characters.')
17
+ const reference = z.string().regex(/^[a-z0-9][a-z0-9._:-]{0,255}$/)
18
+ const outcome = z.enum(['success', 'partial', 'incorrect', 'incomplete', 'blocked'])
19
+ const modelReference = z.string().regex(/^[a-z0-9][a-z0-9._:/-]{0,255}$/)
20
+ const tokenMethod = z.enum(['provider', 'estimate'])
21
+ const safeText = z.string().min(1).max(2_048).refine(
22
+ isSafeStudyText,
23
+ 'Public study text cannot contain paths, URLs, credentials, or secret material',
24
+ )
25
+
26
+ const ModelConfigSchema = z.object({
27
+ id: identifier,
28
+ role: z.enum(['low-cost', 'reference']),
29
+ provider: identifier,
30
+ model: modelReference,
31
+ version: reference,
32
+ parametersHash: hash,
33
+ contextLimit: z.number().int().positive(),
34
+ toolConfigurationHash: hash,
35
+ promptContractHash: hash,
36
+ }).strict()
37
+
38
+ const ScenarioConfigSchema = z.object({
39
+ id: z.enum(['repository-only', 'deterministic-doc-bridge', 'registry-assisted']),
40
+ agentId: identifier.optional(),
41
+ agentVersion: reference.optional(),
42
+ /**
43
+ * The enrichment prompt the assisted arm ran under, and the budget that arm alone may spend.
44
+ *
45
+ * Both are optional in the schema so a plan written before them stays valid, and both are
46
+ * required for the arm to execute: an assisted run whose prompt version nobody recorded cannot
47
+ * be reproduced, and one with no budget of its own cannot be costed apart from the model under
48
+ * test. A plan missing either records the arm as unavailable rather than running it blind.
49
+ */
50
+ promptVersion: reference.optional(),
51
+ agentBudget: z.object({ maxTokens: z.number().int().positive(), maxRuntimeMs: z.number().int().positive() }).strict().optional(),
52
+ network: z.literal(false),
53
+ }).strict().superRefine((value, context) => {
54
+ if (value.id === 'registry-assisted' && (!value.agentId || !value.agentVersion)) context.addIssue({ code: z.ZodIssueCode.custom, message: 'Registry-assisted scenarios require agent identity and version.' })
55
+ if (value.id !== 'registry-assisted' && (value.agentId || value.agentVersion || value.promptVersion || value.agentBudget)) context.addIssue({ code: z.ZodIssueCode.custom, message: 'Only registry-assisted scenarios may declare an agent.' })
56
+ })
57
+
58
+ const TaskExecutionSchema = z.object({
59
+ taskId: identifier,
60
+ repositoryId: identifier,
61
+ category: z.enum(['discovery', 'architecture', 'documentation', 'implementation']),
62
+ scenarioId: ScenarioConfigSchema.shape.id,
63
+ modelId: identifier,
64
+ replicate: z.number().int().nonnegative(),
65
+ variantId: identifier,
66
+ difficulty: z.enum(['easy', 'medium', 'hard']).optional(),
67
+ }).strict()
68
+
69
+ const RunBudgetSchema = z.object({
70
+ maxTokens: z.number().int().positive(),
71
+ maxRuntimeMs: z.number().int().positive(),
72
+ maxOutputBytes: z.number().int().positive(),
73
+ maxAttempts: z.number().int().positive().max(3),
74
+ }).strict()
75
+
76
+ const SamplingSchema = z.discriminatedUnion('strategy', [
77
+ z.object({
78
+ strategy: z.literal('balanced-task-strata'),
79
+ sampleSize: z.number().int().positive(),
80
+ }).strict(),
81
+ z.object({
82
+ strategy: z.literal('pairwise-task-strata'),
83
+ sampleSize: z.number().int().positive(),
84
+ scenarioIds: z.array(ScenarioConfigSchema.shape.id).length(2),
85
+ }).strict(),
86
+ ])
87
+
88
+ const RunPlanPayloadSchema = z.object({
89
+ type: z.literal('controlled-study-run-plan'),
90
+ schemaVersion: z.literal(STUDY_RUNNER_SCHEMA_VERSION),
91
+ planVersion: reference,
92
+ protocolVersion: reference,
93
+ protocolHash: hash,
94
+ taskSuiteHash: hash,
95
+ sourceRevisionHash: hash,
96
+ configurationHash: hash,
97
+ docBridgeVersion: reference,
98
+ models: z.array(ModelConfigSchema).length(2),
99
+ scenarios: z.array(ScenarioConfigSchema).length(3),
100
+ taskIds: z.array(identifier).min(1).max(256),
101
+ sampling: SamplingSchema,
102
+ budget: RunBudgetSchema,
103
+ runId: reference,
104
+ }).strict()
105
+
106
+ export const ControlledStudyRunPlanV1Schema = RunPlanPayloadSchema.extend({
107
+ contentHash: hash,
108
+ contentHashAlgo: z.literal(STUDY_RUNNER_CONTENT_HASH_ALGO),
109
+ }).strict()
110
+
111
+ export type ControlledStudyRunPlanV1 = z.infer<typeof ControlledStudyRunPlanV1Schema>
112
+ export type TaskExecutionV1 = z.infer<typeof TaskExecutionSchema>
113
+
114
+ const ExecutionResultSchema = z.object({
115
+ status: z.enum(['completed', 'failed', 'timed-out', 'unavailable', 'invalid-output', 'budget-exceeded']),
116
+ exitCode: z.number().int().nullable(),
117
+ signal: z.string().nullable(),
118
+ durationMs: z.number().int().nonnegative(),
119
+ responseBytes: z.number().int().nonnegative(),
120
+ stderrBytes: z.number().int().nonnegative(),
121
+ stdoutHash: hash.optional(),
122
+ stderrHash: hash.optional(),
123
+ inputTokens: z.number().int().nonnegative().optional(),
124
+ outputTokens: z.number().int().nonnegative().optional(),
125
+ tokenMethod: tokenMethod.optional(),
126
+ toolCalls: z.number().int().nonnegative().optional(),
127
+ firstEvidenceLatencyMs: z.number().int().nonnegative().optional(),
128
+ errorCode: identifier.optional(),
129
+ }).strict().superRefine((value, context) => {
130
+ if ((value.inputTokens !== undefined || value.outputTokens !== undefined) && !value.tokenMethod) context.addIssue({ code: z.ZodIssueCode.custom, path: ['tokenMethod'], message: 'Token counts require a provider or estimate method.' })
131
+ })
132
+
133
+ const AgentMetricsSchema = z.object({
134
+ taskOutcome: outcome.optional(),
135
+ evidenceQuality: z.enum(['high', 'medium', 'low']).optional(),
136
+ safetyOutcome: z.enum(['safe', 'unsafe', 'not-applicable']).optional(),
137
+ evidenceIds: z.array(evidenceId).max(128).optional(),
138
+ clarificationRequests: z.number().int().nonnegative().optional(),
139
+ reworkCount: z.number().int().nonnegative().optional(),
140
+ measurements: z.record(z.string().min(1).max(128), z.number().finite().nonnegative()).optional(),
141
+ inputTokens: z.number().int().nonnegative().optional(),
142
+ outputTokens: z.number().int().nonnegative().optional(),
143
+ tokenMethod: tokenMethod.optional(),
144
+ toolCalls: z.number().int().nonnegative().optional(),
145
+ firstEvidenceLatencyMs: z.number().int().nonnegative().optional(),
146
+ })
147
+
148
+ const ObservationPayloadSchema = z.object({
149
+ type: z.literal('controlled-study-observation'),
150
+ schemaVersion: z.literal(STUDY_RUNNER_SCHEMA_VERSION),
151
+ observationVersion: reference,
152
+ observedAt: z.string().datetime(),
153
+ runId: reference,
154
+ planHash: hash,
155
+ task: TaskExecutionSchema,
156
+ model: ModelConfigSchema,
157
+ scenario: ScenarioConfigSchema,
158
+ execution: ExecutionResultSchema,
159
+ contextBytes: z.number().int().nonnegative(),
160
+ contextTokens: z.number().int().nonnegative().optional(),
161
+ contextTokenMethod: tokenMethod.optional(),
162
+ evidenceIds: z.array(evidenceId).max(128),
163
+ round: reference.optional(),
164
+ taskOutcome: z.enum(['success', 'partial', 'incorrect', 'incomplete', 'blocked']).optional(),
165
+ evidenceQuality: z.enum(['high', 'medium', 'low']).optional(),
166
+ safetyOutcome: z.enum(['safe', 'unsafe', 'not-applicable']).optional(),
167
+ firstEvidenceLatencyMs: z.number().int().nonnegative().optional(),
168
+ clarificationRequests: z.number().int().nonnegative().optional(),
169
+ reworkCount: z.number().int().nonnegative().optional(),
170
+ measurements: z.record(z.string().min(1).max(128), z.number().finite().nonnegative()).optional(),
171
+ adjudication: z.object({
172
+ status: z.enum(['pending', 'automated', 'human-approved', 'human-rejected']),
173
+ actor: identifier.optional(),
174
+ method: reference.optional(),
175
+ outcome: outcome.optional(),
176
+ confidence: z.number().min(0).max(1).optional(),
177
+ reasonCodes: z.array(identifier).max(16).optional(),
178
+ tokenMethod: tokenMethod.optional(),
179
+ configurationHash: hash.optional(),
180
+ reason: safeText.optional(),
181
+ }).strict().superRefine((value, context) => {
182
+ if (value.status === 'automated' && (!value.actor || !value.method || !value.outcome)) context.addIssue({ code: z.ZodIssueCode.custom, message: 'Automated adjudication requires actor, method, and outcome.' })
183
+ }),
184
+ }).strict().superRefine((value, context) => {
185
+ if (value.contextTokens !== undefined && value.contextTokenMethod === undefined) context.addIssue({ code: z.ZodIssueCode.custom, path: ['contextTokenMethod'], message: 'Context token counts require a provider or estimate method.' })
186
+ })
187
+
188
+ export const ControlledStudyObservationV1Schema = ObservationPayloadSchema.extend({
189
+ contentHash: hash,
190
+ contentHashAlgo: z.literal(STUDY_RUNNER_CONTENT_HASH_ALGO),
191
+ }).strict()
192
+
193
+ export type ControlledStudyObservationV1 = z.infer<typeof ControlledStudyObservationV1Schema>
194
+
195
+ const LedgerPayloadSchema = z.object({
196
+ type: z.literal('controlled-study-observation-ledger'),
197
+ schemaVersion: z.literal(STUDY_RUNNER_SCHEMA_VERSION),
198
+ ledgerVersion: reference,
199
+ observations: z.array(ControlledStudyObservationV1Schema).max(100_000),
200
+ }).strict()
201
+
202
+ export const ControlledStudyObservationLedgerV1Schema = LedgerPayloadSchema.extend({
203
+ contentHash: hash,
204
+ contentHashAlgo: z.literal(STUDY_RUNNER_CONTENT_HASH_ALGO),
205
+ }).strict()
206
+
207
+ export type ControlledStudyObservationLedgerV1 = z.infer<typeof ControlledStudyObservationLedgerV1Schema>
208
+
209
+ const uniqueIds = (values: readonly string[], label: string): void => {
210
+ if (new Set(values).size !== values.length) throw new Error(`Duplicate ${label} id.`)
211
+ }
212
+
213
+ const validatePlan = (plan: ControlledStudyRunPlanV1): void => {
214
+ uniqueIds(plan.models.map((model) => model.id), 'model')
215
+ uniqueIds(plan.scenarios.map((scenario) => scenario.id), 'scenario')
216
+ uniqueIds(plan.taskIds, 'task')
217
+ if (new Set(plan.models.map((model) => model.role)).size !== 2) throw new Error('The run plan requires one low-cost and one reference model.')
218
+ if (new Set(plan.scenarios.map((scenario) => scenario.id)).size !== 3) throw new Error('The run plan requires all three controlled scenarios.')
219
+ if (plan.budget.maxTokens <= 0 || plan.budget.maxRuntimeMs <= 0) throw new Error('Run budgets must be positive.')
220
+ const selectedScenarioIds = plan.sampling.strategy === 'pairwise-task-strata' ? plan.sampling.scenarioIds : plan.scenarios.map((scenario) => scenario.id)
221
+ if (selectedScenarioIds.some((scenarioId) => !plan.scenarios.some((scenario) => scenario.id === scenarioId))) throw new Error('Sampling references an unknown scenario.')
222
+ const stratumCount = plan.models.length * selectedScenarioIds.length
223
+ const maxSampleSize = plan.taskIds.length * stratumCount
224
+ if (plan.sampling.sampleSize > maxSampleSize || plan.sampling.sampleSize % stratumCount !== 0) throw new Error('Sampling must be a positive balanced subset of the task strata.')
225
+ }
226
+
227
+ export const createControlledStudyRunPlan = (input: unknown): ControlledStudyRunPlanV1 => {
228
+ const payload = RunPlanPayloadSchema.parse(input)
229
+ const hashable = { ...payload, contentHashAlgo: STUDY_RUNNER_CONTENT_HASH_ALGO }
230
+ const plan = ControlledStudyRunPlanV1Schema.parse({ ...hashable, contentHash: sha256NormalizedV1(hashable) })
231
+ validatePlan(plan)
232
+ return plan
233
+ }
234
+
235
+ export const parseControlledStudyRunPlan = (input: unknown): ControlledStudyRunPlanV1 => {
236
+ const plan = ControlledStudyRunPlanV1Schema.parse(input)
237
+ if (contentHashForArtifactV1(plan) !== plan.contentHash) throw new Error('Invalid controlled run-plan content hash.')
238
+ validatePlan(plan)
239
+ return plan
240
+ }
241
+
242
+ export const createControlledStudyObservation = (input: unknown): ControlledStudyObservationV1 => {
243
+ const payload = ObservationPayloadSchema.parse(input)
244
+ if (payload.task.modelId !== payload.model.id) throw new Error(`Observation task model ${payload.task.modelId} does not match model ${payload.model.id}.`)
245
+ if (payload.task.scenarioId !== payload.scenario.id) throw new Error(`Observation task scenario ${payload.task.scenarioId} does not match scenario ${payload.scenario.id}.`)
246
+ const hashable = { ...payload, contentHashAlgo: STUDY_RUNNER_CONTENT_HASH_ALGO }
247
+ return ControlledStudyObservationV1Schema.parse({ ...hashable, contentHash: sha256NormalizedV1(hashable) })
248
+ }
249
+
250
+ export const parseControlledStudyObservation = (input: unknown): ControlledStudyObservationV1 => {
251
+ const observation = ControlledStudyObservationV1Schema.parse(input)
252
+ if (contentHashForArtifactV1(observation) !== observation.contentHash) throw new Error('Invalid controlled observation content hash.')
253
+ return observation
254
+ }
255
+
256
+ export const createControlledStudyLedger = (input: unknown): ControlledStudyObservationLedgerV1 => {
257
+ const payload = LedgerPayloadSchema.parse(input)
258
+ const hashable = { ...payload, contentHashAlgo: STUDY_RUNNER_CONTENT_HASH_ALGO }
259
+ const ledger = ControlledStudyObservationLedgerV1Schema.parse({ ...hashable, contentHash: sha256NormalizedV1(hashable) })
260
+ uniqueIds(ledger.observations.map((observation) => observation.contentHash), 'observation')
261
+ return ledger
262
+ }
263
+
264
+ export const parseControlledStudyLedger = (input: unknown): ControlledStudyObservationLedgerV1 => {
265
+ const ledger = ControlledStudyObservationLedgerV1Schema.parse(input)
266
+ if (contentHashForArtifactV1(ledger) !== ledger.contentHash) throw new Error('Invalid observation-ledger content hash.')
267
+ uniqueIds(ledger.observations.map((observation) => observation.contentHash), 'observation')
268
+ return ledger
269
+ }
270
+
271
+ export const upsertControlledStudyObservation = (
272
+ ledger: ControlledStudyObservationLedgerV1,
273
+ observation: ControlledStudyObservationV1,
274
+ ): ControlledStudyObservationLedgerV1 => {
275
+ const existing = ledger.observations.find((item) => item.runId === observation.runId && sha256NormalizedV1(item.task) === sha256NormalizedV1(observation.task))
276
+ if (existing && existing.contentHash !== observation.contentHash) throw new Error(`Observation ${observation.runId} already exists with different contents.`)
277
+ if (existing) return ledger
278
+ const { contentHash: _contentHash, contentHashAlgo: _contentHashAlgo, ...payload } = ledger
279
+ return createControlledStudyLedger({ ...payload, observations: [...ledger.observations, observation] })
280
+ }
281
+
282
+ export type ControlledCommandRequest = {
283
+ readonly plan: ControlledStudyRunPlanV1
284
+ readonly execution: TaskExecutionV1
285
+ readonly command: string
286
+ readonly args?: readonly string[]
287
+ readonly cwd: string
288
+ readonly input?: string
289
+ readonly envAllowlist?: readonly string[]
290
+ readonly maxRuntimeMs?: number
291
+ readonly maxOutputBytes?: number
292
+ readonly contextBytes: number
293
+ readonly contextTokens?: number
294
+ readonly contextTokenMethod?: 'provider' | 'estimate'
295
+ readonly round?: string
296
+ readonly taskOutcome?: 'success' | 'partial' | 'incorrect' | 'incomplete' | 'blocked'
297
+ readonly evidenceQuality?: 'high' | 'medium' | 'low'
298
+ readonly safetyOutcome?: 'safe' | 'unsafe' | 'not-applicable'
299
+ readonly firstEvidenceLatencyMs?: number
300
+ readonly clarificationRequests?: number
301
+ readonly reworkCount?: number
302
+ readonly measurements?: Readonly<Record<string, number>>
303
+ readonly providerPricing?: StudyProviderCostPricing
304
+ }
305
+
306
+ type ChildAttempt = {
307
+ readonly status: z.infer<typeof ExecutionResultSchema>['status']
308
+ readonly exitCode: number | null
309
+ readonly signal: string | null
310
+ readonly durationMs: number
311
+ readonly stdout: string
312
+ readonly stderr: string
313
+ readonly errorCode?: string
314
+ }
315
+
316
+ const terminateChildProcessGroup = (child: ReturnType<typeof spawn>): void => {
317
+ if (child.pid === undefined) return
318
+ try { process.kill(-child.pid, 'SIGTERM') } catch { child.kill('SIGTERM') }
319
+ }
320
+
321
+ const runAttempt = (request: ControlledCommandRequest, sessionId: string): Promise<ChildAttempt> => new Promise((resolveAttempt) => {
322
+ const env = request.envAllowlist === undefined
323
+ ? process.env
324
+ : Object.fromEntries([...new Set(['PATH', 'HOME', 'TMPDIR', ...request.envAllowlist])].flatMap((name) => process.env[name] === undefined ? [] : [[name, process.env[name] as string]]))
325
+ const started = Date.now()
326
+ const maxRuntimeMs = request.maxRuntimeMs ?? request.plan.budget.maxRuntimeMs
327
+ const maxOutputBytes = request.maxOutputBytes ?? request.plan.budget.maxOutputBytes
328
+ const child = spawn(request.command, [...(request.args ?? [])], {
329
+ cwd: resolve(request.cwd),
330
+ shell: false,
331
+ detached: true,
332
+ env: { ...env, DOC_BRIDGE_STUDY_SESSION_ID: sessionId },
333
+ stdio: ['pipe', 'pipe', 'pipe'],
334
+ })
335
+ let stdout = ''
336
+ let stderr = ''
337
+ let settled = false
338
+ const finish = (result: ChildAttempt): void => {
339
+ if (settled) return
340
+ settled = true
341
+ resolveAttempt(result)
342
+ }
343
+ const timer = setTimeout(() => {
344
+ terminateChildProcessGroup(child)
345
+ finish({ status: 'timed-out', exitCode: null, signal: 'SIGTERM', durationMs: Date.now() - started, stdout, stderr, errorCode: 'timeout' })
346
+ }, maxRuntimeMs)
347
+ child.stdout.on('data', (chunk: Buffer) => {
348
+ stdout += chunk.toString('utf8')
349
+ if (Buffer.byteLength(stdout, 'utf8') > maxOutputBytes) {
350
+ clearTimeout(timer)
351
+ terminateChildProcessGroup(child)
352
+ finish({ status: 'budget-exceeded', exitCode: null, signal: 'SIGTERM', durationMs: Date.now() - started, stdout: stdout.slice(0, maxOutputBytes), stderr, errorCode: 'output-limit' })
353
+ }
354
+ })
355
+ child.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString('utf8').slice(0, maxOutputBytes) })
356
+ child.once('error', () => { clearTimeout(timer); finish({ status: 'unavailable', exitCode: null, signal: null, durationMs: Date.now() - started, stdout, stderr, errorCode: 'unavailable' }) })
357
+ child.once('close', (exitCode, signal) => {
358
+ clearTimeout(timer)
359
+ if (settled) return
360
+ if (exitCode !== 0) finish({ status: 'failed', exitCode, signal, durationMs: Date.now() - started, stdout, stderr, errorCode: 'non-zero-exit' })
361
+ else {
362
+ try { JSON.parse(stdout) as unknown; finish({ status: 'completed', exitCode, signal, durationMs: Date.now() - started, stdout, stderr }) }
363
+ catch { finish({ status: 'invalid-output', exitCode, signal, durationMs: Date.now() - started, stdout, stderr, errorCode: 'invalid-json' }) }
364
+ }
365
+ })
366
+ child.stdin.end(request.input ?? '')
367
+ })
368
+
369
+ export const runControlledCommand = async (request: ControlledCommandRequest): Promise<ControlledStudyObservationV1> => {
370
+ const sessionId = sha256NormalizedV1({ runId: request.plan.runId, execution: request.execution }).slice(0, 32)
371
+ let attempt: ChildAttempt | undefined
372
+ for (let index = 0; index < request.plan.budget.maxAttempts; index += 1) {
373
+ attempt = await runAttempt(request, `${sessionId}-${index + 1}`)
374
+ if (attempt.status === 'completed') break
375
+ }
376
+ const result = attempt as ChildAttempt
377
+ const parsedOutput = (() => { try { return JSON.parse(result.stdout) as unknown } catch { return undefined } })()
378
+ const parsedMetrics = AgentMetricsSchema.safeParse(parsedOutput)
379
+ const effectiveResult = result.status === 'completed' && !parsedMetrics.success
380
+ ? { ...result, status: 'invalid-output' as const, errorCode: 'invalid-metrics' }
381
+ : result
382
+ const output = parsedMetrics.success ? parsedMetrics.data : {}
383
+ if (request.contextTokens !== undefined && request.contextTokenMethod === undefined) throw new Error('Context token counts require a provider or estimate method.')
384
+ const contextTokens = request.contextTokens ?? Math.ceil(request.contextBytes / 4)
385
+ const contextTokenMethod = request.contextTokenMethod ?? 'estimate'
386
+ const providerTokenTotal = output.tokenMethod === 'provider' && output.inputTokens !== undefined && output.outputTokens !== undefined
387
+ ? output.inputTokens + output.outputTokens
388
+ : undefined
389
+ const providerCostUsd = calculateStudyCostUsd(request.providerPricing, {
390
+ ...(output.inputTokens === undefined ? {} : { inputTokens: output.inputTokens }),
391
+ ...(output.measurements?.cachedInputTokens === undefined ? {} : { cachedInputTokens: output.measurements.cachedInputTokens }),
392
+ ...(output.outputTokens === undefined ? {} : { outputTokens: output.outputTokens }),
393
+ })
394
+ const measurements = {
395
+ ...(output.measurements ?? request.measurements ?? {}),
396
+ ...(providerTokenTotal === undefined ? {} : { providerTokenCostUnits: providerTokenTotal }),
397
+ ...(providerCostUsd === undefined ? {} : { agentCostUsd: providerCostUsd }),
398
+ }
399
+ const executionResult = {
400
+ status: effectiveResult.status,
401
+ exitCode: effectiveResult.exitCode,
402
+ signal: effectiveResult.signal,
403
+ durationMs: effectiveResult.durationMs,
404
+ responseBytes: Buffer.byteLength(result.stdout, 'utf8'),
405
+ stderrBytes: Buffer.byteLength(result.stderr, 'utf8'),
406
+ stdoutHash: createHash('sha256').update(result.stdout).digest('hex'),
407
+ stderrHash: createHash('sha256').update(result.stderr).digest('hex'),
408
+ ...(output.inputTokens === undefined ? {} : { inputTokens: output.inputTokens }),
409
+ ...(output.outputTokens === undefined ? {} : { outputTokens: output.outputTokens }),
410
+ ...(output.tokenMethod === undefined ? {} : { tokenMethod: output.tokenMethod }),
411
+ ...(output.toolCalls === undefined ? {} : { toolCalls: output.toolCalls }),
412
+ ...(output.firstEvidenceLatencyMs === undefined ? {} : { firstEvidenceLatencyMs: output.firstEvidenceLatencyMs }),
413
+ ...(effectiveResult.errorCode === undefined ? {} : { errorCode: effectiveResult.errorCode }),
414
+ }
415
+ if ((executionResult.inputTokens ?? 0) + (executionResult.outputTokens ?? 0) > request.plan.budget.maxTokens) {
416
+ executionResult.status = 'budget-exceeded'
417
+ executionResult.errorCode = 'token-budget'
418
+ }
419
+ return createControlledStudyObservation({
420
+ type: 'controlled-study-observation',
421
+ schemaVersion: STUDY_RUNNER_SCHEMA_VERSION,
422
+ observationVersion: 'v1',
423
+ observedAt: new Date().toISOString(),
424
+ runId: request.plan.runId,
425
+ planHash: request.plan.contentHash,
426
+ task: request.execution,
427
+ model: request.plan.models.find((model) => model.id === request.execution.modelId),
428
+ scenario: request.plan.scenarios.find((scenario) => scenario.id === request.execution.scenarioId),
429
+ execution: executionResult,
430
+ contextBytes: request.contextBytes,
431
+ contextTokens,
432
+ contextTokenMethod,
433
+ evidenceIds: output.evidenceIds ?? [],
434
+ ...(request.round === undefined ? {} : { round: request.round }),
435
+ ...(output.taskOutcome === undefined ? request.taskOutcome === undefined ? {} : { taskOutcome: request.taskOutcome } : { taskOutcome: output.taskOutcome }),
436
+ ...(output.evidenceQuality === undefined ? request.evidenceQuality === undefined ? {} : { evidenceQuality: request.evidenceQuality } : { evidenceQuality: output.evidenceQuality }),
437
+ ...(output.safetyOutcome === undefined ? request.safetyOutcome === undefined ? {} : { safetyOutcome: request.safetyOutcome } : { safetyOutcome: output.safetyOutcome }),
438
+ ...(output.firstEvidenceLatencyMs === undefined ? request.firstEvidenceLatencyMs === undefined ? {} : { firstEvidenceLatencyMs: request.firstEvidenceLatencyMs } : { firstEvidenceLatencyMs: output.firstEvidenceLatencyMs }),
439
+ ...(output.clarificationRequests === undefined ? request.clarificationRequests === undefined ? {} : { clarificationRequests: request.clarificationRequests } : { clarificationRequests: output.clarificationRequests }),
440
+ ...(output.reworkCount === undefined ? request.reworkCount === undefined ? {} : { reworkCount: request.reworkCount } : { reworkCount: output.reworkCount }),
441
+ ...(Object.keys(measurements).length === 0 ? {} : { measurements }),
442
+ adjudication: { status: 'pending' },
443
+ })
444
+ }
445
+
446
+ export const persistControlledStudyLedger = (path: string, ledger: ControlledStudyObservationLedgerV1): string => {
447
+ parseControlledStudyLedger(ledger)
448
+ mkdirSync(dirname(resolve(path)), { recursive: true })
449
+ writeFileSync(resolve(path), `${JSON.stringify(ledger, null, 2)}\n`, 'utf8')
450
+ return resolve(path)
451
+ }
452
+
453
+ export const formatControlledStudyRunPlanText = (plan: ControlledStudyRunPlanV1): readonly string[] => [
454
+ `Run plan: ${plan.planVersion}`,
455
+ `Tasks: ${plan.taskIds.length}`,
456
+ `Models: ${plan.models.length}`,
457
+ `Scenarios: ${plan.scenarios.length}`,
458
+ `Sample: ${plan.sampling.sampleSize}/${plan.taskIds.length} (${plan.sampling.strategy})`,
459
+ `Budget: ${plan.budget.maxTokens} tokens, ${plan.budget.maxRuntimeMs} ms, ${plan.budget.maxOutputBytes} bytes, ${plan.budget.maxAttempts} attempts`,
460
+ `Content hash: ${plan.contentHash}`,
461
+ ]