@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,1386 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
"""Conformity-gate orchestrator: dispatches every grep against a Write/Edit input.
|
|
4
|
+
|
|
5
|
+
Why this orchestrator exists. The pre-emission gate's mechanical fraction
|
|
6
|
+
is a corpus of per-class greps; the orchestrator is the single dispatch
|
|
7
|
+
surface the harness invokes per `PreToolUse` Write/Edit hook. The
|
|
8
|
+
orchestrator reads the tool-input JSON from stdin, extracts the content
|
|
9
|
+
and target path, runs every grep's `check()` callable in sequence, and
|
|
10
|
+
aggregates findings into a single structured report. The gate is
|
|
11
|
+
advisory by default: it emits the consolidated report plus a per-finding
|
|
12
|
+
summary (so findings are never silent) and exits zero, letting the write
|
|
13
|
+
proceed. Strict mode is opt-in — with the ``--strict`` flag or a truthy
|
|
14
|
+
``APOTHEM_CONFORMITY_STRICT`` environment variable, a non-empty findings
|
|
15
|
+
list exits non-zero so the harness or CI blocks the action.
|
|
16
|
+
|
|
17
|
+
Performance budget. The hook's wall-clock ceiling is the 10s PreToolUse
|
|
18
|
+
limit per `rules/performance-discipline.md` §1, less the
|
|
19
|
+
PowerShell bootstrap stub's ~1500ms and the interpreter-locator's
|
|
20
|
+
~200ms; the cumulative grep budget is ~8300ms. Per-grep budget is the
|
|
21
|
+
``PER_GREP_BUDGET_SECONDS`` constant (~520ms), applied across the registered
|
|
22
|
+
per-Write greps (``len(GREP_MODULES)``) so the figure tracks the live registry
|
|
23
|
+
rather than a hardcoded count. The orchestrator times every grep and reports any
|
|
24
|
+
that approached the per-grep ceiling so future tuning can target the slow path.
|
|
25
|
+
|
|
26
|
+
Per-file vs per-staged-diff dispatch. Most greps run on the content
|
|
27
|
+
plus path. The production-ready-pr grep operates at change-set
|
|
28
|
+
granularity rather than per-file; in the per-Write dispatch path it
|
|
29
|
+
returns clean. The CLI-mode `--staged` flag invokes the substantive
|
|
30
|
+
staged-diff check.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import importlib.util
|
|
36
|
+
import json
|
|
37
|
+
import os
|
|
38
|
+
import subprocess
|
|
39
|
+
import sys
|
|
40
|
+
import time
|
|
41
|
+
from dataclasses import asdict, dataclass, field
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
from typing import Any, Final, Protocol, cast
|
|
44
|
+
|
|
45
|
+
# Environment variable that overrides the default conformity-gate scopes.
|
|
46
|
+
# When set, the gate's --hook mode runs the matcher chain only on writes
|
|
47
|
+
# whose target path falls under this directory; out-of-scope writes
|
|
48
|
+
# short-circuit to a silent pass-through.
|
|
49
|
+
SCOPE_ENV_VAR: Final[str] = "APOTHEM_CONFORMITY_SCOPE"
|
|
50
|
+
|
|
51
|
+
# Default scopes when APOTHEM_CONFORMITY_SCOPE is unset. Hook-capable
|
|
52
|
+
# user-scope harness roots are matcher-applicable territories; writes
|
|
53
|
+
# outside them are pass-through.
|
|
54
|
+
_DEFAULT_CLAUDE_SCOPE: Final[Path] = Path.home() / ".claude"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _default_scopes() -> tuple[Path, ...]:
|
|
58
|
+
"""Return the default user-scope hook-capable harness roots."""
|
|
59
|
+
codex_home = os.environ.get("CODEX_HOME")
|
|
60
|
+
codex_scope = (
|
|
61
|
+
Path(codex_home).expanduser() if codex_home else Path.home() / ".codex"
|
|
62
|
+
)
|
|
63
|
+
return (_DEFAULT_CLAUDE_SCOPE, codex_scope)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _resolve_scope() -> Path:
|
|
67
|
+
"""Return the first configured conformity-gate scope as an absolute Path.
|
|
68
|
+
|
|
69
|
+
Kept for compatibility with tests and callers that inspect the legacy
|
|
70
|
+
single-scope helper. Hook dispatch uses :func:`_resolve_scopes`.
|
|
71
|
+
"""
|
|
72
|
+
return _resolve_scopes()[0]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _resolve_scopes() -> tuple[Path, ...]:
|
|
76
|
+
"""Return the configured conformity-gate scopes as absolute Paths.
|
|
77
|
+
|
|
78
|
+
Resolution order: ``APOTHEM_CONFORMITY_SCOPE`` env var > the default
|
|
79
|
+
user-scope hook-capable harness roots. Returned paths are resolved
|
|
80
|
+
(symlinks followed; relative components collapsed) so in-scope
|
|
81
|
+
comparisons work against absolute canonical forms.
|
|
82
|
+
"""
|
|
83
|
+
override = os.environ.get(SCOPE_ENV_VAR)
|
|
84
|
+
if override:
|
|
85
|
+
return (Path(override).expanduser().resolve(),)
|
|
86
|
+
return tuple(scope.expanduser().resolve() for scope in _default_scopes())
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _path_in_scope(target: Path | None, scope: Path) -> bool:
|
|
90
|
+
"""Return True when *target* lives under *scope*.
|
|
91
|
+
|
|
92
|
+
Resolves *target* to its absolute canonical form, then walks up its
|
|
93
|
+
parent chain looking for *scope*. When *target* is None (no write
|
|
94
|
+
target supplied — e.g., a CLI mode invocation), returns True so the
|
|
95
|
+
matchers run as today; the short-circuit fires only on hook-mode
|
|
96
|
+
invocations with a resolvable out-of-scope target.
|
|
97
|
+
"""
|
|
98
|
+
if target is None:
|
|
99
|
+
return True
|
|
100
|
+
try:
|
|
101
|
+
resolved = target.expanduser().resolve()
|
|
102
|
+
except (OSError, RuntimeError):
|
|
103
|
+
# Fail-closed-but-permissive: if path resolution fails, keep
|
|
104
|
+
# matchers running so the gate does not silently drop writes
|
|
105
|
+
# whose targets are simply unusual but in-scope.
|
|
106
|
+
return True
|
|
107
|
+
try:
|
|
108
|
+
resolved.relative_to(scope)
|
|
109
|
+
except ValueError:
|
|
110
|
+
return False
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _path_in_any_scope(target: Path | None, scopes: tuple[Path, ...]) -> bool:
|
|
115
|
+
"""Return True when *target* lives under any configured scope."""
|
|
116
|
+
return any(_path_in_scope(target, scope) for scope in scopes)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def scope_relative_path(target: Path) -> tuple[Path, Path] | None:
|
|
120
|
+
"""Return ``(scope_root, path-relative-to-scope)`` for *target*'s scope.
|
|
121
|
+
|
|
122
|
+
Tries each configured conformity scope (``APOTHEM_CONFORMITY_SCOPE`` or the
|
|
123
|
+
default hook-capable harness roots) in resolution order and returns the
|
|
124
|
+
first that contains *target*, with *target* rendered relative to it. Returns
|
|
125
|
+
``None`` when *target* is under no configured scope. This is the shared seam
|
|
126
|
+
the per-Write matchers use to evaluate a hook-scoped write (e.g.
|
|
127
|
+
``~/.claude/rules/x.md``) against the same notion of "in scope" the gate
|
|
128
|
+
uses — so a matcher and the gate agree, and a write outside the apothem repo
|
|
129
|
+
root is still checked relative to the harness root it landed under.
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
resolved = target.expanduser().resolve()
|
|
133
|
+
except (OSError, RuntimeError):
|
|
134
|
+
return None
|
|
135
|
+
for scope in _resolve_scopes():
|
|
136
|
+
try:
|
|
137
|
+
return scope, resolved.relative_to(scope)
|
|
138
|
+
except ValueError:
|
|
139
|
+
continue
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# Harness runtime-state subtrees. Under each hook-capable harness root, these
|
|
144
|
+
# top-level subtrees hold the harness's OWN operator-facing state, NOT
|
|
145
|
+
# apothem-managed config:
|
|
146
|
+
# - ``projects/<hash>/`` --- per-project state: project-scoped auto-memory
|
|
147
|
+
# (``memory/MEMORY.md`` plus topic files), session transcripts, todo lists.
|
|
148
|
+
# - ``memory/`` --- the global (cross-project) auto-memory tier.
|
|
149
|
+
# Both are owned by the operator and the harness; apothem never materializes or
|
|
150
|
+
# manages them (apothem's managed config lives in sibling subtrees: ``rules/``,
|
|
151
|
+
# ``skills/``, ``agents/``, ``commands/``, ``hooks/``, ``output-styles/``,
|
|
152
|
+
# ``statuslines/``, ``settings.json``). The hook exempts these subtrees so
|
|
153
|
+
# per-Write dispatch does not block the operator's memory writes --- a
|
|
154
|
+
# ``MEMORY.md`` index is provenance-less and frontmatter-less by the auto-memory
|
|
155
|
+
# convention and would otherwise fail-close on ``orphan-output-grep`` /
|
|
156
|
+
# ``frontmatter-grep``.
|
|
157
|
+
_HARNESS_STATE_SEGMENTS: Final[frozenset[str]] = frozenset({"projects", "memory"})
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _is_harness_state_path(target: Path | None, scopes: tuple[Path, ...]) -> bool:
|
|
161
|
+
"""Return True iff *target* sits under a scope's harness runtime-state subtree.
|
|
162
|
+
|
|
163
|
+
The check is scope-coupled: it fires only when *target* resolves under a
|
|
164
|
+
configured harness-config root AND the first path component below that root
|
|
165
|
+
is one of ``_HARNESS_STATE_SEGMENTS`` (``projects`` / ``memory``). A
|
|
166
|
+
coincidental ``projects/`` or ``memory/`` directory in an unrelated
|
|
167
|
+
workspace is not exempted because that workspace is not a configured scope.
|
|
168
|
+
Returns False for ``None`` targets and for paths that fail resolution.
|
|
169
|
+
"""
|
|
170
|
+
if target is None:
|
|
171
|
+
return False
|
|
172
|
+
try:
|
|
173
|
+
resolved = target.expanduser().resolve()
|
|
174
|
+
except (OSError, RuntimeError):
|
|
175
|
+
return False
|
|
176
|
+
for scope in scopes:
|
|
177
|
+
try:
|
|
178
|
+
relative = resolved.relative_to(scope)
|
|
179
|
+
except ValueError:
|
|
180
|
+
continue
|
|
181
|
+
if relative.parts and relative.parts[0] in _HARNESS_STATE_SEGMENTS:
|
|
182
|
+
return True
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class _CheckCallable(Protocol):
|
|
187
|
+
"""Shape of every grep module's `check()` callable."""
|
|
188
|
+
|
|
189
|
+
def __call__(
|
|
190
|
+
self,
|
|
191
|
+
content: str,
|
|
192
|
+
path: Path | None = None,
|
|
193
|
+
) -> Any: ... # noqa: ANN401 # GrepResult instances are duck-typed across modules.
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# Per-grep dispatch order. The order is intentional: the cheap structural
|
|
197
|
+
# scans (placeholder + arrow notation) run first; the more expensive
|
|
198
|
+
# regex sweeps and the entropy heuristic run later. The dispatch order
|
|
199
|
+
# does not affect the final verdict (every grep is run regardless).
|
|
200
|
+
GREP_MODULES: Final[tuple[str, ...]] = (
|
|
201
|
+
"user_confirm_grep",
|
|
202
|
+
"binding_reciprocity_grep",
|
|
203
|
+
"option_annotation_grep",
|
|
204
|
+
"completion_claim_grep",
|
|
205
|
+
"hedging_grep",
|
|
206
|
+
"brand_mark_grep",
|
|
207
|
+
"diagram_staleness_grep",
|
|
208
|
+
"unpinned_action_grep",
|
|
209
|
+
"bare_except_grep",
|
|
210
|
+
"magic_number_grep",
|
|
211
|
+
"orphan_output_grep",
|
|
212
|
+
"commented_out_code_grep",
|
|
213
|
+
"secret_leak_grep",
|
|
214
|
+
"production_ready_pr_grep",
|
|
215
|
+
"file_header_grep",
|
|
216
|
+
"copilot_instructions_presence_grep",
|
|
217
|
+
"multi_surface_coherence_grep",
|
|
218
|
+
"license_author_consistency_grep",
|
|
219
|
+
"frontmatter_grep",
|
|
220
|
+
"link_check",
|
|
221
|
+
"always_on_budget_grep",
|
|
222
|
+
"token_efficiency_grep",
|
|
223
|
+
# `semver_stability_grep` is change-set-scoped (operates on the git diff
|
|
224
|
+
# between staged + HEAD), not per-Write, so it is NOT in the per-Write
|
|
225
|
+
# registry. Invoke at change-set boundary via
|
|
226
|
+
# `python -m apothem.conformity.semver_stability_grep --staged`.
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Corpus-level standalone validators. These do NOT fit the per-Write
|
|
230
|
+
# `check(content, path)` signature — they walk the working tree, the
|
|
231
|
+
# git index, or a fixed surface set. They are invoked via subprocess
|
|
232
|
+
# in --all mode and exposed via --check <name> and --list. Each script
|
|
233
|
+
# accepts a root directory as its sole argument and exits 0 (PASS) or
|
|
234
|
+
# non-zero (FAIL).
|
|
235
|
+
STANDALONE_MODULES: Final[tuple[str, ...]] = (
|
|
236
|
+
"naming-grep",
|
|
237
|
+
"smoke-install-grep",
|
|
238
|
+
"no-global-plans-grep",
|
|
239
|
+
"plan-suite-structure-grep",
|
|
240
|
+
"no-toplevel-docs-grep",
|
|
241
|
+
"plans-discipline-language-grep",
|
|
242
|
+
"plain-language-grep",
|
|
243
|
+
"reference-token-grep",
|
|
244
|
+
"freshness-token-grep",
|
|
245
|
+
"agnosticism-grep",
|
|
246
|
+
"agent-capability-grep",
|
|
247
|
+
"frontmatter-value-grep",
|
|
248
|
+
"static-version-grep",
|
|
249
|
+
"dynamism-grep",
|
|
250
|
+
"oidc-trusted-publishing-grep",
|
|
251
|
+
"recommend-next-step-grep",
|
|
252
|
+
"plan-next-step-consistency-grep",
|
|
253
|
+
"redundancy-grep",
|
|
254
|
+
"conventional-commit-grep",
|
|
255
|
+
"gitattributes-presence-grep",
|
|
256
|
+
"editorconfig-presence-grep",
|
|
257
|
+
"permissions-minimum-scope-grep",
|
|
258
|
+
"cross-platform-matrix-grep",
|
|
259
|
+
"harden-runner-grep",
|
|
260
|
+
"workflow-concurrency-grep",
|
|
261
|
+
"determinism-grep",
|
|
262
|
+
"agents-md-coverage-grep",
|
|
263
|
+
"registry-capability-consistency-grep",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Per-grep wall-clock budget in seconds. Exceeding the budget surfaces
|
|
267
|
+
# as a watch item in the report but does not block the write — the hook
|
|
268
|
+
# has its own 10s ceiling and the orchestrator avoids hard-killing a
|
|
269
|
+
# grep mid-scan.
|
|
270
|
+
PER_GREP_BUDGET_SECONDS: Final[float] = 0.520
|
|
271
|
+
|
|
272
|
+
# Where the grep modules live. The orchestrator resolves them relative
|
|
273
|
+
# to its own location so the hook works regardless of the operator's
|
|
274
|
+
# current working directory and of the layout the package is mounted in
|
|
275
|
+
# (repo checkout ``src/apothem/conformity/`` or installed
|
|
276
|
+
# ``<install-root>/apothem/conformity/`` — the modules and their
|
|
277
|
+
# ``schemas/`` sibling travel together in both shapes).
|
|
278
|
+
TOOLS_DIR: Final[Path] = Path(__file__).resolve().parent
|
|
279
|
+
|
|
280
|
+
EXIT_PASS: Final[int] = 0
|
|
281
|
+
EXIT_FAIL: Final[int] = 2
|
|
282
|
+
STDIN_FLAG: Final[str] = "--stdin"
|
|
283
|
+
CHECK_FLAG: Final[str] = "--check"
|
|
284
|
+
LIST_FLAG: Final[str] = "--list"
|
|
285
|
+
ALL_FLAG: Final[str] = "--all"
|
|
286
|
+
ALL_PERWRITE_FLAG: Final[str] = "--all-perwrite"
|
|
287
|
+
STRICT_FLAG: Final[str] = "--strict"
|
|
288
|
+
STRICT_ENV: Final[str] = "APOTHEM_CONFORMITY_STRICT"
|
|
289
|
+
_STRICT_TRUTHY: Final[frozenset[str]] = frozenset({"1", "true", "yes", "on"})
|
|
290
|
+
|
|
291
|
+
# --- Corpus per-Write runner: blocking vs advisory classification -----------
|
|
292
|
+
#
|
|
293
|
+
# The ``--all-perwrite`` mode routes every git-tracked file through every
|
|
294
|
+
# per-Write matcher in ``GREP_MODULES`` (the corpus counterpart to the
|
|
295
|
+
# harness's per-write hook dispatch). Per the EN-1 ratified posture at
|
|
296
|
+
# ``.plans/apothem-overhaul/ws5-enforcement-posture.md``, each matcher is
|
|
297
|
+
# classified into exactly one of two disjoint sets:
|
|
298
|
+
#
|
|
299
|
+
# - BLOCKING (``_BLOCKING_PER_WRITE_GREPS``): deterministic, low-false-
|
|
300
|
+
# positive matchers that are GREEN over the live tracked corpus today.
|
|
301
|
+
# A finding from one of these under ``--strict`` fails the corpus run.
|
|
302
|
+
# EN-1's governing principle: a matcher is blocking ONLY once it is
|
|
303
|
+
# green over the tracked corpus — never blocking-AND-failing.
|
|
304
|
+
#
|
|
305
|
+
# - ADVISORY (``_ADVISORY_PER_WRITE_GREPS``): matchers whose findings are
|
|
306
|
+
# reported (so the drift is never silent) but do NOT gate, because they
|
|
307
|
+
# are high-false-positive against the shipped prose / code / config /
|
|
308
|
+
# docs corpus today. Each carries a one-line remediation-owner note in
|
|
309
|
+
# ``_ADVISORY_RATIONALE`` so the classification is visible and testable.
|
|
310
|
+
# EN-1 explicitly authorizes ``hedging`` + ``plain-language`` as advisory
|
|
311
|
+
# until the SR-1 (phase 65) prose-debt clears; the remaining advisory
|
|
312
|
+
# members are matchers whose own ``check()`` fires structurally against
|
|
313
|
+
# legitimate non-target content the corpus enumerates (lockfile entropy,
|
|
314
|
+
# frontmatter-first Markdown, design-token literals, documented pattern
|
|
315
|
+
# references), not against a genuine defect a fix could clear here.
|
|
316
|
+
#
|
|
317
|
+
# The two sets PARTITION ``GREP_MODULES`` — every per-Write matcher is in
|
|
318
|
+
# exactly one, verified by ``_assert_perwrite_partition`` at import time so a
|
|
319
|
+
# matcher can never be silently left out of (or in both of) the classification.
|
|
320
|
+
|
|
321
|
+
_BLOCKING_PER_WRITE_GREPS: Final[frozenset[str]] = frozenset(
|
|
322
|
+
{
|
|
323
|
+
"binding_reciprocity_grep",
|
|
324
|
+
"option_annotation_grep",
|
|
325
|
+
"completion_claim_grep",
|
|
326
|
+
"unpinned_action_grep",
|
|
327
|
+
}
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Advisory rationale: matcher name -> (reason, remediation-owner). The reason
|
|
331
|
+
# names WHY the matcher is high-false-positive in corpus mode; the owner names
|
|
332
|
+
# the phase / surface that, once it lands, lets a successor promote the matcher
|
|
333
|
+
# to the blocking set. ``SR-1`` is the plain-language / rule-rewrite reconcile
|
|
334
|
+
# phase (phase 65) per the EN-1 posture.
|
|
335
|
+
_ADVISORY_RATIONALE: Final[dict[str, tuple[str, str]]] = {
|
|
336
|
+
"bare_except_grep": (
|
|
337
|
+
"the bare `except:` sub-rule (always a real defect) is GREEN over the "
|
|
338
|
+
"tree; the broad `except Exception:`-without-raise sub-heuristic fires "
|
|
339
|
+
"on deliberate, `# noqa: BLE001`-marked fail-open isolation boundaries "
|
|
340
|
+
"(dispatch / gate / statusline / hook) that ruff's BLE001 rule already "
|
|
341
|
+
"governs and `ruff check` already gates in CI",
|
|
342
|
+
"ruff BLE001 (already CI-gating) + SR-1 reconcile (phase 65)",
|
|
343
|
+
),
|
|
344
|
+
"user_confirm_grep": (
|
|
345
|
+
"fires on prose / commands / matcher source that DOCUMENT the "
|
|
346
|
+
"<USER-CONFIRM:id=...> placeholder syntax, not on unresolved "
|
|
347
|
+
"placeholders",
|
|
348
|
+
"SR-1 prose reconcile (phase 65)",
|
|
349
|
+
),
|
|
350
|
+
"hedging_grep": (
|
|
351
|
+
"EN-1-authorized advisory: hedging vocabulary trips a portion of the "
|
|
352
|
+
"shipped rule / doc bodies until the prose is rewritten; SR-1 reconciles "
|
|
353
|
+
"plain-language only, so the rule-body hedging debt is owned by the rule-body "
|
|
354
|
+
"rewrite cluster, not SR-1",
|
|
355
|
+
"SR-4..SR-9 rule-body rewrite cluster",
|
|
356
|
+
),
|
|
357
|
+
"brand_mark_grep": (
|
|
358
|
+
"fires on harness brand slugs (Cursor, Codex, ...) in config / "
|
|
359
|
+
"workflow / catalog files where the slug is a load-bearing catalog "
|
|
360
|
+
"entry, not a privileging brand reference",
|
|
361
|
+
"SR-1 prose reconcile (phase 65)",
|
|
362
|
+
),
|
|
363
|
+
"diagram_staleness_grep": (
|
|
364
|
+
"date-comparison heuristic flags shipped docs diagrams whose verified "
|
|
365
|
+
"date predates a sibling edit; staleness reconcile is doc-rewrite work",
|
|
366
|
+
"SR-1 prose reconcile (phase 65)",
|
|
367
|
+
),
|
|
368
|
+
"magic_number_grep": (
|
|
369
|
+
"fires on CSS design-token values, version pins, and rebuild-script "
|
|
370
|
+
"asset dimensions that are values in a data context, not logic literals",
|
|
371
|
+
"SR-1 prose reconcile (phase 65)",
|
|
372
|
+
),
|
|
373
|
+
"orphan_output_grep": (
|
|
374
|
+
"orphan-output is a multi-step-work-session concept; in corpus mode it "
|
|
375
|
+
"fires on standalone config / data files that carry no provenance "
|
|
376
|
+
"frontmatter by their own ratified convention",
|
|
377
|
+
"SR-1 prose reconcile (phase 65)",
|
|
378
|
+
),
|
|
379
|
+
"commented_out_code_grep": (
|
|
380
|
+
"fires on YAML / TOML comment blocks and shell here-doc bodies that "
|
|
381
|
+
"resemble commented-out code but are deliberate inline documentation",
|
|
382
|
+
"SR-1 prose reconcile (phase 65)",
|
|
383
|
+
),
|
|
384
|
+
"secret_leak_grep": (
|
|
385
|
+
"entropy heuristic fires on package-lock.json integrity hashes and "
|
|
386
|
+
"high-entropy doc tokens (harness slugs, base64-looking examples), and "
|
|
387
|
+
"the literal-shape heuristic fires on test fixtures that DELIBERATELY "
|
|
388
|
+
"embed fake credentials to exercise the detector under test; no genuine "
|
|
389
|
+
"secret among the corpus findings",
|
|
390
|
+
"SR-1 prose reconcile (phase 65)",
|
|
391
|
+
),
|
|
392
|
+
"production_ready_pr_grep": (
|
|
393
|
+
"change-set-scoped matcher; returns clean per-file by design, so it "
|
|
394
|
+
"carries no corpus verdict and is non-gating in per-file corpus mode",
|
|
395
|
+
"n/a (change-set granularity, not per-file)",
|
|
396
|
+
),
|
|
397
|
+
"file_header_grep": (
|
|
398
|
+
"the per-Write insertion-at-index-0 logic does not model the "
|
|
399
|
+
"frontmatter-first Markdown class (rules / agents / commands / docs / "
|
|
400
|
+
"AGENTS.md) that is the dominant ratified head convention across the "
|
|
401
|
+
"shipped tree; the genuine code-surface gaps are fixed in source",
|
|
402
|
+
"SR-1 prose reconcile (phase 65)",
|
|
403
|
+
),
|
|
404
|
+
"copilot_instructions_presence_grep": (
|
|
405
|
+
"single-target surface matcher (.github/copilot-instructions.md); in "
|
|
406
|
+
"corpus mode it returns clean for every non-target file, carrying no "
|
|
407
|
+
"corpus verdict",
|
|
408
|
+
"n/a (single-surface presence check)",
|
|
409
|
+
),
|
|
410
|
+
"multi_surface_coherence_grep": (
|
|
411
|
+
"cross-surface coherence matcher scoped to the AGENTS.md / CLAUDE.md / "
|
|
412
|
+
"copilot triad; non-target files carry no corpus verdict",
|
|
413
|
+
"n/a (cross-surface coherence check)",
|
|
414
|
+
),
|
|
415
|
+
"license_author_consistency_grep": (
|
|
416
|
+
"scoped to the LICENSE consistency surface; non-target files carry no "
|
|
417
|
+
"corpus verdict",
|
|
418
|
+
"n/a (single-surface consistency check)",
|
|
419
|
+
),
|
|
420
|
+
"frontmatter_grep": (
|
|
421
|
+
"class-inference heuristic fires on .mdx / .tsx components and docs content "
|
|
422
|
+
"whose frontmatter contract differs from the rule / skill / agent "
|
|
423
|
+
"schema it infers",
|
|
424
|
+
"SR-1 prose reconcile (phase 65)",
|
|
425
|
+
),
|
|
426
|
+
"link_check": (
|
|
427
|
+
"link reachability / resolution is inherently high-false-positive over "
|
|
428
|
+
"the corpus (relative-link base ambiguity, external-host flakiness)",
|
|
429
|
+
"SR-1 prose reconcile (phase 65)",
|
|
430
|
+
),
|
|
431
|
+
"always_on_budget_grep": (
|
|
432
|
+
"surfaces a genuine ~10-token overage on one always-on rule "
|
|
433
|
+
"(interactive-questions.md); remediation is a rule-body decomposition "
|
|
434
|
+
"owned by the token-budget / SR rewrite cluster, not the EN-3 "
|
|
435
|
+
"enforcement-wiring phase",
|
|
436
|
+
"SR token-budget rewrite cluster (SR-0..SR-9, phases 64-73)",
|
|
437
|
+
),
|
|
438
|
+
"token_efficiency_grep": (
|
|
439
|
+
"filler / qualifier prose heuristic in the same prose-debt class as "
|
|
440
|
+
"hedging; high-false-positive against shipped rule / doc bodies",
|
|
441
|
+
"SR-1 prose reconcile (phase 65)",
|
|
442
|
+
),
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
_ADVISORY_PER_WRITE_GREPS: Final[frozenset[str]] = frozenset(_ADVISORY_RATIONALE)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _assert_perwrite_partition() -> None:
|
|
449
|
+
"""Verify BLOCKING and ADVISORY partition ``GREP_MODULES`` exactly.
|
|
450
|
+
|
|
451
|
+
Raised at import time so a newly-registered per-Write matcher cannot be
|
|
452
|
+
silently left unclassified (it would fall through corpus mode with no
|
|
453
|
+
blocking / advisory verdict) or double-classified (blocking AND advisory).
|
|
454
|
+
"""
|
|
455
|
+
classified = _BLOCKING_PER_WRITE_GREPS | _ADVISORY_PER_WRITE_GREPS
|
|
456
|
+
overlap = _BLOCKING_PER_WRITE_GREPS & _ADVISORY_PER_WRITE_GREPS
|
|
457
|
+
if overlap:
|
|
458
|
+
raise RuntimeError(
|
|
459
|
+
f"per-Write matcher(s) classified BOTH blocking and advisory: "
|
|
460
|
+
f"{sorted(overlap)}"
|
|
461
|
+
)
|
|
462
|
+
registry = set(GREP_MODULES)
|
|
463
|
+
missing = registry - classified
|
|
464
|
+
extra = classified - registry
|
|
465
|
+
if missing:
|
|
466
|
+
raise RuntimeError(
|
|
467
|
+
f"per-Write matcher(s) unclassified for corpus mode: {sorted(missing)}"
|
|
468
|
+
)
|
|
469
|
+
if extra:
|
|
470
|
+
raise RuntimeError(
|
|
471
|
+
f"corpus classification names matcher(s) absent from GREP_MODULES: "
|
|
472
|
+
f"{sorted(extra)}"
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
_assert_perwrite_partition()
|
|
477
|
+
|
|
478
|
+
# Per-suffix applicability for matchers whose RULE is suffix-scoped but whose
|
|
479
|
+
# own ``check()`` does not gate by file type. Each named matcher inspects only
|
|
480
|
+
# files whose suffix is in its set; a file of any other type is skipped for
|
|
481
|
+
# that matcher (so a Markdown file is never bare-except-scanned, an arbitrary
|
|
482
|
+
# file is never workflow-action-scanned). Matchers absent from this map self-
|
|
483
|
+
# gate inside their own ``check()`` (e.g. magic-number / commented-out-code /
|
|
484
|
+
# file-header by variant family) or are content-type-agnostic prose matchers.
|
|
485
|
+
_PER_SUFFIX_APPLICABILITY: Final[dict[str, frozenset[str]]] = {
|
|
486
|
+
# M13.3 error handling is a Python rule; `except:` is Python syntax.
|
|
487
|
+
"bare_except_grep": frozenset({".py", ".pyi"}),
|
|
488
|
+
# GitHub Actions pinning applies to workflow YAML only.
|
|
489
|
+
"unpinned_action_grep": frozenset({".yml", ".yaml"}),
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
# File suffixes whose bytes are opaque to the text matchers; the corpus runner
|
|
493
|
+
# skips them entirely (it cannot decode them as UTF-8 text). Mirrors the binary
|
|
494
|
+
# extension exemptions at schemas/header-exceptions.txt.
|
|
495
|
+
_CORPUS_BINARY_SUFFIXES: Final[frozenset[str]] = frozenset(
|
|
496
|
+
{
|
|
497
|
+
".png",
|
|
498
|
+
".jpg",
|
|
499
|
+
".jpeg",
|
|
500
|
+
".gif",
|
|
501
|
+
".ico",
|
|
502
|
+
".webp",
|
|
503
|
+
".svg",
|
|
504
|
+
".pdf",
|
|
505
|
+
".zip",
|
|
506
|
+
".tar",
|
|
507
|
+
".gz",
|
|
508
|
+
".tgz",
|
|
509
|
+
".7z",
|
|
510
|
+
".rar",
|
|
511
|
+
".exe",
|
|
512
|
+
".dll",
|
|
513
|
+
".so",
|
|
514
|
+
".dylib",
|
|
515
|
+
".bin",
|
|
516
|
+
".pyc",
|
|
517
|
+
".pyo",
|
|
518
|
+
".woff",
|
|
519
|
+
".woff2",
|
|
520
|
+
".ttf",
|
|
521
|
+
".otf",
|
|
522
|
+
".eot",
|
|
523
|
+
".mp3",
|
|
524
|
+
".mp4",
|
|
525
|
+
".webm",
|
|
526
|
+
".mov",
|
|
527
|
+
".avi",
|
|
528
|
+
}
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
@dataclass(frozen=True)
|
|
533
|
+
class GrepInvocation:
|
|
534
|
+
"""One grep's result plus its wall-clock timing.
|
|
535
|
+
|
|
536
|
+
``error`` is non-None only when the matcher's load or ``check()`` raised;
|
|
537
|
+
such an invocation is recorded ``passed=True`` (fail-open isolation) so one
|
|
538
|
+
matcher's internal bug never fail-closes the operator's write, while the
|
|
539
|
+
error text is preserved in the report for diagnosis rather than swallowed.
|
|
540
|
+
"""
|
|
541
|
+
|
|
542
|
+
grep: str
|
|
543
|
+
passed: bool
|
|
544
|
+
findings: list[dict[str, object]]
|
|
545
|
+
elapsed_seconds: float
|
|
546
|
+
over_budget: bool
|
|
547
|
+
error: str | None = None
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
@dataclass(frozen=True)
|
|
551
|
+
class OrchestratorReport:
|
|
552
|
+
"""Aggregate result of one conformity-gate run.
|
|
553
|
+
|
|
554
|
+
Carries the overall pass verdict, the scanned path (``None`` in
|
|
555
|
+
corpus mode), the grep / pass / fail counts, and the per-grep
|
|
556
|
+
:class:`GrepInvocation` records.
|
|
557
|
+
"""
|
|
558
|
+
|
|
559
|
+
passed: bool
|
|
560
|
+
path: str | None
|
|
561
|
+
grep_count: int
|
|
562
|
+
pass_count: int
|
|
563
|
+
fail_count: int
|
|
564
|
+
invocations: list[GrepInvocation] = field(default_factory=list)
|
|
565
|
+
|
|
566
|
+
def to_json(self) -> str:
|
|
567
|
+
"""Serialize the report as indented JSON under the
|
|
568
|
+
``conformity-gate`` orchestrator envelope."""
|
|
569
|
+
payload = {
|
|
570
|
+
"orchestrator": "conformity-gate",
|
|
571
|
+
"passed": self.passed,
|
|
572
|
+
"path": self.path,
|
|
573
|
+
"grep_count": self.grep_count,
|
|
574
|
+
"pass_count": self.pass_count,
|
|
575
|
+
"fail_count": self.fail_count,
|
|
576
|
+
"invocations": [asdict(i) for i in self.invocations],
|
|
577
|
+
}
|
|
578
|
+
return json.dumps(payload, indent=2)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _load_check(module_name: str) -> _CheckCallable:
|
|
582
|
+
"""Load `module_name.check` via importlib (the file uses hyphens).
|
|
583
|
+
|
|
584
|
+
Why the sys.modules registration matters: Python's dataclasses decorator
|
|
585
|
+
inspects `sys.modules.get(cls.__module__)` at class-creation time to
|
|
586
|
+
resolve forward-referenced KW_ONLY sentinels. Without registering the
|
|
587
|
+
module under its qualified name before `exec_module()`, the lookup
|
|
588
|
+
returns None and the decorator raises AttributeError on Python 3.14+.
|
|
589
|
+
"""
|
|
590
|
+
module_path = TOOLS_DIR / f"{module_name}.py"
|
|
591
|
+
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
|
592
|
+
if spec is None or spec.loader is None:
|
|
593
|
+
raise RuntimeError(f"cannot load grep module at {module_path}")
|
|
594
|
+
module = importlib.util.module_from_spec(spec)
|
|
595
|
+
sys.modules[module_name] = module
|
|
596
|
+
try:
|
|
597
|
+
spec.loader.exec_module(module)
|
|
598
|
+
except Exception:
|
|
599
|
+
sys.modules.pop(module_name, None)
|
|
600
|
+
raise
|
|
601
|
+
check = getattr(module, "check", None)
|
|
602
|
+
if not callable(check):
|
|
603
|
+
raise RuntimeError(f"grep module {module_name} exposes no check() callable")
|
|
604
|
+
return check # type: ignore[no-any-return]
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _findings_as_dicts(result: object) -> list[dict[str, object]]:
|
|
608
|
+
"""Extract the findings list from a GrepResult-shaped object."""
|
|
609
|
+
findings = getattr(result, "findings", []) or []
|
|
610
|
+
return [
|
|
611
|
+
asdict(f) if hasattr(f, "__dataclass_fields__") else dict(f) for f in findings
|
|
612
|
+
]
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def _is_plan_suite_path(path: Path | None) -> bool:
|
|
616
|
+
"""Return True iff path is under the canonical project-local plans subtree.
|
|
617
|
+
|
|
618
|
+
The sole canonical project-local plans location is ``.apothem/plans/`` (the
|
|
619
|
+
shared Apothem working directory's plans child); a legacy ``.plans/`` tree
|
|
620
|
+
is no longer canonical — operators upgrade it via ``apothem
|
|
621
|
+
migrate-workspace``.
|
|
622
|
+
|
|
623
|
+
Plan-suite artifacts are gitignored ephemera per the ``.apothem/plans/**``
|
|
624
|
+
class enumerated at ``schemas/header-exceptions.txt`` (the schemas directory
|
|
625
|
+
shipped beside this package in both the repo-checkout and installed
|
|
626
|
+
layouts) and exempt from the
|
|
627
|
+
codebase quality bar (M13 code-craft, M15 production-ready). The
|
|
628
|
+
orchestrator short-circuits on plan-suite paths so the per-Write hook
|
|
629
|
+
does not block plan-suite emissions whose Markdown enumerations (dates,
|
|
630
|
+
version identifiers, phase identifiers, kebab-case directory paths)
|
|
631
|
+
fire structural false positives on matchers designed for codebase
|
|
632
|
+
content.
|
|
633
|
+
|
|
634
|
+
Per-Write narrow-routing (G1) --- DISCLOSED-DEFERRED. A narrower posture
|
|
635
|
+
would route plans writes through a small per-Write matcher subset
|
|
636
|
+
instead of a blanket synthetic PASS. It is deferred because the only
|
|
637
|
+
per-Write matcher whose rule governs plans-suite structure
|
|
638
|
+
(``orphan_output_grep``) fires "provenance absent" on legitimate
|
|
639
|
+
``_inputs/`` scratch files (e.g. a ``prose.txt`` or a frontmatter-less
|
|
640
|
+
working note) --- exactly the volatile, session-local scratch tier the
|
|
641
|
+
closed-vocabulary rule declares exempt from the codebase quality bar.
|
|
642
|
+
Routing per-Write writes through it would flood pre-existing findings on
|
|
643
|
+
conformant scratch and invert the advisory posture. The structural
|
|
644
|
+
invariants (suite-locality, closed vocabularies, numeric-prefix
|
|
645
|
+
discipline, phase coherence) are instead enforced at the CORRECT
|
|
646
|
+
granularity by the directory-walking ``plan-suite-structure-grep``
|
|
647
|
+
standalone validator, which runs over the whole tree in ``--all`` repo
|
|
648
|
+
sweeps (CI + pre-commit). A directory-walker is the right shape for
|
|
649
|
+
suite-structure invariants; the per-Write content matcher is not. When a
|
|
650
|
+
per-Write plans matcher whose rule genuinely governs scratch-tier
|
|
651
|
+
content lands, this short-circuit can be narrowed reversibly.
|
|
652
|
+
"""
|
|
653
|
+
if path is None:
|
|
654
|
+
return False
|
|
655
|
+
parts = path.parts
|
|
656
|
+
# The sole canonical project-local plans home is ``.apothem/plans`` (the
|
|
657
|
+
# shared Apothem working directory's plans child). Match the two-segment
|
|
658
|
+
# adjacency, never a lone ``.apothem`` part — ``.apothem`` also holds
|
|
659
|
+
# operator memory/learning/contexts data, which is NOT plan-suite content.
|
|
660
|
+
return any(
|
|
661
|
+
parts[i] == ".apothem" and parts[i + 1] == "plans"
|
|
662
|
+
for i in range(len(parts) - 1)
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def run_orchestrator(
|
|
667
|
+
content: str,
|
|
668
|
+
path: Path | None,
|
|
669
|
+
only: str | None = None,
|
|
670
|
+
) -> OrchestratorReport:
|
|
671
|
+
"""Run every grep; aggregate the results into a single report.
|
|
672
|
+
|
|
673
|
+
When ``only`` names a single grep module (e.g., ``"file-header-grep"``
|
|
674
|
+
or its short form ``"file-header"``), the orchestrator runs only that
|
|
675
|
+
module. The short form drops the trailing ``-grep`` suffix.
|
|
676
|
+
|
|
677
|
+
Plan-suite paths short-circuit to a synthetic PASS per
|
|
678
|
+
``_is_plan_suite_path`` --- see that helper's docstring for the rationale.
|
|
679
|
+
The short-circuit applies regardless of ``only`` so per-grep CLI
|
|
680
|
+
invocations on plan-suite paths also pass cleanly.
|
|
681
|
+
"""
|
|
682
|
+
if _is_plan_suite_path(path):
|
|
683
|
+
return OrchestratorReport(
|
|
684
|
+
passed=True,
|
|
685
|
+
path=str(path),
|
|
686
|
+
grep_count=0,
|
|
687
|
+
pass_count=0,
|
|
688
|
+
fail_count=0,
|
|
689
|
+
invocations=[],
|
|
690
|
+
)
|
|
691
|
+
if only is not None:
|
|
692
|
+
candidates = (only, f"{only}-grep")
|
|
693
|
+
modules = tuple(m for m in GREP_MODULES if m in candidates)
|
|
694
|
+
if not modules:
|
|
695
|
+
raise ValueError(f"unknown grep: {only!r}")
|
|
696
|
+
else:
|
|
697
|
+
modules = GREP_MODULES
|
|
698
|
+
invocations: list[GrepInvocation] = []
|
|
699
|
+
for module_name in modules:
|
|
700
|
+
start = time.perf_counter()
|
|
701
|
+
error: str | None = None
|
|
702
|
+
result: object = None
|
|
703
|
+
try:
|
|
704
|
+
check = _load_check(module_name)
|
|
705
|
+
result = check(content, path)
|
|
706
|
+
except Exception as exc: # noqa: BLE001, RUF100 - fail-open isolation boundary: one matcher's internal error (load failure or check() raise) must never fail-close the operator's write; the error is recorded on the invocation and surfaced, not swallowed silently
|
|
707
|
+
error = f"{type(exc).__name__}: {exc}"
|
|
708
|
+
elapsed = round(time.perf_counter() - start, 4)
|
|
709
|
+
if error is not None:
|
|
710
|
+
invocations.append(
|
|
711
|
+
GrepInvocation(
|
|
712
|
+
grep=module_name,
|
|
713
|
+
passed=True,
|
|
714
|
+
findings=[],
|
|
715
|
+
elapsed_seconds=elapsed,
|
|
716
|
+
over_budget=False,
|
|
717
|
+
error=error,
|
|
718
|
+
)
|
|
719
|
+
)
|
|
720
|
+
else:
|
|
721
|
+
invocations.append(
|
|
722
|
+
GrepInvocation(
|
|
723
|
+
grep=module_name,
|
|
724
|
+
passed=bool(getattr(result, "passed", False)),
|
|
725
|
+
findings=_findings_as_dicts(result),
|
|
726
|
+
elapsed_seconds=elapsed,
|
|
727
|
+
over_budget=elapsed > PER_GREP_BUDGET_SECONDS,
|
|
728
|
+
)
|
|
729
|
+
)
|
|
730
|
+
pass_count = sum(1 for i in invocations if i.passed)
|
|
731
|
+
fail_count = len(invocations) - pass_count
|
|
732
|
+
return OrchestratorReport(
|
|
733
|
+
passed=fail_count == 0,
|
|
734
|
+
path=str(path) if path is not None else None,
|
|
735
|
+
grep_count=len(invocations),
|
|
736
|
+
pass_count=pass_count,
|
|
737
|
+
fail_count=fail_count,
|
|
738
|
+
invocations=invocations,
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
_POSITION_KEYS: Final[frozenset[str]] = frozenset(
|
|
743
|
+
{
|
|
744
|
+
"line",
|
|
745
|
+
"lineno",
|
|
746
|
+
"line_number",
|
|
747
|
+
"start_line",
|
|
748
|
+
"end_line",
|
|
749
|
+
"column",
|
|
750
|
+
"col",
|
|
751
|
+
"pos",
|
|
752
|
+
"position",
|
|
753
|
+
"range",
|
|
754
|
+
"occurrences",
|
|
755
|
+
"context",
|
|
756
|
+
}
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def _read_tool_input_from_stdin() -> tuple[str, Path | None, str | None]:
|
|
761
|
+
"""Parse the harness tool-input JSON; extract post / path / pre."""
|
|
762
|
+
raw = sys.stdin.read()
|
|
763
|
+
if not raw.strip():
|
|
764
|
+
return "", None, None
|
|
765
|
+
try:
|
|
766
|
+
payload = json.loads(raw)
|
|
767
|
+
except json.JSONDecodeError:
|
|
768
|
+
return raw, None, None
|
|
769
|
+
tool_input = payload.get("tool_input") or {}
|
|
770
|
+
if not isinstance(tool_input, dict):
|
|
771
|
+
return raw, None, None
|
|
772
|
+
file_path_raw = tool_input.get("file_path")
|
|
773
|
+
path = (
|
|
774
|
+
Path(file_path_raw)
|
|
775
|
+
if isinstance(file_path_raw, str) and file_path_raw
|
|
776
|
+
else None
|
|
777
|
+
)
|
|
778
|
+
if "new_string" in tool_input and "old_string" in tool_input and path is not None:
|
|
779
|
+
try:
|
|
780
|
+
existing = path.read_text(encoding="utf-8")
|
|
781
|
+
new_str = tool_input.get("new_string") or ""
|
|
782
|
+
old_str = tool_input.get("old_string") or ""
|
|
783
|
+
replace_all = bool(tool_input.get("replace_all"))
|
|
784
|
+
if replace_all:
|
|
785
|
+
content = existing.replace(old_str, new_str)
|
|
786
|
+
else:
|
|
787
|
+
content = existing.replace(old_str, new_str, 1)
|
|
788
|
+
post = content if isinstance(content, str) else ""
|
|
789
|
+
return post, path, existing
|
|
790
|
+
except (FileNotFoundError, OSError, UnicodeDecodeError):
|
|
791
|
+
pass
|
|
792
|
+
content = tool_input.get("content") or tool_input.get("new_string") or ""
|
|
793
|
+
return content if isinstance(content, str) else "", path, None
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
def _finding_signature(finding: dict[str, object]) -> str:
|
|
797
|
+
"""Canonicalise a finding for pre / post equality, ignoring position."""
|
|
798
|
+
canonical = {k: v for k, v in finding.items() if k not in _POSITION_KEYS}
|
|
799
|
+
return json.dumps(canonical, sort_keys=True, default=str)
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def _diff_invocations(
|
|
803
|
+
pre: list[GrepInvocation], post: list[GrepInvocation]
|
|
804
|
+
) -> list[GrepInvocation]:
|
|
805
|
+
"""Return post invocations with pre-existing findings suppressed."""
|
|
806
|
+
pre_by_matcher: dict[str, list[str]] = {}
|
|
807
|
+
for inv in pre:
|
|
808
|
+
pre_by_matcher.setdefault(inv.grep, []).extend(
|
|
809
|
+
_finding_signature(f) for f in inv.findings
|
|
810
|
+
)
|
|
811
|
+
diffed = []
|
|
812
|
+
for inv in post:
|
|
813
|
+
bag = list(pre_by_matcher.get(inv.grep, ()))
|
|
814
|
+
retained = []
|
|
815
|
+
for finding in inv.findings:
|
|
816
|
+
sig = _finding_signature(finding)
|
|
817
|
+
if sig in bag:
|
|
818
|
+
bag.remove(sig)
|
|
819
|
+
continue
|
|
820
|
+
retained.append(finding)
|
|
821
|
+
diffed.append(
|
|
822
|
+
GrepInvocation(
|
|
823
|
+
grep=inv.grep,
|
|
824
|
+
passed=not retained,
|
|
825
|
+
findings=retained,
|
|
826
|
+
elapsed_seconds=inv.elapsed_seconds,
|
|
827
|
+
over_budget=inv.over_budget,
|
|
828
|
+
error=inv.error,
|
|
829
|
+
)
|
|
830
|
+
)
|
|
831
|
+
return diffed
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def _orchestrator_diff_report(
|
|
835
|
+
pre_content: str,
|
|
836
|
+
post_content: str,
|
|
837
|
+
path: Path | None,
|
|
838
|
+
only: str | None,
|
|
839
|
+
) -> OrchestratorReport:
|
|
840
|
+
"""Run pre / post orchestrator passes; emit a differential report."""
|
|
841
|
+
pre_report = run_orchestrator(pre_content, path, only=only)
|
|
842
|
+
post_report = run_orchestrator(post_content, path, only=only)
|
|
843
|
+
diffed = _diff_invocations(
|
|
844
|
+
list(pre_report.invocations), list(post_report.invocations)
|
|
845
|
+
)
|
|
846
|
+
pass_count = sum(1 for inv in diffed if inv.passed)
|
|
847
|
+
fail_count = len(diffed) - pass_count
|
|
848
|
+
return OrchestratorReport(
|
|
849
|
+
passed=fail_count == 0,
|
|
850
|
+
path=post_report.path,
|
|
851
|
+
grep_count=len(diffed),
|
|
852
|
+
pass_count=pass_count,
|
|
853
|
+
fail_count=fail_count,
|
|
854
|
+
invocations=diffed,
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def _read_cli_input(argv: list[str]) -> tuple[str, Path | None]:
|
|
859
|
+
"""CLI mode — file path or `--stdin` direct content."""
|
|
860
|
+
if len(argv) >= 2 and argv[1] != STDIN_FLAG:
|
|
861
|
+
path = Path(argv[1])
|
|
862
|
+
return path.read_text(encoding="utf-8"), path
|
|
863
|
+
return sys.stdin.read(), None
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
def _split_check_flag(argv: list[str]) -> tuple[list[str], str | None]:
|
|
867
|
+
"""Strip a leading ``--check <name>`` pair; return (rest, name or None).
|
|
868
|
+
|
|
869
|
+
The flag may appear at any leading position before the path or the
|
|
870
|
+
``--stdin`` / ``--hook`` switch. When present, the next argv entry
|
|
871
|
+
is the grep name; both are consumed.
|
|
872
|
+
"""
|
|
873
|
+
if len(argv) >= 3 and argv[1] == CHECK_FLAG:
|
|
874
|
+
return [argv[0], *argv[3:]], argv[2]
|
|
875
|
+
return argv, None
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def _resolve_strict(argv: list[str]) -> tuple[list[str], bool]:
|
|
879
|
+
"""Strip every ``--strict`` flag from *argv*; return (rest, strict_enabled).
|
|
880
|
+
|
|
881
|
+
The gate is advisory by default: findings are reported but never block,
|
|
882
|
+
abort, or force a non-zero exit. Strict mode is opt-in — the operator
|
|
883
|
+
enables it with the ``--strict`` flag or a truthy ``APOTHEM_CONFORMITY_STRICT``
|
|
884
|
+
environment variable (e.g., a CI job that wants findings to fail the build).
|
|
885
|
+
"""
|
|
886
|
+
rest = [arg for arg in argv if arg != STRICT_FLAG]
|
|
887
|
+
flag_present = len(rest) != len(argv)
|
|
888
|
+
env_enabled = os.environ.get(STRICT_ENV, "").strip().lower() in _STRICT_TRUTHY
|
|
889
|
+
return rest, flag_present or env_enabled
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
def _gate_exit(passed: bool, *, strict: bool) -> int:
|
|
893
|
+
"""Map a gate verdict to an exit code under the advisory-by-default posture.
|
|
894
|
+
|
|
895
|
+
A clean run always exits ``EXIT_PASS``. A run with findings exits
|
|
896
|
+
``EXIT_PASS`` (advisory — the findings are reported, the action proceeds)
|
|
897
|
+
unless strict mode is enabled, in which case it exits ``EXIT_FAIL``.
|
|
898
|
+
"""
|
|
899
|
+
if passed:
|
|
900
|
+
return EXIT_PASS
|
|
901
|
+
return EXIT_FAIL if strict else EXIT_PASS
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
def advisory_findings_present(payload: dict[str, object]) -> bool:
|
|
905
|
+
"""Return the corpus / standalone payload's ``advisory_findings_present`` flag.
|
|
906
|
+
|
|
907
|
+
Both ``--all`` (standalone validators) and ``--all-perwrite`` (per-Write
|
|
908
|
+
corpus) emit this top-level boolean: True iff an advisory validator / matcher
|
|
909
|
+
reported a finding. The flag is the load-bearing handle a consumer reads to
|
|
910
|
+
surface advisory drift WITHOUT consulting the gating verdict; it is kept
|
|
911
|
+
separate from ``passed`` precisely because advisory drift is non-gating.
|
|
912
|
+
"""
|
|
913
|
+
return bool(payload.get("advisory_findings_present", False))
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def _strict_exit_with_advisory(
|
|
917
|
+
*,
|
|
918
|
+
blocking_passed: bool,
|
|
919
|
+
advisory_present: bool,
|
|
920
|
+
strict: bool,
|
|
921
|
+
) -> int:
|
|
922
|
+
"""Map a corpus / standalone verdict to an exit code, advisory flag in hand.
|
|
923
|
+
|
|
924
|
+
The EN-1 ratified posture (``.plans/apothem-overhaul/ws5-enforcement-posture.md``)
|
|
925
|
+
fixes the strict split: under ``--strict`` the BLOCKING verdict gates (a
|
|
926
|
+
blocking finding exits non-zero), and ADVISORY drift is surfaced but
|
|
927
|
+
**never gates** — a leaked reference-token / stale-AGENTS advisory does NOT
|
|
928
|
+
fail the strict step. This helper makes ``advisory_present`` load-bearing by
|
|
929
|
+
threading it through the strict decision explicitly: the gating exit code is
|
|
930
|
+
derived from ``blocking_passed`` alone, while ``advisory_present`` is
|
|
931
|
+
surfaced on stderr (so the drift is never silent) and intentionally does not
|
|
932
|
+
alter the exit code. The choice is pinned by a test so promotion of any
|
|
933
|
+
advisory member to gating is a deliberate, reviewed classification change,
|
|
934
|
+
never an accidental flip of the strict step's behavior.
|
|
935
|
+
"""
|
|
936
|
+
if strict and advisory_present:
|
|
937
|
+
sys.stderr.write(
|
|
938
|
+
"conformity-gate: advisory finding(s) present (non-gating per the "
|
|
939
|
+
"EN-1 posture); surfaced for review, exit code unaffected\n"
|
|
940
|
+
)
|
|
941
|
+
return _gate_exit(blocking_passed, strict=strict)
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def _resolve_validator(name: str) -> tuple[str, bool]:
|
|
945
|
+
"""Resolve a short or full validator name to its canonical form.
|
|
946
|
+
|
|
947
|
+
Returns ``(canonical_name, is_standalone)`` or raises ``ValueError``
|
|
948
|
+
when the name is unknown. The short form drops the trailing
|
|
949
|
+
``_grep`` suffix (e.g., ``file_header`` → ``file_header_grep``).
|
|
950
|
+
Accepts both hyphenated and underscored forms from CLI input.
|
|
951
|
+
"""
|
|
952
|
+
normalized = name.replace("-", "_")
|
|
953
|
+
candidates = (normalized, f"{normalized}_grep")
|
|
954
|
+
# Normalize each registry entry to the underscored form before the
|
|
955
|
+
# membership test: GREP_MODULES entries are already underscored, but
|
|
956
|
+
# STANDALONE_MODULES entries are stored hyphenated (CLI ergonomics) and
|
|
957
|
+
# would never match the underscored *candidates* otherwise. The returned
|
|
958
|
+
# canonical preserves each registry's stored form.
|
|
959
|
+
for canonical in GREP_MODULES:
|
|
960
|
+
if canonical.replace("-", "_") in candidates:
|
|
961
|
+
return canonical, False
|
|
962
|
+
for canonical in STANDALONE_MODULES:
|
|
963
|
+
if canonical.replace("-", "_") in candidates:
|
|
964
|
+
return canonical, True
|
|
965
|
+
raise ValueError(f"unknown grep: {name!r}")
|
|
966
|
+
|
|
967
|
+
|
|
968
|
+
def _list_validators() -> str:
|
|
969
|
+
"""Return the JSON enumeration of every registered validator."""
|
|
970
|
+
payload = {
|
|
971
|
+
"orchestrator": "conformity-gate",
|
|
972
|
+
"per_write_greps": list(GREP_MODULES),
|
|
973
|
+
"standalone_validators": list(STANDALONE_MODULES),
|
|
974
|
+
"total": len(GREP_MODULES) + len(STANDALONE_MODULES),
|
|
975
|
+
}
|
|
976
|
+
return json.dumps(payload, indent=2)
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def _run_standalone(name: str, root: Path) -> tuple[bool, str]:
|
|
980
|
+
"""Invoke a standalone validator via subprocess; return (passed, output).
|
|
981
|
+
|
|
982
|
+
The ``STANDALONE_MODULES`` names are hyphenated for CLI ergonomics
|
|
983
|
+
(``naming-grep``), but the on-disk module filenames are underscored
|
|
984
|
+
(``naming_grep.py``). Normalize the name to the underscored form
|
|
985
|
+
before resolving the script path so ``--all`` and ``--check`` both
|
|
986
|
+
locate the script regardless of which form the caller supplied.
|
|
987
|
+
"""
|
|
988
|
+
script = TOOLS_DIR / f"{name.replace('-', '_')}.py"
|
|
989
|
+
if not script.exists():
|
|
990
|
+
return False, f"{name}: script absent at {script}"
|
|
991
|
+
try:
|
|
992
|
+
completed = subprocess.run( # noqa: S603 — trusted invocation: sys.executable + literal in-repo script path against a validated STANDALONE_MODULES name
|
|
993
|
+
[sys.executable, str(script), str(root)],
|
|
994
|
+
capture_output=True,
|
|
995
|
+
text=True,
|
|
996
|
+
check=False,
|
|
997
|
+
encoding="utf-8",
|
|
998
|
+
)
|
|
999
|
+
except OSError as exc:
|
|
1000
|
+
return False, f"{name}: invocation failed: {exc}"
|
|
1001
|
+
output = completed.stdout or completed.stderr or ""
|
|
1002
|
+
return completed.returncode == EXIT_PASS, output
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
def _advisory_verdict(output: str) -> dict[str, object] | None:
|
|
1006
|
+
"""Return an advisory validator's inner verdict from its JSON output.
|
|
1007
|
+
|
|
1008
|
+
An advisory validator (one whose JSON declares ``advisory: true``) exits 0
|
|
1009
|
+
even when it has findings, so its subprocess exit code — the gate's
|
|
1010
|
+
per-validator ``passed`` field — stays green while its JSON reports
|
|
1011
|
+
``passed: false``. This parses that JSON so the inner verdict can be
|
|
1012
|
+
propagated into the gate report; a consumer reading the per-validator
|
|
1013
|
+
result then sees the drift without parsing the embedded ``output`` string.
|
|
1014
|
+
|
|
1015
|
+
Returns ``{"passed": bool, "findings": list}`` when the output is JSON
|
|
1016
|
+
declaring ``advisory: true``; otherwise None (the validator is not
|
|
1017
|
+
advisory, or its output is not parseable JSON).
|
|
1018
|
+
"""
|
|
1019
|
+
try:
|
|
1020
|
+
payload = json.loads(output)
|
|
1021
|
+
except (json.JSONDecodeError, ValueError, TypeError):
|
|
1022
|
+
return None
|
|
1023
|
+
if not isinstance(payload, dict) or not payload.get("advisory"):
|
|
1024
|
+
return None
|
|
1025
|
+
findings = payload.get("findings")
|
|
1026
|
+
return {
|
|
1027
|
+
"passed": bool(payload.get("passed", True)),
|
|
1028
|
+
"findings": findings if isinstance(findings, list) else [],
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
def _run_all(root: Path) -> tuple[bool, str]:
|
|
1033
|
+
"""Run every standalone validator; aggregate exit verdicts.
|
|
1034
|
+
|
|
1035
|
+
Per-Write greps are NOT invoked in --all mode because they require
|
|
1036
|
+
file-content input from the harness's tool-input JSON. The harness
|
|
1037
|
+
itself runs them per-Write via the --hook dispatch path; --all is
|
|
1038
|
+
the corpus-level counterpart for CI / Makefile invocations.
|
|
1039
|
+
|
|
1040
|
+
Advisory validators exit 0 even when they report findings, so the overall
|
|
1041
|
+
gate stays green; their inner verdict is surfaced per-validator as
|
|
1042
|
+
``advisory`` / ``advisory_passed`` / ``findings`` and aggregated into the
|
|
1043
|
+
top-level ``advisory_findings_present`` flag so the drift is visible
|
|
1044
|
+
without parsing each validator's embedded ``output``.
|
|
1045
|
+
"""
|
|
1046
|
+
results: list[dict[str, object]] = []
|
|
1047
|
+
overall_passed = True
|
|
1048
|
+
advisory_findings_present = False
|
|
1049
|
+
for name in STANDALONE_MODULES:
|
|
1050
|
+
passed, output = _run_standalone(name, root)
|
|
1051
|
+
if not passed:
|
|
1052
|
+
overall_passed = False
|
|
1053
|
+
entry: dict[str, object] = {
|
|
1054
|
+
"validator": name,
|
|
1055
|
+
"passed": passed,
|
|
1056
|
+
"output": output.strip(),
|
|
1057
|
+
}
|
|
1058
|
+
verdict = _advisory_verdict(output)
|
|
1059
|
+
if verdict is not None:
|
|
1060
|
+
entry["advisory"] = True
|
|
1061
|
+
entry["advisory_passed"] = verdict["passed"]
|
|
1062
|
+
entry["findings"] = verdict["findings"]
|
|
1063
|
+
if not verdict["passed"]:
|
|
1064
|
+
advisory_findings_present = True
|
|
1065
|
+
results.append(entry)
|
|
1066
|
+
payload = {
|
|
1067
|
+
"orchestrator": "conformity-gate",
|
|
1068
|
+
"mode": "all",
|
|
1069
|
+
"root": str(root),
|
|
1070
|
+
"passed": overall_passed,
|
|
1071
|
+
"advisory_findings_present": advisory_findings_present,
|
|
1072
|
+
"validator_count": len(STANDALONE_MODULES),
|
|
1073
|
+
"results": results,
|
|
1074
|
+
}
|
|
1075
|
+
return overall_passed, json.dumps(payload, indent=2)
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def _corpus_tracked_files(root: Path) -> list[Path]:
|
|
1079
|
+
"""Return the git-tracked files under *root* as resolved absolute Paths.
|
|
1080
|
+
|
|
1081
|
+
Uses ``git ls-files`` so the corpus is exactly the tracked working tree —
|
|
1082
|
+
ignored artifacts (``dist/``, caches, ``.plans/`` ephemera) are excluded by
|
|
1083
|
+
construction. The invocation is read-only. When git is unavailable or
|
|
1084
|
+
*root* is not a work tree, returns an empty list (the caller treats an
|
|
1085
|
+
empty corpus as a clean pass — there is nothing tracked to scan).
|
|
1086
|
+
"""
|
|
1087
|
+
try:
|
|
1088
|
+
completed = subprocess.run(
|
|
1089
|
+
["git", "ls-files", "-z"], # noqa: S607 — read-only invocation; argv list (never shell); `git` resolved from PATH is the standard cross-platform invocation, mirroring the repo's other git subprocess call sites
|
|
1090
|
+
cwd=str(root),
|
|
1091
|
+
capture_output=True,
|
|
1092
|
+
check=False,
|
|
1093
|
+
encoding="utf-8",
|
|
1094
|
+
)
|
|
1095
|
+
except (OSError, ValueError):
|
|
1096
|
+
return []
|
|
1097
|
+
if completed.returncode != EXIT_PASS:
|
|
1098
|
+
return []
|
|
1099
|
+
rels = [entry for entry in completed.stdout.split("\0") if entry]
|
|
1100
|
+
return [(root / rel).resolve() for rel in rels]
|
|
1101
|
+
|
|
1102
|
+
|
|
1103
|
+
def _is_corpus_fixture_path(path: Path) -> bool:
|
|
1104
|
+
"""Return True for deliberately-malformed conformity-test fixture files.
|
|
1105
|
+
|
|
1106
|
+
The ``tests/conformity/<matcher>/`` and ``tests/fixtures/`` trees carry
|
|
1107
|
+
``fail.*`` / ``pass.*`` and other reference fixtures whose content is
|
|
1108
|
+
intentionally non-conformant (a planted violation a matcher's own unit test
|
|
1109
|
+
asserts on). Scanning them in corpus mode would surface those planted
|
|
1110
|
+
violations as corpus findings. They are exempt by the same principle the
|
|
1111
|
+
``.plans/`` short-circuit applies: fixture data is not shipped codebase
|
|
1112
|
+
content held to the quality bar.
|
|
1113
|
+
"""
|
|
1114
|
+
parts = path.parts
|
|
1115
|
+
if "tests" not in parts:
|
|
1116
|
+
return False
|
|
1117
|
+
tail = parts[parts.index("tests") + 1 :]
|
|
1118
|
+
return bool(tail) and tail[0] in {"conformity", "fixtures"}
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def _matcher_applies_to(module_name: str, path: Path) -> bool:
|
|
1122
|
+
"""Return True when *module_name* should inspect *path* in corpus mode.
|
|
1123
|
+
|
|
1124
|
+
Honors the per-suffix applicability map for matchers whose rule is suffix-
|
|
1125
|
+
scoped but whose own ``check()`` does not gate by file type (so a Markdown
|
|
1126
|
+
file is never bare-except-scanned). Matchers absent from the map self-gate
|
|
1127
|
+
inside ``check()`` and are always invoked (they return clean for files
|
|
1128
|
+
their rule does not govern).
|
|
1129
|
+
"""
|
|
1130
|
+
suffixes = _PER_SUFFIX_APPLICABILITY.get(module_name)
|
|
1131
|
+
if suffixes is None:
|
|
1132
|
+
return True
|
|
1133
|
+
return path.suffix.lower() in suffixes
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
def _run_all_perwrite(root: Path) -> tuple[bool, str]:
|
|
1137
|
+
"""Run every per-Write matcher over the git-tracked corpus under *root*.
|
|
1138
|
+
|
|
1139
|
+
The corpus counterpart to the harness's per-write hook dispatch: each
|
|
1140
|
+
tracked file is routed through every ``GREP_MODULES`` matcher with per-
|
|
1141
|
+
suffix applicability (``_matcher_applies_to``) and the same ``.plans/`` /
|
|
1142
|
+
fixture exemptions the per-write path honors. Findings aggregate per
|
|
1143
|
+
matcher and split into a blocking tally and an advisory tally per the
|
|
1144
|
+
``_BLOCKING_PER_WRITE_GREPS`` / ``_ADVISORY_PER_WRITE_GREPS`` partition.
|
|
1145
|
+
|
|
1146
|
+
Returns ``(blocking_clean, payload)`` where ``blocking_clean`` is True iff
|
|
1147
|
+
zero blocking matchers flagged a finding. Advisory findings are reported
|
|
1148
|
+
(so drift is never silent) but never affect ``blocking_clean``. A matcher
|
|
1149
|
+
result carrying a ``note`` (EN-2's "scope not resolvable" skip) is not a
|
|
1150
|
+
finding. The caller maps ``blocking_clean`` to an exit code under the
|
|
1151
|
+
advisory-by-default posture (``--strict`` makes a non-clean blocking run
|
|
1152
|
+
exit non-zero).
|
|
1153
|
+
"""
|
|
1154
|
+
files = _corpus_tracked_files(root)
|
|
1155
|
+
# matcher -> {"finding_count": int, "file_count": int, "files": [rel, ...]}
|
|
1156
|
+
blocking: dict[str, dict[str, object]] = {}
|
|
1157
|
+
advisory: dict[str, dict[str, object]] = {}
|
|
1158
|
+
files_scanned = 0
|
|
1159
|
+
for abs_path in files:
|
|
1160
|
+
if abs_path.suffix.lower() in _CORPUS_BINARY_SUFFIXES:
|
|
1161
|
+
continue
|
|
1162
|
+
if _is_plan_suite_path(abs_path) or _is_corpus_fixture_path(abs_path):
|
|
1163
|
+
continue
|
|
1164
|
+
if "_vendor" in abs_path.parts or "node_modules" in abs_path.parts:
|
|
1165
|
+
# Vendored / third-party trees carry upstream conventions, not the
|
|
1166
|
+
# apothem quality bar — exempt per schemas/header-exceptions.txt
|
|
1167
|
+
# (`**/_vendor/**`, `**/node_modules/**`).
|
|
1168
|
+
continue
|
|
1169
|
+
try:
|
|
1170
|
+
content = abs_path.read_text(encoding="utf-8")
|
|
1171
|
+
except (OSError, UnicodeDecodeError):
|
|
1172
|
+
# Undecodable-as-text tracked file (an unlisted binary suffix);
|
|
1173
|
+
# there is nothing for the text matchers to scan.
|
|
1174
|
+
continue
|
|
1175
|
+
files_scanned += 1
|
|
1176
|
+
try:
|
|
1177
|
+
rel = str(abs_path.relative_to(root))
|
|
1178
|
+
except ValueError:
|
|
1179
|
+
rel = str(abs_path)
|
|
1180
|
+
for module_name in GREP_MODULES:
|
|
1181
|
+
if not _matcher_applies_to(module_name, abs_path):
|
|
1182
|
+
continue
|
|
1183
|
+
try:
|
|
1184
|
+
result = _load_check(module_name)(content, abs_path)
|
|
1185
|
+
except Exception: # noqa: S112, BLE001, RUF100 — fail-open isolation: one matcher's internal error (load failure or check() raise) must never fail-close the corpus run; the matcher contributes no finding for this file and the run proceeds, mirroring run_orchestrator's per-matcher isolation boundary (BLE001 is the intent marker; RUF100 self-suppresses because ruff's BLE family is not active)
|
|
1186
|
+
continue
|
|
1187
|
+
if getattr(result, "note", None) is not None:
|
|
1188
|
+
# EN-2 scope-not-resolvable skip — not a finding.
|
|
1189
|
+
continue
|
|
1190
|
+
findings = getattr(result, "findings", None) or []
|
|
1191
|
+
if not findings:
|
|
1192
|
+
continue
|
|
1193
|
+
bucket = blocking if module_name in _BLOCKING_PER_WRITE_GREPS else advisory
|
|
1194
|
+
entry = bucket.setdefault(
|
|
1195
|
+
module_name,
|
|
1196
|
+
{"finding_count": 0, "file_count": 0, "files": []},
|
|
1197
|
+
)
|
|
1198
|
+
entry["finding_count"] = cast(int, entry["finding_count"]) + len(findings)
|
|
1199
|
+
entry["file_count"] = cast(int, entry["file_count"]) + 1
|
|
1200
|
+
sample = entry["files"]
|
|
1201
|
+
if isinstance(sample, list) and len(sample) < 10:
|
|
1202
|
+
sample.append(rel)
|
|
1203
|
+
blocking_clean = not blocking
|
|
1204
|
+
blocking_results = [
|
|
1205
|
+
{
|
|
1206
|
+
"matcher": name,
|
|
1207
|
+
"finding_count": data["finding_count"],
|
|
1208
|
+
"file_count": data["file_count"],
|
|
1209
|
+
"files": data["files"],
|
|
1210
|
+
}
|
|
1211
|
+
for name, data in sorted(blocking.items())
|
|
1212
|
+
]
|
|
1213
|
+
advisory_results = [
|
|
1214
|
+
{
|
|
1215
|
+
"matcher": name,
|
|
1216
|
+
"finding_count": advisory[name]["finding_count"] if name in advisory else 0,
|
|
1217
|
+
"file_count": advisory[name]["file_count"] if name in advisory else 0,
|
|
1218
|
+
"files": advisory[name]["files"] if name in advisory else [],
|
|
1219
|
+
"reason": _ADVISORY_RATIONALE[name][0],
|
|
1220
|
+
"remediation_owner": _ADVISORY_RATIONALE[name][1],
|
|
1221
|
+
}
|
|
1222
|
+
for name in sorted(_ADVISORY_PER_WRITE_GREPS)
|
|
1223
|
+
if name in advisory
|
|
1224
|
+
]
|
|
1225
|
+
payload = {
|
|
1226
|
+
"orchestrator": "conformity-gate",
|
|
1227
|
+
"mode": "all-perwrite",
|
|
1228
|
+
"root": str(root),
|
|
1229
|
+
"passed": blocking_clean,
|
|
1230
|
+
"files_scanned": files_scanned,
|
|
1231
|
+
"blocking_matcher_count": len(_BLOCKING_PER_WRITE_GREPS),
|
|
1232
|
+
"advisory_matcher_count": len(_ADVISORY_PER_WRITE_GREPS),
|
|
1233
|
+
"blocking_findings_present": not blocking_clean,
|
|
1234
|
+
"advisory_findings_present": bool(advisory),
|
|
1235
|
+
"blocking": blocking_results,
|
|
1236
|
+
"advisory": advisory_results,
|
|
1237
|
+
}
|
|
1238
|
+
return blocking_clean, json.dumps(payload, indent=2)
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
def _silent_pass(path: Path | None) -> int:
|
|
1242
|
+
"""Emit a silent pass-through report to stdout; return EXIT_PASS.
|
|
1243
|
+
|
|
1244
|
+
Shared by the hook-mode short-circuits (out-of-scope target,
|
|
1245
|
+
per-project harness runtime-state target) so the matcher chain is
|
|
1246
|
+
skipped without blocking the write.
|
|
1247
|
+
"""
|
|
1248
|
+
report = OrchestratorReport(
|
|
1249
|
+
passed=True,
|
|
1250
|
+
path=str(path) if path else None,
|
|
1251
|
+
grep_count=0,
|
|
1252
|
+
pass_count=0,
|
|
1253
|
+
fail_count=0,
|
|
1254
|
+
)
|
|
1255
|
+
print(report.to_json())
|
|
1256
|
+
return EXIT_PASS
|
|
1257
|
+
|
|
1258
|
+
|
|
1259
|
+
def _findings_summary(report: OrchestratorReport, *, strict: bool) -> str:
|
|
1260
|
+
"""Build the human-readable findings summary for the hook's stderr surface.
|
|
1261
|
+
|
|
1262
|
+
The harness shows a ``PreToolUse`` hook's stderr to the operator; the JSON
|
|
1263
|
+
report on stdout is machine-facing. This summary surfaces the findings so
|
|
1264
|
+
they are never silent. In advisory mode (the default) the write proceeds
|
|
1265
|
+
and the summary points the operator at the fix plus the ``--strict`` opt-in;
|
|
1266
|
+
in strict mode the summary names the block reason. One line per finding;
|
|
1267
|
+
the matcher name plus its issue and detail are surfaced so it is actionable.
|
|
1268
|
+
"""
|
|
1269
|
+
target = report.path or "<stdin>"
|
|
1270
|
+
if strict:
|
|
1271
|
+
header = (
|
|
1272
|
+
f"conformity-gate (strict) flagged {report.fail_count} matcher(s) "
|
|
1273
|
+
f"on {target} and exits non-zero to fail the CI / pre-commit step. "
|
|
1274
|
+
f"The shipped PreToolUse hook runs advisory and does not pass "
|
|
1275
|
+
f"--strict, so a runtime write proceeds; strict gating bites at "
|
|
1276
|
+
f"merge time, not at the tool call."
|
|
1277
|
+
)
|
|
1278
|
+
else:
|
|
1279
|
+
header = (
|
|
1280
|
+
f"conformity-gate (advisory) flagged {report.fail_count} matcher(s) "
|
|
1281
|
+
f"on {target}; the write proceeds — review and address the findings, "
|
|
1282
|
+
f"or run with --strict (or APOTHEM_CONFORMITY_STRICT=1) to block."
|
|
1283
|
+
)
|
|
1284
|
+
lines = [header]
|
|
1285
|
+
for inv in report.invocations:
|
|
1286
|
+
if inv.passed:
|
|
1287
|
+
continue
|
|
1288
|
+
for finding in inv.findings:
|
|
1289
|
+
label = (
|
|
1290
|
+
finding.get("issue")
|
|
1291
|
+
or finding.get("value")
|
|
1292
|
+
or finding.get("form")
|
|
1293
|
+
or "finding"
|
|
1294
|
+
)
|
|
1295
|
+
detail = finding.get("detail") or finding.get("rule") or ""
|
|
1296
|
+
entry = f" - [{inv.grep}] {label}"
|
|
1297
|
+
if detail:
|
|
1298
|
+
entry += f": {detail}"
|
|
1299
|
+
lines.append(entry)
|
|
1300
|
+
return "\n".join(lines)
|
|
1301
|
+
|
|
1302
|
+
|
|
1303
|
+
def main(argv: list[str] | None = None) -> int:
|
|
1304
|
+
if argv is None:
|
|
1305
|
+
argv = sys.argv
|
|
1306
|
+
argv, strict = _resolve_strict(argv)
|
|
1307
|
+
if len(argv) >= 2 and argv[1] == LIST_FLAG:
|
|
1308
|
+
print(_list_validators())
|
|
1309
|
+
return EXIT_PASS
|
|
1310
|
+
if len(argv) >= 2 and argv[1] == ALL_PERWRITE_FLAG:
|
|
1311
|
+
root = Path(argv[2]) if len(argv) >= 3 else Path.cwd()
|
|
1312
|
+
passed, payload = _run_all_perwrite(root)
|
|
1313
|
+
print(payload)
|
|
1314
|
+
return _strict_exit_with_advisory(
|
|
1315
|
+
blocking_passed=passed,
|
|
1316
|
+
advisory_present=advisory_findings_present(json.loads(payload)),
|
|
1317
|
+
strict=strict,
|
|
1318
|
+
)
|
|
1319
|
+
if len(argv) >= 2 and argv[1] == ALL_FLAG:
|
|
1320
|
+
root = Path(argv[2]) if len(argv) >= 3 else Path.cwd()
|
|
1321
|
+
passed, payload = _run_all(root)
|
|
1322
|
+
print(payload)
|
|
1323
|
+
return _strict_exit_with_advisory(
|
|
1324
|
+
blocking_passed=passed,
|
|
1325
|
+
advisory_present=advisory_findings_present(json.loads(payload)),
|
|
1326
|
+
strict=strict,
|
|
1327
|
+
)
|
|
1328
|
+
argv, only = _split_check_flag(argv)
|
|
1329
|
+
# --check <name> may name a standalone; route via subprocess when so.
|
|
1330
|
+
if only is not None:
|
|
1331
|
+
try:
|
|
1332
|
+
canonical, is_standalone = _resolve_validator(only)
|
|
1333
|
+
except ValueError as exc:
|
|
1334
|
+
sys.stderr.write(f"{exc}\n")
|
|
1335
|
+
return EXIT_FAIL
|
|
1336
|
+
if is_standalone:
|
|
1337
|
+
root = Path(argv[1]) if len(argv) >= 2 else Path.cwd()
|
|
1338
|
+
passed, output = _run_standalone(canonical, root)
|
|
1339
|
+
print(output)
|
|
1340
|
+
return _gate_exit(passed, strict=strict)
|
|
1341
|
+
only = canonical
|
|
1342
|
+
pre_content: str | None = None
|
|
1343
|
+
if len(argv) >= 2 and argv[1] == "--hook":
|
|
1344
|
+
# Harness-dispatched hook mode: parse tool-input JSON from stdin.
|
|
1345
|
+
content, path, pre_content = _read_tool_input_from_stdin()
|
|
1346
|
+
# No resolvable target path: an empty or malformed payload (no
|
|
1347
|
+
# `tool_input.file_path`) carries no write to gate. Fail open rather
|
|
1348
|
+
# than run matchers against a path-less body — a hook invocation we
|
|
1349
|
+
# cannot attribute to a file is not a write the gate should block.
|
|
1350
|
+
if path is None:
|
|
1351
|
+
return _silent_pass(None)
|
|
1352
|
+
scopes = _resolve_scopes()
|
|
1353
|
+
# Scope-aware short-circuit: when the write target lives outside the
|
|
1354
|
+
# configured conformity scope, return a silent pass-through report
|
|
1355
|
+
# so the matchers do not block writes against unrelated workspaces.
|
|
1356
|
+
if not _path_in_any_scope(path, scopes):
|
|
1357
|
+
return _silent_pass(path)
|
|
1358
|
+
# Harness runtime-state short-circuit: the harness's own ``projects/``
|
|
1359
|
+
# (per-project state incl. project memory) and ``memory/`` (global
|
|
1360
|
+
# memory) subtrees are operator/harness-owned state, not apothem-managed
|
|
1361
|
+
# config. Skip the matcher chain so the operator's memory writes ---
|
|
1362
|
+
# provenance-less and frontmatter-less by the auto-memory convention ---
|
|
1363
|
+
# are not fail-closed.
|
|
1364
|
+
if _is_harness_state_path(path, scopes):
|
|
1365
|
+
return _silent_pass(path)
|
|
1366
|
+
else:
|
|
1367
|
+
content, path = _read_cli_input(argv)
|
|
1368
|
+
try:
|
|
1369
|
+
if pre_content is not None:
|
|
1370
|
+
report = _orchestrator_diff_report(pre_content, content, path, only)
|
|
1371
|
+
else:
|
|
1372
|
+
report = run_orchestrator(content, path, only=only)
|
|
1373
|
+
except ValueError as exc:
|
|
1374
|
+
sys.stderr.write(f"{exc}" + chr(10))
|
|
1375
|
+
return EXIT_FAIL
|
|
1376
|
+
print(report.to_json())
|
|
1377
|
+
if not report.passed:
|
|
1378
|
+
# Surface the findings on stderr so they are never silent. The harness
|
|
1379
|
+
# shows a PreToolUse hook's stderr to the operator; in advisory mode the
|
|
1380
|
+
# write proceeds, in strict mode the non-zero exit blocks it.
|
|
1381
|
+
sys.stderr.write(_findings_summary(report, strict=strict) + chr(10))
|
|
1382
|
+
return _gate_exit(report.passed, strict=strict)
|
|
1383
|
+
|
|
1384
|
+
|
|
1385
|
+
if __name__ == "__main__":
|
|
1386
|
+
sys.exit(main(sys.argv))
|