Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/commands/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Before touching any version, run a multi-agent review of everything merged since
3. **TypeScript best practices** — no unjustified `any`/`as`; exhaustive switches; no floating promises; ESM import correctness; closed-shape outputs (no schema-invalid passthroughs); matches the eslint config's intent.
4. **Go best practices** — error wrapping (`%w`), no swallowed errors, `omitempty` consistency, struct-tag correctness, context usage, no goroutine leaks; matches the 39-linter `golangci-lint` intent.
5. **Cross-PR consistency & regression** — for the PRs merged since `BASE`: did any two touch the same area inconsistently? Are shared-code changes reflected in *all* consumers? Is Go↔TS parity preserved where both exist? Did any PR reintroduce something another removed, or silently regress a third? **Do NOT flag the CHANGELOG as missing/out-of-date — the new-version section is authored later, in Phase 5, so its absence at review time is by design, not a finding.** The one CHANGELOG-adjacent thing worth surfacing is a *consumer-visible behavior change* that Phase 5 must call out loudly (report it as a low-severity note so Phase 5 remembers it — not as a blocking gap).
6. **Docs / README accuracy (full-surface, not diff-scoped).** For every package `README.md` (root, `hdf-cli`, `hdf-diff`, and each `@mitre/hdf-*` package), verify what it documents still matches reality: every documented command/subcommand and flag actually exists in the current CLI (`hdf <cmd> --help`) or public API; no *removed* command or renamed syntax is still shown; example invocations use real flags; and any embedded example output is faithful to a real run (statuses, counts, column headers, summary lines — not fabricated or stale). Because drift here predates the release window, this dimension inspects the **current** binary/API surface, not just `BASE..HEAD`. Every mismatch = a finding naming the README, the stale claim, and the correct current form.
6. **Docs / README accuracy (full-surface, not diff-scoped).** For every package `README.md` (root, `hdf-cli`, `hdf-diff`, and each `@mitre/hdf-*` package), verify what it documents still matches reality: every documented command/subcommand and flag actually exists in the current CLI (`hdf <cmd> --help`) or public API; no *removed* command or renamed syntax is still shown; example invocations use real flags; and any embedded example output is faithful to a real run (statuses, counts, column headers, summary lines — not fabricated or stale). Because drift here predates the release window, this dimension inspects the **current** binary/API surface, not just `BASE..HEAD`. Every mismatch = a finding naming the README, the stale claim, and the correct current form. Also confirm the generated converter catalog page (`site/docs/guides/converters.md`, rendered by `site/generate-converters.mjs` from the committed, registry-golden-tested `site/data/converters.json`) is current: if any converter was added or removed this cycle, regenerate the manifest (`go test ./cmd/hdf/cmd -run TestConverterCatalogManifest -update-catalog`) and confirm the golden test then passes — the catalog must list exactly the registry's converters.

**Orchestration** — use the `Workflow` tool (this instruction is the multi-agent opt-in). Fan out one finder per dimension (shard dimension×package when the diff is large), adversarially verify each finding with an independent skeptic prompted to *refute* (drop unless it survives — this kills best-practice nitpicks and hallucinated issues), then synthesize a deduped report grouped by dimension and severity. Pass `BASE`, the changed-file list, and the PR list in via `args`. Skeleton:

Expand All @@ -107,7 +107,7 @@ const DIMENSIONS = [
{ key: 'ts', prompt: `${ctx}\n\nReport TypeScript best-practice violations in the changed .ts files (unjustified any/as, non-exhaustive switch, floating promises, bad ESM imports, schema-invalid passthroughs).` },
{ key: 'go', prompt: `${ctx}\n\nReport Go best-practice violations in the changed .go files (unwrapped/swallowed errors, omitempty drift, struct-tag errors, context misuse, goroutine leaks).` },
{ key: 'crosspr', prompt: `${ctx}\n\nPRs merged since ${args.base}:\n${args.prs}\n\nReport cross-PR inconsistencies/regressions: same area touched inconsistently, shared-code change not reflected in all consumers, broken Go/TS parity, one PR reverting/regressing another. Do NOT report a missing or out-of-date CHANGELOG — its new-version section is written later in Phase 5, so its absence now is expected. The only CHANGELOG-adjacent finding worth raising is a consumer-visible BEHAVIOR CHANGE Phase 5 must document loudly — report that as a low-severity note, not a blocking gap.` },
{ key: 'docs', prompt: `Ignore the diff scope for this one — audit the CURRENT state. For every package README.md (root, hdf-cli, hdf-diff, each @mitre/hdf-* package), verify documented commands/subcommands/flags still exist in the real CLI (build ./hdf and run 'hdf <cmd> --help') or public API, that no removed/renamed command or syntax is still shown, and that any embedded example output is faithful to a real run (status labels, counts, headers, summary lines). Report each mismatch with the README path, the stale claim, and the correct current form.` },
{ key: 'docs', prompt: `Ignore the diff scope for this one — audit the CURRENT state. For every package README.md (root, hdf-cli, hdf-diff, each @mitre/hdf-* package), verify documented commands/subcommands/flags still exist in the real CLI (build ./hdf and run 'hdf <cmd> --help') or public API, that no removed/renamed command or syntax is still shown, and that any embedded example output is faithful to a real run (status labels, counts, headers, summary lines). Also verify the generated converter catalog (site/docs/guides/converters.md, from site/data/converters.json) matches the live registry: run the hdf-cli golden test 'go test ./cmd/hdf/cmd -run TestConverterCatalogManifest' and report a finding if it fails (manifest stale — regenerate with -update-catalog). Report each mismatch with the README/page path, the stale claim, and the correct current form.` },
]

phase('Review')
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ hdf-cli/hdf
site/schemas/*.md
site/v*/
site/.vitepress/versions.json
# Converter catalog page — rendered from site/data/converters.json (which IS
# committed and golden-tested against the registry) by generate-converters.mjs.
site/docs/guides/converters.md
# Per-version raw schema archive IS committed (backs the canonical $id
# URL e.g. /schemas/hdf-amendments/v3.2.0/ and the per-version rendered
# docs). ~70 KB per release; storage cost trivial. See site/seed-archive.mjs.
Expand Down
103 changes: 103 additions & 0 deletions hdf-cli/cmd/hdf/cmd/converter_catalog_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package cmd

import (
"bytes"
"encoding/json"
"flag"
"os"
"path/filepath"
"sort"
"testing"
)

// updateCatalog rewrites the committed converter manifest instead of asserting
// against it. Run after adding or removing a converter:
//
// go test ./cmd/hdf/cmd -run TestConverterCatalogManifest -update-catalog
var updateCatalog = flag.Bool("update-catalog", false, "rewrite site/data/converters.json from the live registry")

// catalogEntry is one registered source→dest conversion, with the metadata the
// docs catalog page renders.
type catalogEntry struct {
Source string `json:"source"`
Dest string `json:"dest"`
Name string `json:"name"`
AcceptsEmpty bool `json:"acceptsEmpty"`
}

// catalogManifest is the serialized shape of site/data/converters.json. It also
// carries the BOM inventory formats that flow through `hdf system create` (into
// an HDF System doc, not Results) so the catalog page can point SPDX/AIBOM users
// at the right command instead of leaving them to conclude those are unsupported.
type catalogManifest struct {
Converters []catalogEntry `json:"converters"`
SystemBomFormats []string `json:"systemBomFormats"`
}

// catalogManifestPath resolves the committed manifest relative to this package
// (go test runs with the package dir as its working directory).
func catalogManifestPath() string {
return filepath.Join("..", "..", "..", "..", "site", "data", "converters.json")
}

// buildCatalog snapshots the live converter registry and the system-BOM import
// formats into the manifest shape.
func buildCatalog(t *testing.T) catalogManifest {
t.Helper()
pairs := ListConverters()
entries := make([]catalogEntry, 0, len(pairs))
for _, p := range pairs {
conv, err := GetConverter(p.Source, p.Dest)
if err != nil {
t.Fatalf("registered pair %s→%s has no retrievable converter: %v", p.Source, p.Dest, err)
}
e := catalogEntry{Source: p.Source, Dest: p.Dest, Name: conv.Name()}
if ae, ok := conv.(EmptyInputAccepting); ok {
e.AcceptsEmpty = ae.AcceptsEmptyInput()
}
entries = append(entries, e)
}
sort.Slice(entries, func(i, j int) bool {
if entries[i].Source != entries[j].Source {
return entries[i].Source < entries[j].Source
}
return entries[i].Dest < entries[j].Dest
})

bomFormats := append([]string(nil), bomFormatAliases...)
sort.Strings(bomFormats)

return catalogManifest{Converters: entries, SystemBomFormats: bomFormats}
}

// TestConverterCatalogManifest keeps site/data/converters.json — the source the
// docs site renders the converter catalog page from — in lockstep with the live
// registry. Adding or removing a converter fails this test until the manifest is
// regenerated (-update-catalog), so the published catalog can never silently
// drift from what the CLI actually supports.
func TestConverterCatalogManifest(t *testing.T) {
manifest := buildCatalog(t)
got, err := json.MarshalIndent(manifest, "", " ")
if err != nil {
t.Fatalf("marshal catalog: %v", err)
}
got = append(got, '\n')

path := catalogManifestPath()
if *updateCatalog {
if err := os.WriteFile(path, got, 0o600); err != nil {
t.Fatalf("write manifest: %v", err)
}
t.Logf("wrote %d converters + %d system BOM formats to %s", len(manifest.Converters), len(manifest.SystemBomFormats), path)
return
}

want, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read manifest (%s): %v\nregenerate with: go test ./cmd/hdf/cmd -run TestConverterCatalogManifest -update-catalog", path, err)
}
if !bytes.Equal(got, want) {
t.Errorf("converter catalog manifest is stale — the registry and site/data/converters.json disagree.\n" +
"regenerate with: go test ./cmd/hdf/cmd -run TestConverterCatalogManifest -update-catalog")
}
}
Loading
Loading