@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,238 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
"""Verify the repo-root ``.gitattributes`` carries the canonical contract.
|
|
4
|
+
|
|
5
|
+
Why this validator exists. The supply-chain release contract requires
|
|
6
|
+
the repository to ship a ``.gitattributes`` file at its root that pins
|
|
7
|
+
line-ending normalisation (``* text=auto eol=lf``), declares binary
|
|
8
|
+
asset classes (``*.png binary``, ``*.jpg`` or ``*.jpeg`` ``binary``),
|
|
9
|
+
preserves SVG as text for diffability (``*.svg text``), and applies
|
|
10
|
+
``export-ignore`` to ephemeral / developer-only trees (``.plans/``,
|
|
11
|
+
``tests/``, ``.github/``) so source-archive consumers receive only
|
|
12
|
+
shippable content. Absence of any one of these contract elements is a
|
|
13
|
+
supply-chain drift class that this validator surfaces at the
|
|
14
|
+
pre-emission gate.
|
|
15
|
+
|
|
16
|
+
Detection strategy. Open ``<root>/.gitattributes``; tokenise on lines
|
|
17
|
+
(comments and blanks ignored); look for each required pattern via a
|
|
18
|
+
permissive whitespace-tolerant regex. Each missing element produces a
|
|
19
|
+
finding whose ``drift_class`` field names the violation. The validator
|
|
20
|
+
does NOT attempt to enforce ordering, repetition, or stylistic
|
|
21
|
+
consistency — only presence.
|
|
22
|
+
|
|
23
|
+
Exit semantics. Exits 0 (PASS) when every required contract element is
|
|
24
|
+
present; exits 2 (FAIL) on any drift class. The exit-2 convention
|
|
25
|
+
matches the conformity-gate orchestrator's ``EXIT_FAIL`` constant.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import re
|
|
32
|
+
import sys
|
|
33
|
+
from dataclasses import asdict, dataclass, field
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Final
|
|
36
|
+
|
|
37
|
+
GREP_NAME: Final[str] = "gitattributes-presence-grep"
|
|
38
|
+
RULE_ANCHOR: Final[str] = "supply-chain .gitattributes contract"
|
|
39
|
+
|
|
40
|
+
EXIT_PASS: Final[int] = 0
|
|
41
|
+
EXIT_FAIL: Final[int] = 2
|
|
42
|
+
|
|
43
|
+
GITATTRIBUTES_FILENAME: Final[str] = ".gitattributes"
|
|
44
|
+
|
|
45
|
+
# Drift class identifiers — surface in findings so the operator can
|
|
46
|
+
# route each failure to its remediation path without re-parsing prose.
|
|
47
|
+
DRIFT_FILE_ABSENT: Final[str] = "file-absent"
|
|
48
|
+
DRIFT_MISSING_TEXT_EOL_BASELINE: Final[str] = "missing-text-eol-baseline"
|
|
49
|
+
DRIFT_MISSING_BINARY_DECLARATION: Final[str] = "missing-binary-declaration"
|
|
50
|
+
DRIFT_MISSING_EXPORT_IGNORE: Final[str] = "missing-export-ignore"
|
|
51
|
+
|
|
52
|
+
# Required-element regexes. Each pattern is whitespace-tolerant so the
|
|
53
|
+
# operator may freely align columns; none require a specific order.
|
|
54
|
+
# ``re.MULTILINE`` lets ``^`` match at line starts so commented-out
|
|
55
|
+
# variants (lines beginning with ``#``) are not matched.
|
|
56
|
+
_TEXT_EOL_RE: Final[re.Pattern[str]] = re.compile(
|
|
57
|
+
r"^\s*\*\s+text=auto\s+eol=lf\b",
|
|
58
|
+
re.MULTILINE,
|
|
59
|
+
)
|
|
60
|
+
_PNG_BINARY_RE: Final[re.Pattern[str]] = re.compile(
|
|
61
|
+
r"^\s*\*\.png\s+binary\b",
|
|
62
|
+
re.MULTILINE,
|
|
63
|
+
)
|
|
64
|
+
# Accept either ``*.jpg`` or ``*.jpeg`` as the JPEG declaration.
|
|
65
|
+
_JPG_BINARY_RE: Final[re.Pattern[str]] = re.compile(
|
|
66
|
+
r"^\s*\*\.jpe?g\s+binary\b",
|
|
67
|
+
re.MULTILINE,
|
|
68
|
+
)
|
|
69
|
+
_SVG_TEXT_RE: Final[re.Pattern[str]] = re.compile(
|
|
70
|
+
r"^\s*\*\.svg\s+text\b",
|
|
71
|
+
re.MULTILINE,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Export-ignore patterns. Accept both trailing-slash (``.plans/``) and
|
|
75
|
+
# bare-name (``.plans``) glob forms — git honours both.
|
|
76
|
+
_EXPORT_IGNORE_PATHS: Final[tuple[str, ...]] = (".plans", "tests", ".github")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _export_ignore_re(path: str) -> re.Pattern[str]:
|
|
80
|
+
"""Compile a permissive export-ignore matcher for *path*.
|
|
81
|
+
|
|
82
|
+
Accepts ``.plans/ export-ignore`` and ``.plans export-ignore`` forms,
|
|
83
|
+
plus glob variants like ``.plans/** export-ignore``. The leading
|
|
84
|
+
anchor is ``^`` under MULTILINE so commented lines do not match.
|
|
85
|
+
"""
|
|
86
|
+
escaped = re.escape(path)
|
|
87
|
+
return re.compile(
|
|
88
|
+
rf"^\s*{escaped}(?:/\S*|/?)\s+export-ignore\b",
|
|
89
|
+
re.MULTILINE,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class Finding:
|
|
95
|
+
"""One missing-contract-element occurrence."""
|
|
96
|
+
|
|
97
|
+
drift_class: str
|
|
98
|
+
detail: str
|
|
99
|
+
rule: str = RULE_ANCHOR
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class GrepResult:
|
|
104
|
+
grep: str
|
|
105
|
+
root: str
|
|
106
|
+
path: str | None
|
|
107
|
+
passed: bool
|
|
108
|
+
findings: list[Finding] = field(default_factory=list)
|
|
109
|
+
|
|
110
|
+
def to_json(self) -> str:
|
|
111
|
+
payload = {
|
|
112
|
+
"grep": self.grep,
|
|
113
|
+
"root": self.root,
|
|
114
|
+
"path": self.path,
|
|
115
|
+
"passed": self.passed,
|
|
116
|
+
"findings": [asdict(f) for f in self.findings],
|
|
117
|
+
}
|
|
118
|
+
return json.dumps(payload, indent=2)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def check(root: Path) -> GrepResult:
|
|
122
|
+
"""Inspect ``<root>/.gitattributes`` and return a structured verdict.
|
|
123
|
+
|
|
124
|
+
Pre-conditions: *root* names a directory the operator wishes to
|
|
125
|
+
audit (typically the repository root).
|
|
126
|
+
Post-conditions: ``result.passed`` is True iff the file exists and
|
|
127
|
+
contains every required contract element; otherwise ``findings``
|
|
128
|
+
enumerates each drift class.
|
|
129
|
+
"""
|
|
130
|
+
path = root / GITATTRIBUTES_FILENAME
|
|
131
|
+
if not path.is_file():
|
|
132
|
+
finding = Finding(
|
|
133
|
+
drift_class=DRIFT_FILE_ABSENT,
|
|
134
|
+
detail=(
|
|
135
|
+
f"{GITATTRIBUTES_FILENAME} is absent at repository root "
|
|
136
|
+
f"({path}); the supply-chain contract requires it"
|
|
137
|
+
),
|
|
138
|
+
)
|
|
139
|
+
return GrepResult(
|
|
140
|
+
grep=GREP_NAME,
|
|
141
|
+
root=str(root),
|
|
142
|
+
path=str(path),
|
|
143
|
+
passed=False,
|
|
144
|
+
findings=[finding],
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
content = path.read_text(encoding="utf-8")
|
|
149
|
+
except OSError as exc:
|
|
150
|
+
finding = Finding(
|
|
151
|
+
drift_class=DRIFT_FILE_ABSENT,
|
|
152
|
+
detail=f"{GITATTRIBUTES_FILENAME} unreadable at {path}: {exc}",
|
|
153
|
+
)
|
|
154
|
+
return GrepResult(
|
|
155
|
+
grep=GREP_NAME,
|
|
156
|
+
root=str(root),
|
|
157
|
+
path=str(path),
|
|
158
|
+
passed=False,
|
|
159
|
+
findings=[finding],
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
findings: list[Finding] = []
|
|
163
|
+
|
|
164
|
+
if not _TEXT_EOL_RE.search(content):
|
|
165
|
+
findings.append(
|
|
166
|
+
Finding(
|
|
167
|
+
drift_class=DRIFT_MISSING_TEXT_EOL_BASELINE,
|
|
168
|
+
detail=(
|
|
169
|
+
"baseline `* text=auto eol=lf` declaration missing; "
|
|
170
|
+
"line-ending normalisation is unpinned"
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if not _PNG_BINARY_RE.search(content):
|
|
176
|
+
findings.append(
|
|
177
|
+
Finding(
|
|
178
|
+
drift_class=DRIFT_MISSING_BINARY_DECLARATION,
|
|
179
|
+
detail="`*.png binary` declaration missing",
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if not _JPG_BINARY_RE.search(content):
|
|
184
|
+
findings.append(
|
|
185
|
+
Finding(
|
|
186
|
+
drift_class=DRIFT_MISSING_BINARY_DECLARATION,
|
|
187
|
+
detail="`*.jpg binary` (or `*.jpeg binary`) declaration missing",
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if not _SVG_TEXT_RE.search(content):
|
|
192
|
+
findings.append(
|
|
193
|
+
Finding(
|
|
194
|
+
drift_class=DRIFT_MISSING_BINARY_DECLARATION,
|
|
195
|
+
detail=(
|
|
196
|
+
"`*.svg text` declaration missing; SVG must be kept "
|
|
197
|
+
"diffable as text rather than treated as binary"
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
for ignore_path in _EXPORT_IGNORE_PATHS:
|
|
203
|
+
if not _export_ignore_re(ignore_path).search(content):
|
|
204
|
+
findings.append(
|
|
205
|
+
Finding(
|
|
206
|
+
drift_class=DRIFT_MISSING_EXPORT_IGNORE,
|
|
207
|
+
detail=(
|
|
208
|
+
f"`{ignore_path}/ export-ignore` declaration "
|
|
209
|
+
"missing; ephemeral / developer-only tree would "
|
|
210
|
+
"leak into source archives"
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return GrepResult(
|
|
216
|
+
grep=GREP_NAME,
|
|
217
|
+
root=str(root),
|
|
218
|
+
path=str(path),
|
|
219
|
+
passed=not findings,
|
|
220
|
+
findings=findings,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _read_input(argv: list[str]) -> Path:
|
|
225
|
+
if len(argv) >= 2:
|
|
226
|
+
return Path(argv[1])
|
|
227
|
+
return Path.cwd()
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _main(argv: list[str]) -> int:
|
|
231
|
+
root = _read_input(argv)
|
|
232
|
+
result = check(root)
|
|
233
|
+
print(result.to_json())
|
|
234
|
+
return EXIT_PASS if result.passed else EXIT_FAIL
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
if __name__ == "__main__":
|
|
238
|
+
sys.exit(_main(sys.argv))
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
"""Verify every workflow job opens with a conformant harden-runner step.
|
|
4
|
+
|
|
5
|
+
Why this enforcement exists. The workflow hardening contract requires
|
|
6
|
+
every GitHub Actions workflow at ``.github/workflows/*.yml`` to invoke
|
|
7
|
+
``step-security/harden-runner@v2`` as the FIRST step of every job and
|
|
8
|
+
configure ``egress-policy: block`` (steady-state) or ``egress-policy:
|
|
9
|
+
audit`` (initial deployment phase). The action installs network-egress
|
|
10
|
+
controls before any user-supplied step runs; placing it second leaves a
|
|
11
|
+
window where checkout / dependency-fetch / arbitrary action steps can
|
|
12
|
+
exfiltrate before the egress policy engages.
|
|
13
|
+
|
|
14
|
+
Detection strategy. The validator walks ``<root>/.github/workflows/`` for
|
|
15
|
+
``*.yml`` and ``*.yaml`` files, parses each via ``yaml.safe_load``, and
|
|
16
|
+
for every job in ``jobs:`` inspects the ``steps[]`` sequence. Three drift
|
|
17
|
+
classes surface:
|
|
18
|
+
|
|
19
|
+
- ``harden-runner-action-absent`` — no step in the job uses
|
|
20
|
+
``step-security/harden-runner``.
|
|
21
|
+
- ``harden-runner-not-first-step`` — the action is present but not
|
|
22
|
+
``steps[0]``.
|
|
23
|
+
- ``egress-policy-too-permissive`` — the step's ``with:`` block omits
|
|
24
|
+
``egress-policy`` or sets it to a value other than ``block`` / ``audit``
|
|
25
|
+
(``log`` and any other free-form value are rejected; the canonical
|
|
26
|
+
taxonomy is closed).
|
|
27
|
+
|
|
28
|
+
Workflows-absent tolerance. When ``<root>/.github/workflows/`` does not
|
|
29
|
+
exist (a repository in its earliest scaffold state), the validator exits
|
|
30
|
+
0 with ``not-yet-materialised: true`` so the gate does not block bootstrap
|
|
31
|
+
work that hasn't yet authored CI.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import json
|
|
37
|
+
import re
|
|
38
|
+
import sys
|
|
39
|
+
from dataclasses import asdict, dataclass, field
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from typing import Final
|
|
42
|
+
|
|
43
|
+
import yaml
|
|
44
|
+
|
|
45
|
+
# A bare ``${{ inputs.<name> }}`` expression — the reusable-workflow shape
|
|
46
|
+
# where egress-policy is parameterized by a workflow_call input.
|
|
47
|
+
_INPUTS_EXPR: Final[re.Pattern[str]] = re.compile(
|
|
48
|
+
r"^\$\{\{\s*inputs\.([A-Za-z_][\w-]*)\s*\}\}$"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
GREP_NAME: Final[str] = "harden-runner-grep"
|
|
52
|
+
RULE_ANCHOR: Final[str] = "harden-runner egress baseline"
|
|
53
|
+
|
|
54
|
+
EXIT_PASS: Final[int] = 0
|
|
55
|
+
EXIT_FAIL: Final[int] = 2
|
|
56
|
+
|
|
57
|
+
WORKFLOWS_SUBPATH: Final[str] = ".github/workflows"
|
|
58
|
+
ACTION_NAME: Final[str] = "step-security/harden-runner"
|
|
59
|
+
CONFORMANT_EGRESS_POLICIES: Final[frozenset[str]] = frozenset({"block", "audit"})
|
|
60
|
+
|
|
61
|
+
DRIFT_ABSENT: Final[str] = "harden-runner-action-absent"
|
|
62
|
+
DRIFT_NOT_FIRST: Final[str] = "harden-runner-not-first-step"
|
|
63
|
+
DRIFT_PERMISSIVE: Final[str] = "egress-policy-too-permissive"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class Finding:
|
|
68
|
+
"""One job's harden-runner drift."""
|
|
69
|
+
|
|
70
|
+
workflow: str
|
|
71
|
+
job: str
|
|
72
|
+
drift: str
|
|
73
|
+
detail: str
|
|
74
|
+
rule: str = RULE_ANCHOR
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class GrepResult:
|
|
79
|
+
grep: str
|
|
80
|
+
root: str
|
|
81
|
+
passed: bool
|
|
82
|
+
not_yet_materialised: bool = False
|
|
83
|
+
findings: list[Finding] = field(default_factory=list)
|
|
84
|
+
|
|
85
|
+
def to_json(self) -> str:
|
|
86
|
+
payload = {
|
|
87
|
+
"grep": self.grep,
|
|
88
|
+
"root": self.root,
|
|
89
|
+
"passed": self.passed,
|
|
90
|
+
"not-yet-materialised": self.not_yet_materialised,
|
|
91
|
+
"findings": [asdict(f) for f in self.findings],
|
|
92
|
+
}
|
|
93
|
+
return json.dumps(payload, indent=2)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _uses_harden_runner(uses_value: object) -> bool:
|
|
97
|
+
"""Return True iff a ``uses:`` value references step-security/harden-runner."""
|
|
98
|
+
if not isinstance(uses_value, str):
|
|
99
|
+
return False
|
|
100
|
+
# Strip any ``@<ref>`` suffix so ``@v2``, ``@v2.10``, and a 40-char SHA
|
|
101
|
+
# pin all match identically.
|
|
102
|
+
name = uses_value.split("@", 1)[0].strip()
|
|
103
|
+
return name == ACTION_NAME
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _workflow_call_inputs(document: dict[object, object]) -> dict[object, object]:
|
|
107
|
+
"""Return the ``on.workflow_call.inputs`` mapping, or empty when absent.
|
|
108
|
+
|
|
109
|
+
GitHub Actions ``on:`` is coerced by PyYAML (YAML 1.1) to the boolean
|
|
110
|
+
key ``True``; handle both the string ``"on"`` and boolean ``True`` keys.
|
|
111
|
+
"""
|
|
112
|
+
on_block = document.get("on", document.get(True))
|
|
113
|
+
if not isinstance(on_block, dict):
|
|
114
|
+
return {}
|
|
115
|
+
workflow_call = on_block.get("workflow_call")
|
|
116
|
+
if not isinstance(workflow_call, dict):
|
|
117
|
+
return {}
|
|
118
|
+
inputs = workflow_call.get("inputs")
|
|
119
|
+
return inputs if isinstance(inputs, dict) else {}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _resolve_egress(egress: object, inputs: dict[object, object]) -> object:
|
|
123
|
+
"""Resolve a ``${{ inputs.<name> }}`` egress-policy to the input default.
|
|
124
|
+
|
|
125
|
+
A reusable workflow may parameterize egress-policy via a ``workflow_call``
|
|
126
|
+
input; the conformant value is then the input's declared ``default``. A
|
|
127
|
+
literal egress value (not an inputs expression) is returned unchanged.
|
|
128
|
+
"""
|
|
129
|
+
if not isinstance(egress, str):
|
|
130
|
+
return egress
|
|
131
|
+
match = _INPUTS_EXPR.match(egress.strip())
|
|
132
|
+
if match is None:
|
|
133
|
+
return egress
|
|
134
|
+
spec = inputs.get(match.group(1))
|
|
135
|
+
if isinstance(spec, dict) and "default" in spec:
|
|
136
|
+
return spec["default"]
|
|
137
|
+
return egress
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _inspect_job(
|
|
141
|
+
workflow: str,
|
|
142
|
+
job_name: str,
|
|
143
|
+
job: object,
|
|
144
|
+
workflow_inputs: dict[object, object] | None = None,
|
|
145
|
+
) -> list[Finding]:
|
|
146
|
+
"""Return drift findings for one job; empty list when conformant."""
|
|
147
|
+
if not isinstance(job, dict):
|
|
148
|
+
return []
|
|
149
|
+
# Reusable-workflow callers have no inline steps; harden-runner is
|
|
150
|
+
# step-level and cannot apply. A job declaring a top-level ``uses:``
|
|
151
|
+
# field and lacking a ``steps:`` field is structurally exempt.
|
|
152
|
+
if "uses" in job and "steps" not in job:
|
|
153
|
+
return []
|
|
154
|
+
steps = job.get("steps")
|
|
155
|
+
if not isinstance(steps, list) or not steps:
|
|
156
|
+
return [
|
|
157
|
+
Finding(
|
|
158
|
+
workflow=workflow,
|
|
159
|
+
job=job_name,
|
|
160
|
+
drift=DRIFT_ABSENT,
|
|
161
|
+
detail="job has no steps[] sequence",
|
|
162
|
+
)
|
|
163
|
+
]
|
|
164
|
+
# Locate harden-runner step(s).
|
|
165
|
+
hr_indices = [
|
|
166
|
+
i
|
|
167
|
+
for i, step in enumerate(steps)
|
|
168
|
+
if isinstance(step, dict) and _uses_harden_runner(step.get("uses"))
|
|
169
|
+
]
|
|
170
|
+
if not hr_indices:
|
|
171
|
+
return [
|
|
172
|
+
Finding(
|
|
173
|
+
workflow=workflow,
|
|
174
|
+
job=job_name,
|
|
175
|
+
drift=DRIFT_ABSENT,
|
|
176
|
+
detail=f"no step uses {ACTION_NAME}",
|
|
177
|
+
)
|
|
178
|
+
]
|
|
179
|
+
first_idx = hr_indices[0]
|
|
180
|
+
findings: list[Finding] = []
|
|
181
|
+
if first_idx != 0:
|
|
182
|
+
first_step = steps[0]
|
|
183
|
+
first_uses = first_step.get("uses") if isinstance(first_step, dict) else None
|
|
184
|
+
findings.append(
|
|
185
|
+
Finding(
|
|
186
|
+
workflow=workflow,
|
|
187
|
+
job=job_name,
|
|
188
|
+
drift=DRIFT_NOT_FIRST,
|
|
189
|
+
detail=(
|
|
190
|
+
f"{ACTION_NAME} appears at steps[{first_idx}]; "
|
|
191
|
+
f"steps[0] uses={first_uses!r}"
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
# Inspect the egress-policy on the first harden-runner step.
|
|
196
|
+
hr_step = steps[first_idx]
|
|
197
|
+
with_block = hr_step.get("with") if isinstance(hr_step, dict) else None
|
|
198
|
+
egress = with_block.get("egress-policy") if isinstance(with_block, dict) else None
|
|
199
|
+
# A reusable workflow may parameterize egress-policy via a workflow_call
|
|
200
|
+
# input (``${{ inputs.<name> }}``); resolve it to the input's declared
|
|
201
|
+
# default so a conformant default is not flagged as too-permissive.
|
|
202
|
+
egress = _resolve_egress(egress, workflow_inputs or {})
|
|
203
|
+
if egress is None:
|
|
204
|
+
findings.append(
|
|
205
|
+
Finding(
|
|
206
|
+
workflow=workflow,
|
|
207
|
+
job=job_name,
|
|
208
|
+
drift=DRIFT_PERMISSIVE,
|
|
209
|
+
detail="egress-policy key absent from with: block",
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
elif not isinstance(egress, str) or egress not in CONFORMANT_EGRESS_POLICIES:
|
|
213
|
+
findings.append(
|
|
214
|
+
Finding(
|
|
215
|
+
workflow=workflow,
|
|
216
|
+
job=job_name,
|
|
217
|
+
drift=DRIFT_PERMISSIVE,
|
|
218
|
+
detail=(
|
|
219
|
+
f"egress-policy={egress!r}; expected one of "
|
|
220
|
+
f"{sorted(CONFORMANT_EGRESS_POLICIES)}"
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
return findings
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _inspect_workflow(path: Path, root: Path) -> list[Finding]:
|
|
228
|
+
"""Parse a workflow file; return drift findings across all its jobs."""
|
|
229
|
+
rel = str(path.relative_to(root)).replace("\\", "/")
|
|
230
|
+
try:
|
|
231
|
+
text = path.read_text(encoding="utf-8")
|
|
232
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
233
|
+
return [
|
|
234
|
+
Finding(
|
|
235
|
+
workflow=rel,
|
|
236
|
+
job="<workflow>",
|
|
237
|
+
drift=DRIFT_ABSENT,
|
|
238
|
+
detail=f"unreadable workflow: {exc}",
|
|
239
|
+
)
|
|
240
|
+
]
|
|
241
|
+
try:
|
|
242
|
+
document = yaml.safe_load(text)
|
|
243
|
+
except yaml.YAMLError as exc:
|
|
244
|
+
return [
|
|
245
|
+
Finding(
|
|
246
|
+
workflow=rel,
|
|
247
|
+
job="<workflow>",
|
|
248
|
+
drift=DRIFT_ABSENT,
|
|
249
|
+
detail=f"YAML parse error: {exc}",
|
|
250
|
+
)
|
|
251
|
+
]
|
|
252
|
+
if not isinstance(document, dict):
|
|
253
|
+
return [
|
|
254
|
+
Finding(
|
|
255
|
+
workflow=rel,
|
|
256
|
+
job="<workflow>",
|
|
257
|
+
drift=DRIFT_ABSENT,
|
|
258
|
+
detail="workflow root is not a mapping",
|
|
259
|
+
)
|
|
260
|
+
]
|
|
261
|
+
jobs = document.get("jobs")
|
|
262
|
+
if not isinstance(jobs, dict) or not jobs:
|
|
263
|
+
# A workflow with no jobs has nothing to harden; skip silently.
|
|
264
|
+
return []
|
|
265
|
+
workflow_inputs = _workflow_call_inputs(document)
|
|
266
|
+
findings: list[Finding] = []
|
|
267
|
+
for job_name, job in jobs.items():
|
|
268
|
+
findings.extend(_inspect_workflow_job(rel, job_name, job, workflow_inputs))
|
|
269
|
+
return findings
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _inspect_workflow_job(
|
|
273
|
+
workflow: str,
|
|
274
|
+
job_name: object,
|
|
275
|
+
job: object,
|
|
276
|
+
workflow_inputs: dict[object, object] | None = None,
|
|
277
|
+
) -> list[Finding]:
|
|
278
|
+
name = str(job_name) if not isinstance(job_name, str) else job_name
|
|
279
|
+
return _inspect_job(workflow, name, job, workflow_inputs)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def check_root(root: Path) -> GrepResult:
|
|
283
|
+
"""Walk ``<root>/.github/workflows/`` and audit every workflow.
|
|
284
|
+
|
|
285
|
+
Pre-conditions: ``root`` is the repository root.
|
|
286
|
+
Post-conditions: ``result.passed`` is True iff every job in every
|
|
287
|
+
discovered workflow opens with ``step-security/harden-runner@v2`` (or
|
|
288
|
+
a pinned variant) and declares ``egress-policy: block`` / ``audit``.
|
|
289
|
+
When the workflows directory is absent, returns passed=True with
|
|
290
|
+
``not_yet_materialised=True``.
|
|
291
|
+
"""
|
|
292
|
+
workflows_dir = root / WORKFLOWS_SUBPATH
|
|
293
|
+
if not workflows_dir.is_dir():
|
|
294
|
+
return GrepResult(
|
|
295
|
+
grep=GREP_NAME,
|
|
296
|
+
root=str(root),
|
|
297
|
+
passed=True,
|
|
298
|
+
not_yet_materialised=True,
|
|
299
|
+
)
|
|
300
|
+
findings: list[Finding] = []
|
|
301
|
+
candidates = sorted([*workflows_dir.glob("*.yml"), *workflows_dir.glob("*.yaml")])
|
|
302
|
+
for workflow_path in candidates:
|
|
303
|
+
findings.extend(_inspect_workflow(workflow_path, root))
|
|
304
|
+
return GrepResult(
|
|
305
|
+
grep=GREP_NAME,
|
|
306
|
+
root=str(root),
|
|
307
|
+
passed=not findings,
|
|
308
|
+
findings=findings,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _main(argv: list[str]) -> int:
|
|
313
|
+
root = Path(argv[1]) if len(argv) >= 2 else Path.cwd()
|
|
314
|
+
result = check_root(root)
|
|
315
|
+
print(result.to_json())
|
|
316
|
+
return EXIT_PASS if result.passed else EXIT_FAIL
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
if __name__ == "__main__":
|
|
320
|
+
sys.exit(_main(sys.argv))
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
"""Flag hedging vocabulary in prescriptive contexts.
|
|
4
|
+
|
|
5
|
+
Why this enforcement exists. The definitiveness rule M8 requires every
|
|
6
|
+
prescription to be unconditional or to carry the conditions that branch
|
|
7
|
+
its outcomes — silent hedges ("usually", "typically", "should probably")
|
|
8
|
+
soften prescriptions before they land, leaving the reader to guess at the
|
|
9
|
+
binding form. The mechanical row in the pre-emission gate scans for the
|
|
10
|
+
closed vocabulary and surfaces every occurrence. The operator triages
|
|
11
|
+
each hit on one of the three paths declared in the definitiveness rule:
|
|
12
|
+
promote to unconditional form with conditions named, demote to an
|
|
13
|
+
explicit conditional with branches enumerated, or remove the prescription
|
|
14
|
+
entirely (option set is underdetermined; route to inquiry).
|
|
15
|
+
|
|
16
|
+
Scope. Hits inside fenced code blocks (between triple-backtick fences) and
|
|
17
|
+
inside inline-code spans (single-backtick delimited) are excluded. Fenced
|
|
18
|
+
blocks typically quote external material, sample inputs, or commands;
|
|
19
|
+
inline-code spans typically cite a forbidden token meta-linguistically (a
|
|
20
|
+
doc that names the hedging vocabulary it forbids) rather than hedge a
|
|
21
|
+
prescription. Hits in prose are reported with line numbers; the operator
|
|
22
|
+
decides whether each hit is a genuine hedge or a quotation.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import re
|
|
28
|
+
import sys
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Final
|
|
32
|
+
|
|
33
|
+
from apothem.conformity._grep_base import GrepResult, run_grep
|
|
34
|
+
|
|
35
|
+
# The closed hedging vocabulary per the definitiveness rule M8 §Hedging
|
|
36
|
+
# vocabulary — eliminate or qualify. Word-boundary regex prevents partial
|
|
37
|
+
# matches inside larger words ("usually" matches; "unusually" does not).
|
|
38
|
+
HEDGING_VOCABULARY: Final[tuple[str, ...]] = (
|
|
39
|
+
"maybe",
|
|
40
|
+
"might",
|
|
41
|
+
"could",
|
|
42
|
+
"should probably",
|
|
43
|
+
"usually",
|
|
44
|
+
"generally",
|
|
45
|
+
"typically",
|
|
46
|
+
"mostly",
|
|
47
|
+
"often",
|
|
48
|
+
"perhaps",
|
|
49
|
+
"possibly",
|
|
50
|
+
"somewhat",
|
|
51
|
+
"fairly",
|
|
52
|
+
"roughly",
|
|
53
|
+
"broadly",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Compile a single combined regex to scan once per line. The `(?i)` makes the
|
|
57
|
+
# match case-insensitive — `Usually` at sentence start hedges identically.
|
|
58
|
+
HEDGING_RE: Final[re.Pattern[str]] = re.compile(
|
|
59
|
+
r"(?i)\b(?:" + "|".join(re.escape(w) for w in HEDGING_VOCABULARY) + r")\b"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Fenced code block delimiter. The standard Markdown form opens and closes
|
|
63
|
+
# with a triple backtick at column 0 (or with whitespace prefix in some
|
|
64
|
+
# dialects); we use the column-0 form per the host's discovered Markdown
|
|
65
|
+
# convention.
|
|
66
|
+
CODE_FENCE_RE: Final[re.Pattern[str]] = re.compile(r"^```")
|
|
67
|
+
|
|
68
|
+
# Inline-code span: a single-backtick-delimited run on one line. A hedge word
|
|
69
|
+
# quoted inside backticks is a meta-linguistic citation (a doc naming the
|
|
70
|
+
# vocabulary it forbids), not a prescription that hedges, so these spans are
|
|
71
|
+
# blanked before the prose scan. The column-preserving blank mirrors the
|
|
72
|
+
# inline-code exclusion in binding_reciprocity_grep.
|
|
73
|
+
INLINE_CODE_RE: Final[re.Pattern[str]] = re.compile(r"`[^`]*`")
|
|
74
|
+
|
|
75
|
+
GREP_NAME: Final[str] = "hedging-grep"
|
|
76
|
+
RULE_ANCHOR: Final[str] = "M8 definitiveness §Hedging"
|
|
77
|
+
EXIT_PASS: Final[int] = 0
|
|
78
|
+
EXIT_FAIL: Final[int] = 2
|
|
79
|
+
STDIN_FLAG: Final[str] = "--stdin"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class Finding:
|
|
84
|
+
"""One hedge occurrence outside a fenced code block."""
|
|
85
|
+
|
|
86
|
+
line: int
|
|
87
|
+
match: str
|
|
88
|
+
context: str
|
|
89
|
+
rule: str = RULE_ANCHOR
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def check(content: str, path: Path | None = None) -> GrepResult:
|
|
93
|
+
"""Scan content; return a structured result.
|
|
94
|
+
|
|
95
|
+
Pre-conditions: `content` is the artifact body about to be emitted.
|
|
96
|
+
Post-conditions: `result.passed` is True iff zero hedges were found
|
|
97
|
+
outside fenced code blocks and inline-code spans.
|
|
98
|
+
"""
|
|
99
|
+
findings: list[Finding] = []
|
|
100
|
+
inside_fence = False
|
|
101
|
+
for line_index, line in enumerate(content.splitlines(), start=1):
|
|
102
|
+
if CODE_FENCE_RE.match(line):
|
|
103
|
+
# Toggle fence state — opening and closing fences both match.
|
|
104
|
+
inside_fence = not inside_fence
|
|
105
|
+
continue
|
|
106
|
+
if inside_fence:
|
|
107
|
+
continue
|
|
108
|
+
# Blank inline-code spans (same-length spaces preserve columns) so a
|
|
109
|
+
# hedge word cited inside backticks — a doc that forbids the `usually`
|
|
110
|
+
# vocabulary — is read as a citation, not a prescription that hedges.
|
|
111
|
+
scanned = INLINE_CODE_RE.sub(lambda m: " " * len(m.group(0)), line)
|
|
112
|
+
for match in HEDGING_RE.finditer(scanned):
|
|
113
|
+
findings.append(
|
|
114
|
+
Finding(
|
|
115
|
+
line=line_index,
|
|
116
|
+
match=match.group(),
|
|
117
|
+
context=line.strip(),
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
return GrepResult(
|
|
121
|
+
grep=GREP_NAME,
|
|
122
|
+
path=str(path) if path is not None else None,
|
|
123
|
+
passed=not findings,
|
|
124
|
+
findings=findings,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
sys.exit(run_grep(check, sys.argv))
|