@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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { unobservedOwnershipPaths } from '../discovery/areas.js'
|
|
2
|
+
import { importCycles } from '../graph/build.js'
|
|
1
3
|
import { contentHashForArtifactV1, sha256NormalizedV1 } from '../index-builder/content-hash.js'
|
|
2
4
|
import {
|
|
3
5
|
ReconciliationReportV1Schema,
|
|
@@ -12,13 +14,22 @@ type EntityResolver = (reference: string) => string
|
|
|
12
14
|
|
|
13
15
|
export type ReconciliationOptions = {
|
|
14
16
|
/** Compare declarations at a semantic level while retaining raw discovery evidence. */
|
|
15
|
-
readonly scope?: 'file' | 'module' | 'package'
|
|
17
|
+
readonly scope?: 'file' | 'module' | 'area' | 'package'
|
|
16
18
|
/** Omit for backwards-compatible all-relation checking; [] disables missing-declaration findings. */
|
|
17
19
|
readonly requiredRelationKinds?: readonly string[]
|
|
18
20
|
/** Limit missing-declaration findings to relations between internal project entities. */
|
|
19
21
|
readonly requiredRelationTargets?: 'all' | 'internal'
|
|
20
22
|
/** Emit one bounded finding for each observed Markdown document without declarations. */
|
|
21
23
|
readonly includeOrphanedDocuments?: boolean
|
|
24
|
+
/**
|
|
25
|
+
* Configured ownership records, so a path that matches nothing observed can be reported.
|
|
26
|
+
* Omit to skip the check.
|
|
27
|
+
*/
|
|
28
|
+
readonly ownership?: readonly { readonly id: string; readonly path: string }[]
|
|
29
|
+
/** Where the ownership records were configured, for the evidence trail. */
|
|
30
|
+
readonly ownershipSource?: string
|
|
31
|
+
/** Report import cycles. On by default; a cycle is structure, and structure is what this compares. */
|
|
32
|
+
readonly reportImportCycles?: boolean
|
|
22
33
|
}
|
|
23
34
|
|
|
24
35
|
const ignoredDocumentationRelations = new Set(['covers'])
|
|
@@ -114,13 +125,34 @@ const semanticEntityId = (
|
|
|
114
125
|
id: string,
|
|
115
126
|
scope: NonNullable<ReconciliationOptions['scope']>,
|
|
116
127
|
entities: ReadonlyMap<string, KnowledgeEntity>,
|
|
117
|
-
|
|
128
|
+
unitByModule: ReadonlyMap<string, string>,
|
|
118
129
|
): string => {
|
|
119
130
|
if (scope === 'file' || id.startsWith('external:') || id.startsWith('unresolved:')) return id
|
|
120
131
|
const entity = entities.get(id)
|
|
121
132
|
if (!entity || entity.kind !== 'module' || !entity.path) return id
|
|
122
133
|
if (scope === 'module') return id
|
|
123
|
-
return
|
|
134
|
+
return unitByModule.get(id) ?? id
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Which area each module belongs to, read from the containment the analyzer recorded.
|
|
139
|
+
*
|
|
140
|
+
* Areas exist so a single-package repository has something to compare. At package scope every
|
|
141
|
+
* internal relation in such a repository aggregates into one self-loop, which the comparison
|
|
142
|
+
* skips — a thousand observed relations and nothing to say. At area scope the same relations
|
|
143
|
+
* become area-to-area edges that a declaration can confirm or fail to.
|
|
144
|
+
*/
|
|
145
|
+
const areaLookup = (
|
|
146
|
+
scope: NonNullable<ReconciliationOptions['scope']>,
|
|
147
|
+
relations: readonly KnowledgeRelation[],
|
|
148
|
+
): Map<string, string> => {
|
|
149
|
+
if (scope !== 'area') return new Map()
|
|
150
|
+
const result = new Map<string, string>()
|
|
151
|
+
for (const relation of relations) {
|
|
152
|
+
if (relation.kind !== 'contains' || !relation.from.startsWith('area:') || !relation.to.startsWith('module:')) continue
|
|
153
|
+
if (!result.has(relation.to)) result.set(relation.to, relation.from)
|
|
154
|
+
}
|
|
155
|
+
return result
|
|
124
156
|
}
|
|
125
157
|
|
|
126
158
|
const packageLookup = (scope: NonNullable<ReconciliationOptions['scope']>, entities: ReadonlyMap<string, KnowledgeEntity>): Map<string, string> => {
|
|
@@ -143,13 +175,13 @@ const aggregatedRelations = (
|
|
|
143
175
|
relations: readonly KnowledgeRelation[],
|
|
144
176
|
scope: NonNullable<ReconciliationOptions['scope']>,
|
|
145
177
|
entities: ReadonlyMap<string, KnowledgeEntity>,
|
|
146
|
-
|
|
178
|
+
unitByModule: ReadonlyMap<string, string>,
|
|
147
179
|
): KnowledgeRelation[] => {
|
|
148
180
|
if (scope === 'file') return [...relations]
|
|
149
181
|
const groups = new Map<string, KnowledgeRelation[]>()
|
|
150
182
|
for (const relation of relations) {
|
|
151
|
-
const from = semanticEntityId(relation.from, scope, entities,
|
|
152
|
-
const to = semanticEntityId(relation.to, scope, entities,
|
|
183
|
+
const from = semanticEntityId(relation.from, scope, entities, unitByModule)
|
|
184
|
+
const to = semanticEntityId(relation.to, scope, entities, unitByModule)
|
|
153
185
|
const detection = relationDetection(relation)
|
|
154
186
|
const key = `${from}\u0000${to}\u0000${relation.kind}\u0000${detection}`
|
|
155
187
|
const group = groups.get(key) ?? []
|
|
@@ -216,10 +248,10 @@ export const reconcileKnowledge = (
|
|
|
216
248
|
const resolveEntity = entityResolver([observed, declared])
|
|
217
249
|
const entities = entityById([observed, declared])
|
|
218
250
|
const scope = options.scope ?? 'file'
|
|
219
|
-
const
|
|
251
|
+
const unitByModule = scope === 'area' ? areaLookup(scope, observed.relations) : packageLookup(scope, entities)
|
|
220
252
|
const allDeclaredRelations = declared.relations.filter((relation) => relation.provenance === 'declared')
|
|
221
|
-
const observedRelations = aggregatedRelations(observed.relations.filter((relation) => relation.provenance === 'observed' && !ignoredDocumentationRelations.has(relation.kind)), scope, entities,
|
|
222
|
-
const declaredRelations = aggregatedRelations(declared.relations.filter((relation) => relation.provenance === 'declared' && !ignoredDocumentationRelations.has(relation.kind)), scope, entities,
|
|
253
|
+
const observedRelations = aggregatedRelations(observed.relations.filter((relation) => relation.provenance === 'observed' && !ignoredDocumentationRelations.has(relation.kind)), scope, entities, unitByModule)
|
|
254
|
+
const declaredRelations = aggregatedRelations(declared.relations.filter((relation) => relation.provenance === 'declared' && !ignoredDocumentationRelations.has(relation.kind)), scope, entities, unitByModule)
|
|
223
255
|
const requiredRelationKinds = options.requiredRelationKinds === undefined ? undefined : new Set(options.requiredRelationKinds)
|
|
224
256
|
const diagnostics: ReconciliationReportV1['diagnostics'][number][] = []
|
|
225
257
|
|
|
@@ -264,11 +296,15 @@ export const reconcileKnowledge = (
|
|
|
264
296
|
}
|
|
265
297
|
|
|
266
298
|
const observedDetectionsByBase = new Map<string, Set<string>>()
|
|
299
|
+
const observedByBase = new Map<string, KnowledgeRelation[]>()
|
|
267
300
|
for (const relation of observedRelations) {
|
|
268
301
|
const base = relationBase(relation, resolveEntity)
|
|
269
302
|
const detections = observedDetectionsByBase.get(base) ?? new Set<string>()
|
|
270
303
|
detections.add(relationDetection(relation))
|
|
271
304
|
observedDetectionsByBase.set(base, detections)
|
|
305
|
+
const group = observedByBase.get(base) ?? []
|
|
306
|
+
group.push(relation)
|
|
307
|
+
observedByBase.set(base, group)
|
|
272
308
|
}
|
|
273
309
|
|
|
274
310
|
for (const group of declaredByBase.values()) {
|
|
@@ -323,7 +359,7 @@ export const reconcileKnowledge = (
|
|
|
323
359
|
}
|
|
324
360
|
|
|
325
361
|
for (const relation of declaredRelations) {
|
|
326
|
-
const candidates =
|
|
362
|
+
const candidates = observedByBase.get(relationBase(relation, resolveEntity)) ?? []
|
|
327
363
|
const detection = relationDetection(relation)
|
|
328
364
|
if (candidates.some((candidate) => relationMatches(candidate, relation, resolveEntity))) continue
|
|
329
365
|
if (isUnresolved(resolveEntity(relation.from), entities) || isUnresolved(resolveEntity(relation.to), entities)) continue
|
|
@@ -354,6 +390,61 @@ export const reconcileKnowledge = (
|
|
|
354
390
|
}
|
|
355
391
|
}
|
|
356
392
|
|
|
393
|
+
/*
|
|
394
|
+
* Import cycles.
|
|
395
|
+
*
|
|
396
|
+
* A cycle is not a documentation gap, so it is not an undocumented relation; it is a structural
|
|
397
|
+
* fact worth a reader's attention, with every edge that forms it as evidence — a diagnostic
|
|
398
|
+
* whose loop a reader cannot trace is a claim, not a finding.
|
|
399
|
+
*/
|
|
400
|
+
if (options.reportImportCycles !== false) {
|
|
401
|
+
for (const cycle of importCycles(observed)) {
|
|
402
|
+
diagnostics.push(reportDiagnostic(
|
|
403
|
+
'IMPORT_CYCLE',
|
|
404
|
+
'unresolved',
|
|
405
|
+
'warn',
|
|
406
|
+
`Import cycle across ${cycle.nodes.length} modules: ${cycle.nodes.join(' → ')} → ${cycle.nodes[0] ?? ''}.`,
|
|
407
|
+
cycle.evidence,
|
|
408
|
+
{ cycle: cycle.nodes },
|
|
409
|
+
cycle.nodes,
|
|
410
|
+
cycle.relationIds,
|
|
411
|
+
'Break the cycle by moving the shared code into a module both sides import, or invert one dependency.',
|
|
412
|
+
))
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/*
|
|
417
|
+
* An ownership path that matches nothing observed.
|
|
418
|
+
*
|
|
419
|
+
* A renamed or mistyped directory is invisible otherwise: the handoff still resolves and still
|
|
420
|
+
* looks correct, it just points an agent at a directory that no longer holds what it claims.
|
|
421
|
+
* The evidence is the configuration, because that is where the claim was made.
|
|
422
|
+
*/
|
|
423
|
+
if (options.ownership?.length) {
|
|
424
|
+
const observedPaths = observed.entities
|
|
425
|
+
.map((entity) => entity.path)
|
|
426
|
+
.filter((path): path is string => typeof path === 'string')
|
|
427
|
+
for (const record of unobservedOwnershipPaths(options.ownership, observedPaths)) {
|
|
428
|
+
diagnostics.push(reportDiagnostic(
|
|
429
|
+
'OWNERSHIP_PATH_UNOBSERVED',
|
|
430
|
+
'stale-or-unverified',
|
|
431
|
+
'warn',
|
|
432
|
+
`Ownership "${record.id}" declares path ${record.path}, which no observed module or document lives under.`,
|
|
433
|
+
[
|
|
434
|
+
{
|
|
435
|
+
source: 'configuration',
|
|
436
|
+
path: options.ownershipSource ?? 'doc-bridge.config.json',
|
|
437
|
+
context: `routing.options.ownership.${record.id}.path = ${record.path}`,
|
|
438
|
+
},
|
|
439
|
+
],
|
|
440
|
+
{ ownership: record.id, path: record.path },
|
|
441
|
+
undefined,
|
|
442
|
+
undefined,
|
|
443
|
+
'Point the ownership record at a directory that exists, or remove the record.',
|
|
444
|
+
))
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
357
448
|
const sortedDiagnostics = [...diagnostics].sort((a, b) => a.id.localeCompare(b.id))
|
|
358
449
|
const packageEntities = observed.entities.filter((entity) => entity.kind === 'package')
|
|
359
450
|
const packageStatus = { fresh: 0, stale: 0, missing: 0, unverified: 0 }
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import type { DocBridgeConfigV1 } from '../config/schema.js'
|
|
2
|
+
import { FILE_BACKED_KINDS } from '../discovery/incremental.js'
|
|
3
|
+
import { handoffForEntity } from '../query/handoff.js'
|
|
4
|
+
import type { AgentHandoffV1 } from '../schemas/agent-handoff.js'
|
|
5
|
+
import type { DocBridgeIndexV1 } from '../schemas/doc-bridge-index.js'
|
|
6
|
+
import type { DiscoverySnapshotV1, KnowledgeEntity, ReconciliationReportV1 } from '../schemas/knowledge.js'
|
|
7
|
+
import type { RetrievalEntry } from '../schemas/retrieval-index.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The view models the templates render.
|
|
11
|
+
*
|
|
12
|
+
* Every value a template can print is computed here, from the canonical artifacts and nothing
|
|
13
|
+
* else: the index and its projection, a reconciliation report, two snapshots, an overlay. Lists
|
|
14
|
+
* are sorted before they reach a template and nothing carries a timestamp, so the same artifacts
|
|
15
|
+
* render to the same bytes. The templates decide how a value looks; this decides what the values
|
|
16
|
+
* are, which is the boundary that keeps rendering deterministic and keeps a template from ever
|
|
17
|
+
* needing to call anything.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type RenderedPage = {
|
|
21
|
+
/** Where the page belongs, relative to the output directory. */
|
|
22
|
+
readonly path: string
|
|
23
|
+
readonly content: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const MAX_SYMBOLS = 12
|
|
27
|
+
const SHORT_HASH = 12
|
|
28
|
+
|
|
29
|
+
const byPath = <T extends { readonly path: string }>(a: T, b: T): number => a.path.localeCompare(b.path) || 0
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A file name for a unit: `src/query` becomes `src-query.md`, and nothing escapes the output
|
|
33
|
+
* directory. The ends are trimmed with a loop, because an alternation anchored at the end
|
|
34
|
+
* backtracks quadratically on a value that is mostly separators.
|
|
35
|
+
*/
|
|
36
|
+
export const pageFileName = (value: string): string => {
|
|
37
|
+
const slug = value.replace(/[^A-Za-z0-9._-]+/g, '-')
|
|
38
|
+
let start = 0
|
|
39
|
+
while (start < slug.length && (slug[start] === '-' || slug[start] === '.')) start += 1
|
|
40
|
+
let end = slug.length
|
|
41
|
+
while (end > start && slug[end - 1] === '-') end -= 1
|
|
42
|
+
return `${slug.slice(start, end) || 'root'}.md`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const shortHash = (hash: string | undefined): string => (hash ? hash.slice(0, SHORT_HASH) : '(none)')
|
|
46
|
+
|
|
47
|
+
const projectionOf = (index: DocBridgeIndexV1) => {
|
|
48
|
+
if (!index.projection) throw new Error('The index carries no retrieval projection; rebuild it with `ak-docs index` before rendering.')
|
|
49
|
+
return index.projection
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const checksSourceOf = (handoff: AgentHandoffV1 | undefined): string => {
|
|
53
|
+
const value = handoff?.metadata?.checksSource
|
|
54
|
+
return typeof value === 'string' ? value : ''
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const safeHandoff = (index: DocBridgeIndexV1, id: string, config: DocBridgeConfigV1, root: string | undefined): AgentHandoffV1 | undefined => {
|
|
58
|
+
try {
|
|
59
|
+
return handoffForEntity(index, id, config, root ? { root } : {})
|
|
60
|
+
} catch {
|
|
61
|
+
// A unit the handoff cannot resolve still gets a page; it just has no derived fields.
|
|
62
|
+
return undefined
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type RelatedView = { readonly path: string; readonly direction: string; readonly strength: number; readonly evidence: string }
|
|
67
|
+
|
|
68
|
+
const relatedView = (handoff: AgentHandoffV1 | undefined): RelatedView[] =>
|
|
69
|
+
[...(handoff?.related ?? [])]
|
|
70
|
+
.map((item) => ({ path: item.path, direction: item.direction, strength: item.strength, evidence: item.evidence.join(', ') }))
|
|
71
|
+
.sort((a, b) => a.path.localeCompare(b.path))
|
|
72
|
+
|
|
73
|
+
// --- area pages
|
|
74
|
+
|
|
75
|
+
export type AreaPageView = {
|
|
76
|
+
readonly id: string
|
|
77
|
+
readonly path: string
|
|
78
|
+
readonly purpose: string
|
|
79
|
+
readonly ownership: string
|
|
80
|
+
readonly modules: readonly { readonly path: string; readonly symbols: string }[]
|
|
81
|
+
readonly documents: readonly { readonly title: string; readonly path: string; readonly relation: string }[]
|
|
82
|
+
readonly related: readonly RelatedView[]
|
|
83
|
+
readonly checks: readonly string[]
|
|
84
|
+
readonly checksSource: string
|
|
85
|
+
readonly findings: readonly { readonly code: string; readonly severity: string; readonly status: string; readonly message: string; readonly evidence: string }[]
|
|
86
|
+
readonly findingsEmpty: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type AreaPagesOptions = {
|
|
90
|
+
readonly root?: string
|
|
91
|
+
/** The reconciliation report whose diagnostics become "open findings". Without one the page says so. */
|
|
92
|
+
readonly reconciliation?: ReconciliationReportV1
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const underPath = (path: string, area: string): boolean => path === area || path.startsWith(`${area}/`)
|
|
96
|
+
|
|
97
|
+
/** Where a finding points: its first evidence location, and the relation it is about when it names one. */
|
|
98
|
+
const findingEvidence = (diagnostic: ReconciliationReportV1['diagnostics'][number]): string => {
|
|
99
|
+
const first = [...diagnostic.evidence].sort((a, b) => `${a.path}:${a.lineStart ?? 0}`.localeCompare(`${b.path}:${b.lineStart ?? 0}`))[0]
|
|
100
|
+
const location = !first ? diagnostic.id : first.lineStart ? `${first.path}:${first.lineStart}` : first.path
|
|
101
|
+
const relation = [...(diagnostic.relationIds ?? [])].sort()[0]
|
|
102
|
+
return relation ? `${location} (${relation})` : location
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const areaPagesView = (index: DocBridgeIndexV1, config: DocBridgeConfigV1, options: AreaPagesOptions = {}): AreaPageView[] => {
|
|
106
|
+
const projection = projectionOf(index)
|
|
107
|
+
const entries = new Map(projection.entries.map((entry) => [entry.id, entry]))
|
|
108
|
+
const documentTitle = (id: string): { readonly title: string; readonly path: string } | undefined => {
|
|
109
|
+
const entry = entries.get(id)
|
|
110
|
+
return entry && entry.kind === 'document' ? { title: entry.title, path: entry.path } : undefined
|
|
111
|
+
}
|
|
112
|
+
const areas = projection.entries.filter((entry) => entry.kind === 'area').sort((a, b) => a.id.localeCompare(b.id))
|
|
113
|
+
|
|
114
|
+
return areas.map((area) => {
|
|
115
|
+
const modules = projection.entries
|
|
116
|
+
.filter((entry) => entry.kind === 'module' && entry.graph.areaId === area.id)
|
|
117
|
+
.sort(byPath)
|
|
118
|
+
const moduleIds = new Set(modules.map((entry) => entry.id))
|
|
119
|
+
const ownership = area.ownershipId ? index.lookup?.ownership?.[area.ownershipId] : undefined
|
|
120
|
+
const handoff = safeHandoff(index, area.id, config, options.root)
|
|
121
|
+
|
|
122
|
+
/*
|
|
123
|
+
* A document belongs on the page when it covers or mentions the area, or a module inside it —
|
|
124
|
+
* the module-level edge is how most documentation reaches an area. The first relation that
|
|
125
|
+
* brought a document in is the one reported, coverage before mention.
|
|
126
|
+
*/
|
|
127
|
+
const documents = new Map<string, { readonly title: string; readonly path: string; readonly relation: string }>()
|
|
128
|
+
const add = (documentId: string, relation: string): void => {
|
|
129
|
+
const document = documentTitle(documentId)
|
|
130
|
+
if (document && !documents.has(documentId)) documents.set(documentId, { ...document, relation })
|
|
131
|
+
}
|
|
132
|
+
for (const documentId of [...area.graph.coveredBy].sort()) add(documentId, 'covers this area')
|
|
133
|
+
for (const module of modules) for (const documentId of [...module.graph.coveredBy].sort()) add(documentId, `covers \`${module.path}\``)
|
|
134
|
+
for (const documentId of [...area.graph.mentionedBy].sort()) add(documentId, 'mentions this area')
|
|
135
|
+
for (const module of modules) for (const documentId of [...module.graph.mentionedBy].sort()) add(documentId, `mentions \`${module.path}\``)
|
|
136
|
+
|
|
137
|
+
const findings = (options.reconciliation?.diagnostics ?? [])
|
|
138
|
+
.filter((diagnostic) => diagnostic.code !== 'RELATION_CONFIRMED')
|
|
139
|
+
.filter(
|
|
140
|
+
(diagnostic) =>
|
|
141
|
+
diagnostic.entityIds?.some((id) => id === area.id || moduleIds.has(id)) ||
|
|
142
|
+
diagnostic.evidence.some((item) => underPath(item.path, area.path)),
|
|
143
|
+
)
|
|
144
|
+
.sort((a, b) => a.code.localeCompare(b.code) || a.id.localeCompare(b.id))
|
|
145
|
+
.map((diagnostic) => ({ code: diagnostic.code, severity: diagnostic.severity, status: diagnostic.status, message: diagnostic.message, evidence: findingEvidence(diagnostic) }))
|
|
146
|
+
|
|
147
|
+
const ownershipParts = ownership
|
|
148
|
+
? [
|
|
149
|
+
`Ownership record \`${ownership.id}\``,
|
|
150
|
+
...(ownership.agentDoc ? [`agent document \`${ownership.agentDoc}\``] : []),
|
|
151
|
+
...(ownership.humanDoc ? [`human guide ${ownership.humanDoc}`] : []),
|
|
152
|
+
]
|
|
153
|
+
: []
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
id: area.id,
|
|
157
|
+
path: area.path,
|
|
158
|
+
purpose: ownership?.purpose ?? area.summary ?? `Code area grouping ${modules.length} module(s) under \`${area.path}\`.`,
|
|
159
|
+
ownership: ownershipParts.join('; ') + (ownershipParts.length ? '.' : ''),
|
|
160
|
+
modules: modules.map((entry) => {
|
|
161
|
+
const symbols = entry.symbols ?? []
|
|
162
|
+
return { path: entry.path, symbols: symbols.slice(0, MAX_SYMBOLS).map((symbol) => `\`${symbol}\``).join(', ') + (symbols.length > MAX_SYMBOLS ? ` and ${symbols.length - MAX_SYMBOLS} more` : '') }
|
|
163
|
+
}),
|
|
164
|
+
documents: [...documents.values()].sort(byPath),
|
|
165
|
+
related: relatedView(handoff),
|
|
166
|
+
checks: handoff?.checks ?? [],
|
|
167
|
+
checksSource: checksSourceOf(handoff),
|
|
168
|
+
findings,
|
|
169
|
+
findingsEmpty: options.reconciliation ? 'No open finding touches this area.' : 'No reconciliation report was available; run `ak-docs reconcile` to list open findings.',
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// --- ownership sidecars
|
|
175
|
+
|
|
176
|
+
export type OwnershipPageView = {
|
|
177
|
+
readonly id: string
|
|
178
|
+
readonly kind: string
|
|
179
|
+
readonly path: string
|
|
180
|
+
readonly humanDoc: string
|
|
181
|
+
readonly purpose: string
|
|
182
|
+
readonly startHere: string
|
|
183
|
+
readonly readBeforeEditing: readonly string[]
|
|
184
|
+
readonly editRoots: readonly string[]
|
|
185
|
+
readonly checks: readonly string[]
|
|
186
|
+
readonly checksSource: string
|
|
187
|
+
readonly related: readonly RelatedView[]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const ownershipPagesView = (index: DocBridgeIndexV1, config: DocBridgeConfigV1, options: { readonly root?: string } = {}): OwnershipPageView[] =>
|
|
191
|
+
Object.values(index.lookup?.ownership ?? {})
|
|
192
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
193
|
+
.map((record) => {
|
|
194
|
+
const handoff = safeHandoff(index, record.id, config, options.root)
|
|
195
|
+
const startHere = handoff?.startHere ?? record.agentDoc ?? config.corpus.agent.index ?? ''
|
|
196
|
+
return {
|
|
197
|
+
id: record.id,
|
|
198
|
+
kind: handoff?.target.type ?? 'package',
|
|
199
|
+
path: record.path,
|
|
200
|
+
humanDoc: record.humanDoc ?? '',
|
|
201
|
+
purpose: record.purpose ?? `Owns \`${record.path}\`.`,
|
|
202
|
+
startHere: startHere || '(no start page)',
|
|
203
|
+
readBeforeEditing: (handoff?.readBeforeEditing ?? []).filter((path) => path !== startHere),
|
|
204
|
+
editRoots: handoff?.editRoots ?? [record.path],
|
|
205
|
+
checks: handoff?.checks ?? record.checks,
|
|
206
|
+
checksSource: checksSourceOf(handoff) || record.checksSource || '',
|
|
207
|
+
related: relatedView(handoff),
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// --- change digest
|
|
212
|
+
|
|
213
|
+
export type SnapshotForDigest = Pick<DiscoverySnapshotV1, 'entities' | 'relations' | 'sourceRevision'>
|
|
214
|
+
|
|
215
|
+
type FileBacked = { readonly id: string; readonly kind: string; readonly path: string; readonly hash: string }
|
|
216
|
+
|
|
217
|
+
export type ChangeDigestView = {
|
|
218
|
+
readonly summary: string
|
|
219
|
+
readonly changed: readonly { readonly id: string; readonly kind: string; readonly path: string; readonly previousHash: string; readonly currentHash: string }[]
|
|
220
|
+
readonly added: readonly { readonly id: string; readonly kind: string; readonly path: string; readonly currentHash: string }[]
|
|
221
|
+
readonly removed: readonly { readonly id: string; readonly kind: string; readonly path: string; readonly previousHash: string }[]
|
|
222
|
+
readonly documentsToReview: readonly { readonly path: string; readonly because: string }[]
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/*
|
|
226
|
+
* The digest is defined over file-backed entities, because those are the ones that carry a hash
|
|
227
|
+
* of their bytes (`docs/spec/incremental-scan-v1.md`). An entity without one — an external
|
|
228
|
+
* package, an area — has nothing to move.
|
|
229
|
+
*/
|
|
230
|
+
const fileBacked = (snapshot: SnapshotForDigest): Map<string, FileBacked> => {
|
|
231
|
+
const result = new Map<string, FileBacked>()
|
|
232
|
+
for (const entity of snapshot.entities) {
|
|
233
|
+
if (!(FILE_BACKED_KINDS as readonly string[]).includes(entity.kind) || !entity.path) continue
|
|
234
|
+
const hash = entity.evidence[0]?.contentHash
|
|
235
|
+
if (hash) result.set(entity.id, { id: entity.id, kind: entity.kind, path: entity.path, hash })
|
|
236
|
+
}
|
|
237
|
+
return result
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const DOCUMENT_RELATIONS = new Set(['covers', 'mentions', 'mentions-symbol', 'links-to'])
|
|
241
|
+
|
|
242
|
+
export const changeDigestView = (previous: SnapshotForDigest, current: SnapshotForDigest): ChangeDigestView => {
|
|
243
|
+
const before = fileBacked(previous)
|
|
244
|
+
const after = fileBacked(current)
|
|
245
|
+
const sortIds = (ids: Iterable<string>, of: Map<string, FileBacked>): string[] =>
|
|
246
|
+
[...ids].sort((a, b) => (of.get(a) as FileBacked).path.localeCompare((of.get(b) as FileBacked).path) || a.localeCompare(b))
|
|
247
|
+
|
|
248
|
+
const changed = sortIds([...after.keys()].filter((id) => before.has(id) && (before.get(id) as FileBacked).hash !== (after.get(id) as FileBacked).hash), after)
|
|
249
|
+
.map((id) => ({ id, kind: (after.get(id) as FileBacked).kind, path: (after.get(id) as FileBacked).path, previousHash: shortHash((before.get(id) as FileBacked).hash), currentHash: shortHash((after.get(id) as FileBacked).hash) }))
|
|
250
|
+
const added = sortIds([...after.keys()].filter((id) => !before.has(id)), after)
|
|
251
|
+
.map((id) => ({ id, kind: (after.get(id) as FileBacked).kind, path: (after.get(id) as FileBacked).path, currentHash: shortHash((after.get(id) as FileBacked).hash) }))
|
|
252
|
+
const removed = sortIds([...before.keys()].filter((id) => !after.has(id)), before)
|
|
253
|
+
.map((id) => ({ id, kind: (before.get(id) as FileBacked).kind, path: (before.get(id) as FileBacked).path, previousHash: shortHash((before.get(id) as FileBacked).hash) }))
|
|
254
|
+
|
|
255
|
+
/*
|
|
256
|
+
* "Which documentation should this change have touched": every document that covers, mentions
|
|
257
|
+
* or links to something that moved — and did not move itself. A document that changed alongside
|
|
258
|
+
* its subject is in `changed`, not here.
|
|
259
|
+
*/
|
|
260
|
+
const moved = new Set([...changed, ...added].map((entity) => entity.id))
|
|
261
|
+
const reasons = new Map<string, Set<string>>()
|
|
262
|
+
for (const relation of current.relations) {
|
|
263
|
+
if (!DOCUMENT_RELATIONS.has(relation.kind) || !moved.has(relation.to) || moved.has(relation.from)) continue
|
|
264
|
+
const document = after.get(relation.from)
|
|
265
|
+
if (!document || document.kind !== 'document') continue
|
|
266
|
+
const target = after.get(relation.to)
|
|
267
|
+
const because = reasons.get(document.path) ?? new Set<string>()
|
|
268
|
+
because.add(`${relation.kind} \`${target?.path ?? relation.to}\``)
|
|
269
|
+
reasons.set(document.path, because)
|
|
270
|
+
}
|
|
271
|
+
const documentsToReview = [...reasons.entries()]
|
|
272
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
273
|
+
.map(([path, because]) => ({ path, because: [...because].sort().join('; ') }))
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
summary: `${changed.length} changed, ${added.length} added, ${removed.length} removed of ${after.size} file-backed entities (previous revision ${previous.sourceRevision.slice(0, SHORT_HASH)}, current ${current.sourceRevision.slice(0, SHORT_HASH)})`,
|
|
277
|
+
changed,
|
|
278
|
+
added,
|
|
279
|
+
removed,
|
|
280
|
+
documentsToReview,
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// --- overlay review
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The shape the review page reads from an enrichment overlay. Deliberately loose: the overlay
|
|
288
|
+
* artifact is defined by the enrichment workstream, and the page needs only what a human needs
|
|
289
|
+
* to judge a proposal — what it proposes about which entity, why, how sure the agent was, and
|
|
290
|
+
* where in the repository the evidence is.
|
|
291
|
+
*/
|
|
292
|
+
export type OverlayReviewInput = {
|
|
293
|
+
readonly pending?: readonly {
|
|
294
|
+
readonly proposalId: string
|
|
295
|
+
readonly kind: string
|
|
296
|
+
readonly entity: string
|
|
297
|
+
readonly reason?: string
|
|
298
|
+
readonly confidence?: number | string
|
|
299
|
+
readonly evidence?: readonly { readonly path: string; readonly lineStart?: number; readonly lineEnd?: number }[]
|
|
300
|
+
}[]
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export type OverlayReviewView = {
|
|
304
|
+
readonly present: boolean
|
|
305
|
+
readonly summary: string
|
|
306
|
+
readonly pending: readonly {
|
|
307
|
+
readonly proposalId: string
|
|
308
|
+
readonly kind: string
|
|
309
|
+
readonly entity: string
|
|
310
|
+
readonly reason: string
|
|
311
|
+
readonly confidence: string
|
|
312
|
+
readonly evidence: readonly { readonly link: string }[]
|
|
313
|
+
}[]
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const evidenceLink = (item: { readonly path: string; readonly lineStart?: number; readonly lineEnd?: number }): string => {
|
|
317
|
+
if (!item.lineStart) return `[${item.path}](${item.path})`
|
|
318
|
+
const range = item.lineEnd && item.lineEnd !== item.lineStart ? `${item.lineStart}-${item.lineEnd}` : `${item.lineStart}`
|
|
319
|
+
const anchor = item.lineEnd && item.lineEnd !== item.lineStart ? `L${item.lineStart}-L${item.lineEnd}` : `L${item.lineStart}`
|
|
320
|
+
return `[${item.path}:${range}](${item.path}#${anchor})`
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export const overlayReviewView = (overlay: OverlayReviewInput | undefined, source?: string): OverlayReviewView => {
|
|
324
|
+
if (!overlay) return { present: false, summary: '', pending: [] }
|
|
325
|
+
const pending = [...(overlay.pending ?? [])]
|
|
326
|
+
.sort((a, b) => a.kind.localeCompare(b.kind) || a.entity.localeCompare(b.entity) || a.proposalId.localeCompare(b.proposalId))
|
|
327
|
+
.map((proposal) => ({
|
|
328
|
+
proposalId: proposal.proposalId,
|
|
329
|
+
kind: proposal.kind,
|
|
330
|
+
entity: proposal.entity,
|
|
331
|
+
reason: proposal.reason ?? '(no reason given)',
|
|
332
|
+
confidence: proposal.confidence === undefined ? 'unknown' : String(proposal.confidence),
|
|
333
|
+
evidence: [...(proposal.evidence ?? [])]
|
|
334
|
+
.sort((a, b) => a.path.localeCompare(b.path) || (a.lineStart ?? 0) - (b.lineStart ?? 0))
|
|
335
|
+
.map((item) => ({ link: evidenceLink(item) })),
|
|
336
|
+
}))
|
|
337
|
+
return {
|
|
338
|
+
present: true,
|
|
339
|
+
summary: `${pending.length} proposal(s) pending review${source ? ` in \`${source}\`` : ''}. Accepting one is a human decision; nothing here is applied automatically.`,
|
|
340
|
+
pending,
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Only for tests and callers that hold a snapshot: the document entities' generated regions, typed. */
|
|
345
|
+
export const generatedRegionsOf = (entity: Pick<KnowledgeEntity, 'metadata'> | undefined): { readonly lineStart: number; readonly lineEnd: number; readonly hash?: string }[] => {
|
|
346
|
+
const raw = entity?.metadata?.generatedRegions
|
|
347
|
+
if (!Array.isArray(raw)) return []
|
|
348
|
+
return raw.flatMap((item) => {
|
|
349
|
+
if (!item || typeof item !== 'object') return []
|
|
350
|
+
const region = item as { lineStart?: unknown; lineEnd?: unknown; hash?: unknown }
|
|
351
|
+
if (typeof region.lineStart !== 'number' || typeof region.lineEnd !== 'number') return []
|
|
352
|
+
return [{ lineStart: region.lineStart, lineEnd: region.lineEnd, ...(typeof region.hash === 'string' ? { hash: region.hash } : {}) }]
|
|
353
|
+
})
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export type { RetrievalEntry }
|