This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Bomly is a customer-facing, security-sensitive CLI for dependency intelligence. Audience: professional developers, security managers, and CI workflows. Expect high standards: correct behavior, clear output, full logging, and no panics.
This is the main public repository for the Bomly CLI: the engine, auditors, and native detectors (internal/*), the cmd/bomly entry point, user documentation (docs/), release automation, install scripts, the npm MCP wrapper, and the binary-driven smoke test suite. Two kinds of modules live outside this repository:
github.com/bomly-dev/bomly-sdk(public, separate repo): the contract both built-in components and external managed plugins implement — domain types, plugin kinds, validation, support metadata, and the shared helper subpackages (system,filecache,logkit,detectorkit,matcherkit,testkit). It has its own tests and releases; this repo pins released versions. Any reference tosdk.<Type>below means that module. Plugin authors start there, withdocs/PLUGINS.md,docs/plugins/, and the public plugin template repo.github.com/bomly-dev/bomly-plugin-*(public, one repo per component): external-integration components consumed as ordinary pinned Go modules — the four reachability analyzers (govulncheck,jsreach,pyreach,jvmreach), theosv/depsdev-license/scorecard/grypematchers, and thesyftdetector. Their implementations are NOT underinternal/; changes to them happen in their repos, and Dependabot bumps the pins here. Auditors and all other detectors are Bomly's own logic and stay in this repository.
make build # build both `bin/bomly` (builtin Syft/Grype) and `bin/bomly-lite`
make build-lite # go build -tags "bomly_external_syft,bomly_external_grype" -o bin/bomly-lite ./cmd/bomly
make test # go test ./...
make smoke # end-to-end tests driving the built binary (slow, requires network)
make smoke ARGS="-update" # regenerate smoke golden files
make verify # everything that gates a push; writes .verify-stamp
make verify SMOKE=1 # the same, including the network-driven smoke suite
make fuzz FUZZTIME=5s # run every registered fuzz target with a short per-target budget
make benchmark # run the hidden local dependency-graph benchmark
make benchmark-report # analyze local benchmark artifacts with Copilot CLI
make evidence # verify the public evidence catalog (test/evidence/cases.json)
make run ARGS="scan" # go run ./cmd/bomly <ARGS>
make generate # regenerate config reference, JSON schemas, schema docs, support matrix, and component docs (binary-driven)Always run make verify before pushing or updating a pull request; it runs formatting, lint, vet and build on both build variants, the unit suite, and the generated-docs drift check. All of it must pass before marking work done.
.githooks/pre-push refuses a push unless make verify has passed since the
last source change (git config core.hooksPath .githooks, or make install-hooks, enables it). The check is a stamp read, not a test run: a
six-minute hook gets bypassed, and a bypassed hook enforces nothing. Smoke is
not required by default because it needs the network and several minutes --
run make verify SMOKE=1 when a change touches detector output, and set
BOMLY_REQUIRE_SMOKE=1 to make the hook insist on it. git push --no-verify
skips the gate deliberately.
If you change internal/cli/config.go, internal/output/*, or internal/registry/support.go, or bump the pinned bomly-dev/bomly-sdk version (its catalog or support-matrix data feeds the generated docs), also run make generate and commit the docs drift.
go.mod pins released versions and must not contain replace directives on main (CI enforces this), so remote go install github.com/bomly-dev/bomly-cli/cmd/bomly@latest stays supported. External component modules (bomly-plugin-*) are ordinary pinned dependencies bumped by Dependabot. Local cross-repo development: go work init . ../bomly-sdk (never commit go.work).
Development may happen inside Git worktrees. Always run commands in the active worktree directory. Do not assume the primary checkout path; use paths relative to the current worktree. Avoid destructive Git operations that can affect sibling worktrees or shared refs.
See dev-docs/ARCHITECTURE.md for full detail (the public overview is docs/ARCHITECTURE.md). Component map:
| Package | Role |
|---|---|
cmd/bomly |
Entry point — calls internal/cli.Execute() |
internal/cli |
Cobra root + all commands (scan, explain, diff, plugin, version) |
sdk (external module) |
Unified domain types: Dependency (detection graph nodes), Package (PURL-keyed matching artifacts in PackageRegistry), Vulnerability (OSV-aligned), reference-style Finding, plus neutral package/ecosystem/support identifiers. See dev-docs/MODELS.md. |
internal/detectors |
Detector contracts, descriptors, requests/results, and detector-only helpers |
internal/engine |
Pipeline, engine, consolidation, auditors, matchers, and orchestration |
internal/registry |
Canonical support/discovery registry and built-in engine registry wiring |
internal/detectors/* |
Concrete native dependency resolution per ecosystem (gomod, gradle, maven, node, python, sbom); the Syft catch-all detector lives in bomly-plugin-syft-detector |
bomly-plugin-* (external modules) |
External-integration components consumed as pinned Go modules: enrichment matchers (osv, grype, deps.dev license, scorecard), reachability analyzers (govulncheck, jsreach, pyreach, jvmreach), and the Syft detector; ClearlyDefined and eol run as external matcher plugins; the shared cache lives in bomly-sdk/filecache |
internal/auditors/* |
Policy evaluators and audit-only logic (policy, noop) |
internal/graphview |
Reads a graph for presentation and publication: a node's published package URL, the children a document can name, top-level parents |
internal/testnodes |
Test-only fixture builders for graph nodes (panic on an unbuildable fixture); label lookups delegate to bomly-sdk/testkit |
internal/baseline |
Portable package-finding baseline codec and audit-integrated policy-status resolver |
internal/remediation |
Canonical vulnerability fix status, version, detector-hint validation, and occurrence suggestions |
internal/sbom |
SBOM codec (SPDX 2.3, CycloneDX) |
internal/benchmark |
Hidden local dependency-graph benchmark, baseline comparison, scoring, and embedded presets |
internal/output |
Output rendering plus structured command payloads and schema generation for scan, diff, explain, JSON, and SARIF 2.1.0 |
internal/plugin |
Plugin discovery, protocol, handshake, and pooled subprocess execution |
internal/composition |
Build-variant composition: wires the full (builtin Syft/Grype) and lite component sets |
internal/engine/diff |
Diff pipeline orchestration and audit delta classification |
internal/engine/explain |
Dependency path traversal (explain command) |
internal/engine/scan |
Scan command pipeline API |
internal/logging |
Zap console wrapper (subprocess logging helpers live in bomly-sdk/logkit) |
internal/support |
Docs generation (config reference, schemas, support matrix, component docs) behind the hidden bomly internal docs-gen command |
Scan pipeline: runtimePreparation → subprojectDiscovery (root-only by default; --recursive walks nested dirs) → detect (per-package-manager chains; resolve + consolidate into one graph; detectors may record CI-readiness resolution warnings on manifests) → scopeFilter → match (package enrichment, vulnerability consolidation, and remediation derivation) → analyze (reachability, when --analyze is set) → audit (including finding policy-status resolution) → format. Consolidation is the tail of the detect stage, and remediation derivation is the tail of enrichment; neither is a separate stage.
Runtime preparation is owned by internal/engine: build the filtered registry once, index the execution target with that same registry, and reuse the prepared runtime for scan, diff, explain, license enrichment, and auditing. The CLI resolves raw execution targets and flags, but it must not discover subprojects with a separate registry.
bomly explain is implemented by newExplainCmd in internal/cli/explain_cmd.go.
- Component kinds are Detector, Matcher, Auditor, and Analyzer; every built-in implements the same SDK contract an external plugin implements.
- External plugins run through a pooled subprocess runtime: one warm subprocess per enabled plugin per command, lazy start, at most one restart on death, always terminated when the command finishes.
- Plugin configuration is kind-scoped:
plugins:config is nested by component kind (detectors / matchers / auditors / analyzers) and keyed by component name; legacy flat plugin-ID keys are accepted with a deprecation warning. - Per-ecosystem detectors are consolidated packages with host-owned chains — e.g.
internal/detectors/nodehosts the npm/pnpm/yarn/bun sub-detectors, and detector name aliases keep old--detectorsselections working. - Build composition lives in
internal/composition(composition_full.go/composition_lite.gobehind build tags); register new built-ins there and ininternal/registry/builder.go.
- Shared helper code (bounded filesystem/subprocess ops, file cache, subprocess logging, detector/matcher helpers, test kit) lives in
bomly-sdksubpackages:system,filecache,logkit,detectorkit,matcherkit,testkit. Do not reintroduce CLI-internal copies. - External-integration components — the reachability analyzers (
bomly-plugin-{govulncheck,jsreach,pyreach,jvmreach}-analyzer), the external enrichment matchers (bomly-plugin-{osv,depsdev-license,scorecard,grype}-matcher), and the Syft detector (bomly-plugin-syft-detector) — live in their own public repositories and are consumed as ordinary Go modules:requireentries pinned in rootgo.mod, bumped by Dependabot like any other dependency. Each repo carries its own tests, fuzz targets, and releases. - Each module's
pluginpackage exposes the embedded constructor surface (Config/DefaultConfig/New, or a plain struct literal) plus aModule()export for managed plugin execution;internal/compositionandinternal/registryconstruct them exactly like the old in-tree packages. The grype and syft modules carry both build-tag variants — the root build'sbomly_external_syft/bomly_external_grypetags select files inside those modules. - Auditors and native detectors stay CLI-internal (
internal/auditors/*,internal/detectors/*). New external-integration components start from the public plugin template repo, get their ownbomly-plugin-*repository, and are wired intointernal/composition(orinternal/registryfor detectors) as a pinned module. - Descriptor names are the compatibility contract: goldens, detector aliases, and generated docs key on them, so component repos must not rename descriptors without a coordinated CLI change.
internal/detectors/*must not importinternal/engineorinternal/registry. Concrete detectors may depend oninternal/detectors(name constants), the SDK and its helper subpackages (systemfor bounded filesystem and subprocess operations,detectorkitfor shared detector helpers), and local helpers.- Built-in reachability analyzers live in their own
bomly-plugin-*-analyzerrepositories, consumed as pinned Go modules. They depend only on the SDK and its helper subpackages (system,filecache,logkit) and must not import anyinternal/*package. internal/detectorsowns detector-facing contracts such asDetector,DetectorDescriptor,ResolveGraphRequest, and detector helper functions.- The SDK owns neutral shared identifiers and support metadata that would otherwise create package cycles, including ecosystems, package managers, detector types, and support-matrix data.
- Reading a node of any kind -- coordinates, display name, version, narrowing over the sealed union -- is the SDK's:
sdk.NodeCoordinates,sdk.NodeDisplayName,sdk.NodeVersion,sdk.AsDependencyNode,sdk.DependencyNodesOf,sdk.IsProjectOwned. Building or mutating a detector graph isbomly-sdk/detectorkit:EnsureNode,PromoteToModule,PropagateScopes. Do not reintroduce a CLI-local copy of either — both were CLI stopgaps until bomly-sdk v0.9.0 and were deleted when it shipped. internal/graphviewowns the three questions every renderer and exporter asks of a node: what package URL it publishes, which of its children a document can actually name (structural nodes are stepped through, never named), and which nodes count as top-level parents. It is a leaf -- SDK only -- so the SBOM codec, the renderers, and the TUI all reach it without depending on each other. A copy per surface is what this replaces, and every one of those copies had shipped a defect the others had already fixed.internal/testnodesis test-only: it routes fixture shapes through the real node constructors, panicking rather than taking atesting.TBso a table entry stays one expression. Label lookups ("name@version" to the canonical package URLs node IDs now are) delegate tobomly-sdk/testkit— the matching rules have one home, not two. Non-test code must not import it.internal/baselineowns the baseline document and matching implementation. It depends on the SDK policy contracts and must not be imported byinternal/engine.internal/remediationowns canonical vulnerability remediation decisions. Detectors may supply validated read-only strategy hints, but they do not choose final actions or versions.- SPDX license expression handling is
bomly-sdk/spdxkit's: validation, identifier classification, composition, deprecated-ID canonicalization, andLicenseRef-*minting. The underlying parser panics on some malformed input, and license strings come from untrusted lockfiles and registry APIs, so no package underinternal/may importgithub.com/github/go-spdxdirectly — the kit carries the panic guard.TestNoDirectSPDXExpressionUse(ininternal/detectors/guards_test.go) enforces this across the whole tree, test files included. The CLI's owninternal/licenseexprwrapper is deleted; it duplicated the kit function for function. internal/registryowns package-manager discovery, support lookups, and built-in registry wiring ininternal/registry/builder.go. Do not create or reintroduce a separateregistrybuilderpackage.internal/enginemay importinternal/detectorsandinternal/registry, but detector packages must not point back intointernal/engine. Runtime planning, prepared subprojects, and detector-chain reuse belong ininternal/engine.
- Do not add PM installation logic. Assume package managers exist.
- Plugin protocol is versioned
v1. External plugins use the SDK/HashiCorp gRPCMetadataand role descriptor contract. - No secrets or credentials in logs. Ever.
- Matcher network calls require explicit enrichment. Built-in matchers may contact OSV (
https://api.osv.dev), CISA KEV, deps.dev (https://api.deps.dev), OpenSSF Scorecard (https://api.scorecard.dev), and Grype's database service (https://grype.anchore.io/databases, plus the archive URL it returns) only during--enrich. Installed external matcher plugins such as ClearlyDefined and endoflife.date may contact their documented services during--enrich.--auditevaluates existing package data and must not trigger matcher calls. Remote Git targets and build-tool detectors have separate, explicit network behavior. - Record architecture decisions as ADRs in
dev-docs/adr/. Copydev-docs/adr/TEMPLATE.md, take the next number, and add a row to the index.dev-docs/ARCHITECTURE.mdstays the architecture narrative;docs/ARCHITECTURE.mdis the public, user-facing overview. - Prefer
internal/. Add new packages insideinternal/unless there is a clear public API need; genuinely public contract surface belongs in the SDK module. - Standard library + Cobra + existing deps only. Do not add new dependencies without discussion.
When the same defect can recur at more than one call site, centralize the rule instead of patching the sites. A fix that has to be remembered will be forgotten, and the next occurrence is found by a reviewer or a user rather than by the codebase.
In practice:
- Two occurrences of one defect is the signal. The first is a bug; the second says the rule has no home. Give it one — a named helper, a shared entry point, or an invariant enforced where the data is created — and route every site through it.
- Name the concept, not the mechanics.
detectorkit.EnsureNode(g, node)says what the caller is doing — insert or return the existing node; a hand-written lookup-then-insert at each site says only what to type, and each copy decides duplicate handling differently. - Add a guard when the rule can be bypassed by writing it out by hand.
TestNodeInsertionGoesThroughTheSharedHelperfails if a lookup-then-insert reappears anywhere underinternal/, andTestExportNeverReadsResolvedURLfails if the export layer touches raw manifest values. A guard is cheap next to the review round it replaces. - The deepest home for shared meaning is the SDK (ADR-0040). When a fix
or feature touches what a shared domain object means — identity,
coordinates, PURLs, licenses, SBOM assertions, graph or merge semantics,
validation gates — it lands in
bomly-dev/bomly-sdkfirst and this repo consumes the new release. CLI-level is presentation, command surface, and orchestration (how Bomly uses the model); plugin-level is one external tool's integration specifics. "Only the CLI needs this today" is not a reason to keep model behavior local — a single consumer is how every drifted copy started. If the release schedule genuinely cannot absorb the SDK-first ordering, ship the local fix with the SDK issue already filed and linked from the code. - Say so when you decline. If centralizing is genuinely out of scope for
the change in hand, record why in an ADR under
dev-docs/adr/and what the durable fix would be, so the next person inherits the reasoning rather than the symptom.
- Use canonical shared types directly instead of creating local type aliases or re-exported constants just to rename them. For example, if
internal/output.Formatowns CLI output formats, downstream packages should store and compareoutput.Format/output.FormatJSONdirectly rather than introducingrender.OutputFormataliases.
return fmt.Errorf("operation context: %w", err) // always wrap with contextNo panics in normal flow. Only process-exit handling in cmd/bomly/main.go.
logger.Debug("osv: fetching vuln", zap.String("id", id))
logger.Info("auditor: found findings", zap.Int("count", n))
logger.Warn("cache miss", zap.Error(err))- Loggers may be
nil— always nil-check or usezap.NewNop()as the zero value. - Prefer compact one-line messages with
fmt.Sprintf(...)when a log only needs one or two fields. - Prefer structured zap fields when a log carries several values or benefits from a machine-readable context.
- Log everything relevant, but aggregate cache/API activity at the operation level by default. Prefer one summary log for a cache pass, API batch, or enrichment run over per-package hit/miss/request logs unless an individual item is required to explain a warning or error.
- No PII, no tokens, no credentials.
import cache "github.com/bomly-dev/bomly-sdk/filecache"
fc, _ := cache.NewFileCache(dir, 24*time.Hour)
key := cache.NewKey(purl, name, ecosystem, version) // SHA256
if v, ok := cache.Get[T](fc, key); ok { ... }
_ = cache.Set(fc, key, value)License and vulnerability matchers share the same cache API from bomly-sdk/filecache.
Cache failures are non-fatal — log a warning and continue without caching.
- Implement
detectors.Detectorfor concrete detectors, orengine.Auditor/engine.Matcherfor audit and license stages. - Detectors may implement
ReadyDetector,ApplicableDetector, andInstallFirstDetector; auditors and matchers have parallelReady*/Applicable*hooks. - Register built-ins in
internal/registry/builder.go, which wires concrete detectors, auditors, matchers, and plugin stages intoengine.Registry. - External enrichment is matcher-based; the osv, deps.dev license, scorecard, and grype matchers live in their own
bomly-plugin-*-matcherrepositories, and ClearlyDefined and endoflife.date run as external matcher plugins. - Detector chains are explicit in
internal/registry/support.goandinternal/registry/builder.go; do not infer priority from technique alone. - Some native detectors are build-tool-backed primaries (
pub-native,swiftpm-native,sbt-native) with committed-file fallbacks. Run the local benchmark and the smoke tests withdart,swift, orsbtonPATHbefore updating graph-shape expectations for those ecosystems.
- Use
internal/cli/render/ansi.gohelpers (Style,Wrap,StripANSI) — never raw escape codes inline. - Interactive TUI uses Bubbletea (
internal/cli/interactive.go) with theinteractiveModelinterface. - SARIF output via
internal/output— do not hand-craft SARIF JSON.
BOMLY_PROTOCOL=v1
BOMLY_CORE_VERSION=<semver>
BOMLY_CWD=<absolute path>
BOMLY_CONFIG=<path>Core passes these env vars. Plugin discovery: ~/.bomly/plugins/bomly-* overrides PATH.
- Every exported type/function has a doc comment.
- Unit tests for new logic; integration tests for new commands.
- Test helpers:
t.TempDir(),testkit.BuildGoBinary()(frombomly-sdk/testkit),httptest.NewServer(). - Generated docs are part of the contract: update
docs/CONFIG_REFERENCE.md,docs/schemas/*, anddocs/SUPPORT_MATRIX.mdviamake generatewhen their source packages change. - Fake binaries (npm, go, Gradle, plugin) are built in
TestMain— seeinternal/cli/root_test_main_test.go. - No test conditionally skipped without a recorded reason.
- Use plain language in documentation and user-facing text. Prefer short, direct sentences; explain necessary technical terms when they first appear.
- Every new or materially changed pure in-process parser for untrusted repository, configuration, baseline, SBOM, plugin, or analyzer data must have a native Go fuzz target. (The SDK's own parsers carry their fuzz targets in the SDK repo.)
- Bound fuzz input before parsing. Use
testkit.MaxFuzzInputSize(frombomly-sdk/testkit) unless the format needs a documented tighter limit. - Seed valid, malformed, and truncated inputs. Assert that parsing never panics and that repeated parsing has deterministic success or failure; graph producers must also call
testkit.RequireFuzzGraphValid. - Register every new fuzz target in
scripts/run-fuzz.shso bothmake fuzzand the scheduled.github/workflows/fuzz.ymlworkflow execute it. - When a parser is command-backed, delegated entirely to the standard library, or otherwise unsuitable for native fuzzing, record the exclusion and reason in
test/assurance/PARSER_FUZZING.md. - When fuzz targets or their runner manifest change, run the focused target and
make fuzz FUZZTIME=5s.
Smoke tests (test/smoke/, make smoke) drive the built binary end-to-end against real public repositories pinned via --url --ref:
- Scan cases come from
test/smoke/testdata/scan_targets.json; keep it in sync withinternal/benchmark/testdata/scan_targets.json(the benchmark target list) when cases change. - Pin every scan case's detectors with
--detectors; normalize volatile fields inhelpers_test.go::normalizeJSONbefore goldens. - Register new tests in both slice matrices (
smoke.ymland exactly one slice inupdate-smoke-goldens.yml);go test -runelements are unanchored regexes — use$anchors to keep slice ownership exact. TestExamplePluginFixtureCompilesruns inmake testand must keep compiling against the pinnedbomly-dev/bomly-sdkrelease; update the fixture source when the SDK contract changes.
When adding a new user-visible feature (new CLI flag, new component class, new pipeline stage, new analyzer, etc.), walk this checklist before requesting review. The surfaces forgotten most often are MCP, plugin command, and smoke test.
If the change adds an input, network client, subprocess, plugin role, output path, MCP field, or automatically discovered repository file, also complete the security assurance review checklist.
- Flag declared in
internal/cli/opts/flag_options.gowith override propagation inapplyFlagOverrides. - Config field added to
internal/config/config.goResolved(withdoc:/env:/default:tags) and the appropriate nestedFileleaf (withyaml:,resolved:, and legacy flat-keylegacy:tags plus a pointer-backed shape). - Flag interactions (requires / conflicts / modifies semantics) get a check in
config.Validateplus a unit test ininternal/config/validate_test.go. Error messages must be actionable ("--audit requires --enrich", not"invalid combination"). - If the flag drives a pipeline stage, propagate the value through
internal/cli/opts/options.go'sPipelineRequestbuilder. - If the flag accepts a selector list, register an
available<Thing>Optionshelper inflag_options.gofor shell completion.
Every new flag on bomly scan / bomly explain / bomly diff must be reachable from the matching MCP tool. AI agents won't get the feature otherwise.
- Add the field to
ScanRequest/ExplainRequest/DiffRequestininternal/mcp/server.go. - Register the
mcplib.WithBoolean/WithStringargument intool_scan.go/tool_explain.go/tool_diff.go. Mirror the CLI flag's help text and call out any prerequisite ("requires enrich"). - Wire the field through
mcpOptionsAdapterininternal/cli/mcp_cmd.go. Add it to themcpOverridesstruct (so future additions stay one-line) and apply it incloneWithOverrides. - MCP tool responses are compact projections (
internal/mcp/types_compact.go, schemamcp/1), not the CLI JSON documents. If the flag adds response data, decide whether it belongs in the compact shape (respect the size caps and truncation counters) or only in the CLI document.
When adding a new component class (a new sibling of Detector / Matcher / Auditor / Analyzer):
- Add a
PluginKind*constant in the SDK'splugin.goand accept it in the SDK'svalidate.go::ValidateMetadata(SDK repo change, released and pinned here). - Add the descriptor pointer to
internal/plugin/types.go::Manifestplus aclone<Kind>Descriptorhelper that deep-copies every slice field. - In
internal/cli/plugin_cmd.go:- Extend
pluginKindFilterand add a--<kind>sfilter flag. - Iterate the new descriptors in
builtInPluginInfos; emit onePluginInfoper registered instance. - Add a
<kind>PluginInfoconstructor and the matching local clone helper. - Extend
pluginInfoEcosystems,pluginInfoPackageManagers, andpluginInfoFeatureswith the new case. - Add a new section to
renderPluginListTableswith sensible columns. If the descriptor exposes axes the existing kinds don't (e.g. analyzers haveSupportedLanguages), add new columns and correspondingpluginInfo<X>/join<X>helpers. - Update
renderPluginInfoto emit any new lines when present.
- Extend
External plugin install/load (gRPC handshake, runtime descriptor fetch) is a separate, larger change and can land in a follow-up PR. Built-in listing is the minimum bar.
Analyzers, matchers, auditors, and any new long-running stage must be observable at -v (INFO) and debuggable at -vv (DEBUG):
- INFO at natural boundaries: stage start (with key inputs — module count, item count, runner name, cache enabled), per-major-unit completion (cache hit/miss, counts per outcome, duration), final summary (totals, overall duration).
- DEBUG for low-level detail: discovered inputs (module roots, manifest paths), exact command lines including args and working dir, cache key components, byte counts of subprocess output, branch decisions worth reproducing.
- WARN for recoverable errors (analyzer failed, cache write failed). Never abort the pipeline for these; degrade and continue.
When invoking subprocesses, the DEBUG line MUST include the binary path, args, and working dir so a user with -vv can copy/paste the command to reproduce outside Bomly.
If a new analyzer / matcher / detector produces deterministic output for a fixed (input, schema version) pair, wrap it with bomly-sdk/filecache.FileCache:
- Cache key folds: schema version (so we can bump and invalidate), input fingerprint (lockfile content hash), runtime version when the underlying tool is sensitive to it, and the runner name when multiple implementations exist.
- Default location:
~/.cache/bomly/<area>/<subarea>/. - Default TTL: 24h (matches OSV / EOL).
- Cache failures are non-fatal — log a warning and proceed.
- Expose
CacheDir,CacheTTL, andDisableCachefields on the component for tests + opt-out.
Any new user-visible feature needs a smoke case under test/smoke/ — follow the golden/normalizer/slice-matrix rules in the Smoke tests section above.
make generateregeneratesdocs/CONFIG_REFERENCE.md,docs/schemas/*,docs/SUPPORT_MATRIX.md, and the component docs through the built binary. Run it wheneverinternal/config/config.goorinternal/output/*change, or when the pinned SDK version (catalog / support-matrix data) is bumped.- Add or update a feature page under
docs/(e.g.docs/REACHABILITY.md) with quick-start usage, semantics, ecosystem coverage, output shape, and limitations. Be explicit about safety caveats (e.g. "tier-3 unreachable does not mean safe"). dev-docs/ARCHITECTURE.md: update the pipeline diagram if the stage list changed; keep the publicdocs/ARCHITECTURE.mdoverview in sync when stages change. Add an ADR underdev-docs/adr/for non-obvious design choices (copyTEMPLATE.md, next number, index row).CLAUDE.mdandAGENTS.md: update the architecture tree and package-boundary list when introducing a new internal package.
Releases are cut deliberately, never by merging. Run the Auto Version workflow from main and choose the bump (patch / minor / major); it rewrites var version in cmd/bomly/main.go, commits, and pushes the vX.Y.Z tag. Pushing that tag is what triggers Release, which runs GoReleaser with signed checksums and SLSA provenance; see dev-docs/RELEASE_CHECKLIST.md.
Nothing about a merge to main starts a release, and no commit prefix chooses the bump — a feat!: squash title does not make the next release major. The person dispatching the workflow picks that, so choosing it is a decision, not a consequence.
| Doc | Covers |
|---|---|
dev-docs/ARCHITECTURE.md |
Full architecture: pipeline, detectors, auditors, plugins, trust model |
dev-docs/adr/ |
Architecture decision records — one file per decision, plus template and index |
docs/ARCHITECTURE.md |
Public, user-facing architecture overview |
dev-docs/MODELS.md |
Domain model reference: Dependency, Package, Vulnerability, Finding, PackageRegistry |
dev-docs/CI.md |
CI setup and workflow (GitHub Actions) |
docs/CONFIG_REFERENCE.md |
Generated config reference (all keys, env vars, defaults) |
docs/SUPPORT_MATRIX.md |
Ecosystem detector coverage |
docs/schemas/*.json, docs/schemas/*.md |
Generated JSON schemas and human-readable output docs for scan, diff, and explain |
CONTRIBUTING.md |
Development setup, conventions, testing |