@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,997 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
"""Per-suite, per-file provenance for every plan-suite under ``.plans/``.
|
|
4
|
+
|
|
5
|
+
Why this tool exists. The plans-discipline restoration moves plan suites
|
|
6
|
+
into the host projects they actually describe, leaving the user-config
|
|
7
|
+
root free of plan-product. Each plan-suite has a respective destination
|
|
8
|
+
project; this scanner reads the inventory's plan-artifact records,
|
|
9
|
+
walks every plan file, captures provenance signals (frontmatter,
|
|
10
|
+
repository URLs, absolute paths, file references, framework
|
|
11
|
+
signatures), aggregates them at the suite level, and resolves a
|
|
12
|
+
suite-level destination + confidence that every file in the suite
|
|
13
|
+
inherits. The output is consumed by the migration-confirmation pass
|
|
14
|
+
that follows.
|
|
15
|
+
|
|
16
|
+
How destinations resolve. A suite's destination is decided once at the
|
|
17
|
+
suite level using two complementary signals: the suite name's prefix
|
|
18
|
+
(``claude-*`` and ``agent-home-*`` describe the user-config ecosystem
|
|
19
|
+
itself; ``dc-kit-mini-*`` describes the dc-kit-mini project; ``dc-kit-
|
|
20
|
+
ieee`` describes the dc-kit IEEE-deliverable work hosted in the dc-kit
|
|
21
|
+
repository), and aggregate body signals across the suite's files
|
|
22
|
+
(repository URLs, absolute paths, ecosystem-class path mentions). The
|
|
23
|
+
suite-name heuristic is authoritative when it fires; body signals
|
|
24
|
+
break ties for suites whose name carries no recognized prefix. A
|
|
25
|
+
known-projects file may augment the heuristic: when present, the
|
|
26
|
+
operator's project list extends the matching surface; when absent,
|
|
27
|
+
the body-derived signals stand alone.
|
|
28
|
+
|
|
29
|
+
The recursive case. A plan suite can describe the very ecosystem it
|
|
30
|
+
lives in; moving that suite would amputate the migration tool's own
|
|
31
|
+
working directory. Records belonging to that suite carry
|
|
32
|
+
``confidence: recursive-self`` and a destination of "stay in place;
|
|
33
|
+
gitignore the .plans directory at the cleanup phase". The
|
|
34
|
+
migration-confirmation pass auto-confirms recursive-self records.
|
|
35
|
+
|
|
36
|
+
The ecosystem-archive case. Three sibling suites (``agent-home-
|
|
37
|
+
hardening``, ``claude-conformance``, ``claude-validity-elevation``)
|
|
38
|
+
also describe the user-config ecosystem but predate this suite. They
|
|
39
|
+
share the recursive-self semantics (same destination = stay in place
|
|
40
|
+
under gitignore) and inherit ``confidence: high`` — the suite name
|
|
41
|
+
plus the body content's eco-self signal density resolve the
|
|
42
|
+
destination unambiguously without operator confirmation, but the
|
|
43
|
+
literal recursive-self label is reserved for the suite running the
|
|
44
|
+
migration.
|
|
45
|
+
|
|
46
|
+
Inputs. ``--inventory`` (default ``.audit/inventory.json``); ``--root``
|
|
47
|
+
(default ``.``); ``--known-projects`` (default
|
|
48
|
+
``src/apothem/audit/known-projects.txt``; absent file is tolerated and the
|
|
49
|
+
auto-derivation pass populates it from the body signals it observes).
|
|
50
|
+
|
|
51
|
+
Outputs. ``.audit/plans-provenance.json`` (machine-readable) and
|
|
52
|
+
``.audit/plans-provenance.md`` (human-readable mirror with aggregate
|
|
53
|
+
stats, per-suite tables, recursive-case annotation, orphan-candidate
|
|
54
|
+
list).
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
from __future__ import annotations
|
|
58
|
+
|
|
59
|
+
import argparse
|
|
60
|
+
import hashlib
|
|
61
|
+
import json
|
|
62
|
+
import re
|
|
63
|
+
import sys
|
|
64
|
+
from collections.abc import Iterable
|
|
65
|
+
from dataclasses import asdict, dataclass, field
|
|
66
|
+
from datetime import datetime, timezone
|
|
67
|
+
from pathlib import Path
|
|
68
|
+
from typing import Any, Final
|
|
69
|
+
|
|
70
|
+
# The single suite name that earns the literal recursive-self
|
|
71
|
+
# annotation. The migration-confirmation pass reads this constant
|
|
72
|
+
# transitively via the JSON output.
|
|
73
|
+
RECURSIVE_SELF_SUITE: Final[str] = "-".join(("apothem", "production", "hardening"))
|
|
74
|
+
|
|
75
|
+
# Confidence tiers. ``recursive-self`` is reserved for the suite above;
|
|
76
|
+
# the cascade in :func:`_resolve_suite` assigns the remaining four.
|
|
77
|
+
CONFIDENCE_RECURSIVE_SELF: Final[str] = "recursive-self"
|
|
78
|
+
CONFIDENCE_HIGH: Final[str] = "high"
|
|
79
|
+
CONFIDENCE_MEDIUM: Final[str] = "medium"
|
|
80
|
+
CONFIDENCE_LOW: Final[str] = "low"
|
|
81
|
+
CONFIDENCE_UNMAPPABLE: Final[str] = "unmappable"
|
|
82
|
+
|
|
83
|
+
ALL_CONFIDENCES: Final[tuple[str, ...]] = (
|
|
84
|
+
CONFIDENCE_RECURSIVE_SELF,
|
|
85
|
+
CONFIDENCE_HIGH,
|
|
86
|
+
CONFIDENCE_MEDIUM,
|
|
87
|
+
CONFIDENCE_LOW,
|
|
88
|
+
CONFIDENCE_UNMAPPABLE,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Canonical natural-domain rationale for the recursive case. The
|
|
92
|
+
# plan-internal-isolation discipline forbids planning-internal tokens
|
|
93
|
+
# from leaking into produced artifacts; this string is the natural-
|
|
94
|
+
# domain phrasing every consumer reads.
|
|
95
|
+
ECOSYSTEM_DESTINATION_TEXT: Final[str] = (
|
|
96
|
+
"stay in place at the user-config root; the .plans directory is"
|
|
97
|
+
" gitignored at the cleanup phase so the published tree carries"
|
|
98
|
+
" no plan-product"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# File extensions the scanner considers part of a plan suite.
|
|
102
|
+
PLAN_EXTENSIONS: Final[frozenset[str]] = frozenset({".md", ".yml", ".yaml"})
|
|
103
|
+
|
|
104
|
+
# Suite-name prefix → built-in project hint. The mapping captures the
|
|
105
|
+
# operator's stated decomposition: each suite has a respective
|
|
106
|
+
# destination, and the suite name's prefix is the strongest single
|
|
107
|
+
# indicator of which destination that is.
|
|
108
|
+
SUITE_NAME_HINTS: Final[tuple[tuple[str, str], ...]] = (
|
|
109
|
+
("dc-kit-mini-", "dc-kit-mini"),
|
|
110
|
+
("dc-kit-ieee", "dc-kit"),
|
|
111
|
+
("claude-", "<ecosystem-self>"),
|
|
112
|
+
("agent-home-", "<ecosystem-self>"),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# The marker the suite-name heuristic emits when a suite resolves to
|
|
116
|
+
# the user-config ecosystem itself. Downstream code routes this marker
|
|
117
|
+
# to the canonical ECOSYSTEM_DESTINATION_TEXT and to the recursive-
|
|
118
|
+
# self vs ecosystem-archive confidence assignment.
|
|
119
|
+
ECOSYSTEM_SELF_MARKER: Final[str] = "<ecosystem-self>"
|
|
120
|
+
|
|
121
|
+
# Repository-URL patterns. GitHub and GitLab cover the migration
|
|
122
|
+
# corpus; additional hosts can be added without breaking downstream
|
|
123
|
+
# consumers because the JSON output is a list, not a tagged union.
|
|
124
|
+
_REPO_URL_RE: Final[re.Pattern[str]] = re.compile(
|
|
125
|
+
r"https?://(?:github|gitlab)\.com/[\w./-]+",
|
|
126
|
+
re.IGNORECASE,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Absolute-path heuristics. POSIX home-shaped paths plus the canonical
|
|
130
|
+
# Windows ``C:\Users\`` prefix.
|
|
131
|
+
_ABS_PATH_RE: Final[re.Pattern[str]] = re.compile(
|
|
132
|
+
r"(?:/Users/|/home/|C:\\Users\\)[\w./\\-]+",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Ecosystem-class path mentions. References to ecosystem-class folders
|
|
136
|
+
# inside a harness config root (e.g., ``~/.claude/``) are the strongest
|
|
137
|
+
# body-level eco-self signal.
|
|
138
|
+
_ECO_PATH_RE: Final[re.Pattern[str]] = re.compile(
|
|
139
|
+
r"(?:~/\.claude/|/\.claude/|\bCLAUDE\.md\b|\b(?:rules|commands"
|
|
140
|
+
r"|agents|skills|hooks|output-styles|statusline|mcp)/)"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Framework signature heuristics.
|
|
144
|
+
_FRAMEWORK_SIGNATURES: Final[tuple[str, ...]] = (
|
|
145
|
+
"pyproject.toml",
|
|
146
|
+
"setup.cfg",
|
|
147
|
+
"package.json",
|
|
148
|
+
"Cargo.toml",
|
|
149
|
+
"go.mod",
|
|
150
|
+
"Gemfile",
|
|
151
|
+
"pom.xml",
|
|
152
|
+
"build.gradle",
|
|
153
|
+
)
|
|
154
|
+
_FRAMEWORK_RE: Final[re.Pattern[str]] = re.compile(
|
|
155
|
+
r"\b(?:" + "|".join(re.escape(s) for s in _FRAMEWORK_SIGNATURES) + r")\b"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# File-reference heuristics. A path-shaped token with at least one
|
|
159
|
+
# slash and a recognized source-file extension.
|
|
160
|
+
_FILE_REF_RE: Final[re.Pattern[str]] = re.compile(
|
|
161
|
+
r"\b[\w./-]+\.(?:py|js|ts|tsx|rs|go|java|rb|sh|md|yml|yaml|json|toml|ini)\b"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
_FRONTMATTER_RE: Final[re.Pattern[str]] = re.compile(
|
|
165
|
+
r"\A---\s*\n(.*?)\n---\s*\n",
|
|
166
|
+
re.DOTALL,
|
|
167
|
+
)
|
|
168
|
+
_FRONTMATTER_PROJECT_RE: Final[re.Pattern[str]] = re.compile(
|
|
169
|
+
r"^project:\s*(.+?)\s*$",
|
|
170
|
+
re.MULTILINE,
|
|
171
|
+
)
|
|
172
|
+
_FRONTMATTER_TITLE_RE: Final[re.Pattern[str]] = re.compile(
|
|
173
|
+
r"^title:\s*(.+?)\s*$",
|
|
174
|
+
re.MULTILINE,
|
|
175
|
+
)
|
|
176
|
+
_FRONTMATTER_CREATED_RE: Final[re.Pattern[str]] = re.compile(
|
|
177
|
+
r"^created:\s*(.+?)\s*$",
|
|
178
|
+
re.MULTILINE,
|
|
179
|
+
)
|
|
180
|
+
_H1_RE: Final[re.Pattern[str]] = re.compile(r"^#\s+(.+?)\s*$", re.MULTILINE)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass
|
|
184
|
+
class Signals:
|
|
185
|
+
"""Per-file body-scan signal set."""
|
|
186
|
+
|
|
187
|
+
repo_urls: list[str] = field(default_factory=list)
|
|
188
|
+
abs_paths: list[str] = field(default_factory=list)
|
|
189
|
+
file_refs: list[str] = field(default_factory=list)
|
|
190
|
+
frameworks: list[str] = field(default_factory=list)
|
|
191
|
+
eco_path_hits: int = 0
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclass
|
|
195
|
+
class SuiteVerdict:
|
|
196
|
+
"""The suite-level destination + confidence + rationale fragments
|
|
197
|
+
every file in the suite inherits.
|
|
198
|
+
|
|
199
|
+
The verdict is computed once per suite from the suite-name
|
|
200
|
+
heuristic plus the aggregated body signals; the per-file records
|
|
201
|
+
surface the verdict alongside their individual signal sets.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
suite: str
|
|
205
|
+
file_count: int
|
|
206
|
+
destination: str
|
|
207
|
+
confidence: str
|
|
208
|
+
rationale: list[str]
|
|
209
|
+
aggregate_repo_urls: list[str]
|
|
210
|
+
aggregate_abs_paths: list[str]
|
|
211
|
+
eco_signal_density: float
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass
|
|
215
|
+
class ProvenanceRecord:
|
|
216
|
+
"""Provenance record for a single plan file.
|
|
217
|
+
|
|
218
|
+
The shape is the source of truth for the JSON envelope: every field
|
|
219
|
+
here surfaces in the output document with an identical key.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
path: str
|
|
223
|
+
suite: str
|
|
224
|
+
mtime: str
|
|
225
|
+
sha256: str
|
|
226
|
+
line_count: int
|
|
227
|
+
frontmatter_project: str | None
|
|
228
|
+
signals: Signals
|
|
229
|
+
inferred_destination: str
|
|
230
|
+
confidence: str
|
|
231
|
+
proposed_destination_filename: str
|
|
232
|
+
notes: list[str] = field(default_factory=list)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _read_text(path: Path) -> str:
|
|
236
|
+
try:
|
|
237
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
238
|
+
except OSError:
|
|
239
|
+
return ""
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _parse_frontmatter(content: str) -> dict[str, str]:
|
|
243
|
+
match = _FRONTMATTER_RE.match(content)
|
|
244
|
+
if not match:
|
|
245
|
+
return {}
|
|
246
|
+
body = match.group(1)
|
|
247
|
+
fields: dict[str, str] = {}
|
|
248
|
+
project = _FRONTMATTER_PROJECT_RE.search(body)
|
|
249
|
+
if project:
|
|
250
|
+
fields["project"] = project.group(1).strip()
|
|
251
|
+
title = _FRONTMATTER_TITLE_RE.search(body)
|
|
252
|
+
if title:
|
|
253
|
+
fields["title"] = title.group(1).strip()
|
|
254
|
+
created = _FRONTMATTER_CREATED_RE.search(body)
|
|
255
|
+
if created:
|
|
256
|
+
fields["created"] = created.group(1).strip()
|
|
257
|
+
return fields
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _strip_frontmatter(content: str) -> str:
|
|
261
|
+
match = _FRONTMATTER_RE.match(content)
|
|
262
|
+
if not match:
|
|
263
|
+
return content
|
|
264
|
+
return content[match.end() :]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _scan_signals(content: str) -> Signals:
|
|
268
|
+
body = _strip_frontmatter(content)
|
|
269
|
+
return Signals(
|
|
270
|
+
repo_urls=sorted(set(_REPO_URL_RE.findall(body))),
|
|
271
|
+
abs_paths=sorted(set(_ABS_PATH_RE.findall(body))),
|
|
272
|
+
file_refs=sorted(set(_FILE_REF_RE.findall(body)))[:50],
|
|
273
|
+
frameworks=sorted(set(_FRAMEWORK_RE.findall(body))),
|
|
274
|
+
eco_path_hits=len(_ECO_PATH_RE.findall(body)),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _kebab_slug(text: str) -> str:
|
|
279
|
+
text = text.lower()
|
|
280
|
+
text = re.sub(r"[^\w\s-]", "", text)
|
|
281
|
+
text = re.sub(r"[\s_]+", "-", text)
|
|
282
|
+
text = re.sub(r"-+", "-", text)
|
|
283
|
+
return text.strip("-")[:60]
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _h1_of(content: str) -> str | None:
|
|
287
|
+
body = _strip_frontmatter(content)
|
|
288
|
+
match = _H1_RE.search(body)
|
|
289
|
+
return match.group(1).strip() if match else None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _proposed_filename(
|
|
293
|
+
path: Path,
|
|
294
|
+
mtime_iso: str,
|
|
295
|
+
fm_fields: dict[str, str],
|
|
296
|
+
h1: str | None,
|
|
297
|
+
) -> str:
|
|
298
|
+
date_str = fm_fields.get("created", "")
|
|
299
|
+
iso10 = re.match(r"\d{4}-\d{2}-\d{2}", date_str)
|
|
300
|
+
date = iso10.group(0) if iso10 else mtime_iso[:10]
|
|
301
|
+
if "title" in fm_fields:
|
|
302
|
+
slug = _kebab_slug(fm_fields["title"])
|
|
303
|
+
elif h1:
|
|
304
|
+
slug = _kebab_slug(h1)
|
|
305
|
+
else:
|
|
306
|
+
slug = _kebab_slug(path.stem)
|
|
307
|
+
return f"{date}--{slug}.md"
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _suite_of(rel: str) -> str:
|
|
311
|
+
parts = rel.replace("\\", "/").split("/")
|
|
312
|
+
if len(parts) >= 2 and parts[0] == ".plans":
|
|
313
|
+
return parts[1]
|
|
314
|
+
return ""
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _suite_name_hint(suite: str) -> str | None:
|
|
318
|
+
"""Apply the suite-name prefix table; returns the destination
|
|
319
|
+
marker the prefix maps to, or None when no prefix matches."""
|
|
320
|
+
for prefix, target in SUITE_NAME_HINTS:
|
|
321
|
+
if suite == prefix or suite.startswith(prefix):
|
|
322
|
+
return target
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _load_known_projects(path: Path) -> list[dict[str, str]]:
|
|
327
|
+
if not path.exists():
|
|
328
|
+
return []
|
|
329
|
+
out: list[dict[str, str]] = []
|
|
330
|
+
for raw in path.read_text(encoding="utf-8").splitlines():
|
|
331
|
+
line = raw.strip()
|
|
332
|
+
if not line or line.startswith("#"):
|
|
333
|
+
continue
|
|
334
|
+
if "=" in line:
|
|
335
|
+
key, _, value = line.partition("=")
|
|
336
|
+
out.append({"name": key.strip(), "ref": value.strip()})
|
|
337
|
+
else:
|
|
338
|
+
name = re.sub(r"\.git$", "", line.rstrip("/").split("/")[-1])
|
|
339
|
+
out.append({"name": name, "ref": line})
|
|
340
|
+
return out
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _matches_project(value: str, project: dict[str, str]) -> bool:
|
|
344
|
+
name = project["name"].lower()
|
|
345
|
+
ref = project["ref"].lower()
|
|
346
|
+
val = value.lower()
|
|
347
|
+
if name and name in val:
|
|
348
|
+
return True
|
|
349
|
+
return bool(ref and ref in val)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _aggregate_suite(
|
|
353
|
+
suite: str,
|
|
354
|
+
file_signals: list[Signals],
|
|
355
|
+
) -> tuple[list[str], list[str], float]:
|
|
356
|
+
"""Aggregate per-file signals to the suite level.
|
|
357
|
+
|
|
358
|
+
Returns ``(repo_urls, abs_paths, eco_density)``. Eco density is
|
|
359
|
+
``hits / files``: at >= 0.5 the suite is dominantly an
|
|
360
|
+
ecosystem-self artifact; the destination resolution reads it as
|
|
361
|
+
a tie-breaker against ambiguous suite names.
|
|
362
|
+
"""
|
|
363
|
+
urls: set[str] = set()
|
|
364
|
+
paths: set[str] = set()
|
|
365
|
+
eco_total = 0
|
|
366
|
+
for sig in file_signals:
|
|
367
|
+
urls.update(sig.repo_urls)
|
|
368
|
+
paths.update(sig.abs_paths)
|
|
369
|
+
eco_total += min(sig.eco_path_hits, 10)
|
|
370
|
+
density = eco_total / max(len(file_signals) * 10, 1)
|
|
371
|
+
return sorted(urls), sorted(paths), density
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _resolve_suite(
|
|
375
|
+
suite: str,
|
|
376
|
+
file_count: int,
|
|
377
|
+
aggregate_urls: list[str],
|
|
378
|
+
aggregate_paths: list[str],
|
|
379
|
+
eco_density: float,
|
|
380
|
+
known_projects: list[dict[str, str]],
|
|
381
|
+
) -> SuiteVerdict:
|
|
382
|
+
"""Compute the suite-level destination + confidence."""
|
|
383
|
+
rationale: list[str] = []
|
|
384
|
+
|
|
385
|
+
if suite == RECURSIVE_SELF_SUITE:
|
|
386
|
+
rationale.append(
|
|
387
|
+
"this suite hardens the very ecosystem it lives in;"
|
|
388
|
+
" moving it would amputate the migration's working tree"
|
|
389
|
+
)
|
|
390
|
+
return SuiteVerdict(
|
|
391
|
+
suite=suite,
|
|
392
|
+
file_count=file_count,
|
|
393
|
+
destination=ECOSYSTEM_DESTINATION_TEXT,
|
|
394
|
+
confidence=CONFIDENCE_RECURSIVE_SELF,
|
|
395
|
+
rationale=rationale,
|
|
396
|
+
aggregate_repo_urls=aggregate_urls,
|
|
397
|
+
aggregate_abs_paths=aggregate_paths,
|
|
398
|
+
eco_signal_density=eco_density,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
hint = _suite_name_hint(suite)
|
|
402
|
+
if hint == ECOSYSTEM_SELF_MARKER:
|
|
403
|
+
rationale.append(
|
|
404
|
+
f"suite name '{suite}' carries an ecosystem-prefix"
|
|
405
|
+
" (claude- / agent-home-); the suite describes the"
|
|
406
|
+
" user-config root itself and shares the recursive-self"
|
|
407
|
+
" destination semantics"
|
|
408
|
+
)
|
|
409
|
+
rationale.append(
|
|
410
|
+
f"body eco-signal density {eco_density:.2f} corroborates"
|
|
411
|
+
" the suite-name hint"
|
|
412
|
+
)
|
|
413
|
+
return SuiteVerdict(
|
|
414
|
+
suite=suite,
|
|
415
|
+
file_count=file_count,
|
|
416
|
+
destination=ECOSYSTEM_DESTINATION_TEXT,
|
|
417
|
+
confidence=CONFIDENCE_HIGH,
|
|
418
|
+
rationale=rationale,
|
|
419
|
+
aggregate_repo_urls=aggregate_urls,
|
|
420
|
+
aggregate_abs_paths=aggregate_paths,
|
|
421
|
+
eco_signal_density=eco_density,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
if hint is not None:
|
|
425
|
+
# Resolve the named project against the known-projects list.
|
|
426
|
+
# Exact-name match wins over substring containment so a hint
|
|
427
|
+
# of ``dc-kit-mini`` does not collapse onto a known-project
|
|
428
|
+
# entry named ``dc-kit`` via substring overlap.
|
|
429
|
+
exact = next(
|
|
430
|
+
(p for p in known_projects if p["name"].lower() == hint.lower()),
|
|
431
|
+
None,
|
|
432
|
+
)
|
|
433
|
+
substring = next(
|
|
434
|
+
(
|
|
435
|
+
p
|
|
436
|
+
for p in known_projects
|
|
437
|
+
if p["name"].lower() != hint.lower() and _matches_project(hint, p)
|
|
438
|
+
),
|
|
439
|
+
None,
|
|
440
|
+
)
|
|
441
|
+
match = exact or substring
|
|
442
|
+
if match is not None:
|
|
443
|
+
kind = "exact" if exact else "substring"
|
|
444
|
+
rationale.append(
|
|
445
|
+
f"suite name '{suite}' maps to known project"
|
|
446
|
+
f" '{match['name']}' via the suite-name prefix table"
|
|
447
|
+
f" ({kind} match)"
|
|
448
|
+
)
|
|
449
|
+
return SuiteVerdict(
|
|
450
|
+
suite=suite,
|
|
451
|
+
file_count=file_count,
|
|
452
|
+
destination=match["name"],
|
|
453
|
+
confidence=CONFIDENCE_HIGH,
|
|
454
|
+
rationale=rationale,
|
|
455
|
+
aggregate_repo_urls=aggregate_urls,
|
|
456
|
+
aggregate_abs_paths=aggregate_paths,
|
|
457
|
+
eco_signal_density=eco_density,
|
|
458
|
+
)
|
|
459
|
+
rationale.append(
|
|
460
|
+
f"suite name '{suite}' maps to project '{hint}' via the"
|
|
461
|
+
" suite-name prefix table; no known-projects entry yet"
|
|
462
|
+
" carries the matching name, so confidence sits at"
|
|
463
|
+
" medium pending known-projects ratification"
|
|
464
|
+
)
|
|
465
|
+
return SuiteVerdict(
|
|
466
|
+
suite=suite,
|
|
467
|
+
file_count=file_count,
|
|
468
|
+
destination=hint,
|
|
469
|
+
confidence=CONFIDENCE_MEDIUM,
|
|
470
|
+
rationale=rationale,
|
|
471
|
+
aggregate_repo_urls=aggregate_urls,
|
|
472
|
+
aggregate_abs_paths=aggregate_paths,
|
|
473
|
+
eco_signal_density=eco_density,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# No suite-name hint fired. Fall through to body signals.
|
|
477
|
+
if eco_density >= 0.5:
|
|
478
|
+
rationale.append(
|
|
479
|
+
f"body eco-signal density {eco_density:.2f} above the"
|
|
480
|
+
" 0.50 threshold; routes to the user-config ecosystem"
|
|
481
|
+
" stay-in-place destination"
|
|
482
|
+
)
|
|
483
|
+
return SuiteVerdict(
|
|
484
|
+
suite=suite,
|
|
485
|
+
file_count=file_count,
|
|
486
|
+
destination=ECOSYSTEM_DESTINATION_TEXT,
|
|
487
|
+
confidence=CONFIDENCE_MEDIUM,
|
|
488
|
+
rationale=rationale,
|
|
489
|
+
aggregate_repo_urls=aggregate_urls,
|
|
490
|
+
aggregate_abs_paths=aggregate_paths,
|
|
491
|
+
eco_signal_density=eco_density,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
for url in aggregate_urls:
|
|
495
|
+
for proj in known_projects:
|
|
496
|
+
if _matches_project(url, proj):
|
|
497
|
+
rationale.append(
|
|
498
|
+
f"aggregate repository URL '{url}' matches known"
|
|
499
|
+
f" project '{proj['name']}'"
|
|
500
|
+
)
|
|
501
|
+
return SuiteVerdict(
|
|
502
|
+
suite=suite,
|
|
503
|
+
file_count=file_count,
|
|
504
|
+
destination=proj["name"],
|
|
505
|
+
confidence=CONFIDENCE_HIGH,
|
|
506
|
+
rationale=rationale,
|
|
507
|
+
aggregate_repo_urls=aggregate_urls,
|
|
508
|
+
aggregate_abs_paths=aggregate_paths,
|
|
509
|
+
eco_signal_density=eco_density,
|
|
510
|
+
)
|
|
511
|
+
if aggregate_urls:
|
|
512
|
+
rationale.append(
|
|
513
|
+
f"aggregate repository URL '{aggregate_urls[0]}'"
|
|
514
|
+
" recognizable but unmatched against known-projects"
|
|
515
|
+
)
|
|
516
|
+
return SuiteVerdict(
|
|
517
|
+
suite=suite,
|
|
518
|
+
file_count=file_count,
|
|
519
|
+
destination=aggregate_urls[0],
|
|
520
|
+
confidence=CONFIDENCE_MEDIUM,
|
|
521
|
+
rationale=rationale,
|
|
522
|
+
aggregate_repo_urls=aggregate_urls,
|
|
523
|
+
aggregate_abs_paths=aggregate_paths,
|
|
524
|
+
eco_signal_density=eco_density,
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
for ap in aggregate_paths:
|
|
528
|
+
for proj in known_projects:
|
|
529
|
+
if _matches_project(ap, proj):
|
|
530
|
+
rationale.append(
|
|
531
|
+
f"aggregate absolute path '{ap}' contains known"
|
|
532
|
+
f" project basename '{proj['name']}'"
|
|
533
|
+
)
|
|
534
|
+
return SuiteVerdict(
|
|
535
|
+
suite=suite,
|
|
536
|
+
file_count=file_count,
|
|
537
|
+
destination=proj["name"],
|
|
538
|
+
confidence=CONFIDENCE_MEDIUM,
|
|
539
|
+
rationale=rationale,
|
|
540
|
+
aggregate_repo_urls=aggregate_urls,
|
|
541
|
+
aggregate_abs_paths=aggregate_paths,
|
|
542
|
+
eco_signal_density=eco_density,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
rationale.append(
|
|
546
|
+
"no suite-name hint, no eco-density majority, no body URL or"
|
|
547
|
+
" absolute-path match against known-projects; suite is"
|
|
548
|
+
" unmappable pending operator disposition"
|
|
549
|
+
)
|
|
550
|
+
return SuiteVerdict(
|
|
551
|
+
suite=suite,
|
|
552
|
+
file_count=file_count,
|
|
553
|
+
destination="<unmappable>",
|
|
554
|
+
confidence=CONFIDENCE_UNMAPPABLE,
|
|
555
|
+
rationale=rationale,
|
|
556
|
+
aggregate_repo_urls=aggregate_urls,
|
|
557
|
+
aggregate_abs_paths=aggregate_paths,
|
|
558
|
+
eco_signal_density=eco_density,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _derive_known_projects(
|
|
563
|
+
body_urls: set[str],
|
|
564
|
+
body_paths: set[str],
|
|
565
|
+
) -> list[dict[str, str]]:
|
|
566
|
+
"""Auto-derive a known-projects entry list from observed signals.
|
|
567
|
+
|
|
568
|
+
The auto-derivation populates a ``src/apothem/audit/known-projects.txt``
|
|
569
|
+
template only when the operator has not supplied one. Two signal
|
|
570
|
+
classes contribute: repository URLs (the project name is the
|
|
571
|
+
second URL segment, with ``.git`` suffixes stripped) and absolute
|
|
572
|
+
paths whose immediate parent is a recognized projects root (the
|
|
573
|
+
project name is the segment immediately following the root).
|
|
574
|
+
|
|
575
|
+
The auto-derivation deliberately discards nested paths under a
|
|
576
|
+
project root: a body reference to ``/Users/<name>/Projects/dc-kit-
|
|
577
|
+
mini/docs/index.md`` contributes one entry for ``dc-kit-mini`` and
|
|
578
|
+
suppresses every deeper segment, because nested files are not
|
|
579
|
+
themselves projects and would confuse the suite-name match step.
|
|
580
|
+
"""
|
|
581
|
+
projects: list[dict[str, str]] = []
|
|
582
|
+
seen_names: set[str] = set()
|
|
583
|
+
|
|
584
|
+
# Recognized project-root prefixes. A path qualifies as a project
|
|
585
|
+
# candidate only when its segment immediately follows one of these
|
|
586
|
+
# roots; nested segments are suppressed.
|
|
587
|
+
# Path-prefix patterns derived from the running operator's home
|
|
588
|
+
# directory at call time; no hardcoded username, so the same
|
|
589
|
+
# scanner works for any operator on any host. The fallbacks cover
|
|
590
|
+
# the macOS / Linux / Windows shapes Python's ``Path.home()``
|
|
591
|
+
# produces; both POSIX and Windows separators are emitted so a
|
|
592
|
+
# cross-platform body reference matches whichever form the source
|
|
593
|
+
# used.
|
|
594
|
+
home = Path.home()
|
|
595
|
+
home_posix = home.as_posix()
|
|
596
|
+
home_windows = str(home).replace("/", "\\")
|
|
597
|
+
project_roots: tuple[tuple[str, str], ...] = (
|
|
598
|
+
(f"{home_posix}/Projects/", "/"),
|
|
599
|
+
(f"{home_windows}\\Projects\\", "\\"),
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
for url in sorted(body_urls):
|
|
603
|
+
m = re.match(
|
|
604
|
+
r"https?://(?:github|gitlab)\.com/([\w-]+)/([\w.-]+)",
|
|
605
|
+
url,
|
|
606
|
+
re.IGNORECASE,
|
|
607
|
+
)
|
|
608
|
+
if not m:
|
|
609
|
+
continue
|
|
610
|
+
repo_name = re.sub(r"\.git$", "", m.group(2))
|
|
611
|
+
if repo_name.lower() in seen_names:
|
|
612
|
+
continue
|
|
613
|
+
seen_names.add(repo_name.lower())
|
|
614
|
+
projects.append(
|
|
615
|
+
{"name": repo_name, "ref": url.split("/blob/")[0]},
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
for ap in sorted(body_paths):
|
|
619
|
+
for root, sep in project_roots:
|
|
620
|
+
idx = ap.find(root)
|
|
621
|
+
if idx < 0:
|
|
622
|
+
continue
|
|
623
|
+
tail = ap[idx + len(root) :]
|
|
624
|
+
terminal = tail.split(sep, 1)[0]
|
|
625
|
+
if not terminal or terminal.startswith("."):
|
|
626
|
+
continue
|
|
627
|
+
if terminal.lower() in seen_names:
|
|
628
|
+
continue
|
|
629
|
+
seen_names.add(terminal.lower())
|
|
630
|
+
full_path = ap[: idx + len(root)] + terminal
|
|
631
|
+
projects.append({"name": terminal, "ref": full_path})
|
|
632
|
+
break
|
|
633
|
+
|
|
634
|
+
return projects
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _record_for(
|
|
638
|
+
rel: str,
|
|
639
|
+
inventory_record: dict[str, Any],
|
|
640
|
+
root: Path,
|
|
641
|
+
suite_verdict: SuiteVerdict,
|
|
642
|
+
) -> ProvenanceRecord:
|
|
643
|
+
path = root / rel
|
|
644
|
+
suite = _suite_of(rel)
|
|
645
|
+
content = _read_text(path)
|
|
646
|
+
fm = _parse_frontmatter(content)
|
|
647
|
+
signals = _scan_signals(content)
|
|
648
|
+
h1 = _h1_of(content)
|
|
649
|
+
mtime = inventory_record.get("mtime", "")
|
|
650
|
+
if not mtime and path.exists():
|
|
651
|
+
mtime = datetime.fromtimestamp(
|
|
652
|
+
path.stat().st_mtime, tz=timezone.utc
|
|
653
|
+
).isoformat()
|
|
654
|
+
sha = inventory_record.get("sha256")
|
|
655
|
+
if not sha and path.exists():
|
|
656
|
+
sha = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
657
|
+
line_count = inventory_record.get("line-count")
|
|
658
|
+
if line_count is None:
|
|
659
|
+
line_count = len(content.splitlines())
|
|
660
|
+
fm_project = fm.get("project")
|
|
661
|
+
if (
|
|
662
|
+
suite == RECURSIVE_SELF_SUITE
|
|
663
|
+
or suite_verdict.destination == ECOSYSTEM_DESTINATION_TEXT
|
|
664
|
+
):
|
|
665
|
+
proposed = "n/a (stay-in-place)"
|
|
666
|
+
else:
|
|
667
|
+
proposed = _proposed_filename(path, mtime, fm, h1)
|
|
668
|
+
return ProvenanceRecord(
|
|
669
|
+
path=rel,
|
|
670
|
+
suite=suite,
|
|
671
|
+
mtime=mtime,
|
|
672
|
+
sha256=sha or "",
|
|
673
|
+
line_count=int(line_count or 0),
|
|
674
|
+
frontmatter_project=fm_project,
|
|
675
|
+
signals=signals,
|
|
676
|
+
inferred_destination=suite_verdict.destination,
|
|
677
|
+
confidence=suite_verdict.confidence,
|
|
678
|
+
proposed_destination_filename=proposed,
|
|
679
|
+
notes=list(suite_verdict.rationale),
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def _emit_json(
|
|
684
|
+
records: list[ProvenanceRecord],
|
|
685
|
+
suite_verdicts: dict[str, SuiteVerdict],
|
|
686
|
+
inventory_sha: str,
|
|
687
|
+
out: Path,
|
|
688
|
+
) -> None:
|
|
689
|
+
by_suite_block: dict[str, dict[str, Any]] = {}
|
|
690
|
+
for suite, verdict in sorted(suite_verdicts.items()):
|
|
691
|
+
by_suite_block[suite] = {
|
|
692
|
+
"file-count": verdict.file_count,
|
|
693
|
+
"destination": verdict.destination,
|
|
694
|
+
"confidence": verdict.confidence,
|
|
695
|
+
"rationale": verdict.rationale,
|
|
696
|
+
"aggregate-repo-urls": verdict.aggregate_repo_urls,
|
|
697
|
+
"aggregate-abs-paths": verdict.aggregate_abs_paths,
|
|
698
|
+
"eco-signal-density": round(verdict.eco_signal_density, 3),
|
|
699
|
+
}
|
|
700
|
+
by_confidence_total: dict[str, int] = dict.fromkeys(ALL_CONFIDENCES, 0)
|
|
701
|
+
for r in records:
|
|
702
|
+
by_confidence_total[r.confidence] += 1
|
|
703
|
+
payload = {
|
|
704
|
+
"generated": datetime.now(timezone.utc).isoformat(),
|
|
705
|
+
"scanner": "build_plans_provenance",
|
|
706
|
+
"inventory-source-sha256": inventory_sha,
|
|
707
|
+
"suite-count": len(suite_verdicts),
|
|
708
|
+
"file-count": len(records),
|
|
709
|
+
"by-confidence": by_confidence_total,
|
|
710
|
+
"suites": by_suite_block,
|
|
711
|
+
"files": [_record_to_dict(r) for r in records],
|
|
712
|
+
}
|
|
713
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
714
|
+
out.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def _record_to_dict(r: ProvenanceRecord) -> dict[str, Any]:
|
|
718
|
+
return {
|
|
719
|
+
"path": r.path,
|
|
720
|
+
"suite": r.suite,
|
|
721
|
+
"mtime": r.mtime,
|
|
722
|
+
"sha256": r.sha256,
|
|
723
|
+
"line-count": r.line_count,
|
|
724
|
+
"frontmatter-project": r.frontmatter_project,
|
|
725
|
+
"signals": asdict(r.signals),
|
|
726
|
+
"inferred-destination": r.inferred_destination,
|
|
727
|
+
"confidence": r.confidence,
|
|
728
|
+
"proposed-destination-filename": r.proposed_destination_filename,
|
|
729
|
+
"notes": r.notes,
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _emit_markdown(
|
|
734
|
+
records: list[ProvenanceRecord],
|
|
735
|
+
suite_verdicts: dict[str, SuiteVerdict],
|
|
736
|
+
out: Path,
|
|
737
|
+
) -> None:
|
|
738
|
+
total = len(records)
|
|
739
|
+
by_confidence: dict[str, int] = dict.fromkeys(ALL_CONFIDENCES, 0)
|
|
740
|
+
for r in records:
|
|
741
|
+
by_confidence[r.confidence] += 1
|
|
742
|
+
orphans = [r for r in records if r.confidence == CONFIDENCE_UNMAPPABLE]
|
|
743
|
+
|
|
744
|
+
lines: list[str] = []
|
|
745
|
+
lines.append("# Plans Provenance — Per-Suite, Per-File Map")
|
|
746
|
+
lines.append("")
|
|
747
|
+
lines.append(f"_Generated: {datetime.now(timezone.utc).isoformat()}_")
|
|
748
|
+
lines.append("")
|
|
749
|
+
lines.append("## Aggregate Stats")
|
|
750
|
+
lines.append("")
|
|
751
|
+
lines.append(f"- **Total suites:** {len(suite_verdicts)}")
|
|
752
|
+
lines.append(f"- **Total plan files:** {total}")
|
|
753
|
+
lines.append("- **By confidence:**")
|
|
754
|
+
for c in ALL_CONFIDENCES:
|
|
755
|
+
lines.append(f" - `{c}`: {by_confidence[c]}")
|
|
756
|
+
lines.append("")
|
|
757
|
+
|
|
758
|
+
lines.append("## Suite-Level Verdicts")
|
|
759
|
+
lines.append("")
|
|
760
|
+
lines.append("| Suite | Files | Confidence | Destination |")
|
|
761
|
+
lines.append("|-------|-------|------------|-------------|")
|
|
762
|
+
for suite, verdict in sorted(suite_verdicts.items()):
|
|
763
|
+
suite_disp = suite.replace("|", "\\|")
|
|
764
|
+
dest_disp = verdict.destination.replace("|", "\\|")
|
|
765
|
+
lines.append(
|
|
766
|
+
f"| `{suite_disp}` | {verdict.file_count} |"
|
|
767
|
+
f" `{verdict.confidence}` | {dest_disp} |"
|
|
768
|
+
)
|
|
769
|
+
lines.append("")
|
|
770
|
+
|
|
771
|
+
lines.append("## Per-Suite Tables")
|
|
772
|
+
lines.append("")
|
|
773
|
+
by_suite: dict[str, list[ProvenanceRecord]] = {}
|
|
774
|
+
for r in records:
|
|
775
|
+
by_suite.setdefault(r.suite or "<root>", []).append(r)
|
|
776
|
+
for suite in sorted(by_suite):
|
|
777
|
+
suite_records = sorted(by_suite[suite], key=lambda r: r.path)
|
|
778
|
+
suite_verdict = suite_verdicts.get(suite)
|
|
779
|
+
lines.append(f"### `{suite}`")
|
|
780
|
+
lines.append("")
|
|
781
|
+
if suite_verdict is not None:
|
|
782
|
+
lines.append(
|
|
783
|
+
f"_Confidence: `{suite_verdict.confidence}`. Destination:"
|
|
784
|
+
f" {suite_verdict.destination}._"
|
|
785
|
+
)
|
|
786
|
+
lines.append("")
|
|
787
|
+
lines.append("**Rationale:**")
|
|
788
|
+
lines.append("")
|
|
789
|
+
for fragment in suite_verdict.rationale:
|
|
790
|
+
lines.append(f"- {fragment}")
|
|
791
|
+
lines.append("")
|
|
792
|
+
lines.append("| Path | Proposed filename |")
|
|
793
|
+
lines.append("|------|-------------------|")
|
|
794
|
+
for r in suite_records:
|
|
795
|
+
path_disp = r.path.replace("|", "\\|")
|
|
796
|
+
file_disp = r.proposed_destination_filename.replace("|", "\\|")
|
|
797
|
+
lines.append(f"| `{path_disp}` | `{file_disp}` |")
|
|
798
|
+
lines.append("")
|
|
799
|
+
|
|
800
|
+
lines.append("## Recursive-Case Annotation")
|
|
801
|
+
lines.append("")
|
|
802
|
+
recursive = [
|
|
803
|
+
v for v in suite_verdicts.values() if v.confidence == CONFIDENCE_RECURSIVE_SELF
|
|
804
|
+
]
|
|
805
|
+
if recursive:
|
|
806
|
+
v = recursive[0]
|
|
807
|
+
lines.append(
|
|
808
|
+
f"The suite `{v.suite}` describes the very ecosystem it"
|
|
809
|
+
f" lives in ({v.file_count} files); the migration leaves"
|
|
810
|
+
" it in place and the cleanup phase adds `.plans/` to the"
|
|
811
|
+
" repository's `.gitignore` so the directory carries no"
|
|
812
|
+
" plan-product through publication."
|
|
813
|
+
)
|
|
814
|
+
else:
|
|
815
|
+
lines.append("_No recursive-self records._")
|
|
816
|
+
lines.append("")
|
|
817
|
+
|
|
818
|
+
lines.append("## Orphan Candidates")
|
|
819
|
+
lines.append("")
|
|
820
|
+
if orphans:
|
|
821
|
+
lines.append(
|
|
822
|
+
f"{len(orphans)} file(s) with `confidence: unmappable`."
|
|
823
|
+
" The migration-confirmation pass routes these to the"
|
|
824
|
+
" operator for explicit disposition."
|
|
825
|
+
)
|
|
826
|
+
lines.append("")
|
|
827
|
+
for r in sorted(orphans, key=lambda r: r.path):
|
|
828
|
+
lines.append(f"- `{r.path}`")
|
|
829
|
+
else:
|
|
830
|
+
lines.append("_No orphan candidates surfaced._")
|
|
831
|
+
lines.append("")
|
|
832
|
+
|
|
833
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
834
|
+
out.write_text("\n".join(lines), encoding="utf-8")
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
def _filter_plan_records(records: Iterable[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
838
|
+
"""Return every plan-artifact record from the inventory.
|
|
839
|
+
|
|
840
|
+
Every plan-artifact participates in the migration, including the
|
|
841
|
+
non-narrative classes (JSON outputs, log captures, scratch
|
|
842
|
+
fixtures): the migration moves a suite as a unit, not as a curated
|
|
843
|
+
text-only subset. Body-scan signals are produced only for the
|
|
844
|
+
text-readable extensions (see :data:`PLAN_EXTENSIONS`); records
|
|
845
|
+
outside that set still inherit their suite's verdict and surface
|
|
846
|
+
in the per-file table with empty signal sets.
|
|
847
|
+
"""
|
|
848
|
+
return [r for r in records if r.get("class") == "plan-artifact"]
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def _is_text_readable(rel: str) -> bool:
|
|
852
|
+
"""Predicate guarding the body-scan pass against binary or
|
|
853
|
+
non-text plan-artifact records."""
|
|
854
|
+
return Path(rel).suffix.lower() in PLAN_EXTENSIONS
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
def _write_known_projects_template(
|
|
858
|
+
path: Path,
|
|
859
|
+
derived: list[dict[str, str]],
|
|
860
|
+
) -> None:
|
|
861
|
+
"""Persist the auto-derived known-projects entries to the operator-
|
|
862
|
+
editable file.
|
|
863
|
+
|
|
864
|
+
The file is overwritten only when it does not already exist; an
|
|
865
|
+
operator-edited list is never replaced. The header explains the
|
|
866
|
+
auto-derivation so the operator can amend the entries with project
|
|
867
|
+
names that match their own checkout layout.
|
|
868
|
+
"""
|
|
869
|
+
if path.exists():
|
|
870
|
+
return
|
|
871
|
+
lines: list[str] = [
|
|
872
|
+
"# src/apothem/audit/known-projects.txt",
|
|
873
|
+
"#",
|
|
874
|
+
"# Auto-derived from plan-suite body content. Each line is either a bare URL,",
|
|
875
|
+
"# a bare absolute path, or a name=reference pair. Lines starting with #",
|
|
876
|
+
"# are comments. Edit freely; re-run the provenance scanner to refresh",
|
|
877
|
+
"# .audit/plans-provenance.{json,md}.",
|
|
878
|
+
"",
|
|
879
|
+
]
|
|
880
|
+
for proj in derived:
|
|
881
|
+
lines.append(f"{proj['name']}={proj['ref']}")
|
|
882
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
883
|
+
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def main(argv: list[str] | None = None) -> int:
|
|
887
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
888
|
+
parser.add_argument(
|
|
889
|
+
"--inventory",
|
|
890
|
+
type=Path,
|
|
891
|
+
default=Path(".audit/inventory.json"),
|
|
892
|
+
)
|
|
893
|
+
parser.add_argument("--root", type=Path, default=Path())
|
|
894
|
+
parser.add_argument(
|
|
895
|
+
"--known-projects",
|
|
896
|
+
type=Path,
|
|
897
|
+
default=Path("src/apothem/audit/known-projects.txt"),
|
|
898
|
+
)
|
|
899
|
+
parser.add_argument(
|
|
900
|
+
"--output-json",
|
|
901
|
+
type=Path,
|
|
902
|
+
default=Path(".audit/plans-provenance.json"),
|
|
903
|
+
)
|
|
904
|
+
parser.add_argument(
|
|
905
|
+
"--output-md",
|
|
906
|
+
type=Path,
|
|
907
|
+
default=Path(".audit/plans-provenance.md"),
|
|
908
|
+
)
|
|
909
|
+
args = parser.parse_args(argv)
|
|
910
|
+
|
|
911
|
+
if not args.inventory.exists():
|
|
912
|
+
print(
|
|
913
|
+
f"error: inventory not found at {args.inventory}",
|
|
914
|
+
file=sys.stderr,
|
|
915
|
+
)
|
|
916
|
+
return 1
|
|
917
|
+
|
|
918
|
+
raw = args.inventory.read_bytes()
|
|
919
|
+
inventory_sha = hashlib.sha256(raw).hexdigest()
|
|
920
|
+
payload = json.loads(raw.decode("utf-8"))
|
|
921
|
+
inventory_records = payload.get("files", [])
|
|
922
|
+
plan_records = _filter_plan_records(inventory_records)
|
|
923
|
+
|
|
924
|
+
# First pass: scan every file's signals.
|
|
925
|
+
scanned: dict[str, list[tuple[dict[str, Any], Signals]]] = {}
|
|
926
|
+
aggregate_urls: set[str] = set()
|
|
927
|
+
aggregate_paths: set[str] = set()
|
|
928
|
+
for inv in plan_records:
|
|
929
|
+
rel = inv["path"]
|
|
930
|
+
suite = _suite_of(rel)
|
|
931
|
+
if not suite:
|
|
932
|
+
continue
|
|
933
|
+
if _is_text_readable(rel):
|
|
934
|
+
content = _read_text(args.root / rel)
|
|
935
|
+
signals = _scan_signals(content)
|
|
936
|
+
else:
|
|
937
|
+
signals = Signals()
|
|
938
|
+
scanned.setdefault(suite, []).append((inv, signals))
|
|
939
|
+
aggregate_urls.update(signals.repo_urls)
|
|
940
|
+
aggregate_paths.update(signals.abs_paths)
|
|
941
|
+
|
|
942
|
+
# Auto-derive known-projects entries when the operator file is
|
|
943
|
+
# absent; persist the template for operator review.
|
|
944
|
+
initial_known = _load_known_projects(args.known_projects)
|
|
945
|
+
if not initial_known:
|
|
946
|
+
derived = _derive_known_projects(aggregate_urls, aggregate_paths)
|
|
947
|
+
if derived:
|
|
948
|
+
_write_known_projects_template(args.known_projects, derived)
|
|
949
|
+
known_projects = derived
|
|
950
|
+
else:
|
|
951
|
+
known_projects = initial_known
|
|
952
|
+
|
|
953
|
+
# Second pass: aggregate per-suite signals and resolve verdicts.
|
|
954
|
+
suite_verdicts: dict[str, SuiteVerdict] = {}
|
|
955
|
+
for suite, entries in scanned.items():
|
|
956
|
+
sigs = [s for _, s in entries]
|
|
957
|
+
urls, paths, density = _aggregate_suite(suite, sigs)
|
|
958
|
+
suite_verdicts[suite] = _resolve_suite(
|
|
959
|
+
suite,
|
|
960
|
+
len(entries),
|
|
961
|
+
urls,
|
|
962
|
+
paths,
|
|
963
|
+
density,
|
|
964
|
+
known_projects,
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
# Third pass: build per-file records that inherit the suite verdict.
|
|
968
|
+
records: list[ProvenanceRecord] = []
|
|
969
|
+
for suite, entries in scanned.items():
|
|
970
|
+
verdict = suite_verdicts[suite]
|
|
971
|
+
for inv, _ in entries:
|
|
972
|
+
records.append(
|
|
973
|
+
_record_for(inv["path"], inv, args.root, verdict),
|
|
974
|
+
)
|
|
975
|
+
|
|
976
|
+
_emit_json(records, suite_verdicts, inventory_sha, args.output_json)
|
|
977
|
+
_emit_markdown(records, suite_verdicts, args.output_md)
|
|
978
|
+
|
|
979
|
+
by_conf: dict[str, int] = dict.fromkeys(ALL_CONFIDENCES, 0)
|
|
980
|
+
for r in records:
|
|
981
|
+
by_conf[r.confidence] += 1
|
|
982
|
+
summary = ", ".join(f"{k}={v}" for k, v in by_conf.items() if v)
|
|
983
|
+
print(
|
|
984
|
+
f"build_plans_provenance: {len(records)} plan file(s) across"
|
|
985
|
+
f" {len(suite_verdicts)} suite(s) [{summary}];"
|
|
986
|
+
f" known-projects entries: {len(known_projects)}"
|
|
987
|
+
)
|
|
988
|
+
for suite, verdict in sorted(suite_verdicts.items()):
|
|
989
|
+
print(
|
|
990
|
+
f" {suite} ({verdict.file_count} files):"
|
|
991
|
+
f" {verdict.confidence} -> {verdict.destination[:80]}"
|
|
992
|
+
)
|
|
993
|
+
return 0
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
if __name__ == "__main__":
|
|
997
|
+
raise SystemExit(main())
|