Files
sovereign_ai/SovereignAI-Starter/scripts/generate_flutter_license_triage.py
T

144 lines
5.4 KiB
Python

"""Create a hash-backed, non-legal triage report for unresolved Flutter licenses."""
from __future__ import annotations
import argparse
import json
import re
from datetime import UTC, datetime
from pathlib import Path
try:
from scripts.generate_component_inventory import (
digest,
flutter_license_file,
package_roots,
parse_lockfile,
)
except ModuleNotFoundError: # direct execution as `python scripts/...py`
from generate_component_inventory import (
digest,
flutter_license_file,
package_roots,
parse_lockfile,
)
def candidate_license(text: str) -> tuple[str | None, str]:
"""Suggest an SPDX identifier only when the license's identifying text is clear.
Suggestions are for human triage. They do not establish provenance, package
applicability, exceptions, or permission to redistribute.
"""
normalized = re.sub(r"(?m)^\s*(?://|#|\*)\s?", "", text).lower()
normalized = re.sub(r"\s+", " ", normalized)
matches: list[tuple[str, str]] = []
if "mozilla public license version 2.0" in normalized:
matches.append(("MPL-2.0", "license text contains the Mozilla Public License 2.0 title"))
if "apache license" in normalized and "version 2.0" in normalized:
matches.append(("Apache-2.0", "license text contains the Apache License 2.0 title"))
if (
"permission is hereby granted, free of charge" in normalized
and "the software is provided" in normalized
and "in no event shall" in normalized
):
matches.append(("MIT", "license text contains the standard MIT grant and warranty disclaimer"))
if (
"redistribution and use in source and binary forms" in normalized
and "neither the name" in normalized
and "disclaimer" in normalized
):
matches.append(("BSD-3-Clause", "license text contains the three-clause BSD endorsement restriction"))
if (
"redistribution and use in source and binary forms" in normalized
and "neither the name" not in normalized
and "disclaimer" in normalized
and "provided that the following conditions are met" in normalized
):
matches.append(("BSD-2-Clause", "license text contains a two-clause BSD-style grant and disclaimer"))
if len(matches) == 1:
return matches[0]
if len(matches) > 1:
return None, "multiple license signatures occur in this file; manual review required"
return None, "no conservative license-text signature matched"
def build_report(root: Path) -> dict[str, object]:
app_root = root / "flutter_app"
packages = parse_lockfile(app_root / "pubspec.lock")
roots = package_roots(app_root / ".dart_tool" / "package_config.json")
entries: list[dict[str, object]] = []
missing: list[dict[str, str]] = []
for package in packages:
name = package["name"]
package_root = roots.get(name)
license_file = flutter_license_file(package_root) if package_root else None
if license_file is None:
missing.append({"name": name, "version": package["version"]})
continue
text = license_file.read_text(encoding="utf-8", errors="replace")
candidate, basis = candidate_license(text)
entries.append(
{
"name": name,
"version": package["version"],
"scope": package.get("dependency", "transitive"),
"license_file": license_file.name,
"license_file_sha256": digest(license_file),
"candidate_spdx": candidate,
"candidate_basis": basis,
"status": "candidate only; human review required",
}
)
counts: dict[str, int] = {}
for entry in entries:
candidate = entry["candidate_spdx"] or "unclassified"
counts[str(candidate)] = counts.get(str(candidate), 0) + 1
return {
"format": "flutter-license-triage-v1",
"generated_at": datetime.now(UTC).isoformat(),
"source": "flutter_app/pubspec.lock and resolved package LICENSE files",
"notice": (
"Text-signature suggestions only. Not legal advice, provenance verification, "
"or approval to redistribute. Review each package, source, notices, and terms."
),
"summary": {
"locked_packages": len(packages),
"packages_with_license_file": len(entries),
"packages_without_license_file": len(missing),
"candidate_counts": dict(sorted(counts.items())),
},
"packages": sorted(entries, key=lambda item: str(item["name"]).lower()),
"missing_license_file": sorted(missing, key=lambda item: item["name"].lower()),
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--project-root",
type=Path,
default=Path(__file__).resolve().parents[1],
)
parser.add_argument("--output", type=Path, default=None)
args = parser.parse_args()
root = args.project_root.resolve()
output = args.output or root / "sbom" / "flutter-license-triage.json"
report = build_report(root)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(
json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
print(json.dumps(report["summary"], ensure_ascii=False, sort_keys=True))
print(f"Wrote {output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())