Skip to content

M8b: samples matrix + API-coverage gate#9

Merged
moisesja merged 2 commits into
mainfrom
feature/m8b-samples-coverage
Jun 24, 2026
Merged

M8b: samples matrix + API-coverage gate#9
moisesja merged 2 commits into
mainfrom
feature/m8b-samples-coverage

Conversation

@moisesja

Copy link
Copy Markdown
Owner

Second of three M8 PRs (after M8a, #8). A first-class, offline samples matrix demonstrating every role × securing form plus status/schema/trust/1.1, and an api-coverage gate proving the samples exercise the public surface.

Verification: build 0-warning under TreatWarningsAsErrors; 352 tests green (338 + 14 sample smoke tests); all 14 samples run to their expected outcome; api-coverage gate passes (53 covered / 0 uncovered / 4 exempted).

What's in it

  • Credentials.Samples.SharedSampleKeys (in-memory did:key), SampleNarrator (FR banners), and AllowlistIssuerTrustPolicy (the one shipped trust policy — per FR-082 it lives in samples, not the library).
  • 14 samples/* console projects, each Program.RunAsync(TextWriter, IServiceProvider?), offline, narrating the FRs it demonstrates and throwing on any unexpected outcome:
    • DataIntegrity (eddsa-jcs-2022, + an ISecuringCapabilities/SecuringSelector query), DataIntegrityRdfc (eddsa-rdfc-2022), JoseEnvelope (vc+jwt), CoseEnvelope (vc+cose)
    • SdJwtVc (selective disclosure), SdJwtPresentation (KB-JWT holder binding), Bbs2023 (bbs-2023 derive, IsAvailable-gated)
    • PresentationDataIntegrity + PresentationJose (holder binding + presentation verification)
    • StatusList (Bitstring Status List revoke/verify), Schema (JSON Schema 2020-12), IssuerTrust (allowlist trusted/untrusted)
    • Vcdm11 (verify a foreign-issued 1.1 credential + the AcceptVcdm11=false opt-out gate), FullPipeline (status + schema + trust composed)
  • Credentials.SampleSmokeTests — runs every sample in-process (14 facts); doubles as the api-coverage driver under coverlet (scoped to Core + DI).
  • tools/api-coverage (+ run-api-coverage.sh, coverage.runsettings, api-coverage-exclusions.txt) — diffs the public surface (via MetadataLoadContext) against the samples' coverage; fails if any gateable public type is exercised by no sample. Type-level (calibrated: only error-path/options types needed exempting); the tool also fails on a stale exclusion that has become covered.
  • CIci.yml gains an api-coverage job (needs: build-test).

Notes / decisions

  • The gate is type-level — every gateable public type is exercised by a sample. Member-level 100% over ~700 members isn't realistic and would be exclusion-heavy; this is the honest, achievable bar. The 4 exemptions (3 error-path types + the tuning options object) are in a documented text file with reasons.
  • SecuringSelector was covered by adding a real capabilities query to the DataIntegrity sample rather than exempted.
  • The Vcdm11 sample embeds a genuine did:key-issued secured-1.1 fixture (public issuance is 2.0-only), so it verifies fully offline.

Adversarial pass

The api-coverage gate fails correctly when an uncovered public type is injected into Core (verified — not hollow, the M8a-ConsumerProbe lesson applied).

Next: PR-C (conformance shim + interop vectors).

🤖 Generated with Claude Code

Second of three M8 PRs. A first-class offline samples matrix + a gate proving the
samples exercise the public surface.

- Credentials.Samples.Shared: did:key key minting, FR-banner narrator, and the allowlist
  IIssuerTrustPolicy (the one shipped trust policy — lives in samples, not the library, FR-082).
- 14 samples/* console projects (Program.RunAsync(TextWriter, IServiceProvider?)), offline,
  each throwing on an unexpected outcome: DataIntegrity (+ a capabilities query), DataIntegrityRdfc,
  Jose/Cose envelope, SdJwtVc, SdJwtPresentation (KB-JWT), Bbs2023 (IsAvailable-gated),
  Presentation DI/Jose, StatusList, Schema, IssuerTrust, Vcdm11 (foreign-1.1 verify + opt-out gate),
  FullPipeline (status+schema+trust composed).
- Credentials.SampleSmokeTests: runs every sample in-process (14 facts) + drives api-coverage.
- tools/api-coverage: MetadataLoadContext surface enumerator + coverlet diff. Type-level gate —
  53 covered, 0 uncovered, 4 documented exemptions (api-coverage-exclusions.txt); also fails on a
  stale exclusion. + tools/run-api-coverage.sh, tools/coverage.runsettings.
- ci.yml: api-coverage job (needs: build-test).

Build 0-warning; 352 tests green (was 338). Adversarial pass: the api-coverage gate fails correctly
when an uncovered public type is injected (not hollow).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@moisesja moisesja self-assigned this Jun 24, 2026

@moisesja moisesja left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — M8b samples matrix + API-coverage gate

Read the whole diff. Overall this is strong, disciplined work: no src/ production code is touched — it's purely additive (14 samples, shared helpers, smoke tests, the coverage tool, one CI job), so runtime risk to the library is ~zero. The samples are real tests, not decoration: each one asserts concrete outcomes and throws on any deviation (decision must be Accepted/Rejected, specific checks must be Passed/Failed, COSE/JWS shapes validated, 1.1 must not upgrade). The smoke tests delegating their assertions to the samples is a sound, documented design. The trust/status/1.1 samples correctly exercise both accept and reject paths.

That said, the entire point of this PR is a gate that must never false-green, so my concerns are concentrated there.

1. (Main concern) The gate silently treats "absent from cobertura" as "not gateable" — a false-green vector

tools/api-coverage/Program.cs:

if (!coveredRate.TryGetValue(name, out var rate)) { notGateable++; continue; }
if (rate > 0) covered.Add(name); else uncovered.Add(name);

The classification of a public type as "not gateable" is inferred purely from its absence in the cobertura report — never from reflection. But the MetadataLoadContext side already knows the truth (type.IsInterface, type.IsEnum, IsAbstract && IsSealed, etc.) and throws that knowledge away.

Coverlet does emit line-rate=0 entries for normal classes, so the common case (a new public class with methods, uncovered) is correctly caught — and your adversarial injection test verifies exactly that path. The residual hole is no-IL concrete surface: a new public static class WellKnownFoo { public const string X = "…"; } or an abstract base with no method bodies produces no cobertura <class> entry → it's silently bucketed into notGateable and exempt-by-absence, with no entry in the exclusions file and no failure. notGateable is printed but never asserted on, so a drift upward is invisible.

Recommendation: classify gateability from reflection metadata, and treat "concrete class with executable members but absent from cobertura" as uncovered (fail), not not-gateable. That closes the gap and makes the "every gateable public type is exercised" claim actually airtight rather than airtight-for-the-cases-coverlet-happens-to-emit.

2. Confirm BBS actually runs on the CI runner

samples/Credentials.Sample.Bbs2023/Program.cs returns early (exit 0, narrates "skipped") when !Bbs2023Cryptosuite().IsAvailable. The smoke test still passes (non-empty narration), so if the native BBS library isn't present on ubuntu-latest, the bbs-2023 derive/verify path is never validated in CI and the green check is misleading. This matches the M5 gating pattern, so it may be intentional/consistent — but please confirm whether the CI runner has the native lib. If it doesn't, the BBS sample is a no-op in CI and that should be called out explicitly (or the lib provisioned in the job).

3. Hardcoded net10.0 in the coverage script is brittle

tools/run-api-coverage.sh:

BIN="$ROOT/tests/Credentials.SampleSmokeTests/bin/Debug/net10.0"

When the TFM bumps, this path silently stops existing → the tool gets bad DLL paths → MetadataLoadContext fails rather than reporting a coverage gap. Derive the TFM (read it from the csproj, or glob bin/Debug/net*/) so a framework bump can't quietly break the gate.

Minor / non-blocking

  • Double build in CI: the api-coverage job needs: build-test but run-api-coverage.sh runs its own dotnet build, so the solution is built twice. Correct, just wasteful.
  • IsCompilerGenerated (Program.cs): the >d__ and __+DisplayClass clauses are already subsumed by Contains('<') (async state machines and closures both contain <). Harmless, but dead conditions.
  • DRY: the provider = services ?? …; ownsProvider = services is null; try/finally dispose boilerplate is repeated in all 14 samples, and InMemorySchemaResolver is duplicated in Schema and FullPipeline. Defensible if you want each sample self-contained as a teaching artifact, but a shared SampleHost helper would cut ~14 copies.
  • Cosmetics: SampleSmokeTests.csproj has … Vcdm11.csproj" /> </ItemGroup> on one line; ProjectReference paths mix \ and /. Builds fine on both OSes.

Verdict

Not requesting changes — nothing here blocks merge and the library code is untouched. But item 1 goes to the integrity of the very guarantee this PR sells, and item 2 decides whether the BBS check is real or theater in CI. I'd like those two addressed (or explained) before this is the gate the rest of M8 leans on.

🤖 Reviewed by Claude Code


Generated by Claude Code

Addresses the PR #9 review:
- (item 1, main) api-coverage gateability is now decided from REFLECTION, not from absence in
  cobertura: a concrete type with executable members that coverlet didn't emit is treated as
  UNCOVERED (fail), closing the no-IL false-green vector. Interfaces/enums/delegates/const-only
  static classes are not-gateable by metadata, and the not-gateable set is now PRINTED (auditable —
  an upward drift is visible, not silently swallowed). Adversarial re-check: a gateable uncovered
  type still fails.
- (item 3) run-api-coverage.sh derives the output dir from the built Credentials.Core.dll instead
  of hardcoding net10.0, so a TFM bump reports a gap rather than crashing on a missing path.
- (minor) IsCompilerGenerated simplified to the subsuming Contains('<'); SampleSmokeTests.csproj
  ProjectReferences reformatted (consistent separators, proper line breaks).

Item 2 (BBS in CI) confirmed in the PR reply: NetCrypto 1.1.0 ships linux-x64 libzkryptium_ffi.so,
so the bbs-2023 path runs for real on ubuntu-latest (not the skip branch).

Gate still: 53 covered, 0 uncovered, 4 exempted. Build 0-warning; 352 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@moisesja

Copy link
Copy Markdown
Owner Author

Thanks — sharp review, especially item 1. All addressed in ff41443 (validated each against the code first).

1. (Main) Gateability now from reflection, not cobertura-absence — false-green closed

You're right that inferring "not gateable" from absence threw away what MetadataLoadContext already knows. The tool now classifies gateability from reflection metadata: a type is not-gateable only if it's an interface, enum, delegate, or has no executable members (e.g. a const-only static class). Everything else is gateable, so a concrete type with executable members that's absent from cobertura is now UNCOVERED (fail) — exactly the no-IL hole you flagged. And the not-gateable set is now printed (auditable), so an upward drift is visible rather than silently swallowed:

not-gateable (no executable IL to cover): ...IDigestService, ...DocumentOrigin, ...ProofPurpose, ...IIssuer, ...CheckKinds, ...VcdmVersion, ... (27 — all interfaces/enums/const-only)

Adversarial re-check: an injected gateable-but-uncovered class still fails (1 uncovered). Gate result unchanged on the real surface (53 covered / 0 uncovered / 4 exempted) — the samples genuinely cover everything; the old logic just happened to be right for cases coverlet emits.

2. BBS does run on CI (not theater)

Confirmed: NetCrypto 1.1.0 ships runtimes/linux-x64/native/libzkryptium_ffi.so (and Nethermind.Crypto.Bls ships linux-x64/native/libblst.so), so on ubuntu-latest (linux-x64) the native lib is restored and IsAvailable is true — the bbs-2023 derive/verify path runs for real, not the skip branch. The IsAvailable gate is kept (the consistent M5 portability pattern) so the sample degrades cleanly on an unsupported RID, but on the actual CI runner it exercises real crypto.

3. TFM no longer hardcoded

run-api-coverage.sh now derives the bin dir from the built Credentials.Core.dll (find … -name Credentials.Core.dll) instead of net10.0, so a framework bump reports a coverage gap rather than crashing on a missing path.

Minor

  • IsCompilerGenerated simplified to the subsuming Contains('<') (dead >d__/DisplayClass clauses removed).
  • SampleSmokeTests.csproj ProjectReferences reformatted — consistent \ separators, proper line breaks.
  • Double build in CI: each CI job runs on a fresh runner with no shared workspace, so the api-coverage job must build the solution itself — it's not rebuilding within build-test's environment. Left as-is.
  • DRY (SampleHost, duplicated InMemorySchemaResolver): kept each sample self-contained on purpose — they're teaching artifacts meant to be read/copied in isolation, so I'd rather repeat the ~6-line provider boilerplate than send a reader to a shared helper. Happy to extract if you'd prefer.

352 tests green, 0-warning build, api-coverage gate green. Ready for another look.

@moisesja moisesja merged commit 45d7fda into main Jun 24, 2026
2 checks passed
@moisesja moisesja deleted the feature/m8b-samples-coverage branch June 24, 2026 13:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant