Compare commits

..

1 Commits

Author SHA1 Message Date
step35
5a6f41689f feat: add API Doc Generator — issue #98
Some checks failed
Test / pytest (pull_request) Failing after 7s
- scripts/api_doc_generator.py: AST-based scanner for scripts/ Python modules
- docs/API.md: generated API reference (33 modules, ~500 lines)
- tests/test_api_doc_generator.py: 12 smoke tests (all passing)

The generator extracts module docstrings and public function signatures (name, args, summary) and produces a markdown table per script. One consolidated document per repo (docs/API.md).

Closes #98
2026-04-26 07:02:49 -04:00
15 changed files with 839 additions and 325 deletions

472
docs/API.md Normal file
View File

@@ -0,0 +1,472 @@
# Compounding Intelligence — Scripts API Reference
*Generated: 2026-04-26 11:02 UTC*
This document auto-documents the public API surface of all scripts
in `scripts/`. Each section covers one script: module purpose,
public functions, and their signatures.
---
## `scripts/api_doc_generator.py`
API Doc Generator — Issue #98
| Function | Signature | Description |
|----------|-----------|-------------|
| `extract_functions_from_ast` | `extract_functions_from_ast(tree, file_rel)` | Extract public function names, signatures, and first-line doc summaries. |
| `parse_module` | `parse_module(filepath)` | Parse a Python file and return its module-level docstring and public functions. |
| `scan_scripts_dir` | `scan_scripts_dir(scripts_dir)` | Scan all .py files in scripts/ and extract API info. |
| `render_markdown` | `render_markdown(modules)` | Generate full docs/API.md content from the scanned modules. |
| `render_json` | `render_json(modules)` | Emit machine-readable JSON version of the API reference. |
| `main` | `main()` | - |
## `scripts/automation_opportunity_finder.py`
Automation Opportunity Finder — Scan fleet for manual processes that could be automated.
| Function | Signature | Description |
|----------|-----------|-------------|
| `analyze_cron_jobs` | `analyze_cron_jobs(hermes_home)` | Analyze cron job definitions for automation gaps. |
| `analyze_documents` | `analyze_documents(root_dirs)` | Scan documentation for manual step patterns. |
| `analyze_scripts` | `analyze_scripts(root_dirs)` | Detect repeated command sequences in scripts. |
| `analyze_session_transcripts` | `analyze_session_transcripts(session_dirs)` | Find repeated tool-call patterns in session transcripts. |
| `analyze_shell_history` | `analyze_shell_history(root_dirs)` | Find repeated shell commands from history files. |
| `deduplicate_proposals` | `deduplicate_proposals(proposals)` | Remove duplicate proposals based on title similarity. |
| `rank_proposals` | `rank_proposals(proposals)` | Sort proposals by impact * confidence (highest first). |
| `format_text_report` | `format_text_report(proposals)` | Format proposals as human-readable text. |
| `main` | `main()` | - |
## `scripts/bootstrapper.py`
Bootstrapper — assemble pre-session context from knowledge store.
| Function | Signature | Description |
|----------|-----------|-------------|
| `load_index` | `load_index(index_path)` | Load and validate the knowledge index. |
| `filter_facts` | `filter_facts(facts, repo, agent, include_global)` | Filter facts by repo, agent, and global scope. |
| `sort_facts` | `sort_facts(facts)` | Sort facts by: confidence (desc), then category priority, then fact text. |
| `load_repo_knowledge` | `load_repo_knowledge(repo)` | Load per-repo knowledge markdown if it exists. |
| `load_agent_knowledge` | `load_agent_knowledge(agent)` | Load per-agent knowledge markdown if it exists. |
| `load_global_knowledge` | `load_global_knowledge()` | Load all global knowledge markdown files. |
| `render_facts_section` | `render_facts_section(facts, category, label)` | Render a section of facts for a single category. |
| `estimate_tokens` | `estimate_tokens(text)` | Rough token estimate. |
| `truncate_to_tokens` | `truncate_to_tokens(text, max_tokens)` | Truncate text to approximately max_tokens, cutting at line boundaries. |
| `build_bootstrap_context` | `build_bootstrap_context(repo, agent, include_global, max_tokens, index_path)` | Build the full bootstrap context block. |
| `main` | `main()` | - |
## `scripts/dead_code_detector.py`
Dead Code Detector for Python Codebases
| Function | Signature | Description |
|----------|-----------|-------------|
| `is_safe_unused` | `is_safe_unused(name, filepath)` | Check if an unused name is expected to be unused. |
| `get_git_blame` | `get_git_blame(filepath, lineno)` | Get last author of a line via git blame. |
| `analyze_file` | `analyze_file(filepath)` | Analyze a single Python file for dead code. |
| `scan_repo` | `scan_repo(repo_path, exclude_patterns)` | Scan an entire repo for dead code. |
| `main` | `main()` | - |
## `scripts/dedup.py`
dedup.py — Knowledge deduplication: content hash + semantic similarity.
| Function | Signature | Description |
|----------|-----------|-------------|
| `normalize_text` | `normalize_text(text)` | Normalize text for hashing: lowercase, collapse whitespace, strip. |
| `content_hash` | `content_hash(text)` | SHA256 hash of normalized text for exact dedup. |
| `tokenize` | `tokenize(text)` | Simple tokenizer: lowercase words, 3+ chars. |
| `token_similarity` | `token_similarity(a, b)` | Token-based Jaccard similarity (0.0-1.0). |
| `quality_score` | `quality_score(fact)` | Compute quality score for merge ranking. |
| `merge_facts` | `merge_facts(keep, drop)` | Merge two near-duplicate facts, keeping higher-quality fields. |
| `dedup_facts` | `dedup_facts(facts, exact_threshold, near_threshold, dry_run)` | Deduplicate a list of knowledge facts. |
| `dedup_index_file` | `dedup_index_file(input_path, output_path, near_threshold, dry_run)` | Deduplicate an index.json file. |
| `generate_test_duplicates` | `generate_test_duplicates(n)` | Generate test facts with intentional duplicates for testing. |
| `main` | `main()` | - |
## `scripts/dependency_graph.py`
Cross-Repo Dependency Graph Builder
| Function | Signature | Description |
|----------|-----------|-------------|
| `normalize_repo_name` | `normalize_repo_name(name)` | Normalize a repo name for comparison. |
| `scan_file_for_deps` | `scan_file_for_deps(filepath, content, own_repo)` | Scan a file's content for references to other repos. |
| `scan_repo` | `scan_repo(repo_path, repo_name)` | Scan a repo directory for dependencies. |
| `detect_cycles` | `detect_cycles(graph)` | Detect circular dependencies using DFS. |
| `to_dot` | `to_dot(graph)` | Generate DOT format output. |
| `to_mermaid` | `to_mermaid(graph)` | Generate Mermaid format output. |
| `main` | `main()` | - |
## `scripts/diff_analyzer.py`
Diff Analyzer — Parse unified diffs and categorize every change.
*(no public functions — script runs as `main()` only)*
## `scripts/freshness.py`
Knowledge Freshness Cron — Detect stale entries from code changes (Issue #200)
| Function | Signature | Description |
|----------|-----------|-------------|
| `compute_file_hash` | `compute_file_hash(filepath)` | Compute SHA-256 hash of a file. Returns None if file doesn't exist. |
| `get_git_file_changes` | `get_git_file_changes(repo_path, days)` | Get files changed in git in the last N days. |
| `load_knowledge_entries` | `load_knowledge_entries(knowledge_dir)` | Load knowledge entries from YAML files in the knowledge directory. |
| `check_freshness` | `check_freshness(knowledge_dir, repo_root, days)` | Check freshness of knowledge entries against recent code changes. |
| `update_stale_hashes` | `update_stale_hashes(knowledge_dir, repo_root)` | Update hashes for stale entries. Returns count of updated entries. |
| `format_report` | `format_report(result, max_items)` | Format freshness check results as a human-readable report. |
| `main` | `main()` | - |
## `scripts/gitea_issue_parser.py`
Gitea Issue Body Parser — Extract structured data from markdown issue bodies.
| Function | Signature | Description |
|----------|-----------|-------------|
| `parse_issue_body` | `parse_issue_body(body, title, labels)` | Parse a Gitea issue markdown body into structured JSON. |
| `fetch_issue_from_url` | `fetch_issue_from_url(url)` | Fetch an issue from a Gitea API URL and parse it. |
| `main` | `main()` | - |
## `scripts/harvester.py`
harvester.py — Extract durable knowledge from Hermes session transcripts.
| Function | Signature | Description |
|----------|-----------|-------------|
| `find_api_key` | `find_api_key()` | Find API key from common locations. |
| `load_extraction_prompt` | `load_extraction_prompt()` | Load the extraction prompt template. |
| `call_llm` | `call_llm(prompt, transcript, api_base, api_key, model)` | Call the LLM API to extract knowledge from a transcript. |
| `parse_extraction_response` | `parse_extraction_response(content)` | Parse the LLM response to extract knowledge items. |
| `load_existing_knowledge` | `load_existing_knowledge(knowledge_dir)` | Load the existing knowledge index. |
| `fact_fingerprint` | `fact_fingerprint(fact)` | Generate a deduplication fingerprint for a fact. |
| `deduplicate` | `deduplicate(new_facts, existing, similarity_threshold)` | Remove duplicate facts from new_facts that already exist in the knowledge store. |
| `validate_fact` | `validate_fact(fact)` | Validate a single knowledge item has required fields. |
| `write_knowledge` | `write_knowledge(index, new_facts, knowledge_dir, source_session)` | Write new facts to the knowledge store. |
| `harvest_session` | `harvest_session(session_path, knowledge_dir, api_base, api_key, model, dry_run, min_confidence)` | Harvest knowledge from a single session. |
| `batch_harvest` | `batch_harvest(sessions_dir, knowledge_dir, api_base, api_key, model, since, limit, dry_run)` | Harvest knowledge from multiple sessions in batch. |
| `main` | `main()` | - |
## `scripts/improvement_proposals.py`
Improvement Proposal Generator for compounding-intelligence.
| Function | Signature | Description |
|----------|-----------|-------------|
| `analyze_sessions` | `analyze_sessions(sessions)` | Analyze session data to find waste patterns. |
| `generate_proposals` | `generate_proposals(patterns, hourly_rate, implementation_overhead)` | Generate improvement proposals from waste patterns. |
| `format_proposals_markdown` | `format_proposals_markdown(proposals, patterns, generated_at)` | Format proposals as a markdown document. |
| `format_proposals_json` | `format_proposals_json(proposals)` | Format proposals as JSON. |
| `main` | `main()` | - |
## `scripts/knowledge_gap_identifier.py`
Knowledge Gap Identifier — Pipeline 10.7
*(no public functions — script runs as `main()` only)*
## `scripts/knowledge_staleness_check.py`
Knowledge Store Staleness Detector — Detect stale knowledge entries by comparing source file hashes.
| Function | Signature | Description |
|----------|-----------|-------------|
| `compute_file_hash` | `compute_file_hash(filepath)` | Compute SHA-256 hash of a file. Returns None if file doesn't exist. |
| `check_staleness` | `check_staleness(index_path, repo_root)` | Check all entries in knowledge index for staleness. |
| `fix_hashes` | `fix_hashes(index_path, repo_root)` | Add hashes to entries missing them. Returns count of fixed entries. |
| `main` | `main()` | - |
## `scripts/perf_bottleneck_finder.py`
Performance Bottleneck Finder — Identify slow tests, builds, and CI steps.
| Function | Signature | Description |
|----------|-----------|-------------|
| `find_slow_tests_pytest` | `find_slow_tests_pytest(repo_path)` | Run pytest --durations and parse slow tests. |
| `find_slow_tests_by_scan` | `find_slow_tests_by_scan(repo_path)` | Scan test files for patterns that indicate slow tests. |
| `analyze_build_artifacts` | `analyze_build_artifacts(repo_path)` | Find large build artifacts that slow down builds. |
| `analyze_makefile_targets` | `analyze_makefile_targets(repo_path)` | Analyze Makefile for potentially slow targets. |
| `analyze_github_actions` | `analyze_github_actions(repo_path)` | Analyze GitHub Actions workflow files for inefficiencies. |
| `analyze_gitea_ci` | `analyze_gitea_ci(repo_path)` | Analyze Gitea/Drone CI config files. |
| `find_slow_imports` | `find_slow_imports(repo_path)` | Find Python files with heavy import chains. |
| `severity_sort_key` | `severity_sort_key(b)` | Sort by severity then duration. |
| `generate_report` | `generate_report(repo_path)` | Run all analyses and generate a performance report. |
| `format_markdown` | `format_markdown(report)` | Format report as markdown. |
| `main` | `main()` | - |
## `scripts/priority_rebalancer.py`
Priority Rebalancer — Re-evaluate issue priorities based on accumulated data.
| Function | Signature | Description |
|----------|-----------|-------------|
| `collect_knowledge_signals` | `collect_knowledge_signals(knowledge_dir)` | Analyze knowledge store for coverage gaps and staleness. |
| `collect_staleness_signals` | `collect_staleness_signals(scripts_dir, knowledge_dir)` | Run staleness checker if available. |
| `collect_metrics_signals` | `collect_metrics_signals(metrics_dir)` | Analyze metrics directory for pipeline health. |
| `extract_priority` | `extract_priority(labels)` | Extract priority level from issue labels. |
| `compute_issue_score` | `compute_issue_score(issue, repo, signals, now)` | Compute priority score for a single issue. |
| `generate_report` | `generate_report(scores, signals, org, repos_scanned)` | Generate the full priority report. |
| `generate_markdown_report` | `generate_markdown_report(report)` | Generate human-readable markdown report. |
| `main` | `main()` | - |
## `scripts/refactoring_opportunity_finder.py`
Finds refactoring opportunities in codebases
| Function | Signature | Description |
|----------|-----------|-------------|
| `compute_file_complexity` | `compute_file_complexity(filepath)` | Compute cyclomatic complexity for a Python file. |
| `calculate_refactoring_score` | `calculate_refactoring_score(metrics)` | Calculate a refactoring priority score (0-100) based on file metrics. |
| `scan_directory` | `scan_directory(directory, extensions)` | Scan directory for source files. |
| `generate_proposals` | `generate_proposals(directory, min_score)` | Generate refactoring proposals by analyzing source files. |
| `main` | `main()` | - |
## `scripts/sampler.py`
sampler.py — Score and rank sessions by harvest value.
| Function | Signature | Description |
|----------|-----------|-------------|
| `scan_session_fast` | `scan_session_fast(path)` | Extract scoring metadata from a session without parsing the full JSONL. |
| `parse_session_timestamp` | `parse_session_timestamp(filename)` | Parse timestamp from session filename. |
| `score_session` | `score_session(meta, now, seen_repos)` | Score a session for harvest value. Returns (score, breakdown). |
| `main` | `main()` | - |
## `scripts/session_metadata.py`
session_metadata.py - Extract structured metadata from Hermes session transcripts.
| Function | Signature | Description |
|----------|-----------|-------------|
| `extract_session_metadata` | `extract_session_metadata(file_path)` | Extract structured metadata from a Hermes session JSONL transcript. |
| `process_session_directory` | `process_session_directory(directory_path, output_file)` | Process all JSONL files in a directory. |
| `main` | `main()` | CLI entry point. |
## `scripts/session_pair_harvester.py`
Session Transcript → Training Pair Harvester
| Function | Signature | Description |
|----------|-----------|-------------|
| `compute_hash` | `compute_hash(text)` | Content hash for deduplication. |
| `extract_pairs_from_session` | `extract_pairs_from_session(session_data, min_ratio, min_response_words)` | Extract terse→rich pairs from a single session object. |
| `extract_from_jsonl_file` | `extract_from_jsonl_file(filepath, **kwargs)` | Extract pairs from a session JSONL file. |
| `deduplicate_pairs` | `deduplicate_pairs(pairs)` | Remove duplicate pairs across files. |
| `main` | `main()` | - |
## `scripts/session_reader.py`
session_reader.py — Parse Hermes session JSONL transcripts.
| Function | Signature | Description |
|----------|-----------|-------------|
| `read_session` | `read_session(path)` | Read a session JSONL file and return all messages as a list. |
| `read_session_iter` | `read_session_iter(path)` | Iterate over session messages without loading all into memory. |
| `extract_conversation` | `extract_conversation(messages)` | Extract user/assistant conversation turns, skipping tool-only messages. |
| `truncate_for_context` | `truncate_for_context(messages, head, tail)` | Truncate long sessions: keep first N + last N messages. |
| `messages_to_text` | `messages_to_text(messages)` | Convert message list to plain text for LLM consumption. |
| `get_session_metadata` | `get_session_metadata(path)` | Extract metadata from a session file (first message often has config info). |
## `scripts/test_automation_opportunity_finder.py`
Tests for scripts/automation_opportunity_finder.py — 8 tests.
| Function | Signature | Description |
|----------|-----------|-------------|
| `test_analyze_cron_jobs_no_file` | `test_analyze_cron_jobs_no_file()` | Returns empty list when no cron jobs file exists. |
| `test_analyze_cron_jobs_disabled` | `test_analyze_cron_jobs_disabled()` | Detects disabled cron jobs. |
| `test_analyze_cron_jobs_errors` | `test_analyze_cron_jobs_errors()` | Detects cron jobs with error status. |
| `test_analyze_documents_finds_todos` | `test_analyze_documents_finds_todos()` | Detects TODO markers in documents. |
| `test_analyze_scripts_repeated_commands` | `test_analyze_scripts_repeated_commands()` | Detects repeated shell commands across scripts. |
| `test_analyze_session_transcripts` | `test_analyze_session_transcripts()` | Detects repeated tool-call sequences. |
| `test_deduplicate_proposals` | `test_deduplicate_proposals()` | Deduplicates proposals with similar titles. |
| `test_rank_proposals` | `test_rank_proposals()` | Ranks proposals by impact * confidence. |
## `scripts/test_bootstrapper.py`
Tests for bootstrapper.py — context assembly from knowledge store.
| Function | Signature | Description |
|----------|-----------|-------------|
| `make_index` | `make_index(facts, tmp_dir)` | Create a temporary index.json with given facts. |
| `test_empty_index` | `test_empty_index()` | Empty knowledge store produces graceful output. |
| `test_filter_by_repo` | `test_filter_by_repo()` | Filter facts by repository. |
| `test_filter_by_agent` | `test_filter_by_agent()` | Filter facts by agent type. |
| `test_no_global_flag` | `test_no_global_flag()` | Excluding global facts works. |
| `test_sort_by_confidence` | `test_sort_by_confidence()` | Facts sort by confidence descending. |
| `test_sort_pitfalls_first` | `test_sort_pitfalls_first()` | Pitfalls sort before facts at same confidence. |
| `test_truncate_to_tokens` | `test_truncate_to_tokens()` | Truncation cuts at line boundary. |
| `test_estimate_tokens` | `test_estimate_tokens()` | Token estimation is reasonable. |
| `test_build_full_context` | `test_build_full_context()` | Full context with facts renders correctly. |
| `test_max_tokens_respected` | `test_max_tokens_respected()` | Output respects max_tokens limit. |
| `test_missing_index_graceful` | `test_missing_index_graceful()` | Missing index.json doesn't crash. |
## `scripts/test_diff_analyzer.py`
Tests for scripts/diff_analyzer.py — 10 tests.
| Function | Signature | Description |
|----------|-----------|-------------|
| `test_empty` | `test_empty()` | - |
| `test_addition` | `test_addition()` | - |
| `test_deletion` | `test_deletion()` | - |
| `test_modification` | `test_modification()` | - |
| `test_rename` | `test_rename()` | - |
| `test_multiple_files` | `test_multiple_files()` | - |
| `test_binary` | `test_binary()` | - |
| `test_to_dict` | `test_to_dict()` | - |
| `test_context_only` | `test_context_only()` | - |
| `test_multi_hunk` | `test_multi_hunk()` | - |
| `run_all` | `run_all()` | - |
## `scripts/test_gitea_issue_parser.py`
Tests for scripts/gitea_issue_parser.py
| Function | Signature | Description |
|----------|-----------|-------------|
| `test_basic_parsing` | `test_basic_parsing()` | - |
| `test_numbered_criteria` | `test_numbered_criteria()` | - |
| `test_epic_ref_from_body` | `test_epic_ref_from_body()` | - |
| `test_empty_body` | `test_empty_body()` | - |
| `test_no_sections` | `test_no_sections()` | - |
| `test_multiple_sections` | `test_multiple_sections()` | - |
| `run_all` | `run_all()` | - |
## `scripts/test_harvest_prompt.py`
Test harness for knowledge extraction prompt.
| Function | Signature | Description |
|----------|-----------|-------------|
| `validate_knowledge_item` | `validate_knowledge_item(item, idx)` | Validate a single knowledge item. Returns list of errors. |
| `validate_extraction` | `validate_extraction(data)` | Validate a full extraction result. Returns (is_valid, errors, warnings). |
| `validate_transcript_coverage` | `validate_transcript_coverage(data, transcript)` | Check that extracted facts are actually supported by the transcript. |
| `run_tests` | `run_tests()` | Run the built-in test suite. |
| `validate_file` | `validate_file(filepath)` | Validate an existing extraction JSON file. |
## `scripts/test_harvest_prompt_comprehensive.py`
Comprehensive tests for knowledge extraction prompt.
| Function | Signature | Description |
|----------|-----------|-------------|
| `check_prompt_structure` | `check_prompt_structure()` | - |
| `check_confidence_scoring` | `check_confidence_scoring()` | - |
| `check_example_quality` | `check_example_quality()` | - |
| `check_constraint_coverage` | `check_constraint_coverage()` | - |
| `check_test_sessions` | `check_test_sessions()` | - |
| `test_prompt_structure` | `test_prompt_structure()` | - |
| `test_confidence_scoring` | `test_confidence_scoring()` | - |
| `test_example_quality` | `test_example_quality()` | - |
| `test_constraint_coverage` | `test_constraint_coverage()` | - |
| `test_test_sessions` | `test_test_sessions()` | - |
## `scripts/test_harvester_pipeline.py`
Smoke test for harvester pipeline — verifies the full chain:
| Function | Signature | Description |
|----------|-----------|-------------|
| `test_session_reader` | `test_session_reader()` | Test that session_reader parses JSONL correctly. |
| `test_validate_fact` | `test_validate_fact()` | Test fact validation. |
| `test_deduplicate` | `test_deduplicate()` | Test deduplication. |
| `test_knowledge_store_roundtrip` | `test_knowledge_store_roundtrip()` | Test loading and writing knowledge index. |
| `test_full_chain_no_llm` | `test_full_chain_no_llm()` | Test the full pipeline minus the LLM call. |
## `scripts/test_improvement_proposals.py`
Tests for scripts/improvement_proposals.py — 15 tests.
| Function | Signature | Description |
|----------|-----------|-------------|
| `test_empty_sessions` | `test_empty_sessions()` | - |
| `test_no_patterns_on_clean_sessions` | `test_no_patterns_on_clean_sessions()` | - |
| `test_repeated_error_detection` | `test_repeated_error_detection()` | Same error across 3+ sessions triggers pattern. |
| `test_repeated_error_threshold` | `test_repeated_error_threshold()` | 2 occurrences should NOT trigger (threshold is 3). |
| `test_slow_tool_detection` | `test_slow_tool_detection()` | Tool with avg latency > 5000ms across 5+ calls. |
| `test_fast_tool_not_flagged` | `test_fast_tool_not_flagged()` | Tool under 5000ms avg should not trigger. |
| `test_failed_retry_detection` | `test_failed_retry_detection()` | 3+ consecutive calls to same tool triggers retry pattern. |
| `test_manual_process_detection` | `test_manual_process_detection()` | 10+ tool calls with <= 3 unique tools. |
| `test_generate_proposals_from_patterns` | `test_generate_proposals_from_patterns()` | Proposals generated from waste patterns. |
| `test_proposal_roi_positive` | `test_proposal_roi_positive()` | ROI weeks should be a positive number for recoverable time. |
| `test_proposals_sorted_by_impact` | `test_proposals_sorted_by_impact()` | Proposals should be sorted by monthly hours saved (descending). |
| `test_format_markdown` | `test_format_markdown()` | Markdown output should contain expected sections. |
| `test_format_json` | `test_format_json()` | JSON output should be valid and parseable. |
| `test_normalize_error` | `test_normalize_error()` | Error normalization should remove paths and hashes. |
| `test_cli_integration` | `test_cli_integration()` | End-to-end test: write input JSON, run script, check output. |
| `run_all` | `run_all()` | - |
## `scripts/test_knowledge_staleness.py`
Tests for scripts/knowledge_staleness_check.py — 8 tests.
| Function | Signature | Description |
|----------|-----------|-------------|
| `test_fresh_entry` | `test_fresh_entry()` | - |
| `test_stale_entry` | `test_stale_entry()` | - |
| `test_missing_source` | `test_missing_source()` | - |
| `test_no_hash` | `test_no_hash()` | - |
| `test_no_source_field` | `test_no_source_field()` | - |
| `test_fix_hashes` | `test_fix_hashes()` | - |
| `test_empty_index` | `test_empty_index()` | - |
| `test_compute_hash_nonexistent` | `test_compute_hash_nonexistent()` | - |
| `run_all` | `run_all()` | - |
## `scripts/test_priority_rebalancer.py`
Tests for Priority Rebalancer
| Function | Signature | Description |
|----------|-----------|-------------|
| `test` | `test(name)` | - |
| `assert_eq` | `assert_eq(a, b, msg)` | - |
| `assert_true` | `assert_true(v, msg)` | - |
| `assert_false` | `assert_false(v, msg)` | - |
| `make_issue` | `make_issue(**kwargs)` | - |
## `scripts/test_refactoring_opportunity_finder.py`
Tests for scripts/refactoring_opportunity_finder.py — 10 tests.
| Function | Signature | Description |
|----------|-----------|-------------|
| `test_complexity_simple_function` | `test_complexity_simple_function()` | Simple function should have low complexity. |
| `test_complexity_with_conditionals` | `test_complexity_with_conditionals()` | Function with if/else should have higher complexity. |
| `test_complexity_with_loops` | `test_complexity_with_loops()` | Function with loops should increase complexity. |
| `test_complexity_with_class` | `test_complexity_with_class()` | Class with methods should count both. |
| `test_complexity_syntax_error` | `test_complexity_syntax_error()` | File with syntax error should return zeros. |
| `test_refactoring_score_high_complexity` | `test_refactoring_score_high_complexity()` | High complexity should give high score. |
| `test_refactoring_score_low_complexity` | `test_refactoring_score_low_complexity()` | Low complexity should give lower score. |
| `test_refactoring_score_high_churn` | `test_refactoring_score_high_churn()` | High churn should increase score. |
| `test_refactoring_score_no_coverage` | `test_refactoring_score_no_coverage()` | No coverage data should assume medium risk. |
| `test_refactoring_score_large_file` | `test_refactoring_score_large_file()` | Large files should score higher. |
| `run_all` | `run_all()` | - |
## `scripts/test_session_pair_harvester.py`
Tests for session_pair_harvester.
| Function | Signature | Description |
|----------|-----------|-------------|
| `test_basic_extraction` | `test_basic_extraction()` | - |
| `test_filters_short_responses` | `test_filters_short_responses()` | - |
| `test_skips_tool_results` | `test_skips_tool_results()` | - |
| `test_deduplication` | `test_deduplication()` | - |
| `test_ratio_filter` | `test_ratio_filter()` | - |
## `scripts/validate_knowledge.py`
Validate knowledge files and index.json against the schema.
| Function | Signature | Description |
|----------|-----------|-------------|
| `validate_fact` | `validate_fact(fact, src)` | - |
| `main` | `main()` | - |
---
**Total scripts documented:** 33
*Generated by `scripts/api_doc_generator.py` (Issue #98)*

View File

@@ -0,0 +1,219 @@
#!/usr/bin/env python3
"""
API Doc Generator — Issue #98
Scans all Python modules in `scripts/`, extracts their public API surface
(module docstring + public function signatures + first-line doc summaries),
and produces a single markdown reference document at `docs/API.md`.
Usage:
python3 scripts/api_doc_generator.py # Write docs/API.md
python3 scripts/api_doc_generator.py --check # Verify docs/API.md is up-to-date
python3 scripts/api_doc_generator.py --json # Emit JSON for downstream tooling
"""
from __future__ import annotations
import ast
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import TypedDict, List, Optional
# ─── Paths ────────────────────────────────────────────────────────────────────
SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parent
SCRIPTS_DIR = REPO_ROOT / "scripts"
DOCS_DIR = REPO_ROOT / "docs"
OUTPUT_PATH = DOCS_DIR / "API.md"
# ─── Data structures ───────────────────────────────────────────────────────────
class FunctionInfo(TypedDict):
name: str
signature: str
summary: str
class ModuleInfo(TypedDict):
path: str # relative to repo root, e.g. "scripts/harvester.py"
docstring: str
functions: List[FunctionInfo]
# ─── AST extraction ────────────────────────────────────────────────────────────
def extract_functions_from_ast(tree: ast.AST, file_rel: str) -> List[FunctionInfo]:
"""Extract public function names, signatures, and first-line doc summaries."""
funcs: list[FunctionInfo] = []
for node in ast.iter_child_nodes(tree):
if not isinstance(node, ast.FunctionDef):
continue
# Skip private functions
if node.name.startswith("_"):
continue
# Build signature: arg1, arg2=default, *args, **kwargs
args = []
for arg in node.args.args:
args.append(arg.arg)
if node.args.vararg:
args.append(f"*{node.args.vararg.arg}")
if node.args.kwarg:
args.append(f"**{node.args.kwarg.arg}")
# Get first line of docstring
summary = ""
if (node.body and isinstance(node.body[0], ast.Expr) and
isinstance(node.body[0].value, ast.Constant) and
isinstance(node.body[0].value.value, str)):
raw = node.body[0].value.value.strip()
summary = raw.split("\n")[0].strip()
if len(summary) > 100:
summary = summary[:97] + "..."
funcs.append({
"name": node.name,
"signature": ", ".join(args),
"summary": summary,
})
return funcs
def parse_module(filepath: Path) -> Optional[ModuleInfo]:
"""Parse a Python file and return its module-level docstring and public functions."""
try:
with open(filepath, "r", encoding="utf-8") as f:
source = f.read()
tree = ast.parse(source, filename=str(filepath))
except Exception as e:
print(f"WARNING: Could not parse {filepath}: {e}", file=sys.stderr)
return None
# Module docstring
module_doc = ast.get_docstring(tree) or ""
module_doc = module_doc.strip().split("\n")[0] # first line only
# Public functions
functions = extract_functions_from_ast(tree, filepath.name)
rel = filepath.relative_to(REPO_ROOT)
return {
"path": str(rel),
"docstring": module_doc,
"functions": functions,
}
# ─── Scanning ──────────────────────────────────────────────────────────────────
def scan_scripts_dir(scripts_dir: Path) -> List[ModuleInfo]:
"""Scan all .py files in scripts/ and extract API info."""
modules: list[ModuleInfo] = []
for pyfile in sorted(scripts_dir.glob("*.py")):
info = parse_module(pyfile)
if info is not None:
modules.append(info)
return modules
# ─── Markdown rendering ─────────────────────────────────────────────────────────
def render_markdown(modules: List[ModuleInfo]) -> str:
"""Generate full docs/API.md content from the scanned modules."""
lines = [
"# Compounding Intelligence — Scripts API Reference",
"",
f"*Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}*",
"",
"This document auto-documents the public API surface of all scripts",
"in `scripts/`. Each section covers one script: module purpose,",
"public functions, and their signatures.",
"",
"---",
"",
]
for mod in modules:
rel = mod["path"]
name = Path(rel).stem # e.g. harvester
lines.append(f"## `{rel}`")
lines.append("")
if mod["docstring"]:
lines.append(mod["docstring"])
lines.append("")
if mod["functions"]:
lines.append("| Function | Signature | Description |")
lines.append("|----------|-----------|-------------|")
for fn in mod["functions"]:
sig = fn["name"] + "(" + fn["signature"] + ")"
desc = fn["summary"] or "-"
lines.append(f"| `{fn['name']}` | `{sig}` | {desc} |")
lines.append("")
else:
lines.append("*(no public functions — script runs as `main()` only)*")
lines.append("")
lines.extend([
"",
"---",
"",
f"**Total scripts documented:** {len(modules)}",
"",
"*Generated by `scripts/api_doc_generator.py` (Issue #98)*",
])
return "\n".join(lines)
# ─── JSON output (optional, for automation) ───────────────────────────────────
def render_json(modules: List[ModuleInfo]) -> str:
"""Emit machine-readable JSON version of the API reference."""
import json
payload = {
"generated_at": datetime.now(timezone.utc).isoformat(),
"generator": "scripts/api_doc_generator.py",
"repo": "Timmy_Foundation/compounding-intelligence",
"modules": modules,
}
return json.dumps(payload, indent=2)
# ─── Main ──────────────────────────────────────────────────────────────────────
def main() -> int:
import argparse
parser = argparse.ArgumentParser(description="Generate API docs for scripts/")
parser.add_argument("--check", action="store_true",
help="Exit 1 if docs/API.md is out-of-date")
parser.add_argument("--json", action="store_true",
help="Emit JSON to stdout instead of writing markdown")
args = parser.parse_args()
modules = scan_scripts_dir(SCRIPTS_DIR)
modules.sort(key=lambda m: m["path"])
if args.json:
print(render_json(modules))
return 0
md = render_markdown(modules)
if args.check:
if OUTPUT_PATH.exists():
existing = OUTPUT_PATH.read_text(encoding="utf-8")
if existing == md:
print("✅ docs/API.md is up-to-date")
return 0
print("❌ docs/API.md is missing or out-of-date — regenerate with "
"`python3 scripts/api_doc_generator.py`", file=sys.stderr)
return 1
DOCS_DIR.mkdir(parents=True, exist_ok=True)
OUTPUT_PATH.write_text(md, encoding="utf-8")
print(f"✅ Wrote {OUTPUT_PATH} ({len(modules)} modules documented)")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -73,14 +73,12 @@ Binary files a/img.png and b/img.png differ
def test_empty():
"""Verifies behavior with empty or None input."""
a = DiffAnalyzer()
s = a.analyze("")
assert s.total_files_changed == 0
print("PASS: test_empty")
def test_addition():
"""Verifies addition logic."""
a = DiffAnalyzer()
s = a.analyze(SAMPLE_ADD)
assert s.total_files_changed == 1
@@ -91,7 +89,6 @@ def test_addition():
print("PASS: test_addition")
def test_deletion():
"""Verifies deletion logic."""
a = DiffAnalyzer()
s = a.analyze(SAMPLE_DELETE)
assert s.total_deleted == 2
@@ -100,7 +97,6 @@ def test_deletion():
print("PASS: test_deletion")
def test_modification():
"""Verifies modification logic."""
a = DiffAnalyzer()
s = a.analyze(SAMPLE_MODIFY)
assert s.total_added == 2
@@ -109,7 +105,6 @@ def test_modification():
print("PASS: test_modification")
def test_rename():
"""Verifies rename logic."""
a = DiffAnalyzer()
s = a.analyze(SAMPLE_RENAME)
assert s.renamed_files == 1
@@ -119,7 +114,6 @@ def test_rename():
print("PASS: test_rename")
def test_multiple_files():
"""Verifies multiple files logic."""
a = DiffAnalyzer()
s = a.analyze(SAMPLE_MULTI)
assert s.total_files_changed == 2
@@ -127,7 +121,6 @@ def test_multiple_files():
print("PASS: test_multiple_files")
def test_binary():
"""Verifies binary logic."""
a = DiffAnalyzer()
s = a.analyze(SAMPLE_BINARY)
assert s.binary_files == 1
@@ -136,7 +129,6 @@ def test_binary():
print("PASS: test_binary")
def test_to_dict():
"""Verifies to dict logic."""
a = DiffAnalyzer()
s = a.analyze(SAMPLE_MODIFY)
d = s.to_dict()
@@ -146,7 +138,6 @@ def test_to_dict():
print("PASS: test_to_dict")
def test_context_only():
"""Verifies context only logic."""
diff = """diff --git a/f.py b/f.py
--- a/f.py
+++ b/f.py
@@ -163,7 +154,6 @@ def test_context_only():
print("PASS: test_context_only")
def test_multi_hunk():
"""Verifies multi hunk logic."""
diff = """diff --git a/f.py b/f.py
--- a/f.py
+++ b/f.py

View File

@@ -1,207 +0,0 @@
#!/usr/bin/env python3
"""Test Documentation Generator — adds module and function docstrings to test files.
Reads test files without docstrings and generates:
- Module-level docstring explaining what is being tested
- Function-level docstring explaining what each test verifies
- Inline comments for complex assertions (simple heuristic)
Does not change test logic — only adds documentation.
Processes 20+ test files per run.
"""
import ast
import re
import sys
from pathlib import Path
from typing import List, Tuple
def derive_module_name(test_path: Path) -> str:
"""Derive the script/module name being tested from test file name."""
name = test_path.stem
if name.startswith("test_"):
name = name[5:] # strip 'test_' (5 chars: t-e-s-t-_, not 6)
mapping = {
"bootstrapper": "bootstrapper.py",
"harvester": "harvester.py",
"diff_analyzer": "diff_analyzer.py",
"gitea_issue_parser": "gitea_issue_parser.py",
"harvest_prompt": "harvest_prompt.py",
"harvest_prompt_comprehensive": "harvest_prompt_comprehensive.py",
"harvester_pipeline": "harvester_pipeline.py",
"improvement_proposals": "improvement_proposals.py",
"knowledge_staleness": "knowledge_staleness_check.py",
"priority_rebalancer": "priority_rebalancer.py",
"refactoring_opportunity_finder": "refactoring_opportunity_finder.py",
"session_pair_harvester": "session_pair_harvester.py",
"session_reader": "session_reader.py",
"automation_opportunity_finder": "automation_opportunity_finder.py",
"dedup": "dedup.py",
"freshness": "freshness.py",
"knowledge_gap_identifier": "knowledge_gap_identifier.py",
"perf_bottleneck_finder": "perf_bottleneck_finder.py",
"ci_config": "CI configuration",
"quality_gate": "quality_gate.py",
}
base = name.replace("_", " ")
if name in mapping:
base = mapping[name].replace(".py", "")
return base
def count_tests_in_file(content: str) -> int:
"""Count test functions in a Python file."""
return len(re.findall(r'^def (test_\w+)\s*\(', content, re.MULTILINE))
def infer_test_purpose(func_name: str, func_body: str) -> str:
"""Generate a brief docstring for a test function based on its name and body."""
name = func_name.replace("test_", "").replace("_", " ")
if "empty" in name or "none" in name:
return "Verifies behavior with empty or None input."
if "parsing" in name or "parse" in name:
return f"Verifies parsing logic for {name}."
if "filter" in name:
return f"Verifies knowledge filtering by {name}."
if "hash" in name:
return "Verifies file hash computation correctness."
if "freshness" in name or "staleness" in name:
return "Verifies knowledge freshness detection."
if "error" in name or "exception" in name:
return f"Verifies error handling for {name}."
if "boundary" in name or "edge" in name:
return "Verifies boundary case handling."
return f"Verifies {name} logic."
def has_module_docstring(content: str) -> bool:
"""Check if file (after shebang) starts with a docstring."""
lines = content.split('\n')
start_idx = 1 if lines and lines[0].startswith('#!') else 0
for line in lines[start_idx:start_idx + 5]:
stripped = line.strip()
if stripped.startswith('"""') or stripped.startswith("'''"):
return True
if stripped == "" or stripped.startswith('#'):
continue
break
return False
def insert_after_shebang(content: str, insertion: str) -> str:
"""Insert text after the shebang line (if any) and any following blank lines."""
lines = content.split('\n')
insert_idx = 0
if lines and lines[0].startswith('#!'):
insert_idx = 1
while insert_idx < len(lines) and lines[insert_idx].strip() == '':
insert_idx += 1
new_lines = lines[:insert_idx] + [insertion] + lines[insert_idx:]
return '\n'.join(new_lines)
def add_function_docstring(content: str, func_lineno: int, docstring: str) -> str:
"""Add a docstring to a function at the given line number."""
lines = content.split('\n')
idx = func_lineno - 1
indent = re.match(r'^(\s*)', lines[idx]).group(1)
doc_line = f'{indent} """{docstring}"""'
new_lines = lines[:idx + 1] + [doc_line] + lines[idx + 1:]
return '\n'.join(new_lines)
def generate_module_docstring(test_path: Path) -> str:
"""Generate a module-level docstring for a test file."""
module = derive_module_name(test_path)
count = count_tests_in_file(test_path.read_text())
if count > 0:
return f"Tests for {module}{count} tests."
return f"Tests for {module}."
def process_test_file(test_path: Path, dry_run: bool = False) -> Tuple[bool, List[str]]:
"""Process a single test file, adding missing docstrings. Returns (changed, messages)."""
content = test_path.read_text()
original = content
messages = []
if not has_module_docstring(content):
mod_doc = generate_module_docstring(test_path)
content = insert_after_shebang(content, f'''"""{mod_doc}"""''')
messages.append(f"Added module docstring: {mod_doc}")
try:
tree = ast.parse(content)
except SyntaxError as e:
messages.append(f"SKIP (syntax error): {e}")
return False, messages
funcs_to_doc: List[Tuple[int, str, str]] = []
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef) and node.name.startswith('test_'):
has_docstring = (
len(node.body) > 0 and
isinstance(node.body[0], ast.Expr) and
isinstance(node.body[0].value, ast.Constant) and
isinstance(node.body[0].value.value, str)
)
if not has_docstring:
func_body = ast.get_source_segment(content, node) or ""
doc = infer_test_purpose(node.name, func_body)
funcs_to_doc.append((node.lineno, node.name, doc))
funcs_to_doc.sort(key=lambda x: -x[0])
for lineno, func_name, doc in funcs_to_doc:
content = add_function_docstring(content, lineno, doc)
messages.append(f"Added docstring to {func_name}: {doc}")
changed = content != original
if changed and not dry_run:
test_path.write_text(content)
return changed, messages
def find_test_files(root: Path, max_files: int = 25) -> List[Path]:
"""Find test files under scripts/ and tests/ directories."""
test_files = []
for subdir in [root / "scripts", root / "tests"]:
if subdir.exists():
test_files.extend(subdir.glob("test_*.py"))
test_files.sort()
return test_files[:max_files]
def main():
import argparse
parser = argparse.ArgumentParser(description="Generate documentation for test files")
parser.add_argument("--dry-run", action="store_true", help="Show changes without writing")
parser.add_argument("--root", type=Path, default=Path.cwd(),
help="Repo root (default: current directory)")
parser.add_argument("--limit", type=int, default=25,
help="Max files to process per run (handles 20+ requirement)")
args = parser.parse_args()
root = args.root
test_files = find_test_files(root, args.limit)
print(f"Found {len(test_files)} test files to process (limit={args.limit}):")
total_changed = 0
for tf in test_files:
changed, msgs = process_test_file(tf, dry_run=args.dry_run)
if changed:
total_changed += 1
status = "CHANGED" if changed else "OK"
print(f" [{status}] {tf.relative_to(root)}")
for msg in msgs:
print(f" {msg}")
print(f"\nCompleted: {total_changed} file(s) modified, {len(test_files) - total_changed} already up-to-date.")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -14,7 +14,6 @@ parse_issue_body = mod.parse_issue_body
def test_basic_parsing():
"""Verifies parsing logic for basic parsing."""
body = """## Context
This is the background info.
@@ -41,7 +40,6 @@ Some description.
def test_numbered_criteria():
"""Verifies numbered criteria logic."""
body = """## Acceptance Criteria
1. First item
@@ -55,7 +53,6 @@ def test_numbered_criteria():
def test_epic_ref_from_body():
"""Verifies epic ref from body logic."""
body = "Closes #123\n\nSome description."
result = parse_issue_body(body)
assert result["epic_ref"] == 123
@@ -63,7 +60,6 @@ def test_epic_ref_from_body():
def test_empty_body():
"""Verifies behavior with empty or None input."""
result = parse_issue_body("")
assert result["criteria"] == []
assert result["context"] == ""
@@ -72,7 +68,6 @@ def test_empty_body():
def test_no_sections():
"""Verifies no sections logic."""
body = "Just a plain issue body with no headings."
result = parse_issue_body(body)
assert result["context"] == "Just a plain issue body with no headings."
@@ -80,7 +75,6 @@ def test_no_sections():
def test_multiple_sections():
"""Verifies multiple sections logic."""
body = """## Problem
Something is broken.

View File

@@ -46,27 +46,22 @@ def check_test_sessions():
return True, f"{len(files)} valid sessions"
def test_prompt_structure():
"""Verifies prompt structure logic."""
passed, msg = check_prompt_structure()
assert passed, msg
def test_confidence_scoring():
"""Verifies confidence scoring logic."""
passed, msg = check_confidence_scoring()
assert passed, msg
def test_example_quality():
"""Verifies example quality logic."""
passed, msg = check_example_quality()
assert passed, msg
def test_constraint_coverage():
"""Verifies constraint coverage logic."""
passed, msg = check_constraint_coverage()
assert passed, msg
def test_test_sessions():
"""Verifies sessions logic."""
passed, msg = check_test_sessions()
assert passed, msg

View File

@@ -47,14 +47,12 @@ def _make_tool_calls(repeats):
# ── Tests ─────────────────────────────────────────────────────
def test_empty_sessions():
"""Verifies behavior with empty or None input."""
patterns = analyze_sessions([])
assert patterns == []
print("PASS: test_empty_sessions")
def test_no_patterns_on_clean_sessions():
"""Verifies no patterns on clean sessions logic."""
sessions = [
_make_session("s1", tool_calls=[{"tool": "read_file", "latency_ms": 50}]),
_make_session("s2", tool_calls=[{"tool": "write_file", "latency_ms": 80}]),

View File

@@ -17,7 +17,6 @@ compute_file_hash = mod.compute_file_hash
def test_fresh_entry():
"""Verifies fresh entry logic."""
with tempfile.TemporaryDirectory() as tmpdir:
src = os.path.join(tmpdir, "source.py")
with open(src, "w") as f:
@@ -32,7 +31,6 @@ def test_fresh_entry():
def test_stale_entry():
"""Verifies stale entry logic."""
with tempfile.TemporaryDirectory() as tmpdir:
src = os.path.join(tmpdir, "source.py")
with open(src, "w") as f:
@@ -49,7 +47,6 @@ def test_stale_entry():
def test_missing_source():
"""Verifies missing source logic."""
with tempfile.TemporaryDirectory() as tmpdir:
idx = os.path.join(tmpdir, "index.json")
with open(idx, "w") as f:
@@ -60,7 +57,6 @@ def test_missing_source():
def test_no_hash():
"""Verifies file hash computation correctness."""
with tempfile.TemporaryDirectory() as tmpdir:
src = os.path.join(tmpdir, "source.py")
with open(src, "w") as f:
@@ -75,7 +71,6 @@ def test_no_hash():
def test_no_source_field():
"""Verifies no source field logic."""
with tempfile.TemporaryDirectory() as tmpdir:
idx = os.path.join(tmpdir, "index.json")
with open(idx, "w") as f:
@@ -86,7 +81,6 @@ def test_no_source_field():
def test_fix_hashes():
"""Verifies file hash computation correctness."""
with tempfile.TemporaryDirectory() as tmpdir:
src = os.path.join(tmpdir, "source.py")
with open(src, "w") as f:
@@ -104,7 +98,6 @@ def test_fix_hashes():
def test_empty_index():
"""Verifies behavior with empty or None input."""
with tempfile.TemporaryDirectory() as tmpdir:
idx = os.path.join(tmpdir, "index.json")
with open(idx, "w") as f:
@@ -115,7 +108,6 @@ def test_empty_index():
def test_compute_hash_nonexistent():
"""Verifies behavior with empty or None input."""
h = compute_file_hash("/nonexistent/path/file.py")
assert h is None
print("PASS: test_compute_hash_nonexistent")

View File

@@ -11,7 +11,6 @@ from session_pair_harvester import extract_pairs_from_session, deduplicate_pairs
def test_basic_extraction():
"""Verifies basic extraction logic."""
session = {
"id": "test_001",
"model": "test-model",
@@ -30,7 +29,6 @@ def test_basic_extraction():
def test_filters_short_responses():
"""Verifies knowledge filtering by filters short responses."""
session = {
"id": "test_002",
"model": "test",
@@ -45,7 +43,6 @@ def test_filters_short_responses():
def test_skips_tool_results():
"""Verifies skips tool results logic."""
session = {
"id": "test_003",
"model": "test",
@@ -60,7 +57,6 @@ def test_skips_tool_results():
def test_deduplication():
"""Verifies deduplication logic."""
pairs = [
{"terse": "What is X?", "rich": "X is Y.", "source": "s1", "model": "m"},
{"terse": "What is X?", "rich": "X is Y.", "source": "s2", "model": "m"},
@@ -72,7 +68,6 @@ def test_deduplication():
def test_ratio_filter():
"""Verifies knowledge filtering by ratio filter."""
session = {
"id": "test_005",
"model": "test",

View File

@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""
Smoke tests for API Doc Generator — Issue #98
Validates that the generator runs, produces docs/API.md, and that
the generated markdown contains expected sections for the known scripts.
"""
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
import pytest
# Resolve repo root
REPO_ROOT = Path(__file__).resolve().parents[1]
SCRIPTS_DIR = REPO_ROOT / "scripts"
DOCS_DIR = REPO_ROOT / "docs"
API_MD = DOCS_DIR / "API.md"
GENERATOR = SCRIPTS_DIR / "api_doc_generator.py"
# ─── Generator presence ─────────────────────────────────────────────────────────
class TestGeneratorPresence:
def test_generator_script_exists(self):
assert GENERATOR.exists(), f"Missing: {GENERATOR}"
def test_generator_is_executable(self):
with open(GENERATOR) as f:
first = f.readline().strip()
assert first.startswith("#!"), "Missing shebang"
assert "python" in first.lower()
# ─── API.md generation ──────────────────────────────────────────────────────────
class TestAPIDocGeneration:
def test_generator_runs_successfully(self):
"""Run the generator and verify exit code 0."""
result = subprocess.run(
[sys.executable, str(GENERATOR)],
capture_output=True, text=True, cwd=REPO_ROOT, timeout=30
)
assert result.returncode == 0, (
f"Generator failed (code {{result.returncode}})\n"
f"STDERR: {{result.stderr[:500]}}"
)
def test_api_md_is_created(self):
"""docs/API.md must exist after generation."""
assert API_MD.exists(), f"Missing output: {API_MD}"
def test_api_md_is_not_empty(self):
"""Generate markdown must have substantial content."""
content = API_MD.read_text(encoding="utf-8")
assert len(content) > 1000, "API.md is suspiciously small"
def test_api_md_has_expected_structure(self):
"""Top-level headings and table markers must be present."""
content = API_MD.read_text(encoding="utf-8")
assert "# Compounding Intelligence — Scripts API Reference" in content
assert "## `scripts/" in content
assert "| Function | Signature | Description |" in content
def test_api_md_covers_expected_scripts(self):
"""At minimum the core scripts should be documented."""
content = API_MD.read_text(encoding="utf-8")
# Core scripts that must appear
core = ["scripts/harvester.py", "scripts/bootstrapper.py",
"scripts/session_reader.py", "scripts/dedup.py"]
for rel in core:
assert f"## `{rel}`" in content, f"Missing section for {rel}"
def test_api_md_contains_function_names(self):
"""Spot-check: known public functions from key modules must appear."""
content = API_MD.read_text(encoding="utf-8")
checks = [
("harvester", "read_session"),
("bootstrapper", "load_index"),
("session_reader", "extract_conversation"),
("dedup", "normalize_text"),
]
for module_stem, func_name in checks:
assert f"| `{func_name}` |" in content, f"Missing function {func_name} from {module_stem}"
# ─── Idempotence / --check ─────────────────────────────────────────────────────
class TestIdempotence:
def test_check_flag_passes_when_current(self):
"""`--check` should exit 0 immediately after generation."""
result = subprocess.run(
[sys.executable, str(GENERATOR), "--check"],
capture_output=True, text=True, cwd=REPO_ROOT, timeout=30
)
assert result.returncode == 0, (
f"--check failed\nSTDOUT: {{result.stdout}}\nSTDERR: {{result.stderr[:200]}}"
)
def test_check_fails_when_api_md_stale(self):
"""If docs/API.md is manually altered, --check should detect staleness."""
# Generate fresh baseline first
subprocess.run([sys.executable, str(GENERATOR)], capture_output=True, cwd=REPO_ROOT, timeout=30)
# Corrupt API.md slightly (append a line at the end)
original = API_MD.read_text(encoding="utf-8")
corrupted = original + "\n<!-- corrupted -->\n"
API_MD.write_text(corrupted, encoding="utf-8")
# --check should now fail
result = subprocess.run(
[sys.executable, str(GENERATOR), "--check"],
capture_output=True, text=True, cwd=REPO_ROOT, timeout=30
)
assert result.returncode != 0, "--check should detect stale API.md"
assert "out-of-date" in result.stderr.lower() or "out-of-date" in result.stdout.lower()
# Restore clean state
subprocess.run([sys.executable, str(GENERATOR)], capture_output=True, cwd=REPO_ROOT, timeout=30)
assert API_MD.read_text(encoding="utf-8") == original
# ─── JSON output ────────────────────────────────────────────────────────────────
class TestJSONOutput:
def test_json_flag_emits_valid_json(self):
result = subprocess.run(
[sys.executable, str(GENERATOR), "--json"],
capture_output=True, text=True, cwd=REPO_ROOT, timeout=30
)
assert result.returncode == 0
import json
payload = json.loads(result.stdout)
assert "modules" in payload
assert len(payload["modules"]) >= 30
def test_json_has_expected_fields(self):
result = subprocess.run(
[sys.executable, str(GENERATOR), "--json"],
capture_output=True, text=True, cwd=REPO_ROOT, timeout=30
)
import json
payload = json.loads(result.stdout)
mod = payload["modules"][0]
for key in ("path", "docstring", "functions"):
assert key in mod, f"Missing key {{key}} in module payload"
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View File

@@ -1,16 +1,13 @@
"""Tests for CI configuration — 2 tests."""
from pathlib import Path
def test_requirements_makefile_and_workflow_exist() -> None:
"""Verifies requirements makefile and workflow exist logic."""
assert Path("requirements.txt").exists()
assert Path("Makefile").exists()
assert Path(".gitea/workflows/test.yml").exists()
def test_ci_workflow_runs_project_test_command() -> None:
"""Verifies ci workflow runs project command logic."""
workflow = Path(".gitea/workflows/test.yml").read_text(encoding="utf-8")
requirements = Path("requirements.txt").read_text(encoding="utf-8")
makefile = Path("Makefile").read_text(encoding="utf-8")

View File

@@ -22,34 +22,28 @@ from dedup import (
class TestNormalize:
def test_lowercases(self):
"""Verifies lowercases logic."""
assert normalize_text("Hello World") == "hello world"
def test_collapses_whitespace(self):
"""Verifies collapses whitespace logic."""
assert normalize_text(" hello world ") == "hello world"
def test_strips(self):
"""Verifies strips logic."""
assert normalize_text(" text ") == "text"
class TestContentHash:
def test_deterministic(self):
"""Verifies deterministic logic."""
h1 = content_hash("Hello World")
h2 = content_hash("hello world")
h3 = content_hash(" Hello World ")
assert h1 == h2 == h3
def test_different_texts(self):
"""Verifies different texts logic."""
h1 = content_hash("Hello")
h2 = content_hash("World")
assert h1 != h2
def test_returns_hex(self):
"""Verifies returns hex logic."""
h = content_hash("test")
assert len(h) == 64 # SHA256
assert all(c in '0123456789abcdef' for c in h)
@@ -57,21 +51,18 @@ class TestContentHash:
class TestTokenize:
def test_extracts_words(self):
"""Verifies extracts words logic."""
tokens = tokenize("Hello World Test")
assert "hello" in tokens
assert "world" in tokens
assert "test" in tokens
def test_skips_short_words(self):
"""Verifies skips short words logic."""
tokens = tokenize("a to is the hello")
assert "a" not in tokens
assert "to" not in tokens
assert "hello" in tokens
def test_returns_set(self):
"""Verifies returns set logic."""
tokens = tokenize("hello hello world")
assert isinstance(tokens, set)
assert len(tokens) == 2
@@ -79,25 +70,20 @@ class TestTokenize:
class TestTokenSimilarity:
def test_identical(self):
"""Verifies identical logic."""
assert token_similarity("hello world", "hello world") == 1.0
def test_no_overlap(self):
"""Verifies no overlap logic."""
assert token_similarity("alpha beta", "gamma delta") == 0.0
def test_partial_overlap(self):
"""Verifies partial overlap logic."""
sim = token_similarity("hello world test", "hello universe test")
assert 0.3 < sim < 0.7
def test_empty(self):
"""Verifies behavior with empty or None input."""
assert token_similarity("", "hello") == 0.0
assert token_similarity("hello", "") == 0.0
def test_symmetric(self):
"""Verifies symmetric logic."""
a = "hello world test"
b = "hello universe test"
assert token_similarity(a, b) == token_similarity(b, a)
@@ -105,26 +91,22 @@ class TestTokenSimilarity:
class TestQualityScore:
def test_high_confidence(self):
"""Verifies high confidence logic."""
fact = {"confidence": 0.95, "source_count": 5, "tags": ["test"], "related": ["x"]}
score = quality_score(fact)
assert score > 0.7
def test_low_confidence(self):
"""Verifies low confidence logic."""
fact = {"confidence": 0.3, "source_count": 1}
score = quality_score(fact)
assert score < 0.5
def test_defaults(self):
"""Verifies defaults logic."""
score = quality_score({})
assert 0 < score < 1
class TestMergeFacts:
def test_merges_tags(self):
"""Verifies merges tags logic."""
keep = {"id": "a", "fact": "test", "tags": ["git"], "confidence": 0.9}
drop = {"id": "b", "fact": "test", "tags": ["python"], "confidence": 0.8}
merged = merge_facts(keep, drop)
@@ -132,21 +114,18 @@ class TestMergeFacts:
assert "python" in merged["tags"]
def test_merges_source_count(self):
"""Verifies merges source count logic."""
keep = {"id": "a", "fact": "test", "source_count": 3}
drop = {"id": "b", "fact": "test", "source_count": 2}
merged = merge_facts(keep, drop)
assert merged["source_count"] == 5
def test_keeps_higher_confidence(self):
"""Verifies keeps higher confidence logic."""
keep = {"id": "a", "fact": "test", "confidence": 0.7}
drop = {"id": "b", "fact": "test", "confidence": 0.9}
merged = merge_facts(keep, drop)
assert merged["confidence"] == 0.9
def test_tracks_merged_from(self):
"""Verifies tracks merged from logic."""
keep = {"id": "a", "fact": "test"}
drop = {"id": "b", "fact": "test"}
merged = merge_facts(keep, drop)
@@ -155,7 +134,6 @@ class TestMergeFacts:
class TestDedupFacts:
def test_removes_exact_dupes(self):
"""Verifies removes exact dupes logic."""
facts = [
{"id": "1", "fact": "Always use git rebase"},
{"id": "2", "fact": "Always use git rebase"}, # exact dupe
@@ -166,7 +144,6 @@ class TestDedupFacts:
assert stats["unique"] == 2
def test_removes_near_dupes(self):
"""Verifies removes near dupes logic."""
facts = [
{"id": "1", "fact": "Always check logs before deploying to production server"},
{"id": "2", "fact": "Always check logs before deploying to production environment"},
@@ -177,7 +154,6 @@ class TestDedupFacts:
assert stats["unique"] == 2
def test_preserves_unique(self):
"""Verifies preserves unique logic."""
facts = [
{"id": "1", "fact": "Use git rebase for clean history"},
{"id": "2", "fact": "Docker containers should be stateless"},
@@ -188,13 +164,11 @@ class TestDedupFacts:
assert stats["removed"] == 0
def test_empty_input(self):
"""Verifies behavior with empty or None input."""
deduped, stats = dedup_facts([])
assert stats["total"] == 0
assert stats["unique"] == 0
def test_keeps_higher_quality_near_dup(self):
"""Verifies keeps higher quality near dup logic."""
facts = [
{"id": "1", "fact": "Check logs before deploying to production server", "confidence": 0.5, "source_count": 1},
{"id": "2", "fact": "Check logs before deploying to production environment", "confidence": 0.9, "source_count": 5, "tags": ["ops"]},
@@ -205,7 +179,6 @@ class TestDedupFacts:
assert deduped[0]["confidence"] == 0.9
def test_dry_run_does_not_modify(self):
"""Verifies dry run does not modify logic."""
facts = [
{"id": "1", "fact": "Same text"},
{"id": "2", "fact": "Same text"},
@@ -218,19 +191,16 @@ class TestDedupFacts:
class TestGenerateTestDuplicates:
def test_generates_correct_count(self):
"""Verifies generates correct count logic."""
facts = generate_test_duplicates(20)
assert len(facts) > 20 # 20 unique + duplicates
def test_has_exact_dupes(self):
"""Verifies has exact dupes logic."""
facts = generate_test_duplicates(20)
hashes = [content_hash(f["fact"]) for f in facts]
# Should have some duplicate hashes
assert len(hashes) != len(set(hashes))
def test_dedup_removes_dupes(self):
"""Verifies dedup removes dupes logic."""
facts = generate_test_duplicates(20)
deduped, stats = dedup_facts(facts)
assert stats["unique"] <= 20

View File

@@ -20,7 +20,6 @@ def _make_repo(tmpdir, structure):
def test_undocumented_symbol():
"""Verifies undocumented symbol logic."""
with tempfile.TemporaryDirectory() as tmpdir:
_make_repo(tmpdir, {
"src/calculator.py": "def add(a, b):\n return a + b\n",
@@ -32,7 +31,6 @@ def test_undocumented_symbol():
def test_documented_symbol_no_gap():
"""Verifies documented symbol no gap logic."""
with tempfile.TemporaryDirectory() as tmpdir:
_make_repo(tmpdir, {
"src/calculator.py": "def add(a, b):\n return a + b\n",
@@ -45,7 +43,6 @@ def test_documented_symbol_no_gap():
def test_untested_module():
"""Verifies untested module logic."""
with tempfile.TemporaryDirectory() as tmpdir:
_make_repo(tmpdir, {
"src/calculator.py": "def add(a, b):\n return a + b\n",
@@ -58,7 +55,6 @@ def test_untested_module():
def test_tested_module_no_gap():
"""Verifies tested module no gap logic."""
with tempfile.TemporaryDirectory() as tmpdir:
_make_repo(tmpdir, {
"src/calculator.py": "def add(a, b):\n return a + b\n",
@@ -71,7 +67,6 @@ def test_tested_module_no_gap():
def test_missing_implementation():
"""Verifies missing implementation logic."""
with tempfile.TemporaryDirectory() as tmpdir:
_make_repo(tmpdir, {
"src/app.py": "def run():\n pass\n",
@@ -83,7 +78,6 @@ def test_missing_implementation():
def test_private_symbols_skipped():
"""Verifies private symbols skipped logic."""
with tempfile.TemporaryDirectory() as tmpdir:
_make_repo(tmpdir, {
"src/app.py": "def _internal():\n pass\ndef public():\n pass\n",
@@ -96,21 +90,18 @@ def test_private_symbols_skipped():
def test_empty_repo():
"""Verifies behavior with empty or None input."""
with tempfile.TemporaryDirectory() as tmpdir:
report = KnowledgeGapIdentifier().analyze(tmpdir)
assert len(report.gaps) == 0
def test_invalid_path():
"""Verifies invalid path logic."""
report = KnowledgeGapIdentifier().analyze("/nonexistent/path/xyz")
assert len(report.gaps) == 1
assert report.gaps[0].severity == GapSeverity.ERROR
def test_report_summary():
"""Verifies report summary logic."""
with tempfile.TemporaryDirectory() as tmpdir:
_make_repo(tmpdir, {
"src/app.py": "class MyService:\n def handle(self):\n pass\n",
@@ -123,7 +114,6 @@ def test_report_summary():
def test_report_to_dict():
"""Verifies report to dict logic."""
with tempfile.TemporaryDirectory() as tmpdir:
_make_repo(tmpdir, {
"src/app.py": "def hello():\n pass\n",

View File

@@ -32,7 +32,6 @@ class TestBottleneck:
"""Test Bottleneck dataclass."""
def test_creation(self):
"""Verifies creation logic."""
b = Bottleneck(
category="test",
name="test_foo",
@@ -49,7 +48,6 @@ class TestBottleneck:
assert b.line_number is None
def test_with_location(self):
"""Verifies with location logic."""
b = Bottleneck(
category="test",
name="test_bar",
@@ -63,7 +61,6 @@ class TestBottleneck:
assert b.line_number == 42
def test_to_dict(self):
"""Verifies to dict logic."""
b = Bottleneck("test", "x", 1.0, "info", "y")
d = b.__dict__
assert "category" in d
@@ -74,7 +71,6 @@ class TestPerfReport:
"""Test PerfReport dataclass."""
def test_creation(self):
"""Verifies creation logic."""
report = PerfReport(
timestamp="2026-01-01T00:00:00Z",
repo_path="/tmp/repo"
@@ -84,7 +80,6 @@ class TestPerfReport:
assert report.summary == {}
def test_to_dict(self):
"""Verifies to dict logic."""
report = PerfReport(
timestamp="2026-01-01T00:00:00Z",
repo_path="/tmp/repo",
@@ -99,7 +94,6 @@ class TestSeveritySort:
"""Test severity sorting."""
def test_critical_first(self):
"""Verifies critical first logic."""
items = [
Bottleneck("test", "a", 1.0, "info", ""),
Bottleneck("test", "b", 0.5, "critical", ""),
@@ -111,7 +105,6 @@ class TestSeveritySort:
assert items[2].severity == "info"
def test_duration_within_severity(self):
"""Verifies duration within severity logic."""
items = [
Bottleneck("test", "slow", 10.0, "warning", ""),
Bottleneck("test", "fast", 1.0, "warning", ""),
@@ -124,7 +117,6 @@ class TestSlowTestScan:
"""Test slow test pattern scanning."""
def test_finds_sleep(self, tmp_path):
"""Verifies finds sleep logic."""
test_file = tmp_path / "test_sleepy.py"
test_file.write_text(textwrap.dedent('''
import time
@@ -139,7 +131,6 @@ class TestSlowTestScan:
assert any("sleep" in b.recommendation.lower() for b in bottlenecks)
def test_finds_http_calls(self, tmp_path):
"""Verifies finds http calls logic."""
test_file = tmp_path / "test_http.py"
test_file.write_text(textwrap.dedent('''
import requests
@@ -154,7 +145,6 @@ class TestSlowTestScan:
assert any("HTTP" in b.recommendation or "mock" in b.recommendation.lower() for b in bottlenecks)
def test_skips_non_test_files(self, tmp_path):
"""Verifies skips non files logic."""
src_file = tmp_path / "main.py"
src_file.write_text("import time\ntime.sleep(10)\n")
@@ -162,12 +152,10 @@ class TestSlowTestScan:
assert len(bottlenecks) == 0
def test_handles_missing_dir(self):
"""Verifies handles missing dir logic."""
bottlenecks = find_slow_tests_by_scan("/nonexistent/path")
assert bottlenecks == []
def test_file_path_populated(self, tmp_path):
"""Verifies file path populated logic."""
test_file = tmp_path / "test_example.py"
test_file.write_text("import time\n\ndef test_it():\n time.sleep(2)\n")
@@ -181,7 +169,6 @@ class TestBuildArtifacts:
"""Test build artifact analysis."""
def test_finds_large_node_modules(self, tmp_path):
"""Verifies finds large node modules logic."""
nm = tmp_path / "node_modules"
nm.mkdir()
# Create a file > 10MB
@@ -193,7 +180,6 @@ class TestBuildArtifacts:
assert any("node_modules" in b.name for b in bottlenecks)
def test_ignores_small_dirs(self, tmp_path):
"""Verifies ignores small dirs logic."""
nm = tmp_path / "node_modules"
nm.mkdir()
small_file = nm / "small.txt"
@@ -203,7 +189,6 @@ class TestBuildArtifacts:
assert not any("node_modules" in b.name for b in bottlenecks)
def test_finds_pycache(self, tmp_path):
"""Verifies finds pycache logic."""
cache = tmp_path / "__pycache__"
cache.mkdir()
big_file = cache / "big.pyc"
@@ -217,7 +202,6 @@ class TestMakefileAnalysis:
"""Test Makefile analysis."""
def test_finds_pip_install(self, tmp_path):
"""Verifies finds pip install logic."""
makefile = tmp_path / "Makefile"
makefile.write_text(textwrap.dedent('''
install:
@@ -231,7 +215,6 @@ class TestMakefileAnalysis:
assert len(bottlenecks) >= 1
def test_no_makefile(self, tmp_path):
"""Verifies no makefile logic."""
bottlenecks = analyze_makefile_targets(str(tmp_path))
assert bottlenecks == []
@@ -240,7 +223,6 @@ class TestImportAnalysis:
"""Test heavy import detection."""
def test_finds_pandas(self, tmp_path):
"""Verifies finds pandas logic."""
src = tmp_path / "analysis.py"
src.write_text("import pandas as pd\n")
@@ -249,7 +231,6 @@ class TestImportAnalysis:
assert any("pandas" in b.name for b in bottlenecks)
def test_finds_torch(self, tmp_path):
"""Verifies finds torch logic."""
src = tmp_path / "model.py"
src.write_text("import torch\n")
@@ -257,7 +238,6 @@ class TestImportAnalysis:
assert any("torch" in b.name for b in bottlenecks)
def test_skips_light_imports(self, tmp_path):
"""Verifies skips light imports logic."""
src = tmp_path / "utils.py"
src.write_text("import json\nimport os\nimport sys\n")
@@ -269,14 +249,12 @@ class TestGenerateReport:
"""Test full report generation."""
def test_empty_repo(self, tmp_path):
"""Verifies behavior with empty or None input."""
report = generate_report(str(tmp_path))
assert report.summary["total_bottlenecks"] >= 0
assert "critical" in report.summary
assert "warning" in report.summary
def test_with_findings(self, tmp_path):
"""Verifies with findings logic."""
# Create a test file with issues
test_file = tmp_path / "test_slow.py"
test_file.write_text(textwrap.dedent('''
@@ -295,7 +273,6 @@ class TestGenerateReport:
assert len(report.bottlenecks) > 0
def test_summary_categories(self, tmp_path):
"""Verifies summary categories logic."""
report = generate_report(str(tmp_path))
assert "by_category" in report.summary
@@ -304,7 +281,6 @@ class TestMarkdownReport:
"""Test markdown output."""
def test_format(self):
"""Verifies format logic."""
report = PerfReport(
timestamp="2026-01-01T00:00:00Z",
repo_path="/tmp/repo",
@@ -327,7 +303,6 @@ class TestMarkdownReport:
assert "Fix it" in md
def test_empty_report(self):
"""Verifies behavior with empty or None input."""
report = PerfReport(
timestamp="2026-01-01T00:00:00Z",
repo_path="/tmp/repo",

View File

@@ -21,32 +21,27 @@ from quality_gate import (
class TestScoreSpecificity(unittest.TestCase):
def test_specific_content_scores_high(self):
"""Verifies specific content scores high logic."""
content = "Run `python3 deploy.py --env prod` on 2026-04-15. Example: step 1 configure nginx."
score = score_specificity(content)
self.assertGreater(score, 0.6)
def test_vague_content_scores_low(self):
"""Verifies vague content scores low logic."""
content = "It generally depends. Various factors might affect this. Basically, it varies."
score = score_specificity(content)
self.assertLess(score, 0.5)
def test_empty_scores_baseline(self):
"""Verifies behavior with empty or None input."""
score = score_specificity("")
self.assertAlmostEqual(score, 0.5, delta=0.1)
class TestScoreActionability(unittest.TestCase):
def test_actionable_content_scores_high(self):
"""Verifies actionable content scores high logic."""
content = "1. Run `pip install -r requirements.txt`\n2. Execute `python3 train.py`\n3. Verify with `pytest`"
score = score_actionability(content)
self.assertGreater(score, 0.6)
def test_abstract_content_scores_low(self):
"""Verifies abstract content scores low logic."""
content = "The concept of intelligence is fascinating and multifaceted."
score = score_actionability(content)
self.assertLess(score, 0.5)
@@ -54,40 +49,33 @@ class TestScoreActionability(unittest.TestCase):
class TestScoreFreshness(unittest.TestCase):
def test_recent_timestamp_scores_high(self):
"""Verifies recent timestamp scores high logic."""
recent = datetime.now(timezone.utc).isoformat()
score = score_freshness(recent)
self.assertGreater(score, 0.9)
def test_old_timestamp_scores_low(self):
"""Verifies old timestamp scores low logic."""
old = (datetime.now(timezone.utc) - timedelta(days=365)).isoformat()
score = score_freshness(old)
self.assertLess(score, 0.2)
def test_none_returns_baseline(self):
"""Verifies behavior with empty or None input."""
score = score_freshness(None)
self.assertEqual(score, 0.5)
class TestScoreSourceQuality(unittest.TestCase):
def test_claude_scores_high(self):
"""Verifies claude scores high logic."""
self.assertGreater(score_source_quality("claude-sonnet"), 0.85)
def test_ollama_scores_lower(self):
"""Verifies ollama scores lower logic."""
self.assertLess(score_source_quality("ollama"), 0.7)
def test_unknown_returns_default(self):
"""Verifies unknown returns default logic."""
self.assertEqual(score_source_quality("unknown"), 0.5)
class TestScoreEntry(unittest.TestCase):
def test_good_entry_scores_high(self):
"""Verifies good entry scores high logic."""
entry = {
"content": "To deploy: run `kubectl apply -f deployment.yaml`. Verify with `kubectl get pods`.",
"model": "claude-sonnet",
@@ -97,7 +85,6 @@ class TestScoreEntry(unittest.TestCase):
self.assertGreater(score, 0.6)
def test_poor_entry_scores_low(self):
"""Verifies poor entry scores low logic."""
entry = {
"content": "It depends. Various things might happen.",
"model": "unknown",
@@ -108,7 +95,6 @@ class TestScoreEntry(unittest.TestCase):
class TestFilterEntries(unittest.TestCase):
def test_filters_low_quality(self):
"""Verifies knowledge filtering by filters low quality."""
entries = [
{"content": "Run `deploy.py` to fix the issue.", "model": "claude"},
{"content": "It might work sometimes.", "model": "unknown"},