Repository navigation
Replay the fork's commits onto upstream main (83a3afc3) - #121
Conversation
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
Warning Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The first concrete artifact PROBE-OGAR-ADAPTER-UNICHARSET consumes (see
lance-graph core-first-transcode-doctrine.md): a reproducible generator for
tesseract::UNICHARSET's has_function / virtually_overrides method-resolution
manifest, harvested from real Tesseract source via the libclang walker.
Run (corpus stays UPSTREAM, never vendored):
TESSERACT_SRC=/tmp/tesseract LIBCLANG_PATH=/usr/lib/llvm-18/lib \
cargo run -p ruff_cpp_spo --features libclang --example harvest_unicharset
Measured against tesseract-ocr/tesseract@5.5.0 src/ccutil/unicharset.h:
16 classes, UNICHARSET = 121 methods, 880 triples / 97 KB ndjson. The v1
probe's leaf targets unichar_to_id / id_to_unichar are present as
rdf:type ogit:Function; UNICHARSET has no base class, so its ClassView
composes purely via has_function (no MRO needed for the probe).
required-features = ["libclang"] keeps a featureless --all-targets build
from compiling it. fmt + clippy -D warnings clean.
https://claude.ai/code/session_016b33swuXE23hKtqxsHu9p1
…n (CPP-SCHEMA-FIT 82%->90%) Ran the CPP-SCHEMA-FIT real-corpus coverage probe (new gated test cpp_schema_fit_real_corpus_coverage + class_body_cursor_histogram walker instrumentation) over all 31 src/ccutil headers of tesseract@5.5.0: 6570 class-body cursors, 5420 mapped = 82%. The walker matched only EntityKind::Method, silently dropping 495 member functions libclang reports under distinct cursor kinds: Constructor (268), Destructor (139), FunctionTemplate (64), ConversionFunction (24). A real correctness gap — the harvester claimed to capture methods but dropped every ctor/dtor. Fix: build_class now maps all five function-like cursor kinds to a has_function; MAPPED_CURSOR_KINDS updated in lockstep so the coverage metric and the extraction can't drift. Hermetic test gains a ctor + virtual-dtor assertion. After: 5915 mapped = 90% (~97% excluding AccessSpecifier noise + recursed nested types). Next follow-up by frequency: FriendDecl (79). 11/11 ruff_cpp_spo tests pass (libclang feature); clippy -D warnings + fmt clean. https://claude.ai/code/session_016b33swuXE23hKtqxsHu9p1
… — CPP-SCHEMA-FIT 90%->92% Next two data-driven coverage follow-ups from the CPP-SCHEMA-FIT histogram: - VarDecl (84 cursors): a VarDecl in a class body is a static data member (`static T x;`); captured as has_field via a `FieldDecl | VarDecl` arm. - FriendDecl (79 cursors): captured as is_friend_of. Grounded against real libclang-18 rather than guessed — the FriendDecl cursor is anonymous, and its TypeRef child's resolved TYPE display is the clean fully-qualified befriended name (Tesseract::TessdataManager); the cursor spelling carries a `class ` elaboration, so build_friend reads the type, not the spelling. MAPPED_CURSOR_KINDS extended in lockstep (9 kinds). Hermetic fixture gains a static member + friend class with assertions. Coverage 90% -> 92% (6078/6570); ~99% of meaningful constructs now mapped (remaining unmapped is AccessSpecifier noise + recursed nested types + a handful of type-alias/using/enum). 11/11 ruff_cpp_spo tests pass (libclang feature); clippy -D warnings + fmt clean. https://claude.ai/code/session_016b33swuXE23hKtqxsHu9p1
…rvest across runs) cpp_ast_rt_determinism (gated on TESSERACT_SRC) walks all of src/ccutil twice in-process and asserts byte-identical ndjson. Green: the harvest is reproducible end-to-end (no RNG in the walker; walk_files dedups into a sorted BTreeMap; expand sorts + dedups). The plan's "do NOT claim faithful harvest until CPP-AST-RT is green" gate is now satisfied for the in-process path. 2 of 3 primary probes green (CPP-SCHEMA-FIT + CPP-AST-RT); only CPP-TEMPLATE-DET remains, gated on class-level template extraction. clippy -D warnings + fmt clean. https://claude.ai/code/session_016b33swuXE23hKtqxsHu9p1
…0->67 classes Researched template handling against the corpus before implementing: - Observed (instrumented libclang walk on genericvector.h): libclang FLATTENS a ClassTemplate cursor — its children are the template params + members directly — so build_class handles it unchanged. - Measured (grep ccutil): 57 primary class templates, 0 explicit specialisations, pervasive instantiation-uses. Shape A: collect_classes + the coverage tally now treat ClassTemplate and ClassTemplatePartialSpecialization as classes. ccutil harvest 50 -> 67 classes (+17 template containers: GenericVector, PointerVector, ...), 1652 -> 2184 triples — container classes + their methods previously invisible to the graph. Deterministic; hermetic class-template fixture asserts capture (Box::get). Measured B-vs-C finding: B's template_specialises captures nothing on ccutil (0 specs) — B is identical to A here, refuting "B>A" on this corpus. The structure that actually exists is instantiation-use (template_instantiates = Shape C), which is the Inferred/per-TU-incomplete tier CPP-TEMPLATE-DET gates; deferred for the data-driven C round. CPP-TEMPLATE-DET is therefore deferred-with-C (A emits no template-relationship predicate). 12/12 ruff_cpp_spo tests pass (libclang feature); clippy -D warnings + fmt clean. https://claude.ai/code/session_016b33swuXE23hKtqxsHu9p1
…types (CPP-TEMPLATE-DET GREEN) Research-first per operator's "best possible C, then compare": Measured first (before implementing): ccutil has 7 instantiation uses in field types (std::vector x5, GenericVector x1, std::function x1), 0 in bases (build_base already resolves to the primary template name). Verified the gap is non-redundant: expand::cpp_field explicitly drops type_name, so field/signature template-uses were INVISIBLE in the triples. Best-shape: capture template-id type strings SYNTACTICALLY from (a) field types (FieldDecl/VarDecl get_type), (b) method signatures (return + parameter types via get_result_type / get_arguments). Not implicit-instantiation cursors (that's the per-TU-incomplete thing the Inferred provenance flags). Determinism is structural: cursor children are in source order; expand sorts the triple set. Helpers: - template_instantiation(&type_display): strips const/volatile prefixes + trailing */&, returns the verbatim template-id (GenericVector<char>). - collect_signature_instantiations(method, decls): one Instantiation declaration per template-id in a method signature. Measured result: ccutil 2184 -> 2215 triples (+31 deterministic template_instantiates edges); hermetic fixture asserts both field-type (Box<int>) and signature-type (Box<char>) capture; cpp_template_det_determinism probe runs extract_dir twice, asserts identical template_instantiates set — GREEN. C vs A vs A+C (now measured): A captured 0 template-relationship triples; C adds 31 strictly non-redundant ones. A+C composes cleanly: A makes GenericVector a node, C makes Recognizer template_instantiates GenericVector<char> an edge to it. All three primary CPP-* probes now green (SCHEMA-FIT ~91%, AST-RT deterministic, TEMPLATE-DET deterministic + non-degenerate). 13/13 ruff_cpp_spo tests pass (libclang feature); clippy -D warnings + fmt clean. https://claude.ai/code/session_016b33swuXE23hKtqxsHu9p1
…ter scales to OCR data model
Free exploration of "what's next beyond CPP-SCHEMA-FIT / AST-RT / TEMPLATE-DET,"
with measure-first discipline:
Option survey (measured against corpus):
- explicit `template Foo<int>;` instantiations: 0 in ccutil. Skip.
- B-revisited (namespace-qualified template_specialises): IR correctness fix
but 0 specs in ccutil. Hold pending paired test update.
- is_const / is_static method flags: high value, blocked on closed-vocab
approval (predicate_count_locked_at_47 -> 49). Council-review, not autoattended.
- has_param_type / returns_type: biggest enrichment, same closed-vocab gate. Defer.
- Walk src/ccstruct (OCR motherlode): existing extract_tree, no predicate
change. SHIPPED below.
Permanent ccstruct_motherlode_smoke test (gated on TESSERACT_SRC):
extract_tree("src/ccstruct") reaches the OCR data model. Measured:
- 155 classes, 5264 triples, 32 deterministic template_instantiates edges
(vs ccutil's 67 / 2215 / 31).
- Captures EVERY OCR core class (BLOCK/WERD/TBLOB/C_BLOB/POLY_BLOCK/TWERD/
BLOBNBOX/...) plus template_instantiates edges to GenericVector<T>,
BandTriMatrix<T>, GENERIC_2D_ARRAY<T>, KDPair<Key,Data>, PointerVector<T>.
- Honest nuance: signature template-ids in template definitions resolve to
canonical-parameter form (GenericVector<T>, not concrete args). Still
deterministic and useful (links to primary template).
Harvester scales past the utility shell to the load-bearing OCR surface with
the same deterministic shape (CPP-AST-RT applies).
14/14 ruff_cpp_spo tests pass (libclang feature); clippy -D warnings + fmt clean.
https://claude.ai/code/session_016b33swuXE23hKtqxsHu9p1
…erator-approved vocab 48->50)
Operator chose option 2 ("AR AST DLL shape preferred"): method signature types
as edges — what the tesseract-rs-ast-dll-codegen needs to generate adapter
signatures. Option 1 (is_const/is_static, "ORM-shape downcast") is the optional
follow-up.
Closed-vocab bump (operator-authorized): +2 predicates ReturnsType, HasParamType.
predicate_count_locked_at_48 -> _at_50; ALL array + doc invariant +
default_provenance (both CppExtracted, syntactically present) in lockstep.
IR: CppMethod gains return_type: Option<String> + param_types: Vec<String>
(#[serde(default)]); 10 construction sites updated.
Shape: returns_type = one edge per non-void method; has_param_type = one edge
per parameter, object "<index>:<type>" so the unordered triple SET preserves
signature order + arity (codegen sorts by index prefix). Determinism structural:
get_arguments is source-order; expand sorts.
Walker: build_method reads get_result_type (skips void/ctor/dtor) + get_arguments.
Hermetic test asserts int Recognize(int) -> returns_type int + has_param_type
0:int, and void stash(const Box<char>&) -> no returns_type + has_param_type
0:const Box<char> &.
Measured enrichment: ccutil 2215 -> 3527 triples (+1312); ccstruct 5264 -> 8164
(+2900). Signatures are now first-class graph structure (codegen-ready AST-DLL
shape). CPP-AST-RT determinism still green.
46 ruff_spo_triplet + 14 ruff_cpp_spo tests pass; clippy -D warnings + fmt clean.
https://claude.ai/code/session_016b33swuXE23hKtqxsHu9p1
…th operator shapes shipped) Operator's optional "1": is_const / is_static method flags (const = read accessor, static = class-level). Same pattern as the signature shape. Closed-vocab bump: +2 predicates IsConst, IsStatic. predicate_count_locked_at_50 -> _at_52; ALL / doc / default_provenance (CppExtracted) in lockstep. 17 C++ machine-plane predicates now. IR: CppMethod gains is_const + is_static bools; #[derive(Default)] added (future construction-site churn relief) + #[expect(struct_excessive_bools)] (4 independent C++ qualifiers, not a state machine — enums would be artificial). Walker: build_method reads is_const_method() / is_static_method(). Hermetic test asserts bool operator==(...) const -> is_const true, not static. Measured: ccutil 3527 -> 3850 triples (+323); ccstruct 8164 -> 8972 (+808). CPP-AST-RT still deterministic. Both operator-requested shapes now shipped. C++ machine-plane vocab = 17 predicates (was 13): + returns_type, has_param_type (AST-DLL signature shape) + is_const, is_static (ORM-downcast shape). Harvester emits a codegen-ready surface: identity, inheritance, fields, function membership, full signatures, const/static qualifiers, template use, friendship. 46 ruff_spo_triplet + 14 ruff_cpp_spo tests pass; clippy -D warnings + fmt clean. https://claude.ai/code/session_016b33swuXE23hKtqxsHu9p1
…dentity Two real correctness bugs codex flagged on PR #17: P2 #1 — overload collision (load-bearing for the codegen-ready claim). Before: void f(int) and void f(double) both attached their signature triples (returns_type / has_param_type / is_noexcept / ...) to the same cpp:C.f node, so the codegen could not reconstruct the two overloads. Fix: per-overload method IRI in cpp_method appends "(<comma-joined-param-types>)" — cpp:C.f(int) vs cpp:C.f(double). The walker's virtually_overrides target uses the same suffix on the base overload (via get_arguments on the overridden cursor), so the override edge joins the EXACT base overload (not just any same-name method). Hermetic probe: two process(int)/process(double) overloads in the fixture must yield distinct param signatures at the IR level. P2 #2 — partial-spec identity collision. A ClassTemplatePartialSpecialization shares its primary's get_name() (libclang spells it as the bare template name); the cross-TU BTreeMap dedup by qualified_name() then dropped one side. Fix: build_class uses get_display_name() for partial specs (Box<T *>) and get_name() for everything else (Box). Hermetic probe: primary Box + partial spec Box<T *> must coexist with distinct members. Measured impact: ccutil 3850 -> 4129 triples (+279, overload discriminator splits methods that previously aliased); ccstruct 8972 -> 9507 (+535). Class counts unchanged. Also resolves the rebase conflict with PR #15 (association_kind) — vocab count gate adjusted to 53 (7 core + 29 AR-shape + 17 C++ machine-plane). 48 ruff_spo_triplet + 14 ruff_cpp_spo tests pass; clippy -D warnings + fmt clean. https://claude.ai/code/session_016b33swuXE23hKtqxsHu9p1
…errides Rails `belongs_to :owner, class_name: 'User'` declares a relation (`:owner`) that points at a class (`User`) whose name doesn't follow the camelcase-singular convention on the relation name. Without surfacing this override, downstream Schema consumers invent a phantom `record<Owner>` for what should be `record<User>`. The `AssocDecl.options: Vec<(String, String)>` already captures the verbatim option list from the parser (D-AR-2). The expand match arm explicitly dropped them (`let _ = a.options;`); this PR lifts the `class_name` key into a sibling triple keyed by the relation IRI: (openproject:WorkPackage.owner, class_name, "User") Symmetry with the existing `association_kind` triple — both are relation-scoped facts the FK-gating consumer needs. Absent triple means "use the Rails convention" — preserves pre-#15 ndjson dumps bit-for-bit. Predicate enum count moves 49 → 50; `predicate_count_locked_at_*` test updated. Added a focused regression `ar_shape_emits_class_name_override_when_option_present` that asserts both the positive case (override emitted) and the negative (no override → no class_name triple). The existing `ar_shape_emits_every_ar_predicate` exhaustiveness check picks up the new predicate via the fixture's pre-existing `class_name: 'Project'` option on its first association.
…_module) Rails STI (Single Table Inheritance — `class Foo < Issue` with a type-discriminator column) is semantically distinct from method- body composition (`include Acts::Customizable`), but `expand` collapsed both into `includes_module` triples. Downstream consumers (graph build, schema visualizer) lose the type- hierarchy signal — about 26% (~223 / 872) of `includes_module` triples on the OP corpus are likely STI parents (Base / *Query / *Filter / *Strategy classes). Reuse the existing `inherits_from` predicate (originally introduced for C++ class inheritance) for Rails STI: wire shape is identical (subject = class, object = base IRI), per-emission provenance differs (`OpenProjectExtracted` for Rails, `CppExtracted` for C++). The C++ frontend's `inherits_from` emission and behavior are unchanged. Updated the predicate doc to reflect dual-frontend use; updated the `ar_shape_emits_every_ar_predicate` test to expect `inherits_from` instead of `includes_module:Issue` for the STI parent, and added a negative assertion that the parent must NOT also appear under `includes_module`. Renamed the C++-leakage guard to `non_cpp_fixtures_emit_no_cpp_only_predicates` and removed `inherits_from` from its list (since it's no longer C++-only). Tests: 51 pass. Downstream consumer (openproject-nexgen-rs) follow-up PR will lift `inherits_from` to a dedicated `inherits:<Parent>` table annotation, separating STI semantics from concern composition in the schema output.
…ection The AST-DLL codegen's stage-1: groups a Triple set back into a per-class ModelGraph (members, methods, bases, templates, friends, macro uses, static asserts). It is the inverse of expand()'s C++ machine-plane projection. Method identity is recovered from the index-prefixed has_param_type triples (0:int, 1:const Image &), and the (params) IRI suffix is reconstructed from them and stripped to recover the method name — never split on `,`. This sidesteps the comma-bearing-templated-param problem (std::map<int, int>) that a naive suffix split would hit. Class attribution is anchor-first: identity comes from the explicit (class, rdf:type, ogit:ObjectType) triple and the (class, has_function, method) edges, never a rightmost-`.` split. Round-trip property (the falsifier): reassemble(expand(g)) == cpp_projection(g) for any C++-plane graph with distinct method IRIs, where cpp_projection blanks the three fields expand never emits (CppField::type_name, CppBase::access, CppBase::virtual_base). It compares against the live graph, not a frozen golden, so it is immune to harvester-vocabulary drift — only a genuine reassembly bug turns it red. In-crate tests cover the full C++ surface plus three adversarial cases that make the property a real measurement: two classes sharing a method name (no cross-attribution), one class with overloads (no overload collapse), and a method with comma-bearing templated params (no `,`-split corruption). ruff_cpp_spo gains CPP-REASSEMBLE-RT, the same falsifier against the REAL corpus (gated on TESSERACT_SRC). Measured on Tesseract 5.5.0 ccutil: 67 classes; class-set preservation and idempotence hold; 48/67 round-trip byte-exact, 19 differ — quantifying a real harvester limitation where const/non-const overloads collide on one method IRI (const-ness is not in the (params) suffix). Surfaced by the falsifier, not asserted away. Co-Authored-By: Claude <noreply@anthropic.com> https://claude.ai/code/session_016b33swuXE23hKtqxsHu9p1
… finding The reassembler (generator stage 1) lands as the re-scoped C-FIRST gate: a round-trip structural-equivalence falsifier, not the tautological self-golden the brutal council rejected. CPP-REASSEMBLE-RT runs green on real ccutil (67 classes; class-set preservation + idempotence hold). New measured finding (GAP-CONST-OVERLOAD, queued): 48/67 classes round-trip byte-exact vs projection; 19 differ because const/non-const overloads with identical param types collide on one method IRI (const-ness is not in the (params) suffix). Quantified by the falsifier, not synthesized; queued as an operator-gated harvester IRI-scheme change, not a silent drop. Co-Authored-By: Claude <noreply@anthropic.com> https://claude.ai/code/session_016b33swuXE23hKtqxsHu9p1
…EMBLE-RT 67/67 The method IRI now carries the cv-qualifier: expand::cpp_method appends ` const` when is_const, clang_walker does the same for the virtually_overrides target (keyed on base_m.is_const_method()), and reassemble reconstructs the suffix from the recovered is_const. A const/non-const overload pair (T& at(i) vs const T& at(i) const) shares name AND params, so without ` const` in the identity they collapsed under expand's (s,p,o) dedup. Correctness fix, not a vocab change (predicate count stays 54). The falsifier then corrected the council's assumption with data: the cv-aware IRI fixed only 3 of the 19 ccutil "differ" classes (48->51). Tracing the rest (per-class methods/templates/fields delta) showed 13 were benign duplicate template_instantiates that expand dedups but cpp_projection did not, 2 were duplicate-harvested methods (same cause), and 1 (UnicityTable) was an equal-sort- key ordering artifact. Two round-trip-metric fixes followed: - canonicalize_cpp now de-duplicates every C++ collection after sorting, mirroring expand's (s,p,o) dedup (real collisions, same key/different content, are not equal so survive; only exact duplicates collapse). - the methods sort key includes is_const so a const/non-const pair sorts deterministically (non-const first) on both the reassembled and projected side. Result: CPP-REASSEMBLE-RT is 67/67 byte-exact, 0 differ, now a HARD gate (differing.is_empty()) — a regression or a new collision source (ref-qualified overloads in a future corpus) reopens it. New in-crate test const_and_nonconst_overload_stay_distinct. 58 ruff_spo_triplet + 15 ruff_cpp_spo green; clippy -D warnings + fmt clean. Co-Authored-By: Claude <noreply@anthropic.com> https://claude.ai/code/session_016b33swuXE23hKtqxsHu9p1
…djson) Pure formatting: fmt reorders the re-exports alphabetically. No behavior change. Co-Authored-By: Claude <noreply@anthropic.com> https://claude.ai/code/session_016b33swuXE23hKtqxsHu9p1
…ethodSig manifest Stage 2 of the C++->Rust AST-DLL pipeline, per the 8-agent step-2 council. A new ruff_cpp_codegen crate (depends on ruff_spo_triplet ONLY — no lance-graph edge): - project(&ModelGraph) -> Vec<ClassManifest>: extract the C++ method plane (one MethodSig per method: name, ordered params, return, is_const, is_static, override target; body-shaping flags intentionally dropped). - decompile(&[ClassManifest], ns) -> Vec<Triple>: regenerate the signature-plane triples (cv-aware IRI), kept in lockstep with expand::cpp_method. - render(&[ClassManifest]) -> String: emit-text-only Rust source naming lance_graph_contract::codegen_manifest::MethodSig (compiled later in the consumer repo, after the MethodSig EXTEND-CORE). Deterministic, ASCII, escaped (a comma-bearing templated param is one quoted element, never split). Every file carries the PARITY: UNRUN (operator-blocked: leptonica) marker, and the hand-curated Frankenstein deny-list annotates intrusive leaves (id_to_unichar_ext, CleanupString) with // HAND-PORT. The gate has teeth (not a self-golden, per the honest-tester): the round-trip decompile(project(g)) must equal expand(g) restricted to the signature plane — a dropped method or mangled param fails it (round_trip_detects_a_dropped_method proves it can fail). This is the codegen_spine::roundtrip_eq pattern over the live harvested triples, implemented over ruff_spo_triplet::Triple to keep the crate lance-graph-free. CPP-CODEGEN-RT (gated end-to-end in ruff_cpp_spo, dev-dep on the emitter): real ccutil harvest -> 67 classes, 857 methods -> 124 KB MethodSig manifest; the signature-plane round-trip holds and the render carries the PARITY marker with one MethodSig literal per method. classid minting stays OGAR-side (name-keyed text). 10 ruff_cpp_codegen + 16 ruff_cpp_spo tests green; clippy + fmt clean. Co-Authored-By: Claude <noreply@anthropic.com> https://claude.ai/code/session_016b33swuXE23hKtqxsHu9p1
Strengthen render_emits_every_method to assert all six MethodSig fields (name/params/ret/is_const/is_static/overrides) appear in the output. This pins the render-side boundary to lance_graph_contract::codegen_manifest::MethodSig so field drift is caught in-env rather than only at the operator-gated tesseract-rs compile (baton-auditor boundary guard). Co-Authored-By: Claude <noreply@anthropic.com> https://claude.ai/code/session_016b33swuXE23hKtqxsHu9p1
… specifier The C++ harvest dropped member access (public/protected/private) everywhere. has_visibility emits it: a new closed-vocab predicate (54 -> 55), CppMethod gains an `access: CppAccess` field (CppAccess now defaults to Public), the libclang walker reads m.get_accessibility() (reusing the base-specifier mapping), expand emits (class.method, has_visibility, "public"|"protected"|"private"), and reassemble recovers it so CPP-REASSEMBLE-RT stays 67/67 byte-exact. It is the OO API-surface fact and the nearest thing to an intrusiveness signal for the codegen's hand-port deny-list (public = adapter surface; private = likely internal). Scoped to methods + the harvest layer (not threaded into the codegen MethodSig manifest, which round-trips the signature plane only — so CPP-CODEGEN-RT is unaffected). Authorized via the "keep building here" pick. 59 ruff_spo_triplet + 16 ruff_cpp_spo + 10 ruff_cpp_codegen green; clippy -D warnings + fmt clean. Co-Authored-By: Claude <noreply@anthropic.com> https://claude.ai/code/session_016b33swuXE23hKtqxsHu9p1
…dation key
The `validates_constraint` triple today carries only the
attribute name (the existence-of-validation fact), so the
downstream Schema consumer can't distinguish `validates :email,
presence: true` from `validates :email, uniqueness: true` —
every validation lowers to the same generic `ASSERT $value != NONE`.
But Rails' validation kinds map to very different SurrealQL
constraints:
| Rails kind | SurrealQL lowering |
|----------------|-----------------------------------|
| presence | ASSERT $value != NONE |
| uniqueness | DEFINE INDEX … UNIQUE |
| length | ASSERT string::len($value) <= MAX |
| format | ASSERT $value REGEX "…" |
| numericality | ASSERT type::is::number($value) |
| inclusion | ASSERT $value IN [...] |
| exclusion | ASSERT $value NOT IN [...] |
Add `Predicate::ValidationKind` (50 → 56 — net +1 since main has
been advancing). In `Expander::validation`, scan
`Validation.options` for the canonical 9-key Rails set and emit
a sibling triple per recognised key:
(openproject:User.email, validation_kind, "presence")
(openproject:User.email, validation_kind, "uniqueness")
Subject is the attribute IRI (mirrors `field_type`'s shape),
NOT the model — multiple kinds per attribute each emit their
own triple. The existence-of-validation
`validates_constraint` triple still fires unchanged for every
declaration (backward compat).
Non-kind option keys (`if:`, `on:`, `allow_nil:`, `message:`)
pass through silently — they're filter-side conditions that
don't change the schema semantics. Block-form `validate { … }`
(no kind options on IR) emits zero `validation_kind` triples.
Tests: 2 new regressions —
`ar_shape_emits_validation_kind_per_recognised_option` locks
multi-kind + block-form + unknown-option behaviour;
`extract_validation_kinds_recognises_canonical_rails_set` is
the unit-level lock on the helper.
`ar_shape_emits_every_ar_predicate` exhaustiveness check
extended with `"validation_kind"`. 60 tests pass.
Downstream consumer (openproject-nexgen-rs) follow-up PR will
lift each recognised kind to a richer SurrealQL clause.
…ating options (codex P2 on #21) Two codex P2 fixes to the validation_kind emitter from #21: 1. **Missing Rails validators** — the recognised set omitted `absence` (inverse of presence — `validates :archived_at, absence: true`) and `comparison` (`validates :end_date, comparison: { greater_than: :start_date }`). Add both keys. The downstream consumer (nexgen-rs #42) already has arms for both — `absence` → `$value == NONE`, `comparison` → presence-style fallback (parametric). 2. **Gating suppression** — Rails validators with `if:`, `unless:`, `on:`, `allow_nil:`, or `allow_blank:` are conditional. Emitting a `validation_kind` triple for `validates :age, numericality: true, allow_nil: true` makes the downstream consumer generate an unconditional `ASSERT type::is_number($value)` that rejects NONE — Rails would have allowed it. Safer to suppress kind emission and let the consumer fall back to the catch-all presence behaviour from the `validates_constraint` triple. 3. **Falsy validator values** — `validates :foo, presence: false` is a no-op in Rails. Skip such keys so the schema doesn't invent constraints where none exist. Tests: existing `extract_validation_kinds_recognises_canonical_rails_set` updated to use non-gating keys; 3 new regressions — `extract_validation_kinds_suppresses_on_gating_options` (each gating key suppresses individually), `extract_validation_kinds_skips_falsy_validator_values` (`false` / `nil` values skipped), `extract_validation_kinds_recognises_absence_and_comparison` (new keys lift correctly). 63 tests pass.
…oject corpus dump The downstream `op-surreal-ast::triples_to_schema` consumer in [openproject-nexgen-rs](https://github.com/AdaWorldAPI/openproject-nexgen-rs) has been growing fast (FK direction gating, polymorphic fallback, field_type, class_name override, inherits_from, +15 class-level annotation lifts, validation_kind composition, UNIQUE indices) — but the corpus dump it tests against was a month stale (pre-ruff#15) so the consumer-side tests against real OP data weren't exercising any of the new predicates. Add a small example binary `harvest_op` mirroring the existing `ruff_cpp_spo::harvest_unicharset` shape: OP_SRC=/path/to/openproject \ cargo run -p ruff_ruby_spo --example harvest_op # writes /tmp/op_triples.ndjson Calls `ruff_ruby_spo::extract(<source>)` then `ruff_spo_triplet::expand(&graph)` and serialises via `to_ndjson`. The OP corpus stays UPSTREAM and is never vendored (iron rule of the harvester family). Verified on the live OP source tree: models found: 694 triples emitted: 9183 bytes: 1.14MB Predicate frequencies on the fresh dump (showing the new predicates that the previous dump lacked entirely): inherits_from: 485 (STI parents, separated from includes_module) validation_kind: 178 (presence/uniqueness/numericality/...) class_name: 122 (FK target overrides) Downstream verification: re-running nexgen-rs's `op_schema_explore` against the fresh dump produces the expected schema-quality jumps: - 485 `inherits:` annotations (vs 0 on stale dump) - 6 UNIQUE indices - 2 `type::is_number($value)` ASSERTs (numericality kind) - 123 record-kind FKs (up from 92 — class_name overrides resolve more targets)
…t (codex P2 on #23) The workspace's clippy config (`clippy::exit = "warn"` → promoted via `cargo clippy --workspace --all-targets --all-features -- -D warnings`) flags `process::exit` calls in examples too, so the new `harvest_op` made the documented clippy invocation fail. Convert main to `fn main() -> Result<(), Box<dyn std::error::Error>>` (matching the `harvest_unicharset` shape in `ruff_cpp_spo`). Both error paths now return descriptive `Err(...)` strings: - Missing OP source path → returns an error that surfaces via the example process's non-zero exit code (set automatically by Rust's main-returns-Result protocol). - Failed `fs::write` → wrapped in `map_err` to add the output path to the message. Also wrapped a few previously-bare identifiers in doc comments in backticks so `clippy::doc-markdown` stays quiet: `OpenProject` and `AdaWorldAPI`. Verified: `cargo clippy -p ruff_ruby_spo --example harvest_op -- -D warnings` is clean; the binary still extracts 9183 triples from /home/user/openproject as before.
…lidators (length / numericality / inclusion / format)
Parametric Rails validators carry their constraint values on
inner hash options:
validates :name, length: { maximum: 255 }
validates :age, numericality: { greater_than: 0, less_than: 150 }
validates :status, inclusion: { in: %w[open closed] }
validates :email, format: { with: /.+@.+/ }
PR #21 emitted `validation_kind` for the OUTER key (`length`,
`numericality`, etc.) but dropped the inner-hash values, so the
downstream schema consumer collapsed parametric kinds to the
catch-all `$value != NONE` ASSERT — `length: { maximum: 255 }`
got the same constraint as `presence: true`. On the live OP
corpus that's 53 lifts wasted (29 length + 16 inclusion + 7
format + 1 confirmation).
This PR adds `Predicate::ValidationParam` (count 56 → 57) and
emits one triple per inner-hash key:
(User.name, validation_param, "length:maximum=255")
(User.age, validation_param, "numericality:greater_than=0")
(User.age, validation_param, "numericality:less_than=150")
Object format: `<kind>:<inner_key>=<inner_value>` — mirrors the
existing `validation_kind` shape so consumers can pattern-match.
`extract_hash_options(s)` parses the verbatim Ruby hash-literal
output from `walk::format_hash_inline`. Depth-tracks
`{}` / `[]` / `()` to handle nested values like
`inclusion: { in: [1,2,3] }`. Best-effort on malformed input
(never panics). Non-hash option values (`presence: true`,
`uniqueness: nil`) emit zero `validation_param` triples.
The downstream nexgen-rs consumer follow-up will compose
richer ASSERT clauses (`length:maximum=255` →
`string::len($value) <= 255`).
Tests: 2 new regressions —
`extract_hash_options_parses_inline_hash_literals` (single
pair, multi-pair, nested, empty, non-hash, depth-tracking),
`ar_shape_emits_validation_param_per_inner_hash_key`
(parametric kind emits per-key params; non-hash kind emits
zero; `validation_kind` still fires for both — backward
compat). `predicate_count_locked_at_57` (renamed from `_56`).
`ar_shape_emits_every_ar_predicate` extended with
`"validation_param"` via the fixture's new
`length: { maximum: 255 }` validation. 65 tests pass.
The producer was emitting one Model per RubyClass entry from parse_models, but Ruby's class-reopen idiom can have N declaration sites for the same class across files (e.g. OpenProject's app/models/work_package.rb defines the rich WorkPackage; the sub-files under app/models/work_package/ — like inexistent_work_package.rb, pdf_export/work_package_to_pdf.rb, progress_hint.rb — reopen `class WorkPackage` to add nested classes, producing empty reopener entries with 0 declarations). Two Models with the same name then collided downstream: - naive .find(|c| c.name == "WorkPackage") landed on the empty reopener (graph index 680) instead of the rich one (index 682) - consumers saw WorkPackage with 0 associations / 0 mixins on the live OP corpus — body was there, just not in the slot anyone reached Fix: merge same-named entries at extract time. Refactor the extract loop into build_models, which deduplicates by name using a HashMap and merges duplicates in-place: every Vec field concatenates in encounter order; sti is first-non-None-wins so empty reopens cannot strip inheritance. First-occurrence keeps its slot, so existing positional consumers stay stable. Verified end-to-end against /home/user/openproject: before: 2x WorkPackage (one with 0/0, one with 18/19) after: 1x WorkPackage (18 associations, 19 concerns) 3 new unit tests pinning the merge contract: - build_models_merges_same_named_reopens_into_one - build_models_preserves_first_occurrence_slot_and_source_order - build_models_sti_first_non_none_wins ruff_ruby_spo 25/25 green; clippy clean.
…e API The hardcoded NAMESPACE = 'openproject' tagged every harvest's IRI prefix as 'openproject:' regardless of source. A Redmine harvest, an OSB (Open-Source-Billing) harvest, or a Spree harvest all came out prefixed 'openproject:', forcing downstream consumers to either rename in a second pass or be wrong about the curator. Add extract_with(source_tree, namespace) — same parser, caller supplies the namespace string the resulting ModelGraph is tagged with. The existing extract(source_tree) is preserved as a thin wrapper that defaults to NAMESPACE for back-compat: every existing caller (ogar-from-rails::extract is the only one in-tree) keeps working unchanged. Two new tests: - extract_defaults_to_openproject_namespace pins the back-compat default. - extract_with_tags_caller_supplied_namespace pins the new API for 'redmine', 'osb', etc. Unblocks the parallel session's OSB harvest from being mis-tagged 'openproject:' and lets ogar-from-rails distinguish Redmine vs OpenProject curators downstream. ruff_ruby_spo 27 green (+2); clippy clean.
extract/extract_with walk only <root>/app/models, so a modular Rails app hides much of its domain. OpenProject keeps ~half its models in modules/*/app/models (e.g. TimeEntry in modules/costs) — invisible to the core-only walk. Add extract_app / extract_app_with: collect the core app/models PLUS every mounted engine's app/models (modules/*, engines/*) and feed all files into one build_models pass, so a class reopened ACROSS roots still merges into a single Model (the cross-root reopen-merge). - parse::parse_models_with_engines + collect_model_roots (bounded by convention to modules/* and engines/*, so vendored gems / spec fixtures are never mistaken for model roots). - extract / extract_with stay core-only (backward compatible); engines are opt-in via the new entry points. Tests: - extract_app_walks_engines_and_merges_across_roots: synthetic core+engine tree; core-only extract misses the engine model, extract_app harvests it, and a class reopened in both roots merges to one Model (assocs unioned). - extract_app_harvests_openproject_engines (env-gated, OPENPROJECT_PATH): on the real corpus, core=681 -> core+engines=922, and TimeEntry (modules/costs/app/models) is harvested. 29 unit tests pass.
The C# sibling of ruff_cpp_spo / ruff_ruby_spo / the Python-Odoo frontend. Roslyn (Microsoft.CodeAnalysis) is the professional C# AST+semantic tool, the analog of libclang (C++) and ruff_python (Python). Because Roslyn is .NET-only the parse step is an out-of-process tool under harvester/ (vs ruff_cpp_spo's in-Rust libclang walk); the seam is the ndjson Triple contract. - harvester/ (.NET): CSharpSyntaxTree walk emitting Triple ndjson with the closed Predicate vocab (rdf:type / inherits_from = is_a; has_field / has_function = part_of; field_type; is_static). - src/lib.rs (Rust): load() over from_ndjson + unknown_predicates() validating every predicate against the closed vocab so harvester drift is a hard error. - README: the per-language tool table, the predicate mapping, and the SemanticModel upgrade path (MSBuildWorkspace + EF/Mongo attribute binding). NOTE: the Rust crate is written but NOT build-verified in this environment — the sandbox proxy blocks ruff's astral-sh/lsp-types git dep (403/offline), so cargo can't resolve the workspace. CI / a networked build is the verification gate. The .NET tool needs a dotnet 8 SDK (absent here) to build. Generated by [Claude Code](https://claude.com/claude-code)
…he duplication-vs-conflation law The deterministic (part_of:is_a) rank-minter (`mint` / `mint_with_classid`) plus the `medcare_probe` example that runs a REAL Roslyn harvest of the MedCare C# corpus through it and measures the **256-cap-is-a-lint law**: every part_of:is_a sibling set over 255 is either **duplication** (what you get by NOT resolving through a ClassView) or a **separation-of-concerns** failure (data `has_field` ⊥ behaviour `has_function`) — never a reason to widen the field. That is the Redmine-renderer-converging-into-askama-after-deduplication direction. Probe sections [A]-[G]: scale, injectivity, truncation, prefix-routability, documentSymbol, is_a redundancy, and [G] the 256-cap classification — 0 counterexamples on the real corpus. Deliberately EXCLUDES the radix-tried "solution" (the `RadixCodebook`, the base-255 `mint_factored` cascade, and the ontological-radix-trie doc): the right resolution is ClassView dedup, not a trie or a cascade. Test: `cargo test -p ruff_spo_address` -> 6 passed. Verified on a branch with the `lsp-types` git dep vendored (this sandbox's proxy blocks the unrelated astral-sh/lsp-types git dep, so a clean branch can't resolve the full workspace here; it resolves in CI). The crate is a leaf (deps: ruff_spo_triplet; dev-dep: ruff_csharp_spo for the harvest probe). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RhpwkHGgia2TuDFvdnuQdE
Both bots reviewed 1abdbe5, before the six-set gate landed, so some findings were already fixed and some were not. Each was verified at source before changing anything; the gate itself found none of these because its fixture lacked the shapes. 1. `EventKind::Param` had NO EMITTER ANYWHERE (Codex P2). The variant was public, documented "a method parameter, in declaration order", and the census measured exactly ZERO across 5502 real methods — an alphabet variant that was pure fiction. Cause: `defs_of` kept only `(name, body)` and discarded the argument list. It now keeps the whole `Def` node and `Walk::params` emits every parameter kind (required / optional / rest / keyword / keyword_optional / keyword_rest / block) in declaration order, with defaults walked as ordinary expressions. Census: 0 -> 3708 Param events. 2. Body-less defs were dropped (CodeRabbit + Codex). `def noop; end` produced no `MethodOre` while the shipped extractor makes a `Function` for it, so the two arms covered different method sets — the one thing the side-by-side must never do. 3. The `For` arm double-counted and mis-filed its iteratee (both bots). It tagged the relation `iteratee` (so the fold filed it under `reads`) and then walked the iteratee again, sending `self.items` through `self_send` for a second untagged Read. The shipped arm files it under `traverses` and never walks it. Now tagged `traversal`, not walked. This is the same defect class as the `general_send` double count fixed in beb9c01 — a second instance the earlier fix did not reach. 4. Raise arguments inflated `reads` (Codex P2). `raise Error, self.message` really does read `message`, but the shipped arm records the exception and returns. The observation is kept — that is the point of this arm — inside a new `ScopeKind::RaiseArgs`, and the fold skips that subtree. A scope, not a `control` tag, because raise arguments nest and a flat tag marks only the outermost event. 5. The non-mutator self-send fallback inflated `calls` (Codex P1). `self.valid?` emitted an untagged `Call` where the shipped arm records nothing at all. Now tagged `self_send` and excluded from the fold. This NARROWS the one named divergence: `self.foo(arg)` now disagrees only in `reads`, not in `calls` as well, and the test asserts the other five sets agree so the disagreement cannot silently widen. 6. The census denominator omitted `set_guarded` (Codex P2). It summed five of the six shipped sets, so the preservation ratio was overstated on any corpus with a blank-guarded default — while `MethodOre` carried the sixth count all along. Also: name-keyed shipped-row lookup became an occurrence-ordered queue, so a class defining the same method name twice no longer hands every `MethodOre` the last definition's counts (Codex P2); and `ore_census` now REQUIRES `RAILS_SRC` instead of defaulting to a machine-specific absolute path, which produced a silent all-zero census — measured once in this session and briefly mistaken for a real result (CodeRabbit). MEASURED, same 1708 OpenProject files: events 40034 -> 44170 (Param 0 -> 3708; RaiseArgs scopes added) scopes 11987 -> 12172 symbols 3526 -> 4352 shipped entries 850 -> 857 (set_guarded contributes 7 corpus-wide) preservation 5.25x -> 5.21x The headline has now moved twice, both times DOWN and both times because a number was wrong: 5.40 -> 5.25 (traversal receiver counted twice) -> 5.21 (denominator missing a set). Worth noting `set_guarded` is only 7 entries across the entire corpus, so the fold's guarded path is barely exercised by real data — which is exactly why the gate fixture constructs it synthetically. The gate fixture now carries every shape above: a relation `for` loop, a raise with a field-reading argument, a non-mutator self-send, and parameters — each with an anti-vacuity assertion that the shape is actually present, so none of these can silently re-open. NOT changed, with reasons: - Codex P1 "correlate bodies instead of transcribing them": answered on the PR. The recipe codebook needs ordered body facts to correlate FROM; this arm is that input, not a competing consumer contract. - Codex P2 "nested declarations share the outer anchor": real, but fixing it needs `walk_class_body` to return per-declaration locations — a change to a shipped API that deserves its own PR rather than a drive-by. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JRoXmYPrE8KubXVrDB71L
…ivers The suppression added in beb9c01 was over-broad, and CodeRabbit caught it. The shipped walker walks EVERY receiver of a traversed send. The two forms then diverge, because the receiver node is different: lines.each recv is `lines` -> Send{recv: None} -> shipped's general Send arm records NOTHING self.lines.each recv is `self.lines` -> Send{recv: Self_} -> shipped's self-send arm records reads["lines"] So suppressing the ore's own receiver re-walk is correct for the bare form (it was emitting a second untagged Read of a name the set arm files only under `traverses`) and WRONG for the explicit-self form, where it deleted a read the shipped arm genuinely has. Now conditioned on the receiver actually being a bare send. Every traversal fixture in the crate used the bare form, so nothing caught it — the third time in this arc that a fix was right about the case in the fixture and wrong about the case that was not. The new test carries both forms side by side and asserts they are DIFFERENT shipped facts (bare: reads 0 / traverses 1; explicit: reads 1 / traverses 1), so neither can be silently collapsed into the other again. Disable-verified: restoring the blanket `recv_consumed = true` fails the new test with reads folding 0 where the shipped arm has 1. Also adds a Delegation section to AGENTS.md: grindwork (call-site sweeps, existence checks, scoped command runs, corpus censuses) goes to Sonnet subagents; judgment (is this finding right, what does this measurement mean, is this divergence defensible) stays on the main thread. Operator-directed, and general across repositories rather than specific to this one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JRoXmYPrE8KubXVrDB71L
Four, each from a measured incident in this session rather than from general advice: 1. Commit before disabling. The disable cycle restores with `git checkout <file>`, which reverts to the last COMMIT — so uncommitted work under test is deleted by its own verification step. A fix and its new test were both lost this way; the loss surfaced only when a later grep showed the old code back in place. 2. A disable that does not apply looks exactly like a guard that is not load-bearing. Twice a patch script anchored on text `cargo fmt` had reformatted, no-opped silently, and the green run nearly entered the record as evidence of inertness. Assert the replacement count. 3. `git status` says clean, not current. A stale branch manufactures findings that do not exist on main — with the fetch/merge-base commands to check, and the rule that a measurement must be re-taken after a rebase rather than carried across it. 4. A green fixture proves only the shape it contains. Three fixes in ruff_ruby_spo were right about the case in the fixture and wrong about the case that was not; when a fix distinguishes two shapes, the test carries both and asserts they differ. Operator-directed, and general rather than crate-specific. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JRoXmYPrE8KubXVrDB71L
The delegation table added in 07d5c34 used a compact pipe table; mdformat pads every cell to the widest in its column, so the prek CI job failed with "Files were modified by following hooks". Replaced with bold-lead prose, which matches the surrounding style and mdformat leaves alone. Recorded as a trap in the same section: prek is unrunnable in this sandbox (uv cannot parse the repo's exclude-newer), so a Markdown edit here ships unchecked, and a green prek on the previous commit is not evidence about this one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JRoXmYPrE8KubXVrDB71L
CodeRabbit flagged docstring coverage at 79.69%. Documented the five helpers where the invariant is not evident from the signature: an empty scope stack means the method scope rather than an error; parent() is what lets the fold attribute a Write to the branch guarding it; attr() consults the class's own declarations, so the same identifier is a relation in one class and an attribute in another, which is why it cannot be free. Left the two Display impls and the two test helpers undocumented — a doc comment there would narrate the signature, which AGENTS.md rules out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JRoXmYPrE8KubXVrDB71L
…d to end The C-library arm could name a function but not its signature: CppFunction carried namespace/name/calls only, while the class plane's CppMethod already carried return_type/param_types/is_static. So a downstream MethodSig manifest built from a C corpus would have held an empty parameter list and ret: None for every entry — a signature plane with no signatures. This captures the three missing facts in collect_functions, using the SAME idiom the class walker already uses (void filtered out, types verbatim from the cursor) so the two planes agree and one manifest can read both. No new predicate is needed: returns_type / has_param_type / is_static already exist in the closed vocabulary as the AST-DLL signature shape, so the ontology gate is untouched. File-scope `static` is read from the storage class, not is_static_method(): in C it means internal linkage, and is_static_method() is false for every free function, so reading the member flag would have made the field constant-false. The paired test asserts both arms so the flag has to carry information. Also adds examples/harvest_openggs.rs — the arm wired from libclang to rendered Rust on a real C corpus (OpenGGS, 55 TUs of plain C: 25 file-scope structs, 181 free functions, no classes, so the class plane harvests nothing from it). A translation unit is the Model, because that is what the C programmer used for grouping and for internal linkage. Measured: 55 TUs, 0 parse failures, 181 functions, 763 triples, 527 on the signature plane. The round-trip oracle gates the run: decompile(project(g)) must equal expand(g) restricted to the signature plane, computed by two different code paths, and a mismatch writes nothing. It earned its place on the first run by catching a real bug — the graph was built with ModelGraph::default(), leaving the namespace empty, so every manifest IRI was keyed differently from the expanded triples (527 lost, 527 invented). A self-golden check would have passed. What this generates is signature manifests, not bodies. That is the 85/15 split: the mechanical structure that classifies and orders a transcode is minted; the function bodies remain the essential-15% hand-port. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HScwwezRdMxFfTs3WLG19d
CodeRabbit finding, verified real: walk_free_functions parsed a TU and returned Ok without inspecting tu.get_diagnostics(), so an unresolved #include produced an incomplete function model with no Err and no marker. libclang recovers from a missing header by DROPPING the declarations that needed it; the functions are simply absent. The sharp half of the finding is what it says about the oracle. The round-trip check compares decompile(project(g)) against expand(g) — both derived from the SAME graph — so when a parse silently drops functions, both sides are equally short and the oracle passes. It proves the projection faithful; it never proved the harvest complete, and the PR description let "computed by two different code paths" imply more than that. The class plane already had this guard and its doc already described this exact failure mode, with the worked Tesseract STATS/scrollview.h case and the warning that "0 failed" does not mean the sweep is complete. It sits thirty lines above the function used here. The mechanism existed; it was not reached for. - walk_free_functions_with_diagnostics mirrors walk_tu_with_diagnostics: one parse, functions plus every diagnostic at Severity::Error or above. walk_free_functions becomes the thin wrapper, matching how walk_tu wraps its own diagnostics-aware form. - harvest_openggs skips any TU with an error diagnostic and reports the count separately from parse failures, rather than minting a manifest from a partial parse. Measured on the corpus before and after: 55 TUs, 0 failed to parse, 0 skipped on error diagnostics, 181 functions, 763 triples, 527 on the signature plane. So the defect was latent here — the numbers already published are unaffected — and this closes the hole rather than fixing a wrong result. Two tests, because a guard that has never been shown to fire carries no information and one that fires on everything carries none either: an unresolvable include must surface a "file not found" diagnostic while the parse still returns Ok, and a clean TU must report none while still harvesting its functions. Also documents main's two failure postures, which the docstring-coverage check wanted: a missing corpus is a skip, a failed oracle is a hard error that writes nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HScwwezRdMxFfTs3WLG19d
Second codex finding, verified real. The example mapped CppFunction::is_static (C file-scope linkage) onto CppMethod::is_static, but that predicate means "a class-level member with no implicit this". A free function is not a class member at all, so neither value is a true claim: true asserts class membership, false asserts an implicit receiver these functions do not have. The result was that exported C functions rendered as though instance-bound while only TU-private ones rendered static, and a consumer had no way to recover linkage without reinterpreting an established predicate. The justification written into the doc comment does not survive contact with the finding, and is corrected rather than quietly dropped: if is_static really meant "no implicit receiver", EVERY free function would carry it, not only the translation-unit-private ones. The expander emits is_static only when the field is true, so leaving it at the default makes NO claim — which is the honest encoding for a function the predicate does not describe. Linkage stays on CppFunction for a consumer reading the harvest directly; representing it in the shared IR needs its own predicate, and that is a deliberate ontology change, not something to obtain by overloading an existing one. Measured: the rendered manifest is BYTE-IDENTICAL before and after, and the corpus explains why — OpenGGS contains zero file-scope static functions, so the flag was constant-false across all 181 and the overload never fired here. Latent, like the diagnostics gap: this is a correctness fix for the next C corpus, where leptonica and zlib are full of them, not a repair of a wrong result. Harvest unchanged at 55 TUs, 0 failed, 0 skipped, 181 functions, 763 triples, 527 on the signature plane. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HScwwezRdMxFfTs3WLG19d
corpus's parity blocker Two CodeRabbit findings on 5fb4785, both verified. Overload loss (Major). The per-model dedup keyed on f.name alone. This corpus is compiled as C++, so f(int) and f(double) are two distinct functions a name-only key collapses into one, dropping the second before it reaches the graph. The oracle is blind to it for the same reason it was blind to the diagnostics gap: it compares a projection of the graph against an expansion of the SAME graph, and both are equally short. The key is now (name, ordered parameter types), matching the identity the method IRI already uses. Measured: 181 functions before, 181 after, and a scan of the rendered manifest finds zero duplicate names within any translation unit. So OpenGGS has no overloaded free functions and the key never collided here — latent, like the previous two, and a correctness fix for the next C++-compiled corpus rather than a repair of a wrong count. Parity marker (Minor; codex, CodeRabbit and the PR description all flagged it). render hardcoded "PARITY: UNRUN (operator-blocked: leptonica)", so every OpenGGS artifact asserted an operator block from an unrelated corpus. An artifact stating something false about its own verification is worse than one that says nothing, because a reader who trusts the marker is misled about what was checked. Fixed additively so no existing caller changes behaviour: render_with_parity(manifests, parity) takes the status as a caller fact — which corpus is blocked, and on what, is known where the harvest runs, not in the renderer — and render delegates with the Tesseract arm's existing leptonica string. harvest_openggs now states its own: "UNRUN (no execution oracle: signatures only, bodies are hand-port)", which is the true status of this arm. Harvest unchanged: 55 TUs, 0 failed, 0 skipped, 181 functions, 763 triples, 527 on the signature plane, oracle green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HScwwezRdMxFfTs3WLG19d
…ng an unchecked round-trip conformance Two findings from the review on b13168b, both verified in-tree before fixing. The calls overload is the unguarded one. The is_static comment closes on the right principle -- a fact needing its own predicate must not be smuggled in by overloading an existing one -- and says nothing about `calls`, which is the other deliberately-absent field and the only one where that overload is a one-token edit. Verified, all four halves of the blindness claim: - CppFunction::calls is "EVERY CallExpr callee" as a bare name (ruff_cpp_spo/src/lib.rs:148-151); CppMethod::calls is the closed ActiveRecord mutator set as "<receiver>.<method>" and means "this method calls a writer" (ruff_spo_triplet/src/ir.rs:321-328). Same name, same Vec<String>, and the construction site uses ..CppMethod::default(). - The oracle cannot see it: `calls` is absent from is_signature_plane (ruff_cpp_codegen/src/manifest.rs:178-189). - A truth-leg check could not either: both Predicate::Calls emission sites stamp Provenance::Inferred (expand.rs:305 and :941), so f/c are identical whichever field feeds them. So the edit would mint every-callee facts as lifecycle-mutator claims with a green build, green clippy and a green oracle. The only thing standing in the way was a doc comment on the other struct, and AGENTS.md says a doc-comment claim is not a behaviour. It is now an assert on the built graph. One correction to the review's framing, checked rather than accepted: the Predicate::Calls site at expand.rs:305 is the AR-shape Function arm, not the CppFunction arm -- its reads are model-qualified and it carries traverses and guarded_writes. So the conclusion stands (the free-function call graph reaches no triple) but for the stated reason, not because no arm emits Calls at all. The round-trip conformance was an unchecked cross-repo claim. Three doc comments named codegen_spine::roundtrip_eq as the pattern this crate implements. That symbol appears nowhere in this repo outside those comments; this crate's only dependency is ruff_spo_triplet and its own description says it never compiles against lance-graph, so the function cannot be called and nothing fails if the two drift. One divergence is already real: roundtrip_eq's truth leg is documented as always run, while the comparison here keys on (s, p, o) and drops f/c -- sound on this plane only because expand and decompile assign identical provenance to all seven signature predicates, which is a property of the current expander rather than a guarantee. The three sites now say "mirrors" rather than "is", and the module doc records that the correspondence is asserted, not checked, and partial. Same class of fix as the PARITY marker in the previous commit: an artifact should not state something false about its own verification. Out of scope and deliberately not done: the missing dispatches_to predicate for the free-function call graph. That is an ontology change, which by this arm's own principle is a separate deliberate act. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HScwwezRdMxFfTs3WLG19d
Both self-inflicted, and the first is a small lesson worth recording. The assert message was written as a backslash-continued string literal. cargo fmt collapsed it onto one line and kept the continuation's leading whitespace as literal spaces, so the committed message read "mapped onto " with ten embedded spaces across 116 columns. The point of that message is to tell a developer which of two same-named fields they just conflated, and a mangled message is a poor way to say it. It is now one short single-line string. The second is a sentence added to the lib.rs gate doc that ran onto the existing following line, making it 114 columns. Split into its own paragraph. The 202-column line remaining in harvest_openggs.rs is pre-existing (it was line 180 before this branch's previous commit) and is left alone rather than widening this change. Re-verified after the edits: ruff_cpp_spo 74 + 6 passing, ruff_cpp_codegen 10 passing, clippy clean on both with --all-targets, cargo fmt --check clean, and the harvest unchanged at 55 TUs / 0 failed / 0 skipped / 181 functions / 763 triples / 527 on the signature plane with the oracle green. The guard was disable-verified before this fix: adding calls: f.calls.clone() to the CppMethod literal -- the exact one-token overload it exists to catch -- panics the harvest, and removing it returns the run to green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HScwwezRdMxFfTs3WLG19d
CodeRabbit finding on f9bd0be, verified before fixing. CppFunction::name is bare and the enclosing scope is captured separately in CppFunction::namespace, which the walker does populate (clang_walker.rs:324 via enclosing_scopes, and per its own doc that covers an owning class scope too, so Widget::helper yields ["Widget"]). The dedup key used only (name, param_types), so a::f(int) and b::f(int) collapsed to one entry and the second was dropped before it reached the graph. The finding's second half is the sharper one and also checks out: the method IRI is "{model_iri}.{name}({params})" with no scope component (expand.rs:784-792), so even had both survived the dedup they would have collided there. Fixing the key alone would have moved the silent loss rather than closed it. This mirrors an identity the class plane already established rather than inventing one: CppClass::qualified_name is used as the dedup key in walk_tu (lib.rs:364) and as the Model name in model_from_class (lib.rs:435). CppFunction::qualified_name is the free-function counterpart, with the same shape and the same global-scope-stays-bare rule, and it now feeds both the dedup key and the name that reaches the IR. Putting the qualified name in CppMethod::name is deliberate and is not the overload this arm keeps rejecting: a scope-qualified name is still a name, it is how C++ writes the identity, and the alternative -- a namespace field on the shared CppMethod -- is an ontology change. The renderer is unaffected because it emits the name as an escaped string literal, never as a Rust identifier. The regression test asserts both arms, since one alone passes for the wrong reason: a global-scope function must keep its bare name (the C-library norm, where qualifying would change every identity), and a namespaced pair must come out distinct. It also carries the anti-vacuity arm -- the fixture is asserted to still collide on the bare-name key being replaced, so the qualification is load-bearing rather than cosmetic. Measured: 75 + 6 passing (one new), clippy clean, fmt clean, and the harvest is BYTE-IDENTICAL before and after on both artifacts -- the rendered manifest and the NDJSON triples. OpenGGS is plain C with no namespaces, so all 181 functions have an empty scope and the qualified name equals the bare one. Latent here, like the diagnostics gap, the is_static overload and the overload dedup before it; this is a correctness fix for the next namespaced corpus, not a repair of a wrong count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HScwwezRdMxFfTs3WLG19d
Workspace-wide sweep to 1.98.1 (rust-toolchain, rust-version, Dockerfile base images, CI MSRV pins). Docker tags rust:1.98.1, -bookworm and -slim-bookworm verified to exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019HnekoM1EidTwQLS3oFVFm
An Odoo extension view patches its parent with locators (an <xpath expr position> element, or any element carrying `position`, e.g. <field name="amount" position="after">). The harvester docked every control in an extension view at the fallback token "root", and the shorthand form was worse: the locator itself was emitted as a control, and the controls it added were skipped as comodel fields because they sat under a <field> element. Now a two-pass join: - views are collected per record with their inherit_id (joined on the local part of the xml id, the odoo_nav convention); - each view is walked after its parent; it records anchors (fields, buttons, containers, named elements) with the region each sits in; - a locator resolves its target against the parent chain's anchors: after/before/replace -> the target's region, inside a container -> that container, inside a field -> comodel subview (not docked), attributes -> no controls. A container opened inside the locator block still wins for its contents. A locator is never a control. - only bare-tag or @name-filtered xpath steps resolve; [hasclass(..)], [@string=..], positional or * steps stay unresolved (-> "root") rather than matching the first element of that tag. Also fixes stack balance: a self-closing non-container element (<separator/>, <filter .../>) pushed a frame no close ever popped, so the arch wrapper's </field> popped it instead of ending the arch. Measured on Odoo 19 addons/ (old = origin/main, same harness): facts 17,154 -> 17,298; screens 3,171 -> 3,092; docked at root 3,513 (20.5%) -> 2,170 (12.5%), -38%. Remaining root: non-@name xpaths and parents outside the scanned tree. Tests: 7 new on an invented fixture (each position, both locator forms, inside-a-field comodel, unresolvable predicate, missing parent, extension of an extension, stack balance, xpath step parsing). ruff_python_spo 91/91; clippy -D warnings clean on 1.98.1. Disable runs: no join -> 3 red; self-closing frame push -> 1 red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NSfzujtWL1BnHgnJGAhg9N
…edicates Two review findings on the inherit_id join: - Parents were indexed by the local part of their xml id only, so an extension in module_b inheriting module_b.view_form could bind to the first-scanned module_a.view_form. Views now carry a module-qualified id (module = first path component under the scanned root; an unqualified ref is qualified with the declaring module, as Odoo does). Lookup is exact on the qualified id, falling back to the local part only when exactly one scanned view carries it; an ambiguous local id stays unresolved and docks at root. - name_in_predicate took the first `@name` anywhere in the predicate, so `[@name='x'][2]`, `[not(@name='x')]`, `and`/`or` compounds resolved as `field x`. Only a predicate that is exactly `[@name=<quoted>]` resolves now. Tests: new inherit_id_resolves_against_the_qualified_parent (two modules, same local id, different containers; plus the ambiguous fallback), falsified by disabling the qualified lookup (fails). Four new None assertions on xpath_target. ruff_python_spo: 92 passed; clippy -D warnings clean. Odoo 19 addons/: root-docked facts 2170 -> 1648 of 17297 (12.5% -> 9.5%). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NSfzujtWL1BnHgnJGAhg9N
Four further review findings, each matching how Odoo applies an inheriting view: - A missing `position` means `inside`: on any <xpath>, and on a named <field> at the top level of an extension's arch (inside an optional <data> wrapper). Such a top-level field is a spec, not a control. - Specs apply in order: a locator resolves against the parent chain's anchors, then against what earlier specs in the SAME view added. - A positioned shorthand spec is matched by Odoo on all its attributes; only `name` is tracked, so a shorthand carrying anything else (`<label for="x" position="after">`) is left unresolved instead of binding to the first element of that tag. - `<field name="inherit_id">` is the parent reference only when it has a `ref`; without one (a form for ir.ui.view) it stays an ordinary control. Test spec_defaults_order_and_identity covers all four; disabling each fix separately makes it fail. ruff_python_spo: 93 passed; clippy -D warnings clean. Module docs updated (also the qualified-id join). Odoo 19 addons/: facts 17297 -> 17252 (top-level spec fields no longer counted as controls), root-docked 1648 -> 1609 (9.3%). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NSfzujtWL1BnHgnJGAhg9N
Two parallel fork PRs added this example and the fork merge commit reconciled them; replaying the commits linearly kept one side. This puts back the file as it was on fork main (9407c74). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HJA9obVWqSQYNtXGnMGLd
Upstream replaced ExprCompare's `left` and `comparators` fields with a single `operands` slice (the first operand is the old `left`). Walking every operand in order visits the same expressions as before, and `operands.len()` equals the old `1 + comparators.len()`, so the observation text is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HJA9obVWqSQYNtXGnMGLd
…-bans check - ruff_python_spo: `x is None` detection now reads the comparison through `ExprCompare::as_single`, which upstream added when it replaced the `left` and `comparators` fields with `operands`. It still matches exactly one `is` operator with a `None` right-hand side. - ruff_python_dto_check: `sha2` is requested with `default-features = false`. Upstream's scripts/check-cargo-bans.py rejects external dependencies that keep default features; only `Sha256` and `Digest` are used, which need none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HJA9obVWqSQYNtXGnMGLd
Operator ruling 2026-10-14 replaces the earlier "never Haiku" line: Haiku runs pre-written scoped commands, lists candidates, counts and quotes; the main thread verifies every Haiku result it relies on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HJA9obVWqSQYNtXGnMGLd
The replay kept upstream's lockfile at each step; this adds the entries the fork crates need. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HJA9obVWqSQYNtXGnMGLd
Seven fork crates pinned `rust-version = "1.98.1"`, the fork's old workspace minimum. Upstream's workspace minimum is 1.97, and CI's `cargo build (msrv)` job builds the whole workspace with that toolchain, so the pin made cargo refuse to build them. They now take the value from `[workspace.package]`. `ruff_r2il` keeps its own pin: it is excluded from the workspace. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HJA9obVWqSQYNtXGnMGLd
The fork builds with the 1.99.0 toolchain (rust-toolchain.toml, taken from upstream), so the minimum supported Rust version now matches it: `workspace.package.rust-version` goes from upstream's 1.97 to 1.99, and the fork crates inherit it. `ruff_r2il` sits outside the workspace and pins the same value by hand. CI's `cargo build (msrv)` job reads this field, so it now builds with 1.99. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HJA9obVWqSQYNtXGnMGLd
e35ee7b to
ae25f1c
Compare
Upstream's `.cargo/deny.toml` now lists every dependency allowed to run a build script, and `cargo deny check bans` rejects the rest. Two fork crates bring one each: `clang-sys` (via `clang`, used by `ruff_cpp_spo`'s libclang walker) and `lib-ruby-parser` (used by `ruff_ruby_spo`). Both are added to the allowlist; scripts/check-cargo-bans.py confirms each entry matches a dependency that has a build script. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017HJA9obVWqSQYNtXGnMGLd
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
mainnow matchesastral-sh/ruffat83a3afc3. This PR puts the fork's own work back on top of it, as a linear replay of the fork's history rather than a merge commit.Fork
mainbefore the sync (9407c748, 405 fork commits) is kept atbackup/main-2026-10-14-aandbackup/main-2026-10-14-b.What was replayed
branch-3.10astral-sh/ruff#26987, Update branch for jrnl astral-sh/ruff#27007) that upstream already has.How conflicts were resolved
Conflicts came only from fork commits that edit upstream-owned files.
Cargo.toml: upstream's crate versions, plus the fork crates in[workspace.dependencies].ruff_cpp_spoexclusion is kept, run through upstream'suv run --locked --only-devwrapper;AGENTS.md: the fork's sections, followed by upstream's.ruff_r2il/examples/slag_partition.rsdifferent from fork main, so a separate commit restores it. Every fork crate and.claude/file now matches fork main (9407c748).Where parallel fork branches overlapped, a few individual replayed commits may not build on their own. The final tree builds.
Commits added on top
ruff_python_dto_check,ruff_python_spo: upstream replacedExprCompare'sleftandcomparatorsfields with anoperandsslice. Both crates now read the new layout, and their output is unchanged.ruff_python_dto_check:sha2is requested withdefault-features = false. Upstream'sscripts/check-cargo-bans.pyrejects external dependencies that keep default features.Cargo.lock: re-resolved to add the fork crates.workspace.package.rust-versiongoes from upstream's 1.97 to 1.99, matching the 1.99.0 toolchain. The fork crates inherit it instead of pinning 1.98.1;ruff_r2il, which sits outside the workspace, pins 1.99 by hand.AGENTS.md: Haiku agents may now do scoped grindwork, and the main thread checks every result it relies on.Verification
Local runs on the 1.99.0 toolchain, all passing:
cargo clippy --workspace --all-targets --all-features -- -D warningscargo test --all-featuresfor the fork cratesscripts/check-cargo-bans.pycargo fmt --all --checkApart from the order of two dependency lines in
Cargo.toml, the final tree is identical to an ordinary merge of upstream into fork main that was also checked.🤖 Generated with Claude Code
https://claude.ai/code/session_017HJA9obVWqSQYNtXGnMGLd