GCMD_Keyword_Classifier_MVP / MVP_PROGRESS.md
igerasimov's picture
MVP Milestone 12
9f2f534
|
Raw
History Blame Contribute Delete
54.2 kB
# MVP Progress
## Milestone 1: Repository Setup and Tooling
Status: completed
Scope completed:
- Packaging and pytest configuration
- Pydantic deterministic-core dependency
- Ruff configuration
- Optional dependency groups for OpenAI and Gradio
- Environment example file
- `.gitignore` for local, Python, test, build, and generated-output artifacts
- Import smoke test
Verification:
- `python -m ruff check .` passed: `All checks passed!`
- `python -m ruff format --check .` passed: `2 files already formatted`
- `python -m pytest` passed: `1 passed in 0.00s`
Out of scope, not started:
- Vocabulary loading
- Article loading
- Model integration
- Classification logic
- Persistence
- UI and root `app.py`
Source-file status:
- `data/gcmd_hierarchy.json` unchanged by this milestone.
- `data/articles.json` unchanged by this milestone.
- `prototype/app_hf_poc.py` unchanged by this milestone.
## Milestone 2: Hierarchy Loader and Vocabulary Index
Status: completed
Scope completed:
- Deterministic loader for `data/gcmd_hierarchy.json`
- Recursive traversal of the GCMD hierarchy through `Category`, `Topic`, `Term`, `Variable_Level_1`, `Variable_Level_2`, and `Variable_Level_3`
- Canonical UUID-bearing concept records
- Vocabulary file SHA-256 version hash
- Canonical paths excluding the root `Category`
- UUID, canonical path, parent, direct-child, Topic, Term, Variable, ancestor, and descendant lookups
- Typed vocabulary errors for malformed hierarchy content
- Focused small hierarchy fixture and full-data smoke tests
Implemented models and lookup structures:
- `CanonicalConceptRecord` in `src/gcmd_classifier/models.py`
- `VocabularyIndex` in `src/gcmd_classifier/vocabulary/index.py`
- `load_vocabulary`, `build_vocabulary_index`, and `calculate_file_hash` in `src/gcmd_classifier/vocabulary/loader.py`
Hierarchy assumptions discovered from the current real file:
- The root node is the non-UUID `Category` named `EARTH SCIENCE`.
- The current file contains only the `EARTH SCIENCE` Category represented by that root.
- Every current Topic and Term has a UUID.
- The only UUID-less node in the current file is the root Category.
- Current UUID-bearing record counts are: 14 Topics, 144 Terms, 1,368 `Variable_Level_1`, 1,456 `Variable_Level_2`, 553 `Variable_Level_3`, and 3,535 total UUID-bearing records.
- Current observed transitions are adjacent only: `Category -> Topic -> Term -> Variable_Level_1 -> Variable_Level_2 -> Variable_Level_3`.
Error cases implemented:
- Invalid JSON
- Non-object root or child nodes
- Missing or empty required `level` and `name`
- Unsupported hierarchy levels
- UUID-bearing root Category
- UUID-less non-root nodes
- Invalid parent-child level transitions
- Children under `Variable_Level_3`
- Non-list `children`
- Non-string `definition`
- Duplicate UUIDs
- Same canonical path assigned to different UUIDs
- Lookup requests for unknown UUIDs or invalid lookup scopes
Verification:
- `python -m ruff check .` passed: `All checks passed!`
- `python -m ruff format --check .` passed: `8 files already formatted`
- `python -m pytest` passed: `23 passed in 0.14s`
Out of scope, not started:
- Article loading
- Model integration
- Topic routing
- Term routing
- Variable-level classification
- Deterministic final classification validation beyond vocabulary lookup integrity
- Redundancy removal
- Persistence, caching, and output generation
- UI and root `app.py`
Source-file status:
- `data/gcmd_hierarchy.json` unchanged by this milestone.
- `data/articles.json` unchanged by this milestone.
- `prototype/app_hf_poc.py` unchanged by this milestone.
Deviations from approved plan:
- None.
Remaining risks or assumptions:
- The loader remains fully dynamic and does not hard-code vocabulary names, UUIDs, paths, branches, or record counts, but full-data smoke tests intentionally assert current-file counts.
- Future approved vocabulary rules would be needed before accepting any UUID-less non-root hierarchy node.
## Milestone 3: Article Loading and Validation
Status: completed
Scope completed:
- Deterministic loader for `data/articles.json`
- Strict source article model preserving exact serialized fields: `DOI`, `Title`, `Year`, and `Abstract`
- Read/decode, individual record validation, duplicate DOI detection, and aggregate result return paths
- Record-level validation errors with source index, code, field, message, and DOI when available
- Valid and invalid article fixtures
- Full-data smoke test for the current `data/articles.json`
Implemented article model and loader APIs:
- `ArticleRecord` in `src/gcmd_classifier/models.py`
- `ArticleValidationIssue` and `ArticleLoadResult` in `src/gcmd_classifier/models.py`
- `read_article_json`, `validate_article_record`, `validate_article_records`, and `load_articles` in `src/gcmd_classifier/articles/loader.py`
Validation and error-collection policy:
- File-level conditions raise typed exceptions: missing file, invalid JSON, and top-level JSON value that is not a list.
- Record-level conditions are collected in `ArticleLoadResult.errors` while all remaining records continue to be validated.
- Duplicate DOI records are reported with `DUPLICATE_DOI` and excluded from `ArticleLoadResult.articles` so returned records have unique DOI values.
Extra-field policy:
- Unexpected fields are rejected for the MVP with `UNEXPECTED_FIELD` because neither `PROJECT_SPEC.md` nor `AGENTS.md` defines an allow-list extension policy for source article records.
Exact-preservation protections:
- Successful `ArticleRecord` values are returned unchanged.
- DOI values are not normalized or replaced.
- Titles and abstracts are not stripped, lowercased, rewritten, summarized, or supplemented.
- Empty `Abstract` values are valid and preserved exactly.
- Whitespace-only strings are preserved; non-empty checks do not trim source values.
- `Year` must be a strict integer and Boolean values are rejected.
Current full-data findings:
- `data/articles.json` contains 468 source records.
- 467 records have valid non-empty unique DOI values.
- Exactly one record, index 467, is invalid because `DOI` is empty.
- 14 records have empty `Abstract` values and otherwise load successfully.
Error cases covered:
- File not found
- Invalid JSON
- Top-level JSON value not a list
- Article entry not an object
- Missing `DOI`, `Title`, `Year`, or `Abstract`
- Empty `DOI` and `Title`
- Non-string `DOI`, `Title`, or `Abstract`
- String, floating-point, and Boolean `Year`
- Null field values
- Duplicate DOI
- Unexpected extra fields
- Multiple invalid records reported in one aggregate result
Verification:
- `python -m ruff check .` passed: `All checks passed!`
- `python -m ruff format --check .` passed: `11 files already formatted`
- `python -m pytest` passed: `51 passed in 0.18s`
Out of scope, not started:
- Model prompts
- Model-provider integration
- Topic routing
- Term routing
- Variable-level classification
- Persistence
- Caching
- Batch processing
- UI and root `app.py`
Source-file status:
- `data/articles.json` unchanged by this milestone.
- `data/gcmd_hierarchy.json` unchanged by this milestone.
- `prototype/app_hf_poc.py` unchanged by this milestone.
Deviations from approved plan:
- `IMPLEMENTATION_PLAN.md` was revised to reflect the clarified Milestone 3 rule that empty `Abstract` values are valid.
Remaining risks or assumptions:
- The current source file remains partially invalid until the empty DOI at index 467 is corrected.
- Whitespace-only DOI or Title values are non-empty and therefore preserved as valid under the explicit no-trimming rule; this can be tightened later only with an approved source-data rule.
## Milestone 4: Shared Models, Output Schema Draft, and Structured Response Schemas
Status: completed
Scope completed:
- Structured Pydantic response schemas for Topic, Term, and Variable decisions
- Shared candidate decision schema with selected candidate ID, optional uncalibrated confidence, evidence, support type, and reason
- Term and Variable response invariants for `stop_at_parent`, `selected`, and `stop_reason`
- Separate status enums for article processing status, article classification outcome, classification final status, and review status
- Article result, classification record, deterministic validation result, warning/error, processing metadata, and run summary models
- Draft JSON Schema files for article classification results and run summaries
- Tests for structured response validation, output model validation, status-scope separation, confidence ranges, and JSON Schema validation
Structured response models implemented:
- `CandidateDecision`
- `TopicResponse`
- `TermResponse`
- `VariableResponse`
Result and status models implemented:
- `SupportType`
- `ArticleProcessingStatus`
- `ArticleClassificationOutcome`
- `ClassificationFinalStatus`
- `ReviewStatus`
- `OutputWarning`
- `OutputError`
- `DeterministicValidationResult`
- `ConfidenceMetadata`
- `OriginalCandidateReference`
- `SemanticValidationResult`
- `ClassificationRecord`
- `ProcessingMetadata`
- `ArticleResult`
- `RunSummary`
Schema validation rules and invariants implemented:
- Candidate IDs must be non-empty strings.
- Candidate confidence, when present, must be between `0.0` and `1.0`.
- Candidate evidence is required and non-empty for selected candidates.
- Unknown fields are rejected in structured model responses and output models.
- `stop_at_parent: true` requires an empty `selected` list.
- Non-empty `selected` is represented only with `stop_at_parent: false`.
- `stop_at_parent: true` requires a non-empty `stop_reason`.
- Accepted and reduced classification records require successful deterministic validation.
- Completed not-classified article results require an empty `classifications` list and a non-empty `no_classification_reason`.
- Completed classified article results require at least one classification record.
- Failed and partial article results can be represented without classification records.
- Article result output preserves exact source field names: `DOI`, `Title`, `Year`, and `Abstract`.
- Empty `Abstract` is valid in article result output.
- Boolean `Year` is rejected by article result validation.
- Review-compatible enum values are representable, but no review-trigger logic was implemented.
JSON Schema files:
- `schemas/classification_result.schema.json`
- `schemas/run_summary.schema.json`
Dependencies:
- No dependency was added. `jsonschema` was already available in the active interpreter and was used for JSON Schema tests.
Verification:
- `python -m ruff check .` passed: `All checks passed!`
- `python -m ruff format --check .` passed: `15 files already formatted`
- `python -m pytest` passed: `93 passed in 0.33s`
Out of scope, not started:
- Live model-provider implementation
- Prompt construction
- Topic routing
- Term routing
- Variable-level traversal or classification
- Semantic validation implementation
- Persistence
- Caching
- Batch processing
- UI and root `app.py`
Source-file status:
- `data/gcmd_hierarchy.json` unchanged by this milestone.
- `data/articles.json` unchanged by this milestone.
- `prototype/app_hf_poc.py` unchanged by this milestone.
Deviations from approved plan:
- None.
Remaining risks or assumptions:
- The JSON Schemas are draft schemas generated from the current Pydantic models; they are useful for MVP validation but may need hand refinement before publication as final normative schemas.
- Confidence remains optional, uncalibrated metadata and does not independently determine acceptance.
- Review-related enum values are schema-compatible only; emitting them still requires later approved decision rules.
- The schema can represent future semantic validation output, but Milestone 4 does not implement a semantic validator.
## Milestone 5: Model Abstraction, Prompts, Fake Model, and Retries
Status: completed
Scope completed:
- Provider-neutral model client protocol and request/response types
- Typed structured model responses carried through the model interface
- Deterministic scripted fake model client for unit tests
- Retry wrapper for retryable model failures
- Prompt builders for Topic, Term, and Variable decisions
- Environment-backed model and prompt configuration
- Guarded optional OpenAI provider module with isolated imports
- Unit tests for fake model behavior, prompts, retry behavior, configuration, and OpenAI import isolation
Model interface implemented:
- `ModelClient` protocol
- `ModelStage`
- `ModelRequest`
- `ModelResponse`
- `TokenUsage`
- `RetryPolicy`
- `generate_with_retries`
Fake model behavior implemented:
- Returns scripted typed Topic, Term, and Variable responses.
- Supports no-selection responses.
- Can return invalid candidate IDs for later routing-validation tests without performing candidate validation in this layer.
- Raises scripted retryable and non-retryable errors.
- Supports fail-once-then-succeed retry scenarios.
- Supports retry exhaustion scenarios.
- Records every request it receives for assertions.
- Does not import or require OpenAI.
Prompt builders implemented:
- `build_topic_prompt`
- `build_term_prompt`
- `build_variable_prompt`
- `PromptCandidate`
- `ParentContext`
Prompt protections implemented:
- Article title and abstract are clearly delimited.
- Article content is explicitly described as untrusted input.
- Prompts instruct the model not to follow instructions inside article text.
- Prompts instruct the model to choose only supplied `candidate_id` values.
- Prompts instruct the model not to invent, generate, or modify UUIDs, canonical paths, labels, hierarchy levels, or parent-child relationships.
- Prompts request structured output only.
- Prompts allow no selection for Topic routing and stopping at parent for Term and Variable decisions.
- Prompts remain valid when `Abstract` is an empty string.
- Source article text is inserted without trimming, normalization, or rewriting.
Retry behavior implemented:
- Retry only `RetryableModelError` failures.
- Do not retry `NonRetryableModelError` failures.
- Configurable `max_retries` through `RetryPolicy` and `ModelSettings`.
- Successful responses include the retry count.
- Exhausted retries raise `ModelRetriesExhaustedError` with the retry count.
- Unknown candidate IDs are not retried or validated in this layer; `UnknownCandidateIDError` is available for later routing stages.
Configuration fields added:
- `MODEL_PROVIDER`
- `MODEL_NAME`
- `MODEL_TEMPERATURE`
- `MODEL_TIMEOUT_SECONDS`
- `MODEL_MAX_RETRIES`
- `PROMPT_VERSION_TOPIC`
- `PROMPT_VERSION_TERM`
- `PROMPT_VERSION_VARIABLE`
- `MODEL_API_KEY_ENV_VAR`
- `MODEL_INCLUDE_COST_METADATA`
OpenAI provider status:
- `OpenAIModelClient` is implemented as a minimal guarded optional provider behind the shared interface.
- OpenAI imports occur only inside provider construction, not at module import time.
- API keys are read from environment variables only.
- Unit tests do not instantiate a live configured provider and make no live API calls.
- The provider is intentionally lightly tested in this milestone; fake-model and interface behavior are the deterministic core.
Dependencies:
- No dependency was added.
Verification:
- `python -m ruff check .` passed: `All checks passed!`
- `python -m ruff format --check .` passed: `25 files already formatted`
- `python -m pytest` passed: `124 passed in 0.37s`
Live model/API status:
- No live model API calls were made.
Out of scope, not started:
- Topic routing implementation
- Term routing implementation
- Variable traversal or end-to-end classification
- Deterministic candidate validation in routing components
- Semantic validation implementation
- Persistence
- Caching
- Batch processing
- Gradio UI
- Root `app.py`
Source-file status:
- `data/gcmd_hierarchy.json` unchanged by this milestone.
- `data/articles.json` unchanged by this milestone.
- `prototype/app_hf_poc.py` unchanged by this milestone.
Deviations from approved plan:
- The OpenAI provider is minimal and guarded rather than fully integration-tested, because live provider behavior and SDK details are intentionally outside deterministic unit tests for this milestone.
Remaining risks or assumptions:
- Provider-specific retry classification for OpenAI SDK exception classes may need refinement when live integration tests are explicitly approved.
- Prompt wording is covered by unit tests, but prompt quality still needs later evaluation with routing components.
- Candidate ID validation belongs to later routing logic and was not implemented here.
## Milestone 6: Topic Routing
Status: completed
Scope completed:
- Topic candidate construction from the loaded vocabulary index
- Topic routing through the provider-neutral model interface
- Structured `TopicResponse` request and parsing via the Milestone 5 model layer
- Selected `candidate_id` validation against the current prompt candidate set
- Topic branch seed output for later Term routing
- Valid no-topic routing result without fallback behavior
- Unit tests for candidate construction, Topic routing, model interaction, invalid candidates, and current full-data Topic count
Topic candidate builder behavior implemented:
- `build_topic_candidates` reads Topic records from `VocabularyIndex.topics()`.
- Only UUID-bearing `Topic` records are converted into candidates.
- The non-UUID root `Category` is never included as a candidate.
- Candidate IDs are application-supplied deterministic IDs such as `topic_0001`, not authoritative UUIDs.
- Candidate IDs map back to authoritative Topic UUIDs in application code.
- Prompt candidates include candidate ID, Topic name, level, optional definition, and canonical path context.
- No Topic names, UUIDs, paths, or counts are hard-coded in application behavior.
Topic router behavior implemented:
- `route_topics` builds Topic candidates, creates a Topic prompt, and calls `generate_with_retries` using the neutral `ModelClient` interface.
- The router requests the `TopicResponse` schema.
- Selected candidate IDs are mapped back to authoritative Topic records from the vocabulary index.
- Each selected Topic produces a `TopicBranchSeed` containing branch ID, Topic UUID, Topic name, level, canonical path, evidence, support type, confidence, model reason, original candidate ID, prompt version, provider, model name, and retry count.
- The router performs Topic routing only and does not route Terms or Variables.
No-topic behavior implemented:
- Empty Topic selections return a successful `TopicRoutingResult` with no branches.
- `no_selection_reason` is preserved when supplied by the model.
- No fallback Topic is created.
- No fake `FALLBACK` behavior from the proof of concept is used.
Invalid candidate behavior implemented:
- Unknown selected candidate IDs raise `UnknownCandidateIDError`.
- Duplicate selected candidate IDs raise `UnknownCandidateIDError`.
- Empty or malformed candidate IDs fail structured model response validation before routing accepts them.
- Invalid Topic candidate IDs are not mapped to vocabulary records and are not silently dropped.
Fake-model tests added:
- Single Topic selection
- Multiple Topic selection
- No Topic selection
- Invalid candidate ID selection
- Empty/malformed candidate ID rejection
- Provider-neutral request assertions
- Empty `Abstract` prompt pass-through
- No live provider usage
Current full-data smoke assertion:
- The current `data/gcmd_hierarchy.json` builds 14 Topic candidates for the EARTH SCIENCE vocabulary.
- All current full-data Topic candidates map to UUID-bearing Topic records.
- This count is a current-file smoke assertion only and is not hard-coded into application behavior.
Verification:
- `python -m ruff check .` passed: `All checks passed!`
- `python -m ruff format --check .` passed: `29 files already formatted`
- `python -m pytest` passed: `143 passed in 0.43s`
Live model/API status:
- No live model API calls were made.
- Unit tests use `FakeModelClient` only.
Out of scope, not started:
- Term routing
- Variable traversal
- End-to-end classification
- Deterministic final classification validation
- Redundancy removal
- Persistence
- Caching
- Batch processing
- Gradio UI
- Root `app.py`
Source-file status:
- `data/gcmd_hierarchy.json` unchanged by this milestone.
- `data/articles.json` unchanged by this milestone.
- `prototype/app_hf_poc.py` unchanged by this milestone.
Deviations from approved plan:
- Tests were added in `tests/test_topic_routing.py` rather than `tests/test_pipeline.py` to keep this milestone focused on Topic routing instead of pipeline behavior.
Remaining risks or assumptions:
- Candidate IDs are stable for deterministic vocabulary order; if a future vocabulary inserts or reorders Topics, candidate IDs may shift for that prompt, but each prompt still carries its own candidate-to-record mapping.
- Topic routing currently raises explicit errors for unknown or duplicate selected IDs rather than returning partial valid selections with warnings.
- Confidence is preserved as optional uncalibrated metadata and is not used to independently accept a Topic.
## Milestone 7: Term Routing
Status: completed
Scope completed:
- Direct Term candidate construction beneath one selected Topic branch
- Term routing through the provider-neutral model interface
- Structured `TermResponse` request and parsing via the Milestone 5 model layer
- Selected Term `candidate_id` validation against the current prompt candidate set
- Topic-to-Term direct-child relationship validation
- Term branch seed output for later Variable routing
- Successful stop-at-Topic routing result when no Term is adequately supported
- Unit tests for Term candidate construction, Term routing, stop behavior, invalid candidates, relationship validation, model interaction, and current full-data Term count
Term candidate builder behavior implemented:
- `build_term_candidates` accepts a selected Topic UUID and reads only `VocabularyIndex.terms_for_topic(topic_uuid)`.
- Only UUID-bearing direct `Term` children of the selected Topic are converted into candidates.
- Terms from sibling Topics are not included.
- Candidate IDs are application-supplied deterministic IDs such as `term_0001`, scoped to the current Term prompt.
- Candidate IDs map back to authoritative Term UUIDs in application code.
- Prompt candidates include candidate ID, Term name, level, optional definition, canonical path context, and parent Topic context.
- No Term names, UUIDs, paths, counts, or Topic-to-Term relationships are hard-coded in application behavior.
Term router behavior implemented:
- `route_terms` consumes a Milestone 6 `TopicBranchSeed`.
- The router builds direct Term candidates only for the selected Topic.
- The router creates a Term prompt with selected parent Topic context and direct-child Term candidates.
- The router calls `generate_with_retries` using the neutral `ModelClient` interface.
- The router requests the `TermResponse` schema.
- Selected candidate IDs are mapped back to authoritative Term records from the vocabulary index.
- Each selected Term produces a `TermBranchSeed` containing branch ID, parent Topic UUID/name, Term UUID, Term name, level, canonical path, evidence, support type, confidence, model reason, original candidate ID, parent branch ID, prompt version, provider, model name, and retry count.
- Multiple selected Terms produce independent branch seeds.
- The router performs Term routing only and does not route Variables.
Stop-at-Topic behavior implemented:
- `stop_at_parent: true` returns a successful `TopicStopResult`.
- `stop_reason` is preserved.
- Parent Topic UUID, name, level, canonical path, evidence, support type, confidence, and candidate ID are preserved.
- No Term branch seeds are created for stop-at-Topic results.
- Stopping at the Topic is not treated as a system failure.
- No fake or fallback Terms are created.
Invalid candidate behavior implemented:
- Unknown selected Term candidate IDs raise `UnknownCandidateIDError`.
- Duplicate selected Term candidate IDs raise `UnknownCandidateIDError`.
- Empty or malformed candidate IDs fail structured model response validation before routing accepts them.
- Invalid Term candidate IDs are not mapped to vocabulary records and are not silently dropped.
Topic-to-Term relationship validation implemented:
- `validate_term_candidate_relationship` verifies that each Term candidate belongs to the selected Topic.
- It checks both the candidate's stored Topic UUID and the vocabulary index parent relationship.
- A manually corrupted candidate-to-record mapping is rejected.
- A sibling Topic Term cannot be accepted because it is absent from the selected Topic prompt candidate set.
Fake-model tests added:
- One Term selected under a selected Topic
- Multiple Terms under one selected Topic
- Stop at Topic with `stop_at_parent: true`
- Topic with no direct/supported Term returning a stop result
- Invalid Term candidate ID selection
- Duplicate Term candidate ID selection
- Empty/malformed candidate ID rejection
- Provider-neutral request assertions
- Empty `Abstract` prompt pass-through
- No live provider usage
Current full-data smoke assertion:
- The current `data/gcmd_hierarchy.json` builds 144 direct Term candidates across all current Topics.
- All current full-data Term candidates map to UUID-bearing Term records.
- Current Term UUIDs are unique across Topic candidate lists.
- This count is a current-file smoke assertion only and is not hard-coded into application behavior.
Verification:
- `python -m ruff check .` passed: `All checks passed!`
- `python -m ruff format --check .` passed: `30 files already formatted`
- `python -m pytest` passed: `168 passed in 0.58s`
Live model/API status:
- No live model API calls were made.
- Unit tests use `FakeModelClient` only.
Out of scope, not started:
- Variable traversal
- End-to-end classification
- Final deterministic classification validation
- Redundancy removal
- Persistence
- Caching
- Batch processing
- Gradio UI
- Root `app.py`
Source-file status:
- `data/gcmd_hierarchy.json` unchanged by this milestone.
- `data/articles.json` unchanged by this milestone.
- `prototype/app_hf_poc.py` unchanged by this milestone.
Deviations from approved plan:
- Tests were added in `tests/test_term_routing.py` rather than `tests/test_pipeline.py` to keep this milestone focused on Term routing instead of pipeline behavior.
- No `classification/traversal.py` module was added because the necessary branch helpers fit cleanly in the existing candidate/router modules without introducing a premature abstraction.
Remaining risks or assumptions:
- Term candidate IDs are stable for deterministic direct-child order within a prompt; if vocabulary order changes, candidate IDs may shift, but each prompt still carries its own candidate-to-record mapping.
- Term routing raises explicit errors for unknown or duplicate selected IDs rather than returning partial valid selections with warnings.
- Confidence is preserved as optional uncalibrated metadata and is not used to independently accept a Term.
## Milestone 8: Variable-Level Controlled Descent
Status: completed
Scope completed:
- Direct Variable candidate construction beneath Term, `Variable_Level_1`, and `Variable_Level_2` parents
- Recursive controlled descent through Variable levels using only direct children
- Structured `VariableResponse` request and parsing via the Milestone 5 model layer
- Selected Variable `candidate_id` validation against the current prompt candidate set
- Direct parent-to-child relationship validation for selected Variables
- Terminal branch outcomes for stop-at-parent, no-child parent, and leaf-node cases
- Branch provenance and evidence trail tracking
- Branch-level error reporting that preserves successful sibling outcomes
- Unit tests for Variable candidate construction, controlled descent, stopping, leaf handling, multi-branch behavior, invalid candidates, provenance, model interaction, and current full-data Variable counts
Variable candidate builder behavior implemented:
- `build_variable_candidates` accepts a current parent UUID and reads only `VocabularyIndex.variables_for_parent(parent_uuid)`.
- Supported parents are `Term`, `Variable_Level_1`, and `Variable_Level_2` records.
- Only UUID-bearing direct Variable children are converted into candidates.
- Grandchildren, sibling branches, and global Variables are not included.
- Candidate IDs are application-supplied deterministic IDs such as `variable_0001`, scoped to the current Variable prompt.
- Candidate IDs map back to authoritative Variable UUIDs in application code.
- Prompt candidates include candidate ID, Variable name, level, optional definition, canonical path context, and parent context.
- No Variable names, UUIDs, paths, counts, or parent-child relationships are hard-coded in application behavior.
Controlled descent behavior implemented:
- `descend_variables` starts from a Milestone 7 `TermBranchSeed`.
- Each parent evaluation calls `generate_with_retries` using the neutral `ModelClient` interface and requests `VariableResponse`.
- Each model call receives only direct Variable children of the current parent.
- Selected children create independent child branches with distinct branch IDs.
- Descent continues recursively for selected child branches only.
- Grandchildren are not evaluated before their parent is selected.
- The traversal does not skip hierarchy levels and does not evaluate Variables globally.
Stop-at-parent behavior implemented:
- A parent with no direct Variable children returns a successful terminal outcome at the current parent.
- `stop_at_parent: true` returns a successful terminal outcome at the current parent and preserves `stop_reason`.
- `Variable_Level_3` selections stop naturally as leaf-node terminal outcomes.
- Term, `Variable_Level_1`, and `Variable_Level_2` parents can all become terminal outcomes when descent is unsupported.
- No fake or fallback Variable is created.
- Descent is not forced to a leaf.
Multi-branch behavior implemented:
- Multiple selected child Variables create independent branch outcomes.
- Child branch IDs preserve the parent branch lineage.
- Sibling branches continue independently when another sibling stops.
- Branch-level failures are recorded as `VariableBranchError` entries and do not discard successful sibling terminal outcomes.
Invalid candidate behavior implemented:
- Unknown selected Variable candidate IDs raise `UnknownCandidateIDError` at the current branch.
- Duplicate selected Variable candidate IDs raise `UnknownCandidateIDError` at the current branch.
- Empty or malformed candidate IDs fail structured model response validation before traversal accepts them.
- Invalid Variable candidate IDs are not mapped to vocabulary records and are not silently dropped.
- `validate_variable_candidate_relationship` rejects candidates that are not direct children of the current parent.
Branch provenance behavior implemented:
- `VariableTerminalOutcome` includes branch ID, parent branch ID, Topic UUID/name, Term UUID/name, final UUID/name/level/canonical path/path components, evidence, support type, confidence, reason, stop reason, candidate ID, prompt/model metadata, and retry count when available.
- `EvidenceStep` preserves the branch lineage and evidence trail from Term through selected Variable stages.
- Confidence remains optional, uncalibrated metadata and does not independently determine acceptance.
Fake-model tests added:
- Stop at Term
- Stop at `Variable_Level_1`
- Stop at `Variable_Level_2`
- Select `Variable_Level_3`
- Natural leaf-node handling
- Multiple independent `Variable_Level_1` branches
- Multiple selected descendants under one branch
- Sibling continuation after one branch stops
- Sibling continuation after one branch fails
- Unknown, duplicate, empty, and malformed candidate IDs
- Provider-neutral request assertions
- Empty `Abstract` prompt pass-through
- No live provider usage
Current full-data smoke assertion:
- The current `data/gcmd_hierarchy.json` builds direct Variable candidates matching current counts: 1,368 `Variable_Level_1`, 1,456 `Variable_Level_2`, and 553 `Variable_Level_3` records.
- Every current full-data Variable candidate maps to a UUID-bearing Variable record.
- Every current full-data Variable candidate is a direct child of the parent used to build it.
- These counts are current-file smoke assertions only and are not hard-coded into application behavior.
Verification:
- `python -m ruff check .` passed: `All checks passed!`
- `python -m ruff format --check .` passed: `32 files already formatted`
- `python -m pytest` passed: `197 passed in 0.58s`
Live model/API status:
- No live model API calls were made.
- Unit tests use `FakeModelClient` only.
Out of scope, not started:
- Redundancy removal
- Final deterministic classification validation
- Final article-level output construction
- Persistence
- Caching
- Batch processing
- Gradio UI
- Root `app.py`
Source-file status:
- `data/gcmd_hierarchy.json` unchanged by this milestone.
- `data/articles.json` unchanged by this milestone.
- `prototype/app_hf_poc.py` unchanged by this milestone.
Deviations from approved plan:
- Tests were added in `tests/test_variable_traversal.py` rather than `tests/test_pipeline.py` to keep this milestone focused on Variable descent instead of pipeline behavior.
- `classification/traversal.py` was added as planned for the reusable descent engine.
- Branch-level partial behavior was implemented only for selected child branches; an initial Term-parent model failure still propagates because there are no sibling Variable branches yet at that point.
Remaining risks or assumptions:
- Variable candidate IDs are stable for deterministic direct-child order within a prompt; if vocabulary order changes, candidate IDs may shift, but each prompt still carries its own candidate-to-record mapping.
- Branch-level errors are represented for traversal continuity but are not yet converted into final article-level statuses.
- Terminal outcomes are routing/descent outcomes only; Milestone 9 still needs final deterministic validation and redundancy removal.
## Milestone 9: Deterministic Validation and Redundancy Removal
Status: completed
Scope completed:
- Deterministic validation of terminal classification candidates against `VocabularyIndex`
- Narrow conversion from validated routing/traversal candidates into final `ClassificationRecord` objects
- Structural diagnostics for rejected invalid candidates
- Redundancy removal for duplicate UUIDs, duplicate canonical paths, and same-lineage ancestor classifications
- Branch-provenance-aware preservation of independent ancestor classifications
- Unit tests for validation, conversion, diagnostics, redundancy removal, and full-data representative validation
Deterministic validation behavior implemented:
- `ClassificationCandidate` represents a terminal candidate awaiting deterministic vocabulary validation.
- `validate_candidate` confirms that the final UUID exists in the loaded vocabulary and is assignable.
- Supplied names, levels, canonical paths, path components, Topic context, Term context, and expected parent UUIDs are verified against authoritative vocabulary records when present.
- Parent-child ancestry is checked through `VocabularyIndex` relationships.
- Topic membership and Term membership are checked with UUID ancestry rather than model-supplied paths.
- Root `Category` candidates are rejected explicitly and never accepted as final classifications.
- Invented UUIDs, labels, levels, paths, and hierarchy relationships produce structured validation errors.
- Validation is structural only and does not make independent semantic-support decisions.
Final classification record conversion behavior implemented:
- Valid candidates are converted to `ClassificationRecord` only after deterministic validation succeeds.
- Accepted records use authoritative UUID, name, level, canonical path, path components, Topic, Term, and parent UUID from the vocabulary index.
- Routing/traversal metadata is preserved where available, including branch ID, evidence, support type, optional uncalibrated confidence, reason or stop reason, and validation result.
- Invalid candidates are returned as rejected validation results and do not appear in accepted `classifications`.
Redundancy removal behavior implemented:
- `remove_redundant_classifications` removes exact duplicate UUIDs.
- Exact duplicate canonical paths are removed even when the UUID differs.
- Ancestors are removed when a deeper descendant exists in the same branch lineage.
- Ancestor classifications are preserved when branch provenance indicates an independent decision.
- When provenance is insufficient, both ancestor and descendant are preserved with a warning.
- Sibling classifications are preserved.
- Same concept names in different branches are preserved when UUIDs and paths differ.
- Redundancy uses `VocabularyIndex.is_ancestor`, not string-prefix path matching, as the primary ancestry test.
Diagnostic behavior implemented:
- Rejected validation candidates include structured `OutputError` diagnostics with error code, stage, field, expected value, and actual value where useful.
- Redundancy actions produce structured `OutputWarning` diagnostics for duplicate UUID removal, duplicate canonical-path removal, same-branch ancestor removal, independent ancestor preservation, and insufficient-provenance preservation.
Tests added:
- `tests/test_validation.py`
- `tests/test_redundancy.py`
Verification:
- `python -m ruff check .` passed: `All checks passed!`
- `python -m ruff format --check .` passed: `36 files already formatted`
- `python -m pytest` passed: `230 passed in 0.69s`
Live model/API status:
- No model calls were made.
- No live API calls were made.
Out of scope, not started:
- Independent semantic validation
- Persistence
- Caching
- Batch processing
- Gradio UI
- Root `app.py`
- End-to-end article-level orchestration
Source-file status:
- `data/gcmd_hierarchy.json` unchanged by this milestone.
- `data/articles.json` unchanged by this milestone.
- `prototype/app_hf_poc.py` unchanged by this milestone.
Deviations from approved plan:
- Tests were added in focused `tests/test_validation.py` and `tests/test_redundancy.py` files, matching the approved likely file list.
- `src/gcmd_classifier/models.py` and `src/gcmd_classifier/errors.py` did not require changes because existing structured output and error models were sufficient.
Remaining risks or assumptions:
- Candidate ID provenance is retained as structured warning metadata on converted records rather than adding a new generated output field to `ClassificationRecord` in this milestone.
- Non-assignable concept rejection is implemented, but the current vocabulary fixture contains only assignable UUID-bearing records.
- Redundancy cleanup is collection-level only; integration into final article result assembly remains deferred to later milestones.
## Milestone 10: Batch Runner, Incremental Persistence, Caching, and Logging
Status: completed
Scope completed:
- Single-article orchestration through Topic routing, Term routing, Variable descent, deterministic validation, and redundancy removal
- Article-level `ArticleResult` generation for classified, not-classified, partial, and failed outcomes
- Batch processing over validated article records with continuation after article failures
- Invalid source article diagnostics without fallback DOI generation
- Incremental JSON checkpoint persistence after each article result
- Final consolidated machine-readable JSON output
- File-backed cache identity and cache reuse
- Structured logging helpers with secret redaction
- Run summary counters for valid/invalid records, processed articles, cache hits/misses, warnings, errors, and accepted classifications
Single-article pipeline behavior implemented:
- `classify_article` coordinates existing Milestone 6-9 components.
- No-topic results become `processing_status: completed`, `classification_outcome: not_classified`, empty classifications, and a no-classification reason.
- Stop-at-Topic, stop-at-Term, and Variable terminal outcomes are converted only after deterministic validation succeeds.
- Branch-level Variable descent errors are retained as structured errors while successful sibling terminal outcomes are preserved.
- Articles with accepted classifications and branch errors become `processing_status: partial` with the accepted classifications retained.
- Failed articles retain source fields exactly and include structured errors.
Batch runner behavior implemented:
- `run_batch` accepts an `ArticleLoadResult`, processes only valid records in source order, and reports invalid source records in the summary.
- One article failure does not stop later valid articles.
- Each article result is saved promptly after completion or failure.
- Batch output avoids duplicate article files for the same DOI-safe key by overwriting the DOI checkpoint path.
- The runner supports `force_reprocess` to bypass cache reuse.
Persistence strategy selected and implemented:
- The MVP uses one JSON checkpoint file per DOI-safe SHA-256 key plus an optional consolidated `results.json`.
- This append/checkpoint-style representation avoids rewriting a single large output document after every article.
- Individual article checkpoint writes and consolidated output writes use temp-file-plus-atomic-replace.
- Temporary and destination files are created in the same destination directory, so atomic replace occurs on the same filesystem.
- No database infrastructure was introduced.
Cache identity and reuse behavior implemented:
- `CacheIdentity` includes DOI, exact article fingerprint, vocabulary version/hash, model provider, model name, model temperature, timeout, max retries, Topic/Term/Variable prompt versions, application version, and relevant configuration hash.
- Cache keys change when article content, vocabulary version, model settings, prompt versions, application version, or relevant configuration changes.
- Cache hits return article results with `processing_metadata.cache_used: true`.
- Cache hits remain completed work and are never marked `skipped`.
- Force reprocessing bypasses cache reuse.
- Stale cache entries are not reused when identity inputs differ.
Logging and diagnostics behavior implemented:
- `log_event` records structured events through standard logging.
- `sanitize_log_details` redacts keys that look like API keys, tokens, authorization headers, secrets, passwords, or sensitive headers.
- Pipeline and batch events include DOI, stage, branch/cache context, status, classification counts, and error counts when available.
- Structured `OutputError` and `OutputWarning` models are used for article, branch, validation, redundancy, and invalid-source diagnostics.
Run summary behavior implemented:
- `RunSummary` now includes explicit `valid_article_records`, `invalid_source_records`, `processed_articles`, `cache_hits`, `cache_misses`, `total_warnings`, `total_errors`, and `duration_seconds` fields.
- `schemas/run_summary.schema.json` was regenerated from the updated Pydantic model.
- Summary output remains machine-readable and validates against the draft JSON Schema.
Tests added:
- `tests/test_cache.py`
- `tests/test_persistence.py`
- `tests/test_pipeline.py`
Verification:
- `python -m ruff check .` passed: `All checks passed!`
- `python -m ruff format --check .` passed: `46 files already formatted`
- `python -m pytest` passed: `261 passed in 0.77s`
Live model/API status:
- No live model API calls were made.
- Unit tests use `FakeModelClient` only.
Out of scope, not started:
- Gradio UI
- Hugging Face launcher
- Root `app.py`
- Live-model smoke tests
- Independent semantic validation
- Production deployment infrastructure
Source-file status:
- `data/gcmd_hierarchy.json` unchanged by this milestone.
- `data/articles.json` unchanged by this milestone.
- `prototype/app_hf_poc.py` unchanged by this milestone.
Deviations from approved plan:
- `src/gcmd_classifier/errors.py` did not require changes because existing typed exceptions and structured `OutputError` diagnostics were sufficient.
- `src/gcmd_classifier/config.py` did not require changes because existing model settings already covered the cache identity inputs needed for this milestone.
- `RunSummary` and `schemas/run_summary.schema.json` were updated to represent required Milestone 10 counters explicitly.
Remaining risks or assumptions:
- The checkpoint store uses DOI-safe SHA-256 filenames, so humans inspect consolidated JSON rather than filename-readable DOI values.
- Cached failed and partial results can be reused when the cache identity matches exactly; future policy may choose to cache only completed results.
- Batch processing is sequential for the MVP. Parallel execution remains deferred.
- Run summary model-call totals are available for fake clients and providers that expose request counts or metadata; richer token/cost aggregation remains provider-dependent.
## Milestone 11: Gradio Integration and Hugging Face Launcher
Status: completed
Scope completed:
- Lightweight Gradio demonstration UI package under `src/gcmd_classifier/ui/`
- `create_demo()` factory for a Gradio `Interface`
- Root-level thin Hugging Face launcher in `app.py`
- UI formatting for classifications, UUIDs, canonical paths, hierarchy levels, evidence, support type, confidence metadata, stopping reason, deterministic validation status, no-classification reason, warnings, and errors
- UI tests for importability, construction with a fake Gradio module, pipeline delegation, formatting helpers, empty `Abstract` handling, root launcher import behavior, and frozen prototype immutability
`create_demo()` behavior implemented:
- Builds a Gradio interface with `Title`, `Abstract`, optional `DOI`, and optional `Year` inputs.
- Returns summary Markdown, detailed `ArticleResult` JSON, and structured diagnostics JSON outputs.
- Defers all classification work until the user submits input.
- Uses `ModelSettings.from_environment()` for provider/model configuration.
- Uses a guarded Gradio import so non-UI modules and deterministic tests can run without Gradio import success.
Root `app.py` launcher behavior implemented:
- Imports only `create_demo` from `gcmd_classifier.ui.gradio_app`.
- Creates `demo = create_demo()` for Hugging Face Spaces compatibility.
- Calls `demo.launch()` only under `if __name__ == "__main__"`.
- Contains no routing, validation, model-provider, or classification business logic.
How the UI calls the pipeline service:
- `run_demo_classification` constructs an `ArticleRecord` from UI inputs.
- It loads the configured vocabulary when one is not injected.
- It creates a model client through a factory and calls `pipeline_service.classify_article`.
- It does not call Topic routing, Term routing, Variable descent, deterministic validation, redundancy removal, OpenAI, or any model provider directly.
Display behavior implemented:
- Classified results render a Markdown table with level, name, UUID, canonical path, evidence, support type, confidence metadata, stopping reason, and deterministic validation status.
- No-classification results display the no-classification reason.
- Warnings and errors appear in both the summary panel and structured diagnostics JSON.
- Detailed output returns the full `ArticleResult` JSON payload.
- Empty `Abstract` values are accepted and passed through to the pipeline input path.
Dependency handling:
- No dependency changes were made.
- Gradio remains an optional dependency group in `pyproject.toml`.
- In the active interpreter, importing Gradio fails because an optional audio dependency is unavailable (`pyaudioop`), so tests use a fake Gradio module for construction checks while keeping the real UI import guarded.
- No API keys or secrets are hard-coded or printed.
Tests added:
- `tests/test_ui.py`
Verification:
- `python -m ruff check .` passed: `All checks passed!`
- `python -m ruff format --check .` passed: `50 files already formatted`
- `python -m pytest` passed: `272 passed in 0.78s`
Live model/API status:
- No live model API calls were made in tests.
- UI tests use injected fake Gradio and fake pipeline/model behavior only.
Out of scope, not started:
- Production review UI
- Live-model smoke testing
- External deployment automation
- Milestone 12 evaluation work
Source-file status:
- `data/gcmd_hierarchy.json` unchanged by this milestone.
- `data/articles.json` unchanged by this milestone.
- `prototype/app_hf_poc.py` unchanged by this milestone.
Deviations from approved plan:
- Tests use a fake Gradio module for `create_demo()` construction because the active interpreter cannot import Gradio successfully. The UI still constructs a real Gradio interface when the optional dependency imports correctly.
- The UI supports a deterministic fake/demo model mode by default via `FakeModelClient`, but the fake response still runs through the pipeline service and is clearly represented as fake demo behavior.
- When DOI is omitted in manual UI input, the UI creates a deterministic `demo-ui:` DOI and emits a UI warning; source JSON article loading behavior is unchanged and still never invents identifiers.
Remaining risks or assumptions:
- Manual live-provider use depends on optional provider dependencies and environment variables being available in the runtime environment.
- The demo UI is intentionally simple and does not implement production human-review workflows.
- Root `app.py` requires a working Gradio optional dependency in the deployment environment, as expected for Hugging Face Spaces.
## Milestone 12: MVP Smoke/Evaluation Harness
Status: completed
Scope completed:
- Pytest-based MVP smoke/evaluation harness
- Fake-model end-to-end smoke run over a small current-data subset
- Optional live-model integration test marked `integration` and skipped by default
- Pytest marker registration for integration tests
- Baseline preservation checks for `prototype/app_hf_poc.py`
- Source-file immutability checks for current data files
Smoke/evaluation harness behavior implemented:
- The smoke command is `python -m pytest tests/test_mvp_smoke.py`.
- The smoke test loads the current full vocabulary from `data/gcmd_hierarchy.json`.
- The smoke test loads and validates the current full article source from `data/articles.json`.
- It verifies the current invalid source record is reported in diagnostics rather than silently ignored.
- It processes only two valid articles in routine tests to avoid full-data model processing.
- It uses the existing batch runner, cache, JSON store, pipeline service, routing, traversal, deterministic validation, redundancy removal, and output models.
- It validates article results and run summary output against the existing JSON Schemas.
Fake-model smoke behavior implemented:
- The fake smoke script dynamically selects a real Topic, Term, and Variable branch from the loaded vocabulary without hard-coding names, UUIDs, paths, or counts into application behavior.
- One fake article produces an accepted classification through Topic routing, Term routing, Variable descent, deterministic validation, redundancy removal, article result creation, and persistence.
- One fake article produces a completed no-classification result.
- Accepted classifications are asserted to have `deterministic_validation.valid: true`.
- The fake smoke run makes no live API calls.
Optional live integration behavior:
- `tests/test_mvp_smoke.py` includes an optional `pytest.mark.integration` live smoke test.
- It is skipped unless `GCMD_RUN_LIVE_INTEGRATION=1` is set.
- It is skipped unless `MODEL_PROVIDER=openai` is configured.
- It is skipped unless the configured API key environment variable is present.
- Normal `python -m pytest` does not run live model calls.
- If explicitly enabled, the live test processes only one article and asserts structural validity only.
Metrics recorded:
- Total source records considered
- Valid article records
- Invalid source records
- Articles processed in the smoke run
- Classified articles
- Not-classified articles
- Partial articles
- Failed articles
- Accepted classifications
- Deterministic validation failures through accepted-record assertions
- Warnings and errors
- Model calls
- Cache hits and misses
- Processing start time
- Processing end time
- Duration
- Token usage and estimated cost remain available when providers supply them
Baseline preservation behavior:
- The smoke harness reads `prototype/app_hf_poc.py` bytes before and after the fake smoke run and confirms they are unchanged.
- The smoke harness asserts the prototype module is not imported during the fake smoke run.
- No prototype execution, refactor, or comparison study was added.
Verification:
- `python -m ruff check .` passed: `All checks passed!`
- `python -m ruff format --check .` passed: `51 files already formatted`
- `python -m pytest` passed: `273 passed, 1 skipped in 0.89s`
Live model/API status:
- Normal tests made no live model API calls.
- The single skipped test is the optional live integration smoke test.
Out of scope, not started:
- Full expert-reviewed evaluation dashboard
- Publication benchmark
- Production monitoring system
- Model-comparison framework
- Full proof-of-concept comparison study
Source-file status:
- `data/gcmd_hierarchy.json` unchanged by this milestone.
- `data/articles.json` unchanged by this milestone.
- `prototype/app_hf_poc.py` unchanged by this milestone.
Deviations from approved plan:
- No standalone script was added because a pytest smoke harness is the simplest maintainable command and was explicitly allowed.
- `pyproject.toml` was updated only to register the `integration` pytest marker.
Remaining risks or assumptions:
- Fake-model smoke validates structural pipeline behavior, not scientific correctness.
- Live integration remains opt-in and depends on optional dependencies, credentials, and provider availability.
- Future baseline comparison with the frozen proof of concept remains deferred.