Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
b868196
fix(producer): four of the parked Important findings, and one that on…
ncitnea Sep 4, 2026
433dbf6
test(producer): make two stated guarantees executable, behind a two-m…
ncitnea Sep 4, 2026
16e0a62
test(producer): put the prefix-root guard and the validate verb on a …
ncitnea Sep 4, 2026
9c785f4
refactor(producer): remove two dead properties, rename a test to what…
ncitnea Sep 4, 2026
86253bd
fix(producer): separate two declaration shapes the grammar's name fie…
ncitnea Sep 4, 2026
4ada5b9
fix(producer): stop a namespace's contents from being emitted inside …
ncitnea Sep 4, 2026
1447225
feat(producer): lock the engine versions and record them in the artefact
ncitnea Sep 6, 2026
fb7a49e
test(producer): give the golden a fixture that can fail, and fix the …
ncitnea Sep 6, 2026
bedd3ce
test(producer): stop the golden's own tests from being able to disarm…
ncitnea Sep 6, 2026
1a29328
fix(producer): an unborn branch is not a branch a permalink can point at
ncitnea Sep 6, 2026
1237991
fix(producer): refuse an offset that names no boundary, and an edge f…
ncitnea Sep 6, 2026
0f67010
docs(producer): pin the indexer/operator ripple, and say why test pro…
ncitnea Sep 6, 2026
364cbc3
fix(producer): a code block's content is source, and stops being flat…
ncitnea Sep 6, 2026
82833e6
test(producer): execute the two MSBuild degradation paths that spawni…
ncitnea Sep 6, 2026
7e1fad2
test(producer): a batch of Minor findings, and the four the sift had …
ncitnea Sep 6, 2026
d29f69c
fix(producer): refuse a bound that does not bound, and stop leaking a…
ncitnea Sep 6, 2026
926c861
docs(producer): correct a comment that named the wrong guard, and sto…
ncitnea Sep 6, 2026
d89c891
fix(producer): match a file to its project the way the filesystem would
ncitnea Sep 6, 2026
42c9e58
docs(spec): write down two shipped behaviours the design never mentioned
ncitnea Sep 6, 2026
85e85a7
fix(producer): stage the manifest write, say why a source-less candid…
ncitnea Sep 6, 2026
ff075b6
fix(producer): report an unowned file at a FAILED id, and name the go…
ncitnea Sep 6, 2026
cde3899
perf(producer): build the callee index in one pass over the file, not…
ncitnea Sep 6, 2026
883b3e8
test(producer): make four tests assert what their names promise
ncitnea Sep 6, 2026
23b14fb
docs(producer): name the two-read window on the join key, and measure…
ncitnea Sep 6, 2026
057c7f8
docs(producer): stop calling Core language-agnostic, and price a seco…
ncitnea Sep 6, 2026
f1bd17a
test(producer): assert that --help succeeds, not only what it prints
ncitnea Sep 6, 2026
818d9fd
feat(producer): --roslyn-timeout, an opt-in bound on the one stage th…
ncitnea Sep 7, 2026
e0eaeea
refactor(producer): lift the markdown text layer out of ConceptGenerator
ncitnea Sep 7, 2026
8a644f9
test(producer): make the id scheme's one remaining churn route execut…
ncitnea Sep 7, 2026
508fbae
Merge remote-tracking branch 'origin/dev' into worktree-search-diversify
ncitnea Sep 7, 2026
e915029
docs: announce the two behaviour changes, and complete a table that r…
ncitnea Sep 7, 2026
7c76e6a
fix(producer): four defects an adversarial review found in this branc…
ncitnea Sep 7, 2026
4b74916
fix(producer): the review's remaining majors, and the doc comments my…
ncitnea Sep 7, 2026
3777ac1
fix(producer): close the review's minors, and propagate a rename I ha…
ncitnea Sep 7, 2026
fd66d4d
fix(producer): scope on effective visibility, so an internal type's p…
ncitnea Sep 7, 2026
309c36a
docs: announce the three output changes the review's fixes made
ncitnea Sep 7, 2026
e2a6efc
feat(producer): say when a public member was capped at its internal c…
ncitnea Sep 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,44 @@ and this project adheres to

### Added

- **`okfgen generate --roslyn-timeout <seconds>`** — a wall-clock budget for the
whole Roslyn stage, the `dotnet msbuild` queries and the compilations after
them. Absent by default, and absent means unbounded: each query is capped at
two minutes on its own, but nothing caps their sum, so a large repository runs
for as long as it runs. It is not a default because a budget makes the emitted
bundle a function of how fast the machine is, and determinism is pinned at a
fixed extractor version, not a fixed CPU. If the budget runs out the stage is
abandoned **whole**, never truncated: the run lands in exactly the
`--no-msbuild` state, with a note naming the same two losses, rather than
emitting a bundle whose exact and name-matched links are divided by machine
speed with nothing recording where the line fell.
- **`okfgen` gains a C# code-graph stage** (`producers/OkfProducer`, outside
`OKF4net.sln` and outside CI by decision). `generate` now emits one `code/`
concept per namespace, type and member, with resolved `## Calls` links. Two
engines behind one contract: tree-sitter extracts symbols and call sites
language-agnostically, and Roslyn resolves C# call sites exactly — without
`MSBuildWorkspace`, querying project inputs through a bounded `msbuild -getItem`
subprocess — with a name-match resolver covering what Roslyn cannot reach. Call
sites are identified by UTF-8 byte offset, since the two engines natively speak
UTF-16.
- **`okfgen generate` prints a completeness report** to stderr, prefixed `run: `,
on every run that reaches the generation stage: files visited and how many fell
to each cause, whether the traversal was complete, projects detected and how
many of the closure compiled, exact-resolver coverage, and how many `code`
concepts are reachable from `overview`. It exists because every other account a
run gives of itself is a note gated on its own trigger, so a run printing
nothing was indistinguishable from a mechanism that did not fire. On stderr, so
no CI gate reading stdout changes and nothing lands in the bundle.
- **`okfgen generate --check`** compares a regenerated bundle against the one on
disk, over a copy, and reports drift without writing. Backed by a golden
fixture.
- **Project detection follows every `*.sln` in the tree**, not only one at the
repository root. A root-only lookup let the first root solution decide the whole
answer: measured on this repository, 9 of 17 `.csproj` were detected, and the
194 `code` concepts of the undetected projects belonged to no package concept
and were unreachable from `overview` — which `okf validate` does not report,
because an orphan dangles nothing. A `.csproj` that no solution references is
still not a package.
- **`ConceptSearch.TopDiversified`** — picks the top N of a scored result set
while rotating across top-level id families, so one family cannot take every
slot in a truncated window. `ConceptSearch.Search` is unchanged; this is an
Expand Down Expand Up @@ -263,6 +301,40 @@ and this project adheres to

### Fixed

- **`generated.by` is an actor again, and the engine versions moved to
`generated.engines`.** §5.2 makes that field an actor and §7 defines an actor as
exactly one of `<producer>/<version>`, `human:<id>`, `process:<id>`. It was written
as `okfgen/0.1.0 tree-sitter/1.3.0 roslyn/5.3.0`, which is none of them — and the
failure was silent, because `Actor.Parse` splits on the first `/` and reported it
well-formed with a version of `0.1.0 tree-sitter/1.3.0 roslyn/5.3.0`. `okf validate`
called such a bundle clean while every consumer reading the version got a string
naming no release. The provenance is preserved in a sibling key, which OKF keeps
across a round-trip. **Regenerate to update an existing bundle's `overview`.**
- **Scope filters on effective visibility.** A `public` member of an `internal` type is
capped at internal by C#, so it is now out of scope by default. It used to be emitted
with `--include-internal` off *and* tagged `public` — a visibility the language does
not give it — so a bundle generated to exclude internal API published it anyway.
**This removes concepts from regenerated bundles**, which is the point.
- **Generic types are disambiguated with a backtick, not `_`.** `Holder<T>` was spelled
`Holder_1`, drawn from the C# identifier alphabet, so a type genuinely named
`Holder_1` collapsed into the same concept — both signatures under one description.
The ids move from `holder_1` to `holder-1`.
- **`okfgen` no longer names Roslyn in `generated.engines` on a run where Roslyn never
ran** — including the common one, where every project failed to query or compile
(an unrestored checkout, no `dotnet` on `PATH`). That field is a determinism claim
— *these engine versions produced these bytes* — so naming an engine the run never
invoked makes it false in the direction that matters, by promising reproducibility
against a tool that was not there. It was written unconditionally on every run that
was not `--no-code`, which also covers `--no-msbuild`, a repository with no project
file, and an exhausted `--roslyn-timeout`. The golden fixture could not catch this:
the fixture harness had the rule right while the shipped CLI did not, so the two
disagreed about the same repository.
- **`--roslyn-timeout` is read invariantly, and its whole range is validated.** Without
a custom parser the value was converted with the machine's culture and
`AllowThousands`: `1.5` meant 15 on a comma-decimal locale, silently, and was refused
outright on another. Values above `TimeSpan`'s range and below one tick escaped the
range guard as unhandled exceptions. `--roslyn-timeout 0` was accepted and meant
*unbounded*, the opposite of the smallest bound; it is refused now.
- **Cancelling an attested computation now stops it, whatever the host stage
does.** The orchestrator handed its token to each stage and trusted them to
observe it; a stage that ignores its token — any client predating cancellation
Expand Down
10 changes: 10 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,16 @@ are the concrete entry points.
verification command, the packaging step and the project layout.

Open follow-ups, still open:
- **The pruning guard compares scope FLAGS, not the scope RULE.** `BundleWriter` refuses to prune
when the previous run covered a wider scope, and it decides that by comparing the flags recorded
in the manifest — so it is blind to a run whose flags are identical but whose *rule* narrowed.
Measured on exactly that: a bundle generated before scope moved to effective visibility, then
regenerated with the same flags, lost five concepts and the guard stayed silent, because nothing
in the manifest said the rule had changed. The producer now prints a note when it caps a public
member at an internal container, which covers the one case that exists today; the general fix is
to record a scope-rule identifier beside `scope` in the manifest so the existing guard fires on a
rule change as it does on a flag change. Deliberately not done in the fix round that found it:
it changes the manifest format, which is a compatibility decision of its own.
- **More ecosystems.** Package detection is npm and NuGet only, and the code stage is C# only.
The architecture is multi-language by construction (one `LanguageProfile` per language, one
`ISymbolResolver` per precision level); a second profile would test the generality of that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,21 @@ Effet de bord bienvenu : les `partial class` réparties sur plusieurs fichiers f

- **Segments réservés.** `BundleConceptWriter` rejette les concepts nommés `index` ou `log` (ils écraseraient les fichiers propres du bundle) — vérifié dans `src/OKF4net/BundleConceptWriter.cs`. Une propriété nommée `Index` est parfaitement plausible ; on **réutilise** `IsReservedSegment` de `ConceptGenerator.cs` au lieu d'en écrire un second.
- **Collision résiduelle** (casse seule — `Parse` vs `parse` — ou type imbriqué homonyme d'un membre) : départage déterministe par **ordre Ordinal du nom d'origine**, le premier garde le slug nu, les suivants prennent `-2`, `-3`. Ordinal **sur le nom** et non sur (fichier, ligne), pour que le départage survive à un déplacement de fichier ou à un décalage de lignes. Mesuré à **0 occurrence** sur ce repo ; la règle existe pour Go et JS, où c'est courant.

> **Correction apportée à l'implémentation (dépilage des findings Important, 2026-09-04) : une TROISIÈME collision résiduelle existait, non listée ici.** Une classe `Bar` dans le namespace `Foo` et une classe `Baz` dans le namespace `Foo.Bar` se réduisent au même chemin brut `[code, csharp, Foo, Bar]` pour leur parent. Le conteneur n'était donc jamais synthétisé — un groupe réclamait déjà le chemin — et `Baz` s'enregistrait sous l'identifiant du **type**. Mesuré sur cette fixture exacte avant correctif : `code/csharp/foo/bar/baz`, c'est-à-dire un type de premier niveau rendu comme un type imbriqué, le namespace n'ayant aucun concept.
>
> `SymbolFact.Container` ne peut pas trancher : c'est une chaîne pointée aplatie, et une classe imbriquée dans `Bar` comme une classe de premier niveau dans `Foo.Bar` y rapportent toutes deux `"Foo.Bar"`. L'extracteur, lui, connaît la différence — il descend les ancêtres et voit un nœud namespace dans un cas, un nœud type dans l'autre — et l'aplatissement la jetait.
>
> `SymbolFact.ContainerNamespace` (propriété `init`, défaut `null`, même forme que `HeaderEndLine`) porte désormais la partie namespace du conteneur. La règle : **le parent d'un groupe est un namespace exactement quand sa profondeur égale celle du namespace.** Deux lignes du tableau ci-dessous ont des segments identiques et des parents différents, ce qui est précisément ce qu'aucune règle sur la chaîne ne peut exprimer.
>
> | déclaration | segments | profondeur ns | parent |
> |---|---|---|---|
> | `Bar`, type dans ns `Foo` | `[code,csharp,Foo,Bar]` | 3 | namespace `Foo` |
> | `M`, membre de `Bar` | `[code,csharp,Foo,Bar,M]` | 3 | type `Bar` |
> | `Baz`, type dans ns `Foo.Bar` | `[code,csharp,Foo,Bar,Baz]` | 4 | namespace `Foo.Bar` |
> | `Baz`, imbriquée dans `Bar` | `[code,csharp,Foo,Bar,Baz]` | 3 | type `Bar` |
>
> Le namespace reçoit un chemin brut marqué et donc son propre identifiant (`code/csharp/foo/bar-2`) ; **le type garde le chemin qu'il avait**, donc aucun identifiant de type existant ne bouge. Un groupe dont `ContainerNamespace` est `null` — toute fixture de test, tout futur extracteur qui ne l'enregistre pas — est laissé exactement tel quel : la passe est inerte plutôt que devinatrice.
- **Profondeur.** Un type devient à la fois le fichier `link-scanner.md` et le dossier `link-scanner/`. C'est légal, et `IndexGenerator` les liste dans deux rubriques distinctes du parent (document / `Subdirectories`). Conséquence assumée : **un `index.md` par dossier de type** (~170 sur ce repo). Sur un projet Java profond (`com/example/…`), les chemins s'allongent — **à surveiller vis-à-vis de `MAX_PATH` sous Windows**.

### 3.4 Un registre d'ids unique
Expand Down Expand Up @@ -190,6 +205,16 @@ Scans a concept body for §6 markdown links, returning them in source order.
- `Enumerable.Where`
```

> **Correction apportée à l'implémentation (dépilage des findings Minor, 2026-09-06) : deux comportements livrés n'apparaissaient dans aucune section de cette spec.**
>
> **1. Le plafond d'en-tête sur un `resource` de TYPE (R48).** L'exemple ci-dessus est un *membre*, et un membre garde son span complet `StartLine..EndLine` — c'est utile, un permalien vers son corps entier. Un **type**, lui, voit son span coupé à la fin de son en-tête. La raison est la promesse de rayon d'impact de §8.3 : le span d'une déclaration de type court jusqu'à son accolade fermante, donc *toute* édition dans le corps — ajouter un membre privé, ajouter une surcharge, supprimer une méthode — déplacerait `EndLine` et réécrirait le concept du type. Ce serait du churn causé par la position de l'édition et non par ce que le type déclare, ce qui falsifie « ajouter un membre privé ne change aucun concept ».
>
> Une édition *au-dessus* du type le déplace encore, et c'est correct : la déclaration a réellement bougé. `SymbolFact.HeaderEndLine` porte la ligne, et `producers/tests/.../fixtures/golden` contient depuis 2026-09-04 un type dont l'en-tête tient sur trois lignes — sans lui, le plafond aurait produit un golden identique s'il retournait `StartLine + 1`.
>
> Le finding qui a relevé cette omission ajoutait que le plafond « contredit l'exemple de §4.1 ». Vérifié : il ne le contredit pas, l'exemple étant un membre.
>
> **2. La neutralisation du texte repris.** Une `description` dérivée d'un commentaire de documentation est du texte écrit par autrui qui atterrit dans un document markdown, et rien ici ne le disait. Ce qui est appliqué : les liens markdown sont neutralisés, un marqueur de bloc en début de ligne est échappé, une fence **non fermée** est défusée (une fence équilibrée est laissée telle quelle), le contenu d'un bloc `<code>` est **jeté** — c'est de la source, pas de la prose, le même argument que le sanitizer du viewer fait pour `<script>`/`<style>` — tandis que `<c>` inline est conservé parce qu'il fait partie de la phrase. Un run non fermé qui *ressemble* à une balise est laissé verbatim, par la règle qui garde `List<T> of results` intact.

### 4.2 `description` : une chaîne de sources, pas un LLM

tree-sitter capture le nœud de commentaire précédant la déclaration ; on en extrait le `<summary>` en C#, le JSDoc en TS, la docstring en Python, le doc comment en Go. **C'est l'avantage net sur okf-rs**, qui a besoin de `generate --enrich` et d'un endpoint OpenAI pour remplir ce champ.
Expand Down Expand Up @@ -489,7 +514,9 @@ La correction 1 récupère bien les fichiers générés **par le SDK** (`*.Globa
>
> Vérifié par `grep` sur tout `producers/src` : aucun lecteur de production pour l'une ou l'autre propriété ; les seuls lecteurs sont les tests. Ce que l'opérateur voit vient de `GenerateRun.ReportProjects`, qui itère `RoslynResolver.Projects` et émet **une note par projet non compilé**, en nommant le projet, sa `RoslynProjectAvailability` et le détail. C'est strictement plus d'information qu'un booléen.
>
> Arbitrage retenu : **corriger le document et les commentaires plutôt que câbler une seconde voie**. Câbler `IsComplete` dans le rapport ajouterait une ligne agrégée à côté d'une voie par projet qui fonctionne déjà et qui dit davantage. Les deux propriétés restent : c'est la question qu'un hôte intégrant le resolver pose, et le fixture de `RoslynResolverTests` s'appuie sur `IsComplete` pour prouver que son dépôt scratch a réellement compilé.
> Arbitrage retenu : **corriger le document et les commentaires plutôt que câbler une seconde voie**. Câbler `IsComplete` dans le rapport ajouterait une ligne agrégée à côté d'une voie par projet qui fonctionne déjà et qui dit davantage.
>
> **Suite (dépilage des findings Important, 2026-09-04) : les deux propriétés ont été SUPPRIMÉES.** La vague 2b les avait gardées en écrivant ici qu'« un hôte intégrant le resolver pose cette question » et que le fixture de `RoslynResolverTests` s'appuyait sur `IsComplete`. Arbitrage de l'utilisateur : aucun hôte ne les lit, `producers/` n'est pas une bibliothèque publiée, et une propriété dont les seuls lecteurs sont les tests qui l'assertent est exactement la forme que cette branche a payée treize fois (« assertion incapable d'échouer »). `AnyCompiled` et `AllCompiled` vivent désormais dans `RoslynResolverTests`, dérivées de `RoslynResolver.Projects` en une ligne — ce qu'écrirait tout appelant voulant le résumé. La clause `Count > 0` reste documentée là-bas : `All` sur une liste vide est vrai par vacuité, et une liste vide est précisément l'état où *tous* les appels sont retombés sur le name matching.

**Rayon d'impact — il déborde du projet en échec.** C'est le point le moins intuitif et il doit être écrit noir sur blanc. Un projet qui ne compile pas ne coûte pas seulement la précision *sur ses propres fichiers* :

Expand Down Expand Up @@ -649,6 +676,14 @@ Drapeaux ajoutés à `okfgen generate` par ce lot :
| `--include-internal` | off | descend sous la visibilité publique | 5.4 |
| `--no-code` | off | désactive l'étage graphe de code (comportement actuel) | 5.4 |
| `--max-file-size <n>` | 2 Mo | plafond par fichier source, appliqué par les deux moteurs ; au-delà, l'extracteur ignore **et compte** (run partiel), la porte Roslyn laisse tomber l'item `Compile` **silencieusement** | 2.3 |
| `--no-msbuild` | off | saute tout l'étage Roslyn, et avec lui l'évaluation MSBuild qu'il exige. Coûte **deux** choses, pas une : les liens d'appel viennent de la seule baseline par correspondance de noms, et il n'y a **aucune** carte de propriété des sources, donc aucun lien `packages` → namespace n'est émis (sous `--update`, cela écrase ceux d'un run précédent) | 2.1, 5.1 |
| `--roslyn-timeout <s>` | *absent* | budget horloge pour tout l'étage Roslyn — les requêtes `dotnet msbuild` et les compilations qui suivent. **Absent veut dire non borné** : chaque requête est plafonnée à deux minutes isolément, rien ne plafonne leur somme. Opt-in parce qu'un budget fait dépendre le bundle émis de la vitesse de la machine, alors que §6.2 fixe le déterminisme à une version d'extracteur, pas à un CPU. Épuisé, l'étage est abandonné **en entier** : le run atterrit exactement dans l'état `--no-msbuild`, avec la même note, plutôt que d'émettre un bundle dont les liens exacts et par nom sont séparés par la vitesse de la machine sans rien pour dire où passe la ligne | 2.3, 6.2 |

> **Les deux dernières lignes ont été ajoutées après coup, et l'omission mérite d'être nommée.**
> `--no-msbuild` est décrit longuement en §7.2 (« Levier ajouté ») mais n'avait jamais rejoint ce
> tableau, qui se présente pourtant comme la surface CLI du lot. Un tableau récapitulatif incomplet est
> pire qu'absent : il se lit comme exhaustif. Vérifié cette fois contre `OkfgenCli.Run` plutôt que
> contre le souvenir de ce qui a été ajouté.

`--update` conserve son nom mais change de sémantique sur `code/` (élagage, §6.3).

Expand Down
Loading