// SPDX-FileCopyrightText: 2026 FocusFlow contributors // SPDX-License-Identifier: AGPL-3.0-only /// Runs the local SPDX and REUSE metadata validation tool. library; import 'dart:io'; /// SPDX expression used by package and app source metadata. /// /// The top-level `LICENSE.md` contains the GNU Affero General Public License /// version 3 text, so the repository uses the corresponding SPDX identifier. const _licenseIdentifier = 'AGPL-3.0-only'; /// Repository-local copyright text used in SPDX headers. /// /// The project uses a contributor-oriented copyright holder because individual /// source files do not currently carry more specific ownership notices. const _copyrightText = '2026 FocusFlow contributors'; /// Package directories that must include a pointer to the root license file. /// /// Each Dart package is publishable as its own unit even though publishing is /// disabled, so every package folder carries a small `LICENSE.md` reference. const _packageDirectories = [ 'packages/scheduler_backup', 'packages/scheduler_core', 'packages/scheduler_export', 'packages/scheduler_export_json', 'packages/scheduler_integration_tests', 'packages/scheduler_notifications', 'packages/scheduler_notifications_desktop', 'packages/scheduler_persistence', 'packages/scheduler_persistence_memory', 'packages/scheduler_persistence_sqlite', ]; /// App directories that must include a pointer to the root license file. /// /// The Flutter app is outside the root Dart workspace, but it still ships with /// repository code and therefore follows the same licensing metadata rules. const _appDirectories = ['apps/focus_flow_flutter']; /// Validates SPDX headers, REUSE metadata, and package/app license pointers. void main(List arguments) { final failures = [ ..._reuseTomlFailures(), ..._licensePointerFailures(), ..._spdxHeaderFailures(), ]; if (failures.isEmpty) { stdout.writeln('SPDX and REUSE metadata checks passed.'); return; } stderr.writeln('SPDX and REUSE metadata checks failed:'); for (final failure in failures) { stderr.writeln('- $failure'); } exitCode = 1; } /// Checks that `REUSE.toml` exists and covers package/app trees. /// /// This does not replace the upstream REUSE tool, but it keeps the repository's /// expected local metadata shape from drifting when contributors add files. List _reuseTomlFailures() { final file = File('REUSE.toml'); if (!file.existsSync()) { return ['REUSE.toml is missing.']; } final contents = file.readAsStringSync(); final failures = []; for (final required in [ 'packages/**', 'apps/focus_flow_flutter/**', 'SPDX-License-Identifier = "$_licenseIdentifier"', 'SPDX-FileCopyrightText = "$_copyrightText"', ]) { if (!contents.contains(required)) { failures.add('REUSE.toml does not contain `$required`.'); } } return failures; } /// Checks package and app `LICENSE.md` pointer files. /// /// Pointer files keep each package understandable when opened in isolation while /// still making the root `LICENSE.md` the single license text source. List _licensePointerFailures() { final failures = []; for (final directory in [ ..._packageDirectories, ..._appDirectories ]) { final file = File('$directory/LICENSE.md'); if (!file.existsSync()) { failures.add('$directory/LICENSE.md is missing.'); continue; } final contents = file.readAsStringSync(); if (!contents.contains('../../LICENSE.md')) { failures .add('$directory/LICENSE.md does not reference ../../LICENSE.md.'); } if (!contents.contains('SPDX-License-Identifier: $_licenseIdentifier')) { failures.add('$directory/LICENSE.md is missing its SPDX license header.'); } } return failures; } /// Checks safe text files for inline SPDX headers. /// /// Binary files, JSON resources, generated platform project files, and XML/plist /// files are covered by `REUSE.toml` annotations because adding comments to /// those formats can either be invalid or be overwritten by platform tooling. List _spdxHeaderFailures() { final failures = []; for (final path in _trackedFiles()) { if (!_requiresInlineSpdxHeader(path)) { continue; } final contents = File(path).readAsStringSync(); if (!contents.contains('SPDX-License-Identifier: $_licenseIdentifier')) { failures.add('$path is missing SPDX-License-Identifier.'); } if (!contents.contains('SPDX-FileCopyrightText: $_copyrightText')) { failures.add('$path is missing SPDX-FileCopyrightText.'); } } return failures; } /// Returns tracked repository files in stable path order. /// /// Git is used so ignored build output and local editor files do not affect the /// metadata check. List _trackedFiles() { final result = Process.runSync( 'git', ['ls-files'], runInShell: Platform.isWindows, ); if (result.exitCode != 0) { throw ProcessException( 'git', ['ls-files'], 'Unable to list tracked files.', result.exitCode, ); } return (result.stdout as String) .split('\n') .where((path) => path.trim().isNotEmpty) .toList(growable: false) ..sort(); } /// Reports whether [path] should carry an inline SPDX header. /// /// The allow-list is intentionally conservative and includes only formats where /// adding a leading comment is safe and unlikely to break generated tooling. bool _requiresInlineSpdxHeader(String path) { if (!_isComplianceScope(path)) { return false; } if (path.endsWith('.g.dart') || path.endsWith('LICENSE.md')) { return false; } return path.endsWith('.dart') || path.endsWith('.yaml') || path.endsWith('.yml') || path.endsWith('.sh') || path.endsWith('.md'); } /// Reports whether [path] belongs to the checked compliance surface. /// /// The package and app folders are the main requested scope; root scripts and /// tool files are included because they enforce the same standards. bool _isComplianceScope(String path) { return path.startsWith('packages/') || path.startsWith('apps/focus_flow_flutter/') || path.startsWith('scripts/') || path.startsWith('tool/') || path == 'pubspec.yaml' || path == 'analysis_options.yaml' || path == '.github/workflows/ci.yml'; }