Skip to content

refactor: SPDX expression handling is the SDK's, not a local wrapper (phase 2.2) - #428

Merged
bomly-guy merged 1 commit into
mainfrom
claude/phase-2.4-licenseref
Sep 5, 2026
Merged

refactor: SPDX expression handling is the SDK's, not a local wrapper (phase 2.2)#428
bomly-guy merged 1 commit into
mainfrom
claude/phase-2.4-licenseref

Conversation

@bomly-guy

Copy link
Copy Markdown
Collaborator

Phase 2.2, which the train skipped — and the prerequisite for 2.4.

What was wrong

internal/licenseexpr exposed Valid, ValidateAll, Identifier, Compose, Satisfies, Extract. bomly-sdk/spdxkit exposes the same six, with the same signatures. Two implementations of one specification's semantics, and the second one is the authoritative home the SDK program built for exactly this.

I found it while starting 2.4: nothing under internal/ imported spdxkit at all.

What changes

  • The call sites (internal/auditors/license, internal/sbom/{transform,spdx23,cyclonedx}) move to the kit.
  • internal/licenseexpr is deleted; github.com/github/go-spdx drops to indirect.
  • The panic guard survives — the kit carries it — so the rule it protected changes shape rather than disappearing. It was "route through the local wrapper"; it is now "do not import the parser at all". TestNoDirectSPDXExpressionUse moves to internal/detectors/guards_test.go alongside the other tree-wide structural guards, and now scans test files too, since a test reaching the parser directly proves the same crash is reachable.
  • Package tree and boundary rules updated in CLAUDE.md, AGENTS.md, dev-docs/ARCHITECTURE.md, and test/assurance/PARSER_FUZZING.md.

Why it blocks 2.4

Everything #410 asks for is already in spdxkitMintLicenseRef (SHA-256 of whitespace-normalized text, 128-bit hex, SPDX-idstring-safe), Classify, Compose, LicenseRefsIn, ReplaceLicenseRef — and its doc comments cite the issue by number. Building LicenseRef emission on the local wrapper would have meant hand-rolling identifier sanitization and collision-avoidance next to a library that already owns both.

Delegation check

  • SPDX expression validation, classification, composition, deprecated-ID canonicalization, LicenseRef-* minting: bomly-sdk/spdxkit — delegated, local duplicate deleted.
  • Panic containment for github.com/github/go-spdx: the kit — enforced tree-wide by TestNoDirectSPDXExpressionUse.

Verification

make verify SMOKE=1 green, zero golden drift — the check that mattered, since Compose is the one function where parenthesization could have differed between the two implementations. make fuzz FUZZTIME=5s clean, including FuzzSPDXLicenseValue. Guard mutation-checked by importing go-spdx into internal/sbom.

🤖 Generated with Claude Code

Phase 2.2, which the train skipped: internal/licenseexpr exposed Valid,
ValidateAll, Identifier, Compose, Satisfies and Extract, and bomly-sdk/spdxkit
exposes the same six with the same signatures. Two implementations of one
specification's semantics is the drift this program exists to remove, and it
was sitting directly under the next phase's work.

The call sites move to the kit, the package is deleted, and go-spdx drops to
indirect. No golden moves: the two Compose implementations agree on every
fixture the smoke suite produces, which is the check that mattered, since
composition is the one function where parenthesization could have differed.

The panic guard survives the move -- the kit carries it -- so the rule it
protected changes shape rather than disappearing. It used to be "route through
the local wrapper"; it is now "do not import the parser at all", and
TestNoDirectSPDXExpressionUse moves to internal/detectors/guards_test.go where
the other tree-wide structural guards live. It scans test files too, because a
test reaching the parser directly proves the same crash is reachable and is
where the temptation lives. Mutation-checked by importing go-spdx into
internal/sbom.

This is the prerequisite for 2.4 (#410): LicenseRef-* minting,
hasExtractedLicensingInfos, and mixed-validity composition are all already in
spdxkit -- MintLicenseRef, Classify, Compose -- and its doc comments cite the
issue by number. Building that on the local wrapper would have meant
hand-rolling identifier sanitization and collision-avoidance beside a library
that already owns both.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 37 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 88166194-5d15-40d8-9fa2-2fac0ba2d8f9

📥 Commits

Reviewing files that changed from the base of the PR and between 6adc734 and c5fffcc.

📒 Files selected for processing (13)
  • AGENTS.md
  • CLAUDE.md
  • dev-docs/ARCHITECTURE.md
  • go.mod
  • internal/auditors/license/auditor.go
  • internal/detectors/guards_test.go
  • internal/licenseexpr/licenseexpr.go
  • internal/licenseexpr/licenseexpr_test.go
  • internal/sbom/codec_fuzz_test.go
  • internal/sbom/cyclonedx.go
  • internal/sbom/spdx23.go
  • internal/sbom/transform.go
  • test/assurance/PARSER_FUZZING.md

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.

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Bomly Diff Summary

Compared 909825d3bbafe8ebf6e2b509ca634412257adeab to c5fffccad46e6a4220d637f8b68aca431859ddae.

Overview

Status Manifests Dependencies Findings Duration
✅ Pass +0 / ~1 / -0 0 added / 0 version changed / 1 detail changes / 0 removed 0 introduced / 0 persisted / 0 resolved 1m 24s

Dependency Changes

Summary: 0 added, 0 version changed, 1 detail change, 0 removed.

Dependency Detail Changes

Review: 0 of 1 detail change needs extra review.

Attention Package Version Changes
Info github.com/github/go-spdx/v2 v2.7.0 relationship: direct → transitive

Vulnerabilities

✅ No vulnerability changes.

License Changes

✅ No license changes.

Project Posture

✅ No project posture changes (--matchers +scorecard was not selected).

Policy Findings

✅ No policy differences were identified.

@bomly-guy
bomly-guy merged commit 5960876 into main Sep 5, 2026
16 checks passed
@bomly-guy
bomly-guy deleted the claude/phase-2.4-licenseref branch September 5, 2026 07:03
bomly-guy added a commit that referenced this pull request Sep 5, 2026
… (phase 2.4 + SDK v0.9.2) (#429)

* feat(sbom): unrecognized licenses export as LicenseRef, not free text

Closes #410. Phase 2.4.

SPDX 2.3 has no free-text license field: licenseDeclared must hold a valid
expression, NOASSERTION, NONE, or a LicenseRef. Bomly wrote an unrecognized
value verbatim -- "see LICENSE file" landed in licenseDeclared as-is --
producing a document a strict consumer can reject. Each such value now mints a
reference and the original text travels beside it in
hasExtractedLicensingInfos, which is what SPDX defines for this case and,
unlike NOASSERTION, keeps the information: ingest reads the text back.

The quieter half was the mixed-validity fallback. A set of one recognized and
one unrecognized license kept only the first and dropped the rest, which a
comment described as deliberate because composing free text would produce
something that does not parse. A LicenseRef is a valid expression element, so
the set composes whole now and nothing a source declared is lost.

Minting is bomly-sdk/spdxkit's, not this package's. A reference has to be
deterministic, collision-free across components assembled without
coordination, and confined to the characters the SPDX idstring grammar allows;
MintLicenseRef answers all three by hashing the whitespace-normalized text,
and a sanitizer written here would be a second, worse answer to a question the
SDK already settled. What stays local is policy: which values classify how,
where the extracted texts attach, and how ingest maps them back.

Ingest resolves only an atomic reference. A compound expression naming one
among other terms has no single text to become, and substituting free text
into it would produce something that no longer parses. A foreign document's
own reference-to-text pairing is taken as stated rather than re-minted: there
the document is the authority, not our hash.

docs/SBOM.md and ADR-0035 drop the limitation they recorded; the ADR gets a
dated note rather than a rewritten body, since it recorded a decision that was
true when made. The note also redirects its references to internal/licenseexpr,
which #428 deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: adopt bomly-sdk v0.9.2, retiring two CLI stopgaps

v0.9.2 closes both SDK issues this branch's predecessors filed, so the
workarounds they left behind can go.

sdk#43 landed as NodePURL. The projection existed twice here and deliberately
so -- internal/graphview for the renderers and internal/sbom for the codec,
because a codec importing the CLI's output layer inverts the layering -- with
both commented as converging on the accessor once it shipped. It shipped;
both delegate; the question every surface asks of a node has one answer now.
graphview keeps ChildrenAmong and TopLevelParentIDs, which are CLI policy
rather than model semantics.

sdk#39 landed as PEP 440 canonicalization before minting, so two spellings of
one PyPI release fold again. TestPythonVersionCaseIsNotFoldedYet existed to
make that gap visible rather than silent, and its failure message said to fold
the case back into TestEquivalentPythonSpellingsFoldOnInsertion and delete it
once a normalizer arrived. Done exactly that: the fold test carries the
"1.0.0RC1" spelling again.

No golden drift. The fixtures' PyPI versions are already canonical -- pip,
poetry and uv all write normalized versions -- so the change reaches only the
hand-written requirements case it was filed for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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