@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,656 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
"""Managed-block + JSON/YAML key-merge and the operator-owned write path."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import contextlib
|
|
8
|
+
import difflib
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from string import Template
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
from apothem.lib.harness_materializer import (
|
|
17
|
+
merge_managed_block,
|
|
18
|
+
)
|
|
19
|
+
from apothem.lib.propagation import (
|
|
20
|
+
InstallEntry,
|
|
21
|
+
resolve_target,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from .install_driver_backup import write_bytes_safely
|
|
25
|
+
from .install_driver_pathsafety import (
|
|
26
|
+
_allowed_write_root,
|
|
27
|
+
_root_for,
|
|
28
|
+
_validate_target_path,
|
|
29
|
+
)
|
|
30
|
+
from .install_driver_types import (
|
|
31
|
+
AuthorizationRequest,
|
|
32
|
+
AuthorizeFn,
|
|
33
|
+
MaterializationOutcome,
|
|
34
|
+
MaterializationResult,
|
|
35
|
+
_path_text,
|
|
36
|
+
_result,
|
|
37
|
+
_with_detail,
|
|
38
|
+
resolve_source,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def apply_managed_block_anchor(
|
|
43
|
+
target: Path,
|
|
44
|
+
body: str,
|
|
45
|
+
*,
|
|
46
|
+
install_root: Path,
|
|
47
|
+
harness_name: str,
|
|
48
|
+
allowed_root: Path | None = None,
|
|
49
|
+
) -> MaterializationResult:
|
|
50
|
+
"""Fold a profile-projected managed block into an operator-owned anchor.
|
|
51
|
+
|
|
52
|
+
Reads the existing anchor (if any), merges *body* into the canonical Apothem
|
|
53
|
+
sentinel block via :func:`merge_managed_block` (operator prose outside the
|
|
54
|
+
sentinels preserved verbatim), and writes the result atomically with
|
|
55
|
+
backup-before-replace + no-op detection. This is the reusable projection
|
|
56
|
+
write path every instruction-anchor adapter shares: the profile-projection
|
|
57
|
+
seam renders *body*, this helper lands it in the harness's Markdown anchor.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
existing = target.read_text(encoding="utf-8") if target.exists() else ""
|
|
61
|
+
except OSError:
|
|
62
|
+
existing = ""
|
|
63
|
+
merged = merge_managed_block(existing, body)
|
|
64
|
+
return write_bytes_safely(
|
|
65
|
+
target,
|
|
66
|
+
merged.encode("utf-8"),
|
|
67
|
+
install_root=install_root,
|
|
68
|
+
harness_name=harness_name,
|
|
69
|
+
operation="sentinel_merge",
|
|
70
|
+
allowed_root=allowed_root,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def project_profile_document(
|
|
75
|
+
harness_root: Path,
|
|
76
|
+
*,
|
|
77
|
+
harness_id: str,
|
|
78
|
+
harness_name: str,
|
|
79
|
+
profile: dict[str, Any],
|
|
80
|
+
relative_path: str = "apothem/rules/00-apothem-profile.md",
|
|
81
|
+
) -> MaterializationResult:
|
|
82
|
+
"""Write the profile's projected managed block to a config adapter's anchor.
|
|
83
|
+
|
|
84
|
+
The single-file-config adapters render a native config whose schema accepts
|
|
85
|
+
only documented keys (skills pointers, MCP), so the profile's identity /
|
|
86
|
+
preferences / rules / opted-in behaviors reach the harness through this
|
|
87
|
+
dedicated apothem profile document under the adapter's apothem rules
|
|
88
|
+
directory — the same directory the native config's instructions pointer
|
|
89
|
+
references. Written through the gated managed-block anchor path, so it is
|
|
90
|
+
operator-preserving and idempotent.
|
|
91
|
+
"""
|
|
92
|
+
from apothem.lib.profile import coerce_profile
|
|
93
|
+
from apothem.lib.profile_projection import project
|
|
94
|
+
|
|
95
|
+
for_harness = coerce_profile(profile).for_harness(harness_id)
|
|
96
|
+
surfaces = project(for_harness, harness_id)
|
|
97
|
+
return apply_managed_block_anchor(
|
|
98
|
+
harness_root / relative_path,
|
|
99
|
+
surfaces.managed_block_body,
|
|
100
|
+
install_root=harness_root,
|
|
101
|
+
harness_name=harness_name,
|
|
102
|
+
allowed_root=harness_root.parent,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
#: Substrings that identify an Apothem-managed hook handler. Covers both the
|
|
107
|
+
#: module-invocation spelling and the installed script-path spelling (the
|
|
108
|
+
#: settings templates point hooks at the materialized ``hooks/dispatch.py``
|
|
109
|
+
#: and ``conformity/gate.py`` scripts under the harness root).
|
|
110
|
+
_APOTHEM_HOOK_MARKERS: tuple[str, ...] = (
|
|
111
|
+
"apothem.hooks.dispatch",
|
|
112
|
+
"apothem.conformity.gate",
|
|
113
|
+
"hooks/dispatch.py",
|
|
114
|
+
"conformity/gate.py",
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _is_apothem_hook(handler: object) -> bool:
|
|
119
|
+
"""Return True when a hook handler belongs to Apothem's managed surface."""
|
|
120
|
+
if not isinstance(handler, dict):
|
|
121
|
+
return False
|
|
122
|
+
command = str(handler.get("command", ""))
|
|
123
|
+
args = handler.get("args", [])
|
|
124
|
+
args_text = " ".join(str(arg) for arg in args) if isinstance(args, list) else ""
|
|
125
|
+
haystacks = (command, args_text)
|
|
126
|
+
return any(
|
|
127
|
+
marker in haystack for marker in _APOTHEM_HOOK_MARKERS for haystack in haystacks
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _dedupe_json_list(existing: list[object], incoming: list[object]) -> list[object]:
|
|
132
|
+
"""Append incoming JSON values that are not already present."""
|
|
133
|
+
merged = list(existing)
|
|
134
|
+
for item in incoming:
|
|
135
|
+
if item not in merged:
|
|
136
|
+
merged.append(item)
|
|
137
|
+
return merged
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _merge_hook_entry(
|
|
141
|
+
existing_entries: list[object],
|
|
142
|
+
incoming_entry: dict[str, object],
|
|
143
|
+
) -> dict[str, object]:
|
|
144
|
+
"""Merge one incoming hook matcher with existing non-Apothem handlers."""
|
|
145
|
+
matcher = incoming_entry.get("matcher")
|
|
146
|
+
preserved_handlers: list[object] = []
|
|
147
|
+
for entry in existing_entries:
|
|
148
|
+
if not isinstance(entry, dict) or entry.get("matcher") != matcher:
|
|
149
|
+
continue
|
|
150
|
+
handlers = entry.get("hooks", [])
|
|
151
|
+
if not isinstance(handlers, list):
|
|
152
|
+
continue
|
|
153
|
+
preserved_handlers.extend(
|
|
154
|
+
handler for handler in handlers if not _is_apothem_hook(handler)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
incoming_handlers = incoming_entry.get("hooks", [])
|
|
158
|
+
if not isinstance(incoming_handlers, list):
|
|
159
|
+
incoming_handlers = []
|
|
160
|
+
|
|
161
|
+
merged_entry = dict(incoming_entry)
|
|
162
|
+
merged_entry["hooks"] = preserved_handlers + incoming_handlers
|
|
163
|
+
return merged_entry
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _merge_hooks(
|
|
167
|
+
existing_hooks: dict[str, object],
|
|
168
|
+
incoming_hooks: dict[str, object],
|
|
169
|
+
) -> dict[str, object]:
|
|
170
|
+
"""Merge Claude Code hook settings while replacing Apothem-owned handlers."""
|
|
171
|
+
merged = dict(existing_hooks)
|
|
172
|
+
for event_name, incoming_entries in incoming_hooks.items():
|
|
173
|
+
if not isinstance(incoming_entries, list):
|
|
174
|
+
merged[event_name] = incoming_entries
|
|
175
|
+
continue
|
|
176
|
+
existing_entries_obj = existing_hooks.get(event_name, [])
|
|
177
|
+
existing_entries = (
|
|
178
|
+
existing_entries_obj if isinstance(existing_entries_obj, list) else []
|
|
179
|
+
)
|
|
180
|
+
incoming_matchers = {
|
|
181
|
+
entry.get("matcher")
|
|
182
|
+
for entry in incoming_entries
|
|
183
|
+
if isinstance(entry, dict)
|
|
184
|
+
}
|
|
185
|
+
retained = [
|
|
186
|
+
entry
|
|
187
|
+
for entry in existing_entries
|
|
188
|
+
if not isinstance(entry, dict)
|
|
189
|
+
or entry.get("matcher") not in incoming_matchers
|
|
190
|
+
]
|
|
191
|
+
for incoming_entry in incoming_entries:
|
|
192
|
+
if isinstance(incoming_entry, dict):
|
|
193
|
+
retained.append(_merge_hook_entry(existing_entries, incoming_entry))
|
|
194
|
+
else:
|
|
195
|
+
retained.append(incoming_entry)
|
|
196
|
+
merged[event_name] = retained
|
|
197
|
+
return merged
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _merge_json_settings(existing: object, incoming: object) -> object:
|
|
201
|
+
"""Merge JSON settings while preserving operator-authored keys."""
|
|
202
|
+
return _merge_json_values(existing, incoming, prefer_existing=True)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _overlay_json_settings(existing: object, incoming: object) -> object:
|
|
206
|
+
"""Merge JSON settings while incoming managed values take precedence."""
|
|
207
|
+
return _merge_json_values(existing, incoming, prefer_existing=False)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _merge_json_values(
|
|
211
|
+
existing: object, incoming: object, *, prefer_existing: bool
|
|
212
|
+
) -> object:
|
|
213
|
+
"""Merge JSON objects, preserving keys absent from the incoming object."""
|
|
214
|
+
if isinstance(existing, dict) and isinstance(incoming, dict):
|
|
215
|
+
merged: dict[str, object] = dict(existing)
|
|
216
|
+
for key, value in incoming.items():
|
|
217
|
+
current = existing.get(key)
|
|
218
|
+
if key == "hooks" and isinstance(current, dict) and isinstance(value, dict):
|
|
219
|
+
merged[key] = _merge_hooks(current, value)
|
|
220
|
+
elif (
|
|
221
|
+
prefer_existing
|
|
222
|
+
and isinstance(current, list)
|
|
223
|
+
and isinstance(value, list)
|
|
224
|
+
):
|
|
225
|
+
merged[key] = _dedupe_json_list(current, value)
|
|
226
|
+
elif isinstance(current, dict) and isinstance(value, dict):
|
|
227
|
+
merged[key] = _merge_json_values(
|
|
228
|
+
current, value, prefer_existing=prefer_existing
|
|
229
|
+
)
|
|
230
|
+
elif key not in merged:
|
|
231
|
+
merged[key] = value
|
|
232
|
+
elif prefer_existing:
|
|
233
|
+
merged[key] = current
|
|
234
|
+
else:
|
|
235
|
+
merged[key] = value
|
|
236
|
+
return merged
|
|
237
|
+
if isinstance(existing, list) and isinstance(incoming, list):
|
|
238
|
+
if prefer_existing:
|
|
239
|
+
return _dedupe_json_list(existing, incoming)
|
|
240
|
+
return incoming
|
|
241
|
+
return existing
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _merged_json_text(
|
|
245
|
+
target: Path, content: str, *, prefer_existing: bool = True
|
|
246
|
+
) -> str:
|
|
247
|
+
"""Return JSON text merged with an existing JSON target when possible."""
|
|
248
|
+
incoming = json.loads(content)
|
|
249
|
+
try:
|
|
250
|
+
existing = json.loads(target.read_text(encoding="utf-8"))
|
|
251
|
+
except (OSError, json.JSONDecodeError):
|
|
252
|
+
return json.dumps(incoming, indent=2, ensure_ascii=False) + "\n"
|
|
253
|
+
merger = _merge_json_settings if prefer_existing else _overlay_json_settings
|
|
254
|
+
merged = merger(existing, incoming)
|
|
255
|
+
return json.dumps(merged, indent=2, ensure_ascii=False) + "\n"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def write_text_safely(
|
|
259
|
+
target: Path,
|
|
260
|
+
content: str,
|
|
261
|
+
*,
|
|
262
|
+
install_root: Path,
|
|
263
|
+
harness_name: str,
|
|
264
|
+
prefer_existing_json_values: bool = True,
|
|
265
|
+
allowed_root: Path | None = None,
|
|
266
|
+
) -> MaterializationResult:
|
|
267
|
+
"""Write text with backup-before-replace and JSON-preserving merge."""
|
|
268
|
+
target_error = _validate_target_path(
|
|
269
|
+
target,
|
|
270
|
+
allowed_root=allowed_root or install_root,
|
|
271
|
+
operation="write_text",
|
|
272
|
+
)
|
|
273
|
+
if target_error is not None:
|
|
274
|
+
return target_error
|
|
275
|
+
output = content
|
|
276
|
+
if target.suffix.lower() == ".json" and target.exists():
|
|
277
|
+
with contextlib.suppress(json.JSONDecodeError):
|
|
278
|
+
output = _merged_json_text(
|
|
279
|
+
target,
|
|
280
|
+
content,
|
|
281
|
+
prefer_existing=prefer_existing_json_values,
|
|
282
|
+
)
|
|
283
|
+
return write_bytes_safely(
|
|
284
|
+
target,
|
|
285
|
+
output.encode("utf-8"),
|
|
286
|
+
install_root=install_root,
|
|
287
|
+
harness_name=harness_name,
|
|
288
|
+
operation="write_text",
|
|
289
|
+
allowed_root=allowed_root,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _leading_comment_block(text: str) -> str:
|
|
294
|
+
"""Return the leading comment/blank lines of *text* (a managed YAML header).
|
|
295
|
+
|
|
296
|
+
PyYAML drops comments on round-trip, so the incoming config's leading
|
|
297
|
+
``#`` header is captured here and re-prepended to a merged YAML body. This
|
|
298
|
+
keeps the merge byte-stable: re-merging identical content reproduces the
|
|
299
|
+
same header + body, so install stays idempotent.
|
|
300
|
+
"""
|
|
301
|
+
kept: list[str] = []
|
|
302
|
+
for line in text.splitlines(keepends=True):
|
|
303
|
+
stripped = line.lstrip()
|
|
304
|
+
if stripped == "" or stripped.startswith("#"):
|
|
305
|
+
kept.append(line)
|
|
306
|
+
else:
|
|
307
|
+
break
|
|
308
|
+
return "".join(kept)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _merged_yaml_text(target: Path, content: str, *, prefer_existing: bool) -> str:
|
|
312
|
+
"""Return YAML text merged with an existing YAML target when possible.
|
|
313
|
+
|
|
314
|
+
The YAML mirror of :func:`_merged_json_text`: parse both documents, overlay
|
|
315
|
+
operator keys per the shared key-merge, re-dump, and re-prepend the incoming
|
|
316
|
+
managed header comment. Operator keys absent from the incoming managed
|
|
317
|
+
config are preserved; operator *comments* are not retained across a
|
|
318
|
+
merge-over-existing (PyYAML round-trips values, not comments). A clean
|
|
319
|
+
install with no existing target writes *content* verbatim.
|
|
320
|
+
"""
|
|
321
|
+
try:
|
|
322
|
+
incoming = yaml.safe_load(content)
|
|
323
|
+
existing = yaml.safe_load(target.read_text(encoding="utf-8"))
|
|
324
|
+
except (OSError, yaml.YAMLError):
|
|
325
|
+
return content
|
|
326
|
+
if not isinstance(incoming, dict) or not isinstance(existing, dict):
|
|
327
|
+
return content
|
|
328
|
+
merger = _merge_json_settings if prefer_existing else _overlay_json_settings
|
|
329
|
+
merged = merger(existing, incoming)
|
|
330
|
+
header = _leading_comment_block(content)
|
|
331
|
+
return header + yaml.safe_dump(merged, sort_keys=False, allow_unicode=True)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _merge_native_content(target: Path, content: str, *, existed: bool) -> str:
|
|
335
|
+
"""Return the prospective merged text for a materializer-rendered config.
|
|
336
|
+
|
|
337
|
+
JSON and YAML targets key-merge with the operator's existing file (incoming
|
|
338
|
+
managed values authoritative, operator-added keys preserved); other suffixes
|
|
339
|
+
and non-existent targets render *content* verbatim.
|
|
340
|
+
"""
|
|
341
|
+
if not existed:
|
|
342
|
+
return content
|
|
343
|
+
suffix = target.suffix.lower()
|
|
344
|
+
if suffix == ".json":
|
|
345
|
+
with contextlib.suppress(json.JSONDecodeError, OSError):
|
|
346
|
+
return _merged_json_text(target, content, prefer_existing=False)
|
|
347
|
+
elif suffix in {".yaml", ".yml"}:
|
|
348
|
+
return _merged_yaml_text(target, content, prefer_existing=False)
|
|
349
|
+
return content
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def apply_operator_owned_content(
|
|
353
|
+
target: Path,
|
|
354
|
+
content: str,
|
|
355
|
+
*,
|
|
356
|
+
install_root: Path,
|
|
357
|
+
harness_name: str,
|
|
358
|
+
ownership_class: str = "operator-owned",
|
|
359
|
+
allowed_root: Path | None = None,
|
|
360
|
+
authorize: AuthorizeFn | None = None,
|
|
361
|
+
) -> MaterializationResult:
|
|
362
|
+
"""Write materializer-rendered content to an operator-owned native config.
|
|
363
|
+
|
|
364
|
+
The content-based sibling of :func:`_apply_operator_owned_file` (which reads
|
|
365
|
+
from a source file): the same gated apply path for adapters whose config is
|
|
366
|
+
rendered dynamically from the profile. Computes a suffix-aware
|
|
367
|
+
key-preserving merge (JSON / YAML), records a unified diff on the result,
|
|
368
|
+
consults *authorize* on a non-additive overwrite (declining leaves the
|
|
369
|
+
operator file untouched), then writes atomically with backup-before-replace
|
|
370
|
+
and no-op detection.
|
|
371
|
+
"""
|
|
372
|
+
write_allowed = allowed_root or install_root
|
|
373
|
+
target_error = _validate_target_path(
|
|
374
|
+
target, allowed_root=write_allowed, operation="write_text"
|
|
375
|
+
)
|
|
376
|
+
if target_error is not None:
|
|
377
|
+
return target_error
|
|
378
|
+
existed = target.exists()
|
|
379
|
+
before = ""
|
|
380
|
+
if existed:
|
|
381
|
+
with contextlib.suppress(OSError):
|
|
382
|
+
before = target.read_text(encoding="utf-8")
|
|
383
|
+
merged = _merge_native_content(target, content, existed=existed)
|
|
384
|
+
diff = _unified_diff(before, merged, target)
|
|
385
|
+
detail: dict[str, str] = {"ownership_class": ownership_class}
|
|
386
|
+
if diff:
|
|
387
|
+
detail["diff"] = diff
|
|
388
|
+
if existed and before != merged:
|
|
389
|
+
detail["destructive_gate"] = "required"
|
|
390
|
+
if authorize is not None and not authorize(
|
|
391
|
+
AuthorizationRequest(
|
|
392
|
+
harness=harness_name,
|
|
393
|
+
path=_path_text(target),
|
|
394
|
+
operation="write_text",
|
|
395
|
+
ownership_class=ownership_class,
|
|
396
|
+
diff=diff,
|
|
397
|
+
)
|
|
398
|
+
):
|
|
399
|
+
return _with_detail(
|
|
400
|
+
_result(
|
|
401
|
+
"skipped",
|
|
402
|
+
"write_text",
|
|
403
|
+
target,
|
|
404
|
+
"operator declined the native-config change",
|
|
405
|
+
),
|
|
406
|
+
detail,
|
|
407
|
+
)
|
|
408
|
+
result = write_bytes_safely(
|
|
409
|
+
target,
|
|
410
|
+
merged.encode("utf-8"),
|
|
411
|
+
install_root=install_root,
|
|
412
|
+
harness_name=harness_name,
|
|
413
|
+
operation="write_text",
|
|
414
|
+
allowed_root=allowed_root,
|
|
415
|
+
)
|
|
416
|
+
return _with_detail(result, detail)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _unified_diff(before: str, after: str, target: Path) -> str:
|
|
420
|
+
"""Return a unified diff from *before* to *after* for *target*."""
|
|
421
|
+
return "".join(
|
|
422
|
+
difflib.unified_diff(
|
|
423
|
+
before.splitlines(keepends=True),
|
|
424
|
+
after.splitlines(keepends=True),
|
|
425
|
+
fromfile=f"a/{target.name}",
|
|
426
|
+
tofile=f"b/{target.name}",
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def render_content_tokens(
|
|
432
|
+
content: str,
|
|
433
|
+
*,
|
|
434
|
+
harness_root: Path | None,
|
|
435
|
+
project_root: Path | None = None,
|
|
436
|
+
) -> str:
|
|
437
|
+
"""Substitute ``${HARNESS_ROOT}`` / ``${PROJECT_ROOT}`` inside template text.
|
|
438
|
+
|
|
439
|
+
The content-side mirror of :func:`apothem.lib.propagation.resolve_target`:
|
|
440
|
+
target paths resolve placeholders at the manifest layer, and this helper
|
|
441
|
+
resolves the same placeholders inside the template's *body* so a template
|
|
442
|
+
can embed install-time absolute paths — the hook entries in
|
|
443
|
+
``settings.json`` and ``hooks.json`` point at the dispatcher's on-disk
|
|
444
|
+
location this way. Substituted paths use forward-slash form so the result
|
|
445
|
+
stays valid inside JSON string literals on every platform. An absent root
|
|
446
|
+
leaves its placeholder untouched, matching the path-resolution helper.
|
|
447
|
+
"""
|
|
448
|
+
if "${" not in content:
|
|
449
|
+
return content
|
|
450
|
+
mapping: dict[str, str] = {}
|
|
451
|
+
if harness_root is not None:
|
|
452
|
+
mapping["HARNESS_ROOT"] = harness_root.resolve().as_posix()
|
|
453
|
+
if project_root is not None:
|
|
454
|
+
mapping["PROJECT_ROOT"] = project_root.resolve().as_posix()
|
|
455
|
+
if not mapping:
|
|
456
|
+
return content
|
|
457
|
+
return Template(content).safe_substitute(mapping)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _operator_owned_merge_text(
|
|
461
|
+
entry: InstallEntry, target: Path, content: str, *, existed: bool
|
|
462
|
+
) -> str:
|
|
463
|
+
"""Return the prospective merged text for an operator-owned target.
|
|
464
|
+
|
|
465
|
+
``sentinel_merge`` entries fold *content* into the operator anchor as a
|
|
466
|
+
sentinel-delimited managed block, preserving operator prose outside the
|
|
467
|
+
block. ``write_text`` entries on an existing JSON target run the JSON
|
|
468
|
+
merge with the incoming template authoritative: template-carried keys
|
|
469
|
+
update in place so fixes ship to existing installs, while operator-added
|
|
470
|
+
keys absent from the template are preserved. The backup-before-replace
|
|
471
|
+
and destructive-authorization gate still guard the write. A non-existent
|
|
472
|
+
target merges to *content* verbatim.
|
|
473
|
+
"""
|
|
474
|
+
if entry.mode == "sentinel_merge":
|
|
475
|
+
existing = ""
|
|
476
|
+
if existed:
|
|
477
|
+
with contextlib.suppress(OSError):
|
|
478
|
+
existing = target.read_text(encoding="utf-8")
|
|
479
|
+
return merge_managed_block(existing, content)
|
|
480
|
+
if target.suffix.lower() == ".json" and existed:
|
|
481
|
+
with contextlib.suppress(json.JSONDecodeError, OSError):
|
|
482
|
+
return _merged_json_text(target, content, prefer_existing=False)
|
|
483
|
+
return content
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _operator_owned_preview(
|
|
487
|
+
entry: InstallEntry,
|
|
488
|
+
*,
|
|
489
|
+
harness_root: Path | None,
|
|
490
|
+
project_root: Path | None,
|
|
491
|
+
profile_body: str | None = None,
|
|
492
|
+
) -> tuple[Path, MaterializationOutcome, str, bool] | None:
|
|
493
|
+
"""Return (target, outcome, unified-diff, gate-required) for an entry.
|
|
494
|
+
|
|
495
|
+
Reads the source and the current target without writing, then computes the
|
|
496
|
+
prospective merge exactly as :func:`_apply_operator_owned_file` would — the
|
|
497
|
+
projected *profile_body* is folded into a ``sentinel_merge`` anchor so the
|
|
498
|
+
preview reflects the same managed block the real write lands — and
|
|
499
|
+
classifies the no-write outcome against the current on-disk bytes:
|
|
500
|
+
|
|
501
|
+
- ``created`` when the target does not yet exist,
|
|
502
|
+
- ``unchanged`` when the prospective merge reproduces the current bytes,
|
|
503
|
+
- ``updated`` when the prospective merge changes existing bytes.
|
|
504
|
+
|
|
505
|
+
The unified diff and the destructive-gate flag (``True`` only for an
|
|
506
|
+
``updated`` outcome, i.e. an overwrite of differing operator content)
|
|
507
|
+
accompany the outcome so a dry-run preview can render the same diff the
|
|
508
|
+
gated write would show. ``None`` when the source file is absent (nothing to
|
|
509
|
+
preview — the plan validator already errors on a missing source, so this is
|
|
510
|
+
a defensive fallback).
|
|
511
|
+
"""
|
|
512
|
+
src = resolve_source(entry.source)
|
|
513
|
+
if not src.is_file():
|
|
514
|
+
return None
|
|
515
|
+
target = resolve_target(
|
|
516
|
+
entry.target, harness_root=harness_root, project_root=project_root
|
|
517
|
+
)
|
|
518
|
+
content = render_content_tokens(
|
|
519
|
+
src.read_text(encoding="utf-8"),
|
|
520
|
+
harness_root=harness_root,
|
|
521
|
+
project_root=project_root,
|
|
522
|
+
)
|
|
523
|
+
if entry.mode == "sentinel_merge" and profile_body:
|
|
524
|
+
content = f"{content.rstrip()}\n\n{profile_body}"
|
|
525
|
+
existed = target.exists()
|
|
526
|
+
before = ""
|
|
527
|
+
if existed:
|
|
528
|
+
with contextlib.suppress(OSError):
|
|
529
|
+
before = target.read_text(encoding="utf-8")
|
|
530
|
+
after = _operator_owned_merge_text(entry, target, content, existed=existed)
|
|
531
|
+
diff = _unified_diff(before, after, target)
|
|
532
|
+
gate_required = existed and before != after
|
|
533
|
+
outcome: MaterializationOutcome = (
|
|
534
|
+
"created" if not existed else ("updated" if gate_required else "unchanged")
|
|
535
|
+
)
|
|
536
|
+
return target, outcome, diff, gate_required
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _apply_operator_owned_file(
|
|
540
|
+
entry: InstallEntry,
|
|
541
|
+
*,
|
|
542
|
+
target: Path,
|
|
543
|
+
src: Path,
|
|
544
|
+
root: Path,
|
|
545
|
+
harness_name: str,
|
|
546
|
+
allowed_root: Path,
|
|
547
|
+
authorize: AuthorizeFn | None,
|
|
548
|
+
harness_root: Path | None = None,
|
|
549
|
+
project_root: Path | None = None,
|
|
550
|
+
profile_body: str | None = None,
|
|
551
|
+
) -> list[MaterializationResult]:
|
|
552
|
+
"""Merge-write an operator-owned file with backup, diff, and gate.
|
|
553
|
+
|
|
554
|
+
The Apothem-managed content (a sentinel block for Markdown anchors, the
|
|
555
|
+
key-merged object for JSON config) is folded into the operator's file so
|
|
556
|
+
operator content is preserved. Path tokens inside the template body are
|
|
557
|
+
rendered first per ``render_content_tokens``. For ``sentinel_merge`` anchors,
|
|
558
|
+
*profile_body* (the projected shared-profile managed block) is appended to
|
|
559
|
+
the template governance so the anchor's managed block carries both. A
|
|
560
|
+
unified diff is recorded on the result. When the change is a non-additive
|
|
561
|
+
overwrite of an existing file and an *authorize* gate is supplied, the gate
|
|
562
|
+
is consulted per target; declining skips the write and leaves the operator
|
|
563
|
+
file untouched.
|
|
564
|
+
"""
|
|
565
|
+
content = render_content_tokens(
|
|
566
|
+
src.read_text(encoding="utf-8"),
|
|
567
|
+
harness_root=harness_root,
|
|
568
|
+
project_root=project_root,
|
|
569
|
+
)
|
|
570
|
+
if entry.mode == "sentinel_merge" and profile_body:
|
|
571
|
+
content = f"{content.rstrip()}\n\n{profile_body}"
|
|
572
|
+
existed = target.exists()
|
|
573
|
+
before = ""
|
|
574
|
+
if existed:
|
|
575
|
+
with contextlib.suppress(OSError):
|
|
576
|
+
before = target.read_text(encoding="utf-8")
|
|
577
|
+
merged = _operator_owned_merge_text(entry, target, content, existed=existed)
|
|
578
|
+
diff = _unified_diff(before, merged, target)
|
|
579
|
+
detail: dict[str, str] = {"ownership_class": entry.ownership_class}
|
|
580
|
+
if diff:
|
|
581
|
+
detail["diff"] = diff
|
|
582
|
+
if existed and before != merged:
|
|
583
|
+
detail["destructive_gate"] = "required"
|
|
584
|
+
if authorize is not None and not authorize(
|
|
585
|
+
AuthorizationRequest(
|
|
586
|
+
harness=harness_name,
|
|
587
|
+
path=_path_text(target),
|
|
588
|
+
operation=entry.mode,
|
|
589
|
+
ownership_class=entry.ownership_class,
|
|
590
|
+
diff=diff,
|
|
591
|
+
)
|
|
592
|
+
):
|
|
593
|
+
return [
|
|
594
|
+
_with_detail(
|
|
595
|
+
_result(
|
|
596
|
+
"skipped",
|
|
597
|
+
entry.mode,
|
|
598
|
+
target,
|
|
599
|
+
"operator declined the managed-block change",
|
|
600
|
+
source=src,
|
|
601
|
+
),
|
|
602
|
+
detail,
|
|
603
|
+
)
|
|
604
|
+
]
|
|
605
|
+
result = write_bytes_safely(
|
|
606
|
+
target,
|
|
607
|
+
merged.encode("utf-8"),
|
|
608
|
+
install_root=root,
|
|
609
|
+
harness_name=harness_name,
|
|
610
|
+
operation=entry.mode,
|
|
611
|
+
source=src,
|
|
612
|
+
allowed_root=allowed_root,
|
|
613
|
+
)
|
|
614
|
+
return [_with_detail(result, detail)]
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def apply_sentinel_merge(
|
|
618
|
+
entry: InstallEntry,
|
|
619
|
+
*,
|
|
620
|
+
harness_root: Path | None = None,
|
|
621
|
+
project_root: Path | None = None,
|
|
622
|
+
harness_name: str = "manual",
|
|
623
|
+
authorize: AuthorizeFn | None = None,
|
|
624
|
+
profile_body: str | None = None,
|
|
625
|
+
) -> list[MaterializationResult]:
|
|
626
|
+
"""Apply a ``sentinel_merge`` entry: fold a managed block into an anchor.
|
|
627
|
+
|
|
628
|
+
When *profile_body* is supplied (the projected shared profile), it is folded
|
|
629
|
+
into the anchor's managed block alongside the template governance.
|
|
630
|
+
"""
|
|
631
|
+
target = resolve_target(
|
|
632
|
+
entry.target, harness_root=harness_root, project_root=project_root
|
|
633
|
+
)
|
|
634
|
+
src = resolve_source(entry.source)
|
|
635
|
+
if not src.is_file():
|
|
636
|
+
return [
|
|
637
|
+
_result(
|
|
638
|
+
"skipped",
|
|
639
|
+
"sentinel_merge",
|
|
640
|
+
target,
|
|
641
|
+
"source file does not exist",
|
|
642
|
+
source=src,
|
|
643
|
+
)
|
|
644
|
+
]
|
|
645
|
+
return _apply_operator_owned_file(
|
|
646
|
+
entry,
|
|
647
|
+
target=target,
|
|
648
|
+
src=src,
|
|
649
|
+
root=_root_for(harness_root, project_root),
|
|
650
|
+
harness_name=harness_name,
|
|
651
|
+
allowed_root=_allowed_write_root(harness_root, project_root),
|
|
652
|
+
authorize=authorize,
|
|
653
|
+
harness_root=harness_root,
|
|
654
|
+
project_root=project_root,
|
|
655
|
+
profile_body=profile_body,
|
|
656
|
+
)
|