diff --git a/CHANGELOG.md b/CHANGELOG.md index e7e01d68..6aa5cda2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,44 @@ and this project adheres to ### Added +- **`okfgen generate --roslyn-timeout `** — 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 @@ -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 `/`, `human:`, `process:`. 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` 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 diff --git a/ROADMAP.md b/ROADMAP.md index 35c15a18..1233a49f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 diff --git a/docs/superpowers/specs/2026-08-31-okf-producer-code-graph-design.md b/docs/superpowers/specs/2026-08-31-okf-producer-code-graph-design.md index 09fe47e8..00e7d157 100644 --- a/docs/superpowers/specs/2026-08-31-okf-producer-code-graph-design.md +++ b/docs/superpowers/specs/2026-08-31-okf-producer-code-graph-design.md @@ -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 @@ -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 `` est **jeté** — c'est de la source, pas de la prose, le même argument que le sanitizer du viewer fait pour `