@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,291 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
"""Verify release workflows use OIDC trusted publishing.
|
|
4
|
+
|
|
5
|
+
Why this validator exists. The release-engineering contract retires
|
|
6
|
+
long-lived registry tokens (PYPI_API_TOKEN, NPM_TOKEN) in favour of
|
|
7
|
+
OIDC trusted publishing. Trusted publishing
|
|
8
|
+
eliminates an entire class of credential-exfiltration risk by binding
|
|
9
|
+
the publish step to a short-lived GitHub-issued OIDC token whose
|
|
10
|
+
audience is the registry. The mechanical floor: every release workflow
|
|
11
|
+
under ``.github/workflows/`` that publishes to PyPI or npm declares
|
|
12
|
+
``id-token: write`` permission, references no legacy secret token, and
|
|
13
|
+
(for npm) runs on the npm Trusted Publishing runtime floor: Node 24 and
|
|
14
|
+
npm CLI 11.5.1 or newer. npm now emits provenance automatically for
|
|
15
|
+
public packages published from public GitHub Actions workflows.
|
|
16
|
+
|
|
17
|
+
Scope. The validator walks ``<root>/.github/workflows/`` and inspects
|
|
18
|
+
filenames matching ``*publish*py*.yml`` (PyPI release workflows) and
|
|
19
|
+
``*publish*npm*.yml`` or ``*packaging*npm*.yml`` (npm release
|
|
20
|
+
workflows). Each detected workflow is parsed via ``yaml.safe_load``
|
|
21
|
+
when PyYAML is available; otherwise the validator falls back to
|
|
22
|
+
permissive line-oriented string parsing so it remains operable
|
|
23
|
+
without extra packages on the conformity-gate execution path.
|
|
24
|
+
|
|
25
|
+
Workflow-absent tolerance. When neither workflow class exists under
|
|
26
|
+
``.github/workflows/``, the validator exits 0 with the informational
|
|
27
|
+
field ``not-yet-materialised: true``. Before publishing workflows are
|
|
28
|
+
materialized, the matcher is advisory and never blocks the gate.
|
|
29
|
+
|
|
30
|
+
Drift classes.
|
|
31
|
+
|
|
32
|
+
- ``missing-id-token-permission`` — neither the top-level nor any
|
|
33
|
+
job-level ``permissions:`` block declares ``id-token: write``.
|
|
34
|
+
- ``action-version-too-old`` — ``pypa/gh-action-pypi-publish`` is
|
|
35
|
+
pinned below v1.10; OIDC trusted publishing support landed at v1.10.
|
|
36
|
+
- ``legacy-token-secret-reference`` — a reference to
|
|
37
|
+
``secrets.PYPI_API_TOKEN`` or ``secrets.NPM_TOKEN`` (or the bare
|
|
38
|
+
``PYPI_API_TOKEN`` / ``NPM_TOKEN`` env-var name in a workflow env
|
|
39
|
+
block) survives in the workflow.
|
|
40
|
+
- ``npm-runtime-too-old`` — the npm workflow does not select Node 24 or
|
|
41
|
+
a newer major Node line.
|
|
42
|
+
- ``npm-cli-floor-absent`` — the npm workflow does not pin npm CLI
|
|
43
|
+
11.5.1+ before publishing.
|
|
44
|
+
|
|
45
|
+
Exit semantics. Exits 0 when no drift is detected (including the
|
|
46
|
+
workflow-absent informational case). Exits 2 when any drift class
|
|
47
|
+
fires.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
from __future__ import annotations
|
|
51
|
+
|
|
52
|
+
import json
|
|
53
|
+
import re
|
|
54
|
+
import sys
|
|
55
|
+
from dataclasses import asdict, dataclass, field
|
|
56
|
+
from pathlib import Path
|
|
57
|
+
from typing import Final
|
|
58
|
+
|
|
59
|
+
GREP_NAME: Final[str] = "oidc-trusted-publishing-grep"
|
|
60
|
+
RULE_ANCHOR: Final[str] = "OIDC trusted publishing"
|
|
61
|
+
|
|
62
|
+
EXIT_PASS: Final[int] = 0
|
|
63
|
+
EXIT_FAIL: Final[int] = 2
|
|
64
|
+
|
|
65
|
+
# Minimum pypa/gh-action-pypi-publish version with OIDC trusted-publishing
|
|
66
|
+
# support per the upstream changelog.
|
|
67
|
+
MIN_PYPI_ACTION_MAJOR: Final[int] = 1
|
|
68
|
+
MIN_PYPI_ACTION_MINOR: Final[int] = 10
|
|
69
|
+
|
|
70
|
+
# Workflow filename predicates. We match permissively so renamed but
|
|
71
|
+
# semantically equivalent filenames remain in scope.
|
|
72
|
+
_PYPI_WORKFLOW_RE: Final[re.Pattern[str]] = re.compile(r"(?i)publish.*py.*\.ya?ml$")
|
|
73
|
+
_NPM_WORKFLOW_RE: Final[re.Pattern[str]] = re.compile(
|
|
74
|
+
r"(?i)(?:publish|packaging).*npm.*\.ya?ml$"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Detection patterns operating on raw workflow text. These are
|
|
78
|
+
# deliberately substring-based so the validator works without PyYAML.
|
|
79
|
+
_ID_TOKEN_WRITE_RE: Final[re.Pattern[str]] = re.compile(r"id-token\s*:\s*write")
|
|
80
|
+
_PYPI_ACTION_USES_RE: Final[re.Pattern[str]] = re.compile(
|
|
81
|
+
r"pypa/gh-action-pypi-publish@v?(\d+)\.(\d+)(?:\.\d+)?"
|
|
82
|
+
)
|
|
83
|
+
_PYPI_TOKEN_REF_RE: Final[re.Pattern[str]] = re.compile(r"PYPI_API_TOKEN")
|
|
84
|
+
_NPM_TOKEN_REF_RE: Final[re.Pattern[str]] = re.compile(r"NPM_TOKEN")
|
|
85
|
+
_NPM_PUBLISH_RE: Final[re.Pattern[str]] = re.compile(r"npm\s+publish")
|
|
86
|
+
_NPM_NODE_BASELINE_RE: Final[re.Pattern[str]] = re.compile(
|
|
87
|
+
r"node-version\s*:\s*[\"']?(?:24|2[5-9]|[3-9]\d)(?:[\"']|\b)"
|
|
88
|
+
)
|
|
89
|
+
_NPM_CLI_BASELINE_RE: Final[re.Pattern[str]] = re.compile(
|
|
90
|
+
r"npm@(?:11\.(?:[5-9]|\d{2,})(?:\.\d+)?|1[2-9](?:\.\d+){0,2}|[2-9]\d(?:\.\d+){0,2})"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class Finding:
|
|
96
|
+
"""One drift occurrence in a release workflow."""
|
|
97
|
+
|
|
98
|
+
workflow: str
|
|
99
|
+
drift_class: str
|
|
100
|
+
detail: str
|
|
101
|
+
rule: str = RULE_ANCHOR
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(frozen=True)
|
|
105
|
+
class GrepResult:
|
|
106
|
+
"""Aggregated walk result over the release workflows under root."""
|
|
107
|
+
|
|
108
|
+
grep: str
|
|
109
|
+
root: str
|
|
110
|
+
workflows_inspected: list[str]
|
|
111
|
+
not_yet_materialised: bool
|
|
112
|
+
passed: bool
|
|
113
|
+
findings: list[Finding] = field(default_factory=list)
|
|
114
|
+
|
|
115
|
+
def to_json(self) -> str:
|
|
116
|
+
payload = {
|
|
117
|
+
"grep": self.grep,
|
|
118
|
+
"root": self.root,
|
|
119
|
+
"workflows-inspected": self.workflows_inspected,
|
|
120
|
+
"not-yet-materialised": self.not_yet_materialised,
|
|
121
|
+
"passed": self.passed,
|
|
122
|
+
"findings": [asdict(f) for f in self.findings],
|
|
123
|
+
}
|
|
124
|
+
return json.dumps(payload, indent=2)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _check_pypi_workflow(workflow: Path, text: str) -> list[Finding]:
|
|
128
|
+
"""Inspect a PyPI release workflow for trusted-publishing drift."""
|
|
129
|
+
findings: list[Finding] = []
|
|
130
|
+
name = workflow.name
|
|
131
|
+
if not _ID_TOKEN_WRITE_RE.search(text):
|
|
132
|
+
findings.append(
|
|
133
|
+
Finding(
|
|
134
|
+
workflow=name,
|
|
135
|
+
drift_class="missing-id-token-permission",
|
|
136
|
+
detail=(
|
|
137
|
+
"no `id-token: write` permission block; OIDC trusted "
|
|
138
|
+
"publishing requires the workflow (or its publish job) "
|
|
139
|
+
"to request the id-token scope"
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
for match in _PYPI_ACTION_USES_RE.finditer(text):
|
|
144
|
+
major = int(match.group(1))
|
|
145
|
+
minor = int(match.group(2))
|
|
146
|
+
too_old = (major, minor) < (MIN_PYPI_ACTION_MAJOR, MIN_PYPI_ACTION_MINOR)
|
|
147
|
+
if too_old:
|
|
148
|
+
findings.append(
|
|
149
|
+
Finding(
|
|
150
|
+
workflow=name,
|
|
151
|
+
drift_class="action-version-too-old",
|
|
152
|
+
detail=(
|
|
153
|
+
f"pypa/gh-action-pypi-publish pinned at v{major}."
|
|
154
|
+
f"{minor}; OIDC trusted publishing requires v"
|
|
155
|
+
f"{MIN_PYPI_ACTION_MAJOR}."
|
|
156
|
+
f"{MIN_PYPI_ACTION_MINOR} or higher"
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
if _PYPI_TOKEN_REF_RE.search(text):
|
|
161
|
+
findings.append(
|
|
162
|
+
Finding(
|
|
163
|
+
workflow=name,
|
|
164
|
+
drift_class="legacy-token-secret-reference",
|
|
165
|
+
detail=(
|
|
166
|
+
"PYPI_API_TOKEN reference survives; OIDC trusted "
|
|
167
|
+
"publishing retires the long-lived token"
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
return findings
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _check_npm_workflow(workflow: Path, text: str) -> list[Finding]:
|
|
175
|
+
"""Inspect an npm release workflow for trusted-publishing drift."""
|
|
176
|
+
findings: list[Finding] = []
|
|
177
|
+
name = workflow.name
|
|
178
|
+
if not _ID_TOKEN_WRITE_RE.search(text):
|
|
179
|
+
findings.append(
|
|
180
|
+
Finding(
|
|
181
|
+
workflow=name,
|
|
182
|
+
drift_class="missing-id-token-permission",
|
|
183
|
+
detail=(
|
|
184
|
+
"no `id-token: write` permission block; OIDC trusted "
|
|
185
|
+
"publishing requires the workflow (or its publish job) "
|
|
186
|
+
"to request the id-token scope"
|
|
187
|
+
),
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
if _NPM_TOKEN_REF_RE.search(text):
|
|
191
|
+
findings.append(
|
|
192
|
+
Finding(
|
|
193
|
+
workflow=name,
|
|
194
|
+
drift_class="legacy-token-secret-reference",
|
|
195
|
+
detail=(
|
|
196
|
+
"NPM_TOKEN reference survives; OIDC trusted publishing "
|
|
197
|
+
"retires the long-lived token"
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
if _NPM_PUBLISH_RE.search(text) and not _NPM_NODE_BASELINE_RE.search(text):
|
|
202
|
+
findings.append(
|
|
203
|
+
Finding(
|
|
204
|
+
workflow=name,
|
|
205
|
+
drift_class="npm-runtime-too-old",
|
|
206
|
+
detail=(
|
|
207
|
+
"npm Trusted Publishing requires Node 22.14.0+; the "
|
|
208
|
+
"canonical workflow floor is Node 24 for a stable OIDC "
|
|
209
|
+
"runtime"
|
|
210
|
+
),
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
if _NPM_PUBLISH_RE.search(text) and not _NPM_CLI_BASELINE_RE.search(text):
|
|
214
|
+
findings.append(
|
|
215
|
+
Finding(
|
|
216
|
+
workflow=name,
|
|
217
|
+
drift_class="npm-cli-floor-absent",
|
|
218
|
+
detail=(
|
|
219
|
+
"npm Trusted Publishing requires npm CLI 11.5.1+; pin "
|
|
220
|
+
"npm@11.5.1 or newer before running `npm publish`"
|
|
221
|
+
),
|
|
222
|
+
)
|
|
223
|
+
)
|
|
224
|
+
return findings
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def check(root: Path) -> GrepResult:
|
|
228
|
+
"""Walk release workflows under root; aggregate trusted-publishing drift."""
|
|
229
|
+
workflows_dir = root / ".github" / "workflows"
|
|
230
|
+
findings: list[Finding] = []
|
|
231
|
+
inspected: list[str] = []
|
|
232
|
+
if not workflows_dir.is_dir():
|
|
233
|
+
return GrepResult(
|
|
234
|
+
grep=GREP_NAME,
|
|
235
|
+
root=str(root),
|
|
236
|
+
workflows_inspected=[],
|
|
237
|
+
not_yet_materialised=True,
|
|
238
|
+
passed=True,
|
|
239
|
+
findings=[],
|
|
240
|
+
)
|
|
241
|
+
pypi_workflows: list[Path] = []
|
|
242
|
+
npm_workflows: list[Path] = []
|
|
243
|
+
for candidate in sorted(workflows_dir.iterdir()):
|
|
244
|
+
if not candidate.is_file():
|
|
245
|
+
continue
|
|
246
|
+
if _PYPI_WORKFLOW_RE.search(candidate.name):
|
|
247
|
+
pypi_workflows.append(candidate)
|
|
248
|
+
elif _NPM_WORKFLOW_RE.search(candidate.name):
|
|
249
|
+
npm_workflows.append(candidate)
|
|
250
|
+
if not pypi_workflows and not npm_workflows:
|
|
251
|
+
return GrepResult(
|
|
252
|
+
grep=GREP_NAME,
|
|
253
|
+
root=str(root),
|
|
254
|
+
workflows_inspected=[],
|
|
255
|
+
not_yet_materialised=True,
|
|
256
|
+
passed=True,
|
|
257
|
+
findings=[],
|
|
258
|
+
)
|
|
259
|
+
for workflow in pypi_workflows:
|
|
260
|
+
text = workflow.read_text(encoding="utf-8")
|
|
261
|
+
inspected.append(workflow.name)
|
|
262
|
+
findings.extend(_check_pypi_workflow(workflow, text))
|
|
263
|
+
for workflow in npm_workflows:
|
|
264
|
+
text = workflow.read_text(encoding="utf-8")
|
|
265
|
+
inspected.append(workflow.name)
|
|
266
|
+
findings.extend(_check_npm_workflow(workflow, text))
|
|
267
|
+
return GrepResult(
|
|
268
|
+
grep=GREP_NAME,
|
|
269
|
+
root=str(root),
|
|
270
|
+
workflows_inspected=inspected,
|
|
271
|
+
not_yet_materialised=False,
|
|
272
|
+
passed=not findings,
|
|
273
|
+
findings=findings,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _read_input(argv: list[str]) -> Path:
|
|
278
|
+
if len(argv) >= 2:
|
|
279
|
+
return Path(argv[1])
|
|
280
|
+
return Path.cwd()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _main(argv: list[str]) -> int:
|
|
284
|
+
root = _read_input(argv)
|
|
285
|
+
result = check(root)
|
|
286
|
+
print(result.to_json())
|
|
287
|
+
return EXIT_PASS if result.passed else EXIT_FAIL
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
if __name__ == "__main__":
|
|
291
|
+
sys.exit(_main(sys.argv))
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
"""Enforce the per-option Recommended label-to-body bind (H6).
|
|
4
|
+
|
|
5
|
+
Why this enforcement exists. Option sets surfaced for operator decision
|
|
6
|
+
annotate the recommended path so the operator sees the agent's share of
|
|
7
|
+
evaluation. The canonical-channel rule §3 binds each option's label to its
|
|
8
|
+
body: an option whose body `recommendation:` value is exactly `recommended`
|
|
9
|
+
carries the canonical `(Recommended)` postfix on its own label; no option
|
|
10
|
+
carries the postfix without the matching body value; and a single-select
|
|
11
|
+
question recommends at most one option. The prior file-wide "any one marker
|
|
12
|
+
anywhere" check accepted a body-only or rationale-only recommendation — this
|
|
13
|
+
matcher tightens that to a per-option bind so the marker lands on the right
|
|
14
|
+
label.
|
|
15
|
+
|
|
16
|
+
Canonical postfix. The canonical postfix string is capital `(Recommended)`
|
|
17
|
+
per `_spec/spec.md` §4.2 and `rules/interactive-questions-canonical-shapes.md`
|
|
18
|
+
§2.1; it is recognized case-correctly. The lowercase `(recommended)` form is a
|
|
19
|
+
banned variant and is itself a finding when it appears as a label postfix.
|
|
20
|
+
|
|
21
|
+
Detection strategy. An option is a label line — backtick form
|
|
22
|
+
``- `Label`:`` or YAML form ``label: Label`` — whose body (the lines up to the
|
|
23
|
+
next label) carries a `recommendation:` taxonomy value. For each option the
|
|
24
|
+
matcher computes two booleans (label-carries-canonical-postfix,
|
|
25
|
+
body-is-recommended) and flags every direction of the bind violation.
|
|
26
|
+
Cardinality is checked per invocation block (options grouped by the nearest
|
|
27
|
+
preceding invocation head — a prose channel mention or a canonical YAML
|
|
28
|
+
`question:` field); a non-`multiSelect: true` block with more than one
|
|
29
|
+
recommended option is a finding.
|
|
30
|
+
|
|
31
|
+
Narrative-marker leak. The `(Recommended)` (and the prose-and-document
|
|
32
|
+
`**Recommended**`) marker lives SOLELY in the option label per
|
|
33
|
+
`rules/interactive-questions-canonical-shapes.md` §2.1; the body carries
|
|
34
|
+
verifiable concrete-driver evidence instead. A marker surfacing on a body
|
|
35
|
+
segment line (``rationale:`` / ``recommendation:`` / ``default-pointer:``) is a
|
|
36
|
+
`narrative-marker-leak` finding. The label's own legitimate postfix is never a
|
|
37
|
+
leak — a label line carries no body-segment lead-in token.
|
|
38
|
+
|
|
39
|
+
Definitional exclusion. The rule files that *define* the option-annotation
|
|
40
|
+
convention quote non-canonical and deliberately-bound-violating examples as
|
|
41
|
+
specification material; sweeping them would flag their definitional examples.
|
|
42
|
+
Those files are excluded by path, mirroring the §3 exclusion zones of
|
|
43
|
+
`rules/interactive-questions-sweep-matchers.md`.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
import re
|
|
49
|
+
import sys
|
|
50
|
+
from dataclasses import dataclass
|
|
51
|
+
from pathlib import Path
|
|
52
|
+
from typing import Final
|
|
53
|
+
|
|
54
|
+
from apothem.conformity._grep_base import GrepResult, run_grep
|
|
55
|
+
|
|
56
|
+
# Recommendation taxonomy value on a body segment, per the canonical channel §4.
|
|
57
|
+
_RECOMMENDATION_VALUE_RE: Final[re.Pattern[str]] = re.compile(
|
|
58
|
+
r"recommendation:\s*(?P<value>recommended|acceptable|discouraged|destructive-no-default)\b"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Option label shapes. Backtick form ``- `Label`:`` (command / fallback prose)
|
|
62
|
+
# and YAML form ``label: Label`` / ``- label: Label`` (fenced worked examples).
|
|
63
|
+
_BACKTICK_LABEL_RE: Final[re.Pattern[str]] = re.compile(
|
|
64
|
+
r"^\s*>?\s*-\s*`(?P<label>[^`]+)`\s*:"
|
|
65
|
+
)
|
|
66
|
+
_YAML_LABEL_RE: Final[re.Pattern[str]] = re.compile(
|
|
67
|
+
r"^\s*-?\s*label:\s*(?P<label>\S.*?)\s*$"
|
|
68
|
+
)
|
|
69
|
+
_BOLD_LABEL_RE: Final[re.Pattern[str]] = re.compile(
|
|
70
|
+
r"^\s*>?\s*-\s*\*\*(?P<label>[^*]+)\*\*\s*:"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Canonical postfix is capital `(Recommended)`, case-correct. The lowercase
|
|
74
|
+
# form is the banned variant.
|
|
75
|
+
_CANONICAL_POSTFIX_RE: Final[re.Pattern[str]] = re.compile(r"\(Recommended\)\s*$")
|
|
76
|
+
_LOWERCASE_POSTFIX_RE: Final[re.Pattern[str]] = re.compile(r"\(recommended\)\s*$")
|
|
77
|
+
|
|
78
|
+
# The canonical recommended marker in either of its two surface forms: the
|
|
79
|
+
# structured-inquiry label postfix `(Recommended)` and the prose-and-document
|
|
80
|
+
# inline marker `**Recommended**`. The marker lives SOLELY in the option label
|
|
81
|
+
# per `rules/interactive-questions-canonical-shapes.md` §2.1; a marker surfacing
|
|
82
|
+
# inside a body/narrative segment is a `narrative-marker-leak`.
|
|
83
|
+
_BODY_MARKER_RE: Final[re.Pattern[str]] = re.compile(
|
|
84
|
+
r"\(Recommended\)|\*\*Recommended\*\*"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Body-segment lead-in tokens. A leak is flagged only when the marker rides on
|
|
88
|
+
# one of the three canonical body segments, so a stray prose line near an option
|
|
89
|
+
# is not mistaken for a narrative-embedded marker.
|
|
90
|
+
_BODY_SEGMENT_RE: Final[re.Pattern[str]] = re.compile(
|
|
91
|
+
r"(?:rationale|recommendation|default-pointer)\s*:"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Invocation-head markers used only to bound cardinality blocks. Over-
|
|
95
|
+
# segmentation (a prose mention of the channel) is conservative — it only
|
|
96
|
+
# shrinks a block, which can never manufacture a false cardinality finding.
|
|
97
|
+
_INVOCATION_HEAD_RE: Final[re.Pattern[str]] = re.compile(
|
|
98
|
+
r"structured[- ]inquiry\s*:\s*question"
|
|
99
|
+
r"|Invoke the structured-inquiry channel"
|
|
100
|
+
r"|structured inquiry:"
|
|
101
|
+
# Canonical YAML invocation head: a `question:` field at line start opens a
|
|
102
|
+
# structured-inquiry block, so each YAML question bounds its own cardinality
|
|
103
|
+
# block. This stops two independent single-select YAML option sets from
|
|
104
|
+
# being read as one block, and a multiSelect:true block from masking a
|
|
105
|
+
# sibling single-select block's cardinality.
|
|
106
|
+
r"|^\s*question:\s*\S",
|
|
107
|
+
re.IGNORECASE,
|
|
108
|
+
)
|
|
109
|
+
_MULTISELECT_TRUE_RE: Final[re.Pattern[str]] = re.compile(r"multiSelect:\s*true\b")
|
|
110
|
+
|
|
111
|
+
# Body window cap: an option's body is the lines from its label up to the next
|
|
112
|
+
# label line, bounded by this many lines so a runaway scan cannot bind an
|
|
113
|
+
# option to a distant unrelated recommendation value.
|
|
114
|
+
_BODY_WINDOW_LINES: Final[int] = 14
|
|
115
|
+
|
|
116
|
+
# Rule / skill files that DEFINE the option-annotation convention. Their
|
|
117
|
+
# worked examples are specification material, not live invocations.
|
|
118
|
+
_EXCLUDED_BASENAMES: Final[frozenset[str]] = frozenset(
|
|
119
|
+
{
|
|
120
|
+
"interactive-questions.md",
|
|
121
|
+
"interactive-questions-canonical-shapes.md",
|
|
122
|
+
"interactive-questions-sweep-matchers.md",
|
|
123
|
+
"option-annotation.md",
|
|
124
|
+
"option-annotation-form.md",
|
|
125
|
+
"operational-mandates.md",
|
|
126
|
+
"master_template.md",
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
# Full relative-tail exclusions for files whose basename is generic.
|
|
130
|
+
_EXCLUDED_TAILS: Final[tuple[str, ...]] = (
|
|
131
|
+
"skills/ecosystem-audit/SKILL.md",
|
|
132
|
+
"skills/plan-suite/SKILL.md",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
GREP_NAME: Final[str] = "option-annotation-grep"
|
|
136
|
+
RULE_ANCHOR: Final[str] = "M7 option-annotation (H6 per-option bind)"
|
|
137
|
+
EXIT_PASS: Final[int] = 0
|
|
138
|
+
EXIT_FAIL: Final[int] = 2
|
|
139
|
+
STDIN_FLAG: Final[str] = "--stdin"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass(frozen=True)
|
|
143
|
+
class Finding:
|
|
144
|
+
"""One per-option (or per-block) bind violation."""
|
|
145
|
+
|
|
146
|
+
line: int
|
|
147
|
+
kind: str
|
|
148
|
+
label: str
|
|
149
|
+
detail: str
|
|
150
|
+
rule: str = RULE_ANCHOR
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass(frozen=True)
|
|
154
|
+
class _Option:
|
|
155
|
+
line: int # 1-indexed label line
|
|
156
|
+
label: str
|
|
157
|
+
has_canonical_postfix: bool
|
|
158
|
+
has_lowercase_postfix: bool
|
|
159
|
+
body_recommended: bool
|
|
160
|
+
# 1-indexed line of a marker leak inside a body segment, or None.
|
|
161
|
+
narrative_marker_leak_line: int | None = None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _is_excluded(path: Path | None) -> bool:
|
|
165
|
+
if path is None:
|
|
166
|
+
return False
|
|
167
|
+
if path.name in _EXCLUDED_BASENAMES:
|
|
168
|
+
return True
|
|
169
|
+
posix = path.as_posix()
|
|
170
|
+
return any(posix.endswith(tail) for tail in _EXCLUDED_TAILS)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _label_line(line: str) -> str | None:
|
|
174
|
+
"""Return the option label text if *line* is an option label, else None."""
|
|
175
|
+
for pattern in (_BACKTICK_LABEL_RE, _BOLD_LABEL_RE, _YAML_LABEL_RE):
|
|
176
|
+
m = pattern.match(line)
|
|
177
|
+
if m:
|
|
178
|
+
return m.group("label").strip()
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _parse_options(lines: list[str]) -> list[_Option]:
|
|
183
|
+
"""Extract every annotated option (a label whose body carries a value)."""
|
|
184
|
+
# Pre-compute the line index of every label so a body scan can stop at the
|
|
185
|
+
# next label rather than bleeding into the following option.
|
|
186
|
+
label_indices = [i for i, ln in enumerate(lines) if _label_line(ln) is not None]
|
|
187
|
+
label_index_set = set(label_indices)
|
|
188
|
+
options: list[_Option] = []
|
|
189
|
+
for i in label_indices:
|
|
190
|
+
label = _label_line(lines[i])
|
|
191
|
+
if label is None: # pragma: no cover - guarded by label_indices
|
|
192
|
+
continue
|
|
193
|
+
window_end = min(i + 1 + _BODY_WINDOW_LINES, len(lines))
|
|
194
|
+
body_value: str | None = None
|
|
195
|
+
leak_line: int | None = None
|
|
196
|
+
for j in range(i + 1, window_end):
|
|
197
|
+
if j in label_index_set:
|
|
198
|
+
break # body ends at the next option label
|
|
199
|
+
if body_value is None:
|
|
200
|
+
value_match = _RECOMMENDATION_VALUE_RE.search(lines[j])
|
|
201
|
+
if value_match:
|
|
202
|
+
body_value = value_match.group("value")
|
|
203
|
+
# Marker-leak detection runs across the WHOLE body window, not just
|
|
204
|
+
# up to the recommendation value, so a marker riding a later
|
|
205
|
+
# `default-pointer:` segment is caught too. A leak is a marker on a
|
|
206
|
+
# canonical body segment line.
|
|
207
|
+
if (
|
|
208
|
+
leak_line is None
|
|
209
|
+
and _BODY_SEGMENT_RE.search(lines[j])
|
|
210
|
+
and _BODY_MARKER_RE.search(lines[j])
|
|
211
|
+
):
|
|
212
|
+
leak_line = j + 1
|
|
213
|
+
if body_value is None:
|
|
214
|
+
# No recommendation value in this label's body: not an annotated
|
|
215
|
+
# option subject to the bind (e.g., a prose list item). Skip.
|
|
216
|
+
continue
|
|
217
|
+
options.append(
|
|
218
|
+
_Option(
|
|
219
|
+
line=i + 1,
|
|
220
|
+
label=label,
|
|
221
|
+
has_canonical_postfix=_CANONICAL_POSTFIX_RE.search(label) is not None,
|
|
222
|
+
has_lowercase_postfix=_LOWERCASE_POSTFIX_RE.search(label) is not None,
|
|
223
|
+
body_recommended=body_value == "recommended",
|
|
224
|
+
narrative_marker_leak_line=leak_line,
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
return options
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _block_of(line_idx: int, head_indices: list[int]) -> int:
|
|
231
|
+
"""Return the index of the nearest preceding invocation head (or -1)."""
|
|
232
|
+
block = -1
|
|
233
|
+
for k, head in enumerate(head_indices):
|
|
234
|
+
if head <= line_idx:
|
|
235
|
+
block = k
|
|
236
|
+
else:
|
|
237
|
+
break
|
|
238
|
+
return block
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def check(content: str, path: Path | None = None) -> GrepResult:
|
|
242
|
+
"""Scan *content* for per-option Recommended-bind violations.
|
|
243
|
+
|
|
244
|
+
Pre-conditions: *content* is the artifact body about to be emitted; *path*
|
|
245
|
+
is its destination (used only for definitional-file exclusion).
|
|
246
|
+
Post-conditions: ``result.passed`` is True when every annotated option
|
|
247
|
+
satisfies the bidirectional bind (capital canonical postfix iff body value
|
|
248
|
+
is ``recommended``), no label carries the lowercase variant, and no
|
|
249
|
+
single-select block recommends more than one option.
|
|
250
|
+
"""
|
|
251
|
+
if _is_excluded(path):
|
|
252
|
+
return GrepResult(grep=GREP_NAME, path=_path_str(path), passed=True)
|
|
253
|
+
|
|
254
|
+
lines = content.splitlines()
|
|
255
|
+
options = _parse_options(lines)
|
|
256
|
+
findings: list[Finding] = []
|
|
257
|
+
|
|
258
|
+
for opt in options:
|
|
259
|
+
if opt.has_lowercase_postfix:
|
|
260
|
+
findings.append(
|
|
261
|
+
Finding(
|
|
262
|
+
line=opt.line,
|
|
263
|
+
kind="non-canonical-postfix-case",
|
|
264
|
+
label=opt.label,
|
|
265
|
+
detail="label uses lowercase (recommended); canonical form is (Recommended)",
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
if (
|
|
269
|
+
opt.body_recommended
|
|
270
|
+
and not opt.has_canonical_postfix
|
|
271
|
+
and not opt.has_lowercase_postfix
|
|
272
|
+
):
|
|
273
|
+
findings.append(
|
|
274
|
+
Finding(
|
|
275
|
+
line=opt.line,
|
|
276
|
+
kind="missing-canonical-postfix",
|
|
277
|
+
label=opt.label,
|
|
278
|
+
detail="body recommendation: recommended but label lacks the (Recommended) postfix",
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
if opt.has_canonical_postfix and not opt.body_recommended:
|
|
282
|
+
findings.append(
|
|
283
|
+
Finding(
|
|
284
|
+
line=opt.line,
|
|
285
|
+
kind="spurious-postfix",
|
|
286
|
+
label=opt.label,
|
|
287
|
+
detail="label carries (Recommended) but body recommendation is not recommended",
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
if opt.narrative_marker_leak_line is not None:
|
|
291
|
+
findings.append(
|
|
292
|
+
Finding(
|
|
293
|
+
line=opt.narrative_marker_leak_line,
|
|
294
|
+
kind="narrative-marker-leak",
|
|
295
|
+
label=opt.label,
|
|
296
|
+
detail=(
|
|
297
|
+
"the (Recommended) / **Recommended** marker appears in a "
|
|
298
|
+
"body segment (rationale:/recommendation:/default-pointer:); "
|
|
299
|
+
"it lives solely in the option label"
|
|
300
|
+
),
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Cardinality: per invocation block, a non-multiSelect-true question carries
|
|
305
|
+
# at most one recommended option.
|
|
306
|
+
head_indices = [i for i, ln in enumerate(lines) if _INVOCATION_HEAD_RE.search(ln)]
|
|
307
|
+
multiselect_blocks: set[int] = set()
|
|
308
|
+
if head_indices:
|
|
309
|
+
for i, ln in enumerate(lines):
|
|
310
|
+
if _MULTISELECT_TRUE_RE.search(ln):
|
|
311
|
+
multiselect_blocks.add(_block_of(i, head_indices))
|
|
312
|
+
else:
|
|
313
|
+
# No invocation head: treat the whole file as one implicit block.
|
|
314
|
+
if any(_MULTISELECT_TRUE_RE.search(ln) for ln in lines):
|
|
315
|
+
multiselect_blocks.add(0)
|
|
316
|
+
block_recommended: dict[int, list[_Option]] = {}
|
|
317
|
+
for opt in options:
|
|
318
|
+
if not opt.body_recommended:
|
|
319
|
+
continue
|
|
320
|
+
block = _block_of(opt.line - 1, head_indices) if head_indices else 0
|
|
321
|
+
block_recommended.setdefault(block, []).append(opt)
|
|
322
|
+
for block, recs in block_recommended.items():
|
|
323
|
+
if block in multiselect_blocks:
|
|
324
|
+
continue
|
|
325
|
+
if len(recs) > 1:
|
|
326
|
+
findings.append(
|
|
327
|
+
Finding(
|
|
328
|
+
line=recs[1].line,
|
|
329
|
+
kind="single-select-multi-recommended",
|
|
330
|
+
label=recs[1].label,
|
|
331
|
+
detail=(
|
|
332
|
+
f"single-select block recommends {len(recs)} options; "
|
|
333
|
+
"at most one is permitted"
|
|
334
|
+
),
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
findings.sort(key=lambda f: (f.line, f.kind))
|
|
339
|
+
return GrepResult(
|
|
340
|
+
grep=GREP_NAME,
|
|
341
|
+
path=_path_str(path),
|
|
342
|
+
passed=not findings,
|
|
343
|
+
findings=findings,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _path_str(path: Path | None) -> str | None:
|
|
348
|
+
return str(path) if path is not None else None
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
if __name__ == "__main__":
|
|
352
|
+
sys.exit(run_grep(check, sys.argv))
|