@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,607 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
"""Build a node-edge graph of cross-artifact references in the ecosystem.
|
|
4
|
+
|
|
5
|
+
Why this graph exists. Agents call skills. Commands invoke agents and
|
|
6
|
+
reference skills. Hooks bind to harness events. Output-styles route work
|
|
7
|
+
to specific surfaces. The textual cross-references between these
|
|
8
|
+
artifacts form a directed graph; without an explicit map of that graph,
|
|
9
|
+
orphan artifacts (no inbound edges) and dangling references (edges to
|
|
10
|
+
non-existent targets) stay invisible until they break at runtime. This
|
|
11
|
+
tool emits ``capability-graph.json`` once; downstream graph analysis
|
|
12
|
+
(orphan detection, dangling-reference detection) reads it.
|
|
13
|
+
|
|
14
|
+
What the graph captures. Nodes correspond to addressable ecosystem
|
|
15
|
+
units, keyed by their content-root-relative path: agents (one per
|
|
16
|
+
``agents/<name>.md``), commands (one per ``commands/*.md``), skills (one per
|
|
17
|
+
``skills/*/``), hooks (one per hook script under ``hooks/``), output-styles
|
|
18
|
+
(one per ``output-styles/*``), statuslines, and the harness events
|
|
19
|
+
themselves (SessionStart,
|
|
20
|
+
PreToolUse, PostToolUse, UserPromptSubmit, Notification, Stop,
|
|
21
|
+
PreCompact, PostCompact). Edges record observed textual references:
|
|
22
|
+
``invokes`` (agent / command body cites another agent or command),
|
|
23
|
+
``references`` (any artifact mentions another by name in prose),
|
|
24
|
+
``binds-to`` (hook artifact bound to a harness event in the engine hook
|
|
25
|
+
config), ``imports`` (Python module imports another).
|
|
26
|
+
|
|
27
|
+
Detection strategy. The tool first registers every node from the
|
|
28
|
+
inventory. It then scans the text of every node-bearing source file for
|
|
29
|
+
mentions of every other node's identifier. A textual match within an
|
|
30
|
+
artifact body emits an edge from the source artifact to the target
|
|
31
|
+
node. Hook bindings come from the ``hooks`` block of the engine
|
|
32
|
+
``hooks/hooks.json`` config rather than text scanning.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import argparse
|
|
38
|
+
import json
|
|
39
|
+
import re
|
|
40
|
+
import sys
|
|
41
|
+
from dataclasses import dataclass, field
|
|
42
|
+
from datetime import datetime, timezone
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
from typing import Final
|
|
45
|
+
|
|
46
|
+
# Edge relation taxonomy. Each edge carries exactly one relation value.
|
|
47
|
+
RELATION_INVOKES: Final[str] = "invokes"
|
|
48
|
+
RELATION_REFERENCES: Final[str] = "references"
|
|
49
|
+
RELATION_BINDS_TO: Final[str] = "binds-to"
|
|
50
|
+
RELATION_IMPORTS: Final[str] = "imports"
|
|
51
|
+
|
|
52
|
+
# Node-kind taxonomy mirrors the inventory's ecosystem classes for the
|
|
53
|
+
# subset that participates in the capability graph.
|
|
54
|
+
KIND_AGENT: Final[str] = "agent"
|
|
55
|
+
KIND_COMMAND: Final[str] = "command"
|
|
56
|
+
KIND_SKILL: Final[str] = "skill"
|
|
57
|
+
KIND_HOOK: Final[str] = "hook"
|
|
58
|
+
KIND_OUTPUT_STYLE: Final[str] = "output-style"
|
|
59
|
+
KIND_STATUSLINE: Final[str] = "statusline"
|
|
60
|
+
KIND_HARNESS_EVENT: Final[str] = "harness-event"
|
|
61
|
+
KIND_RULE: Final[str] = "rule"
|
|
62
|
+
KIND_MCP: Final[str] = "mcp"
|
|
63
|
+
|
|
64
|
+
# Canonical Claude Code hook events. Treated as virtual nodes so hook
|
|
65
|
+
# bindings have valid targets even though no source file represents the
|
|
66
|
+
# event itself.
|
|
67
|
+
HARNESS_EVENTS: Final[tuple[str, ...]] = (
|
|
68
|
+
"SessionStart",
|
|
69
|
+
"UserPromptSubmit",
|
|
70
|
+
"PreToolUse",
|
|
71
|
+
"PostToolUse",
|
|
72
|
+
"Notification",
|
|
73
|
+
"Stop",
|
|
74
|
+
"SubagentStop",
|
|
75
|
+
"PreCompact",
|
|
76
|
+
"PostCompact",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Content-root-relative paths whose ``hooks`` block contributes binds-to
|
|
80
|
+
# edges. The committed ``hooks/hooks.json`` is the source-of-truth engine
|
|
81
|
+
# hook block the plugin tree and the per-harness settings templates render
|
|
82
|
+
# from; it lives inside the scanned content root, so its variable-prefixed
|
|
83
|
+
# script references resolve to bare hook node ids without crossing the root.
|
|
84
|
+
HOOK_CONFIG_RELPATHS: Final[tuple[str, ...]] = ("hooks/hooks.json",)
|
|
85
|
+
|
|
86
|
+
# Hook-artifact references inside a ``hooks.json`` command string carry a
|
|
87
|
+
# variable-prefixed absolute form (e.g.
|
|
88
|
+
# ``"${CLAUDE_PLUGIN_ROOT}/src/apothem/hooks/lib/bootstrap.sh"``). The capture
|
|
89
|
+
# group isolates the content-root-relative tail (``hooks/lib/bootstrap.sh``),
|
|
90
|
+
# which equals the hook node id the inventory assigns; the optional
|
|
91
|
+
# ``src/apothem/`` segment is consumed so the captured id never carries the
|
|
92
|
+
# content-root prefix. Message ``.md`` files count alongside the script
|
|
93
|
+
# extensions because the engine config binds them to context-emitting events.
|
|
94
|
+
_HOOK_REF_PATTERN: Final[re.Pattern[str]] = re.compile(
|
|
95
|
+
r"(?:src/apothem/)?(hooks/[\w./\-]+\.(?:ps1|sh|py|md))"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Filenames inside src/apothem/skills/ that mark the entry point.
|
|
99
|
+
SKILL_ENTRY_FILENAME: Final[str] = "SKILL.md"
|
|
100
|
+
|
|
101
|
+
# Filename suffix that marks a persistent agent (flat <name>.md under src/apothem/agents/).
|
|
102
|
+
AGENT_ENTRY_SUFFIX: Final[str] = ".md"
|
|
103
|
+
|
|
104
|
+
# Maximum bytes read from a source file when scanning for references.
|
|
105
|
+
# The reference scan only inspects body content; large files (rare in
|
|
106
|
+
# this corpus) are truncated to keep the scan bounded.
|
|
107
|
+
SCAN_MAX_BYTES: Final[int] = 256_000
|
|
108
|
+
|
|
109
|
+
# Hook-script extensions. Shell and PowerShell stubs reference their siblings
|
|
110
|
+
# by bare filename, not by the content-root-relative node id: a POSIX stub
|
|
111
|
+
# sources ``. lib/find-python.sh``, a PowerShell stub joins
|
|
112
|
+
# ``hooks\lib\find-python.ps1`` with backslashes, and sibling comments name the
|
|
113
|
+
# counterpart as ``find-pwsh.ps1``. None of those carry the forward-slash node
|
|
114
|
+
# id the reference scan otherwise matches, so script-to-script references
|
|
115
|
+
# resolve by unique basename instead (see ``scan_text_references``).
|
|
116
|
+
SCRIPT_SUFFIXES: Final[tuple[str, ...]] = (".sh", ".ps1")
|
|
117
|
+
|
|
118
|
+
EXIT_OK: Final[int] = 0
|
|
119
|
+
EXIT_ERROR: Final[int] = 1
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass(slots=True)
|
|
123
|
+
class Node:
|
|
124
|
+
"""A capability-graph node — one unit with an addressable identifier."""
|
|
125
|
+
|
|
126
|
+
id: str
|
|
127
|
+
kind: str
|
|
128
|
+
path: str | None # None for harness-event virtual nodes.
|
|
129
|
+
|
|
130
|
+
def to_json(self) -> dict[str, object]:
|
|
131
|
+
return {"id": self.id, "kind": self.kind, "path": self.path}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(slots=True)
|
|
135
|
+
class Edge:
|
|
136
|
+
"""A directed reference from one node to another."""
|
|
137
|
+
|
|
138
|
+
source: str
|
|
139
|
+
target: str
|
|
140
|
+
relation: str
|
|
141
|
+
evidence_path: str # Source-side path where the reference was observed.
|
|
142
|
+
|
|
143
|
+
def to_json(self) -> dict[str, object]:
|
|
144
|
+
return {
|
|
145
|
+
"source": self.source,
|
|
146
|
+
"target": self.target,
|
|
147
|
+
"relation": self.relation,
|
|
148
|
+
"evidence-path": self.evidence_path,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass(slots=True)
|
|
153
|
+
class Graph:
|
|
154
|
+
"""The full capability graph — nodes plus edges, deduplicated."""
|
|
155
|
+
|
|
156
|
+
nodes: list[Node] = field(default_factory=list)
|
|
157
|
+
edges: list[Edge] = field(default_factory=list)
|
|
158
|
+
edge_keys: set[tuple[str, str, str]] = field(default_factory=set)
|
|
159
|
+
|
|
160
|
+
def add_node(self, node: Node) -> None:
|
|
161
|
+
if not any(n.id == node.id and n.kind == node.kind for n in self.nodes):
|
|
162
|
+
self.nodes.append(node)
|
|
163
|
+
|
|
164
|
+
def add_edge(self, edge: Edge) -> None:
|
|
165
|
+
key = (edge.source, edge.target, edge.relation)
|
|
166
|
+
if key in self.edge_keys:
|
|
167
|
+
return
|
|
168
|
+
self.edge_keys.add(key)
|
|
169
|
+
self.edges.append(edge)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def load_inventory(path: Path) -> dict[str, object]:
|
|
173
|
+
"""Read inventory.json and return its parsed payload."""
|
|
174
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
175
|
+
payload: dict[str, object] = json.load(handle)
|
|
176
|
+
return payload
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def derive_node_id(file_class: str, relative_path: str) -> str | None:
|
|
180
|
+
"""Convert an inventory entry to its capability-graph node identifier.
|
|
181
|
+
|
|
182
|
+
Inventory paths are relative to the ecosystem content root (``src/apothem``
|
|
183
|
+
in this repo), so they carry bare top-level directory prefixes —
|
|
184
|
+
``agents/<name>.md``, ``commands/<name>.md``, ``skills/<name>/SKILL.md``,
|
|
185
|
+
``rules/<name>.md`` — matching the inventory's own ``classify_file``
|
|
186
|
+
keys. Agents, commands, output-styles, and rules drop that bare prefix and
|
|
187
|
+
the suffix; skills derive from the parent-directory name beneath
|
|
188
|
+
``skills/``; hooks, statuslines, and MCP files use the relative path
|
|
189
|
+
verbatim.
|
|
190
|
+
"""
|
|
191
|
+
if (
|
|
192
|
+
file_class == KIND_AGENT
|
|
193
|
+
and relative_path.startswith("agents/")
|
|
194
|
+
and relative_path.endswith(AGENT_ENTRY_SUFFIX)
|
|
195
|
+
):
|
|
196
|
+
return relative_path.removeprefix("agents/").removesuffix(AGENT_ENTRY_SUFFIX)
|
|
197
|
+
if file_class == KIND_COMMAND and relative_path.endswith(".md"):
|
|
198
|
+
return relative_path.removeprefix("commands/").removesuffix(".md")
|
|
199
|
+
if file_class == KIND_SKILL and relative_path.endswith(SKILL_ENTRY_FILENAME):
|
|
200
|
+
# The parent directory of SKILL.md identifies the skill.
|
|
201
|
+
parts = relative_path.split("/")
|
|
202
|
+
if len(parts) >= 3 and parts[0] == "skills":
|
|
203
|
+
return parts[1]
|
|
204
|
+
return None
|
|
205
|
+
if file_class == KIND_HOOK:
|
|
206
|
+
return relative_path
|
|
207
|
+
if file_class == KIND_OUTPUT_STYLE:
|
|
208
|
+
return (
|
|
209
|
+
relative_path.removeprefix("output-styles/")
|
|
210
|
+
.removesuffix(".md")
|
|
211
|
+
.removesuffix(".json")
|
|
212
|
+
)
|
|
213
|
+
if file_class == KIND_STATUSLINE:
|
|
214
|
+
return relative_path
|
|
215
|
+
if file_class == "docs" and relative_path.startswith("rules/"):
|
|
216
|
+
return relative_path.removeprefix("rules/").removesuffix(".md")
|
|
217
|
+
if file_class == KIND_MCP:
|
|
218
|
+
return relative_path
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def register_nodes(inventory: dict[str, object], graph: Graph) -> None:
|
|
223
|
+
"""Walk the inventory and populate ``graph.nodes`` for every addressable unit."""
|
|
224
|
+
files = inventory["files"]
|
|
225
|
+
if isinstance(files, list):
|
|
226
|
+
_register_file_nodes(files, graph)
|
|
227
|
+
|
|
228
|
+
# Register the canonical Claude Code hook events as virtual nodes so
|
|
229
|
+
# hook bindings can target them even though no source file represents
|
|
230
|
+
# an event itself.
|
|
231
|
+
for event in HARNESS_EVENTS:
|
|
232
|
+
graph.add_node(Node(id=event, kind=KIND_HARNESS_EVENT, path=None))
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _register_file_nodes(files: list[object], graph: Graph) -> None:
|
|
236
|
+
"""Register one node per addressable inventory record in ``files``."""
|
|
237
|
+
for record in files:
|
|
238
|
+
if not isinstance(record, dict):
|
|
239
|
+
continue
|
|
240
|
+
cls = record.get("class")
|
|
241
|
+
path = record.get("path")
|
|
242
|
+
if not isinstance(cls, str) or not isinstance(path, str):
|
|
243
|
+
continue
|
|
244
|
+
identifier = derive_node_id(cls, path)
|
|
245
|
+
if identifier is None:
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
kind_map = {
|
|
249
|
+
"agent": KIND_AGENT,
|
|
250
|
+
"command": KIND_COMMAND,
|
|
251
|
+
"skill": KIND_SKILL,
|
|
252
|
+
"hook": KIND_HOOK,
|
|
253
|
+
"output-style": KIND_OUTPUT_STYLE,
|
|
254
|
+
"statusline": KIND_STATUSLINE,
|
|
255
|
+
"mcp": KIND_MCP,
|
|
256
|
+
}
|
|
257
|
+
if cls in kind_map:
|
|
258
|
+
graph.add_node(Node(id=identifier, kind=kind_map[cls], path=path))
|
|
259
|
+
elif cls == "docs" and path.startswith("rules/"):
|
|
260
|
+
graph.add_node(Node(id=identifier, kind=KIND_RULE, path=path))
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def read_text(absolute_path: Path) -> str:
|
|
264
|
+
"""Read up to SCAN_MAX_BYTES of the file as UTF-8 text."""
|
|
265
|
+
try:
|
|
266
|
+
with absolute_path.open("rb") as handle:
|
|
267
|
+
data = handle.read(SCAN_MAX_BYTES)
|
|
268
|
+
return data.decode("utf-8", errors="replace")
|
|
269
|
+
except OSError:
|
|
270
|
+
return ""
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def scan_text_references(graph: Graph, root: Path) -> None:
|
|
274
|
+
"""For each text source, scan for mentions of every other node identifier.
|
|
275
|
+
|
|
276
|
+
Source nodes are agents, commands, skills, output-styles, rules, and
|
|
277
|
+
hook bodies — Markdown, Python, shell, and PowerShell. The body of each
|
|
278
|
+
source is read and matched against every other node's identifier as a
|
|
279
|
+
whole-word reference. Matches emit an ``invokes`` edge for agent / command
|
|
280
|
+
sources and a ``references`` edge for any other source kind.
|
|
281
|
+
|
|
282
|
+
Hook scripts additionally resolve their siblings by bare filename: a shell
|
|
283
|
+
or PowerShell stub names ``find-python.sh``, the backslash path
|
|
284
|
+
``hooks\\lib\\find-python.ps1``, or the counterpart ``find-pwsh.ps1`` — never
|
|
285
|
+
the forward-slash node id — so script-source bodies are scanned for the
|
|
286
|
+
unique basenames of script nodes as well.
|
|
287
|
+
"""
|
|
288
|
+
text_kinds = {
|
|
289
|
+
KIND_AGENT,
|
|
290
|
+
KIND_COMMAND,
|
|
291
|
+
KIND_SKILL,
|
|
292
|
+
KIND_OUTPUT_STYLE,
|
|
293
|
+
KIND_RULE,
|
|
294
|
+
KIND_HOOK,
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
non_event_nodes = [n for n in graph.nodes if n.kind != KIND_HARNESS_EVENT]
|
|
298
|
+
target_ids = sorted(
|
|
299
|
+
{n.id for n in non_event_nodes},
|
|
300
|
+
key=len,
|
|
301
|
+
reverse=True,
|
|
302
|
+
)
|
|
303
|
+
patterns: dict[str, re.Pattern[str]] = {
|
|
304
|
+
identifier: re.compile(rf"\b{re.escape(identifier)}\b")
|
|
305
|
+
for identifier in target_ids
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# Script-sibling basename index: a shell / PowerShell stub references its
|
|
309
|
+
# siblings by filename, not by the content-root-relative node id. Map each
|
|
310
|
+
# script node's basename to its id, dropping basenames shared by more than
|
|
311
|
+
# one node so an ambiguous filename never mis-attributes an edge.
|
|
312
|
+
basename_ids: dict[str, list[str]] = {}
|
|
313
|
+
for node in non_event_nodes:
|
|
314
|
+
if node.path and node.path.endswith(SCRIPT_SUFFIXES):
|
|
315
|
+
basename_ids.setdefault(node.path.rsplit("/", 1)[-1], []).append(node.id)
|
|
316
|
+
script_targets: dict[str, str] = {
|
|
317
|
+
base: ids[0] for base, ids in basename_ids.items() if len(ids) == 1
|
|
318
|
+
}
|
|
319
|
+
script_patterns: dict[str, re.Pattern[str]] = {
|
|
320
|
+
base: re.compile(rf"\b{re.escape(base)}\b") for base in script_targets
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
for source in graph.nodes:
|
|
324
|
+
if source.kind not in text_kinds or source.path is None:
|
|
325
|
+
continue
|
|
326
|
+
if source.kind == KIND_HOOK and not source.path.endswith(
|
|
327
|
+
(".md", ".py", *SCRIPT_SUFFIXES)
|
|
328
|
+
):
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
absolute = root / source.path
|
|
332
|
+
body = read_text(absolute)
|
|
333
|
+
if not body:
|
|
334
|
+
continue
|
|
335
|
+
|
|
336
|
+
for target_id in target_ids:
|
|
337
|
+
if target_id == source.id:
|
|
338
|
+
continue
|
|
339
|
+
if patterns[target_id].search(body):
|
|
340
|
+
relation = (
|
|
341
|
+
RELATION_INVOKES
|
|
342
|
+
if source.kind in (KIND_AGENT, KIND_COMMAND)
|
|
343
|
+
else RELATION_REFERENCES
|
|
344
|
+
)
|
|
345
|
+
graph.add_edge(
|
|
346
|
+
Edge(
|
|
347
|
+
source=source.id,
|
|
348
|
+
target=target_id,
|
|
349
|
+
relation=relation,
|
|
350
|
+
evidence_path=source.path,
|
|
351
|
+
)
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Script siblings resolve their counterparts by bare filename, which
|
|
355
|
+
# the content-root-relative node-id scan above never matches.
|
|
356
|
+
if not source.path.endswith(SCRIPT_SUFFIXES):
|
|
357
|
+
continue
|
|
358
|
+
for base, pattern in script_patterns.items():
|
|
359
|
+
target_id = script_targets[base]
|
|
360
|
+
if target_id == source.id:
|
|
361
|
+
continue
|
|
362
|
+
if pattern.search(body):
|
|
363
|
+
graph.add_edge(
|
|
364
|
+
Edge(
|
|
365
|
+
source=source.id,
|
|
366
|
+
target=target_id,
|
|
367
|
+
relation=RELATION_REFERENCES,
|
|
368
|
+
evidence_path=source.path,
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def scan_hook_bindings(graph: Graph, root: Path) -> None:
|
|
374
|
+
"""Parse the engine hook config for event-name → hook-artifact bindings.
|
|
375
|
+
|
|
376
|
+
The committed ``hooks/hooks.json`` is the source-of-truth hook block the
|
|
377
|
+
plugin tree and the per-harness settings templates render from. Each
|
|
378
|
+
entry in its ``hooks`` block binds one or more hook artifacts — a
|
|
379
|
+
bootstrap stub plus, for context-emitting events, a message file — to a
|
|
380
|
+
harness event; every such pairing emits a ``binds-to`` edge from the hook
|
|
381
|
+
node to the event node. A single command may reference several artifacts,
|
|
382
|
+
so every distinct on-disk reference contributes its own edge.
|
|
383
|
+
"""
|
|
384
|
+
for relpath in HOOK_CONFIG_RELPATHS:
|
|
385
|
+
path = root / relpath
|
|
386
|
+
if not path.is_file():
|
|
387
|
+
continue
|
|
388
|
+
try:
|
|
389
|
+
with path.open("r", encoding="utf-8") as handle:
|
|
390
|
+
config = json.load(handle)
|
|
391
|
+
except (OSError, json.JSONDecodeError):
|
|
392
|
+
continue
|
|
393
|
+
if not isinstance(config, dict):
|
|
394
|
+
continue
|
|
395
|
+
|
|
396
|
+
hooks_block = config.get("hooks", {})
|
|
397
|
+
if not isinstance(hooks_block, dict):
|
|
398
|
+
continue
|
|
399
|
+
|
|
400
|
+
for event_name, entries in hooks_block.items():
|
|
401
|
+
if event_name not in HARNESS_EVENTS:
|
|
402
|
+
continue
|
|
403
|
+
if not isinstance(entries, list):
|
|
404
|
+
continue
|
|
405
|
+
for entry in entries:
|
|
406
|
+
if not isinstance(entry, dict):
|
|
407
|
+
continue
|
|
408
|
+
hook_handlers = entry.get("hooks", [])
|
|
409
|
+
if not isinstance(hook_handlers, list):
|
|
410
|
+
continue
|
|
411
|
+
for handler in hook_handlers:
|
|
412
|
+
if not isinstance(handler, dict):
|
|
413
|
+
continue
|
|
414
|
+
command = handler.get("command", "")
|
|
415
|
+
if not isinstance(command, str):
|
|
416
|
+
continue
|
|
417
|
+
for node_id in _extract_hook_node_ids(command, root):
|
|
418
|
+
graph.add_edge(
|
|
419
|
+
Edge(
|
|
420
|
+
source=node_id,
|
|
421
|
+
target=event_name,
|
|
422
|
+
relation=RELATION_BINDS_TO,
|
|
423
|
+
evidence_path=relpath,
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _extract_hook_node_ids(command: str, root: Path) -> list[str]:
|
|
429
|
+
"""Extract the content-root-relative hook node ids a command references.
|
|
430
|
+
|
|
431
|
+
A ``hooks.json`` ``command`` field is a shell invocation (PowerShell or
|
|
432
|
+
bash) that runs one or more hook artifacts — a bootstrap stub, and for
|
|
433
|
+
context-emitting events the per-event message file. Each is named by a
|
|
434
|
+
variable-prefixed path (e.g.
|
|
435
|
+
``"${CLAUDE_PLUGIN_ROOT}/src/apothem/hooks/messages/stop.md"``); the
|
|
436
|
+
meaningful ``binds-to`` target is the content-root-relative tail
|
|
437
|
+
(``hooks/messages/stop.md``), which equals the inventory's hook node id.
|
|
438
|
+
Every distinct reference that resolves to a file on disk is returned in
|
|
439
|
+
first-seen order, so a single command binds every hook node it names.
|
|
440
|
+
"""
|
|
441
|
+
node_ids: list[str] = []
|
|
442
|
+
seen: set[str] = set()
|
|
443
|
+
for match in _HOOK_REF_PATTERN.finditer(command):
|
|
444
|
+
node_id = match.group(1)
|
|
445
|
+
if node_id in seen:
|
|
446
|
+
continue
|
|
447
|
+
if not (root / node_id).is_file():
|
|
448
|
+
continue
|
|
449
|
+
seen.add(node_id)
|
|
450
|
+
node_ids.append(node_id)
|
|
451
|
+
return node_ids
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _python_import_names(relative_path: str, package_root: str) -> list[str]:
|
|
455
|
+
"""Module names by which the Python file at ``relative_path`` is importable.
|
|
456
|
+
|
|
457
|
+
A file is importable under several names depending on which ancestor
|
|
458
|
+
directory is on ``sys.path`` at runtime. Returns the content-root-relative
|
|
459
|
+
dotted path (``hooks.lib.log``), the installed-package form
|
|
460
|
+
(``apothem.hooks.lib.log``), and — for a non-package module — the bare leaf
|
|
461
|
+
name (``log``). The hook bootstrap inserts both ``hooks/`` and ``hooks/lib/``
|
|
462
|
+
onto ``sys.path``, so the lib modules are imported by bare leaf name
|
|
463
|
+
(``from log import get_logger``); the bare-leaf candidate is what resolves
|
|
464
|
+
those edges. A package ``__init__.py`` is named for its directory, never the
|
|
465
|
+
literal ``__init__`` leaf, so it is never imported by leaf name.
|
|
466
|
+
"""
|
|
467
|
+
parts = relative_path.removesuffix(".py").split("/")
|
|
468
|
+
is_package = parts[-1] == "__init__"
|
|
469
|
+
if is_package:
|
|
470
|
+
parts = parts[:-1]
|
|
471
|
+
if not parts:
|
|
472
|
+
return []
|
|
473
|
+
dotted = ".".join(parts)
|
|
474
|
+
names = [dotted, f"{package_root}.{dotted}"]
|
|
475
|
+
if not is_package:
|
|
476
|
+
names.append(parts[-1])
|
|
477
|
+
return names
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def scan_python_imports(graph: Graph, root: Path) -> None:
|
|
481
|
+
"""Trace import edges between Python source files in the graph.
|
|
482
|
+
|
|
483
|
+
Each Python node is indexed under every module name it answers to (see
|
|
484
|
+
``_python_import_names``). An import statement whose module equals a known
|
|
485
|
+
name — or names a submodule of a known package — emits an ``imports`` edge
|
|
486
|
+
to that node. Bare-leaf indexing is what makes the hook lib modules resolve:
|
|
487
|
+
they are imported by bare name (``from log import …``) because the bootstrap
|
|
488
|
+
puts ``hooks/lib/`` on ``sys.path``, never by their content-root dotted path.
|
|
489
|
+
"""
|
|
490
|
+
# Pair each Python node with its non-None path so the edge endpoints are
|
|
491
|
+
# typed ``str`` rather than ``str | None``.
|
|
492
|
+
py_paths: list[tuple[Node, str]] = [
|
|
493
|
+
(n, n.path)
|
|
494
|
+
for n in graph.nodes
|
|
495
|
+
if n.path is not None and n.path.endswith(".py")
|
|
496
|
+
]
|
|
497
|
+
# Index every importable name to its file. First-seen wins on the rare
|
|
498
|
+
# basename collision; package ``__init__`` modules are excluded from bare-
|
|
499
|
+
# leaf indexing, so collisions are vanishingly unlikely in practice.
|
|
500
|
+
module_index: dict[str, str] = {}
|
|
501
|
+
for _node, path in py_paths:
|
|
502
|
+
for candidate in _python_import_names(path, root.name):
|
|
503
|
+
module_index.setdefault(candidate, path)
|
|
504
|
+
|
|
505
|
+
import_pattern = re.compile(
|
|
506
|
+
r"^\s*(?:from\s+([\w.]+)\s+import|import\s+([\w.]+))",
|
|
507
|
+
re.MULTILINE,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
for _source, source_path in py_paths:
|
|
511
|
+
body = read_text(root / source_path)
|
|
512
|
+
if not body:
|
|
513
|
+
continue
|
|
514
|
+
for match in import_pattern.finditer(body):
|
|
515
|
+
module = match.group(1) or match.group(2)
|
|
516
|
+
if not module:
|
|
517
|
+
continue
|
|
518
|
+
for known_module, known_path in module_index.items():
|
|
519
|
+
if module == known_module or module.startswith(known_module + "."):
|
|
520
|
+
if known_path == source_path:
|
|
521
|
+
continue
|
|
522
|
+
graph.add_edge(
|
|
523
|
+
Edge(
|
|
524
|
+
source=source_path,
|
|
525
|
+
target=known_path,
|
|
526
|
+
relation=RELATION_IMPORTS,
|
|
527
|
+
evidence_path=source_path,
|
|
528
|
+
)
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def emit_graph(graph: Graph, output: Path) -> None:
|
|
533
|
+
"""Serialise the graph to ``output`` as pretty-printed JSON."""
|
|
534
|
+
payload = {
|
|
535
|
+
"generated-at": datetime.now(tz=timezone.utc).isoformat(),
|
|
536
|
+
"node-count": len(graph.nodes),
|
|
537
|
+
"edge-count": len(graph.edges),
|
|
538
|
+
"nodes": sorted(
|
|
539
|
+
(n.to_json() for n in graph.nodes),
|
|
540
|
+
key=lambda n: (n["kind"], n["id"]),
|
|
541
|
+
),
|
|
542
|
+
"edges": sorted(
|
|
543
|
+
(e.to_json() for e in graph.edges),
|
|
544
|
+
key=lambda e: (e["source"], e["relation"], e["target"]),
|
|
545
|
+
),
|
|
546
|
+
}
|
|
547
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
548
|
+
with output.open("w", encoding="utf-8") as handle:
|
|
549
|
+
json.dump(payload, handle, indent=2, sort_keys=False, ensure_ascii=False)
|
|
550
|
+
handle.write("\n")
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def parse_arguments(argv: list[str]) -> argparse.Namespace:
|
|
554
|
+
"""CLI surface — root, inventory, and output are required."""
|
|
555
|
+
parser = argparse.ArgumentParser(
|
|
556
|
+
prog="build_capability_graph",
|
|
557
|
+
description="Build a capability graph from the inventory.",
|
|
558
|
+
)
|
|
559
|
+
parser.add_argument(
|
|
560
|
+
"--root",
|
|
561
|
+
type=Path,
|
|
562
|
+
required=True,
|
|
563
|
+
help="Working-tree root containing the source artifacts.",
|
|
564
|
+
)
|
|
565
|
+
parser.add_argument(
|
|
566
|
+
"--inventory",
|
|
567
|
+
type=Path,
|
|
568
|
+
required=True,
|
|
569
|
+
help="Inventory JSON path produced by build_inventory.py.",
|
|
570
|
+
)
|
|
571
|
+
parser.add_argument(
|
|
572
|
+
"--output",
|
|
573
|
+
type=Path,
|
|
574
|
+
required=True,
|
|
575
|
+
help="Output JSON path (e.g., .audit/capability-graph.json).",
|
|
576
|
+
)
|
|
577
|
+
return parser.parse_args(argv)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def main(argv: list[str]) -> int:
|
|
581
|
+
"""Entry point — returns the exit code."""
|
|
582
|
+
args = parse_arguments(argv)
|
|
583
|
+
root = args.root.resolve()
|
|
584
|
+
inventory_path = args.inventory.resolve()
|
|
585
|
+
output = args.output.resolve()
|
|
586
|
+
|
|
587
|
+
if not inventory_path.is_file():
|
|
588
|
+
print(f"error: inventory not found: {inventory_path}", file=sys.stderr)
|
|
589
|
+
return EXIT_ERROR
|
|
590
|
+
|
|
591
|
+
inventory = load_inventory(inventory_path)
|
|
592
|
+
graph = Graph()
|
|
593
|
+
register_nodes(inventory, graph)
|
|
594
|
+
scan_text_references(graph, root)
|
|
595
|
+
scan_hook_bindings(graph, root)
|
|
596
|
+
scan_python_imports(graph, root)
|
|
597
|
+
emit_graph(graph, output)
|
|
598
|
+
|
|
599
|
+
print(
|
|
600
|
+
f"capability-graph: {len(graph.nodes)} nodes; "
|
|
601
|
+
f"{len(graph.edges)} edges; output={output}"
|
|
602
|
+
)
|
|
603
|
+
return EXIT_OK
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
if __name__ == "__main__":
|
|
607
|
+
raise SystemExit(main(sys.argv[1:]))
|