@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,347 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
"""Agnostic, operator-portable memory surface for the durable knowledge store.
|
|
4
|
+
|
|
5
|
+
The memory surface holds durable knowledge records — stable facts, ratified
|
|
6
|
+
conventions, operator preferences, derived insights, and external references.
|
|
7
|
+
Records are fully agnostic: a record carries no tool-specific or vendor-specific
|
|
8
|
+
identifier, so the same record round-trips between any two installation targets
|
|
9
|
+
without loss.
|
|
10
|
+
|
|
11
|
+
A :class:`MemoryStore` is bound to a :class:`~apothem.lib.data_home.DataHome`'s
|
|
12
|
+
``memory`` directory and persists every record to a single canonical
|
|
13
|
+
``records.json`` file. Serialization is deterministic — records are sorted by
|
|
14
|
+
id and emitted with sorted keys and stable indentation — so two stores holding
|
|
15
|
+
the same record set produce byte-identical files, which is what makes the
|
|
16
|
+
export/import round-trip a byte-for-byte guarantee.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
from collections.abc import Mapping, Sequence
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Final, cast
|
|
26
|
+
|
|
27
|
+
from jsonschema import Draft202012Validator
|
|
28
|
+
|
|
29
|
+
from apothem.lib.atomic_io import advisory_lock, write_bytes_atomically
|
|
30
|
+
from apothem.lib.data_home import DataHome
|
|
31
|
+
from apothem.schemas import memory_record_schema_path
|
|
32
|
+
|
|
33
|
+
#: The canonical filename, under a data home's ``memory`` dir, holding the
|
|
34
|
+
#: serialized JSON array of records.
|
|
35
|
+
_RECORDS_FILENAME: Final[str] = "records.json"
|
|
36
|
+
|
|
37
|
+
#: The advisory-lock filename guarding the records read-modify-write window.
|
|
38
|
+
_RECORDS_LOCK_FILENAME: Final[str] = ".records.lock"
|
|
39
|
+
|
|
40
|
+
#: The canonical serialization of an empty store — a JSON array with the same
|
|
41
|
+
#: indentation and trailing newline a populated store would carry.
|
|
42
|
+
_EMPTY_CANONICAL: Final[bytes] = b"[]\n"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class MemoryError(ValueError):
|
|
46
|
+
"""Raised when a memory record fails schema validation."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _schema() -> dict[str, object]:
|
|
50
|
+
"""Load and parse the memory-record JSON schema.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
The parsed schema document.
|
|
54
|
+
"""
|
|
55
|
+
raw = memory_record_schema_path().read_text(encoding="utf-8")
|
|
56
|
+
return cast("dict[str, object]", json.loads(raw))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def validate_record(data: Mapping[str, object]) -> None:
|
|
60
|
+
"""Validate *data* against the memory-record schema.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
data: The candidate record mapping to validate.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
MemoryError: When *data* violates the schema. The message lists every
|
|
67
|
+
validation error discovered.
|
|
68
|
+
"""
|
|
69
|
+
validator = Draft202012Validator(_schema())
|
|
70
|
+
errors = sorted(validator.iter_errors(dict(data)), key=lambda error: error.path)
|
|
71
|
+
if errors:
|
|
72
|
+
details = "; ".join(error.message for error in errors)
|
|
73
|
+
raise MemoryError(f"invalid memory record: {details}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class MemoryRecord:
|
|
78
|
+
"""One durable, agnostic knowledge record in the memory surface.
|
|
79
|
+
|
|
80
|
+
The record shape mirrors the memory-record schema and carries no
|
|
81
|
+
harness-specific or vendor-specific field, so it is portable across any
|
|
82
|
+
installation target.
|
|
83
|
+
|
|
84
|
+
Attributes:
|
|
85
|
+
id: Stable record identifier, unique within a store.
|
|
86
|
+
title: One-line summary used to decide relevance during recall.
|
|
87
|
+
body: The knowledge itself, stated specifically.
|
|
88
|
+
kind: Record taxonomy; one of ``fact``, ``convention``, ``preference``,
|
|
89
|
+
``insight``, or ``reference``.
|
|
90
|
+
created: ISO 8601 date-time the record was first written.
|
|
91
|
+
tags: Discovery tags for recall filtering.
|
|
92
|
+
updated: ISO 8601 date-time the record was last amended, if any.
|
|
93
|
+
source: Provenance of the knowledge, if recorded.
|
|
94
|
+
confidence: Optional confidence in the record's accuracy, in ``[0, 1]``.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
id: str
|
|
98
|
+
title: str
|
|
99
|
+
body: str
|
|
100
|
+
kind: str
|
|
101
|
+
created: str
|
|
102
|
+
tags: tuple[str, ...] = ()
|
|
103
|
+
updated: str | None = None
|
|
104
|
+
source: str | None = None
|
|
105
|
+
confidence: float | None = None
|
|
106
|
+
|
|
107
|
+
def to_dict(self) -> dict[str, object]:
|
|
108
|
+
"""Render the record as a schema-conformant mapping.
|
|
109
|
+
|
|
110
|
+
Optional keys are omitted when unset (``None`` scalars, empty ``tags``)
|
|
111
|
+
so the output validates under ``additionalProperties: false`` and
|
|
112
|
+
round-trips cleanly.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
A JSON-serializable mapping of the record's set fields.
|
|
116
|
+
"""
|
|
117
|
+
data: dict[str, object] = {
|
|
118
|
+
"id": self.id,
|
|
119
|
+
"title": self.title,
|
|
120
|
+
"body": self.body,
|
|
121
|
+
"kind": self.kind,
|
|
122
|
+
"created": self.created,
|
|
123
|
+
}
|
|
124
|
+
if self.tags:
|
|
125
|
+
data["tags"] = list(self.tags)
|
|
126
|
+
if self.updated is not None:
|
|
127
|
+
data["updated"] = self.updated
|
|
128
|
+
if self.source is not None:
|
|
129
|
+
data["source"] = self.source
|
|
130
|
+
if self.confidence is not None:
|
|
131
|
+
data["confidence"] = self.confidence
|
|
132
|
+
return data
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def from_dict(cls, data: Mapping[str, object]) -> MemoryRecord:
|
|
136
|
+
"""Reconstruct a record from a schema-conformant mapping.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
data: A mapping carrying at least the required record fields.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
The reconstructed :class:`MemoryRecord`.
|
|
143
|
+
"""
|
|
144
|
+
raw_tags = data.get("tags", ())
|
|
145
|
+
tags: tuple[str, ...] = ()
|
|
146
|
+
if isinstance(raw_tags, Sequence) and not isinstance(raw_tags, (str, bytes)):
|
|
147
|
+
tags = tuple(str(tag) for tag in raw_tags)
|
|
148
|
+
|
|
149
|
+
updated = data.get("updated")
|
|
150
|
+
source = data.get("source")
|
|
151
|
+
raw_confidence = data.get("confidence")
|
|
152
|
+
confidence: float | None = None
|
|
153
|
+
if isinstance(raw_confidence, (int, float)) and not isinstance(
|
|
154
|
+
raw_confidence, bool
|
|
155
|
+
):
|
|
156
|
+
confidence = float(raw_confidence)
|
|
157
|
+
return cls(
|
|
158
|
+
id=str(data["id"]),
|
|
159
|
+
title=str(data["title"]),
|
|
160
|
+
body=str(data["body"]),
|
|
161
|
+
kind=str(data["kind"]),
|
|
162
|
+
created=str(data["created"]),
|
|
163
|
+
tags=tags,
|
|
164
|
+
updated=None if updated is None else str(updated),
|
|
165
|
+
source=None if source is None else str(source),
|
|
166
|
+
confidence=confidence,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _serialize(records: Sequence[MemoryRecord]) -> bytes:
|
|
171
|
+
"""Serialize *records* into the canonical on-disk byte form.
|
|
172
|
+
|
|
173
|
+
Records are sorted by id and emitted with sorted keys, two-space
|
|
174
|
+
indentation, and a trailing newline, so the same record set always yields
|
|
175
|
+
byte-identical output.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
records: The records to serialize.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
The canonical serialized bytes.
|
|
182
|
+
"""
|
|
183
|
+
payload = [record.to_dict() for record in sorted(records, key=lambda r: r.id)]
|
|
184
|
+
text = json.dumps(payload, indent=2, sort_keys=True, ensure_ascii=False)
|
|
185
|
+
return (text + "\n").encode("utf-8")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _parse(data: bytes) -> list[MemoryRecord]:
|
|
189
|
+
"""Parse canonical bytes into records, validating each.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
data: The serialized JSON array of records.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
The parsed records.
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
MemoryError: When the payload is not a JSON array or any record fails
|
|
199
|
+
schema validation.
|
|
200
|
+
"""
|
|
201
|
+
loaded = json.loads(data.decode("utf-8"))
|
|
202
|
+
if not isinstance(loaded, list):
|
|
203
|
+
raise MemoryError("memory payload must be a JSON array of records")
|
|
204
|
+
records: list[MemoryRecord] = []
|
|
205
|
+
for item in loaded:
|
|
206
|
+
if not isinstance(item, Mapping):
|
|
207
|
+
raise MemoryError("each memory record must be a JSON object")
|
|
208
|
+
validate_record(item)
|
|
209
|
+
records.append(MemoryRecord.from_dict(item))
|
|
210
|
+
return records
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class MemoryStore:
|
|
214
|
+
"""A canonical, deterministic on-disk store of memory records.
|
|
215
|
+
|
|
216
|
+
The store persists to a single ``records.json`` file under a data home's
|
|
217
|
+
``memory`` directory. All mutations re-serialize the full record set
|
|
218
|
+
canonically, so the file is always byte-stable for a given set of records.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
def __init__(self, data_home: DataHome) -> None:
|
|
222
|
+
"""Bind the store to *data_home*'s memory directory.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
data_home: The per-target data home whose ``memory`` directory
|
|
226
|
+
holds this store's canonical records file.
|
|
227
|
+
"""
|
|
228
|
+
self._memory_dir = data_home.memory
|
|
229
|
+
self._records_path = data_home.memory / _RECORDS_FILENAME
|
|
230
|
+
self._lock_path = data_home.memory / _RECORDS_LOCK_FILENAME
|
|
231
|
+
|
|
232
|
+
def records(self) -> list[MemoryRecord]:
|
|
233
|
+
"""Load every record from disk.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
The stored records, or an empty list when the file is absent.
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
MemoryError: When the on-disk payload is malformed.
|
|
240
|
+
"""
|
|
241
|
+
if not self._records_path.exists():
|
|
242
|
+
return []
|
|
243
|
+
return _parse(self._records_path.read_bytes())
|
|
244
|
+
|
|
245
|
+
def add(self, record: MemoryRecord) -> None:
|
|
246
|
+
"""Validate and persist *record*, replacing any record with its id.
|
|
247
|
+
|
|
248
|
+
The store directory is created if absent. Re-adding a record whose id
|
|
249
|
+
already exists replaces the prior entry, so the operation is idempotent
|
|
250
|
+
on identity.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
record: The record to persist.
|
|
254
|
+
|
|
255
|
+
Raises:
|
|
256
|
+
MemoryError: When *record* fails schema validation.
|
|
257
|
+
"""
|
|
258
|
+
validate_record(record.to_dict())
|
|
259
|
+
self._memory_dir.mkdir(parents=True, exist_ok=True)
|
|
260
|
+
# Serialize the read-modify-write window so a concurrent add cannot read
|
|
261
|
+
# a stale record set and clobber the other writer's record.
|
|
262
|
+
with advisory_lock(self._lock_path):
|
|
263
|
+
existing = [r for r in self.records() if r.id != record.id]
|
|
264
|
+
existing.append(record)
|
|
265
|
+
self._write(existing)
|
|
266
|
+
|
|
267
|
+
def contains(self, record_id: str) -> bool:
|
|
268
|
+
"""Report whether a record with *record_id* is stored.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
record_id: The identifier to look up.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
``True`` when a record with the id exists, else ``False``.
|
|
275
|
+
"""
|
|
276
|
+
return any(record.id == record_id for record in self.records())
|
|
277
|
+
|
|
278
|
+
def count(self) -> int:
|
|
279
|
+
"""Count the stored records.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
The number of records on disk.
|
|
283
|
+
"""
|
|
284
|
+
return len(self.records())
|
|
285
|
+
|
|
286
|
+
def ensure_initialized(self) -> Path:
|
|
287
|
+
"""Create the canonical records file as an empty store when absent.
|
|
288
|
+
|
|
289
|
+
Materialization calls this so a freshly-installed target carries a
|
|
290
|
+
concrete, empty memory artifact. The operation is idempotent and
|
|
291
|
+
non-destructive: when the file already exists its records are left
|
|
292
|
+
untouched, so re-materializing a populated store never loses data.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
The path to the canonical records file.
|
|
296
|
+
"""
|
|
297
|
+
# Serialize the check-then-write under the same lock add()/import_bytes()
|
|
298
|
+
# hold, so a concurrent first add() cannot create the file with a record
|
|
299
|
+
# between the existence check and the empty-store write that clobbers it.
|
|
300
|
+
with advisory_lock(self._lock_path):
|
|
301
|
+
if not self._records_path.exists():
|
|
302
|
+
self._write([])
|
|
303
|
+
return self._records_path
|
|
304
|
+
|
|
305
|
+
def export_bytes(self) -> bytes:
|
|
306
|
+
"""Export the store's canonical serialized bytes.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
The bytes on disk, or the canonical empty array when absent.
|
|
310
|
+
"""
|
|
311
|
+
if not self._records_path.exists():
|
|
312
|
+
return _EMPTY_CANONICAL
|
|
313
|
+
return self._records_path.read_bytes()
|
|
314
|
+
|
|
315
|
+
def import_bytes(self, data: bytes) -> None:
|
|
316
|
+
"""Replace the store's contents with the records parsed from *data*.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
data: A canonical serialized JSON array of records.
|
|
320
|
+
|
|
321
|
+
Raises:
|
|
322
|
+
MemoryError: When the payload is malformed or any record is invalid.
|
|
323
|
+
"""
|
|
324
|
+
# Parse outside the lock (it reads only the input, never the store),
|
|
325
|
+
# then serialize the full-replace write under the same advisory lock the
|
|
326
|
+
# add() read-modify-write holds. Without the lock a concurrent add()
|
|
327
|
+
# could interleave with this unlocked write and lose records.
|
|
328
|
+
records = _parse(data)
|
|
329
|
+
with advisory_lock(self._lock_path):
|
|
330
|
+
self._write(records)
|
|
331
|
+
|
|
332
|
+
def _write(self, records: Sequence[MemoryRecord]) -> None:
|
|
333
|
+
"""Persist *records* canonically, creating the directory if absent.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
records: The records to write.
|
|
337
|
+
"""
|
|
338
|
+
self._memory_dir.mkdir(parents=True, exist_ok=True)
|
|
339
|
+
write_bytes_atomically(self._records_path, _serialize(records))
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
__all__ = [
|
|
343
|
+
"MemoryError",
|
|
344
|
+
"MemoryRecord",
|
|
345
|
+
"MemoryStore",
|
|
346
|
+
"validate_record",
|
|
347
|
+
]
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
"""Parallel file-sweep utilities backed by ProcessPoolExecutor.
|
|
4
|
+
|
|
5
|
+
Why this module exists. Many ecosystem tools sweep across hundreds of
|
|
6
|
+
files at a time (authorship-header sweeps at 200+ files, audit scanners
|
|
7
|
+
at 200+ files, link-checkers across the docs tree).
|
|
8
|
+
Sequential per-file processing on these tools dominates wall-clock
|
|
9
|
+
time on operations whose per-file work exceeds process-spawn overhead.
|
|
10
|
+
This module provides a generic ProcessPoolExecutor-backed parallel
|
|
11
|
+
helper plus a sequential fallback.
|
|
12
|
+
|
|
13
|
+
When to use vs. avoid. Parallelism helps when:
|
|
14
|
+
|
|
15
|
+
- Per-file work exceeds ProcessPoolExecutor spawn overhead (~50-200ms
|
|
16
|
+
per worker on Windows, ~10-50ms on POSIX).
|
|
17
|
+
- File count is high (≥ 20 files for a noticeable wall-clock win).
|
|
18
|
+
- Per-file work is CPU-bound (matcher pattern-matching, regex, parse).
|
|
19
|
+
|
|
20
|
+
Parallelism HURTS when per-file work is sub-millisecond (for example,
|
|
21
|
+
the conformity-gate orchestrator's per-matcher work is below process
|
|
22
|
+
spawn overhead). Choose the API consciously; benchmark before committing.
|
|
23
|
+
|
|
24
|
+
Public API. Two callables — `parallel_grep` for pattern-matching across
|
|
25
|
+
files (returns Match records); `parallel_apply` for arbitrary per-file
|
|
26
|
+
function application (returns the function's per-file results). Both
|
|
27
|
+
honor `max_workers=1` as the sequential fallback signal.
|
|
28
|
+
|
|
29
|
+
Current production callers. None today. This module is kept available for
|
|
30
|
+
tools that meet the criteria above and is exercised by
|
|
31
|
+
``tests/unit/test_parallel_sweep.py``; the conformity-gate orchestrator runs
|
|
32
|
+
its matchers sequentially because their per-matcher work sits below
|
|
33
|
+
process-spawn overhead. The absence of a production caller is deliberate, not
|
|
34
|
+
neglect — adopt this module only after a benchmark confirms a win.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import os
|
|
40
|
+
import re
|
|
41
|
+
from collections.abc import Callable
|
|
42
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
43
|
+
from dataclasses import dataclass
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
from typing import Final, TypeVar
|
|
46
|
+
|
|
47
|
+
T = TypeVar("T")
|
|
48
|
+
|
|
49
|
+
DEFAULT_MAX_WORKERS: Final[int] = os.cpu_count() or 4
|
|
50
|
+
SEQUENTIAL_THRESHOLD: Final[int] = 1
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ParallelSweepError(Exception):
|
|
54
|
+
"""Raised when a parallel-sweep invocation cannot complete.
|
|
55
|
+
|
|
56
|
+
Wraps lower-level concurrent.futures errors with the originating
|
|
57
|
+
file path so callers can route per-file failures without parsing
|
|
58
|
+
the underlying executor's exception chain.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True, slots=True)
|
|
63
|
+
class Match:
|
|
64
|
+
"""A single pattern match found in a file by `parallel_grep`.
|
|
65
|
+
|
|
66
|
+
Attributes:
|
|
67
|
+
path: The file the match was found in (relative or absolute,
|
|
68
|
+
preserved from the input).
|
|
69
|
+
line: 1-based line number of the match.
|
|
70
|
+
text: The full text of the matching line, with trailing
|
|
71
|
+
newline stripped.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
path: Path
|
|
75
|
+
line: int
|
|
76
|
+
text: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def parallel_grep(
|
|
80
|
+
paths: list[Path],
|
|
81
|
+
pattern: str,
|
|
82
|
+
max_workers: int | None = None,
|
|
83
|
+
) -> list[Match]:
|
|
84
|
+
"""Search `paths` in parallel for lines matching `pattern`.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
paths: Files to search. Directory paths are NOT recursed; the
|
|
88
|
+
caller resolves directory expansion before invocation.
|
|
89
|
+
pattern: Regular-expression pattern compiled with `re.compile`
|
|
90
|
+
on each worker. The pattern is line-oriented; multiline
|
|
91
|
+
patterns require pre-compilation by the caller and use of
|
|
92
|
+
`parallel_apply` instead.
|
|
93
|
+
max_workers: Worker process count. `None` resolves to
|
|
94
|
+
`DEFAULT_MAX_WORKERS` (the host's CPU count, fallback 4).
|
|
95
|
+
`1` activates the sequential fallback (no executor
|
|
96
|
+
spawned; useful for measurement comparison or when the
|
|
97
|
+
host disallows multi-process execution).
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
A flat list of `Match` records across all files, ordered by
|
|
101
|
+
the input path order then by line number within each file.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ParallelSweepError: When a worker raises an unhandled exception
|
|
105
|
+
on a file. The wrapped exception carries the file path.
|
|
106
|
+
"""
|
|
107
|
+
workers = max_workers if max_workers is not None else DEFAULT_MAX_WORKERS
|
|
108
|
+
if workers <= SEQUENTIAL_THRESHOLD:
|
|
109
|
+
return _grep_sequential(paths, pattern)
|
|
110
|
+
return _grep_parallel(paths, pattern, workers)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def parallel_apply(
|
|
114
|
+
paths: list[Path],
|
|
115
|
+
func: Callable[[Path], T],
|
|
116
|
+
max_workers: int | None = None,
|
|
117
|
+
) -> list[T]:
|
|
118
|
+
"""Apply `func` to each path in `paths` in parallel.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
paths: Files (or any path-like inputs) to apply `func` to.
|
|
122
|
+
func: A pure callable taking one `Path` and returning a value.
|
|
123
|
+
Must be picklable (defined at module scope, not a lambda
|
|
124
|
+
or nested closure) for ProcessPoolExecutor marshalling.
|
|
125
|
+
max_workers: Worker process count. `None` resolves to
|
|
126
|
+
`DEFAULT_MAX_WORKERS`. `1` activates the sequential
|
|
127
|
+
fallback.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Per-path results in the input path order. The order invariant
|
|
131
|
+
is enforced via path-keyed result collection (results gathered
|
|
132
|
+
via `as_completed` then re-sorted).
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ParallelSweepError: When `func` raises on any path. The wrapped
|
|
136
|
+
exception carries the file path.
|
|
137
|
+
"""
|
|
138
|
+
workers = max_workers if max_workers is not None else DEFAULT_MAX_WORKERS
|
|
139
|
+
if workers <= SEQUENTIAL_THRESHOLD:
|
|
140
|
+
return [func(p) for p in paths]
|
|
141
|
+
return _apply_parallel(paths, func, workers)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _grep_sequential(paths: list[Path], pattern: str) -> list[Match]:
|
|
145
|
+
"""Sequential grep fallback (no executor)."""
|
|
146
|
+
compiled = re.compile(pattern)
|
|
147
|
+
matches: list[Match] = []
|
|
148
|
+
for path in paths:
|
|
149
|
+
matches.extend(_grep_one(path, compiled))
|
|
150
|
+
return matches
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _grep_parallel(
|
|
154
|
+
paths: list[Path],
|
|
155
|
+
pattern: str,
|
|
156
|
+
workers: int,
|
|
157
|
+
) -> list[Match]:
|
|
158
|
+
"""Parallel grep via ProcessPoolExecutor."""
|
|
159
|
+
results: dict[Path, list[Match]] = {}
|
|
160
|
+
with ProcessPoolExecutor(max_workers=workers) as executor:
|
|
161
|
+
future_to_path = {
|
|
162
|
+
executor.submit(_grep_one_compiled, path, pattern): path for path in paths
|
|
163
|
+
}
|
|
164
|
+
for future in as_completed(future_to_path):
|
|
165
|
+
path = future_to_path[future]
|
|
166
|
+
try:
|
|
167
|
+
results[path] = future.result()
|
|
168
|
+
except Exception as exc:
|
|
169
|
+
raise ParallelSweepError(
|
|
170
|
+
f"parallel_grep failed on {path}: {exc!r}",
|
|
171
|
+
) from exc
|
|
172
|
+
flattened: list[Match] = []
|
|
173
|
+
for path in paths:
|
|
174
|
+
flattened.extend(results.get(path, []))
|
|
175
|
+
return flattened
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _apply_parallel(
|
|
179
|
+
paths: list[Path],
|
|
180
|
+
func: Callable[[Path], T],
|
|
181
|
+
workers: int,
|
|
182
|
+
) -> list[T]:
|
|
183
|
+
"""Parallel apply via ProcessPoolExecutor with order preservation."""
|
|
184
|
+
# Key by position, not by path: a duplicate path in *paths* submits two
|
|
185
|
+
# distinct futures, and keying results by path would collapse them — the
|
|
186
|
+
# second result would overwrite the first and the output list would carry
|
|
187
|
+
# one entry twice. Position keys preserve one result per input slot.
|
|
188
|
+
results: dict[int, T] = {}
|
|
189
|
+
with ProcessPoolExecutor(max_workers=workers) as executor:
|
|
190
|
+
future_to_index = {
|
|
191
|
+
executor.submit(func, path): index for index, path in enumerate(paths)
|
|
192
|
+
}
|
|
193
|
+
for future in as_completed(future_to_index):
|
|
194
|
+
index = future_to_index[future]
|
|
195
|
+
try:
|
|
196
|
+
results[index] = future.result()
|
|
197
|
+
except Exception as exc:
|
|
198
|
+
raise ParallelSweepError(
|
|
199
|
+
f"parallel_apply failed on {paths[index]}: {exc!r}",
|
|
200
|
+
) from exc
|
|
201
|
+
return [results[index] for index in range(len(paths))]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _grep_one(path: Path, compiled: re.Pattern[str]) -> list[Match]:
|
|
205
|
+
"""Grep a single file (sequential-fallback path).
|
|
206
|
+
|
|
207
|
+
Splits the read error path so non-UTF-8 byte content (binaries, mixed
|
|
208
|
+
encodings) is still scanned with a lossy decode rather than silently
|
|
209
|
+
dropped as "no matches" alongside legitimately unreadable files.
|
|
210
|
+
"""
|
|
211
|
+
try:
|
|
212
|
+
text = path.read_text(encoding="utf-8")
|
|
213
|
+
except OSError:
|
|
214
|
+
return []
|
|
215
|
+
except UnicodeDecodeError:
|
|
216
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
217
|
+
return [
|
|
218
|
+
Match(path=path, line=lineno, text=line)
|
|
219
|
+
for lineno, line in enumerate(text.splitlines(), start=1)
|
|
220
|
+
if compiled.search(line)
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _grep_one_compiled(path: Path, pattern: str) -> list[Match]:
|
|
225
|
+
"""Grep a single file in a worker process (compiles pattern per call).
|
|
226
|
+
|
|
227
|
+
The pattern is recompiled per worker call rather than passed as a
|
|
228
|
+
pre-compiled `re.Pattern` because compiled patterns are not always
|
|
229
|
+
cleanly picklable across worker process boundaries on every Python
|
|
230
|
+
runtime; recompilation per call is fast (microseconds) and removes
|
|
231
|
+
the marshalling-fragility surface.
|
|
232
|
+
"""
|
|
233
|
+
compiled = re.compile(pattern)
|
|
234
|
+
return _grep_one(path, compiled)
|