@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,356 @@
|
|
|
1
|
+
import Graph, { type GraphologyGraph } from 'graphology'
|
|
2
|
+
import louvain from 'graphology-communities-louvain'
|
|
3
|
+
import hasCycle from 'graphology-dag/has-cycle.js'
|
|
4
|
+
import betweennessCentrality from 'graphology-metrics/centrality/betweenness.js'
|
|
5
|
+
import pagerank from 'graphology-metrics/centrality/pagerank.js'
|
|
6
|
+
import { singleSourceLength } from 'graphology-shortest-path/unweighted.js'
|
|
7
|
+
|
|
8
|
+
import type { Coverage, DiscoverySnapshotV1, Evidence, KnowledgeRelation } from '../schemas/knowledge.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Graph signals over the knowledge snapshot.
|
|
12
|
+
*
|
|
13
|
+
* The rules engine used to derive `centrality-risk` from how many undocumented-relation findings
|
|
14
|
+
* were attached to an entity — a measure of documentation debt wearing the name of an
|
|
15
|
+
* architectural signal. A module can be the single point every import path runs through and carry
|
|
16
|
+
* no findings at all; a well-connected but thoroughly documented module carried the highest score.
|
|
17
|
+
* This module computes the real thing: betweenness over the import graph, PageRank over the
|
|
18
|
+
* documentation graph, bounded shortest paths, and cycles.
|
|
19
|
+
*
|
|
20
|
+
* The graph is a working structure. It is built from the snapshot on demand, never serialised, and
|
|
21
|
+
* `DiscoverySnapshotV1` does not learn about graphology. Nodes are inserted in sorted order and
|
|
22
|
+
* every score is rounded before it is returned, so two runs over the same snapshot agree exactly
|
|
23
|
+
* and an artifact built from these numbers keeps its hash.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export const GRAPH_ANALYZER_VERSION = '1.0.0'
|
|
27
|
+
|
|
28
|
+
/** Edges that say "this documentation points at that": the canonicality signal. */
|
|
29
|
+
export const DOCUMENTATION_EDGE_KINDS = ['links-to', 'covers'] as const
|
|
30
|
+
|
|
31
|
+
/** Edges that say "this code needs that": the centrality and cycle signal. */
|
|
32
|
+
export const IMPORT_EDGE_KINDS = ['imports', 're-exports'] as const
|
|
33
|
+
|
|
34
|
+
/** Enough precision to rank, little enough to compare exactly across runs and platforms. */
|
|
35
|
+
const PRECISION = 1e6
|
|
36
|
+
|
|
37
|
+
const round = (value: number): number => Math.round(value * PRECISION) / PRECISION
|
|
38
|
+
|
|
39
|
+
export type BuildGraphOptions = {
|
|
40
|
+
/** Relation kinds to include. Every other relation is left out of this view. */
|
|
41
|
+
readonly kinds?: readonly string[]
|
|
42
|
+
/** Undirected graphs are for community detection; everything else is directed. */
|
|
43
|
+
readonly undirected?: boolean
|
|
44
|
+
/** Keep external and unresolved endpoints. Off by default: they are not project architecture. */
|
|
45
|
+
readonly includeExternal?: boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const isInternal = (id: string): boolean => !id.startsWith('external:') && !id.startsWith('unresolved:')
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build a view of the snapshot as a graph.
|
|
52
|
+
*
|
|
53
|
+
* Nodes are added in sorted order, then edges in sorted order, so the library's internal iteration
|
|
54
|
+
* order is a function of the snapshot's content and not of how the snapshot happened to be built.
|
|
55
|
+
* That is what makes a metric reproducible after the input order is shuffled.
|
|
56
|
+
*/
|
|
57
|
+
export const buildKnowledgeGraph = (
|
|
58
|
+
snapshot: Pick<DiscoverySnapshotV1, 'entities' | 'relations'>,
|
|
59
|
+
options: BuildGraphOptions = {},
|
|
60
|
+
): GraphologyGraph => {
|
|
61
|
+
const kinds = options.kinds ? new Set(options.kinds) : undefined
|
|
62
|
+
const includeExternal = options.includeExternal ?? false
|
|
63
|
+
const graph = new Graph({ type: options.undirected ? 'undirected' : 'directed', multi: false, allowSelfLoops: false })
|
|
64
|
+
|
|
65
|
+
const relations = [...snapshot.relations]
|
|
66
|
+
.filter((relation) => !kinds || kinds.has(relation.kind))
|
|
67
|
+
.filter((relation) => includeExternal || (isInternal(relation.from) && isInternal(relation.to)))
|
|
68
|
+
.filter((relation) => relation.from !== relation.to)
|
|
69
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
70
|
+
|
|
71
|
+
const referenced = new Set(relations.flatMap((relation) => [relation.from, relation.to]))
|
|
72
|
+
const entities = new Set(snapshot.entities.map((entity) => entity.id))
|
|
73
|
+
for (const id of [...referenced].sort()) {
|
|
74
|
+
// An endpoint with no entity is still a node: the relation observed it, and dropping it
|
|
75
|
+
// silently would change a path length without saying so.
|
|
76
|
+
if (!graph.hasNode(id)) graph.addNode(id, { known: entities.has(id) })
|
|
77
|
+
}
|
|
78
|
+
for (const relation of relations) {
|
|
79
|
+
if (graph.hasEdge(relation.from, relation.to)) continue
|
|
80
|
+
graph.addEdge(relation.from, relation.to, { kind: relation.kind, relationId: relation.id })
|
|
81
|
+
}
|
|
82
|
+
return graph
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type GraphSignal = ReadonlyMap<string, number>
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Canonicality: PageRank over the documentation graph.
|
|
89
|
+
*
|
|
90
|
+
* A page many documents link to, or that covers many entities, is where a reader should start. A
|
|
91
|
+
* leaf note nothing points at is not, however recently it was edited. PageRank says that without
|
|
92
|
+
* anyone maintaining a list of entry points.
|
|
93
|
+
*/
|
|
94
|
+
export const canonicality = (
|
|
95
|
+
snapshot: Pick<DiscoverySnapshotV1, 'entities' | 'relations'>,
|
|
96
|
+
options: { readonly alpha?: number } = {},
|
|
97
|
+
): GraphSignal => {
|
|
98
|
+
const graph = buildKnowledgeGraph(snapshot, { kinds: DOCUMENTATION_EDGE_KINDS })
|
|
99
|
+
if (!graph.order) return new Map()
|
|
100
|
+
const scores = pagerank(graph, { getEdgeWeight: null, alpha: options.alpha ?? 0.85 })
|
|
101
|
+
return new Map(
|
|
102
|
+
Object.entries(scores)
|
|
103
|
+
.map(([id, score]): [string, number] => [id, round(score)])
|
|
104
|
+
.sort(([a], [b]) => a.localeCompare(b)),
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Centrality: betweenness over the import graph.
|
|
110
|
+
*
|
|
111
|
+
* How much of the repository's dependency structure runs through this module. High betweenness is
|
|
112
|
+
* a review signal — a change here reaches further than its diff suggests — and nothing more: it is
|
|
113
|
+
* a static count of shortest paths, not a statement about runtime availability.
|
|
114
|
+
*/
|
|
115
|
+
export const centrality = (
|
|
116
|
+
snapshot: Pick<DiscoverySnapshotV1, 'entities' | 'relations'>,
|
|
117
|
+
options: { readonly kinds?: readonly string[] } = {},
|
|
118
|
+
): GraphSignal => {
|
|
119
|
+
const graph = buildKnowledgeGraph(snapshot, { kinds: options.kinds ?? IMPORT_EDGE_KINDS })
|
|
120
|
+
if (!graph.order) return new Map()
|
|
121
|
+
const scores = betweennessCentrality(graph, { getEdgeWeight: null, normalized: true })
|
|
122
|
+
return new Map(
|
|
123
|
+
Object.entries(scores)
|
|
124
|
+
.map(([id, score]): [string, number] => [id, round(score)])
|
|
125
|
+
.sort(([a], [b]) => a.localeCompare(b)),
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type ProximityOptions = {
|
|
130
|
+
readonly kinds?: readonly string[]
|
|
131
|
+
/** Hops beyond which two entities are not usefully related. */
|
|
132
|
+
readonly maxDepth?: number
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const DEFAULT_PROXIMITY_DEPTH = 3
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Relation kinds that make two entities *related*, as opposed to merely filed together.
|
|
139
|
+
*
|
|
140
|
+
* `contains` is left out on purpose: it is hierarchy, and including it puts every module in an
|
|
141
|
+
* area two hops from every other one, which is true and tells a reader nothing. What makes a
|
|
142
|
+
* module close to a document is that the document covers or mentions it; what makes two modules
|
|
143
|
+
* close is that one imports the other.
|
|
144
|
+
*/
|
|
145
|
+
export const PROXIMITY_EDGE_KINDS = [
|
|
146
|
+
...IMPORT_EDGE_KINDS,
|
|
147
|
+
...DOCUMENTATION_EDGE_KINDS,
|
|
148
|
+
'mentions',
|
|
149
|
+
'mentions-symbol',
|
|
150
|
+
'depends-on',
|
|
151
|
+
] as const
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Proximity: how many hops from one entity to another, bounded.
|
|
155
|
+
*
|
|
156
|
+
* Bounded because an unbounded answer is not useful — at ten hops everything is related to
|
|
157
|
+
* everything — and because ranking and handoff selection need a cheap neighbourhood, not a
|
|
158
|
+
* complete distance matrix.
|
|
159
|
+
*/
|
|
160
|
+
export const proximity = (
|
|
161
|
+
snapshot: Pick<DiscoverySnapshotV1, 'entities' | 'relations'>,
|
|
162
|
+
from: string,
|
|
163
|
+
options: ProximityOptions = {},
|
|
164
|
+
): GraphSignal => {
|
|
165
|
+
const maxDepth = options.maxDepth ?? DEFAULT_PROXIMITY_DEPTH
|
|
166
|
+
const graph = buildKnowledgeGraph(snapshot, {
|
|
167
|
+
kinds: options.kinds ?? PROXIMITY_EDGE_KINDS,
|
|
168
|
+
undirected: true,
|
|
169
|
+
})
|
|
170
|
+
if (!graph.hasNode(from)) return new Map()
|
|
171
|
+
const lengths = singleSourceLength(graph, from)
|
|
172
|
+
// Nearest first, then by id: a proximity map is read from the top, and ties must not depend on
|
|
173
|
+
// the order the snapshot happened to arrive in.
|
|
174
|
+
return new Map(
|
|
175
|
+
Object.entries(lengths)
|
|
176
|
+
.filter(([id, length]) => id !== from && length <= maxDepth)
|
|
177
|
+
.sort(([leftId, left], [rightId, right]) => left - right || leftId.localeCompare(rightId)),
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export type ImportCycle = {
|
|
182
|
+
/** The nodes of the cycle, rotated so the lexicographically smallest is first. */
|
|
183
|
+
readonly nodes: readonly string[]
|
|
184
|
+
readonly relationIds: readonly string[]
|
|
185
|
+
readonly evidence: readonly Evidence[]
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** A cycle beyond this many is the same architectural problem reported again. */
|
|
189
|
+
export const MAX_REPORTED_CYCLES = 16
|
|
190
|
+
|
|
191
|
+
const rotated = (nodes: readonly string[]): readonly string[] => {
|
|
192
|
+
let pivot = 0
|
|
193
|
+
for (let index = 1; index < nodes.length; index += 1) {
|
|
194
|
+
if ((nodes[index] as string) < (nodes[pivot] as string)) pivot = index
|
|
195
|
+
}
|
|
196
|
+
return [...nodes.slice(pivot), ...nodes.slice(0, pivot)]
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Import cycles, with the edges that form them.
|
|
201
|
+
*
|
|
202
|
+
* `graphology-dag` answers whether a cycle exists; reporting one needs the path, because a
|
|
203
|
+
* diagnostic without the edges is a claim a reader cannot check. This is a depth-first search that
|
|
204
|
+
* records the stack when it closes a loop, bounded in both count and depth, and deterministic
|
|
205
|
+
* because the graph iterates in sorted order.
|
|
206
|
+
*/
|
|
207
|
+
export const importCycles = (
|
|
208
|
+
snapshot: Pick<DiscoverySnapshotV1, 'entities' | 'relations'>,
|
|
209
|
+
options: { readonly kinds?: readonly string[]; readonly limit?: number } = {},
|
|
210
|
+
): readonly ImportCycle[] => {
|
|
211
|
+
const kinds = options.kinds ?? IMPORT_EDGE_KINDS
|
|
212
|
+
const graph = buildKnowledgeGraph(snapshot, { kinds })
|
|
213
|
+
if (!graph.order || !hasCycle(graph)) return []
|
|
214
|
+
|
|
215
|
+
const relations = new Map(snapshot.relations.map((relation) => [relation.id, relation]))
|
|
216
|
+
const limit = options.limit ?? MAX_REPORTED_CYCLES
|
|
217
|
+
const found = new Map<string, ImportCycle>()
|
|
218
|
+
const onStack = new Set<string>()
|
|
219
|
+
const visited = new Set<string>()
|
|
220
|
+
const stack: string[] = []
|
|
221
|
+
|
|
222
|
+
const edgeOf = (from: string, to: string): KnowledgeRelation | undefined => {
|
|
223
|
+
const attributes = graph.getEdgeAttributes(from, to) as { relationId?: string }
|
|
224
|
+
return attributes.relationId ? relations.get(attributes.relationId) : undefined
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const record = (cycle: readonly string[]): void => {
|
|
228
|
+
const nodes = rotated(cycle)
|
|
229
|
+
const key = nodes.join('→')
|
|
230
|
+
if (found.has(key) || found.size >= limit) return
|
|
231
|
+
const edges = nodes.map((node, index) => edgeOf(node, nodes[(index + 1) % nodes.length] as string))
|
|
232
|
+
found.set(key, {
|
|
233
|
+
nodes,
|
|
234
|
+
relationIds: edges.flatMap((edge) => (edge ? [edge.id] : [])),
|
|
235
|
+
evidence: edges.flatMap((edge) => (edge?.evidence[0] ? [edge.evidence[0]] : [])),
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const visit = (node: string): void => {
|
|
240
|
+
if (found.size >= limit) return
|
|
241
|
+
visited.add(node)
|
|
242
|
+
onStack.add(node)
|
|
243
|
+
stack.push(node)
|
|
244
|
+
for (const next of [...graph.outNeighbors(node)].sort()) {
|
|
245
|
+
if (onStack.has(next)) {
|
|
246
|
+
const start = stack.indexOf(next)
|
|
247
|
+
if (start >= 0) record(stack.slice(start))
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
if (!visited.has(next)) visit(next)
|
|
251
|
+
}
|
|
252
|
+
stack.pop()
|
|
253
|
+
onStack.delete(node)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (const node of [...graph.nodes()].sort()) if (!visited.has(node)) visit(node)
|
|
257
|
+
return [...found.values()].sort((a, b) => a.nodes.join().localeCompare(b.nodes.join()))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Deterministic pseudo-randomness, so a community run is reproducible. */
|
|
261
|
+
export const seededRandom = (seed: number): (() => number) => {
|
|
262
|
+
let state = seed | 0
|
|
263
|
+
return () => {
|
|
264
|
+
state = (state + 0x6d2b79f5) | 0
|
|
265
|
+
let t = Math.imul(state ^ (state >>> 15), 1 | state)
|
|
266
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
|
|
267
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export const DEFAULT_COMMUNITY_SEED = 20_260_401
|
|
272
|
+
|
|
273
|
+
export type AreaSuggestion = {
|
|
274
|
+
/** Suggested area label, from the deepest directory the members share. */
|
|
275
|
+
readonly label: string
|
|
276
|
+
readonly members: readonly string[]
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const commonDirectory = (paths: readonly string[]): string | undefined => {
|
|
280
|
+
if (!paths.length) return undefined
|
|
281
|
+
const split = paths.map((path) => path.split('/').slice(0, -1))
|
|
282
|
+
const first = split[0] as string[]
|
|
283
|
+
const shared: string[] = []
|
|
284
|
+
for (let index = 0; index < first.length; index += 1) {
|
|
285
|
+
const segment = first[index] as string
|
|
286
|
+
if (!split.every((parts) => parts[index] === segment)) break
|
|
287
|
+
shared.push(segment)
|
|
288
|
+
}
|
|
289
|
+
return shared.length ? shared.join('/') : undefined
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Area suggestions from seeded Louvain communities over the import graph.
|
|
294
|
+
*
|
|
295
|
+
* A community is a hypothesis: these modules move together, so perhaps they are one unit. It is
|
|
296
|
+
* never an area on its own — an area is derived from the repository's own structure or declared by
|
|
297
|
+
* a human, and a clustering algorithm is neither. Suggestions travel as `coverage` entries for a
|
|
298
|
+
* person or the curator agent to promote into configuration, which keeps a guess out of the graph
|
|
299
|
+
* while still surfacing it.
|
|
300
|
+
*/
|
|
301
|
+
export const areaSuggestions = (
|
|
302
|
+
snapshot: Pick<DiscoverySnapshotV1, 'entities' | 'relations'>,
|
|
303
|
+
options: { readonly seed?: number; readonly minMembers?: number; readonly limit?: number } = {},
|
|
304
|
+
): readonly AreaSuggestion[] => {
|
|
305
|
+
const graph = buildKnowledgeGraph(snapshot, { kinds: IMPORT_EDGE_KINDS, undirected: true })
|
|
306
|
+
if (graph.order < 2 || !graph.size) return []
|
|
307
|
+
|
|
308
|
+
const communities = louvain(graph, { rng: seededRandom(options.seed ?? DEFAULT_COMMUNITY_SEED) }) as Record<string, number>
|
|
309
|
+
const paths = new Map(snapshot.entities.flatMap((entity) => (entity.path ? [[entity.id, entity.path] as const] : [])))
|
|
310
|
+
const grouped = new Map<number, string[]>()
|
|
311
|
+
for (const [id, community] of Object.entries(communities).sort(([a], [b]) => a.localeCompare(b))) {
|
|
312
|
+
const members = grouped.get(community) ?? []
|
|
313
|
+
members.push(id)
|
|
314
|
+
grouped.set(community, members)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const minMembers = options.minMembers ?? 3
|
|
318
|
+
const suggestions: AreaSuggestion[] = []
|
|
319
|
+
for (const [, members] of [...grouped.entries()].sort(([a], [b]) => a - b)) {
|
|
320
|
+
if (members.length < minMembers) continue
|
|
321
|
+
const label = commonDirectory(members.flatMap((id) => (paths.get(id) ? [paths.get(id) as string] : [])))
|
|
322
|
+
if (!label) continue
|
|
323
|
+
suggestions.push({ label, members: [...members].sort() })
|
|
324
|
+
}
|
|
325
|
+
return suggestions
|
|
326
|
+
.sort((a, b) => b.members.length - a.members.length || a.label.localeCompare(b.label))
|
|
327
|
+
.slice(0, options.limit ?? 8)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Community suggestions as coverage.
|
|
332
|
+
*
|
|
333
|
+
* `status: 'not-analyzed'` is the honest status: the clustering ran, but whether the cluster is an
|
|
334
|
+
* area is a question nobody has answered yet.
|
|
335
|
+
*/
|
|
336
|
+
export const areaSuggestionCoverage = (
|
|
337
|
+
snapshot: Pick<DiscoverySnapshotV1, 'entities' | 'relations'>,
|
|
338
|
+
options: { readonly seed?: number; readonly minMembers?: number; readonly limit?: number } = {},
|
|
339
|
+
): readonly Coverage[] => {
|
|
340
|
+
const declared = new Set(
|
|
341
|
+
snapshot.entities.filter((entity) => entity.kind === 'area').map((entity) => entity.path ?? ''),
|
|
342
|
+
)
|
|
343
|
+
return areaSuggestions(snapshot, options)
|
|
344
|
+
.filter((suggestion) => !declared.has(suggestion.label))
|
|
345
|
+
.map((suggestion) => ({
|
|
346
|
+
analyzer: 'graph',
|
|
347
|
+
analyzerVersion: GRAPH_ANALYZER_VERSION,
|
|
348
|
+
scope: `area-suggestion:${suggestion.label}`,
|
|
349
|
+
status: 'not-analyzed' as const,
|
|
350
|
+
reason: `${suggestion.members.length} modules cluster around ${suggestion.label}; promote it to an area in configuration to make it part of the graph.`,
|
|
351
|
+
evidence: suggestion.members.slice(0, 8).flatMap((id) => {
|
|
352
|
+
const path = snapshot.entities.find((entity) => entity.id === id)?.path
|
|
353
|
+
return path ? [{ source: 'derived' as const, path }] : []
|
|
354
|
+
}),
|
|
355
|
+
}))
|
|
356
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { DiscoverySnapshotV1, KnowledgeEntity, KnowledgeRelation } from '../schemas/knowledge.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The repository graph behind the ecosystem's `GraphMemory` contract.
|
|
5
|
+
*
|
|
6
|
+
* An agent built on AgentsKit already walks its own memory with `getNode`, `findEdges` and
|
|
7
|
+
* `neighbors`. This exposes the repository's structure through the same three calls, so asking
|
|
8
|
+
* "what does this module import, and what documents cover it" needs no Doc Bridge-specific client.
|
|
9
|
+
*
|
|
10
|
+
* The contract is mirrored rather than imported: `@agentskit/memory` is an optional peer, and a
|
|
11
|
+
* projection that only typechecked when an optional package happened to be installed would be a
|
|
12
|
+
* published type that breaks on a clean install. `tests/graph.test.ts` asserts this implementation
|
|
13
|
+
* is assignable to the real `GraphMemory` and behaves like `createInMemoryGraph` on the same
|
|
14
|
+
* inputs, so the mirror cannot drift.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export type GraphNode<TProps = Record<string, unknown>> = {
|
|
18
|
+
id: string
|
|
19
|
+
/** Type or label — here the entity kind: `module`, `document`, `area`, `package`. */
|
|
20
|
+
kind: string
|
|
21
|
+
properties?: TProps
|
|
22
|
+
createdAt?: string
|
|
23
|
+
updatedAt?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type GraphEdge<TProps = Record<string, unknown>> = {
|
|
27
|
+
id: string
|
|
28
|
+
/** Verb — here the relation kind: `imports`, `covers`, `links-to`, `contains`. */
|
|
29
|
+
label: string
|
|
30
|
+
from: string
|
|
31
|
+
to: string
|
|
32
|
+
weight?: number
|
|
33
|
+
properties?: TProps
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type GraphQuery = {
|
|
37
|
+
kind?: string
|
|
38
|
+
label?: string
|
|
39
|
+
from?: string
|
|
40
|
+
to?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type GraphMemory = {
|
|
44
|
+
upsertNode: <T>(node: GraphNode<T>) => Promise<GraphNode<T>>
|
|
45
|
+
upsertEdge: <T>(edge: GraphEdge<T>) => Promise<GraphEdge<T>>
|
|
46
|
+
getNode: <T>(id: string) => Promise<GraphNode<T> | null>
|
|
47
|
+
findNodes: <T>(query?: GraphQuery) => Promise<GraphNode<T>[]>
|
|
48
|
+
findEdges: <T>(query?: GraphQuery) => Promise<GraphEdge<T>[]>
|
|
49
|
+
/** Breadth-first neighbours of `id` up to `depth`, in both directions. Default 1. */
|
|
50
|
+
neighbors: <T>(id: string, options?: { depth?: number; label?: string }) => Promise<GraphNode<T>[]>
|
|
51
|
+
deleteNode: (id: string) => Promise<void>
|
|
52
|
+
deleteEdge: (id: string) => Promise<void>
|
|
53
|
+
clear?: () => Promise<void>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extra entities and relations layered over the observed snapshot.
|
|
58
|
+
*
|
|
59
|
+
* Same vocabulary as the snapshot, so the approved enrichment overlay plugs in unchanged when it
|
|
60
|
+
* exists. Overlay records win over observed ones with the same id, which is what "approved" has to
|
|
61
|
+
* mean for it to be worth approving.
|
|
62
|
+
*/
|
|
63
|
+
export type KnowledgeOverlay = {
|
|
64
|
+
readonly entities?: readonly KnowledgeEntity[]
|
|
65
|
+
readonly relations?: readonly KnowledgeRelation[]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const nodeOf = (entity: KnowledgeEntity): GraphNode => ({
|
|
69
|
+
id: entity.id,
|
|
70
|
+
kind: entity.kind,
|
|
71
|
+
properties: {
|
|
72
|
+
name: entity.name,
|
|
73
|
+
...(entity.path !== undefined ? { path: entity.path } : {}),
|
|
74
|
+
provenance: entity.provenance,
|
|
75
|
+
...(entity.aliases?.length ? { aliases: [...entity.aliases] } : {}),
|
|
76
|
+
...(entity.metadata ?? {}),
|
|
77
|
+
evidenceCount: entity.evidence.length,
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const edgeOf = (relation: KnowledgeRelation): GraphEdge => ({
|
|
82
|
+
id: relation.id,
|
|
83
|
+
label: relation.kind,
|
|
84
|
+
from: relation.from,
|
|
85
|
+
to: relation.to,
|
|
86
|
+
properties: {
|
|
87
|
+
provenance: relation.provenance,
|
|
88
|
+
...(relation.discriminator !== undefined ? { discriminator: relation.discriminator } : {}),
|
|
89
|
+
...(relation.metadata ?? {}),
|
|
90
|
+
evidenceCount: relation.evidence.length,
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const matchesNode = (node: GraphNode, query?: GraphQuery): boolean => !query?.kind || node.kind === query.kind
|
|
95
|
+
|
|
96
|
+
const matchesEdge = (edge: GraphEdge, query?: GraphQuery): boolean => {
|
|
97
|
+
if (!query) return true
|
|
98
|
+
if (query.label && edge.label !== query.label) return false
|
|
99
|
+
if (query.from && edge.from !== query.from) return false
|
|
100
|
+
if (query.to && edge.to !== query.to) return false
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Project a snapshot, and an optional overlay, as a `GraphMemory`.
|
|
106
|
+
*
|
|
107
|
+
* Writes are accepted and kept in process: the snapshot is an observation and cannot be edited by
|
|
108
|
+
* a caller, so `upsertNode` and `upsertEdge` land in a working layer above it and `deleteNode`
|
|
109
|
+
* masks rather than erases. That keeps the contract honest in both directions — an agent can
|
|
110
|
+
* annotate what it is exploring without any of it being mistaken for something the repository
|
|
111
|
+
* said. `clear` drops the working layer, never the projection.
|
|
112
|
+
*/
|
|
113
|
+
export const createDocBridgeGraphMemory = (
|
|
114
|
+
snapshot: Pick<DiscoverySnapshotV1, 'entities' | 'relations'>,
|
|
115
|
+
overlay: KnowledgeOverlay = {},
|
|
116
|
+
): GraphMemory => {
|
|
117
|
+
const projectedNodes = new Map<string, GraphNode>()
|
|
118
|
+
const projectedEdges = new Map<string, GraphEdge>()
|
|
119
|
+
for (const entity of [...snapshot.entities, ...(overlay.entities ?? [])]) projectedNodes.set(entity.id, nodeOf(entity))
|
|
120
|
+
for (const relation of [...snapshot.relations, ...(overlay.relations ?? [])]) projectedEdges.set(relation.id, edgeOf(relation))
|
|
121
|
+
|
|
122
|
+
const writtenNodes = new Map<string, GraphNode>()
|
|
123
|
+
const writtenEdges = new Map<string, GraphEdge>()
|
|
124
|
+
const maskedNodes = new Set<string>()
|
|
125
|
+
const maskedEdges = new Set<string>()
|
|
126
|
+
|
|
127
|
+
const nodes = (): GraphNode[] => {
|
|
128
|
+
const merged = new Map(projectedNodes)
|
|
129
|
+
for (const [id, node] of writtenNodes) merged.set(id, node)
|
|
130
|
+
for (const id of maskedNodes) merged.delete(id)
|
|
131
|
+
return [...merged.values()].sort((a, b) => a.id.localeCompare(b.id))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const edges = (): GraphEdge[] => {
|
|
135
|
+
const merged = new Map(projectedEdges)
|
|
136
|
+
for (const [id, edge] of writtenEdges) merged.set(id, edge)
|
|
137
|
+
for (const id of maskedEdges) merged.delete(id)
|
|
138
|
+
return [...merged.values()]
|
|
139
|
+
.filter((edge) => !maskedNodes.has(edge.from) && !maskedNodes.has(edge.to))
|
|
140
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
async upsertNode<T>(node: GraphNode<T>): Promise<GraphNode<T>> {
|
|
145
|
+
const now = new Date().toISOString()
|
|
146
|
+
const existing = writtenNodes.get(node.id) ?? projectedNodes.get(node.id)
|
|
147
|
+
const merged = { ...(existing ?? {}), ...node, createdAt: existing?.createdAt ?? now, updatedAt: now }
|
|
148
|
+
maskedNodes.delete(node.id)
|
|
149
|
+
writtenNodes.set(node.id, merged as GraphNode)
|
|
150
|
+
return merged as GraphNode<T>
|
|
151
|
+
},
|
|
152
|
+
async upsertEdge<T>(edge: GraphEdge<T>): Promise<GraphEdge<T>> {
|
|
153
|
+
maskedEdges.delete(edge.id)
|
|
154
|
+
writtenEdges.set(edge.id, edge as GraphEdge)
|
|
155
|
+
return edge
|
|
156
|
+
},
|
|
157
|
+
async getNode<T>(id: string): Promise<GraphNode<T> | null> {
|
|
158
|
+
if (maskedNodes.has(id)) return null
|
|
159
|
+
const node = writtenNodes.get(id) ?? projectedNodes.get(id)
|
|
160
|
+
return node ? ({ ...node } as GraphNode<T>) : null
|
|
161
|
+
},
|
|
162
|
+
async findNodes<T>(query?: GraphQuery): Promise<GraphNode<T>[]> {
|
|
163
|
+
return nodes().filter((node) => matchesNode(node, query)).map((node) => ({ ...node }) as GraphNode<T>)
|
|
164
|
+
},
|
|
165
|
+
async findEdges<T>(query?: GraphQuery): Promise<GraphEdge<T>[]> {
|
|
166
|
+
return edges().filter((edge) => matchesEdge(edge, query)).map((edge) => ({ ...edge }) as GraphEdge<T>)
|
|
167
|
+
},
|
|
168
|
+
async neighbors<T>(id: string, options: { depth?: number; label?: string } = {}): Promise<GraphNode<T>[]> {
|
|
169
|
+
const depth = Math.max(1, options.depth ?? 1)
|
|
170
|
+
const all = edges().filter((edge) => !options.label || edge.label === options.label)
|
|
171
|
+
const visited = new Set([id])
|
|
172
|
+
let frontier = new Set([id])
|
|
173
|
+
for (let step = 0; step < depth; step += 1) {
|
|
174
|
+
const next = new Set<string>()
|
|
175
|
+
for (const edge of all) {
|
|
176
|
+
if (frontier.has(edge.from) && !visited.has(edge.to)) next.add(edge.to)
|
|
177
|
+
if (frontier.has(edge.to) && !visited.has(edge.from)) next.add(edge.from)
|
|
178
|
+
}
|
|
179
|
+
for (const node of next) visited.add(node)
|
|
180
|
+
frontier = next
|
|
181
|
+
if (!next.size) break
|
|
182
|
+
}
|
|
183
|
+
visited.delete(id)
|
|
184
|
+
const byId = new Map(nodes().map((node) => [node.id, node]))
|
|
185
|
+
return [...visited]
|
|
186
|
+
.sort()
|
|
187
|
+
.flatMap((nodeId) => {
|
|
188
|
+
const node = byId.get(nodeId)
|
|
189
|
+
return node ? [{ ...node } as GraphNode<T>] : []
|
|
190
|
+
})
|
|
191
|
+
},
|
|
192
|
+
async deleteNode(id: string): Promise<void> {
|
|
193
|
+
writtenNodes.delete(id)
|
|
194
|
+
maskedNodes.add(id)
|
|
195
|
+
for (const edge of edges()) if (edge.from === id || edge.to === id) maskedEdges.add(edge.id)
|
|
196
|
+
},
|
|
197
|
+
async deleteEdge(id: string): Promise<void> {
|
|
198
|
+
writtenEdges.delete(id)
|
|
199
|
+
maskedEdges.add(id)
|
|
200
|
+
},
|
|
201
|
+
async clear(): Promise<void> {
|
|
202
|
+
writtenNodes.clear()
|
|
203
|
+
writtenEdges.clear()
|
|
204
|
+
maskedNodes.clear()
|
|
205
|
+
maskedEdges.clear()
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -24,6 +24,7 @@ export type OwnershipRecord = {
|
|
|
24
24
|
layer?: string
|
|
25
25
|
purpose?: string
|
|
26
26
|
checks: string[]
|
|
27
|
+
checksSource?: 'ownership' | 'frontmatter' | 'package-scripts' | 'default'
|
|
27
28
|
agentDoc?: string
|
|
28
29
|
humanDoc?: string
|
|
29
30
|
readme?: string
|
|
@@ -161,22 +162,32 @@ export const buildLookup = (
|
|
|
161
162
|
const startHere = agentDoc ?? ''
|
|
162
163
|
const purpose = override?.purpose ?? fm?.purpose
|
|
163
164
|
const path = override?.path ?? fm?.path ?? pkg.path
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
165
|
+
/*
|
|
166
|
+
* Where the checks come from is decided here, and recorded here: a handoff that reports its
|
|
167
|
+
* checks' origin cannot reconstruct it later from the merged list.
|
|
168
|
+
*/
|
|
169
|
+
const resolvedChecks: { readonly checks: readonly string[]; readonly source: OwnershipRecord['checksSource'] } = override?.checks
|
|
170
|
+
? { checks: override.checks, source: 'ownership' }
|
|
171
|
+
: fm?.checks
|
|
172
|
+
? { checks: fm.checks, source: 'frontmatter' }
|
|
173
|
+
: pkg.checks
|
|
174
|
+
? { checks: pkg.checks, source: 'package-scripts' }
|
|
175
|
+
: {
|
|
176
|
+
checks: defaultChecksForTarget(root, {
|
|
177
|
+
packageId: pkg.id,
|
|
178
|
+
packagePath: path,
|
|
179
|
+
...(pkg.name ? { packageName: pkg.name } : {}),
|
|
180
|
+
strict,
|
|
181
|
+
}),
|
|
182
|
+
source: 'default',
|
|
183
|
+
}
|
|
184
|
+
const checks = [...resolvedChecks.checks]
|
|
175
185
|
const humanDoc = resolveHumanDoc(pkg.id, override?.humanDoc, fm?.humanDoc, humanDocs)
|
|
176
186
|
const record: OwnershipRecord = {
|
|
177
187
|
id: pkg.id,
|
|
178
188
|
path,
|
|
179
189
|
checks,
|
|
190
|
+
...(resolvedChecks.source ? { checksSource: resolvedChecks.source } : {}),
|
|
180
191
|
...(override?.group ? { group: override.group } : {}),
|
|
181
192
|
...(override?.layer ? { layer: override.layer } : {}),
|
|
182
193
|
...(purpose ? { purpose } : {}),
|