attocode-code-intel 0.1.0__py3-none-any.whl
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.
- attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
- attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
- attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
- attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
- attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
- attocode_intel/GUIDELINES.md +432 -0
- attocode_intel/__init__.py +52 -0
- attocode_intel/_internal/__init__.py +1 -0
- attocode_intel/_internal/errors/__init__.py +126 -0
- attocode_intel/_internal/integrations/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
- attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
- attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
- attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
- attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
- attocode_intel/_internal/integrations/context/cas.py +466 -0
- attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
- attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
- attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
- attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
- attocode_intel/_internal/integrations/context/cross_references.py +616 -0
- attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
- attocode_intel/_internal/integrations/context/embeddings.py +282 -0
- attocode_intel/_internal/integrations/context/frecency.py +427 -0
- attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
- attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
- attocode_intel/_internal/integrations/context/hydration.py +90 -0
- attocode_intel/_internal/integrations/context/index_repair.py +87 -0
- attocode_intel/_internal/integrations/context/index_store.py +586 -0
- attocode_intel/_internal/integrations/context/memory_store.py +602 -0
- attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
- attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
- attocode_intel/_internal/integrations/context/query_history.py +417 -0
- attocode_intel/_internal/integrations/context/reranker.py +351 -0
- attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
- attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
- attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
- attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
- attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
- attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
- attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
- attocode_intel/_internal/integrations/context/vector_store.py +795 -0
- attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
- attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
- attocode_intel/_internal/integrations/lsp/client.py +1140 -0
- attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
- attocode_intel/_internal/integrations/mcp/client.py +253 -0
- attocode_intel/_internal/integrations/security/__init__.py +1 -0
- attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
- attocode_intel/_internal/integrations/security/dataflow.py +522 -0
- attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
- attocode_intel/_internal/integrations/security/matcher.py +44 -0
- attocode_intel/_internal/integrations/security/patterns.py +724 -0
- attocode_intel/_internal/integrations/security/scanner.py +351 -0
- attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
- attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
- attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
- attocode_intel/_internal/tricks/__init__.py +1 -0
- attocode_intel/_internal/tricks/recursive_context.py +357 -0
- attocode_intel/_internal/types/__init__.py +1 -0
- attocode_intel/_internal/types/events.py +289 -0
- attocode_intel/_internal/types/severity.py +15 -0
- attocode_intel/_shared.py +341 -0
- attocode_intel/analysis_coverage.py +56 -0
- attocode_intel/api/__init__.py +1 -0
- attocode_intel/api/access.py +32 -0
- attocode_intel/api/app.py +247 -0
- attocode_intel/api/auth/__init__.py +128 -0
- attocode_intel/api/auth/api_keys.py +61 -0
- attocode_intel/api/auth/context.py +34 -0
- attocode_intel/api/auth/jwt.py +66 -0
- attocode_intel/api/auth/oauth.py +146 -0
- attocode_intel/api/auth/passwords.py +15 -0
- attocode_intel/api/deps.py +351 -0
- attocode_intel/api/knowledge_bridge.py +13 -0
- attocode_intel/api/mcp_transport.py +51 -0
- attocode_intel/api/middleware.py +411 -0
- attocode_intel/api/models.py +698 -0
- attocode_intel/api/providers/__init__.py +1 -0
- attocode_intel/api/providers/db_provider.py +1106 -0
- attocode_intel/api/providers/local_provider.py +476 -0
- attocode_intel/api/providers/remote_provider.py +740 -0
- attocode_intel/api/repository_source.py +21 -0
- attocode_intel/api/routes/__init__.py +1 -0
- attocode_intel/api/routes/_pin_helper.py +158 -0
- attocode_intel/api/routes/activity.py +148 -0
- attocode_intel/api/routes/adr.py +102 -0
- attocode_intel/api/routes/analysis.py +515 -0
- attocode_intel/api/routes/api_keys.py +170 -0
- attocode_intel/api/routes/auth.py +428 -0
- attocode_intel/api/routes/branches.py +262 -0
- attocode_intel/api/routes/cross_repo_search.py +198 -0
- attocode_intel/api/routes/embeddings.py +386 -0
- attocode_intel/api/routes/files.py +132 -0
- attocode_intel/api/routes/files_v2.py +388 -0
- attocode_intel/api/routes/gc.py +322 -0
- attocode_intel/api/routes/git_v2.py +433 -0
- attocode_intel/api/routes/graph.py +174 -0
- attocode_intel/api/routes/graph_viz.py +160 -0
- attocode_intel/api/routes/health.py +62 -0
- attocode_intel/api/routes/history.py +175 -0
- attocode_intel/api/routes/jobs.py +84 -0
- attocode_intel/api/routes/learning.py +205 -0
- attocode_intel/api/routes/lsp.py +272 -0
- attocode_intel/api/routes/notify.py +623 -0
- attocode_intel/api/routes/operations.py +27 -0
- attocode_intel/api/routes/orgs.py +924 -0
- attocode_intel/api/routes/preferences.py +100 -0
- attocode_intel/api/routes/presence.py +42 -0
- attocode_intel/api/routes/projects.py +116 -0
- attocode_intel/api/routes/repos.py +47 -0
- attocode_intel/api/routes/search.py +180 -0
- attocode_intel/api/routes/snapshots.py +614 -0
- attocode_intel/api/routes/webhooks.py +313 -0
- attocode_intel/api/routes/websocket.py +116 -0
- attocode_intel/api/static/graph.html +239 -0
- attocode_intel/api/utils.py +96 -0
- attocode_intel/artifacts/__init__.py +45 -0
- attocode_intel/artifacts/hashing.py +230 -0
- attocode_intel/artifacts/provenance.py +190 -0
- attocode_intel/artifacts/retrieval_pin.py +159 -0
- attocode_intel/audit.py +59 -0
- attocode_intel/bug_finder.py +206 -0
- attocode_intel/bundle.py +80 -0
- attocode_intel/catalog.py +242 -0
- attocode_intel/change_review.py +75 -0
- attocode_intel/cli.py +3062 -0
- attocode_intel/community.py +63 -0
- attocode_intel/confidence/__init__.py +144 -0
- attocode_intel/confidence/jev.py +234 -0
- attocode_intel/confidence/llm.py +330 -0
- attocode_intel/confidence/redact.py +44 -0
- attocode_intel/confidence/settings.py +74 -0
- attocode_intel/config.py +256 -0
- attocode_intel/crypto.py +31 -0
- attocode_intel/db/__init__.py +7 -0
- attocode_intel/db/base.py +33 -0
- attocode_intel/db/engine.py +61 -0
- attocode_intel/db/models.py +529 -0
- attocode_intel/entrypoint.py +185 -0
- attocode_intel/focused_evidence.py +130 -0
- attocode_intel/freshness.py +154 -0
- attocode_intel/gateway.py +734 -0
- attocode_intel/git/__init__.py +7 -0
- attocode_intel/git/credentials.py +75 -0
- attocode_intel/git/manager.py +493 -0
- attocode_intel/git/models.py +96 -0
- attocode_intel/git/storage.py +98 -0
- attocode_intel/graph_query_parser.py +761 -0
- attocode_intel/helpers.py +712 -0
- attocode_intel/indexing/__init__.py +29 -0
- attocode_intel/indexing/debouncer.py +136 -0
- attocode_intel/indexing/delta_indexer.py +151 -0
- attocode_intel/indexing/full_indexer.py +323 -0
- attocode_intel/indexing/incremental.py +240 -0
- attocode_intel/indexing/parser.py +403 -0
- attocode_intel/indexing/summarizer.py +264 -0
- attocode_intel/installer.py +1896 -0
- attocode_intel/knowledge.py +269 -0
- attocode_intel/local_knowledge.py +77 -0
- attocode_intel/migrations/alembic.ini +36 -0
- attocode_intel/migrations/env.py +71 -0
- attocode_intel/migrations/script.py.mako +27 -0
- attocode_intel/migrations/versions/001_initial_schema.py +219 -0
- attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
- attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
- attocode_intel/migrations/versions/004_audit_events.py +38 -0
- attocode_intel/migrations/versions/005_user_preferences.py +30 -0
- attocode_intel/migrations/versions/006_google_oauth.py +27 -0
- attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
- attocode_intel/migrations/versions/008_symbol_references.py +37 -0
- attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
- attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
- attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
- attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
- attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
- attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
- attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
- attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
- attocode_intel/migrations/versions/017_provenance_table.py +102 -0
- attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
- attocode_intel/migrations/versions/019_symref_caller.py +42 -0
- attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
- attocode_intel/onboarding.py +258 -0
- attocode_intel/output.py +178 -0
- attocode_intel/precision.py +133 -0
- attocode_intel/presence.py +160 -0
- attocode_intel/probe.py +110 -0
- attocode_intel/project_dir.py +97 -0
- attocode_intel/pubsub.py +113 -0
- attocode_intel/query_ranking.py +242 -0
- attocode_intel/readiness.py +972 -0
- attocode_intel/redis.py +51 -0
- attocode_intel/reference_pages.py +60 -0
- attocode_intel/remote.py +131 -0
- attocode_intel/repo_ranker.py +246 -0
- attocode_intel/request_context.py +82 -0
- attocode_intel/rules/__init__.py +28 -0
- attocode_intel/rules/ci.py +320 -0
- attocode_intel/rules/combinators.py +293 -0
- attocode_intel/rules/enricher.py +127 -0
- attocode_intel/rules/evolution.py +452 -0
- attocode_intel/rules/executor.py +531 -0
- attocode_intel/rules/filters/__init__.py +5 -0
- attocode_intel/rules/filters/pipeline.py +151 -0
- attocode_intel/rules/formatter.py +198 -0
- attocode_intel/rules/hygiene.py +242 -0
- attocode_intel/rules/importers/__init__.py +1 -0
- attocode_intel/rules/importers/semgrep.py +382 -0
- attocode_intel/rules/loader.py +292 -0
- attocode_intel/rules/marketplace.py +375 -0
- attocode_intel/rules/metavar.py +181 -0
- attocode_intel/rules/model.py +317 -0
- attocode_intel/rules/packs/__init__.py +1 -0
- attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
- attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
- attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
- attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
- attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
- attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
- attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
- attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
- attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
- attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
- attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
- attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
- attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
- attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
- attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
- attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
- attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
- attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
- attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
- attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
- attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
- attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
- attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
- attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
- attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
- attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
- attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
- attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
- attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
- attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
- attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
- attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
- attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/pack_loader.py +244 -0
- attocode_intel/rules/plugins/__init__.py +1 -0
- attocode_intel/rules/plugins/plugin_loader.py +132 -0
- attocode_intel/rules/profiling.py +332 -0
- attocode_intel/rules/registry.py +213 -0
- attocode_intel/rules/sarif.py +187 -0
- attocode_intel/rules/synthesis.py +420 -0
- attocode_intel/rules/testing.py +326 -0
- attocode_intel/server.py +734 -0
- attocode_intel/service.py +3416 -0
- attocode_intel/sharing.py +57 -0
- attocode_intel/storage/__init__.py +6 -0
- attocode_intel/storage/branch_overlay.py +352 -0
- attocode_intel/storage/content_store.py +220 -0
- attocode_intel/storage/dependency_store.py +143 -0
- attocode_intel/storage/diff_engine.py +187 -0
- attocode_intel/storage/embedding_store.py +594 -0
- attocode_intel/storage/provenance_store.py +137 -0
- attocode_intel/storage/security_scanner_db.py +164 -0
- attocode_intel/storage/symbol_store.py +160 -0
- attocode_intel/structural_evidence.py +144 -0
- attocode_intel/symbol_inspection.py +141 -0
- attocode_intel/symbol_links.py +167 -0
- attocode_intel/telemetry.py +42 -0
- attocode_intel/test_ranking.py +73 -0
- attocode_intel/testing/__init__.py +43 -0
- attocode_intel/testing/fixtures.py +122 -0
- attocode_intel/testing/helpers.py +75 -0
- attocode_intel/testing/mocks.py +99 -0
- attocode_intel/tools/__init__.py +2 -0
- attocode_intel/tools/adr_tools.py +628 -0
- attocode_intel/tools/analysis_tools.py +382 -0
- attocode_intel/tools/composite_tools.py +695 -0
- attocode_intel/tools/cross_mode_tools.py +173 -0
- attocode_intel/tools/dead_code_tools.py +582 -0
- attocode_intel/tools/distill_tools.py +353 -0
- attocode_intel/tools/frecency_tools.py +220 -0
- attocode_intel/tools/fuzzy_tools.py +255 -0
- attocode_intel/tools/history_tools.py +486 -0
- attocode_intel/tools/learning_tools.py +222 -0
- attocode_intel/tools/lsp_tools.py +218 -0
- attocode_intel/tools/maintenance_tools.py +1662 -0
- attocode_intel/tools/navigation_tools.py +220 -0
- attocode_intel/tools/overlay_tools.py +540 -0
- attocode_intel/tools/pin_store.py +432 -0
- attocode_intel/tools/pin_tools.py +266 -0
- attocode_intel/tools/query_constraints_tools.py +262 -0
- attocode_intel/tools/query_history_tools.py +158 -0
- attocode_intel/tools/readiness_tools.py +44 -0
- attocode_intel/tools/rule_tools.py +992 -0
- attocode_intel/tools/search_tools.py +770 -0
- attocode_intel/tools/snapshot_tools.py +624 -0
- attocode_intel/workers/__init__.py +1 -0
- attocode_intel/workers/job_utils.py +111 -0
- attocode_intel/workers/jobs.py +573 -0
- attocode_intel/workers/run.py +30 -0
- attocode_intel/workers/settings.py +61 -0
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
"""Readiness report engine -- comprehensive codebase audit.
|
|
2
|
+
|
|
3
|
+
Orchestrates existing code-intel tools across 8 phases to produce a
|
|
4
|
+
ground-truth assessment of codebase health, completeness, and readiness.
|
|
5
|
+
|
|
6
|
+
Phases:
|
|
7
|
+
0. Baseline: build health, git status, dependencies
|
|
8
|
+
1. API & Business Logic: routes, stubs, TODOs
|
|
9
|
+
2. Frontend Flows: components, dead-ends
|
|
10
|
+
3. Data Layer: migrations, models, constraints
|
|
11
|
+
4. Test Coverage: test-to-source mapping, gaps, skips
|
|
12
|
+
5. Security Surface: secrets, auth, validation
|
|
13
|
+
6. Observability: logging, metrics, health endpoints
|
|
14
|
+
7. Deployability: Docker, CI/CD, env config
|
|
15
|
+
|
|
16
|
+
Plus: Tracer Bullets -- end-to-end journey verification.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import datetime
|
|
22
|
+
import logging
|
|
23
|
+
import os
|
|
24
|
+
import re
|
|
25
|
+
import subprocess
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from enum import IntEnum, StrEnum
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# =============================================================================
|
|
35
|
+
# Data Model
|
|
36
|
+
# =============================================================================
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ReadinessPhase(IntEnum):
|
|
40
|
+
BASELINE = 0
|
|
41
|
+
API_BUSINESS_LOGIC = 1
|
|
42
|
+
FRONTEND_FLOWS = 2
|
|
43
|
+
DATA_LAYER = 3
|
|
44
|
+
TEST_COVERAGE = 4
|
|
45
|
+
SECURITY_SURFACE = 5
|
|
46
|
+
OBSERVABILITY = 6
|
|
47
|
+
DEPLOYABILITY = 7
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_PHASE_NAMES: dict[ReadinessPhase, str] = {
|
|
51
|
+
ReadinessPhase.BASELINE: "Baseline",
|
|
52
|
+
ReadinessPhase.API_BUSINESS_LOGIC: "API & Business Logic",
|
|
53
|
+
ReadinessPhase.FRONTEND_FLOWS: "Frontend Flows",
|
|
54
|
+
ReadinessPhase.DATA_LAYER: "Data Layer",
|
|
55
|
+
ReadinessPhase.TEST_COVERAGE: "Test Coverage",
|
|
56
|
+
ReadinessPhase.SECURITY_SURFACE: "Security Surface",
|
|
57
|
+
ReadinessPhase.OBSERVABILITY: "Observability",
|
|
58
|
+
ReadinessPhase.DEPLOYABILITY: "Deployability",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class ReadinessSeverity(StrEnum):
|
|
63
|
+
PASS = "pass"
|
|
64
|
+
INFO = "info"
|
|
65
|
+
WARNING = "warning"
|
|
66
|
+
CRITICAL = "critical"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(slots=True)
|
|
70
|
+
class ReadinessFinding:
|
|
71
|
+
phase: ReadinessPhase
|
|
72
|
+
severity: ReadinessSeverity
|
|
73
|
+
title: str
|
|
74
|
+
detail: str = ""
|
|
75
|
+
evidence: list[str] = field(default_factory=list)
|
|
76
|
+
recommendation: str = ""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(slots=True)
|
|
80
|
+
class TracerBulletResult:
|
|
81
|
+
journey_name: str
|
|
82
|
+
status: str # production_ready, happy_path_only, partial, not_implemented
|
|
83
|
+
steps_checked: int
|
|
84
|
+
steps_passed: int
|
|
85
|
+
weak_link: str = ""
|
|
86
|
+
evidence: list[str] = field(default_factory=list)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(slots=True)
|
|
90
|
+
class PhaseResult:
|
|
91
|
+
phase: ReadinessPhase
|
|
92
|
+
phase_name: str
|
|
93
|
+
score: float # 0.0-1.0
|
|
94
|
+
findings: list[ReadinessFinding] = field(default_factory=list)
|
|
95
|
+
summary: str = ""
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(slots=True)
|
|
99
|
+
class ReadinessReport:
|
|
100
|
+
project_name: str
|
|
101
|
+
timestamp: str
|
|
102
|
+
phases_run: list[ReadinessPhase] = field(default_factory=list)
|
|
103
|
+
phase_results: list[PhaseResult] = field(default_factory=list)
|
|
104
|
+
tracer_bullets: list[TracerBulletResult] = field(default_factory=list)
|
|
105
|
+
overall_score: float = 0.0
|
|
106
|
+
overall_grade: str = "F"
|
|
107
|
+
|
|
108
|
+
def to_dict(self) -> dict[str, Any]:
|
|
109
|
+
return {
|
|
110
|
+
"project_name": self.project_name,
|
|
111
|
+
"timestamp": self.timestamp,
|
|
112
|
+
"overall_score": self.overall_score,
|
|
113
|
+
"overall_grade": self.overall_grade,
|
|
114
|
+
"phases": [
|
|
115
|
+
{
|
|
116
|
+
"phase": pr.phase,
|
|
117
|
+
"phase_name": pr.phase_name,
|
|
118
|
+
"score": pr.score,
|
|
119
|
+
"summary": pr.summary,
|
|
120
|
+
"findings": [
|
|
121
|
+
{
|
|
122
|
+
"severity": f.severity,
|
|
123
|
+
"title": f.title,
|
|
124
|
+
"detail": f.detail,
|
|
125
|
+
"evidence": f.evidence,
|
|
126
|
+
"recommendation": f.recommendation,
|
|
127
|
+
}
|
|
128
|
+
for f in pr.findings
|
|
129
|
+
],
|
|
130
|
+
}
|
|
131
|
+
for pr in self.phase_results
|
|
132
|
+
],
|
|
133
|
+
"tracer_bullets": [
|
|
134
|
+
{
|
|
135
|
+
"journey_name": tb.journey_name,
|
|
136
|
+
"status": tb.status,
|
|
137
|
+
"steps_checked": tb.steps_checked,
|
|
138
|
+
"steps_passed": tb.steps_passed,
|
|
139
|
+
"weak_link": tb.weak_link,
|
|
140
|
+
"evidence": tb.evidence,
|
|
141
|
+
}
|
|
142
|
+
for tb in self.tracer_bullets
|
|
143
|
+
],
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# =============================================================================
|
|
148
|
+
# Readiness Engine
|
|
149
|
+
# =============================================================================
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# Route decorator patterns for different frameworks
|
|
153
|
+
_ROUTE_DECORATOR_RE = re.compile(
|
|
154
|
+
r"@(?:app|router|api|bp|blueprint)\."
|
|
155
|
+
r"(?:route|get|post|put|delete|patch|options|head|websocket)\s*\(",
|
|
156
|
+
re.IGNORECASE,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Stub body patterns
|
|
160
|
+
_STUB_PATTERNS = [
|
|
161
|
+
re.compile(r"^\s*pass\s*$"),
|
|
162
|
+
re.compile(r"^\s*\.\.\.\s*$"),
|
|
163
|
+
re.compile(r"^\s*raise\s+NotImplementedError"),
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
# Test decorator skip patterns
|
|
167
|
+
_SKIP_PATTERNS = [
|
|
168
|
+
re.compile(r"@pytest\.mark\.skip"),
|
|
169
|
+
re.compile(r"@unittest\.skip"),
|
|
170
|
+
re.compile(r"\.skip\("),
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
# Framework import patterns for observability
|
|
174
|
+
_LOGGING_IMPORTS = re.compile(r"(?:import\s+logging|from\s+logging\s+import|import\s+structlog|import\s+loguru)")
|
|
175
|
+
_METRICS_IMPORTS = re.compile(r"(?:import\s+prometheus|from\s+prometheus|import\s+statsd|import\s+datadog)")
|
|
176
|
+
_ERROR_TRACKING_IMPORTS = re.compile(r"(?:import\s+sentry_sdk|import\s+bugsnag|import\s+rollbar)")
|
|
177
|
+
|
|
178
|
+
# Auth decorator patterns
|
|
179
|
+
_AUTH_DECORATOR_RE = re.compile(
|
|
180
|
+
r"@(?:login_required|auth_required|requires_auth|authenticated|"
|
|
181
|
+
r"permission_required|jwt_required|token_required|Depends\(.*auth)",
|
|
182
|
+
re.IGNORECASE,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Lock file names
|
|
186
|
+
_LOCK_FILES = {"uv.lock", "poetry.lock", "Pipfile.lock", "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "Cargo.lock", "go.sum"}
|
|
187
|
+
|
|
188
|
+
# Frontend file extensions
|
|
189
|
+
_FRONTEND_EXTS = frozenset({".jsx", ".tsx", ".vue", ".svelte"})
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class ReadinessEngine:
|
|
193
|
+
"""Orchestrates the 8-phase readiness audit."""
|
|
194
|
+
|
|
195
|
+
def __init__(self, project_dir: str) -> None:
|
|
196
|
+
self._project_dir = os.path.abspath(project_dir)
|
|
197
|
+
|
|
198
|
+
def run(
|
|
199
|
+
self,
|
|
200
|
+
*,
|
|
201
|
+
phases: list[int] | None = None,
|
|
202
|
+
scope: str = "",
|
|
203
|
+
tracer_bullets: bool = True,
|
|
204
|
+
prd_text: str = "",
|
|
205
|
+
) -> ReadinessReport:
|
|
206
|
+
"""Run the readiness audit.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
phases: Phase numbers to run (0-7). None = all.
|
|
210
|
+
scope: Restrict to files under this directory prefix.
|
|
211
|
+
tracer_bullets: Whether to run tracer bullet analysis.
|
|
212
|
+
prd_text: Optional PRD text for plan comparison.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
A ReadinessReport with findings and scores.
|
|
216
|
+
"""
|
|
217
|
+
from attocode_intel.helpers import _detect_project_name
|
|
218
|
+
|
|
219
|
+
project_name = _detect_project_name(self._project_dir)
|
|
220
|
+
timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds")
|
|
221
|
+
|
|
222
|
+
all_phases = list(ReadinessPhase)
|
|
223
|
+
requested = [ReadinessPhase(p) for p in phases] if phases else all_phases
|
|
224
|
+
|
|
225
|
+
phase_methods = {
|
|
226
|
+
ReadinessPhase.BASELINE: self._phase_baseline,
|
|
227
|
+
ReadinessPhase.API_BUSINESS_LOGIC: self._phase_api_logic,
|
|
228
|
+
ReadinessPhase.FRONTEND_FLOWS: self._phase_frontend,
|
|
229
|
+
ReadinessPhase.DATA_LAYER: self._phase_data_layer,
|
|
230
|
+
ReadinessPhase.TEST_COVERAGE: self._phase_test_coverage,
|
|
231
|
+
ReadinessPhase.SECURITY_SURFACE: self._phase_security,
|
|
232
|
+
ReadinessPhase.OBSERVABILITY: self._phase_observability,
|
|
233
|
+
ReadinessPhase.DEPLOYABILITY: self._phase_deployability,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
results: list[PhaseResult] = []
|
|
237
|
+
for phase in requested:
|
|
238
|
+
method = phase_methods.get(phase)
|
|
239
|
+
if method:
|
|
240
|
+
try:
|
|
241
|
+
result = method(scope)
|
|
242
|
+
results.append(result)
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.warning("Phase %s failed: %s", phase, e, exc_info=True)
|
|
245
|
+
results.append(PhaseResult(
|
|
246
|
+
phase=phase,
|
|
247
|
+
phase_name=_PHASE_NAMES[phase],
|
|
248
|
+
score=0.0,
|
|
249
|
+
findings=[ReadinessFinding(
|
|
250
|
+
phase=phase,
|
|
251
|
+
severity=ReadinessSeverity.CRITICAL,
|
|
252
|
+
title=f"Phase {phase.name} failed",
|
|
253
|
+
detail=str(e),
|
|
254
|
+
)],
|
|
255
|
+
summary=f"Phase failed: {e}",
|
|
256
|
+
))
|
|
257
|
+
|
|
258
|
+
# Tracer bullets
|
|
259
|
+
bullets: list[TracerBulletResult] = []
|
|
260
|
+
if tracer_bullets:
|
|
261
|
+
try:
|
|
262
|
+
bullets = self._run_tracer_bullets(scope)
|
|
263
|
+
except Exception as e:
|
|
264
|
+
logger.warning("Tracer bullets failed: %s", e, exc_info=True)
|
|
265
|
+
|
|
266
|
+
# Compute overall score
|
|
267
|
+
overall_score, overall_grade = self._compute_overall(results)
|
|
268
|
+
|
|
269
|
+
return ReadinessReport(
|
|
270
|
+
project_name=project_name,
|
|
271
|
+
timestamp=timestamp,
|
|
272
|
+
phases_run=requested,
|
|
273
|
+
phase_results=results,
|
|
274
|
+
tracer_bullets=bullets,
|
|
275
|
+
overall_score=overall_score,
|
|
276
|
+
overall_grade=overall_grade,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
def format_report(self, report: ReadinessReport) -> str:
|
|
280
|
+
"""Format a ReadinessReport as human-readable text."""
|
|
281
|
+
lines: list[str] = []
|
|
282
|
+
score_pct = int(report.overall_score * 100)
|
|
283
|
+
lines.append(f"# Readiness Report: {report.project_name}")
|
|
284
|
+
lines.append(f"Generated: {report.timestamp}")
|
|
285
|
+
lines.append(f"Overall Grade: {report.overall_grade} ({score_pct}/100)")
|
|
286
|
+
lines.append("")
|
|
287
|
+
|
|
288
|
+
for pr in report.phase_results:
|
|
289
|
+
phase_pct = int(pr.score * 100)
|
|
290
|
+
lines.append(f"## Phase {pr.phase}: {pr.phase_name} ({phase_pct}/100)")
|
|
291
|
+
if pr.summary:
|
|
292
|
+
lines.append(f" {pr.summary}")
|
|
293
|
+
for f in pr.findings:
|
|
294
|
+
tag = _severity_tag(f.severity)
|
|
295
|
+
lines.append(f" {tag} {f.title}")
|
|
296
|
+
if f.detail:
|
|
297
|
+
lines.append(f" {f.detail}")
|
|
298
|
+
for ev in f.evidence[:5]:
|
|
299
|
+
lines.append(f" {ev}")
|
|
300
|
+
if f.recommendation:
|
|
301
|
+
lines.append(f" -> {f.recommendation}")
|
|
302
|
+
lines.append("")
|
|
303
|
+
|
|
304
|
+
if report.tracer_bullets:
|
|
305
|
+
passing = sum(1 for tb in report.tracer_bullets if tb.status == "production_ready")
|
|
306
|
+
total = len(report.tracer_bullets)
|
|
307
|
+
lines.append(f"## Tracer Bullets ({passing}/{total} passing)")
|
|
308
|
+
for tb in report.tracer_bullets:
|
|
309
|
+
tag = _status_tag(tb.status)
|
|
310
|
+
lines.append(f" {tag} {tb.journey_name} ({tb.steps_passed}/{tb.steps_checked})")
|
|
311
|
+
if tb.weak_link:
|
|
312
|
+
lines.append(f" Weak link: {tb.weak_link}")
|
|
313
|
+
for ev in tb.evidence[:3]:
|
|
314
|
+
lines.append(f" {ev}")
|
|
315
|
+
lines.append("")
|
|
316
|
+
|
|
317
|
+
return "\n".join(lines)
|
|
318
|
+
|
|
319
|
+
# ------------------------------------------------------------------
|
|
320
|
+
# Phase implementations
|
|
321
|
+
# ------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
def _phase_baseline(self, scope: str) -> PhaseResult:
|
|
324
|
+
"""Phase 0: Can it run?"""
|
|
325
|
+
findings: list[ReadinessFinding] = []
|
|
326
|
+
phase = ReadinessPhase.BASELINE
|
|
327
|
+
score = 1.0
|
|
328
|
+
|
|
329
|
+
# Git status
|
|
330
|
+
try:
|
|
331
|
+
result = subprocess.run(
|
|
332
|
+
["git", "status", "--porcelain"],
|
|
333
|
+
capture_output=True, text=True, timeout=10,
|
|
334
|
+
cwd=self._project_dir,
|
|
335
|
+
)
|
|
336
|
+
dirty_count = len([l for l in result.stdout.strip().splitlines() if l.strip()])
|
|
337
|
+
if dirty_count == 0:
|
|
338
|
+
findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title="Git status: clean"))
|
|
339
|
+
else:
|
|
340
|
+
findings.append(ReadinessFinding(
|
|
341
|
+
phase=phase, severity=ReadinessSeverity.WARNING,
|
|
342
|
+
title=f"Git has {dirty_count} uncommitted change(s)",
|
|
343
|
+
recommendation="Commit or stash before deployment",
|
|
344
|
+
))
|
|
345
|
+
score -= 0.05
|
|
346
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
347
|
+
findings.append(ReadinessFinding(
|
|
348
|
+
phase=phase, severity=ReadinessSeverity.INFO,
|
|
349
|
+
title="Not a git repository or git not available",
|
|
350
|
+
))
|
|
351
|
+
|
|
352
|
+
# Build system detection
|
|
353
|
+
from attocode_intel.helpers import _detect_build_system
|
|
354
|
+
files = self._get_file_list(scope)
|
|
355
|
+
build_system = _detect_build_system(files)
|
|
356
|
+
if build_system and build_system != "unknown":
|
|
357
|
+
findings.append(ReadinessFinding(
|
|
358
|
+
phase=phase, severity=ReadinessSeverity.PASS,
|
|
359
|
+
title=f"Build system: {build_system}",
|
|
360
|
+
))
|
|
361
|
+
else:
|
|
362
|
+
findings.append(ReadinessFinding(
|
|
363
|
+
phase=phase, severity=ReadinessSeverity.WARNING,
|
|
364
|
+
title="No build system detected",
|
|
365
|
+
recommendation="Add pyproject.toml, package.json, or equivalent",
|
|
366
|
+
))
|
|
367
|
+
score -= 0.15
|
|
368
|
+
|
|
369
|
+
# Lock files
|
|
370
|
+
basenames = {os.path.basename(fi.relative_path) for fi in files}
|
|
371
|
+
found_locks = basenames & _LOCK_FILES
|
|
372
|
+
if found_locks:
|
|
373
|
+
findings.append(ReadinessFinding(
|
|
374
|
+
phase=phase, severity=ReadinessSeverity.PASS,
|
|
375
|
+
title=f"Lock file(s): {', '.join(sorted(found_locks))}",
|
|
376
|
+
))
|
|
377
|
+
else:
|
|
378
|
+
findings.append(ReadinessFinding(
|
|
379
|
+
phase=phase, severity=ReadinessSeverity.WARNING,
|
|
380
|
+
title="No dependency lock file found",
|
|
381
|
+
recommendation="Run `uv lock`, `npm install`, or equivalent to generate a lock file",
|
|
382
|
+
))
|
|
383
|
+
score -= 0.1
|
|
384
|
+
|
|
385
|
+
# Entry points
|
|
386
|
+
from attocode_intel.helpers import _find_entry_points
|
|
387
|
+
try:
|
|
388
|
+
ast_svc = self._get_ast_service()
|
|
389
|
+
entry_points = _find_entry_points(files, ast_svc.index)
|
|
390
|
+
if entry_points:
|
|
391
|
+
findings.append(ReadinessFinding(
|
|
392
|
+
phase=phase, severity=ReadinessSeverity.PASS,
|
|
393
|
+
title=f"{len(entry_points)} entry point(s) detected",
|
|
394
|
+
evidence=[f"{ep[0]} ({ep[1]})" for ep in entry_points[:5]],
|
|
395
|
+
))
|
|
396
|
+
else:
|
|
397
|
+
findings.append(ReadinessFinding(
|
|
398
|
+
phase=phase, severity=ReadinessSeverity.INFO,
|
|
399
|
+
title="No clear entry points detected",
|
|
400
|
+
))
|
|
401
|
+
score -= 0.05
|
|
402
|
+
except Exception:
|
|
403
|
+
pass
|
|
404
|
+
|
|
405
|
+
return PhaseResult(
|
|
406
|
+
phase=phase, phase_name=_PHASE_NAMES[phase],
|
|
407
|
+
score=max(0.0, score),
|
|
408
|
+
findings=findings,
|
|
409
|
+
summary=f"Build: {build_system}, Locks: {', '.join(sorted(found_locks)) or 'none'}",
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
def _phase_api_logic(self, scope: str) -> PhaseResult:
|
|
413
|
+
"""Phase 1: Route inventory, stubs, TODOs."""
|
|
414
|
+
findings: list[ReadinessFinding] = []
|
|
415
|
+
phase = ReadinessPhase.API_BUSINESS_LOGIC
|
|
416
|
+
score = 1.0
|
|
417
|
+
|
|
418
|
+
route_handlers: list[tuple[str, int, str]] = [] # (file, line, name)
|
|
419
|
+
stub_functions: list[tuple[str, int, str]] = []
|
|
420
|
+
todo_count = 0
|
|
421
|
+
|
|
422
|
+
for fi in self._get_source_files(scope):
|
|
423
|
+
abs_path = os.path.join(self._project_dir, fi.relative_path)
|
|
424
|
+
try:
|
|
425
|
+
content = Path(abs_path).read_text(encoding="utf-8", errors="replace")
|
|
426
|
+
except OSError:
|
|
427
|
+
continue
|
|
428
|
+
|
|
429
|
+
lines = content.splitlines()
|
|
430
|
+
for i, line in enumerate(lines, 1):
|
|
431
|
+
# Route detection
|
|
432
|
+
if _ROUTE_DECORATOR_RE.search(line):
|
|
433
|
+
# Next non-decorator line is likely the function def
|
|
434
|
+
for j in range(i, min(i + 5, len(lines) + 1)):
|
|
435
|
+
if j - 1 < len(lines) and lines[j - 1].strip().startswith("def "):
|
|
436
|
+
fname = lines[j - 1].strip().split("(")[0].replace("def ", "")
|
|
437
|
+
route_handlers.append((fi.relative_path, j, fname))
|
|
438
|
+
break
|
|
439
|
+
|
|
440
|
+
# TODO/FIXME
|
|
441
|
+
if re.search(r"\b(TODO|FIXME|HACK|XXX)\b", line):
|
|
442
|
+
todo_count += 1
|
|
443
|
+
|
|
444
|
+
# Stub detection in function bodies
|
|
445
|
+
self._detect_stubs(fi.relative_path, lines, stub_functions)
|
|
446
|
+
|
|
447
|
+
if route_handlers:
|
|
448
|
+
findings.append(ReadinessFinding(
|
|
449
|
+
phase=phase, severity=ReadinessSeverity.PASS,
|
|
450
|
+
title=f"{len(route_handlers)} route handler(s) detected",
|
|
451
|
+
evidence=[f"{rh[0]}:{rh[1]} -- {rh[2]}()" for rh in route_handlers[:10]],
|
|
452
|
+
))
|
|
453
|
+
else:
|
|
454
|
+
findings.append(ReadinessFinding(
|
|
455
|
+
phase=phase, severity=ReadinessSeverity.INFO,
|
|
456
|
+
title="No route handlers detected (may not be a web API)",
|
|
457
|
+
))
|
|
458
|
+
|
|
459
|
+
if stub_functions:
|
|
460
|
+
findings.append(ReadinessFinding(
|
|
461
|
+
phase=phase, severity=ReadinessSeverity.CRITICAL,
|
|
462
|
+
title=f"{len(stub_functions)} stub function(s) found",
|
|
463
|
+
detail="Functions with only pass, ..., or raise NotImplementedError",
|
|
464
|
+
evidence=[f"{s[0]}:{s[1]} -- {s[2]}()" for s in stub_functions[:10]],
|
|
465
|
+
recommendation="Implement stub functions before shipping",
|
|
466
|
+
))
|
|
467
|
+
score -= min(0.3, len(stub_functions) * 0.05)
|
|
468
|
+
|
|
469
|
+
if todo_count > 0:
|
|
470
|
+
sev = ReadinessSeverity.WARNING if todo_count > 10 else ReadinessSeverity.INFO
|
|
471
|
+
findings.append(ReadinessFinding(
|
|
472
|
+
phase=phase, severity=sev,
|
|
473
|
+
title=f"{todo_count} TODO/FIXME markers in source code",
|
|
474
|
+
))
|
|
475
|
+
if todo_count > 20:
|
|
476
|
+
score -= 0.1
|
|
477
|
+
|
|
478
|
+
return PhaseResult(
|
|
479
|
+
phase=phase, phase_name=_PHASE_NAMES[phase],
|
|
480
|
+
score=max(0.0, score),
|
|
481
|
+
findings=findings,
|
|
482
|
+
summary=f"{len(route_handlers)} routes, {len(stub_functions)} stubs, {todo_count} TODOs",
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
def _phase_frontend(self, scope: str) -> PhaseResult:
|
|
486
|
+
"""Phase 2: Frontend component inventory."""
|
|
487
|
+
findings: list[ReadinessFinding] = []
|
|
488
|
+
phase = ReadinessPhase.FRONTEND_FLOWS
|
|
489
|
+
|
|
490
|
+
frontend_files = [
|
|
491
|
+
fi for fi in self._get_source_files(scope)
|
|
492
|
+
if Path(fi.relative_path).suffix.lower() in _FRONTEND_EXTS
|
|
493
|
+
]
|
|
494
|
+
|
|
495
|
+
if not frontend_files:
|
|
496
|
+
findings.append(ReadinessFinding(
|
|
497
|
+
phase=phase, severity=ReadinessSeverity.PASS,
|
|
498
|
+
title="No frontend files detected (not a frontend project)",
|
|
499
|
+
))
|
|
500
|
+
return PhaseResult(
|
|
501
|
+
phase=phase, phase_name=_PHASE_NAMES[phase],
|
|
502
|
+
score=1.0, findings=findings,
|
|
503
|
+
summary="Skipped (no frontend files)",
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
findings.append(ReadinessFinding(
|
|
507
|
+
phase=phase, severity=ReadinessSeverity.PASS,
|
|
508
|
+
title=f"{len(frontend_files)} frontend component file(s)",
|
|
509
|
+
evidence=[fi.relative_path for fi in frontend_files[:10]],
|
|
510
|
+
))
|
|
511
|
+
|
|
512
|
+
return PhaseResult(
|
|
513
|
+
phase=phase, phase_name=_PHASE_NAMES[phase],
|
|
514
|
+
score=0.8,
|
|
515
|
+
findings=findings,
|
|
516
|
+
summary=f"{len(frontend_files)} frontend files",
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
def _phase_data_layer(self, scope: str) -> PhaseResult:
|
|
520
|
+
"""Phase 3: Migrations, models, constraints."""
|
|
521
|
+
findings: list[ReadinessFinding] = []
|
|
522
|
+
phase = ReadinessPhase.DATA_LAYER
|
|
523
|
+
score = 1.0
|
|
524
|
+
|
|
525
|
+
# Migration detection
|
|
526
|
+
migration_dirs: list[str] = []
|
|
527
|
+
for dirpath, dirnames, filenames in os.walk(self._project_dir):
|
|
528
|
+
rel = os.path.relpath(dirpath, self._project_dir)
|
|
529
|
+
basename = os.path.basename(dirpath)
|
|
530
|
+
if basename in ("migrations", "alembic", "prisma", "versions"):
|
|
531
|
+
migration_dirs.append(rel)
|
|
532
|
+
# Don't recurse into common skip dirs
|
|
533
|
+
dirnames[:] = [d for d in dirnames if d not in {".git", "node_modules", "__pycache__", ".venv"}]
|
|
534
|
+
|
|
535
|
+
if migration_dirs:
|
|
536
|
+
findings.append(ReadinessFinding(
|
|
537
|
+
phase=phase, severity=ReadinessSeverity.PASS,
|
|
538
|
+
title=f"Migration directory found: {', '.join(migration_dirs[:3])}",
|
|
539
|
+
))
|
|
540
|
+
else:
|
|
541
|
+
findings.append(ReadinessFinding(
|
|
542
|
+
phase=phase, severity=ReadinessSeverity.INFO,
|
|
543
|
+
title="No migration directories found",
|
|
544
|
+
detail="May not be a database-backed project",
|
|
545
|
+
))
|
|
546
|
+
|
|
547
|
+
# ORM model detection
|
|
548
|
+
model_count = 0
|
|
549
|
+
for fi in self._get_source_files(scope):
|
|
550
|
+
abs_path = os.path.join(self._project_dir, fi.relative_path)
|
|
551
|
+
try:
|
|
552
|
+
content = Path(abs_path).read_text(encoding="utf-8", errors="replace")
|
|
553
|
+
except OSError:
|
|
554
|
+
continue
|
|
555
|
+
# SQLAlchemy/Django model patterns
|
|
556
|
+
if re.search(r"class\s+\w+\(.*(?:Base|Model|db\.Model|DeclarativeBase)", content):
|
|
557
|
+
model_count += 1
|
|
558
|
+
|
|
559
|
+
if model_count:
|
|
560
|
+
findings.append(ReadinessFinding(
|
|
561
|
+
phase=phase, severity=ReadinessSeverity.PASS,
|
|
562
|
+
title=f"{model_count} ORM model file(s) detected",
|
|
563
|
+
))
|
|
564
|
+
|
|
565
|
+
return PhaseResult(
|
|
566
|
+
phase=phase, phase_name=_PHASE_NAMES[phase],
|
|
567
|
+
score=max(0.0, score),
|
|
568
|
+
findings=findings,
|
|
569
|
+
summary=f"Migrations: {len(migration_dirs)}, Models: {model_count}",
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
def _phase_test_coverage(self, scope: str) -> PhaseResult:
|
|
573
|
+
"""Phase 4: Test file inventory, coverage gaps, skipped tests."""
|
|
574
|
+
findings: list[ReadinessFinding] = []
|
|
575
|
+
phase = ReadinessPhase.TEST_COVERAGE
|
|
576
|
+
score = 1.0
|
|
577
|
+
|
|
578
|
+
all_files = self._get_file_list(scope)
|
|
579
|
+
test_files = [fi for fi in all_files if fi.is_test]
|
|
580
|
+
source_files = [fi for fi in all_files if not fi.is_test and not fi.is_config]
|
|
581
|
+
|
|
582
|
+
if not source_files:
|
|
583
|
+
return PhaseResult(
|
|
584
|
+
phase=phase, phase_name=_PHASE_NAMES[phase],
|
|
585
|
+
score=1.0, findings=[],
|
|
586
|
+
summary="No source files to test",
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
test_ratio = len(test_files) / max(len(source_files), 1)
|
|
590
|
+
|
|
591
|
+
findings.append(ReadinessFinding(
|
|
592
|
+
phase=phase, severity=ReadinessSeverity.PASS if test_ratio >= 0.3 else ReadinessSeverity.WARNING,
|
|
593
|
+
title=f"{len(test_files)} test file(s) for {len(source_files)} source file(s) ({test_ratio:.0%})",
|
|
594
|
+
))
|
|
595
|
+
|
|
596
|
+
if test_ratio < 0.1:
|
|
597
|
+
score -= 0.4
|
|
598
|
+
elif test_ratio < 0.3:
|
|
599
|
+
score -= 0.2
|
|
600
|
+
|
|
601
|
+
# Detect skipped tests
|
|
602
|
+
skip_count = 0
|
|
603
|
+
for fi in test_files:
|
|
604
|
+
abs_path = os.path.join(self._project_dir, fi.relative_path)
|
|
605
|
+
try:
|
|
606
|
+
content = Path(abs_path).read_text(encoding="utf-8", errors="replace")
|
|
607
|
+
except OSError:
|
|
608
|
+
continue
|
|
609
|
+
for pat in _SKIP_PATTERNS:
|
|
610
|
+
skip_count += len(pat.findall(content))
|
|
611
|
+
|
|
612
|
+
if skip_count > 0:
|
|
613
|
+
findings.append(ReadinessFinding(
|
|
614
|
+
phase=phase, severity=ReadinessSeverity.WARNING,
|
|
615
|
+
title=f"{skip_count} skipped test(s) detected",
|
|
616
|
+
recommendation="Review and either fix or remove skipped tests",
|
|
617
|
+
))
|
|
618
|
+
score -= min(0.15, skip_count * 0.02)
|
|
619
|
+
|
|
620
|
+
return PhaseResult(
|
|
621
|
+
phase=phase, phase_name=_PHASE_NAMES[phase],
|
|
622
|
+
score=max(0.0, score),
|
|
623
|
+
findings=findings,
|
|
624
|
+
summary=f"{len(test_files)} tests / {len(source_files)} source ({test_ratio:.0%}), {skip_count} skipped",
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
def _phase_security(self, scope: str) -> PhaseResult:
|
|
628
|
+
"""Phase 5: Security scan + auth checks."""
|
|
629
|
+
findings: list[ReadinessFinding] = []
|
|
630
|
+
phase = ReadinessPhase.SECURITY_SURFACE
|
|
631
|
+
score = 1.0
|
|
632
|
+
|
|
633
|
+
# Use existing SecurityScanner
|
|
634
|
+
try:
|
|
635
|
+
from attocode_intel._internal.integrations.security.scanner import SecurityScanner
|
|
636
|
+
scanner = SecurityScanner(root_dir=self._project_dir)
|
|
637
|
+
report = scanner.scan(mode="full", path=scope)
|
|
638
|
+
compliance = report.compliance_score / 100.0 if hasattr(report, "compliance_score") else 0.8
|
|
639
|
+
|
|
640
|
+
critical_findings = [f for f in report.findings if getattr(f, "severity", "") == "high"]
|
|
641
|
+
if critical_findings:
|
|
642
|
+
findings.append(ReadinessFinding(
|
|
643
|
+
phase=phase, severity=ReadinessSeverity.CRITICAL,
|
|
644
|
+
title=f"{len(critical_findings)} high-severity security finding(s)",
|
|
645
|
+
evidence=[str(f)[:100] for f in critical_findings[:5]],
|
|
646
|
+
))
|
|
647
|
+
score -= min(0.4, len(critical_findings) * 0.1)
|
|
648
|
+
else:
|
|
649
|
+
findings.append(ReadinessFinding(
|
|
650
|
+
phase=phase, severity=ReadinessSeverity.PASS,
|
|
651
|
+
title="No high-severity security findings",
|
|
652
|
+
))
|
|
653
|
+
|
|
654
|
+
findings.append(ReadinessFinding(
|
|
655
|
+
phase=phase, severity=ReadinessSeverity.PASS if compliance >= 0.8 else ReadinessSeverity.WARNING,
|
|
656
|
+
title=f"Security compliance score: {compliance:.0%}",
|
|
657
|
+
))
|
|
658
|
+
score = min(score, compliance)
|
|
659
|
+
except Exception as e:
|
|
660
|
+
findings.append(ReadinessFinding(
|
|
661
|
+
phase=phase, severity=ReadinessSeverity.INFO,
|
|
662
|
+
title=f"Security scanner unavailable: {e}",
|
|
663
|
+
))
|
|
664
|
+
|
|
665
|
+
return PhaseResult(
|
|
666
|
+
phase=phase, phase_name=_PHASE_NAMES[phase],
|
|
667
|
+
score=max(0.0, score),
|
|
668
|
+
findings=findings,
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
def _phase_observability(self, scope: str) -> PhaseResult:
|
|
672
|
+
"""Phase 6: Logging, metrics, error tracking."""
|
|
673
|
+
findings: list[ReadinessFinding] = []
|
|
674
|
+
phase = ReadinessPhase.OBSERVABILITY
|
|
675
|
+
score = 0.5 # Start lower, add for each capability found
|
|
676
|
+
|
|
677
|
+
has_logging = False
|
|
678
|
+
has_metrics = False
|
|
679
|
+
has_error_tracking = False
|
|
680
|
+
|
|
681
|
+
for fi in self._get_source_files(scope):
|
|
682
|
+
abs_path = os.path.join(self._project_dir, fi.relative_path)
|
|
683
|
+
try:
|
|
684
|
+
content = Path(abs_path).read_text(encoding="utf-8", errors="replace")
|
|
685
|
+
except OSError:
|
|
686
|
+
continue
|
|
687
|
+
if _LOGGING_IMPORTS.search(content):
|
|
688
|
+
has_logging = True
|
|
689
|
+
if _METRICS_IMPORTS.search(content):
|
|
690
|
+
has_metrics = True
|
|
691
|
+
if _ERROR_TRACKING_IMPORTS.search(content):
|
|
692
|
+
has_error_tracking = True
|
|
693
|
+
|
|
694
|
+
if has_logging:
|
|
695
|
+
findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title="Logging framework detected"))
|
|
696
|
+
score += 0.2
|
|
697
|
+
else:
|
|
698
|
+
findings.append(ReadinessFinding(
|
|
699
|
+
phase=phase, severity=ReadinessSeverity.WARNING, title="No logging framework detected",
|
|
700
|
+
recommendation="Add structured logging (logging, structlog, or loguru)",
|
|
701
|
+
))
|
|
702
|
+
|
|
703
|
+
if has_metrics:
|
|
704
|
+
findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title="Metrics framework detected"))
|
|
705
|
+
score += 0.15
|
|
706
|
+
else:
|
|
707
|
+
findings.append(ReadinessFinding(
|
|
708
|
+
phase=phase, severity=ReadinessSeverity.INFO, title="No metrics framework detected",
|
|
709
|
+
))
|
|
710
|
+
|
|
711
|
+
if has_error_tracking:
|
|
712
|
+
findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title="Error tracking detected"))
|
|
713
|
+
score += 0.15
|
|
714
|
+
else:
|
|
715
|
+
findings.append(ReadinessFinding(
|
|
716
|
+
phase=phase, severity=ReadinessSeverity.INFO, title="No error tracking detected",
|
|
717
|
+
))
|
|
718
|
+
|
|
719
|
+
return PhaseResult(
|
|
720
|
+
phase=phase, phase_name=_PHASE_NAMES[phase],
|
|
721
|
+
score=min(1.0, score),
|
|
722
|
+
findings=findings,
|
|
723
|
+
summary=f"Logging: {'yes' if has_logging else 'no'}, Metrics: {'yes' if has_metrics else 'no'}, Errors: {'yes' if has_error_tracking else 'no'}",
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
def _phase_deployability(self, scope: str) -> PhaseResult:
|
|
727
|
+
"""Phase 7: Docker, CI/CD, env config."""
|
|
728
|
+
findings: list[ReadinessFinding] = []
|
|
729
|
+
phase = ReadinessPhase.DEPLOYABILITY
|
|
730
|
+
score = 0.5
|
|
731
|
+
|
|
732
|
+
pd = self._project_dir
|
|
733
|
+
|
|
734
|
+
# Dockerfile
|
|
735
|
+
if os.path.isfile(os.path.join(pd, "Dockerfile")):
|
|
736
|
+
findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title="Dockerfile present"))
|
|
737
|
+
score += 0.15
|
|
738
|
+
elif os.path.isfile(os.path.join(pd, "docker-compose.yml")) or os.path.isfile(os.path.join(pd, "docker-compose.yaml")):
|
|
739
|
+
findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title="docker-compose config present"))
|
|
740
|
+
score += 0.1
|
|
741
|
+
else:
|
|
742
|
+
findings.append(ReadinessFinding(
|
|
743
|
+
phase=phase, severity=ReadinessSeverity.INFO, title="No Dockerfile found",
|
|
744
|
+
))
|
|
745
|
+
|
|
746
|
+
# CI/CD
|
|
747
|
+
ci_found = False
|
|
748
|
+
if os.path.isdir(os.path.join(pd, ".github", "workflows")):
|
|
749
|
+
ci_found = True
|
|
750
|
+
findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title="GitHub Actions workflows found"))
|
|
751
|
+
if os.path.isfile(os.path.join(pd, ".gitlab-ci.yml")):
|
|
752
|
+
ci_found = True
|
|
753
|
+
findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title="GitLab CI config found"))
|
|
754
|
+
if os.path.isfile(os.path.join(pd, "Jenkinsfile")):
|
|
755
|
+
ci_found = True
|
|
756
|
+
findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title="Jenkinsfile found"))
|
|
757
|
+
|
|
758
|
+
if ci_found:
|
|
759
|
+
score += 0.2
|
|
760
|
+
else:
|
|
761
|
+
findings.append(ReadinessFinding(
|
|
762
|
+
phase=phase, severity=ReadinessSeverity.WARNING, title="No CI/CD configuration found",
|
|
763
|
+
recommendation="Add .github/workflows/, .gitlab-ci.yml, or equivalent",
|
|
764
|
+
))
|
|
765
|
+
|
|
766
|
+
# .env.example
|
|
767
|
+
if os.path.isfile(os.path.join(pd, ".env.example")):
|
|
768
|
+
findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title=".env.example present"))
|
|
769
|
+
score += 0.1
|
|
770
|
+
elif os.path.isfile(os.path.join(pd, ".env.template")):
|
|
771
|
+
findings.append(ReadinessFinding(phase=phase, severity=ReadinessSeverity.PASS, title=".env.template present"))
|
|
772
|
+
score += 0.1
|
|
773
|
+
|
|
774
|
+
return PhaseResult(
|
|
775
|
+
phase=phase, phase_name=_PHASE_NAMES[phase],
|
|
776
|
+
score=min(1.0, score),
|
|
777
|
+
findings=findings,
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
# ------------------------------------------------------------------
|
|
781
|
+
# Tracer Bullets
|
|
782
|
+
# ------------------------------------------------------------------
|
|
783
|
+
|
|
784
|
+
def _run_tracer_bullets(self, scope: str) -> list[TracerBulletResult]:
|
|
785
|
+
"""Trace top entry point -> dependency chains for end-to-end verification."""
|
|
786
|
+
results: list[TracerBulletResult] = []
|
|
787
|
+
try:
|
|
788
|
+
ast_svc = self._get_ast_service()
|
|
789
|
+
except Exception:
|
|
790
|
+
return results
|
|
791
|
+
|
|
792
|
+
# Find entry-like files (high importance or entry filenames)
|
|
793
|
+
from attocode_intel.helpers import _find_entry_points
|
|
794
|
+
files = self._get_file_list(scope)
|
|
795
|
+
entry_points = _find_entry_points(files, ast_svc.index)
|
|
796
|
+
|
|
797
|
+
for ep_path, ep_reason in entry_points[:10]:
|
|
798
|
+
# Trace dependency chain
|
|
799
|
+
deps = list(ast_svc.index.file_dependencies.get(ep_path, set()))[:10]
|
|
800
|
+
steps_total = 1 + len(deps)
|
|
801
|
+
steps_passed = 1 # Entry point exists
|
|
802
|
+
|
|
803
|
+
weak_link = ""
|
|
804
|
+
evidence = [f"Entry: {ep_path} ({ep_reason})"]
|
|
805
|
+
|
|
806
|
+
for dep in deps:
|
|
807
|
+
dep_abs = os.path.join(self._project_dir, dep)
|
|
808
|
+
if os.path.isfile(dep_abs):
|
|
809
|
+
steps_passed += 1
|
|
810
|
+
else:
|
|
811
|
+
if not weak_link:
|
|
812
|
+
weak_link = f"Missing dependency: {dep}"
|
|
813
|
+
|
|
814
|
+
ratio = steps_passed / steps_total if steps_total > 0 else 0
|
|
815
|
+
if ratio >= 1.0:
|
|
816
|
+
status = "production_ready"
|
|
817
|
+
elif ratio >= 0.7:
|
|
818
|
+
status = "happy_path_only"
|
|
819
|
+
elif ratio > 0:
|
|
820
|
+
status = "partial"
|
|
821
|
+
else:
|
|
822
|
+
status = "not_implemented"
|
|
823
|
+
|
|
824
|
+
results.append(TracerBulletResult(
|
|
825
|
+
journey_name=ep_path,
|
|
826
|
+
status=status,
|
|
827
|
+
steps_checked=steps_total,
|
|
828
|
+
steps_passed=steps_passed,
|
|
829
|
+
weak_link=weak_link,
|
|
830
|
+
evidence=evidence,
|
|
831
|
+
))
|
|
832
|
+
|
|
833
|
+
return results
|
|
834
|
+
|
|
835
|
+
# ------------------------------------------------------------------
|
|
836
|
+
# Scoring
|
|
837
|
+
# ------------------------------------------------------------------
|
|
838
|
+
|
|
839
|
+
@staticmethod
|
|
840
|
+
def _compute_overall(results: list[PhaseResult]) -> tuple[float, str]:
|
|
841
|
+
"""Compute weighted overall score and letter grade."""
|
|
842
|
+
if not results:
|
|
843
|
+
return 0.0, "F"
|
|
844
|
+
|
|
845
|
+
weights = {
|
|
846
|
+
ReadinessPhase.BASELINE: 2.0,
|
|
847
|
+
ReadinessPhase.TEST_COVERAGE: 2.0,
|
|
848
|
+
ReadinessPhase.SECURITY_SURFACE: 2.0,
|
|
849
|
+
}
|
|
850
|
+
total_weight = 0.0
|
|
851
|
+
weighted_sum = 0.0
|
|
852
|
+
for pr in results:
|
|
853
|
+
w = weights.get(pr.phase, 1.0)
|
|
854
|
+
weighted_sum += pr.score * w
|
|
855
|
+
total_weight += w
|
|
856
|
+
|
|
857
|
+
score = weighted_sum / total_weight if total_weight > 0 else 0.0
|
|
858
|
+
|
|
859
|
+
if score >= 0.9:
|
|
860
|
+
grade = "A"
|
|
861
|
+
elif score >= 0.75:
|
|
862
|
+
grade = "B"
|
|
863
|
+
elif score >= 0.6:
|
|
864
|
+
grade = "C"
|
|
865
|
+
elif score >= 0.45:
|
|
866
|
+
grade = "D"
|
|
867
|
+
else:
|
|
868
|
+
grade = "F"
|
|
869
|
+
|
|
870
|
+
return round(score, 3), grade
|
|
871
|
+
|
|
872
|
+
# ------------------------------------------------------------------
|
|
873
|
+
# Helpers
|
|
874
|
+
# ------------------------------------------------------------------
|
|
875
|
+
|
|
876
|
+
def _get_ast_service(self):
|
|
877
|
+
"""Get the AST service for the project."""
|
|
878
|
+
from attocode_intel._internal.integrations.context.ast_service import ASTService
|
|
879
|
+
svc = ASTService.get_instance(self._project_dir)
|
|
880
|
+
if not svc.initialized:
|
|
881
|
+
svc.initialize()
|
|
882
|
+
return svc
|
|
883
|
+
|
|
884
|
+
def _get_file_list(self, scope: str) -> list:
|
|
885
|
+
"""Get the file list from CodebaseContextManager."""
|
|
886
|
+
from attocode_intel._internal.integrations.context.codebase_context import CodebaseContextManager
|
|
887
|
+
ctx = CodebaseContextManager(root_dir=self._project_dir)
|
|
888
|
+
ctx.discover_files()
|
|
889
|
+
files = ctx._files
|
|
890
|
+
if scope:
|
|
891
|
+
files = [fi for fi in files if fi.relative_path.startswith(scope)]
|
|
892
|
+
return files
|
|
893
|
+
|
|
894
|
+
def _get_source_files(self, scope: str) -> list:
|
|
895
|
+
"""Get non-test, non-config source files."""
|
|
896
|
+
return [
|
|
897
|
+
fi for fi in self._get_file_list(scope)
|
|
898
|
+
if not fi.is_test and not fi.is_config
|
|
899
|
+
]
|
|
900
|
+
|
|
901
|
+
@staticmethod
|
|
902
|
+
def _detect_stubs(
|
|
903
|
+
rel_path: str,
|
|
904
|
+
lines: list[str],
|
|
905
|
+
stubs: list[tuple[str, int, str]],
|
|
906
|
+
) -> None:
|
|
907
|
+
"""Detect stub functions in source code."""
|
|
908
|
+
i = 0
|
|
909
|
+
while i < len(lines):
|
|
910
|
+
line = lines[i]
|
|
911
|
+
stripped = line.strip()
|
|
912
|
+
|
|
913
|
+
# Look for function definitions
|
|
914
|
+
if stripped.startswith("def ") and "(" in stripped:
|
|
915
|
+
fname = stripped.split("(")[0].replace("def ", "").strip()
|
|
916
|
+
# Skip dunder methods and abstract methods
|
|
917
|
+
if fname.startswith("__") and fname.endswith("__"):
|
|
918
|
+
i += 1
|
|
919
|
+
continue
|
|
920
|
+
|
|
921
|
+
# Check the function body (next non-empty, non-comment line)
|
|
922
|
+
body_start = i + 1
|
|
923
|
+
while body_start < len(lines):
|
|
924
|
+
body_line = lines[body_start].strip()
|
|
925
|
+
if body_line and not body_line.startswith("#") and not body_line.startswith('"""') and not body_line.startswith("'''"):
|
|
926
|
+
break
|
|
927
|
+
# Skip docstrings
|
|
928
|
+
if body_line.startswith('"""') or body_line.startswith("'''"):
|
|
929
|
+
quote = body_line[:3]
|
|
930
|
+
if body_line.count(quote) >= 2:
|
|
931
|
+
body_start += 1
|
|
932
|
+
else:
|
|
933
|
+
body_start += 1
|
|
934
|
+
while body_start < len(lines) and quote not in lines[body_start]:
|
|
935
|
+
body_start += 1
|
|
936
|
+
body_start += 1
|
|
937
|
+
continue
|
|
938
|
+
body_start += 1
|
|
939
|
+
|
|
940
|
+
if body_start < len(lines):
|
|
941
|
+
body_line = lines[body_start].strip()
|
|
942
|
+
for pat in _STUB_PATTERNS:
|
|
943
|
+
if pat.match(body_line):
|
|
944
|
+
stubs.append((rel_path, i + 1, fname))
|
|
945
|
+
break
|
|
946
|
+
|
|
947
|
+
i += 1
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
# =============================================================================
|
|
951
|
+
# Formatting helpers
|
|
952
|
+
# =============================================================================
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
def _severity_tag(severity: ReadinessSeverity) -> str:
|
|
956
|
+
tags = {
|
|
957
|
+
ReadinessSeverity.PASS: "[PASS]",
|
|
958
|
+
ReadinessSeverity.INFO: "[INFO]",
|
|
959
|
+
ReadinessSeverity.WARNING: "[WARN]",
|
|
960
|
+
ReadinessSeverity.CRITICAL: "[CRIT]",
|
|
961
|
+
}
|
|
962
|
+
return tags.get(severity, "[????]")
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def _status_tag(status: str) -> str:
|
|
966
|
+
tags = {
|
|
967
|
+
"production_ready": "[PASS]",
|
|
968
|
+
"happy_path_only": "[WARN]",
|
|
969
|
+
"partial": "[PART]",
|
|
970
|
+
"not_implemented": "[FAIL]",
|
|
971
|
+
}
|
|
972
|
+
return tags.get(status, "[????]")
|