@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
package/src/mcp/server.ts
CHANGED
|
@@ -8,15 +8,20 @@ import { retrieveDocBridgeChunks } from '../retriever/doc-bridge-retriever.js'
|
|
|
8
8
|
import { runGates } from '../gates/run-gates.js'
|
|
9
9
|
import { ingestMemoryCandidates } from '../memory/ingest.js'
|
|
10
10
|
import { classifyMemoryCandidates, draftMemoryPromotion } from '../memory/pipeline.js'
|
|
11
|
-
import {
|
|
11
|
+
import { loadFreshDocBridgeIndex } from '../query/load-index.js'
|
|
12
12
|
import { runQuery } from '../query/query.js'
|
|
13
13
|
import { searchIndex } from '../query/search.js'
|
|
14
|
+
import { findingsFromDiagnostics } from '../findings/report.js'
|
|
15
|
+
import { budgetedHandoff, formatKnowledgeLookupText, formatKnowledgeSearchText, knowledgeLookup, knowledgeSearch, MAX_LOOKUP_DEPTH } from './knowledge.js'
|
|
16
|
+
import type { AgentHandoffV1 } from '../schemas/agent-handoff.js'
|
|
17
|
+
import { RetrievalKindSchema } from '../schemas/retrieval-index.js'
|
|
14
18
|
import type { DocBridgeIndexV1 } from '../schemas/doc-bridge-index.js'
|
|
15
19
|
import { PACKAGE_VERSION } from '../version.js'
|
|
16
20
|
import { loadWorkflowManifest, loadWorkflowStepOutput } from '../workflow/engine.js'
|
|
17
21
|
import { parseDiscoverySnapshot, parseReconciliationReport } from '../validate.js'
|
|
18
22
|
import { applyFixProposal, approveFixProposal, createArtifactNormalizationProposal, createMarkdownLinkFixProposal } from '../fixes/proposals.js'
|
|
19
23
|
import { createRegistryAgentAdapter, loadRegistryAgentRunner, persistRegistryAgentProposal } from '../agents/registry-adapter.js'
|
|
24
|
+
import { decideEnrichment, listEnrichment } from '../enrich/review.js'
|
|
20
25
|
import { sha256NormalizedV1 } from '../index-builder/content-hash.js'
|
|
21
26
|
import { discoverRepository } from '../discovery/repository.js'
|
|
22
27
|
import { FixProposalV1Schema, type DiscoverySnapshotV1, type ReconciliationReportV1, type FixProposalV1 } from '../schemas/knowledge.js'
|
|
@@ -43,7 +48,7 @@ export const MCP_TOOLS = [
|
|
|
43
48
|
annotations: { readOnlyHint: true },
|
|
44
49
|
inputSchema: {
|
|
45
50
|
type: 'object',
|
|
46
|
-
properties: { id: { type: 'string' }, kind: { type: 'string', enum: ['package', 'ownership'] } },
|
|
51
|
+
properties: { id: { type: 'string' }, kind: { type: 'string', enum: ['package', 'ownership'] }, budgetTokens: { type: 'number' } },
|
|
47
52
|
required: ['id'],
|
|
48
53
|
},
|
|
49
54
|
},
|
|
@@ -54,7 +59,7 @@ export const MCP_TOOLS = [
|
|
|
54
59
|
annotations: { readOnlyHint: true },
|
|
55
60
|
inputSchema: {
|
|
56
61
|
type: 'object',
|
|
57
|
-
properties: { term: { type: 'string' }, limit: { type: 'number' } },
|
|
62
|
+
properties: { term: { type: 'string' }, limit: { type: 'number' }, agent: { type: 'boolean' }, mode: { type: 'string', enum: ['discovery', 'editing', 'debugging', 'documentation'] }, contextBudgetTokens: { type: 'number' } },
|
|
58
63
|
required: ['term'],
|
|
59
64
|
},
|
|
60
65
|
},
|
|
@@ -126,7 +131,7 @@ export const MCP_TOOLS = [
|
|
|
126
131
|
title: 'Read reconciliation diagnostics',
|
|
127
132
|
description: 'Read bounded diagnostics from the latest canonical reconciliation report.',
|
|
128
133
|
annotations: { readOnlyHint: true },
|
|
129
|
-
inputSchema: { type: 'object', properties: { status: { type: 'string' }, severity: { type: 'string' } } },
|
|
134
|
+
inputSchema: { type: 'object', properties: { status: { type: 'string' }, severity: { type: 'string' }, format: { type: 'string', enum: ['diagnostic', 'finding'] } } },
|
|
130
135
|
},
|
|
131
136
|
{
|
|
132
137
|
name: 'docbridge.relations',
|
|
@@ -146,21 +151,81 @@ export const MCP_TOOLS = [
|
|
|
146
151
|
name: 'docbridge.proposals',
|
|
147
152
|
title: 'Read or approve proposals',
|
|
148
153
|
description: 'Create, inspect, approve and apply deterministic proposals through the shared human-gated workflow.',
|
|
149
|
-
inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['list', 'propose-links', 'propose-normalize', 'suggest', 'approve', 'apply'] }, proposalHash: { type: 'string' }, artifactPath: { type: 'string' }, approvedBy: { type: 'string' }, proposal: { type: 'object' } } },
|
|
154
|
+
inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['list', 'propose-links', 'propose-normalize', 'suggest', 'approve', 'apply', 'enrich-list', 'enrich-approve', 'enrich-reject'] }, proposalHash: { type: 'string' }, artifactPath: { type: 'string' }, approvedBy: { type: 'string' }, proposal: { type: 'object' }, proposalId: { type: 'string' }, reason: { type: 'string' } } },
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'knowledge.search',
|
|
158
|
+
title: 'Search the knowledge graph',
|
|
159
|
+
description: 'Rank every indexed entity for a query, optionally by kind, with an explanation and within a token budget.',
|
|
160
|
+
annotations: { readOnlyHint: true },
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: {
|
|
164
|
+
query: { type: 'string' },
|
|
165
|
+
kinds: { type: 'array', items: { type: 'string', enum: ['document', 'module', 'area', 'package', 'intent', 'change'] } },
|
|
166
|
+
limit: { type: 'number' },
|
|
167
|
+
explain: { type: 'boolean' },
|
|
168
|
+
budgetTokens: { type: 'number' },
|
|
169
|
+
format: { type: 'string', enum: ['json', 'text'] },
|
|
170
|
+
},
|
|
171
|
+
required: ['query'],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'knowledge.lookup',
|
|
176
|
+
title: 'Look up one entity',
|
|
177
|
+
description: 'Return an entity with its neighbours by relation kind, the documents about it, its handoff, its open diagnostics and its evidence, within a token budget.',
|
|
178
|
+
annotations: { readOnlyHint: true },
|
|
179
|
+
inputSchema: {
|
|
180
|
+
type: 'object',
|
|
181
|
+
properties: {
|
|
182
|
+
id: { type: 'string' },
|
|
183
|
+
path: { type: 'string' },
|
|
184
|
+
depth: { type: 'number' },
|
|
185
|
+
budgetTokens: { type: 'number' },
|
|
186
|
+
format: { type: 'string', enum: ['json', 'text'] },
|
|
187
|
+
},
|
|
188
|
+
},
|
|
150
189
|
},
|
|
151
190
|
] as const
|
|
152
191
|
|
|
153
192
|
const asRecord = (value: unknown): Record<string, unknown> =>
|
|
154
193
|
value && typeof value === 'object' ? (value as Record<string, unknown>) : {}
|
|
155
194
|
|
|
195
|
+
/** A budget is a positive count of tokens; the upper bound keeps a typo from meaning "unbounded". */
|
|
196
|
+
const BudgetTokensSchema = z.number().int().positive().max(1_000_000)
|
|
197
|
+
|
|
156
198
|
const HandoffResolveArgsSchema = z.object({
|
|
157
199
|
id: z.string().min(1),
|
|
158
200
|
kind: z.enum(['package', 'ownership']).optional(),
|
|
201
|
+
budgetTokens: BudgetTokensSchema.optional(),
|
|
159
202
|
})
|
|
160
203
|
|
|
204
|
+
const KnowledgeSearchArgsSchema = z.object({
|
|
205
|
+
query: z.string().min(1),
|
|
206
|
+
kinds: z.array(RetrievalKindSchema).max(6).optional(),
|
|
207
|
+
limit: z.number().int().positive().max(100).optional(),
|
|
208
|
+
explain: z.boolean().optional(),
|
|
209
|
+
budgetTokens: BudgetTokensSchema.optional(),
|
|
210
|
+
format: z.enum(['json', 'text']).optional(),
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const KnowledgeLookupArgsSchema = z
|
|
214
|
+
.object({
|
|
215
|
+
id: z.string().min(1).optional(),
|
|
216
|
+
path: z.string().min(1).optional(),
|
|
217
|
+
depth: z.number().int().min(1).max(MAX_LOOKUP_DEPTH).optional(),
|
|
218
|
+
budgetTokens: BudgetTokensSchema.optional(),
|
|
219
|
+
format: z.enum(['json', 'text']).optional(),
|
|
220
|
+
})
|
|
221
|
+
.refine((args) => args.id || args.path, 'knowledge.lookup requires id or path')
|
|
222
|
+
|
|
161
223
|
const DocSearchArgsSchema = z.object({
|
|
162
224
|
term: z.string().min(1),
|
|
163
225
|
limit: z.number().int().positive().max(100).optional(),
|
|
226
|
+
agent: z.boolean().optional(),
|
|
227
|
+
mode: z.enum(['discovery', 'editing', 'debugging', 'documentation']).optional(),
|
|
228
|
+
contextBudgetTokens: z.number().int().positive().max(1_000_000).optional(),
|
|
164
229
|
})
|
|
165
230
|
|
|
166
231
|
const RetrieverQueryArgsSchema = z.object({
|
|
@@ -176,9 +241,9 @@ const DocGetArgsSchema = z
|
|
|
176
241
|
.refine((args) => args.id || args.path, 'doc.get requires id or path')
|
|
177
242
|
|
|
178
243
|
const WorkflowRunArgsSchema = z.object({ runId: z.string().min(1).optional() })
|
|
179
|
-
const DiagnosticsArgsSchema = z.object({ status: z.string().min(1).optional(), severity: z.string().min(1).optional() })
|
|
244
|
+
const DiagnosticsArgsSchema = z.object({ status: z.string().min(1).optional(), severity: z.string().min(1).optional(), format: z.enum(['diagnostic', 'finding']).optional() })
|
|
180
245
|
const RelationsArgsSchema = z.object({ kind: z.string().min(1).optional(), limit: z.number().int().positive().max(500).optional() })
|
|
181
|
-
const ProposalsArgsSchema = z.object({ action: z.enum(['list', 'propose-links', 'propose-normalize', 'suggest', 'approve', 'apply']).optional(), proposalHash: z.string().min(1).optional(), artifactPath: z.string().min(1).optional(), approvedBy: z.string().min(1).optional(), proposal: z.unknown().optional() })
|
|
246
|
+
const ProposalsArgsSchema = z.object({ action: z.enum(['list', 'propose-links', 'propose-normalize', 'suggest', 'approve', 'apply', 'enrich-list', 'enrich-approve', 'enrich-reject']).optional(), proposalHash: z.string().min(1).optional(), artifactPath: z.string().min(1).optional(), approvedBy: z.string().min(1).optional(), proposal: z.unknown().optional(), proposalId: z.string().min(1).optional(), reason: z.string().max(1_024).optional() })
|
|
182
247
|
|
|
183
248
|
const parseToolArgs = <T>(tool: string, schema: z.ZodType<T>, value: unknown): T => {
|
|
184
249
|
try {
|
|
@@ -279,21 +344,37 @@ export const handleMcpRequest = (ctx: McpContext, request: JsonRpcRequest): unkn
|
|
|
279
344
|
const args = asRecord(params.arguments)
|
|
280
345
|
if (typeof name !== 'string') throw new Error('MCP tools/call requires a tool name.')
|
|
281
346
|
assertMcpToolEnabled(ctx, name)
|
|
282
|
-
const index = () => ctx.loadIndex?.() ??
|
|
347
|
+
const index = () => ctx.loadIndex?.() ?? loadFreshDocBridgeIndex(ctx.root, ctx.config)
|
|
283
348
|
|
|
284
349
|
if (name === 'handoff.resolve') {
|
|
285
350
|
const parsed = parseToolArgs('handoff.resolve', HandoffResolveArgsSchema, args)
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
351
|
+
const loaded = index()
|
|
352
|
+
const handoff = runQuery(loaded, ctx.config, {
|
|
353
|
+
kind: parsed.kind === 'package' ? 'package' : 'ownership',
|
|
354
|
+
id: parsed.id,
|
|
355
|
+
agent: true,
|
|
356
|
+
}, { root: ctx.root })
|
|
357
|
+
// The payload is the handoff it always was; a declared budget adds `budget` and may shed `related` and the summary note.
|
|
358
|
+
return textResult(parsed.budgetTokens === undefined ? handoff : budgetedHandoff(loaded, handoff as AgentHandoffV1, parsed.budgetTokens))
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (name === 'knowledge.search') {
|
|
362
|
+
const { format, ...parsed } = parseToolArgs('knowledge.search', KnowledgeSearchArgsSchema, args)
|
|
363
|
+
const response = knowledgeSearch(index(), parsed)
|
|
364
|
+
return textResult(format === 'text' ? formatKnowledgeSearchText(response) : response)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (name === 'knowledge.lookup') {
|
|
368
|
+
const { format, ...parsed } = parseToolArgs('knowledge.lookup', KnowledgeLookupArgsSchema, args)
|
|
369
|
+
// Diagnostics come from the latest workflow run when there is one; a repository never checked still gets its lookup.
|
|
370
|
+
const report = () => { try { return workflowReport(ctx) } catch { return undefined } }
|
|
371
|
+
const response = knowledgeLookup(index(), ctx.config, parsed, { root: ctx.root, report })
|
|
372
|
+
return textResult(format === 'text' ? formatKnowledgeLookupText(response) : response)
|
|
293
373
|
}
|
|
294
374
|
|
|
295
375
|
if (name === 'doc.search') {
|
|
296
376
|
const parsed = parseToolArgs('doc.search', DocSearchArgsSchema, args)
|
|
377
|
+
if (parsed.agent) return textResult(runQuery(index(), ctx.config, { kind: 'search', term: parsed.term, agent: true, ...(parsed.mode === undefined ? {} : { mode: parsed.mode }), ...(parsed.contextBudgetTokens === undefined ? {} : { contextBudgetTokens: parsed.contextBudgetTokens }) }))
|
|
297
378
|
return textResult(searchIndex(index(), parsed.term, parsed.limit ?? 20))
|
|
298
379
|
}
|
|
299
380
|
|
|
@@ -342,6 +423,8 @@ export const handleMcpRequest = (ctx: McpContext, request: JsonRpcRequest): unkn
|
|
|
342
423
|
const diagnostics = workflowReport(ctx).diagnostics.filter((diagnostic) =>
|
|
343
424
|
(!parsed.status || diagnostic.status === parsed.status) && (!parsed.severity || diagnostic.severity === parsed.severity),
|
|
344
425
|
)
|
|
426
|
+
// `finding` is the ecosystem shape; the filters above still speak the internal vocabulary.
|
|
427
|
+
if (parsed.format === 'finding') return textResult(redactValue({ reportHash: workflowReport(ctx).contentHash, findings: findingsFromDiagnostics(diagnostics) }))
|
|
345
428
|
return textResult(redactValue({ reportHash: workflowReport(ctx).contentHash, diagnostics }))
|
|
346
429
|
}
|
|
347
430
|
|
|
@@ -359,20 +442,32 @@ export const handleMcpRequest = (ctx: McpContext, request: JsonRpcRequest): unkn
|
|
|
359
442
|
if (name === 'docbridge.proposals') {
|
|
360
443
|
const parsed = parseToolArgs('docbridge.proposals', ProposalsArgsSchema, args)
|
|
361
444
|
const run = (() => { try { return workflowRun(ctx) } catch { return undefined } })()
|
|
445
|
+
// KR-10: enrichment proposals share this tool; a decision goes through the ecosystem approval gate.
|
|
446
|
+
if (parsed.action === 'enrich-list') return textResult(redactValue({ ...(run ? { runId: run.runId } : {}), enrichment: listEnrichment(ctx.root) ?? null }))
|
|
447
|
+
if (parsed.action === 'enrich-approve' || parsed.action === 'enrich-reject') {
|
|
448
|
+
if (!parsed.proposalId) throw new Error(`docbridge.proposals ${parsed.action} requires proposalId`)
|
|
449
|
+
const proposalId = parsed.proposalId
|
|
450
|
+
return (async () => {
|
|
451
|
+
const snapshot = (() => { try { return workflowSnapshot(ctx) } catch { return undefined } })()
|
|
452
|
+
const decided = await decideEnrichment({ root: ctx.root, proposalId, decision: parsed.action === 'enrich-approve' ? 'approved' : 'rejected', by: parsed.approvedBy ?? 'human', ...(parsed.reason ? { reason: parsed.reason } : {}), ...(snapshot ? { snapshot } : {}) })
|
|
453
|
+
return textResult(redactValue({ ...(run ? { runId: run.runId } : {}), approvalId: decided.approvalId, entry: decided.entry, overlayHash: decided.overlay.contentHash }))
|
|
454
|
+
})()
|
|
455
|
+
}
|
|
362
456
|
if (!parsed.action || parsed.action === 'list') {
|
|
363
457
|
let proposal: FixProposalV1 | undefined
|
|
364
458
|
try { proposal = readSavedProposal(ctx, undefined) } catch { proposal = undefined }
|
|
365
459
|
return textResult(redactValue({ ...(run ? { runId: run.runId } : {}), proposals: proposal ? [proposal] : [] }))
|
|
366
460
|
}
|
|
367
461
|
if (parsed.action === 'suggest') {
|
|
368
|
-
return
|
|
462
|
+
return (async () => {
|
|
463
|
+
const runner = ctx.config.intelligence?.registry?.cli ? undefined : await loadRegistryAgentRunner(ctx.root, ctx.config)
|
|
369
464
|
const snapshot = workflowSnapshot(ctx)
|
|
370
465
|
const report = workflowReport(ctx)
|
|
371
|
-
const adapter = createRegistryAgentAdapter(ctx.root, ctx.config, runner)
|
|
466
|
+
const adapter = createRegistryAgentAdapter(ctx.root, ctx.config, ctx.config.intelligence?.registry?.cli ? undefined : runner)
|
|
372
467
|
const proposal = await adapter.run(snapshot, report)
|
|
373
468
|
const savedPath = persistRegistryAgentProposal(workflowStateDir(ctx), proposal)
|
|
374
469
|
return textResult(redactValue({ ...(run ? { runId: run.runId } : {}), proposal, proposalPath: savedPath }))
|
|
375
|
-
})
|
|
470
|
+
})()
|
|
376
471
|
}
|
|
377
472
|
const discovered = discoverRepository({ root: ctx.root, config: ctx.config })
|
|
378
473
|
const options = { baseRevision: discovered.sourceRevision, configurationHash: sha256NormalizedV1(ctx.config), ...(ctx.config.project?.name ? { projectName: ctx.config.project.name } : {}) }
|
package/src/metrics/benchmark.ts
CHANGED
|
@@ -166,6 +166,27 @@ export const measureAgentEfficiency = (observation: AgentEfficiencyObservation)
|
|
|
166
166
|
contextReduction: observation.corpusBytes > 0 ? 1 - (percentile95(observation.responseBytes) / observation.corpusBytes) : 0,
|
|
167
167
|
})
|
|
168
168
|
|
|
169
|
+
export type AgentTaskEfficiencyObservation = {
|
|
170
|
+
readonly correct: boolean
|
|
171
|
+
readonly latencyMs: number
|
|
172
|
+
readonly responseBytes: number
|
|
173
|
+
readonly estimatedTokens: number
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export const measureAgentTaskEfficiency = (observations: readonly AgentTaskEfficiencyObservation[]) => {
|
|
177
|
+
const correct = observations.filter((observation) => observation.correct)
|
|
178
|
+
return {
|
|
179
|
+
taskCount: observations.length,
|
|
180
|
+
correctTaskCount: correct.length,
|
|
181
|
+
correctnessRate: ratio(correct.length, observations.length),
|
|
182
|
+
latencyP95Ms: percentile95(observations.map((observation) => observation.latencyMs)),
|
|
183
|
+
responseBytesP95: percentile95(observations.map((observation) => observation.responseBytes)),
|
|
184
|
+
estimatedTokensP95: percentile95(observations.map((observation) => observation.estimatedTokens)),
|
|
185
|
+
tokensToCorrectAnswerP95: correct.length ? percentile95(correct.map((observation) => observation.estimatedTokens)) : null,
|
|
186
|
+
timeToCorrectAnswerP95Ms: correct.length ? percentile95(correct.map((observation) => observation.latencyMs)) : null,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
169
190
|
export const formatBenchmarkText = (result: BenchmarkResult): string => [
|
|
170
191
|
`Precision: entities ${result.quality.entities.precision.toFixed(3)}, relations ${result.quality.relations.precision.toFixed(3)}, findings ${result.quality.findings.precision.toFixed(3)}`,
|
|
171
192
|
`Recall: entities ${result.quality.entities.recall.toFixed(3)}, relations ${result.quality.relations.recall.toFixed(3)}, findings ${result.quality.findings.recall.toFixed(3)}`,
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
|
|
6
|
+
import { contentHashForArtifactV1, sha256NormalizedV1 } from '../index-builder/content-hash.js'
|
|
7
|
+
import { redactSecrets } from '../safety/repository.js'
|
|
8
|
+
import {
|
|
9
|
+
claimPattern,
|
|
10
|
+
claimSurfaces,
|
|
11
|
+
exceptionFor,
|
|
12
|
+
renderClaim,
|
|
13
|
+
type PublicClaim,
|
|
14
|
+
type PublicClaimsV1,
|
|
15
|
+
} from './claims.js'
|
|
16
|
+
import { resolveClaim, type ResolveContext } from './resolve.js'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Compare what this repository says in public with what it can prove.
|
|
20
|
+
*
|
|
21
|
+
* The four outcomes are kept apart on purpose, because they need different actions. `stale` means a
|
|
22
|
+
* surface states a value the repository has moved past — someone edits one line. `missing` means a
|
|
23
|
+
* surface that promised to carry the claim does not. `contradiction` means two public surfaces
|
|
24
|
+
* disagree with each other, which is the one an agent cannot resolve on its own and the reason this
|
|
25
|
+
* check exists. `not-analyzed` means the claim could not be resolved at all: reported, never
|
|
26
|
+
* counted as a pass.
|
|
27
|
+
*
|
|
28
|
+
* The report is publication-safe by construction. It carries claim ids, repository-relative surface
|
|
29
|
+
* paths, line numbers, the two values, and a bounded excerpt with secrets redacted — never a
|
|
30
|
+
* document's contents, never an absolute path.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export const PUBLIC_PARITY_SCHEMA_VERSION = 1 as const
|
|
34
|
+
export const PARITY_EXCERPT_LIMIT = 160
|
|
35
|
+
|
|
36
|
+
const hash = z.string().regex(/^[a-f0-9]{64}$/)
|
|
37
|
+
|
|
38
|
+
export const PARITY_CODES = ['PARITY_STALE', 'PARITY_MISSING', 'PARITY_CONTRADICTION', 'PARITY_NOT_ANALYZED'] as const
|
|
39
|
+
export type ParityCode = (typeof PARITY_CODES)[number]
|
|
40
|
+
|
|
41
|
+
const ParityFindingSchema = z
|
|
42
|
+
.object({
|
|
43
|
+
id: hash,
|
|
44
|
+
code: z.enum(PARITY_CODES),
|
|
45
|
+
claimId: z.string().min(1).max(64),
|
|
46
|
+
owner: z.string().min(1).max(128),
|
|
47
|
+
severity: z.enum(['error', 'warn', 'info']),
|
|
48
|
+
/** False when an exception accepted it. The reason travels with the finding, not in a comment. */
|
|
49
|
+
blocking: z.boolean(),
|
|
50
|
+
surface: z.string().max(512).optional(),
|
|
51
|
+
line: z.number().int().positive().optional(),
|
|
52
|
+
stated: z.string().max(256).optional(),
|
|
53
|
+
canonical: z.string().max(256).optional(),
|
|
54
|
+
message: z.string().min(1).max(1_024),
|
|
55
|
+
remediation: z.string().min(1).max(1_024),
|
|
56
|
+
excerpt: z.string().max(PARITY_EXCERPT_LIMIT).optional(),
|
|
57
|
+
acceptedReason: z.string().max(512).optional(),
|
|
58
|
+
})
|
|
59
|
+
.strict()
|
|
60
|
+
|
|
61
|
+
export type ParityFinding = z.infer<typeof ParityFindingSchema>
|
|
62
|
+
|
|
63
|
+
export const PublicParityReportV1Schema = z
|
|
64
|
+
.object({
|
|
65
|
+
type: z.literal('public-parity-report'),
|
|
66
|
+
schemaVersion: z.literal(PUBLIC_PARITY_SCHEMA_VERSION),
|
|
67
|
+
contentHash: hash,
|
|
68
|
+
contentHashAlgo: z.literal('sha256-normalized-v1'),
|
|
69
|
+
project: z.object({ name: z.string().min(1).max(128) }).strict(),
|
|
70
|
+
sourceRevision: z.string().min(1).max(128),
|
|
71
|
+
sourceRevisionKind: z.enum(['git', 'content']),
|
|
72
|
+
registryVersion: z.string().min(1).max(64),
|
|
73
|
+
registryHash: hash,
|
|
74
|
+
status: z.enum(['pass', 'needs-review', 'blocked']),
|
|
75
|
+
metrics: z
|
|
76
|
+
.object({
|
|
77
|
+
claims: z.number().int().nonnegative(),
|
|
78
|
+
surfaces: z.number().int().nonnegative(),
|
|
79
|
+
matched: z.number().int().nonnegative(),
|
|
80
|
+
stale: z.number().int().nonnegative(),
|
|
81
|
+
missing: z.number().int().nonnegative(),
|
|
82
|
+
contradictions: z.number().int().nonnegative(),
|
|
83
|
+
notAnalyzed: z.number().int().nonnegative(),
|
|
84
|
+
accepted: z.number().int().nonnegative(),
|
|
85
|
+
blocking: z.number().int().nonnegative(),
|
|
86
|
+
})
|
|
87
|
+
.strict(),
|
|
88
|
+
findings: z.array(ParityFindingSchema).max(10_000),
|
|
89
|
+
/** Surfaces the registry names that do not exist in this checkout. */
|
|
90
|
+
missingSurfaces: z.array(z.string().max(512)).max(64),
|
|
91
|
+
limitations: z.array(z.string().min(1).max(512)).max(32),
|
|
92
|
+
})
|
|
93
|
+
.strict()
|
|
94
|
+
|
|
95
|
+
export type PublicParityReportV1 = z.infer<typeof PublicParityReportV1Schema>
|
|
96
|
+
|
|
97
|
+
export type CheckParityOptions = ResolveContext & {
|
|
98
|
+
readonly registry: PublicClaimsV1
|
|
99
|
+
readonly project: { readonly name: string }
|
|
100
|
+
readonly sourceRevision: string
|
|
101
|
+
readonly sourceRevisionKind: 'git' | 'content'
|
|
102
|
+
/** File contents by surface path. Defaults to reading under `root`. */
|
|
103
|
+
readonly readSurface?: (path: string) => string | undefined
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
type Occurrence = { readonly surface: string; readonly line: number; readonly stated: string; readonly excerpt: string }
|
|
107
|
+
|
|
108
|
+
const findingId = (parts: Record<string, unknown>): string => sha256NormalizedV1(parts)
|
|
109
|
+
|
|
110
|
+
const excerptOf = (line: string): string => {
|
|
111
|
+
const collapsed = redactSecrets(line.trim()).replace(/\s+/g, ' ')
|
|
112
|
+
return collapsed.length <= PARITY_EXCERPT_LIMIT ? collapsed : `${collapsed.slice(0, PARITY_EXCERPT_LIMIT - 1)}…`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Every occurrence of a claim in one surface, with the line it sits on.
|
|
117
|
+
*
|
|
118
|
+
* A `cli-command` claim has no value to extract: the command either appears or it does not, and
|
|
119
|
+
* the canonical value is the command itself. Everything else is read through the claim's template
|
|
120
|
+
* for that surface, so one fact stated two ways is still one claim.
|
|
121
|
+
*/
|
|
122
|
+
const occurrences = (claim: PublicClaim, surface: string, content: string): readonly Occurrence[] => {
|
|
123
|
+
const found: Occurrence[] = []
|
|
124
|
+
const lines = content.split('\n')
|
|
125
|
+
for (const [index, line] of lines.entries()) {
|
|
126
|
+
if (claim.evidence.kind === 'cli-command') {
|
|
127
|
+
if (line.includes(claim.evidence.command)) {
|
|
128
|
+
found.push({ surface, line: index + 1, stated: claim.evidence.command, excerpt: excerptOf(line) })
|
|
129
|
+
}
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
// A fresh pattern per line: a global regex carries lastIndex between calls.
|
|
133
|
+
for (const match of line.matchAll(claimPattern(claim, surface))) {
|
|
134
|
+
const stated = match[1]
|
|
135
|
+
if (stated === undefined) continue
|
|
136
|
+
found.push({ surface, line: index + 1, stated, excerpt: excerptOf(line) })
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return found
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const checkPublicParity = (options: CheckParityOptions): PublicParityReportV1 => {
|
|
143
|
+
const { registry } = options
|
|
144
|
+
const read = options.readSurface ?? ((path: string): string | undefined => {
|
|
145
|
+
const absolute = resolve(options.root, path)
|
|
146
|
+
return existsSync(absolute) ? readFileSync(absolute, 'utf8') : undefined
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const surfaces = claimSurfaces(registry)
|
|
150
|
+
const contents = new Map<string, string | undefined>(surfaces.map((surface) => [surface, read(surface)]))
|
|
151
|
+
const missingSurfaces = surfaces.filter((surface) => contents.get(surface) === undefined)
|
|
152
|
+
|
|
153
|
+
const findings: ParityFinding[] = []
|
|
154
|
+
const limitations: string[] = []
|
|
155
|
+
let matched = 0
|
|
156
|
+
|
|
157
|
+
for (const claim of [...registry.claims].sort((a, b) => a.claimId.localeCompare(b.claimId))) {
|
|
158
|
+
const resolved = resolveClaim(claim, options)
|
|
159
|
+
const declared = [...claim.required, ...(claim.optional ?? [])]
|
|
160
|
+
|
|
161
|
+
if (resolved.status === 'not-analyzed') {
|
|
162
|
+
findings.push({
|
|
163
|
+
id: findingId({ code: 'PARITY_NOT_ANALYZED', claimId: claim.claimId }),
|
|
164
|
+
code: 'PARITY_NOT_ANALYZED',
|
|
165
|
+
claimId: claim.claimId,
|
|
166
|
+
owner: claim.owner,
|
|
167
|
+
severity: 'warn',
|
|
168
|
+
blocking: false,
|
|
169
|
+
message: `${claim.statement} could not be resolved: ${resolved.reason}`,
|
|
170
|
+
remediation: claim.remediation,
|
|
171
|
+
})
|
|
172
|
+
limitations.push(`${claim.claimId}: ${resolved.reason}`)
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const stated = new Map<string, readonly Occurrence[]>()
|
|
177
|
+
for (const surface of declared) {
|
|
178
|
+
const content = contents.get(surface)
|
|
179
|
+
if (content === undefined) continue
|
|
180
|
+
stated.set(surface, occurrences(claim, surface, content))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/*
|
|
184
|
+
* A contradiction is about the surfaces agreeing with each other, so it is decided over the
|
|
185
|
+
* whole claim rather than per surface: two public pages stating different numbers is a finding
|
|
186
|
+
* even when neither of them matches the repository.
|
|
187
|
+
*/
|
|
188
|
+
const distinct = [...new Set([...stated.values()].flat().map((entry) => entry.stated))].sort()
|
|
189
|
+
if (distinct.length > 1) {
|
|
190
|
+
findings.push({
|
|
191
|
+
id: findingId({ code: 'PARITY_CONTRADICTION', claimId: claim.claimId, distinct }),
|
|
192
|
+
code: 'PARITY_CONTRADICTION',
|
|
193
|
+
claimId: claim.claimId,
|
|
194
|
+
owner: claim.owner,
|
|
195
|
+
severity: 'error',
|
|
196
|
+
blocking: true,
|
|
197
|
+
canonical: resolved.value,
|
|
198
|
+
message: `Public surfaces state ${distinct.join(' and ')} for ${claim.statement.toLowerCase()}; the repository says ${resolved.value}.`,
|
|
199
|
+
remediation: claim.remediation,
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for (const surface of declared) {
|
|
204
|
+
const content = contents.get(surface)
|
|
205
|
+
if (content === undefined) continue
|
|
206
|
+
const here = stated.get(surface) ?? []
|
|
207
|
+
const exception = exceptionFor(registry, claim.claimId, surface)
|
|
208
|
+
const accepted = exception !== undefined
|
|
209
|
+
|
|
210
|
+
if (!here.length) {
|
|
211
|
+
if (!claim.required.includes(surface)) continue
|
|
212
|
+
findings.push({
|
|
213
|
+
id: findingId({ code: 'PARITY_MISSING', claimId: claim.claimId, surface }),
|
|
214
|
+
code: 'PARITY_MISSING',
|
|
215
|
+
claimId: claim.claimId,
|
|
216
|
+
owner: claim.owner,
|
|
217
|
+
severity: accepted ? 'info' : claim.severity,
|
|
218
|
+
blocking: !accepted && claim.severity === 'error',
|
|
219
|
+
surface,
|
|
220
|
+
canonical: resolved.value,
|
|
221
|
+
message: `${surface} does not state ${claim.statement.toLowerCase()}`,
|
|
222
|
+
remediation: `Add "${renderClaim(claim, resolved.value, surface)}" to ${surface}, or record an exception with a reason.`,
|
|
223
|
+
...(accepted ? { acceptedReason: exception.reason } : {}),
|
|
224
|
+
})
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
for (const occurrence of here) {
|
|
229
|
+
if (occurrence.stated === resolved.value) {
|
|
230
|
+
matched += 1
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
findings.push({
|
|
234
|
+
id: findingId({ code: 'PARITY_STALE', claimId: claim.claimId, surface, line: occurrence.line }),
|
|
235
|
+
code: 'PARITY_STALE',
|
|
236
|
+
claimId: claim.claimId,
|
|
237
|
+
owner: claim.owner,
|
|
238
|
+
severity: accepted ? 'info' : claim.severity,
|
|
239
|
+
blocking: !accepted && claim.severity === 'error',
|
|
240
|
+
surface,
|
|
241
|
+
line: occurrence.line,
|
|
242
|
+
stated: occurrence.stated,
|
|
243
|
+
canonical: resolved.value,
|
|
244
|
+
message: `${surface}:${occurrence.line} states ${occurrence.stated} where the repository says ${resolved.value}.`,
|
|
245
|
+
remediation: claim.remediation,
|
|
246
|
+
excerpt: occurrence.excerpt,
|
|
247
|
+
...(accepted ? { acceptedReason: exception.reason } : {}),
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
for (const surface of missingSurfaces) {
|
|
254
|
+
limitations.push(`${surface} is named by the registry but absent from this checkout.`)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
findings.sort((a, b) => a.claimId.localeCompare(b.claimId) || a.code.localeCompare(b.code) || (a.surface ?? '').localeCompare(b.surface ?? '') || (a.line ?? 0) - (b.line ?? 0))
|
|
258
|
+
|
|
259
|
+
const count = (code: ParityCode): number => findings.filter((finding) => finding.code === code).length
|
|
260
|
+
const blocking = findings.filter((finding) => finding.blocking).length
|
|
261
|
+
const payload = {
|
|
262
|
+
type: 'public-parity-report' as const,
|
|
263
|
+
schemaVersion: PUBLIC_PARITY_SCHEMA_VERSION,
|
|
264
|
+
contentHashAlgo: 'sha256-normalized-v1' as const,
|
|
265
|
+
project: { name: options.project.name },
|
|
266
|
+
sourceRevision: options.sourceRevision,
|
|
267
|
+
sourceRevisionKind: options.sourceRevisionKind,
|
|
268
|
+
registryVersion: registry.registryVersion,
|
|
269
|
+
registryHash: registry.contentHash,
|
|
270
|
+
status: blocking ? ('blocked' as const) : findings.length ? ('needs-review' as const) : ('pass' as const),
|
|
271
|
+
metrics: {
|
|
272
|
+
claims: registry.claims.length,
|
|
273
|
+
surfaces: surfaces.length - missingSurfaces.length,
|
|
274
|
+
matched,
|
|
275
|
+
stale: count('PARITY_STALE'),
|
|
276
|
+
missing: count('PARITY_MISSING'),
|
|
277
|
+
contradictions: count('PARITY_CONTRADICTION'),
|
|
278
|
+
notAnalyzed: count('PARITY_NOT_ANALYZED'),
|
|
279
|
+
accepted: findings.filter((finding) => finding.acceptedReason !== undefined).length,
|
|
280
|
+
blocking,
|
|
281
|
+
},
|
|
282
|
+
findings,
|
|
283
|
+
missingSurfaces,
|
|
284
|
+
limitations: [...limitations].sort(),
|
|
285
|
+
}
|
|
286
|
+
return PublicParityReportV1Schema.parse({ ...payload, contentHash: sha256NormalizedV1(payload) })
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export const parsePublicParityReport = (input: unknown): PublicParityReportV1 => {
|
|
290
|
+
const report = PublicParityReportV1Schema.parse(input)
|
|
291
|
+
if (contentHashForArtifactV1(report) !== report.contentHash) throw new Error('Invalid public-parity report content hash.')
|
|
292
|
+
return report
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export const formatPublicParityText = (report: PublicParityReportV1): readonly string[] => [
|
|
296
|
+
`Public parity: ${report.status} (registry ${report.registryVersion}, ${report.metrics.claims} claim(s) over ${report.metrics.surfaces} surface(s))`,
|
|
297
|
+
`Matched: ${report.metrics.matched} Stale: ${report.metrics.stale} Missing: ${report.metrics.missing} Contradictions: ${report.metrics.contradictions} Not analyzed: ${report.metrics.notAnalyzed} Accepted: ${report.metrics.accepted}`,
|
|
298
|
+
...report.findings.flatMap((finding) => {
|
|
299
|
+
const at = finding.surface ? ` ${finding.surface}${finding.line ? `:${finding.line}` : ''}` : ''
|
|
300
|
+
const mark = finding.blocking ? 'BLOCKING' : finding.acceptedReason ? 'accepted' : finding.severity
|
|
301
|
+
return [
|
|
302
|
+
` [${mark}] ${finding.code}${at} (${finding.claimId}, owner ${finding.owner}): ${finding.message}`,
|
|
303
|
+
` → ${finding.remediation}`,
|
|
304
|
+
...(finding.acceptedReason ? [` accepted: ${finding.acceptedReason}`] : []),
|
|
305
|
+
]
|
|
306
|
+
}),
|
|
307
|
+
...report.limitations.map((limitation) => ` limitation: ${limitation}`),
|
|
308
|
+
`Content hash: ${report.contentHash}`,
|
|
309
|
+
]
|