@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.
- package/CHANGELOG.md +471 -0
- package/CONTRIBUTING.md +29 -4
- package/README.md +87 -40
- package/SECURITY.md +7 -0
- package/action.yml +1 -1
- package/bin/ak-docs.js +2 -2
- package/bin/ak-verify.js +13 -7
- package/dist/cli/program.d.ts +3 -1
- package/dist/cli/program.js +15888 -6061
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +91 -9
- package/dist/config/index.js.map +1 -1
- package/dist/index-Beor6Yhi.d.ts +792 -0
- package/dist/index.d.ts +9979 -3257
- package/dist/index.js +15954 -5774
- package/dist/index.js.map +1 -1
- package/docs/MARKETPLACE.md +1 -1
- package/docs/PRD-documentation-efficiency-study.md +406 -0
- package/docs/PRD-knowledge-retrieval-and-enrichment.md +466 -0
- package/docs/RELEASE.md +22 -8
- package/docs/adr/0002-documentation-audit-boundary.md +22 -0
- package/docs/adr/0003-study-protocol-and-historical-evidence.md +40 -0
- package/docs/adr/0004-controlled-study-runner.md +25 -0
- package/docs/adr/0005-documentation-quality-and-criticality.md +20 -0
- package/docs/adr/0006-registry-semantic-grounding.md +20 -0
- package/docs/adr/0007-longitudinal-study-metrics.md +21 -0
- package/docs/adr/0008-study-verification-boundary.md +21 -0
- package/docs/adr/0009-study-provider-cli-adapter.md +25 -0
- package/docs/agent-corpus/INDEX.md +14 -3
- package/docs/agent-corpus/OVERVIEW.md +25 -0
- package/docs/agent-corpus/chat.md +7 -3
- package/docs/agent-corpus/cli.md +18 -2
- package/docs/agent-corpus/conformance.md +14 -2
- package/docs/agent-corpus/doc-bridge.md +48 -1
- package/docs/agent-corpus/doctor.md +10 -2
- package/docs/agent-corpus/gates.md +6 -2
- package/docs/agent-corpus/mcp.md +15 -2
- package/docs/agent-corpus/memory.md +6 -2
- package/docs/agent-corpus/query.md +35 -2
- package/docs/bench/README.md +122 -0
- package/docs/bench/retrieval-baseline-v1.json +28 -0
- package/docs/bench/retrieval-suite-v1.json +1033 -0
- package/docs/chat-and-rag.md +3 -2
- package/docs/for-agents.md +9 -1
- package/docs/getting-started.md +4 -11
- package/docs/guides/gate-ci.md +11 -1
- package/docs/guides/install-and-run.md +9 -65
- package/docs/index.md +22 -1
- package/docs/knowledge-engine-runbook.md +51 -4
- package/docs/landing/assets/context-payload-reduction.svg +21 -0
- package/docs/landing/assets/controlled-ab-comparison.svg +30 -0
- package/docs/landing/index.html +119 -5
- package/docs/loop-workflow.md +117 -0
- package/docs/mcp.md +6 -1
- package/docs/parity/public-claims-v1.json +145 -0
- package/docs/playbook/doc-bridge-pattern.md +1 -1
- package/docs/query.md +90 -2
- package/docs/recipes/index-pipeline.md +1 -1
- package/docs/schemas/agent-handoff-v1.md +15 -0
- package/docs/schemas/doc-bridge-index-v1.md +65 -0
- package/docs/spec/benchmark-v1.md +39 -1
- package/docs/spec/cli.md +30 -10
- package/docs/spec/config-v1.md +192 -8
- package/docs/spec/documentation-audit-v1.md +61 -0
- package/docs/spec/enrichment-overlay-v1.md +241 -0
- package/docs/spec/graph-signals-v1.md +92 -0
- package/docs/spec/incremental-scan-v1.md +102 -0
- package/docs/spec/markdown-analyzer-v1.md +73 -0
- package/docs/spec/mcp-knowledge-tools-v1.md +147 -0
- package/docs/spec/measured-enrichment-v1.md +229 -0
- package/docs/spec/public-parity-v1.md +119 -0
- package/docs/spec/registry-agents.md +6 -0
- package/docs/spec/render-v1.md +122 -0
- package/docs/spec/retrieval-index-v1.md +164 -0
- package/docs/spec/study-metrics-v1.md +58 -0
- package/docs/spec/study-protocol-v1.md +46 -0
- package/docs/spec/study-provider-cli-v1.md +116 -0
- package/docs/spec/study-runner-v1.md +35 -0
- package/docs/spec/study-task-suite-v1.md +41 -0
- package/docs/spec/study-verification-v1.md +40 -0
- package/docs/study/README.md +84 -0
- package/docs/study/ab-adjudicated-cost-analysis-v1.md +29 -0
- package/docs/study/ab-adjudicated-cost-plan-v1.json +33 -0
- package/docs/study/ab-adjudicated-cost-plan-v2-v1.json +33 -0
- package/docs/study/ab-adjudicated-cost-result-v1.json +80 -0
- package/docs/study/ab-baseline-analysis-v1.md +21 -0
- package/docs/study/ab-baseline-plan-v1.json +33 -0
- package/docs/study/ab-baseline-recovery-plan-v1.json +33 -0
- package/docs/study/ab-baseline-result-v1.json +79 -0
- package/docs/study/documentation-audit-round-2026-08-31.json +183 -0
- package/docs/study/historical-evidence-v1.json +252 -0
- package/docs/study/observation-ledger-v1.json +30632 -0
- package/docs/study/phase3-task-coverage-v1.json +34 -0
- package/docs/study/phase4-public-pilot-ledger-v1.json +1344 -0
- package/docs/study/phase4-public-pilot-result-v1.json +52 -0
- package/docs/study/phase4-public-pilot-run-plan-v1.json +26 -0
- package/docs/study/phase4-public-pilot-task-suite-v1.json +71 -0
- package/docs/study/pilot-round-2026-08-31.json +46 -0
- package/docs/study/protocol-v1.json +90 -0
- package/docs/study/publication-gate-v1.md +45 -0
- package/docs/study/quality-scorecard-cycle-plan.md +545 -0
- package/docs/study/quality-scorecard-v1.json +38 -0
- package/docs/study/round-1-adjudicated-smoke-v1.json +30642 -0
- package/docs/study/round-1-instrumentation-plan-v1.md +39 -0
- package/docs/study/round-2-expanded-adjudication-v1.json +91 -0
- package/docs/study/round-2-expanded-validation-v1.md +58 -0
- package/docs/study/round-3-evidence-contract-v1.json +75 -0
- package/docs/study/round-3-evidence-contract-v1.md +57 -0
- package/docs/study/round-4-confirmation-v1.json +75 -0
- package/docs/study/round-4-confirmation-v1.md +55 -0
- package/docs/study/run-plan-v1.json +33 -0
- package/docs/study/semantic-adjudication-cycle-8.md +20 -0
- package/docs/study/task-suite-v1.json +96 -0
- package/docs/study/token-efficiency-plan-v1.md +337 -0
- package/docs/study/token-efficiency-protocol-v2.json +62 -0
- package/docs/study/verification-binding-v1.json +27 -0
- package/docs/validation-cycle-plan.md +33 -0
- package/docs/verification-harness.md +15 -6
- package/ecosystem-claims.json +2 -2
- package/ecosystem-upstream.json +2 -2
- package/ecosystem.json +4 -4
- package/mcpb/manifest.json +9 -1
- package/package.json +89 -72
- package/scripts/check-ecosystem-upstream.mjs +36 -7
- package/scripts/report-visual-check.mjs +20 -3
- package/skills/doc-bridge-handoff/fixtures/synthetic-repo/docs/for-agents/packages/payments.md +7 -0
- package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
- package/src/agents/registry-adapter.ts +192 -24
- package/src/audit/documentation.ts +513 -0
- package/src/bench/baseline.ts +198 -0
- package/src/bench/overlay-delta.ts +139 -0
- package/src/bench/retrieval.ts +319 -0
- package/src/budget/compile.ts +91 -0
- package/src/budget/sections.ts +70 -0
- package/src/cli/demo.ts +2 -2
- package/src/cli/program.ts +699 -79
- package/src/cli/usage.ts +71 -0
- package/src/config/defaults.ts +1 -0
- package/src/config/index.ts +4 -0
- package/src/config/load-config.ts +7 -1
- package/src/config/schema.ts +121 -4
- package/src/conformance/documentation-standard-v1.ts +22 -14
- package/src/discovery/areas.ts +182 -0
- package/src/discovery/documentation.ts +255 -23
- package/src/discovery/identity.ts +24 -0
- package/src/discovery/incremental.ts +314 -0
- package/src/discovery/inputs.ts +110 -0
- package/src/discovery/markdown.ts +481 -0
- package/src/discovery/repository.ts +557 -125
- package/src/doctor/run-doctor.ts +246 -27
- package/src/enrich/approvals.ts +190 -0
- package/src/enrich/cache.ts +93 -0
- package/src/enrich/context-pack.ts +272 -0
- package/src/enrich/overlay.ts +255 -0
- package/src/enrich/review.ts +106 -0
- package/src/enrich/stage.ts +374 -0
- package/src/enrich/stats.ts +100 -0
- package/src/enrich/validate.ts +410 -0
- package/src/federation/llms.ts +74 -24
- package/src/findings/report.ts +103 -0
- package/src/fixes/proposals.ts +4 -3
- package/src/graph/build.ts +356 -0
- package/src/graph/memory.ts +208 -0
- package/src/index-builder/build-handoffs.ts +22 -11
- package/src/index-builder/build-index.ts +132 -3
- package/src/index-builder/human-adapters/fumadocs.ts +1 -1
- package/src/index-builder/llms-txt.ts +48 -8
- package/src/index-builder/project-corpus.ts +111 -0
- package/src/index-builder/watch-index.ts +1 -1
- package/src/index.ts +630 -2
- package/src/lib/bounded-text.ts +15 -10
- package/src/lib/fuzzy-match.ts +235 -0
- package/src/mcp/knowledge.ts +554 -0
- package/src/mcp/server.ts +113 -18
- package/src/metrics/benchmark.ts +21 -0
- package/src/parity/check.ts +309 -0
- package/src/parity/claims.ts +259 -0
- package/src/parity/resolve.ts +160 -0
- package/src/query/handoff.ts +326 -0
- package/src/query/load-index.ts +53 -1
- package/src/query/query.ts +92 -59
- package/src/query/search.ts +289 -92
- package/src/query/text.ts +155 -0
- package/src/reconciliation/reconcile.ts +148 -15
- package/src/render/data.ts +356 -0
- package/src/render/engine.ts +398 -0
- package/src/render/generated.ts +77 -0
- package/src/render/render.ts +209 -0
- package/src/render/template-source.ts +52 -0
- package/src/render/templates.ts +289 -0
- package/src/report/html.ts +23 -17
- package/src/retrieval/bm25.ts +161 -0
- package/src/retrieval/project.ts +495 -0
- package/src/retrieval/rank.ts +383 -0
- package/src/retrieval/weights.ts +39 -0
- package/src/retriever/doc-bridge-retriever.ts +100 -15
- package/src/rules/engine.ts +45 -12
- package/src/safety/repository.ts +1 -1
- package/src/schemas/agent-handoff.ts +56 -0
- package/src/schemas/budget.ts +37 -0
- package/src/schemas/doc-bridge-index.ts +53 -2
- package/src/schemas/enrichment.ts +369 -0
- package/src/schemas/json-schemas.ts +39 -2
- package/src/schemas/knowledge.ts +19 -3
- package/src/schemas/retrieval-index.ts +152 -0
- package/src/shims/graphology.d.ts +91 -0
- package/src/study/adjudication.ts +196 -0
- package/src/study/execution.ts +350 -0
- package/src/study/expectations.ts +219 -0
- package/src/study/metrics.ts +467 -0
- package/src/study/protocol.ts +271 -0
- package/src/study/provider-cli.ts +115 -0
- package/src/study/provider-telemetry.ts +47 -0
- package/src/study/quality-scorecard.ts +164 -0
- package/src/study/runner.ts +461 -0
- package/src/study/task-suite.ts +321 -0
- package/src/study/verification.ts +134 -0
- package/src/validate.ts +8 -5
- package/src/version.ts +1 -1
- package/src/workflow/engine.ts +36 -11
- package/dist/index-C2PCQSrB.d.ts +0 -2251
- 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
|
+
]
|