@agentskit/doc-bridge 1.7.45 → 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 +465 -0
- package/CONTRIBUTING.md +23 -0
- 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 +15861 -6223
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +86 -6
- package/dist/config/index.js.map +1 -1
- package/dist/{index-BUL0q7s8.d.ts → index-Beor6Yhi.d.ts} +133 -1
- package/dist/index.d.ts +9871 -1062
- package/dist/index.js +16126 -6125
- 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/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 +12 -1
- package/docs/agent-corpus/OVERVIEW.md +25 -0
- package/docs/agent-corpus/chat.md +5 -1
- package/docs/agent-corpus/cli.md +16 -0
- package/docs/agent-corpus/conformance.md +12 -0
- package/docs/agent-corpus/doc-bridge.md +47 -0
- package/docs/agent-corpus/doctor.md +8 -0
- package/docs/agent-corpus/gates.md +4 -0
- package/docs/agent-corpus/mcp.md +13 -0
- package/docs/agent-corpus/memory.md +4 -0
- package/docs/agent-corpus/query.md +33 -0
- 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 +38 -3
- 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 +33 -1
- package/docs/spec/cli.md +30 -10
- package/docs/spec/config-v1.md +147 -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 +14 -0
- package/docs/verification-harness.md +11 -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 +25 -6
- package/scripts/check-ecosystem-upstream.mjs +34 -6
- 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/program.ts +684 -74
- package/src/cli/usage.ts +71 -0
- package/src/config/defaults.ts +1 -0
- package/src/config/index.ts +4 -0
- package/src/config/schema.ts +117 -2
- package/src/conformance/documentation-standard-v1.ts +8 -6
- package/src/discovery/areas.ts +182 -0
- package/src/discovery/documentation.ts +211 -5
- 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 +484 -101
- package/src/doctor/run-doctor.ts +244 -12
- 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 +2 -4
- package/src/findings/report.ts +103 -0
- 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/llms-txt.ts +48 -8
- package/src/index-builder/project-corpus.ts +111 -0
- package/src/index.ts +630 -2
- 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 +101 -10
- 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 +2 -2
- 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 +30 -10
- 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 +14 -1
- 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 +1 -4
- package/src/version.ts +1 -1
- package/src/workflow/engine.ts +36 -11
- package/scripts/verification-harness.mjs +0 -482
package/src/schemas/knowledge.ts
CHANGED
|
@@ -166,7 +166,7 @@ export const ReconciliationReportV1Schema = z
|
|
|
166
166
|
entityCount: z.number().int().nonnegative(),
|
|
167
167
|
relationCount: z.number().int().nonnegative(),
|
|
168
168
|
diagnosticCount: z.number().int().nonnegative(),
|
|
169
|
-
scope: z.enum(['file', 'module', 'package']).optional(),
|
|
169
|
+
scope: z.enum(['file', 'module', 'area', 'package']).optional(),
|
|
170
170
|
requiredRelationKinds: z.array(boundedString(128)).max(128).optional(),
|
|
171
171
|
requiredRelationTargets: z.enum(['all', 'internal']).optional(),
|
|
172
172
|
diagnosticsByCode: z.record(z.string().max(128), z.number().int().nonnegative()).optional(),
|
|
@@ -228,11 +228,23 @@ export const WorkflowTransitionSchema = z
|
|
|
228
228
|
.strict()
|
|
229
229
|
export type WorkflowTransition = z.infer<typeof WorkflowTransitionSchema>
|
|
230
230
|
|
|
231
|
+
/** Optional identity envelope shared with AgentsKit runtime and Chat events. */
|
|
232
|
+
export const CorrelationContextV1Schema = z.object({
|
|
233
|
+
operationId: boundedString(128),
|
|
234
|
+
runId: boundedString(128).optional(),
|
|
235
|
+
sessionId: boundedString(128).optional(),
|
|
236
|
+
turnId: boundedString(128).optional(),
|
|
237
|
+
actionId: boundedString(128).optional(),
|
|
238
|
+
traceId: boundedString(128).optional(),
|
|
239
|
+
}).strict()
|
|
240
|
+
export type CorrelationContextV1 = z.infer<typeof CorrelationContextV1Schema>
|
|
241
|
+
|
|
231
242
|
export const WorkflowRunV1Schema = z
|
|
232
243
|
.object({
|
|
233
244
|
type: z.literal('workflow-run'),
|
|
234
245
|
...ArtifactMetadata,
|
|
235
246
|
runId: boundedString(128),
|
|
247
|
+
correlation: CorrelationContextV1Schema.optional(),
|
|
236
248
|
state: WorkflowStateSchema,
|
|
237
249
|
steps: z.array(WorkflowStepSchema).max(32),
|
|
238
250
|
transitions: z.array(WorkflowTransitionSchema).max(1_000),
|
|
@@ -264,6 +276,7 @@ export const AgentProposalV1Schema = z
|
|
|
264
276
|
proposalId: boundedString(128),
|
|
265
277
|
baseSnapshotHash: hash,
|
|
266
278
|
baseReportHash: hash,
|
|
279
|
+
baseDocumentationAuditHash: hash.optional(),
|
|
267
280
|
relatedDiagnosticIds: z.array(boundedString(256)).max(64),
|
|
268
281
|
rationale: boundedString(4_000),
|
|
269
282
|
confidence: z.number().min(0).max(1),
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { ProvenanceSchema } from './knowledge.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The retrieval index: what ranking reads.
|
|
7
|
+
*
|
|
8
|
+
* It is a projection of the discovery snapshot — a pure function of the snapshot, the accepted
|
|
9
|
+
* enrichment overlay and the configuration — and nothing else. It has no scanner of its own,
|
|
10
|
+
* which is the invariant that makes "the index" and "the snapshot" describe the same repository:
|
|
11
|
+
* an entity retrieval can find is an entity discovery observed, with the same id, the same
|
|
12
|
+
* content hash and the same evidence.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const RETRIEVAL_INDEX_SCHEMA_VERSION = 1 as const
|
|
16
|
+
|
|
17
|
+
const hash = z.string().regex(/^[a-f0-9]{64}$/)
|
|
18
|
+
|
|
19
|
+
export const RetrievalKindSchema = z.enum(['document', 'module', 'area', 'package', 'intent', 'change'])
|
|
20
|
+
export type RetrievalKind = z.infer<typeof RetrievalKindSchema>
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* How much to trust a result, from most to least.
|
|
24
|
+
*
|
|
25
|
+
* `observed` came from code or a manifest; `declared` from a documentation declaration or the
|
|
26
|
+
* configuration; `fuzzy` from a reference that resolved by similarity rather than by name;
|
|
27
|
+
* `proposed` from an agent whose proposal has not been accepted. A result's confidence is the
|
|
28
|
+
* weakest link between the entry and the relation that surfaced it.
|
|
29
|
+
*/
|
|
30
|
+
export const ConfidenceSchema = z.enum(['observed', 'declared', 'fuzzy', 'proposed'])
|
|
31
|
+
export type Confidence = z.infer<typeof ConfidenceSchema>
|
|
32
|
+
|
|
33
|
+
export const AudienceSchema = z.enum(['agent', 'human', 'human-and-agent'])
|
|
34
|
+
export type Audience = z.infer<typeof AudienceSchema>
|
|
35
|
+
|
|
36
|
+
/** Text the lexical ranker indexes, one string per field, already bounded. */
|
|
37
|
+
export const RetrievalFieldsSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
title: z.string().max(512),
|
|
40
|
+
headings: z.string().max(4_096),
|
|
41
|
+
path: z.string().max(1_024),
|
|
42
|
+
symbols: z.string().max(8_192),
|
|
43
|
+
summary: z.string().max(2_048),
|
|
44
|
+
body: z.string().max(16_000),
|
|
45
|
+
aliases: z.string().max(2_048),
|
|
46
|
+
})
|
|
47
|
+
.strict()
|
|
48
|
+
export type RetrievalFields = z.infer<typeof RetrievalFieldsSchema>
|
|
49
|
+
|
|
50
|
+
/** One edge into or out of an entry, with the confidence of the relation it came from. */
|
|
51
|
+
export const RetrievalEdgeSchema = z
|
|
52
|
+
.object({
|
|
53
|
+
kind: z.string().min(1).max(64),
|
|
54
|
+
id: z.string().min(1).max(256),
|
|
55
|
+
confidence: ConfidenceSchema,
|
|
56
|
+
})
|
|
57
|
+
.strict()
|
|
58
|
+
export type RetrievalEdge = z.infer<typeof RetrievalEdgeSchema>
|
|
59
|
+
|
|
60
|
+
export const RetrievalGraphSchema = z
|
|
61
|
+
.object({
|
|
62
|
+
/** Canonicality: PageRank over `links-to` and `covers`. */
|
|
63
|
+
pagerank: z.number().min(0).max(1),
|
|
64
|
+
inboundLinks: z.number().int().nonnegative(),
|
|
65
|
+
coveredBy: z.array(z.string().min(1).max(256)).max(64),
|
|
66
|
+
mentionedBy: z.array(z.string().min(1).max(256)).max(64),
|
|
67
|
+
areaId: z.string().min(1).max(256).optional(),
|
|
68
|
+
packageId: z.string().min(1).max(256).optional(),
|
|
69
|
+
/** Documentation edges into this entry, with their confidence. The explain view reads these. */
|
|
70
|
+
inbound: z.array(RetrievalEdgeSchema).max(64),
|
|
71
|
+
/** Where a document's own links go, and which internal modules a module imports. */
|
|
72
|
+
outbound: z.array(RetrievalEdgeSchema).max(64),
|
|
73
|
+
})
|
|
74
|
+
.strict()
|
|
75
|
+
export type RetrievalGraph = z.infer<typeof RetrievalGraphSchema>
|
|
76
|
+
|
|
77
|
+
export const RetrievalEntrySchema = z
|
|
78
|
+
.object({
|
|
79
|
+
id: z.string().min(1).max(256),
|
|
80
|
+
kind: RetrievalKindSchema,
|
|
81
|
+
path: z.string().min(1).max(512),
|
|
82
|
+
title: z.string().min(1).max(256),
|
|
83
|
+
summary: z.string().max(2_048).optional(),
|
|
84
|
+
audience: AudienceSchema.optional(),
|
|
85
|
+
/** Names this entry also answers to: an ownership id, a sidecar id, a package's short name. */
|
|
86
|
+
aliases: z.array(z.string().min(1).max(256)).max(32),
|
|
87
|
+
/** A module's exported names, kept as a list so an exact-symbol match is exact. */
|
|
88
|
+
symbols: z.array(z.string().min(1).max(128)).max(256).optional(),
|
|
89
|
+
tags: z.array(z.string().min(1).max(64)).max(32),
|
|
90
|
+
fields: RetrievalFieldsSchema,
|
|
91
|
+
graph: RetrievalGraphSchema,
|
|
92
|
+
contentHash: hash,
|
|
93
|
+
provenance: ProvenanceSchema,
|
|
94
|
+
confidence: ConfidenceSchema,
|
|
95
|
+
/** The ownership record this entry stands for, when one is attached to it. */
|
|
96
|
+
ownershipId: z.string().min(1).max(256).optional(),
|
|
97
|
+
/**
|
|
98
|
+
* The accepted enrichment overlay's share of the bounded agent weight for this entry, 0..1.
|
|
99
|
+
* Absent when no live accepted entry names it; the ranker treats absent as zero.
|
|
100
|
+
*/
|
|
101
|
+
agentSignal: z.number().min(0).max(1).optional(),
|
|
102
|
+
})
|
|
103
|
+
.strict()
|
|
104
|
+
export type RetrievalEntry = z.infer<typeof RetrievalEntrySchema>
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* What the lexical ranker needs beyond the entries themselves.
|
|
108
|
+
*
|
|
109
|
+
* The postings are not stored: `fields` already is the serialised index, because tokenisation is
|
|
110
|
+
* versioned (`lexiconVersion`) and deterministic, so the postings a reader rebuilds are the
|
|
111
|
+
* postings the writer would have stored. What is recorded is the shape of the collection —
|
|
112
|
+
* enough to check that a reader rebuilt the same index, and to score against it.
|
|
113
|
+
*/
|
|
114
|
+
export const RetrievalLexicalSchema = z
|
|
115
|
+
.object({
|
|
116
|
+
version: z.number().int().nonnegative().max(1_000),
|
|
117
|
+
documentCount: z.number().int().nonnegative(),
|
|
118
|
+
fieldNames: z.array(z.string().min(1).max(64)).max(32),
|
|
119
|
+
averageFieldLength: z.record(z.string().min(1).max(64), z.number().nonnegative()),
|
|
120
|
+
})
|
|
121
|
+
.strict()
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The most entries one index can carry.
|
|
125
|
+
*
|
|
126
|
+
* A bound, because an unbounded artifact is not an artifact: a reader has to be able to refuse a
|
|
127
|
+
* file before parsing it all. It is deliberately above what a large monorepo produces — the
|
|
128
|
+
* repository that first exceeded the previous bound projects about eleven thousand entries — so the
|
|
129
|
+
* limit reports a corpus nobody meant to index rather than a corpus that is merely big.
|
|
130
|
+
*/
|
|
131
|
+
export const RETRIEVAL_MAX_ENTRIES = 50_000
|
|
132
|
+
|
|
133
|
+
export const RetrievalIndexV1Schema = z
|
|
134
|
+
.object({
|
|
135
|
+
type: z.literal('retrieval-index'),
|
|
136
|
+
schemaVersion: z.literal(RETRIEVAL_INDEX_SCHEMA_VERSION),
|
|
137
|
+
contentHash: hash,
|
|
138
|
+
contentHashAlgo: z.literal('sha256-normalized-v1'),
|
|
139
|
+
/** The three inputs the projection is a function of. Same three hashes, same projection. */
|
|
140
|
+
snapshotHash: hash,
|
|
141
|
+
overlayHash: hash,
|
|
142
|
+
configurationHash: hash,
|
|
143
|
+
lexiconVersion: z.number().int().nonnegative().max(1_000),
|
|
144
|
+
graphMetricsVersion: z.string().min(1).max(64),
|
|
145
|
+
weights: z.record(z.string().min(1).max(64), z.number().min(0).max(1_000)),
|
|
146
|
+
params: z.object({ k1: z.number().min(0).max(100), b: z.number().min(0).max(1) }).strict(),
|
|
147
|
+
lexical: RetrievalLexicalSchema,
|
|
148
|
+
entries: z.array(RetrievalEntrySchema).max(RETRIEVAL_MAX_ENTRIES),
|
|
149
|
+
})
|
|
150
|
+
.strict()
|
|
151
|
+
|
|
152
|
+
export type RetrievalIndexV1 = z.infer<typeof RetrievalIndexV1Schema>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* graphology under `moduleResolution: NodeNext`.
|
|
3
|
+
*
|
|
4
|
+
* These packages ship CommonJS type declarations with an ESM-style `export default`. Node loads
|
|
5
|
+
* their ESM build (they declare an `import` condition) and gets the class or function, but
|
|
6
|
+
* TypeScript reads the CommonJS declarations and types the default import as the module namespace
|
|
7
|
+
* — so `new Graph()` reads as "not constructable" and `pagerank(graph)` as "not callable", both
|
|
8
|
+
* wrongly. The packages publish one `types` path for both conditions, so there is nothing to
|
|
9
|
+
* select.
|
|
10
|
+
*
|
|
11
|
+
* Rather than cast at every call site, this declares the surface the graph layer actually uses,
|
|
12
|
+
* the way it behaves at runtime. It is deliberately narrow: nothing here is a guess, every
|
|
13
|
+
* signature is exercised by `tests/graph.test.ts` against real numeric expectations, so a drift
|
|
14
|
+
* from the library fails a test rather than hiding behind a cast.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
declare module 'graphology' {
|
|
18
|
+
export type GraphologyAttributes = Record<string, unknown>
|
|
19
|
+
|
|
20
|
+
export type GraphologyOptions = {
|
|
21
|
+
readonly type?: 'directed' | 'undirected' | 'mixed'
|
|
22
|
+
readonly multi?: boolean
|
|
23
|
+
readonly allowSelfLoops?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface GraphologyGraph {
|
|
27
|
+
readonly order: number
|
|
28
|
+
readonly size: number
|
|
29
|
+
hasNode(node: string): boolean
|
|
30
|
+
addNode(node: string, attributes?: GraphologyAttributes): string
|
|
31
|
+
hasEdge(source: string, target: string): boolean
|
|
32
|
+
addEdge(source: string, target: string, attributes?: GraphologyAttributes): string
|
|
33
|
+
getEdgeAttributes(source: string, target: string): GraphologyAttributes
|
|
34
|
+
nodes(): string[]
|
|
35
|
+
edges(): string[]
|
|
36
|
+
outNeighbors(node: string): string[]
|
|
37
|
+
neighbors(node: string): string[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const Graph: new (options?: GraphologyOptions) => GraphologyGraph
|
|
41
|
+
export default Graph
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
declare module 'graphology-metrics/centrality/pagerank.js' {
|
|
45
|
+
import type { GraphologyGraph } from 'graphology'
|
|
46
|
+
|
|
47
|
+
const pagerank: (
|
|
48
|
+
graph: GraphologyGraph,
|
|
49
|
+
options?: {
|
|
50
|
+
readonly getEdgeWeight?: string | null
|
|
51
|
+
readonly alpha?: number
|
|
52
|
+
readonly maxIterations?: number
|
|
53
|
+
readonly tolerance?: number
|
|
54
|
+
},
|
|
55
|
+
) => Record<string, number>
|
|
56
|
+
export default pagerank
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
declare module 'graphology-metrics/centrality/betweenness.js' {
|
|
60
|
+
import type { GraphologyGraph } from 'graphology'
|
|
61
|
+
|
|
62
|
+
const betweennessCentrality: (
|
|
63
|
+
graph: GraphologyGraph,
|
|
64
|
+
options?: { readonly getEdgeWeight?: string | null; readonly normalized?: boolean },
|
|
65
|
+
) => Record<string, number>
|
|
66
|
+
export default betweennessCentrality
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
declare module 'graphology-shortest-path/unweighted.js' {
|
|
70
|
+
import type { GraphologyGraph } from 'graphology'
|
|
71
|
+
|
|
72
|
+
export function singleSourceLength(graph: GraphologyGraph, source: string): Record<string, number>
|
|
73
|
+
export function bidirectional(graph: GraphologyGraph, source: string, target: string): string[] | null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
declare module 'graphology-dag/has-cycle.js' {
|
|
77
|
+
import type { GraphologyGraph } from 'graphology'
|
|
78
|
+
|
|
79
|
+
const hasCycle: (graph: GraphologyGraph) => boolean
|
|
80
|
+
export default hasCycle
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
declare module 'graphology-communities-louvain' {
|
|
84
|
+
import type { GraphologyGraph } from 'graphology'
|
|
85
|
+
|
|
86
|
+
const louvain: (
|
|
87
|
+
graph: GraphologyGraph,
|
|
88
|
+
options?: { readonly rng?: () => number; readonly resolution?: number; readonly randomWalk?: boolean },
|
|
89
|
+
) => Record<string, number>
|
|
90
|
+
export default louvain
|
|
91
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
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 {
|
|
8
|
+
calculateStudyCostUsd,
|
|
9
|
+
validateStudyProviderCommand,
|
|
10
|
+
type StudyAdjudicatorCli,
|
|
11
|
+
} from './provider-cli.js'
|
|
12
|
+
import {
|
|
13
|
+
createControlledStudyLedger,
|
|
14
|
+
createControlledStudyObservation,
|
|
15
|
+
parseControlledStudyLedger,
|
|
16
|
+
type ControlledStudyObservationLedgerV1,
|
|
17
|
+
type ControlledStudyObservationV1,
|
|
18
|
+
} from './runner.js'
|
|
19
|
+
import { mechanicalRubricItems, modelRubricItems, parseStudyTaskSuite, type StudyTaskV1 } from './task-suite.js'
|
|
20
|
+
|
|
21
|
+
export const STUDY_ADJUDICATION_METHOD = 'independent-rubric-v1' as const
|
|
22
|
+
|
|
23
|
+
const outcome = z.enum(['success', 'partial', 'incorrect', 'incomplete', 'blocked'])
|
|
24
|
+
const identifier = z.string().regex(/^[a-z][a-z0-9-]{0,127}$/)
|
|
25
|
+
const AdjudicatorOutputSchema = z.object({
|
|
26
|
+
outcome,
|
|
27
|
+
confidence: z.number().min(0).max(1),
|
|
28
|
+
reasonCodes: z.array(identifier).max(16),
|
|
29
|
+
inputTokens: z.number().int().nonnegative().optional(),
|
|
30
|
+
outputTokens: z.number().int().nonnegative().optional(),
|
|
31
|
+
tokenMethod: z.enum(['provider', 'estimate']).optional(),
|
|
32
|
+
measurements: z.record(z.string().min(1).max(128), z.number().finite().nonnegative()).optional(),
|
|
33
|
+
}).strict().superRefine((value, context) => {
|
|
34
|
+
if ((value.inputTokens !== undefined || value.outputTokens !== undefined) && value.tokenMethod === undefined) context.addIssue({ code: z.ZodIssueCode.custom, path: ['tokenMethod'], message: 'Adjudicator token counts require a token method.' })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
type AdjudicatorOutput = z.infer<typeof AdjudicatorOutputSchema>
|
|
38
|
+
|
|
39
|
+
type ProcessResult = {
|
|
40
|
+
readonly status: 'completed' | 'timed-out' | 'unavailable' | 'invalid-output' | 'budget-exceeded'
|
|
41
|
+
readonly stdout: string
|
|
42
|
+
readonly stderrBytes: number
|
|
43
|
+
readonly durationMs: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const terminate = (child: ReturnType<typeof spawn>): void => {
|
|
47
|
+
if (child.pid === undefined) return
|
|
48
|
+
try { process.kill(-child.pid, 'SIGTERM') } catch { child.kill('SIGTERM') }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const runAdjudicatorProcess = (config: StudyAdjudicatorCli, cwd: string, input: string, maxRuntimeMs: number): Promise<ProcessResult> => new Promise((resolveResult) => {
|
|
52
|
+
const env = Object.fromEntries([...new Set(['PATH', 'HOME', 'TMPDIR', ...config.envAllowlist])].flatMap((name) => process.env[name] === undefined ? [] : [[name, process.env[name] as string]]))
|
|
53
|
+
const started = Date.now()
|
|
54
|
+
const child = spawn(config.command, [...config.args], { cwd: resolve(cwd), shell: false, detached: true, env, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
55
|
+
let stdout = ''
|
|
56
|
+
let stderrBytes = 0
|
|
57
|
+
let settled = false
|
|
58
|
+
const finish = (result: ProcessResult): void => { if (settled) return; settled = true; resolveResult(result) }
|
|
59
|
+
const timer = setTimeout(() => { terminate(child); finish({ status: 'timed-out', stdout, stderrBytes, durationMs: Date.now() - started }) }, maxRuntimeMs)
|
|
60
|
+
child.stdout.on('data', (chunk: Buffer) => {
|
|
61
|
+
stdout += chunk.toString('utf8')
|
|
62
|
+
if (Buffer.byteLength(stdout, 'utf8') > config.maxOutputBytes) { clearTimeout(timer); terminate(child); finish({ status: 'budget-exceeded', stdout: stdout.slice(0, config.maxOutputBytes), stderrBytes, durationMs: Date.now() - started }) }
|
|
63
|
+
})
|
|
64
|
+
child.stderr.on('data', (chunk: Buffer) => { stderrBytes += Buffer.byteLength(chunk) })
|
|
65
|
+
child.once('error', () => { clearTimeout(timer); finish({ status: 'unavailable', stdout, stderrBytes, durationMs: Date.now() - started }) })
|
|
66
|
+
child.once('close', (code) => {
|
|
67
|
+
clearTimeout(timer)
|
|
68
|
+
if (settled) return
|
|
69
|
+
if (code !== 0) finish({ status: 'unavailable', stdout, stderrBytes, durationMs: Date.now() - started })
|
|
70
|
+
else { try { JSON.parse(stdout) as unknown; finish({ status: 'completed', stdout, stderrBytes, durationMs: Date.now() - started }) } catch { finish({ status: 'invalid-output', stdout, stderrBytes, durationMs: Date.now() - started }) } }
|
|
71
|
+
})
|
|
72
|
+
child.stdin.end(input)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* What the adjudicator is asked, and what it is not.
|
|
77
|
+
*
|
|
78
|
+
* A rubric item with a mechanical check has already been decided by the runner — acceptance
|
|
79
|
+
* commands and their exit status, evidence coverage, retrieval expectations. Showing such an item
|
|
80
|
+
* to a model invites it to disagree with a measurement, which is how a study ends up with an
|
|
81
|
+
* opinion where it had a number. So the input carries only the items no checker can settle, and
|
|
82
|
+
* the settled ones travel next to them as `mechanical`: verdicts, for context, not for review.
|
|
83
|
+
*/
|
|
84
|
+
export const adjudicatorRubric = (task: StudyTaskV1): {
|
|
85
|
+
readonly rubric: ReturnType<typeof modelRubricItems>
|
|
86
|
+
readonly mechanical: ReturnType<typeof mechanicalRubricItems>
|
|
87
|
+
} => ({ rubric: modelRubricItems(task), mechanical: mechanicalRubricItems(task) })
|
|
88
|
+
|
|
89
|
+
const adjudicatorInput = (task: StudyTaskV1, observation: ControlledStudyObservationV1): string => JSON.stringify({
|
|
90
|
+
protocol: 'doc-bridge.study-adjudicator.v1',
|
|
91
|
+
instruction: 'Evaluate only the bounded candidate record against the rubric items supplied. Items already decided by a mechanical check are listed under mechanical and are not yours to re-decide. Use evidenceCoverage and acceptanceExecution to distinguish missing evidence from unexecuted checks. Do not infer missing evidence. Return one JSON object and no prose.',
|
|
92
|
+
task: { id: task.id, category: task.category, difficulty: task.difficulty, objective: task.objective, expectedOutcome: task.expectedOutcome, evidenceRequirements: task.evidenceRequirements, acceptanceChecks: task.acceptanceChecks, ...adjudicatorRubric(task) },
|
|
93
|
+
candidate: {
|
|
94
|
+
taskOutcome: observation.taskOutcome ?? null,
|
|
95
|
+
evidenceQuality: observation.evidenceQuality ?? null,
|
|
96
|
+
safetyOutcome: observation.safetyOutcome ?? null,
|
|
97
|
+
evidenceIds: observation.evidenceIds,
|
|
98
|
+
evidenceCoverage: {
|
|
99
|
+
required: task.evidenceRequirements.map((requirement) => ({ id: requirement.id, present: observation.evidenceIds.includes(requirement.id) })),
|
|
100
|
+
referenceCount: observation.evidenceIds.length,
|
|
101
|
+
},
|
|
102
|
+
acceptanceExecution: {
|
|
103
|
+
status: observation.execution.status,
|
|
104
|
+
passed: observation.measurements?.acceptanceChecksPassed ?? null,
|
|
105
|
+
total: observation.measurements?.acceptanceChecksTotal ?? null,
|
|
106
|
+
executed: observation.measurements?.acceptanceChecksExecuted ?? null,
|
|
107
|
+
measurementPresent: observation.measurements?.acceptanceChecksPassed !== undefined
|
|
108
|
+
&& observation.measurements?.acceptanceChecksTotal !== undefined
|
|
109
|
+
&& observation.measurements?.acceptanceChecksExecuted !== undefined,
|
|
110
|
+
},
|
|
111
|
+
measurements: observation.measurements ?? {},
|
|
112
|
+
execution: { status: observation.execution.status, durationMs: observation.execution.durationMs, responseBytes: observation.execution.responseBytes },
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const withUpdatedObservation = (observation: ControlledStudyObservationV1, adjudication: ControlledStudyObservationV1['adjudication'], measurements: Readonly<Record<string, number>>): ControlledStudyObservationV1 => {
|
|
117
|
+
const { contentHash: _contentHash, contentHashAlgo: _contentHashAlgo, ...payload } = observation
|
|
118
|
+
return createControlledStudyObservation({ ...payload, measurements, adjudication })
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const independentlyAdjudicateStudyObservation = async (task: StudyTaskV1, observation: ControlledStudyObservationV1, config: StudyAdjudicatorCli, cwd: string, maxRuntimeMs: number, configurationHash: string): Promise<ControlledStudyObservationV1> => {
|
|
122
|
+
validateStudyProviderCommand(config, cwd)
|
|
123
|
+
const input = adjudicatorInput(task, observation)
|
|
124
|
+
if (Buffer.byteLength(input, 'utf8') > config.maxInputBytes) throw new Error(`Adjudicator input limit ${config.maxInputBytes} bytes exceeded for ${task.id}.`)
|
|
125
|
+
const processResult = await runAdjudicatorProcess(config, cwd, input, maxRuntimeMs)
|
|
126
|
+
const baseMeasurements = { ...(observation.measurements ?? {}), adjudicatorLatencyMs: processResult.durationMs }
|
|
127
|
+
if (processResult.status !== 'completed') return withUpdatedObservation(observation, { status: 'pending', actor: config.id, method: STUDY_ADJUDICATION_METHOD, reason: `Independent adjudicator ${processResult.status}.` }, baseMeasurements)
|
|
128
|
+
const parsed = AdjudicatorOutputSchema.safeParse(JSON.parse(processResult.stdout))
|
|
129
|
+
if (!parsed.success) return withUpdatedObservation(observation, { status: 'pending', actor: config.id, method: STUDY_ADJUDICATION_METHOD, reason: 'Independent adjudicator returned invalid output.' }, baseMeasurements)
|
|
130
|
+
const output: AdjudicatorOutput = parsed.data
|
|
131
|
+
const costUsd = calculateStudyCostUsd(config.pricing, {
|
|
132
|
+
...(output.inputTokens === undefined ? {} : { inputTokens: output.inputTokens }),
|
|
133
|
+
...(output.measurements?.cachedInputTokens === undefined ? {} : { cachedInputTokens: output.measurements.cachedInputTokens }),
|
|
134
|
+
...(output.outputTokens === undefined ? {} : { outputTokens: output.outputTokens }),
|
|
135
|
+
})
|
|
136
|
+
const tokenUnits = output.tokenMethod === 'provider' && output.inputTokens !== undefined && output.outputTokens !== undefined ? output.inputTokens + output.outputTokens : undefined
|
|
137
|
+
return withUpdatedObservation(observation, {
|
|
138
|
+
status: 'automated',
|
|
139
|
+
actor: config.id,
|
|
140
|
+
method: STUDY_ADJUDICATION_METHOD,
|
|
141
|
+
configurationHash,
|
|
142
|
+
outcome: output.outcome,
|
|
143
|
+
confidence: output.confidence,
|
|
144
|
+
reasonCodes: output.reasonCodes,
|
|
145
|
+
...(output.tokenMethod === undefined ? {} : { tokenMethod: output.tokenMethod }),
|
|
146
|
+
reason: 'Independent adjudicator evaluated the anonymized bounded candidate record against the task rubric.',
|
|
147
|
+
}, {
|
|
148
|
+
...baseMeasurements,
|
|
149
|
+
...(output.measurements ?? {}),
|
|
150
|
+
...(output.inputTokens === undefined ? {} : { adjudicatorInputTokens: output.inputTokens }),
|
|
151
|
+
...(output.outputTokens === undefined ? {} : { adjudicatorOutputTokens: output.outputTokens }),
|
|
152
|
+
...(tokenUnits === undefined ? {} : { adjudicatorTokenCostUnits: tokenUnits }),
|
|
153
|
+
...(costUsd === undefined ? {} : { adjudicatorCostUsd: costUsd }),
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export type IndependentStudyAdjudicationOptions = {
|
|
158
|
+
readonly ledger: ControlledStudyObservationLedgerV1
|
|
159
|
+
readonly taskSuite: ReturnType<typeof parseStudyTaskSuite>
|
|
160
|
+
readonly config: StudyAdjudicatorCli
|
|
161
|
+
readonly configurationHash: string
|
|
162
|
+
readonly cwd: string
|
|
163
|
+
readonly maxRuntimeMs: number
|
|
164
|
+
readonly runId?: string
|
|
165
|
+
readonly limit?: number
|
|
166
|
+
readonly offset?: number
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const independentlyAdjudicateStudyLedger = async (options: IndependentStudyAdjudicationOptions): Promise<ControlledStudyObservationLedgerV1> => {
|
|
170
|
+
if (options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit < 1)) throw new Error('Adjudication limit must be a positive integer.')
|
|
171
|
+
if (options.offset !== undefined && (!Number.isInteger(options.offset) || options.offset < 0)) throw new Error('Adjudication offset must be a non-negative integer.')
|
|
172
|
+
const taskMap = new Map(options.taskSuite.tasks.map((task) => [task.id, task]))
|
|
173
|
+
const observations = [] as ControlledStudyObservationV1[]
|
|
174
|
+
let selected = 0
|
|
175
|
+
let skipped = 0
|
|
176
|
+
for (const observation of options.ledger.observations) {
|
|
177
|
+
if (options.runId !== undefined && observation.runId !== options.runId) { observations.push(observation); continue }
|
|
178
|
+
if (options.offset !== undefined && skipped < options.offset) { skipped += 1; observations.push(observation); continue }
|
|
179
|
+
if (options.limit !== undefined && selected >= options.limit) { observations.push(observation); continue }
|
|
180
|
+
const task = taskMap.get(observation.task.taskId)
|
|
181
|
+
if (!task) throw new Error(`No task definition exists for ${observation.task.taskId}.`)
|
|
182
|
+
selected += 1
|
|
183
|
+
observations.push(await independentlyAdjudicateStudyObservation(task, observation, options.config, options.cwd, options.maxRuntimeMs, options.configurationHash))
|
|
184
|
+
}
|
|
185
|
+
const { contentHash: _contentHash, contentHashAlgo: _contentHashAlgo, ...payload } = options.ledger
|
|
186
|
+
return createControlledStudyLedger({ ...payload, observations })
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const persistIndependentlyAdjudicatedLedger = (path: string, ledger: ControlledStudyObservationLedgerV1): string => {
|
|
190
|
+
parseControlledStudyLedger(ledger)
|
|
191
|
+
mkdirSync(dirname(resolve(path)), { recursive: true })
|
|
192
|
+
writeFileSync(resolve(path), `${JSON.stringify(ledger, null, 2)}\n`, 'utf8')
|
|
193
|
+
return resolve(path)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export const adjudicatedLedgerInputHash = (task: StudyTaskV1, observation: ControlledStudyObservationV1): string => createHash('sha256').update(adjudicatorInput(task, observation)).digest('hex')
|