-
Notifications
You must be signed in to change notification settings - Fork 0
fix(license): reconcile every SDK manifest to Apache-2.0 and ship the license text #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| name: license-truth | ||
|
|
||
| # LEGAL-001 gate for dispatch-edge. Blocks the class of defect fixed in #451/#39: every | ||
| # one of the five SDK manifests here (go, js, python, ruby, rust) declared MIT while | ||
| # LICENSE had been the Apache-2.0 text since e3b3fb8 — nothing in the existing gates read | ||
| # a license FILE, so nothing could see it. Also blocks the subtler sibling: a manifest | ||
| # declaring a license while its own package directory ships no LICENSE text at all (npm | ||
| # and cargo pack from the PACKAGE directory only, never a monorepo root, so a root-level | ||
| # LICENSE never travels with a `sdk/js` or `sdk/rust` publish). | ||
| # | ||
| # FAILS CLOSED. `node scripts/license-truth.mjs` exits 1 on divergence AND on "could not | ||
| # measure anything" (0 manifests found, or manifests found but none declares a license) — | ||
| # never green on an empty denominator. See the header comment in scripts/license-truth.mjs. | ||
| # | ||
| # The selftest runs first, every time: a detector that cannot catch a PLANTED bug is not | ||
| # trusted to report on the real tree. | ||
|
|
||
| on: | ||
| pull_request: | ||
| push: | ||
| branches: [main] | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| license-truth: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 5 | ||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | ||
| with: | ||
| node-version: 22 | ||
|
|
||
| - name: Drill the detector RED on seeded violations first | ||
| run: node scripts/license-truth.selftest.mjs | ||
|
|
||
| - name: License truth gate | ||
| run: node scripts/license-truth.mjs |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| // license-truth — pure license-identity logic for LEGAL-001 (dispatch-edge#39 gate). | ||
| // | ||
| // Vendored from wave-av/claude-workstation governance/lib/license-truth.mjs, which is | ||
| // designed to be shared: "the fleet driver, a repo-local gate and the drill can all | ||
| // share one definition of 'what license is this really'." No IO, no network — every | ||
| // function here takes text it has already been given and returns a verdict. | ||
| // | ||
| // The one idea worth holding: you cannot catch a declared-vs-shipped license | ||
| // contradiction by comparing declarations to each other. Both sides can agree and | ||
| // still both be wrong about the file that actually travels. The only way is to READ | ||
| // THE LICENSE TEXT AND NAME IT. | ||
| // | ||
| // dispatch-edge is a monorepo with five SDK subdirectories (go, js, python, ruby, | ||
| // rust) each publishing to its own registry. npm and cargo pack from the PACKAGE | ||
| // directory and never walk up to a repo root — that is exactly the defect class | ||
| // this gate exists to catch (a subpackage declaring a license while shipping none | ||
| // of its own, silently relying on a root LICENSE that never travels with the | ||
| // published artifact). `resolveShippedLicense` is always called here with | ||
| // `strict: true` for that reason: every subpackage must carry its own copy. | ||
|
|
||
| // Ordered most-specific-first. Each pattern targets a phrase that appears only | ||
| // inside that license's body, never a mere mention of its name. | ||
| export const LICENSE_FINGERPRINTS = [ | ||
| [/GNU AFFERO GENERAL PUBLIC LICENSE/i, "AGPL-3.0"], | ||
| [/GNU LESSER GENERAL PUBLIC LICENSE/i, "LGPL"], | ||
| [/GNU GENERAL PUBLIC LICENSE\s+Version 3/i, "GPL-3.0"], | ||
| [/GNU GENERAL PUBLIC LICENSE\s+Version 2/i, "GPL-2.0"], | ||
| [/Mozilla Public License Version 2\.0/i, "MPL-2.0"], | ||
| [/Apache License\s+Version 2\.0/i, "Apache-2.0"], | ||
| [/Permission is hereby granted, free of charge, to any person obtaining a copy/i, "MIT"], | ||
| [/Permission to use, copy, modify, and\/or distribute this software/i, "ISC"], | ||
| // BSD-2-Clause and BSD-3-Clause share this exact preamble verbatim — resolved | ||
| // below by checking for the extra "endorse or promote" clause BSD-3 alone carries. | ||
| [/Redistribution and use in source and binary forms/i, "BSD"], | ||
| [/This is free and unencumbered software released into the public domain/i, "Unlicense"], | ||
| ]; | ||
|
|
||
| const BSD3_ENDORSEMENT_CLAUSE = /may be used to endorse or promote products derived from this software/i; | ||
|
|
||
| /** Name the license a blob of text actually IS. Never guesses from a filename. */ | ||
| export function identifyLicenseText(text) { | ||
| if (typeof text !== "string" || text.trim() === "") return "EMPTY"; | ||
| const flat = text.replace(/\s+/g, " "); | ||
| for (const [re, id] of LICENSE_FINGERPRINTS) { | ||
| if (!re.test(flat)) continue; | ||
| if (id === "BSD") return BSD3_ENDORSEMENT_CLAUSE.test(flat) ? "BSD-3-Clause" : "BSD-2-Clause"; | ||
| return id; | ||
| } | ||
| return "UNRECOGNIZED"; | ||
| } | ||
|
|
||
| /** Normalize a declaration for comparison. A compound SPDX expression escalates. */ | ||
| export function normalizeDeclared(decl) { | ||
| if (!decl || typeof decl !== "string") return null; | ||
| const d = decl.trim(); | ||
| if (d === "") return null; | ||
| if (/\s(OR|AND)\s/i.test(d) || d.startsWith("(")) return { expression: d }; | ||
| const id = d | ||
| .replace(/-only$|-or-later$/i, "") | ||
| .replace(/^GPL-2\.0.*$/i, "GPL-2.0") | ||
| .replace(/^LGPL-.*$/i, "LGPL"); | ||
| return { id }; | ||
| } | ||
|
|
||
| /** The verdict. Every non-ok path names WHY. */ | ||
| export function declarationMatches(declared, shippedId) { | ||
| const n = normalizeDeclared(declared); | ||
| if (!n) return { ok: false, reason: "no-declaration" }; | ||
| if (n.expression) return { ok: false, reason: "spdx-expression-needs-human" }; | ||
| const shipped = String(shippedId).replace(/^GPL-2\.0.*$/i, "GPL-2.0"); | ||
| if (shipped === "EMPTY") return { ok: false, reason: "license-file-empty" }; | ||
| if (shipped === "NO-LICENSE-FILE") return { ok: false, reason: "license-file-missing" }; | ||
| if (shipped === "UNREADABLE") return { ok: false, reason: "license-file-unreadable" }; | ||
| if (shipped === "UNRECOGNIZED") return { ok: false, reason: "license-text-unrecognized" }; | ||
| return n.id === shipped ? { ok: true } : { ok: false, reason: "declared-vs-shipped-divergence" }; | ||
| } | ||
|
|
||
| export const MANIFEST_RE = /(^|\/)(package\.json|pyproject\.toml|Cargo\.toml|[^/]+\.gemspec)$/; | ||
| export const LICENSE_FILE_RE = /(^|\/)(LICEN[CS]E|COPYING)(\.(md|txt|rst))?$/i; | ||
| export const VENDOR_RE = /(^|\/)(node_modules|vendor|third_party|\.venv|target|dist|fixtures?|testdata)(\/|$)/; | ||
|
|
||
| const pyName = (t) => t.match(/^\s*name\s*=\s*["']([^"']+)["']/mi)?.[1] ?? null; | ||
|
|
||
| /** Read a manifest's license DECLARATION plus enough context to judge it. */ | ||
| export function parseManifest(path, text) { | ||
| try { | ||
| if (path.endsWith("package.json")) { | ||
| const j = JSON.parse(text); | ||
| return { | ||
| declared: typeof j.license === "string" ? j.license : null, | ||
| private: j.private === true, | ||
| name: j.name ?? null, | ||
| ecosystem: "npm", | ||
| files: Array.isArray(j.files) ? j.files : null, | ||
| }; | ||
| } | ||
| if (path.endsWith("pyproject.toml")) { | ||
| const block = text.match(/^\s*license\s*=\s*(.+)$/mi)?.[1] ?? ""; | ||
| const classifier = text.match(/License :: OSI Approved :: ([^"'\]]+?) License/i)?.[1]?.trim() ?? null; | ||
| if (/file\s*=/.test(block)) return { declared: null, classifier, private: false, name: pyName(text), ecosystem: "pypi", licenseByFile: true }; | ||
| const declared = block.match(/text\s*=\s*["']([^"']+)["']/)?.[1] ?? block.match(/^["']([^"']+)["']/)?.[1] ?? null; | ||
| return { declared, classifier, private: false, name: pyName(text), ecosystem: "pypi" }; | ||
| } | ||
| if (path.endsWith("Cargo.toml")) { | ||
| // A virtual workspace root ships nothing itself. | ||
| if (/^\s*\[workspace\]/m.test(text) && !/^\s*\[package\]/m.test(text)) return { declared: null, private: true, name: null, ecosystem: "cargo" }; | ||
| const excludeMatch = text.match(/^\s*exclude\s*=\s*\[([^\]]*)\]/mi)?.[1] ?? ""; | ||
| const includeMatch = text.match(/^\s*include\s*=\s*\[([^\]]*)\]/mi)?.[1] ?? null; | ||
| return { | ||
| declared: text.match(/^\s*license\s*=\s*["']([^"']+)["']/mi)?.[1] ?? null, | ||
| private: /^\s*publish\s*=\s*false/mi.test(text), | ||
| name: text.match(/^\s*name\s*=\s*["']([^"']+)["']/mi)?.[1] ?? null, | ||
| ecosystem: "cargo", | ||
| excludesLicense: /LICEN[CS]E/i.test(excludeMatch), | ||
| include: includeMatch, | ||
| }; | ||
| } | ||
| if (path.endsWith(".gemspec")) { | ||
| const filesMatch = text.match(/\.files\s*=\s*\[([^\]]*)\]/)?.[1] ?? null; | ||
| return { | ||
| declared: text.match(/\.licenses?\s*=\s*\[?\s*["']([^"']+)["']/)?.[1] ?? null, | ||
| private: false, | ||
| name: text.match(/\.name\s*=\s*["']([^"']+)["']/)?.[1] ?? null, | ||
| ecosystem: "rubygems", | ||
| files: filesMatch, | ||
| }; | ||
| } | ||
| } catch { return null; } | ||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Find the LICENSE that would actually travel with a manifest. | ||
| * | ||
| * `strict: true` models npm/cargo, which pack the LICENSE from the PACKAGE | ||
| * directory and never walk up to a repo root. Always strict here: every | ||
| * dispatch-edge subpackage must carry its own copy. | ||
| */ | ||
| export function resolveShippedLicense(manifestPath, licensePaths, { strict = true } = {}) { | ||
| const dirOf = (p) => (p.includes("/") ? p.slice(0, p.lastIndexOf("/")) : ""); | ||
| let dir = dirOf(manifestPath); | ||
| for (;;) { | ||
| const hit = licensePaths.find((L) => dirOf(L) === dir); | ||
| if (hit) return hit; | ||
| if (strict || dir === "") return null; | ||
| dir = dirOf(dir); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| #!/usr/bin/env node | ||
| // license-truth — the repo-local LEGAL-001 gate for dispatch-edge. | ||
| // | ||
| // WHAT IT CATCHES: any of the five SDK subdirectories (go, js, python, ruby, rust) | ||
| // declaring a license in its own manifest that the LICENSE TEXT beside it does not | ||
| // back up — either because the text says something else (the #451 defect: every | ||
| // manifest here said MIT while LICENSE had been Apache-2.0 since e3b3fb8) or | ||
| // because there is no LICENSE text in that subpackage's own directory at all (npm | ||
| // and cargo pack from the package directory only, and never walk up to the repo | ||
| // root — a green root-level check would miss this class entirely). | ||
| // | ||
| // FAILS CLOSED. Absent input is a FAILURE, never a pass: zero manifests found, or | ||
| // zero of them declaring a license, means this gate could not measure anything — | ||
| // that is reported as a failure (exit 1), not a skip and not a green. | ||
| // | ||
| // Usage: node scripts/license-truth.mjs [--json] | ||
| // Exit: 0 consistent · 1 divergence found OR could not measure anything. | ||
| import { readFileSync, readdirSync, statSync } from "node:fs"; | ||
| import { dirname, join, relative, resolve } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
| import { | ||
| identifyLicenseText, declarationMatches, parseManifest, | ||
| resolveShippedLicense, MANIFEST_RE, LICENSE_FILE_RE, VENDOR_RE, | ||
| } from "./lib/license-truth.mjs"; | ||
|
|
||
| const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); | ||
|
|
||
| function walk(dir, out) { | ||
| let entries; | ||
| try { entries = readdirSync(dir); } catch { return; } | ||
| for (const name of entries) { | ||
| const p = join(dir, name); | ||
| const rel = relative(ROOT, p); | ||
| if (rel.startsWith(".git")) continue; | ||
| if (VENDOR_RE.test(rel)) continue; | ||
| let st; | ||
| try { st = statSync(p); } catch { continue; } | ||
| if (st.isDirectory()) walk(p, out); | ||
| else out.push(rel); | ||
| } | ||
| } | ||
|
|
||
| export function auditRepo(root = ROOT) { | ||
| const files = []; | ||
| walk(root, files); | ||
| const licensePaths = files.filter((f) => LICENSE_FILE_RE.test(f)); | ||
| const manifestPaths = files.filter((f) => MANIFEST_RE.test(f)); | ||
|
|
||
| const licenseId = new Map(); | ||
| for (const p of licensePaths) { | ||
| try { licenseId.set(p, identifyLicenseText(readFileSync(join(root, p), "utf8"))); } | ||
| catch { licenseId.set(p, "UNREADABLE"); } | ||
| } | ||
|
|
||
| const findings = []; | ||
| const units = []; | ||
| let declaredCount = 0; | ||
|
|
||
| for (const mp of manifestPaths) { | ||
| let raw; | ||
| try { raw = readFileSync(join(root, mp), "utf8"); } catch { continue; } | ||
| const m = parseManifest(mp, raw); | ||
| if (!m || m.private || (!m.declared && !m.licenseByFile)) continue; | ||
| declaredCount += 1; | ||
|
|
||
| const shippedPath = resolveShippedLicense(mp, licensePaths, { strict: true }); | ||
| const shipped = shippedPath ? licenseId.get(shippedPath) : "NO-LICENSE-FILE"; | ||
| const unit = { manifest: mp, pkg: m.name, ecosystem: m.ecosystem, declared: m.declared, shipped, shippedFrom: shippedPath }; | ||
| units.push(unit); | ||
| const dir = mp.replace(/[^/]+$/, "") || "./"; | ||
|
|
||
| if (!shippedPath) { | ||
| findings.push({ ...unit, rule: "license-file-missing-in-package-dir", | ||
| detail: `${m.name ?? mp} declares ${m.declared} but ${dir} contains no LICENSE — the published artifact would carry no license text of its own.` }); | ||
| continue; | ||
| } | ||
|
|
||
| // Ecosystems that do NOT auto-include a colocated LICENSE in the published | ||
| // artifact must say so explicitly. npm always includes a colocated LICENSE | ||
| // regardless of `files` (documented npm-packlist behavior), and cargo's | ||
| // default packlist is every git-tracked file in the package directory unless | ||
| // `exclude` says otherwise — so both are covered by the colocation check | ||
| // above, PLUS an explicit veto check here for the ways each can still opt out. | ||
| if (m.ecosystem === "rubygems") { | ||
| const licenseBase = shippedPath.split("/").pop(); | ||
| if (m.files === null || !new RegExp(`(^|[\\s'",])${licenseBase}([\\s'",]|$)`).test(m.files)) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Semgrep identified an issue in your code: Dataflow graphflowchart LR
classDef invis fill:white, stroke: none
classDef default fill:#e7f5ff, color:#1c7fd6, stroke: none
subgraph File0["<b>scripts/license-truth.mjs</b>"]
direction LR
%% Source
subgraph Source
direction LR
v0["<a href=https://github.com/wave-av/dispatch-edge/blob/cea90346d1befa4a6d4c6f5ebe113bab6b94a32e/scripts/license-truth.mjs#L43 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 43] root</a>"]
end
%% Intermediate
subgraph Traces0[Traces]
direction TB
v2["<a href=https://github.com/wave-av/dispatch-edge/blob/cea90346d1befa4a6d4c6f5ebe113bab6b94a32e/scripts/license-truth.mjs#L43 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 43] root</a>"]
v3["<a href=https://github.com/wave-av/dispatch-edge/blob/cea90346d1befa4a6d4c6f5ebe113bab6b94a32e/scripts/license-truth.mjs#L45 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 45] files</a>"]
v4["<a href=https://github.com/wave-av/dispatch-edge/blob/cea90346d1befa4a6d4c6f5ebe113bab6b94a32e/scripts/license-truth.mjs#L28 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 28] dir</a>"]
v5["<a href=https://github.com/wave-av/dispatch-edge/blob/cea90346d1befa4a6d4c6f5ebe113bab6b94a32e/scripts/license-truth.mjs#L32 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 32] p</a>"]
v6["<a href=https://github.com/wave-av/dispatch-edge/blob/cea90346d1befa4a6d4c6f5ebe113bab6b94a32e/scripts/license-truth.mjs#L33 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 33] rel</a>"]
v7["<a href=https://github.com/wave-av/dispatch-edge/blob/cea90346d1befa4a6d4c6f5ebe113bab6b94a32e/scripts/license-truth.mjs#L39 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 39] out</a>"]
v8["<a href=https://github.com/wave-av/dispatch-edge/blob/cea90346d1befa4a6d4c6f5ebe113bab6b94a32e/scripts/license-truth.mjs#L45 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 45] files</a>"]
v9["<a href=https://github.com/wave-av/dispatch-edge/blob/cea90346d1befa4a6d4c6f5ebe113bab6b94a32e/scripts/license-truth.mjs#L46 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 46] licensePaths</a>"]
v10["<a href=https://github.com/wave-av/dispatch-edge/blob/cea90346d1befa4a6d4c6f5ebe113bab6b94a32e/scripts/license-truth.mjs#L66 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 66] shippedPath</a>"]
v11["<a href=https://github.com/wave-av/dispatch-edge/blob/cea90346d1befa4a6d4c6f5ebe113bab6b94a32e/scripts/license-truth.mjs#L85 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 85] licenseBase</a>"]
v12["<a href=https://github.com/wave-av/dispatch-edge/blob/cea90346d1befa4a6d4c6f5ebe113bab6b94a32e/scripts/license-truth.mjs#L86 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 86] `</a>"]
end
v2 --> v3
v3 --> v4
v4 --> v5
v5 --> v6
v6 --> v7
v7 --> v8
v8 --> v9
v9 --> v10
v10 --> v11
v11 --> v12
%% Sink
subgraph Sink
direction LR
v1["<a href=https://github.com/wave-av/dispatch-edge/blob/cea90346d1befa4a6d4c6f5ebe113bab6b94a32e/scripts/license-truth.mjs#L86 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 86] new RegExp(`(^|[\\s'",])${licenseBase}([\\s'",]|$)`)</a>"]
end
end
%% Class Assignment
Source:::invis
Sink:::invis
Traces0:::invis
File0:::invis
%% Connections
Source --> Traces0
Traces0 --> Sink
To resolve this comment: 🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods. 💬 Ignore this findingReply with Semgrep commands to ignore this finding.
Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by detect-non-literal-regexp. You can view more details about this finding in the Semgrep AppSec Platform. |
||
| findings.push({ ...unit, rule: "license-not-in-gem-files", | ||
| detail: `${m.name ?? mp}: ${shippedPath} exists but s.files does not list it — RubyGems ships exactly the files named in s.files, nothing more.` }); | ||
| } | ||
| } | ||
| if (m.ecosystem === "cargo" && m.excludesLicense) { | ||
| findings.push({ ...unit, rule: "license-excluded-from-crate", | ||
| detail: `${m.name ?? mp}: Cargo.toml's [package].exclude removes the LICENSE file from the published crate.` }); | ||
| } | ||
| if (m.ecosystem === "cargo" && m.include && !new RegExp(`LICEN[CS]E`, "i").test(m.include)) { | ||
| findings.push({ ...unit, rule: "license-absent-from-crate-include", | ||
| detail: `${m.name ?? mp}: Cargo.toml sets [package].include and it does not name the LICENSE file, so cargo would omit it.` }); | ||
| } | ||
|
|
||
| const verdict = declarationMatches(m.declared, shipped); | ||
| if (!verdict.ok) { | ||
| findings.push({ ...unit, rule: verdict.reason, | ||
| detail: `${m.name ?? mp} declares ${m.declared ?? "(nothing)"} but ships ${shipped} — ${shippedPath}.` }); | ||
| } | ||
| } | ||
|
|
||
| return { files, licensePaths, manifestPaths, declaredCount, units, findings }; | ||
| } | ||
|
|
||
| function main() { | ||
| const json = process.argv.includes("--json"); | ||
| const result = auditRepo(ROOT); | ||
|
|
||
| if (result.manifestPaths.length === 0) { | ||
| console.error("license-truth: 0 publishable manifests found in this repo — CANNOT MEASURE. An empty denominator is not a pass."); | ||
| return 1; | ||
| } | ||
| if (result.declaredCount === 0) { | ||
| console.error(`license-truth: ${result.manifestPaths.length} manifest(s) found but none declares a license — CANNOT MEASURE. An unmeasured license state is not a passing one.`); | ||
| return 1; | ||
| } | ||
|
|
||
| if (json) { | ||
| process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); | ||
| return result.findings.length ? 1 : 0; | ||
| } | ||
|
|
||
| console.log(`license-truth: ${result.manifestPaths.length} manifest(s) checked, ${result.declaredCount} declaring a license, ${result.licensePaths.length} LICENSE file(s) found`); | ||
| for (const u of result.units) console.log(` ${u.manifest}: declares ${u.declared ?? "(none)"} — ships ${u.shipped}${u.shippedFrom ? ` (${u.shippedFrom})` : ""}`); | ||
|
|
||
| if (result.findings.length === 0) { | ||
| console.log("\nOK — every declared license matches the license text shipped beside it, in its own package directory."); | ||
| return 0; | ||
| } | ||
|
|
||
| console.error(`\n${result.findings.length} unit(s) declare a license they do not (correctly) ship:`); | ||
| for (const f of result.findings) console.error(` [${f.rule}] ${f.manifest}\n ${f.detail}`); | ||
| return 1; | ||
| } | ||
|
|
||
| if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) { | ||
| process.exit(main()); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🤖 get_repo_knowledge executed:
get_repo_knowledge wave-av/dispatch-edge /tmp/coderabbit-repo-knowledge/wave-av-dispatch-edge-badd3cb3/architecture /tmp/coderabbit-repo-knowledge/wave-av-dispatch-edge-badd3cb3/conventionsLength of output: 22221
🏁 Script executed:
Repository: wave-av/dispatch-edge
Length of output: 23940
🏁 Script executed:
Repository: wave-av/dispatch-edge
Length of output: 27529
Align the README copyright attribution.
LICENSEandNOTICEidentifyWAVE, Inc., butREADME.mdidentifiesWAVE Online, LLC.Change the README toWAVE, Inc.if the license files are authoritative. Otherwise, document the entities' roles.🤖 Prompt for AI Agents