@agentskit/doc-bridge 1.7.45 → 1.10.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 (213) hide show
  1. package/CHANGELOG.md +488 -0
  2. package/CONTRIBUTING.md +23 -0
  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 +15871 -6218
  10. package/dist/cli/program.js.map +1 -1
  11. package/dist/config/index.d.ts +1 -1
  12. package/dist/config/index.js +95 -6
  13. package/dist/config/index.js.map +1 -1
  14. package/dist/{index-BUL0q7s8.d.ts → index-7wYGbllW.d.ts} +134 -1
  15. package/dist/index.d.ts +9880 -1062
  16. package/dist/index.js +16143 -6127
  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/adr/0002-documentation-audit-boundary.md +22 -0
  22. package/docs/adr/0003-study-protocol-and-historical-evidence.md +40 -0
  23. package/docs/adr/0004-controlled-study-runner.md +25 -0
  24. package/docs/adr/0005-documentation-quality-and-criticality.md +20 -0
  25. package/docs/adr/0006-registry-semantic-grounding.md +20 -0
  26. package/docs/adr/0007-longitudinal-study-metrics.md +21 -0
  27. package/docs/adr/0008-study-verification-boundary.md +21 -0
  28. package/docs/adr/0009-study-provider-cli-adapter.md +25 -0
  29. package/docs/agent-corpus/INDEX.md +12 -1
  30. package/docs/agent-corpus/OVERVIEW.md +25 -0
  31. package/docs/agent-corpus/chat.md +5 -1
  32. package/docs/agent-corpus/cli.md +16 -0
  33. package/docs/agent-corpus/conformance.md +12 -0
  34. package/docs/agent-corpus/doc-bridge.md +47 -0
  35. package/docs/agent-corpus/doctor.md +8 -0
  36. package/docs/agent-corpus/gates.md +4 -0
  37. package/docs/agent-corpus/mcp.md +13 -0
  38. package/docs/agent-corpus/memory.md +4 -0
  39. package/docs/agent-corpus/query.md +33 -0
  40. package/docs/bench/README.md +122 -0
  41. package/docs/bench/retrieval-baseline-v1.json +28 -0
  42. package/docs/bench/retrieval-suite-v1.json +1033 -0
  43. package/docs/chat-and-rag.md +3 -2
  44. package/docs/for-agents.md +9 -1
  45. package/docs/getting-started.md +4 -11
  46. package/docs/guides/gate-ci.md +11 -1
  47. package/docs/guides/install-and-run.md +9 -65
  48. package/docs/index.md +22 -1
  49. package/docs/knowledge-engine-runbook.md +38 -3
  50. package/docs/landing/assets/context-payload-reduction.svg +21 -0
  51. package/docs/landing/assets/controlled-ab-comparison.svg +30 -0
  52. package/docs/landing/index.html +119 -5
  53. package/docs/loop-workflow.md +117 -0
  54. package/docs/mcp.md +6 -1
  55. package/docs/parity/public-claims-v1.json +145 -0
  56. package/docs/playbook/doc-bridge-pattern.md +1 -1
  57. package/docs/query.md +90 -2
  58. package/docs/recipes/index-pipeline.md +1 -1
  59. package/docs/schemas/agent-handoff-v1.md +15 -0
  60. package/docs/schemas/doc-bridge-index-v1.md +65 -0
  61. package/docs/spec/benchmark-v1.md +33 -1
  62. package/docs/spec/cli.md +30 -10
  63. package/docs/spec/config-v1.md +157 -8
  64. package/docs/spec/documentation-audit-v1.md +61 -0
  65. package/docs/spec/enrichment-overlay-v1.md +241 -0
  66. package/docs/spec/graph-signals-v1.md +92 -0
  67. package/docs/spec/incremental-scan-v1.md +102 -0
  68. package/docs/spec/markdown-analyzer-v1.md +73 -0
  69. package/docs/spec/mcp-knowledge-tools-v1.md +147 -0
  70. package/docs/spec/measured-enrichment-v1.md +229 -0
  71. package/docs/spec/public-parity-v1.md +119 -0
  72. package/docs/spec/registry-agents.md +6 -0
  73. package/docs/spec/render-v1.md +122 -0
  74. package/docs/spec/retrieval-index-v1.md +164 -0
  75. package/docs/spec/study-metrics-v1.md +58 -0
  76. package/docs/spec/study-protocol-v1.md +46 -0
  77. package/docs/spec/study-provider-cli-v1.md +116 -0
  78. package/docs/spec/study-runner-v1.md +35 -0
  79. package/docs/spec/study-task-suite-v1.md +41 -0
  80. package/docs/spec/study-verification-v1.md +40 -0
  81. package/docs/study/README.md +84 -0
  82. package/docs/study/ab-adjudicated-cost-analysis-v1.md +29 -0
  83. package/docs/study/ab-adjudicated-cost-plan-v1.json +33 -0
  84. package/docs/study/ab-adjudicated-cost-plan-v2-v1.json +33 -0
  85. package/docs/study/ab-adjudicated-cost-result-v1.json +80 -0
  86. package/docs/study/ab-baseline-analysis-v1.md +21 -0
  87. package/docs/study/ab-baseline-plan-v1.json +33 -0
  88. package/docs/study/ab-baseline-recovery-plan-v1.json +33 -0
  89. package/docs/study/ab-baseline-result-v1.json +79 -0
  90. package/docs/study/documentation-audit-round-2026-08-31.json +183 -0
  91. package/docs/study/historical-evidence-v1.json +252 -0
  92. package/docs/study/observation-ledger-v1.json +30632 -0
  93. package/docs/study/phase3-task-coverage-v1.json +34 -0
  94. package/docs/study/phase4-public-pilot-ledger-v1.json +1344 -0
  95. package/docs/study/phase4-public-pilot-result-v1.json +52 -0
  96. package/docs/study/phase4-public-pilot-run-plan-v1.json +26 -0
  97. package/docs/study/phase4-public-pilot-task-suite-v1.json +71 -0
  98. package/docs/study/pilot-round-2026-08-31.json +46 -0
  99. package/docs/study/protocol-v1.json +90 -0
  100. package/docs/study/publication-gate-v1.md +45 -0
  101. package/docs/study/quality-scorecard-cycle-plan.md +545 -0
  102. package/docs/study/quality-scorecard-v1.json +38 -0
  103. package/docs/study/round-1-adjudicated-smoke-v1.json +30642 -0
  104. package/docs/study/round-1-instrumentation-plan-v1.md +39 -0
  105. package/docs/study/round-2-expanded-adjudication-v1.json +91 -0
  106. package/docs/study/round-2-expanded-validation-v1.md +58 -0
  107. package/docs/study/round-3-evidence-contract-v1.json +75 -0
  108. package/docs/study/round-3-evidence-contract-v1.md +57 -0
  109. package/docs/study/round-4-confirmation-v1.json +75 -0
  110. package/docs/study/round-4-confirmation-v1.md +55 -0
  111. package/docs/study/run-plan-v1.json +33 -0
  112. package/docs/study/semantic-adjudication-cycle-8.md +20 -0
  113. package/docs/study/task-suite-v1.json +96 -0
  114. package/docs/study/token-efficiency-plan-v1.md +337 -0
  115. package/docs/study/token-efficiency-protocol-v2.json +62 -0
  116. package/docs/study/verification-binding-v1.json +27 -0
  117. package/docs/validation-cycle-plan.md +14 -0
  118. package/docs/verification-harness.md +11 -6
  119. package/ecosystem-claims.json +2 -2
  120. package/ecosystem-upstream.json +2 -2
  121. package/ecosystem.json +4 -4
  122. package/mcpb/manifest.json +9 -1
  123. package/package.json +34 -15
  124. package/scripts/check-ecosystem-upstream.mjs +34 -6
  125. package/skills/doc-bridge-handoff/fixtures/synthetic-repo/docs/for-agents/packages/payments.md +7 -0
  126. package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
  127. package/src/agents/registry-adapter.ts +192 -24
  128. package/src/audit/documentation.ts +513 -0
  129. package/src/bench/baseline.ts +198 -0
  130. package/src/bench/overlay-delta.ts +139 -0
  131. package/src/bench/retrieval.ts +319 -0
  132. package/src/budget/compile.ts +91 -0
  133. package/src/budget/sections.ts +70 -0
  134. package/src/cli/program.ts +684 -74
  135. package/src/cli/usage.ts +71 -0
  136. package/src/config/defaults.ts +1 -0
  137. package/src/config/index.ts +4 -0
  138. package/src/config/schema.ts +126 -2
  139. package/src/conformance/documentation-standard-v1.ts +8 -6
  140. package/src/discovery/areas.ts +197 -0
  141. package/src/discovery/documentation.ts +211 -5
  142. package/src/discovery/identity.ts +24 -0
  143. package/src/discovery/incremental.ts +314 -0
  144. package/src/discovery/inputs.ts +110 -0
  145. package/src/discovery/markdown.ts +481 -0
  146. package/src/discovery/repository.ts +485 -101
  147. package/src/doctor/run-doctor.ts +244 -12
  148. package/src/enrich/approvals.ts +190 -0
  149. package/src/enrich/cache.ts +93 -0
  150. package/src/enrich/context-pack.ts +272 -0
  151. package/src/enrich/overlay.ts +255 -0
  152. package/src/enrich/review.ts +106 -0
  153. package/src/enrich/stage.ts +374 -0
  154. package/src/enrich/stats.ts +100 -0
  155. package/src/enrich/validate.ts +410 -0
  156. package/src/federation/llms.ts +2 -4
  157. package/src/findings/report.ts +103 -0
  158. package/src/graph/build.ts +356 -0
  159. package/src/graph/memory.ts +208 -0
  160. package/src/index-builder/build-handoffs.ts +22 -11
  161. package/src/index-builder/build-index.ts +132 -3
  162. package/src/index-builder/llms-txt.ts +48 -8
  163. package/src/index-builder/project-corpus.ts +111 -0
  164. package/src/index.ts +630 -2
  165. package/src/lib/fuzzy-match.ts +235 -0
  166. package/src/mcp/knowledge.ts +554 -0
  167. package/src/mcp/server.ts +113 -18
  168. package/src/metrics/benchmark.ts +21 -0
  169. package/src/parity/check.ts +309 -0
  170. package/src/parity/claims.ts +259 -0
  171. package/src/parity/resolve.ts +160 -0
  172. package/src/query/handoff.ts +326 -0
  173. package/src/query/load-index.ts +53 -1
  174. package/src/query/query.ts +92 -59
  175. package/src/query/search.ts +289 -92
  176. package/src/query/text.ts +155 -0
  177. package/src/reconciliation/reconcile.ts +101 -10
  178. package/src/render/data.ts +356 -0
  179. package/src/render/engine.ts +398 -0
  180. package/src/render/generated.ts +77 -0
  181. package/src/render/render.ts +209 -0
  182. package/src/render/template-source.ts +52 -0
  183. package/src/render/templates.ts +289 -0
  184. package/src/report/html.ts +2 -2
  185. package/src/retrieval/bm25.ts +161 -0
  186. package/src/retrieval/project.ts +495 -0
  187. package/src/retrieval/rank.ts +383 -0
  188. package/src/retrieval/weights.ts +39 -0
  189. package/src/retriever/doc-bridge-retriever.ts +100 -15
  190. package/src/rules/engine.ts +30 -10
  191. package/src/schemas/agent-handoff.ts +56 -0
  192. package/src/schemas/budget.ts +37 -0
  193. package/src/schemas/doc-bridge-index.ts +53 -2
  194. package/src/schemas/enrichment.ts +369 -0
  195. package/src/schemas/json-schemas.ts +39 -2
  196. package/src/schemas/knowledge.ts +14 -1
  197. package/src/schemas/retrieval-index.ts +152 -0
  198. package/src/shims/graphology.d.ts +91 -0
  199. package/src/study/adjudication.ts +196 -0
  200. package/src/study/execution.ts +350 -0
  201. package/src/study/expectations.ts +219 -0
  202. package/src/study/metrics.ts +467 -0
  203. package/src/study/protocol.ts +271 -0
  204. package/src/study/provider-cli.ts +115 -0
  205. package/src/study/provider-telemetry.ts +47 -0
  206. package/src/study/quality-scorecard.ts +164 -0
  207. package/src/study/runner.ts +461 -0
  208. package/src/study/task-suite.ts +321 -0
  209. package/src/study/verification.ts +134 -0
  210. package/src/validate.ts +1 -4
  211. package/src/version.ts +1 -1
  212. package/src/workflow/engine.ts +36 -11
  213. package/scripts/verification-harness.mjs +0 -482
@@ -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')