Repository navigation
fix(core-engine): the api rule's own declarations are not new orphan surface (#2731) - #2734
Conversation
…surface (#2731) galaxyscope.py's Contextual Baseline Fix converts an imported file's orphans into API exposure without asking whether the language's `api` rule had already counted those same declarations, so a function that is both declared public and uncalled contributed twice. keyword-rosetta's `data/go/a.go` -- three exported functions, none called in-file, the file imported by main.go -- recorded raw_arch_api 3 + orphans 3 = arch_api 6: three functions, six units of public surface. The overlap is the common case rather than the exception. A library's public functions are exactly the ones with no in-repo caller, so `risk_api_exposure` and `risk_documentation` (both read the adjusted `api`) were inflated most for the codebases that are most library-shaped. detector.py now reports `api_declared_orphans`: how many of the file's orphans the api rule matched at their own declaration. The test is by NAME, not by span -- an orphan's name occurs exactly once in the file, which is what the census just proved, so a name appearing on a line the api rule matched can only be its own declaration. Span containment would be both looser (C's api rule matches every non-static local declaration, so any long body suppresses its own function) and tighter (JS/TS `export` sits outside the slicer's start_idx, php's span starts a line early, a java `@Test` line pulls start_line off the `public` one). The conversion then credits only the remainder; the debt wipe stays unconditional, since an already-public orphan is surface the rule counted already, not dead weight. Golden masters re-blessed (both modes). Every one of the 1254 full-precision / 1253 zero-dependency differences is attributable: 61 files lose api mass, and with them Structural Magnitude, API Exposure %, Documentation Exposure % and their directory/ecosystem roll-ups, plus the topological coordinates that re-solve corpus-wide on any mass change. No api count increases anywhere, and no other structural signal moves. On keyword-rosetta 20 languages go 6 -> 3 (three probe functions, three units of surface) and c goes 12 -> 9 / 10 -> 7; the corpus re-bless follows in keyword-rosetta. Fixes #2731. Part of #2669. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
CI: everything green except All 21 failing languages report only Per the no-pins flow the corpus re-blesses against merged
|
|
Labelled |
main moved to 074bbef while this branch was open; #2734 (api-rule orphan surface) rewrote both fixtures wholesale, so the merge conflicted on them. Resolved by taking main's fixtures and regenerating from scratch on the merged engine rather than hand-merging -- the two changes are orthogonal, and the drift against main is again exactly 28 differences, the same jcl-only set as before the merge (13 files' Commented-out Code counts, their derived Commented Logic Exposure percentages, and the repo-level average). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8F1cmMsujEjWnnw4d1e4W
…eam rules (#2732) (#2736) * feat(core-engine): jcl dead_code + jcl/yaml spec_exposure comment-stream rules (#2732) Fills the three comment-stream rule absences #2732 found: jcl had neither dead_code nor spec_exposure, yaml had no spec_exposure. Both jcl absences are the #2610 leftover -- jcl's comment stream was structurally empty until _strip_jcl_comments, and #2610 added the debt/ownership rules without revisiting these two. jcl/dead_code: commenting a statement out by turning `//` into `//*` is the universal JCL practice. The operand guard (a keyword must be followed by operand-shaped text) is load-bearing rather than decorative: cics-genapp's CICSTS56.jcl carries `//* SET THE RETURN CODE TO CONTROL...` and `//* EXECUTE DUMP UTILITY PROGRAM...` English banners a few lines from four genuinely commented-out `//* DD DSN=CSQ901...` statements. A bare `(?:EXEC|DD|JOB|SET|INCLUDE)\b` would have counted all of them. yaml/spec_exposure departs from the shape the issue proposed, and the departure is the substance of this change. #2732 asked for the generic bracket-tag rule verbatim, reasoning that "spec_exposure never sees the code stream, so YAML's [a, b] flow-sequence syntax cannot FP against it". That premise is wrong: coding_analysis applies every non-underscore rule to the code stream and comment_analysis then adds a second pass over the comments -- it supplements the code-stream pass, it does not replace it. Dropped in verbatim, the generic rule scores spec_exposure=1 on a workflow with no comments at all, off `needs: [audit, lint]` alone. YAML is the one language where a bracket holding bare unquoted words is ordinary syntax rather than a tag, so the absence was never as arbitrary as it looked. Both new spec_exposure rules are therefore anchored to their language's comment marker (`^[ \t]*#`, `^//\*`) -- the same anchoring yaml's own dead_code and jcl's own ownership rule already use -- which makes them structurally comment-only, since prism strips those markers out of the code stream. Second measured fix carried into both: the generic rule's bare `spec` branch has no trailing boundary, so it matches "specified" and "species". 2 of the 3 code-stream hits across the 41,815 pool .yml/.yaml files were exactly that. Added `\b` after the alternation. Differential Scan: 28 differences in both modes, all jcl, all attributable to jcl/dead_code -- 13 files in cics-banking-sample-application-cbsa gaining 16 occurrences (CICSTS56 4, the twelve CREL*/CREDB2L 1 each, matching an independent count over the 443 licensed .jcl/.prc files in the local pool), plus the derived Commented Logic Exposure percentages and the repo average. No coordinate churn, no other language moved, and yaml moved nothing at all -- the anchored rule finds no tag in the corpus, which is why the paired corpus plant below is required rather than optional. The regenerated fixtures also carry four machine-specific metadata lines per file (absolute path, remote URL, timestamp, duration); golden_diff.load_and_sanitize pops all four. Cross-repo: pairs with keyword-rosetta PR planting the jcl commented-out statement and the jcl/yaml [SPEC-n] tags, and retiring ledger entry comment-stream-rules-missing-jcl-yaml. Per docs/GATING.md's n/a semantics, filling a None rule ends the cell's incomparability whether or not the corpus has anything to match, so without that plant these three cells would flip from documented-n/a to a manufactured measured-0. THIS PR MERGES FIRST (rosetta CI checks out engine main); the corpus PR re-blesses against engine main after. Closes #2732 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8F1cmMsujEjWnnw4d1e4W * test: re-bless golden master after merging main (#2732) main moved to 074bbef while this branch was open; #2734 (api-rule orphan surface) rewrote both fixtures wholesale, so the merge conflicted on them. Resolved by taking main's fixtures and regenerating from scratch on the merged engine rather than hand-merging -- the two changes are orthogonal, and the drift against main is again exactly 28 differences, the same jcl-only set as before the merge (13 files' Commented-out Code counts, their derived Commented Logic Exposure percentages, and the repo-level average). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8F1cmMsujEjWnnw4d1e4W --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…2747) #2729's fix shape asks for the split between the api rule's own count and the Contextual Baseline Fix's converted orphans to be explicit in the engine/test surface, because `api` in the risk equations is not the `api` rule. The gate half landed corpus-side (keyword-rosetta reads `raw_arch_api` as `api` and pins the conversion separately as `api_orphan_credit`), and #2731/#2734 added the detector and galaxyscope tests for the conversion itself. This is the last piece: the recorder test was still documenting the pre-#2731 arithmetic as an engine invariant. `test_raw_columns_persist_pre_adjustment_values` asserted `arch_api == raw_arch_api + raw_state_slop_orphans` and called it "the Contextual Baseline Fix invariant". It never proved that -- both numbers are fed in as literals -- and since #2734 it is false in general: the conversion credits `orphans - api_declared_orphans`, so a file whose api rule already declared some of its orphans records an adjusted value BELOW that sum, and total overlap (every public function uncalled -- the library case, and every file in the keyword-rosetta corpus) makes it `raw_arch_api` exactly. A reader would take the assertion as the engine's contract, and it is the double count #2731 removed, written down as an expectation. Replaced with what the raw columns are actually for: the credit is recoverable as `adjusted - raw` without assuming what it equals -- the same subtraction keyword-rosetta's gate reads. Added `test_raw_columns_recover_a_partial_ conversion_credit` for the shape the sum-based assertion excluded by construction (4 declared + 3 orphans, 2 already public -> adjusted 5, credit 1), which fails against the old sum on purpose. Header comment now says which layer owns which number, so the next reader does not re-derive the conversion from the recorder's columns: galaxyscope.py owns the adjusted value, detector.py owns `api_declared_orphans`, and these tests own the round-trip only. No production code touched; no golden-master, tri-comparison or tree-sitter movement is possible from a test-only change. Full suite 7626 passed. Claude-Session: https://claude.ai/code/session_01WfeHRWra1d6Z5uSReFajSq Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ill a declaration (#2823, roadmap Phase 3) (#2828) `unreferenced_by_name` read 0.25 per keyword-rosetta file for haskell and scheme against a 2.50 corpus median -- 12 of each language's 13 probe functions reading as REFERENCED while nothing called any of them. That is #2806's defect with the sign flipped: there the census called a function unreferenced that the language reaches without naming it; here it called a function referenced on an occurrence that is a declaration, which corollary 2 of the stated contract says must not clear the flag. One outside occurrence each, and it is the export: module A (probeGlobals, probeTest, probeSafety) where -- outside the span probeGlobals :: Int -> Int -- INSIDE the span probeGlobals env = env -- INSIDE the span (export probe-globals) -- outside the span (define (probe-globals env) env) The Haskell type signature needed no rule of its own: haskell's slicer span STARTS at the signature, so that repetition is already inside the function's own definition. Measured -- declaring `^([a-z]\w*)[ \t]*::` as an export form moves the count not at all, which is what the issue found and could not explain. #2774's `_visibility_export` captures ONE name per match, which is the whole statement for `export -f foo` and its four siblings and cannot express a construct that names every exported function at once. Adds `_visibility_export_list`, the plural form: its capture groups are REGIONS holding exported names, and `_export_declaration_offsets` records the start offset of every name token inside a region. Two keys rather than one generalised key, deliberately -- the singular form records its group's own start offset verbatim, and assembly exports `.foo` while ruby exports `save!`, whose first and last characters a region tokenizer does not treat as part of a name; tokenizing those captures would put the offset one character off and silently stop discounting them. A language declares one key or the other, and a test is the gate on that. haskell declares the module header's parenthesised list (bounded by the `) where` anchor, since `[^)]*` stops at the first `Opt(..)`); scheme declares both of its clause forms, R7RS `(export a b c)` and Guile `#:export (a b c)`. Measured, both corpora: * keyword-rosetta: haskell and scheme 0.25 -> 3.00 per file, and NO other language moves -- structurally guaranteed, since the singular path is unchanged and only these two declare the new key. 3.00 rather than the 2.50 median because both carry the same plant gap #2806 found in abap/m4/makefile/objective-c, hidden until now behind the export false-negative: `main`'s `entry` dispatched one of its three probes. The corpus PR plants the other two and both land on 2.50. * language-crucible: haskell 1.4% -> 27.9% unreferenced (2 of 147 units -> 41). Every one of the 39 new hits is a pandoc utility whose name occurs exactly three times in its own file -- export list, type signature, equation -- with no caller there. That is corollary 5 working as designed: `Text.Pandoc.Shared` is a library module of 100 exported helpers, 36 of which nothing in that file calls, and saying so is the input galaxyscope.py's orphan -> api conversion needs. Real-world scheme does not move: neither crucible file uses an export clause outside a macro template. Golden masters re-blessed. NINE value differences, identical in both fixtures, every one of them haskell/pandoc: three files' Unreferenced By Name (Shared 0 -> 36, Filter 2 -> 4, Options 0 -> 1), their Tech Debt Exposure (the census is weighted 2.0 in slop_stress), the directory group's average 3.76 -> 17.51, and global avg_tech_debt 27.332 -> 27.386. No topological X/Y/Z coordinates moved at all -- unusual for a bless, and the reason is that no node's structural mass changed. One neighbour moved and it is a PRE-EXISTING defect this made visible, filed as #2827 rather than fixed here: with scheme finally producing orphans, its `api_orphan_credit` went 0 -> 3, the double count #2731/#2734 exists to prevent. `api_declared_orphans` tokenises the api-matched line with `\b\w+\b`, which cannot produce a token holding a hyphen, so `probe-globals` is never recognised in `(export probe-globals)`. haskell is the control that proves the diagnosis -- its probe names are all-\w, its api_declared_orphans reads 3, and its credit correctly stays 0. cobol carries the same double count on main today, and the crucible makes it much larger than the control corpus does: 4983 -> 5122 api_declared_orphans under a name-aware test, +139 across zig, lua and powershell, whose count is 0 for the ENTIRE language because a Verb-Noun name always has a hyphen. Docs: corollary 2 gains the plural form, the audit table's haskell/scheme row moves 0.25 -> 2.50, and a new section states the whole finding including why two keys and what the crucible says. how_to_add_a_language rule 19 gains the opposite-mistake bullet -- a census that reads too CLEAN is the same defect as one that reads too dirty. Closes #2823. Part of #2812. Cross-repo: keyword-rosetta PR (branch plant/2823-haskell-scheme-dispatch) plants the two dispatch calls and re-blesses scheme's credit cell against the #2827 ledger entry. This merges FIRST; that one goes green against gitgalaxy main once it lands. Co-authored-by: Joe Esquibel <squid-protocol@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #2731. Part of #2669.
The double count
galaxyscope.py's Contextual Baseline Fix converts an imported file's orphans into APIexposure. It never asked whether the language's own
apirule had already counted thosedeclarations, so a function that is both declared public and uncalled contributed twice.
keyword-rosetta
data/go/a.go— three exported functions, none called in-file, imported bymain.go:function_countraw_arch_apiraw_state_slop_orphansarch_api(what scoring reads)risk_api_exposurerisk_documentationThe fix
detector.pyreportsapi_declared_orphans— how many of the file's orphans theapirulematched at their own declaration — and the conversion credits only the remainder
(
orphans - api_declared_orphans, clamped). The debt wipe stays unconditional: the file isimported, so an already-public orphan is surface the rule counted already, not dead weight.
The test is by name, not by span. An orphan's name occurs exactly once in the whole file —
that is precisely what the orphan census just proved (
token_counts[name] <= 1) — so a nameappearing on a line the
apirule matched can only be its own declaration. No false positive ispossible. Span containment (
hit_vector["api"] > 0) is both looser and tighter than that:apirule matches any non-staticdeclaration-shaped line, includingbody-local
int counter = 0;, so every long function suppresses itself. Measured: the spanform suppressed all 73 orphans in
c/cpython/ceval.cand all 27 inframeobject.c(bothfiles of
statichelpers); the name form suppresses 6 and 0.exportprecedes
start_idx, php's span starts a line early, a java@Testline pullsstart_lineback off the
publicone. The span form scored javascript/typescript 0-of-3.New
api_declared_orphansis consumed by the orchestrator only — it is not a signal, does notreach the recorder schema, and appears in no output artifact (checked against the audit JSON,
LLM digest, SBOM and the
file_dataschema).Verification
Golden masters re-blessed, both modes. Every one of the 1254 full-precision / 1253
zero-dependency differences is attributed, with nothing left over:
Exposed API / Public Exportstotal_masscomposition/*/impact,health/avg_documentationapi_exposure/highest,undocumented_critical_path)61 files move on the real-world corpus, all downward, zero increases, and no structural
signal other than
apimoves at all. A control A/B (same venv,origin/mainvs this branch)measured total
arch_api35892 → 35127 (−2.13%).keyword-rosetta: 21 of 46 languages move — 20 go 6 → 3 (three probe functions, three units
of public surface),
cgoes 12 → 9 / 10 → 7,makefile8 → 7. The other 25 are unchanged(their
apirule marks export lists or non-function surfaces, which never overlapped theorphan set). The corpus re-bless follows as a keyword-rosetta PR against this merged engine,
per the no-pins flow.
pytest tests/— 7568 passed (3 new regression tests: the go double count, the Cstatic-vs-exported discrimination, and the orchestrator's credit/clamp arithmetic).
ruff_audit.py --ci,mypy_audit.py --ci,dead_key_audit.py --ci— all clean againstbaseline.
tree_sitter_accuracy_audit.py/tri_comparison_chart.pymeasure function/class/argextraction and never read
api(neither tool contains the string); this change cannot move them.Follow-up for the corpus side
#2729's gate pins
api_orphan_credit(arch_api - raw_arch_api) as its only assertion that themain → a → b → cimport chain resolved. That credit legitimately becomes 0 for every languagewhose probes are all declared public.
raw_state_slop_orphans 3alongsidestate_slop_orphans 0still proves the conversion ran, so the gate has a replacement available; it needs to be swapped
in the same PR that re-blesses the manifests.
🤖 Generated with Claude Code