fix(core-engine): give the api rule a stated contract and audit all 46 languages against it (#2730) - #2743
Merged
Merged
Conversation
…6 languages against it (#2730) The `api` rule had no contract, so 46 languages disagreed about what "public surface" means in both directions: seven could not see the idiom their own language uses to make a function public, and eleven counted a token where no declaration exists. Contract, now stated in docs/api_rule_contract.md and next to the rule in how_to_add_a_language.md's output schema: api matches a declaration that makes a named function or type visible outside the file it is declared in. Too narrow (the language's own idiom was invisible): abap FORM, ada's library-level subprogram, dart's top-level public function, matlab's function file, objective-c's @interface method line, ruby's `public :name`, shell's `export -f`, plus yaml's `workflow_call` and perl's `sub`. Too broad (a reference, a statement or a bare modifier counted): c (7534 of 8675 crucible matches were body-local declarations and statements like `return NULL;`), cobol (CALL/END-CALL), agc_assembly (the EXTEND opcode), powershell (`<name>(` call sites), go (struct-literal field keys), lua (anonymous `function ()`), assembly (EXTERN/IMPORT), dart (@pragma), and the bare-`public` family: java, csharp, kotlin, php, groovy, swift, typescript. 91 new strict cases across 21 files, each verified against the real compiled regex before being written down; ReDoS detonations on every new quantifier shape, all measured linear (~2.0x per doubling at n=2k..32k). Both golden masters re-blessed: 3384 differences per mode, fully attributed, zero off-target. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WfeHRWra1d6Z5uSReFajSq
Contributor
Conflict was confined to the two golden-master fixtures, both auto-generated: #2742 (jcl sync_locks) re-blessed them on main while this branch was in flight. Resolved per .agents/skills/ci-push-checklist section 6 -- never hand-merge a generated file: took origin/main's version of both to clear the markers, then re-ran `crucible_check.py --update --yes` so they are recalculated from the merged code rather than stitched together. Verified the resolution rather than trusting the regen: re-attributed the regenerated fixtures against origin/main and got the same 3384-diff profile as before the merge, bucket for bucket (2120 topology X/Y/Z, 300 API Exposure, 257 Documentation Exposure, 255 Public Exports, 255 Structural Magnitude, 197 roll-ups) and the same per-language counts. `jcl` appears nowhere in the per-file diffs, which is the positive check that #2742's blessing survived intact instead of being overwritten. Both modes PASS, full suite 7625 passed serially, audit_check and tree-sitter-accuracy audit clear. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WfeHRWra1d6Z5uSReFajSq
This was referenced Sep 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resolves #2730.
Part of #2669.
The problem
apihad a one-line description ("Code exposed to the outside world. Captures explicitvisibility markers AND implicit architectural defaults") and no contract, so 46 languages
disagreed about what a public surface is, in both directions at once. #2729's corpus wave
measured it: seven languages cannot see the idiom their own language uses to make a function
public, and five count a bare
publictoken anywhere in the code stream.apifeeds_calc_api_exposureand_calc_documentation, so both directions land in scoredrisk. On the control corpus it reads as language bias with nothing wrong in the corpus:
identical planted intent,
api0 in one language and 3 in its neighbour, purely from what theregex accepts.
The contract
Stated in the new
docs/api_rule_contract.md, and in one line next to the rule inhow_to_add_a_language.md's output schema:Three corollaries, each of which the audit found a language violating:
switchcase on the keyword.pythonhas always used, extended to the languages that needed it.Plus a documented fallback family for languages with no per-function visibility concept at
all (
dockerfileEXPOSE,yamlon:,css@property,htmlid=,yacc%define,m4AC_SUBST,cobolENTRY,agc_assemblyEQUALS,makefile,sqlite, and nowmatlab), so the substitution is a recorded decision rather than an accident.The audit — all 46 corpus languages
Full table with per-language counts is in
docs/api_rule_contract.md. 23 languages werealready inside the contract and are untouched; 18 changed.
Too narrow — the language's own visibility idiom was invisible
abapFORM/FUNCTIONare public by defaultCALL FUNCTION '...'cannot matchadaprocedure/functionat column 0; nested (private) ones are indenteddart_(matlabfunction(the fallback family)objective-c@interface-/+method line ending;rather than{rubypublic :name(a top-leveldefis private onObject)public/public_class_method+ symbolshellexport -f nameexportand the nameyamlworkflow_callperlsubis package-public^sub <name>perlis the case the issue called out separately: every alternative it had was an exportlist that also lands on a rule which already owns it (
@EXPORT_OK = (...)is astate_mutation,use Exporter|parent|baseanimport), soapicould only move by movinganother planted count with it.
Too broad — a token counted where no declaration exists
creturn NULL;alone was 513)cobolCALL/INVOKEcall sites, plusEND-CALLvia the hyphen boundaryagc_assemblyEXTENDopcode (SUBROUTINE/BEXTnever fired at all)gopowershell<name>(at line start — a .NET call or a statementluafunction (), an anonymous function that declares no namedart@pragma(...), a compiler hintassemblyEXTERN/IMPORT, which import a name rather than publish onejavacsharpkotlinphpgroovyswifttypescriptThe bare-modifier family, with what each one was actually matching:
import okhttp3.internal.X(kotlin, 12 of 17),let package = Package(...)in a SwiftPMmanifest (swift),
$publicand'path.public'(php, 7),case "public":in a compiler thatparses the keyword (typescript, csharp), a log message about a "public key ring" (groovy, 4).
javaandcsharpmove zero matches on either corpus — real Java and C# putpublicinfront of a declaration nearly always, so the anchor is a precision guard rather than a recount.
That is the answer to "why did a real fix produce no diff" for those two; the strict tests hold
the guard in place.
Two findings surfaced while doing this that are not #2730's shape and are not fixed here:
powershell's #2656 keyword-exclusion set is lowercase against anre.Ipattern, soIf (andParam(matched anyway (moot now — the alternative is gone, and the test records it), andgo/java'sencapsulationrules carry the same unanchored shape theirapirules had.encapsulationis a different rule with its own contract question and is left alone.Verification
pytest tests/ -v).test_<lang>_strict.pyfiles, in atest_<lang>_api_contract_2730function each. Every case was run against the real compiledregex before being written down (AGENTS.md rule 3) — the harness reported 0 mismatches across
all 91 before a line of test code was committed.
c,go,dart,objective-c,shell,java,csharp,kotlin,groovy,php,typescript), plus ageometric scaling sweep at n = 2k/4k/8k/16k/32k on all of them — every one ~2.0x per doubling
(linear), none above 5ms at n=32000. Rules that gained no new quantifier deliberately got no
detonation: 16 extra spawn-context subprocesses measurably perturb the ratio-based
spec_exposuretiming tests elsewhere in the suite.ruff_audit --ci,mypy_audit --ci,audit_check.py— all clear against their baselines.tree_sitter_accuracy_audit --ci --all— 30/30 OK, no baseline regeneration needed.tri_comparison_chart --all --ci— 3/3 committed baselines OK (apidoes not feedfunc/class precision, so no movement was expected and none occurred).
Golden masters
Both re-blessed. 3384 differences per mode, identical in structure between them, every one
attributed and zero off-target:
Exposed API / Public Exportstotal_massapi_exposure/documentation/composition/*/impacthealth/avg_documentationThe X/Y/Z bucket is why a language-scoped
scope_check --expectcannot be the gate here:apifeeds Structural Magnitude, which feeds total mass, which the 3D topology solver uses toposition every file in the corpus. So the scoping claim is made on the other 1264 instead —
every per-file signal/risk diff resolves to a file the engine detected as one of the 23 touched
languages. Two looked off-target and are not:
cpp/godot/object.his detected as C(Identity Proof:
Sibling Anchor (.c)), and the extensionlessperl/exiftool/exiftoolisdetected as Perl by shebang. Both go through a rule this PR changed.
Direction of change matches intent everywhere — narrowed rules negative, widened rules positive:
Public ExportsPublic Exportscperlcobolmatlabcpp/godot/object.h(C)objective-cagc_assemblygodart−31,powershell−25,lua−16,kotlin−9,php−8,assembly/groovy−3,swift/typescript−2Cross-repo
Companion: squid-protocol/keyword-rosetta#56 ("corpus owes a re-bless against engine main",
already open before this PR).
This PR is corpus-visible per AGENTS.md rule 8 and #2669's paired-work rule, so it carries the
rosetta:rebless-owedlabel perkeyword-rosetta/docs/GATING.md's cross-repo flow. Itsrosetta-auditis red as designed, and its classification is exactly right:abap,ada,dart,matlab,perl— the five languageswhose own idiom this PR makes visible. Each goes
api0 → 3 per file (4 inmain), and fourof them also go
api_orphan_credit3 → 0, because a declaration the rule now counts is nolonger new orphan surface (fix(core-engine): the api rule's own declarations are not new orphan surface (#2731) #2734's
api_declared_orphans).adakeeps its credit at 0 eitherway: its syntax repeats the subprogram name (
end Probe_Globals;), so its probes were neverorphans.
c— these are docs: add new page to GitGalaxy documentation #56's existing debt from the already-mergedfix(core-engine): the api rule's own declarations are not new orphan surface (#2731) #2734, not this PR.
calso moves its rawapihere (6/9/7/7 → 4/5/3/3), which the re-blesspicks up in the same pass.
tools/verify_language.pyacross all 46: the same26 languages, 97 cells, and the same split. No language outside those 26 moves.
Per GATING.md's flow, nothing in the corpus repo is edited beforehand:
main: the 97 manifest cells, plus the ledger —api-no-plantable-idiomnarrows from seven languages toobjective-c+sqlite(
objective-cstays 0 because planting an@interfacewould break SPEC.md rule 2'sclass_start == 0tripwire, even though the rule can now see one), andapi-double-count-inflates-scored-apiflipsstill_reproducesto false — its own verdictsays it "resolves when arch_api counts a declared-public, uncalled function twice: 3 functions record api 6 #2731 credits only the orphans the rule did not already count", which
is what the 3 → 0 cells are. Also swaps
shell's plant fromexport PROBE_X=1toexport -f probe_x, so shell'sapimeasures an exported function rather than asimilarly-named variable.
bias-history.ymlregenerates the report on that merge and closes docs: add new page to GitGalaxy documentation #56; thentools/issue_status.pyfor the languages that moved, per Rosetta to green: cause-first batch plan + multi-session coordination (child of #2560) #2669 F.6.🤖 Generated with Claude Code
https://claude.ai/code/session_01WfeHRWra1d6Z5uSReFajSq