fieldkit 0.4.1__tar.gz → 0.4.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. {fieldkit-0.4.1 → fieldkit-0.4.3}/.gitignore +2 -1
  2. {fieldkit-0.4.1 → fieldkit-0.4.3}/CHANGELOG.md +56 -0
  3. {fieldkit-0.4.1 → fieldkit-0.4.3}/PKG-INFO +1 -1
  4. {fieldkit-0.4.1 → fieldkit-0.4.3}/docs/api/eval.md +61 -2
  5. {fieldkit-0.4.1 → fieldkit-0.4.3}/docs/api/publish.md +6 -1
  6. {fieldkit-0.4.1 → fieldkit-0.4.3}/pyproject.toml +1 -0
  7. {fieldkit-0.4.1 → fieldkit-0.4.3}/src/fieldkit/_version.py +1 -1
  8. {fieldkit-0.4.1 → fieldkit-0.4.3}/src/fieldkit/eval/__init__.py +419 -0
  9. fieldkit-0.4.3/src/fieldkit/eval/rubrics/office_action_argument.md +51 -0
  10. fieldkit-0.4.3/src/fieldkit/eval/rubrics/patent_claim_validity.md +53 -0
  11. {fieldkit-0.4.1 → fieldkit-0.4.3}/src/fieldkit/eval/vertical.py +105 -3
  12. {fieldkit-0.4.1 → fieldkit-0.4.3}/src/fieldkit/publish/__init__.py +27 -1
  13. fieldkit-0.4.3/tests/eval/__init__.py +0 -0
  14. fieldkit-0.4.3/tests/eval/test_irac_structure.py +169 -0
  15. fieldkit-0.4.3/tests/eval/test_judge_backed_scorers.py +233 -0
  16. fieldkit-0.4.3/tests/eval/test_mcq_letter.py +111 -0
  17. fieldkit-0.4.3/tests/eval/test_prior_art_relevance.py +137 -0
  18. {fieldkit-0.4.1 → fieldkit-0.4.3}/tests/test_publish.py +49 -0
  19. {fieldkit-0.4.1 → fieldkit-0.4.3}/tests/test_vertical_bench.py +318 -0
  20. {fieldkit-0.4.1 → fieldkit-0.4.3}/LICENSE +0 -0
  21. {fieldkit-0.4.1 → fieldkit-0.4.3}/README.md +0 -0
  22. {fieldkit-0.4.1 → fieldkit-0.4.3}/docs/api/capabilities.md +0 -0
  23. {fieldkit-0.4.1 → fieldkit-0.4.3}/docs/api/cli.md +0 -0
  24. {fieldkit-0.4.1 → fieldkit-0.4.3}/docs/api/lineage.md +0 -0
  25. {fieldkit-0.4.1 → fieldkit-0.4.3}/docs/api/nim.md +0 -0
  26. {fieldkit-0.4.1 → fieldkit-0.4.3}/docs/api/quant.md +0 -0
  27. {fieldkit-0.4.1 → fieldkit-0.4.3}/docs/api/rag.md +0 -0
  28. {fieldkit-0.4.1 → fieldkit-0.4.3}/docs/api/training.md +0 -0
  29. {fieldkit-0.4.1 → fieldkit-0.4.3}/samples/bench-rag.py +0 -0
  30. {fieldkit-0.4.1 → fieldkit-0.4.3}/samples/feasibility-math.py +0 -0
  31. {fieldkit-0.4.1 → fieldkit-0.4.3}/samples/hello-lineage.py +0 -0
  32. {fieldkit-0.4.1 → fieldkit-0.4.3}/samples/hello-nim.py +0 -0
  33. {fieldkit-0.4.1 → fieldkit-0.4.3}/samples/naive-rag.py +0 -0
  34. {fieldkit-0.4.1 → fieldkit-0.4.3}/src/fieldkit/__init__.py +0 -0
  35. {fieldkit-0.4.1 → fieldkit-0.4.3}/src/fieldkit/capabilities/__init__.py +0 -0
  36. {fieldkit-0.4.1 → fieldkit-0.4.3}/src/fieldkit/capabilities/data/__init__.py +0 -0
  37. {fieldkit-0.4.1 → fieldkit-0.4.3}/src/fieldkit/capabilities/data/spark-capabilities.json +0 -0
  38. {fieldkit-0.4.1 → fieldkit-0.4.3}/src/fieldkit/cli/__init__.py +0 -0
  39. {fieldkit-0.4.1 → fieldkit-0.4.3}/src/fieldkit/lineage/__init__.py +0 -0
  40. {fieldkit-0.4.1 → fieldkit-0.4.3}/src/fieldkit/nim/__init__.py +0 -0
  41. {fieldkit-0.4.1 → fieldkit-0.4.3}/src/fieldkit/quant/__init__.py +0 -0
  42. {fieldkit-0.4.1 → fieldkit-0.4.3}/src/fieldkit/rag/__init__.py +0 -0
  43. {fieldkit-0.4.1 → fieldkit-0.4.3}/src/fieldkit/training/__init__.py +0 -0
  44. {fieldkit-0.4.1 → fieldkit-0.4.3}/tests/__init__.py +0 -0
  45. {fieldkit-0.4.1 → fieldkit-0.4.3}/tests/conftest.py +0 -0
  46. {fieldkit-0.4.1 → fieldkit-0.4.3}/tests/test_capabilities.py +0 -0
  47. {fieldkit-0.4.1 → fieldkit-0.4.3}/tests/test_cli.py +0 -0
  48. {fieldkit-0.4.1 → fieldkit-0.4.3}/tests/test_eval.py +0 -0
  49. {fieldkit-0.4.1 → fieldkit-0.4.3}/tests/test_lineage.py +0 -0
  50. {fieldkit-0.4.1 → fieldkit-0.4.3}/tests/test_nim.py +0 -0
  51. {fieldkit-0.4.1 → fieldkit-0.4.3}/tests/test_nim_spark.py +0 -0
  52. {fieldkit-0.4.1 → fieldkit-0.4.3}/tests/test_quant.py +0 -0
  53. {fieldkit-0.4.1 → fieldkit-0.4.3}/tests/test_rag.py +0 -0
  54. {fieldkit-0.4.1 → fieldkit-0.4.3}/tests/test_rag_spark.py +0 -0
  55. {fieldkit-0.4.1 → fieldkit-0.4.3}/tests/test_training.py +0 -0
@@ -25,7 +25,8 @@ pnpm-debug.log*
25
25
  # local-only working material (not for the public blog)
26
26
  ideas/
27
27
  HANDOFF.md
28
- .claude/
28
+ .claude/*
29
+ !.claude/skills/
29
30
 
30
31
  # transient vibe-test artifacts (Playwright screenshots written to repo root)
31
32
  .playwright-mcp/
@@ -6,6 +6,62 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.3] — 2026-05-17
10
+
11
+ ### Added — `fieldkit.eval` patent-strategist scorer build-out (T6)
12
+
13
+ Four new scorers in `fieldkit.eval` round out the `format='patent-strategist'` branch landed in v0.4.2 (T4) and the `mcq_letter` promotion (T5), per `specs/patent-strategist-v1.md` §3.3:
14
+
15
+ - **`patent_claim_validity(predicted, expected, *, judge, rubric=None)`** — PatentScore-methodology 7-dim claim-validity scorer (novelty / non-obviousness / written-description / enablement / indefiniteness / subject-matter-eligibility / dependent-claim-structure). LLM-judge backed; caller supplies a `Judge(client=..., rubric=RUBRIC_PATENT_CLAIM_VALIDITY)`. Per-row `rubric` dict (e.g. `cited_prior_art`, `claim_type`) is rendered into a sorted, deterministic `Hints:` block fed to the judge as context. PatentScore methodology only — no data reuse from the cited paper (license unclear).
16
+ - **`office_action_argument(predicted, expected, *, judge, rubric=None)`** — 4-dim office-action-response scorer (rejection-type identification, statutory citation accuracy, argument structure, persuasiveness). Same `Judge`-wrapping shape; per-row hints like `rejection_type`, `required_citations`, `claim_count`, `relies_on_official_notice` flow through the `Hints:` block.
17
+ - **`irac_structure(predicted, expected="")`** — deterministic 4-checklist scorer for Patent-Bar-style IRAC responses. One regex per component (Issue / Rule / Application / Conclusion); returns `{0.0, 0.25, 0.5, 0.75, 1.0}` based on how many fire. Tolerant patterns — markdown headings, all-caps section labels, transition prose ("Whether…", "Under 35 USC 103…", "Here…", "Therefore…") all count. False positives are far less harmful than false negatives at quarter-granularity. The only T6 scorer that needs no network, so it's the one wired end-to-end through `VerticalBench` in the integration test.
18
+ - **`prior_art_relevance(predicted, expected) -> float`** — Spearman ρ on ranked prior-art lists, returning just the rho per spec §3.3. Tolerant parser accepts JSON arrays (`'["a","b","c"]'`), comma-separated, or newline-separated (with `1.`, `1)`, `- `, `* ` prefixes stripped) as well as `list[str]` directly. Missing-from-pred gold items get worst-rank padding so omissions still penalize. The paired-rank vectors are re-rankified before correlation so positional gaps from dup-skipping or padding collapse to contiguous ranks — without this, `["a","a","b","c"]` vs `["a","b","c"]` would yield ρ≈0.98 instead of the intuitive 1.0. **`prior_art_relevance_full`** returns the same rho plus an `mse_likert` field (populated only when both sides parse as numeric Likert vectors) and `n`, packaged as the frozen `PriorArtRelevanceResult` dataclass.
19
+
20
+ ### Added — rubric markdown bundled in the wheel
21
+
22
+ - **`fieldkit/src/fieldkit/eval/rubrics/{patent_claim_validity,office_action_argument}.md`** — system-prompt markdown shipped alongside the module. Loaded lazily via the new **`load_rubric(name)`** helper (and exposed via the **`RUBRIC_PATENT_CLAIM_VALIDITY`** / **`RUBRIC_OFFICE_ACTION_ARGUMENT`** module constants for the common case). `[tool.hatch.build.targets.wheel].include` extended with `src/fieldkit/eval/rubrics/*.md` so the markdown lands in the wheel.
23
+
24
+ ### Added — `fieldkit.eval.vertical` live-callable dispatch
25
+
26
+ - **`PATENT_STRATEGIST_SCORER_FNS: dict[str, Callable[..., float]]`** — companion to the existing string-keyed `PATENT_STRATEGIST_SCORERS` map. Resolves the four T6 scorers + the promoted `mcq_letter` to live functions (skips the two `judge_rubric` slots ("C", "E") which are open-ended `Judge.grade(...)` calls without a single named scorer fn). Drift-detection test asserts every fn's `__name__` matches the matching string-map entry.
27
+
28
+ ### Test suite
29
+
30
+ **+93 new tests** across three new test files + the existing vertical-bench test class:
31
+
32
+ - `tests/eval/test_irac_structure.py` — perfect / partial / per-component-detector coverage; quarter-granularity parametrize; whitespace-only / empty / expected-arg-ignored edges.
33
+ - `tests/eval/test_prior_art_relevance.py` — perfect / reversed / partial-overlap; string-parsing variants (JSON, comma, newline-numbered, bullet, paren-numbered); Likert MSE branch (perfect, off-by-one, length-mismatch fallback, non-numeric); dataclass shape (frozen, three fields); the known-value `n=4` swap (ρ=0.8) plus the dup-skip test that drove the `_rankify`-on-paired-vectors fix.
34
+ - `tests/eval/test_judge_backed_scorers.py` — `load_rubric` round-trip + missing-file error; `_format_rubric_hints` (empty / scalar / list-bullet / sorted-determinism / nested-dict JSON); both judge-backed scorers wired against a `_FakeJudge` fixture (no network) covering happy path, `None`-score fallback to `0.0`, rubric→`Hints:` threading, empty-reference collapse to `None`; signature-introspection tests ensuring `judge` and `rubric` stay keyword-only so `VerticalBench.scorer_kwargs` plumbing works.
35
+ - `tests/test_vertical_bench.py::TestPatentStrategistFormat` — 3 new tests: `PATENT_STRATEGIST_SCORER_FNS` resolves each key to the expected callable; name-map vs fn-map drift assertion; full end-to-end `VerticalBench.run` exercising `irac_structure` over a 2-row JSONL with one perfect and one half-formed IRAC response (mean accuracy = 0.75).
36
+
37
+ Total suite: **507 passed, 2 skipped** offline (`pytest -q`, `/tmp/fk` venv). The 2 skips are the long-standing `--spark`-gated live-NIM / pgvector integration tests.
38
+
39
+ ### Articles in this release
40
+
41
+ - `articles/becoming-a-patent-strategist-on-spark/` — patent-strategist v1.0 article (W3 publish target per spec §1 deliverables). T6's scorer build-out is the load-bearing dependency for the article's bench-comparison numbers; v0.4.3 is the version the article will pin against.
42
+
43
+ ## [0.4.2] — 2026-05-15
44
+
45
+ Patch release. Two card-rendering polish lifts on `fieldkit.publish` driven by the 2026-05-15 cyber-vertical cycle (`Orionfold/SecurityLLM-GGUF`, the third vertical card on this surface — zero fieldkit source changes between Saul / cyber, the v0.4.1 publishing surface generalized exactly as designed). Both lifts are additive (one new `ModelCard` field already shipped on `main` in `ff1b92f`; one new `ArtifactManifest` field added here). No new modules, no new public classes, no breaking changes — purely a tightening pass.
46
+
47
+ ### Added — `fieldkit.publish` card-rendering polish
48
+
49
+ - **`ModelCard.llama_cpp_example_prompt: Optional[str]`** — new field. Threads through `publish_quant(..., llama_cpp_example_prompt=...)` and from a duck-typed report's `.llama_cpp_example_prompt` attribute. The default `## How to run` body's `llama-cpp-python` snippet now uses this string for the user-message; when omitted it falls back to a neutral `"Summarize the key idea in one paragraph."` placeholder instead of the previously-hardcoded `"Explain working capital."` (which leaked into the legal + cyber vertical cards on first push). Multi-line MCQ-shaped prompts are JSON-escaped (`\n`) so the snippet stays single-line + valid Python — caller passes the raw prompt, the renderer handles escaping.
50
+ - **Side fix:** the previous renderer rendered the hardcoded finance prompt on every vertical card; the cyber + legal cards on HF were patched out-of-band on 2026-05-15 (commits `365dfe2`, `0824439`). Going forward, every `publish_quant` call should pass `llama_cpp_example_prompt=...` matching the article's "Using this release" section, per `[[feedback_customer_link_audit]]`.
51
+ - **`ArtifactManifest.recommended_variant: Optional[str]`** — new field. Was already on `ModelCard` (so the README's How-to-run snippets template against the article's pick) but did NOT flow into the `<slug>.yaml` manifest, so the destination catalog couldn't see the article's narrative choice and ran its own rank-avg picker instead. `publish_quant` now threads `recommended_variant` into both surfaces — the HF README badge and the destination "Sweet spot" badge stay in sync from one kwarg. Mac added the matching `recommended_variant: z.string().optional()` to its artifacts schema in PR #6 (`mac-sweep/2026-05-15-cyber-vertical`) and pinned cyber's catalog `Q4_K_M` manually; source `src/content.config.ts` now mirrors that field for forward-compat. Motivated by cyber-vertical (2026-05-15): `Q4_K_M` topped CyberMetric at 40% but its worst-in-class perplexity dragged its rank-avg down, so without the override the picker selected `Q5_K_M`.
52
+
53
+ ### Test suite
54
+
55
+ **+3 new tests:** `test_artifact_manifest_carries_recommended_variant_when_set` + `test_artifact_manifest_omits_recommended_variant_when_unset` (round-trip + elision on the new manifest field) and `test_publish_quant_threads_recommended_variant_into_card_and_manifest` (kwarg threads to both surfaces via `publish_quant`). Total: **378 passed, 3 skipped** offline (`pytest -q`). The 3 skips are the two `--spark`-gated live-integration tests + the `torch`-import skip in `test_training.py` (CPU-only venv).
56
+
57
+ ### Articles in this release
58
+
59
+ - [`becoming-a-cyber-curator-on-spark`](https://ainative.business/field-notes/becoming-a-cyber-curator-on-spark/) — third Orionfold quant card. Drives both lifts: surfaces the `llama_cpp_example_prompt` leak (cyber's MCQ prompt would have shipped as "Explain working capital." otherwise) and motivates `ArtifactManifest.recommended_variant` (the destination's rank-avg picker would have surfaced `Q5_K_M` instead of `Q4_K_M`).
60
+
61
+ ### Verified on Spark
62
+
63
+ - **Live HF push:** `Orionfold/SecurityLLM-GGUF` (5 GGUF variants + README, ~26 GB) shipped 2026-05-15 via the same `publish_quant(dry_run=False)` path as Saul and finance-chat. Zero source changes in `fieldkit.publish` between Saul (v0.4.1) and cyber (the cycle that drove this v0.4.2 patch) — the surface generalized as designed across three verticals.
64
+
9
65
  ## [0.4.1] — 2026-05-14
10
66
 
11
67
  Patch release. The `fieldkit.eval.VerticalBench` overlay introduced in v0.4.0 needed two kwargs to score FinanceBench correctly (open-book context-prepend) and to bound a JSONL slice (subset filter on `question_type`). Both lifts came out of the 2026-05-13 V1 attempt on `AdaptLLM/finance-chat` (0/50 closed-book vs. 14–18%/50 open-book on the same JSONL) and the 2026-05-14 legal-curator scoring run on `Equall/Saul-7B-Instruct-v1`. The two scripts under `scripts/g3_*` that carried duplicated loaders now call into the package surface. No new modules, no new public classes — additive kwargs only.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fieldkit
3
- Version: 0.4.1
3
+ Version: 0.4.3
4
4
  Summary: Verified-on-Spark patterns lifted from the ai-field-notes blog into one importable Python package.
5
5
  Project-URL: Homepage, https://ainative.business/fieldkit/
6
6
  Project-URL: Source, https://github.com/manavsehgal/ai-field-notes/tree/main/fieldkit
@@ -11,7 +11,7 @@ The eval harnesses the project keeps reinventing: a per-call latency benchmarker
11
11
 
12
12
  **v0.4.x additions** (vertical-curator surface for the G3 GGUF publisher pipeline):
13
13
 
14
- - `VerticalBench` — Spark-overlay scorer for FinanceBench / LegalBench / SemEval-style JSONL test sets. Wraps `Bench`, so latency aggregates alongside accuracy and refusal. Network access lives in the caller (`llama-cli`, NIM, vLLM) — the bench itself is offline-only and unit-testable.
14
+ - `VerticalBench` — Spark-overlay scorer for FinanceBench / LegalBench / SemEval-style JSONL test sets. Wraps `Bench`, so latency aggregates alongside accuracy and refusal. Network access lives in the caller (`llama-cli`, NIM, vLLM) — the bench itself is offline-only and unit-testable. **v0.4.1 lift:** `from_jsonl(..., open_book=…, subset=…)` — open-book mode prepends FinanceBench evidence text to the question (default-on for `financebench`, default-off elsewhere); `subset` filters FinanceBench rows by `question_type` before the `limit` cap.
15
15
  - `VerticalQA` — one test case (qid + question + expected + tags) lifted from a vertical-eval JSONL.
16
16
  - `exact_match` / `contains` / `numeric_match` — the three built-in scorers. `numeric_match` is the FinanceBench default (first-number ±1% rel-tol); `exact_match` is the LegalBench default; `contains` is the right pick when the model answers in prose around a key fact.
17
17
 
@@ -54,6 +54,14 @@ from fieldkit.eval import (
54
54
  # v0.4.x — vertical-curator surface
55
55
  VerticalBench, VerticalQA,
56
56
  contains, exact_match, numeric_match,
57
+
58
+ # v0.4.3 — patent-strategist scorers
59
+ mcq_letter,
60
+ irac_structure,
61
+ prior_art_relevance, prior_art_relevance_full, PriorArtRelevanceResult,
62
+ patent_claim_validity, office_action_argument,
63
+ RUBRIC_PATENT_CLAIM_VALIDITY, RUBRIC_OFFICE_ACTION_ARGUMENT,
64
+ load_rubric,
57
65
  )
58
66
  ```
59
67
 
@@ -241,6 +249,8 @@ vb = VerticalBench.from_jsonl(
241
249
  "financebench.jsonl",
242
250
  scorer=numeric_match, # FinanceBench → first-number ±1%
243
251
  limit=50,
252
+ subset="metrics-generated", # v0.4.1 — filter question_type before limit
253
+ open_book=True, # v0.4.1 — prepend evidence_text to the question
244
254
  )
245
255
 
246
256
  def model_fn(prompt: str) -> str:
@@ -250,7 +260,10 @@ bench = vb.run(model_fn, extra_tags={"variant": "Q4_K_M"})
250
260
  print(bench.report()) # accuracy + refusal_rate + latency
251
261
  ```
252
262
 
253
- `VerticalBench.from_jsonl(path, *, format="auto", limit=None, scorer=None, scorer_kwargs=None)` auto-sniffs FinanceBench / LegalBench / generic schemas from the first JSON row. Rows missing the question or expected field are silently dropped (the row-count delta vs the JSONL is the diagnostic). The default scorer is `numeric_match` for FinanceBench and `exact_match` everywhere else; pass `scorer=` to override.
263
+ `VerticalBench.from_jsonl(path, *, format="auto", limit=None, scorer=None, scorer_kwargs=None, open_book=None, subset=None)` auto-sniffs FinanceBench / LegalBench / generic schemas from the first JSON row. Rows missing the question or expected field are silently dropped (the row-count delta vs the JSONL is the diagnostic). The default scorer is `numeric_match` for FinanceBench and `exact_match` everywhere else; pass `scorer=` to override.
264
+
265
+ - **`open_book=` *(v0.4.1)*** — when `True`, FinanceBench rows have their `evidence[*].evidence_text` prepended to the question (templated as `Context from <doc>: …\n\nQuestion: …\n\nAnswer with just the numeric value.`) so the model sees the 10-K excerpt the gold answer was derived from. Default `None` auto-resolves to `True` for `financebench` and `False` for `legalbench` / `generic` — the right defaults per benchmark convention. The 2026-05-13 V1 attempt on `AdaptLLM/finance-chat` scored 0/50 closed-book and 14–18%/50 open-book on the same JSONL; open-book is the load-bearing flag for FinanceBench scoring. Lifted from inline helpers in `scripts/g3_preflight_bench.py` and `scripts/g3_measure_variants.py` into the package surface.
266
+ - **`subset=` *(v0.4.1)*** — FinanceBench-only convenience filter on the `question_type` column. Drops non-matching rows *before* the loader hits the `limit` cap, so callers can score the `metrics-generated` subset with `limit=50` and get 50 metrics-generated questions (not 50 mixed rows of which N happen to be metrics-generated). No-op on `legalbench` / `generic` formats.
254
267
 
255
268
  `VerticalBench.run(model_fn, *, limit=None, on_error="record", extra_tags=None)` returns the underlying `Bench` so callers route through the existing `.summary()` / `.report()` / `.dump()` pipeline. Each `BenchCall` carries `accuracy` (0.0/1.0 from the scorer) and `refusal` (0.0/1.0 from `is_refusal`) metrics; per-row metadata (company, doc_period, question_type) flows through to `BenchCall.tags` for downstream slice-by aggregation.
256
269
 
@@ -287,6 +300,52 @@ numeric_match("Revenue was $4.55B", "4.5B",
287
300
  | `contains(p, e)` | The model is asked to answer in prose and the reference is a key fact/number/phrase that must appear somewhere in the answer. |
288
301
  | `numeric_match(p, e, *, rel_tolerance=0.01)` | FinanceBench-style quantitative answers. Extracts the first number from each side (commas stripped), compares under relative tolerance. Defaults to ±1% per FinanceBench's grading convention. Returns 0.0 if either side has no parseable number — including refusals, so the refusal counter elsewhere doesn't need to gate this scorer. |
289
302
 
303
+ ### Patent-strategist scorers *(v0.4.3)*
304
+
305
+ Five scorers + two rubric constants land in v0.4.3 to round out the `format='patent-strategist'` branch of `VerticalBench`. Wire them through `VerticalBench(scorer=…, scorer_kwargs=…)` or import the live-callable dispatch map at `fieldkit.eval.vertical.PATENT_STRATEGIST_SCORER_FNS`. The 1-paragraph-per-scorer cheat sheet:
306
+
307
+ #### `mcq_letter(predicted, expected, *, strip_think=True) -> float`
308
+
309
+ MCQ letter scorer promoted from `scripts/g3_*.py` after three vertical-bench reuses (cybermetric, medmcqa, patent-strategist). Decision order: stripped one-letter (`"B"`), then `"answer: X"` / `"answer is X"` / `"option X"` / `"choice X"`, then first word-bounded `[A-D]`. Case-insensitive throughout. When `strip_think=True` (default), `<think>...</think>` blocks are regex-stripped *before* the three-step decision — keeps reasoning-trace verbosity on R1-distill family models from polluting the letter pick. The flag is a no-op regex on cyber/medical text without `<think>` tags, so existing callers flip the default on safely.
310
+
311
+ #### `irac_structure(predicted, expected="") -> float`
312
+
313
+ Deterministic 4-checklist Patent-Bar IRAC detector. Returns one of `{0.0, 0.25, 0.5, 0.75, 1.0}` based on Issue / Rule / Application / Conclusion regex hits. Tolerant patterns: markdown headings, all-caps section labels, transition prose (`"Whether…"`, `"Under 35 USC 103…"`, `"Here…"`, `"Therefore…"`) all count. `expected` is ignored — the scorer measures structural form, not factual agreement; kept in the signature for `VerticalBench` compatibility. False positives are far less harmful than false negatives at this granularity; the score's job is to flag *structural absence*, not grade rhetorical polish.
314
+
315
+ #### `prior_art_relevance(predicted, expected) -> float`
316
+
317
+ Spearman ρ between predicted and gold prior-art rankings — the bench-facing scalar per `specs/patent-strategist-v1.md` §3.3. Accepts `list[str]` directly or a tolerant string parse (JSON arrays `'["a","b","c"]'`, comma-separated `"a, b, c"`, or newline-separated with `1.` / `1)` / `- ` / `* ` prefixes stripped). Items missing from `predicted` get worst-rank padding so omissions still penalize. The paired-rank vectors get re-rankified before correlation so positional gaps from dup-skipping or padding collapse to contiguous ranks — without this, `["a","a","b","c"]` vs `["a","b","c"]` would yield ρ≈0.98 instead of 1.0.
318
+
319
+ #### `prior_art_relevance_full(predicted, expected) -> PriorArtRelevanceResult`
320
+
321
+ Returns the same ρ plus an `mse_likert` field (populated only when both sides parse as numeric Likert vectors, e.g. `"5,4,3,2,1"`) and an `n` count, packaged as a frozen `PriorArtRelevanceResult(spearman_rho, mse_likert, n)` dataclass. The bench surface uses `prior_art_relevance` because the scorer contract is `Callable[..., float]`; this full variant is for callers that want both metrics in a single pass.
322
+
323
+ #### `patent_claim_validity(predicted, expected, *, judge, rubric=None) -> float`
324
+
325
+ PatentScore-methodology 7-dim claim-validity scorer (novelty / non-obviousness / written-description / enablement / indefiniteness / subject-matter-eligibility / dependent-claim-structure). LLM-judge backed; caller supplies a `Judge` instance constructed with `rubric=RUBRIC_PATENT_CLAIM_VALIDITY`. Per-row `rubric` dict (convention keys: `cited_prior_art`, `claim_type`, `dependency_target`, `statutory_focus`) renders into a deterministic sorted `Hints:` block fed to the judge as context. Returns the parsed score, mapping `None` → `0.0` so bench accuracy-averaging stays well-defined. **PatentScore methodology only — no data reuse from the cited paper** (license unclear).
326
+
327
+ ```python
328
+ from fieldkit.eval import Judge, RUBRIC_PATENT_CLAIM_VALIDITY, patent_claim_validity
329
+ from fieldkit.nim import NIMClient
330
+
331
+ with NIMClient(base_url="http://localhost:8000/v1", model="...") as c:
332
+ judge = Judge(client=c, rubric=RUBRIC_PATENT_CLAIM_VALIDITY)
333
+ score = patent_claim_validity(
334
+ predicted_claim_text,
335
+ reference_claim_text,
336
+ judge=judge,
337
+ rubric={"cited_prior_art": ["US10987654", "US20210123456"]},
338
+ )
339
+ ```
340
+
341
+ #### `office_action_argument(predicted, expected, *, judge, rubric=None) -> float`
342
+
343
+ 4-dim office-action-response scorer (rejection-type identification, statutory citation accuracy, argument structure, persuasiveness). Same `Judge`-wrapping shape as `patent_claim_validity`; pair with `RUBRIC_OFFICE_ACTION_ARGUMENT`. Convention rubric keys: `rejection_type` (`102` / `103` / `112(a)` / `112(b)` / `101` / `double-patenting` / `restriction`), `required_citations` (list of expected MPEP/CFR/case cites), `claim_count`, `relies_on_official_notice`.
344
+
345
+ #### Rubric loader: `load_rubric(name) -> str`
346
+
347
+ The two `RUBRIC_PATENT_CLAIM_VALIDITY` and `RUBRIC_OFFICE_ACTION_ARGUMENT` module constants are populated at import time from markdown files shipped under `fieldkit/eval/rubrics/`. Pass `load_rubric("patent_claim_validity")` to re-read the file (or your own rubric named `my_rubric.md` if you ship a fork). The `[tool.hatch.build.targets.wheel].include` glob ships `*.md` under that subtree, so the rubrics travel with the wheel.
348
+
290
349
  ## Samples
291
350
 
292
351
  - [`samples/bench-rag.py`](https://github.com/manavsehgal/ai-field-notes/blob/main/fieldkit/samples/bench-rag.py) — offline `Bench` + `Judge.parse` walkthrough.
@@ -70,6 +70,7 @@ ModelCard(
70
70
  hf_repo="Orionfold/finance-chat-GGUF", # drives default `## How to run` body
71
71
  chat_format="llama-2", # → llama_cpp.Llama(chat_format=...)
72
72
  recommended_variant="Q5_K_M", # featured in default snippets
73
+ llama_cpp_example_prompt="Explain working capital.", # user-message in the default `llama-cpp-python` snippet; falls back to a neutral placeholder when omitted
73
74
  ollama_pull_handle=None, # opt-in override; default body wins otherwise
74
75
  transformers_snippet=None,
75
76
  lineage_prompt=None, # injected by publish_quant if a LineageStore is supplied
@@ -98,6 +99,7 @@ m = ArtifactManifest(
98
99
  sustained_load_minutes=2.18,
99
100
  vertical_eval={"Q4_K_M": 0.14, ...},
100
101
  vertical_eval_name="FinanceBench (n=50, numeric_match)",
102
+ recommended_variant="Q5_K_M", # article-narrative pick; destination pins the "Sweet spot" badge to this variant
101
103
  lineage_run_id=None,
102
104
  license_tier="free", # Orionfold commercial tier (free / pro)
103
105
  license_commercial_tier=None,
@@ -112,6 +114,8 @@ print(m.to_yaml())
112
114
 
113
115
  The `license_tier` / `license_commercial_tier` fields live alongside `model_license` under a nested `license:` block in YAML output. Mac destination's Zod schema mirrors this shape.
114
116
 
117
+ `recommended_variant` (v0.4.2+) lets the article's narrative pick — the variant the writeup recommends — override the destination's rank-avg picker. Cyber's `Q4_K_M` topped CyberMetric but its worst-in-class perplexity dragged its rank-avg down, so without this field the catalog page would pin `Q5_K_M` as the "Sweet spot" instead. Same value flows into both `ModelCard` (HF README's How-to-run snippets) and `ArtifactManifest` (destination catalog) so the badge and the snippet stay in sync.
118
+
115
119
  ### `write_artifact_manifest(manifest, *, artifacts_dir)`
116
120
 
117
121
  Writes the manifest to `<artifacts_dir>/<slug>.yaml`. Creates the directory if missing. Returns the absolute path of the written file — callers can stage it alongside the article for the next git commit.
@@ -134,7 +138,7 @@ Token resolution order: explicit `token=` arg → `HF_TOKEN` env → `HUGGING_FA
134
138
 
135
139
  ### `publish_quant(*, quant_report, base_model, repo_name, staging_dir, ...) → PublishResult`
136
140
 
137
- The one-line orchestrator. Reads the duck-typed `quant_report` fields (`.format`, `.variants`, `.perplexity`, `.tokens_per_sec`, `.sustained_load_minutes`, `.variant_files`, `.vertical_eval`, `.vertical_eval_name`, `.model_license`, `.chat_format`, `.recommended_variant`), builds a `ModelCard`, stages the README + variant files, writes the `ArtifactManifest` (if `artifacts_dir` supplied), and invokes `HFHubAdapter.push_folder()`. Explicit kwargs override duck-typed report attrs.
141
+ The one-line orchestrator. Reads the duck-typed `quant_report` fields (`.format`, `.variants`, `.perplexity`, `.tokens_per_sec`, `.sustained_load_minutes`, `.variant_files`, `.vertical_eval`, `.vertical_eval_name`, `.model_license`, `.chat_format`, `.recommended_variant`, `.llama_cpp_example_prompt`), builds a `ModelCard`, stages the README + variant files, writes the `ArtifactManifest` (if `artifacts_dir` supplied), and invokes `HFHubAdapter.push_folder()`. Explicit kwargs override duck-typed report attrs.
138
142
 
139
143
  ```python
140
144
  result = publish_quant(
@@ -150,6 +154,7 @@ result = publish_quant(
150
154
  model_license="llama2", # critical — never default silently to apache-2.0
151
155
  chat_format="llama-2",
152
156
  recommended_variant="Q5_K_M",
157
+ llama_cpp_example_prompt="Explain working capital.", # mirror the article's example user-message
153
158
  lineage_store=store, # optional; injects ## Lineage block
154
159
  dry_run=True, # flip to False for the actual push
155
160
  )
@@ -59,6 +59,7 @@ packages = ["src/fieldkit"]
59
59
  include = [
60
60
  "src/fieldkit/**/*.py",
61
61
  "src/fieldkit/**/data/*.json",
62
+ "src/fieldkit/eval/rubrics/*.md",
62
63
  ]
63
64
 
64
65
  [tool.hatch.build.targets.sdist]
@@ -6,4 +6,4 @@
6
6
  build time, so bumping it here is enough to bump the wheel version too.
7
7
  """
8
8
 
9
- __version__ = "0.4.1"
9
+ __version__ = "0.4.3"