@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.
- package/CHANGELOG.md +488 -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 +15871 -6218
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +95 -6
- package/dist/config/index.js.map +1 -1
- package/dist/{index-BUL0q7s8.d.ts → index-7wYGbllW.d.ts} +134 -1
- package/dist/index.d.ts +9880 -1062
- package/dist/index.js +16143 -6127
- 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 +157 -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 +34 -15
- 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 +126 -2
- package/src/conformance/documentation-standard-v1.ts +8 -6
- package/src/discovery/areas.ts +197 -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 +485 -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/cli/program.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { scanHumanDocRecords } from '../index-builder/human-adapters/index.js'
|
|
|
17
17
|
import { retrieveHybridChunks } from '../federation/llms.js'
|
|
18
18
|
import { runGates, type GateId } from '../gates/run-gates.js'
|
|
19
19
|
import { evaluateRules, parseRuleId, parseRuleSeverity, type RuleMode } from '../rules/engine.js'
|
|
20
|
+
import { centrality } from '../graph/build.js'
|
|
20
21
|
import { runChatOnce, startInkChat } from '../intelligence/chat.js'
|
|
21
22
|
import { PeerMissingError, layer1InstallHint } from '../intelligence/peers.js'
|
|
22
23
|
import { createDocBridgeRag } from '../intelligence/rag.js'
|
|
@@ -32,23 +33,72 @@ import {
|
|
|
32
33
|
} from '../doctor/badge.js'
|
|
33
34
|
import { docBridgePatternMarkdown, docBridgePatternPayload } from '../playbook/doc-bridge-pattern.js'
|
|
34
35
|
import { formatDemoText, runDemo, withDemoWorkspace, type DemoFixture } from './demo.js'
|
|
35
|
-
import { formatDoctorText, runDoctor } from '../doctor/run-doctor.js'
|
|
36
|
+
import { DEFAULT_RETRIEVAL_SUITE, formatDoctorText, runDoctor } from '../doctor/run-doctor.js'
|
|
36
37
|
import { installMcpConfig, mcpSnippet } from '../mcp/install.js'
|
|
37
38
|
import { startMcpStdioServer } from '../mcp/server.js'
|
|
38
|
-
import {
|
|
39
|
+
import { measureOverlayRetrievalDelta, formatOverlayRetrievalDeltaText } from '../bench/overlay-delta.js'
|
|
40
|
+
import { enrichmentCost } from '../enrich/stats.js'
|
|
41
|
+
import { checkStudyExpectations, formatStudyExpectationsText, parseStudyExpectations } from '../study/expectations.js'
|
|
42
|
+
import { IndexNotFoundError, loadFreshDocBridgeIndex } from '../query/load-index.js'
|
|
39
43
|
import { runQuery, type QueryKind } from '../query/query.js'
|
|
40
44
|
import { searchIndex } from '../query/search.js'
|
|
41
45
|
import type { DocBridgeIndexV1 } from '../schemas/doc-bridge-index.js'
|
|
42
|
-
import { parseAgentHandoff, parseDocBridgeConfig, parseReconciliationReport } from '../validate.js'
|
|
46
|
+
import { parseAgentHandoff, parseDocBridgeConfig, parseDocBridgeIndex, parseReconciliationReport } from '../validate.js'
|
|
43
47
|
import { parseDiscoverySnapshot } from '../validate.js'
|
|
48
|
+
import { findingsFromDiagnostics } from '../findings/report.js'
|
|
49
|
+
import {
|
|
50
|
+
formatRetrievalBenchText,
|
|
51
|
+
parseRetrievalSuite,
|
|
52
|
+
runRetrievalBench,
|
|
53
|
+
type RetrievalBenchResultV1,
|
|
54
|
+
} from '../bench/retrieval.js'
|
|
55
|
+
import {
|
|
56
|
+
compareRetrievalBaseline,
|
|
57
|
+
createRetrievalBaseline,
|
|
58
|
+
formatRetrievalComparisonText,
|
|
59
|
+
parseRetrievalBaseline,
|
|
60
|
+
} from '../bench/baseline.js'
|
|
44
61
|
import { reconcileKnowledge } from '../reconciliation/reconcile.js'
|
|
45
62
|
import type { DiscoverySnapshotV1, ReconciliationReportV1 } from '../schemas/knowledge.js'
|
|
46
63
|
import { sha256NormalizedV1 } from '../index-builder/content-hash.js'
|
|
47
64
|
import { applyFixProposal, approveFixProposal, createArtifactNormalizationProposal, createMarkdownLinkFixProposal } from '../fixes/proposals.js'
|
|
48
65
|
import { createRegistryAgentAdapter, loadRegistryAgentRunner, persistRegistryAgentProposal } from '../agents/registry-adapter.js'
|
|
66
|
+
import { fixApprovalId, recordApproval, FIX_APPROVAL_GATE } from '../enrich/approvals.js'
|
|
67
|
+
import { readEnrichmentOverlay, withAcceptedRelations } from '../enrich/overlay.js'
|
|
68
|
+
import { decideEnrichment, listEnrichment } from '../enrich/review.js'
|
|
69
|
+
import { formatEnrichmentText, runEnrichment, type EnrichmentRunResult } from '../enrich/stage.js'
|
|
49
70
|
import { renderOfflineReportArtifact } from '../report/html.js'
|
|
50
71
|
import { benchmarkFixture, formatBenchmarkText, measureBenchmark } from '../metrics/benchmark.js'
|
|
51
72
|
import { PACKAGE_VERSION } from '../version.js'
|
|
73
|
+
import { auditDocumentation, formatDocumentationAuditText } from '../audit/documentation.js'
|
|
74
|
+
import { renderArtifact, writeRenderedPages } from '../render/render.js'
|
|
75
|
+
import { CLI_COMMAND_USAGE } from './usage.js'
|
|
76
|
+
import { checkPublicParity, formatPublicParityText } from '../parity/check.js'
|
|
77
|
+
import { parsePublicClaims } from '../parity/claims.js'
|
|
78
|
+
import { resolveTemplateSource } from '../render/template-source.js'
|
|
79
|
+
import { RENDER_TEMPLATE_NAMES, RENDER_TEMPLATES, isRenderTemplateName } from '../render/templates.js'
|
|
80
|
+
import {
|
|
81
|
+
formatHistoricalEvidenceText,
|
|
82
|
+
formatStudyProtocolText,
|
|
83
|
+
parseHistoricalEvidenceRegistry,
|
|
84
|
+
parseStudyProtocol,
|
|
85
|
+
validateHistoricalEvidenceRegistry,
|
|
86
|
+
} from '../study/protocol.js'
|
|
87
|
+
import {
|
|
88
|
+
formatStudyTaskSuiteText,
|
|
89
|
+
parseStudyTaskSuite,
|
|
90
|
+
selectTaskExecutions,
|
|
91
|
+
} from '../study/task-suite.js'
|
|
92
|
+
import {
|
|
93
|
+
formatControlledStudyRunPlanText,
|
|
94
|
+
parseControlledStudyLedger,
|
|
95
|
+
parseControlledStudyRunPlan,
|
|
96
|
+
} from '../study/runner.js'
|
|
97
|
+
import { formatControlledStudyRunText, parseStudyRepositoryConfig, runControlledStudy } from '../study/execution.js'
|
|
98
|
+
import { independentlyAdjudicateStudyLedger, persistIndependentlyAdjudicatedLedger } from '../study/adjudication.js'
|
|
99
|
+
import { formatStudyProviderCliText, parseStudyProviderCliConfig } from '../study/provider-cli.js'
|
|
100
|
+
import { calculateStudyMetrics, formatStudyMetricsText } from '../study/metrics.js'
|
|
101
|
+
import { formatStudyVerificationText, parseStudyVerificationBinding } from '../study/verification.js'
|
|
52
102
|
|
|
53
103
|
type Command =
|
|
54
104
|
| 'help'
|
|
@@ -62,12 +112,15 @@ type Command =
|
|
|
62
112
|
| 'registry'
|
|
63
113
|
| 'discover'
|
|
64
114
|
| 'benchmark'
|
|
115
|
+
| 'bench'
|
|
116
|
+
| 'study'
|
|
65
117
|
| 'scan'
|
|
66
118
|
| 'reconcile'
|
|
67
119
|
| 'check'
|
|
68
120
|
| 'map'
|
|
69
121
|
| 'fix'
|
|
70
122
|
| 'suggest'
|
|
123
|
+
| 'enrich'
|
|
71
124
|
| 'index'
|
|
72
125
|
| 'gate'
|
|
73
126
|
| 'rules'
|
|
@@ -82,48 +135,11 @@ type Command =
|
|
|
82
135
|
| 'rag'
|
|
83
136
|
| 'list'
|
|
84
137
|
| 'conformance'
|
|
138
|
+
| 'audit'
|
|
139
|
+
| 'parity'
|
|
140
|
+
| 'render'
|
|
85
141
|
|
|
86
|
-
const usage =
|
|
87
|
-
|
|
88
|
-
Core (no API key):
|
|
89
|
-
ak-docs init [--demo] [--scaffold-workspaces]
|
|
90
|
-
ak-docs demo [--fixture example|monorepo] [--text] [--in-project]
|
|
91
|
-
ak-docs doctor [--text] [--badge] [--write-badge]
|
|
92
|
-
ak-docs index [--watch]
|
|
93
|
-
ak-docs discover [--text|--json]
|
|
94
|
-
ak-docs benchmark <fixture.json> <observation.json> [--text|--json]
|
|
95
|
-
ak-docs scan | reconcile | check | map [--text|--json] [--html] [--report-threshold <bytes>]
|
|
96
|
-
ak-docs fix propose links|normalize <artifact> [--output <file>]
|
|
97
|
-
ak-docs fix approve|apply <proposal.json> [--by <name>]
|
|
98
|
-
ak-docs suggest [--json|--text] run the configured local Registry agent
|
|
99
|
-
ak-docs query [package|ownership|intent|change] <id> [--agent] [--text]
|
|
100
|
-
ak-docs search <term> [--agent] [--text]
|
|
101
|
-
ak-docs list <packages|intents|changes|knowledge> [--text]
|
|
102
|
-
ak-docs ask [question] local consult (no LLM)
|
|
103
|
-
ak-docs gate run [gate-id]
|
|
104
|
-
ak-docs rules run <report.json> [--preset default|recommended|strict] [--severity rule=level] [--ignore rule]
|
|
105
|
-
ak-docs conformance run documentation-standard-v1 [--text|--json]
|
|
106
|
-
ak-docs mcp
|
|
107
|
-
ak-docs mcp install --cursor | --claude
|
|
108
|
-
ak-docs memory ingest|classify|promote [--pr] [--dry-run]
|
|
109
|
-
ak-docs bootstrap agent-docs
|
|
110
|
-
ak-docs validate-config | validate-handoff <file>
|
|
111
|
-
|
|
112
|
-
Intelligence (optional AgentsKit peers):
|
|
113
|
-
ak-docs rag ingest|search <query>
|
|
114
|
-
ak-docs chat terminal chat (Ink + RAG)
|
|
115
|
-
ak-docs ask <question> --chat one-shot grounded answer
|
|
116
|
-
|
|
117
|
-
Advanced / ecosystem:
|
|
118
|
-
ak-docs retrieve <query>
|
|
119
|
-
ak-docs registry topology
|
|
120
|
-
ak-docs playbook draft | pattern [--text]
|
|
121
|
-
|
|
122
|
-
Global flags:
|
|
123
|
-
-h, --help --version
|
|
124
|
-
--config <path> (project root = config file directory)
|
|
125
|
-
--agent --json --text --chat --demo
|
|
126
|
-
`
|
|
142
|
+
const usage = CLI_COMMAND_USAGE
|
|
127
143
|
|
|
128
144
|
const QUERY_KINDS = new Set<QueryKind>(['package', 'ownership', 'intent', 'change', 'search'])
|
|
129
145
|
const LIST_KINDS = new Set(['packages', 'intents', 'changes', 'knowledge'])
|
|
@@ -148,6 +164,12 @@ const parseArgs = (argv: readonly string[]) => {
|
|
|
148
164
|
i += 1
|
|
149
165
|
continue
|
|
150
166
|
}
|
|
167
|
+
if (arg === '--mode' || arg === '--context-budget') {
|
|
168
|
+
const value = argv[i + 1]
|
|
169
|
+
if (value !== undefined && !value.startsWith('-')) flags.add(`${arg}=${value}`)
|
|
170
|
+
i += 1
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
151
173
|
if (arg.startsWith('-')) {
|
|
152
174
|
flags.add(arg)
|
|
153
175
|
continue
|
|
@@ -167,12 +189,15 @@ const parseArgs = (argv: readonly string[]) => {
|
|
|
167
189
|
else if (positional[0] === 'registry') command = 'registry'
|
|
168
190
|
else if (positional[0] === 'discover') command = 'discover'
|
|
169
191
|
else if (positional[0] === 'benchmark') command = 'benchmark'
|
|
192
|
+
else if (positional[0] === 'bench') command = 'bench'
|
|
193
|
+
else if (positional[0] === 'study') command = 'study'
|
|
170
194
|
else if (positional[0] === 'scan') command = 'scan'
|
|
171
195
|
else if (positional[0] === 'reconcile') command = 'reconcile'
|
|
172
196
|
else if (positional[0] === 'check') command = 'check'
|
|
173
197
|
else if (positional[0] === 'map') command = 'map'
|
|
174
198
|
else if (positional[0] === 'fix') command = 'fix'
|
|
175
199
|
else if (positional[0] === 'suggest') command = 'suggest'
|
|
200
|
+
else if (positional[0] === 'enrich') command = 'enrich'
|
|
176
201
|
else if (positional[0] === 'index') command = 'index'
|
|
177
202
|
else if (positional[0] === 'gate') command = 'gate'
|
|
178
203
|
else if (positional[0] === 'rules') command = 'rules'
|
|
@@ -187,6 +212,9 @@ const parseArgs = (argv: readonly string[]) => {
|
|
|
187
212
|
else if (positional[0] === 'rag') command = 'rag'
|
|
188
213
|
else if (positional[0] === 'list') command = 'list'
|
|
189
214
|
else if (positional[0] === 'conformance') command = 'conformance'
|
|
215
|
+
else if (positional[0] === 'audit') command = 'audit'
|
|
216
|
+
else if (positional[0] === 'parity') command = 'parity'
|
|
217
|
+
else if (positional[0] === 'render') command = 'render'
|
|
190
218
|
|
|
191
219
|
return { command, flags, configPath, positional }
|
|
192
220
|
}
|
|
@@ -219,6 +247,10 @@ const writeJson = (payload: unknown): void => {
|
|
|
219
247
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`)
|
|
220
248
|
}
|
|
221
249
|
|
|
250
|
+
const writeAgentJson = (payload: unknown): void => {
|
|
251
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`)
|
|
252
|
+
}
|
|
253
|
+
|
|
222
254
|
const writeLines = (lines: readonly string[]): void => {
|
|
223
255
|
process.stdout.write(lines.length ? `${lines.join('\n')}\n` : '')
|
|
224
256
|
}
|
|
@@ -270,10 +302,23 @@ const formatSearchMatch = (match: {
|
|
|
270
302
|
readonly path: string
|
|
271
303
|
readonly summary?: string
|
|
272
304
|
readonly score?: number
|
|
305
|
+
readonly confidence?: string
|
|
306
|
+
readonly explain?: { readonly matched: Readonly<Record<string, readonly string[]>>; readonly components: Readonly<Record<string, number>>; readonly surfacedBy?: { readonly kind: string; readonly id: string } }
|
|
273
307
|
}): string => {
|
|
274
308
|
const summary = match.summary ? match.summary.replace(/\s+/g, ' ').slice(0, 100) : ''
|
|
275
309
|
const score = typeof match.score === 'number' ? ` score=${match.score}` : ''
|
|
276
|
-
|
|
310
|
+
const confidence = match.confidence ? ` confidence=${match.confidence}` : ''
|
|
311
|
+
const lines = [` [${match.type}] ${match.id}${score}${confidence}`, ` ${match.path}`, ...(summary ? [` ${summary}`] : [])]
|
|
312
|
+
if (match.explain) {
|
|
313
|
+
// Every component with a contribution, so a wrong ranking is reportable with numbers.
|
|
314
|
+
const parts = Object.entries(match.explain.components).filter(([name, value]) => value !== 0 && name !== 'prior').map(([name, value]) => `${name}=${value}`)
|
|
315
|
+
if (match.explain.components.prior !== undefined && match.explain.components.prior !== 1) parts.push(`prior=×${match.explain.components.prior}`)
|
|
316
|
+
const matched = Object.entries(match.explain.matched).map(([field, terms]) => `${field}: ${terms.join(', ')}`)
|
|
317
|
+
lines.push(` why: ${parts.join(' ') || 'no scoring component'}`)
|
|
318
|
+
if (matched.length) lines.push(` matched: ${matched.join(' | ')}`)
|
|
319
|
+
if (match.explain.surfacedBy) lines.push(` via: ${match.explain.surfacedBy.kind} from ${match.explain.surfacedBy.id}`)
|
|
320
|
+
}
|
|
321
|
+
return lines.join('\n')
|
|
277
322
|
}
|
|
278
323
|
|
|
279
324
|
const writeTextSearch = (
|
|
@@ -367,7 +412,7 @@ const writeAsk = (
|
|
|
367
412
|
}
|
|
368
413
|
|
|
369
414
|
const readIndexedDoc = (root: string, config: DocBridgeConfigV1, idOrPath: string): string => {
|
|
370
|
-
const index =
|
|
415
|
+
const index = loadFreshDocBridgeIndex(root, config)
|
|
371
416
|
const entry = index.knowledge.find((doc) => doc.id === idOrPath || doc.path === idOrPath)
|
|
372
417
|
if (!entry) throw new Error(`Unknown indexed doc "${idOrPath}". Try: search ${idOrPath}`)
|
|
373
418
|
|
|
@@ -380,7 +425,7 @@ const readIndexedDoc = (root: string, config: DocBridgeConfigV1, idOrPath: strin
|
|
|
380
425
|
}
|
|
381
426
|
|
|
382
427
|
const runAskRepl = async (root: string, config: DocBridgeConfigV1): Promise<number> => {
|
|
383
|
-
const index =
|
|
428
|
+
const index = loadFreshDocBridgeIndex(root, config)
|
|
384
429
|
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true })
|
|
385
430
|
try {
|
|
386
431
|
for (;;) {
|
|
@@ -465,7 +510,7 @@ const workflowOptions = (
|
|
|
465
510
|
root: string,
|
|
466
511
|
config: DocBridgeConfigV1,
|
|
467
512
|
sourceRevision: string,
|
|
468
|
-
stage: 'collect' | 'normalize' | 'reconcile' | 'evaluate' | 'report',
|
|
513
|
+
stage: 'collect' | 'normalize' | 'reconcile' | 'enrich' | 'evaluate' | 'report',
|
|
469
514
|
handlers: Parameters<typeof runWorkflow>[0]['handlers'],
|
|
470
515
|
versions?: Pick<Parameters<typeof runWorkflow>[0], 'pipelineVersion' | 'analyzerVersions'>,
|
|
471
516
|
): Parameters<typeof runWorkflow>[0] => ({
|
|
@@ -491,6 +536,12 @@ const documentationInputs = (root: string, snapshot: DiscoverySnapshotV1) => sna
|
|
|
491
536
|
.filter((entity) => entity.kind === 'document' && entity.path)
|
|
492
537
|
.map((entity) => ({ path: entity.path as string, content: readFileSync(resolve(root, entity.path as string), 'utf8') }))
|
|
493
538
|
|
|
539
|
+
/** Ownership records as reconciliation sees them, so a path matching nothing gets reported. */
|
|
540
|
+
const ownershipOptions = (config: DocBridgeConfigV1): { readonly ownership?: readonly { readonly id: string; readonly path: string }[] } => {
|
|
541
|
+
const ownership = Object.entries(config.routing?.options?.ownership ?? {}).map(([id, record]) => ({ id, path: record.path }))
|
|
542
|
+
return ownership.length ? { ownership } : {}
|
|
543
|
+
}
|
|
544
|
+
|
|
494
545
|
const reconcileWorkflow = (root: string, config: DocBridgeConfigV1): WorkflowExecutionResult => {
|
|
495
546
|
const scanned = scanWorkflow(root, config)
|
|
496
547
|
const snapshot = parseDiscoverySnapshot(loadWorkflowStepOutput(scanned.stateDir, 'normalize'))
|
|
@@ -502,15 +553,51 @@ const reconcileWorkflow = (root: string, config: DocBridgeConfigV1): WorkflowExe
|
|
|
502
553
|
...(config.reconciliation?.requiredRelationKinds === undefined ? {} : { requiredRelationKinds: config.reconciliation.requiredRelationKinds }),
|
|
503
554
|
...(config.reconciliation?.requiredRelationTargets === undefined ? {} : { requiredRelationTargets: config.reconciliation.requiredRelationTargets }),
|
|
504
555
|
...(config.reconciliation?.includeOrphanedDocuments === undefined ? {} : { includeOrphanedDocuments: config.reconciliation.includeOrphanedDocuments }),
|
|
556
|
+
...ownershipOptions(config),
|
|
505
557
|
})
|
|
506
558
|
return runWorkflow(workflowOptions(root, config, snapshot.sourceRevision, 'reconcile', { reconcile: () => report }, { pipelineVersion: snapshot.pipelineVersion, analyzerVersions: snapshot.analyzerVersions }))
|
|
507
559
|
}
|
|
508
560
|
|
|
509
|
-
|
|
510
|
-
|
|
561
|
+
/**
|
|
562
|
+
* Record the overlay as the enrich step of the current run.
|
|
563
|
+
*
|
|
564
|
+
* The step's input names the overlay it attaches, not only the report it was made from: a person
|
|
565
|
+
* approving a proposal changes the overlay without changing the report, and the engine refuses a
|
|
566
|
+
* step whose input has not moved but whose output has. Keying on the overlay's own hash keeps that
|
|
567
|
+
* rule honest — same overlay, same artifact; a new overlay, a new one.
|
|
568
|
+
*/
|
|
569
|
+
const attachEnrichmentStage = (root: string, config: DocBridgeConfigV1, report: ReconciliationReportV1, result: EnrichmentRunResult): WorkflowExecutionResult =>
|
|
570
|
+
runWorkflow({
|
|
571
|
+
...workflowOptions(root, config, report.sourceRevision, 'enrich', { enrich: () => result.overlay }, { pipelineVersion: report.pipelineVersion, analyzerVersions: report.analyzerVersions }),
|
|
572
|
+
inputs: { enrich: { reportHash: report.contentHash, overlayHash: result.overlay.contentHash } },
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* The enrich stage, run only on request. Its failure is reported, never propagated: a `check`
|
|
577
|
+
* that asked for enrichment and did not get it is still a `check`, with the same result.
|
|
578
|
+
*/
|
|
579
|
+
const enrichWorkflow = async (root: string, config: DocBridgeConfigV1, reconciled: WorkflowExecutionResult): Promise<{ readonly status: 'ok' | 'failed'; readonly result?: EnrichmentRunResult; readonly error?: string }> => {
|
|
580
|
+
try {
|
|
581
|
+
const snapshot = parseDiscoverySnapshot(loadWorkflowStepOutput(reconciled.stateDir, 'normalize'))
|
|
582
|
+
const report = parseReconciliationReport(loadWorkflowStepOutput(reconciled.stateDir, 'reconcile'))
|
|
583
|
+
const result = await runEnrichment({ root, config, snapshot, report })
|
|
584
|
+
attachEnrichmentStage(root, config, report, result)
|
|
585
|
+
return { status: 'ok', result }
|
|
586
|
+
} catch (error) {
|
|
587
|
+
return { status: 'failed', error: error instanceof Error ? error.message : String(error) }
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const checkWorkflow = (root: string, config: DocBridgeConfigV1, reconciled = reconcileWorkflow(root, config)): WorkflowExecutionResult => {
|
|
511
592
|
const report = parseReconciliationReport(loadWorkflowStepOutput(reconciled.stateDir, 'reconcile'))
|
|
512
593
|
const versions = { pipelineVersion: report.pipelineVersion, analyzerVersions: report.analyzerVersions }
|
|
513
|
-
|
|
594
|
+
// `centrality-risk` needs betweenness over the import graph, which only the snapshot can give.
|
|
595
|
+
const snapshot = parseDiscoverySnapshot(loadWorkflowStepOutput(reconciled.stateDir, 'normalize'))
|
|
596
|
+
const evaluate = () => evaluateRules(report, {
|
|
597
|
+
...(config.rules ? { config: config.rules } : {}),
|
|
598
|
+
centrality: centrality(snapshot),
|
|
599
|
+
})
|
|
600
|
+
runWorkflow(workflowOptions(root, config, report.sourceRevision, 'evaluate', { evaluate }, versions))
|
|
514
601
|
return runWorkflow(workflowOptions(root, config, report.sourceRevision, 'report', { report: ({ input }) => input }, versions))
|
|
515
602
|
}
|
|
516
603
|
|
|
@@ -592,14 +679,85 @@ const runWorkflowCommand = (
|
|
|
592
679
|
flags: ReadonlySet<string>,
|
|
593
680
|
configPath: string | undefined,
|
|
594
681
|
argv: readonly string[],
|
|
595
|
-
): number => {
|
|
682
|
+
): number | Promise<number> => {
|
|
683
|
+
/*
|
|
684
|
+
* Validated before anything runs, and for every path: `--enrich` returns early, and a run that
|
|
685
|
+
* ends up printing findings must have been allowed to ask for them.
|
|
686
|
+
*/
|
|
687
|
+
const format = optionValues(argv, '--format')[0]
|
|
688
|
+
if (format !== undefined && format !== 'json' && format !== 'finding') {
|
|
689
|
+
process.stderr.write('--format must be json or finding.\n')
|
|
690
|
+
return 2
|
|
691
|
+
}
|
|
692
|
+
if (format === 'finding' && command !== 'check') {
|
|
693
|
+
process.stderr.write('--format finding is only available for ak-docs check.\n')
|
|
694
|
+
return 2
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (command === 'check' && flags.has('--enrich')) {
|
|
698
|
+
return (async () => {
|
|
699
|
+
try {
|
|
700
|
+
const { config, root } = loadProject(configPath)
|
|
701
|
+
const reconciled = reconcileWorkflow(root, config)
|
|
702
|
+
const enrichment = await enrichWorkflow(root, config, reconciled)
|
|
703
|
+
return finishWorkflowCommand(command, flags, argv, config, root, checkWorkflow(root, config, reconciled), {
|
|
704
|
+
enrichment: enrichment.status === 'ok' && enrichment.result
|
|
705
|
+
? { status: 'ok', overlayHash: enrichment.result.overlay.contentHash, agentCalls: enrichment.result.agentCalls, cacheHits: enrichment.result.cacheHits, accepted: enrichment.result.overlay.accepted.length, pending: enrichment.result.overlay.pending.length, rejected: enrichment.result.overlay.rejected.length }
|
|
706
|
+
: { status: 'failed', error: enrichment.error },
|
|
707
|
+
})
|
|
708
|
+
} catch (error) {
|
|
709
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
710
|
+
return 2
|
|
711
|
+
}
|
|
712
|
+
})()
|
|
713
|
+
}
|
|
596
714
|
try {
|
|
597
715
|
const { config, root } = loadProject(configPath)
|
|
598
716
|
const result = command === 'scan' ? scanWorkflow(root, config) : command === 'reconcile' ? reconcileWorkflow(root, config) : checkWorkflow(root, config)
|
|
599
|
-
|
|
717
|
+
return finishWorkflowCommand(command, flags, argv, config, root, result, {})
|
|
718
|
+
} catch (error) {
|
|
719
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
720
|
+
return 2
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const finishWorkflowCommand = (
|
|
725
|
+
command: 'scan' | 'reconcile' | 'check' | 'map',
|
|
726
|
+
flags: ReadonlySet<string>,
|
|
727
|
+
argv: readonly string[],
|
|
728
|
+
config: DocBridgeConfigV1,
|
|
729
|
+
root: string,
|
|
730
|
+
result: WorkflowExecutionResult,
|
|
731
|
+
extra: Record<string, unknown>,
|
|
732
|
+
): number => {
|
|
733
|
+
try {
|
|
734
|
+
const output = { ...workflowOutput(result), ...extra }
|
|
600
735
|
if (command === 'map') output.kind = 'architecture-map'
|
|
736
|
+
// Read from argv rather than taken as an argument: every caller already passes argv, and the
|
|
737
|
+
// flag is validated where the command is dispatched.
|
|
738
|
+
if (optionValues(argv, '--format')[0] === 'finding') {
|
|
739
|
+
/*
|
|
740
|
+
* The canonical shape the ecosystem consumes: every reconciliation diagnostic as a `Finding`
|
|
741
|
+
* with a severity from `SEVERITY_ORDER`. The rule verdict still decides the exit code, so a
|
|
742
|
+
* dashboard reading findings and a CI job reading the exit code agree on what failed.
|
|
743
|
+
*/
|
|
744
|
+
const diagnostics = Array.isArray(output.diagnostics) ? (output.diagnostics as Parameters<typeof findingsFromDiagnostics>[0]) : []
|
|
745
|
+
writeJson({
|
|
746
|
+
ok: output.ok,
|
|
747
|
+
runId: output.runId,
|
|
748
|
+
state: output.state,
|
|
749
|
+
...(output.snapshotHash ? { snapshotHash: output.snapshotHash } : {}),
|
|
750
|
+
...(output.reportHash ? { reportHash: output.reportHash } : {}),
|
|
751
|
+
format: 'finding',
|
|
752
|
+
findings: findingsFromDiagnostics(diagnostics),
|
|
753
|
+
})
|
|
754
|
+
const exitCode = output.rules && typeof output.rules === 'object' && 'exitCode' in output.rules && (output.rules as { exitCode?: unknown }).exitCode === 1 ? 1 : 0
|
|
755
|
+
return result.run.state === 'failed' ? 1 : exitCode
|
|
756
|
+
}
|
|
601
757
|
if (command === 'map' && flags.has('--html')) {
|
|
602
|
-
const
|
|
758
|
+
const observed = parseDiscoverySnapshot(loadWorkflowStepOutput(result.stateDir, 'normalize'))
|
|
759
|
+
// Accepted proposed relations render as dashed edges; nothing observed is removed or redrawn.
|
|
760
|
+
const snapshot = config.intelligence?.registry?.enabled ? withAcceptedRelations(observed, readEnrichmentOverlay(root)) : observed
|
|
603
761
|
const report = parseReconciliationReport(loadWorkflowStepOutput(result.stateDir, 'reconcile'))
|
|
604
762
|
const outputPath = optionValues(argv, '--output')[0] ?? '.doc-bridge/report.html'
|
|
605
763
|
const htmlPath = resolve(root, outputPath)
|
|
@@ -622,6 +780,187 @@ const runWorkflowCommand = (
|
|
|
622
780
|
}
|
|
623
781
|
}
|
|
624
782
|
|
|
783
|
+
const buildDocumentationAuditReport = (root: string, config: DocBridgeConfigV1) => {
|
|
784
|
+
const scanned = scanWorkflow(root, config)
|
|
785
|
+
const snapshot = parseDiscoverySnapshot(loadWorkflowStepOutput(scanned.stateDir, 'normalize'))
|
|
786
|
+
const analysis = applyDocumentationDeclarations(snapshot, documentationInputs(root, snapshot), { agentRoot: config.corpus.agent.root })
|
|
787
|
+
const reconciliation = reconcileKnowledge(snapshot, analysis.snapshot, {
|
|
788
|
+
...(config.reconciliation?.scope === undefined ? {} : { scope: config.reconciliation.scope }),
|
|
789
|
+
...(config.reconciliation?.requiredRelationKinds === undefined ? {} : { requiredRelationKinds: config.reconciliation.requiredRelationKinds }),
|
|
790
|
+
...(config.reconciliation?.requiredRelationTargets === undefined ? {} : { requiredRelationTargets: config.reconciliation.requiredRelationTargets }),
|
|
791
|
+
includeOrphanedDocuments: config.reconciliation?.includeOrphanedDocuments ?? true,
|
|
792
|
+
...ownershipOptions(config),
|
|
793
|
+
})
|
|
794
|
+
return auditDocumentation({
|
|
795
|
+
root,
|
|
796
|
+
snapshot,
|
|
797
|
+
declared: analysis.snapshot,
|
|
798
|
+
reconciliation,
|
|
799
|
+
declarationDiagnostics: analysis.diagnostics,
|
|
800
|
+
...(config.audit?.documentation ? { config: config.audit.documentation } : {}),
|
|
801
|
+
})
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const BENCH_USAGE = [
|
|
805
|
+
'Usage: ak-docs bench retrieval <suite.json> [--index <file>] [--baseline <file>] [--limit <n>] [--text|--json]',
|
|
806
|
+
' ak-docs bench retrieval <suite.json> --baseline <file> --update-baseline --by <name> [--reason <text>]',
|
|
807
|
+
].join('\n')
|
|
808
|
+
|
|
809
|
+
const runBenchCommand = (
|
|
810
|
+
flags: ReadonlySet<string>,
|
|
811
|
+
positional: readonly string[],
|
|
812
|
+
configPath: string | undefined,
|
|
813
|
+
argv: readonly string[],
|
|
814
|
+
): number => {
|
|
815
|
+
if (positional[1] !== 'retrieval' || !positional[2]) {
|
|
816
|
+
process.stderr.write(`${BENCH_USAGE}\n`)
|
|
817
|
+
return 1
|
|
818
|
+
}
|
|
819
|
+
try {
|
|
820
|
+
const { config, root } = loadProject(configPath)
|
|
821
|
+
const suite = parseRetrievalSuite(JSON.parse(readFileSync(resolve(root, positional[2]), 'utf8')) as unknown)
|
|
822
|
+
|
|
823
|
+
const limitOption = optionValues(argv, '--limit')[0]
|
|
824
|
+
const limit = limitOption === undefined ? undefined : Number(limitOption)
|
|
825
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit <= 0)) {
|
|
826
|
+
throw new Error('--limit must be a positive integer.')
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
if (flags.has('--overlay')) {
|
|
830
|
+
/*
|
|
831
|
+
* Does the accepted overlay earn its cost? The same suite over the same snapshot, once with
|
|
832
|
+
* the overlay projected and once without. A drop in hit@3 exits non-zero: an overlay that
|
|
833
|
+
* makes retrieval worse is a finding about the agent, not a new baseline.
|
|
834
|
+
*
|
|
835
|
+
* Both indexes are projected here, from the snapshot, so this answer needs no index on disk.
|
|
836
|
+
*/
|
|
837
|
+
const overlay = readEnrichmentOverlay(root)
|
|
838
|
+
if (!overlay) throw new Error('No enrichment overlay at .doc-bridge/enrich/overlay.json. Run: ak-docs enrich')
|
|
839
|
+
const stateDir = resolve(root, config.workflow?.stateDir ?? '.doc-bridge/workflow')
|
|
840
|
+
const snapshot = (() => {
|
|
841
|
+
try { return parseDiscoverySnapshot(loadWorkflowStepOutput(stateDir, 'normalize')) }
|
|
842
|
+
catch { return discoverRepository({ root, config }) }
|
|
843
|
+
})()
|
|
844
|
+
const delta = measureOverlayRetrievalDelta({ root, config, snapshot, overlay, suite, ...(limit === undefined ? {} : { limit }) })
|
|
845
|
+
if (wantsTextOutput(flags, config)) writeLines(formatOverlayRetrievalDeltaText(delta))
|
|
846
|
+
else writeJson({ ok: !delta.regression, overlayDelta: delta })
|
|
847
|
+
return delta.regression ? 1 : 0
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const indexOption = optionValues(argv, '--index')[0]
|
|
851
|
+
const index = indexOption
|
|
852
|
+
? parseDocBridgeIndex(JSON.parse(readFileSync(resolve(root, indexOption), 'utf8')) as unknown)
|
|
853
|
+
: loadFreshDocBridgeIndex(root, config)
|
|
854
|
+
|
|
855
|
+
const result: RetrievalBenchResultV1 = runRetrievalBench({
|
|
856
|
+
index,
|
|
857
|
+
suite,
|
|
858
|
+
...(limit === undefined ? {} : { limit }),
|
|
859
|
+
})
|
|
860
|
+
|
|
861
|
+
const baselineOption = optionValues(argv, '--baseline')[0]
|
|
862
|
+
const baselinePath = baselineOption ? resolve(root, baselineOption) : undefined
|
|
863
|
+
|
|
864
|
+
if (flags.has('--update-baseline')) {
|
|
865
|
+
const approvedBy = optionValues(argv, '--by')[0]
|
|
866
|
+
if (!baselinePath) throw new Error('--update-baseline requires --baseline <file>.')
|
|
867
|
+
if (!approvedBy) throw new Error('--update-baseline requires --by <name>: a baseline is an approved figure, not a side effect of a run.')
|
|
868
|
+
const reason = optionValues(argv, '--reason')[0]
|
|
869
|
+
const baseline = createRetrievalBaseline({ result, approvedBy, ...(reason ? { reason } : {}) })
|
|
870
|
+
mkdirSync(dirname(baselinePath), { recursive: true })
|
|
871
|
+
writeFileSync(baselinePath, `${JSON.stringify(baseline, null, 2)}\n`, 'utf8')
|
|
872
|
+
if (wantsTextOutput(flags, config)) {
|
|
873
|
+
writeLines([...formatRetrievalBenchText(result), `Baseline written: ${relative(root, baselinePath)} (approved by ${approvedBy})`])
|
|
874
|
+
} else writeJson({ ok: true, result, baseline, baselinePath })
|
|
875
|
+
return 0
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (baselinePath && !existsSync(baselinePath)) {
|
|
879
|
+
throw new Error(
|
|
880
|
+
`No baseline at ${relative(root, baselinePath)}. Record the current figures with: ak-docs bench retrieval ${positional[2]} --baseline ${baselineOption} --update-baseline --by <name>`,
|
|
881
|
+
)
|
|
882
|
+
}
|
|
883
|
+
const comparison = baselinePath
|
|
884
|
+
? compareRetrievalBaseline(result, parseRetrievalBaseline(JSON.parse(readFileSync(baselinePath, 'utf8')) as unknown))
|
|
885
|
+
: undefined
|
|
886
|
+
|
|
887
|
+
if (wantsTextOutput(flags, config)) {
|
|
888
|
+
writeLines([...formatRetrievalBenchText(result), ...(comparison ? formatRetrievalComparisonText(comparison) : [])])
|
|
889
|
+
} else {
|
|
890
|
+
writeJson({ ok: !comparison?.blocking, result, ...(comparison ? { comparison } : {}) })
|
|
891
|
+
}
|
|
892
|
+
return comparison?.blocking ? 1 : 0
|
|
893
|
+
} catch (error) {
|
|
894
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
895
|
+
return 2
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const runDocumentationAuditCommand = (
|
|
900
|
+
flags: ReadonlySet<string>,
|
|
901
|
+
positional: readonly string[],
|
|
902
|
+
configPath: string | undefined,
|
|
903
|
+
): number => {
|
|
904
|
+
if (positional[1] !== 'documentation') {
|
|
905
|
+
process.stderr.write('Usage: ak-docs audit documentation [--text|--json]\n')
|
|
906
|
+
return 1
|
|
907
|
+
}
|
|
908
|
+
try {
|
|
909
|
+
const { config, root } = loadProject(configPath)
|
|
910
|
+
const report = buildDocumentationAuditReport(root, config)
|
|
911
|
+
if (wantsTextOutput(flags, config)) writeLines(formatDocumentationAuditText(report))
|
|
912
|
+
else writeJson({ ok: report.status !== 'blocked', report })
|
|
913
|
+
return report.status === 'blocked' ? 1 : 0
|
|
914
|
+
} catch (error) {
|
|
915
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
916
|
+
return 2
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const RENDER_USAGE = [
|
|
921
|
+
`Usage: ak-docs render <${RENDER_TEMPLATE_NAMES.join('|')}> [--data <artifact>] [--output <path>] [--print-template] [--json]`,
|
|
922
|
+
...RENDER_TEMPLATE_NAMES.map((name) => ` ${name.padEnd(16)} ${RENDER_TEMPLATES[name].description} (--data: ${RENDER_TEMPLATES[name].data})`),
|
|
923
|
+
].join('\n')
|
|
924
|
+
|
|
925
|
+
const runRenderCommand = (
|
|
926
|
+
flags: ReadonlySet<string>,
|
|
927
|
+
positional: readonly string[],
|
|
928
|
+
configPath: string | undefined,
|
|
929
|
+
argv: readonly string[],
|
|
930
|
+
): number => {
|
|
931
|
+
// parseArgs files option values as positionals; the template name is the first one no option consumed.
|
|
932
|
+
const dataPath = optionValues(argv, '--data')[0]
|
|
933
|
+
const outputPath = optionValues(argv, '--output')[0]
|
|
934
|
+
const consumed = new Set([dataPath, outputPath].filter(Boolean))
|
|
935
|
+
const name = positional.slice(1).find((value) => !consumed.has(value))
|
|
936
|
+
if (!name || !isRenderTemplateName(name)) {
|
|
937
|
+
process.stderr.write(`${name ? `Unknown template "${name}".\n` : ''}${RENDER_USAGE}\n`)
|
|
938
|
+
return 1
|
|
939
|
+
}
|
|
940
|
+
try {
|
|
941
|
+
const { config, root } = loadProject(configPath)
|
|
942
|
+
if (flags.has('--print-template')) {
|
|
943
|
+
process.stdout.write(resolveTemplateSource(name, config, root).source)
|
|
944
|
+
return 0
|
|
945
|
+
}
|
|
946
|
+
const result = renderArtifact({ root, config, template: name, ...(dataPath ? { dataPath } : {}) })
|
|
947
|
+
if (outputPath) {
|
|
948
|
+
const written = writeRenderedPages(result, resolve(root, outputPath), root)
|
|
949
|
+
writeJson({ ok: true, template: name, source: result.origin, written })
|
|
950
|
+
return 0
|
|
951
|
+
}
|
|
952
|
+
if (flags.has('--json')) {
|
|
953
|
+
writeJson({ ok: true, template: name, source: result.origin, pages: result.pages })
|
|
954
|
+
return 0
|
|
955
|
+
}
|
|
956
|
+
process.stdout.write(result.pages.map((page) => page.content).join('\n'))
|
|
957
|
+
return 0
|
|
958
|
+
} catch (error) {
|
|
959
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
960
|
+
return 2
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
625
964
|
const runRulesCommand = (
|
|
626
965
|
argv: readonly string[],
|
|
627
966
|
flags: ReadonlySet<string>,
|
|
@@ -672,7 +1011,7 @@ const runRulesCommand = (
|
|
|
672
1011
|
}
|
|
673
1012
|
}
|
|
674
1013
|
|
|
675
|
-
const runFixCommand = (argv: readonly string[], positional: readonly string[], configPath: string | undefined): number => {
|
|
1014
|
+
const runFixCommand = async (argv: readonly string[], positional: readonly string[], configPath: string | undefined): Promise<number> => {
|
|
676
1015
|
try {
|
|
677
1016
|
const { config, root } = loadProject(configPath)
|
|
678
1017
|
const action = positional[1]
|
|
@@ -693,8 +1032,12 @@ const runFixCommand = (argv: readonly string[], positional: readonly string[], c
|
|
|
693
1032
|
const file = resolve(root, proposalPath)
|
|
694
1033
|
const proposal = JSON.parse(readFileSync(file, 'utf8')) as unknown
|
|
695
1034
|
const result = action === 'approve' ? approveFixProposal(proposal, optionValues(argv, '--by')[0] ?? 'human') : applyFixProposal(root, proposal, { currentRevision: sourceRevision })
|
|
1035
|
+
// An approval is recorded through the ecosystem gate too, bound to the proposal and its exact content hash.
|
|
1036
|
+
const recorded = action === 'approve' && result.approval
|
|
1037
|
+
? await recordApproval(root, { id: fixApprovalId(result.proposalId, result.approval.proposalHash), name: FIX_APPROVAL_GATE, payload: { proposalId: result.proposalId, proposalHash: result.approval.proposalHash }, decision: 'approved', by: result.approval.approvedBy })
|
|
1038
|
+
: undefined
|
|
696
1039
|
writeFileSync(file, `${JSON.stringify(result, null, 2)}\n`, 'utf8')
|
|
697
|
-
writeJson({ ok: true, proposal: result, proposalPath: file })
|
|
1040
|
+
writeJson({ ok: true, proposal: result, proposalPath: file, ...(recorded ? { approvalId: recorded.approval.id } : {}) })
|
|
698
1041
|
return 0
|
|
699
1042
|
} catch (error) {
|
|
700
1043
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
@@ -702,17 +1045,128 @@ const runFixCommand = (argv: readonly string[], positional: readonly string[], c
|
|
|
702
1045
|
}
|
|
703
1046
|
}
|
|
704
1047
|
|
|
1048
|
+
const runEnrichCommand = async (flags: ReadonlySet<string>, positional: readonly string[], argv: readonly string[], configPath: string | undefined): Promise<number> => {
|
|
1049
|
+
try {
|
|
1050
|
+
const { config, root } = loadProject(configPath)
|
|
1051
|
+
const action = positional[1]
|
|
1052
|
+
if (action === 'list') {
|
|
1053
|
+
const review = listEnrichment(root)
|
|
1054
|
+
if (wantsTextOutput(flags, config)) {
|
|
1055
|
+
writeLines(review
|
|
1056
|
+
? [`Overlay: ${review.overlayHash}`, `Accepted: ${review.accepted.length} Pending: ${review.pending.length} Rejected: ${review.rejected.length}`, ...review.pending.map((entry) => ` pending ${entry.proposal.proposalId.slice(0, 16)} ${entry.proposal.kind} ${entry.proposal.entity}${entry.note ? ` (${entry.note})` : ''}`)]
|
|
1057
|
+
: ['No enrichment overlay. Run `ak-docs enrich` first.'])
|
|
1058
|
+
} else writeJson({ ok: true, enrichment: review ?? null })
|
|
1059
|
+
return 0
|
|
1060
|
+
}
|
|
1061
|
+
if (action === 'approve' || action === 'reject') {
|
|
1062
|
+
const proposalId = positional[2]
|
|
1063
|
+
const by = optionValues(argv, '--by')[0]
|
|
1064
|
+
if (!proposalId || !by) throw new Error('Usage: ak-docs enrich approve|reject <proposalId> --by <name> [--reason <text>]')
|
|
1065
|
+
const stateDir = resolve(root, config.workflow?.stateDir ?? '.doc-bridge/workflow')
|
|
1066
|
+
const snapshot = (() => { try { return parseDiscoverySnapshot(loadWorkflowStepOutput(stateDir, 'normalize')) } catch { return undefined } })()
|
|
1067
|
+
const reason = optionValues(argv, '--reason')[0]
|
|
1068
|
+
const decided = await decideEnrichment({ root, proposalId, decision: action === 'approve' ? 'approved' : 'rejected', by, ...(reason ? { reason } : {}), ...(snapshot ? { snapshot } : {}) })
|
|
1069
|
+
const decidedId = 'proposal' in decided.entry ? decided.entry.proposal.proposalId : decided.entry.proposalId
|
|
1070
|
+
if (wantsTextOutput(flags, config)) writeLines([`${action === 'approve' ? 'Approved' : 'Rejected'}: ${decidedId}`, `Approval: ${decided.approvalId} (${decided.gateSource})`, `Overlay: ${decided.overlay.contentHash}`])
|
|
1071
|
+
else writeJson({ ok: true, approvalId: decided.approvalId, gate: decided.gateSource, entry: decided.entry, overlayHash: decided.overlay.contentHash })
|
|
1072
|
+
return 0
|
|
1073
|
+
}
|
|
1074
|
+
if (action !== undefined) throw new Error('Usage: ak-docs enrich [list | approve <proposalId> --by <name> | reject <proposalId> --by <name>] [--json|--text]')
|
|
1075
|
+
const reconciled = reconcileWorkflow(root, config)
|
|
1076
|
+
const snapshot = parseDiscoverySnapshot(loadWorkflowStepOutput(reconciled.stateDir, 'normalize'))
|
|
1077
|
+
const report = parseReconciliationReport(loadWorkflowStepOutput(reconciled.stateDir, 'reconcile'))
|
|
1078
|
+
const result = await runEnrichment({ root, config, snapshot, report })
|
|
1079
|
+
attachEnrichmentStage(root, config, report, result)
|
|
1080
|
+
/*
|
|
1081
|
+
* The retrieval delta is opt-in: it runs the golden suite twice, which is the right cost for
|
|
1082
|
+
* an answer about whether the overlay helped and the wrong cost for every routine run.
|
|
1083
|
+
*/
|
|
1084
|
+
const delta = flags.has('--retrieval-delta')
|
|
1085
|
+
? measureOverlayRetrievalDelta({
|
|
1086
|
+
root,
|
|
1087
|
+
config,
|
|
1088
|
+
snapshot,
|
|
1089
|
+
overlay: result.overlay,
|
|
1090
|
+
suite: parseRetrievalSuite(JSON.parse(readFileSync(resolve(root, config.retrieval?.benchmark?.suite ?? DEFAULT_RETRIEVAL_SUITE), 'utf8')) as unknown),
|
|
1091
|
+
})
|
|
1092
|
+
: undefined
|
|
1093
|
+
if (wantsTextOutput(flags, config)) writeLines([...formatEnrichmentText(result), ...(delta ? formatOverlayRetrievalDeltaText(delta) : [])])
|
|
1094
|
+
else writeJson({
|
|
1095
|
+
ok: !delta?.regression,
|
|
1096
|
+
overlayPath: result.overlayPath,
|
|
1097
|
+
overlayHash: result.overlay.contentHash,
|
|
1098
|
+
baseSnapshotHash: result.overlay.baseSnapshotHash,
|
|
1099
|
+
roles: result.roles,
|
|
1100
|
+
packs: result.packs,
|
|
1101
|
+
agentCalls: result.agentCalls,
|
|
1102
|
+
cacheHits: result.cacheHits,
|
|
1103
|
+
rerun: result.rerun,
|
|
1104
|
+
expired: result.expired,
|
|
1105
|
+
stats: result.overlay.stats,
|
|
1106
|
+
cost: enrichmentCost(result.overlay.stats),
|
|
1107
|
+
stability: result.stability,
|
|
1108
|
+
accepted: result.overlay.accepted.length,
|
|
1109
|
+
pending: result.overlay.pending.length,
|
|
1110
|
+
rejected: result.overlay.rejected.length,
|
|
1111
|
+
...(delta ? { retrievalDelta: delta } : {}),
|
|
1112
|
+
})
|
|
1113
|
+
return delta?.regression ? 1 : 0
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
1116
|
+
return 2
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/** Where the claim registry lives unless a caller points somewhere else. */
|
|
1121
|
+
export const DEFAULT_PUBLIC_CLAIMS = 'docs/parity/public-claims-v1.json'
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* `ak-docs parity`: what this repository says in public, against what it can prove.
|
|
1125
|
+
*
|
|
1126
|
+
* The gate fails on a blocking finding — a stale or contradictory claim nobody accepted — and
|
|
1127
|
+
* leaves warnings and unresolved claims visible without failing, so a repository can adopt the
|
|
1128
|
+
* registry one claim at a time. The doctor runs only when a claim asks for one of its figures:
|
|
1129
|
+
* measuring it costs an index and a benchmark, and most registries never need it.
|
|
1130
|
+
*/
|
|
1131
|
+
const runParityCommand = (flags: ReadonlySet<string>, configPath: string | undefined, argv: readonly string[]): number => {
|
|
1132
|
+
try {
|
|
1133
|
+
const { config, root } = loadProject(configPath)
|
|
1134
|
+
const claimsPath = optionValues(argv, '--claims')[0] ?? DEFAULT_PUBLIC_CLAIMS
|
|
1135
|
+
const registry = parsePublicClaims(JSON.parse(readFileSync(resolve(root, claimsPath), 'utf8')) as unknown)
|
|
1136
|
+
const snapshot = discoverRepository({ root, config })
|
|
1137
|
+
const doctor = registry.claims.some((claim) => claim.evidence.kind === 'doctor-metric') ? runDoctor(root, config) : undefined
|
|
1138
|
+
const report = checkPublicParity({
|
|
1139
|
+
root,
|
|
1140
|
+
config,
|
|
1141
|
+
registry,
|
|
1142
|
+
snapshot,
|
|
1143
|
+
...(doctor ? { doctor } : {}),
|
|
1144
|
+
project: { name: snapshot.project.name },
|
|
1145
|
+
sourceRevision: snapshot.sourceRevision,
|
|
1146
|
+
sourceRevisionKind: snapshot.sourceRevisionKind,
|
|
1147
|
+
})
|
|
1148
|
+
if (wantsTextOutput(flags, config)) writeLines(formatPublicParityText(report))
|
|
1149
|
+
else writeJson({ ok: report.metrics.blocking === 0, parity: report })
|
|
1150
|
+
return report.metrics.blocking ? 1 : 0
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
1153
|
+
return 2
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
705
1157
|
const runSuggestCommand = async (flags: ReadonlySet<string>, configPath: string | undefined): Promise<number> => {
|
|
706
1158
|
try {
|
|
707
1159
|
const { config, root } = loadProject(configPath)
|
|
708
1160
|
const stateDir = resolve(root, config.workflow?.stateDir ?? '.doc-bridge/workflow')
|
|
709
1161
|
const snapshot = parseDiscoverySnapshot(loadWorkflowStepOutput(stateDir, 'normalize'))
|
|
710
1162
|
const report = parseReconciliationReport(loadWorkflowStepOutput(stateDir, 'reconcile'))
|
|
711
|
-
const
|
|
712
|
-
const
|
|
1163
|
+
const runner = config.intelligence?.registry?.cli ? undefined : await loadRegistryAgentRunner(root, config)
|
|
1164
|
+
const adapter = createRegistryAgentAdapter(root, config, runner)
|
|
1165
|
+
const documentation = flags.has('--documentation') ? buildDocumentationAuditReport(root, config) : undefined
|
|
1166
|
+
const proposal = await adapter.run(snapshot, report, undefined, documentation)
|
|
713
1167
|
const proposalPath = persistRegistryAgentProposal(stateDir, proposal)
|
|
714
|
-
if (flags.has('--text')) writeLines([`Agent: ${adapter.metadata.id}`, `Proposal: ${proposal.proposalId}`, `Hash: ${proposal.contentHash}`, `Saved: ${proposalPath}`])
|
|
715
|
-
else writeJson({ ok: true, proposal, proposalPath })
|
|
1168
|
+
if (flags.has('--text')) writeLines([`Agent: ${adapter.metadata.id}`, ...(documentation ? [`Documentation audit: ${documentation.contentHash}`] : []), `Proposal: ${proposal.proposalId}`, `Hash: ${proposal.contentHash}`, `Saved: ${proposalPath}`])
|
|
1169
|
+
else writeJson({ ok: true, ...(documentation ? { documentationAuditHash: documentation.contentHash } : {}), proposal, proposalPath })
|
|
716
1170
|
return 0
|
|
717
1171
|
} catch (error) {
|
|
718
1172
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
@@ -901,6 +1355,143 @@ const registryTopology = () => ({
|
|
|
901
1355
|
mergePolicy: { autoMerge: false, requiresHuman: true },
|
|
902
1356
|
})
|
|
903
1357
|
|
|
1358
|
+
const runStudyCommand = async (flags: ReadonlySet<string>, positional: readonly string[], argv: readonly string[], configPath: string | undefined): Promise<number> => {
|
|
1359
|
+
const action = positional[1]
|
|
1360
|
+
const inputPath = positional[2]
|
|
1361
|
+
if (!inputPath || !['protocol', 'history', 'tasks', 'select', 'plan', 'providers', 'run', 'adjudicate', 'ledger', 'metrics', 'verification', 'expectations'].includes(action ?? '')) {
|
|
1362
|
+
process.stderr.write('Usage: ak-docs study protocol|history|tasks|select|plan|providers|run|adjudicate|ledger|metrics|verification|expectations <artifact.json> [--protocol <protocol.json>] [--providers <provider-cli.json>] [--adjudicator <provider-cli.json>] [--repositories <repositories.json>] [--ledger <ledger.json>] [--output <ledger.json>] [--run-id <id>] [--limit <n>] [--round <id>] [--dry-run] [--baseline-round <id>] [--current-round <id>] [--expectations <expectations.json>] [--index <index.json>] [--repository <id>] [--text|--json]\n')
|
|
1363
|
+
return 1
|
|
1364
|
+
}
|
|
1365
|
+
try {
|
|
1366
|
+
if (action === 'expectations') {
|
|
1367
|
+
/*
|
|
1368
|
+
* The mechanical half of the study, checked by the same benchmark that gates this
|
|
1369
|
+
* repository's retrieval. The suite carries opaque references; the local expectations file
|
|
1370
|
+
* resolves them to entities and documents, which is why it is never published.
|
|
1371
|
+
*/
|
|
1372
|
+
const expectationsPath = optionValues(argv, '--expectations')[0]
|
|
1373
|
+
if (!expectationsPath) throw new Error('Study expectations require --expectations <expectations.json>.')
|
|
1374
|
+
const suite = parseStudyTaskSuite(JSON.parse(readFileSync(resolve(inputPath), 'utf8')) as unknown)
|
|
1375
|
+
const expectations = parseStudyExpectations(JSON.parse(readFileSync(resolve(expectationsPath), 'utf8')) as unknown)
|
|
1376
|
+
const repositoryId = optionValues(argv, '--repository')[0]
|
|
1377
|
+
const indexOption = optionValues(argv, '--index')[0]
|
|
1378
|
+
const { config, root } = loadProject(configPath)
|
|
1379
|
+
const index = indexOption
|
|
1380
|
+
? parseDocBridgeIndex(JSON.parse(readFileSync(resolve(root, indexOption), 'utf8')) as unknown)
|
|
1381
|
+
: loadFreshDocBridgeIndex(root, config)
|
|
1382
|
+
const check = checkStudyExpectations({ taskSuite: suite, expectations, index, ...(repositoryId ? { repositoryId } : {}) })
|
|
1383
|
+
if (flags.has('--text')) writeLines(formatStudyExpectationsText(check))
|
|
1384
|
+
else writeJson({ ok: check.ok, expectations: check })
|
|
1385
|
+
return check.ok ? 0 : 1
|
|
1386
|
+
}
|
|
1387
|
+
if (action === 'run') {
|
|
1388
|
+
const taskSuitePath = positional[3]
|
|
1389
|
+
const providersPath = optionValues(argv, '--providers')[0]
|
|
1390
|
+
const repositoriesPath = optionValues(argv, '--repositories')[0]
|
|
1391
|
+
const ledgerPath = optionValues(argv, '--ledger')[0]
|
|
1392
|
+
if (!taskSuitePath || !providersPath || !repositoriesPath || !ledgerPath) throw new Error('Study run requires a task suite, --providers, --repositories, and --ledger.')
|
|
1393
|
+
const summary = await runControlledStudy({
|
|
1394
|
+
plan: parseControlledStudyRunPlan(JSON.parse(readFileSync(resolve(inputPath), 'utf8')) as unknown),
|
|
1395
|
+
suite: parseStudyTaskSuite(JSON.parse(readFileSync(resolve(taskSuitePath), 'utf8')) as unknown),
|
|
1396
|
+
providers: parseStudyProviderCliConfig(JSON.parse(readFileSync(resolve(providersPath), 'utf8')) as unknown),
|
|
1397
|
+
repositories: parseStudyRepositoryConfig(JSON.parse(readFileSync(resolve(repositoriesPath), 'utf8')) as unknown),
|
|
1398
|
+
ledgerPath: resolve(ledgerPath),
|
|
1399
|
+
...(optionValues(argv, '--round')[0] === undefined ? {} : { round: optionValues(argv, '--round')[0] }),
|
|
1400
|
+
...(flags.has('--dry-run') ? { dryRun: true } : {}),
|
|
1401
|
+
})
|
|
1402
|
+
if (flags.has('--text')) writeLines(formatControlledStudyRunText(summary))
|
|
1403
|
+
else writeJson({ ok: summary.status === 'dry-run' || summary.status === 'completed', summary })
|
|
1404
|
+
return 0
|
|
1405
|
+
}
|
|
1406
|
+
if (action === 'adjudicate') {
|
|
1407
|
+
const taskSuitePath = positional[3]
|
|
1408
|
+
const adjudicatorConfigPath = optionValues(argv, '--adjudicator')[0]
|
|
1409
|
+
const outputPath = optionValues(argv, '--output')[0]
|
|
1410
|
+
if (!taskSuitePath || !adjudicatorConfigPath || !outputPath) throw new Error('Study adjudication requires a task suite, --adjudicator with an adjudicator, and --output.')
|
|
1411
|
+
const config = parseStudyProviderCliConfig(JSON.parse(readFileSync(resolve(adjudicatorConfigPath), 'utf8')) as unknown)
|
|
1412
|
+
if (!config.adjudicator) throw new Error('Study provider config must declare an adjudicator.')
|
|
1413
|
+
const ledger = parseControlledStudyLedger(JSON.parse(readFileSync(resolve(inputPath), 'utf8')) as unknown)
|
|
1414
|
+
const suite = parseStudyTaskSuite(JSON.parse(readFileSync(resolve(taskSuitePath), 'utf8')) as unknown)
|
|
1415
|
+
const limitValue = optionValues(argv, '--limit')[0]
|
|
1416
|
+
const limit = limitValue === undefined ? undefined : Number(limitValue)
|
|
1417
|
+
const offsetValue = optionValues(argv, '--offset')[0]
|
|
1418
|
+
const offset = offsetValue === undefined ? undefined : Number(offsetValue)
|
|
1419
|
+
const result = await independentlyAdjudicateStudyLedger({ ledger, taskSuite: suite, config: config.adjudicator, configurationHash: config.contentHash, cwd: process.cwd(), maxRuntimeMs: suite.maxRuntimeMsPerTask, ...(optionValues(argv, '--run-id')[0] === undefined ? {} : { runId: optionValues(argv, '--run-id')[0] }), ...(offset === undefined ? {} : { offset }), ...(limit === undefined ? {} : { limit }) })
|
|
1420
|
+
persistIndependentlyAdjudicatedLedger(outputPath, result)
|
|
1421
|
+
if (flags.has('--text')) writeLines([`Adjudicated observations: ${result.observations.length}`, `Ledger: ${resolve(outputPath)}`, `Content hash: ${result.contentHash}`])
|
|
1422
|
+
else writeJson({ ok: true, ledger: result })
|
|
1423
|
+
return 0
|
|
1424
|
+
}
|
|
1425
|
+
const input = JSON.parse(readFileSync(resolve(inputPath), 'utf8')) as unknown
|
|
1426
|
+
if (action === 'protocol') {
|
|
1427
|
+
const protocol = parseStudyProtocol(input)
|
|
1428
|
+
if (flags.has('--text')) writeLines(formatStudyProtocolText(protocol))
|
|
1429
|
+
else writeJson({ ok: true, protocol })
|
|
1430
|
+
return 0
|
|
1431
|
+
}
|
|
1432
|
+
if (action === 'tasks') {
|
|
1433
|
+
const suite = parseStudyTaskSuite(input)
|
|
1434
|
+
if (flags.has('--text')) writeLines(formatStudyTaskSuiteText(suite))
|
|
1435
|
+
else writeJson({ ok: true, suite })
|
|
1436
|
+
return 0
|
|
1437
|
+
}
|
|
1438
|
+
if (action === 'select') {
|
|
1439
|
+
const suite = parseStudyTaskSuite(input)
|
|
1440
|
+
const executions = selectTaskExecutions(suite)
|
|
1441
|
+
if (flags.has('--text')) writeLines([`Selected executions: ${executions.length}`, `First execution: ${executions[0]?.taskId ?? 'none'} / ${executions[0]?.variantId ?? 'none'}`])
|
|
1442
|
+
else writeJson({ ok: true, executions })
|
|
1443
|
+
return 0
|
|
1444
|
+
}
|
|
1445
|
+
if (action === 'plan') {
|
|
1446
|
+
const plan = parseControlledStudyRunPlan(input)
|
|
1447
|
+
if (flags.has('--text')) writeLines(formatControlledStudyRunPlanText(plan))
|
|
1448
|
+
else writeJson({ ok: true, plan })
|
|
1449
|
+
return 0
|
|
1450
|
+
}
|
|
1451
|
+
if (action === 'providers') {
|
|
1452
|
+
const providers = parseStudyProviderCliConfig(input)
|
|
1453
|
+
if (flags.has('--text')) writeLines(formatStudyProviderCliText(providers))
|
|
1454
|
+
else writeJson({ ok: true, providers })
|
|
1455
|
+
return 0
|
|
1456
|
+
}
|
|
1457
|
+
if (action === 'ledger') {
|
|
1458
|
+
const ledger = parseControlledStudyLedger(input)
|
|
1459
|
+
if (flags.has('--text')) writeLines([`Ledger: ${ledger.ledgerVersion}`, `Observations: ${ledger.observations.length}`, `Content hash: ${ledger.contentHash}`])
|
|
1460
|
+
else writeJson({ ok: true, ledger })
|
|
1461
|
+
return 0
|
|
1462
|
+
}
|
|
1463
|
+
if (action === 'metrics') {
|
|
1464
|
+
const ledger = parseControlledStudyLedger(input)
|
|
1465
|
+
const baselineRound = optionValues(argv, '--baseline-round')[0]
|
|
1466
|
+
const currentRound = optionValues(argv, '--current-round')[0]
|
|
1467
|
+
const baselineRunId = optionValues(argv, '--baseline-run-id')[0]
|
|
1468
|
+
const currentRunId = optionValues(argv, '--current-run-id')[0]
|
|
1469
|
+
const report = calculateStudyMetrics(ledger.observations, { ...(baselineRound === undefined ? {} : { baselineRound }), ...(currentRound === undefined ? {} : { currentRound }), ...(baselineRunId === undefined ? {} : { baselineRunId }), ...(currentRunId === undefined ? {} : { currentRunId }) })
|
|
1470
|
+
if (flags.has('--text')) writeLines(formatStudyMetricsText(report))
|
|
1471
|
+
else writeJson({ ok: true, report })
|
|
1472
|
+
return report.comparisons.some((comparison) => comparison.status === 'regressed') && !flags.has('--allow-regressions') ? 1 : 0
|
|
1473
|
+
}
|
|
1474
|
+
if (action === 'verification') {
|
|
1475
|
+
const binding = parseStudyVerificationBinding(input)
|
|
1476
|
+
if (flags.has('--text')) writeLines(formatStudyVerificationText(binding))
|
|
1477
|
+
else writeJson({ ok: true, binding })
|
|
1478
|
+
return 0
|
|
1479
|
+
}
|
|
1480
|
+
const registry = parseHistoricalEvidenceRegistry(input)
|
|
1481
|
+
const protocolPath = optionValues(argv, '--protocol')[0]
|
|
1482
|
+
if (protocolPath) {
|
|
1483
|
+
const protocol = parseStudyProtocol(JSON.parse(readFileSync(resolve(protocolPath), 'utf8')) as unknown)
|
|
1484
|
+
validateHistoricalEvidenceRegistry(registry, protocol)
|
|
1485
|
+
}
|
|
1486
|
+
if (flags.has('--text')) writeLines(formatHistoricalEvidenceText(registry))
|
|
1487
|
+
else writeJson({ ok: true, registry })
|
|
1488
|
+
return 0
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
|
|
1491
|
+
return 2
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
904
1495
|
export const runCli = (argv: readonly string[]): number | undefined | Promise<number> => {
|
|
905
1496
|
const { command, flags, configPath, positional } = parseArgs(argv)
|
|
906
1497
|
|
|
@@ -990,12 +1581,19 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
990
1581
|
}
|
|
991
1582
|
}
|
|
992
1583
|
|
|
1584
|
+
if (command === 'study') return runStudyCommand(flags, positional, argv, configPath)
|
|
1585
|
+
|
|
993
1586
|
if (command === 'scan' || command === 'reconcile' || command === 'check' || command === 'map') {
|
|
994
1587
|
return runWorkflowCommand(command, flags, configPath, argv)
|
|
995
1588
|
}
|
|
1589
|
+
if (command === 'audit') return runDocumentationAuditCommand(flags, positional, configPath)
|
|
1590
|
+
if (command === 'parity') return runParityCommand(flags, configPath, argv)
|
|
1591
|
+
if (command === 'render') return runRenderCommand(flags, positional, configPath, argv)
|
|
1592
|
+
if (command === 'bench') return runBenchCommand(flags, positional, configPath, argv)
|
|
996
1593
|
|
|
997
1594
|
if (command === 'fix') return runFixCommand(argv, positional, configPath)
|
|
998
1595
|
if (command === 'suggest') return runSuggestCommand(flags, configPath)
|
|
1596
|
+
if (command === 'enrich') return runEnrichCommand(flags, positional, argv, configPath)
|
|
999
1597
|
|
|
1000
1598
|
if (command === 'init') {
|
|
1001
1599
|
const root = process.cwd()
|
|
@@ -1066,7 +1664,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
1066
1664
|
writeJson({ ok: true, count: candidates.length, candidates })
|
|
1067
1665
|
return 0
|
|
1068
1666
|
}
|
|
1069
|
-
const index =
|
|
1667
|
+
const index = loadFreshDocBridgeIndex(root, config)
|
|
1070
1668
|
const classifications = classifyMemoryCandidates(candidates, index)
|
|
1071
1669
|
if (positional[1] === 'classify') {
|
|
1072
1670
|
writeJson({ ok: true, count: classifications.length, classifications })
|
|
@@ -1124,7 +1722,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
1124
1722
|
}
|
|
1125
1723
|
try {
|
|
1126
1724
|
const { config, root } = loadProject(configPath)
|
|
1127
|
-
const index =
|
|
1725
|
+
const index = loadFreshDocBridgeIndex(root, config)
|
|
1128
1726
|
const draft = draftMemoryPromotion(classifyMemoryCandidates(ingestMemoryCandidates(root), index))
|
|
1129
1727
|
writeJson({
|
|
1130
1728
|
...draft,
|
|
@@ -1329,9 +1927,10 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
1329
1927
|
}
|
|
1330
1928
|
try {
|
|
1331
1929
|
const { config, root } = loadProject(configPath)
|
|
1332
|
-
const index =
|
|
1333
|
-
const result = runQuery(index, config, { kind, id, agent: flags.has('--agent') })
|
|
1930
|
+
const index = loadFreshDocBridgeIndex(root, config)
|
|
1931
|
+
const result = runQuery(index, config, { kind, id, agent: flags.has('--agent') }, { root })
|
|
1334
1932
|
if (wantsTextOutput(flags, config)) writeTextQuery(result)
|
|
1933
|
+
else if (flags.has('--agent')) writeAgentJson(result)
|
|
1335
1934
|
else writeJson(result)
|
|
1336
1935
|
return 0
|
|
1337
1936
|
} catch (error) {
|
|
@@ -1348,12 +1947,23 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
1348
1947
|
}
|
|
1349
1948
|
try {
|
|
1350
1949
|
const { config, root } = loadProject(configPath)
|
|
1351
|
-
|
|
1950
|
+
const index = loadFreshDocBridgeIndex(root, config)
|
|
1951
|
+
const explain = flags.has('--explain')
|
|
1352
1952
|
if (flags.has('--agent')) {
|
|
1353
|
-
const
|
|
1354
|
-
|
|
1953
|
+
const mode = optionValues(argv, '--mode')[0]
|
|
1954
|
+
const budgetValue = optionValues(argv, '--context-budget')[0]
|
|
1955
|
+
const contextBudgetTokens = budgetValue === undefined ? undefined : Number(budgetValue)
|
|
1956
|
+
const result = runQuery(index, config, {
|
|
1957
|
+
kind: 'search',
|
|
1958
|
+
term,
|
|
1959
|
+
agent: true,
|
|
1960
|
+
...(explain ? { explain: true } : {}),
|
|
1961
|
+
...(mode === undefined ? {} : { mode: mode as 'discovery' | 'editing' | 'debugging' | 'documentation' }),
|
|
1962
|
+
...(contextBudgetTokens === undefined ? {} : { contextBudgetTokens }),
|
|
1963
|
+
})
|
|
1964
|
+
writeAgentJson(result)
|
|
1355
1965
|
} else {
|
|
1356
|
-
const matches = searchIndex(index, term)
|
|
1966
|
+
const matches = searchIndex(index, term, 20, explain ? { explain: true } : {})
|
|
1357
1967
|
if (wantsTextOutput(flags, config)) writeTextSearch(term, matches)
|
|
1358
1968
|
else writeJson({ term, count: matches.length, matches })
|
|
1359
1969
|
}
|
|
@@ -1377,7 +1987,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
1377
1987
|
return (async () => {
|
|
1378
1988
|
try {
|
|
1379
1989
|
const { config, root } = loadProject(configPath)
|
|
1380
|
-
const index =
|
|
1990
|
+
const index = loadFreshDocBridgeIndex(root, config)
|
|
1381
1991
|
writeJson({ query, chunks: await retrieveHybridChunks(root, config, index, query) })
|
|
1382
1992
|
return 0
|
|
1383
1993
|
} catch (error) {
|
|
@@ -1396,7 +2006,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
1396
2006
|
return (async () => {
|
|
1397
2007
|
try {
|
|
1398
2008
|
const { config, root } = loadProject(configPath)
|
|
1399
|
-
const index =
|
|
2009
|
+
const index = loadFreshDocBridgeIndex(root, config)
|
|
1400
2010
|
const rag = await createDocBridgeRag(root, config, index)
|
|
1401
2011
|
if (action === 'ingest') {
|
|
1402
2012
|
const result = await rag.ingest()
|
|
@@ -1433,7 +2043,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
1433
2043
|
)
|
|
1434
2044
|
return 1
|
|
1435
2045
|
}
|
|
1436
|
-
const index =
|
|
2046
|
+
const index = loadFreshDocBridgeIndex(root, config)
|
|
1437
2047
|
await startInkChat(root, config, index)
|
|
1438
2048
|
return 0
|
|
1439
2049
|
} catch (error) {
|
|
@@ -1465,7 +2075,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
1465
2075
|
}
|
|
1466
2076
|
return (async () => {
|
|
1467
2077
|
try {
|
|
1468
|
-
const index =
|
|
2078
|
+
const index = loadFreshDocBridgeIndex(root, config)
|
|
1469
2079
|
const result = await runChatOnce(root, config, index, question)
|
|
1470
2080
|
writeLines([result.content])
|
|
1471
2081
|
return 0
|
|
@@ -1484,7 +2094,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
1484
2094
|
process.stderr.write('Usage: ak-docs ask <question>, or run ak-docs ask in an interactive terminal.\n')
|
|
1485
2095
|
return 1
|
|
1486
2096
|
}
|
|
1487
|
-
const index =
|
|
2097
|
+
const index = loadFreshDocBridgeIndex(root, config)
|
|
1488
2098
|
writeAsk(question, searchIndex(index, question, 8), index, config)
|
|
1489
2099
|
return 0
|
|
1490
2100
|
} catch (error) {
|
|
@@ -1501,7 +2111,7 @@ export const runCli = (argv: readonly string[]): number | undefined | Promise<nu
|
|
|
1501
2111
|
}
|
|
1502
2112
|
try {
|
|
1503
2113
|
const { config, root } = loadProject(configPath)
|
|
1504
|
-
const index =
|
|
2114
|
+
const index = loadFreshDocBridgeIndex(root, config)
|
|
1505
2115
|
|
|
1506
2116
|
if (kind === 'packages') {
|
|
1507
2117
|
const items = index.lookup?.packages ?? []
|