@ahmed-g-gad/apothem 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/LICENSE +21 -0
- package/LICENSES/MIT.txt +18 -0
- package/LICENSES/PSF-2.0.txt +47 -0
- package/README.md +549 -0
- package/bin/README.md +37 -0
- package/bin/apothem.mjs +78 -0
- package/package.json +75 -0
- package/pyproject.toml +347 -0
- package/src/apothem/README.md +52 -0
- package/src/apothem/__init__.py +66 -0
- package/src/apothem/__main__.py +28 -0
- package/src/apothem/_vendor/.keep +0 -0
- package/src/apothem/_vendor/__init__.py +25 -0
- package/src/apothem/_vendor/attr/__init__.py +104 -0
- package/src/apothem/_vendor/attr/__init__.pyi +389 -0
- package/src/apothem/_vendor/attr/_cmp.py +160 -0
- package/src/apothem/_vendor/attr/_cmp.pyi +13 -0
- package/src/apothem/_vendor/attr/_compat.py +99 -0
- package/src/apothem/_vendor/attr/_config.py +31 -0
- package/src/apothem/_vendor/attr/_funcs.py +497 -0
- package/src/apothem/_vendor/attr/_make.py +3406 -0
- package/src/apothem/_vendor/attr/_next_gen.py +674 -0
- package/src/apothem/_vendor/attr/_typing_compat.pyi +15 -0
- package/src/apothem/_vendor/attr/_version_info.py +89 -0
- package/src/apothem/_vendor/attr/_version_info.pyi +9 -0
- package/src/apothem/_vendor/attr/converters.py +162 -0
- package/src/apothem/_vendor/attr/converters.pyi +19 -0
- package/src/apothem/_vendor/attr/exceptions.py +95 -0
- package/src/apothem/_vendor/attr/exceptions.pyi +17 -0
- package/src/apothem/_vendor/attr/filters.py +72 -0
- package/src/apothem/_vendor/attr/filters.pyi +6 -0
- package/src/apothem/_vendor/attr/py.typed +0 -0
- package/src/apothem/_vendor/attr/setters.py +79 -0
- package/src/apothem/_vendor/attr/setters.pyi +20 -0
- package/src/apothem/_vendor/attr/validators.py +750 -0
- package/src/apothem/_vendor/attr/validators.pyi +140 -0
- package/src/apothem/_vendor/attr.LICENSE +21 -0
- package/src/apothem/_vendor/attrs/__init__.py +72 -0
- package/src/apothem/_vendor/attrs/__init__.pyi +314 -0
- package/src/apothem/_vendor/attrs/converters.py +3 -0
- package/src/apothem/_vendor/attrs/exceptions.py +3 -0
- package/src/apothem/_vendor/attrs/filters.py +3 -0
- package/src/apothem/_vendor/attrs/py.typed +0 -0
- package/src/apothem/_vendor/attrs/setters.py +3 -0
- package/src/apothem/_vendor/attrs/validators.py +3 -0
- package/src/apothem/_vendor/attrs.LICENSE +21 -0
- package/src/apothem/_vendor/jsonschema/__init__.py +120 -0
- package/src/apothem/_vendor/jsonschema/__main__.py +6 -0
- package/src/apothem/_vendor/jsonschema/_format.py +546 -0
- package/src/apothem/_vendor/jsonschema/_keywords.py +449 -0
- package/src/apothem/_vendor/jsonschema/_legacy_keywords.py +449 -0
- package/src/apothem/_vendor/jsonschema/_types.py +204 -0
- package/src/apothem/_vendor/jsonschema/_typing.py +29 -0
- package/src/apothem/_vendor/jsonschema/_utils.py +355 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/__init__.py +5 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/const_vs_enum.py +30 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/contains.py +28 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/import_benchmark.py +31 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/issue232/issue.json +2653 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/issue232.py +25 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/json_schema_test_suite.py +12 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/nested_schemas.py +56 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/subcomponents.py +42 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/unused_registry.py +35 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/useless_applicator_schemas.py +106 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/useless_keywords.py +32 -0
- package/src/apothem/_vendor/jsonschema/benchmarks/validator_creation.py +14 -0
- package/src/apothem/_vendor/jsonschema/cli.py +292 -0
- package/src/apothem/_vendor/jsonschema/exceptions.py +490 -0
- package/src/apothem/_vendor/jsonschema/protocols.py +230 -0
- package/src/apothem/_vendor/jsonschema/validators.py +1410 -0
- package/src/apothem/_vendor/jsonschema.LICENSE +19 -0
- package/src/apothem/_vendor/jsonschema_specifications/__init__.py +12 -0
- package/src/apothem/_vendor/jsonschema_specifications/_core.py +38 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/metaschema.json +42 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/applicator +56 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/content +17 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/core +57 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/format +14 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/meta-data +37 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/validation +98 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/metaschema.json +58 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/applicator +48 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/content +17 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/core +51 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/format-annotation +14 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/format-assertion +14 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/meta-data +37 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/unevaluated +15 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/validation +98 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft3/metaschema.json +172 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft4/metaschema.json +149 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft6/metaschema.json +153 -0
- package/src/apothem/_vendor/jsonschema_specifications/schemas/draft7/metaschema.json +166 -0
- package/src/apothem/_vendor/jsonschema_specifications.LICENSE +19 -0
- package/src/apothem/_vendor/referencing/__init__.py +7 -0
- package/src/apothem/_vendor/referencing/_attrs.py +31 -0
- package/src/apothem/_vendor/referencing/_attrs.pyi +21 -0
- package/src/apothem/_vendor/referencing/_core.py +739 -0
- package/src/apothem/_vendor/referencing/exceptions.py +165 -0
- package/src/apothem/_vendor/referencing/jsonschema.py +642 -0
- package/src/apothem/_vendor/referencing/py.typed +0 -0
- package/src/apothem/_vendor/referencing/retrieval.py +94 -0
- package/src/apothem/_vendor/referencing/typing.py +61 -0
- package/src/apothem/_vendor/referencing.LICENSE +19 -0
- package/src/apothem/_vendor/rpds/__init__.py +251 -0
- package/src/apothem/_vendor/typing_extensions.LICENSE +279 -0
- package/src/apothem/_vendor/typing_extensions.py +4317 -0
- package/src/apothem/_vendor/vendor.txt +22 -0
- package/src/apothem/_vendor/yaml/__init__.py +389 -0
- package/src/apothem/_vendor/yaml/composer.py +138 -0
- package/src/apothem/_vendor/yaml/constructor.py +748 -0
- package/src/apothem/_vendor/yaml/cyaml.py +100 -0
- package/src/apothem/_vendor/yaml/dumper.py +61 -0
- package/src/apothem/_vendor/yaml/emitter.py +1137 -0
- package/src/apothem/_vendor/yaml/error.py +74 -0
- package/src/apothem/_vendor/yaml/events.py +85 -0
- package/src/apothem/_vendor/yaml/loader.py +63 -0
- package/src/apothem/_vendor/yaml/nodes.py +48 -0
- package/src/apothem/_vendor/yaml/parser.py +588 -0
- package/src/apothem/_vendor/yaml/reader.py +185 -0
- package/src/apothem/_vendor/yaml/representer.py +388 -0
- package/src/apothem/_vendor/yaml/resolver.py +226 -0
- package/src/apothem/_vendor/yaml/scanner.py +1435 -0
- package/src/apothem/_vendor/yaml/serializer.py +110 -0
- package/src/apothem/_vendor/yaml/tokens.py +103 -0
- package/src/apothem/_vendor/yaml.LICENSE +20 -0
- package/src/apothem/agents/README.md +60 -0
- package/src/apothem/agents/codebase-explorer.md +91 -0
- package/src/apothem/agents/convention-auditor.md +93 -0
- package/src/apothem/agents/dependency-auditor.md +97 -0
- package/src/apothem/agents/fact-checker.md +84 -0
- package/src/apothem/agents/mcp-builder.md +86 -0
- package/src/apothem/agents/memory-auditor.md +93 -0
- package/src/apothem/agents/prompt-evaluator.md +87 -0
- package/src/apothem/agents/quality-gate.md +103 -0
- package/src/apothem/agents/refactor-surgeon.md +74 -0
- package/src/apothem/agents/research-scout.md +73 -0
- package/src/apothem/agents/security-scanner.md +83 -0
- package/src/apothem/agents/test-runner.md +84 -0
- package/src/apothem/audit/README.md +73 -0
- package/src/apothem/audit/_scan_lib.py +182 -0
- package/src/apothem/audit/analyze_graph.py +260 -0
- package/src/apothem/audit/build_capability_graph.py +607 -0
- package/src/apothem/audit/build_inventory.py +657 -0
- package/src/apothem/audit/build_plans_provenance.py +997 -0
- package/src/apothem/audit/check_links.py +389 -0
- package/src/apothem/audit/classify_artifacts.py +381 -0
- package/src/apothem/audit/deprecated-tokens.txt +10 -0
- package/src/apothem/audit/execute_plans_migration.py +491 -0
- package/src/apothem/audit/known-projects.txt +15 -0
- package/src/apothem/audit/render_capability_index.py +467 -0
- package/src/apothem/audit/render_inventory.py +405 -0
- package/src/apothem/audit/scan_ai_surfaces.py +1125 -0
- package/src/apothem/audit/scan_ai_surfaces_coarse.py +261 -0
- package/src/apothem/audit/scan_drift_features.py +143 -0
- package/src/apothem/audit/scan_frontmatter.py +293 -0
- package/src/apothem/audit/scan_header_coverage.py +1134 -0
- package/src/apothem/audit/scan_plan_leakage.py +540 -0
- package/src/apothem/audit/scan_plans_discipline.py +188 -0
- package/src/apothem/audit/scan_secrets_pii.py +245 -0
- package/src/apothem/audit/scan_stale_tokens.py +296 -0
- package/src/apothem/audit/synthesize_drift.py +205 -0
- package/src/apothem/benchmarks/README.md +33 -0
- package/src/apothem/benchmarks/__init__.py +3 -0
- package/src/apothem/benchmarks/bench_agents.py +63 -0
- package/src/apothem/benchmarks/bench_hooks.py +93 -0
- package/src/apothem/benchmarks/bench_install.py +58 -0
- package/src/apothem/benchmarks/bench_tests.py +93 -0
- package/src/apothem/benchmarks/bench_validate_ecosystem.py +84 -0
- package/src/apothem/cli/README.md +33 -0
- package/src/apothem/cli/__init__.py +229 -0
- package/src/apothem/cli/_cmd_completion.py +88 -0
- package/src/apothem/cli/_cmd_diff.py +181 -0
- package/src/apothem/cli/_cmd_doctor.py +143 -0
- package/src/apothem/cli/_cmd_harnesses.py +167 -0
- package/src/apothem/cli/_cmd_install.py +327 -0
- package/src/apothem/cli/_cmd_migrate_workspace.py +143 -0
- package/src/apothem/cli/_cmd_profile.py +341 -0
- package/src/apothem/cli/_cmd_status.py +180 -0
- package/src/apothem/cli/_cmd_uninstall.py +215 -0
- package/src/apothem/cli/_cmd_update.py +397 -0
- package/src/apothem/cli/_cmd_verify.py +194 -0
- package/src/apothem/cli/_common_flags.py +90 -0
- package/src/apothem/cli/_epilogs.py +296 -0
- package/src/apothem/cli/_helpers.py +857 -0
- package/src/apothem/cli/_json_formatter.py +21 -0
- package/src/apothem/cli/_materialize.py +376 -0
- package/src/apothem/cli/completions/apothem.bash +30 -0
- package/src/apothem/cli/completions/apothem.fish +19 -0
- package/src/apothem/cli/completions/apothem.ps1 +27 -0
- package/src/apothem/cli/completions/apothem.zsh +42 -0
- package/src/apothem/cli/reference_export.py +126 -0
- package/src/apothem/commands/README.md +125 -0
- package/src/apothem/commands/a11y-audit.md +203 -0
- package/src/apothem/commands/architecture-review.md +194 -0
- package/src/apothem/commands/audit.md +165 -0
- package/src/apothem/commands/code-audit.md +218 -0
- package/src/apothem/commands/code-review.md +193 -0
- package/src/apothem/commands/dependency-audit.md +209 -0
- package/src/apothem/commands/docs-review.md +199 -0
- package/src/apothem/commands/elevate.md +285 -0
- package/src/apothem/commands/eval.md +149 -0
- package/src/apothem/commands/fortress.md +172 -0
- package/src/apothem/commands/freshify.md +168 -0
- package/src/apothem/commands/github-deploy-fresh.md +178 -0
- package/src/apothem/commands/github-deploy-next.md +167 -0
- package/src/apothem/commands/perf-audit.md +198 -0
- package/src/apothem/commands/plan-amend.md +104 -0
- package/src/apothem/commands/plan-audit.md +127 -0
- package/src/apothem/commands/plan-design.md +257 -0
- package/src/apothem/commands/plan-execute.md +495 -0
- package/src/apothem/commands/plan-generate.md +351 -0
- package/src/apothem/commands/plan-review.md +555 -0
- package/src/apothem/commands/plan-spec.md +359 -0
- package/src/apothem/commands/plan-status.md +222 -0
- package/src/apothem/commands/plan.md +173 -0
- package/src/apothem/commands/projectify.md +142 -0
- package/src/apothem/commands/release-readiness.md +142 -0
- package/src/apothem/commands/research-analysis.md +241 -0
- package/src/apothem/commands/research-design.md +231 -0
- package/src/apothem/commands/research-disseminate.md +225 -0
- package/src/apothem/commands/research-experiment.md +232 -0
- package/src/apothem/commands/research-ideate.md +213 -0
- package/src/apothem/commands/research-paper.md +252 -0
- package/src/apothem/commands/research-proposal.md +220 -0
- package/src/apothem/commands/research-publish.md +255 -0
- package/src/apothem/commands/research-review.md +251 -0
- package/src/apothem/commands/research-sources.md +266 -0
- package/src/apothem/commands/research-spec.md +255 -0
- package/src/apothem/commands/research-synthesis.md +233 -0
- package/src/apothem/commands/research-theory.md +218 -0
- package/src/apothem/commands/research.md +181 -0
- package/src/apothem/commands/security-audit.md +196 -0
- package/src/apothem/commands/supply-chain-audit.md +192 -0
- package/src/apothem/commands/test-suite.md +146 -0
- package/src/apothem/commands/threat-model-audit.md +199 -0
- package/src/apothem/commands/ux-review.md +202 -0
- package/src/apothem/commands/workflow.md +162 -0
- package/src/apothem/conformity/README.md +173 -0
- package/src/apothem/conformity/__init__.py +1 -0
- package/src/apothem/conformity/_grep_base.py +93 -0
- package/src/apothem/conformity/agent_capability_grep.py +306 -0
- package/src/apothem/conformity/agents_md_coverage_grep.py +382 -0
- package/src/apothem/conformity/agnosticism_grep.py +311 -0
- package/src/apothem/conformity/always_on_budget_grep.py +318 -0
- package/src/apothem/conformity/bare_except_grep.py +115 -0
- package/src/apothem/conformity/binding_reciprocity_grep.py +151 -0
- package/src/apothem/conformity/brand_mark_grep.py +272 -0
- package/src/apothem/conformity/commented_out_code_grep.py +176 -0
- package/src/apothem/conformity/completion_claim_grep.py +169 -0
- package/src/apothem/conformity/conventional_commit_grep.py +319 -0
- package/src/apothem/conformity/copilot_instructions_presence_grep.py +324 -0
- package/src/apothem/conformity/cross_platform_matrix_grep.py +297 -0
- package/src/apothem/conformity/determinism_grep.py +306 -0
- package/src/apothem/conformity/diagram_staleness_grep.py +154 -0
- package/src/apothem/conformity/dynamism_grep.py +284 -0
- package/src/apothem/conformity/editorconfig_presence_grep.py +281 -0
- package/src/apothem/conformity/file_header_grep.py +502 -0
- package/src/apothem/conformity/freshness_token_grep.py +233 -0
- package/src/apothem/conformity/frontmatter_grep.py +274 -0
- package/src/apothem/conformity/frontmatter_value_grep.py +386 -0
- package/src/apothem/conformity/gate.py +1386 -0
- package/src/apothem/conformity/gitattributes_presence_grep.py +238 -0
- package/src/apothem/conformity/harden_runner_grep.py +320 -0
- package/src/apothem/conformity/hedging_grep.py +129 -0
- package/src/apothem/conformity/license_author_consistency_grep.py +204 -0
- package/src/apothem/conformity/link_check.py +327 -0
- package/src/apothem/conformity/magic_number_grep.py +182 -0
- package/src/apothem/conformity/multi_surface_coherence_grep.py +620 -0
- package/src/apothem/conformity/naming_grep.py +224 -0
- package/src/apothem/conformity/no_global_plans_grep.py +339 -0
- package/src/apothem/conformity/no_toplevel_docs_grep.py +120 -0
- package/src/apothem/conformity/oidc_trusted_publishing_grep.py +291 -0
- package/src/apothem/conformity/option_annotation_grep.py +352 -0
- package/src/apothem/conformity/orphan_output_grep.py +206 -0
- package/src/apothem/conformity/permissions_minimum_scope_grep.py +299 -0
- package/src/apothem/conformity/plain_language_grep.py +559 -0
- package/src/apothem/conformity/plan_next_step_consistency_grep.py +450 -0
- package/src/apothem/conformity/plan_suite_structure_grep.py +534 -0
- package/src/apothem/conformity/plans_discipline_language_grep.py +245 -0
- package/src/apothem/conformity/production_ready_pr_grep.py +200 -0
- package/src/apothem/conformity/recommend_next_step_grep.py +250 -0
- package/src/apothem/conformity/redundancy_grep.py +401 -0
- package/src/apothem/conformity/reference_token_grep.py +230 -0
- package/src/apothem/conformity/registry_capability_consistency_grep.py +368 -0
- package/src/apothem/conformity/secret_leak_grep.py +193 -0
- package/src/apothem/conformity/semver_stability_grep.py +358 -0
- package/src/apothem/conformity/smoke_install_grep.py +194 -0
- package/src/apothem/conformity/static_version_grep.py +284 -0
- package/src/apothem/conformity/token_efficiency_grep.py +185 -0
- package/src/apothem/conformity/unpinned_action_grep.py +115 -0
- package/src/apothem/conformity/user_confirm_grep.py +74 -0
- package/src/apothem/conformity/workflow_concurrency_grep.py +283 -0
- package/src/apothem/harnesses/README.md +63 -0
- package/src/apothem/harnesses/__init__.py +16 -0
- package/src/apothem/harnesses/_shared/README.md +36 -0
- package/src/apothem/harnesses/_shared/__init__.py +12 -0
- package/src/apothem/harnesses/_shared/install_driver.py +281 -0
- package/src/apothem/harnesses/_shared/install_driver_apply.py +612 -0
- package/src/apothem/harnesses/_shared/install_driver_backup.py +535 -0
- package/src/apothem/harnesses/_shared/install_driver_converters.py +310 -0
- package/src/apothem/harnesses/_shared/install_driver_lifecycle.py +495 -0
- package/src/apothem/harnesses/_shared/install_driver_materialize.py +675 -0
- package/src/apothem/harnesses/_shared/install_driver_merge.py +656 -0
- package/src/apothem/harnesses/_shared/install_driver_pathsafety.py +137 -0
- package/src/apothem/harnesses/_shared/install_driver_planvalidation.py +240 -0
- package/src/apothem/harnesses/_shared/install_driver_removal.py +366 -0
- package/src/apothem/harnesses/_shared/install_driver_treeops.py +248 -0
- package/src/apothem/harnesses/_shared/install_driver_types.py +330 -0
- package/src/apothem/harnesses/_shared/wrapper_factories.py +448 -0
- package/src/apothem/harnesses/antigravity/STANDARD-CONVENTION-PIN.md +91 -0
- package/src/apothem/harnesses/antigravity/__init__.py +70 -0
- package/src/apothem/harnesses/antigravity/capabilities.yml +40 -0
- package/src/apothem/harnesses/antigravity/install.py +63 -0
- package/src/apothem/harnesses/antigravity/templates/GEMINI.md +40 -0
- package/src/apothem/harnesses/antigravity/templates/plugin.json +5 -0
- package/src/apothem/harnesses/antigravity/uninstall.py +22 -0
- package/src/apothem/harnesses/antigravity/update.py +10 -0
- package/src/apothem/harnesses/antigravity/verify.py +11 -0
- package/src/apothem/harnesses/claude_code/STANDARD-CONVENTION-PIN.md +65 -0
- package/src/apothem/harnesses/claude_code/__init__.py +107 -0
- package/src/apothem/harnesses/claude_code/capabilities.yml +42 -0
- package/src/apothem/harnesses/claude_code/install.py +147 -0
- package/src/apothem/harnesses/claude_code/templates/settings.json +351 -0
- package/src/apothem/harnesses/claude_code/uninstall.py +23 -0
- package/src/apothem/harnesses/claude_code/update.py +10 -0
- package/src/apothem/harnesses/claude_code/verify.py +11 -0
- package/src/apothem/harnesses/codebuddy/STANDARD-CONVENTION-PIN.md +74 -0
- package/src/apothem/harnesses/codebuddy/__init__.py +49 -0
- package/src/apothem/harnesses/codebuddy/capabilities.yml +34 -0
- package/src/apothem/harnesses/codebuddy/install.py +40 -0
- package/src/apothem/harnesses/codebuddy/templates/apothem-rules.md +37 -0
- package/src/apothem/harnesses/codebuddy/uninstall.py +25 -0
- package/src/apothem/harnesses/codebuddy/update.py +10 -0
- package/src/apothem/harnesses/codebuddy/verify.py +11 -0
- package/src/apothem/harnesses/codex/STANDARD-CONVENTION-PIN.md +79 -0
- package/src/apothem/harnesses/codex/__init__.py +72 -0
- package/src/apothem/harnesses/codex/capabilities.yml +40 -0
- package/src/apothem/harnesses/codex/install.py +69 -0
- package/src/apothem/harnesses/codex/templates/AGENTS.md +40 -0
- package/src/apothem/harnesses/codex/templates/hooks.json +127 -0
- package/src/apothem/harnesses/codex/uninstall.py +23 -0
- package/src/apothem/harnesses/codex/update.py +10 -0
- package/src/apothem/harnesses/codex/verify.py +11 -0
- package/src/apothem/harnesses/cursor/STANDARD-CONVENTION-PIN.md +79 -0
- package/src/apothem/harnesses/cursor/__init__.py +48 -0
- package/src/apothem/harnesses/cursor/capabilities.yml +42 -0
- package/src/apothem/harnesses/cursor/install.py +38 -0
- package/src/apothem/harnesses/cursor/templates/apothem-rules.mdc +40 -0
- package/src/apothem/harnesses/cursor/uninstall.py +25 -0
- package/src/apothem/harnesses/cursor/update.py +10 -0
- package/src/apothem/harnesses/cursor/verify.py +11 -0
- package/src/apothem/harnesses/gemini_cli/STANDARD-CONVENTION-PIN.md +102 -0
- package/src/apothem/harnesses/gemini_cli/__init__.py +52 -0
- package/src/apothem/harnesses/gemini_cli/capabilities.yml +43 -0
- package/src/apothem/harnesses/gemini_cli/install.py +43 -0
- package/src/apothem/harnesses/gemini_cli/templates/GEMINI.md +38 -0
- package/src/apothem/harnesses/gemini_cli/uninstall.py +25 -0
- package/src/apothem/harnesses/gemini_cli/update.py +10 -0
- package/src/apothem/harnesses/gemini_cli/verify.py +11 -0
- package/src/apothem/harnesses/github_copilot/STANDARD-CONVENTION-PIN.md +84 -0
- package/src/apothem/harnesses/github_copilot/__init__.py +47 -0
- package/src/apothem/harnesses/github_copilot/capabilities.yml +42 -0
- package/src/apothem/harnesses/github_copilot/install.py +40 -0
- package/src/apothem/harnesses/github_copilot/templates/copilot-instructions.md +33 -0
- package/src/apothem/harnesses/github_copilot/uninstall.py +25 -0
- package/src/apothem/harnesses/github_copilot/update.py +10 -0
- package/src/apothem/harnesses/github_copilot/verify.py +11 -0
- package/src/apothem/harnesses/glm/STANDARD-CONVENTION-PIN.md +77 -0
- package/src/apothem/harnesses/glm/__init__.py +56 -0
- package/src/apothem/harnesses/glm/capabilities.yml +33 -0
- package/src/apothem/harnesses/glm/install.py +45 -0
- package/src/apothem/harnesses/glm/templates/glm.toml +58 -0
- package/src/apothem/harnesses/glm/uninstall.py +25 -0
- package/src/apothem/harnesses/glm/update.py +10 -0
- package/src/apothem/harnesses/glm/verify.py +11 -0
- package/src/apothem/harnesses/hermes/STANDARD-CONVENTION-PIN.md +57 -0
- package/src/apothem/harnesses/hermes/__init__.py +33 -0
- package/src/apothem/harnesses/hermes/capabilities.yml +36 -0
- package/src/apothem/harnesses/hermes/install.py +17 -0
- package/src/apothem/harnesses/hermes/materializer.py +35 -0
- package/src/apothem/harnesses/hermes/uninstall.py +33 -0
- package/src/apothem/harnesses/hermes/update.py +10 -0
- package/src/apothem/harnesses/hermes/verify.py +11 -0
- package/src/apothem/harnesses/kimi_code/STANDARD-CONVENTION-PIN.md +128 -0
- package/src/apothem/harnesses/kimi_code/__init__.py +59 -0
- package/src/apothem/harnesses/kimi_code/capabilities.yml +40 -0
- package/src/apothem/harnesses/kimi_code/install.py +42 -0
- package/src/apothem/harnesses/kimi_code/templates/AGENTS.md +43 -0
- package/src/apothem/harnesses/kimi_code/uninstall.py +27 -0
- package/src/apothem/harnesses/kimi_code/update.py +10 -0
- package/src/apothem/harnesses/kimi_code/verify.py +11 -0
- package/src/apothem/harnesses/kiro/STANDARD-CONVENTION-PIN.md +77 -0
- package/src/apothem/harnesses/kiro/__init__.py +49 -0
- package/src/apothem/harnesses/kiro/capabilities.yml +36 -0
- package/src/apothem/harnesses/kiro/install.py +39 -0
- package/src/apothem/harnesses/kiro/templates/apothem-rules.md +36 -0
- package/src/apothem/harnesses/kiro/uninstall.py +25 -0
- package/src/apothem/harnesses/kiro/update.py +10 -0
- package/src/apothem/harnesses/kiro/verify.py +11 -0
- package/src/apothem/harnesses/open_claw/STANDARD-CONVENTION-PIN.md +62 -0
- package/src/apothem/harnesses/open_claw/__init__.py +35 -0
- package/src/apothem/harnesses/open_claw/capabilities.yml +35 -0
- package/src/apothem/harnesses/open_claw/install.py +17 -0
- package/src/apothem/harnesses/open_claw/materializer.py +36 -0
- package/src/apothem/harnesses/open_claw/uninstall.py +32 -0
- package/src/apothem/harnesses/open_claw/update.py +10 -0
- package/src/apothem/harnesses/open_claw/verify.py +11 -0
- package/src/apothem/harnesses/opencode/STANDARD-CONVENTION-PIN.md +76 -0
- package/src/apothem/harnesses/opencode/__init__.py +35 -0
- package/src/apothem/harnesses/opencode/capabilities.yml +43 -0
- package/src/apothem/harnesses/opencode/install.py +17 -0
- package/src/apothem/harnesses/opencode/materializer.py +31 -0
- package/src/apothem/harnesses/opencode/uninstall.py +34 -0
- package/src/apothem/harnesses/opencode/update.py +10 -0
- package/src/apothem/harnesses/opencode/verify.py +11 -0
- package/src/apothem/harnesses/qwen_code/STANDARD-CONVENTION-PIN.md +87 -0
- package/src/apothem/harnesses/qwen_code/__init__.py +37 -0
- package/src/apothem/harnesses/qwen_code/capabilities.yml +43 -0
- package/src/apothem/harnesses/qwen_code/install.py +19 -0
- package/src/apothem/harnesses/qwen_code/materializer.py +174 -0
- package/src/apothem/harnesses/qwen_code/templates/QWEN.md +30 -0
- package/src/apothem/harnesses/qwen_code/uninstall.py +34 -0
- package/src/apothem/harnesses/qwen_code/update.py +10 -0
- package/src/apothem/harnesses/qwen_code/verify.py +11 -0
- package/src/apothem/harnesses/trae/STANDARD-CONVENTION-PIN.md +70 -0
- package/src/apothem/harnesses/trae/__init__.py +49 -0
- package/src/apothem/harnesses/trae/capabilities.yml +34 -0
- package/src/apothem/harnesses/trae/install.py +38 -0
- package/src/apothem/harnesses/trae/templates/apothem-rules.md +37 -0
- package/src/apothem/harnesses/trae/uninstall.py +25 -0
- package/src/apothem/harnesses/trae/update.py +10 -0
- package/src/apothem/harnesses/trae/verify.py +11 -0
- package/src/apothem/harnesses/windsurf/STANDARD-CONVENTION-PIN.md +91 -0
- package/src/apothem/harnesses/windsurf/__init__.py +52 -0
- package/src/apothem/harnesses/windsurf/capabilities.yml +40 -0
- package/src/apothem/harnesses/windsurf/install.py +41 -0
- package/src/apothem/harnesses/windsurf/templates/apothem-rules.md +37 -0
- package/src/apothem/harnesses/windsurf/uninstall.py +25 -0
- package/src/apothem/harnesses/windsurf/update.py +10 -0
- package/src/apothem/harnesses/windsurf/verify.py +11 -0
- package/src/apothem/harnesses/zed/STANDARD-CONVENTION-PIN.md +92 -0
- package/src/apothem/harnesses/zed/__init__.py +57 -0
- package/src/apothem/harnesses/zed/capabilities.yml +38 -0
- package/src/apothem/harnesses/zed/install.py +41 -0
- package/src/apothem/harnesses/zed/templates/apothem-rules.md +32 -0
- package/src/apothem/harnesses/zed/uninstall.py +28 -0
- package/src/apothem/harnesses/zed/update.py +10 -0
- package/src/apothem/harnesses/zed/verify.py +11 -0
- package/src/apothem/hooks/README.md +81 -0
- package/src/apothem/hooks/__init__.py +24 -0
- package/src/apothem/hooks/askuserquestion_validator.py +380 -0
- package/src/apothem/hooks/dispatch.py +296 -0
- package/src/apothem/hooks/emit_hook_context.py +444 -0
- package/src/apothem/hooks/hooks.json +318 -0
- package/src/apothem/hooks/lib/README.md +39 -0
- package/src/apothem/hooks/lib/__init__.py +18 -0
- package/src/apothem/hooks/lib/bootstrap.ps1 +129 -0
- package/src/apothem/hooks/lib/bootstrap.sh +103 -0
- package/src/apothem/hooks/lib/events.py +51 -0
- package/src/apothem/hooks/lib/find-pwsh.ps1 +78 -0
- package/src/apothem/hooks/lib/find-pwsh.sh +76 -0
- package/src/apothem/hooks/lib/find-python.ps1 +63 -0
- package/src/apothem/hooks/lib/find-python.sh +97 -0
- package/src/apothem/hooks/lib/log.py +43 -0
- package/src/apothem/hooks/lib/resolve_root.py +264 -0
- package/src/apothem/hooks/messages/postcompact.md +14 -0
- package/src/apothem/hooks/messages/posttooluse-proactive-compaction.md +46 -0
- package/src/apothem/hooks/messages/precompact.md +14 -0
- package/src/apothem/hooks/messages/pretooluse-askuserquestion-recommended.md +65 -0
- package/src/apothem/hooks/messages/pretooluse-bash-plan-guard.md +97 -0
- package/src/apothem/hooks/messages/pretooluse-bash.md +39 -0
- package/src/apothem/hooks/messages/pretooluse-conformity.md +70 -0
- package/src/apothem/hooks/messages/pretooluse-dependency-guard.md +21 -0
- package/src/apothem/hooks/messages/pretooluse-edit-header-guard.md +61 -0
- package/src/apothem/hooks/messages/pretooluse-edit.md +21 -0
- package/src/apothem/hooks/messages/pretooluse-eval-guard.md +39 -0
- package/src/apothem/hooks/messages/pretooluse-notebookedit.md +11 -0
- package/src/apothem/hooks/messages/pretooluse-write-header-guard.md +45 -0
- package/src/apothem/hooks/messages/pretooluse-write-plan-guard.md +72 -0
- package/src/apothem/hooks/messages/pretooluse-write.md +21 -0
- package/src/apothem/hooks/messages/sessionstart.md +15 -0
- package/src/apothem/hooks/messages/stop.md +27 -0
- package/src/apothem/hooks/proactive_compaction_tracker.py +327 -0
- package/src/apothem/hooks/session_start_bootstrap.py +472 -0
- package/src/apothem/lib/README.md +42 -0
- package/src/apothem/lib/__init__.py +13 -0
- package/src/apothem/lib/atomic_io.py +189 -0
- package/src/apothem/lib/auditor.py +687 -0
- package/src/apothem/lib/clean_slate.py +396 -0
- package/src/apothem/lib/contexts.py +352 -0
- package/src/apothem/lib/data_home.py +255 -0
- package/src/apothem/lib/frontmatter.py +101 -0
- package/src/apothem/lib/harness_materializer.py +213 -0
- package/src/apothem/lib/harness_protocol.py +59 -0
- package/src/apothem/lib/harness_registry.py +282 -0
- package/src/apothem/lib/harness_registry_data.py +843 -0
- package/src/apothem/lib/install_ledger.py +347 -0
- package/src/apothem/lib/learning.py +540 -0
- package/src/apothem/lib/memory.py +347 -0
- package/src/apothem/lib/parallel_sweep.py +234 -0
- package/src/apothem/lib/plan_tiers.py +200 -0
- package/src/apothem/lib/plugin_bootstrap.py +132 -0
- package/src/apothem/lib/plugin_tree.py +599 -0
- package/src/apothem/lib/profile.py +755 -0
- package/src/apothem/lib/profile_projection.py +198 -0
- package/src/apothem/lib/propagation-manifest.yaml +878 -0
- package/src/apothem/lib/propagation.py +220 -0
- package/src/apothem/lib/python_resolver.py +189 -0
- package/src/apothem/lib/reporter.py +62 -0
- package/src/apothem/lib/workspace_migration.py +323 -0
- package/src/apothem/output-styles/README.md +41 -0
- package/src/apothem/output-styles/concise-engineer.md +49 -0
- package/src/apothem/output-styles/default-architect.md +52 -0
- package/src/apothem/output-styles/default.md +113 -0
- package/src/apothem/output-styles/forensic-auditor.md +63 -0
- package/src/apothem/py.typed +0 -0
- package/src/apothem/rules/README.md +121 -0
- package/src/apothem/rules/agent-capability-discipline-matrix.md +89 -0
- package/src/apothem/rules/agent-capability-discipline.md +78 -0
- package/src/apothem/rules/agent-orchestration-patterns.md +144 -0
- package/src/apothem/rules/agent-orchestration.md +65 -0
- package/src/apothem/rules/agents-md-convention.md +86 -0
- package/src/apothem/rules/agile-sprints-elements.md +135 -0
- package/src/apothem/rules/agile-sprints.md +64 -0
- package/src/apothem/rules/agnostic-posture-checklist.md +47 -0
- package/src/apothem/rules/agnostic-posture.md +48 -0
- package/src/apothem/rules/authoritative-referencing-quotation.md +50 -0
- package/src/apothem/rules/authoritative-referencing.md +66 -0
- package/src/apothem/rules/authority-inquiry-categories.md +58 -0
- package/src/apothem/rules/authority-inquiry.md +54 -0
- package/src/apothem/rules/auto-memory-topic-files.md +86 -0
- package/src/apothem/rules/auto-memory.md +67 -0
- package/src/apothem/rules/bidirectional-binding.md +123 -0
- package/src/apothem/rules/canonical-layout-reporting-tiers.md +212 -0
- package/src/apothem/rules/canonical-layout.md +60 -0
- package/src/apothem/rules/clean-architecture-layers.md +186 -0
- package/src/apothem/rules/clean-room-generation-protocols.md +124 -0
- package/src/apothem/rules/clean-room-generation.md +59 -0
- package/src/apothem/rules/code-craft-conventions.md +101 -0
- package/src/apothem/rules/code-craft-markdown.md +138 -0
- package/src/apothem/rules/code-craft-python.md +154 -0
- package/src/apothem/rules/code-craft-shell.md +192 -0
- package/src/apothem/rules/cognitive-identity-techniques.md +180 -0
- package/src/apothem/rules/cognitive-identity.md +81 -0
- package/src/apothem/rules/context-management-budget.md +46 -0
- package/src/apothem/rules/context-management-protocol.md +161 -0
- package/src/apothem/rules/context-management-scratch.md +128 -0
- package/src/apothem/rules/context-management.md +85 -0
- package/src/apothem/rules/definitiveness-virtues.md +67 -0
- package/src/apothem/rules/definitiveness.md +58 -0
- package/src/apothem/rules/determinism.md +81 -0
- package/src/apothem/rules/disclosure-ledger-markers.md +58 -0
- package/src/apothem/rules/disclosure-ledger.md +52 -0
- package/src/apothem/rules/dynamism.md +38 -0
- package/src/apothem/rules/etc-extension.md +57 -0
- package/src/apothem/rules/expertise-posture-elements.md +68 -0
- package/src/apothem/rules/expertise-posture.md +54 -0
- package/src/apothem/rules/freshness-facade.md +64 -0
- package/src/apothem/rules/harness-adapter-shape-schemas.md +162 -0
- package/src/apothem/rules/harness-adapter-shape.md +42 -0
- package/src/apothem/rules/host-discovery-manifests.md +50 -0
- package/src/apothem/rules/host-discovery.md +56 -0
- package/src/apothem/rules/i18n-discipline-locale-cohorts.md +120 -0
- package/src/apothem/rules/i18n-discipline.md +70 -0
- package/src/apothem/rules/interactive-questions-canonical-shapes.md +590 -0
- package/src/apothem/rules/interactive-questions-detail.md +41 -0
- package/src/apothem/rules/interactive-questions-sweep-matchers.md +184 -0
- package/src/apothem/rules/interactive-questions.md +89 -0
- package/src/apothem/rules/large-file-generation.md +112 -0
- package/src/apothem/rules/large-file-reading.md +59 -0
- package/src/apothem/rules/living-docs.md +85 -0
- package/src/apothem/rules/multi-agent-workflow.md +57 -0
- package/src/apothem/rules/operational-mandates-expanded.md +78 -0
- package/src/apothem/rules/operational-mandates.md +88 -0
- package/src/apothem/rules/option-annotation-form.md +60 -0
- package/src/apothem/rules/option-annotation.md +45 -0
- package/src/apothem/rules/own-voice-reimplementation.md +86 -0
- package/src/apothem/rules/performance-discipline.md +91 -0
- package/src/apothem/rules/persistent-conventions-vigilance-checklist.md +54 -0
- package/src/apothem/rules/persistent-conventions-vigilance.md +61 -0
- package/src/apothem/rules/plain-language.md +56 -0
- package/src/apothem/rules/planning-techniques.md +130 -0
- package/src/apothem/rules/pre-emission-gate-bars.md +86 -0
- package/src/apothem/rules/pre-emission-gate.md +54 -0
- package/src/apothem/rules/production-ready-prs-surfaces.md +162 -0
- package/src/apothem/rules/production-ready-prs.md +83 -0
- package/src/apothem/rules/propagation.md +63 -0
- package/src/apothem/rules/recommend-next-step.md +106 -0
- package/src/apothem/rules/refactoring-discipline.md +76 -0
- package/src/apothem/rules/session-closure.md +44 -0
- package/src/apothem/rules/sota-elevation-exemplars.md +76 -0
- package/src/apothem/rules/sota-elevation.md +52 -0
- package/src/apothem/rules/source-accessibility.md +58 -0
- package/src/apothem/rules/surgical-manipulation.md +48 -0
- package/src/apothem/rules/systemic-participation-relations.md +108 -0
- package/src/apothem/rules/systemic-participation.md +70 -0
- package/src/apothem/rules/ten-dimension-check-dimensions.md +52 -0
- package/src/apothem/rules/ten-dimension-check.md +59 -0
- package/src/apothem/rules/token-budget-discipline.md +81 -0
- package/src/apothem/rules/token-efficiency-rewrite-protocol.md +79 -0
- package/src/apothem/rules/token-efficiency-rewrite.md +77 -0
- package/src/apothem/rules/tool-use-discipline.md +48 -0
- package/src/apothem/rules/visual-leverage.md +102 -0
- package/src/apothem/schemas/NOTICE.md +9 -0
- package/src/apothem/schemas/README.md +104 -0
- package/src/apothem/schemas/__init__.py +176 -0
- package/src/apothem/schemas/advisory-finding.schema.json +111 -0
- package/src/apothem/schemas/agent.schema.json +106 -0
- package/src/apothem/schemas/authorship-header.txt +1 -0
- package/src/apothem/schemas/cohort-manifest.yaml +248 -0
- package/src/apothem/schemas/cohort-metadata-vocabulary.yaml +168 -0
- package/src/apothem/schemas/cohort.schema.json +113 -0
- package/src/apothem/schemas/command.schema.json +68 -0
- package/src/apothem/schemas/compatibility-matrix.yaml +432 -0
- package/src/apothem/schemas/context-fragment.schema.json +64 -0
- package/src/apothem/schemas/freshness-token-denylist.txt +51 -0
- package/src/apothem/schemas/handoff-manifest.yaml +353 -0
- package/src/apothem/schemas/header-exceptions.txt +141 -0
- package/src/apothem/schemas/header-visibility.yaml +39 -0
- package/src/apothem/schemas/learning-signal.schema.json +46 -0
- package/src/apothem/schemas/memory-record.schema.json +61 -0
- package/src/apothem/schemas/output-style.schema.json +40 -0
- package/src/apothem/schemas/plan.schema.json +51 -0
- package/src/apothem/schemas/plugin.schema.json +83 -0
- package/src/apothem/schemas/profile.example.yaml +70 -0
- package/src/apothem/schemas/profile.minimal.yaml +6 -0
- package/src/apothem/schemas/profile.schema.json +396 -0
- package/src/apothem/schemas/reference-token-denylist.txt +25 -0
- package/src/apothem/schemas/skill.schema.json +75 -0
- package/src/apothem/skills/README.md +93 -0
- package/src/apothem/skills/dependency-upgrade/SKILL.md +105 -0
- package/src/apothem/skills/dev-toolkit/SKILL.md +120 -0
- package/src/apothem/skills/diagram-authoring/SKILL.md +113 -0
- package/src/apothem/skills/document-authoring/SKILL.md +118 -0
- package/src/apothem/skills/ecosystem-audit/SKILL.md +108 -0
- package/src/apothem/skills/ecosystem-audit/references/audit-fortress.md +85 -0
- package/src/apothem/skills/ecosystem-audit/references/procedure.md +162 -0
- package/src/apothem/skills/eval-harness/SKILL.md +88 -0
- package/src/apothem/skills/incident-runbook/SKILL.md +92 -0
- package/src/apothem/skills/multi-source-research/SKILL.md +90 -0
- package/src/apothem/skills/plan-suite/SKILL.md +118 -0
- package/src/apothem/skills/plan-suite/master_template.md +1324 -0
- package/src/apothem/skills/projectify/SKILL.md +117 -0
- package/src/apothem/skills/prompt-engineering/SKILL.md +122 -0
- package/src/apothem/skills/refactor-extract/SKILL.md +85 -0
- package/src/apothem/skills/research-suite/SKILL.md +170 -0
- package/src/apothem/skills/research-suite/references/directory-structure.md +47 -0
- package/src/apothem/skills/research-suite/references/lifecycle.md +67 -0
- package/src/apothem/skills/research-suite/references/principal-investigator-framework.md +37 -0
- package/src/apothem/skills/research-suite/references/rigor-mandates.md +30 -0
- package/src/apothem/skills/research-suite/research_template.md +476 -0
- package/src/apothem/skills/secret-rotation/SKILL.md +87 -0
- package/src/apothem/skills/source-synthesis/SKILL.md +92 -0
- package/src/apothem/skills/surgical-guard/SKILL.md +118 -0
- package/src/apothem/skills/test-authoring/SKILL.md +85 -0
- package/src/apothem/skills/vuln-triage/SKILL.md +91 -0
- package/src/apothem/skills/workflow/SKILL.md +139 -0
- package/src/apothem/statuslines/README.md +26 -0
- package/src/apothem/statuslines/__init__.py +20 -0
- package/src/apothem/statuslines/conformity.json +5 -0
- package/src/apothem/statuslines/render.py +334 -0
- package/src/apothem/statuslines/statusline.md +50 -0
- package/src/apothem/templates/README.md +43 -0
- package/src/apothem/templates/agents-md-template.md +80 -0
- package/src/apothem/templates/consideration-log.md +39 -0
- package/src/apothem/templates/expertise-gap-log.md +56 -0
- package/src/apothem/templates/master-index-template.md +93 -0
- package/src/apothem/templates/potency-map.md +53 -0
- package/src/apothem/templates/preservation-audit.md +60 -0
- package/src/apothem/templates/question-resolution-audit.md +52 -0
- package/src/apothem/templates/trace-matrix-template.md +77 -0
|
@@ -0,0 +1,1134 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
"""Detailed authorship-header coverage scan against the inventory snapshot.
|
|
4
|
+
|
|
5
|
+
Why this tool exists. The inventory pass at the prior audit phase carries
|
|
6
|
+
a coarse four-mark heuristic for the authorship banner — sufficient to
|
|
7
|
+
size the work, insufficient to drive an injector. The downstream banner
|
|
8
|
+
ratification, fixture authoring, validator authoring, and injection
|
|
9
|
+
passes need a per-file authoritative record: applicability per the
|
|
10
|
+
exception fixture, header-status by byte-precise comparison against the
|
|
11
|
+
canonical variant for the file's filetype family, the line range the
|
|
12
|
+
banner occupies (if present), the malformation class (if malformed),
|
|
13
|
+
and a per-file injection plan with a unified-diff preview. This tool
|
|
14
|
+
emits ``header-coverage.json`` and ``header-coverage.md`` once; the
|
|
15
|
+
operator-confirm and injection phases consume them.
|
|
16
|
+
|
|
17
|
+
What the tool captures. Per applicable file: ``path``, ``variant-family``
|
|
18
|
+
(one of ``hash`` / ``double-slash`` / ``html`` / ``c-block`` /
|
|
19
|
+
``semicolon`` / ``double-dash`` / ``exempt``), ``header-status`` (
|
|
20
|
+
``present-canonical`` / ``present-malformed`` / ``absent`` /
|
|
21
|
+
``not-applicable``), ``header-line-range`` (the inclusive 1-based span
|
|
22
|
+
the banner occupies, or ``null``), ``malformation-class`` (one of the
|
|
23
|
+
eight classification slots when ``present-malformed``), and an
|
|
24
|
+
``injection-plan`` block carrying the unified-diff preview the injector
|
|
25
|
+
would apply. Aggregates: counts by variant family, counts by
|
|
26
|
+
malformation class, coverage percentage (``present-canonical /
|
|
27
|
+
applicable-total``).
|
|
28
|
+
|
|
29
|
+
Scope boundary. The tool ONLY inspects file heads (the first sixty
|
|
30
|
+
lines, which absorbs every shebang + frontmatter + banner shape the
|
|
31
|
+
canonical variants emit) and ONLY reads bytes — it never writes the
|
|
32
|
+
banner. The injection pass at the downstream phase consumes this
|
|
33
|
+
output's diff previews and applies them.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import argparse
|
|
39
|
+
import difflib
|
|
40
|
+
import hashlib
|
|
41
|
+
import json
|
|
42
|
+
import sys
|
|
43
|
+
from dataclasses import dataclass, field
|
|
44
|
+
from datetime import datetime, timezone
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
from typing import Final
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Header-status taxonomy. Mirrors the inventory's four-value taxonomy.
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
HEADER_PRESENT_CANONICAL: Final[str] = "present-canonical"
|
|
52
|
+
HEADER_PRESENT_MALFORMED: Final[str] = "present-malformed"
|
|
53
|
+
HEADER_ABSENT: Final[str] = "absent"
|
|
54
|
+
HEADER_NOT_APPLICABLE: Final[str] = "not-applicable"
|
|
55
|
+
|
|
56
|
+
ALL_HEADER_STATUSES: Final[tuple[str, ...]] = (
|
|
57
|
+
HEADER_PRESENT_CANONICAL,
|
|
58
|
+
HEADER_PRESENT_MALFORMED,
|
|
59
|
+
HEADER_ABSENT,
|
|
60
|
+
HEADER_NOT_APPLICABLE,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Variant-family taxonomy per spec §4.6.2.
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
VARIANT_HASH: Final[str] = "hash"
|
|
67
|
+
VARIANT_DOUBLE_SLASH: Final[str] = "double-slash"
|
|
68
|
+
VARIANT_HTML: Final[str] = "html"
|
|
69
|
+
VARIANT_C_BLOCK: Final[str] = "c-block"
|
|
70
|
+
VARIANT_SEMICOLON: Final[str] = "semicolon"
|
|
71
|
+
VARIANT_DOUBLE_DASH: Final[str] = "double-dash"
|
|
72
|
+
VARIANT_EXEMPT: Final[str] = "exempt"
|
|
73
|
+
|
|
74
|
+
ALL_VARIANTS: Final[tuple[str, ...]] = (
|
|
75
|
+
VARIANT_HASH,
|
|
76
|
+
VARIANT_DOUBLE_SLASH,
|
|
77
|
+
VARIANT_HTML,
|
|
78
|
+
VARIANT_C_BLOCK,
|
|
79
|
+
VARIANT_SEMICOLON,
|
|
80
|
+
VARIANT_DOUBLE_DASH,
|
|
81
|
+
VARIANT_EXEMPT,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Header-malformation taxonomy consumed by the coverage scanner.
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
MALFORM_WRONG_VARIANT: Final[str] = "wrong-variant"
|
|
88
|
+
MALFORM_WRONG_LINE_ORDER: Final[str] = "wrong-line-order"
|
|
89
|
+
MALFORM_DRIFTED_CONTACT: Final[str] = "drifted-contact-info"
|
|
90
|
+
MALFORM_SMART_QUOTE: Final[str] = "smart-quote-pollution"
|
|
91
|
+
MALFORM_BOM_PREFIX: Final[str] = "bom-prefix"
|
|
92
|
+
MALFORM_TRAILING_WHITESPACE: Final[str] = "trailing-whitespace"
|
|
93
|
+
MALFORM_WRONG_LINE_COUNT: Final[str] = "wrong-line-count"
|
|
94
|
+
MALFORM_MIXED: Final[str] = "mixed"
|
|
95
|
+
|
|
96
|
+
ALL_MALFORMATIONS: Final[tuple[str, ...]] = (
|
|
97
|
+
MALFORM_WRONG_VARIANT,
|
|
98
|
+
MALFORM_WRONG_LINE_ORDER,
|
|
99
|
+
MALFORM_DRIFTED_CONTACT,
|
|
100
|
+
MALFORM_SMART_QUOTE,
|
|
101
|
+
MALFORM_BOM_PREFIX,
|
|
102
|
+
MALFORM_TRAILING_WHITESPACE,
|
|
103
|
+
MALFORM_WRONG_LINE_COUNT,
|
|
104
|
+
MALFORM_MIXED,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
# Variant-family resolution per spec §4.6.2.
|
|
109
|
+
#
|
|
110
|
+
# The mapping is consulted by suffix (lowercase) first; basename overrides
|
|
111
|
+
# follow for files with no suffix or with a name-only convention
|
|
112
|
+
# (Makefile, Dockerfile, etc.). Files whose suffix is not registered fall
|
|
113
|
+
# to ``exempt`` — the exception fixture is the authoritative gate, so a
|
|
114
|
+
# fall-through here only matters when the path also escapes the fixture.
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
SUFFIX_VARIANT: Final[dict[str, str]] = {
|
|
117
|
+
# `#` family
|
|
118
|
+
".sh": VARIANT_HASH,
|
|
119
|
+
".bash": VARIANT_HASH,
|
|
120
|
+
".zsh": VARIANT_HASH,
|
|
121
|
+
".py": VARIANT_HASH,
|
|
122
|
+
".rb": VARIANT_HASH,
|
|
123
|
+
".pl": VARIANT_HASH,
|
|
124
|
+
".ps1": VARIANT_HASH,
|
|
125
|
+
".psm1": VARIANT_HASH,
|
|
126
|
+
".psd1": VARIANT_HASH,
|
|
127
|
+
".yml": VARIANT_HASH,
|
|
128
|
+
".yaml": VARIANT_HASH,
|
|
129
|
+
".toml": VARIANT_HASH,
|
|
130
|
+
".cff": VARIANT_HASH,
|
|
131
|
+
".gitignore": VARIANT_HASH,
|
|
132
|
+
".gitattributes": VARIANT_HASH,
|
|
133
|
+
".editorconfig": VARIANT_HASH,
|
|
134
|
+
".shellcheckrc": VARIANT_HASH,
|
|
135
|
+
".env.example": VARIANT_HASH,
|
|
136
|
+
".cfg": VARIANT_HASH,
|
|
137
|
+
".conf": VARIANT_HASH,
|
|
138
|
+
# `//` family
|
|
139
|
+
".js": VARIANT_DOUBLE_SLASH,
|
|
140
|
+
".jsx": VARIANT_DOUBLE_SLASH,
|
|
141
|
+
".mjs": VARIANT_DOUBLE_SLASH,
|
|
142
|
+
".cjs": VARIANT_DOUBLE_SLASH,
|
|
143
|
+
".ts": VARIANT_DOUBLE_SLASH,
|
|
144
|
+
".tsx": VARIANT_DOUBLE_SLASH,
|
|
145
|
+
".go": VARIANT_DOUBLE_SLASH,
|
|
146
|
+
".rs": VARIANT_DOUBLE_SLASH,
|
|
147
|
+
".java": VARIANT_DOUBLE_SLASH,
|
|
148
|
+
".kt": VARIANT_DOUBLE_SLASH,
|
|
149
|
+
".swift": VARIANT_DOUBLE_SLASH,
|
|
150
|
+
".scala": VARIANT_DOUBLE_SLASH,
|
|
151
|
+
".dart": VARIANT_DOUBLE_SLASH,
|
|
152
|
+
".cs": VARIANT_DOUBLE_SLASH,
|
|
153
|
+
".jsonc": VARIANT_DOUBLE_SLASH,
|
|
154
|
+
# block-comment family (HTML-style wrapper)
|
|
155
|
+
".html": VARIANT_HTML,
|
|
156
|
+
".htm": VARIANT_HTML,
|
|
157
|
+
".md": VARIANT_HTML,
|
|
158
|
+
".markdown": VARIANT_HTML,
|
|
159
|
+
".mdc": VARIANT_HTML,
|
|
160
|
+
".xml": VARIANT_HTML,
|
|
161
|
+
".vue": VARIANT_HTML,
|
|
162
|
+
".php": VARIANT_HTML,
|
|
163
|
+
# `/* */` block family
|
|
164
|
+
".c": VARIANT_C_BLOCK,
|
|
165
|
+
".cc": VARIANT_C_BLOCK,
|
|
166
|
+
".cpp": VARIANT_C_BLOCK,
|
|
167
|
+
".h": VARIANT_C_BLOCK,
|
|
168
|
+
".hpp": VARIANT_C_BLOCK,
|
|
169
|
+
".css": VARIANT_C_BLOCK,
|
|
170
|
+
".scss": VARIANT_C_BLOCK,
|
|
171
|
+
".less": VARIANT_C_BLOCK,
|
|
172
|
+
".sql": VARIANT_DOUBLE_DASH,
|
|
173
|
+
# `;` family
|
|
174
|
+
".ini": VARIANT_SEMICOLON,
|
|
175
|
+
".lisp": VARIANT_SEMICOLON,
|
|
176
|
+
".scm": VARIANT_SEMICOLON,
|
|
177
|
+
# `--` family
|
|
178
|
+
".lua": VARIANT_DOUBLE_DASH,
|
|
179
|
+
".hs": VARIANT_DOUBLE_DASH,
|
|
180
|
+
".elm": VARIANT_DOUBLE_DASH,
|
|
181
|
+
".ada": VARIANT_DOUBLE_DASH,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
BASENAME_VARIANT: Final[dict[str, str]] = {
|
|
185
|
+
"Makefile": VARIANT_HASH,
|
|
186
|
+
"Dockerfile": VARIANT_HASH,
|
|
187
|
+
"Procfile": VARIANT_HASH,
|
|
188
|
+
"CODEOWNERS": VARIANT_HASH,
|
|
189
|
+
"apothem": VARIANT_HASH,
|
|
190
|
+
"PKGBUILD": VARIANT_HASH,
|
|
191
|
+
".gitignore": VARIANT_HASH,
|
|
192
|
+
".gitattributes": VARIANT_HASH,
|
|
193
|
+
".editorconfig": VARIANT_HASH,
|
|
194
|
+
".shellcheckrc": VARIANT_HASH,
|
|
195
|
+
".env.example": VARIANT_HASH,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
# Canonical header — the single SPDX-License-Identifier line (D-007).
|
|
200
|
+
#
|
|
201
|
+
# The header was narrowed from a six-line branded banner box to one
|
|
202
|
+
# machine-readable license-identifier line per comment-syntax variant.
|
|
203
|
+
# Only comment syntax varies between variants; the identifier text is
|
|
204
|
+
# invariant. Drift in the identifier line is a CI failure.
|
|
205
|
+
#
|
|
206
|
+
# SPDX_PREFIX_TEXT is the substring that identifies the SPDX line in any
|
|
207
|
+
# variant. LEGACY_AUTHOR_MARK is retained purely as a detection constant:
|
|
208
|
+
# a file still carrying the retired branded-banner author line (but not the
|
|
209
|
+
# narrowed SPDX line at the canonical site) is a not-yet-narrowed header and
|
|
210
|
+
# is counted malformed/uncovered, so the scan surfaces the remaining work.
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
SPDX_PREFIX_TEXT: Final[str] = "SPDX-License-Identifier:"
|
|
213
|
+
LEGACY_AUTHOR_MARK: Final[str] = "Copyright (c) Ahmed G. Gad"
|
|
214
|
+
|
|
215
|
+
# Smart-quote codepoints whose presence inside a banner is malformation
|
|
216
|
+
# class ``smart-quote-pollution``. Source-escaped so the
|
|
217
|
+
# scanner's own bytes never trigger the ambiguous-Unicode lint rule
|
|
218
|
+
# that would otherwise flag literal smart quotes.
|
|
219
|
+
SMART_QUOTES: Final[tuple[str, ...]] = (
|
|
220
|
+
"‘", # noqa: RUF001 - detection codepoint U+2018
|
|
221
|
+
"’", # noqa: RUF001 - detection codepoint U+2019
|
|
222
|
+
"“",
|
|
223
|
+
"”",
|
|
224
|
+
"–", # noqa: RUF001 - detection codepoint U+2013
|
|
225
|
+
"—",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
BOM_PREFIX: Final[str] = ""
|
|
229
|
+
|
|
230
|
+
# Number of leading lines scanned for banner detection. Must accommodate
|
|
231
|
+
# shebang + (optional) interpreter-pragma + frontmatter + banner shape.
|
|
232
|
+
SCAN_LINE_BUDGET: Final[int] = 60
|
|
233
|
+
|
|
234
|
+
# Per-file unified-diff context lines. Three lines is unified-diff
|
|
235
|
+
# default; the injection-plan diff uses a tighter window because the
|
|
236
|
+
# patch is always at the head of the file.
|
|
237
|
+
DIFF_CONTEXT_LINES: Final[int] = 3
|
|
238
|
+
|
|
239
|
+
EXIT_OK: Final[int] = 0
|
|
240
|
+
EXIT_ERROR: Final[int] = 1
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
# Canonical header rendering per variant family.
|
|
245
|
+
#
|
|
246
|
+
# The load / render / canonical-block helpers mirror
|
|
247
|
+
# src/apothem/conformity/file_header_grep.py byte-for-byte so the scanner,
|
|
248
|
+
# the validator, and the injector agree on the canonical form without
|
|
249
|
+
# sharing a module. The fixture at src/apothem/schemas/authorship-header.txt
|
|
250
|
+
# is a single hash-form SPDX line; every other variant is rendered from it
|
|
251
|
+
# by comment-marker substitution (hash / double-slash / semicolon /
|
|
252
|
+
# double-dash) or by wrapper composition (html / c-block).
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
def _replace_marker(line: str, source: str, target: str) -> str:
|
|
255
|
+
"""Substitute the leading comment marker on a header line.
|
|
256
|
+
|
|
257
|
+
The narrowed header line carries the comment marker on its leading edge
|
|
258
|
+
only; this swaps that edge. Mirrors scripts/inject-header.py and
|
|
259
|
+
file_header_grep.py to preserve injector / validator / scanner parity.
|
|
260
|
+
"""
|
|
261
|
+
if not line.startswith(source):
|
|
262
|
+
return line
|
|
263
|
+
if len(line) >= 2 * len(source) and line.endswith(source):
|
|
264
|
+
inner = line[len(source) : -len(source)]
|
|
265
|
+
return f"{target}{inner}{target}"
|
|
266
|
+
return target + line[len(source) :]
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _render_variant(hash_form_line: str, spdx_text: str, variant: str) -> list[str]:
|
|
270
|
+
"""Render the canonical SPDX header line for ``variant`` (no trailing blank)."""
|
|
271
|
+
if variant == VARIANT_HASH:
|
|
272
|
+
return [hash_form_line]
|
|
273
|
+
if variant == VARIANT_DOUBLE_SLASH:
|
|
274
|
+
return [_replace_marker(hash_form_line, "#", "//")]
|
|
275
|
+
if variant == VARIANT_SEMICOLON:
|
|
276
|
+
return [_replace_marker(hash_form_line, "#", ";")]
|
|
277
|
+
if variant == VARIANT_DOUBLE_DASH:
|
|
278
|
+
return [_replace_marker(hash_form_line, "#", "--")]
|
|
279
|
+
if variant == VARIANT_HTML:
|
|
280
|
+
return [f"<!-- {spdx_text} -->"]
|
|
281
|
+
if variant == VARIANT_C_BLOCK:
|
|
282
|
+
return [f"/* {spdx_text} */"]
|
|
283
|
+
raise ValueError(f"variant not renderable: {variant!r}")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _render_canonical_block(
|
|
287
|
+
hash_form_line: str, spdx_text: str, variant: str
|
|
288
|
+
) -> list[str]:
|
|
289
|
+
"""Render the canonical block including the mandatory trailing blank line."""
|
|
290
|
+
return [*_render_variant(hash_form_line, spdx_text, variant), ""]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _load_banner(schemas_dir: Path) -> tuple[str, str]:
|
|
294
|
+
"""Load the narrowed SPDX-line header fixture.
|
|
295
|
+
|
|
296
|
+
Mirrors file_header_grep.py ``_load_banner``: the fixture is the single
|
|
297
|
+
``# SPDX-License-Identifier: MIT`` line. Returns ``(hash_form_line,
|
|
298
|
+
spdx_text)`` on success, or two empty strings when the fixture is absent
|
|
299
|
+
or malformed (the caller treats the empty result as a skip).
|
|
300
|
+
"""
|
|
301
|
+
banner_path = schemas_dir / "authorship-header.txt"
|
|
302
|
+
if not banner_path.is_file():
|
|
303
|
+
return "", ""
|
|
304
|
+
raw = banner_path.read_text(encoding="utf-8")
|
|
305
|
+
lines = [line for line in raw.splitlines() if line.strip()]
|
|
306
|
+
if len(lines) != 1:
|
|
307
|
+
return "", ""
|
|
308
|
+
spdx_line = lines[0]
|
|
309
|
+
if SPDX_PREFIX_TEXT not in spdx_line or not spdx_line.startswith("#"):
|
|
310
|
+
return "", ""
|
|
311
|
+
return spdx_line, spdx_line[1:].strip()
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# Resolve the canonical header fixture once at import. The fixture lives at
|
|
315
|
+
# src/apothem/schemas/ relative to this file (audit/ → apothem/ → schemas/).
|
|
316
|
+
_SCHEMAS_DIR: Final[Path] = Path(__file__).resolve().parent.parent / "schemas"
|
|
317
|
+
_HASH_FORM_LINE, _SPDX_TEXT = _load_banner(_SCHEMAS_DIR)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def canonical_banner_lines(variant: str) -> list[str]:
|
|
321
|
+
"""Return the canonical header lines for ``variant``.
|
|
322
|
+
|
|
323
|
+
The narrowed header is a single line per variant. Raises ``ValueError``
|
|
324
|
+
if ``variant`` is not a renderable family; the caller filters exempt
|
|
325
|
+
files before requesting a canonical form.
|
|
326
|
+
"""
|
|
327
|
+
return _render_variant(_HASH_FORM_LINE, _SPDX_TEXT, variant)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def canonical_banner_text(variant: str) -> str:
|
|
331
|
+
"""Return the canonical header as one newline-terminated text block."""
|
|
332
|
+
return "\n".join(canonical_banner_lines(variant)) + "\n"
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# ---------------------------------------------------------------------------
|
|
336
|
+
# Variant resolution.
|
|
337
|
+
# ---------------------------------------------------------------------------
|
|
338
|
+
def variant_family_for(relative_path: Path) -> str:
|
|
339
|
+
"""Resolve the canonical variant family for the file's filetype.
|
|
340
|
+
|
|
341
|
+
Suffix lookup is the primary signal; basename overrides cover
|
|
342
|
+
suffix-less artifacts (Makefile / Dockerfile / dotfiles whose
|
|
343
|
+
name encodes the convention). When neither resolves, the file is
|
|
344
|
+
treated as ``exempt`` — the exception fixture remains the
|
|
345
|
+
authoritative applicability gate, so a fall-through here only
|
|
346
|
+
matters for files the fixture also fails to cover.
|
|
347
|
+
"""
|
|
348
|
+
name = relative_path.name
|
|
349
|
+
if name in BASENAME_VARIANT:
|
|
350
|
+
return BASENAME_VARIANT[name]
|
|
351
|
+
|
|
352
|
+
suffix = relative_path.suffix.lower()
|
|
353
|
+
if suffix in SUFFIX_VARIANT:
|
|
354
|
+
return SUFFIX_VARIANT[suffix]
|
|
355
|
+
|
|
356
|
+
return VARIANT_EXEMPT
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# ---------------------------------------------------------------------------
|
|
360
|
+
# Exception-fixture parsing.
|
|
361
|
+
# ---------------------------------------------------------------------------
|
|
362
|
+
def load_exception_globs(fixture_path: Path) -> list[str]:
|
|
363
|
+
"""Parse ``src/apothem/schemas/header-exceptions.txt`` into a glob list.
|
|
364
|
+
|
|
365
|
+
Comment lines (``#``-prefixed) and blank lines are stripped; every
|
|
366
|
+
other line becomes a pathspec. The order is preserved so a future
|
|
367
|
+
deny-list / allow-list extension can rely on first-match semantics.
|
|
368
|
+
"""
|
|
369
|
+
if not fixture_path.is_file():
|
|
370
|
+
return []
|
|
371
|
+
globs: list[str] = []
|
|
372
|
+
for raw in fixture_path.read_text(encoding="utf-8").splitlines():
|
|
373
|
+
line = raw.strip()
|
|
374
|
+
if not line or line.startswith("#"):
|
|
375
|
+
continue
|
|
376
|
+
globs.append(line)
|
|
377
|
+
return globs
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def matches_exception(relative_path: str, globs: list[str]) -> str | None:
|
|
381
|
+
"""Return the first matching glob, or ``None`` when the path is in scope.
|
|
382
|
+
|
|
383
|
+
Patterns containing ``**`` are translated to fnmatch-friendly form by
|
|
384
|
+
treating ``**`` as ``*`` across path separators (fnmatch handles
|
|
385
|
+
cross-segment expansion when the path is expressed as POSIX-style).
|
|
386
|
+
"""
|
|
387
|
+
posix = relative_path.replace("\\", "/")
|
|
388
|
+
for pattern in globs:
|
|
389
|
+
if _fnmatch_with_globstar(posix, pattern):
|
|
390
|
+
return pattern
|
|
391
|
+
return None
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _fnmatch_with_globstar(path: str, pattern: str) -> bool:
|
|
395
|
+
"""Return ``True`` when ``pattern`` matches ``path`` under fnmatch
|
|
396
|
+
semantics extended for ``**`` cross-segment expansion.
|
|
397
|
+
|
|
398
|
+
fnmatch alone treats ``*`` as non-greedy across separators; ``**``
|
|
399
|
+
here matches zero or more path segments (the conventional gitignore
|
|
400
|
+
/gitattributes / Unix glob semantics). Translation is deliberately
|
|
401
|
+
direct — character-by-character regex synthesis with the four
|
|
402
|
+
glob-significant tokens (``**``, ``*``, ``?``, character classes)
|
|
403
|
+
handled inline; ordinary characters are escaped via ``re.escape``.
|
|
404
|
+
"""
|
|
405
|
+
if (
|
|
406
|
+
"**" not in pattern
|
|
407
|
+
and "*" not in pattern
|
|
408
|
+
and "?" not in pattern
|
|
409
|
+
and "[" not in pattern
|
|
410
|
+
):
|
|
411
|
+
return path == pattern
|
|
412
|
+
|
|
413
|
+
import re
|
|
414
|
+
|
|
415
|
+
regex_parts: list[str] = []
|
|
416
|
+
i = 0
|
|
417
|
+
pattern_length = len(pattern)
|
|
418
|
+
while i < pattern_length:
|
|
419
|
+
char = pattern[i]
|
|
420
|
+
if char == "*":
|
|
421
|
+
if i + 1 < pattern_length and pattern[i + 1] == "*":
|
|
422
|
+
# `**` token. When followed by `/`, the segment is
|
|
423
|
+
# optional — `**/foo` matches `foo` AND `a/foo`. When
|
|
424
|
+
# standalone (e.g. trailing `**`), the token matches
|
|
425
|
+
# any remaining suffix including empty.
|
|
426
|
+
if i + 2 < pattern_length and pattern[i + 2] == "/":
|
|
427
|
+
regex_parts.append("(?:.*/)?")
|
|
428
|
+
i += 3
|
|
429
|
+
continue
|
|
430
|
+
regex_parts.append(".*")
|
|
431
|
+
i += 2
|
|
432
|
+
continue
|
|
433
|
+
# Single `*` — match within a segment only.
|
|
434
|
+
regex_parts.append("[^/]*")
|
|
435
|
+
i += 1
|
|
436
|
+
continue
|
|
437
|
+
if char == "?":
|
|
438
|
+
regex_parts.append("[^/]")
|
|
439
|
+
i += 1
|
|
440
|
+
continue
|
|
441
|
+
regex_parts.append(re.escape(char))
|
|
442
|
+
i += 1
|
|
443
|
+
|
|
444
|
+
full = "^" + "".join(regex_parts) + "$"
|
|
445
|
+
return re.match(full, path) is not None
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# ---------------------------------------------------------------------------
|
|
449
|
+
# Per-file scan.
|
|
450
|
+
# ---------------------------------------------------------------------------
|
|
451
|
+
@dataclass(slots=True)
|
|
452
|
+
class FileCoverage:
|
|
453
|
+
"""Per-file coverage record emitted to ``header-coverage.json``."""
|
|
454
|
+
|
|
455
|
+
path: str
|
|
456
|
+
applicable: bool
|
|
457
|
+
exception_class: str | None
|
|
458
|
+
header_status: str
|
|
459
|
+
variant_family: str
|
|
460
|
+
header_line_range: tuple[int, int] | None
|
|
461
|
+
malformation_class: str | None
|
|
462
|
+
malformation_detail: str | None
|
|
463
|
+
injection_plan: dict[str, object] | None
|
|
464
|
+
|
|
465
|
+
def to_json(self) -> dict[str, object]:
|
|
466
|
+
return {
|
|
467
|
+
"path": self.path,
|
|
468
|
+
"applicable": self.applicable,
|
|
469
|
+
"exception-class": self.exception_class,
|
|
470
|
+
"header-status": self.header_status,
|
|
471
|
+
"variant-family": self.variant_family,
|
|
472
|
+
"header-line-range": (
|
|
473
|
+
list(self.header_line_range) if self.header_line_range else None
|
|
474
|
+
),
|
|
475
|
+
"malformation-class": self.malformation_class,
|
|
476
|
+
"malformation-detail": self.malformation_detail,
|
|
477
|
+
"injection-plan": self.injection_plan,
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
@dataclass(slots=True)
|
|
482
|
+
class CoverageSummary:
|
|
483
|
+
"""Aggregate counts emitted alongside the per-file array."""
|
|
484
|
+
|
|
485
|
+
total_files: int = 0
|
|
486
|
+
applicable_total: int = 0
|
|
487
|
+
present_canonical: int = 0
|
|
488
|
+
present_malformed: int = 0
|
|
489
|
+
absent: int = 0
|
|
490
|
+
not_applicable: int = 0
|
|
491
|
+
coverage_pct: float = 0.0
|
|
492
|
+
by_variant_family: dict[str, int] = field(default_factory=dict)
|
|
493
|
+
by_malformation_class: dict[str, int] = field(default_factory=dict)
|
|
494
|
+
|
|
495
|
+
def to_json(self) -> dict[str, object]:
|
|
496
|
+
return {
|
|
497
|
+
"total-files": self.total_files,
|
|
498
|
+
"applicable-total": self.applicable_total,
|
|
499
|
+
"present-canonical": self.present_canonical,
|
|
500
|
+
"present-malformed": self.present_malformed,
|
|
501
|
+
"absent": self.absent,
|
|
502
|
+
"not-applicable": self.not_applicable,
|
|
503
|
+
"coverage-pct": round(self.coverage_pct, 2),
|
|
504
|
+
"by-variant-family": dict(sorted(self.by_variant_family.items())),
|
|
505
|
+
"by-malformation-class": dict(sorted(self.by_malformation_class.items())),
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def read_head(absolute_path: Path, line_budget: int = SCAN_LINE_BUDGET) -> list[str]:
|
|
510
|
+
"""Return the file's leading lines (without trailing newlines).
|
|
511
|
+
|
|
512
|
+
UTF-8 with replacement fallback keeps mixed-encoding corpora
|
|
513
|
+
walkable. A binary file mis-classified as text yields garbled lines
|
|
514
|
+
but never crashes the scan.
|
|
515
|
+
"""
|
|
516
|
+
try:
|
|
517
|
+
with absolute_path.open("r", encoding="utf-8", errors="replace") as handle:
|
|
518
|
+
head: list[str] = []
|
|
519
|
+
for index, line in enumerate(handle):
|
|
520
|
+
if index >= line_budget:
|
|
521
|
+
break
|
|
522
|
+
head.append(line.rstrip("\r\n"))
|
|
523
|
+
return head
|
|
524
|
+
except OSError:
|
|
525
|
+
return []
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def has_shebang(head_lines: list[str]) -> bool:
|
|
529
|
+
"""Return ``True`` when the first line begins with ``#!``."""
|
|
530
|
+
return bool(head_lines) and head_lines[0].startswith("#!")
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def insertion_line(head_lines: list[str]) -> int:
|
|
534
|
+
"""Return the 1-based line at which the canonical banner is inserted.
|
|
535
|
+
|
|
536
|
+
The banner is the file's first content with the single exception of
|
|
537
|
+
a shebang line (``#!``), which always remains line 1; the banner
|
|
538
|
+
starts on line 2 in that case. Frontmatter (``---``) follows the
|
|
539
|
+
banner per spec §4.6.3.
|
|
540
|
+
"""
|
|
541
|
+
return 2 if has_shebang(head_lines) else 1
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def scan_for_banner(
|
|
545
|
+
head_lines: list[str], variant: str
|
|
546
|
+
) -> tuple[str, tuple[int, int] | None, str | None, str | None]:
|
|
547
|
+
"""Inspect leading lines for the canonical header and return a verdict.
|
|
548
|
+
|
|
549
|
+
Returns a 4-tuple ``(header_status, line_range, malformation_class,
|
|
550
|
+
malformation_detail)``. ``line_range`` is ``None`` for absent headers;
|
|
551
|
+
the others carry detail when the header is present but malformed.
|
|
552
|
+
|
|
553
|
+
The narrowed header is a single line, so the verdict reduces to a
|
|
554
|
+
byte-exact comparison of the canonical block (the SPDX line plus its
|
|
555
|
+
mandatory trailing blank) at the insertion site — the same block the
|
|
556
|
+
validator at file_header_grep.py compares. A header present at the site
|
|
557
|
+
but not byte-exact is classified against the malformation slots a
|
|
558
|
+
one-line header can exhibit: bom-prefix, smart-quote-pollution,
|
|
559
|
+
trailing-whitespace, wrong-variant, and a wrong-line-count fall-through.
|
|
560
|
+
The retired branded-banner box (whose first line was the SPDX line but
|
|
561
|
+
which lacks the trailing blank) lands here as not-yet-narrowed; a file
|
|
562
|
+
with neither the SPDX line nor the legacy author mark is absent.
|
|
563
|
+
"""
|
|
564
|
+
if not head_lines:
|
|
565
|
+
return HEADER_ABSENT, None, None, None
|
|
566
|
+
|
|
567
|
+
# BOM detection is independent of variant: any BOM byte before the
|
|
568
|
+
# header is itself a malformation, even if the rest is byte-exact.
|
|
569
|
+
bom_observed = head_lines[0].startswith(BOM_PREFIX)
|
|
570
|
+
|
|
571
|
+
# Canonical block = the variant line plus one trailing blank, mirroring
|
|
572
|
+
# _render_canonical_block / file_header_grep's _is_canonical_at_position.
|
|
573
|
+
canonical_block = _render_canonical_block(_HASH_FORM_LINE, _SPDX_TEXT, variant)
|
|
574
|
+
|
|
575
|
+
# The header sits at the insertion site: line index 1 after a shebang,
|
|
576
|
+
# else line index 0. The reported range is the SPDX line itself (the
|
|
577
|
+
# trailing blank completes the block but is not part of the header).
|
|
578
|
+
site_index = 1 if has_shebang(head_lines) else 0
|
|
579
|
+
if site_index >= len(head_lines):
|
|
580
|
+
# The file is shorter than the insertion site — header absent.
|
|
581
|
+
return HEADER_ABSENT, None, None, None
|
|
582
|
+
|
|
583
|
+
observed_line = head_lines[site_index]
|
|
584
|
+
line_range = (site_index + 1, site_index + 1)
|
|
585
|
+
|
|
586
|
+
# Byte-exact comparison of the full canonical block at the site.
|
|
587
|
+
block_end = site_index + len(canonical_block)
|
|
588
|
+
block_canonical = (
|
|
589
|
+
block_end <= len(head_lines)
|
|
590
|
+
and head_lines[site_index:block_end] == canonical_block
|
|
591
|
+
)
|
|
592
|
+
if block_canonical and not bom_observed:
|
|
593
|
+
return HEADER_PRESENT_CANONICAL, line_range, None, None
|
|
594
|
+
|
|
595
|
+
# The site does not carry the byte-exact canonical line. Decide whether
|
|
596
|
+
# any recognizable header is present at all: a SPDX line in some shape,
|
|
597
|
+
# or the retired branded-banner author mark anywhere in the head.
|
|
598
|
+
spdx_at_site = SPDX_PREFIX_TEXT in observed_line
|
|
599
|
+
legacy_present = any(LEGACY_AUTHOR_MARK in line for line in head_lines)
|
|
600
|
+
|
|
601
|
+
if not spdx_at_site and not legacy_present and not bom_observed:
|
|
602
|
+
return HEADER_ABSENT, None, None, None
|
|
603
|
+
|
|
604
|
+
# A recognizable-but-non-canonical header is present. Classify the
|
|
605
|
+
# malformation against the slots a one-line header can exhibit; multiple
|
|
606
|
+
# matches collapse to ``mixed``.
|
|
607
|
+
detected: list[tuple[str, str]] = []
|
|
608
|
+
|
|
609
|
+
if bom_observed:
|
|
610
|
+
detected.append((MALFORM_BOM_PREFIX, "U+FEFF byte order mark prefix"))
|
|
611
|
+
|
|
612
|
+
# Smart-quote pollution: any non-ASCII smart quote on the header line.
|
|
613
|
+
smart_hits = [q for q in SMART_QUOTES if q in observed_line]
|
|
614
|
+
if smart_hits:
|
|
615
|
+
detected.append(
|
|
616
|
+
(
|
|
617
|
+
MALFORM_SMART_QUOTE,
|
|
618
|
+
"non-ASCII typography in header: "
|
|
619
|
+
+ ", ".join(repr(q) for q in smart_hits),
|
|
620
|
+
)
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
# Trailing whitespace: the header line carries whitespace beyond the
|
|
624
|
+
# canonical line's content.
|
|
625
|
+
if observed_line.rstrip() != observed_line:
|
|
626
|
+
detected.append(
|
|
627
|
+
(MALFORM_TRAILING_WHITESPACE, "trailing whitespace on header line"),
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
# Wrong-variant: the SPDX line uses a comment marker different from the
|
|
631
|
+
# canonical for this filetype.
|
|
632
|
+
if spdx_at_site:
|
|
633
|
+
wrong_variant = _detect_wrong_variant(observed_line, variant)
|
|
634
|
+
if wrong_variant is not None:
|
|
635
|
+
detected.append(
|
|
636
|
+
(MALFORM_WRONG_VARIANT, f"header uses {wrong_variant!r} marker"),
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
# Fall-through: present but not byte-exact and no finer class fired.
|
|
640
|
+
# The retired branded-banner box is a not-yet-narrowed header and lands
|
|
641
|
+
# here as a wrong-line-count (the multi-line legacy box vs. one line).
|
|
642
|
+
if not detected:
|
|
643
|
+
if legacy_present and not spdx_at_site:
|
|
644
|
+
detected.append(
|
|
645
|
+
(
|
|
646
|
+
MALFORM_WRONG_LINE_COUNT,
|
|
647
|
+
"retired branded-banner box present; expected single SPDX line",
|
|
648
|
+
),
|
|
649
|
+
)
|
|
650
|
+
else:
|
|
651
|
+
detected.append(
|
|
652
|
+
(
|
|
653
|
+
MALFORM_WRONG_LINE_COUNT,
|
|
654
|
+
"header present but does not match canonical bytes",
|
|
655
|
+
),
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
# Resolve to a single class: more than one finding ⇒ mixed.
|
|
659
|
+
if len(detected) == 1:
|
|
660
|
+
cls, detail = detected[0]
|
|
661
|
+
else:
|
|
662
|
+
cls = MALFORM_MIXED
|
|
663
|
+
detail = "; ".join(f"{c}: {d}" for c, d in detected)
|
|
664
|
+
|
|
665
|
+
return HEADER_PRESENT_MALFORMED, line_range, cls, detail
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def _detect_wrong_variant(observed_line: str, target_variant: str) -> str | None:
|
|
669
|
+
"""Return the comment-syntax marker observed on the SPDX header line when
|
|
670
|
+
it disagrees with the target variant; ``None`` when the marker is
|
|
671
|
+
correct or the line shape does not let us tell.
|
|
672
|
+
|
|
673
|
+
The comment-marker prefix (or html/c-block wrapper) determines which
|
|
674
|
+
variant the header *was* written in.
|
|
675
|
+
"""
|
|
676
|
+
stripped = observed_line.lstrip()
|
|
677
|
+
if target_variant == VARIANT_HTML:
|
|
678
|
+
if not stripped.startswith("<!--"):
|
|
679
|
+
return "missing <!-- wrapper"
|
|
680
|
+
return None
|
|
681
|
+
if target_variant == VARIANT_C_BLOCK:
|
|
682
|
+
if not stripped.startswith("/*"):
|
|
683
|
+
return "missing /* wrapper"
|
|
684
|
+
return None
|
|
685
|
+
|
|
686
|
+
target_prefix_map = {
|
|
687
|
+
VARIANT_HASH: "#",
|
|
688
|
+
VARIANT_DOUBLE_SLASH: "//",
|
|
689
|
+
VARIANT_SEMICOLON: ";",
|
|
690
|
+
VARIANT_DOUBLE_DASH: "--",
|
|
691
|
+
}
|
|
692
|
+
target_prefix = target_prefix_map.get(target_variant)
|
|
693
|
+
if target_prefix is None:
|
|
694
|
+
return None
|
|
695
|
+
if not stripped.startswith(target_prefix):
|
|
696
|
+
# Sniff which marker the line uses instead.
|
|
697
|
+
for sniff_prefix in ("//", "--", ";", "#", "<!--", "/*"):
|
|
698
|
+
if stripped.startswith(sniff_prefix):
|
|
699
|
+
return sniff_prefix
|
|
700
|
+
return "unrecognized marker"
|
|
701
|
+
return None
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
# ---------------------------------------------------------------------------
|
|
705
|
+
# Injection-plan emission.
|
|
706
|
+
# ---------------------------------------------------------------------------
|
|
707
|
+
def build_injection_plan(
|
|
708
|
+
relative_path: str,
|
|
709
|
+
head_lines: list[str],
|
|
710
|
+
header_status: str,
|
|
711
|
+
variant: str,
|
|
712
|
+
header_line_range: tuple[int, int] | None,
|
|
713
|
+
) -> dict[str, object] | None:
|
|
714
|
+
"""Return the unified-diff-bearing injection plan for an absent or
|
|
715
|
+
malformed banner; ``None`` when no action is required.
|
|
716
|
+
"""
|
|
717
|
+
if header_status == HEADER_PRESENT_CANONICAL:
|
|
718
|
+
return None
|
|
719
|
+
if header_status == HEADER_NOT_APPLICABLE:
|
|
720
|
+
return None
|
|
721
|
+
if variant == VARIANT_EXEMPT:
|
|
722
|
+
return None
|
|
723
|
+
|
|
724
|
+
canonical_lines = canonical_banner_lines(variant)
|
|
725
|
+
insert_at = insertion_line(head_lines)
|
|
726
|
+
|
|
727
|
+
if header_status == HEADER_ABSENT:
|
|
728
|
+
action = "insert"
|
|
729
|
+
before_lines = list(head_lines)
|
|
730
|
+
# Insertion: shebang preserved at line 1; banner begins at
|
|
731
|
+
# `insert_at`; existing content shifts down. A single blank
|
|
732
|
+
# separator follows the banner.
|
|
733
|
+
after_lines: list[str] = []
|
|
734
|
+
for idx, line in enumerate(before_lines, start=1):
|
|
735
|
+
if idx == insert_at:
|
|
736
|
+
after_lines.extend(canonical_lines)
|
|
737
|
+
after_lines.append("")
|
|
738
|
+
after_lines.append(line)
|
|
739
|
+
else:
|
|
740
|
+
after_lines.append(line)
|
|
741
|
+
if not before_lines:
|
|
742
|
+
after_lines = list(canonical_lines)
|
|
743
|
+
after_lines.append("")
|
|
744
|
+
replacement_lines = None
|
|
745
|
+
else:
|
|
746
|
+
# present-malformed — replace the malformed range with canonical.
|
|
747
|
+
action = "replace"
|
|
748
|
+
before_lines = list(head_lines)
|
|
749
|
+
if header_line_range is None:
|
|
750
|
+
return None
|
|
751
|
+
start_1based, end_1based = header_line_range
|
|
752
|
+
start_idx = start_1based - 1
|
|
753
|
+
end_idx = end_1based # slice is half-open
|
|
754
|
+
after_lines = (
|
|
755
|
+
before_lines[:start_idx] + canonical_lines + before_lines[end_idx:]
|
|
756
|
+
)
|
|
757
|
+
replacement_lines = [start_1based, end_1based]
|
|
758
|
+
|
|
759
|
+
diff = list(
|
|
760
|
+
difflib.unified_diff(
|
|
761
|
+
before_lines,
|
|
762
|
+
after_lines,
|
|
763
|
+
fromfile=f"a/{relative_path}",
|
|
764
|
+
tofile=f"b/{relative_path}",
|
|
765
|
+
n=DIFF_CONTEXT_LINES,
|
|
766
|
+
lineterm="",
|
|
767
|
+
)
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
truncated = False
|
|
771
|
+
diff_text = "\n".join(diff)
|
|
772
|
+
if len(diff_text) > 4_000:
|
|
773
|
+
diff_text = diff_text[:4_000] + "\n[... diff truncated ...]"
|
|
774
|
+
truncated = True
|
|
775
|
+
|
|
776
|
+
return {
|
|
777
|
+
"needed": True,
|
|
778
|
+
"action": action,
|
|
779
|
+
"insertion-line": insert_at if action == "insert" else None,
|
|
780
|
+
"replacement-lines": replacement_lines,
|
|
781
|
+
"diff": diff_text,
|
|
782
|
+
"diff-truncated": truncated,
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
# ---------------------------------------------------------------------------
|
|
787
|
+
# Top-level scan.
|
|
788
|
+
# ---------------------------------------------------------------------------
|
|
789
|
+
def scan_inventory(
|
|
790
|
+
inventory_path: Path,
|
|
791
|
+
fixture_path: Path,
|
|
792
|
+
root: Path,
|
|
793
|
+
) -> tuple[list[FileCoverage], CoverageSummary, dict[str, str]]:
|
|
794
|
+
"""Walk inventory records, classify every file, and emit coverage rows."""
|
|
795
|
+
raw = inventory_path.read_bytes()
|
|
796
|
+
inventory_sha = hashlib.sha256(raw).hexdigest()
|
|
797
|
+
inventory = json.loads(raw.decode("utf-8"))
|
|
798
|
+
records = inventory.get("files", [])
|
|
799
|
+
|
|
800
|
+
fixture_bytes = fixture_path.read_bytes() if fixture_path.is_file() else b""
|
|
801
|
+
fixture_sha = hashlib.sha256(fixture_bytes).hexdigest() if fixture_bytes else ""
|
|
802
|
+
globs = load_exception_globs(fixture_path)
|
|
803
|
+
|
|
804
|
+
rows: list[FileCoverage] = []
|
|
805
|
+
summary = CoverageSummary(total_files=len(records))
|
|
806
|
+
for variant in ALL_VARIANTS:
|
|
807
|
+
summary.by_variant_family[variant] = 0
|
|
808
|
+
for malform in ALL_MALFORMATIONS:
|
|
809
|
+
summary.by_malformation_class[malform] = 0
|
|
810
|
+
|
|
811
|
+
for record in records:
|
|
812
|
+
rel_path = str(record.get("path", ""))
|
|
813
|
+
if not rel_path:
|
|
814
|
+
continue
|
|
815
|
+
absolute = root / rel_path
|
|
816
|
+
relative_obj = Path(rel_path)
|
|
817
|
+
|
|
818
|
+
# Applicability: the exception fixture is the authoritative gate.
|
|
819
|
+
# A path matching any pattern is `not-applicable`. Otherwise the
|
|
820
|
+
# variant family resolution decides whether the file's filetype
|
|
821
|
+
# carries a banner at all.
|
|
822
|
+
exception_class = matches_exception(rel_path, globs)
|
|
823
|
+
variant = variant_family_for(relative_obj)
|
|
824
|
+
|
|
825
|
+
if exception_class is not None or variant == VARIANT_EXEMPT:
|
|
826
|
+
row = FileCoverage(
|
|
827
|
+
path=rel_path,
|
|
828
|
+
applicable=False,
|
|
829
|
+
exception_class=exception_class
|
|
830
|
+
or (None if variant != VARIANT_EXEMPT else "unsupported-extension"),
|
|
831
|
+
header_status=HEADER_NOT_APPLICABLE,
|
|
832
|
+
variant_family=variant,
|
|
833
|
+
header_line_range=None,
|
|
834
|
+
malformation_class=None,
|
|
835
|
+
malformation_detail=None,
|
|
836
|
+
injection_plan=None,
|
|
837
|
+
)
|
|
838
|
+
rows.append(row)
|
|
839
|
+
summary.not_applicable += 1
|
|
840
|
+
summary.by_variant_family[variant] += 1
|
|
841
|
+
continue
|
|
842
|
+
|
|
843
|
+
head = read_head(absolute)
|
|
844
|
+
status, line_range, malform, detail = scan_for_banner(head, variant)
|
|
845
|
+
plan = build_injection_plan(rel_path, head, status, variant, line_range)
|
|
846
|
+
|
|
847
|
+
row = FileCoverage(
|
|
848
|
+
path=rel_path,
|
|
849
|
+
applicable=True,
|
|
850
|
+
exception_class=None,
|
|
851
|
+
header_status=status,
|
|
852
|
+
variant_family=variant,
|
|
853
|
+
header_line_range=line_range,
|
|
854
|
+
malformation_class=malform,
|
|
855
|
+
malformation_detail=detail,
|
|
856
|
+
injection_plan=plan,
|
|
857
|
+
)
|
|
858
|
+
rows.append(row)
|
|
859
|
+
summary.applicable_total += 1
|
|
860
|
+
summary.by_variant_family[variant] += 1
|
|
861
|
+
if status == HEADER_PRESENT_CANONICAL:
|
|
862
|
+
summary.present_canonical += 1
|
|
863
|
+
elif status == HEADER_PRESENT_MALFORMED:
|
|
864
|
+
summary.present_malformed += 1
|
|
865
|
+
if malform is not None:
|
|
866
|
+
summary.by_malformation_class[malform] += 1
|
|
867
|
+
elif status == HEADER_ABSENT:
|
|
868
|
+
summary.absent += 1
|
|
869
|
+
else:
|
|
870
|
+
summary.not_applicable += 1
|
|
871
|
+
|
|
872
|
+
if summary.applicable_total > 0:
|
|
873
|
+
summary.coverage_pct = (
|
|
874
|
+
100.0 * summary.present_canonical / summary.applicable_total
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
provenance = {
|
|
878
|
+
"inventory-source": str(inventory_path),
|
|
879
|
+
"inventory-sha256": inventory_sha,
|
|
880
|
+
"exception-fixture-source": str(fixture_path),
|
|
881
|
+
"exception-fixture-sha256": fixture_sha,
|
|
882
|
+
}
|
|
883
|
+
return rows, summary, provenance
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
# ---------------------------------------------------------------------------
|
|
887
|
+
# Markdown mirror emission.
|
|
888
|
+
# ---------------------------------------------------------------------------
|
|
889
|
+
def render_markdown(
|
|
890
|
+
rows: list[FileCoverage],
|
|
891
|
+
summary: CoverageSummary,
|
|
892
|
+
provenance: dict[str, str],
|
|
893
|
+
generated_at: str,
|
|
894
|
+
) -> str:
|
|
895
|
+
"""Render the human-readable coverage mirror."""
|
|
896
|
+
lines: list[str] = []
|
|
897
|
+
lines.append("# Authorship-Header Coverage Map")
|
|
898
|
+
lines.append("")
|
|
899
|
+
lines.append(f"- **Generated:** {generated_at}")
|
|
900
|
+
lines.append(f"- **Inventory source:** `{provenance['inventory-source']}`")
|
|
901
|
+
lines.append(f"- **Inventory SHA-256:** `{provenance['inventory-sha256']}`")
|
|
902
|
+
lines.append(f"- **Exception fixture:** `{provenance['exception-fixture-source']}`")
|
|
903
|
+
lines.append(
|
|
904
|
+
f"- **Exception fixture SHA-256:** `{provenance['exception-fixture-sha256']}`"
|
|
905
|
+
)
|
|
906
|
+
lines.append("")
|
|
907
|
+
lines.append("## Aggregate Statistics")
|
|
908
|
+
lines.append("")
|
|
909
|
+
lines.append("| Metric | Value |")
|
|
910
|
+
lines.append("|---|---:|")
|
|
911
|
+
lines.append(f"| Total files inventoried | {summary.total_files} |")
|
|
912
|
+
lines.append(f"| Applicable | {summary.applicable_total} |")
|
|
913
|
+
lines.append(f"| Present (canonical) | {summary.present_canonical} |")
|
|
914
|
+
lines.append(f"| Present (malformed) | {summary.present_malformed} |")
|
|
915
|
+
lines.append(f"| Absent | {summary.absent} |")
|
|
916
|
+
lines.append(f"| Not applicable | {summary.not_applicable} |")
|
|
917
|
+
lines.append(f"| Coverage % | {summary.coverage_pct:.2f}% |")
|
|
918
|
+
lines.append("")
|
|
919
|
+
|
|
920
|
+
lines.append("## Counts by Variant Family")
|
|
921
|
+
lines.append("")
|
|
922
|
+
lines.append("| Variant family | Count |")
|
|
923
|
+
lines.append("|---|---:|")
|
|
924
|
+
for variant in ALL_VARIANTS:
|
|
925
|
+
lines.append(f"| `{variant}` | {summary.by_variant_family.get(variant, 0)} |")
|
|
926
|
+
lines.append("")
|
|
927
|
+
|
|
928
|
+
lines.append("## Counts by Malformation Class")
|
|
929
|
+
lines.append("")
|
|
930
|
+
lines.append("| Malformation class | Count |")
|
|
931
|
+
lines.append("|---|---:|")
|
|
932
|
+
for malform in ALL_MALFORMATIONS:
|
|
933
|
+
lines.append(
|
|
934
|
+
f"| `{malform}` | {summary.by_malformation_class.get(malform, 0)} |"
|
|
935
|
+
)
|
|
936
|
+
lines.append("")
|
|
937
|
+
|
|
938
|
+
# Per-file injection-plan tables, partitioned by status.
|
|
939
|
+
absent_rows = [r for r in rows if r.header_status == HEADER_ABSENT]
|
|
940
|
+
malformed_rows = [r for r in rows if r.header_status == HEADER_PRESENT_MALFORMED]
|
|
941
|
+
canonical_rows = [r for r in rows if r.header_status == HEADER_PRESENT_CANONICAL]
|
|
942
|
+
|
|
943
|
+
if canonical_rows:
|
|
944
|
+
lines.append(f"## Files with Canonical Banner ({len(canonical_rows)})")
|
|
945
|
+
lines.append("")
|
|
946
|
+
lines.append("| Path | Variant | Lines |")
|
|
947
|
+
lines.append("|---|---|---|")
|
|
948
|
+
for row in sorted(canonical_rows, key=lambda r: r.path):
|
|
949
|
+
rng = (
|
|
950
|
+
f"{row.header_line_range[0]}-{row.header_line_range[1]}"
|
|
951
|
+
if row.header_line_range
|
|
952
|
+
else "—"
|
|
953
|
+
)
|
|
954
|
+
lines.append(f"| `{row.path}` | `{row.variant_family}` | {rng} |")
|
|
955
|
+
lines.append("")
|
|
956
|
+
|
|
957
|
+
if malformed_rows:
|
|
958
|
+
lines.append(f"## Files with Malformed Banner ({len(malformed_rows)})")
|
|
959
|
+
lines.append("")
|
|
960
|
+
lines.append("| Path | Variant | Lines | Class | Detail |")
|
|
961
|
+
lines.append("|---|---|---|---|---|")
|
|
962
|
+
for row in sorted(malformed_rows, key=lambda r: r.path):
|
|
963
|
+
rng = (
|
|
964
|
+
f"{row.header_line_range[0]}-{row.header_line_range[1]}"
|
|
965
|
+
if row.header_line_range
|
|
966
|
+
else "—"
|
|
967
|
+
)
|
|
968
|
+
detail = (row.malformation_detail or "").replace("|", r"\|")
|
|
969
|
+
lines.append(
|
|
970
|
+
f"| `{row.path}` | `{row.variant_family}` | {rng} | "
|
|
971
|
+
f"`{row.malformation_class}` | {detail} |"
|
|
972
|
+
)
|
|
973
|
+
lines.append("")
|
|
974
|
+
|
|
975
|
+
if absent_rows:
|
|
976
|
+
lines.append(f"## Files Missing the Banner ({len(absent_rows)})")
|
|
977
|
+
lines.append("")
|
|
978
|
+
lines.append("| Path | Variant | Insertion line |")
|
|
979
|
+
lines.append("|---|---|---:|")
|
|
980
|
+
for row in sorted(absent_rows, key=lambda r: r.path):
|
|
981
|
+
ip = row.injection_plan
|
|
982
|
+
insertion = (
|
|
983
|
+
str(ip.get("insertion-line"))
|
|
984
|
+
if isinstance(ip, dict) and ip.get("insertion-line") is not None
|
|
985
|
+
else "—"
|
|
986
|
+
)
|
|
987
|
+
lines.append(f"| `{row.path}` | `{row.variant_family}` | {insertion} |")
|
|
988
|
+
lines.append("")
|
|
989
|
+
|
|
990
|
+
# Sample injection-plan diffs (first three of each non-trivial class).
|
|
991
|
+
sample_rows = (malformed_rows + absent_rows)[:6]
|
|
992
|
+
if sample_rows:
|
|
993
|
+
lines.append("## Sample Injection-Plan Diffs")
|
|
994
|
+
lines.append("")
|
|
995
|
+
for row in sample_rows:
|
|
996
|
+
ip = row.injection_plan
|
|
997
|
+
if not isinstance(ip, dict):
|
|
998
|
+
continue
|
|
999
|
+
lines.append(
|
|
1000
|
+
f"### `{row.path}` — {row.header_status} / `{row.variant_family}`"
|
|
1001
|
+
)
|
|
1002
|
+
lines.append("")
|
|
1003
|
+
lines.append("```diff")
|
|
1004
|
+
diff_text = str(ip.get("diff", ""))
|
|
1005
|
+
for diff_line in diff_text.splitlines()[:30]:
|
|
1006
|
+
lines.append(diff_line)
|
|
1007
|
+
if len(diff_text.splitlines()) > 30:
|
|
1008
|
+
lines.append("[... truncated for readability ...]")
|
|
1009
|
+
lines.append("```")
|
|
1010
|
+
lines.append("")
|
|
1011
|
+
|
|
1012
|
+
# Exception-class breakdown so the operator can audit the fixture's
|
|
1013
|
+
# reach.
|
|
1014
|
+
not_applicable_rows = [r for r in rows if r.header_status == HEADER_NOT_APPLICABLE]
|
|
1015
|
+
if not_applicable_rows:
|
|
1016
|
+
lines.append(f"## Files Marked Not Applicable ({len(not_applicable_rows)})")
|
|
1017
|
+
lines.append("")
|
|
1018
|
+
from collections import Counter
|
|
1019
|
+
|
|
1020
|
+
class_counts = Counter(
|
|
1021
|
+
r.exception_class or "no-fixture-match" for r in not_applicable_rows
|
|
1022
|
+
)
|
|
1023
|
+
lines.append("| Exception class | Count |")
|
|
1024
|
+
lines.append("|---|---:|")
|
|
1025
|
+
for cls, count in class_counts.most_common():
|
|
1026
|
+
lines.append(f"| `{cls}` | {count} |")
|
|
1027
|
+
lines.append("")
|
|
1028
|
+
|
|
1029
|
+
return "\n".join(lines) + "\n"
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
# ---------------------------------------------------------------------------
|
|
1033
|
+
# CLI.
|
|
1034
|
+
# ---------------------------------------------------------------------------
|
|
1035
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
1036
|
+
"""Parse the CLI argument vector."""
|
|
1037
|
+
parser = argparse.ArgumentParser(
|
|
1038
|
+
description=(
|
|
1039
|
+
"Detailed authorship-header coverage scan against the inventory "
|
|
1040
|
+
"snapshot. Emits header-coverage.json and header-coverage.md."
|
|
1041
|
+
),
|
|
1042
|
+
)
|
|
1043
|
+
parser.add_argument(
|
|
1044
|
+
"--root",
|
|
1045
|
+
type=Path,
|
|
1046
|
+
default=Path.cwd(),
|
|
1047
|
+
help="Working-tree root (defaults to the current directory).",
|
|
1048
|
+
)
|
|
1049
|
+
parser.add_argument(
|
|
1050
|
+
"--inventory",
|
|
1051
|
+
type=Path,
|
|
1052
|
+
default=Path(".audit/inventory.json"),
|
|
1053
|
+
help="Path to inventory.json (relative to --root or absolute).",
|
|
1054
|
+
)
|
|
1055
|
+
parser.add_argument(
|
|
1056
|
+
"--exception-fixture",
|
|
1057
|
+
type=Path,
|
|
1058
|
+
default=Path("src/apothem/schemas/header-exceptions.txt"),
|
|
1059
|
+
help="Path to the exception-list glob fixture.",
|
|
1060
|
+
)
|
|
1061
|
+
parser.add_argument(
|
|
1062
|
+
"--out-json",
|
|
1063
|
+
type=Path,
|
|
1064
|
+
default=Path(".audit/header-coverage.json"),
|
|
1065
|
+
help="Output path for the machine-readable coverage record.",
|
|
1066
|
+
)
|
|
1067
|
+
parser.add_argument(
|
|
1068
|
+
"--out-md",
|
|
1069
|
+
type=Path,
|
|
1070
|
+
default=Path(".audit/header-coverage.md"),
|
|
1071
|
+
help="Output path for the human-readable coverage mirror.",
|
|
1072
|
+
)
|
|
1073
|
+
return parser.parse_args(argv)
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
def resolve_path(root: Path, candidate: Path) -> Path:
|
|
1077
|
+
"""Resolve ``candidate`` against ``root`` when it is relative."""
|
|
1078
|
+
return candidate if candidate.is_absolute() else root / candidate
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
def main(argv: list[str] | None = None) -> int:
|
|
1082
|
+
"""CLI entry point — orchestrate the scan and emit outputs."""
|
|
1083
|
+
args = parse_args(argv)
|
|
1084
|
+
root = args.root.resolve()
|
|
1085
|
+
inventory_path = resolve_path(root, args.inventory)
|
|
1086
|
+
fixture_path = resolve_path(root, args.exception_fixture)
|
|
1087
|
+
out_json = resolve_path(root, args.out_json)
|
|
1088
|
+
out_md = resolve_path(root, args.out_md)
|
|
1089
|
+
|
|
1090
|
+
if not inventory_path.is_file():
|
|
1091
|
+
print(
|
|
1092
|
+
f"error: inventory not found at {inventory_path}",
|
|
1093
|
+
file=sys.stderr,
|
|
1094
|
+
)
|
|
1095
|
+
return EXIT_ERROR
|
|
1096
|
+
|
|
1097
|
+
rows, summary, provenance = scan_inventory(inventory_path, fixture_path, root)
|
|
1098
|
+
|
|
1099
|
+
generated_at = datetime.now(timezone.utc).isoformat()
|
|
1100
|
+
payload: dict[str, object] = {
|
|
1101
|
+
"scanner": "scan_header_coverage",
|
|
1102
|
+
"scanner-version": "1.0.0",
|
|
1103
|
+
"generated-at": generated_at,
|
|
1104
|
+
"root": str(root),
|
|
1105
|
+
**provenance,
|
|
1106
|
+
"summary": summary.to_json(),
|
|
1107
|
+
"files": [row.to_json() for row in rows],
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
out_json.parent.mkdir(parents=True, exist_ok=True)
|
|
1111
|
+
out_json.write_text(
|
|
1112
|
+
json.dumps(payload, indent=2, sort_keys=False) + "\n",
|
|
1113
|
+
encoding="utf-8",
|
|
1114
|
+
)
|
|
1115
|
+
|
|
1116
|
+
out_md.parent.mkdir(parents=True, exist_ok=True)
|
|
1117
|
+
out_md.write_text(
|
|
1118
|
+
render_markdown(rows, summary, provenance, generated_at),
|
|
1119
|
+
encoding="utf-8",
|
|
1120
|
+
)
|
|
1121
|
+
|
|
1122
|
+
print(
|
|
1123
|
+
f"scan_header_coverage: {summary.applicable_total} applicable, "
|
|
1124
|
+
f"{summary.present_canonical} canonical "
|
|
1125
|
+
f"({summary.coverage_pct:.2f}%), "
|
|
1126
|
+
f"{summary.present_malformed} malformed, "
|
|
1127
|
+
f"{summary.absent} absent, "
|
|
1128
|
+
f"{summary.not_applicable} not-applicable",
|
|
1129
|
+
)
|
|
1130
|
+
return EXIT_OK
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
if __name__ == "__main__":
|
|
1134
|
+
raise SystemExit(main())
|