Commands

ospac has three top-level commands and two command groups:

ospac evaluate      # is this set of licenses acceptable?
ospac check         # can these two licenses be combined?
ospac obligations   # what do I owe if I ship them?
ospac policy ...    # create and validate policy files
ospac data ...      # inspect and regenerate the license dataset

Every command that reads a policy accepts -p/--policy-dir. When you omit it, ospac loads the bundled default enterprise policy and prints a notice to stderr. Because the notice goes to stderr, -o json output stays valid when piped.

evaluate, check and obligations all default to JSON output, which is what makes them usable from scripts without flags.

evaluate

Evaluates a set of licenses against a policy for a given distribution type, and returns a single decision.

ospac evaluate -l LICENSES [-d DIST] [-c CONTEXT] [-p DIR] [-o FORMAT]
Option Effect
-l, --licenses Comma-separated licenses. Required. Whitespace after commas is fine.
-d, --distribution How you ship: internal, commercial, saas, embedded, mobile, desktop, web, open_source. Default commercial.
-c, --context Evaluation context, chiefly static_linking or dynamic_linking. Default general.
-p, --policy-dir Policy file or directory. Defaults to the bundled enterprise policy.
-o, --output json (default), text, or markdown.

The distribution type is what makes the same input produce different answers:

ospac evaluate -l GPL-3.0 -d mobile       # deny, app store terms
ospac evaluate -l GPL-3.0 -d open_source  # policy-dependent
ospac evaluate -l MIT -d embedded         # approve

The action field is the answer; remediation tells you what to do about a denial.

$ ospac evaluate -l GPL-3.0 -d mobile
{
  "licenses": [
    "GPL-3.0"
  ],
  "context": "general",
  "distribution": "mobile",
  "result": {
    "rule_id": "aggregate",
    "action": "deny",
    "severity": "error",
    "message": "Evaluated 1 rules",
    "requirements": [
      "GPL-3.0: Retain copyright notices",
      "GPL-3.0: Include license text",
      "GPL-3.0: Provide or offer access to complete source code",
      "GPL-3.0: Distribute modifications under the same license"
    ],
    "remediation": "Replace with MIT, Apache-2.0, or BSD licensed alternative"
  },
  "per_license": {
    "GPL-3.0": {
      "action": "deny",
      "message": "Evaluated 2 rules"
    }
  },
  "using_default_policy": true
}

action is one of approve, deny, or flag_for_review. Each license is evaluated independently and the verdicts aggregate with the most severe action winning, reported under the synthetic rule_id of aggregate; per_license attributes the outcome, so in a mixed set you can see which license drove a denial. A license that matches no rule comes back flag_for_review on its own, so one permissive license cannot answer for the others. using_default_policy tells you whether the decision came from your policy or the bundled one, worth asserting on in CI.

Linking context matters for weak copyleft, where the same license is fine dynamically and needs review statically:

ospac evaluate -l LGPL-2.1 -c static_linking -d commercial

check

Checks whether two licenses can be combined. Narrower than evaluate: two licenses, and the answer is a boolean plus the violations behind it.

ospac check LICENSE1 LICENSE2 [-c CONTEXT] [-p DIR] [-o FORMAT]
Option Effect
-c, --context static_linking, dynamic_linking, or general (default).
-p, --policy-dir Policy file or directory.
-o, --output json (default) or text. No markdown for this command.
$ ospac check GPL-2.0 Apache-2.0
{
  "license1": "GPL-2.0",
  "license2": "Apache-2.0",
  "context": "general",
  "compatible": false,
  "requires_review": false,
  "violations": [
    {
      "rule_id": "aggregate",
      "message": "Evaluated 2 rules",
      "severity": "error"
    }
  ],
  "warnings": [],
  "using_default_policy": true
}

compatible: false with requires_review: true means a human needs to look, not that a conflict is known. When no conflict rule matches at all, the answer is “no known conflicts”, so a license is always compatible with itself, and a license id that does not resolve in the dataset adds a warning rather than reading as a clean pass.

GPL-2.0 and Apache-2.0 are the canonical incompatible pair. Apache’s patent termination clause is an additional restriction GPL-2.0 does not permit. Text output is more readable when a human is watching:

$ ospac check MIT GPL-3.0 -o text
✓ MIT and GPL-3.0 are compatible

Compatibility is directional in practice even though the flag is a boolean: combining MIT code into a GPL-3.0 project is fine, while the reverse is not. Read the result as “can these coexist in one distributed work under this policy”, and use evaluate when what you actually need is a decision about shipping.

obligations

Lists what each license requires of you. This reads the dataset rather than making a policy decision, so it works the same regardless of distribution type.

ospac obligations -l LICENSES [-f FORMAT] [-p DIR] [-d DATA_DIR]
Option Effect
-l, --licenses Comma-separated licenses. Required.
-f, --format json (default), text, checklist, or markdown.
-p, --policy-dir Policy directory, if it contributes extra obligations.
-d, --data-dir Read license data from elsewhere instead of the bundled dataset.

checklist is the format for a human working through a release:

$ ospac obligations -l MIT -f checklist

MIT:
----------------------------------------
  ☐ Retain copyright notices
  ☐ Include license text

The json format returns the full license record under license_data, not just the obligation strings, the same structure documented in The dataset. That is the format to consume programmatically, because it carries requirements, properties and compatibility alongside the human-readable obligation list.

policy

Creates and validates policy files. See Policies for what goes in them.

policy init

Writes a starter policy for a build target.

ospac policy init [-t TEMPLATE] [-o FILE] [-f FORMAT]
Option Effect
-t, --template mobile, desktop, web, server, embedded, library, or custom. Default web.
-o, --output Output path. Prints to stdout when omitted.
-f, --format yaml (default) or json.
$ ospac policy init --template mobile --output mobile_policy.yaml
✓ Created YAML policy file: mobile_policy.yaml

The templates differ in how they treat copyleft: mobile denies both strong and weak copyleft outright, while library and server are more permissive. Start from the nearest one and edit.

policy validate

Checks a policy file for structural errors before you rely on it.

ospac policy validate ./my_policy.yaml

Run this in CI on any change to a policy file. A policy that fails to load falls back to the default, which means a typo can silently replace your rules with someone else’s.

data

Inspects the bundled dataset and, for maintainers, regenerates it. Read The dataset before using the generation commands.

data show

Prints one license record from the bundled dataset.

ospac data show LICENSE_ID [-f FORMAT]

-f accepts yaml (default), json, or text. Use json or yaml:

$ ospac data show MIT -f json
{
  "id": "MIT",
  "name": "MIT License",
  "type": "permissive",
  "spdx_id": "MIT",
  "properties": {
    "commercial_use": true,
    "distribution": true,
    "modification": true,
    "patent_grant": false,
    "private_use": true
  },
  ...
}

-f text prints a human-readable summary: type, permissions, conditions, limitations, obligations and SPDX metadata, with false values shown explicitly () rather than omitted, and a marker when the identifier is deprecated.

License IDs are validated before use, so a path-traversal attempt in the ID is rejected rather than resolved. An unknown ID exits non-zero and lists a sample of valid IDs.

data generate

Regenerates the license dataset from upstream SPDX. This is a maintainer operation that normally runs in CI once a month, you do not need it to use ospac.

ospac data generate [-o DIR] [--use-llm] [--llm-provider P] [--llm-model M]
                    [--llm-api-key K] [--force] [--force-reprocess] [--limit N]
Option Effect
-o, --output-dir Where to write. Default data. CI passes ospac/data to regenerate in place.
--use-llm Required. Without a provider every record would be a fail-closed placeholder, so the command refuses to run rather than fabricate a dataset.
--llm-provider openai, claude, or ollama.
--llm-model Model name, if you want something other than the provider default.
--llm-api-key API key. Prefer the provider’s environment variable.
--force Overwrite existing output.
--force-reprocess Reanalyze every license, not just new ones. Expensive.
--limit N Process at most N new licenses. Useful for a cheap trial run.
# Regenerate only what is new, no LLM
ospac data generate --output-dir ./data

# What CI runs
ospac data generate --output-dir ospac/data --use-llm --llm-provider openai --force

data download-spdx

Fetches the raw upstream SPDX license list without processing it.

ospac data download-spdx [-o DIR] [--force]

data validate

Validates every record in a dataset against the schema and its semantic invariants, including the restriction rules: a NonCommercial identifier must not permit commercial use, NoDerivatives must not permit modification, ShareAlike must carry the same-license requirement, and a type must not contradict the record’s own booleans.

ospac data validate [-d DIR] [--strict]

Defaults to the packaged data directory. Errors exit non-zero; --strict fails on warnings too. The same rules back scripts/validate_data.py, the maintainer script CI runs, so the two cannot disagree.

Per-record validation cannot notice that a whole dataset is templated, which is how years of fabricated records once shipped with every record individually well formed. scripts/corpus_quality.py covers that: it fails a dataset whose decision fields are uniform across the corpus or whose records match a known fallback fingerprint, and runs in CI on full regenerations.