Skip to content

feat(rules)!: deprecate the hygiene score and stop it certifying empty scans (META-284) - #42

Merged
qmarcelle merged 3 commits into
mainfrom
feature/meta-284-deprecate-hygiene-score
Aug 17, 2026
Merged

feat(rules)!: deprecate the hygiene score and stop it certifying empty scans (META-284)#42
qmarcelle merged 3 commits into
mainfrom
feature/meta-284-deprecate-hygiene-score

Conversation

@qmarcelle

@qmarcelle qmarcelle commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Implements ADR-003 amendment A-002, ratified 2026-08-03 (disposition: Remove; authority: sole steward). The ADR that docs/versioning.md requires before a breaking change already exists, so this is execution rather than a fresh decision.

The defect

computeHygieneScore([], 0) returned { value: 100, grade: 'A' }. No findings meant no penalty, no penalty meant a full score, a full score meant an A — and coverageRatio was computed but never consulted by the scoring path. A scan that observed nothing certified a repository as flawless, and that value reached a published artifact.

What changed

computeHygieneScore returns HygieneScore | null. null when the scan observed nothing: no findings, and no denominator to say anything was examined. null is not a bad grade — it is the absence of one, and a caller has to handle it rather than inherit an A. Where evidence exists, the arithmetic is untouched.

coverageRatio is number | undefined. It was 0 whenever no total was supplied, which is every current call site — that zero was the default parameter arriving unchanged, never a measurement. "Not measured" and "zero" no longer share a value.

AuditResult.score is HygieneScore | null, so a caller handed no evidence has somewhere truthful to put that. The previous non-nullable field left fabricating a perfect score as the only way to satisfy it.

Deprecations on all three surfaces, each carrying the migration. Nothing the migration needs is private — Finding.state, .severity, .confidence and .temporalWeight are the only inputs the function ever had.

Why minor and not patch

Both type changes are source-level breaks for TypeScript readers: code assigning the result to a bare HygieneScore, or coverageRatio to a bare number, stops compiling. That is the intended alarm — it is exactly the code that would otherwise read absence as a pass. Same shape as A-009 and A-010, which were also minors for the same reason.

changeset status still resolves to 0.5.0. This does not move the release number.

What is deliberately NOT here

Nothing is removed and no schema bytes change. ADR-003 §5 gives a normative-optional field a deprecation notice and a documented migration now, with removal at the next declared breaking boundary. The document profile stays at generated.specVersion: "0.4", so this release is not that boundary.

generated.hygiene remains declared in the schema, because a first-party producer still emits it — removing the declaration while that is true would describe the artifact incorrectly. Emission ceases first, on the producer's schedule, and the field and exports go afterwards.

Downstream, and intended

Two call sites in workspacejson/cli will stop compiling when it bumps to 0.5.0: packages/cli/src/producer/generate.ts and packages/agents-audit-compat/src/audit.ts. Both call computeHygieneScore(run.findings) with no denominator. The migration is not merely to handle null — it is to pass repo.files.length, which is already in scope at the producer's call site, so coverage becomes a measurement for the first time; then emit generated.hygiene where a score exists and omit the optional field where it does not.

A test asserted the defect

it('clean repo scores 100') pinned computeHygieneScore([]) at value: 100, grade: 'A'. An empty findings array is not a clean repository; it is an absence of evidence, and the two were indistinguishable in the return value. Replaced by four tests encoding the new truth, including the exact input traced in META-284.

Verification

All fourteen standard gates pass locally: architecture + red tests, docs, ADR index, build, typecheck, test, schema provenance, examples, path-identity corpus, packed tarballs + red tests, clean-room consumer install.

Summary by Sourcery

Deprecate the hygiene score while making empty scans and unmeasured coverage explicit instead of representing them as successful results.

Bug Fixes:

  • Prevent hygiene scoring from treating scans with no observed evidence as a perfect A-grade result by returning no score instead.

Enhancements:

  • Distinguish unmeasured coverage from zero coverage by making coverage ratios optional.
  • Deprecate hygiene scoring and its audit-result surface, documenting migration toward consumer-owned evaluation.

Tests:

  • Update hygiene-score invariants and integration coverage to validate null scores for unobserved scans and preserve scoring for measured clean repositories.

…y scans (META-284)

Implements ADR-003 amendment A-002, ratified 2026-08-03.

computeHygieneScore([], 0) returned { value: 100, grade: 'A' }. No findings
meant no penalty, no penalty meant a full score, a full score meant an A, and
coverageRatio was computed but never consulted by the scoring path. A scan that
observed nothing certified a repository as flawless, and that value reached a
published artifact.

The function returns HygieneScore | null now, and null when nothing was
observed. null is not a bad grade; it is the absence of one, and a caller has to
handle it rather than inherit an A. coverageRatio is number | undefined, because
the old 0 was the default parameter arriving unchanged at every call site rather
than a measurement.

Both are source-level breaks for TypeScript readers, which is the intended
alarm: that is exactly the code that would otherwise read absence as a pass.
AuditResult.score is nullable for the same reason.

Nothing is removed and no schema bytes change. ADR-003 §5 gives a
normative-optional field a deprecation notice and a documented migration now,
with removal at the next declared breaking boundary; the profile stays at 0.4,
so this release is not it. generated.hygiene stays declared while a first-party
producer still emits it — emission ceases first, then the field and exports go
together.

The old behavior was pinned by a test named 'clean repo scores 100'. An empty
findings array is not a clean repository; it is an absence of evidence, and the
two were indistinguishable in the return value.
Copilot AI lite review requested due to automatic review settings August 17, 2026 20:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

qmarcelle has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@sourcery-ai

sourcery-ai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

Deprecates the hygiene score by making it nullable when a scan observes nothing, distinguishes unmeasured coverage from zero coverage, and propagates these type and behavior changes through the rules engine, tests, audit types, and release metadata with deprecation guidance.

File-Level Changes

Change Details Files
Change hygiene scoring semantics so scans that observe nothing yield no score and coverage that is not measured is distinct from zero coverage.
  • Update computeHygieneScore to accept an optional total file count, return HygieneScore
null, and return null when there are no findings and no meaningful denominator.
  • Adjust coverageRatio calculation to return undefined when no totalRepoFiles is provided, and only compute a numeric ratio when a denominator is supplied.
  • Add deprecation documentation to computeHygieneScore explaining that scoring is prescriptive and should move to consumers.
  • Update types to reflect nullable scores and optional coverage, with deprecation annotations for downstream consumers.
    • Change HygieneScore.coverageRatio to be an optional number, documented as undefined when coverage is not measured.
    • Change AuditResult.score from a required HygieneScore to HygieneScore
    null and document the semantics and deprecation.
  • Annotate HygieneScore and AuditResult.score as deprecated with references to ADR-003 amendment A-002 and migration guidance.
  • Align tests with the new semantics around absent evidence, nullable scores, and unmeasured coverage.
    • Replace the previous "clean repo scores 100" invariant with a new suite asserting null for empty scans without a denominator, scoring only when a denominator or findings exist, and undefined coverage when unmeasured.
    • Guard invariant loops against the empty-findings case and update expectations to handle computeHygieneScore returning null using non-null assertions where evidence exists.
    • Update the real-repos integration test to assert score bounds only when a non-null score is produced.
    packages/rules/src/engine/__tests__/hygiene-score.invariants.test.ts
    packages/rules/src/testing/__tests__/real-repos.integration.test.ts
    Record the breaking-but-minor change and migration path in the changeset metadata.
    • Add a changeset marking @workspacejson/rules for a minor release and explaining the behavioral and type-level changes.
    • Document the deprecation of hygiene scoring, the null-return semantics for empty scans, the new coverageRatio typing, and the rationale tied to ADR-003 amendment A-002.
    .changeset/quiet-moons-admit.md

    Tips and commands

    Interacting with Sourcery

    • Trigger a new review: Comment @sourcery-ai review on the pull request.
    • Continue discussions: Reply directly to Sourcery's review comments.
    • Generate a GitHub issue from a review comment: Ask Sourcery to create an
      issue from a review comment by replying to it. You can also reply to a
      review comment with @sourcery-ai issue to create an issue from it.
    • Generate a pull request title: Write @sourcery-ai anywhere in the pull
      request title to generate a title at any time. You can also comment
      @sourcery-ai title on the pull request to (re-)generate the title at any time.
    • Generate a pull request summary: Write @sourcery-ai summary anywhere in
      the pull request body to generate a PR summary at any time exactly where you
      want it. You can also comment @sourcery-ai summary on the pull request to
      (re-)generate the summary at any time.
    • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
      request to (re-)generate the reviewer's guide at any time.
    • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
      pull request to resolve all Sourcery comments. Useful if you've already
      addressed all the comments and don't want to see them anymore.
    • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
      request to dismiss all existing Sourcery reviews. Especially useful if you
      want to start fresh with a new review - don't forget to comment
      @sourcery-ai review to trigger a new review!

    Customizing Your Experience

    Access your dashboard to:

    • Enable or disable review features such as the Sourcery-generated pull request
      summary, the reviewer's guide, and others.
    • Change the review language.
    • Add, remove or edit custom review instructions.
    • Adjust other review settings.

    Getting Help

    @sourcery-ai sourcery-ai Bot left a comment

    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

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

    Hey - I've left some high level feedback:

    • The HygieneScore type defines coverageRatio?: number | undefined;, which is redundant given the optional property marker; consider simplifying this to coverageRatio?: number to avoid confusion and better reflect the intended undefined-for-unmeasured semantics.
    • The observedNothing check treats totalRepoFiles === 0 the same as an undefined denominator, so a repo with zero files and no findings yields null; if a zero-file repo should be considered a fully observed scan, you may want to distinguish that case explicitly (for example, by treating totalRepoFiles === 0 as a valid denominator rather than as absence).
    Prompt for AI Agents
    Please address the comments from this code review:
    
    ## Overall Comments
    - The `HygieneScore` type defines `coverageRatio?: number | undefined;`, which is redundant given the optional property marker; consider simplifying this to `coverageRatio?: number` to avoid confusion and better reflect the intended `undefined`-for-unmeasured semantics.
    - The `observedNothing` check treats `totalRepoFiles === 0` the same as an undefined denominator, so a repo with zero files and no findings yields `null`; if a zero-file repo should be considered a fully observed scan, you may want to distinguish that case explicitly (for example, by treating `totalRepoFiles === 0` as a valid denominator rather than as absence).

    Sourcery is free for open source - if you like our reviews please consider sharing them ✨
    Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

    The three coverage states — not measured, measured as zero, measured as a ratio
    — were stacked in a nested ternary. Written as a guard instead, so each state is
    visible on its own line. No behavior change; build, typecheck and tests pass
    unchanged.

    @greptile-apps greptile-apps Bot left a comment

    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

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

    qmarcelle has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

    @qmarcelle

    Copy link
    Copy Markdown
    Contributor Author

    Review-gate disposition

    Required checks: all green. test (20), test (22), Four-path producer conformance, plus Sourcery review, Socket Security (both), and the producer-conformance job that builds the CLI against this candidate.

    SonarCloud: red, and dispositioned rather than fixed. Full detail so this is reviewable rather than asserted.

    Fixed

    typescript:S3358 (MAJOR) — "Extract this nested ternary operation into an independent statement." Real finding. The three coverage states — not measured, measured as zero, measured as a ratio — were stacked in a nested ternary. Rewritten as a guard so each state sits on its own line. Build, typecheck and tests unchanged. Confirmed cleared in the re-analysis.

    Not fixed — false positive by construction

    Three remaining, all typescript:S1874 (MINOR), "'HygieneScore' is deprecated":

    Location Why it fires
    hygiene-score.ts:1 the implementation imports the type it returns
    hygiene-score.ts:34 the return-type annotation names it
    types.ts:330 AuditResult.score is typed as it

    S1874 flags usage of a deprecated API. A deprecation's own declaration site and implementation necessarily use the thing being deprecated. There is no arrangement of this code that both deprecates HygieneScore and avoids referencing it.

    One alternative was considered and rejected: move @deprecated off the interface and keep it only on computeHygieneScore and AuditResult.score. That clears all three findings without any suppression — but it loses real signal. A consumer writing function render(s: HygieneScore) is broken by the v0.5 removal and would no longer be warned. Reshaping correct code to satisfy a rule that cannot distinguish a deprecation's implementation from a consumer's use is the wrong trade.

    Effect on the gate

    These three alone hold new_maintainability_rating at 3 (C) against a required 1 (A) — a small diff makes a small amount of debt decisive:

    OK     new_reliability_rating        1
    OK     new_security_rating           1
    ERROR  new_maintainability_rating    3  (required 1)
    OK     new_duplicated_lines_density  0.0
    OK     new_security_hotspots_reviewed 100.0
    

    SonarCloud is not a required status check on main (verified against branch protection: test (20), test (22), Four-path producer conformance). Under the repository's review protocol, advisory findings may be dispositioned without code changes. Steward action if the red is unwanted: mark the three S1874 issues Accepted in SonarCloud with the reason above. That needs Sonar auth and is not something this branch can do.

    @greptile-apps greptile-apps Bot left a comment

    Copy link
    Copy Markdown

    Choose a reason for hiding this comment

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

    qmarcelle has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

    @sonarqubecloud

    Copy link
    Copy Markdown

    Quality Gate Failed Quality Gate failed

    Failed conditions
    C Maintainability Rating on New Code (required ≥ A)

    See analysis details on SonarQube Cloud

    Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

    @qmarcelle
    qmarcelle merged commit 4871663 into main Aug 17, 2026
    6 of 7 checks passed
    @qmarcelle
    qmarcelle deleted the feature/meta-284-deprecate-hygiene-score branch August 17, 2026 23:15
    qmarcelle added a commit that referenced this pull request Aug 17, 2026
    … exhausted (#43)
    
    The protocol document still named Greptile the required automated status gate
    and told agents not to merge while its review was pending. That requirement
    cannot be satisfied and would block every merge indefinitely.
    
    It also recorded the wrong cause. It said the app was 'likely not installed';
    the app IS installed and does respond, with a 50-credit trial-limit notice and
    zero check runs. Observed on PR #37 on 2026-08-13, and again on PR #42, which
    carries the notice twice. An uninstalled app is a setup gap; an exhausted quota
    is a reviewer that answers and says nothing. A quota notice is never a pass.
    
    Three claims corrected against measured state:
    
      * Greptile as required status check -> withdrawn 2026-08-13, verified against
        branch protection, which requires exactly test (20), test (22) and
        Four-path producer conformance
      * 'Require code owner review: currently required' -> disabled;
        require_code_owner_reviews is false
      * the Greptile-premised remediation sequence -> both of its first two steps
        have already executed, and the outcome is that no reviewer requirement
        remains at all
    
    docs/repository-settings.md already recorded all of this accurately on
    2026-08-13. This document had drifted away from its own sibling, which is the
    defect: two files in one repository disagreeing about whether a merge gate
    exists.
    
    The .greptile/ rules are kept. They are correct and cost nothing while dormant,
    and credits are the only thing standing between them and running again.
    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.

    2 participants