Skip to content

fix(usai-catalog): preserve and validate catalog role recommendations - #553

Merged
wz-gsa merged 4 commits into
mainfrom
fix/419-catalog-recommendations-contract
Oct 30, 2026
Merged

wz-gsa merged 4 commits into
mainfrom
fix/419-catalog-recommendations-contract

Conversation

@mogul

@mogul mogul commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

🤖: AI agent (First Mate), AI-assisted change (OpenCode). Human review required before merge.

Merge status

Merged main after #551; no longer stacked. The diff against main is now only this PR's own changes: integrations/providers/usai/{README.md, scripts/build-catalog.mjs, tests/recommendations.test.mjs} and a docstring correction in scripts/tests/test_usai_catalog_schema.py. Merged with a plain git merge (no rebase, no force-push).

Summary

Part of #419 (slice S1). Makes the USAi catalog's recommendations (role defaults) a validated, preserved, deterministic part of the catalog pipeline, so a catalog refresh can no longer silently delete them. catalog.json is unchanged.

Eligibility & Accountability

  • Federal employee or contractor attestation: for the human author of record to complete.
  • Author understands the change and can explain it: for the human author of record to complete.
  • Right-to-release (CC0-1.0) attestation: for the human author of record to complete.

AI Assistance Disclosure

  • AI tools were used: OpenCode (First Mate agent) drafted the change, tests and this description.
  • Human verification of AI-generated content: pending reviewer.

Type of Change

  • Bug fix
  • Infrastructure/tooling change

Context

Found in the #419 audit and re-verified on main@7dd2ddc0:

  • build-catalog.mjs neither produced nor preserved recommendations. A bootstrap run to a temp file dropped the block, and a fixture refresh did the same.
  • recommendations was missing from KEY_ORDER, so serialize(JSON.parse(catalog.json)) did not reproduce the committed file.
  • validateCatalog did not check role names or that role ids exist in models[]. A dangling id plus a made-up role produced 0 errors.
  • describeDrift ignored recommendations.
  • The schema omitted summary and title. That is fixed by fix(usai-catalog): allow summary and title roles in catalog recommendations schema #551, not here.

Plan (what changed)

  • scripts/build-catalog.mjs
    • KEY_ORDER: recommendations after sources, roles in fixed order.
    • validateCatalog: role names come from schema.properties.recommendations.properties; each value must be a non-empty string naming a model in models[]; guards against non-object blocks.
    • Carry-forward: readExistingRecommendations reads the catalog at --out; carryForwardRecommendations keeps the values and fails closed. Applies to the live/fixture and bootstrap paths and to --check.
    • describeDrift: reports recommendations.<role>: "old" -> "new".
  • tests/recommendations.test.mjs: 31 offline tests (fixtures only, temp output paths, no network).
  • README.md: new "Role defaults" section.
  • Review fix: the file at --out must be absent or a real catalog (object, schemaVersion usai-model-catalog/v1, models array). [], null, 123, "text", {}, a wrong schemaVersion, or a catalog without models[] now fail before any write and leave the file untouched. Previously valid JSON without a recommendations key was read as "no catalog yet" and overwritten. A real catalog with no recommendations yet is still accepted (no roles to lose; roles are only added by a person); the shipped catalog always has them.
  • scripts/tests/test_usai_catalog_schema.py: docstring corrected, because the hand-written validator now enforces role names and referential ids.

Blast radius: 4 files.

Decisions recorded (owner, binding for later slices)

  • D4: role defaults carry forward. When a recommended model disappears the refresh fails with an actionable message naming each role and id, writes nothing, and a human re-picks. There is no priority-list auto-selection (the idea in the stale draft PR 495 is deliberately not carried).
  • D1: the lifecycle ADR (slice S2) will state that repo-time authoring refresh is out of scope for ADR 0002 and is not a runtime component.
  • D2: no USAi API key in this repo's Actions secrets; the later drift workflow is offline-only and reports BLOCKED for the live check.
  • D3: no Prime Agent work now. Nothing Prime Agent was touched; its drift test is unaffected because catalog.json is unchanged.
  • D5 to D7 apply to later slices.

From PR 495 this carries only fail-closed referential validation of role ids against models[]. Deterministic key order was already mostly in serialize; this finishes it for recommendations.

Verification

Local run on this branch (head 6ffada1):

Check Result
New tests against the base script (old fails) 17 of 22 failed on the first push; for the review fix, 8 of 9 new non-catalog-output tests failed on the pre-fix code (the 9th, a valid catalog with no roles, documents behavior that already held)
New tests with the change (new passes) 31 passed
catalog.json re-serialization byte-identical yes (test "re-serializing the committed catalog is byte-identical")
catalog.json modified in this PR no
node scripts/build-catalog.mjs --check "catalog.json is up to date (24 models)"
providers/usai suite 153 passed
make validate pass
make test-kits (opencode 62, usai-provider 18, paseo 65, providers/usai 153) all pass
PYTHONPATH=scripts pytest scripts/tests 740 passed
ruff check / ruff format --check on scripts/ pass
make validate-kits, make validate-provider-input-contract, make generate-check pass

Independent read-only AI reviews (blockers only): NO BLOCKERS on the original change, NO BLOCKERS on the merge of main plus the review fix. Non-blocking notes: with --check the role lines in describeDrift are reachable only through a direct call, because carry-forward copies the existing roles into the fresh catalog; the non-catalog tests exercise write mode, and --check shares the same code path.

CI results: see the checks on this PR.

Rollback

Revert the commit. catalog.json is unchanged, so no data needs restoring.

Security Impact

None to authentication, authorization or attack surface. No credential handling changed and no secret value appears in the diff. The carry-forward only reads and writes the catalog path the build already uses. The fail-closed paths reduce the chance of shipping a catalog with missing defaults.

Related: #357.

mogul and others added 2 commits October 29, 2026 03:17
…ations schema

catalog.schema.json restricted recommendations to primary/small/compaction
while the shipped catalog.json and the OpenCode renderer also use summary
and title. Add both roles and a schema-level test (real JSON Schema
validator) so schema, data, and renderer vocabulary stay aligned.

Co-authored-by: OpenCode Agent <user@gsa.gov>
A write-mode build of the USAi catalog neither produced nor kept the
`recommendations` block, so refreshing the catalog silently deleted the role
defaults that the OpenCode kit renders. The block also serialized in a
different position than the committed file, the validator did not check role
names or that the ids exist, and the drift check could not see a change.

- Serialize `recommendations` after `sources`, with roles in a fixed order, so
  re-serializing the committed catalog is byte-identical.
- validateCatalog: roles must come from the schema, and every value must name
  a model in models[].
- Regeneration carries the existing recommendations forward. If a recommended
  model is no longer in the model list, or the existing catalog cannot be read
  as JSON, the build fails before writing and a person re-picks. There is no
  automatic selection.
- describeDrift names recommendation changes.
- Document the behavior in the catalog README.

catalog.json is unchanged.

Co-authored-by: OpenCode Agent <bret.amogilefsky@gsa.gov>

@basiliskus basiliskus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: request changes — c8e9b8f2c1a7962d26e7711cab8261f049b1e898

readExistingRecommendations() treats any valid JSON without a recommendations key as if no existing catalog exists. I reproduced this with an output file containing []: write mode exited successfully, replaced it with a 24-model catalog, and omitted recommendations. That contradicts the fail-closed contract at integrations/providers/usai/scripts/build-catalog.mjs:1162-1164 and can still silently erase the curated role defaults this PR is intended to preserve. Please validate that an existing output is actually a catalog (or minimally a plain object with the expected catalog identity/shape) before carrying recommendations forward.

Minor: scripts/tests/test_usai_catalog_schema.py:3-4 says the hand-written validator does not enforce recommendation role names, but this PR now adds that enforcement.

Assisted-by: OpenCode [usai/gpt-5.6-sol-latest-guardrails-defaultv2] skill:change-review

mogul and others added 2 commits October 30, 2026 00:24
readExistingRecommendations treated any valid JSON without a
`recommendations` key as "no catalog yet", so a write-mode build over a file
containing `[]` replaced it with a role-less catalog and exited 0. That
defeated the fail-closed carry-forward contract.

The file at the output path must now be absent or a real catalog (object,
`usai-model-catalog/v1`, models[] array). Anything else fails before any
write and leaves the file untouched. A real catalog that has no
`recommendations` yet is still accepted and documented as such.

Also correct the schema test docstring: the hand-written validator now
enforces recommendation role names and referential ids.

Co-authored-by: OpenCode Agent <bret.amogilefsky@gsa.gov>
@mogul

mogul commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

🤖: AI agent (First Mate), posting on behalf of @mogul. Reply to the review at c8e9b8f2.

Fixed in 9f926348. readExistingRecommendations now requires the file at --out to be absent or a real catalog (a JSON object with schemaVersion usai-model-catalog/v1 and a models array); anything else fails before any write with "cannot carry recommendations forward", and the file is left byte-identical. Covered by tests for invalid JSON, [], null, 123, a string, {}, a wrong schemaVersion, a missing models, and a non-array models (8 of the 9 new tests fail on the previous code; the ninth documents the accepted case below). A valid catalog that has no recommendations yet is accepted and written without roles: there are no roles to lose, the build never invents them, and the shipped catalog always carries them, so removing the block is a visible diff; this is documented in the catalog README. The docstring in test_usai_catalog_schema.py is corrected. This head also merges main (after the schema-fix PR landed) with no conflicts; the diff against main is four files and catalog.json is unchanged. Known edge: a dangling symlink at --out reads as missing; left as is.

@mogul
mogul marked this pull request as ready for review October 30, 2026 00:57
@mogul
mogul requested a review from a team as a code owner October 30, 2026 00:57

@basiliskus basiliskus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: approve — 9f9263486f3d87562a6ade8256f43bf9da3056d6

The previous non-catalog overwrite finding is resolved: eight regression cases fail on the prior head and pass here, including byte-preservation assertions. The validator docstring finding is also resolved. No new findings.

Nit: integrations/providers/usai/README.md:48-49 permits editing recommendations while line 15 forbids all hand-editing. Qualify line 15: “Model data is generated; only curated recommendations are hand-edited.”

Live USAi refresh was not exercised. CI job/step statuses are successful at this head; raw logs were unavailable.

Assisted-by: OpenCode [usai/gpt-6.1-sol-latest-guardrails-defaultv2] skill:change-review

@wz-gsa
wz-gsa added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 228cc4b Oct 30, 2026
7 checks passed
@wz-gsa
wz-gsa deleted the fix/419-catalog-recommendations-contract branch October 30, 2026 15:08
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.

3 participants