Skip to content

feat: refine — graded work items to training data with provenance (#14)#15

Open
OriNachum wants to merge 6 commits into
mainfrom
spec/291-integration
Open

feat: refine — graded work items to training data with provenance (#14)#15
OriNachum wants to merge 6 commits into
mainfrom
spec/291-integration

Conversation

@OriNachum

Copy link
Copy Markdown
Contributor

Summary

Lands refine dataset and refine lineage (issue #14, colleague#291 slice S6): a pure-function pipeline (data_refinery/refine/dataset.py) that turns a colleague feedback export JSONL stream — one graded work item per line — into a sloth-ready chat-schema train/eval split plus a provenance lineage.json. Each stage is independently testable: shape-validate an export line (a malformed line is skipped, never silently dropped — one diagnostic per bad line, plus a counted total), filter to status == "ok" and a graded rating at/above --min-rating, map to the sloth chat schema, dedup same-content examples by reusing the existing find_duplicate_groups same-hash-same-scope primitive, and split train/eval via a deterministic seeded shuffle so the two sets are disjoint by construction. Every emitted example carries its per-example provenance (source task id, rating, exported-at timestamp).

This closes the loop between colleague's ROI feedback store and unsloth-cli's fine-tuning surface: a graded work item can now become a training example without hand-rolled glue scripts. docs/contract.md gains Wave 4 documenting the refine consumer contract (agent-first: --json on every verb, results to stdout/diagnostics to stderr, exit 1 on a missing --from/--dataset path with a hint:), and explain/overview/learn gain matching catalog entries.

Test plan

  • uv run pytest -n auto -q — 205 passed, 4 skipped (live-stack tests gated behind DATA_REFINERY_LIVE=1)
  • uv run teken cli doctor . --strict — 26/26 checks pass

Stdlib-only, no network, no LLM call throughout the new pipeline — no known deviations or honest limits beyond what's already documented in the shipped code/tests.

🤖 Generated with Claude Code

  • colleague (Claude)

OriNachum and others added 2 commits July 6, 2026 21:38
…ady train/eval JSONL with provenance (#14)

Co-Authored-By: Claude Fable 5 <[email protected]>
)

Record the CHANGELOG entry for the S6 integration slice: `refine dataset`
(graded colleague work items -> sloth chat-schema train/eval JSONL with
provenance) and `refine lineage`, plus docs/contract.md Wave 4.
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add refine dataset/lineage to build sloth-ready train/eval data with provenance

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add data-refinery refine dataset/lineage to convert graded colleague exports into train/eval
 JSONL.
• Emit lineage.json with per-example provenance plus aggregate counts and file hashes.
• Document the stable Wave 4 consumer contract and add full unit/CLI coverage.
Diagram

graph TD
  A(["data-refinery CLI"]) --> B[["refine CLI group"]] --> C[["refine.dataset pipeline"]] --> D[["dedup: find_duplicate_groups"]] --> E[("--out dataset dir")]
  E --> F["train.jsonl"] --> G["lineage.json"]
  E --> H["eval.jsonl"]
  subgraph Legend
    direction LR
    _cli(["CLI entry"]) ~~~ _mod[["Module/flow"]] ~~~ _dir[("Directory")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Stream JSONL processing (line-by-line) instead of read_text()
  • ➕ Handles arbitrarily large exports without loading the full file into memory
  • ➕ Enables earlier progress/reporting and potentially faster time-to-first-output
  • ➖ Makes deterministic shuffling/splitting harder without buffering items anyway
  • ➖ More complex control flow if you still need dedup + split + lineage aggregates
2. Deterministic split by content hash (no shuffle)
  • ➕ Order-independent and stable even if input line ordering changes
  • ➕ Can be implemented in a streaming-friendly way (bucket by hash prefix)
  • ➖ Harder to reason about exact A/B sizes (may drift from requested ratio)
  • ➖ Potential distribution bias if hashes correlate with content patterns
3. Schema validation via a typed model (e.g., pydantic/dataclasses + validators)
  • ➕ Less hand-rolled validation logic; clearer error reporting for complex shapes
  • ➕ Easier to evolve schema while keeping strictness explicit
  • ➖ Adds a third-party dependency, conflicting with the PR's stdlib-only constraint
  • ➖ Potentially slower and heavier for a CLI pipeline

Recommendation: Keep the current stdlib-only, pure-function pipeline + deterministic seeded shuffle: it matches the documented consumer contract, is easy to unit test stage-by-stage, and cleanly reuses existing dedup primitives. If exports are expected to grow very large, consider a follow-up to stream parsing and/or adopt a hash-based split to reduce memory pressure while preserving reproducibility guarantees.

Files changed (12) +1536 / -3

Enhancement (4) +756 / -0
__init__.pyRegister the new 'refine' noun group in the CLI +2/-0

Register the new 'refine' noun group in the CLI

• Imports and registers the refine command group so 'data-refinery refine ...' becomes available alongside existing nouns.

data_refinery/cli/init.py

refine.pyAdd 'refine dataset', 'refine lineage', and 'refine overview' CLI verbs +212/-0

Add 'refine dataset', 'refine lineage', and 'refine overview' CLI verbs

• Introduces the refine noun group with agent-first conventions ('--json', stdout results, stderr diagnostics). Implements argument parsing, clean user errors for missing paths, dataset orchestration, and lineage read-back with both text and JSON output modes.

data_refinery/cli/_commands/refine.py

__init__.pyIntroduce refine package API surface +42/-0

Introduce refine package API surface

• Creates a new 'data_refinery.refine' package that re-exports the dataset pipeline entry points and key pure functions/types for library use.

data_refinery/refine/init.py

dataset.pyImplement the refine dataset pipeline + lineage read/write +500/-0

Implement the refine dataset pipeline + lineage read/write

• Adds a pure-function pipeline to validate and parse colleague export JSONL, filter by status and min rating, map to sloth chat schema, dedup via existing duplicate-grouping primitives, deterministically split train/eval with a seeded shuffle, and write train/eval JSONL plus a provenance-rich lineage.json. Includes atomic writes, file-hash recording, and a 'read_lineage' helper with clean error signaling.

data_refinery/refine/dataset.py

Tests (2) +537 / -0
test_refine_dataset.pyAdd unit + CLI tests for refine dataset pipeline +404/-0

Add unit + CLI tests for refine dataset pipeline

• Adds comprehensive tests covering shape validation (including null ratings and stats), malformed-line diagnostics and counting, filtering semantics, dedup behavior, disjoint deterministic splitting, '--split' parsing, schema errors, and end-to-end CLI wiring/help/overview/explain integration.

tests/test_refine_dataset.py

test_refine_lineage.pyAdd tests for refine lineage read-back and error modes +133/-0

Add tests for refine lineage read-back and error modes

• Covers library and CLI round-trip of lineage.json, grade distribution semantics, text-mode output fields, sha256 integrity checks against emitted files, and clean exit behavior for missing/corrupt lineage files.

tests/test_refine_lineage.py

Documentation (5) +242 / -2
CHANGELOG.mdDocument v0.10.0 refine dataset/lineage release notes +28/-0

Document v0.10.0 refine dataset/lineage release notes

• Adds a 0.10.0 changelog entry describing the new refine dataset pipeline, lineage inspection verb, and the agent-first contract expectations. Calls out stdlib-only/no-network/no-LLM constraints.

CHANGELOG.md

learn.pyExpose refine verbs in 'learn' catalog output +10/-0

Expose refine verbs in 'learn' catalog output

• Updates the human help text and the machine-readable JSON payload to include 'refine dataset' and 'refine lineage' entries.

data_refinery/cli/_commands/learn.py

overview.pyAdd refine to top-level overview command list +1/-0

Add refine to top-level overview command list

• Extends the overview summary list to mention 'refine dataset|lineage' as part of the supported CLI surface.

data_refinery/cli/_commands/overview.py

catalog.pyAdd 'explain' documentation entry for refine +69/-0

Add 'explain' documentation entry for refine

• Adds a dedicated explain entry describing input shape, dataset generation rules (filter/map/dedup/split), and lineage semantics and failure modes. Registers refine paths in the explain catalog routing.

data_refinery/explain/catalog.py

contract.mdBump consumer contract to Wave 4 and specify refine invariants +134/-2

Bump consumer contract to Wave 4 and specify refine invariants

• Updates the contract version to 4 and documents the refine surface: input line schema, filtering/dedup/split rules, determinism and disjointness invariants, lineage.json structure, and explicit limits (e.g., chat-only schema and small-split behavior).

docs/contract.md

Other (1) +1 / -1
pyproject.tomlBump package version to 0.10.0 +1/-1

Bump package version to 0.10.0

• Updates the project version from 0.8.0 to 0.10.0 to reflect the new Wave 4 refine surface and associated contract change.

pyproject.toml

@qodo-code-review

qodo-code-review Bot commented Jul 6, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📎 Requirement gaps (1) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 25 rules

Grey Divider


Action required

1. Examples missing provenance metadata 📎 Requirement gap ≡ Correctness
Description
Emitted train.jsonl/eval.jsonl examples contain only messages and do not attach provenance
using the Envelope metadata shape. This fails the requirement that each emitted example carry
per-example provenance fields.
Code

data_refinery/refine/dataset.py[R173-187]

+def to_example(record: dict[str, Any]) -> dict[str, Any]:
+    """Map one kept record to the sloth **chat** schema.
+
+    Shape matches ``sloth.tune.datasets._validate_chat_record`` exactly: the
+    record has **only** the ``"messages"`` key (an extra key fails sloth's
+    ``extra = set(record.keys()) - CHAT_KEYS`` check), a non-empty list of
+    ``{"role", "content"}`` messages, roles ``"user"`` then ``"assistant"``.
+    """
+    return {
+        "messages": [
+            {"role": "user", "content": record["request"]},
+            {"role": "assistant", "content": record["summary"]},
+        ]
+    }
+
Relevance

⭐⭐ Medium

No historical evidence enforcing Envelope-style per-example metadata in output JSONL; provenance
patterns not previously reviewed.

PR-#5

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 1217785 requires every emitted example to include provenance fields stored in/compatible with
the Envelope metadata shape. The PR’s to_example() returns only a messages object, so emitted
JSONL examples do not contain any provenance metadata.

Attach per-example provenance metadata using the Envelope metadata shape
data_refinery/refine/dataset.py[173-187]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Each emitted training example must carry provenance fields (`source`, `task_id`, `rating`, `content_sha256`, `exported_at`) stored in/compatible with the Envelope `metadata` shape. The current `to_example()` output omits provenance entirely.

## Issue Context
Right now provenance is only written to `lineage.json`, but the compliance requirement is per-example provenance on the emitted examples themselves using the Envelope metadata shape.

## Fix Focus Areas
- data_refinery/refine/dataset.py[173-187]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. refine JSON emits diagnostics ✗ Dismissed 📘 Rule violation ◔ Observability
Description
In --json mode, refine dataset still emits plain-text diagnostics to stderr for malformed lines,
rather than single-line JSON objects. This violates the JSON-mode contract and makes stderr
non-machine-parseable.
Code

data_refinery/cli/_commands/refine.py[R56-58]

+    for diag in result["diagnostics"]:
+        emit_diagnostic(diag)
+
Relevance

⭐⭐ Medium

No historical evidence on JSON-mode diagnostic serialization; prior reviews don’t mention
emit_diagnostic behavior.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 1077535 requires that in JSON mode, each diagnostic/error/result be serialized as a single-line
JSON object per message, without mixing human text. The new command unconditionally calls
emit_diagnostic(diag) even when --json is enabled.

Rule 1077535: Emit single-line JSON objects per message in JSON mode
data_refinery/cli/_commands/refine.py[56-58]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When `--json` is enabled, diagnostics must be emitted as single-line JSON objects (one per message) to stderr. The new `refine dataset` command currently prints plain text diagnostics even in JSON mode.

## Issue Context
`emit_diagnostic()` currently writes raw strings. For JSON mode, introduce a structured diagnostic emitter (or extend `emit_diagnostic` to accept `json_mode`) that outputs e.g. `{ "type": "diagnostic", "message": ... }` as a single JSON object per line on stderr.

## Fix Focus Areas
- data_refinery/cli/_commands/refine.py[56-58]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Changelog uses ### Notes ✓ Resolved 📜 Skill insight ✧ Quality
Description
The new CHANGELOG entry introduces a ### Notes section, which is not an allowed Keep-a-Changelog
subsection heading under this repo’s policy. This can break tooling or audits that expect only `###
Added/### Changed/### Fixed` sections.
Code

CHANGELOG.md[R31-34]

+### Notes
+
+- Stdlib-only, no network, no LLM call — the pipeline is unit-testable stage
+  by stage.
Relevance

⭐⭐ Medium

Past CHANGELOG entries use only ### Added/Changed (PRs #10,#11); no prior evidence rejecting extra
headings.

PR-#10
PR-#11

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 1217552 restricts changelog subsection headings to ### Added, ### Changed,
and/or ### Fixed. The PR adds a ### Notes heading in the new 0.10.0 entry.

CHANGELOG.md[31-34]
Skill: version-bump

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`CHANGELOG.md` adds a `### Notes` subsection, but policy only permits `### Added`, `### Changed`, and/or `### Fixed`.

## Issue Context
Compliance requires Keep-a-Changelog-compatible subsection headings.

## Fix Focus Areas
- CHANGELOG.md[31-34]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. UnicodeDecodeError misclassified ✓ Resolved 🐞 Bug ☼ Reliability
Description
parse_export_jsonl() and read_lineage() only catch OSError around read_text(encoding="utf-8"), so a
UnicodeDecodeError (non‑UTF8/corrupt file) will bubble to main() and be wrapped as EXIT_USER_ERROR
"unexpected" instead of a structured EXIT_ENV_ERROR.
Code

data_refinery/refine/dataset.py[R115-122]

+    try:
+        text = path.read_text(encoding="utf-8")
+    except OSError as exc:
+        raise CliError(
+            code=EXIT_ENV_ERROR,
+            message=f"cannot read {path}: {exc.strerror or exc}",
+            remediation="check that the file exists and is readable",
+        ) from exc
Relevance

⭐⭐⭐ High

PR #9 accepted wrapping filesystem read errors into structured CliError(EXIT_ENV_ERROR) instead of
bubbling raw exceptions.

PR-#9

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
read_text(..., encoding='utf-8') is guarded only by except OSError, so UnicodeDecodeError
escapes these functions and is handled by _dispatch's generic except Exception, which wraps it
into a user-error CliError (exit 1).

data_refinery/refine/dataset.py[115-122]
data_refinery/refine/dataset.py[485-492]
data_refinery/cli/init.py[106-126]
data_refinery/cli/_errors.py[16-24]
PR-#9

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Path.read_text(encoding="utf-8")` can raise `UnicodeDecodeError`, but the new refine code only catches `OSError`. When that happens, the exception falls through to the top-level CLI wrapper and gets reported as an `EXIT_USER_ERROR` "unexpected" error, misclassifying a corrupt/environment input.

## Issue Context
- This affects both reading the input JSONL (`parse_export_jsonl`) and reading back `lineage.json` (`read_lineage`).
- The top-level dispatcher wraps unknown exceptions into `CliError(code=EXIT_USER_ERROR, message="unexpected: ...")`, so leaving `UnicodeDecodeError` uncaught changes exit codes and diagnostics.

## Fix Focus Areas
- data_refinery/refine/dataset.py[115-122]
- data_refinery/refine/dataset.py[485-492]
- data_refinery/cli/__init__.py[106-126]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. JSONL not streamed 🐞 Bug ➹ Performance
Description
parse_export_jsonl() reads the entire JSONL file into memory via read_text()+splitlines(), which
contradicts the documented “JSONL stream” contract and can cause unnecessary memory spikes or
MemoryError on large exports.
Code

data_refinery/refine/dataset.py[R115-127]

+    try:
+        text = path.read_text(encoding="utf-8")
+    except OSError as exc:
+        raise CliError(
+            code=EXIT_ENV_ERROR,
+            message=f"cannot read {path}: {exc.strerror or exc}",
+            remediation="check that the file exists and is readable",
+        ) from exc
+
+    valid: list[tuple[int, dict[str, Any]]] = []
+    malformed: list[dict[str, Any]] = []
+    for line_no, raw_line in enumerate(text.splitlines(), start=1):
+        line = raw_line.strip()
Relevance

⭐⭐ Medium

No historical evidence enforcing streaming JSONL; repo previously used read_text() patterns (PR #9).

PR-#9

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The implementation uses read_text() and then iterates text.splitlines(), which necessarily reads
the whole file first, while the contract describes consuming a JSONL stream (one object per line).

data_refinery/refine/dataset.py[105-127]
docs/contract.md[174-179]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`parse_export_jsonl()` currently loads the full JSONL file into memory (`read_text()` then `splitlines()`). For large exports this creates avoidable memory pressure and undermines the stream-oriented contract language.

## Issue Context
The parser can be implemented in a streaming way using `path.open(...): for line_no, raw_line in enumerate(f, start=1)` while keeping the same diagnostics and counts.

## Fix Focus Areas
- data_refinery/refine/dataset.py[105-140]
- docs/contract.md[174-179]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

6. Refine handlers lack int|None 📘 Rule violation ⌂ Architecture
Description
The new refine command handlers are annotated to return int instead of int | None as required
for CLI command handlers. This breaks the standardized handler signature contract for command
modules.
Code

data_refinery/cli/_commands/refine.py[R36-99]

+def cmd_refine_dataset(args: argparse.Namespace) -> int:
+    json_mode = bool(getattr(args, "json", False))
+    from_path = Path(args.from_path)
+    if not from_path.is_file():
+        raise CliError(
+            code=EXIT_USER_ERROR,
+            message=f"--from file not found: {from_path}",
+            remediation=(
+                "pass --from pointing at a JSONL file produced by " "'colleague feedback export'"
+            ),
+        )
+
+    result = dataset.run_refine_dataset(
+        from_path,
+        schema=args.schema,
+        min_rating=args.min_rating,
+        split=args.split,
+        seed=args.seed,
+        out_dir=Path(args.out),
+    )
+    for diag in result["diagnostics"]:
+        emit_diagnostic(diag)
+
+    payload = result["payload"]
+    if json_mode:
+        emit_result(payload, json_mode=True)
+    else:
+        agg = payload["aggregate"]
+        emit_result(
+            f"refine dataset: kept {agg['kept_count']} "
+            f"(train {agg['train_size']} / eval {agg['eval_size']}) from "
+            f"{agg['input_count']} input line(s) — {agg['malformed_count']} malformed, "
+            f"{agg['filtered_count']} filtered, {agg['duplicates_removed']} duplicate(s) "
+            f"removed\nwrote {payload['train_file']}, {payload['eval_file']}, "
+            f"{payload['lineage_file']}",
+            json_mode=False,
+        )
+    return 0
+
+
+def cmd_refine_lineage(args: argparse.Namespace) -> int:
+    json_mode = bool(getattr(args, "json", False))
+    payload = dataset.read_lineage(Path(args.dataset))
+    if json_mode:
+        emit_result(payload, json_mode=True)
+    else:
+        agg = payload.get("aggregate", {})
+        dist = agg.get("grade_distribution", {})
+        lines = [
+            f"lineage: {args.dataset}",
+            f"kept: {agg.get('kept_count')} "
+            f"(train {agg.get('train_size')} / eval {agg.get('eval_size')})",
+            f"input: {agg.get('input_count')} "
+            f"(malformed {agg.get('malformed_count')}, filtered {agg.get('filtered_count')}, "
+            f"duplicates removed {agg.get('duplicates_removed')})",
+            f"min_rating: {agg.get('min_rating')}, split: {agg.get('split')}, "
+            f"seed: {agg.get('seed')}",
+            "grade distribution: " + ", ".join(f"{k}={v}" for k, v in sorted(dist.items())),
+            f"train_sha256: {agg.get('train_sha256')}",
+            f"eval_sha256: {agg.get('eval_sha256')}",
+        ]
+        emit_result("\n".join(lines), json_mode=False)
+    return 0
+
Relevance

⭐ Low

Existing CLI handlers annotated -> int (e.g., stack commands in PR #6); suggests int|None contract
not enforced.

PR-#6

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 1077539 requires CLI command modules to expose a handler returning int | None. The new module
defines handlers cmd_refine_dataset and cmd_refine_lineage with -> int return annotations.

Rule 1077539: CLI command modules must expose register(sub) and a handler returning int | None
data_refinery/cli/_commands/refine.py[36-99]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
CLI command handlers must be typed to return `int | None`. The newly added `cmd_refine_dataset` and `cmd_refine_lineage` return annotations are `-> int`.

## Issue Context
Other parts of the CLI dispatcher allow `None` for success; the compliance rule requires the handler type signature to reflect `int | None`.

## Fix Focus Areas
- data_refinery/cli/_commands/refine.py[36-99]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread CHANGELOG.md Outdated
Comment thread data_refinery/cli/_commands/refine.py
Comment on lines +173 to +187
def to_example(record: dict[str, Any]) -> dict[str, Any]:
"""Map one kept record to the sloth **chat** schema.

Shape matches ``sloth.tune.datasets._validate_chat_record`` exactly: the
record has **only** the ``"messages"`` key (an extra key fails sloth's
``extra = set(record.keys()) - CHAT_KEYS`` check), a non-empty list of
``{"role", "content"}`` messages, roles ``"user"`` then ``"assistant"``.
"""
return {
"messages": [
{"role": "user", "content": record["request"]},
{"role": "assistant", "content": record["summary"]},
]
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

4. Examples missing provenance metadata 📎 Requirement gap ≡ Correctness

Emitted train.jsonl/eval.jsonl examples contain only messages and do not attach provenance
using the Envelope metadata shape. This fails the requirement that each emitted example carry
per-example provenance fields.
Agent Prompt
## Issue description
Each emitted training example must carry provenance fields (`source`, `task_id`, `rating`, `content_sha256`, `exported_at`) stored in/compatible with the Envelope `metadata` shape. The current `to_example()` output omits provenance entirely.

## Issue Context
Right now provenance is only written to `lineage.json`, but the compliance requirement is per-example provenance on the emitted examples themselves using the Envelope metadata shape.

## Fix Focus Areas
- data_refinery/refine/dataset.py[173-187]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushback — provenance is per-example, just not inline. Each surviving example's provenance (source, task_id, rating, content_sha256, exported_at) is written to lineage.json's examples[] array (docs/contract.md Wave 4, and asserted by test_three_line_fixture_provenance_shape). It cannot go inline in train.jsonl/eval.jsonl: sloth's chat-schema validator (_validate_chat_record) rejects any key beyond messages (extra = set(record.keys()) - CHAT_KEYS), so an inline provenance field would fail issue #14's own acceptance criterion that sloth train --dry-run accept the output verbatim. Co-locating provenance in the paired lineage file is the design, not a gap. Leaving open for your call.

  • data-refinery-cli (Claude)

Comment thread data_refinery/refine/dataset.py
Comment on lines +115 to +127
try:
text = path.read_text(encoding="utf-8")
except OSError as exc:
raise CliError(
code=EXIT_ENV_ERROR,
message=f"cannot read {path}: {exc.strerror or exc}",
remediation="check that the file exists and is readable",
) from exc

valid: list[tuple[int, dict[str, Any]]] = []
malformed: list[dict[str, Any]] = []
for line_no, raw_line in enumerate(text.splitlines(), start=1):
line = raw_line.strip()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

6. Jsonl not streamed 🐞 Bug ➹ Performance

parse_export_jsonl() reads the entire JSONL file into memory via read_text()+splitlines(), which
contradicts the documented “JSONL stream” contract and can cause unnecessary memory spikes or
MemoryError on large exports.
Agent Prompt
## Issue description
`parse_export_jsonl()` currently loads the full JSONL file into memory (`read_text()` then `splitlines()`). For large exports this creates avoidable memory pressure and undermines the stream-oriented contract language.

## Issue Context
The parser can be implemented in a streaming way using `path.open(...): for line_no, raw_line in enumerate(f, start=1)` while keeping the same diagnostics and counts.

## Fix Focus Areas
- data_refinery/refine/dataset.py[105-140]
- docs/contract.md[174-179]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushback (weak finding). The only occurrence of "stream" in contract.md describes the data shape — "a colleague feedback export JSONL stream (one graded work item per line)", i.e. one JSON object per line vs a single JSON array — not an I/O guarantee. Nothing in the contract promises incremental reads or bounded memory, so read_text() contradicts no documented behavior. Streaming is a reasonable optimization if exports ever grow large (Qodo's own summary listed it as a follow-up), but it is not a bug or contract violation for this PR. Can file a follow-up if you want it tracked. Leaving open.

  • data-refinery-cli (Claude)

Triage of the Qodo review on PR #15 — three FIXes:

- Finding 4 (Bug, High): parse_export_jsonl() and read_lineage() caught only
  OSError, so a non-UTF-8 --from/lineage.json raised an uncaught
  UnicodeDecodeError (a ValueError) that the dispatcher wrapped as exit 1
  "unexpected: file a bug" instead of a structured EXIT_ENV_ERROR. Now caught
  alongside OSError at both read sites with an exit-2 "not valid UTF-8" hint.
  Adds two regression tests (parse + read_lineage).
- SonarCloud gate (the actual merge-blocker): the "C Security Rating on New
  Code" is a single python:S2245 (weak PRNG) false positive on the seeded,
  deterministic train/eval shuffle — a non-cryptographic reproducible split.
  The existing `# nosec B311` is bandit-only; add `# NOSONAR` so SonarCloud
  stops flagging it and the New Code security rating returns to A.
- Finding 1 (Skill insight): dropped the non-standard `### Notes` changelog
  heading (Keep-a-Changelog / rule 1217552 allows Added/Changed/Fixed); the
  stdlib-only note is folded into Added, and a Fixed entry documents the above.

Pushed back (no code change; reasoning in the PR reply): #2 JSON diagnostics
(repo-wide pattern, consistent with stack.py), #3 per-example provenance (it is
in lineage.json — sloth's chat schema rejects extra keys), #5 JSONL streaming
(contract's "stream" = data shape, not an I/O promise), #6 int|None handlers
(matches 12/13 existing handlers).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KM34gJRiKcuJii66Aw4xso
@OriNachum

Copy link
Copy Markdown
Contributor Author

Triage of this review — 3 fixed, 3 pushed back, + the real CI blocker

Pushed fb6b9ce.

Fixed:

Pushed back (reasoning in each inline thread, left open for you):

Still blocking merge: this branch is merge-conflicting with main (CONFLICTING/DIRTY) — bookkeeping files plus the files.py/migrate.py/test_store_* that #13 just landed in main (this branch predates #13). That's a separate rebase of main into spec/291-integration, left untouched pending a go-ahead since it reconciles another slice of work.

  • data-refinery-cli (Claude)

Resolves the conflicts that appeared after #13 (fail-closed .gitignore) merged
to main while this branch was open:

- pyproject.toml: keep 0.10.0 (refine), ahead of main's 0.9.0 (gitignore).
- CHANGELOG.md: keep both release sections — [0.10.0] refine (incl. the UTF-8
  Fixed entry) above [0.9.0] gitignore from main.
- docs/contract.md: keep both additions — main's `### Fail-closed .gitignore
  opt-in` stays a Wave-3 subsection; the new `## Wave 4 — refine` is its own
  top-level section before Versioning policy.
- uv.lock: regenerated via `uv lock` (data-refinery-cli 0.10.0).

Brings in #13's store work (files.py O_EXCL gitignore, migrate.py,
test_store_*). Full suite green: 219 passed, 4 skipped; black/isort/flake8
clean; teken rubric 26/26.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KM34gJRiKcuJii66Aw4xso
…properties

The inline `# NOSONAR` added in fb6b9ce did not clear the finding — SonarCloud
kept flagging `random.Random(seed).shuffle(order)` (split_items) and the New
Code security rating stayed C, failing the gate. Move the waiver to a
server-side `sonar.issue.ignore.multicriteria` rule (ruleKey python:S2245,
resourceKey **/refine/dataset.py), which is honored reliably. It's a genuine
false positive: the shuffle is a deterministic, seed-reproducible train/eval
split — a non-cryptographic use of randomness. Inline comment trimmed to
`# nosec B311` (bandit) with a pointer to the Sonar waiver.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KM34gJRiKcuJii66Aw4xso
All maintainability/reliability smells flagged on PR #15 (none were gate-
blocking); behavior unchanged, full suite green (219 passed).

- S3776 (dataset.py, validate_export_line): cognitive complexity 21 -> under
  15 by extracting _required_field_errors / _at_errors / _rating_errors /
  _notes_errors (mirroring the existing _stats_errors helper). Error strings
  and check order are byte-identical.
- S7519 (dataset.py, _grade_distribution): {b: 0 for b in _RATING_BUCKETS} ->
  dict.fromkeys(_RATING_BUCKETS, 0).
- S5799 (cli/_commands/refine.py): merged two implicitly-concatenated string
  literals in a remediation message into one.
- S5958 x2 (tests/test_refine_dataset.py): pytest.raises(Exception) ->
  pytest.raises(CliError) for the parse_split_ratio failure tests (more
  specific, and a stronger assertion since parse_split_ratio raises CliError).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KM34gJRiKcuJii66Aw4xso
@OriNachum OriNachum deployed to testpypi July 6, 2026 22:28 — with GitHub Actions Active
@sonarqubecloud

sonarqubecloud Bot commented Jul 6, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant