@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,52 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { DocBridgeConfigV1 } from '../config/schema.js'
|
|
5
|
+
import { toPosix } from '../lib/paths.js'
|
|
6
|
+
import { compileTemplate, renderCompiledTemplate, type CompiledTemplate, type TemplateVariables } from './engine.js'
|
|
7
|
+
import { BUNDLED_TEMPLATES, type RenderTemplateName } from './templates.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Which template renders a name: the project's, when `render.templates` names one, otherwise
|
|
11
|
+
* the bundled one. An override is a file the project owns; it replaces the bundled template
|
|
12
|
+
* entirely and is compiled against the same engine, so a project changes its house style without
|
|
13
|
+
* a code change and a template that does not parse fails before anything is written.
|
|
14
|
+
*/
|
|
15
|
+
export type TemplateSource = {
|
|
16
|
+
readonly name: RenderTemplateName
|
|
17
|
+
readonly source: string
|
|
18
|
+
/** `bundled`, or the override path as configured. */
|
|
19
|
+
readonly origin: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const resolveTemplateSource = (name: RenderTemplateName, config: DocBridgeConfigV1, root?: string): TemplateSource => {
|
|
23
|
+
const override = config.render?.templates?.[name]
|
|
24
|
+
if (!override) return { name, source: BUNDLED_TEMPLATES[name], origin: 'bundled' }
|
|
25
|
+
const path = root ? resolve(root, override) : resolve(override)
|
|
26
|
+
let source: string
|
|
27
|
+
try {
|
|
28
|
+
source = readFileSync(path, 'utf8')
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error(`render.templates["${name}"] points at ${toPosix(override)}, which could not be read: ${error instanceof Error ? error.message : String(error)}`)
|
|
31
|
+
}
|
|
32
|
+
return { name, source, origin: toPosix(override) }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/*
|
|
36
|
+
* Compiled once per distinct source. The index builder and the conformance profile render
|
|
37
|
+
* `llms.txt` on every run, and parsing the same bundled template twice per run buys nothing.
|
|
38
|
+
*/
|
|
39
|
+
const compiled = new Map<string, CompiledTemplate>()
|
|
40
|
+
|
|
41
|
+
const compile = (template: TemplateSource): CompiledTemplate => {
|
|
42
|
+
const key = JSON.stringify([template.name, template.origin, template.source])
|
|
43
|
+
let result = compiled.get(key)
|
|
44
|
+
if (!result) {
|
|
45
|
+
result = compileTemplate(template.source, template.origin === 'bundled' ? `template "${template.name}"` : template.origin)
|
|
46
|
+
compiled.set(key, result)
|
|
47
|
+
}
|
|
48
|
+
return result
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const renderNamedTemplate = (name: RenderTemplateName, variables: TemplateVariables, config: DocBridgeConfigV1, root?: string): string =>
|
|
52
|
+
renderCompiledTemplate(compile(resolveTemplateSource(name, config, root)), variables)
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bundled templates.
|
|
3
|
+
*
|
|
4
|
+
* They live in a TypeScript module rather than next to it as `.md` files because the package
|
|
5
|
+
* ships `dist/` built by tsup, which bundles source and nothing else: a template on disk is a
|
|
6
|
+
* template the published package does not have. `ak-docs render <name> --print-template` prints
|
|
7
|
+
* one, which is how a project starts an override under `render.templates`.
|
|
8
|
+
*
|
|
9
|
+
* knap syntax, with one rule the syntax does not advertise: a block tag whose output is empty
|
|
10
|
+
* swallows the line break after it, and a block whose output is not empty swallows the line
|
|
11
|
+
* break at its own start instead. So a list is written as `{% for %}` followed by a standalone
|
|
12
|
+
* `{% if not … %}` block for its empty state — exactly one of the two renders, exactly one line
|
|
13
|
+
* break is consumed, and the blank line before the next heading survives either way. A test
|
|
14
|
+
* renders every template through knap's own engine to keep these shapes honest.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const RENDER_TEMPLATE_NAMES = ['llms.txt', 'area', 'ownership', 'change-digest', 'overlay-review'] as const
|
|
18
|
+
|
|
19
|
+
export type RenderTemplateName = (typeof RENDER_TEMPLATE_NAMES)[number]
|
|
20
|
+
|
|
21
|
+
export type RenderTemplateInfo = {
|
|
22
|
+
readonly name: RenderTemplateName
|
|
23
|
+
readonly description: string
|
|
24
|
+
/** What `--data` may point at, in place of the default source. */
|
|
25
|
+
readonly data: string
|
|
26
|
+
/** Whether the output is one Markdown page per unit rather than a single file. */
|
|
27
|
+
readonly multiPage: boolean
|
|
28
|
+
/** Whether the output carries generated-region markers. `llms.txt` is a whole-file artifact with its own consumers and carries none. */
|
|
29
|
+
readonly generatedRegion: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const RENDER_TEMPLATES: Readonly<Record<RenderTemplateName, RenderTemplateInfo>> = {
|
|
33
|
+
'llms.txt': {
|
|
34
|
+
name: 'llms.txt',
|
|
35
|
+
description: 'The curated reading order for agents, as `ak-docs index` writes it',
|
|
36
|
+
data: 'a DocBridgeIndex (.doc-bridge/index.json)',
|
|
37
|
+
multiPage: false,
|
|
38
|
+
generatedRegion: false,
|
|
39
|
+
},
|
|
40
|
+
area: {
|
|
41
|
+
name: 'area',
|
|
42
|
+
description: 'One page per code area: purpose, modules, documents, related areas, checks and open findings',
|
|
43
|
+
data: 'a DocBridgeIndex (.doc-bridge/index.json)',
|
|
44
|
+
multiPage: true,
|
|
45
|
+
generatedRegion: true,
|
|
46
|
+
},
|
|
47
|
+
ownership: {
|
|
48
|
+
name: 'ownership',
|
|
49
|
+
description: 'One sidecar per ownership record: where to start, what to read, what to run',
|
|
50
|
+
data: 'a DocBridgeIndex (.doc-bridge/index.json)',
|
|
51
|
+
multiPage: true,
|
|
52
|
+
generatedRegion: true,
|
|
53
|
+
},
|
|
54
|
+
'change-digest': {
|
|
55
|
+
name: 'change-digest',
|
|
56
|
+
description: 'Entities and documents whose content hash moved since the last scan, and the documents that should have moved with them',
|
|
57
|
+
data: 'the previous discovery snapshot to compare against (default: the last `ak-docs scan`)',
|
|
58
|
+
multiPage: false,
|
|
59
|
+
generatedRegion: true,
|
|
60
|
+
},
|
|
61
|
+
'overlay-review': {
|
|
62
|
+
name: 'overlay-review',
|
|
63
|
+
description: 'Pending agent proposals with their evidence, for a human to judge',
|
|
64
|
+
data: 'an enrichment overlay (default: .doc-bridge/enrich/overlay.json)',
|
|
65
|
+
multiPage: false,
|
|
66
|
+
generatedRegion: true,
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const isRenderTemplateName = (value: string): value is RenderTemplateName =>
|
|
71
|
+
(RENDER_TEMPLATE_NAMES as readonly string[]).includes(value)
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
* `llms.txt` must stay byte-identical to what the string concatenation produced, including the
|
|
75
|
+
* empty corpus: a preamble, a blank line, the heading, a blank line, the entries and a final
|
|
76
|
+
* newline — which for no entries is `## Knowledge` followed by three line breaks. An empty loop
|
|
77
|
+
* swallows the line break after `{% endfor %}`, so the empty case gets its own block: its three
|
|
78
|
+
* line breaks become two after the closing tag's line is trimmed and one after the block's own
|
|
79
|
+
* trim, which is the one the loop swallowed. The template ends at `{% endif %}` on purpose.
|
|
80
|
+
*/
|
|
81
|
+
const LLMS_TXT = `{{ preamble }}
|
|
82
|
+
|
|
83
|
+
## Knowledge
|
|
84
|
+
|
|
85
|
+
{% for entry in entries %}
|
|
86
|
+
- [{{ entry.title }}]({{ entry.url }}){% if entry.description %}: {{ entry.description }}{% endif %}
|
|
87
|
+
{% endfor %}
|
|
88
|
+
{% if not entries %}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
{% endif %}`
|
|
92
|
+
|
|
93
|
+
const AREA = `{{ region.open }}
|
|
94
|
+
# Area \`{{ area.path }}\`
|
|
95
|
+
|
|
96
|
+
{{ area.purpose }}
|
|
97
|
+
{% if area.ownership %}
|
|
98
|
+
|
|
99
|
+
{{ area.ownership }}
|
|
100
|
+
{% endif %}
|
|
101
|
+
|
|
102
|
+
## Modules
|
|
103
|
+
|
|
104
|
+
{% for module in area.modules %}
|
|
105
|
+
- \`{{ module.path }}\`{% if module.symbols %}: exports {{ module.symbols }}{% endif %}
|
|
106
|
+
{% endfor %}
|
|
107
|
+
{% if not area.modules %}
|
|
108
|
+
No module was observed in this area.
|
|
109
|
+
{% endif %}
|
|
110
|
+
|
|
111
|
+
## Documents
|
|
112
|
+
|
|
113
|
+
{% for document in area.documents %}
|
|
114
|
+
- [{{ document.title }}]({{ document.path }}): {{ document.relation }}
|
|
115
|
+
{% endfor %}
|
|
116
|
+
{% if not area.documents %}
|
|
117
|
+
No document covers or mentions this area.
|
|
118
|
+
{% endif %}
|
|
119
|
+
|
|
120
|
+
## Related areas
|
|
121
|
+
|
|
122
|
+
{% for related in area.related %}
|
|
123
|
+
- \`{{ related.path }}\`: {{ related.direction }}, {{ related.strength }} import(s) — {{ related.evidence }}
|
|
124
|
+
{% endfor %}
|
|
125
|
+
{% if not area.related %}
|
|
126
|
+
No import crosses this area's boundary.
|
|
127
|
+
{% endif %}
|
|
128
|
+
|
|
129
|
+
## Checks
|
|
130
|
+
|
|
131
|
+
{% for check in area.checks %}
|
|
132
|
+
- \`{{ check }}\`
|
|
133
|
+
{% endfor %}
|
|
134
|
+
{% if not area.checks %}
|
|
135
|
+
No check is declared for this area.
|
|
136
|
+
{% endif %}
|
|
137
|
+
{% if area.checksSource %}
|
|
138
|
+
|
|
139
|
+
Source: {{ area.checksSource }}.
|
|
140
|
+
{% endif %}
|
|
141
|
+
|
|
142
|
+
## Open findings
|
|
143
|
+
|
|
144
|
+
{% for finding in area.findings %}
|
|
145
|
+
- **{{ finding.code }}** ({{ finding.severity }}, {{ finding.status }}): {{ finding.message }} — \`{{ finding.evidence }}\`
|
|
146
|
+
{% endfor %}
|
|
147
|
+
{% if not area.findings %}
|
|
148
|
+
{{ area.findingsEmpty }}
|
|
149
|
+
{% endif %}
|
|
150
|
+
{{ region.close }}
|
|
151
|
+
`
|
|
152
|
+
|
|
153
|
+
const OWNERSHIP = `---
|
|
154
|
+
type: {{ owner.kind }}
|
|
155
|
+
id: {{ owner.id }}
|
|
156
|
+
editRoot: {{ owner.path }}
|
|
157
|
+
{% if owner.humanDoc %}
|
|
158
|
+
humanDoc: {{ owner.humanDoc }}
|
|
159
|
+
{% endif %}
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
{{ region.open }}
|
|
163
|
+
# {{ owner.id }}
|
|
164
|
+
|
|
165
|
+
{{ owner.purpose }}
|
|
166
|
+
|
|
167
|
+
## Start here
|
|
168
|
+
|
|
169
|
+
{{ owner.startHere }}
|
|
170
|
+
|
|
171
|
+
## Read before editing
|
|
172
|
+
|
|
173
|
+
{% for path in owner.readBeforeEditing %}
|
|
174
|
+
- {{ path }}
|
|
175
|
+
{% endfor %}
|
|
176
|
+
{% if not owner.readBeforeEditing %}
|
|
177
|
+
Nothing beyond the start page.
|
|
178
|
+
{% endif %}
|
|
179
|
+
|
|
180
|
+
## Edit roots
|
|
181
|
+
|
|
182
|
+
{% for path in owner.editRoots %}
|
|
183
|
+
- \`{{ path }}\`
|
|
184
|
+
{% endfor %}
|
|
185
|
+
|
|
186
|
+
## Checks
|
|
187
|
+
|
|
188
|
+
{% for check in owner.checks %}
|
|
189
|
+
- \`{{ check }}\`
|
|
190
|
+
{% endfor %}
|
|
191
|
+
{% if not owner.checks %}
|
|
192
|
+
No check is declared.
|
|
193
|
+
{% endif %}
|
|
194
|
+
{% if owner.checksSource %}
|
|
195
|
+
|
|
196
|
+
Source: {{ owner.checksSource }}.
|
|
197
|
+
{% endif %}
|
|
198
|
+
|
|
199
|
+
## Related areas
|
|
200
|
+
|
|
201
|
+
{% for related in owner.related %}
|
|
202
|
+
- \`{{ related.path }}\`: {{ related.direction }}, {{ related.strength }} import(s) — {{ related.evidence }}
|
|
203
|
+
{% endfor %}
|
|
204
|
+
{% if not owner.related %}
|
|
205
|
+
No import crosses this unit's boundary.
|
|
206
|
+
{% endif %}
|
|
207
|
+
{{ region.close }}
|
|
208
|
+
`
|
|
209
|
+
|
|
210
|
+
const CHANGE_DIGEST = `{{ region.open }}
|
|
211
|
+
# Change digest
|
|
212
|
+
|
|
213
|
+
Compared the current tree with the previous snapshot: {{ digest.summary }}.
|
|
214
|
+
|
|
215
|
+
## Changed
|
|
216
|
+
|
|
217
|
+
{% for entity in digest.changed %}
|
|
218
|
+
- \`{{ entity.path }}\` ({{ entity.kind }}): {{ entity.previousHash }} → {{ entity.currentHash }}
|
|
219
|
+
{% endfor %}
|
|
220
|
+
{% if not digest.changed %}
|
|
221
|
+
No file-backed entity changed.
|
|
222
|
+
{% endif %}
|
|
223
|
+
|
|
224
|
+
## Added
|
|
225
|
+
|
|
226
|
+
{% for entity in digest.added %}
|
|
227
|
+
- \`{{ entity.path }}\` ({{ entity.kind }}): {{ entity.currentHash }}
|
|
228
|
+
{% endfor %}
|
|
229
|
+
{% if not digest.added %}
|
|
230
|
+
Nothing was added.
|
|
231
|
+
{% endif %}
|
|
232
|
+
|
|
233
|
+
## Removed
|
|
234
|
+
|
|
235
|
+
{% for entity in digest.removed %}
|
|
236
|
+
- \`{{ entity.path }}\` ({{ entity.kind }}): {{ entity.previousHash }}
|
|
237
|
+
{% endfor %}
|
|
238
|
+
{% if not digest.removed %}
|
|
239
|
+
Nothing was removed.
|
|
240
|
+
{% endif %}
|
|
241
|
+
|
|
242
|
+
## Documentation to review
|
|
243
|
+
|
|
244
|
+
{% for document in digest.documentsToReview %}
|
|
245
|
+
- \`{{ document.path }}\`: {{ document.because }}
|
|
246
|
+
{% endfor %}
|
|
247
|
+
{% if not digest.documentsToReview %}
|
|
248
|
+
No unchanged document covers or mentions a changed entity.
|
|
249
|
+
{% endif %}
|
|
250
|
+
{{ region.close }}
|
|
251
|
+
`
|
|
252
|
+
|
|
253
|
+
const OVERLAY_REVIEW = `{{ region.open }}
|
|
254
|
+
# Overlay review
|
|
255
|
+
|
|
256
|
+
{% if overlay.present %}
|
|
257
|
+
{{ overlay.summary }}
|
|
258
|
+
|
|
259
|
+
{% for proposal in overlay.pending %}
|
|
260
|
+
## {{ proposal.kind }}: \`{{ proposal.entity }}\`
|
|
261
|
+
|
|
262
|
+
{{ proposal.reason }}
|
|
263
|
+
|
|
264
|
+
Confidence {{ proposal.confidence }} · proposal \`{{ proposal.proposalId }}\`
|
|
265
|
+
|
|
266
|
+
{% for item in proposal.evidence %}
|
|
267
|
+
- {{ item.link }}
|
|
268
|
+
{% endfor %}
|
|
269
|
+
{% if not proposal.evidence %}
|
|
270
|
+
No evidence was attached to this proposal.
|
|
271
|
+
{% endif %}
|
|
272
|
+
|
|
273
|
+
{% endfor %}
|
|
274
|
+
{% if not overlay.pending %}
|
|
275
|
+
No proposal is pending review.
|
|
276
|
+
{% endif %}
|
|
277
|
+
{% else %}
|
|
278
|
+
No enrichment overlay exists for this repository. Run \`ak-docs enrich\` to produce one, or pass \`--data <overlay.json>\`.
|
|
279
|
+
{% endif %}
|
|
280
|
+
{{ region.close }}
|
|
281
|
+
`
|
|
282
|
+
|
|
283
|
+
export const BUNDLED_TEMPLATES: Readonly<Record<RenderTemplateName, string>> = {
|
|
284
|
+
'llms.txt': LLMS_TXT,
|
|
285
|
+
area: AREA,
|
|
286
|
+
ownership: OWNERSHIP,
|
|
287
|
+
'change-digest': CHANGE_DIGEST,
|
|
288
|
+
'overlay-review': OVERLAY_REVIEW,
|
|
289
|
+
}
|
package/src/report/html.ts
CHANGED
|
@@ -354,7 +354,7 @@ styles += String.raw`.graph-node .node-label,.graph-node text.node-label{fill:va
|
|
|
354
354
|
styles += String.raw`.report-tabs{display:flex;gap:0;align-items:flex-end;border-bottom:1px solid var(--line);margin:24px 0 0}.report-tabs .tab{font-size:13px}.breadcrumbs{display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin:0 0 14px;color:var(--muted);font-size:12px}.breadcrumbs button{border:0;background:none;color:var(--blue);padding:3px 0;font-weight:700}.breadcrumbs button:hover{text-decoration:underline}.breadcrumbs .current{color:var(--ink);font-weight:700}.scope-note{margin:0 0 12px}.report-dashboard{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(280px,1fr);gap:24px}.report-table{width:100%;border-collapse:collapse;font-size:12px}.report-table th,.report-table td{border-top:1px solid var(--line);padding:10px 8px;text-align:left;vertical-align:top}.report-table th{color:var(--muted);font-size:10px;letter-spacing:.08em;text-transform:uppercase}.report-table td:first-child{font-weight:750}.bar-list{display:grid;gap:10px}.bar-item{display:grid;grid-template-columns:minmax(120px,1fr) 2fr auto;gap:10px;align-items:center;font-size:12px}.bar-item i{height:8px;background:var(--line);display:block;overflow:hidden}.bar-item i b{display:block;height:100%;background:var(--green)}.map-wrap{touch-action:none;cursor:grab}.map-wrap.dragging{cursor:grabbing}@media(max-width:700px){.report-tabs{overflow:auto}.report-tabs .tab{white-space:nowrap}.report-dashboard{grid-template-columns:1fr}.bar-item{grid-template-columns:minmax(100px,1fr) 1fr auto}}`
|
|
355
355
|
|
|
356
356
|
styles += String.raw`body[data-report-view="architecture"] #insights,body[data-report-view="architecture"] .filters,body[data-report-view="architecture"] .run,body[data-report-view="architecture"] .metadata{display:none}body[data-report-view="insights"] .workspace,body[data-report-view="insights"] .filters,body[data-report-view="insights"] .run,body[data-report-view="insights"] .metadata{display:none}body[data-report-view="findings"] .workspace,body[data-report-view="findings"] #insights,body[data-report-view="findings"] #report-dashboard,body[data-report-view="findings"] #coverage-title,body[data-report-view="findings"] #coverage-title~*{display:none}body[data-report-view="findings"] .run[aria-labelledby="coverage-title"]{display:none}body[data-report-view="coverage"] .workspace,body[data-report-view="coverage"] #insights,body[data-report-view="coverage"] #report-dashboard,body[data-report-view="coverage"] .filters,body[data-report-view="coverage"] .run[aria-labelledby="run-title"],body[data-report-view="coverage"] .metadata{display:none}body[data-report-view="insights"] #report-dashboard{display:block}body[data-report-view="map"] #report-dashboard{display:none}`
|
|
357
|
-
styles += String.raw`body[data-report-view="architecture"] #report-dashboard{display:none!important}.edge{fill:none}`
|
|
357
|
+
styles += String.raw`body[data-report-view="architecture"] #report-dashboard{display:none!important}.edge{fill:none}.edge.proposed{stroke-dasharray:6 4}`
|
|
358
358
|
|
|
359
359
|
styles += String.raw`:root{--ink:#17251f;--muted:#586b63;--paper:#f2f5f1;--panel:#fbfcf9;--line:#c9d5cc;--green:#13734a;--blue:#155f78;--amber:#995d08;--red:#a52e2b}html,body{max-width:100%;overflow-x:hidden}body{font-size:15px;line-height:1.55}.shell{width:100%;max-width:1600px;margin:0 auto;padding:28px clamp(16px,3vw,48px) 56px}.masthead{gap:32px;padding:0 0 24px;align-items:flex-start}.masthead h1{font-size:clamp(2.8rem,5.5vw,5.4rem);line-height:.94;margin:12px 0 16px;max-width:100%;overflow-wrap:anywhere}.lede{max-width:60ch;font-size:16px}.run-meta{min-width:0;max-width:300px}.summary{margin:0;padding:18px 0 20px;grid-template-columns:repeat(5,minmax(0,1fr));gap:0}.metric{min-width:0;padding:0 16px;border-inline-start:1px solid var(--line)}.metric:first-child{padding-inline-start:0;border-inline-start:0}.metric b{font-size:clamp(1.8rem,3vw,3rem);max-width:100%;overflow-wrap:anywhere}.metric span{font-size:10px;letter-spacing:.06em;line-height:1.25}.lens-bar{margin:24px 0 4px;align-items:flex-end}.lens-bar>.subtle{max-width:52ch}.report-tabs{max-width:100%;overflow-x:auto}.tab,.level{min-height:40px;padding:8px 12px}.filters{max-width:100%;align-items:center}.filters label{min-width:0}.filters input,.filters select{max-width:100%;min-width:0}.workspace{min-width:0;grid-template-columns:minmax(0,1fr) minmax(280px,340px);gap:20px}.panel{min-width:0}.map-wrap{min-width:0;max-width:100%;width:100%;overflow:hidden;contain:layout paint}#graph{display:block;width:100%!important;min-width:0!important;max-width:100%;height:clamp(480px,60vh,680px)!important}.side{min-width:0}.detail-title,.path,.list,.finding,.finding-group,.report-table{min-width:0;overflow-wrap:anywhere}.report-dashboard{min-width:0}.report-table{table-layout:fixed}.report-table th,.report-table td{overflow-wrap:anywhere}.bar-item{min-width:0}.bar-item span{min-width:0;overflow-wrap:anywhere}.tab:focus-visible,.level:focus-visible,input:focus-visible,select:focus-visible{outline:3px solid var(--blue);outline-offset:2px}@media(max-width:1100px){.workspace{grid-template-columns:1fr}.side{position:static}.summary{grid-template-columns:repeat(3,minmax(0,1fr));row-gap:16px}.summary .metric:nth-child(4){border-inline-start:0;padding-inline-start:0}}@media(max-width:700px){.shell{padding:12px 16px 32px}.masthead{padding-bottom:14px}.masthead h1{font-size:clamp(2.2rem,11vw,3.4rem);margin:6px 0 10px}.lede{font-size:14px;line-height:1.35}.run-meta{display:none}.summary{padding:10px 0 12px;grid-template-columns:repeat(2,minmax(0,1fr));row-gap:8px}.summary .metric:nth-child(even){border-inline-start:1px solid var(--line);padding-inline-start:12px}.summary .metric:nth-child(odd){border-inline-start:0;padding-inline-start:0}.metric{padding-inline:10px}.metric b{font-size:1.65rem}.lens-bar{display:block;margin-top:14px}.lens-bar>.subtle{display:none}.filters{display:grid;grid-template-columns:1fr;gap:10px}.filters label{display:grid;grid-template-columns:1fr;gap:5px}.filters input,.filters select,.filters button{width:100%;min-height:44px}.panel{padding:16px}.panel-head{display:block}.panel-head>.tabs{margin-top:12px}.finding-groups{grid-template-columns:1fr}.report-dashboard{grid-template-columns:1fr}#graph{height:520px!important}.lens-bar + #lens-caption{margin:8px 0}.lens-bar + #lens-caption + #insights{margin-top:8px}}@media(prefers-color-scheme:dark){:root{--ink:#edf5ef;--muted:#b7c6ba;--paper:#111815;--panel:#1a2821;--line:#405248;--green:#73d19d;--blue:#8bd1e6;--amber:#f0bd70;--red:#ff9389}}`
|
|
360
360
|
|
|
@@ -419,7 +419,7 @@ document.addEventListener("click",(event)=>{const node=event.target.closest?.("[
|
|
|
419
419
|
document.addEventListener("click",(event)=>{const crumb=event.target.closest?.("[data-breadcrumb-level]");if(!crumb)return;event.preventDefault();event.stopImmediatePropagation();state.level=crumb.dataset.breadcrumbLevel;state.selected=crumb.dataset.breadcrumbSelected||null;mapState.scale=1;mapState.x=0;mapState.y=0;render()},true);
|
|
420
420
|
const diagnosticsFor=(id)=>(findingIndex.get(id)||[]).filter((finding)=>finding.status!=="confirmed");
|
|
421
421
|
const renderDetails=()=>{const panel=document.querySelector("#details"),entity=state.selected?byId.get(state.selected):null,group=state.selected?groupById.get(state.selected):null;if(group){const members=group.members.map((id)=>byId.get(id)).filter(Boolean),relations=new Set(),ids=groupEntityIds.get(state.selected)||new Set();ids.forEach((id)=>(relationsByEntity.get(id)||[]).forEach((relation)=>relations.add(relation)));const findings=nodeIssues(state.selected),evidence=members.flatMap((member)=>member.evidence||[]).slice(0,8),unit=group.kind==="external"?"dependencies":"packages";panel.innerHTML="<div class=\"eyebrow\">Selected group</div><h2 class=\"detail-title\">"+esc(group.name)+"</h2><span class=\"tag\">"+esc(group.kind)+"</span><p class=\"path\">"+esc(group.path||"Derived from stable package identity and repository structure")+"</p><div class=\"detail-grid\"><div><b>"+members.length+"</b><span>"+unit+"</span></div><div><b>"+relations.size+"</b><span>relations</span></div><div><b>"+findings.length+"</b><span>findings</span></div><div><b>"+evidence.length+"</b><span>evidence items</span></div></div><h3>Members</h3><ul class=\"list\">"+(members.length?members.slice(0,8).map((member)=>"<li>"+esc(label(member))+"</li>").join(""):"<li>No members recorded.</li>")+(members.length>8?"<li class=\"subtle\">+"+(members.length-8)+" more — choose Package level to inspect all.</li>":"")+"</ul>"+(findings.length?"<h3 style=\"margin-top:16px\">Attention</h3><ul class=\"list\">"+findings.slice(0,5).map((finding)=>"<li><span class=\"tag "+esc(finding.severity)+"\">"+esc(finding.severity)+"</span> "+esc(finding.code)+"</li>").join("")+"</ul>":"");return}if(!entity){panel.innerHTML="<p class=\"subtle\">Select a node in the map to inspect its evidence, connectivity, and findings.</p>";return}const relations=relationsByEntity.get(entity.id)||[],incoming=relations.filter((relation)=>relation.to===entity.id),outgoing=relations.filter((relation)=>relation.from===entity.id),findings=diagnosticsFor(entity.id),evidence=[...(entity.evidence||[]),...incoming.flatMap((relation)=>relation.evidence||[]),...outgoing.flatMap((relation)=>relation.evidence||[])].slice(0,8);panel.innerHTML="<div class=\"eyebrow\">Selected entity</div><h2 class=\"detail-title\">"+esc(label(entity))+"</h2><span class=\"tag\">"+esc(entity.kind)+"</span><p class=\"path\">"+esc(entity.path||entity.id)+"</p><div class=\"detail-grid\"><div><b>"+incoming.length+"</b><span>incoming</span></div><div><b>"+outgoing.length+"</b><span>outgoing</span></div><div><b>"+findings.length+"</b><span>findings</span></div><div><b>"+evidence.length+"</b><span>evidence items</span></div></div><h3>Evidence</h3><ul class=\"list\">"+(evidence.length?evidence.map((item)=>"<li>"+esc(item.path+(item.lineStart?":"+item.lineStart:"")+(item.context?" — "+item.context:""))+"</li>").join(""):"<li>No evidence recorded.</li>")+"</ul>"+(findings.length?"<h3 style=\"margin-top:16px\">Attention</h3><ul class=\"list\">"+findings.slice(0,5).map((finding)=>"<li><span class=\"tag "+esc(finding.severity)+"\">"+esc(finding.severity)+"</span> "+esc(finding.code)+"</li>").join("")+"</ul>":"")};
|
|
422
|
-
const renderGraph=()=>{const model=graphModel(),svg=document.querySelector("#graph"),dense=model.edges.length>64,edges=dense?[...model.edges].sort((a,b)=>(b.health?1:0)-(a.health?1:0)||b.count-a.count||a.from.localeCompare(b.from)||a.to.localeCompare(b.to)).slice(0,64):model.edges;if(!model.nodes.length){svg.innerHTML="<text x=\"500\" y=\"270\" text-anchor=\"middle\" class=\"subtle\">Select an app or package to expand this view.</text>";return}const ids=new Set(model.nodes.map((node)=>node.id)),incoming=new Map(model.nodes.map((node)=>[node.id,0])),outgoing=new Map(model.nodes.map((node)=>[node.id,[]]));edges.forEach((edge)=>{if(!ids.has(edge.from)||!ids.has(edge.to))return;incoming.set(edge.to,(incoming.get(edge.to)||0)+1);outgoing.get(edge.from).push(edge.to)});const rank=new Map(),work=[];model.nodes.filter((node)=>(incoming.get(node.id)||0)===0).sort((a,b)=>label(a).localeCompare(label(b))||a.id.localeCompare(b.id)).forEach((node)=>{rank.set(node.id,0);work.push(node)});for(let index=0;index<work.length;index++){const node=work[index];for(const next of outgoing.get(node.id)||[]){const nextRank=Math.max(rank.get(next)||0,(rank.get(node.id)||0)+1);rank.set(next,nextRank);if(!work.some((item)=>item.id===next))work.push(byId.get(next)||{id:next,name:next})}}model.nodes.forEach((node)=>{if(!rank.has(node.id))rank.set(node.id,0)});const columns=new Map();model.nodes.forEach((node)=>{const items=columns.get(rank.get(node.id))||[];items.push(node);columns.set(rank.get(node.id),items)});for(const items of columns.values())items.sort((a,b)=>label(a).localeCompare(label(b))||a.id.localeCompare(b.id));if(dense){columns.clear();model.nodes.forEach((node,index)=>{const column=Math.floor(index/8),items=columns.get(column)||[];items.push(node);columns.set(column,items)})}const maxRows=Math.max(1,...[...columns.values()].map((items)=>items.length)),cellWidth=190,cellHeight=82,pad=28,columnCount=Math.max(1,...columns.keys())+1,width=Math.max(620,columnCount*cellWidth+pad*2,Math.min(1000,svg.clientWidth*1.05)),height=Math.max(520,maxRows*cellHeight+pad*2);svg.setAttribute("viewBox","0 0 "+width+" "+height);svg.style.width=dense?Math.max(1000,width)+"px":"100%";svg.style.minWidth="0";svg.style.height="100%";document.querySelector(".map-wrap")?.classList.toggle("dense",dense);const positions=new Map();for(const [column,items] of columns)items.forEach((node,row)=>positions.set(node.id,{x:pad+column*cellWidth+78,y:pad+row*cellHeight+27}));const defs="<defs><marker id=\"arrow\" markerWidth=\"8\" markerHeight=\"8\" refX=\"7\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L0,6 L7,3 z\" fill=\"#789087\"/></marker></defs>",edgeMarkup=edges.map((edge)=>{const from=positions.get(edge.from),to=positions.get(edge.to);if(!from||!to)return "";const midX=(from.x+to.x)/2,midY=(from.y+to.y)/2;return "<g><line class=\"edge "+(edge.health?"alert":"")+"\" x1=\""+from.x+"\" y1=\""+from.y+"\" x2=\""+to.x+"\" y2=\""+to.y+"\" marker-end=\"url(#arrow)\"/><text class=\"edge-label\" x=\""+midX+"\" y=\""+midY+"\">"+esc(edge.count>1?edge.count+"×":"")+"</text></g>"}).join(""),nodes=model.nodes.map((node)=>{const pos=positions.get(node.id),issues=nodeIssues(node.id),degree=degreeMap(edges).get(node.id)||0,selected=state.selected===node.id?" selected":"",issue=issues.length?" issue":"",nodeLabel=label(node),nodeKind=node.kind==="domain"?(node.memberCount||0)+" packages":node.kind;return "<g class=\"graph-node"+selected+issue+"\" transform=\"translate("+(pos.x-78)+","+(pos.y-27)+")\"><title>"+esc(nodeLabel)+" · "+esc(nodeKind)+"</title><rect width=\"156\" height=\"54\" rx=\"8\" role=\"button\" tabindex=\"0\" aria-label=\""+esc(nodeLabel+" "+nodeKind)+"\" data-node=\""+esc(node.id)+"\"></rect><text class=\"node-label\" x=\"10\" y=\"19\">"+esc(short(nodeLabel,22))+"</text><text class=\"node-kind\" x=\"10\" y=\"34\">"+esc(short(nodeKind,22))+"</text><text class=\"node-count\" x=\"144\" y=\"19\" text-anchor=\"end\">"+(degree||"")+"</text></g>"}).join("");svg.innerHTML=defs+edgeMarkup+nodes;routeGraphEdges();applyMapTransform();document.querySelector("#map-note").textContent=(state.level==="overview"?"Bounded domain view. ":state.level+" view. ")+"Directional edges are aggregated from canonical relations; "+model.edges.length+" visible connection groups"+(dense?" (showing "+edges.length+" prioritized of "+model.edges.length+").":".");};
|
|
422
|
+
const renderGraph=()=>{const model=graphModel(),svg=document.querySelector("#graph"),dense=model.edges.length>64,edges=dense?[...model.edges].sort((a,b)=>(b.health?1:0)-(a.health?1:0)||b.count-a.count||a.from.localeCompare(b.from)||a.to.localeCompare(b.to)).slice(0,64):model.edges;if(!model.nodes.length){svg.innerHTML="<text x=\"500\" y=\"270\" text-anchor=\"middle\" class=\"subtle\">Select an app or package to expand this view.</text>";return}const ids=new Set(model.nodes.map((node)=>node.id)),incoming=new Map(model.nodes.map((node)=>[node.id,0])),outgoing=new Map(model.nodes.map((node)=>[node.id,[]]));edges.forEach((edge)=>{if(!ids.has(edge.from)||!ids.has(edge.to))return;incoming.set(edge.to,(incoming.get(edge.to)||0)+1);outgoing.get(edge.from).push(edge.to)});const rank=new Map(),work=[];model.nodes.filter((node)=>(incoming.get(node.id)||0)===0).sort((a,b)=>label(a).localeCompare(label(b))||a.id.localeCompare(b.id)).forEach((node)=>{rank.set(node.id,0);work.push(node)});for(let index=0;index<work.length;index++){const node=work[index];for(const next of outgoing.get(node.id)||[]){const nextRank=Math.max(rank.get(next)||0,(rank.get(node.id)||0)+1);rank.set(next,nextRank);if(!work.some((item)=>item.id===next))work.push(byId.get(next)||{id:next,name:next})}}model.nodes.forEach((node)=>{if(!rank.has(node.id))rank.set(node.id,0)});const columns=new Map();model.nodes.forEach((node)=>{const items=columns.get(rank.get(node.id))||[];items.push(node);columns.set(rank.get(node.id),items)});for(const items of columns.values())items.sort((a,b)=>label(a).localeCompare(label(b))||a.id.localeCompare(b.id));if(dense){columns.clear();model.nodes.forEach((node,index)=>{const column=Math.floor(index/8),items=columns.get(column)||[];items.push(node);columns.set(column,items)})}const maxRows=Math.max(1,...[...columns.values()].map((items)=>items.length)),cellWidth=190,cellHeight=82,pad=28,columnCount=Math.max(1,...columns.keys())+1,width=Math.max(620,columnCount*cellWidth+pad*2,Math.min(1000,svg.clientWidth*1.05)),height=Math.max(520,maxRows*cellHeight+pad*2);svg.setAttribute("viewBox","0 0 "+width+" "+height);svg.style.width=dense?Math.max(1000,width)+"px":"100%";svg.style.minWidth="0";svg.style.height="100%";document.querySelector(".map-wrap")?.classList.toggle("dense",dense);const positions=new Map();for(const [column,items] of columns)items.forEach((node,row)=>positions.set(node.id,{x:pad+column*cellWidth+78,y:pad+row*cellHeight+27}));const defs="<defs><marker id=\"arrow\" markerWidth=\"8\" markerHeight=\"8\" refX=\"7\" refY=\"3\" orient=\"auto\"><path d=\"M0,0 L0,6 L7,3 z\" fill=\"#789087\"/></marker></defs>",edgeMarkup=edges.map((edge)=>{const from=positions.get(edge.from),to=positions.get(edge.to);if(!from||!to)return "";const midX=(from.x+to.x)/2,midY=(from.y+to.y)/2;const proposed=[...edge.relationIds].length>0&&[...edge.relationIds].every((id)=>relationById.get(id)?.provenance==="proposed");return "<g><line class=\"edge "+(edge.health?"alert":"")+(proposed?" proposed":"")+"\" x1=\""+from.x+"\" y1=\""+from.y+"\" x2=\""+to.x+"\" y2=\""+to.y+"\" marker-end=\"url(#arrow)\"/><text class=\"edge-label\" x=\""+midX+"\" y=\""+midY+"\">"+esc(edge.count>1?edge.count+"×":"")+"</text></g>"}).join(""),nodes=model.nodes.map((node)=>{const pos=positions.get(node.id),issues=nodeIssues(node.id),degree=degreeMap(edges).get(node.id)||0,selected=state.selected===node.id?" selected":"",issue=issues.length?" issue":"",nodeLabel=label(node),nodeKind=node.kind==="domain"?(node.memberCount||0)+" packages":node.kind;return "<g class=\"graph-node"+selected+issue+"\" transform=\"translate("+(pos.x-78)+","+(pos.y-27)+")\"><title>"+esc(nodeLabel)+" · "+esc(nodeKind)+"</title><rect width=\"156\" height=\"54\" rx=\"8\" role=\"button\" tabindex=\"0\" aria-label=\""+esc(nodeLabel+" "+nodeKind)+"\" data-node=\""+esc(node.id)+"\"></rect><text class=\"node-label\" x=\"10\" y=\"19\">"+esc(short(nodeLabel,22))+"</text><text class=\"node-kind\" x=\"10\" y=\"34\">"+esc(short(nodeKind,22))+"</text><text class=\"node-count\" x=\"144\" y=\"19\" text-anchor=\"end\">"+(degree||"")+"</text></g>"}).join("");svg.innerHTML=defs+edgeMarkup+nodes;routeGraphEdges();applyMapTransform();document.querySelector("#map-note").textContent=(state.level==="overview"?"Bounded domain view. ":state.level+" view. ")+"Directional edges are aggregated from canonical relations; "+model.edges.length+" visible connection groups"+(dense?" (showing "+edges.length+" prioritized of "+model.edges.length+").":".");};
|
|
423
423
|
const findingMatches=(finding)=>{const query=state.query.toLowerCase();return(!query||[finding.id,finding.code,finding.message,...finding.entityIds,...finding.relationIds].join(" ").toLowerCase().includes(query))&&(!state.status||finding.status===state.status)&&(!state.severity||finding.severity===state.severity)};
|
|
424
424
|
const findingScope=(finding)=>data.view?.diagnosticGroup?.[finding.id]?.[0]||(()=>{const ids=[...(finding.entityIds||[])];(finding.relationIds||[]).forEach((id)=>{const relation=relationById.get(id);if(relation)ids.push(relation.from,relation.to)});return ids.map(groupFor).sort()[0]||"repository"})();
|
|
425
425
|
const findingGroups=(findings)=>{const groups=new Map();findings.forEach((finding)=>{const scope=findingScope(finding),key=[scope,finding.code,finding.status,finding.severity].join("|"),group=groups.get(key)||{key,scope,code:finding.code,status:finding.status,severity:finding.severity,findings:[]};group.findings.push(finding);groups.set(key,group)});return[...groups.values()].sort((left,right)=>right.findings.length-left.findings.length||left.code.localeCompare(right.code)||left.key.localeCompare(right.key))};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { searchTokens } from '../query/text.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Field-weighted BM25 (BM25F).
|
|
5
|
+
*
|
|
6
|
+
* Retrieval used to score a record by summing token lengths over one flattened haystack, which
|
|
7
|
+
* made a long document beat a short exact match and gave every field the same authority. BM25
|
|
8
|
+
* fixes both: a term that appears in nearly every record earns almost nothing (IDF), a term in a
|
|
9
|
+
* short title outranks the same term buried in a body (per-field length normalization), and the
|
|
10
|
+
* field a term matched in is part of the result, so ranking can be explained rather than guessed.
|
|
11
|
+
*
|
|
12
|
+
* Ordering is total: score descending, then `ref` ascending. Scores are rounded before comparison
|
|
13
|
+
* so a ranking is reproducible and never depends on floating-point accumulation order.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const BM25_VERSION = 1 as const
|
|
17
|
+
|
|
18
|
+
const DEFAULT_K1 = 1.2
|
|
19
|
+
const DEFAULT_B = 0.75
|
|
20
|
+
const SCORE_PRECISION = 1_000_000
|
|
21
|
+
|
|
22
|
+
/** Per-field multipliers. A field absent from the map is not indexed at all. */
|
|
23
|
+
export type Bm25FieldWeights = Readonly<Record<string, number>>
|
|
24
|
+
|
|
25
|
+
export type Bm25Params = {
|
|
26
|
+
/** Term-frequency saturation. Higher rewards repetition more. */
|
|
27
|
+
readonly k1?: number
|
|
28
|
+
/** Length-normalization strength, 0 to 1. */
|
|
29
|
+
readonly b?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type Bm25FieldValue = string | readonly string[] | undefined
|
|
33
|
+
|
|
34
|
+
export type Bm25Input = {
|
|
35
|
+
readonly ref: string
|
|
36
|
+
readonly fields: Readonly<Record<string, Bm25FieldValue>>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type Bm25Hit = {
|
|
40
|
+
readonly ref: string
|
|
41
|
+
readonly score: number
|
|
42
|
+
/** Query terms that matched, per field, sorted. The raw material for an explain view. */
|
|
43
|
+
readonly matched: Readonly<Record<string, readonly string[]>>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type FieldPosting = {
|
|
47
|
+
readonly frequency: number
|
|
48
|
+
readonly length: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type IndexedDocument = {
|
|
52
|
+
readonly ref: string
|
|
53
|
+
/** field -> term -> frequency, plus the field's token length. */
|
|
54
|
+
readonly fields: ReadonlyMap<string, { readonly tokens: ReadonlyMap<string, number>; readonly length: number }>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type Bm25Index = {
|
|
58
|
+
readonly documents: readonly IndexedDocument[]
|
|
59
|
+
readonly documentFrequency: ReadonlyMap<string, number>
|
|
60
|
+
readonly averageFieldLength: ReadonlyMap<string, number>
|
|
61
|
+
readonly weights: Bm25FieldWeights
|
|
62
|
+
readonly k1: number
|
|
63
|
+
readonly b: number
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const fieldText = (value: Bm25FieldValue): string =>
|
|
67
|
+
value === undefined ? '' : Array.isArray(value) ? value.join(' ') : String(value)
|
|
68
|
+
|
|
69
|
+
const round = (value: number): number => Math.round(value * SCORE_PRECISION) / SCORE_PRECISION
|
|
70
|
+
|
|
71
|
+
export const buildBm25Index = (
|
|
72
|
+
inputs: readonly Bm25Input[],
|
|
73
|
+
weights: Bm25FieldWeights,
|
|
74
|
+
params: Bm25Params = {},
|
|
75
|
+
): Bm25Index => {
|
|
76
|
+
const fieldNames = Object.keys(weights).sort()
|
|
77
|
+
const documents: IndexedDocument[] = []
|
|
78
|
+
const documentFrequency = new Map<string, number>()
|
|
79
|
+
const fieldLengthTotal = new Map<string, number>(fieldNames.map((name) => [name, 0]))
|
|
80
|
+
|
|
81
|
+
for (const input of inputs) {
|
|
82
|
+
const fields = new Map<string, { tokens: ReadonlyMap<string, number>; length: number }>()
|
|
83
|
+
const seen = new Set<string>()
|
|
84
|
+
for (const name of fieldNames) {
|
|
85
|
+
const tokens = searchTokens(fieldText(input.fields[name]))
|
|
86
|
+
const frequencies = new Map<string, number>()
|
|
87
|
+
for (const token of tokens) {
|
|
88
|
+
frequencies.set(token, (frequencies.get(token) ?? 0) + 1)
|
|
89
|
+
seen.add(token)
|
|
90
|
+
}
|
|
91
|
+
fields.set(name, { tokens: frequencies, length: tokens.length })
|
|
92
|
+
fieldLengthTotal.set(name, (fieldLengthTotal.get(name) ?? 0) + tokens.length)
|
|
93
|
+
}
|
|
94
|
+
for (const token of seen) documentFrequency.set(token, (documentFrequency.get(token) ?? 0) + 1)
|
|
95
|
+
documents.push({ ref: input.ref, fields })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const count = documents.length || 1
|
|
99
|
+
return {
|
|
100
|
+
documents,
|
|
101
|
+
documentFrequency,
|
|
102
|
+
averageFieldLength: new Map(fieldNames.map((name) => [name, (fieldLengthTotal.get(name) ?? 0) / count])),
|
|
103
|
+
weights,
|
|
104
|
+
k1: params.k1 ?? DEFAULT_K1,
|
|
105
|
+
b: params.b ?? DEFAULT_B,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Probabilistic IDF, floored at zero so a term in every document can never subtract score. */
|
|
110
|
+
export const bm25Idf = (documentCount: number, documentFrequency: number): number =>
|
|
111
|
+
Math.max(0, Math.log(1 + (documentCount - documentFrequency + 0.5) / (documentFrequency + 0.5)))
|
|
112
|
+
|
|
113
|
+
const posting = (document: IndexedDocument, field: string, term: string): FieldPosting | undefined => {
|
|
114
|
+
const stats = document.fields.get(field)
|
|
115
|
+
if (!stats) return undefined
|
|
116
|
+
const frequency = stats.tokens.get(term)
|
|
117
|
+
return frequency === undefined ? undefined : { frequency, length: stats.length }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Score every document against the query terms. Documents with no matched term are omitted, so an
|
|
122
|
+
* unrelated query returns nothing rather than a list of weak guesses.
|
|
123
|
+
*/
|
|
124
|
+
export const bm25Search = (index: Bm25Index, terms: readonly string[]): Bm25Hit[] => {
|
|
125
|
+
if (!terms.length || !index.documents.length) return []
|
|
126
|
+
const unique = [...new Set(terms)].sort()
|
|
127
|
+
const fieldNames = Object.keys(index.weights).sort()
|
|
128
|
+
const hits: Bm25Hit[] = []
|
|
129
|
+
|
|
130
|
+
for (const document of index.documents) {
|
|
131
|
+
let score = 0
|
|
132
|
+
const matched = new Map<string, string[]>()
|
|
133
|
+
|
|
134
|
+
for (const term of unique) {
|
|
135
|
+
const idf = bm25Idf(index.documents.length, index.documentFrequency.get(term) ?? 0)
|
|
136
|
+
if (idf <= 0) continue
|
|
137
|
+
let weightedFrequency = 0
|
|
138
|
+
for (const field of fieldNames) {
|
|
139
|
+
const found = posting(document, field, term)
|
|
140
|
+
if (!found) continue
|
|
141
|
+
const average = index.averageFieldLength.get(field) ?? 0
|
|
142
|
+
const normalization = average > 0 ? 1 - index.b + index.b * (found.length / average) : 1
|
|
143
|
+
weightedFrequency += ((index.weights[field] ?? 0) * found.frequency) / normalization
|
|
144
|
+
const list = matched.get(field)
|
|
145
|
+
if (list) list.push(term)
|
|
146
|
+
else matched.set(field, [term])
|
|
147
|
+
}
|
|
148
|
+
if (weightedFrequency <= 0) continue
|
|
149
|
+
score += (idf * weightedFrequency) / (index.k1 + weightedFrequency)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (score <= 0) continue
|
|
153
|
+
hits.push({
|
|
154
|
+
ref: document.ref,
|
|
155
|
+
score: round(score),
|
|
156
|
+
matched: Object.fromEntries([...matched.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([field, list]) => [field, [...list].sort()])),
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return hits.sort((a, b) => (b.score !== a.score ? b.score - a.score : a.ref.localeCompare(b.ref)))
|
|
161
|
+
}
|