Skip to content

feat(e2e): assemble the scorecard report, holdout adapter, and operator script - #182

Open
ahrav wants to merge 1 commit into
stack/scorecard-04-gates-familiesfrom
stack/scorecard-05-report-script
Open

feat(e2e): assemble the scorecard report, holdout adapter, and operator script#182
ahrav wants to merge 1 commit into
stack/scorecard-04-gates-familiesfrom
stack/scorecard-05-report-script

Conversation

@ahrav

@ahrav ahrav commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Summary

Stack 5/6 of the release scorecard.

  • buildScorecardReport(bundle) emits the twelve fixed sections in order (target, five families, safety gates, regret, adverse deltas, limitations, evidence, outcome). promotionAllowed and mandatoryEvidenceComplete are derived inside from gate rows, lane statuses, required slots, and blocking count; no caller supplies them and no scalar aggregate exists. The builder validates its own output through parseScorecardReport.
  • scorecardExitCode: 2 = any gate non-passing; 1 = evidence incomplete, non-comparable baseline, or blocking regressions over tolerance; 0 = promotion allowed.
  • publishScorecardReport: refuses (scorecard: privacy-rejected) when the report fails the shared privacy scan, else canonical 2-space JSON via writeJsonAtomically.
  • createScorecardAdapter fills the prospective-holdout ScorecardAdapter seam: rejects a foreign policy fingerprint or paired facts the paired-delta report did not analyze; its resultFingerprint pins policy, freeze manifest, every lane report, paired facts, baseline, and the scorecard report.
  • scripts/run-scorecard.ts: --freeze --freeze-fingerprint --artifacts --out [--policies] [--paired-delta-policy] [--baseline]. Refusals exit 2 with no report.
  • README ### Scorecard: pre-registration, gate/lane statuses, reason codes, exit codes, gh run download → score → review steps, and the fail-closed expectation for the first release run.

Deviation from the plan: the prospective-holdout unit tests keep their controllable stub adapter; the real-adapter end-to-end case (hard-gate-failed, recompute to the same scorecardResultFingerprint, substitution changes it) lives in src/scorecard/report.test.ts.

Testing

  • bun run test:scorecard-unit (54 passed at this head) incl. AE1, AE2, AE8, AE9 and the release-shape script run: exit 2, four not-observed gates, one passed injection gate.
  • bun run test:prospective-unit (125 passed)

Stack

Depends on #181. Next: #183.

Make scorecard generation reproducible from one evidence bundle so the
release reviewer and the prospective-holdout validator reach the same
decision from the same bytes. Promotion allowance is computed inside the
builder from gate rows, lane statuses, required slots, and blocking
regressions; no caller supplies it, and the builder validates its own
output through the wire parser before returning it.

The prospective-holdout seam is filled by an adapter whose result
fingerprint pins every input that shaped the outcome, so substituting a
lane report or a paired fact changes the recorded fingerprint. The
operator command refuses without writing when the policy is not frozen
or the report fails the privacy scan, and otherwise exits by the
report's own outcome. The README states that the first release-grade
run exits 2 until the four unproduced gate probes exist.
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Essentials

Run ID: 0e2b6a39-da30-4746-9db5-ec2a35dd4ef1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.


export function scorecardExitCode(report: ScorecardReport): ScorecardExitCode {
if (report.body.outcome.hardGateFailures.length > 0) return 2;
return report.body.outcome.promotionAllowed && report.body.evidence.baseline.status !== "schema-mismatch" ? 0 : 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

report.body.outcome.promotionAllowed (computed by deriveOutcome in report-contract.ts) never accounts for baseline comparability — it's derived purely from gates, lanes, families, and adverse deltas. scorecardExitCode bolts a baseline.status !== "schema-mismatch" check on top here, but that check lives only in this function, not in the outcome itself.

Concretely: if the policy pins a baselineScorecardReportFingerprint but the baseline file is corrupt/unreadable, loadBaseline returns status: "schema-mismatch". compareWithBaseline then takes the baseline.status !== "present" branch, returning adverseDeltas: [] (so blockingRegressionCount stays 0), and with all gates passed and lanes present, deriveOutcome computes promotionAllowed: true.

run-scorecard.ts still exits 1 correctly because of the special case here, but createScorecardAdapter.evaluate() (line 104 below) reads report.body.outcome.promotionAllowed directly and has no equivalent baseline-comparability guard — so the prospective-holdout pipeline could reach decision: "promote" off a report whose baseline was never actually comparable. This schema-mismatch baseline path also isn't covered by any test in report.test.ts.

Worth considering: fold the baseline-comparability check into deriveOutcome itself (e.g. pass baseline status in) so promotionAllowed is correct everywhere it's read, rather than re-derived ad hoc per caller.

if (policyFingerprint !== bundle.policyFingerprint) throw new ScorecardContractError(["adapter: policy-fingerprint-mismatch"]);
const pairedDelta = laneEvidence(bundle, "paired-delta");
const pairedFacts = pairedFactsFingerprint(holdout.pairs);
if (pairedDelta.report === null || pairedDelta.report.body.analysis.pairedFactsFingerprint !== pairedFacts) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

evaluate() throws an uncaught ScorecardContractError("adapter: evidence-pairs-mismatch") whenever the paired-delta lane report is simply null — which conflates two very different situations: genuine tampering (facts don't match a present paired-delta report) vs. the paired-delta artifact being legitimately absent/incomplete (a normal LaneEvidence status the rest of the bundle already models gracefully via laneEvidence/mandatoryEvidenceComplete).

buildProspectiveReport (prospective-holdout/report.ts:133) calls input.scorecard.evaluate(...) with no try/catch, so once this adapter is wired into the real release flow, a missing paired-delta artifact crashes prospective report generation entirely instead of yielding the insufficient-evidence/hold decision the rest of the pipeline is designed to express for exactly this case.

Only report.test.ts exercises the throw, and only for the true-mismatch case (PAIRED_FACTS.slice(1) against a present paired-delta report) — there's no test for pairedDelta.report === null (lane missing/incomplete) reaching this adapter.

Might be worth distinguishing: if pairedDelta.report === null, return an outcome reflecting the report's own (already-correct) mandatoryEvidenceComplete: false rather than throwing, and reserve the throw for when a report is present but its pairedFactsFingerprint doesn't match.

@claude

claude Bot commented Sep 2, 2026

Copy link
Copy Markdown

Review summary

Reviewed the scorecard report/adapter/CLI stack (report.ts, report-contract.ts, run-scorecard.ts, tests, README). Overall the design is solid — self-validating builder (parseScorecardReport re-derives and cross-checks the outcome fields), atomic + privacy-gated publish, fail-closed adapter for foreign policy fingerprints. Two correctness gaps worth addressing before this feeds the real release pipeline (left as inline comments):

  1. promotionAllowed doesn't account for a schema-mismatch baseline. deriveOutcome in report-contract.ts computes promotionAllowed from gates/lanes/families/adverse-deltas only; the baseline-comparability check (baseline.status !== "schema-mismatch") is bolted onto scorecardExitCode alone. createScorecardAdapter.evaluate() reads report.body.outcome.promotionAllowed directly, so it inherits the gap — a corrupt/unreadable baseline file can still yield promotionAllowed: true from the adapter, even though the CLI would correctly exit 1. No test covers a schema-mismatch baseline through this path.

  2. createScorecardAdapter.evaluate() throws (uncaught) when the paired-delta report is simply missing, not just when it's tampered. pairedDelta.report === null and "facts don't match a present report" both raise adapter: evidence-pairs-mismatch, but buildProspectiveReport calls evaluate() with no try/catch — so a legitimately absent/incomplete paired-delta artifact (a normal LaneEvidence status elsewhere handled gracefully) crashes prospective report generation instead of producing insufficient-evidence/hold.

No security issues found beyond the existing privacy-scan gate, which looks correctly wired (publishScorecardReport refuses to write before checking scanForSensitiveContent). Test coverage otherwise looks thorough (54 + 125 passing, including the fingerprint-substitution and hard-gate-failed end-to-end cases called out in the PR description).

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 34cc3a237c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +67 to +68
evidence: { lanes, baseline: { status: bundle.baseline.status, reportFingerprint: bundle.baseline.reportFingerprint } },
outcome,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Make baseline comparability part of the release outcome

When the policy names a baseline but --baseline is omitted, unreadable, or fingerprint-mismatched, loadBaseline returns schema-mismatch, yet deriveOutcome does not receive that status and can still set promotionAllowed to true. The CLI happens to return 1, but createScorecardAdapter forwards this outcome directly, so the prospective report can promote once the gates pass despite having no comparable baseline. Make baseline comparability part of the outcome or mandatory-evidence calculation rather than only the CLI exit-code check.

Useful? React with 👍 / 👎.

Comment on lines +88 to +92
evaluate(holdout, policyFingerprint): ScorecardOutcome {
if (policyFingerprint !== bundle.policyFingerprint) throw new ScorecardContractError(["adapter: policy-fingerprint-mismatch"]);
const pairedDelta = laneEvidence(bundle, "paired-delta");
const pairedFacts = pairedFactsFingerprint(holdout.pairs);
if (pairedDelta.report === null || pairedDelta.report.body.analysis.pairedFactsFingerprint !== pairedFacts) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Bind the adapter to the supplied estimator result

When the holdout estimator comes from a different analysis of the same pairs and policy—for example, one with different bootstrap results—the paired-facts check passes while holdout.estimator is ignored. The prospective report then combines direction and evidence sufficiency from one analysis with a scorecard promotion decision derived from another, and recomputation with the same mismatched adapters will reproduce rather than detect it. Verify the supplied estimator result fingerprint against the paired-delta analysis used by this scorecard.

Useful? React with 👍 / 👎.

Comment on lines +321 to +323
```sh
gh run download <run-id> --name <artifact> --dir artifacts/
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize downloaded lane artifacts before scoring

Following this command does not produce the filenames or shapes consumed by loadEvidenceBundle: gh run download --help states that a selected artifact's contents are extracted without renaming, while the checked workflows upload names such as paired-delta-${mode}-report.json (paired-delta-eval.yml:126-160) and historian-eval-report.json (historian-eval.yml:118-127); the dreamer artifact is nested per-run JSON files (dreamer-eval.yml:67-77), and retrieval contains three reports (retrieval-benchmark.yml:72-99). Therefore the documented collect-then-score flow reports lanes as missing or malformed. Add a collector/normalizer, or document the required renames, dreamer array assembly, and retrieval report selection.

Useful? React with 👍 / 👎.

Comment on lines +57 to +59
export function runScorecard(args: ScorecardCliArgs, log: (line: string) => void = console.log): ScorecardExitCode {
try {
const bundle = loadEvidenceBundle(args.sources);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Remove a stale output before starting a scorecard run

When an operator reruns the command with the same --out path and evidence loading or privacy validation now fails, the catch path returns 2 without touching the previous report. That leaves an older, potentially promotion-allowed scorecard at the documented output path, where later archival or review can mistake it for the failed run's result. Remove or otherwise invalidate the destination before the first fallible validation step, as the incident runner does for the same stale-artifact hazard.

Useful? React with 👍 / 👎.

Comment on lines +84 to +85
export function createScorecardAdapter(input: { bundle: ScorecardEvidenceBundle; report: ScorecardReport }): ScorecardAdapter {
const { bundle, report } = input;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Verify the report was built from the adapter bundle

When callers pair a current evidence bundle with a valid report built from another bundle under the same policy, the factory accepts both independently. Evaluation then validates the policy and paired facts against the current bundle but copies gate failures, promotion state, and the report fingerprint from the unrelated report, producing a hybrid result that deterministic recomputation with the same adapter will accept. Rebuild the report inside the factory or verify its fingerprint and target/evidence projections against bundle before exposing the adapter.

Useful? React with 👍 / 👎.

adverseDeltas: comparison.adverseDeltas,
maxToleratedRegressions: bundle.policy.maxToleratedRegressions,
});
const limitations = [

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Deduplicate reason codes in limitations array to prevent contract validation failure

buildScorecardReport constructs limitations by concatenating reason codes from bundle.limitations, comparison.limitations, bundle.baseline.diagnostics, and conditional reason codes. When parseScorecardReport validates the report body, idArray(value.limitations, ...) calls unique(), which throws a contract error (report.body.limitations: duplicate) if any reason code is present in more than one source. Deduplicating the array (e.g. [...new Set([...])]) before constructing the body ensures the report adheres to contract uniqueness invariants.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

const value = argv[index + 1];
if (!KNOWN_FLAGS.includes(flag)) throw new Error(`unknown argument: ${flag}\n${USAGE}`);
if (value === undefined || value.startsWith("--")) throw new Error(`${flag} requires a value\n${USAGE}`);
if (values.has(flag)) throw new Error(`${flag} given twice`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Short flags like -h are not detected as missing parameter values

Line 30 checks value.startsWith("--") to detect missing flag values. If the command is invoked with a parameter followed immediately by a short flag (such as run-scorecard.ts --freeze -h), "-h" is not recognized as a missing value because it does not start with "--". Consequently, "-h" is accepted as the value for --freeze and the help request is ignored. Checking value.startsWith("-") properly flags both short and long options passed as argument values.

Suggested change
if (values.has(flag)) throw new Error(`${flag} given twice`);
if (value === undefined || value.startsWith("-")) throw new Error(`${flag} requires a value\n${USAGE}`);

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

const hex = "a".repeat(64);
const base = ["--freeze", "freeze", "--freeze-fingerprint", hex, "--artifacts", "artifacts", "--out", "out.json"];

it("resolves paths, defaults the policy locations to the e2e root, and accepts an optional baseline", () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Incomplete CLI flag and path assertions in parseArgs test

The parseArgs test suite does not assert args.sources.artifactsDir, args.out, or the default resolution for args.sources.policies.analysisPath. In addition, custom path overrides for --policies and --paired-delta-policy, as well as missing-flag validation for --artifacts and --out, are currently not exercised.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

: { ...row, status: "passed", observedCount: 0, evidenceFingerprint: H1, sourceLane: "incident", diagnostic: null });
body.outcome.hardGateFailures = [];
body.limitations = body.limitations.filter((code) => code !== "hard-gates-unobserved");
body.outcome.promotionAllowed = body.outcome.mandatoryEvidenceComplete && body.outcome.blockingRegressionCount <= bundle.policy.maxToleratedRegressions;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Test helper allGatesPassed bypasses deriveOutcome calculation

allGatesPassed directly re-implements and assigns body.outcome.promotionAllowed = body.outcome.mandatoryEvidenceComplete && body.outcome.blockingRegressionCount <= bundle.policy.maxToleratedRegressions on the cloned report. This means test cases calling allGatesPassed assert against the helper's manual assignment rather than testing deriveOutcome's computation of promotionAllowed when safety gates pass.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
packages/e2e-tests/src/scorecard/report.ts 57 Deduplicate reason codes in limitations array to prevent contract validation failure
packages/e2e-tests/scripts/run-scorecard.ts 30 Short flags like -h are not detected as missing parameter values
packages/e2e-tests/src/scorecard/report.test.ts 38 Test helper allGatesPassed bypasses deriveOutcome calculation

SUGGESTION

File Line Issue
packages/e2e-tests/scripts/run-scorecard.test.ts 61 Incomplete CLI flag and path assertions in parseArgs test
Files Reviewed (6 files)
  • packages/e2e-tests/README.md
  • packages/e2e-tests/scripts/run-scorecard.test.ts - 1 issue
  • packages/e2e-tests/scripts/run-scorecard.ts - 1 issue
  • packages/e2e-tests/src/scorecard/report-contract.ts
  • packages/e2e-tests/src/scorecard/report.test.ts - 1 issue
  • packages/e2e-tests/src/scorecard/report.ts - 1 issue

Fix these issues in Kilo Cloud


Reviewed by gemini-3.7-flash · Input: 376.6K · Output: 22.5K · Cached: 779.8K

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