@agentskit/doc-bridge 1.7.45 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +465 -0
- package/CONTRIBUTING.md +23 -0
- package/README.md +87 -40
- package/SECURITY.md +7 -0
- package/action.yml +1 -1
- package/bin/ak-docs.js +2 -2
- package/bin/ak-verify.js +13 -7
- package/dist/cli/program.d.ts +3 -1
- package/dist/cli/program.js +15861 -6223
- package/dist/cli/program.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +86 -6
- package/dist/config/index.js.map +1 -1
- package/dist/{index-BUL0q7s8.d.ts → index-Beor6Yhi.d.ts} +133 -1
- package/dist/index.d.ts +9871 -1062
- package/dist/index.js +16126 -6125
- package/dist/index.js.map +1 -1
- package/docs/MARKETPLACE.md +1 -1
- package/docs/PRD-documentation-efficiency-study.md +406 -0
- package/docs/PRD-knowledge-retrieval-and-enrichment.md +466 -0
- package/docs/adr/0002-documentation-audit-boundary.md +22 -0
- package/docs/adr/0003-study-protocol-and-historical-evidence.md +40 -0
- package/docs/adr/0004-controlled-study-runner.md +25 -0
- package/docs/adr/0005-documentation-quality-and-criticality.md +20 -0
- package/docs/adr/0006-registry-semantic-grounding.md +20 -0
- package/docs/adr/0007-longitudinal-study-metrics.md +21 -0
- package/docs/adr/0008-study-verification-boundary.md +21 -0
- package/docs/adr/0009-study-provider-cli-adapter.md +25 -0
- package/docs/agent-corpus/INDEX.md +12 -1
- package/docs/agent-corpus/OVERVIEW.md +25 -0
- package/docs/agent-corpus/chat.md +5 -1
- package/docs/agent-corpus/cli.md +16 -0
- package/docs/agent-corpus/conformance.md +12 -0
- package/docs/agent-corpus/doc-bridge.md +47 -0
- package/docs/agent-corpus/doctor.md +8 -0
- package/docs/agent-corpus/gates.md +4 -0
- package/docs/agent-corpus/mcp.md +13 -0
- package/docs/agent-corpus/memory.md +4 -0
- package/docs/agent-corpus/query.md +33 -0
- package/docs/bench/README.md +122 -0
- package/docs/bench/retrieval-baseline-v1.json +28 -0
- package/docs/bench/retrieval-suite-v1.json +1033 -0
- package/docs/chat-and-rag.md +3 -2
- package/docs/for-agents.md +9 -1
- package/docs/getting-started.md +4 -11
- package/docs/guides/gate-ci.md +11 -1
- package/docs/guides/install-and-run.md +9 -65
- package/docs/index.md +22 -1
- package/docs/knowledge-engine-runbook.md +38 -3
- package/docs/landing/assets/context-payload-reduction.svg +21 -0
- package/docs/landing/assets/controlled-ab-comparison.svg +30 -0
- package/docs/landing/index.html +119 -5
- package/docs/loop-workflow.md +117 -0
- package/docs/mcp.md +6 -1
- package/docs/parity/public-claims-v1.json +145 -0
- package/docs/playbook/doc-bridge-pattern.md +1 -1
- package/docs/query.md +90 -2
- package/docs/recipes/index-pipeline.md +1 -1
- package/docs/schemas/agent-handoff-v1.md +15 -0
- package/docs/schemas/doc-bridge-index-v1.md +65 -0
- package/docs/spec/benchmark-v1.md +33 -1
- package/docs/spec/cli.md +30 -10
- package/docs/spec/config-v1.md +147 -8
- package/docs/spec/documentation-audit-v1.md +61 -0
- package/docs/spec/enrichment-overlay-v1.md +241 -0
- package/docs/spec/graph-signals-v1.md +92 -0
- package/docs/spec/incremental-scan-v1.md +102 -0
- package/docs/spec/markdown-analyzer-v1.md +73 -0
- package/docs/spec/mcp-knowledge-tools-v1.md +147 -0
- package/docs/spec/measured-enrichment-v1.md +229 -0
- package/docs/spec/public-parity-v1.md +119 -0
- package/docs/spec/registry-agents.md +6 -0
- package/docs/spec/render-v1.md +122 -0
- package/docs/spec/retrieval-index-v1.md +164 -0
- package/docs/spec/study-metrics-v1.md +58 -0
- package/docs/spec/study-protocol-v1.md +46 -0
- package/docs/spec/study-provider-cli-v1.md +116 -0
- package/docs/spec/study-runner-v1.md +35 -0
- package/docs/spec/study-task-suite-v1.md +41 -0
- package/docs/spec/study-verification-v1.md +40 -0
- package/docs/study/README.md +84 -0
- package/docs/study/ab-adjudicated-cost-analysis-v1.md +29 -0
- package/docs/study/ab-adjudicated-cost-plan-v1.json +33 -0
- package/docs/study/ab-adjudicated-cost-plan-v2-v1.json +33 -0
- package/docs/study/ab-adjudicated-cost-result-v1.json +80 -0
- package/docs/study/ab-baseline-analysis-v1.md +21 -0
- package/docs/study/ab-baseline-plan-v1.json +33 -0
- package/docs/study/ab-baseline-recovery-plan-v1.json +33 -0
- package/docs/study/ab-baseline-result-v1.json +79 -0
- package/docs/study/documentation-audit-round-2026-08-31.json +183 -0
- package/docs/study/historical-evidence-v1.json +252 -0
- package/docs/study/observation-ledger-v1.json +30632 -0
- package/docs/study/phase3-task-coverage-v1.json +34 -0
- package/docs/study/phase4-public-pilot-ledger-v1.json +1344 -0
- package/docs/study/phase4-public-pilot-result-v1.json +52 -0
- package/docs/study/phase4-public-pilot-run-plan-v1.json +26 -0
- package/docs/study/phase4-public-pilot-task-suite-v1.json +71 -0
- package/docs/study/pilot-round-2026-08-31.json +46 -0
- package/docs/study/protocol-v1.json +90 -0
- package/docs/study/publication-gate-v1.md +45 -0
- package/docs/study/quality-scorecard-cycle-plan.md +545 -0
- package/docs/study/quality-scorecard-v1.json +38 -0
- package/docs/study/round-1-adjudicated-smoke-v1.json +30642 -0
- package/docs/study/round-1-instrumentation-plan-v1.md +39 -0
- package/docs/study/round-2-expanded-adjudication-v1.json +91 -0
- package/docs/study/round-2-expanded-validation-v1.md +58 -0
- package/docs/study/round-3-evidence-contract-v1.json +75 -0
- package/docs/study/round-3-evidence-contract-v1.md +57 -0
- package/docs/study/round-4-confirmation-v1.json +75 -0
- package/docs/study/round-4-confirmation-v1.md +55 -0
- package/docs/study/run-plan-v1.json +33 -0
- package/docs/study/semantic-adjudication-cycle-8.md +20 -0
- package/docs/study/task-suite-v1.json +96 -0
- package/docs/study/token-efficiency-plan-v1.md +337 -0
- package/docs/study/token-efficiency-protocol-v2.json +62 -0
- package/docs/study/verification-binding-v1.json +27 -0
- package/docs/validation-cycle-plan.md +14 -0
- package/docs/verification-harness.md +11 -6
- package/ecosystem-claims.json +2 -2
- package/ecosystem-upstream.json +2 -2
- package/ecosystem.json +4 -4
- package/mcpb/manifest.json +9 -1
- package/package.json +25 -6
- package/scripts/check-ecosystem-upstream.mjs +34 -6
- package/skills/doc-bridge-handoff/fixtures/synthetic-repo/docs/for-agents/packages/payments.md +7 -0
- package/skills/doc-bridge-handoff/scripts/resolve-handoff.mjs +1 -1
- package/src/agents/registry-adapter.ts +192 -24
- package/src/audit/documentation.ts +513 -0
- package/src/bench/baseline.ts +198 -0
- package/src/bench/overlay-delta.ts +139 -0
- package/src/bench/retrieval.ts +319 -0
- package/src/budget/compile.ts +91 -0
- package/src/budget/sections.ts +70 -0
- package/src/cli/program.ts +684 -74
- package/src/cli/usage.ts +71 -0
- package/src/config/defaults.ts +1 -0
- package/src/config/index.ts +4 -0
- package/src/config/schema.ts +117 -2
- package/src/conformance/documentation-standard-v1.ts +8 -6
- package/src/discovery/areas.ts +182 -0
- package/src/discovery/documentation.ts +211 -5
- package/src/discovery/identity.ts +24 -0
- package/src/discovery/incremental.ts +314 -0
- package/src/discovery/inputs.ts +110 -0
- package/src/discovery/markdown.ts +481 -0
- package/src/discovery/repository.ts +484 -101
- package/src/doctor/run-doctor.ts +244 -12
- package/src/enrich/approvals.ts +190 -0
- package/src/enrich/cache.ts +93 -0
- package/src/enrich/context-pack.ts +272 -0
- package/src/enrich/overlay.ts +255 -0
- package/src/enrich/review.ts +106 -0
- package/src/enrich/stage.ts +374 -0
- package/src/enrich/stats.ts +100 -0
- package/src/enrich/validate.ts +410 -0
- package/src/federation/llms.ts +2 -4
- package/src/findings/report.ts +103 -0
- package/src/graph/build.ts +356 -0
- package/src/graph/memory.ts +208 -0
- package/src/index-builder/build-handoffs.ts +22 -11
- package/src/index-builder/build-index.ts +132 -3
- package/src/index-builder/llms-txt.ts +48 -8
- package/src/index-builder/project-corpus.ts +111 -0
- package/src/index.ts +630 -2
- package/src/lib/fuzzy-match.ts +235 -0
- package/src/mcp/knowledge.ts +554 -0
- package/src/mcp/server.ts +113 -18
- package/src/metrics/benchmark.ts +21 -0
- package/src/parity/check.ts +309 -0
- package/src/parity/claims.ts +259 -0
- package/src/parity/resolve.ts +160 -0
- package/src/query/handoff.ts +326 -0
- package/src/query/load-index.ts +53 -1
- package/src/query/query.ts +92 -59
- package/src/query/search.ts +289 -92
- package/src/query/text.ts +155 -0
- package/src/reconciliation/reconcile.ts +101 -10
- package/src/render/data.ts +356 -0
- package/src/render/engine.ts +398 -0
- package/src/render/generated.ts +77 -0
- package/src/render/render.ts +209 -0
- package/src/render/template-source.ts +52 -0
- package/src/render/templates.ts +289 -0
- package/src/report/html.ts +2 -2
- package/src/retrieval/bm25.ts +161 -0
- package/src/retrieval/project.ts +495 -0
- package/src/retrieval/rank.ts +383 -0
- package/src/retrieval/weights.ts +39 -0
- package/src/retriever/doc-bridge-retriever.ts +100 -15
- package/src/rules/engine.ts +30 -10
- package/src/schemas/agent-handoff.ts +56 -0
- package/src/schemas/budget.ts +37 -0
- package/src/schemas/doc-bridge-index.ts +53 -2
- package/src/schemas/enrichment.ts +369 -0
- package/src/schemas/json-schemas.ts +39 -2
- package/src/schemas/knowledge.ts +14 -1
- package/src/schemas/retrieval-index.ts +152 -0
- package/src/shims/graphology.d.ts +91 -0
- package/src/study/adjudication.ts +196 -0
- package/src/study/execution.ts +350 -0
- package/src/study/expectations.ts +219 -0
- package/src/study/metrics.ts +467 -0
- package/src/study/protocol.ts +271 -0
- package/src/study/provider-cli.ts +115 -0
- package/src/study/provider-telemetry.ts +47 -0
- package/src/study/quality-scorecard.ts +164 -0
- package/src/study/runner.ts +461 -0
- package/src/study/task-suite.ts +321 -0
- package/src/study/verification.ts +134 -0
- package/src/validate.ts +1 -4
- package/src/version.ts +1 -1
- package/src/workflow/engine.ts +36 -11
- package/scripts/verification-harness.mjs +0 -482
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { minimatch } from 'minimatch'
|
|
4
|
+
|
|
5
|
+
import type { DocumentationAuditConfig } from '../config/schema.js'
|
|
6
|
+
import { contentHashForArtifactV1, sha256NormalizedV1 } from '../index-builder/content-hash.js'
|
|
7
|
+
import { frontmatterString, parseFrontmatter } from '../lib/markdown.js'
|
|
8
|
+
import type { DocumentationDiagnostic } from '../discovery/documentation.js'
|
|
9
|
+
import { generatedRegionsOf } from '../render/data.js'
|
|
10
|
+
import { verifyGeneratedRegions } from '../render/generated.js'
|
|
11
|
+
import {
|
|
12
|
+
DiagnosticSeveritySchema,
|
|
13
|
+
EvidenceSchema,
|
|
14
|
+
FindingStatusSchema,
|
|
15
|
+
type DiscoverySnapshotV1,
|
|
16
|
+
type Evidence,
|
|
17
|
+
type FindingStatus,
|
|
18
|
+
type ReconciliationReportV1,
|
|
19
|
+
} from '../schemas/knowledge.js'
|
|
20
|
+
import { z } from 'zod'
|
|
21
|
+
|
|
22
|
+
export const DOCUMENTATION_AUDIT_SCHEMA_VERSION = 1 as const
|
|
23
|
+
|
|
24
|
+
const AuditCategorySchema = z.enum([
|
|
25
|
+
'quality',
|
|
26
|
+
'coverage',
|
|
27
|
+
'structure-gap',
|
|
28
|
+
'contradiction',
|
|
29
|
+
'stale',
|
|
30
|
+
'redundancy',
|
|
31
|
+
'generated-freshness',
|
|
32
|
+
'limitation',
|
|
33
|
+
])
|
|
34
|
+
const AuditConfidenceSchema = z.enum(['high', 'medium', 'low'])
|
|
35
|
+
const DocumentationTierSchema = z.enum(['tier-0', 'tier-1', 'tier-2'])
|
|
36
|
+
const DocumentationDimensionStatusSchema = z.enum(['validated', 'partial', 'not-analyzed'])
|
|
37
|
+
const DimensionAssessmentSchema = z.object({
|
|
38
|
+
status: DocumentationDimensionStatusSchema,
|
|
39
|
+
reason: z.string().min(1).max(1_024),
|
|
40
|
+
}).strict()
|
|
41
|
+
|
|
42
|
+
export const DocumentationAuditFindingSchema = z.object({
|
|
43
|
+
id: z.string().regex(/^[a-f0-9]{64}$/),
|
|
44
|
+
code: z.string().min(1).max(128),
|
|
45
|
+
category: AuditCategorySchema,
|
|
46
|
+
status: FindingStatusSchema,
|
|
47
|
+
severity: DiagnosticSeveritySchema,
|
|
48
|
+
confidence: AuditConfidenceSchema,
|
|
49
|
+
blocking: z.boolean(),
|
|
50
|
+
message: z.string().min(1).max(2_048),
|
|
51
|
+
evidence: z.array(EvidenceSchema).max(64),
|
|
52
|
+
remediation: z.string().max(2_048).optional(),
|
|
53
|
+
}).strict()
|
|
54
|
+
|
|
55
|
+
export const DocumentationAuditDocumentSchema = z.object({
|
|
56
|
+
path: z.string().min(1).max(512),
|
|
57
|
+
classification: z.object({
|
|
58
|
+
type: z.string().min(1).max(128),
|
|
59
|
+
audience: z.string().min(1).max(128),
|
|
60
|
+
lifecycle: z.string().min(1).max(128),
|
|
61
|
+
tier: DocumentationTierSchema,
|
|
62
|
+
critical: z.boolean(),
|
|
63
|
+
}).strict(),
|
|
64
|
+
metadata: z.object({
|
|
65
|
+
owner: z.boolean(),
|
|
66
|
+
lifecycle: z.boolean(),
|
|
67
|
+
sourceOfTruth: z.boolean(),
|
|
68
|
+
validationPath: z.boolean(),
|
|
69
|
+
complete: z.boolean(),
|
|
70
|
+
missing: z.array(z.enum(['owner', 'lifecycle', 'sourceOfTruth', 'validationPath'])).max(4),
|
|
71
|
+
}).strict(),
|
|
72
|
+
dimensions: z.object({
|
|
73
|
+
correctness: DimensionAssessmentSchema,
|
|
74
|
+
completeness: DimensionAssessmentSchema,
|
|
75
|
+
clarity: DimensionAssessmentSchema,
|
|
76
|
+
agentEfficiency: DimensionAssessmentSchema,
|
|
77
|
+
maintainability: DimensionAssessmentSchema,
|
|
78
|
+
}).strict(),
|
|
79
|
+
example: z.object({ present: z.boolean(), validation: DocumentationDimensionStatusSchema }).strict(),
|
|
80
|
+
}).strict()
|
|
81
|
+
|
|
82
|
+
export type DocumentationAuditDocument = z.infer<typeof DocumentationAuditDocumentSchema>
|
|
83
|
+
|
|
84
|
+
export const DocumentationAuditReportV1Schema = z.object({
|
|
85
|
+
type: z.literal('documentation-audit-report'),
|
|
86
|
+
schemaVersion: z.literal(DOCUMENTATION_AUDIT_SCHEMA_VERSION),
|
|
87
|
+
contentHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
88
|
+
contentHashAlgo: z.literal('sha256-normalized-v1'),
|
|
89
|
+
project: z.object({ name: z.string().min(1).max(128), root: z.string().max(512).optional() }).strict(),
|
|
90
|
+
sourceRevision: z.string().min(1).max(128),
|
|
91
|
+
sourceRevisionKind: z.enum(['git', 'content']),
|
|
92
|
+
configurationHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
93
|
+
pipelineVersion: z.string().min(1).max(64),
|
|
94
|
+
analyzerVersions: z.record(z.string().min(1).max(128), z.string().min(1).max(64)),
|
|
95
|
+
snapshotHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
96
|
+
reconciliationHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
97
|
+
status: z.enum(['pass', 'needs-review', 'blocked']),
|
|
98
|
+
findings: z.array(DocumentationAuditFindingSchema).max(100_000),
|
|
99
|
+
metrics: z.object({
|
|
100
|
+
documentCount: z.number().int().nonnegative(),
|
|
101
|
+
generatedDocumentCount: z.number().int().nonnegative(),
|
|
102
|
+
packageCount: z.number().int().nonnegative(),
|
|
103
|
+
coveredPackageCount: z.number().int().nonnegative(),
|
|
104
|
+
/** The unit `packageCount` counts: areas in a single-package repository, packages otherwise. */
|
|
105
|
+
coverageUnit: z.enum(['package', 'area']).optional(),
|
|
106
|
+
coverageRate: z.number().min(0).max(1).nullable(),
|
|
107
|
+
documentsWithTitle: z.number().int().nonnegative(),
|
|
108
|
+
titleRate: z.number().min(0).max(1).nullable(),
|
|
109
|
+
documentsWithExamples: z.number().int().nonnegative(),
|
|
110
|
+
examplesRate: z.number().min(0).max(1).nullable(),
|
|
111
|
+
documentsMeetingRequiredSections: z.number().int().nonnegative(),
|
|
112
|
+
requiredSectionsRate: z.number().min(0).max(1).nullable(),
|
|
113
|
+
exactDuplicateGroups: z.number().int().nonnegative(),
|
|
114
|
+
structureGapCount: z.number().int().nonnegative(),
|
|
115
|
+
contradictionCount: z.number().int().nonnegative(),
|
|
116
|
+
staleCount: z.number().int().nonnegative(),
|
|
117
|
+
notAnalyzedCount: z.number().int().nonnegative(),
|
|
118
|
+
blockingCount: z.number().int().nonnegative(),
|
|
119
|
+
tierCounts: z.object({ 'tier-0': z.number().int().nonnegative(), 'tier-1': z.number().int().nonnegative(), 'tier-2': z.number().int().nonnegative() }).strict(),
|
|
120
|
+
criticalDocumentCount: z.number().int().nonnegative(),
|
|
121
|
+
criticalDocumentsWithOwner: z.number().int().nonnegative(),
|
|
122
|
+
criticalDocumentsWithLifecycle: z.number().int().nonnegative(),
|
|
123
|
+
criticalDocumentsWithSourceOfTruth: z.number().int().nonnegative(),
|
|
124
|
+
criticalDocumentsWithValidationPath: z.number().int().nonnegative(),
|
|
125
|
+
dimensionStatus: z.object({
|
|
126
|
+
correctness: z.object({ validated: z.number().int().nonnegative(), partial: z.number().int().nonnegative(), 'not-analyzed': z.number().int().nonnegative() }).strict(),
|
|
127
|
+
completeness: z.object({ validated: z.number().int().nonnegative(), partial: z.number().int().nonnegative(), 'not-analyzed': z.number().int().nonnegative() }).strict(),
|
|
128
|
+
clarity: z.object({ validated: z.number().int().nonnegative(), partial: z.number().int().nonnegative(), 'not-analyzed': z.number().int().nonnegative() }).strict(),
|
|
129
|
+
agentEfficiency: z.object({ validated: z.number().int().nonnegative(), partial: z.number().int().nonnegative(), 'not-analyzed': z.number().int().nonnegative() }).strict(),
|
|
130
|
+
maintainability: z.object({ validated: z.number().int().nonnegative(), partial: z.number().int().nonnegative(), 'not-analyzed': z.number().int().nonnegative() }).strict(),
|
|
131
|
+
}).strict(),
|
|
132
|
+
}).strict(),
|
|
133
|
+
documentAssessments: z.array(DocumentationAuditDocumentSchema).max(100_000),
|
|
134
|
+
generatedDocuments: z.array(z.object({ path: z.string().min(1).max(512), freshness: z.literal('not-analyzed') }).strict()).max(128),
|
|
135
|
+
limitations: z.array(z.string().min(1).max(1_024)).max(32),
|
|
136
|
+
}).strict()
|
|
137
|
+
|
|
138
|
+
export type DocumentationAuditFinding = z.infer<typeof DocumentationAuditFindingSchema>
|
|
139
|
+
export type DocumentationAuditReportV1 = z.infer<typeof DocumentationAuditReportV1Schema>
|
|
140
|
+
|
|
141
|
+
type DocumentInput = { readonly path: string; readonly content: string }
|
|
142
|
+
|
|
143
|
+
const normalizedPath = (path: string): string => path.replaceAll('\\', '/').replace(/^\.\//, '')
|
|
144
|
+
const evidenceFor = (path: string, lineStart?: number): Evidence => ({
|
|
145
|
+
source: 'documentation',
|
|
146
|
+
path: normalizedPath(path),
|
|
147
|
+
...(lineStart === undefined ? {} : { lineStart }),
|
|
148
|
+
})
|
|
149
|
+
const derivedEvidence: Evidence = { source: 'derived', path: '.doc-bridge/documentation-audit' }
|
|
150
|
+
const words = (content: string): number => content.replace(/```[\s\S]*?```/g, ' ').match(/[A-Za-z0-9][A-Za-z0-9'-]*/g)?.length ?? 0
|
|
151
|
+
const hasTitle = (content: string): boolean => {
|
|
152
|
+
if (frontmatterString(parseFrontmatter(content).data, 'title')) return true
|
|
153
|
+
return /^\s*#\s+\S/m.test(content) || /<h1(?:\s[^>]*)?>\s*[^<]+\s*<\/h1>/i.test(content)
|
|
154
|
+
}
|
|
155
|
+
const hasExample = (content: string): boolean => /```[\s\S]*?```/m.test(content) || /^#{1,6}\s+(?:examples?|usage)\b/im.test(content)
|
|
156
|
+
const hasHeading = (content: string, section: string): boolean => {
|
|
157
|
+
const escaped = section.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
158
|
+
return new RegExp(`^#{1,6}\\s+${escaped}\\s*$`, 'im').test(content)
|
|
159
|
+
}
|
|
160
|
+
const bodyForDuplicate = (content: string): string => {
|
|
161
|
+
const lines = content.replace(/^\uFEFF/, '').split(/\r?\n/)
|
|
162
|
+
const start = lines[0] === '---' ? lines.findIndex((line, index) => index > 0 && line === '---') + 1 : 0
|
|
163
|
+
return lines.slice(start).join('\n').replace(/\s+/g, ' ').trim().toLocaleLowerCase()
|
|
164
|
+
}
|
|
165
|
+
const matches = (path: string, patterns: readonly string[]): boolean => patterns.some((pattern) => minimatch(path, pattern, { dot: true }))
|
|
166
|
+
const rate = (count: number, total: number): number | null => total ? count / total : null
|
|
167
|
+
type DocumentationTier = z.infer<typeof DocumentationTierSchema>
|
|
168
|
+
type DocumentationDimensionStatus = z.infer<typeof DocumentationDimensionStatusSchema>
|
|
169
|
+
|
|
170
|
+
const metadataPresent = (content: string, key: string): boolean => {
|
|
171
|
+
const value = parseFrontmatter(content).data[key]
|
|
172
|
+
return typeof value === 'string' ? value.trim().length > 0 : value === true
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const inferredDocumentType = (path: string): string => {
|
|
176
|
+
const lower = path.toLocaleLowerCase()
|
|
177
|
+
if (lower === 'agents.md' || lower.endsWith('/agents.md') || lower.includes('/for-agents/')) return 'agent-guidance'
|
|
178
|
+
if (lower.includes('/adr/') || lower.startsWith('adr/')) return 'architecture-decision'
|
|
179
|
+
if (lower.includes('runbook') || lower.includes('/operations/')) return 'runbook'
|
|
180
|
+
if (lower.includes('security')) return 'security'
|
|
181
|
+
if (lower.includes('contribut')) return 'contribution'
|
|
182
|
+
if (lower.includes('architecture')) return 'architecture'
|
|
183
|
+
if (lower.includes('/api/') || lower.includes('/reference/')) return 'reference'
|
|
184
|
+
if (lower.includes('/example') || lower.includes('/recipe')) return 'example'
|
|
185
|
+
return 'guide'
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const inferredAudience = (path: string): string => {
|
|
189
|
+
const lower = path.toLocaleLowerCase()
|
|
190
|
+
if (lower.includes('/agent-corpus/') || lower.includes('/for-agents/') || lower.endsWith('agents.md')) return 'agent'
|
|
191
|
+
if (lower === 'readme.md' || lower.includes('/readme.')) return 'human-and-agent'
|
|
192
|
+
return 'human'
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const inferredLifecycle = (path: string): string => /(?:^|\/)(?:archive|archived|historical)(?:\/|$)/i.test(path) ? 'archived' : 'active'
|
|
196
|
+
|
|
197
|
+
const inferredTier = (path: string): DocumentationTier => {
|
|
198
|
+
const lower = path.toLocaleLowerCase()
|
|
199
|
+
if (lower === 'agents.md' || lower.endsWith('/agents.md') || lower.includes('/agent-corpus/') || lower.includes('/for-agents/') || lower.includes('security') || lower.includes('contribut') || lower.includes('runbook') || lower.includes('/operations/')) return 'tier-0'
|
|
200
|
+
if (lower.includes('/adr/') || lower.includes('architecture') || lower.includes('/api/') || lower.includes('/integration') || lower.includes('/packages/') || lower.includes('/apps/') || lower.includes('/spec/')) return 'tier-1'
|
|
201
|
+
return 'tier-2'
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const tierFor = (path: string, content: string, config: DocumentationAuditConfig): { readonly tier: DocumentationTier; readonly critical: boolean } => {
|
|
205
|
+
const data = parseFrontmatter(content).data
|
|
206
|
+
const rule = config.tierRules?.find((candidate) => matches(path, [candidate.pattern]))
|
|
207
|
+
const explicit = frontmatterString(data, 'tier')
|
|
208
|
+
const tier = (explicit === 'tier-0' || explicit === 'tier-1' || explicit === 'tier-2')
|
|
209
|
+
? explicit
|
|
210
|
+
: rule?.tier ?? config.defaultTier ?? inferredTier(path)
|
|
211
|
+
const explicitCritical = data.critical === true ? true : data.critical === false ? false : undefined
|
|
212
|
+
return { tier, critical: explicitCritical ?? rule?.critical ?? tier === 'tier-0' }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const dimension = (status: DocumentationDimensionStatus, reason: string): { readonly status: DocumentationDimensionStatus; readonly reason: string } => ({ status, reason })
|
|
216
|
+
|
|
217
|
+
const isCritical = (finding: Pick<DocumentationAuditFinding, 'evidence'>, paths: readonly string[]): boolean =>
|
|
218
|
+
paths.length > 0 && finding.evidence.some((item) => matches(item.path, paths))
|
|
219
|
+
|
|
220
|
+
const auditId = (code: string, value: unknown): string => sha256NormalizedV1({ code, value })
|
|
221
|
+
|
|
222
|
+
const createFinding = (
|
|
223
|
+
code: string,
|
|
224
|
+
category: DocumentationAuditFinding['category'],
|
|
225
|
+
status: FindingStatus,
|
|
226
|
+
severity: DocumentationAuditFinding['severity'],
|
|
227
|
+
confidence: DocumentationAuditFinding['confidence'],
|
|
228
|
+
message: string,
|
|
229
|
+
evidence: readonly Evidence[],
|
|
230
|
+
value: unknown,
|
|
231
|
+
criticalPaths: readonly string[],
|
|
232
|
+
remediation?: string,
|
|
233
|
+
): DocumentationAuditFinding => {
|
|
234
|
+
const normalizedEvidence = [...evidence].sort((a, b) => `${a.path}:${a.lineStart ?? 0}`.localeCompare(`${b.path}:${b.lineStart ?? 0}`))
|
|
235
|
+
const blocking = confidence === 'high' && isCritical({ evidence: normalizedEvidence }, criticalPaths)
|
|
236
|
+
return {
|
|
237
|
+
id: auditId(code, value),
|
|
238
|
+
code,
|
|
239
|
+
category,
|
|
240
|
+
status,
|
|
241
|
+
severity: blocking ? 'error' : severity,
|
|
242
|
+
confidence,
|
|
243
|
+
blocking,
|
|
244
|
+
message,
|
|
245
|
+
evidence: normalizedEvidence,
|
|
246
|
+
...(remediation ? { remediation } : {}),
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const diagnosticMapping = (diagnostic: ReconciliationReportV1['diagnostics'][number]): {
|
|
251
|
+
category: DocumentationAuditFinding['category']; status: FindingStatus; confidence: DocumentationAuditFinding['confidence']; severity: DocumentationAuditFinding['severity']
|
|
252
|
+
} | undefined => {
|
|
253
|
+
if (diagnostic.code === 'RELATION_CONFIRMED') return undefined
|
|
254
|
+
if (diagnostic.code === 'DOCUMENTATION_ORPHANED') return undefined
|
|
255
|
+
if (diagnostic.code === 'RELATION_UNDOCUMENTED') return { category: 'structure-gap', status: 'undocumented', confidence: 'high', severity: diagnostic.severity }
|
|
256
|
+
if (diagnostic.code === 'CONFLICTING_DECLARATIONS') return { category: 'contradiction', status: 'conflict', confidence: 'high', severity: diagnostic.severity }
|
|
257
|
+
if (diagnostic.code === 'DECLARED_RELATION_STALE') return { category: 'stale', status: 'stale-or-unverified', confidence: 'high', severity: diagnostic.severity }
|
|
258
|
+
if (diagnostic.code === 'OWNERSHIP_PATH_UNOBSERVED') return { category: 'stale', status: 'stale-or-unverified', confidence: 'high', severity: diagnostic.severity }
|
|
259
|
+
// A cycle is an architecture finding, not a documentation one: reported, never counted as a gap.
|
|
260
|
+
if (diagnostic.code === 'IMPORT_CYCLE') return undefined
|
|
261
|
+
if (diagnostic.code === 'RELATION_NOT_ANALYZED') return { category: 'limitation', status: 'not-analyzed', confidence: 'low', severity: diagnostic.severity }
|
|
262
|
+
if (diagnostic.code === 'UNRESOLVED_ENTITY_REFERENCE') return { category: 'contradiction', status: 'unresolved', confidence: 'high', severity: diagnostic.severity }
|
|
263
|
+
return { category: 'quality', status: 'unresolved', confidence: 'high', severity: diagnostic.severity }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export type DocumentationAuditOptions = {
|
|
267
|
+
readonly root: string
|
|
268
|
+
readonly snapshot: DiscoverySnapshotV1
|
|
269
|
+
readonly declared: DiscoverySnapshotV1
|
|
270
|
+
readonly reconciliation: ReconciliationReportV1
|
|
271
|
+
readonly declarationDiagnostics?: readonly DocumentationDiagnostic[]
|
|
272
|
+
readonly config?: DocumentationAuditConfig
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export const auditDocumentation = (options: DocumentationAuditOptions): DocumentationAuditReportV1 => {
|
|
276
|
+
const config = options.config ?? {}
|
|
277
|
+
const excluded = config.exclude ?? []
|
|
278
|
+
const generatedPaths = config.generatedPaths ?? []
|
|
279
|
+
const criticalPaths = config.criticalPaths ?? []
|
|
280
|
+
const requiredSections = config.requiredSections ?? []
|
|
281
|
+
const documents: DocumentInput[] = options.snapshot.entities
|
|
282
|
+
.filter((entity) => entity.kind === 'document' && entity.path)
|
|
283
|
+
.map((entity) => ({ path: normalizedPath(entity.path as string), content: readFileSync(resolve(options.root, entity.path as string), 'utf8') }))
|
|
284
|
+
.filter((document) => !matches(document.path, excluded))
|
|
285
|
+
.sort((a, b) => a.path.localeCompare(b.path))
|
|
286
|
+
const findings: DocumentationAuditFinding[] = []
|
|
287
|
+
const generated = documents.filter((document) => matches(document.path, generatedPaths))
|
|
288
|
+
const analyzed = documents.filter((document) => !matches(document.path, generatedPaths))
|
|
289
|
+
|
|
290
|
+
/*
|
|
291
|
+
* A generated region whose body no longer hashes to what its marker claims was edited by hand
|
|
292
|
+
* (or written by a different generator). The analyzer records the regions on the document
|
|
293
|
+
* entity; the audit only recomputes the hash of the lines between the markers. Regenerating
|
|
294
|
+
* the region would silently discard the edit, which is why it is a finding first.
|
|
295
|
+
*/
|
|
296
|
+
const documentEntities = new Map(
|
|
297
|
+
options.snapshot.entities
|
|
298
|
+
.filter((entity) => entity.kind === 'document' && entity.path)
|
|
299
|
+
.map((entity) => [normalizedPath(entity.path as string), entity] as const),
|
|
300
|
+
)
|
|
301
|
+
for (const document of documents) {
|
|
302
|
+
for (const mismatch of verifyGeneratedRegions(document.content, generatedRegionsOf(documentEntities.get(document.path)))) {
|
|
303
|
+
findings.push(createFinding(
|
|
304
|
+
'GENERATED_REGION_EDITED', 'generated-freshness', 'stale-or-unverified', 'warn', 'high',
|
|
305
|
+
`Generated region at ${document.path}:${mismatch.lineStart}-${mismatch.lineEnd} no longer matches its marker: the marker says hash=${mismatch.expected}, the content hashes to ${mismatch.actual}. It was edited by hand or written by a different generator.`,
|
|
306
|
+
[{ source: 'documentation', path: document.path, lineStart: mismatch.lineStart, lineEnd: mismatch.lineEnd }],
|
|
307
|
+
`${document.path}:${mismatch.lineStart}`, criticalPaths,
|
|
308
|
+
'Regenerate the region with ak-docs render, or move the manual text outside the generated markers so it is kept.',
|
|
309
|
+
))
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/*
|
|
313
|
+
* The unit coverage is measured against.
|
|
314
|
+
*
|
|
315
|
+
* Packages, except in a single-package repository — there the only package is the repository
|
|
316
|
+
* itself, the filter left nothing, and the audit reported "Packages covered: 0/0" while the
|
|
317
|
+
* doctor reported full health. Most repositories are one package, so for them the unit is the
|
|
318
|
+
* area: the directory level that actually has an owner and a document.
|
|
319
|
+
*/
|
|
320
|
+
const packageCandidates = options.snapshot.entities.filter((entity) => entity.kind === 'package')
|
|
321
|
+
const singlePackage = packageCandidates.length === 1
|
|
322
|
+
const coverageUnitKind = singlePackage ? 'area' : 'package'
|
|
323
|
+
const packageEntities = options.snapshot.entities
|
|
324
|
+
.filter((entity) =>
|
|
325
|
+
singlePackage ? entity.kind === 'area' : entity.kind === 'package' && entity.path !== '.',
|
|
326
|
+
)
|
|
327
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
328
|
+
const coveredPackages = new Set(options.declared.relations.filter((relation) => relation.kind === 'covers' && relation.from.startsWith('document:')).map((relation) => relation.to))
|
|
329
|
+
const requiredCriticalMetadata = config.requiredCriticalMetadata ?? ['owner', 'lifecycle', 'sourceOfTruth', 'validationPath']
|
|
330
|
+
const documentAssessments: DocumentationAuditDocument[] = documents.map((document) => {
|
|
331
|
+
const data = parseFrontmatter(document.content).data
|
|
332
|
+
const tiering = tierFor(document.path, document.content, config)
|
|
333
|
+
const metadata = {
|
|
334
|
+
owner: metadataPresent(document.content, 'owner'),
|
|
335
|
+
lifecycle: metadataPresent(document.content, 'lifecycle'),
|
|
336
|
+
sourceOfTruth: metadataPresent(document.content, 'sourceOfTruth'),
|
|
337
|
+
validationPath: metadataPresent(document.content, 'validationPath'),
|
|
338
|
+
}
|
|
339
|
+
const missing = requiredCriticalMetadata.filter((key) => !metadata[key])
|
|
340
|
+
const metadataState = { ...metadata, complete: missing.length === 0, missing }
|
|
341
|
+
const title = hasTitle(document.content)
|
|
342
|
+
const examples = hasExample(document.content)
|
|
343
|
+
const sections = requiredSections.every((section) => hasHeading(document.content, section))
|
|
344
|
+
const qualityAnalyzed = !matches(document.path, generatedPaths)
|
|
345
|
+
const assessment: DocumentationAuditDocument = {
|
|
346
|
+
path: document.path,
|
|
347
|
+
classification: {
|
|
348
|
+
type: frontmatterString(data, 'type') ?? inferredDocumentType(document.path),
|
|
349
|
+
audience: frontmatterString(data, 'audience') ?? inferredAudience(document.path),
|
|
350
|
+
lifecycle: frontmatterString(data, 'lifecycle') ?? inferredLifecycle(document.path),
|
|
351
|
+
tier: tiering.tier,
|
|
352
|
+
critical: tiering.critical,
|
|
353
|
+
},
|
|
354
|
+
metadata: {
|
|
355
|
+
...metadataState,
|
|
356
|
+
},
|
|
357
|
+
dimensions: {
|
|
358
|
+
correctness: dimension('not-analyzed', qualityAnalyzed ? 'Semantic correctness requires code, configuration, and applicable runtime evidence review.' : 'Generated-document correctness is outside this deterministic audit.'),
|
|
359
|
+
completeness: dimension(!qualityAnalyzed ? 'not-analyzed' : title && sections ? 'partial' : 'not-analyzed', title && sections ? 'Required structural signals are present; semantic completeness remains unverified.' : 'Required structural signals are incomplete or not configured.'),
|
|
360
|
+
clarity: dimension(!qualityAnalyzed ? 'not-analyzed' : title ? 'partial' : 'not-analyzed', title ? 'Title and basic structure are present; human clarity review remains unverified.' : 'A title is required before clarity can be assessed.'),
|
|
361
|
+
agentEfficiency: dimension(!qualityAnalyzed ? 'not-analyzed' : title && examples ? 'partial' : 'not-analyzed', title && examples ? 'Title and an example are present; task usefulness and retrieval efficiency remain unverified.' : 'Agent efficiency requires a clear title and example before semantic review.'),
|
|
362
|
+
maintainability: dimension(metadataState.complete ? 'validated' : 'partial', metadataState.complete ? 'Required maintainability metadata is present.' : 'Ownership, lifecycle, source-of-truth, or validation metadata is incomplete.'),
|
|
363
|
+
},
|
|
364
|
+
example: { present: examples, validation: 'not-analyzed' },
|
|
365
|
+
}
|
|
366
|
+
if (tiering.critical && qualityAnalyzed && missing.length > 0) findings.push(createFinding(
|
|
367
|
+
'DOCUMENTATION_CRITICAL_METADATA_MISSING', 'quality', 'undocumented', 'warn', 'high',
|
|
368
|
+
`Critical ${document.path} is missing maintainability metadata: ${missing.join(', ')}.`,
|
|
369
|
+
[evidenceFor(document.path)], [document.path, missing], criticalPaths,
|
|
370
|
+
'Add the missing owner, lifecycle, source-of-truth, or validation-path metadata, or declare a tracked exception.',
|
|
371
|
+
))
|
|
372
|
+
return assessment
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
for (const document of generated) {
|
|
376
|
+
findings.push(createFinding(
|
|
377
|
+
'GENERATED_DOCUMENT_FRESHNESS_UNVERIFIED', 'generated-freshness', 'not-analyzed', 'info', 'low',
|
|
378
|
+
'Generated documentation is included in the corpus, but this audit only verifies that it is present; freshness must be proven by its generating check.',
|
|
379
|
+
[evidenceFor(document.path)], document.path, criticalPaths,
|
|
380
|
+
'Configure the generator command as a freshness check and keep generated content out of direct manual edits.',
|
|
381
|
+
))
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
for (const document of analyzed) {
|
|
385
|
+
const evidence = evidenceFor(document.path)
|
|
386
|
+
if (!hasTitle(document.content)) findings.push(createFinding('DOCUMENTATION_TITLE_MISSING', 'quality', 'undocumented', 'warn', 'high', 'Documentation file has no level-one title.', [evidence], document.path, criticalPaths, 'Add one concise level-one title that identifies the documented subject.'))
|
|
387
|
+
if (config.minWords !== undefined && words(document.content) < config.minWords) findings.push(createFinding('DOCUMENTATION_TOO_SHORT', 'quality', 'undocumented', 'warn', 'high', `Documentation file has fewer than ${config.minWords} words.`, [evidence], [document.path, config.minWords], criticalPaths, 'Add the missing context or lower the threshold when this file is intentionally a short index.'))
|
|
388
|
+
if (config.requireExamples && !hasExample(document.content)) findings.push(createFinding('DOCUMENTATION_EXAMPLE_MISSING', 'quality', 'undocumented', 'warn', 'high', 'Documentation file has no usage heading or fenced example.', [evidence], document.path, criticalPaths, 'Add a minimal, runnable example or explicitly exempt this document from example requirements.'))
|
|
389
|
+
const missingSections = requiredSections.filter((section) => !hasHeading(document.content, section))
|
|
390
|
+
if (missingSections.length) findings.push(createFinding('DOCUMENTATION_SECTIONS_MISSING', 'quality', 'undocumented', 'warn', 'high', `Documentation file is missing required section(s): ${missingSections.join(', ')}.`, [evidence], [document.path, missingSections], criticalPaths, 'Add the required sections or document a tracked exception.'))
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (config.exactDuplicates !== false) {
|
|
394
|
+
const duplicateGroups = new Map<string, DocumentInput[]>()
|
|
395
|
+
for (const document of analyzed) {
|
|
396
|
+
const body = bodyForDuplicate(document.content)
|
|
397
|
+
if (!body) continue
|
|
398
|
+
const group = duplicateGroups.get(body) ?? []
|
|
399
|
+
group.push(document)
|
|
400
|
+
duplicateGroups.set(body, group)
|
|
401
|
+
}
|
|
402
|
+
for (const group of [...duplicateGroups.values()].filter((items) => items.length > 1)) {
|
|
403
|
+
findings.push(createFinding('DOCUMENTATION_EXACT_DUPLICATE', 'redundancy', 'unresolved', 'warn', 'high', `Documentation files contain identical normalized content: ${group.map((item) => item.path).join(', ')}.`, group.map((item) => evidenceFor(item.path)), group.map((item) => item.path), criticalPaths, 'Keep one canonical document and replace the others with links or clearly differentiated scope.'))
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
for (const packageEntity of packageEntities) {
|
|
408
|
+
if (coveredPackages.has(packageEntity.id)) continue
|
|
409
|
+
const evidence = packageEntity.evidence.length ? packageEntity.evidence : [derivedEvidence]
|
|
410
|
+
const label = coverageUnitKind === 'area' ? 'Area' : 'Package'
|
|
411
|
+
findings.push(createFinding(
|
|
412
|
+
coverageUnitKind === 'area' ? 'AREA_DOCUMENTATION_MISSING' : 'PACKAGE_DOCUMENTATION_MISSING',
|
|
413
|
+
'coverage',
|
|
414
|
+
'undocumented',
|
|
415
|
+
'warn',
|
|
416
|
+
'high',
|
|
417
|
+
`${label} ${packageEntity.path ?? packageEntity.name} has no documentation coverage declaration.`,
|
|
418
|
+
evidence,
|
|
419
|
+
packageEntity.id,
|
|
420
|
+
criticalPaths,
|
|
421
|
+
`Add a docbridge covers declaration to the ${coverageUnitKind} documentation or explicitly exclude the ${coverageUnitKind}.`,
|
|
422
|
+
))
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
for (const diagnostic of options.reconciliation.diagnostics) {
|
|
426
|
+
const mapping = diagnosticMapping(diagnostic)
|
|
427
|
+
if (!mapping) continue
|
|
428
|
+
findings.push(createFinding(diagnostic.code, mapping.category, mapping.status, mapping.severity, mapping.confidence, diagnostic.message, diagnostic.evidence, diagnostic.id, criticalPaths, diagnostic.remediation))
|
|
429
|
+
}
|
|
430
|
+
for (const diagnostic of options.declarationDiagnostics ?? []) {
|
|
431
|
+
findings.push(createFinding(diagnostic.code, 'quality', 'unresolved', 'error', 'high', diagnostic.message, [diagnostic.evidence], diagnostic.code + diagnostic.evidence.path + diagnostic.evidence.lineStart, criticalPaths, 'Fix the declaration syntax or reference before relying on the documentation graph.'))
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
findings.push(createFinding('DOCUMENTATION_SEMANTICS_NOT_ANALYZED', 'limitation', 'not-analyzed', 'info', 'low', 'Natural-language redundancy, unnecessary prose, and contradictions not expressed as structured Doc Bridge claims require agent or human review.', [derivedEvidence], options.snapshot.contentHash, criticalPaths, 'Run a configured Registry agent review and require human approval before applying any proposed documentation change.'))
|
|
435
|
+
const sortedFindings = [...new Map(findings.map((finding) => [finding.id, finding])).values()].sort((a, b) => a.id.localeCompare(b.id))
|
|
436
|
+
const qualityDocs = analyzed.length
|
|
437
|
+
const requiredSectionDocs = analyzed.filter((document) => requiredSections.every((section) => hasHeading(document.content, section))).length
|
|
438
|
+
const statusCounts = (dimensionName: keyof DocumentationAuditDocument['dimensions']) => ({
|
|
439
|
+
validated: documentAssessments.filter((assessment) => assessment.dimensions[dimensionName].status === 'validated').length,
|
|
440
|
+
partial: documentAssessments.filter((assessment) => assessment.dimensions[dimensionName].status === 'partial').length,
|
|
441
|
+
'not-analyzed': documentAssessments.filter((assessment) => assessment.dimensions[dimensionName].status === 'not-analyzed').length,
|
|
442
|
+
})
|
|
443
|
+
const criticalAssessments = documentAssessments.filter((assessment) => assessment.classification.critical)
|
|
444
|
+
const base = {
|
|
445
|
+
type: 'documentation-audit-report' as const,
|
|
446
|
+
schemaVersion: DOCUMENTATION_AUDIT_SCHEMA_VERSION,
|
|
447
|
+
contentHash: '0'.repeat(64),
|
|
448
|
+
contentHashAlgo: 'sha256-normalized-v1' as const,
|
|
449
|
+
project: options.snapshot.project,
|
|
450
|
+
sourceRevision: options.snapshot.sourceRevision,
|
|
451
|
+
sourceRevisionKind: options.snapshot.sourceRevisionKind,
|
|
452
|
+
configurationHash: options.snapshot.configurationHash,
|
|
453
|
+
pipelineVersion: options.snapshot.pipelineVersion,
|
|
454
|
+
analyzerVersions: options.snapshot.analyzerVersions,
|
|
455
|
+
snapshotHash: options.snapshot.contentHash,
|
|
456
|
+
reconciliationHash: options.reconciliation.contentHash,
|
|
457
|
+
status: sortedFindings.some((finding) => finding.blocking) ? 'blocked' as const : sortedFindings.some((finding) => finding.severity === 'warn' || finding.status !== 'confirmed') ? 'needs-review' as const : 'pass' as const,
|
|
458
|
+
findings: sortedFindings,
|
|
459
|
+
metrics: {
|
|
460
|
+
documentCount: documents.length,
|
|
461
|
+
generatedDocumentCount: generated.length,
|
|
462
|
+
coverageUnit: coverageUnitKind,
|
|
463
|
+
packageCount: packageEntities.length,
|
|
464
|
+
coveredPackageCount: packageEntities.filter((entity) => coveredPackages.has(entity.id)).length,
|
|
465
|
+
coverageRate: rate(packageEntities.filter((entity) => coveredPackages.has(entity.id)).length, packageEntities.length),
|
|
466
|
+
documentsWithTitle: analyzed.filter((document) => hasTitle(document.content)).length,
|
|
467
|
+
titleRate: rate(analyzed.filter((document) => hasTitle(document.content)).length, qualityDocs),
|
|
468
|
+
documentsWithExamples: analyzed.filter((document) => hasExample(document.content)).length,
|
|
469
|
+
examplesRate: rate(analyzed.filter((document) => hasExample(document.content)).length, qualityDocs),
|
|
470
|
+
documentsMeetingRequiredSections: requiredSectionDocs,
|
|
471
|
+
requiredSectionsRate: rate(requiredSectionDocs, qualityDocs),
|
|
472
|
+
exactDuplicateGroups: sortedFindings.filter((finding) => finding.code === 'DOCUMENTATION_EXACT_DUPLICATE').length,
|
|
473
|
+
structureGapCount: sortedFindings.filter((finding) => finding.category === 'structure-gap' || finding.code === 'PACKAGE_DOCUMENTATION_MISSING' || finding.code === 'AREA_DOCUMENTATION_MISSING').length,
|
|
474
|
+
contradictionCount: sortedFindings.filter((finding) => finding.category === 'contradiction').length,
|
|
475
|
+
staleCount: sortedFindings.filter((finding) => finding.category === 'stale').length,
|
|
476
|
+
notAnalyzedCount: sortedFindings.filter((finding) => finding.status === 'not-analyzed').length,
|
|
477
|
+
blockingCount: sortedFindings.filter((finding) => finding.blocking).length,
|
|
478
|
+
tierCounts: {
|
|
479
|
+
'tier-0': documentAssessments.filter((assessment) => assessment.classification.tier === 'tier-0').length,
|
|
480
|
+
'tier-1': documentAssessments.filter((assessment) => assessment.classification.tier === 'tier-1').length,
|
|
481
|
+
'tier-2': documentAssessments.filter((assessment) => assessment.classification.tier === 'tier-2').length,
|
|
482
|
+
},
|
|
483
|
+
criticalDocumentCount: criticalAssessments.length,
|
|
484
|
+
criticalDocumentsWithOwner: criticalAssessments.filter((assessment) => assessment.metadata.owner).length,
|
|
485
|
+
criticalDocumentsWithLifecycle: criticalAssessments.filter((assessment) => assessment.metadata.lifecycle).length,
|
|
486
|
+
criticalDocumentsWithSourceOfTruth: criticalAssessments.filter((assessment) => assessment.metadata.sourceOfTruth).length,
|
|
487
|
+
criticalDocumentsWithValidationPath: criticalAssessments.filter((assessment) => assessment.metadata.validationPath).length,
|
|
488
|
+
dimensionStatus: {
|
|
489
|
+
correctness: statusCounts('correctness'),
|
|
490
|
+
completeness: statusCounts('completeness'),
|
|
491
|
+
clarity: statusCounts('clarity'),
|
|
492
|
+
agentEfficiency: statusCounts('agentEfficiency'),
|
|
493
|
+
maintainability: statusCounts('maintainability'),
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
documentAssessments,
|
|
497
|
+
generatedDocuments: generated.map((document) => ({ path: document.path, freshness: 'not-analyzed' as const })),
|
|
498
|
+
limitations: [
|
|
499
|
+
'Generated documentation receives presence and freshness-boundary reporting only; generator checks must prove freshness.',
|
|
500
|
+
'Natural-language semantic contradiction and unnecessary-content detection are not deterministic and remain not-analyzed.',
|
|
501
|
+
'Agent proposals are advisory and require human approval before any edit.',
|
|
502
|
+
],
|
|
503
|
+
}
|
|
504
|
+
return DocumentationAuditReportV1Schema.parse({ ...base, contentHash: contentHashForArtifactV1(base) })
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export const formatDocumentationAuditText = (report: DocumentationAuditReportV1): readonly string[] => [
|
|
508
|
+
`Documentation audit: ${report.status}`,
|
|
509
|
+
`Documents: ${report.metrics.documentCount} | ${report.metrics.coverageUnit === 'area' ? 'Areas' : 'Packages'} covered: ${report.metrics.coveredPackageCount}/${report.metrics.packageCount}`,
|
|
510
|
+
`Title: ${report.metrics.titleRate === null ? 'n/a' : `${Math.round(report.metrics.titleRate * 100)}%`} | Examples: ${report.metrics.examplesRate === null ? 'n/a' : `${Math.round(report.metrics.examplesRate * 100)}%`}`,
|
|
511
|
+
`Gaps: ${report.metrics.structureGapCount} | Contradictions: ${report.metrics.contradictionCount} | Stale: ${report.metrics.staleCount} | Not analyzed: ${report.metrics.notAnalyzedCount}`,
|
|
512
|
+
`Blocking findings: ${report.metrics.blockingCount}`,
|
|
513
|
+
]
|