Skip to content

arch_api conflates api-rule hits with converted orphans, and the rosetta gate reads the conflated column -- 36 of 44 languages pass api without the rule matching anything #2729

Closed

Description

@squid-protocol

Found by the #2669 F.4 census. This is the biggest of the three and it needs a decision before anything is changed.

What the gate is actually asserting

The Contextual Baseline Fix (galaxyscope.py L2228) rewrites api in place when a file is imported:

if popularity > 0 and orphans > 0:
    meta["equations"]["api"] = meta["equations"].get("api", 0) + orphans
    meta["equations"]["orphaned_logic"] = 0

raw_arch_api keeps the pre-fix value (#2536); arch_api carries the sum. keyword-rosetta's verify_language.py builds its column map from RecordKeeper.SHORT_KEY_MAP, which maps api → arch_api — the conflated column. Measured per file:

file manifest api rule finds orphans converted gate reads
shell/a.sh 3 0 3 3 ✓
go/a.go 3 0 3 3 ✓
python/a.py 6 3 3 6 ✓
c/a.c 12 9 3 12 ✓

Shell's api rule is ^export NAME and no probe exports anything. Go's is ^func [A-Z]… and every probe function is lowercase. Both measure zero and both pass, because three uncalled functions become three API surfaces. The manifests were recorded from the adjusted column, so the conversion is baked into the expectation.

Corpus-wide, raw_arch_api is 0 in 36 of the 44 languages that define an api rule. The median is therefore 0, and the bias report flags the ten languages whose rule does fire (c, python, embedded_python, fortran, lua, scala, livecode, css, makefile, html) as the deviants. The finding is inverted: those ten are the only ones measuring anything.

Why it matters beyond the corpus

_calc_api_exposure and _calc_documentation read the adjusted api, so on real code an uncalled-but-imported function is scored as public surface. That may well be the intent (it is what #2536 argued), but it means api in the risk equations is not the api rule, and nothing in the test surface distinguishes them. It also interacts with #2727: a language whose syntax repeats function names produces no orphans, hence no conversion, hence api == 0, hence _calc_api_exposure's if api_hits == 0: return 0.0 short-circuit — which is exactly why ada, haskell, sqlite, yaml and dockerfile sit at the bottom of risk_api_exposure and risk_documentation.

Fix shape (two halves, both needed)

  1. Engine/test surface: make the split explicit. RISK_INPUT_COLUMNS in keyword-rosetta's bias_report.py already maps api → raw_arch_api for exactly this reason; the gate should do the same, so api asserts what the rule found and the conversion is asserted separately (as arch_api - raw_arch_api, or not at all).
  2. Corpus: once the gate reads the raw column, ~36 languages go red on api and each needs a call — either the probe grows an exported construct in that language's own vocabulary (export in shell, a capitalised func in Go, pub in rust) or the language's api rule is too narrow. That is a corpus wave of its own, comparable in size to Batch A, and should not be folded into F.4.

F.4 bakes the current behaviour into deviation_ledger.json per GATING's upstream rule and cites this issue. Part of #2669; siblings #2727, #2728.

Activity

  1. added
    bugUnintended behavior or logic failure in the engine
    metricsHeuristics, risk exposures, and topological math updates
    core-engineModifications to the central physics and parsing engine
    on Sep 4, 2026
  2. squid-protocol commented on Sep 5, 2026

    @squid-protocol
    OwnerAuthor

    Both halves are done, corpus-side, in squid-protocol/keyword-rosetta#53. No engine change was needed: raw_arch_api already exists (#2536), so the fix was to make the gate read it and then give the corpus something for the rules to find.

    1. The gate. verify_language.py maps api to raw_arch_api (RAW_COLUMNS, the same column bias_report.py's RISK_INPUT_COLUMNS already used) and pins the conversion separately as api_orphan_credit = arch_api − raw_arch_api. The credit stays in the gate deliberately: it is the only assertion that the main→a→b→c chain resolved, which is how #2543 and #2540 were caught. All 46 manifests were then re-blessed mechanically from one full-precision scan — api := raw, api_orphan_credit := adjusted − raw — with no hand-edited numbers.

    2. The corpus wave, the one this issue said was Batch-A-sized. 31 languages planted, one language at a time, each verified through the real pipeline:

    shape languages api/file
    per-function visibility (pub, public, export, global, a capitalised Go name, a per-function export statement) 23 3
    module-level export list haskell 1
    file-level declaration, no per-function concept (EXPOSE, on:, @property, id=, %define, ENTRY, EQUALS) 7 1
    nothing the rule can see abap, ada, dart, matlab, objective-c, perl, sqlite 0

    api now fires in 37 of 46 languages, against 10 before. Result: verify_language.py 46/46 PASS, na_check.py --ci clean, bias_report.py --gate exit 0, 0 unexplained.

    What the wave refused to do. dart and objective-c can be made to fire — a typedef per probe, a property per probe — and both were measured doing so cleanly. Planting a declaration nobody would write, to make a number move, is the measurement this corpus exists to avoid. perl and sqlite have no rule-visible form that does not move a planted count (@EXPORT_OK adds state_mutation; CREATE VIEW is a func_start hit). All seven are ledgered and filed as #2730: the api rule has no stated contract — seven languages' own visibility idiom is invisible to it, while java/csharp/php/kotlin count any bare public token and go counts capitalised references.

    What the wave uncovered. With a real rule count and a conversion credit in the same file for the first time, data/go/a.go reads function_count 3, raw_arch_api 3, raw_state_slop_orphans 3 — and arch_api 6. A function that is both declared public and uncalled is counted twice, because the Contextual Baseline Fix never asks whether the rule already counted it. That is #2731, and it is the cause of the 21 bias cells that this issue's "beyond the corpus" section predicted would matter on real code: a library's public functions are exactly the ones with no in-repo caller.

    Also confirmed while planting, and ledgered as api-export-names-suppress-orphan-credit: in assembly, powershell, ruby and tcl the export statement names the function, so under #2727's name-recurrence orphan census the probes stop reading as orphans and the credit drops 3 → 0. makefile and m4 were re-planted (export VAR / AC_SUBST rather than .PHONY: probe_x / m4_provide([probe_x])) to keep theirs.

    Closing when keyword-rosetta#53 merges. Open siblings: #2730, #2727, #2731.

  3. squid-protocol commented on Sep 5, 2026

    @squid-protocol
    OwnerAuthor

    Both halves are now verified against current main, and the close criteria this issue's own status comment set are restored.

    The gate half (keyword-rosetta#53) was already done. The corpus half — the wave that gave the rules something to find — is done too, but it had drifted out of the state that comment measured: #2731 (shipped as #2734) and #2730 (shipped as #2743) both moved corpus-observed counts after it was written, so verify_language.py was failing 26 of 46 and bias_report.py --gate could not run clean. keyword-rosetta#57 re-blesses that — 100 cells across 27 languages, mechanically from one full-precision scan at engine 19262fb0, no hand-edited numbers:

    Result: verify_language.py 46/46 PASS, na_check.py --ci clean, bias_report.py --gate exit 0 with 0 unexplained. Ledgered cells fall 194 → 184 and derived 116 → 115 — fewer cells need excusing because fewer are out of band. The headline is raw_arch_api, the column this issue was filed about: abap and ada rise 0 → 3.25, c falls 7.25 → 3.75, and the inverted finding this issue described ("those ten are the only ones measuring anything") is measured gone.

    One residue found while closing this out, fixed in #2747: the recorder test was still asserting arch_api == raw_arch_api + raw_state_slop_orphans and calling it "the Contextual Baseline Fix invariant". It never proved that — both sides are literals in the fixture — and #2734 made it false in general. The only recorder-level statement of the contract was therefore the double count #2731 removed, written down as an expectation. Replaced with the recoverable-credit form (adjusted − raw, the same subtraction the corpus gate reads) plus a partial-credit test that fails against the old sum on purpose.

    Closes on keyword-rosetta#57 + #2747. Remaining siblings, both still open and both independent of this: #2727 (orphan census is name recurrence) and #2728 (slicer keyword buckets reach the orphan checks).

  4. squid-protocol commented on Sep 5, 2026

    @squid-protocol
    OwnerAuthor

    Closing. Both halves are merged and both close criteria hold on main, verified by a fresh scan rather than inherited from this issue's earlier status comment.

    The gate half — keyword-rosetta#53: verify_language.py reads raw_arch_api as api and pins the conversion separately as api_orphan_credit = arch_api − raw_arch_api. The corpus half — keyword-rosetta#53's wave, then keyword-rosetta#57, which re-blessed it against current engine main after #2731/#2734 and #2730/#2743 moved the counts underneath it.

    Where the api system stands now, at engine 0e521e8d / corpus f14449e:

    then now
    languages whose api rule fires 10 of 44 42 of 46
    raw_arch_api in band median 0, the ten firing read as the outliers median 3.0, 35/46 in band
    risk_api_exposure pinned at 0 by the api_hits == 0 guard 5 languages 1 (sqlite)
    bias_report.py --gate could not run clean exit 0, 0 unexplained

    The four languages still reading 0 are each accounted for: jcl and markdown define no api rule; objective-c — #2730 taught the rule to see an @interface method declaration, so the engine half is done, but SPEC.md rule 2 pins class_start at 0, so the corpus cannot plant one; sqlite — every form its rule accepts already moves a planted count.

    Three things this issue's own predictions got wrong, recorded because they are more useful than the parts it got right:

    1. It expected the conflation to be resolved by the gate alone. It also needed arch_api counts a declared-public, uncalled function twice: 3 functions record api 6 #2731 — writing a real rule count and a conversion credit into the same file for the first time is what exposed go/a.go recording 3 functions as arch_api 6.
    2. It expected ~36 languages to need a plant. 31 got one; the remaining seven turned out to be a rule defect, not a corpus gap, and became The api rule has no stated contract: 7 languages' own visibility idiom is invisible to it, and 5 others count any public token #2730.
    3. Its two downstream ledger entries closed on "resolves when detector: an orphan is a name seen once in the file, so any language that repeats the name in its own syntax reports zero orphans #2727 gives these languages orphans again". detector: an orphan is a name seen once in the file, so any language that repeats the name in its own syntax reports zero orphans #2727 is still open and was not needed: dockerfile/haskell/yaml resolved via the wave's file-level plants and ada via The api rule has no stated contract: 7 languages' own visibility idiom is invisible to it, and 5 others count any public token #2730's rule fix, all through the api rule rather than the orphan census. Narrowed in keyword-rosetta#58 — risk-api-exposure-zero-api-shortcircuit 5 languages → 1 (sqlite), risk-documentation-zero-evidence-guard retired.

    Last engine residue, fixed in #2747: the recorder test still asserted arch_api == raw_arch_api + raw_state_slop_orphans as "the Contextual Baseline Fix invariant". It never proved that (both sides were fixture literals) and #2734 made it false in general, so the only recorder-level statement of the contract was the double count #2731 removed, written down as an expectation.

    Siblings still open and genuinely independent: #2727 (an orphan is a name seen once in the file) and #2728 (slicer keyword buckets reach the orphan checks).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugUnintended behavior or logic failure in the enginecore-engineModifications to the central physics and parsing enginemetricsHeuristics, risk exposures, and topological math updates

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions