Skip to content

feat(docs): doc-comment links for eight more languages, diagrams and OpenAPI operations in the graph - #2593

Merged
DeusData merged 20 commits into
mainfrom
feat/doc-links-languages-diagrams
Oct 11, 2026
Merged

DeusData merged 20 commits into
mainfrom
feat/doc-links-languages-diagrams

Conversation

@DeusData

@DeusData DeusData commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Documentation names code. This PR brings three more kinds of it into the graph.

  1. Doc-comment references in eight more languages. C# came first. A reference in a doc comment now becomes a
    MENTIONS edge from the documented definition to the code it names, resolved by each language's own rules:

    Language References
    Go [Name], [pkg.Name], [Recv.Method]
    Java {@link}, {@linkplain}, @see, @throws, {@value}
    Kotlin [x], @see, @throws, @sample
    Rust intra-doc links and link definitions
    Perl POD L<>
    TypeScript and JavaScript {@link}
    PHP @see, {@see}
    C Doxygen @ref, @see
  2. Diagrams. Mermaid, PlantUML, Graphviz DOT, D2, draw.io and Excalidraw get nodes. They count whether they are
    fenced in Markdown, a reST directive, an AsciiDoc block, or a file of their own. Each one is a Diagram node with a
    DiagramElement per element (DEFINES) and a DIAGRAM_ARROW edge per arrow.

  3. OpenAPI operations. Each operation of an OpenAPI or Swagger document is a Route on the same qualified name
    a handler's route registration gets in code.

How the links were measured before shipping

Every new link family is gated (doclink.h, ships). A family ships only after a pre-registered held-out
audit
passes it:

  • Sample. 150 sampled links per tier (a census below 150) from 17 repositories that were never used to build or
    tune the resolvers.
  • Reading. Three blind reading rounds by independent readers.
  • Bar. Wilson 95% lower bound >= 0.90 and point estimate >= 0.95.
tier population judged correct Wilson low
Go doc links 5,980 150 150 0.975 ships
Java {@link} 23,934 150 150 0.975 ships
Java {@linkplain} 1,372 150 150 0.975 ships
Java @see 7,711 150 150 0.975 ships
Java @throws 650 150 150 0.975 ships
Java {@value} 79 79 79 0.954 ships
Kotlin [x] 3,888 150 150 0.975 ships
Kotlin @sample 279 150 150 0.975 ships
Kotlin @see 369 150 150 0.975 ships
Kotlin @throws 73 73 73 0.950 ships
Rust intra-doc 14,119 150 149 0.963 ships
Rust link definitions 1,491 150 150 0.975 ships
Perl L<> 2,255 150 150 0.975 ships
TS/JS {@link} 1,222 150 150 0.975 ships
PHP @see 5,171 150 150 0.975 ships
C @ref 463 150 150 0.975 ships
C @see 509 150 145 0.924 ships
TS/JS @see 18 18 18 0.824 held: too few held-out links
TS/JS {@linkcode} 10 10 10 0.722 held: too few held-out links

The families that fail on size still resolve. Their links are stored as rows (doc_link_unresolved, reason
below_bar_tier), not as edges.

The diagram and OpenAPI link families stay held. These are Mermaid click, PlantUML [[url]]/!include, the
draw.io/Excalidraw/DOT/D2 links and OpenAPI operationId. On 21 held-out repositories they produced 3 links in all,
too few to audit. A bare operationId links only when exactly one non-test function has that name. On the held-out
specs no such case occurred: the names matched nothing (31) or were ambiguous (24).

Diagram text in section candidates (experiment E)

A Markdown section embedding a diagram used to carry the diagram's syntax (-->, participant) in its text. Three
variants were compared on 147 held-out sections that embed diagrams: 503 section/candidate pairs, judged blind.

variant precision sections with a correct candidate
fence as written (before) 0.45 42
labels instead of syntax (shipped) 0.54 56
fence dropped 0.54 29

The shipped variant gains 20 sections and loses 6 (exact McNemar p = 0.009).

How likely a diagram section is about its candidates

A section that embeds a diagram is about its candidate functions more often than the Markdown curve says. How much
more depends on the kind of diagram:

  • structural: sequence, class, ER, C4, architecture, PlantUML use-case and component;
  • prose: flowcharts, state diagrams, mind maps, activity diagrams and other Mermaid or PlantUML kinds.

553 candidates from such sections in 16 held-out repositories were judged blind in three rounds. Each family now
carries its own p:

tfidf 0.20-0.30 0.30-0.40 0.40+ local extras
Markdown before (whole-project doc / doc inside a folder) not stored 0.22 / 0.33 0.51 / 0.53 0.32
prose diagram section 0.24 0.28 0.45 0.28
structural diagram section 0.32 0.46 0.57 0.78

A section's family is the highest of the diagrams it defines. The first band is now stored for both families.

The structural local-extras value rests on all 9 such candidates (95% interval 0.45-0.94).

Graph diagrams (DOT, D2) did not occur in Markdown sections. Their sections, like every other section, keep the
Markdown curve.

Compatibility

Verification

  • Held-out identity. The final production binary re-extracted all 20 held-out repositories, and the result was
    compared with the audited links. Every shipped link equals an audited link. The C# rows are identical, and so are all other unresolved rows apart from the below_bar_tier rows of held families. In two TS/JS doc comments the shipped {@link} edge now counts one reference instead of two: the second is an @see to the same target, a held family, so it is stored as a below_bar_tier row.
  • Proof on real input. Kubernetes' apiserver folder, a development corpus, was indexed with the binary before
    and after the diagram-section curve:
    • the sequence-diagram section's candidates moved from 0.22/0.51 to 0.46/0.57;
    • the flowchart sections gained five first-band candidates at 0.24;
    • every other section, and the graph (17,467 nodes, 102,672 edges), is unchanged.
  • Tests. Each new behaviour has tests. Reverting the diagram labels in section text, or the diagram family p,
    turns its test red.
  • Lint. clang-format (LLVM), the NOLINT whitelist and the memory-core linter pass. cppcheck 2.20.0, the version CI
    uses, is clean on the 37 changed C files.
  • Platforms. The Linux and Windows legs are left to this PR's CI.

…udit

Go doc comments name code as doc links ([Name], [Recv.Name], [pkg.Name],
[pkg.Recv.Name], an import path for pkg), the way go/doc/comment reads them.
The Go leg (doclink_go.c extracts them and a go.mod's module path as the
scope; doc_links_go.c resolves them against the package's declarations and
its imports) is ported onto the restacked core: the family row carries its
edge type, and resolve() takes the per-file state the core now passes (Go
keeps none).

The family is held (ships=false) until its held-out audit passes: a resolved
doc link is a below_bar_tier row until then. The suite ships it through the
test seam, like the other held families; doc_mentions_go_tokens asserts the
production default.

Kubernetes at 8c078bc32e90 with the family shipped (a measurement build):
1,928 edges (1,115 exact, 813 unique) and 101 rows (external 61, graph_gap
22, missing 18), the same numbers the leg measured on the old core.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…edges, held until their audit

Java's {@link}, {@linkplain}, @see, @throws and {@value}, and Kotlin's [Name]
links, @see, @throws and @sample, bound the way javadoc and Dokka search for a
member (one resolver for both languages, so a Kotlin doc can name a Java type
and back). The leg (doclink_jvm.c extracts the references and each file's
scope; doc_links_jvm.c resolves them) is ported onto the restacked core:
family rows carry their edge type, resolve() takes the per-file state (none
here). All nine families are held (ships=false) until their held-out audit
passes; the suite ships them through the test seam.

The graph moved under the leg since it was written: a Java field's node is
named by the field alone (no longer by its whole declarator, `MAX = 10`), and
a field named like a method of its class has its own node,
<Owner>.<name>#field, where the method used to take the name. The tests
follow: the expected field names lose the initializer, a changed
initializer is a local change repaired file by file (it renamed the node
before, which forced a full run), and a field that shares its accessor's name
is now an edge target and a source (doc_mentions_jvm_java_families_and_reasons,
doc_mentions_jvm_node_ownership), where both were graph_gap rows.

Measured with the leg's binary (1078036f) and this one on the same input:
- elasticsearch subset (libs/ + server/src/main at f3dc4b0bd3a0, 5,956 files):
  no edge lost; 66 new edges and 7 edges moved onto a field's own #field node;
  81 graph_gap rows gone, no new row, no reason changed. A seeded sample of 8
  of the gone rows read against the source: every new edge is right (a field
  named like its accessor, enum constants, a field's own doc as the source).
- Exposed (Kotlin, c419caaab61f): identical, 896 edges and 371 rows.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…eir audit

Rustdoc's intra-doc links ([`Name`], [path], [text](path), the kind
disambiguators struct@, fn@, macro@ ...) and links that take their destination
from a link reference definition, bound the way rustdoc resolves them: the
item's module scope, its use declarations, the crate root and the crates
Cargo.toml names. The leg (doclink_rs.c extracts the links and each file's
scope, a Cargo.toml its crate facts; doc_links_rs.c resolves them) is ported
onto the restacked core: family rows carry their edge type, resolve() takes
the per-file state (none here). Both families are held (ships=false) until
their held-out audit passes; the suite ships them through the test seam.

The graph moved under the leg: cfg-gated twins are one definition with its
variants on one node (42a9320), so a name declared twice under cfg now binds
that node instead of being ambiguous, and a #[cfg(test)] item keeps its plain
qualified name. The two tests follow.

Made lint-clean for CI's cppcheck 2.20 by refactoring, no suppression: the
`///` / `//!` test reads the line's indent through a helper and decides by
early returns (2.20 took the combined condition for always false), and
rs_put_node returns early for a missing definition (and for one without a
qualified name, which strlen would have read) before any arithmetic on it.
clang-format applied to both files.

meilisearch at 979167f13b5f, the leg's binary (5d8cb835) against this one with
the families shipped: edges and rows byte-identical (120 edges, 406 rows).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A POD section whose =head / =item names a sub is that sub's doc (#2459).
Everything else (NAME, DESCRIPTION, SEE ALSO, the text of a section around
the subs it holds, sections that name an attribute or helper without a node)
was stored nowhere, so nothing could read it. It is now the file's module doc,
stored on the File node as Go package comments and Rust inner docs are: every
POD block with the sections a sub's doc holds cut out, each remaining run as
it stands in the source, in document order and joined by a blank line (a sub's
doc joins its sections the same way); a run of only =cut, =pod, =over and
=back lines is dropped. A section is marked when a sub's doc takes it, so the
module doc is built after the definitions and the two never hold the same
text. Its first line is noted for the doc-link layer.

Mojolicious at 93f591742a7d: L< codes in stored docs 624 (subs, unchanged) +
1,595 (File nodes of 112 modules) = 2,219, the count of L<> codes in the POD
of its .pm and .pl files, none stored twice.

Test: doc_perl_module_doc_is_the_pod_no_sub_holds (the module doc's exact
text, the sub's doc unchanged, no module doc when every section is a sub's);
red at its first assertion with the module doc not built.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…r audit

A sub's POD section and the module's own POD (the File node's doc) name code
with L<> codes: L<Module::Name>, L<Module::Name/section>, L</section>, with a
`text|` prefix or not. doclink_perl.c reads them the way perlpodspec and
Pod::Simple do (verbatim paragraphs and data regions hold none, an L inside an
L is no link, a URL is the core's href family) and writes each file's scope:
its packages, its subs and its headings. Lines are exact: a sub's doc is
matched against the file's sections, and the module doc's runs (extract_defs.c)
are found again in the source in order; a module doc is POD from its start,
so a run that begins with text is read too.

doc_links_perl.c resolves them:
- a module is the file that declares its package; of several, the one the
  library layout names (.../Name/Space.pm), else ambiguous. The target is the
  file's File node, as the field test's audited prototype has it (a Module
  node is missing where a same-named Folder holds its qualified name,
  lib/Mojo.pm next to lib/Mojo/).
- L<Module/section>: the sub its heading names when the module's file
  defines that sub; any other section is a place in the module's
  documentation, the module.
- L</section>: a sub the heading names; a heading that names no sub is the
  page itself (local, neither edge nor row); a section the page has no
  heading for is missing.
- a module the repository does not declare (CPAN, the core, a .pod page:
  discovery keeps no .pod file) and a man page are external; test code
  (t/, xt/, *.t, and cbm_is_test_file, which has no Perl rule) binds only
  from test code.
There is no scope_delta hook yet: a change to a file's packages, subs or
headings re-resolves every Perl reference.

The family is held (ships=false) until its held-out audit passes; the suite
ships it through the test seam.

Tests (doc_mentions_perl): tokens with their lines, file-level and sub-level,
each read once; every resolution rule; incremental equals full (a module doc
edit and an added module-level reference repair the file, a heading change is
a full run); 60 modules give the same edges and rows with one worker and
several. Each rule red with it reverted (the module doc's line mapping, a
module run read from its start, the missing section, the library layout).

Mojolicious at 93f591742a7d (the family shipped, a measurement build) against
the field test's prototype records of its .pm/.pl files: 950 (source, target)
pairs both have; the 197 only the prototype has are all references within one
file (570), which the core makes no edge of; 8 only here come from the
extensionless scripts the prototype did not read. Every prototype row has its
row here: external 102, missing 1, and the 172 targets in .pod pages
external by the ruling for .pod pages.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…l their audit

A TypeScript, TSX or JavaScript doc comment (a block comment opening with a
slash and two stars) names code with {@link Ref}, {@linkcode Ref} (each with
an optional `|` label) and `@see Ref` at the start of a tag line that holds
no inline tag. doclink_ts.c harvests them (code spans and fenced blocks hold
none, {@linkplain} is not harvested, a hyphenated word after @see is prose, a
reference naming the declaration's own parameter is dropped, a URL is the
core's href family) and writes each file's scope: imports, exports, export *
barrels, top-level declarations, namespace bodies and functions or classes
declared inside a function body.

doc_links_ts.c resolves a reference as the TypeScript checker sees it from
the documented declaration, in one index over TS, TSX and JS files:
1. the namespaces around the reference, innermost first;
2. a function or class declared in the enclosing function body (the graph
   gives it a file-level name: two of one name in one file share one node,
   a graph gap);
3. the file's own top-level declarations;
4. its imports, followed through export { a as b }, export ... from and
   export * barrels (cycle guard, depth bound; two barrels that answer
   differently are ambiguous); a relative specifier resolves with the usual
   extension and index rules, a bare one is external;
5. the standard library's global names: external;
6. the top-level declarations of script files (no import, no export): one
   binds, several are ambiguous;
7. the members of the documented class or interface, `this.` included.
The other segments walk members or a module's exports. A member declared
without a node of its own is a graph gap; test code binds only from test
code. There is no scope_delta hook yet: an export change is a full run.

The families are held (ships=false) until their held-out audit passes; the
suite ships them through the test seam.

Proof: the TypeScript compiler (perf-bench/typescript b465fdbfe175), ship
build, against the field test's prototype records (cascade results left
out): 69 edges; 57 of the prototype's 61 pairs bound alike. The 4 others are
doc blocks without a definition of their own (on an `if` statement, a local
`var`, an enum member, a free-standing @typedef block), which the core does
not harvest. Of the 12 edges the prototype has no pair for, 4 are the same
targets the prototype resolved without recording a source node, and 8 bind
PropertyDescriptor to the repository's own src/lib/es5.d.ts, which the
prototype called an external library global (kept: the repository declares
it). Unresolved rows agree with the prototype's reasons apart from the
documented reason split (not_in_scope -> missing/ambiguous).

Known limit: package.json and tsconfig.json are not indexed (discovery
ignores them), so workspace packages and tsconfig `paths` stay external.

Tests (doc_mentions_ts): tokens with their lines; the scope blob and its
persisted form; every resolution rule, including a function-body
declaration that binds and two of one name that do not; incremental equals
full (a doc edit and a body edit repair one file, an export change is a full
run); 60 modules give the same edges and rows with one worker and several.
Each rule red with it reverted (the body declaration's visibility and its
line check, barrel ambiguity, the parameter rule, code spans, the library
globals).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…til their audit

A PHP doc block names code with `@see Ref` at the start of a tag line (a
description may follow) and with inline `{@see Ref}`, both family `see`.
doclink_php.c harvests them (code spans and fenced blocks hold none; @link,
@uses and the other tags are not harvested; a URL is the core's href family)
and writes each file's scope: namespace blocks, use imports, types with
their supertypes already qualified in their own file, members, functions.
The persisted scope drops lines and imports: other files read names only.
Declarations inside a top-level conditional count as top level
(`if (!function_exists('f')) { function f() {} }`), never a function body.

doc_links_php.c resolves a reference as a FQSEN with PHP's own name rules:
- a leading backslash is taken as written; otherwise the first segment is
  replaced when it is the alias of a class `use` of the namespace block the
  reference is written in, else the block's namespace is put in front
  (phpDocumentor's resolver);
- beyond that tool, as PHP itself has them: an unqualified function name is
  taken from a `use function` import, else from the current namespace, else
  from the global namespace; and self/static/parent name the documented
  type and its superclass;
- a bare `name()` that would bind a function but is written in a type that
  declares or inherits a method of that name is ambiguous (the method or the
  function; the reference does not say which);
- members are looked up on the type, its traits, its superclass and its
  interfaces (depth first); type, function and method names compare without
  case, properties and constants with it;
- properties, class constants and enum cases have no graph node: graph_gap;
  so has a type or function the graph folds into another one of the same
  name in its file (two namespace blocks), which the scope flags;
- a name the repository does not declare is external outside the
  repository's namespace roots (built-ins, Composer dependencies), else
  missing. Residual: a function a dependency declares in the current
  namespace is invisible, so the fallback would bind a global function of
  that name.
There is no scope_delta hook yet: a declaration change is a full run.

The family is held (ships=false) until its held-out audit passes; the suite
ships it through the test seam.

Proof (ship build, Apple M5 Pro / 48 GB, base 196ca71):
- koel 04ed8beaec34 vs the field test's prototype: 4 of its 5 pairs bound
  alike; the fifth sits in a class constant's doc block, which has no node.
- cakephp a75828db: 342 edge mentions + 39 unresolved rows = every one of
  the 381 non-URL references in function and type doc blocks; 36 more sit
  in property and constant doc blocks (no node, not harvested).
- symfony 9917e115: 452 edge mentions (434 edges) + 205 rows = all 657; 24
  in property, constant and other doc blocks.
- edges only through the global fallback: 1 (cakephp `@see debug()` in
  Cake\Core, the global debug() helper: correct); one more on symfony was
  wrong (`@see run()` meaning the class's own method bound a global script
  function) and the method-name rule above makes it ambiguous. Through
  self/static/parent: 20, all correct (12 + 5 self, 3 parent).
- a random sample of 24 edges (12 per corpus) read against their source
  lines: all correct.

Tests (doc_mentions_php): tokens with their lines; the scope blob, its
persisted form, namespace blocks and conditional declarations; every
resolution rule; the global fallback and self/static/parent each in a test
of its own; incremental equals full (a doc edit and a body edit repair one
file, a removed function is a full run); 60 modules give the same edges and
rows with one worker and several. Each rule red with it reverted (global
fallback, self/static, parent, trait members, the folded-name gap, use
aliases, method names without case, the bare-name method lookup, the
namespace roots, code spans, the method-name guard, conditional
declarations).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
… until their audit

A Doxygen comment in a .c or .h file (a block opening with slash-star-star
or slash-star-bang, a run of /// or //! lines) names code with `@ref name`
or `\ref name` (family doxygen_ref) and with the names an `@see`, `\see`,
`@sa` or `\sa` paragraph lists that have the shape of a symbol (`name`,
`name()`, `::name`, `Type::member`; family see). Code blocks, fences, code
spans, quoted labels and the word after \c, \p, \a, \b, \e, \em hold none; a
URL is the core's href family. A .h is parsed as C++, which does not change
the rule; other C++ and Objective-C files are not part of the leg.

C keeps no node for a function prototype, so the docs of a header's API
were never stored anywhere. The doc-link driver gets an optional loose_docs
hook, called after the definitions and the file's own doc: the C leg reads
every Doxygen block outside function bodies that no definition took, and
its references belong to the file (the File node is the source).

The scope ("c1") lists the definitions with nodes (function, variable,
type, enum constant, field, object-like and function-like macro) with their
qualified names, their linkage (static, or a type, constant or macro of a
.c file, is the file's own) and the parent of a constant or field, and the
pages, groups, anchors and sections the file's docs define.

doc_links_c.c resolves a name as Doxygen does, without imports:
- a page, group, anchor or section name is no code entity (graph_gap) and
  never binds a symbol of the same name;
- then the documented file's own definitions (static ones too), its fields,
  then what every file sees (external linkage, a header's types, constants
  and macros); a static definition of another file is not visible;
- candidates count by qualified name (a prototype and its definition, #if
  variants: one node); with `()` written a function or function-like macro
  wins, then a definition wins over a macro;
- `Type::member`: a field of the struct or union Type, or a constant of the
  enum Type;
- one candidate binds, several are ambiguous; a word of an @see paragraph
  that names nothing and is a plain lowercase word is prose (Doxygen prints
  it as text): no row; test code binds only from test code.
There is no scope_delta hook yet: a declaration or linkage change is a full
run.

The families are held (ships=false) until their held-out audit passes; the
suite ships them through the test seam.

Proof (ship build, Apple M5 Pro / 48 GB, base 7a321ca):
- xxhash (redis 4f20cb489344, a clean copy), the field test's 174 audited
  links, 110 of which had hit a rename macro before C1: every harvested one
  now names the right entity (169 edges, 2 self-mentions: XXH_NAMESPACE's
  own doc); the other 3 are @copydoc, which this leg does not harvest.
- redis: doxygen_ref 74 edges (179 mentions), see 25 (30); rows: graph_gap
  50, missing 6, ambiguous 3, unparseable 1. The prose rule took the 75
  rows of words like "for" and "details" out.
- pico-sdk 079c6f39: doxygen_ref 488 (730), see 199 (219); rows: graph_gap
  142 (groups and pages), missing 94, ambiguous 68 (one type per chip,
  rp2040 and rp2350), test_only_target 28 (assembly functions with only a
  C test version), unparseable 5.
- libusb abba63ed: doxygen_ref 256 (284), see 39 (44); rows: graph_gap 62,
  missing 18, unparseable 2, ambiguous 2. 46 of the edges are
  Type::member references.
- libevent d82464a2: see 176 (230); rows: missing 9.
- a random sample of 24 edges (8 per corpus, pico-sdk, libusb, libevent)
  read against their source lines: all correct, among them a header
  prototype's doc linking to the definition in the .c file.
Finding outside the leg: libevent's event_assign has no node (its return
type and name are split by an #if block), so its references are missing.

Tests (doc_mentions_c): tokens with their lines and the file-level source
of a prototype's doc; C++ files out, headers in; the scope blob; every
resolution rule; incremental equals full (a doc edit and a body edit repair
one file, a definition made static is a full run); 60 modules give the same
edges and rows with one worker and several. Each rule red with it reverted.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…to its type

PHP properties, class constants and enum cases have no graph node, so their
doc blocks were never read: cakephp has 36 @see references there, symfony
21. The PHP leg now uses the driver's loose_docs hook: a /** */ block right
above a property, constant or case is harvested with the type around it as
the source (as the field test's prototype attached it), and resolved like
any other reference written in that type (self:: is the type).

The driver marks as the file's only what a hook pushes with the file
stand-in (the C leg's prototype docs); a hook may push with a definition of
the file instead.

Proof (ship build, Apple M5 Pro / 48 GB, base 80e000a):
- koel 04ed8beaec34: all 5 of the prototype's pairs bound alike (the fifth
  sits in a class constant's doc).
- cakephp a75828db: 373 edge mentions + 44 rows = every one of the 417
  non-URL references (was 381 of 417).
- symfony 9917e115: 459 + 219 = 678 of 681 (the other 3 sit in blocks before
  code the census does not classify).

Tests: doc_mentions_php resolves a property's doc block to an edge from its
class; red with the hook's walk reverted, and red when the driver marks
every token of the file pass as the file's.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
phpDocumentor reads `@see name()` as a function. Authors write it for the
documented class's own method: symfony has 97 such references that ended as
external rows (Command `@see setCode()`, ProgressBar `{@see resumeAll()}`),
and one that bound a global build-script function (ProcessHelper `@see
run()`, meaning its own run()). In the doc of a type or of one of its
members, a bare name() is now a method the type declares or inherits; a
`use function` import of the name still wins, and outside a type the
function rules stay. The ambiguity guard this replaces is gone.

Proof (ship build, Apple M5 Pro / 48 GB): symfony 9917e115 +91 edges
(external bare name() rows 97 -> 6), cakephp a75828db +6 (7 -> 1); a sample
of 14 new edges read against their lines: all correct, inherited methods
included (UploadedFile -> File::guessExtension).

Test: doc_mentions_php_current_class (the method wins over a global function
of the same name; the documented method itself is a self-mention), red with
the rule reverted.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…ackages

A bare import specifier was always external. The TS resolver now asks the
pipeline what it already knows for import resolution, without indexing
package.json or tsconfig.json: the nearest tsconfig/jsconfig `paths` and
`baseUrl` (the alias collection; the first target, as TypeScript tries it
first), then the workspace package whose `name` the specifier is (the
package map), bound to the package's entry module (`exports`, `main`,
`module`) when the repository indexes it. Every other bare specifier, a
package's subpath included, stays external. Incremental runs stay exact:
a tsconfig change re-resolves the files under it, a package.json change is
a manifest change.

path_alias gets cbm_path_alias_resolve_into, which writes into the caller's
buffer; cbm_path_alias_resolve now wraps it (two raw allocations fewer, the
memory-core baseline lowered to match).

Also fixed: a declaration in an index file has no `index` in its QN (fqn.c
strips it from a symbol's QN, not from the module's), so a reference to one
missed its node; the resolver now names a file's declarations with that
prefix.

Proof (ship build, Apple M5 Pro / 48 GB): the TypeScript compiler
b465fdbfe175 unchanged (69 edges, 57 of the prototype's 61 pairs);
discord.js 4dc1acc6 unchanged (no doc reference names a workspace import;
its packages' entries are build output). Tests: a tsconfig alias, a
workspace package's entry and an index file's declaration each bind, each
red with its rule reverted; path_alias, pipeline, edge_imports, incremental
and all doc suites pass (1298).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Diagrams name the parts of a system and how they connect; an OpenAPI
document names its endpoints and their handlers. Both now enter the graph.

Diagrams:
- a Diagram node per diagram: a fenced block of a Markdown file (under its
  Section), a directive of a reST file (`.. mermaid::`, `.. uml::`,
  `.. graphviz::`, Sphinx `.. digraph::`), an Asciidoctor Diagram block of
  an AsciiDoc file (`[plantuml]`, `[source,mermaid]`, ...), or a diagram
  file of its own, named by its title, else by its file;
- a DiagramElement node per element, DEFINES from its Diagram, and a
  DIAGRAM_ARROW edge per arrow (label, style, line);
- formats: Mermaid (flowchart, sequence, class, ER, state, C4,
  architecture), PlantUML, Graphviz DOT, D2, draw.io (plain, compressed,
  .drawio.svg) and Excalidraw (.excalidraw, .excalidraw.json,
  .excalidraw.svg); .drawio.svg and .excalidraw.svg are no longer dropped
  as images;
- an element's explicit link (Mermaid click/link, PlantUML [[url]], url of
  and !include, draw.io and Excalidraw link, DOT URL/href, D2 link) is a
  doc link read as a Markdown link, MENTIONS when it resolves.

OpenAPI:
- each operation of an OpenAPI or Swagger document (openapi.json and
  swagger.json, now read with the JSON grammar but no node per key, and
  any YAML or JSON file whose root says openapi/swagger) is a Route, named
  by the same QN a route its handler registers in code gets
  (cbm_route_canon_path, moved into the extraction library);
- its operationId is a doc link to the handler: a dotted one (Connexion,
  with x-openapi-router-controller) as a qualified code name, a bare one
  when exactly one function or method of the repository has that name.

The eight new link families are held until their held-out audit passes.

Read on real repositories, which shaped the rules: Excalidraw drawings put
text over shapes and draw arrows up to them without binding, so loose text
names the smallest box it sits in and a loose arrow end meets the smallest
named box it touches (the drawing's frame only at its border); arrows bound
to free text meet the text; D2 block strings and nested blocks; DOT line
breaks and sentence-long ids; PlantUML kinds of classes inside packages
and nested classes (`..+`, nested -> container, so they do not collide with
the container's association). Connexion's 24 specs: 160 operationIds, all
linked to their handlers.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…C, Perl

The TS/JS, PHP, C and Perl doc-link layers keep a scope blob per file but
had no scope_delta hook, so any change of a file's declarations made the
incremental closure repair decline (doc_scope_changed) and the index
rebuild in full: removing a TypeScript function forced a full run.

cbm_doclinks_scope_delta_lines compares two blobs line by line in order,
as C# does: a dropped record that declares one name is repaired file by
file (the name is reported, so the files whose references named it are
re-resolved); anything new or changed, and every record others resolve
through, stays a full run. The records that declare one name: TS D
(a top-level declaration), PHP F and K (a function, a member), C D of a
function, variable, enum constant, field or macro and A (a Doxygen
anchor), Perl S (a sub).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A Markdown Section's docstring is its body collapsed and cut at 500 bytes.
A diagram fence in that body put the format's syntax into it (`-->`,
`participant`, `classDiagram`, `subgraph`), which names no code, and pushed
the section's prose out of the cap. The doc -> code candidates are scored
on that text.

Each top-level diagram fence (mermaid, plantuml, puml, uml, dot, graphviz,
d2) is now replaced in the section's text by the labels its diagram parser
reads: the names of its elements, then its arrow labels, in source order.
The parse runs into a scratch result, so the file's own Diagram and
DiagramElement nodes are untouched. A section without such a fence keeps
its text byte for byte; other fences stay as written.

Measured before it was chosen, on held-out repositories (147 sections that
embed diagrams, 503 section/candidate pairs, three blind reading rounds):

  fence as written   277 stored  precision 0.45  42 sections with a correct candidate
  labels (this)      310 stored  precision 0.54  56 sections with a correct candidate
  fence dropped      127 stored  precision 0.54  29 sections with a correct candidate

Labels gave 20 sections a correct candidate that the fence as written did
not, and lost 6 (exact McNemar p = 0.009). Dropping the fence lost more
than it gained.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…udit

The held-out audit of the eight new languages is scored. Each tier is a
language and a reference form, read on a sample of 150 links (census below
150) from repositories never used to build or tune the resolvers. Three
blind reading rounds; bar: Wilson 95% lower bound >= 0.90 and point >= 0.95.

  tier             population  judged  correct  Wilson low
  go doc_link            5980     150      150      0.975
  java link             23934     150      150      0.975
  java linkplain         1372     150      150      0.975
  java see               7711     150      150      0.975
  java throws             650     150      150      0.975
  java value               79      79       79      0.954
  kotlin kdoc_link       3888     150      150      0.975
  kotlin sample           279     150      150      0.975
  kotlin see              369     150      150      0.975
  kotlin throws            73      73       73      0.950
  rust intra_doc        14119     150      149      0.963
  rust link_def          1491     150      150      0.975
  perl pod_L             2255     150      150      0.975
  ts/js link             1222     150      150      0.975
  php see                5171     150      150      0.975
  c doxygen_ref           463     150      150      0.975
  c see                   509     150      145      0.924

These 17 families now ship: a resolved reference is a MENTIONS edge.

TS/JS {@linkcode} (10 held-out links) and @see (18) were all judged
correct, but are too few to reach the bar; they stay held, as do the
diagram and OpenAPI link families (3 links on 21 held-out repositories).

The Go ship-gate test turned its family off and restored only the held
families afterwards; with Go shipping it left Go off for the tests that
follow it. It restores the production gates first now.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A Markdown section that embeds a diagram is about its candidate functions
more often than the Markdown curve says, by how much depends on the
diagram's kind. 553 candidates from such sections in 16 held-out
repositories, judged blind in three rounds:

                 0.20-0.30  0.30-0.40  0.40+  local extras
  prose            0.24       0.28      0.45     0.275
  structural       0.32       0.46      0.57     0.78 (9 items)

Structural is Mermaid sequence/class/ER/C4/architecture and PlantUML
C4/class/sequence/use-case/component; prose any other Mermaid or PlantUML
kind. A section takes the highest family of the Diagram nodes it defines;
its function candidates at or inside the doc's home and its local extras
carry the family's value, and the first band is now stored. Outside
functions, files, folders, other formats and sections without a diagram
keep the Markdown curve.

On kubernetes' apiserver folder the sequence-diagram section's
candidates move from 0.22/0.51 to 0.46/0.57, the flowchart sections gain
five first-band candidates at 0.24, other sections and the graph are
unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The Windows headers define far and near as empty macros, so
'const char *far = ...' became 'const char * = ...' on the CLANG64 leg.
The variable is far_src now.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData
DeusData merged commit 879c11e into main Oct 11, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant