Skip to content

Bridge product facts v1 to typed v2 - #67

Merged
karlwaldman merged 12 commits into
mainfrom
codex/product-facts-v2-compat
Aug 11, 2026
Merged

Bridge product facts v1 to typed v2#67
karlwaldman merged 12 commits into
mainfrom
codex/product-facts-v2-compat

Conversation

@karlwaldman

@karlwaldman karlwaldman commented Aug 11, 2026

Copy link
Copy Markdown
Member

Summary

  • bridge the exact reviewed production product-facts v1 response to a coherent typed-v2 50/day payload, while accepting the canonical native-v2 contract after the atomic API rollout
  • reject unknown v1/v2 fields, noncanonical schema/product identity, future-dated remote reviews, checksum drift, and malformed Registry booleans before assigning reviewed provenance
  • remove stale catalog, plan, Free-entitlement, as_of, version, customer-cohort, and fixed tool-count claims from authored and packed public surfaces
  • harden npm and official MCP Registry publication with protected-main provenance, immutable actions/publisher/npm CLI inputs, exact artifact handoff, and bounded public readback
  • keep production API credentials out of all pull-request code, including same-repository pull requests
  • ship a deterministic, pinned, non-root container with exact facts/capabilities/provenance and live keyless stdio smoke
  • prepare release 3.2.1; manifest.json remains compatibility metadata only, not an MCPB artifact or MCPB release

TDD / red-green proof

  • product-facts exactness began red on unknown legacy/native fields, arbitrary schema/canonical identities, empty or mismatched checksums, future dates against the injected clock, inconsistent normalized schema, and raw-vs-canonical checksum drift
  • cache, stale bound, expiry, timeout, legacy checksum-option compatibility, native v2, and reviewed v1 negative paths are covered
  • checksum-bound pinned, canonical, and cached facts are deeply immutable; red tests prove one consumer cannot mutate a later delivery while retaining the old digest
  • public-claim tests began red on detector misses and exposed stale .mcp.json, README, source inventory, and public distribution-doc claims; recursive authored plus exact-packed scan is green
  • Registry negatives reject wrong identity/version/description/repository/transport/environment, extra fields, malformed present booleans, direct-invocation bypass, and stale latest state
  • npm negatives reject wrong integrity/version/name/latest, direct-invocation bypass, and missing or wrong attestation signature material, package subject, source repository, workflow, tag, and commit
  • release-provenance tests force a hostile inherited mode and reject unmerged/tag/ref drift
  • protected-main live-smoke tests fail when the synthetic key is absent, while explicit optional local runs retain a clean skip
  • release-job regression began red because the OIDC job executed npm install -g; the replacement receives a SHA-256/SRI-pinned npm CLI tarball from the unprivileged verify job and rejects all install/ci/exec commands
  • startup regression proves the previous fixed /32 denominator was wrong for write scope; unit and packed stdio smokes now derive the registered count and verify read 32/36 plus write 36/36
  • the original Dockerfile failed by downloading unrelated deprecated tsc@2.0.4; the replacement image passes non-root/provenance/facts/capability/CLI/live-stdio checks

Release safety

  • release event tag, checkout, and protected origin/main ancestry must agree
  • all release/backfill actions are pinned to full commit SHAs; the Node container is pinned by multi-arch digest
  • MCP publisher v1.8.1 is pinned to SHA-256 a06c9096dcb9727c13555b6be26c7effa707b01f06a4c561ba7a3635443cf2cc
  • npm CLI 11.12.1 is acquired before OIDC and bound by exact SRI plus SHA-256 e679850e663b16f5f146ee425d0eb0e3442c1d2bda3d513bbfd7c81f5ee5db38
  • tests, public compatibility, official publisher validation, packaging, and container smoke run before OIDC is granted
  • the OIDC npm job receives only the verified package, pinned CLI, metadata, and verifier; lifecycle scripts remain disabled and no dependency install/exec command is allowed
  • Registry OIDC jobs invoke built-in-only Node verifiers directly without package dependency execution
  • npm success requires exact public integrity, latest, signed Sigstore/SLSA material, package subject, source repository, workflow, release tag, and source commit
  • Registry success requires the complete normalized server.json contract plus active/latest status; npm and Registry remain honestly sequential and recoverable, not atomic

Exact verification

  • head 2b8f6979c592a2782fe54e518c75329d0a5fb8a1
  • npm ci: clean install
  • npm test: 7 files / 193 tests passed
  • npm audit --audit-level=low: 0 vulnerabilities
  • deterministic TypeScript build and release metadata: passed at exact head
  • recursive authored + exact npm tarball claim scan: 46 surfaces passed
  • product-facts tool/resource smoke: passed without an API key
  • live production compatibility: source v1.0.0 -> coherent schema v2 -> reviewed-v1-daily-bridge -> 50/day
  • packed CLI/config/doctor/capability/scope/protocol/startup-inventory smoke: passed
  • pinned multi-arch Node container: non-root, exact revision/timestamp/checksums, exact read-only CLI, live keyless MCP stdio passed at exact head
  • npm and MCP Registry mocked negative-path verifiers: passed
  • npm verifier independently accepted the public 3.2.0 release only after exact real attestation schema/source binding
  • protected-main provenance smoke, workflow contract, git diff --check, and clean worktree: passed
  • pull-request and manually dispatched feature-branch CI cannot receive OILPRICEAPI_TEST_KEY; the secret-bearing live smoke runs only from the repository default branch

Rollout boundary

Merge and release/cold-smoke MCP 3.2.1 against the current v1 API first. Only then may the atomic API product-facts/quota/auth deployment proceed, followed by a live MCP v2 smoke. No API deployment, customer send, account mutation, or MCPB publication is included here.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Version 3.2.1 updates product facts to schema v2, adds reviewed v1 normalization, removes fixed entitlement claims, introduces reproducible Node 22 containers, and separates verified npm and MCP registry publication with provenance and readback checks.

Changes

Product facts and entitlement surface

Layer / File(s) Summary
Product facts v2 contract and delivery
src/productFacts.ts, src/product-facts.v2.*, src/__tests__/productFacts.test.ts
Product facts now use typed request limits, strict validation, v2 checksums, and delivery metadata. Reviewed v1 inputs normalize to the v2 daily contract.
Account entitlement messaging
src/index.ts, src/__tests__/index.test.ts, README.md
Tool descriptions, errors, plans output, and documentation now use account-specific entitlement guidance and current product facts.

Container and release automation

Layer / File(s) Summary
Reproducible container build and smoke coverage
Dockerfile, scripts/docker-smoke.mjs, scripts/live-product-facts-smoke.mjs, scripts/clean-build.mjs
The Docker image uses pinned Node 22 builder and runtime stages, provenance labels, production dependencies, and a non-root user. Smoke tests validate image contents and keyless product-facts responses.
Release artifact and registry verification
scripts/verify-*.mjs, scripts/public-claims-smoke.mjs, .github/workflows/publish.yml, .github/workflows/registry-backfill.yml, package.json, server.json, .mcp.json
Release checks validate metadata, protected-main provenance, npm integrity and provenance, MCP registry metadata, public claims, and exact publication artifacts. Publication skips already-current releases and verifies public readback.
CI gates and structural checks
.github/workflows/live-tests.yml, .github/dependabot.yml, src/__tests__/directorySource.test.ts, .dockerignore, tsconfig.json
CI adds pinned actions, timeouts, audits, smoke checks, Docker dependency updates, repository exclusions, and workflow-structure assertions.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant VerifyJob
  participant NpmRegistry
  participant NpmPublish
  participant MCPRegistry
  VerifyJob->>NpmRegistry: verify release metadata and provenance
  VerifyJob->>MCPRegistry: validate expected server metadata
  VerifyJob->>NpmPublish: provide verified tarball
  NpmPublish->>NpmRegistry: publish tarball with provenance
  NpmPublish->>NpmRegistry: verify version, integrity, and latest tag
  NpmPublish->>MCPRegistry: publish when registry state is stale
  MCPRegistry-->>NpmPublish: return public release metadata
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary v1-to-v2 product facts bridge implemented by the pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/product-facts-v2-compat

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@karlwaldman
karlwaldman marked this pull request as ready for review August 11, 2026 13:34
@karlwaldman
karlwaldman force-pushed the codex/product-facts-v2-compat branch from c2453ae to 6144dc5 Compare August 11, 2026 14:47

@coderabbitai coderabbitai 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.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/__tests__/productFacts.test.ts (1)

216-241: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The injected clock does not control date validation.

The test injects now: () => Date.parse("2026-08-11T20:00:00Z"), but requiredIsoDate in src/productFacts.ts calls new Date() directly for the non-future check. The fixture dates are validated against the real wall clock. This test and the fixtures at line 24, line 25, line 38, and line 39 depend on the machine date. See the related comment on src/productFacts.ts lines 176-192 for the root cause.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/__tests__/productFacts.test.ts` around lines 216 - 241, Update
ProductFactsProvider date validation, specifically requiredIsoDate in
productFacts.ts, to use the injected now clock instead of constructing a
real-time Date directly. Ensure the test’s now value controls validation of the
fixture dates and remove any remaining dependence on the machine’s wall-clock
date in the affected fixtures and test.
🧹 Nitpick comments (12)
src/__tests__/productFacts.test.ts (1)

31-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive nativeV2Facts from the pinned artifact, not from the v1 downgrade.

nativeV2Facts calls legacyV1Facts, which deletes the typed allowance fields, and then restores freeRequestLimit: 50 and freeRequestWindow: "day". The fixture therefore hardcodes the allowance instead of reading it from PINNED_PRODUCT_FACTS. If the pinned artifact later changes the limit or the window, this fixture keeps the old values and the provider tests stop testing the shipped contract.

♻️ Proposed refactor
 function nativeV2Facts(): Record<string, unknown> {
-  const facts = legacyV1Facts();
-  const offer = facts.offer as Record<string, unknown>;
-  delete offer.freeRequestsPerMonth;
-  offer.freeRequestLimit = 50;
-  offer.freeRequestWindow = "day";
-  facts.schemaVersion = "2.0.0";
-  facts.contractVersion = "2026-08-11";
-  facts.reviewedAt = "2026-08-11";
-  facts.schemaUrl =
-    "https://api.oilpriceapi.com/schemas/product-facts-v2.schema.json";
-  return facts;
+  return cloneFacts();
 }

Then build legacyV1Facts from nativeV2Facts instead of the reverse.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/__tests__/productFacts.test.ts` around lines 31 - 43, Refactor the test
fixtures so nativeV2Facts() is derived directly from PINNED_PRODUCT_FACTS rather
than legacyV1Facts(), preserving the pinned allowance values without hardcoding
them. Then update legacyV1Facts() to derive from nativeV2Facts() and remove the
typed allowance fields for the v1 shape.
Dockerfile (1)

14-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The length check fails without a message.

Line 15 prints a reason when SOURCE_COMMIT is empty or contains a non-hex character. Line 17 fails silently when the value is hex but not 40 characters long. Fold the length into the pattern so every rejection prints the reason.

♻️ Proposed refactor
 RUN case "$SOURCE_COMMIT" in \
-      (*[!0-9a-f]*|'') echo "SOURCE_COMMIT must be a lowercase 40-character Git SHA" >&2; exit 1;; \
+      ([0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]) ;; \
+      (*) echo "SOURCE_COMMIT must be a lowercase 40-character Git SHA" >&2; exit 1;; \
     esac && \
-    test "${`#SOURCE_COMMIT`}" -eq 40 && \
     case "$SOURCE_DATE_EPOCH" in \
       (*[!0-9]*|'') echo "SOURCE_DATE_EPOCH must be an integer" >&2; exit 1;; \
     esac && \

A shorter alternative keeps the current structure and adds an explicit message:

-    test "${`#SOURCE_COMMIT`}" -eq 40 && \
+    { test "${`#SOURCE_COMMIT`}" -eq 40 || \
+      { echo "SOURCE_COMMIT must be 40 characters" >&2; exit 1; }; } && \
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` around lines 14 - 20, Update the SOURCE_COMMIT validation in the
Dockerfile RUN command so the 40-character length requirement is enforced within
the same rejection path as the lowercase hexadecimal check, ensuring invalid
lengths print the existing descriptive error before exiting. Remove the silent
standalone test while preserving validation of SOURCE_DATE_EPOCH.
scripts/verify-mcp-registry-release.mjs (2)

17-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Include the unexpected field names in the error message.

The current message reports only a count. This runs after publication in the release chain, so the operator needs the field names to diagnose registry drift quickly. The keys derive from a public server projection, so no secret is exposed.

♻️ Proposed improvement
 function assertOnlyKeys(record, allowedKeys, name) {
   const allowed = new Set(allowedKeys);
   const unexpected = Object.keys(record).filter((key) => !allowed.has(key));
   if (unexpected.length > 0) {
-    throw new Error(`${name} returned unsupported fields (${unexpected.length})`);
+    throw new Error(
+      `${name} returned unsupported fields: ${unexpected.sort().join(", ")}`,
+    );
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/verify-mcp-registry-release.mjs` around lines 17 - 23, Update
assertOnlyKeys to include the unexpected field names in the thrown Error
message, while preserving the existing count and validation behavior. Format the
names from the unexpected array so release operators can identify the
unsupported registry fields directly.

146-152: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the promise resolver to avoid shadowing the resolve import.

Line 149 names the executor parameter resolve, which shadows the resolve import from node:path at line 5. The behavior is correct today because the outer resolve is used only at line 159. The sibling script scripts/verify-npm-release.mjs already uses resolveDelay at line 98. Align the two files.

♻️ Proposed change
-      if (attempt < attempts) {
-        await new Promise((resolve) => setTimeout(resolve, delayMs));
-      }
+      if (attempt < attempts) {
+        await new Promise((resolveDelay) => setTimeout(resolveDelay, delayMs));
+      }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/verify-mcp-registry-release.mjs` around lines 146 - 152, Rename the
Promise executor parameter in the retry delay within the catch block of the
release verification flow from resolve to resolveDelay, matching
verify-npm-release.mjs and avoiding shadowing the node:path resolve import;
preserve the existing timeout behavior.
src/__tests__/directorySource.test.ts (1)

47-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reject every audit level weaker than low, not only moderate.

Line 57 rejects --audit-level=moderate only. A change to --audit-level=high or --audit-level=critical passes this test while weakening the gate. Assert that no audit level other than low appears.

♻️ Proposed change
     for (const source of [
       packageJson.scripts.prepublishOnly,
       liveWorkflow,
       publishWorkflow,
     ]) {
-      expect(source).not.toContain("--audit-level=moderate");
+      for (const match of source.matchAll(/--audit-level=(\w+)/g)) {
+        expect(match[1]).toBe("low");
+      }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/__tests__/directorySource.test.ts` around lines 47 - 59, Update the
“requires a literal zero-vulnerability audit gate” test to reject every explicit
audit level other than low across prepublishOnly, liveWorkflow, and
publishWorkflow, while preserving the required low-level assertions. Validate
each source so moderate, high, critical, or any other weaker level cannot pass.
scripts/verify-npm-release.mjs (1)

110-117: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Cross-check NPM_RELEASE_EXPECTED_VERSION against package.json.

scripts/verify-mcp-registry-release.mjs rejects a configured version that disagrees with server.json at lines 165-169. This script trusts the environment value. If the workflow exports a stale version, the verifier confirms the wrong release and reports success. Mirror the sibling check.

♻️ Proposed change
+import { readFile } from "node:fs/promises";
   const expectedName = process.env.NPM_RELEASE_EXPECTED_NAME;
   const expectedVersion = process.env.NPM_RELEASE_EXPECTED_VERSION;
   const expectedIntegrity = process.env.NPM_RELEASE_EXPECTED_INTEGRITY;
   if (!expectedName || !expectedVersion || !expectedIntegrity) {
     throw new Error(
       "NPM_RELEASE_EXPECTED_NAME, NPM_RELEASE_EXPECTED_VERSION, and NPM_RELEASE_EXPECTED_INTEGRITY are required",
     );
   }
+  const packageJson = JSON.parse(
+    await readFile(new URL("../package.json", import.meta.url), "utf8"),
+  );
+  if (
+    expectedName !== packageJson.name ||
+    expectedVersion !== packageJson.version
+  ) {
+    throw new Error(
+      `NPM_RELEASE_EXPECTED_NAME/VERSION=${expectedName}@${expectedVersion} did not match package.json=${packageJson.name}@${packageJson.version}`,
+    );
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/verify-npm-release.mjs` around lines 110 - 117, Update the validation
in verify-npm-release around expectedVersion to load the package version from
package.json and reject any configured NPM_RELEASE_EXPECTED_VERSION that differs
from it, mirroring the sibling verifier’s cross-check while preserving the
existing required-environment validation.
scripts/verify-npm-release-smoke.mjs (2)

8-12: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive the package name and version from package.json.

Lines 9-10 hard-code oilpriceapi-mcp and 3.2.1, and line 35 hard-codes the drift value 3.2.0. After the next version bump these literals go stale, and the smoke test still passes, so the drift stays invisible. The sibling test scripts/verify-mcp-registry-release-smoke.mjs already reads ../server.json at line 10. Use the same approach here.

♻️ Proposed change
+import { readFile } from "node:fs/promises";
 import {
   validateNpmVersionDocument,
   verifyNpmRelease,
 } from "./verify-npm-release.mjs";
 
+const packageJson = JSON.parse(
+  await readFile(new URL("../package.json", import.meta.url), "utf8"),
+);
+const staleVersion = `${packageJson.version}-stale`;
 const expected = {
-  expectedName: "oilpriceapi-mcp",
-  expectedVersion: "3.2.1",
+  expectedName: packageJson.name,
+  expectedVersion: packageJson.version,
   expectedIntegrity: "sha512-reviewed",
 };

Then replace the two "3.2.0" literals at lines 35 and 71 with staleVersion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/verify-npm-release-smoke.mjs` around lines 8 - 12, Update the smoke
test’s expected package metadata to derive the package name and version from
package.json, following the existing sibling test’s file-reading approach. Store
the derived version as staleVersion and replace both hard-coded "3.2.0" values
in the verification flow, while preserving the expectedIntegrity check.

29-57: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a mutation for a non-npm attestations.url.

validateNpmVersionDocument rejects an attestations.url that is not a string or does not start with https://registry.npmjs.org/ (scripts/verify-npm-release.mjs lines 39-40). No mutation here exercises that branch. This is the branch that binds the provenance record to the npm registry origin, so it should be covered.

♻️ Proposed change
   (document) => {
     document.dist.attestations.provenance.predicateType =
       "https://example.com/not-slsa";
   },
+  (document) => {
+    document.dist.attestations.url =
+      "https://registry.npmjs.org.example.com/attestations";
+  },
+  (document) => {
+    delete document.dist.attestations.url;
+  },
 ]) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/verify-npm-release-smoke.mjs` around lines 29 - 57, Add a mutation
case to the array in the npm metadata verification test that changes
document.dist.attestations.url to a non-npm URL, then rely on the existing
rejection assertion to verify validateNpmVersionDocument rejects it. Keep the
mutation isolated from the existing provenance, integrity, and package metadata
cases.
scripts/verify-mcp-registry-release-smoke.mjs (1)

75-83: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add mutations for a non-active status and for a nested unexpected field.

validateRegistryPayload rejects on status !== "active" as well as isLatest !== true, but only the isLatest branch is covered here. assertOnlyKeys also runs on server.repository, packages[].transport, and each environment variable, and none of those nested branches is exercised. Cover both so a weakened projection fails this smoke test.

♻️ Proposed change
+  withServerMutation((server) => {
+    server.packages[0].transport.unreviewedField = true;
+  }),
+  withServerMutation((server) => {
+    server.repository.unreviewedField = true;
+  }),
   {
     ...valid,
     _meta: {
       "io.modelcontextprotocol.registry/official": {
         status: "active",
         isLatest: false,
       },
     },
   },
+  {
+    ...valid,
+    _meta: {
+      "io.modelcontextprotocol.registry/official": {
+        status: "deleted",
+        isLatest: true,
+      },
+    },
+  },
+  { ...valid, _meta: {} },
 ]) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/verify-mcp-registry-release-smoke.mjs` around lines 75 - 83, Extend
the mutations in the smoke-test cases around validateRegistryPayload to include
a non-"active" official status and an unexpected nested field in a structure
checked by assertOnlyKeys, such as server.repository, packages[].transport, or
an environment variable. Assert both mutations are rejected so the status
validation and nested-key projection checks are covered.
.github/workflows/registry-backfill.yml (1)

46-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive the package name from package.json instead of hard-coding it.

Line 49 hard-codes oilpriceapi-mcp. Line 48 already reads the version from package.json. If the package is ever renamed or scoped, this step checks a different package and reports a false pass. .github/workflows/publish.yml Line 140 derives the name from the tarball, so the two workflows use different sources of truth.

♻️ Proposed refactor
       - name: Verify version exists on npm (registry must never lead npm)
         run: |
+          NAME=$(jq -r .name package.json)
           PKG=$(jq -r .version package.json)
-          PUBLISHED=$(timeout 30s npm view "oilpriceapi-mcp@$PKG" version)
+          PUBLISHED=$(timeout 30s npm view "$NAME@$PKG" version)
           if [ "$PUBLISHED" != "$PKG" ]; then
-            echo "::error::npm returned $PUBLISHED instead of $PKG"
+            echo "::error::npm returned '$PUBLISHED' for $NAME instead of $PKG"
             exit 1
           fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/registry-backfill.yml around lines 46 - 53, Update the
“Verify version exists on npm” step to read the package name from package.json
and use that value in the npm view command, alongside the existing version
lookup. Remove the hard-coded oilpriceapi-mcp identifier so registry-backfill
tracks the package metadata as its source of truth.
scripts/public-claims-smoke.mjs (1)

177-205: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Guard the scan so importing the module does not run it.

Line 69 exports findMutableClaims, which signals that unit tests import this file. Any import executes assertDetectorContract(), runs npm pack, and can throw at module scope. Run the scan only when the file is the entry point.

♻️ Proposed refactor
+import { argv } from "node:process";
...
-assertDetectorContract();
-
-const surfaces = [
-  ...
-process.stdout.write(
-  `Public-claims smoke passed across ${surfaces.length} source and packed surfaces.\n`,
-);
+export function runPublicClaimsSmoke() {
+  assertDetectorContract();
+  const surfaces = [/* ...unchanged... */];
+  // ...unchanged failure collection...
+  process.stdout.write(
+    `Public-claims smoke passed across ${surfaces.length} source and packed surfaces.\n`,
+  );
+}
+
+if (resolve(argv[1] ?? "") === fileURLToPath(import.meta.url)) {
+  runPublicClaimsSmoke();
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/public-claims-smoke.mjs` around lines 177 - 205, Wrap the top-level
smoke-scan execution, including assertDetectorContract(), surface collection,
failure checks, and success output, in an entry-point guard so it runs only when
scripts/public-claims-smoke.mjs is invoked directly. Keep findMutableClaims and
other exports import-safe for unit tests.
.github/workflows/publish.yml (1)

73-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The pinned MCP publisher install is copied into three steps. The download, checksum verification, extraction, and PATH update are identical in all three locations. A publisher version or checksum change must be applied to all three, and a missed copy produces a mismatched toolchain between the verify, registry, and backfill jobs. Extract the block into a local composite action, for example .github/actions/install-mcp-publisher/action.yml, with version, archive, and sha256 inputs.

  • .github/workflows/publish.yml#L73-L81: replace the inline install script with uses: ./.github/actions/install-mcp-publisher.
  • .github/workflows/publish.yml#L200-L208: replace the duplicated install script with the same composite action reference.
  • .github/workflows/registry-backfill.yml#L55-L63: replace the duplicated install script with the same composite action reference, and move the pinned MCP_PUBLISHER_* values into the action defaults or keep them as workflow-level inputs.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish.yml around lines 73 - 81, Extract the duplicated
MCP publisher installation into a local composite action, such as
.github/actions/install-mcp-publisher/action.yml, accepting version, archive,
and sha256 inputs and performing download, checksum verification, extraction,
and PATH setup. Replace the inline blocks at
.github/workflows/publish.yml#L73-L81 and
.github/workflows/publish.yml#L200-L208 with the action reference, and replace
the block at .github/workflows/registry-backfill.yml#L55-L63 likewise;
centralize the pinned MCP_PUBLISHER_* values in the action defaults or pass them
consistently as workflow-level inputs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/live-tests.yml:
- Around line 65-70: The live-test job condition currently allows secret-bearing
execution for same-repository pull requests; restrict the condition around the
smoke-test job so it runs only for post-merge events, not any pull request.
Update the adjacent comment to accurately state that behavior, while preserving
the existing checkout and secret-absence handling.

In @.github/workflows/publish.yml:
- Around line 213-223: Add an npm ci --ignore-scripts step after Node setup and
before the verification command in the registry job at
.github/workflows/publish.yml lines 213-223 and
.github/workflows/registry-backfill.yml lines 39-44; the publish workflow should
install dependencies before Check whether the public registry already matches,
and the backfill workflow before npm run verify:release-provenance.

In `@scripts/live-product-facts-smoke.mjs`:
- Around line 35-59: Require the exposed facts schema version in both canonical
smoke checks: in scripts/live-product-facts-smoke.mjs lines 35-59, update
assertReviewedContract to reject payloads when facts?.schemaVersion is not
"2.0.0"; in scripts/docker-smoke.mjs lines 181-193, add the equivalent
livePayload?.facts?.schemaVersion validation to the existing release-check
condition. Preserve the current allowance and delivery metadata checks.

In `@scripts/public-claims-smoke.mjs`:
- Around line 102-110: Update the source filter callback around `walkFiles` so
the test-directory and test/spec-file exclusions apply to the entire
allowed-extension condition by grouping the extension checks together. Normalize
`relative(repositoryRoot, path)` using the `sep` symbol from `node:path` before
checking for `__tests__`, ensuring the exclusion works on Windows as well as
POSIX systems.

In `@scripts/verify-npm-release.mjs`:
- Around line 108-109: Update the entrypoint guards in
scripts/verify-npm-release.mjs#L108-L109 and
scripts/verify-mcp-registry-release.mjs#L158-L159 to compare
realpathSync(process.argv[1]) with realpathSync(fileURLToPath(import.meta.url)),
preserving the existing release-check execution. Also make each script exit
non-zero when it is neither imported nor recognized as the entrypoint,
preventing silent successful no-ops.

In `@scripts/verify-release-provenance-smoke.mjs`:
- Around line 24-30: Update runVerifier to remove or explicitly neutralize the
inherited MCP_PROVENANCE_MODE before spawning the verifier, while preserving
environment overrides supplied through env. Ensure release assertions execute in
the default release mode regardless of the caller’s shell environment.

In `@src/__tests__/directorySource.test.ts`:
- Around line 89-91: Update the ordering assertion for mcp-publisher publish to
first assert its index is greater than -1, then verify it appears before
verify:mcp-registry-release, matching the existing explicit presence check for
mcp-publisher validate.
- Around line 123-130: Broaden the action-reference regexes in the “pins every
workflow action that participates in release proof” test to inspect every
non-local uses: reference, not only actions/ entries. Continue excluding local
workflow references, require each matched external reference to use a
40-character lowercase hexadecimal SHA, and preserve the existing checks across
publishWorkflow, backfillWorkflow, and liveWorkflow.
- Around line 26-34: Update the README text used by the “does not hard-code a
numeric post-trial Free allowance” test so whitespace is normalized before
applying the allowance regex, allowing matches across Markdown line breaks while
preserving the existing assertion and required URL/text checks.

In `@src/__tests__/index.test.ts`:
- Around line 1499-1536: Ensure the fetch stub created in the pricing test is
always restored, including when opa_get_plans.handler or an assertion fails.
Move vi.unstubAllGlobals into test lifecycle cleanup such as afterEach, or wrap
the stubbed execution and assertions in try/finally so cleanup runs
unconditionally.

In `@src/productFacts.ts`:
- Around line 176-192: Update requiredIsoDate to make the future-date validation
opt-in, and have loadPinnedProductFacts skip that gate when validating the
packaged artifact. In src/productFacts.ts lines 176-192, preserve format and
calendar validity checks while allowing callers to request the non-future check.
In src/__tests__/productFacts.test.ts lines 216-241, make fixture dates
independent of the machine clock and add coverage proving a future reviewedAt
from a remote response still throws.
- Around line 631-657: Update the pinned checksum guards in the validation flow
around stableFactsDigest to check options.pinnedChecksum !== undefined instead
of truthiness. Apply this consistently to the format validation,
projected-checksum comparison, default-checksum override check, and assignment
so an empty string is rejected rather than emitted as the provider checksum.
- Around line 636-659: Update the default branch in the pinned checksum
initialization to derive this contract checksum with
stableFactsDigest(pinnedValidation.facts), matching the algorithm used for
supplied pinned facts and other delivery sources; do not use
PINNED_PRODUCT_FACTS_CHECKSUM for this field, while preserving validation of any
explicitly supplied checksum against the same computed value.

---

Outside diff comments:
In `@src/__tests__/productFacts.test.ts`:
- Around line 216-241: Update ProductFactsProvider date validation, specifically
requiredIsoDate in productFacts.ts, to use the injected now clock instead of
constructing a real-time Date directly. Ensure the test’s now value controls
validation of the fixture dates and remove any remaining dependence on the
machine’s wall-clock date in the affected fixtures and test.

---

Nitpick comments:
In @.github/workflows/publish.yml:
- Around line 73-81: Extract the duplicated MCP publisher installation into a
local composite action, such as
.github/actions/install-mcp-publisher/action.yml, accepting version, archive,
and sha256 inputs and performing download, checksum verification, extraction,
and PATH setup. Replace the inline blocks at
.github/workflows/publish.yml#L73-L81 and
.github/workflows/publish.yml#L200-L208 with the action reference, and replace
the block at .github/workflows/registry-backfill.yml#L55-L63 likewise;
centralize the pinned MCP_PUBLISHER_* values in the action defaults or pass them
consistently as workflow-level inputs.

In @.github/workflows/registry-backfill.yml:
- Around line 46-53: Update the “Verify version exists on npm” step to read the
package name from package.json and use that value in the npm view command,
alongside the existing version lookup. Remove the hard-coded oilpriceapi-mcp
identifier so registry-backfill tracks the package metadata as its source of
truth.

In `@Dockerfile`:
- Around line 14-20: Update the SOURCE_COMMIT validation in the Dockerfile RUN
command so the 40-character length requirement is enforced within the same
rejection path as the lowercase hexadecimal check, ensuring invalid lengths
print the existing descriptive error before exiting. Remove the silent
standalone test while preserving validation of SOURCE_DATE_EPOCH.

In `@scripts/public-claims-smoke.mjs`:
- Around line 177-205: Wrap the top-level smoke-scan execution, including
assertDetectorContract(), surface collection, failure checks, and success
output, in an entry-point guard so it runs only when
scripts/public-claims-smoke.mjs is invoked directly. Keep findMutableClaims and
other exports import-safe for unit tests.

In `@scripts/verify-mcp-registry-release-smoke.mjs`:
- Around line 75-83: Extend the mutations in the smoke-test cases around
validateRegistryPayload to include a non-"active" official status and an
unexpected nested field in a structure checked by assertOnlyKeys, such as
server.repository, packages[].transport, or an environment variable. Assert both
mutations are rejected so the status validation and nested-key projection checks
are covered.

In `@scripts/verify-mcp-registry-release.mjs`:
- Around line 17-23: Update assertOnlyKeys to include the unexpected field names
in the thrown Error message, while preserving the existing count and validation
behavior. Format the names from the unexpected array so release operators can
identify the unsupported registry fields directly.
- Around line 146-152: Rename the Promise executor parameter in the retry delay
within the catch block of the release verification flow from resolve to
resolveDelay, matching verify-npm-release.mjs and avoiding shadowing the
node:path resolve import; preserve the existing timeout behavior.

In `@scripts/verify-npm-release-smoke.mjs`:
- Around line 8-12: Update the smoke test’s expected package metadata to derive
the package name and version from package.json, following the existing sibling
test’s file-reading approach. Store the derived version as staleVersion and
replace both hard-coded "3.2.0" values in the verification flow, while
preserving the expectedIntegrity check.
- Around line 29-57: Add a mutation case to the array in the npm metadata
verification test that changes document.dist.attestations.url to a non-npm URL,
then rely on the existing rejection assertion to verify
validateNpmVersionDocument rejects it. Keep the mutation isolated from the
existing provenance, integrity, and package metadata cases.

In `@scripts/verify-npm-release.mjs`:
- Around line 110-117: Update the validation in verify-npm-release around
expectedVersion to load the package version from package.json and reject any
configured NPM_RELEASE_EXPECTED_VERSION that differs from it, mirroring the
sibling verifier’s cross-check while preserving the existing
required-environment validation.

In `@src/__tests__/directorySource.test.ts`:
- Around line 47-59: Update the “requires a literal zero-vulnerability audit
gate” test to reject every explicit audit level other than low across
prepublishOnly, liveWorkflow, and publishWorkflow, while preserving the required
low-level assertions. Validate each source so moderate, high, critical, or any
other weaker level cannot pass.

In `@src/__tests__/productFacts.test.ts`:
- Around line 31-43: Refactor the test fixtures so nativeV2Facts() is derived
directly from PINNED_PRODUCT_FACTS rather than legacyV1Facts(), preserving the
pinned allowance values without hardcoding them. Then update legacyV1Facts() to
derive from nativeV2Facts() and remove the typed allowance fields for the v1
shape.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ce191838-a6c9-45ae-87cd-78b7dce12152

📥 Commits

Reviewing files that changed from the base of the PR and between 106d26d and c2453ae.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (35)
  • .dockerignore
  • .github/dependabot.yml
  • .github/workflows/live-tests.yml
  • .github/workflows/publish.yml
  • .github/workflows/registry-backfill.yml
  • .mcp.json
  • Dockerfile
  • README.md
  • docs/DISTRIBUTION_CONVERSION_PRD.md
  • manifest.json
  • package.json
  • scripts/clean-build.mjs
  • scripts/copy-product-facts.mjs
  • scripts/docker-smoke.mjs
  • scripts/live-product-facts-smoke.mjs
  • scripts/package-smoke.mjs
  • scripts/product-facts-smoke.mjs
  • scripts/public-claims-smoke.mjs
  • scripts/verify-mcp-registry-release-smoke.mjs
  • scripts/verify-mcp-registry-release.mjs
  • scripts/verify-npm-release-smoke.mjs
  • scripts/verify-npm-release.mjs
  • scripts/verify-release-metadata.mjs
  • scripts/verify-release-provenance-smoke.mjs
  • scripts/verify-release-provenance.mjs
  • server.json
  • src/__tests__/directorySource.test.ts
  • src/__tests__/index.test.ts
  • src/__tests__/productFacts.test.ts
  • src/index.ts
  • src/product-facts.v1.sha256
  • src/product-facts.v2.json
  • src/product-facts.v2.sha256
  • src/productFacts.ts
  • tsconfig.json
💤 Files with no reviewable changes (1)
  • src/product-facts.v1.sha256

Comment thread .github/workflows/live-tests.yml Outdated
Comment thread .github/workflows/publish.yml
Comment thread scripts/live-product-facts-smoke.mjs
Comment thread scripts/public-claims-smoke.mjs
Comment thread scripts/verify-npm-release.mjs Outdated
Comment thread src/__tests__/directorySource.test.ts
Comment thread src/__tests__/index.test.ts Outdated
Comment thread src/productFacts.ts Outdated
Comment thread src/productFacts.ts Outdated
Comment thread src/productFacts.ts Outdated
@karlwaldman
karlwaldman merged commit 10e78b2 into main Aug 11, 2026
8 checks passed
@karlwaldman
karlwaldman deleted the codex/product-facts-v2-compat branch August 11, 2026 15:46
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