Skip to content

raw_arch_api bands a per-file declaration count against a per-function median — one granularity mismatch, three red columns #2771

Closed

Description

@squid-protocol

Same pass as #2769/#2770 (2026-09-05, engine 3b3568e0, corpus main 6183c4b). #2730/#2743 gave the api rule a contract and a documented fallback family for languages with no per-function visibility concept. That fixed what each rule matches. It left the magnitude question untouched, and the magnitude question is what three consecutive red columns are made of:

metric consistency what it reads
raw_arch_api 76% the rule's own hits
risk_api_exposure 71% api, encapsulation
risk_documentation 76% api × 2 + opaque execution, over a length floor

The measurement

Every language expresses the same 12-probe program with the same public surface. The per-function-visibility languages declare it three times per file (one modifier per probe); the fallback-family languages declare it once per file, because their idiom has one file-level declaration and no per-function form:

raw_arch_api, per file, corpus median 3.0

3.00  c, cpp, java, javascript, python, go, rust, swift, …   (per-function modifier)
1.25  html         (a <div id=> per probe plus main's ownership meta)
1.00  agc_assembly, css, dockerfile, haskell, yacc, yaml     (one file-level declaration)
0.25  jcl          (one PROC declaration, in main only)
0.00  markdown (no rule), objective-c, sqlite (no plantable idiom)

Eleven red cells on raw_arch_api, and eight on risk_api_exposure — agc_assembly −76%, haskell −75%, css −56%, perl −52%, dockerfile/rust/yaml −50%, sqlite −100%. risk_documentation then adds css/haskell/dockerfile/yaml at −56/−57% for the same reason, because its risk numerator is api × 2 and its defence term (doc, ownership, doc_loc) is near-constant across the corpus — the documentation column is, in this corpus, an api column with extra steps.

The point

None of those languages has less public surface. EXPOSE 8080 publishes a Dockerfile's whole interface; module Probe (probeGlobals, probeTest, probeSafety) where publishes all three Haskell functions in one line; //ROSPROC PROC publishes the JCL procedure that thirteen EXEC steps invoke. The count differs because the metric counts declarations and the two idioms declare at different granularity — one per name versus one per file. Banding a per-file count against a per-function median measures the idiom, not the surface, which is precisely the class of comparison #2669 exists to remove.

The engine already has the pattern for handling this. bias_report.py bands constant_sensitive metrics against their own strictness stratum (#2669 F.3) rather than the global median, because a language-level constant makes a global comparison meaningless. This is the same shape: a declaration-granularity constant, known per language, documented in docs/api_rule_contract.md's own audit table.

Candidate shapes

(a) Scoring layer — normalise by declaration granularity. _calc_api_exposure and _calc_documentation read api as a count of exposed surface. If the language declares per-file, the surface it names is the file's whole function population, so api should enter those formulas as min(api × functions_found, functions_found) for the fallback family, or equivalently as an exposure ratio rather than a count. This is the fix that makes the risk scores comparable, and it is a #2655-shaped design change to two formulas, not 46 rules.

(b) Reporting layer — a second stratum. docs/api_rule_contract.md already lists the fallback family by name. bias_report.py bands raw_arch_api, risk_api_exposure and risk_documentation against the median of the declaring language's own granularity class, exactly as it already does for irc0–irc4. Cheaper, and it fixes the reading of the corpus without changing any score a customer sees — which also means it fixes nothing for real repositories.

(c) Reject both and mark the family n/a. Defensible under GATING's n/a criterion only if the absence is structural, and it is not: these languages do record a nonzero, so the cell is comparable in the mechanical sense the n/a rule uses. This would be a widening of the n/a criterion, and should be argued explicitly rather than fallen into.

(a) and (b) are not exclusive — (a) is the engine answer and (b) is what makes the corpus able to check it. My reading is that (a) is the real one: risk_api_exposure for a Dockerfile that publishes a port genuinely should not sit at −50% against a Java file that publishes three methods.

Corpus pairing

api is planted (SPEC.md, one file-level declaration per fallback-family file, planted in the #2729 wave). No authoring change under (a) or (b); the re-bless moves raw_arch_api's banding, the eight risk_api_exposure cells and the four risk_documentation cells. Ledger entries that narrow: api-non-function-surfaces (which states the granularity fact and files it as morphology), api-column-is-rule-hits-only, api-export-list-morphology, risk-api-exposure-zero-api-shortcircuit.

Part of #2669. Direct follow-up to #2730/#2743.

Activity

  1. added
    enhancementNew feature, sensor, or structural signature
    metricsHeuristics, risk exposures, and topological math updates
    core-engineModifications to the central physics and parsing engine
    on Sep 5, 2026
  2. squid-protocol commented on Sep 10, 2026

    @squid-protocol
    OwnerAuthor

    Closing per #2908's issue-disposition table ("answered by D2's per-unit name set; close when Phase 2 lands").

    The granularity mismatch this issue named — a per-file declaration count banded against a per-function median — is resolved at the source: since #2935 (Phase 2) every extracted unit carries is_public (the D2 name-set union: api-header match ∪ export list ∪ healed orphans, a union of names that cannot double-count), and since #2938 (Phase 3) risk_documentation consumes exactly that per-unit set. The score contract is stated (#2939; signal_contracts.py score-contract section, first entry).

    What remains of raw_arch_api's red columns (2 of 46 cells open on the corpus scoreboard) is the api rule's own contract territory, carried by the ledger's api entries and inherited by the risk_api_exposure epic, which copies #2908's phase list and reads the same D2 name set.

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

    core-engineModifications to the central physics and parsing engineenhancementNew feature, sensor, or structural signaturemetricsHeuristics, risk exposures, and topological math updates

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions