Files
james.maes e116c60b88 Merge feature/GH-29-init-tui-wizard (#29): init full TUI wizard
# Conflicts:
#	CHANGELOG.md
#	cmd/license-tool/commands_test.go
#	docs/SESSION-STATE.md
2026-06-09 10:11:34 -05:00

427 lines
16 KiB
Go

// Package model defines the shared types and interfaces that flow between every
// other package in license-tool. It is deliberately dependency-free (stdlib only)
// so that config, enumerate, detect, render, resolve, policy, report, and applier
// can all depend on it without import cycles.
//
// WHY this package is frozen: downstream feature packages are built in parallel
// against these types. Changing a field or signature here ripples into every
// consumer, so the contract is fixed up front and treated as load-bearing.
package model
// License is a single SPDX-identified license, as vendored from the SPDX
// license-list-data snapshot and classified into a Category. Text is the full
// license body (for the top-level LICENSE / LICENSES/<id>.txt files); StandardHeader
// is the per-file notice block to embed in source headers when the style asks for it.
type License struct {
// SPDXID is the canonical SPDX identifier, e.g. "AGPL-3.0-or-later".
SPDXID string
// Name is the human-readable license name, e.g. "GNU Affero General Public License v3.0 or later".
Name string
// Category is the copyleft/permissive classification used by policy checks.
Category Category
// Text is the full, verbatim license text from the SPDX snapshot.
Text string
// StandardHeader is the per-file notice block (with placeholders or canonical
// wording) emitted when style includes a notice. Empty when the license defines none.
StandardHeader string
}
// Category classifies a license by the obligations it imposes, driving policy
// heterogeneity checks and the curated incompatibility table.
type Category int
const (
// CategoryUnknown is the zero value: classification could not be determined.
CategoryUnknown Category = iota
// CategoryPermissive covers MIT/Apache/BSD/ISC/Unlicense/CC0-style licenses.
CategoryPermissive
// CategoryWeakCopyleft covers file/library-scoped copyleft (LGPL, MPL).
CategoryWeakCopyleft
// CategoryStrongCopyleft covers project-scoped copyleft (GPL, AGPL).
CategoryStrongCopyleft
// CategoryNetworkCopyleft covers network-use copyleft (AGPL); AGPL is also strong.
CategoryNetworkCopyleft
// CategoryProprietary covers all-rights-reserved / non-open licenses.
CategoryProprietary
)
// String renders the Category as the lowercase, hyphenated token used in config
// and reports (e.g. "weak-copyleft").
func (c Category) String() string {
switch c {
case CategoryPermissive:
return "permissive"
case CategoryWeakCopyleft:
return "weak-copyleft"
case CategoryStrongCopyleft:
return "strong-copyleft"
case CategoryNetworkCopyleft:
return "network-copyleft"
case CategoryProprietary:
return "proprietary"
default:
return "unknown"
}
}
// Risk renders the obligation severity of a category as a coarse, sortable token
// ("high"|"medium"|"low"|"unknown"), so reports and machine consumers can branch on a
// single value instead of pattern-matching category names. Strong/network copyleft and
// proprietary are high; weak copyleft is medium; permissive is low.
func (c Category) Risk() string {
switch c {
case CategoryStrongCopyleft, CategoryNetworkCopyleft, CategoryProprietary:
return "high"
case CategoryWeakCopyleft:
return "medium"
case CategoryPermissive:
return "low"
default:
return "unknown"
}
}
// HeaderStyle selects which parts of the canonical header are emitted into a file.
type HeaderStyle int
const (
// StyleReuse emits only REUSE tags (SPDX-FileCopyrightText + SPDX-License-Identifier).
StyleReuse HeaderStyle = iota
// StyleNotice emits only the full standard notice block (no REUSE tags).
StyleNotice
// StyleReusePlusNotice emits REUSE tags followed by the full notice block (default).
StyleReusePlusNotice
)
// String renders the HeaderStyle as the config token (e.g. "reuse+notice").
func (s HeaderStyle) String() string {
switch s {
case StyleReuse:
return "reuse"
case StyleNotice:
return "notice"
case StyleReusePlusNotice:
return "reuse+notice"
default:
return "reuse+notice"
}
}
// YearKind enumerates how the copyright year in a header is determined.
type YearKind int
const (
// YearCurrent uses the current calendar year.
YearCurrent YearKind = iota
// YearExplicit uses a single fixed year (Start).
YearExplicit
// YearRange uses an explicit Start-End range.
YearRange
// YearGit derives a first-commit-year-to-current range from git history.
YearGit
)
// YearSpec captures the year policy. Kind selects the strategy; Start/End hold the
// explicit values for YearExplicit (Start only) and YearRange (Start and End).
type YearSpec struct {
Kind YearKind
Start int
End int
}
// YearResolver resolves a YearSpec to the concrete year string rendered into a
// header (e.g. "2021-2026"). The repoPath lets the git strategy inspect history;
// nowYear is injected so callers (and tests) control the notion of "current".
//
// Implemented by internal/render (or internal/gitutil); declared here so the
// interface is part of the frozen contract.
type YearResolver interface {
Resolve(repoPath string, nowYear int) (string, error)
}
// CommentStyle describes how to wrap header text into a file's comments. A file
// type is either block-commented (Block true, using Open/Close delimiters) or
// line-commented (Block false, using LinePrefix on every line).
type CommentStyle struct {
// Block selects block comments (Open/Close) over line comments (LinePrefix).
Block bool
// Open is the block-comment opener, e.g. "/*" (block styles only).
Open string
// Close is the block-comment closer, e.g. "*/" (block styles only).
Close string
// LinePrefix is prepended to each header line, e.g. "// " or "# " (line styles only).
LinePrefix string
}
// PreserveKind enumerates the leading constructs that must remain at the very top
// of a file; the license header is inserted after any present preserve-first
// constructs (and before constructs like a Go/Java package declaration).
type PreserveKind int
const (
// PreserveShebang is a "#!" interpreter line (shell, python, etc.).
PreserveShebang PreserveKind = iota
// PreserveXMLDecl is an "<?xml ... ?>" declaration.
PreserveXMLDecl
// PreservePHPOpen is a "<?php" opening tag.
PreservePHPOpen
// PreserveBOM is a UTF-8 byte-order mark.
PreserveBOM
// PreserveCodingPragma is an encoding pragma, e.g. "# -*- coding: utf-8 -*-".
PreserveCodingPragma
// PreservePackageDecl is a package/module declaration (Go, Java, Kotlin) that
// the header must precede rather than follow.
PreservePackageDecl
// PreserveGoBuildConstraint is a Go "//go:build" or "// +build" constraint
// block that must remain in the file's leading comment group.
PreserveGoBuildConstraint
// PreserveCSSCharset is a CSS "@charset" rule that must lead the stylesheet.
PreserveCSSCharset
// PreserveDoctype is an HTML/XML "<!DOCTYPE ...>" declaration.
PreserveDoctype
)
// PreserveRule is a single preserve-first directive in a FileType's ordered list.
// Before marks constructs the header is placed BEFORE (e.g. package decl); when
// false the header is placed AFTER the construct (e.g. shebang, xml-decl).
type PreserveRule struct {
// Kind is the construct this rule matches.
Kind PreserveKind
// Before is true when the header must precede this construct (package decl),
// false when the header follows it (shebang, BOM, xml-decl, php-open, pragma).
Before bool
}
// FileType is one entry in the data-driven comment-syntax table. Skip marks
// formats that cannot carry a comment (e.g. JSON): they are reported, never edited.
type FileType struct {
// Name is the human label, e.g. "Java", "YAML".
Name string
// Extensions are matched against the file's extension, e.g. ".java" (lowercase, with dot).
Extensions []string
// Filenames are matched against the exact base name, e.g. "Dockerfile".
Filenames []string
// CommentStyle describes how to wrap the header for this type.
CommentStyle CommentStyle
// PreserveFirst is the ordered list of preserve-first rules for this type.
PreserveFirst []PreserveRule
// Skip marks an uncommentable type: detection/report only, never written.
Skip bool
}
// DetectedHeader is the result of scanning a single file's leading region for a
// managed license header. Present is false when no managed header was found
// (StartByte/EndByte are then meaningless). ViaSentinel records that detection
// matched our own sentinel rather than a fingerprint, which raises confidence
// for safe replacement.
type DetectedHeader struct {
// Present is true when a managed/replaceable license header was identified.
Present bool
// SPDXID is the license id extracted from the header, if any.
SPDXID string
// Holder is the copyright holder extracted from the header, if any.
Holder string
// Year is the copyright year/range extracted from the header, if any.
Year string
// StartByte is the byte offset (inclusive) where the header block begins.
StartByte int
// EndByte is the byte offset (exclusive) where the header block ends.
EndByte int
// ViaSentinel is true when detection matched our sentinel (highest confidence).
ViaSentinel bool
}
// Config is the fully-resolved configuration after layering flags > repo config >
// user/global config > built-in defaults. It is both the apply input and the
// check expectation for a repo.
type Config struct {
// License is the target SPDX id, e.g. "AGPL-3.0-or-later".
License string
// Holder is the copyright holder text.
Holder string
// Year is the resolved year policy.
Year YearSpec
// Style selects which header parts are emitted.
Style HeaderStyle
// ManageLicenseFile enables writing top-level LICENSE + LICENSES/<id>.txt.
ManageLicenseFile bool
// Includes are glob patterns that restrict source-file processing when non-empty.
Includes []string
// Excludes are gitignore-style patterns added on top of .gitignore.
Excludes []string
// Policy drives audit classification and check exit codes.
Policy Policy
// FileTypeOverrides are user additions/overrides to the built-in file-type table,
// keyed by extension (e.g. ".myext").
FileTypeOverrides map[string]FileType
}
// Policy is the config-defined license policy enforced by check and surfaced by audit.
type Policy struct {
// Required is the SPDX id every managed file is expected to carry, if set.
Required string
// Allow is the allowlist of acceptable SPDX ids; empty means "no allowlist".
Allow []string
// Deny is the denylist of forbidden SPDX ids.
Deny []string
// FailOn lists the conditions that cause check to exit non-zero.
FailOn []FailCondition
}
// FailCondition enumerates the conditions check can gate CI on.
type FailCondition int
const (
// FailOnMissingHeader fails when a managed file lacks a license header.
FailOnMissingHeader FailCondition = iota
// FailOnUnknownLicense fails when a detected license cannot be classified.
FailOnUnknownLicense
// FailOnPolicyViolation fails on an allow/deny/required violation.
FailOnPolicyViolation
// FailOnUnresolvedDependency fails when a dependency license cannot be resolved.
FailOnUnresolvedDependency
)
// String renders the FailCondition as its config token (e.g. "missing-header").
func (f FailCondition) String() string {
switch f {
case FailOnMissingHeader:
return "missing-header"
case FailOnUnknownLicense:
return "unknown-license"
case FailOnPolicyViolation:
return "policy-violation"
case FailOnUnresolvedDependency:
return "unresolved-dependency"
default:
return "unknown"
}
}
// DependencyResolution is the outcome of resolving a single dependency's license.
type DependencyResolution int
const (
// ResolutionUnresolved is the zero value: the license could not be determined
// and was not guessed.
ResolutionUnresolved DependencyResolution = iota
// ResolutionResolved means a license was determined from metadata or a tool.
ResolutionResolved
)
// String renders the DependencyResolution as a report token.
func (r DependencyResolution) String() string {
switch r {
case ResolutionResolved:
return "resolved"
default:
return "unresolved"
}
}
// DependencyLicense is one third-party dependency and its resolved (or unresolved)
// license, produced by an ecosystem Resolver.
type DependencyLicense struct {
// Ecosystem is the package ecosystem, e.g. "maven", "npm", "gradle".
Ecosystem string
// Name is the dependency's package coordinate/name.
Name string
// Version is the dependency's version, if known.
Version string
// SPDXID is the resolved SPDX id; empty when unresolved.
SPDXID string
// Resolution records whether the license was resolved or left unresolved.
Resolution DependencyResolution
// Reason explains an unresolved result (e.g. "no metadata on disk; tool not run").
Reason string
}
// ResolveOptions tunes a Resolver run, selecting the resolution tier and tracing.
type ResolveOptions struct {
// AllowToolShellOut permits shelling out to the ecosystem's native tool when
// on-disk metadata is insufficient. When false, resolution is on-disk only.
AllowToolShellOut bool
// Verbose enables diagnostic logging during resolution.
Verbose bool
}
// Resolver detects and resolves dependency licenses for one ecosystem. Detect is
// a cheap manifest-presence check; Resolve does the work. Implementations live in
// internal/resolve; the interface is part of the frozen contract so audit can
// iterate over a slice of Resolvers uniformly.
type Resolver interface {
// Ecosystem returns the resolver's ecosystem label, e.g. "maven".
Ecosystem() string
// Detect reports whether this ecosystem is present at path (manifest check).
Detect(path string) bool
// Resolve returns the dependency licenses found at path under the given options.
Resolve(path string, opts ResolveOptions) ([]DependencyLicense, error)
}
// FileResult is the per-file outcome of an audit or apply pass over one source file.
type FileResult struct {
// Path is the file path relative to the scan root.
Path string
// FileType is the matched file-type name, e.g. "Java"; empty when unmatched.
FileType string
// Skipped is true when the file was not processed (binary, uncommentable, unmatched).
Skipped bool
// SkipReason explains a skip (e.g. "uncommentable", "binary", "unknown type").
SkipReason string
// Detected is the header detection result for the file.
Detected DetectedHeader
// Action is what apply did or would do: "none", "insert", "replace", "skip".
Action string
// Diff is the unified diff for an apply dry-run; empty otherwise.
Diff string
// Violations lists policy violation tokens attached to this file.
Violations []string
// Err holds a per-file error message; empty on success.
Err string
}
// Report is the top-level audit/apply result model rendered to text/JSON/Markdown.
type Report struct {
// Root is the absolute path that was scanned.
Root string
// Config is the effective configuration the run used.
Config Config
// Files holds the per-file results.
Files []FileResult
// Dependencies holds resolved/unresolved dependency licenses (audit only).
Dependencies []DependencyLicense
// LicenseCounts maps SPDX id to the number of source files carrying it.
LicenseCounts map[string]int
// CategoryCounts maps category token to source-file count.
CategoryCounts map[string]int
// FileTypeCounts maps file-type name to count.
FileTypeCounts map[string]int
// Violations lists repo-level policy violation tokens.
Violations []string
// ViolationDetails carries every policy finding (file, dependency, and repo level)
// with its attribution: the condition, the offending SPDX id, the file/dependency
// path, and a human-readable message. WHY both this and Violations: Violations is
// the legacy repo-level token set; ViolationDetails is the full, attributable set
// reports and machine consumers need to explain WHICH license/rule/file failed.
ViolationDetails []ViolationDetail
// Passed is true when no fail_on condition tripped (drives check exit code).
Passed bool
}
// ViolationDetail is one policy finding with its attribution, so a reader (or an LLM)
// can see the cause, not just a token.
type ViolationDetail struct {
// Condition is the fail_on category this finding belongs to.
Condition FailCondition
// SPDXID is the offending license id, if applicable.
SPDXID string
// Path is the offending file or dependency label; empty for whole-repo findings.
Path string
// Message is the human-readable explanation.
Message string
}