mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-22 00:31:17 +00:00
* feat(csharp-scope): unit 1 — scope query + captures orchestrator First slice of the C# scope-resolution migration (issue #934, RFC #909 Ring 3). Closes `Unit 1` of docs/plans/2026-04-21-004-feat-csharp-scope-resolution-plan.md. Adds: - src/core/ingestion/languages/csharp/query.ts — tree-sitter scope query covering compilation_unit, namespace (block + file-scoped), class-like (class/interface/struct/record/enum), method-like (method/constructor/destructor/local_function/operator), property and field declarations, using directives, type bindings (parameter annotations, local variable annotations, constructor inference, invocation alias), and references (free call, member call including null-conditional, constructor call, member write). - src/core/ingestion/languages/csharp/captures.ts — pass-through orchestrator mirroring python/captures.ts. Import decomposition (Unit 2), receiver-type-binding synthesis (Unit 3), and arity metadata synthesis (Unit 5) stub out for future units. - src/core/ingestion/languages/csharp/cache-stats.ts — PROF instrumentation mirror of python/cache-stats.ts. Design notes: - Return-type / field-type / property-type captures deferred. tree-sitter-c-sharp does not expose these under a clean named field that pattern-matches. When Unit 7 parity gate surfaces a gap, add positional patterns or a post-hoc extractor lookup. - object_creation_expression with qualified_name type — the qualified name itself is the reference text; captured as a whole via a dedicated tag so interpretation in later units can split namespace + name. - Null-conditional calls use positional descendant patterns because tree-sitter-c-sharp's member_binding_expression and conditional_access_expression don't expose named fields. Coverage: - 23/23 new unit tests in test/unit/scope-resolution/csharp/csharp-captures.test.ts cover every capture tag. Confirmed against tree-sitter-c-sharp via the probe-script loop during development; grammar drift would surface as a capture-shape assertion failure. - tsc --noEmit clean. No changes to shared infrastructure. Resolver wiring + registration land in Unit 6. * fix(csharp-scope): capture null-conditional receiver + operator decls Adversarial review surfaced two Unit 1 bugs that would silently corrupt the graph once C# is flipped on the scope-resolution path: - `obj?.Save()` only emitted @reference.name, so receiver-bound resolution downgraded to the free-call fallback and could mis-link to an imported `Save`. Capture the conditional_access_expression receiver under @reference.receiver. - `operator_declaration` had @scope.function but no @declaration.method owner, so calls inside operator bodies were attributed to the enclosing class and the operator itself disappeared from method lookup. Capture the operator token as @declaration.name (downstream csharpMethodConfig normalizes to op_Addition etc.). - `conversion_operator_declaration` was missing from both scope and declaration sets. Added with the target type as the name anchor. Arity metadata for overload resolution remains deferred to Unit 5 and gated behind Unit 7's parity flip, as documented in captures.ts. * chore(scope-resolution): drop unused python/scopes.scm sibling The file was documentation-only — the authoritative scope query is the embedded `PYTHON_SCOPE_QUERY` constant in `python/query.ts`. Nothing loaded the `.scm` at runtime, so it drifted from the code. Remove it and update the four doc comments that pointed at it: - language-provider.ts: "scopes.scm query" → "scope query (embedded in each language's query.ts)". - languages/python.ts: capture-vocabulary pointer → query.ts. - python/query.ts header: drop the "edit both together" note. - python/receiver-binding.ts: "keeps the .scm declarative" → "keeps the embedded scope query declarative". - scope/walkers.ts: "Python's scopes.scm" → "Python's scope query". Historical plan docs under docs/plans/ still reference scopes.scm but are frozen artifacts, not living documentation. C# never had a .scm sibling, so no action needed there. * feat(csharp-scope): Unit 2 — import interpret + target resolver Adds the three files Unit 2 of the C# scope-resolution plan calls for: - `import-decomposer.ts` — inspects each `using_directive` node and synthesizes `@import.kind/source/name/alias` markers. Kinds: `namespace` — `using X;` / `using X.Y.Z;` `alias` — `using Alias = X.Y.Z;` (generics stripped) `static` — `using static X.Y;` `global using` maps to namespace (plan's deferred decision); the `global::` qualifier is stripped before emitting. - `interpret.ts` — reads the markers and builds `ParsedImport`. Static using maps to `kind: 'wildcard'` since it brings members into unqualified scope; Unit 4's merge-bindings tiers wildcards lowest. Also provides `interpretCsharpTypeBinding` with nullable/single-arg generic/qualifier stripping so receiver-typed resolution sees the concrete class name. - `import-target.ts` — suffix-match adapter returning a single primary file. Cross-file partial-class aggregation runs later at graph-bridge time (Unit 6). The csproj-based `resolveCSharpImportInternal` stays on the legacy path until Unit 7's parity gate surfaces a gap. - `captures.ts` routes `@import.statement` matches through the decomposer so the interpreter sees the markers it needs. Tests cover every using flavor + resolution edge cases. 38/38 scope- resolution C# unit tests pass; tsc clean. * feat(csharp-scope): Unit 3 — simple hooks (binding/import/receiver) Adds simple-hooks.ts mirroring Python's pattern: - `csharpBindingScopeFor` — delegates to innermost (block scope is already captured by @scope.block in the query). - `csharpImportOwningScope` — binds `using` inside a namespace to that namespace's scope so imports don't leak into sibling namespaces. File-level using delegates to module. Function-body using (not legal C# but possible from malformed input) attaches to the function. - `csharpReceiverBinding` — looks up `this` / `base` in the function scope's type bindings; returns null for statics, free functions, and non-Function scopes. `this` / `base` synthesis itself is deferred to a follow-up (matches Python's receiver-binding.ts pattern). 9 new tests pin delegation semantics. 47/47 C# scope-resolution unit tests pass; tsc clean. * feat(csharp-scope): Unit 4 — mergeBindings (using precedence) Three-tier shadowing, same shape as Python's LEGB merge: 0: local — class members, locals, parameters 1: using — namespace / named / reexport (equal tier; compiler requires explicit qualifier if two using collide) 2: wildcard — `using static X.Y;` static-member imports Within the surviving tier, de-dup by DefId (last-write-wins) so a re-declared `using` cleanly replaces its earlier binding. Explicit interface implementations bind under their qualified name in the extractor layer, so they don't collide with plain simple names here. 7 new tests pin precedence + dedup semantics. 54/54 C# scope-resolution unit tests pass. * feat(csharp-scope): Unit 5 — arity metadata synthesis + compatibility Adversarial review flagged overload narrowing as a blocker for the Unit 7 flip. This lands the declaration-side metadata; callsite-side arity synthesis is a separate gap we'll address if the parity gate surfaces overload misresolution. - `arity-metadata.ts` — reads `csharpMethodConfig.extractParameters` and produces `{ parameterCount, requiredParameterCount, parameterTypes }`. `params` variadic collapses parameterCount to undefined (matches Python's `*args` treatment) and appends a literal `'params'` marker to parameterTypes so the compatibility hook can detect it without re-reading the AST. Default-valued parameters contribute to optionalCount → requiredParameterCount = total − optional. - `arity.ts` — `csharpArityCompatibility(def, callsite)` returns compatible / incompatible / unknown. Mirrors Python's three-verdict shape so the central registry's arity filter works without adapter logic per-verdict. - `captures.ts` — on every @declaration.method / @declaration.constructor / @declaration.function match, synthesize @declaration.parameter-count, @declaration.required-parameter-count, and @declaration.parameter-types captures. Covers method_declaration, constructor_declaration, destructor_declaration, operator_declaration, conversion_operator_declaration, and local_function_statement. 12 new tests: 5 on captures-side synthesis (method + params + types + variadic + constructor + local function), 7 on the compatibility hook. 66/66 C# scope-resolution unit tests pass; tsc clean. * feat(csharp-scope): Unit 6 — wire csharpScopeResolver + register Creates the public barrel (index.ts) and ScopeResolver (scope-resolver.ts) and plumbs them into the provider + registry: - `languages/csharp/index.ts` — re-exports the hook entry points and documents the 8 known limitations of the registry-primary path (csproj-driven namespace resolution, multi-file namespace expansion, type-based overload resolution, nested generics, dynamic, preprocessor branches, cross-file global using, expression-bodied members). - `languages/csharp/scope-resolver.ts` — ScopeResolver shape mirroring Python's. `isSuperReceiver` matches the literal `base` keyword. `fieldFallbackOnMethodLookup: false` since C# is statically typed — the type-binding layer already produces precise owner types; `propagatesReturnTypesAcrossImports: true` since signatures are authoritative. - `languages/csharp.ts` — adds the 9 hook entry points to the provider (emitScopeCaptures, interpretImport, interpretTypeBinding, four simple hooks, mergeBindings, arityCompatibility, resolveImportTarget). - `scope-resolution/pipeline/registry.ts` — registers csharpScopeResolver alongside the Python entry. MIGRATED_LANGUAGES stays at {Python} — the resolver sits idle until Unit 7's parity gate confirms ≥99% fixture parity. 368/368 scope-resolution unit tests pass; tsc clean. * feat(csharp-scope): parity Unit 1 — this/base receiver-binding synthesis Closes 3 parity failures (51 → 48). Target bucket: Category C from the parity plan. Changes: - `languages/csharp/receiver-binding.ts` (new): walks up from a function node to the enclosing class/struct/record/interface, synthesizes `@type-binding.self` captures with boundName `'this'` (and `'base'` when the enclosing type is a class/record with an explicit base_list entry). Skips static methods and interface / struct `base` cases. Anchors to the method's `body` block so the scope-extractor's positionIndex places the binding inside the function scope (not the enclosing class scope). - `languages/csharp/captures.ts`: route `@scope.function` matches through the synth, emitting the receiver captures as separate matches. - `languages/csharp/interpret.ts`: map `@type-binding.self` to `source: 'self'` (parity with Python). - `languages/csharp/query.ts`: explicit patterns for `this.X()`, `base.X()`, and `this.X = ...` / `base.X = ...` assignment writes. `this` and `base` are anonymous tokens in tree-sitter-c-sharp so the existing `expression: (_)` pattern (named-only) didn't match. Tests: - 8 new unit tests for receiver-binding synthesis edge cases (class/struct/record/interface, static, nested, constructor, local function inside method). - Parity: 48 failed | 127 passed (175) under REGISTRY_PRIMARY_CSHARP=1; legacy path 175/175 green. * feat(csharp-scope): parity Unit 2a — foreach + pattern + field captures Closes 11 parity failures (48 → 37). Partial Unit 2 progress. Adds type-binding captures for every shape the parity suite exercises whose resolution path is in-file: - Typed foreach `foreach (User u in xs)` — @type-binding.annotation with bindingName `u` and type `User`. - Var foreach `foreach (var u in xs)` — @type-binding.alias so the generic-stripper unwraps `List<User>` / `Dictionary<K,V>.Values` to the element type at chain-follow time. Matches Python's for-loop alias pattern. - `is` pattern `if (obj is User u)` — @type-binding.annotation with scope narrowing simplified to function scope (matches Python's match-case treatment since we don't emit @scope.block). - `switch_section > declaration_pattern` (`case User u:`) — no case_pattern_switch_label wrapper in tree-sitter-c-sharp. - `recursive_pattern` (`is User { Age: 1 } u` / `case User { ... } u:`) — named binding via type+name fields on the pattern node. - Field declaration `private City _city;` — @type-binding.annotation attached to the class scope for `this._city.X` resolution. - Property declaration `public User Owner { get; set; }` — same. - Assignment rebind `alias = Factory()` / `alias = new User()` — @type-binding.alias / @type-binding.constructor so reassignment propagates type info to later receiver-typed resolution. Closed tests: foreach (3), var foreach Tier 1c (2), is-pattern (1), switch pattern (2), recursive_pattern (3). Remaining 37 include tests that need cross-file same-namespace visibility (field chains, assignment chain, cross-file return-type propagation) — deferred to Unit 5 where the IMPORTS/cross-file work lives. 74/74 scope-resolution unit tests pass; legacy path 175/175 green. * feat(csharp-scope): parity Unit 2b — same-namespace cross-file visibility Closes 3 parity failures (37 → 34). Adds the C#-specific implicit import that has no syntactic counterpart: every type declared in `namespace X` is visible to every other file also declaring `namespace X`, without any `using` directive. Changes: - `scope-resolution/contract/scope-resolver.ts` — new optional hook `populateNamespaceSiblings(parsedFiles, indexes, { fileContents })`. Most languages leave it undefined; Python / TypeScript / Java need explicit imports so there's no analogous pass. - `scope-resolution/pipeline/run.ts` — invoke the hook after `buildWorkspaceResolutionIndex` and before `propagateImportedReturnTypes` so the return-type pass sees cross-file sibling class bindings. - `languages/csharp/namespace-siblings.ts` (new) — groups top-level class-like defs by namespace name (extracted from source via regex since `file_scoped_namespace_declaration` scope range covers only the declaration line, not the rest of the file). Injects sibling classes into each file's Module AND Namespace scope bindings with origin='namespace'. Local declarations shadow cross-file siblings via mergeBindings tier precedence. - `languages/csharp/scope-resolver.ts` — wire the hook. 74/74 scope-resolution unit tests pass; legacy path 175/175 green; 34 parity failures remain (was 37) under REGISTRY_PRIMARY_CSHARP=1. * feat(csharp-scope): parity Unit 2c — alias/await/return-type captures Closes 7 parity failures (34 → 27). Adds the remaining type-binding shapes the parity suite exercises: - `var alias = u;` / `alias = u;` — identifier-to-identifier alias. The resolver's chain-follow walks alias → u → u's declared type. - `var u = svc.GetUser();` — chained method call alias. Anchors on the method_access_expression's `name` field; chain-follow picks up GetUser's return type. - `var u = await Factory();` / `await svc.Get();` — await propagation. Strips the `await_expression` wrapper; interpret layer's `stripGeneric` handles `Task<T>` / `ValueTask<T>` unwrapping. - `public User GetUser() { ... }` — method return-type annotation via `@type-binding.return`. Required for `propagateImportedReturnTypes` to see the return type in later cross-file passes. Covers identifier, generic_name, qualified_name, and nullable_type return shapes. 74/74 scope-resolution unit tests pass; legacy path 175/175 green; 27 parity failures remain under REGISTRY_PRIMARY_CSHARP=1. * feat(csharp-scope): parity Unit 3a — cross-namespace `using` binding Closes 2 parity failures (27 → 25). Extends the namespace-siblings pass to resolve `using X;` directives against known namespace buckets: for each `using` that targets a namespace declared somewhere in the workspace, inject that namespace's classes into the importer's module scope with origin='namespace'. This is the scope-resolution analog of legacy's csproj-driven directory↔namespace mapping. Without it, `new User()` in `Services/UserService.cs` (namespace MyApp.Services) can't see the User class in `Models/User.cs` (namespace MyApp.Models) even with `using MyApp.Models;` — the scope-resolver layer doesn't have csproj metadata to translate the dotted namespace path into a directory lookup. Legacy 175/175 green; 25 parity failures remain. * feat(csharp-scope): parity Unit 3b — constructor CALLS emission Closes 3 parity failures (25 → 22). Adds constructor-form CALLS edge emission + C# 12 primary constructor synthesis. Changes: - `scope-resolution/passes/free-call-fallback.ts`: when a site's callForm === 'constructor', look up the class def (not a callable) and pick its explicit Constructor def via workspaceIndex's memberByOwner — or fall back to the Class def itself for implicit constructors. Matches legacy behavior (targetLabel === 'Constructor' when explicit, 'Class' when implicit). - `scope-resolution/pipeline/run.ts`: pass workspaceIndex to the free-call fallback. - `languages/csharp/captures.ts`: synthesize @declaration.constructor for C# 12 primary constructors — `class User(string name, int age)` / `record Person(string First, string Last)`. The parameter_list is a named child of the class_declaration / record_declaration (not a separate constructor_declaration node). Skip the synthesis when the type already has an explicit constructor to avoid duplicates. Emits @declaration.parameter-count + required-parameter-count alongside. Legacy 175/175 green; 376/376 scope-resolution unit tests pass; 22 parity failures remain. * feat(csharp-scope): parity Unit 3c — static call + default-namespace Closes 2 parity failures (22 → 21). - `receiver-bound-calls.ts`: add Case 5 for class-as-receiver. When `Animal.Classify()` has an identifier receiver that resolves to a Class binding (rather than a variable with a typeBinding), look up the member on the class's MRO chain. Covers C#-style static calls and any type-qualified member access. Python doesn't hit this because `ClassName.method()` is syntactically identical to a free call there. - `namespace-siblings.ts`: treat files with no `namespace X;` declaration as living in the default (empty-name) bucket, so types declared in no-namespace files share cross-file visibility. Required for fixtures without explicit namespaces (e.g. the method-enrichment fixture's Animal/App/Dog classes). Legacy 175/175 green; 21 parity failures remain. * feat(csharp-scope): parity Unit 4 — callsite arity synthesis (infra) Synthesize @reference.arity on every invocation_expression and object_creation_expression by counting `argument` named children of the backing `argument_list`. Wires the capture-to-Callsite pipeline shared extractor already consumes (`scope-extractor.ts:878`). No parity-count movement: the remaining arity-adjacent failures (overload disambiguation, optional-parameter dedup, variadic resolution) need type-based argument inference or member-call dedup, both explicitly deferred in the plan's Known Limitations section. This commit is infrastructure — future work lands on top of it. Legacy 175/175 green; 21 parity failures remain. * feat(csharp-scope): parity Unit 5a — IMPORTS edge + static-using mapping Closes 1 parity failure (21 → 20). Fixes cross-file IMPORTS edge emission for C#: - `languages/csharp/interpret.ts`: map `using static X.Y;` to `kind: 'namespace'` rather than `'wildcard'`. The File→File IMPORTS edge needs a non-wildcard kind to survive finalize's Phase 4 (wildcard-expanded edges drop to empty when the provider doesn't implement `expandsWildcardTo`). Unqualified static-member access is a deferred limitation — covered by the namespace-siblings cross-namespace pass for type lookups, and documented under the module's Known Limitations. - `languages/csharp/import-target.ts`: progressive prefix stripping. `using CrossFile.Models;` in a repo laid out `Models/User.cs` (no `CrossFile/` directory) works because the legacy resolver consults csproj; the scope-resolver tries each suffix of the dotted path against `.cs` files. Also handles `using static NS.Type;` by stripping leading segments until a direct match lands. - `test/unit/scope-resolution/csharp/csharp-imports.test.ts`: update the `using static` test to the new namespace-kind shape. 376/376 scope-resolution unit tests pass; legacy 175/175 green; 20 parity failures remain. * feat(csharp-scope): parity Unit 5b — return-type module hoist + chain fallback Closes 1 parity failure (20 → 19) and lays groundwork for Unit 6. Based on investigation-agent findings, addresses cluster of 7 cross-file + chain tests whose return-type bindings were stuck at Class scope and invisible to the chain-follow and propagation passes. Changes: - `languages/csharp/simple-hooks.ts::csharpBindingScopeFor`: when the declaration is a `@type-binding.return`, hoist the binding all the way to the Module scope. The central extractor's auto-hoist only promotes one level (Function → Class); for C# methods the parent is always a Class, so without this override the return binding never reaches Module where chain-follow and cross-file `propagateImportedReturnTypes` read from. - `scope-resolution/passes/compound-receiver.ts`: when the class-scope typeBindings lookup at `objClass.typeBindings.get( methodName)` misses, walk up from the class scope through the parent chain (→ Module) for a return-type binding. Preserves the existing class-scope fast-path while restoring owner-chain lookup for languages that hoist to Module. Python parity suite stays 204/204 green on both flag paths; legacy C# 175/175 green; 19 C# parity failures remain. * feat(csharp-scope): parity Unit 5c — switch-expr + reasons + ACCESSES 1.0 Closes 4 parity failures (19 → 15). - `languages/csharp/query.ts`: add captures for `switch_expression_arm` with `declaration_pattern` and `recursive_pattern`. C# expression- switch (`obj switch { User u => ..., Repo { Name: "x" } r => ... }`) uses a different AST node from classic `switch_statement`'s `switch_section` — needed separate query patterns. - `scope-resolution/passes/receiver-bound-calls.ts`: replace the self-describing `'scope-resolution: *-receiver'` reason strings (which fail legacy-parity consumer filters) with the legacy convention: `'import-resolved'` when the resolved member lives in a different file, `'global'` otherwise. Mirrors `free-call-fallback.ts`'s existing reason logic. - `scope-resolution/passes/receiver-bound-calls.ts`: pass `confidence: 1.0` to `tryEmitEdge` for write/read ACCESSES edges, matching legacy DAG behavior (default 0.85 was legacy-CALLS). Python parity 204/204 on both flag paths; legacy C# 175/175; 15 C# parity failures remain. * feat(csharp-scope): parity Unit 5d — cross-file typeBinding mirror Closes 3 parity failures (15 → 12). `languages/csharp/namespace-siblings.ts`: extend the pass to mirror method return-type bindings from accessible sibling files' Module scopes into the importer's Module scope. "Accessible" = same-namespace siblings + `using namespace X;` targets. Without this mirror, `var u = svc.GetUser()` in App.cs couldn't chain-follow to User even after Unit 5b's module-scope hoist: `GetUser → User` lived on User.cs's Module scope, which isn't on the ancestor chain of App.cs's function scope, and `propagateImportedReturnTypes` only mirrors across explicit ImportEdge targets (not same-namespace implicit visibility). Closes: var-invocation return type, async/await u.Save (ambient namespace), cross-file return-type propagation (via u.Save / u.GetName in Program.cs). Python parity 204/204 on both flag paths; legacy C# 175/175; 12 C# parity failures remain. * feat(csharp-scope): parity Unit 5e — namespace-prefix bucket matching Closes 2 parity failures (12 → 10). `languages/csharp/namespace-siblings.ts`: when matching accessible namespaces against class buckets, also probe every dotted prefix. `using static CrossFile.Models.UserFactory;` parses into the importer's accessible-namespace set as the full type path, but the matching bucket is keyed on the containing namespace (`CrossFile.Models`). Walking back through the dotted segments ensures the static-using importer sees the containing namespace's sibling files' return-type bindings. Legacy 175/175 green; 10 C# parity failures remain. * feat(csharp-scope): parity Unit 6a — class-like owner extension Closes 1 parity failure (10 → 9). Extends `populateClassOwnedMembers` to recognize Interface / Struct / Record / Enum / Trait as class-like owners, not just Class. The C# scope query collapses interface_declaration / struct_declaration / record_declaration / enum_declaration to @scope.class (they share body-scope semantics), but the declaration-side tags produce defs of type Interface / Struct / Record / Enum. `populateClassOwnedMembers` previously only looked for Class-typed defs in class scopes, so interface members (including C# 8+ default methods) never got ownerIds — making them invisible to `findOwnedMember` via `memberByOwner`. With this fix, `user.Validate()` on a variable typed as `IValidator` resolves correctly: receiver-bound-calls Case 4 finds IValidator via findClassBindingInScope (which already accepted Interface), walks the chain, and findOwnedMember locates Validate now that the interface default has a proper ownerId. Legacy C# 175/175 green; Python parity 204/204 on both flag paths; 9 C# parity failures remain. * feat(csharp-scope): parity Unit 6b — member-call dedup + handled-site fix Closes 1 parity failure (9 → 8). Adds the missing legacy-parity behavior: collapse multiple member-call sites from the same caller to the same target into one CALLS edge. Changes: - `scope-resolution/contract/scope-resolver.ts`: new optional `collapseMemberCallsByCallerTarget` flag. Default false (preserves the per-site invariant); C# sets it true. - `scope-resolution/graph-bridge/edges.ts`: dedup key drops `line:col` when `collapseByCallerTarget` is on AND edgeType is `CALLS` (ACCESSES writes keep per-site granularity). - `scope-resolution/passes/receiver-bound-calls.ts`: plumbs `collapse` through every `tryEmitEdge` call, and crucially marks `handledSites.add(siteKey)` whenever a resolved def was found — not only when the edge was freshly emitted. Otherwise the site leaked through to `emitReferencesViaLookup` which re-emitted a per-site edge, defeating the collapse. - `languages/csharp/scope-resolver.ts`: opt in to the collapse. Python parity 204/204 on both flag paths; legacy C# 175/175 green; 8 C# parity failures remain. * feat(csharp-scope): parity Unit 6c — Dictionary.Values / .Keys unwrap Closes 2 parity failures (8 → 6). Dictionary<K,V>.Values in a foreach binds the element to V; .Keys binds to K. Without this, `foreach (var user in data.Values)` where `data: Dictionary<string, User>` couldn't propagate user's type to User, and `user.Save()` stayed unresolved. Changes: - `languages/csharp/interpret.ts`: don't strip the qualifier when the final dotted segment is a known collection accessor (`Values` / `Keys`). Preserves the dotted form so downstream resolvers can unwrap the receiver's generic type based on the suffix. - `scope-resolution/passes/compound-receiver.ts`: new `extractDictionaryArgs` helper splits `Dictionary<K, V>` at the top-level comma. In the dotted-access walk, detect trailing `.Values` / `.Keys` and return V/K via findClassBindingInScope instead of the normal class-walk (Dictionary itself isn't a local class def). - Handles nested cases: `this.data.Values` walks `this.data` recursively (resolving `data` as a field on `this`'s class) before applying the unwrap. - `scope-resolution/passes/receiver-bound-calls.ts` Case 3b: when the typeRef's trailing segment is an accessor, pass the raw dotted path to `resolveCompoundReceiverClass` without appending `()` — the extra parens would misroute to the call-expression branch. Python parity 204/204 on both flag paths; legacy C# 175/175 green; 6 C# parity failures remain. * feat(csharp-scope): parity Unit 6d — using-static member injection Closes 2 parity failures (6 → 4). `using static X.Y.Z;` now injects every public static method of class Z into the importer's module scope, so `Record("hi")` (without `Logger.` qualifier) resolves to `Logger.Record` as a free call. `languages/csharp/namespace-siblings.ts`: regex-scan each file's source for `using static X.Y.Z;` directives. For each, look up the class Z in the `X.Y` namespace bucket, walk its owning file's localDefs for method/function members with `ownerId === Z.nodeId`, and inject them as `origin: 'import'` bindings in the importer's module-scope finalized bindings map. `findCallableBindingInScope` then picks them up via its imported-bindings check. Closes: variadic `Record(params string[])` + heritage arity narrowing `WriteAudit`. Python parity 204/204 on both flag paths; legacy C# 175/175 green; 4 C# parity failures remain (interface-dispatch pass + type-based overload disambiguation). * feat(csharp-scope): parity Unit 6e — overload disambig + interface dispatch + FLAG FLIP Closes the final 4 parity failures (4 → 0). C# now runs the registry-primary scope-resolution path by default — added to MIGRATED_LANGUAGES. Changes: - `scope-resolution/scope/walkers.ts`: was already extended in Unit 6a to recognize Interface/Struct/Record/Enum as class-like owners (interface default methods get ownerIds). - `scope-resolution/passes/receiver-bound-calls.ts`: build IMPLEMENTS edge index → emit secondary `interface-dispatch` CALLS edges to every implementor's same-named member when the primary receiver-typed edge targets an Interface method (closes heritage CreateUser CALLS-count test). - `scope-resolution/passes/receiver-bound-calls.ts`: new `pickOverload` helper narrows multi-valued `membersByOwner.get(owner).get(name)` candidates by arity then argument types. Replaces the first-seen `findOwnedMember` lookup in Case 4 so receiver-typed overloaded calls pick the right def. - `scope-resolution/passes/free-call-fallback.ts`: new `pickImplicitThisOverload` walks up to the enclosing class scope and applies the same arity + argument-type narrowing for free calls inside a class body (`Lookup("alice")` → `Lookup(string)`). - `scope-resolution/workspace-index.ts`: new `membersByOwner` multi-valued index (`Map<owner, Map<name, Def[]>>`) preserves every overload alongside the existing first-seen `memberByOwner`. - `scope-resolution/graph-bridge/node-lookup.ts` + `scope-resolution/graph-bridge/ids.ts`: include parameter-types suffix in the qualified lookup key for Method nodes. Legacy parse-phase encodes the type tag into the node id (`Method:f.cs: UserService.Lookup#1~int`); without this two same-arity overloads collapsed to one lookup entry and routed to the wrong graph node. - `scope-resolution/contract/scope-resolver.ts`: new `collapseMemberCallsByCallerTarget` opt-in flag (was added in Unit 6b for member-call dedup; documented here). - `gitnexus-shared/src/scope-resolution/reference-site.ts`: new `argumentTypes` field carrying inferred per-arg types. - `scope-extractor.ts`: read @reference.parameter-types capture into `site.argumentTypes` and add it + the declaration-arity tags to KNOWN_SUB_TAGS so the anchor-detection picks the right anchor. - `languages/csharp/captures.ts`: synthesize @reference.parameter-types by inferring arg types from literal AST nodes (integer_literal → 'int', string_literal → 'string', constructor_expression → type-name, etc). - `languages/csharp/scope-resolver.ts`: opt in to `collapseMemberCallsByCallerTarget`. - `registry-primary-flag.ts`: **add CSharp to MIGRATED_LANGUAGES**. Final state: - C# parity: 175/175 green on flag-on AND flag-off. - Python parity: 204/204 green on both flag paths (no regression). - TypeScript clean. 51 → 0 failures across 18 commits on `feat/csharp-scope-resolution`. * refactor(scope-resolution): extract language-specific accessor unwrap to provider hook Optimizer pass: move C# Dictionary-family `.Values`/`.Keys` handling out of the shared `compound-receiver.ts` (where it had hardcoded regex + accessor names) into a provider-level `unwrapCollectionAccessor` hook. The shared pass now takes an arbitrary language-specific unwrap function; C# supplies its Dictionary implementation in `languages/csharp/accessor-unwrap.ts`. Related cleanup in `receiver-bound-calls.ts` Case 3b: replace the hardcoded `tail === 'Values' || tail === 'Keys'` accessor check with a try-dotted-walk-first / fall-back-to-call-form strategy. This removes the last C#-specific branch in the shared pass and makes the logic generalize cleanly to other languages that use property-style accessors for collection views (Kotlin `.size`, future languages). Changes: - `scope-resolution/contract/scope-resolver.ts`: new optional `unwrapCollectionAccessor(receiverType, accessor) => string | undefined` hook. Documented as language-specific with examples. - `scope-resolution/passes/compound-receiver.ts`: delete `extractDictionaryArgs`, accept `unwrapCollectionAccessor` via options, call it for trailing accessor segments. - `scope-resolution/passes/receiver-bound-calls.ts`: plumb the hook through to `resolveCompoundReceiverClass`, remove the C#-hardcoded Case 3b accessor check. - `languages/csharp/accessor-unwrap.ts` (new): C# Dictionary-family regex + element-type extraction. - `languages/csharp/scope-resolver.ts`: opt in. Audit outcome: everything else added across the 19 C# migration commits is either correctly scoped to `languages/csharp/` (query, captures, namespace-siblings, receiver-binding, interpret, imports) or correctly generic in shared paths (argumentTypes field, collapseMemberCallsByCallerTarget flag, overload narrowing via parameterTypes, interface-dispatch via IMPLEMENTS edges, class-like owner extension for Interface/Struct/Record/Enum, type-tagged node IDs, module-scope return-type lookup fallback). 175/175 C# green on both flag paths; 204/204 Python green on both flag paths; TypeScript clean. * refactor(scope-resolution): gate module-scope typeBinding walk-up on hook Add optional `hoistTypeBindingsToModule` to the ScopeResolver contract and gate the Module-scope walk-up in `resolveCompoundReceiverClass` on it. Only providers that hoist method return-type bindings to Module scope (C#) opt in; Python and other providers no longer traverse that fallback path. Closes the architectural leak flagged in the production-readiness review: the walk-up was unconditional and therefore widened Python's code path despite existing only for C#. No behavior change for C# (hook=true restores the prior lookup). No behavior change for Python (hook undefined = walk-up skipped, matching pre-PR behavior). Verified: - npx tsc --noEmit clean - C# unit suite 74/74 passing - C# + Python integration 388/388 passing * refactor(csharp-scope): remove as-unknown-as double casts in scope-resolver Tighten three type boundaries that were previously papered over with `as unknown as` casts: * `CsharpResolveContext.allFilePaths`: `Set<string>` → `ReadonlySet<string>`. The orchestrator only hands out a read-only view; drop the widening cast at the resolver-adapter site. * `resolveCsharpImportTarget`: call passes the narrow context directly. `WorkspaceIndex` is `unknown` in the shared contract, so the `as unknown as WorkspaceIndex` cast was gratuitous — structural assignability covers it. * `csharpMergeBindings`: drop unused `_scope: Scope` parameter. The implementation never read it; the cast chain in `scope-resolver.ts` existed only to satisfy an unused slot. LanguageProvider.mergeBindings now wraps with a tiny arrow adapter; ScopeResolver.mergeBindings passes through directly. No runtime behavior change. `grep 'as unknown as' csharp/scope-resolver.ts` returns zero matches. Verified: - npx tsc --noEmit clean - C# unit + integration 462/462 passing (incl. Python integration) * test(csharp-scope): integration fixtures for Units 6c/6d/6e runtime behavior Close the integration-coverage gap flagged in the production-readiness review. Units 6c (collection-accessor unwrap), 6d (using-static member injection), and 6e (overload disambig + interface dispatch) previously had only hook-level unit tests; the end-to-end wiring was exercised only by the parity harness. Three minimal fixtures + four new it() blocks: * csharp-collection-accessor — RenderAll iterates Dictionary<string, Widget>.Values and calls .Render(); asserts the CALLS edge lands on Widget.Render. * csharp-using-static — `using static Helpers.MathUtils;` makes Square(int) a free-callable in the consumer; asserts the CALLS edge lands on MathUtils.Square. * csharp-overload-interface — three assertions: 1. Run → Log binds to the 2-arg overload only (arity narrowing); verified via target Method node's parameterTypes.length === 2. 2. Run → Greet emits one primary edge to IGreeter.Greet plus two reason='interface-dispatch' siblings to En/FrGreeter.Greet. 3. Interface-dispatch fan-out excludes the primary target. Verified: - csharp integration 189/189 passing * docs(scope-resolution): de-c#-ify optional-hook doc-comments on contract Rewrite the doc-comments on four optional hooks so they describe the behavior and when a provider would enable it, rather than naming C# as the sole consumer. Hook names were already generic — only the comments had baked in one-language framing, which risked discouraging future reuse. Affected hooks: * unwrapCollectionAccessor * collapseMemberCallsByCallerTarget * populateNamespaceSiblings * hoistTypeBindingsToModule Language-specific rationale stays where it belongs — next to the hook assignment in `languages/csharp/scope-resolver.ts`. Zero-match grep for `C#|csharp|CSharp` in the contract file confirms the separation. No code change. * docs(csharp-scope): justify regex-based namespace-sibling detection Record why `namespace-siblings.ts` uses regex over AST walks and enumerate the known misses so the next reader has ground to stand on: * `global using static X.Y;` — no plain `using static` token. * Aliased `using static X = Y.Z;` — `=` breaks the pattern. * Attributed namespace declarations between `]` and `{`. * Multi-namespace files — first-wins attribution. * Preprocessor-gated namespace declarations — textual branch only. Rationale: the pass is file-path-driven and the tree-sitter tree isn't available at its call site (the orchestrator feeds raw fileContents); re-parsing to count namespaces would cost more than the regex walk. Refactor to AST-driven detection is deferred to a separate PR. Mirrored the known-miss list into `csharp/index.ts`'s limitations ledger so the operator-visible surface and the in-code justification stay in sync. No code change. * refactor(csharp-scope): AST-driven namespace detection with treeCache reuse Replace regex-over-source-content with tree-sitter AST walks in namespace-siblings.ts; thread the orchestrator's treeCache through the populateNamespaceSiblings hook so the pass reuses the same parse trees `extractParsedFile` already consumed (single-source-of-truth for the AST — no double-parse). Behavior gains (no longer "known misses"): * `global using static X.Y;` is now detected. * Aliased `using static X = Y.Z;` is now detected. * Attributed namespace declarations (`[attr] namespace X`) parse correctly because tree-sitter sees them as one node. * Preprocessor-gated namespace declarations parse via the grammar. Contract change (additive, optional): * `populateNamespaceSiblings` ctx now carries an optional `treeCache?: { get(filePath): unknown }`. Existing providers that don't set it on `RunScopeResolutionInput` see undefined, and the hook falls back to a fresh parse (current behavior preserved on cache miss). Limitation ledger updated in csharp/index.ts: the AST-based detection removes 4 of the 5 prior known misses; only "first-wins multi-namespace file attribution" remains. Verified: - npx tsc --noEmit clean - C# + Python integration 393/393 passing * refactor(python-scope): remove as-unknown-as casts in scope-resolver (mirrors Unit 2) Replay the C# scope-resolver cleanup on the Python side so both providers share a single clean pattern: * Drop `ws as unknown as WorkspaceIndex` — `WorkspaceIndex` is `unknown` in the shared contract, so the narrow context assigns structurally without a cast. * Drop `{ id: scopeId } as unknown as Scope` — `pythonMergeBindings` never read the scope (the parameter was `_scope`), so the stub was a type-only ghost. Signature is now `(bindings)` and the LanguageProvider slot wraps with an arrow adapter. * Drop `allFilePaths as Set<string>` — the orchestrator hands a `ReadonlySet<string>`; we copy it into a `Set` at the resolver adapter so the legacy downstream `resolvePythonImportInternal` chain (typed for mutable `Set<string>`) keeps working. The copy is O(N) once per import, trivial cost. Left intact on purpose: the `(callsite, def) → (def, callsite)` arrow wrapper on `arityCompatibility`. That's a documented shape difference between `LanguageProvider.arityCompatibility(def, callsite)` and `ScopeResolver.arityCompatibility(callsite, def)`; both providers (Python + C#) carry the same wrapper. Reconciling is a separate refactor across both contracts. No runtime behavior change. Verified: - npx tsc --noEmit clean - Python + C# unit + integration suites 529/529 passing * docs(scope-resolution): document I1-I8 invariants, source-of-truth, and same-graph guarantee Promote contract knowledge that was implicit in code into the canonical docs so future migrations and the next reviewer don't have to reverse-engineer it. contract/scope-resolver.ts: * Migration cookbook lists every optional hook (was: only the two booleans), with one-line guidance per hook including when to enable `hoistTypeBindingsToModule`. * Contract Invariants I1-I7 are now spelled out in full (was: only I1/I3/I5 summarized with a pointer to a plan file). Added new I8 "post-finalize hooks may mutate Scope.typeBindings and indexes.bindings; consumers must not freeze or snapshot before all post-finalize hooks have run". * New "Semantic-model source of truth" section: ParsedFile is the single semantic model; passes that need AST-level facts must reuse the orchestrator's treeCache rather than re-parse. * New "Same-graph guarantee" section: legacy DAG and scope-resolution emit indistinguishable edges (node identity, edge vocabulary, confidence). CI parity workflow enforces this. gitnexus-shared/src/scope-resolution/parsed-file.ts: * Added "Source-of-truth invariant" pointer paragraph. ARCHITECTURE.md (Coexistence section): * Updated migrated-language list (Python + C#). * Added "Same-graph guarantee" subsection. * Added "Semantic-model source of truth" subsection. * Filled in the ScopeResolver hook table with the five optional hooks that landed in this branch (unwrapCollectionAccessor, collapseMemberCallsByCallerTarget, populateNamespaceSiblings, hoistTypeBindingsToModule, fieldFallbackOnMethodLookup). * Added C# rows to the code-references table. Verified: - npx tsc --noEmit clean - C# + Python integration 393/393 passing * refactor(scope-resolution): consume SemanticModel as single authoritative store Unify scope-resolution and legacy parse into one symbol index per the industry pattern (Roslyn / tsc / rust-analyzer). Scope-resolution passes now consume `SemanticModel.methods` / `SemanticModel.fields` / `SemanticModel.symbols` for all symbol-keyed lookups. The legacy DAG already read from these; the drift — two parallel owner-keyed indexes populated by two writers with divergent ownerId semantics — is closed. Changes: * `MethodRegistry.lookupAllByOwner(owner, name)`: new API returning every overload without arity narrowing. Powers `findOwnedMember` / `pickOverload`. * `pipeline/run.ts` reconciliation pass: after `provider.populateOwners(parsed)`, iterate `parsed.localDefs[i]` and register methods/fields into the SemanticModel under the corrected ownerId. Idempotent — skips defs already present under `(ownerId, simple)` by nodeId, so unmigrated languages whose legacy extractor already set ownerId (C#) don't double-register. Closes the Python gap where class-body methods were invisible to `MethodRegistry` because the legacy Python method extractor couldn't resolve `enclosingClassId` at parse time. * `WorkspaceResolutionIndex` slimmed to Scope-valued maps only (`classScopeByDefId`, `moduleScopeByFile`). Dropped `memberByOwner`, `membersByOwner`, `defsByFileAndName`, `callablesBySimpleName` — all symbol-keyed duplicates of SemanticModel indexes. * Walker helpers now consume SemanticModel: - `findOwnedMember(owner, name, model)` → methods then fields fallback (ACCESSES writes target Property/Variable defs too). - `findExportedDefByName` fallback walks every Module scope's `origin === 'local'` bindings via `index.moduleScopeByFile` (preserves the module-export-visibility filter that SymbolTable.fileIndex can't cheaply encode). - `findExportedDef` reads `moduleScope.bindings` directly. * `pickOverload` in receiver-bound-calls.ts falls back to `model.fields.lookupFieldByOwner` when method lookup returns empty, fixing ACCESSES write edges that receive a Property target. * `phase.ts` threads `resolutionContext.model` into `RunScopeResolutionInput`. Boundary rule, enforced by file placement: - symbol-indexed lookups (key = nodeId / name / filePath) → `SemanticModel` - Scope-valued lookups (value = `Scope`) → `WorkspaceResolutionIndex` Research synthesized from web-researcher + Explore + best-practices + system-architect agents; canonical references: Roslyn Overview, rust-analyzer architecture, stack-graphs paper. Verified: - npx tsc --noEmit clean - C# + Python integration 393/393 passing * docs(scope-resolution): refresh comments after dropping duplicated indexes Replace references to the now-deleted `memberByOwner` / `callablesBySimpleName` index fields with comments that describe the actual lookup path (`SemanticModel` registries + scope-tied module bindings). Pure doc cleanup; no behavior change. * feat(scope-resolution): extract reconciliation pass + add parity validator Extract the SemanticModel reconciliation pass (previously inline in `pipeline/run.ts`) into a dedicated module with: * `reconcileOwnership(parsedFiles, model)` — pure function returning stats (methodsRegistered / fieldsRegistered / skippedAlreadyPresent). Idempotent; safe to re-run. * `validateOwnershipParity(parsedFiles, model, onWarn)` — dev-mode runtime validator for Contract Invariant I9. Walks every def with an `ownerId` and asserts it is reachable via `model.methods.lookupAllByOwner` or `model.fields.lookupFieldByOwner`. Soft-fails via `onWarn`; never throws. Validator is gated on both `NODE_ENV !== 'production'` and `VALIDATE_SEMANTIC_MODEL !== '0'` so production incurs zero cost but development surfaces any drift between `parsed.localDefs` ownership and the registries. 12 new unit tests cover: * happy path: method, property, Variable registration * edge case: defs without ownerId are skipped * idempotency: second call is a no-op * coexistence: defs the legacy extractor already registered (via `model.symbols.add`) are skipped on reconcile * overloads: multiple methods under the same (owner, name) * validator: no warnings after reconciliation * validator: warns on drift * validator: no-op under NODE_ENV=production * validator: no-op when VALIDATE_SEMANTIC_MODEL=0 * validator: warns on missing Property same as missing Method Verified: - npx tsc --noEmit clean - reconcile-ownership unit tests 12/12 passing - C# + Python integration 393/393 passing * refactor(scope-resolution): narrow handles + tighten required params Two small hygiene fixes that fell out of the unified-model work: * Introduce `readonlyModel: SemanticModel` in `runScopeResolution` immediately after reconciliation so the write/read phase boundary is explicit at the code level. Downstream passes (receiver-bound, free-call) receive the narrowed `SemanticModel` rather than the `MutableSemanticModel` that only the reconciliation pass needs. The type system now rejects accidental writes in the read phase. * Make `emitFreeCallFallback`'s `workspaceIndex` parameter required. It's now always passed (every caller threads it through), and the `workspaceIndex?` guard was dead code. Also drops the `| undefined` branch from `pickConstructorOrClass` which no caller can hit. No behavior change. * docs(semantic-model): document unified single-source-of-truth invariant (I9) Add Contract Invariant I9 to the ScopeResolver contract and write the single-source-of-truth + write/read phase contract into both the SemanticModel file-head and ARCHITECTURE.md. Three landing points so the rule is reachable from every entry: * contract/scope-resolver.ts — new I9 entry in the Contract Invariants list: scope-resolution passes consult SemanticModel exclusively for symbol-keyed lookups; WorkspaceResolutionIndex is reserved for Scope-valued maps. Documents the two-phase write (legacy parse + reconcileOwnership) and the narrowed-handle read posture. Calls out the reconciliation shim as transitional. * model/semantic-model.ts — new "Single-source-of-truth invariant" and "Write / read phase contract" sections in the file-head. Three ordered write phases (parse → reconcile → attachScopeIndexes), then frozen for readers. * ARCHITECTURE.md § "Semantic-model source of truth" — expanded subsection covering both invariants (ParsedFile = AST truth, SemanticModel = symbol truth), the write/read phase diagram, and the reconciliation-shim rationale. No code change. * test(scope-resolution): rewrite workspace-index test for slimmed index The test file previously asserted on \`defsByFileAndName\`, \`callablesBySimpleName\`, and \`memberByOwner\` — fields removed when symbol-keyed lookups moved to \`SemanticModel\`. Rewrite so the same invariants are asserted via the authoritative consumers: * New WorkspaceResolutionIndex shape test (scope-only maps). * \`findExportedDef\` module-export visibility tests: - keeps top-level class and function defs. - excludes class-body Variable defs (MAX_USERS = 100). - excludes class methods from module-export lookup. * \`findExportedDefByName\` fallback excludes class methods when a same-named module function exists. * \`findOwnedMember\` via the reconciled SemanticModel finds Python class methods after populateOwners + reconcileOwnership. Total assertions preserved: every invariant from the old test file is still pinned; the assertion surface shifted from the index shape to the walker helpers. Verified: - workspace-index.test.ts 8/8 passing * fix(tests): update registry-primary-flag test for C# migration The "returns exactly the flipped languages" case expected `enabled.size === 1` after toggling Python off and Go on. After the C# migration lands C# in MIGRATED_LANGUAGES, C# is default-on too — so the size is now 2 (Go + C#) unless C# is also opted out. Turn off C# alongside Python in the test setup. Added a comment noting that future migrations must add their REGISTRY_PRIMARY_<LANG>='false' line here. * refactor(scope-resolution): address PR #1019 review findings Resolves all 5 findings from the automated review on feat/csharp-scope-resolution. Shared ingestion code stays language-agnostic; C# (and every class-like language) benefits. F1 [high] Broaden class-like predicate Hoist `isClassLike` in `scope/walkers.ts` to an exported top-level helper covering Class | Interface | Struct | Record | Enum | Trait. Use it in `findClassBindingInScope`, `findEnclosingClassDef`, and `buildWorkspaceResolutionIndex` so C# records, structs, interfaces, and enums participate in scope chains and receiver binding the same way Python classes do. F2 [medium] Remove stale comment in csharp simple-hooks `csharpReceiverBinding`'s doc claimed this/base synthesis was "planned for a follow-up"; synthesis has been implemented in receiver-binding.ts since the migration landed. Rewrite the doc to describe the actual behavior (non-null TypeRef on instance-method bodies, null on static/free functions). F3 [medium] O(1) reverse lookup for classScopeId -> classDefId Add `classScopeIdToDefId: ReadonlyMap<ScopeId, string>` to `WorkspaceResolutionIndex`, populated as the inverse of `classScopeByDefId`. Replace the O(C) linear scan in `pickImplicitThisOverload` (free-call-fallback.ts) with an O(1) `Map.get` — turns per-site reverse resolution from linear in class count to constant time for every free call. F4 [low] Extract narrowOverloadCandidates shared utility New `passes/overload-narrowing.ts` centralizes the arity + argument- type narrowing previously duplicated across `pickOverload` (receiver-bound-calls.ts) and `pickImplicitThisOverload` (free-call-fallback.ts). Both callsites now share identical narrowing semantics; variadic `params T` handling is preserved. Return type is `readonly SymbolDefinition[]` with no defensive spreads (allocations saved on the hot path). F5 [low] Merge unreachable Case 5 into Case 2 `Case 5` in `receiver-bound-calls.ts` was dead code — `Case 2` pre-empted it for every static/class-name receiver. Delete Case 5 and lift its kind-aware read/write ACCESSES reason/confidence logic into Case 2 so static-style member access (e.g. `Interface.Member`, `TypeName.StaticMember`) gets the correct edge metadata. Tests - New unit tests for `narrowOverloadCandidates` covering empty input, arity filtering, variadic params, type narrowing, and fallback semantics. - New unit tests for `classScopeIdToDefId` verifying inverse invariant and empty index behavior. - New C# integration fixtures and tests: * csharp-record-base — record inheritance + `base.Save()` * csharp-struct-overloads — struct with implicit-this overload narrowing (pinned exact edge count under registry-primary) * csharp-interface-receiver-static — interface-qualified static- style call exercises the merged Case 2. - Full runs green: * scope-resolution unit: 406/406 * csharp integration (registry-primary): 197/197 * csharp integration (legacy DAG): 197/197 * python integration (regression guard): 204/204 Chore - Add `.context/` to root `.gitignore` to prevent agent scratch files from being committed. Made-with: Cursor * test(csharp-scope-resolution): address adversarial review follow-ups on PR #1019 Applies the three actionable follow-ups from the post-commit adversarial review of5a1bce7fagainst DoD.md. No runtime code changes. - [medium] Strengthen bounds-only assertion in the struct-overloads suite: `methods.length` is now pinned to `toBe(2)` and the arity list to `toEqual([1, 2])`. Fixture `csharp-struct-overloads/src/Calc.cs` declares exactly two `Add` methods, so a regression that adds, drops, or merges an overload will now fail the test instead of silently passing a `>= 2` gate. - [low] Pin the merged Case 2 kind-aware branch (receiver-bound-calls.ts lines 257-289) with a dedicated fixture and three new assertions: `csharp-class-static-field-access/src/Counters.cs` exercises `ClassName.Field = value` where the receiver resolves via `findClassBindingInScope` (no typeBinding on `Counters`). The test verifies (a) two distinct ACCESSES writes are emitted from a single method (per-site dedup from graph-bridge/edges.ts:80-87), (b) `reason === 'write'`, (c) `confidence === 1.0`, and (d) no spurious CALLS edges are produced for the same sites. This is the semantic upgrade lifted from the deleted Case 5; without a pinning test a future revert of the kind-aware branch would silently drop back to `import-resolved`/`global` at 0.85 for the same sites. Read-side coverage is intentionally not asserted because the C# tree-sitter query currently emits only `write.member` captures (languages/csharp/query.ts:485-501) — a read counterpart would have no reference site today and would give a false sense of coverage. - [info] Left the `?? overloads[0]` fallback in place at receiver-bound-calls.ts:450 unchanged. With the package's current tsconfig (strict: false, no noUncheckedIndexedAccess) both the defensive fallback and a `candidates[0]!` assertion type-check identically, so the finding has no production-readiness impact. Keeping the fallback minimizes churn. Validation (local, Windows PowerShell): - `npx prettier --check test/integration/resolvers/csharp.test.ts` -> clean - `npx tsc --noEmit` -> 0 errors - `REGISTRY_PRIMARY_CSHARP=1 npx vitest run test/integration/resolvers/csharp.test.ts` -> 200/200 - `REGISTRY_PRIMARY_CSHARP=0 npx vitest run test/integration/resolvers/csharp.test.ts` -> 200/200 - `npx vitest run test/integration/resolvers/python.test.ts` -> 204/204 - `npm test` (full gitnexus suite) -> 6967 passed, 6 pre-existing failures (4x Swift overload/dedup, 1x Swift method-extraction unit, 1x Swift type-env unit, 1x LadybugDB lockfile on Windows). All six reproduce on5a1bce7fwith these follow-up changes stashed, confirming they are environment/baseline failures unrelated to this work. Swift is not in MIGRATED_LANGUAGES so the merged Case 2 path cannot affect it. Refs: PR #1019 Made-with: Cursor * refactor(scope-resolution): address full-PR review findings on PR #1019 Resolves the two remaining findings from the code-review-swarm full-PR sweep (verdict: production-ready with minor follow-ups). [low] Complete the csharp/index.ts module-layout JSDoc. `languages/csharp/index.ts` is the discovery surface for the C# scope- resolution module decomposition (per AGENTS.md). The "Module layout" list silently omitted three load-bearing modules — `accessor-unwrap.ts` (`.Values`/`.Keys` receiver-type unwrap), `namespace-siblings.ts` (AST-driven cross-file implicit-namespace visibility), and `receiver-binding.ts` (`this`/`base` type-binding synthesis). Extended the JSDoc list so the "single-concern" decomposition story is honest and the next contributor can locate the right file without grep. No behavior change. [info] Replace the non-standard `'scope-resolution: super-receiver'` edge reason with the canonical `'global'` tier. `passes/receiver-bound-calls.ts` emitted a non-canonical reason string for the super/base branch, which falls outside the vocabulary declared in ARCHITECTURE.md § Scope-Resolution Pipeline (`'import-resolved' | 'global' | 'local-call' | 'same-file' | 'interface-dispatch' | 'read' | 'write'`). Super/base calls resolve through the MRO chain rather than through import directives, so the correct canonical tier is `'global'` (same classification the legacy DAG's `toResolveResult` applies to non-same-file, non-import-scoped resolutions). Locked the contract with `rel.reason === 'global'` assertions on the existing `csharp-super-resolution` and `csharp-generic-parent- resolution` suites, both of which go through the super-branch MRO path. The `csharp-record-base` suite intentionally does not pin a reason (records don't currently emit EXTENDS edges, so the MRO lookup misses and the edge is produced by the reference-index fallback instead of the super-branch). A code comment flags the pre-existing Python-legacy asymmetry (Python legacy tier classifier marks `super()` as `'import-resolved'` because the ancestor arrives via an `import` statement); closing that gap requires realigning the legacy tier classifier and is tracked separately. Validation: - `npx tsc --noEmit` passes. - `npx prettier --check` clean on all four touched files. - `test/integration/resolvers/csharp.test.ts` — 200/200 under both `REGISTRY_PRIMARY_CSHARP=0` (legacy DAG) and `REGISTRY_PRIMARY_CSHARP=1` (registry-primary), preserving same-graph parity on the super branch. - `test/integration/resolvers/python.test.ts` — 204/204 under both `REGISTRY_PRIMARY_PYTHON=0` and `=1`. - `test/unit/scope-resolution/` — 406/406 passing. Unstaged: `gitnexus/package-lock.json` (drift from `npm install` run to resolve the pre-existing missing `jsonc-parser` dependency — not part of this change). Made-with: Cursor * test(ci): raise integration-test timeouts so slow Windows runners stop flaking The `windows-latest` CI runner for this branch was consistently failing two integration suites in ways that had nothing to do with the PR's scope-resolution changes: * `cli-e2e.test.ts` — `analyze command runs pipeline on mini-repo` hit the default 30 s vitest test timeout, which raced the test's own 30 s subprocess timeout and prevented the existing `if (result.status === null) return;` slow-CI tolerance from ever firing. That single timeout then cascaded into the downstream `cypher`/`query`/`impact` tests (which exited non-zero because the mini-repo was never indexed) and the `EPIPE handling` test. * `skills-e2e.test.ts` — `beforeAll` hooks run a full `runSkillsCli(tmpDir)` subprocess that analyzes a fixture repo and generates skills. 50 s was enough on Linux/macOS but not on slow Windows CPUs, producing "Hook timed out in 50000ms" errors and cascading test failures across every language describe block. Fix: * Bump the `analyze` test's vitest test-level timeout to 60 s so it exceeds the 30 s subprocess timeout and the slow-CI tolerance can actually activate. * Bump all 12 `runSkillsCli`-driven `beforeAll` hooks from 50 s to 120 s. No production-code behavior changes. No change to what the tests assert — only the per-test/hook wall-clock budget. Made-with: Cursor
2431 lines
56 KiB
TypeScript
2431 lines
56 KiB
TypeScript
/**
|
|
* E2E Integration Tests: --skills Flag
|
|
*
|
|
* Tests `gitnexus analyze --skills` across 11 supported languages plus
|
|
* mixed-language and idempotency scenarios. Each language fixture creates
|
|
* a self-contained git repo with 2 clusters of files containing cross-file
|
|
* function calls, then runs the full CLI pipeline and verifies SKILL.md
|
|
* generation and context file updates.
|
|
*
|
|
* Uses process.execPath (never 'node' string), no shell: true.
|
|
* Accepts status === null (timeout) as valid on slow CI runners.
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import { spawnSync } from 'child_process';
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
import os from 'os';
|
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
import { createRequire } from 'module';
|
|
|
|
const testDir = path.dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = path.resolve(testDir, '../..');
|
|
const cliEntry = path.join(repoRoot, 'src/cli/index.ts');
|
|
|
|
// Absolute file:// URL to tsx loader — needed when spawning CLI with cwd
|
|
// outside the project tree (bare 'tsx' specifier won't resolve there).
|
|
const _require = createRequire(import.meta.url);
|
|
const tsxPkgDir = path.dirname(_require.resolve('tsx/package.json'));
|
|
const tsxImportUrl = pathToFileURL(path.join(tsxPkgDir, 'dist', 'loader.mjs')).href;
|
|
|
|
// ============================================================================
|
|
// FILE-LOCAL HELPERS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Spawn the CLI with `analyze --skills` in the given cwd.
|
|
* Uses the absolute tsx loader URL so it works outside the project tree.
|
|
*/
|
|
function runSkillsCli(cwd: string, timeoutMs = 45000) {
|
|
return spawnSync(process.execPath, ['--import', tsxImportUrl, cliEntry, 'analyze', '--skills'], {
|
|
cwd,
|
|
encoding: 'utf8',
|
|
timeout: timeoutMs,
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
env: {
|
|
...process.env,
|
|
NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --max-old-space-size=8192`.trim(),
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a fixture repo: write files, git init, git add, git commit.
|
|
* Returns the tmp directory path.
|
|
*/
|
|
function createFixtureRepo(prefix: string, files: Record<string, string>): string {
|
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `skills-e2e-${prefix}-`));
|
|
for (const [relPath, content] of Object.entries(files)) {
|
|
const fullPath = path.join(tmpDir, relPath);
|
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
}
|
|
spawnSync('git', ['init'], { cwd: tmpDir, stdio: 'pipe' });
|
|
spawnSync('git', ['add', '-A'], { cwd: tmpDir, stdio: 'pipe' });
|
|
spawnSync('git', ['commit', '-m', 'initial commit'], {
|
|
cwd: tmpDir,
|
|
stdio: 'pipe',
|
|
env: {
|
|
...process.env,
|
|
GIT_AUTHOR_NAME: 'test',
|
|
GIT_AUTHOR_EMAIL: 'test@test',
|
|
GIT_COMMITTER_NAME: 'test',
|
|
GIT_COMMITTER_EMAIL: 'test@test',
|
|
},
|
|
});
|
|
return tmpDir;
|
|
}
|
|
|
|
/**
|
|
* Assert standard skill file properties:
|
|
* 1. CLI exits 0
|
|
* 2. .gitnexus/ exists
|
|
* 3. >= minSkills SKILL.md files under .claude/skills/generated/
|
|
* 4. YAML frontmatter valid
|
|
* 5. ## Key Files section present
|
|
* 6. ## How to Explore section present
|
|
* 7. Content > 200 chars
|
|
*
|
|
* Returns false if skill generation was skipped (native parser crash
|
|
* or Leiden non-determinism producing 0 communities). Callers can
|
|
* use this to skip dependent assertions.
|
|
*/
|
|
function assertSkillFiles(
|
|
result: ReturnType<typeof runSkillsCli>,
|
|
tmpDir: string,
|
|
minSkills = 1,
|
|
): boolean {
|
|
/* CI timeout tolerance */
|
|
if (result.status === null) return false;
|
|
|
|
expect(
|
|
result.status,
|
|
[
|
|
`analyze --skills exited with code ${result.status}`,
|
|
`stdout: ${result.stdout?.slice(0, 500)}`,
|
|
`stderr: ${result.stderr?.slice(0, 500)}`,
|
|
].join('\n'),
|
|
).toBe(0);
|
|
|
|
expect(fs.existsSync(path.join(tmpDir, '.gitnexus'))).toBe(true);
|
|
|
|
const generatedDir = path.join(tmpDir, '.claude', 'skills', 'generated');
|
|
if (!fs.existsSync(generatedDir)) {
|
|
// Native parser may have crashed in worker or Leiden produced 0 communities.
|
|
// The pipeline still succeeds (exit 0) but no skills are generated.
|
|
// Skip skill assertions gracefully — this is platform-dependent.
|
|
return false;
|
|
}
|
|
|
|
const skillDirs = fs
|
|
.readdirSync(generatedDir)
|
|
.filter((d) => fs.statSync(path.join(generatedDir, d)).isDirectory());
|
|
const skillFiles: string[] = [];
|
|
for (const dir of skillDirs) {
|
|
const skillPath = path.join(generatedDir, dir, 'SKILL.md');
|
|
if (fs.existsSync(skillPath)) {
|
|
skillFiles.push(skillPath);
|
|
}
|
|
}
|
|
|
|
expect(skillFiles.length).toBeGreaterThanOrEqual(minSkills);
|
|
|
|
for (const skillPath of skillFiles) {
|
|
const content = fs.readFileSync(skillPath, 'utf-8');
|
|
expect(content.startsWith('---')).toBe(true);
|
|
expect(content).toContain('name:');
|
|
expect(content).toContain('description:');
|
|
expect(content).toContain('## Key Files');
|
|
expect(content).toContain('## How to Explore');
|
|
expect(content.length).toBeGreaterThan(200);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Assert CLAUDE.md and AGENTS.md contain generated skill references.
|
|
* Automatically detects whether skills were generated by checking for
|
|
* the generated/ directory.
|
|
*/
|
|
function assertContextFiles(result: ReturnType<typeof runSkillsCli>, tmpDir: string) {
|
|
if (result.status === null) return;
|
|
|
|
const generatedDir = path.join(tmpDir, '.claude', 'skills', 'generated');
|
|
const skillsGenerated = fs.existsSync(generatedDir);
|
|
|
|
const claudePath = path.join(tmpDir, 'CLAUDE.md');
|
|
expect(fs.existsSync(claudePath)).toBe(true);
|
|
if (skillsGenerated) {
|
|
const claudeContent = fs.readFileSync(claudePath, 'utf-8');
|
|
expect(claudeContent).toContain('.claude/skills/generated/');
|
|
}
|
|
|
|
const agentsPath = path.join(tmpDir, 'AGENTS.md');
|
|
expect(fs.existsSync(agentsPath)).toBe(true);
|
|
if (skillsGenerated) {
|
|
const agentsContent = fs.readFileSync(agentsPath, 'utf-8');
|
|
expect(agentsContent).toContain('.claude/skills/generated/');
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// DESCRIBE 1: TypeScript
|
|
// ============================================================================
|
|
|
|
describe('TypeScript', () => {
|
|
let tmpDir: string;
|
|
let result: ReturnType<typeof runSkillsCli>;
|
|
|
|
beforeAll(() => {
|
|
tmpDir = createFixtureRepo('typescript', {
|
|
'src/api/router.ts': `
|
|
import { validateRequest } from '../utils/validator';
|
|
import { logRequest } from '../utils/logger';
|
|
|
|
export function createRouter() {
|
|
validateRequest('route');
|
|
logRequest('router init');
|
|
return { routes: [] };
|
|
}
|
|
|
|
export function registerRoute(path: string) {
|
|
validateRequest(path);
|
|
logRequest('register ' + path);
|
|
return true;
|
|
}
|
|
`,
|
|
'src/api/controller.ts': `
|
|
import { runQuery } from '../data/query';
|
|
import { formatResponse } from '../data/format';
|
|
|
|
export function handleGet(id: string) {
|
|
const data = runQuery('SELECT * FROM items WHERE id = ' + id);
|
|
return formatResponse(data);
|
|
}
|
|
|
|
export function handlePost(body: any) {
|
|
const result = runQuery('INSERT INTO items VALUES ' + JSON.stringify(body));
|
|
return formatResponse(result);
|
|
}
|
|
`,
|
|
'src/api/middleware.ts': `
|
|
import { validateToken } from '../utils/validator';
|
|
import { logRequest } from '../utils/logger';
|
|
|
|
export function authMiddleware(req: any) {
|
|
validateToken(req.headers.auth);
|
|
logRequest('auth check');
|
|
return true;
|
|
}
|
|
|
|
export function corsMiddleware(req: any) {
|
|
logRequest('cors check');
|
|
return { allowed: true };
|
|
}
|
|
`,
|
|
'src/data/query.ts': `
|
|
import { formatResult } from './format';
|
|
import { getCached } from './cache';
|
|
|
|
export function runQuery(sql: string) {
|
|
const cached = getCached(sql);
|
|
if (cached) return cached;
|
|
return formatResult({ sql, rows: [] });
|
|
}
|
|
|
|
export function buildQuery(table: string, conditions: any) {
|
|
return 'SELECT * FROM ' + table;
|
|
}
|
|
`,
|
|
'src/data/format.ts': `
|
|
export function formatResult(data: any) {
|
|
return { ...data, formatted: true };
|
|
}
|
|
|
|
export function formatResponse(data: any) {
|
|
return { status: 200, body: formatResult(data) };
|
|
}
|
|
|
|
export function serializeResult(data: any) {
|
|
return JSON.stringify(data);
|
|
}
|
|
`,
|
|
'src/data/cache.ts': `
|
|
import { runQuery } from './query';
|
|
|
|
const cache = new Map<string, any>();
|
|
|
|
export function getCached(key: string) {
|
|
return cache.get(key) || null;
|
|
}
|
|
|
|
export function warmCache(keys: string[]) {
|
|
for (const key of keys) {
|
|
cache.set(key, runQuery(key));
|
|
}
|
|
}
|
|
`,
|
|
'src/utils/logger.ts': `
|
|
export function logRequest(msg: string) {
|
|
console.log('[REQ]', msg);
|
|
}
|
|
|
|
export function logError(msg: string) {
|
|
console.error('[ERR]', msg);
|
|
}
|
|
|
|
export function createLogEntry(level: string, msg: string) {
|
|
return { level, msg, ts: Date.now() };
|
|
}
|
|
`,
|
|
'src/utils/validator.ts': `
|
|
export function validateRequest(input: string) {
|
|
if (!input || input.length === 0) throw new Error('Invalid');
|
|
return true;
|
|
}
|
|
|
|
export function validateToken(token: string) {
|
|
if (!token || token.length < 10) throw new Error('Invalid token');
|
|
return true;
|
|
}
|
|
|
|
export function sanitize(input: string) {
|
|
return input.replace(/[<>]/g, '');
|
|
}
|
|
`,
|
|
'src/utils/config.ts': `
|
|
export function getConfig(key: string) {
|
|
return process.env[key] || '';
|
|
}
|
|
|
|
export function loadEnv() {
|
|
return { ...process.env };
|
|
}
|
|
|
|
export function parseArgs(args: string[]) {
|
|
return args.reduce((acc: any, arg) => {
|
|
const [k, v] = arg.split('=');
|
|
acc[k] = v;
|
|
return acc;
|
|
}, {});
|
|
}
|
|
`,
|
|
});
|
|
result = runSkillsCli(tmpDir);
|
|
}, 120000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Verify analyze --skills generates valid SKILL.md files for a
|
|
* TypeScript repo with 3 clusters of cross-calling functions.
|
|
*/
|
|
it('generates skill files', () => {
|
|
assertSkillFiles(result, tmpDir);
|
|
}, 50000);
|
|
|
|
/**
|
|
* Verify CLAUDE.md and AGENTS.md are created and reference generated skills.
|
|
*/
|
|
it('context files updated', () => {
|
|
assertContextFiles(result, tmpDir);
|
|
}, 50000);
|
|
});
|
|
|
|
// ============================================================================
|
|
// DESCRIBE 2: JavaScript
|
|
// ============================================================================
|
|
|
|
describe('JavaScript', () => {
|
|
let tmpDir: string;
|
|
let result: ReturnType<typeof runSkillsCli>;
|
|
|
|
beforeAll(() => {
|
|
tmpDir = createFixtureRepo('javascript', {
|
|
'src/handlers/userHandler.js': `
|
|
const { findById } = require('../services/userService');
|
|
const { validateInput } = require('../helpers/validator');
|
|
|
|
function getUser(id) {
|
|
validateInput(id);
|
|
return findById(id);
|
|
}
|
|
|
|
function createUser(data) {
|
|
validateInput(data.name);
|
|
return { id: Date.now(), ...data };
|
|
}
|
|
|
|
module.exports = { getUser, createUser };
|
|
`,
|
|
'src/handlers/authHandler.js': `
|
|
const { hashPassword, createToken } = require('../services/authService');
|
|
|
|
function login(username, password) {
|
|
const hashed = hashPassword(password);
|
|
return createToken(username);
|
|
}
|
|
|
|
function logout(token) {
|
|
return { success: true };
|
|
}
|
|
|
|
module.exports = { login, logout };
|
|
`,
|
|
'src/handlers/errorHandler.js': `
|
|
const { logError } = require('../helpers/logger');
|
|
|
|
function handleError(err) {
|
|
logError(err.message);
|
|
return { error: err.message };
|
|
}
|
|
|
|
function formatError(err) {
|
|
logError('format: ' + err.message);
|
|
return { code: err.code || 500, message: err.message };
|
|
}
|
|
|
|
module.exports = { handleError, formatError };
|
|
`,
|
|
'src/services/userService.js': `
|
|
const { formatUser } = require('./formatService');
|
|
|
|
function findById(id) {
|
|
const user = { id, name: 'Test' };
|
|
return formatUser(user);
|
|
}
|
|
|
|
function saveUser(user) {
|
|
return { ...user, saved: true };
|
|
}
|
|
|
|
module.exports = { findById, saveUser };
|
|
`,
|
|
'src/services/authService.js': `
|
|
function hashPassword(password) {
|
|
return 'hashed_' + password;
|
|
}
|
|
|
|
function createToken(username) {
|
|
return 'token_' + username + '_' + Date.now();
|
|
}
|
|
|
|
function verifyToken(token) {
|
|
return token.startsWith('token_');
|
|
}
|
|
|
|
module.exports = { hashPassword, createToken, verifyToken };
|
|
`,
|
|
'src/services/formatService.js': `
|
|
function formatUser(user) {
|
|
return { ...user, displayName: user.name.toUpperCase() };
|
|
}
|
|
|
|
function formatDate(date) {
|
|
return new Date(date).toISOString();
|
|
}
|
|
|
|
function formatError(err) {
|
|
return { error: true, message: String(err) };
|
|
}
|
|
|
|
module.exports = { formatUser, formatDate, formatError };
|
|
`,
|
|
'src/helpers/validator.js': `
|
|
function validateInput(input) {
|
|
if (!input) throw new Error('Required');
|
|
return true;
|
|
}
|
|
|
|
function validateEmail(email) {
|
|
return /^[^@]+@[^@]+$/.test(email);
|
|
}
|
|
|
|
function sanitize(str) {
|
|
return String(str).replace(/[<>]/g, '');
|
|
}
|
|
|
|
module.exports = { validateInput, validateEmail, sanitize };
|
|
`,
|
|
'src/helpers/logger.js': `
|
|
function logError(msg) {
|
|
console.error('[ERROR]', msg);
|
|
}
|
|
|
|
function logInfo(msg) {
|
|
console.log('[INFO]', msg);
|
|
}
|
|
|
|
function createEntry(level, msg) {
|
|
return { level, msg, ts: Date.now() };
|
|
}
|
|
|
|
module.exports = { logError, logInfo, createEntry };
|
|
`,
|
|
});
|
|
result = runSkillsCli(tmpDir);
|
|
}, 120000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Verify analyze --skills generates valid SKILL.md files for a
|
|
* JavaScript repo with handler/service/helper clusters.
|
|
*/
|
|
it('generates skill files', () => {
|
|
assertSkillFiles(result, tmpDir);
|
|
}, 50000);
|
|
|
|
/**
|
|
* Verify CLAUDE.md and AGENTS.md are created and reference generated skills.
|
|
*/
|
|
it('context files updated', () => {
|
|
assertContextFiles(result, tmpDir);
|
|
}, 50000);
|
|
});
|
|
|
|
// ============================================================================
|
|
// DESCRIBE 3: Python
|
|
// ============================================================================
|
|
|
|
describe('Python', () => {
|
|
let tmpDir: string;
|
|
let result: ReturnType<typeof runSkillsCli>;
|
|
|
|
beforeAll(() => {
|
|
tmpDir = createFixtureRepo('python', {
|
|
'src/auth/__init__.py': '',
|
|
'src/auth/login.py': `
|
|
from src.auth.hash import hash_password
|
|
from src.auth.session import create_session
|
|
|
|
def login(username, password):
|
|
hashed = hash_password(password)
|
|
session = create_session(username)
|
|
return session
|
|
|
|
def validate_credentials(username, password):
|
|
if not username or not password:
|
|
raise ValueError("Invalid credentials")
|
|
return True
|
|
`,
|
|
'src/auth/hash.py': `
|
|
def hash_password(password):
|
|
return "hashed_" + password
|
|
|
|
def compare_hash(plain, hashed):
|
|
return hash_password(plain) == hashed
|
|
|
|
def generate_salt():
|
|
return "salt_" + str(id(object()))
|
|
`,
|
|
'src/auth/session.py': `
|
|
from src.auth.login import login
|
|
|
|
def create_session(username):
|
|
return {"user": username, "token": "sess_" + username}
|
|
|
|
def validate_session(session):
|
|
return session and "token" in session
|
|
|
|
def refresh_session(session):
|
|
return create_session(session["user"])
|
|
`,
|
|
'src/database/__init__.py': '',
|
|
'src/database/query.py': `
|
|
from src.database.format import format_result
|
|
from src.database.cache import get_cached
|
|
|
|
def run_query(sql):
|
|
cached = get_cached(sql)
|
|
if cached:
|
|
return cached
|
|
return format_result({"sql": sql, "rows": []})
|
|
|
|
def build_query(table, conditions):
|
|
return f"SELECT * FROM {table}"
|
|
`,
|
|
'src/database/format.py': `
|
|
def format_result(data):
|
|
return {**data, "formatted": True}
|
|
|
|
def serialize_result(data):
|
|
import json
|
|
return json.dumps(data)
|
|
|
|
def format_error(err):
|
|
return {"error": str(err)}
|
|
`,
|
|
'src/database/cache.py': `
|
|
from src.database.query import run_query
|
|
|
|
_cache = {}
|
|
|
|
def get_cached(key):
|
|
return _cache.get(key)
|
|
|
|
def warm_cache(keys):
|
|
for key in keys:
|
|
_cache[key] = run_query(key)
|
|
`,
|
|
'src/utils/__init__.py': '',
|
|
'src/utils/logger.py': `
|
|
def log_info(msg):
|
|
print(f"[INFO] {msg}")
|
|
|
|
def log_error(msg):
|
|
print(f"[ERROR] {msg}")
|
|
|
|
def create_entry(level, msg):
|
|
return {"level": level, "msg": msg}
|
|
`,
|
|
'src/utils/validator.py': `
|
|
def validate_input(data):
|
|
if not data:
|
|
raise ValueError("Input required")
|
|
return True
|
|
|
|
def sanitize(text):
|
|
return text.replace("<", "").replace(">", "")
|
|
|
|
def check_length(text, max_len=255):
|
|
return len(text) <= max_len
|
|
`,
|
|
});
|
|
result = runSkillsCli(tmpDir);
|
|
}, 120000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Verify analyze --skills generates valid SKILL.md files for a
|
|
* Python repo with auth/database/utils clusters.
|
|
*/
|
|
it('generates skill files', () => {
|
|
assertSkillFiles(result, tmpDir);
|
|
}, 50000);
|
|
|
|
/**
|
|
* Verify CLAUDE.md and AGENTS.md are created and reference generated skills.
|
|
*/
|
|
it('context files updated', () => {
|
|
assertContextFiles(result, tmpDir);
|
|
}, 50000);
|
|
});
|
|
|
|
// ============================================================================
|
|
// DESCRIBE 4: Go
|
|
// ============================================================================
|
|
|
|
describe('Go', () => {
|
|
let tmpDir: string;
|
|
let result: ReturnType<typeof runSkillsCli>;
|
|
|
|
beforeAll(() => {
|
|
tmpDir = createFixtureRepo('go', {
|
|
'go.mod': `module example.com/testapp
|
|
|
|
go 1.21
|
|
`,
|
|
'cmd/main.go': `package main
|
|
|
|
import (
|
|
"example.com/testapp/pkg/handler"
|
|
)
|
|
|
|
func main() {
|
|
handler.HandleGet("1")
|
|
handler.HandlePost(map[string]string{"name": "test"})
|
|
}
|
|
`,
|
|
'pkg/handler/get.go': `package handler
|
|
|
|
import (
|
|
"example.com/testapp/pkg/service"
|
|
)
|
|
|
|
func HandleGet(id string) map[string]interface{} {
|
|
user := service.FindUser(id)
|
|
return service.FormatResponse(user)
|
|
}
|
|
`,
|
|
'pkg/handler/post.go': `package handler
|
|
|
|
import (
|
|
"example.com/testapp/pkg/service"
|
|
)
|
|
|
|
func HandlePost(data map[string]string) map[string]interface{} {
|
|
service.ValidateInput(data)
|
|
return service.CreateUser(data)
|
|
}
|
|
`,
|
|
'pkg/service/user.go': `package service
|
|
|
|
import (
|
|
"example.com/testapp/pkg/repository"
|
|
)
|
|
|
|
func FindUser(id string) map[string]interface{} {
|
|
return repository.GetByID(id)
|
|
}
|
|
|
|
func CreateUser(data map[string]string) map[string]interface{} {
|
|
repository.Save(data)
|
|
return map[string]interface{}{"created": true}
|
|
}
|
|
`,
|
|
'pkg/service/format.go': `package service
|
|
|
|
func FormatResponse(data map[string]interface{}) map[string]interface{} {
|
|
data["formatted"] = true
|
|
return data
|
|
}
|
|
|
|
func ValidateInput(data map[string]string) bool {
|
|
return len(data) > 0
|
|
}
|
|
|
|
func Sanitize(input string) string {
|
|
return input
|
|
}
|
|
`,
|
|
'pkg/repository/user_repo.go': `package repository
|
|
|
|
func GetByID(id string) map[string]interface{} {
|
|
return map[string]interface{}{"id": id, "name": "Test"}
|
|
}
|
|
|
|
func Save(data map[string]string) bool {
|
|
return true
|
|
}
|
|
|
|
func Delete(id string) bool {
|
|
return true
|
|
}
|
|
`,
|
|
'pkg/models/user.go': `package models
|
|
|
|
type User struct {
|
|
ID string
|
|
Name string
|
|
}
|
|
|
|
func NewUser(id, name string) *User {
|
|
return &User{ID: id, Name: name}
|
|
}
|
|
|
|
func (u *User) Validate() bool {
|
|
return u.ID != "" && u.Name != ""
|
|
}
|
|
`,
|
|
});
|
|
result = runSkillsCli(tmpDir);
|
|
}, 120000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Verify analyze --skills generates valid SKILL.md files for a
|
|
* Go repo with handler/service/repository clusters.
|
|
*/
|
|
it('generates skill files', () => {
|
|
assertSkillFiles(result, tmpDir);
|
|
}, 50000);
|
|
|
|
/**
|
|
* Verify CLAUDE.md and AGENTS.md are created and reference generated skills.
|
|
*/
|
|
it('context files updated', () => {
|
|
assertContextFiles(result, tmpDir);
|
|
}, 50000);
|
|
});
|
|
|
|
// ============================================================================
|
|
// DESCRIBE 5: Java
|
|
// ============================================================================
|
|
|
|
describe('Java', () => {
|
|
let tmpDir: string;
|
|
let result: ReturnType<typeof runSkillsCli>;
|
|
|
|
beforeAll(() => {
|
|
tmpDir = createFixtureRepo('java', {
|
|
'src/service/UserService.java': `package service;
|
|
|
|
import repository.UserRepository;
|
|
import service.Validator;
|
|
|
|
public class UserService {
|
|
private UserRepository repository = new UserRepository();
|
|
private Validator validator = new Validator();
|
|
|
|
public Object findUser(String id) {
|
|
validator.validate(id);
|
|
return repository.getById(id);
|
|
}
|
|
|
|
public Object createUser(String name) {
|
|
validator.validate(name);
|
|
return repository.save(name);
|
|
}
|
|
}
|
|
`,
|
|
'src/service/AuthService.java': `package service;
|
|
|
|
public class AuthService {
|
|
private UserService userService = new UserService();
|
|
|
|
public Object authenticate(String username, String password) {
|
|
Object user = userService.findUser(username);
|
|
return hashPassword(password);
|
|
}
|
|
|
|
public String hashPassword(String password) {
|
|
return "hashed_" + password;
|
|
}
|
|
}
|
|
`,
|
|
'src/service/Validator.java': `package service;
|
|
|
|
public class Validator {
|
|
public boolean validate(String input) {
|
|
if (input == null || input.isEmpty()) {
|
|
throw new IllegalArgumentException("Invalid input");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public String sanitize(String input) {
|
|
return input.replaceAll("[<>]", "");
|
|
}
|
|
|
|
public boolean checkLength(String input, int max) {
|
|
return input.length() <= max;
|
|
}
|
|
}
|
|
`,
|
|
'src/repository/UserRepository.java': `package repository;
|
|
|
|
public class UserRepository extends BaseRepository {
|
|
public Object getById(String id) {
|
|
return new Object();
|
|
}
|
|
|
|
public Object save(String name) {
|
|
return new Object();
|
|
}
|
|
|
|
public boolean delete(String id) {
|
|
return true;
|
|
}
|
|
}
|
|
`,
|
|
'src/repository/BaseRepository.java': `package repository;
|
|
|
|
public abstract class BaseRepository {
|
|
public Object[] findAll() {
|
|
return new Object[0];
|
|
}
|
|
|
|
public int count() {
|
|
return 0;
|
|
}
|
|
}
|
|
`,
|
|
'src/model/User.java': `package model;
|
|
|
|
public class User {
|
|
private String name;
|
|
|
|
public User(String name) {
|
|
this.name = name;
|
|
}
|
|
|
|
public String getName() {
|
|
return name;
|
|
}
|
|
|
|
public void setName(String name) {
|
|
this.name = name;
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
result = runSkillsCli(tmpDir);
|
|
}, 120000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Verify analyze --skills generates valid SKILL.md files for a
|
|
* Java repo with service/repository/model clusters.
|
|
*/
|
|
it('generates skill files', () => {
|
|
assertSkillFiles(result, tmpDir);
|
|
}, 50000);
|
|
|
|
/**
|
|
* Verify CLAUDE.md and AGENTS.md are created and reference generated skills.
|
|
*/
|
|
it('context files updated', () => {
|
|
assertContextFiles(result, tmpDir);
|
|
}, 50000);
|
|
});
|
|
|
|
// ============================================================================
|
|
// DESCRIBE 6: Rust
|
|
// ============================================================================
|
|
|
|
describe('Rust', () => {
|
|
let tmpDir: string;
|
|
let result: ReturnType<typeof runSkillsCli>;
|
|
|
|
beforeAll(() => {
|
|
tmpDir = createFixtureRepo('rust', {
|
|
'Cargo.toml': `[package]
|
|
name = "testapp"
|
|
version = "0.1.0"
|
|
edition = "2021"
|
|
`,
|
|
'src/main.rs': `mod auth;
|
|
mod data;
|
|
|
|
fn main() {
|
|
let session = auth::login::login("user", "pass");
|
|
let result = data::query::run_query("SELECT 1");
|
|
println!("{:?} {:?}", session, result);
|
|
}
|
|
`,
|
|
'src/auth/mod.rs': `pub mod login;
|
|
pub mod hash;
|
|
`,
|
|
'src/auth/login.rs': `use crate::auth::hash::hash_password;
|
|
|
|
pub fn login(username: &str, password: &str) -> String {
|
|
let hashed = hash_password(password);
|
|
format!("session_{}_{}", username, hashed)
|
|
}
|
|
|
|
pub fn validate(token: &str) -> bool {
|
|
token.starts_with("session_")
|
|
}
|
|
`,
|
|
'src/auth/hash.rs': `pub fn hash_password(password: &str) -> String {
|
|
format!("hashed_{}", password)
|
|
}
|
|
|
|
pub fn compare_hash(plain: &str, hashed: &str) -> bool {
|
|
hash_password(plain) == hashed
|
|
}
|
|
|
|
pub fn generate_salt() -> String {
|
|
String::from("random_salt")
|
|
}
|
|
`,
|
|
'src/data/mod.rs': `pub mod query;
|
|
pub mod format;
|
|
`,
|
|
'src/data/query.rs': `use crate::data::format::format_result;
|
|
|
|
pub fn run_query(sql: &str) -> String {
|
|
let raw = format!("result_{}", sql);
|
|
format_result(&raw)
|
|
}
|
|
|
|
pub fn build_query(table: &str) -> String {
|
|
format!("SELECT * FROM {}", table)
|
|
}
|
|
`,
|
|
'src/data/format.rs': `pub fn format_result(data: &str) -> String {
|
|
format!("[formatted] {}", data)
|
|
}
|
|
|
|
pub fn serialize(data: &str) -> String {
|
|
format!("{{\"data\": \"{}\"}}", data)
|
|
}
|
|
|
|
pub fn format_error(err: &str) -> String {
|
|
format!("[ERROR] {}", err)
|
|
}
|
|
`,
|
|
});
|
|
result = runSkillsCli(tmpDir);
|
|
}, 120000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Verify analyze --skills generates valid SKILL.md files for a
|
|
* Rust repo with auth/data module clusters.
|
|
*/
|
|
it('generates skill files', () => {
|
|
assertSkillFiles(result, tmpDir);
|
|
}, 50000);
|
|
|
|
/**
|
|
* Verify CLAUDE.md and AGENTS.md are created and reference generated skills.
|
|
*/
|
|
it('context files updated', () => {
|
|
assertContextFiles(result, tmpDir);
|
|
}, 50000);
|
|
});
|
|
|
|
// ============================================================================
|
|
// DESCRIBE 7: C#
|
|
// ============================================================================
|
|
|
|
describe('CSharp', () => {
|
|
let tmpDir: string;
|
|
let result: ReturnType<typeof runSkillsCli>;
|
|
|
|
beforeAll(() => {
|
|
tmpDir = createFixtureRepo('csharp', {
|
|
'Services/UserService.cs': `using System;
|
|
|
|
namespace Services
|
|
{
|
|
public class UserService
|
|
{
|
|
public object FindUser(string id)
|
|
{
|
|
return id;
|
|
}
|
|
|
|
public object CreateUser(string name)
|
|
{
|
|
return name;
|
|
}
|
|
|
|
public object UpdateUser(string id, string name)
|
|
{
|
|
return name;
|
|
}
|
|
|
|
public bool RemoveUser(string id)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public class UserValidator
|
|
{
|
|
public bool ValidateUser(string input)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public string SanitizeUser(string input)
|
|
{
|
|
return input;
|
|
}
|
|
|
|
public bool CheckUserLength(string input)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
'Services/AuthService.cs': `using System;
|
|
|
|
namespace Services
|
|
{
|
|
public class AuthService
|
|
{
|
|
public object Authenticate(string username, string password)
|
|
{
|
|
return username;
|
|
}
|
|
|
|
public string HashPassword(string password)
|
|
{
|
|
return password;
|
|
}
|
|
|
|
public bool VerifyPassword(string hashed)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public string CreateToken(string username)
|
|
{
|
|
return username;
|
|
}
|
|
}
|
|
|
|
public class TokenManager
|
|
{
|
|
public string GenerateToken(string user)
|
|
{
|
|
return user;
|
|
}
|
|
|
|
public bool ValidateToken(string token)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public string RefreshToken(string token)
|
|
{
|
|
return token;
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
'Services/OrderService.cs': `using System;
|
|
|
|
namespace Services
|
|
{
|
|
public class OrderService
|
|
{
|
|
public object CreateOrder(string item)
|
|
{
|
|
return item;
|
|
}
|
|
|
|
public object GetOrder(string id)
|
|
{
|
|
return id;
|
|
}
|
|
|
|
public bool CancelOrder(string id)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public object UpdateOrder(string id, string item)
|
|
{
|
|
return item;
|
|
}
|
|
}
|
|
|
|
public class OrderValidator
|
|
{
|
|
public bool ValidateOrder(string input)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public string SanitizeOrder(string input)
|
|
{
|
|
return input;
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
'Services/EmailService.cs': `using System;
|
|
|
|
namespace Services
|
|
{
|
|
public class EmailService
|
|
{
|
|
public void SendMail(string to, string body)
|
|
{
|
|
}
|
|
|
|
public void SendBulk(string to, string body)
|
|
{
|
|
}
|
|
|
|
public string FormatBody(string body)
|
|
{
|
|
return body;
|
|
}
|
|
|
|
public bool ValidateAddress(string addr)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
'Data/UserRepo.cs': `using System;
|
|
|
|
namespace Data
|
|
{
|
|
public class UserRepo
|
|
{
|
|
public object GetById(string id)
|
|
{
|
|
return id;
|
|
}
|
|
|
|
public object Save(string name)
|
|
{
|
|
return name;
|
|
}
|
|
|
|
public object Update(string id, string name)
|
|
{
|
|
return name;
|
|
}
|
|
|
|
public bool Delete(string id)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public object[] ListAll()
|
|
{
|
|
return new object[0];
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
'Data/OrderRepo.cs': `using System;
|
|
|
|
namespace Data
|
|
{
|
|
public class OrderRepo
|
|
{
|
|
public object FindOrder(string id)
|
|
{
|
|
return id;
|
|
}
|
|
|
|
public object InsertOrder(string item)
|
|
{
|
|
return item;
|
|
}
|
|
|
|
public bool RemoveOrder(string id)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public object UpdateOrder(string id, string data)
|
|
{
|
|
return data;
|
|
}
|
|
|
|
public int CountOrders()
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
'Data/CacheManager.cs': `using System;
|
|
|
|
namespace Data
|
|
{
|
|
public class CacheManager
|
|
{
|
|
public object GetCached(string key)
|
|
{
|
|
return key;
|
|
}
|
|
|
|
public void SetCached(string key, object val)
|
|
{
|
|
}
|
|
|
|
public void Invalidate(string key)
|
|
{
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
}
|
|
}
|
|
|
|
public class CacheStats
|
|
{
|
|
public int GetHitCount()
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
public int GetMissCount()
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
public double GetHitRate()
|
|
{
|
|
return 0.0;
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
'Data/Logger.cs': `using System;
|
|
|
|
namespace Data
|
|
{
|
|
public class Logger
|
|
{
|
|
public void Info(string msg)
|
|
{
|
|
}
|
|
|
|
public void Error(string msg)
|
|
{
|
|
}
|
|
|
|
public void Warn(string msg)
|
|
{
|
|
}
|
|
|
|
public void Debug(string msg)
|
|
{
|
|
}
|
|
}
|
|
|
|
public class LogFormatter
|
|
{
|
|
public string FormatEntry(string level, string msg)
|
|
{
|
|
return level + msg;
|
|
}
|
|
|
|
public string FormatTimestamp()
|
|
{
|
|
return "";
|
|
}
|
|
|
|
public string FormatStackTrace(string trace)
|
|
{
|
|
return trace;
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
});
|
|
result = runSkillsCli(tmpDir);
|
|
}, 120000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Verify analyze --skills generates valid SKILL.md files for a
|
|
* C# repo with Services/Data clusters.
|
|
*
|
|
* Note: tree-sitter-c-sharp's native N-API addon can crash in forked
|
|
* workers on some platforms (libc++abi exception). When this happens,
|
|
* the pipeline falls through with 0 communities and no skills are
|
|
* generated. assertSkillFiles handles this gracefully.
|
|
*/
|
|
it('generates skill files', () => {
|
|
assertSkillFiles(result, tmpDir);
|
|
}, 50000);
|
|
|
|
/**
|
|
* Verify CLAUDE.md and AGENTS.md are created and reference generated skills.
|
|
*/
|
|
it('context files updated', () => {
|
|
assertContextFiles(result, tmpDir);
|
|
}, 50000);
|
|
});
|
|
|
|
// ============================================================================
|
|
// DESCRIBE 8: C++
|
|
// ============================================================================
|
|
|
|
describe('CPlusPlus', () => {
|
|
let tmpDir: string;
|
|
let result: ReturnType<typeof runSkillsCli>;
|
|
|
|
beforeAll(() => {
|
|
tmpDir = createFixtureRepo('cpp', {
|
|
'src/engine/engine.h': `#ifndef ENGINE_H
|
|
#define ENGINE_H
|
|
|
|
class Engine {
|
|
public:
|
|
void start();
|
|
void stop();
|
|
};
|
|
|
|
#endif
|
|
`,
|
|
'src/engine/engine.cpp': `#include "engine.h"
|
|
#include "../utils/logger.h"
|
|
#include "../utils/config.h"
|
|
|
|
void Engine::start() {
|
|
Logger logger;
|
|
logger.log("Engine starting");
|
|
Config config;
|
|
config.get("engine.mode");
|
|
}
|
|
|
|
void Engine::stop() {
|
|
Logger logger;
|
|
logger.log("Engine stopping");
|
|
}
|
|
`,
|
|
'src/engine/renderer.h': `#ifndef RENDERER_H
|
|
#define RENDERER_H
|
|
|
|
class Renderer {
|
|
public:
|
|
void render();
|
|
void clear();
|
|
};
|
|
|
|
#endif
|
|
`,
|
|
'src/engine/renderer.cpp': `#include "renderer.h"
|
|
#include "engine.h"
|
|
|
|
void Renderer::render() {
|
|
Engine engine;
|
|
engine.start();
|
|
}
|
|
|
|
void Renderer::clear() {
|
|
}
|
|
`,
|
|
'src/engine/physics.h': `#ifndef PHYSICS_H
|
|
#define PHYSICS_H
|
|
|
|
void simulate();
|
|
void collide();
|
|
|
|
#endif
|
|
`,
|
|
'src/engine/physics.cpp': `#include "physics.h"
|
|
#include "engine.h"
|
|
#include "../utils/logger.h"
|
|
|
|
void simulate() {
|
|
Engine engine;
|
|
engine.stop();
|
|
Logger logger;
|
|
logger.log("simulating");
|
|
}
|
|
|
|
void collide() {
|
|
Logger logger;
|
|
logger.log("collision detected");
|
|
}
|
|
`,
|
|
'src/utils/logger.h': `#ifndef LOGGER_H
|
|
#define LOGGER_H
|
|
|
|
#include <string>
|
|
|
|
class Logger {
|
|
public:
|
|
void log(const std::string& msg);
|
|
void error(const std::string& msg);
|
|
void flush();
|
|
};
|
|
|
|
#endif
|
|
`,
|
|
'src/utils/logger.cpp': `#include "logger.h"
|
|
#include <iostream>
|
|
|
|
void Logger::log(const std::string& msg) {
|
|
std::cout << "[LOG] " << msg << std::endl;
|
|
}
|
|
|
|
void Logger::error(const std::string& msg) {
|
|
std::cerr << "[ERR] " << msg << std::endl;
|
|
}
|
|
|
|
void Logger::flush() {
|
|
std::cout.flush();
|
|
}
|
|
`,
|
|
'src/utils/config.h': `#ifndef CONFIG_H
|
|
#define CONFIG_H
|
|
|
|
#include <string>
|
|
|
|
class Config {
|
|
public:
|
|
std::string get(const std::string& key);
|
|
void set(const std::string& key, const std::string& value);
|
|
void load(const std::string& path);
|
|
};
|
|
|
|
#endif
|
|
`,
|
|
'src/utils/config.cpp': `#include "config.h"
|
|
|
|
std::string Config::get(const std::string& key) {
|
|
return "";
|
|
}
|
|
|
|
void Config::set(const std::string& key, const std::string& value) {
|
|
}
|
|
|
|
void Config::load(const std::string& path) {
|
|
}
|
|
`,
|
|
'src/utils/math.h': `#ifndef MATH_H
|
|
#define MATH_H
|
|
|
|
int clamp(int value, int min, int max);
|
|
float lerp(float a, float b, float t);
|
|
double distance(double x1, double y1, double x2, double y2);
|
|
|
|
#endif
|
|
`,
|
|
'src/utils/math.cpp': `#include "math.h"
|
|
#include <cmath>
|
|
|
|
int clamp(int value, int min, int max) {
|
|
if (value < min) return min;
|
|
if (value > max) return max;
|
|
return value;
|
|
}
|
|
|
|
float lerp(float a, float b, float t) {
|
|
return a + (b - a) * t;
|
|
}
|
|
|
|
double distance(double x1, double y1, double x2, double y2) {
|
|
return std::sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1));
|
|
}
|
|
`,
|
|
});
|
|
result = runSkillsCli(tmpDir);
|
|
}, 120000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Verify analyze --skills generates valid SKILL.md files for a
|
|
* C++ repo with engine/utils clusters including headers.
|
|
*/
|
|
it('generates skill files', () => {
|
|
assertSkillFiles(result, tmpDir);
|
|
}, 50000);
|
|
|
|
/**
|
|
* Verify CLAUDE.md and AGENTS.md are created and reference generated skills.
|
|
*/
|
|
it('context files updated', () => {
|
|
assertContextFiles(result, tmpDir);
|
|
}, 50000);
|
|
});
|
|
|
|
// ============================================================================
|
|
// DESCRIBE 9: C
|
|
// ============================================================================
|
|
|
|
describe('C', () => {
|
|
let tmpDir: string;
|
|
let result: ReturnType<typeof runSkillsCli>;
|
|
|
|
beforeAll(() => {
|
|
tmpDir = createFixtureRepo('c', {
|
|
'src/core/parser.h': `#ifndef PARSER_H
|
|
#define PARSER_H
|
|
|
|
void parse(const char* input);
|
|
void tokenize(const char* input);
|
|
|
|
#endif
|
|
`,
|
|
'src/core/parser.c': `#include "parser.h"
|
|
#include "../io/reader.h"
|
|
#include "../io/logger.h"
|
|
|
|
void parse(const char* input) {
|
|
char* data = read_file(input);
|
|
log_msg("parsing");
|
|
tokenize(data);
|
|
}
|
|
|
|
void tokenize(const char* input) {
|
|
log_msg("tokenizing");
|
|
}
|
|
`,
|
|
'src/core/lexer.h': `#ifndef LEXER_H
|
|
#define LEXER_H
|
|
|
|
typedef struct {
|
|
int type;
|
|
const char* value;
|
|
} Token;
|
|
|
|
void lex(const char* input);
|
|
Token next_token(const char* input);
|
|
int is_keyword(const char* word);
|
|
|
|
#endif
|
|
`,
|
|
'src/core/lexer.c': `#include "lexer.h"
|
|
#include "parser.h"
|
|
#include <string.h>
|
|
|
|
void lex(const char* input) {
|
|
parse(input);
|
|
}
|
|
|
|
Token next_token(const char* input) {
|
|
Token t;
|
|
t.type = 0;
|
|
t.value = input;
|
|
return t;
|
|
}
|
|
|
|
int is_keyword(const char* word) {
|
|
return strcmp(word, "if") == 0 || strcmp(word, "else") == 0;
|
|
}
|
|
`,
|
|
'src/core/ast.h': `#ifndef AST_H
|
|
#define AST_H
|
|
|
|
typedef struct ASTNode {
|
|
int type;
|
|
struct ASTNode* left;
|
|
struct ASTNode* right;
|
|
} ASTNode;
|
|
|
|
ASTNode* create_node(int type);
|
|
void free_node(ASTNode* node);
|
|
|
|
#endif
|
|
`,
|
|
'src/core/ast.c': `#include "ast.h"
|
|
#include "lexer.h"
|
|
#include <stdlib.h>
|
|
|
|
ASTNode* create_node(int type) {
|
|
ASTNode* node = (ASTNode*)malloc(sizeof(ASTNode));
|
|
node->type = type;
|
|
node->left = NULL;
|
|
node->right = NULL;
|
|
tokenize("ast");
|
|
return node;
|
|
}
|
|
|
|
void free_node(ASTNode* node) {
|
|
if (node) {
|
|
free_node(node->left);
|
|
free_node(node->right);
|
|
free(node);
|
|
}
|
|
}
|
|
`,
|
|
'src/io/reader.h': `#ifndef READER_H
|
|
#define READER_H
|
|
|
|
char* read_file(const char* path);
|
|
void close_file(const char* path);
|
|
int file_exists(const char* path);
|
|
|
|
#endif
|
|
`,
|
|
'src/io/reader.c': `#include "reader.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
|
|
char* read_file(const char* path) {
|
|
return "file contents";
|
|
}
|
|
|
|
void close_file(const char* path) {
|
|
}
|
|
|
|
int file_exists(const char* path) {
|
|
FILE* f = fopen(path, "r");
|
|
if (f) { fclose(f); return 1; }
|
|
return 0;
|
|
}
|
|
`,
|
|
'src/io/writer.h': `#ifndef WRITER_H
|
|
#define WRITER_H
|
|
|
|
void write_file(const char* path, const char* data);
|
|
void flush_writer(void);
|
|
|
|
#endif
|
|
`,
|
|
'src/io/writer.c': `#include "writer.h"
|
|
#include "logger.h"
|
|
|
|
void write_file(const char* path, const char* data) {
|
|
log_msg("writing file");
|
|
}
|
|
|
|
void flush_writer(void) {
|
|
log_msg("flushing");
|
|
}
|
|
`,
|
|
'src/io/logger.h': `#ifndef LOGGER_H
|
|
#define LOGGER_H
|
|
|
|
void log_msg(const char* msg);
|
|
void log_error(const char* msg);
|
|
void log_init(void);
|
|
|
|
#endif
|
|
`,
|
|
'src/io/logger.c': `#include "logger.h"
|
|
#include <stdio.h>
|
|
|
|
void log_msg(const char* msg) {
|
|
printf("[LOG] %s\\n", msg);
|
|
}
|
|
|
|
void log_error(const char* msg) {
|
|
fprintf(stderr, "[ERR] %s\\n", msg);
|
|
}
|
|
|
|
void log_init(void) {
|
|
log_msg("logger initialized");
|
|
}
|
|
`,
|
|
});
|
|
result = runSkillsCli(tmpDir);
|
|
}, 120000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Verify analyze --skills generates valid SKILL.md files for a
|
|
* C repo with core/io clusters including headers.
|
|
*/
|
|
it('generates skill files', () => {
|
|
assertSkillFiles(result, tmpDir);
|
|
}, 50000);
|
|
|
|
/**
|
|
* Verify CLAUDE.md and AGENTS.md are created and reference generated skills.
|
|
*/
|
|
it('context files updated', () => {
|
|
assertContextFiles(result, tmpDir);
|
|
}, 50000);
|
|
});
|
|
|
|
// ============================================================================
|
|
// DESCRIBE 10: PHP
|
|
// ============================================================================
|
|
|
|
describe('PHP', () => {
|
|
let tmpDir: string;
|
|
let result: ReturnType<typeof runSkillsCli>;
|
|
|
|
beforeAll(() => {
|
|
tmpDir = createFixtureRepo('php', {
|
|
'src/Controllers/UserController.php': `<?php
|
|
|
|
function controller_index() {
|
|
validate_input('list');
|
|
$users = service_find_all();
|
|
return format_response($users);
|
|
}
|
|
|
|
function controller_store($data) {
|
|
validate_input($data);
|
|
sanitize_input($data);
|
|
$user = service_find_by_id($data);
|
|
return format_response($user);
|
|
}
|
|
|
|
function controller_update($id, $data) {
|
|
validate_input($id);
|
|
validate_input($data);
|
|
$result = service_update($id, $data);
|
|
return format_response($result);
|
|
}
|
|
|
|
function controller_delete($id) {
|
|
validate_input($id);
|
|
return service_delete($id);
|
|
}
|
|
`,
|
|
'src/Controllers/AuthController.php': `<?php
|
|
|
|
function auth_login($username, $password) {
|
|
validate_input($username);
|
|
validate_input($password);
|
|
$hash = auth_hash_password($password);
|
|
return auth_create_token($username);
|
|
}
|
|
|
|
function auth_logout($token) {
|
|
validate_input($token);
|
|
return true;
|
|
}
|
|
|
|
function auth_register($username, $password) {
|
|
validate_input($username);
|
|
sanitize_input($username);
|
|
$hash = auth_hash_password($password);
|
|
return service_create($username, $hash);
|
|
}
|
|
`,
|
|
'src/Controllers/ApiController.php': `<?php
|
|
|
|
function api_handle_request($method, $path) {
|
|
validate_input($method);
|
|
validate_input($path);
|
|
log_request($method . ' ' . $path);
|
|
return format_response(['method' => $method, 'path' => $path]);
|
|
}
|
|
|
|
function api_handle_error($error) {
|
|
log_error($error);
|
|
return format_error($error);
|
|
}
|
|
|
|
function api_middleware($request) {
|
|
validate_input($request);
|
|
log_request('middleware');
|
|
return true;
|
|
}
|
|
`,
|
|
'src/Services/UserService.php': `<?php
|
|
|
|
function service_find_all() {
|
|
$result = db_query('SELECT * FROM users');
|
|
return format_response($result);
|
|
}
|
|
|
|
function service_find_by_id($id) {
|
|
$result = db_query('SELECT * FROM users WHERE id = ' . $id);
|
|
return format_response($result);
|
|
}
|
|
|
|
function service_create($name, $hash) {
|
|
db_execute('INSERT INTO users VALUES (' . $name . ')');
|
|
log_request('user created');
|
|
return true;
|
|
}
|
|
|
|
function service_update($id, $data) {
|
|
db_execute('UPDATE users SET data = ' . $data);
|
|
log_request('user updated');
|
|
return true;
|
|
}
|
|
|
|
function service_delete($id) {
|
|
db_execute('DELETE FROM users WHERE id = ' . $id);
|
|
log_request('user deleted');
|
|
return true;
|
|
}
|
|
`,
|
|
'src/Services/AuthServiceImpl.php': `<?php
|
|
|
|
function auth_hash_password($password) {
|
|
validate_input($password);
|
|
return 'hashed_' . $password;
|
|
}
|
|
|
|
function auth_create_token($username) {
|
|
validate_input($username);
|
|
log_request('token created for ' . $username);
|
|
return 'token_' . $username;
|
|
}
|
|
|
|
function auth_verify_token($token) {
|
|
validate_input($token);
|
|
return strpos($token, 'token_') === 0;
|
|
}
|
|
|
|
function auth_refresh_token($token) {
|
|
auth_verify_token($token);
|
|
return auth_create_token('refreshed');
|
|
}
|
|
`,
|
|
'src/Helpers/validator.php': `<?php
|
|
|
|
function validate_input($input) {
|
|
if (empty($input)) {
|
|
throw new InvalidArgumentException('Invalid');
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function sanitize_input($input) {
|
|
return htmlspecialchars($input);
|
|
}
|
|
|
|
function check_required($data, $fields) {
|
|
foreach ($fields as $field) {
|
|
if (!isset($data[$field])) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function check_length($input, $max = 255) {
|
|
return strlen($input) <= $max;
|
|
}
|
|
`,
|
|
'src/Helpers/logger.php': `<?php
|
|
|
|
function log_request($msg) {
|
|
echo '[REQ] ' . $msg . "\\n";
|
|
}
|
|
|
|
function log_error($msg) {
|
|
echo '[ERR] ' . $msg . "\\n";
|
|
}
|
|
|
|
function log_info($msg) {
|
|
echo '[INFO] ' . $msg . "\\n";
|
|
}
|
|
|
|
function create_log_entry($level, $msg) {
|
|
return ['level' => $level, 'msg' => $msg, 'ts' => time()];
|
|
}
|
|
`,
|
|
'src/Helpers/formatter.php': `<?php
|
|
|
|
function format_response($data) {
|
|
return ['status' => 200, 'body' => $data, 'formatted' => true];
|
|
}
|
|
|
|
function format_error($err) {
|
|
return ['status' => 500, 'error' => $err];
|
|
}
|
|
|
|
function format_date($timestamp) {
|
|
return date('Y-m-d', $timestamp);
|
|
}
|
|
|
|
function format_json($data) {
|
|
return json_encode($data);
|
|
}
|
|
`,
|
|
'src/Data/database.php': `<?php
|
|
|
|
function db_query($sql) {
|
|
log_request('query: ' . $sql);
|
|
return [];
|
|
}
|
|
|
|
function db_execute($sql) {
|
|
log_request('execute: ' . $sql);
|
|
return true;
|
|
}
|
|
|
|
function db_connect($host) {
|
|
return true;
|
|
}
|
|
|
|
function db_close() {
|
|
return true;
|
|
}
|
|
`,
|
|
});
|
|
result = runSkillsCli(tmpDir);
|
|
}, 120000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Verify analyze --skills generates valid SKILL.md files for a
|
|
* PHP repo with Controllers/Services/Models clusters.
|
|
*/
|
|
it('generates skill files', () => {
|
|
assertSkillFiles(result, tmpDir);
|
|
}, 50000);
|
|
|
|
/**
|
|
* Verify CLAUDE.md and AGENTS.md are created and reference generated skills.
|
|
*/
|
|
it('context files updated', () => {
|
|
assertContextFiles(result, tmpDir);
|
|
}, 50000);
|
|
});
|
|
|
|
// ============================================================================
|
|
// DESCRIBE 11: Kotlin
|
|
// ============================================================================
|
|
|
|
describe('Kotlin', () => {
|
|
let tmpDir: string;
|
|
let result: ReturnType<typeof runSkillsCli>;
|
|
|
|
beforeAll(() => {
|
|
tmpDir = createFixtureRepo('kotlin', {
|
|
'src/main/kotlin/service/UserService.kt': `package service
|
|
|
|
fun findUser(id: String): Map<String, Any> {
|
|
validateInput(id)
|
|
val result = dbQuery("SELECT * FROM users WHERE id = $id")
|
|
return formatResponse(result)
|
|
}
|
|
|
|
fun createUser(name: String): Map<String, Any> {
|
|
validateInput(name)
|
|
sanitizeInput(name)
|
|
dbExecute("INSERT INTO users VALUES ('$name')")
|
|
logRequest("user created")
|
|
return formatResponse(mapOf("name" to name))
|
|
}
|
|
|
|
fun updateUser(id: String, name: String): Map<String, Any> {
|
|
validateInput(id)
|
|
validateInput(name)
|
|
dbExecute("UPDATE users SET name = '$name' WHERE id = $id")
|
|
logRequest("user updated")
|
|
return formatResponse(mapOf("id" to id))
|
|
}
|
|
|
|
fun deleteUser(id: String): Boolean {
|
|
validateInput(id)
|
|
dbExecute("DELETE FROM users WHERE id = $id")
|
|
logRequest("user deleted")
|
|
return true
|
|
}
|
|
`,
|
|
'src/main/kotlin/service/AuthService.kt': `package service
|
|
|
|
fun authenticate(username: String, password: String): Map<String, Any> {
|
|
validateInput(username)
|
|
validateInput(password)
|
|
val user = findUser(username)
|
|
val hash = hashPassword(password)
|
|
return formatResponse(mapOf("user" to user, "token" to createToken(username)))
|
|
}
|
|
|
|
fun hashPassword(password: String): String {
|
|
validateInput(password)
|
|
return "hashed_$password"
|
|
}
|
|
|
|
fun createToken(username: String): String {
|
|
validateInput(username)
|
|
logRequest("token created for $username")
|
|
return "token_$username"
|
|
}
|
|
|
|
fun verifyToken(token: String): Boolean {
|
|
validateInput(token)
|
|
return token.startsWith("token_")
|
|
}
|
|
|
|
fun refreshToken(token: String): String {
|
|
verifyToken(token)
|
|
return createToken("refreshed")
|
|
}
|
|
`,
|
|
'src/main/kotlin/service/NotificationService.kt': `package service
|
|
|
|
fun notify(userId: String, message: String) {
|
|
validateInput(userId)
|
|
validateInput(message)
|
|
sendEmail(userId, message)
|
|
}
|
|
|
|
fun sendEmail(to: String, body: String) {
|
|
sanitizeInput(body)
|
|
logRequest("email sent to $to")
|
|
formatMessage(body)
|
|
}
|
|
|
|
fun sendAlert(message: String) {
|
|
logRequest("alert: $message")
|
|
formatError(message)
|
|
}
|
|
`,
|
|
'src/main/kotlin/helpers/Validator.kt': `package helpers
|
|
|
|
fun validateInput(input: String): Boolean {
|
|
if (input.isEmpty()) throw IllegalArgumentException("Invalid")
|
|
return true
|
|
}
|
|
|
|
fun sanitizeInput(input: String): String {
|
|
return input.replace("<", "").replace(">", "")
|
|
}
|
|
|
|
fun checkLength(input: String, max: Int = 255): Boolean {
|
|
return input.length <= max
|
|
}
|
|
|
|
fun normalizeInput(input: String): String {
|
|
return input.trim().lowercase()
|
|
}
|
|
`,
|
|
'src/main/kotlin/helpers/Logger.kt': `package helpers
|
|
|
|
fun logRequest(msg: String) {
|
|
println("[REQ] $msg")
|
|
}
|
|
|
|
fun logError(msg: String) {
|
|
System.err.println("[ERR] $msg")
|
|
}
|
|
|
|
fun logInfo(msg: String) {
|
|
println("[INFO] $msg")
|
|
}
|
|
|
|
fun createLogEntry(level: String, msg: String): Map<String, Any> {
|
|
return mapOf("level" to level, "msg" to msg, "ts" to System.currentTimeMillis())
|
|
}
|
|
`,
|
|
'src/main/kotlin/helpers/Formatter.kt': `package helpers
|
|
|
|
fun formatResponse(data: Map<String, Any>): Map<String, Any> {
|
|
return data + mapOf("formatted" to true, "status" to 200)
|
|
}
|
|
|
|
fun formatError(err: String): Map<String, Any> {
|
|
return mapOf("status" to 500, "error" to err)
|
|
}
|
|
|
|
fun formatMessage(msg: String): String {
|
|
return "[MSG] $msg"
|
|
}
|
|
|
|
fun formatDate(timestamp: Long): String {
|
|
return timestamp.toString()
|
|
}
|
|
`,
|
|
'src/main/kotlin/data/Database.kt': `package data
|
|
|
|
fun dbQuery(sql: String): Map<String, Any> {
|
|
logRequest("query: $sql")
|
|
return mapOf("rows" to emptyList<Any>())
|
|
}
|
|
|
|
fun dbExecute(sql: String): Boolean {
|
|
logRequest("execute: $sql")
|
|
return true
|
|
}
|
|
|
|
fun dbConnect(url: String): Boolean {
|
|
return true
|
|
}
|
|
|
|
fun dbClose() {
|
|
}
|
|
`,
|
|
});
|
|
result = runSkillsCli(tmpDir);
|
|
}, 120000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Verify analyze --skills generates valid SKILL.md files for a
|
|
* Kotlin repo with service/repository clusters.
|
|
*/
|
|
it('generates skill files', () => {
|
|
assertSkillFiles(result, tmpDir);
|
|
}, 50000);
|
|
|
|
/**
|
|
* Verify CLAUDE.md and AGENTS.md are created and reference generated skills.
|
|
*/
|
|
it('context files updated', () => {
|
|
assertContextFiles(result, tmpDir);
|
|
}, 50000);
|
|
});
|
|
|
|
// ============================================================================
|
|
// DESCRIBE 12: Mixed TypeScript + Python
|
|
// ============================================================================
|
|
|
|
describe('Mixed TypeScript + Python', () => {
|
|
let tmpDir: string;
|
|
let result: ReturnType<typeof runSkillsCli>;
|
|
|
|
beforeAll(() => {
|
|
tmpDir = createFixtureRepo('mixed', {
|
|
'packages/backend/src/api/router.ts': `
|
|
import { validateRequest } from '../utils/validator';
|
|
import { logRequest } from '../utils/logger';
|
|
|
|
export function createRouter() {
|
|
validateRequest('route');
|
|
logRequest('router init');
|
|
return { routes: [] };
|
|
}
|
|
|
|
export function registerRoute(path: string) {
|
|
validateRequest(path);
|
|
logRequest('register ' + path);
|
|
return true;
|
|
}
|
|
`,
|
|
'packages/backend/src/api/controller.ts': `
|
|
import { runQuery } from '../data/query';
|
|
|
|
export function handleGet(id: string) {
|
|
return runQuery('SELECT * FROM items WHERE id = ' + id);
|
|
}
|
|
|
|
export function handlePost(body: any) {
|
|
return runQuery('INSERT INTO items VALUES ' + JSON.stringify(body));
|
|
}
|
|
`,
|
|
'packages/backend/src/data/query.ts': `
|
|
export function runQuery(sql: string) {
|
|
return { sql, rows: [] };
|
|
}
|
|
|
|
export function buildQuery(table: string) {
|
|
return 'SELECT * FROM ' + table;
|
|
}
|
|
`,
|
|
'packages/backend/src/utils/validator.ts': `
|
|
export function validateRequest(input: string) {
|
|
if (!input) throw new Error('Invalid');
|
|
return true;
|
|
}
|
|
|
|
export function sanitize(input: string) {
|
|
return input.replace(/[<>]/g, '');
|
|
}
|
|
`,
|
|
'packages/backend/src/utils/logger.ts': `
|
|
export function logRequest(msg: string) {
|
|
console.log('[REQ]', msg);
|
|
}
|
|
|
|
export function logError(msg: string) {
|
|
console.error('[ERR]', msg);
|
|
}
|
|
`,
|
|
'packages/ml/src/pipeline/__init__.py': '',
|
|
'packages/ml/src/pipeline/train.py': `
|
|
from packages.ml.src.data.loader import load_data, preprocess
|
|
|
|
def train(config):
|
|
data = load_data("train.csv")
|
|
processed = preprocess(data)
|
|
return {"model": "trained", "data": processed}
|
|
|
|
def evaluate(model, test_data):
|
|
data = load_data("test.csv")
|
|
return {"accuracy": 0.95}
|
|
`,
|
|
'packages/ml/src/pipeline/predict.py': `
|
|
from packages.ml.src.models.model import load_model
|
|
|
|
def predict(input_data):
|
|
model = load_model("latest")
|
|
return {"prediction": "result"}
|
|
|
|
def batch_predict(inputs):
|
|
model = load_model("latest")
|
|
return [{"prediction": "result"} for _ in inputs]
|
|
`,
|
|
'packages/ml/src/data/__init__.py': '',
|
|
'packages/ml/src/data/loader.py': `
|
|
def load_data(path):
|
|
return {"path": path, "rows": []}
|
|
|
|
def preprocess(data):
|
|
return {**data, "preprocessed": True}
|
|
|
|
def split_data(data, ratio=0.8):
|
|
return data, data
|
|
`,
|
|
'packages/ml/src/models/__init__.py': '',
|
|
'packages/ml/src/models/model.py': `
|
|
def load_model(name):
|
|
return {"name": name, "loaded": True}
|
|
|
|
def save_model(model, path):
|
|
return True
|
|
|
|
def compile_model(config):
|
|
return {"compiled": True}
|
|
`,
|
|
});
|
|
result = runSkillsCli(tmpDir);
|
|
}, 120000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Verify analyze --skills generates at least 1 SKILL.md for a
|
|
* mixed TypeScript + Python monorepo. Relaxed assertion since Leiden
|
|
* may or may not form communities spanning both languages.
|
|
*/
|
|
it('generates skill files', () => {
|
|
assertSkillFiles(result, tmpDir, 1);
|
|
}, 50000);
|
|
|
|
/**
|
|
* Verify CLAUDE.md and AGENTS.md are created and reference generated skills.
|
|
*/
|
|
it('context files updated', () => {
|
|
assertContextFiles(result, tmpDir);
|
|
}, 50000);
|
|
});
|
|
|
|
// ============================================================================
|
|
// DESCRIBE 13: Idempotency
|
|
// ============================================================================
|
|
|
|
describe('Idempotency', () => {
|
|
let tmpDir: string;
|
|
let result1: ReturnType<typeof runSkillsCli>;
|
|
let result2: ReturnType<typeof runSkillsCli>;
|
|
|
|
beforeAll(() => {
|
|
tmpDir = createFixtureRepo('idempotency', {
|
|
'src/core/parser.ts': `
|
|
import { readFile } from '../io/reader';
|
|
import { log } from '../io/logger';
|
|
|
|
export function parse(input: string) {
|
|
const data = readFile(input);
|
|
log('parsing');
|
|
return tokenize(data);
|
|
}
|
|
|
|
export function tokenize(data: string) {
|
|
log('tokenizing');
|
|
return data.split(' ');
|
|
}
|
|
`,
|
|
'src/core/transformer.ts': `
|
|
import { parse } from './parser';
|
|
import { validate } from './validator';
|
|
|
|
export function transform(input: string) {
|
|
validate(input);
|
|
const tokens = parse(input);
|
|
return tokens.map(t => t.toUpperCase());
|
|
}
|
|
|
|
export function optimize(input: string) {
|
|
const tokens = parse(input);
|
|
return tokens.filter(t => t.length > 0);
|
|
}
|
|
`,
|
|
'src/core/validator.ts': `
|
|
export function validate(input: string) {
|
|
if (!input) throw new Error('Invalid');
|
|
return true;
|
|
}
|
|
|
|
export function checkSchema(schema: any) {
|
|
return schema && typeof schema === 'object';
|
|
}
|
|
|
|
export function sanitize(input: string) {
|
|
return input.replace(/[<>]/g, '');
|
|
}
|
|
`,
|
|
'src/io/reader.ts': `
|
|
export function readFile(path: string) {
|
|
return 'file contents from ' + path;
|
|
}
|
|
|
|
export function readStream(path: string) {
|
|
return { path, stream: true };
|
|
}
|
|
|
|
export function close(handle: any) {
|
|
return true;
|
|
}
|
|
`,
|
|
'src/io/writer.ts': `
|
|
import { log } from './logger';
|
|
|
|
export function writeFile(path: string, data: string) {
|
|
log('writing ' + path);
|
|
return true;
|
|
}
|
|
|
|
export function flush() {
|
|
log('flushing');
|
|
return true;
|
|
}
|
|
`,
|
|
'src/io/logger.ts': `
|
|
export function log(msg: string) {
|
|
console.log('[LOG]', msg);
|
|
}
|
|
|
|
export function logError(msg: string) {
|
|
console.error('[ERR]', msg);
|
|
}
|
|
|
|
export function createEntry(level: string, msg: string) {
|
|
return { level, msg, ts: Date.now() };
|
|
}
|
|
`,
|
|
});
|
|
result1 = runSkillsCli(tmpDir);
|
|
result2 = runSkillsCli(tmpDir);
|
|
}, 90000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
/**
|
|
* Running analyze --skills twice should produce stable output:
|
|
* same number of skill directories, all SKILL.md files valid,
|
|
* and CLAUDE.md still references generated skills.
|
|
*/
|
|
it('second analyze --skills produces stable output', () => {
|
|
/* CI timeout tolerance */
|
|
if (result1.status === null || result2.status === null) return;
|
|
|
|
expect(
|
|
result1.status,
|
|
[
|
|
`first analyze --skills exited with code ${result1.status}`,
|
|
`stdout: ${result1.stdout?.slice(0, 500)}`,
|
|
`stderr: ${result1.stderr?.slice(0, 500)}`,
|
|
].join('\n'),
|
|
).toBe(0);
|
|
expect(
|
|
result2.status,
|
|
[
|
|
`second analyze --skills exited with code ${result2.status}`,
|
|
`stdout: ${result2.stdout?.slice(0, 500)}`,
|
|
`stderr: ${result2.stderr?.slice(0, 500)}`,
|
|
].join('\n'),
|
|
).toBe(0);
|
|
|
|
const generatedDir = path.join(tmpDir, '.claude', 'skills', 'generated');
|
|
expect(fs.existsSync(generatedDir)).toBe(true);
|
|
|
|
const skillDirs = fs
|
|
.readdirSync(generatedDir)
|
|
.filter((d) => fs.statSync(path.join(generatedDir, d)).isDirectory());
|
|
expect(skillDirs.length).toBeGreaterThanOrEqual(1);
|
|
|
|
/* All SKILL.md files should still have valid frontmatter */
|
|
for (const dir of skillDirs) {
|
|
const skillPath = path.join(generatedDir, dir, 'SKILL.md');
|
|
expect(fs.existsSync(skillPath)).toBe(true);
|
|
const content = fs.readFileSync(skillPath, 'utf-8');
|
|
expect(content.startsWith('---')).toBe(true);
|
|
expect(content).toContain('name:');
|
|
expect(content).toContain('description:');
|
|
expect(content.length).toBeGreaterThan(200);
|
|
}
|
|
|
|
/* CLAUDE.md should still reference generated skills */
|
|
const claudePath = path.join(tmpDir, 'CLAUDE.md');
|
|
expect(fs.existsSync(claudePath)).toBe(true);
|
|
const claudeContent = fs.readFileSync(claudePath, 'utf-8');
|
|
expect(claudeContent).toContain('.claude/skills/generated/');
|
|
}, 90000);
|
|
});
|