File size: 54,150 Bytes
f70d732
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0f2ecac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
413d5a1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0539596
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d840c10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
531d9de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5d05dd0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19d779e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57b6fe2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e726170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7dab768
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f2f534
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
# 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.