Commit Graph

335 Commits

Author SHA1 Message Date
Shivam 4e95d4c71f
feat(echo-trap): add tokenizer-aware repetition scoring (#242)
Closes #241.

Adds opt-in tokenizer-aware n-gram path for the v0.70.0 Part F echo-trap detector. The existing whitespace `score_echo_signal` is unchanged; callers opt in via the new `score_trajectory_repetition_tokenized` / `score_echo_signal_tokenized` helpers or the `--echo-trap-tokenizer-aware` train flag.

Acceptance criterion from #241 verified: synthetic case where decoded strings differ by punctuation but token-id sequence repeats — whitespace path returns OK, tokenizer-aware path returns TRAP.
2026-05-25 19:46:51 +05:00
Alpamys 2ed7b44ade docs(v0.70.0): backfill `## All Commands` table + CONTRIBUTING utils tree
Post-release audit caught two pre-existing doc drifts that v0.70.0 should
have closed in the initial commit:

1. README.md `## All Commands` table was missing the 6 new v0.70.0 surfaces
   (`soup iterative-dpo`, `soup train --reward-hack-detector`,
   `--uld-strategy`, `--minillm-enabled`, `--rl-checkpoint-save-every-steps`,
   `--echo-trap-enabled`). Added 6 lines mirroring the v0.69.0 entry format.

2. CONTRIBUTING.md `utils/` directory tree had stale drift from v0.66.0 →
   v0.70.0 (24 modules missing across 5 releases). Brought in sync:
   v0.66 (sae_diff, sleeper_probe, interference, probe_pack), v0.67
   (cmaes_merge, vector_bank, mole_routing, adapter_pr, soup_lock,
   adapter_bisect), v0.68 (prompt_compile, prompt_distill, compile_tools,
   apple_adapter, local_rl), v0.69 (build_dag, expectations, magpie,
   persona_hub, brain_rot), v0.70 (reward_hacking, uld, minillm,
   rl_checkpoint, iterative_dpo, echo_trap).

No code changes — docs-only hotfix per the checklist
"CI-only / docs-only hotfixes" rule. No version bump, no tag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 18:03:21 +05:00
Alpamys 74edac95d1 feat(v0.70.0): Loop Hardening — reward-hacking + ULD + MiniLLM + RL ckpt + iterative DPO + echo-trap
Six-part schema-only release shipping the axis-3 + axis-13 training-loop
hardening. Every live trainer-callback / math kernel is deferred to v0.70.1
per the project's established stub-then-live cadence
(matches v0.50.0 / v0.62.0 / v0.69.0).

Part A — Reward-hacking detector (soup_cli/utils/reward_hacking.py):
  InfoRM Cluster-Separation Index (Wang et al. 2024 arXiv:2402.09345) +
  RM-ensemble pairwise variance + OK/WARN/HACK taxonomy at 0.10/0.30
  thresholds. TrainingConfig.reward_hack_detector / reward_hack_halt;
  SoupConfig task-gate (grpo/ppo only, mlx rejected, halt requires detector).

Part B — Cross-tokenizer ULD (soup_cli/utils/uld.py):
  Universal Logit Distillation (Boizard et al. 2024 arXiv:2402.12030).
  wasserstein + topk_align allowlist; ULDConfig frozen with topk/strategy
  cross-validators; vocab-size cap 262144. Schema-gated to task='distill'.

Part C — MiniLLM reverse-KL on-policy distillation (soup_cli/utils/minillm.py):
  Gu et al. 2024 (arXiv:2306.08543) — bundles teacher-mixed sampling +
  length-norm + pretrain-loss anchor stability tricks. Anchor weight↔path
  mutual-requirement cross-validators reject silent no-op combos.

Part D — Mid-epoch RL checkpoint (soup_cli/utils/rl_checkpoint.py):
  Optimizer-state serialization TorchTune explicitly punts. RLCheckpointConfig
  + RLCheckpointState frozen + JSON-serialisable manifest. RL-task gate.

Part E — Iterative DPO loop driver (soup_cli/utils/iterative_dpo.py +
  commands/iterative_dpo.py): sample → RM-score → re-pair → retrain over
  N rounds. IterativeDPOPlan with consecutive-round_index invariant.
  New `soup iterative-dpo` CLI; --plan-only live, runner deferred.

Part F — RAGEN echo-trap detector (soup_cli/utils/echo_trap.py):
  Zhu et al. 2025 (arXiv:2504.14437) — n-gram trajectory-repetition kernels
  with DoS caps (max 32 ngram_n, 1M tokens, 100k trajectories). OK/WARN/TRAP
  at 0.30/0.60. Composes with v0.53.11 #127 GRPOStabilityCallback.

Cross-cutting hardening:
- 6 new util modules + 1 new top-level CLI + 11 new TrainingConfig fields
  + 6 new SoupConfig cross-validators + 3 new field validators
- Closed allowlists (frozenset) + MappingProxyType registries everywhere
- Frozen dataclasses with post-init validation on every public record
- Bool-as-int rejection on every numeric (matches v0.30.0 / v0.41.0 policy)
- math.isfinite NaN/Inf rejection on every float
- Null-byte rejection + per-field length caps on every string
- No top-level torch imports (4 source-grep regression tests)
- Deferred-live stubs validate inputs FIRST then raise NotImplementedError
  with explicit v0.70.1 marker
- CLI exit codes split: 2 = validation rejection, 3 = deferred-live

Test count: 11487 → 11824 (+337 net). 12-invariant self-review against
the full project checklist (closed allowlists, frozen dataclasses,
MappingProxyType, bool-as-int rejection, finite check, null-byte, length
caps, no top-level torch, TypeError/ValueError split, deferred-live,
tuples-not-lists, CLI exit codes) all green across all 6 Parts.

Manual CPU smokes (Step 6): every CLI happy + failure path exercised —
`soup iterative-dpo --plan-only` 3-round plan rendered end-to-end with
per-round artifacts; 5 happy-path YAML loads + 5 failure-mode rejections
across reward_hack / uld / minillm / rl_checkpoint / echo_trap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 17:17:03 +05:00
Alpamys 49943a5af6 feat(v0.69.0): Data Engineering Pro — soup build + expect + gen-magpie + persona-mix + brain-rot
5 parts shipping axis-2 (dbt-for-SFT) + axis-13 (data ops):

- soup build — dbt-for-SFT DAG with refs / incremental materialization /
  content-hash row-diff kernel (run_build live runner deferred → v0.69.1)
- soup expect <data> <suite> — LIVE expectations suite: PII / token-length /
  refusal / chosen-vs-rejected judge; exit 3 on suite failure
- soup data gen-magpie — Magpie synthetic generator plan (live → v0.69.1)
- soup data persona-mix — Persona-Hub × style sampler with bundled 12×5 set,
  atomic JSONL write (LIVE)
- soup data brain-rot — arXiv 2510.13928 detector with --strict CI gate,
  worst-signal composite (LIVE)

Centralised TOCTOU defence behind utils/paths.enforce_under_cwd_and_no_symlink
in build_dag / expectations / expect.py (code-review CRIT — replaces 3
duplicate os.lstat + S_ISLNK + realpath + is_under_cwd blocks). DoS caps on
every new JSONL loader (brain-rot 1 GiB + 1M rows; persona-mix 100 MiB + 100k
entries). persona-mix --output TOCTOU symlink rejection. magpie quality_filter
validator + expectations._dispatch_expectation raw-args pass-through (no
int/float coercion bypass). BuildModel seed/derived cross-validator rejects
ambiguous shapes at schema load.

Review-fix coverage across 4 waves (security + code + python + TDD):
1 CRITICAL + 4 HIGH + 5 MEDIUM + 4 LOW.

Test count: 11225 → 11487 (+262 net across 5 new files).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 16:00:15 +05:00
Alpamys 1d4892ea80 docs(v0.67.0 + v0.68.0): backfill `## All Commands` table with missing entries
The v0.67.0 and v0.68.0 releases shipped 14 new commands but neither
release updated the README `## All Commands` reference table per Release
Checklist Step 9. Adds the missing rows:

- v0.67.0 (5): adapters merge --strategy cmaes / adapters pr / adapters
  bisect / lock write / lock show + check.
- v0.68.0 (9): compile / distill-prompt / compile-tools / apple-adapter +
  local-rl init / status / record / harvest / train.

Docs-only — no source code change, no test count delta, no version bump
per the checklist's CI-only / docs-only hotfix policy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:47:12 +05:00
Alpamys aa71658f50 feat(v0.68.0): Anti-trend Insurance — compile (DSPy/GEPA) + distill-prompt + compile-tools + apple-adapter + local-rl
5 commands that hedge Soup against paradigm shifts. If 1M-context kills FT,
`soup compile` (DSPy + GEPA + TextGrad prompt-program compilation) takes its
place. If teams hit prompt-cost walls, `soup distill-prompt` bridges to small
FT. If only Apple Foundation Models win on-device, `soup apple-adapter` ships
the converter+signing surface. If personal-LLM flywheels become the shape,
`soup local-rl` captures thumbs into SQLite and emits DPO pairs.

- Part A: `soup compile <program.py> --eval <suite> [--optimizer mipro|gepa|...]`
- Part B: `soup distill-prompt --traces <jsonl> --teacher --student --strategy`
- Part C: `soup compile-tools <spec.json|yaml> --eval <jsonl>`
- Part D: `soup apple-adapter <source-dir> --direction hf-to-mlx|... --output`
- Part E: `soup local-rl init/status/record/harvest/train` (LIVE except train)

Schema + path containment + symlink rejection + atomic-write surface ship now;
live runners for Parts A/B/C/D + Part E nightly scheduler deferred to v0.68.1
(stub-then-live, mirrors v0.50.0 / v0.61.0 / v0.62.0 / v0.67.0 cadence).

Test count: 11021 -> 11225 (+204). Review-fix: 0 CRIT + 4 HIGH + 10 MED + 4 LOW.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 20:24:39 +05:00
Alpamys 4f8a0c11be docs(v0.67.0): update SECURITY supported-version list + CONTRIBUTING test count
- SECURITY.md: add v0.67.0 to supported-versions list
- CONTRIBUTING.md: bump test count 244 files / 10836 tests -> 251 / 11021

Docs-only follow-up to v0.67.0 release; no code change, no version bump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 16:13:55 +05:00
Alpamys 32145097ec feat(v0.67.0): Adapter Lifecycle Finish — CMA-ES merge + VeRA bank + MoLE + PRs + soup.lock + bisect
Six surfaces close v0.57:

- Part A: pure-Python rank-mu CMA-ES evolutionary merge (cmaes_merge.py)
  + soup adapters merge --strategy cmaes --eval <s> --budget 1h
- Part B: VeRA / VB-LoRA vector-bank schema + atomic JSON I/O (vector_bank.py)
- Part C: MoLE per-token routing schema + new task='moe_lora_routing' (mole_routing.py)
- Part D: GitHub-shaped adapter PR renderer (adapter_pr.py)
  + soup adapters pr <title> --base-sha --adapter --eval --samples
- Part E: soup.lock shared run lockfile (soup_lock.py + commands/lock.py)
  + soup lock write/show/check (exit 3 on drift)
- Part F: training-history binary search (adapter_bisect.py)
  + soup adapters bisect <ckpts> --eval-command "..."

Live wiring deferred to v0.67.1: CMA-ES eval-suite auto-bind, VeRA serving,
MoLE gating kernel.

+185 tests (10836 -> 11021) across 7 new test files. Review-fix coverage
from 2 sequential waves (security + tdd-guide). All step-6 smokes green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 15:58:30 +05:00
Alpamys a015ccc812 feat(v0.66.0): Post-train X-rays — SAE diff + live blame + sleeper probe + interference matrix + probe pack
Extends `soup diagnose` from 6 failure modes to 10. Closes v0.57 #171 —
live blame runner replaces the NotImplementedError stub.

- `soup probe sae-diff`: SAE feature attribution (pure-numpy; HF_HUB_ALLOWLIST)
- `soup adapters blame --top-k 50`: live DataInf influence runner
  (closes #171; replaces v0.57 stub with cos(grad_row, grad_probe) × |grad_row|)
- `soup probe sleeper`: calibrated defection probe (6 bundled bases;
  OK/MINOR/MAJOR at 1%/5%; exit 2 on MAJOR)
- `soup probe interference`: pairwise N×N matrix
  (OK/MINOR/MAJOR at 5%/20%; exit 2 on MAJOR worst-pair)
- `soup probe pack`: per-base probe manifest assembler

Review-fix coverage across 3 sequential waves: 0 CRITICAL + 9 HIGH +
14 MEDIUM + 5 LOW. Notable hardening:
- TOCTOU O_NOFOLLOW probe-open in load_sae_weights + _count_dataset_rows
- hashlib.sha256 replaces process-salted hash() for CI reproducibility
- Rich-markup escape on adapter / verdict / description / layer
- TypeError on bool/non-str verdict before membership check
- Non-numeric loss rejection in `probe interference` CLI
- 10M-row hard reject (no silent truncate); 100k synthetic-probe cap
- _LOWER_INDEX MappingProxyType for O(1) case-insensitive lookup
- Mapping from collections.abc (PEP 585); frozenset[str] type params
- Frozen dataclasses + FrozenInstanceError regression tests

Note: Windows cp1251 print on stdout-capturing Python wrappers can crash
on Rich's '→' arrow output; the soup CLI itself uses force_utf8_stdio.

Test count: 10577 → 10836 (+259 net across test_v0660_part_{a-e}.py,
test_v0660_cli.py, test_v0660_followups.py). Full suite green
(10836 passed, 81 skipped); ruff clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 14:08:12 +05:00
Alpamys 799f5e8522 fix(tests): floor-check version assertion in test_version_bumped_to_0640
Was `assert soup_cli.__version__ == "0.64.0"` — broke on v0.65.0 bump on CI
across all 9 platform×Python matrix jobs. The test intent is "v0.64.0 has
shipped at least once"; switch to a tuple floor check matching the v0.51 /
v0.54 / v0.57 / v0.60 idiom.

Caught by CI red on v0.65.0 push to main; local pytest passed because we
ran the v0.65 test files in isolation per the Release Checklist Step 4
``pytest --no-cov`` invocation. Lesson: include the full suite in step 4
or grep for ``soup_cli.__version__ ==`` exact-equality assertions in any
future version bump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:46:48 +05:00
Alpamys 1e822af461 feat(v0.65.0): Eval Depth — judge calibration + behaviour battery + capability suite + CheckList DSL + IRT subset
5 LIVE parts closing axis 4 of the roadmap. Evals as first-class surface, not afterthought:

- Judge calibration: SCOPE/CJE-style bidirectional pairwise judging in eval/calibrate.py
  with PairwiseJudgement / fit_position_bias / conformal_threshold +
  ensure_judge_calibrated production gate that refuses to score with an uncalibrated
  judge (RuntimeError on None / calibrated=False / low agreement / extreme bias).

- Behaviour battery (soup eval behavior): closed allowlist of XSTest / HarmBench /
  JailbreakBench / ELEPHANT / SycEval with 5 tiny bundled redacted probe sets under
  soup_cli/data/_fixtures/behavior/. Word-boundary regex agreement rejects
  "safe" in "unsafe" false positives. Pre/post diff with OK/MINOR/MAJOR verdict
  (matches v0.26 / v0.56 taxonomy).

- Capability auto-suite (soup eval capability): MMLU-Pro / GPQA / BBEH / AIME /
  MATH-500 / HumanEval+ / SWE-bench-Verified with full / fast / math / code profile
  selector. Emits (benchmark, lm-eval task) manifest for downstream
  soup eval benchmark chaining.

- CheckList DSL (soup eval checklist): Ribeiro et al. 2020 MFT / INV / DIR test kinds
  rendered from YAML. Word-boundary matching prevents "and" matching "sand".
  Per-test pass/fail + OK/MINOR/MAJOR overall verdict.

- IRT eval-cost optimizer (soup eval irt-subset): 1PL Rasch closed-form fit on
  per-item correctness signals + high-info subset selector (full / small / tiny
  profiles). 5-10x cut in eval bills without losing ranking power.

Cross-cutting hardening (review-fix coverage across 2 review waves):

- TOCTOU defence: every new read path uses O_NOFOLLOW + os.fstat on SAME fd
  (load_checklist_spec, load_response_rows, _read_evidence_json). Earlier
  double-lstat-on-path was a race the attacker could win by swapping the file
  between calls.
- Namespace-package safety: load_battery_probes uses importlib.resources.files
  Traversable / op + as_file (was Path(os.path.join(str(pkg_root), ...)) which
  silently fails is_file() on MultiplexedPath installs).
- Word-boundary regex agreement in behavior_battery + checklist_dsl.
- CLI _validate_run_id gate; 16 MiB --evidence cap with O_NOFOLLOW;
  _MAX_ROWS=1_000_000 cap counts skipped lines toward total in load_response_rows.
- INV empty-string normalisation no longer spuriously passes.
- _write_json_output / _read_evidence_json / _validate_run_id dedup helpers in
  commands/_eval_v0650.py.

Review fixes: 0 CRITICAL + 6 HIGH + 9 MEDIUM + 7 LOW resolved across 2 waves.

Test count: 10306 -> 10577 (+271 net across test_v0650_part_{a,b,c,d,e}.py +
test_v0650_followups.py). Full suite 10577/10577 passing. 0 regressions.

Step 6 smoke: every new CLI command + 3 failure modes exercised end-to-end
(behavior with --evidence happy + MAJOR exit 2; capability fast with output;
checklist with real YAML; irt-subset on 600-row synthetic data; unknown
battery / size / kind all exit 2; outside-cwd evidence rejected).

Known limitations:
1. Live lm-eval-harness invocation deferred — soup eval capability emits the
   manifest for downstream soup eval benchmark chaining (Typer commands aren't
   safe to re-enter; matches v0.46.0 / v0.44.0 design).
2. Live model-driven soup eval behavior deferred — without --evidence, emits a
   neutral OK report (v0.65.1).
3. Behaviour battery probe sets ship as tiny redacted placeholders — operators
   pull real harmful prompts from upstream papers.
4. IRT model is 1PL Rasch only (2PL / 3PL deferred to v0.65.x).

Step 6 quirk worth noting: --evidence containment rejects /tmp/ on Windows
WSL bash; operators must run from cwd or pass cwd-contained paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 15:35:47 +05:00
Alpamys 8b5991674b feat(v0.64.0): Pre-flight & Tooling — tunability, plan/apply, env, hardware-fit, completions, license-advisor
Six new top-level commands close axis 1 + 11 of the roadmap: pick the
right base, lock the env, refuse OOMs before launch, and clear
license-clean deploys.

- soup tunability: probe-train 8 candidate bases (Qwen3-0.6/1.7B,
  Llama-3.2-1/3B, Gemma-3-E2B, Phi-4-mini, SmolLM3, Qwen2.5-1.5B) ->
  Pareto frontier over (delta x cost x license). Live LoRA probe -> v0.64.1.

- soup plan / soup apply: Terraform-shape lock-and-execute. `apply`
  refuses on drift between soup.yaml and soup.tfstate (exit 3).

- soup env lock / status / check: hermetic env lockfile via
  importlib.metadata across 15 ABI-sensitive packages + Python + CUDA.
  `env check` exits 3 on drift.

- Hardware-fit calculator: static analytical 5-bucket VRAM predictor
  with 10% safety margin + actionable hint on OOM.

- soup completions bash|zsh|fish: sourceable shell completion scripts;
  recipe names auto-complete from the 115-recipe catalogue.

- soup license-advisor: per-deploy-target license matrix
  (b2c/defense/embedded) + Llama community + 700M MAU gate (exit 3).
  Composes with v0.60 license-conflict matrix.

Tests: 10035 -> 10306 (+271 net in 7 new files).
Review-fix coverage: 0 CRITICAL + 6 HIGH + 8 MEDIUM + 4 LOW across
consolidated code+security+TDD review wave. Every HIGH lands a regression
test in tests/test_v0640_followups.py (POSIX-skipped symlink rejection,
containment-before-existence ordering, drift-refusal exit-3 end-to-end).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:45:04 +05:00
Alpamys 5afdf610a7 docs(v0.63.0): list 5 new commands in README All Commands reference
Docs-only hotfix — adds the v0.63.0 commands (soup ingest /
prune-prompt / data active-sample / ab / drift-alarm) to the
"## All Commands" code-block reference card. The release v0.63.0 had
dedicated ## feature sections + the ## What's New block updated, but
the All Commands quick-reference was missed during the doc pass.

No source code changed — no version bump, no tag, no release.
Per CLAUDE.md hotfix policy: docs-only commits don't ship to PyPI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 22:24:09 +05:00
Alpamys 40bd6251a2 feat(v0.63.0): Production Trace Ecosystem — soup ingest + prune-prompt + active-sample + ab + drift-alarm
5 new top-level commands close axis 7 of the roadmap. Every Part LIVE on
day one (no deferred stubs):

- soup ingest: universal trace importer (Langfuse / LangSmith / Helicone /
               OpenPipe / OTel / OpenAI Stored Completions). 6 adapters
               + frozen TraceRecord with MappingProxyType-wrapped metadata.
               Zero credential-handling threat surface — Soup parses the
               JSONL export, never makes the SaaS network call.
- soup prune-prompt: detect + strip a shared system-prompt prefix so the
                     FT model internalises it (OpenPipe's signature trick,
                     OSS). Binary-search over up to 32 templates finds the
                     longest threshold-meeting prefix.
- soup data active-sample: surface top-uncertainty prod traces for human
                           review. Max-entropy on single rm_score or
                           pairwise disagreement on dual rm_scores.
- soup ab: Wald sequential SPRT for the point alternative. LLR is a
           martingale under H0 so Type-I error is controlled at every
           stopping time per the optional stopping theorem.
- soup drift-alarm: rolling KL on whitespace-tokenised output distribution
                    + SSRF-hardened Slack/Discord webhook (full parity with
                    v0.51.0 validate_hub_endpoint). Exit 3 on drift for
                    cron-friendly automation.

Test count: 9816 -> 10035 (+219 net across 6 new test files).

Review-fix coverage (code-reviewer + tdd-guide returned actionable;
python-reviewer + security-reviewer agents context-thrashed on the large
CLAUDE.md release-notes history — matches the v0.58.0 / v0.59.0 / v0.60.0
/ v0.61.0 / v0.62.0 idiom; verified manually):

- 1 CRITICAL: mSPRT log-likelihood-ratio sign error drove Type-I error
              to 1.0 as n grew. Replaced with Wald's classic point-
              alternative SPRT (martingale under H0).
- 2 HIGH: detect_common_prefix early-exit on 100% match returned the
          shortest qualifying prefix instead of the longest;
          _MAX_SCAN_ROWS DoS cap used 'pass' instead of 'break'.
- 3 MEDIUM: TraceRecord.metadata now MappingProxyType-wrapped post-init
            (frozen-dataclass mutation hazard); _AUTH_ENV table
            deduplicated; drift_alarm precedence parens on SSRF gate.
- 2 LOW: pooled_se dead-branch refactor; mean_uncertainty NaN guard.
- 8 follow-up tests: msprt zero-variance, partial-majority binary-search
  activation, score_uncertainty exact boundaries, rolling_kl identical
  + disjoint, validate_budget + validate_threshold exact endpoints,
  _signal_from_thumbs boundaries, no-heavy-top-level-imports source-grep
  guard across all 5 new util modules.

Step 6 smoke verified for all 5 commands + 6 failure-mode rejection
paths.

CRLF gotcha note for future maintainers: PowerShell wrote the smoke
fixtures with a UTF-8 BOM on Windows during Step 6 — switched to
inline Python for the fixture write. Production CLI input handling is
already BOM-tolerant (utf-8-sig in JSONL loaders via v0.40.1 Part E).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 22:02:54 +05:00
Alpamys 0d6f95181a feat(v0.62.0): RAG & Activation Steering — RAFT + RA-DIT + soup steer + citation-faithful + GRACE codebook
5 Parts shipping wedge 14 of the roadmap (RAG-aware fine-tuning +
activation steering + lifelong edit codebook). Schema-only release;
live training loops + decode-hook intervention + codebook lookup all
land in v0.62.1 (mirrors v0.50.0 / v0.52.0 / v0.61.0 stub-then-live).

Part A — RAFT data format: new data.format='raft' schema +
_convert_raft validator (64 KiB per-field cap, 64-distractor cap,
null-byte rejection on every field) + raft-llama3-8b recipe.

Part B — RA-DIT two-stage: TrainingConfig.ra_dit_stage Literal
{retriever, generator} + ra_dit_retriever_model field + closed
allowlist + cross-validator enforcing stage to base-task pairing
(retriever to embedding, generator to sft) + 2 recipes.

Part C — soup steer (CAA / ITI / RepE): closed-allowlist control-vector
methods + validate_steering_method/name/strength + Typer subcommands
train/apply/list + soup serve --steer/--steer-strength flags +
steering_vector Registry artifact kind. apply_steering +
build_steering_vector deferred-live stubs raise NotImplementedError
with v0.62.1 marker after validating inputs.

Part D — Citation-faithful FT: score_citations precision/recall/F1
kernel + extract_citation_ids public API + citation_faithful /
citation_style / citation_recall_threshold schema. Cross-validator:
citation_faithful=true requires data.format='raft' AND task in
{sft, pretrain} (silent-no-op footgun rejection mirroring v0.52.0
distill / classifier task-gate policy).

Part E — GRACE codebook: GraceCodebookConfig + bounded size [1, 100k]
+ bounded dim [1, 16384]. Extends v0.61.0 SUPPORTED_EDIT_METHODS
allowlist with 'grace'; apply_edit routes grace plans to v0.62.1
marker while legacy rome/memit/alphaedit retain v0.61.1 marker
(regression-guarded via TestEditMarkerRegressionGuard).

Test count: 9571 -> 9786 (+215 net). 4 review-agent waves resolved
0 CRITICAL + 0 HIGH + 4 MEDIUM + 11 LOW (broken list_steers registry
context-manager + dict-key access; missing version bump;
citation_faithful task-gate; shared TOCTOU helper delegation; Rich
markup escape on --steer exception messages; --base length cap +
null-byte rejection; typing.Iterable -> collections.abc.Iterable
migration; except Exception -> except ImportError narrowing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 20:20:30 +05:00
Alpamys efbb05759f chore: attribute @dreamer0129 as co-author of #174 fix
The list/info portions of 4f54179 follow the structure of @dreamer0129's
PR #175 draft. Adding Co-Authored-By trailer so GitHub contributors graph
reflects the original draft author.

Co-Authored-By: dreamer0129 <185807357+dreamer0129@users.noreply.github.com>
2026-05-20 00:20:39 +05:00
Alpamys 4f54179ea4 fix(security): backfill Rich markup escape in legacy adapters commands
Closes #174. Picked up after PR #175 (dreamer0129) went quiet — the
list/info path was clean, but compare() escaped only inside the
highlight branch, leaving a shared crafted base_model = "[link=evil]
click[/]" un-escaped on equal-value rows.

Fix follows the "escape always at the value layer, decoration wraps
after" pattern mirroring v0.57.0 `adapters diff` / `info`:

- list_adapters: wrap base / lora_r / peft_type / rel_path with
  rich.markup.escape() before table.add_row(); also escape
  adapter_path in the JSONDecodeError fallback.
- info: wrap base_model / peft_type / task_type / lora_r / lora_alpha
  / lora_dropout / modules_str inside the Rich Panel f-string; also
  escape adapter_path.name in the Panel title.
- compare: escape val1_str / val2_str unconditionally; [yellow]
  highlight wraps already-escaped values when they differ. Equal-value
  rows now also escape (was the v0.57.0 known-limitation gap).

+4 regression tests in tests/test_adapters.py::TestAdaptersMarkupEscape:
- test_list_escapes_crafted_base_model — asserts no ANSI hyperlink
  sequence (\x1b]8;) leaks from a crafted [link=http://evil/...] payload.
- test_info_escapes_crafted_base_model — same assertion for Panel.
- test_compare_escapes_equal_crafted_values — the specific regression
  for the PR #175 review gap (identical crafted values on both sides
  must NOT smuggle live markup through the equal-branch).
- test_compare_escapes_differing_crafted_values — highlight branch
  also escapes.

Closes v0.57.0 Known Limitation (9).

Verified locally:
- ruff check soup_cli/commands/adapters.py tests/test_adapters.py -> clean
- pytest tests/test_adapters.py --no-cov -> 20 passed
2026-05-20 00:12:05 +05:00
Alpamys 6ddaeb30d1 polish(v0.60.0 strict-safetensors): tighten header cap + input guards
Follow-up to PR #198 (issue #189):
- _MAX_SAFETENSORS_HEADER_BYTES: 1 GiB -> 100 MiB. Real safetensors
  headers are <10 MiB even for 70B-parameter models; 100 MiB is a
  generous defence-in-depth ceiling that still rejects an adversary's
  "header_len = 999 MiB" allocation attempt before fh.read() commits.
- is_safetensors_magic: input-shape guards (non-string / empty /
  null-byte path return False, never raise). Matches project policy
  for detection-style helpers (mirrors v0.30.0 Candidate, v0.41.0
  lr_groups, v0.53.3 is_known_vlm_base).
- +2 regression tests in tests/test_v0600_part_c.py:
  - test_is_safetensors_magic_rejects_invalid_input (5 bad inputs)
  - test_max_safetensors_header_bytes_tightened (guards against
    re-widening to 1 GiB in a future patch)

Module docstring updated to reference PR #198 / issue #189.

Verified locally:
- ruff check soup_cli/utils/strict_safetensors.py
  tests/test_v0600_part_c.py -> clean
- pytest tests/test_v0600_part_c.py --no-cov -> 21 passed,
  1 POSIX-skipped on Windows

Closes v0.60.0 Known Limitation (6) — full magic-byte + JSON-header
shape verification with hardened input surface.
2026-05-19 23:56:42 +05:00
Vivaan Dhawan 914a299965
Check safetensors magic bytes (#198)
Co-authored-by: Sumit Dhawan <sumitdhawan@Sumits-MacBook-Air.local>
2026-05-19 23:52:04 +05:00
Alpamys f2c74040ef fix(v0.61.0): POSIX CI green — lstat RAW path before realpath in 3 loaders
CI on ubuntu / macOS exposed a missed TOCTOU detail: `os.lstat(realpath(path))`
silently resolves symlinks before the lstat, so `S_ISLNK` never trips and a
symlinked input path passes the rejection. Windows CI was skipped (POSIX-only
symlink test), so the bug didn't surface in local smoke.

Three fixes, matching the v0.53.7 #106 project policy of "lstat the RAW path
before realpath":

- `edit_diff.load_probes` — lstat path first, then realpath after rejection.
- `unlearning_eval.load_evidence_file` — same.
- `unlearning_eval.get_fixture_path` — lstat raw candidate before realpath.

No new tests — the existing `test_symlink_rejected` covers it; it now passes
on POSIX where it previously failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:27:50 +05:00
Alpamys 740832e1b4 feat(unlearn/edit): v0.61.0 — Unlearning & Knowledge Edit (NPO/SimNPO/RMU + ROME/MEMIT/AlphaEdit)
5 Parts shipping schema + CLI surface for two of the most under-served axes
in fine-tuning: GDPR right-to-be-forgotten unlearning (the legal-liability
axis upstream TRL avoids) and surgical knowledge editing (research-coded
everywhere, productized nowhere). Schema-only release; live trainer +
kernel wiring deferred to v0.61.1 (matches established v0.50.0 / v0.52.0
/ v0.53.0 stub-then-live cadence).

Part A — task='unlearn' + NPO/SimNPO/RMU allowlist + UnlearnTrainerWrapper
  + data.forget_set / data.retain_set + training.unlearn_method/_alpha
Part B — soup eval unlearning (TOFU/MUSE/WMDP) with Forget Quality + Model
  Utility + PrivLeak kernels + OK/MINOR/MAJOR taxonomy; bundled TOFU
  mini-fixture under soup_cli/data/_fixtures/unlearning/
Part C — soup edit set (ROME/MEMIT/AlphaEdit) + EditPlan + per-method
  default layer; --plan-only ships live, apply_edit kernel deferred
Part D — Sequential edit governor: norm-blowup detection (OK/WARN/BLOWUP),
  auto-switch ROME→AlphaEdit at edit#10 or BLOWUP, refuses past cap
Part E — soup edit diff: cwd-contained probe loader, atomic JSONL out,
  shape + table renderer (live before/after generation v0.61.1)

Net: +125 tests (9446 → 9571), +5 utility modules + 1 trainer wrapper +
2 commands. Review-fix coverage: 0 CRITICAL + 5 HIGH + 11 MEDIUM + 11 LOW.
All ruff + pytest green; Step 6 smokes (CLI plumbing + happy paths + 5
schema rejection paths) confirmed end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:16:15 +05:00
Alpamys 47409df730 fix(tests): v0.60.0 CI green — widen version floors + strip ANSI from merge help assert
Three failures on CI run 26084542388 — all are version-pin / Rich-wrap
artefacts, not real regressions in v0.60.0 functionality:

- test_v0560 test_pyproject_version: regex-based >=0.56 floor check
  (was substring `version = "0.5`)
- test_v0590 test_version_is_0_59 -> test_version_is_at_least_0_59:
  >=0.59 floor (matches v0.51/v0.54 floor-check idiom)
- test_v0600_part_e merge_help_lists_license_flags: strip ANSI codes
  before substring check (Rich splits `--license` across `\x1b[1;36m`
  escapes in the wrapped Typer table)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:15:28 +05:00
Alpamys f3c40e7753 feat(security): v0.60.0 — Supply Chain Security wedge (adapter scan/sign/verify, strict-safetensors, namespace-pin, license-matrix, airgap-bundle)
Six controls that hosted vendors structurally can't provide:

- soup adapters scan: spectral backdoor scanner (rank-1 dominance + energy
  concentration + NaN/Inf + Frobenius outlier via robust median+MAD);
  pure numpy, reuses v0.57.0 adapter_diff loader
- soup adapters sign / verify: Merkle-root manifest in .soup-signature.json;
  recursive file enumeration (catches nested tokenizer/ tamper);
  sigstore + ed25519 backends stub-then-live (v0.60.1)
- soup adapters check-safetensors: closed 8-entry unsafe-extension allowlist;
  strict exit 3 for CI gating
- NamespacePinStore: TOFU SQLite anti-AI-Jacking; author + created_at
  fingerprint compared via datetime.fromisoformat for offset-aware order;
  bool opt-in rejected so --allow-namespace-shift cannot be a free-for-all
- License-conflict matrix: 33 SPDX-ish ids in MappingProxyType compat table;
  soup adapters merge --license <id> --license-override <reason> gate
- soup airgap-bundle: signed tarball with deterministic dataset labeling
  (sorted basename, NOT argv order); TOCTOU lstat+S_ISLNK on parent + output;
  atomic os.replace; tarfile.data_filter for future extractall callers

Test count: 9294 -> 9446 (+152 net across 6 new test files).
Review-fix coverage across 5 waves (python / security / code / tdd / smoke):
0 CRITICAL + 12 HIGH + 11 MEDIUM + 6 LOW fixed before commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:05:14 +05:00
Alpamys 124a72e042 docs(README): fix v0.59.0 test count typo (+99 actual, not +93)
The v0.59.0 What's New block claimed +93 new tests, but the actual count
is +99 new tests in tests/test_v0590.py (9193 -> 9294). Numbers in
CLAUDE.md / SECURITY.md / CONTRIBUTING.md / the release notes already
have the correct count.

Docs-only hotfix; does not require a version bump (per release checklist).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:58:18 +05:00
Alpamys 9563699eca fix(v0.59.0): rewrite null-byte env test — OS layer rejects setenv on every platform
The previous test_default_log_path_rejects_null_byte_env used monkeypatch.setenv
to inject a null byte into SOUP_AUDIT_LOG_PATH and expected default_log_path
to fall back gracefully. But the OS layer rejects null bytes in env vars on
every platform we ship on:

- POSIX (Linux/macOS): `ValueError: embedded null byte`
- Windows: `ValueError: embedded null character`

The setenv call itself raises, never reaching default_log_path. Split into two
tests that hit the actual validation surfaces:

1. test_default_log_path_rejects_null_byte_override — calls the private
   _validate_log_path_override helper directly with a null-byte string and
   asserts it returns None (so the caller falls back to the safe default).

2. test_default_log_path_handles_env_read_value_error — monkeypatches
   os.environ.get to raise ValueError, exercising the defence-in-depth
   try/except around the env read in default_log_path().

Both tests pass on Linux + macOS + Windows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:25:56 +05:00
Alpamys d0f5e35c99 fix(v0.59.0): macOS CI — strip ANSI from train --help assert + handle null-byte env
Three macOS-3.11 CI failures in test_v0590.py post-merge:

1+2. test_train_annex_xi_flag_present_in_help / test_train_repro_receipt_flag_present_in_help
     — Typer's Rich-renderer wraps long lines and inserts ANSI colour codes
     BETWEEN the two dashes of `--annex-xi` / `--repro-receipt`, so the
     literal substring match fails. Strip ANSI escape codes via regex before
     asserting; also accept the bare option name as a defence-in-depth
     fallback against future Rich line-wrap quirks.

3. test_default_log_path_rejects_null_byte_env — POSIX `os.environ.get` raises
   `ValueError("embedded null byte")` when the env value contains a NUL
   character, while Windows allows the read. Wrap the env read in
   `try/except ValueError` so the function falls back to the safe default
   (~/.soup/audit.jsonl) on either platform.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:16:50 +05:00
Alpamys 6d44f0f931 feat(governance): v0.59.0 — CycloneDX/SPDX BOM + in-toto/SLSA-3 attest + Annex XI/XII + audit-log + repro-receipt + energy schema
Six Parts ship the procurement-floor moat — every Soup run can now emit
the formats regulated orgs demand, with no SaaS structurally able to
follow. Pure orchestration on top of v0.26 Registry + v0.34 cost
tracker + v0.56 diagnose — schema + atomic-write surface only; live
Sigstore signing, CodeCarbon hook, PDF rendering deferred to v0.59.1.

Part A — `soup bom emit` CycloneDX 1.6 ML-BOM + SPDX 2.3 AI-profile
dual emitter from any RegistryEntry. SHA-256 validation on every sha
field, license-id chain, base-model component with hash, per-artifact
file components, energy properties under metadata.properties.

Part B — `soup attest emit` in-toto v1 Statement wrapping SLSA-3
provenance v1 predicate. Stage allowlist (extract/train/eval/export/
publish), subject SHA locked to 64-hex, builder_id capped, SignatureBackend
enum with UNSIGNED live + SIGSTORE/ED25519 stubs raising NotImplementedError
with explicit v0.59.1 marker.

Part C — `soup train --annex-xi` EU AI Act Annex XI Sections 1+2 +
Annex XII Article 53(1)(d) markdown auto-doc. Top-10 domain cap,
modality breakdown, FLOPs/kWh/CO2. `_md_escape` neutralises |[](){}!<>
plus newline/CR/tab in every operator-controlled field — defends
against forged-heading + Markdown-link injection in downstream PDF/HTML
renderers (mirrors v0.29.0 model-card v2 policy).

Part D — `soup audit-log tail/rotate` HIPAA/SOC2-shaped JSONL with
PII redaction across every string field via v0.40.3 _SECRET_RE policy.
POSIX O_NOFOLLOW on append + 0o600 perms + lstat-based symlink rejection
at rotation backup path (no lexists race). SOUP_AUDIT_LOG_PATH env
override containment-checked to $HOME / $CWD / $TMPDIR.

Part E — `soup train --repro-receipt` SR 11-7-style receipt: seeds
(torch/numpy/python), kernel versions (CUDA/cuDNN/NCCL via best-effort
torch probes), GPU model + driver, OS + arch, Python version. Atomic
write, cwd-contained.

Part F — CodeCarbon hook schema + electricityMap SSRF validator with
full parity to v0.51.0 hubs.validate_hub_endpoint (scheme allowlist,
loopback-only HTTP, RFC1918 / link-local / reserved / multicast IP
rejection via ipaddress.ip_address, control-char + null-byte
rejection). PUE math + attach_energy populating BomEntry.

Cross-cutting: new paths.atomic_write_text shared TOCTOU-safe helper
centralises the v0.33.0 #22 / v0.43.0 / v0.55.0 / v0.56.0 / v0.57.0
/ v0.58.0 atomic-write pattern from four separate copies into one
single-source-of-truth (mirrors v0.40.6 / v0.53.5 peft_wiring policy).

Four review waves (python-reviewer + general-purpose security/code/tdd):
0 CRITICAL + 8 HIGH + 12 MEDIUM + 4 LOW resolved before commit.
HIGH fixes: audit-log lstat-before-write TOCTOU, O_NOFOLLOW on
append, redaction extended to host_id/operator_id/command, audit-log
env override containment, bom artifact size_bytes validation,
BomEntry attach_energy type-hint fix, default_log_path public symbol,
duplicated seeds validation removed.

Test count 9193 → 9294 (+99 net in tests/test_v0590.py; 93 pass +
6 POSIX-skipped on Windows for symlink rejection branches). v0.58.0
floor-check assertions widened from exact-match in test_v0580.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:05:12 +05:00
Alpamys 38e562831c refactor(doctor): harden NCCL benchmark from PR #178
PR #178 wired the live NCCL bandwidth measurement on top of the
v0.43.0 nccl_bandwidth_check reference table. Four polish items
on top of the merged version:

- Bump timing samples 5 -> 10 and report MEDIAN rather than mean.
  Median is robust to one-off jitter (kernel preemption, page
  faults, OS scheduler hiccups) that can scew a 5-sample mean by
  10-30%. Median of 10 gives a much more honest GB/s number.

- Bump warmup 1 -> 3 iters. First all_reduce in a fresh process
  pays for CUDA kernel JIT plus the initial NCCL collective
  handshake; one warmup isn't always enough to amortise that out.
  3 warmups is the standard NVIDIA recommendation for collective
  benchmarks.

- Time each iteration separately (collect a per-iter list, then
  median) instead of averaging total elapsed / iters. Same headline
  cost, but exposes per-call variance for future percentile output.

- Snapshot + restore MASTER_ADDR / MASTER_PORT around the spawn.
  The worker sets them unconditionally, so prior to this patch a
  second doctor invocation in the same process (test harness,
  notebook, future TUI auto-refresh) would inherit stale env. Now
  we restore exactly the prior state (including absent vars).

Plus three module-level constants (_NCCL_BENCHMARK_TENSOR_BYTES
/ _WARMUP_ITERS / _TIMED_ITERS) with comments explaining each
number so future tuning is informed, not magic. Status line now
prints the actual params so users see what we measured.

Existing tests (mock mp.spawn at the outer boundary) keep passing
unchanged; no new tests needed for this polish layer.
2026-05-17 18:38:34 +05:00
Salil M 168ecc0100
Add `--nccl` flag to `soup doctor` for multi-GPU bandwidth checks (#178)
* feat(doctor): add --nccl flag to measure and validate multi-GPU bandwidth

* test(doctor): add mocked CUDA tests to verify --nccl skip and success behaviors

* docs(readme): document the new --nccl bandwidth check flag for the doctor command
2026-05-17 18:35:51 +05:00
Alpamys b344aa881a feat(loop): soup loop CLI-first data flywheel capstone (v0.58.0)
Connects 8 existing uniques into one workflow: production traces ->
preference pairs -> Eval-Gated DPO -> canary deploy -> rollback, all
from a single CLI with budget guardrails and per-iteration replay.

Modules (live):
- utils/loop_state.py: LoopState frozen + atomic .soup/loop.yaml I/O
- utils/canary_router.py: deterministic SHA-256 routing + BucketStats
- utils/loop_budget.py: parse_budget_string + check_budget + UTC rollover
- utils/loop_iteration.py: IterationRecord + write/read/list manifests
- utils/loop_daemon.py: WatchConfig + run_once + watch daemon
- commands/loop.py: init / status / pause / resume / watch / canary / replay

Three review waves fixed 1 CRITICAL + 7 HIGH + 9 MEDIUM + 2 LOW total:
python-review wave 1 (BucketStats lock scope + TOCTOU lstat-before-write
on _check_path + init_state + NUL-byte on _bucket_for_key); code-review
wave 2 (watch preserves paused / budget-skip writes no manifest / canary
autoroll persisted to LoopState / route() math.ceil for sub-bucket
predictability / parse_budget_string usd-only friendly error /
list_iterations swallows OSError / module-top replace import);
security + tdd wave 3 (_check_dir TOCTOU mirrors _check_path pattern,
exact-boundary tests at _MAX_STR_FIELD=512 and _MAX_FILE_BYTES=1 MiB,
bool-rejection on 4 counters, empty-string rejection on 3 optional-str).

verification-loop: manual CPU smoke covering init / status / pause /
resume / watch --max-iterations / canary / replay end-to-end.

Notes:
- ASCII arrows (->) in user-facing help text (CI test_help_output_is_ascii_safe).
- Source-grep tests use Path(__file__).resolve().parent.parent for cwd-
  independence (defends against monkeypatch.chdir side-effects from
  earlier tests in the suite).
- Stage callbacks ship as no-op stubs; v0.26 trace-to-pref / eval-gate /
  v0.30 multi-adapter deploy wiring is operator-driven via WatchConfig
  fields. Pre-wired versions tracked for v0.58.1.

Test count: 8998 -> 9193 (+195 net in tests/test_v0580.py).
Lint clean. Full repo pytest green (9105 pass + 53 skipped pre-fixes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:52:37 +05:00
Alpamys 6b11da386a docs(release): v0.57.0 — CONTRIBUTING test-file table + counts (step 11 follow-up)
- Bump tests/ count from "197 files, 8849 tests" to "201 files, 8998 tests"
- Append v0.57.0 adapter modules to the soup_cli/utils/ inventory:
  adapter_diff, adapter_merge, blame, adapter_branch
- Add 4 v0.57.0 test-file rows (test_v0570_part_{a,b,c,d}.py) to the
  test-file table covering Frobenius diff math + 4 merge strategies +
  blame plan emitter + SHA-256 snapshot pointers

plan.md banner + heading also flipped to (shipped 2026-05-15 — PyPI +
GHCR live); plan.md is gitignored so that change persists locally only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:42:41 +05:00
Alpamys 77f7b71b23 docs(release): v0.57.0 — README body section + SECURITY.md notes
Step 9 follow-up: add dedicated `## Adapter Management (git for LoRA)`
section to the README body so the surface is self-contained when the
single-slot `## What's New` block is overwritten in v0.58.0.

Step 10 follow-up: add v0.57.0 to the SECURITY.md supported-versions
list + a detailed entry in the per-version fix notes covering all
9 HIGH fixes (TIES sign-tie default, 4× symlink TOCTOU rejections,
atomic writes, env CRLF rejection, allowlist policy migration, etc.)
and the 7 known limitations.

Tracked follow-ups filed as GitHub issues #171–#174:
  #171 — live blame ablation runner
  #172 — merge canary verdict via v0.55 eval gate
  #173 — branch pointers → v0.26 Registry lineage
  #174 — Rich-markup backfill for legacy adapters list/info/compare

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:25:14 +05:00
Alpamys 76dfbb363e test(adapters): mock os.environ.get for null-byte/CRLF env tests (CI fix)
POSIX setenv (and Windows equivalent) reject null bytes + control chars
at the syscall boundary, so monkeypatch.setenv("SOUP_BRANCHES_DIR",
"/some\x00path") raises ValueError on every CI runner before our code
ever sees the env var.

Stub os.environ.get directly so the helper's rejection branch is
exercised exactly as it would be if the env var arrived through some
other channel (subprocess env inheritance, in-process programmatic
mutation, etc).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:00:04 +05:00
Alpamys 8577bc2800 test(adapters): strip ANSI before help-output substring asserts (v0.57.0 CI fix)
Rich-wrap-CI workaround — same fix pattern as v0.55.0 / v0.56.0:
CliRunner output contains ANSI color escapes that break literal
'--top-k' in output substring matches because Rich renders option
names as -\x1b[0m\x1b[1;36m-top-k.

Adds _ANSI_RE + _strip_ansi() helper to each of the 4 test files
(test_v0570_part_{a,b,c,d}.py) and routes every help-output
substring assertion through it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:50:52 +05:00
Alpamys 7da82d355a feat(adapters): v0.57.0 — git for LoRA (diff, merge, blame, branch)
Ships "soup adapters {diff,merge,blame,branch,checkout,branches}" — git-shaped
UX for LoRA adapter management. Pure-numpy math (no torch); TOCTOU defence on
every path; atomic writes throughout.

Part A — adapters diff (utils/adapter_diff.py, ~270 LOC):
  Per-layer ΔW Frobenius norm + relative drift; effective-rank delta via
  SVD entropy; top-K changed projections; JSON/Markdown/table output.

Part B — adapters merge (utils/adapter_merge.py, ~280 LOC):
  Four strategies — linear (weighted avg), ties (Yadav et al. — trim/
  elect-sign/disjoint), dare (Yu et al. — drop+rescale, deterministic),
  svd (low-rank reconstruction). MergeReport.verdict='UNKNOWN' is a stub;
  live canary verdict via v0.55 eval gate ships in v0.57.1.

Part C — adapters blame (utils/blame.py, ~190 LOC):
  Leave-one-out plan emitter + budget tracker (parse_budget mirrors v0.48.0
  data_mix idiom). Per-shard work table + feasibility check. Live ablation
  runner raises NotImplementedError v0.57.1 (mirrors v0.27.0 / v0.50.0 /
  v0.56.0 stub-then-live pattern).

Part D — adapters branch / checkout / branches (utils/adapter_branch.py,
~230 LOC):
  SHA-256 snapshot pointers under ~/.soup/branches/ (SOUP_BRANCHES_DIR
  override, $HOME/$CWD/$TMPDIR-bounded). Drift detection on checkout —
  refuses restore when source SHA != snapshot SHA. CRLF/null-byte
  rejection on env override (mirrors v0.51.0 hub-endpoint policy).

5-agent review-fix wave (1 CRITICAL + 9 HIGH + 11 MEDIUM + 4 LOW):
  - TIES tied-sign defaults to +1 (np.sign(0)==0 would silently zero all
    tied parameters)
  - load_branch / delete_branch reject symlinks via os.lstat + S_ISLNK
    before read/unlink
  - merge output safetensors + adapter_config.json atomic writes with
    symlink target rejection at output path; source config size-capped
    at 256 KB
  - compute_adapter_diff weights-file path symlink-rejected via lstat
    BEFORE is_file() (defends against .safetensors -> /etc/passwd escape)
  - _count_dataset_rows opens via realpath captured at containment check
    (closes TOCTOU window)
  - diff --output write is atomic (tempfile + os.replace)
  - SOUP_BRANCHES_DIR rejects every C0 control char, not just null
  - SUPPORTED_STRATEGIES is now frozenset (matches v0.41.0+ allowlist
    policy); STRATEGY_ORDER tuple preserved for canonical iteration
  - 5× pytest.raises(Exception) tightened to FrozenInstanceError
  - 2 zero-assertion Part D tests converted to real assertions
  - Added: bool base_model rejection, top_k boundary 1/201, density=1.0
    inclusive bound, inf weight rejection, tied-sign positive default,
    bool False for num_shards/budget_seconds, POSIX symlink rejections
    for diff weights / merge output / load_branch / delete_branch,
    no-top-level-torch source-grep guards, traversal delete_branch.

Plus v0.56.0 follow-up: test_v0560.py version-floor tests widened from
exact-match to floor-check (matches v0.51.0 / v0.54.0 idiom — every
subsequent release would otherwise edit this one line).

Test count: 8849 → 8998 (+149 net in 4 new files; 4 POSIX-only symlink
tests skipped on Windows). Full suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 18:37:14 +05:00
Alpamys a3810823d1 refactor(train): harden diagnose-gate rank guard from PR #169
PR #169 wired LOCAL_RANK==0 guard on _run_diagnose_gate so distributed
launches only run the gate on one worker per machine. Two minor polish
items on top of the merged version:

- Wrap the int() parse in try/except ValueError. A malformed LOCAL_RANK
  (garbage value from a misconfigured launcher) would previously crash
  the post-training gate. Falling back to True is safer than silently
  skipping the gate -- over-running is recoverable, under-running hides
  failures.
- Expand the docstring to explain why we use LOCAL_RANK (per-machine)
  rather than RANK (global): the gate reads the local output_dir, so
  one gate per machine is the right granularity for typical single-
  machine multi-GPU runs. Documents the choice for future readers.
- Add a focused test (test_diagnose_gate_handles_malformed_local_rank)
  asserting the safe fallback path.
2026-05-15 17:29:26 +05:00
Yixuan Xu 4c2a578ac0
Guard diagnose gate on distributed worker ranks (#169)
Co-authored-by: mzl2233 <mzl2233@users.noreply.github.com>
2026-05-15 17:27:54 +05:00
Alpamys 7d81496c69 test(diagnose): strip ANSI before help-output substring asserts (v0.56.0 CI fix)
Rich's CliRunner output on CI carries ANSI escape codes that split long
option names like `--badge` and `--diagnose-gate` across colour-reset
boundaries (`-\x1b[0m\x1b[1;36m-badge`), breaking naive `"--badge" in
result.output` substring checks. Same fix pattern as v0.55.0 CI hotfix.

Failures: tests/test_v0560.py::TestCli::test_diagnose_help and
TestTrainDiagnoseGate::test_help_lists_flag on all 9 CI matrix cells.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:16:54 +05:00
Alpamys 72189baba7 feat(diagnose): soup diagnose — post-training model report card (v0.56.0)
Six failure-mode probes (forgetting / refusal / format / mode_collapse /
memorization / contamination) + FailureReport frozen dataclass + SVG
badge + soup train --diagnose-gate. Same OK/MINOR/MAJOR taxonomy as
v0.26.0 Quant-Lobotomy.

- soup diagnose <run-id> [--evidence|--output|--badge|--attach-to-registry]
- soup train --diagnose-gate <evidence.json> refuses MAJOR runs
- diagnose_report added to registry._VALID_KINDS

Review wave (4 agents): 4 HIGH + 8 MEDIUM + 2 LOW addressed —
atomic+TOCTOU-safe badge write, typer.Exit (not sys.exit), realpath
containment, evidence size cap, contamination combined-complexity cap,
ReDoS probe, extras null-byte sanitisation, extract_row_text
centralisation, tokenize delegates to _eval_text.

Test count: 8676 -> 8849 (+123 in test_v0560.py + 50 net adjustments).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:03:00 +05:00
Alpamys 04c504e761 test(eval): strip ANSI before help-output substring asserts (v0.55.0 CI fix)
3 macOS CI failures from the v0.55.0 push — Rich wraps option names
with ANSI escapes when the terminal is narrow (macOS CI runners
default to a smaller width than Linux/Windows), so substring searches
like `"--goal" in result.output` fail because the actual output
contains `\x1b[1;36m-\x1b[0m\x1b[1;36m-goal\x1b[0m`.

Project precedent: v0.53.5 / v0.53.6 / v0.53.8 / v0.53.9 all hit the
same pattern; tests/test_auto_tuning.py and tests/test_eval_platform.py
already ship `_ANSI_RE` + `_strip_ansi` helpers.

Failures fixed:
  tests/test_v0550.py::TestCLIPlumbing::test_eval_design_help
  tests/test_v0550.py::TestEvalAgainst::test_against_help
  tests/test_v0550_followups.py::TestEvalAgainst::test_against_cli_help_lists_flag

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:10:53 +05:00
Alpamys 58d7d510bf feat(eval): soup eval design — derive evals from data (v0.55.0)
Trainer libraries help you RUN evals — none help you DEFINE them.
v0.55.0 closes that gap with 5 new subcommands:

- soup eval design <data> --goal "..."  → goal-conditioned EvalDesign
                                          (TF-IDF salience + scorer dispatch)
- soup eval discover <data>             → held-out canaries + memorization probes
                                          (farthest-first Jaccard clustering)
- soup eval lock + soup eval coverage   → SHA-256-checksummed artifact +
                                          gap analysis vs v0.54.0 task taxonomy
- soup eval gate-install --baseline R   → pre-push regression gate
                                          (paired-bootstrap CI, shlex.quote)
- soup eval against B --candidate C     → run-vs-run paired-bootstrap CI

Heuristic / CPU-only — no GPU required. Lazy imports across all 6 new
modules so `soup --help` startup remains < 200 ms.

New registry artifact kinds: eval_suite, canaries.
New tracker accessor: ExperimentTracker.get_metric_series(run_id, metric).

Security policy (all atomic-write + read surfaces):
  - cwd containment via os.path.realpath + commonpath
  - unconditional os.lstat + stat.S_ISLNK rejection (TOCTOU defence)
  - atomic write via tempfile.mkstemp + os.replace
  - shlex.quote for shell-script generation (NO hand-rolled escape)
  - MappingProxyType on every registry / metric / scorer map
  - frozen dataclass on every public return type
  - bool-as-int rejection on every numeric input
  - DoS caps: 10k subsample for TF-IDF + clustering hot paths

Review-fix coverage across 4 agents (python / security / code / tdd):
0 CRITICAL + 7 HIGH + 11 MEDIUM + 6 LOW resolved before commit.

Tests: 8571 → 8676 (+105 net).
Lint: ruff clean.
Smoke: every CLI command + every failure mode exercised in /tmp/soup_smoke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:44:14 +05:00
Alpamys 9e18643ea0 test(advise): fix cross-platform CI failures in test_v0540
Two failures on ubuntu/macos/windows × py3.9/3.11/3.12 after v0.54.0
push:

1. test_env_null_byte_falls_back: monkeypatch.setenv can't set raw
   NUL into the OS env layer (POSIX execve + Win32 SetEnv both
   refuse). Switched to a temporary `advise_history.os.environ` swap
   so the helper's defence-in-depth NUL guard is still exercised
   without going through the C env layer.

2. test_default_missing_data: Click 8.0–8.1 returns rc=0 on
   `no_args_is_help=True` invocations; Click 8.2+ returns rc=2 (the
   "missing command" convention). CI runners had the newer Click;
   dev box had the older. Accept both renderings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:58:58 +05:00
Alpamys 600686cd70 feat(advise): soup advise — pre-flight decision (v0.54.0)
`soup advise <data.jsonl> --goal "..."` returns one of PROMPT_ENG /
RAG / SFT / DPO / GRPO with a confidence, reason, and reverse-when
criterion BEFORE the user spends 8 hours on a GPU. Layer above
autopilot — autopilot picks hyperparams AFTER the training decision;
advise picks the training decision itself.

Three Parts:
- Part A: Verdict engine — TASK_CATEGORIES + CHOICES allowlists,
  frozen Verdict / DatasetProfile / ROIEstimate dataclasses, pure-
  Python classify_task + compute_dataset_profile + build_verdict
  rubric (DPO / GRPO floor 500 / PROMPT_ENG floor 50 / RAG / SFT).
- Part B: Probe runner — synth_probe_baselines + synth_probe_lora_delta
  heuristic stubs with forward-compat model/device/lr/timeout_seconds
  kwargs (v0.54.1 lifts to live model loading per stub-then-live
  cadence used by v0.27.0 MII / v0.37.0 multipack / v0.50.0 GRPO Plus).
- Part C: Cross-project learning — ~/.soup/advise_history.jsonl with
  cross-process file locking (fcntl on POSIX, sidecar <path>.lock +
  msvcrt on Windows). `soup advise compare` reads history; env
  override SOUP_ADVISE_HISTORY_PATH containment-checked to $HOME /
  $CWD / tempdir (mirrors v0.36.0 SOUP_BATCH_CACHE_PATH policy).

CLI: Typer subcommand group `run` / `explain` / `compare` plus argv
preprocessor in cli.py that maps `soup advise data.jsonl` →
`soup advise run data.jsonl`. Scoped to argv[1] == "advise" only
(code-review HIGH fix — defends against rewrites when an unrelated
arg contains the literal string "advise").

Schema: AdviseConfig (goal / probe / record) field on SoupConfig
honors the plan's cross-cutting bullet.

Security: cwd-containment + os.lstat + S_ISLNK symlink reject on
every path input; atomic writes via tempfile.mkstemp + os.replace
on scratch + history; per-line 64 KB cap + 16 MiB file cap on
history reads; bool / finite / NUL / oversize guards on every public
input; Rich markup escape on user-controlled output.

Reviewed by python / code / security / tdd / architect agents — every
finding fixed before commit (0 CRITICAL + 5 HIGH + 7 MEDIUM + 4 LOW).

Test count: 8400 → 8571 (+136 in tests/test_v0540.py, +35 net
adjustments to v0.53.x version-pin assertions to forward-compat >=).

Note: Windows CRLF / LF warnings during stage are .gitattributes-
governed and benign. CI runs on ubuntu-latest / windows-latest /
macos-latest × Python 3.9 / 3.11 / 3.12.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:48:19 +05:00
Alpamys cfdabf2b3b feat(v0.53.11): GRPO Plus finish + preference live
Closes v0.50.1 (#123, #126, #127), v0.49.1 (#119), v0.40.1 (#68).

#123 — live math kernels for 6 GRPO variants (gspo/dapo/dr_grpo/bnpo/
two_sided/rft) + `_GRPOTrainerVariant` HF Trainer subclass via
`make_grpo_trainer_variant` factory. Variant compute_loss reads kernel
inputs FIRST (no double-forward); falls back to super() only on missing
attrs. Case-insensitive variant normalisation before lru_cache.

#126 — PRMTrainerWrapper + `_PRMTrainer` HF Trainer subclass with real
compute_loss (gather hidden states at step_positions -> reward_head ->
MSE via compute_prm_loss). Dataset wrapped in datasets.Dataset.from_list
for HF Trainer compatibility. Bool-before-isinstance guard on batch_size.

#127 — GRPOStabilityCallback inherits transformers.TrainerCallback
(lazy), live EMA ref-model update in on_step_end with strict=True +
fallback-to-strict=False-with-WARNING on key mismatch (silent corruption
defence). math.isfinite guard on alpha.

#119 — LongLoRA forward override via LongLoRAForwardOverride context
manager with idempotent install (_soup_longlora_patched marker prevents
re-entry double-wrap), 256-char class name cap on regex match, restore
on __exit__ AND on exception.

#68 — true per-batch weighted-sum preference combine reading policy/ref
logps from TRL inputs + each compute_*_term kernel + combine_losses.
Explicit None checks on trainer attrs (no `or` on possibly-tensor),
DEBUG log on per-term skip.

Review fixes from 4 agents (python/code/security/tdd): 10 HIGH + 8
MEDIUM + 7 LOW — see CLAUDE.md v0.53.11 entry for the full list.

Test count: 8330 -> 8400 (+75 in test_v05311.py: 54 initial + 21
review-fix coverage gaps).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:40:34 +05:00
Alpamys 76f033a6ff feat(v0.53.10): Quick wins + packaging + UX wiring
7 issues closed:
- #150 [mix] pyproject extra bundles scikit-optimize so `soup data mix
  --optimize` runs the Bayesian loop instead of the v0.48.0 Dirichlet
  fallback; new describe_default_optimizer() helper labels the active
  backend without paying skopt's import cost.
- #113 [data-pro] extras (langdetect + presidio-analyzer) with lazy
  fall-through helpers in utils/data_score (broader language coverage +
  Presidio entity recognition on top of the v0.47.0 regex baseline).
  Llama-Guard-3-1B documented as a manual recipe (license + size).
- #154 SOUP_POSTHOG_KEY / SOUP_POSTHOG_ENDPOINT env override via
  sentinel-based explicit-vs-env precedence; HTTPS-only +
  RFC1918/link-local rejection on the endpoint; null-byte / control-char
  / >256-char rejection on the key.
- #152 --hub flag plumbed on chat / serve / infer / merge / export /
  push via shared utils/hubs.apply_hub_to_cli_model +
  prefetch_model_from_hub helpers; push uses upload_repo (skips
  HF-specific Collections + model-card auto-render on non-HF hubs).
- #153 `soup data download --hub modelscope|modelers` live SDK
  (lifts the v0.53.8 advisory-only path); friendly ImportError
  advisory when the SDK is missing.
- #155 Web UI Tool Outputs panel — `loadToolOutputs` polls
  /api/tool-outputs every 3s; XSS-safe DOM-built table (textContent
  per cell, no innerHTML for user-controlled fields); Bearer token
  threaded via the v0.53.9 window._authToken bootstrap.
- #156 SoupTrainerCallback.on_step_end records tool_calls counts
  from kwargs['inputs'] into the global tool buffer. Best-effort
  (# noqa: BLE001 per project policy — training must never crash).

13 review-fixes applied (4 HIGH / 5 MEDIUM / 4 LOW):
- HIGH PostHog explicit-endpoint precedence sentinel
- HIGH absolute path leak in local_path advisory reduced to relpath
- HIGH Rich markup escape on base / local_path / cache_dir
- HIGH callback # noqa: BLE001 per project policy
- MED `import time` moved out of try block
- MED oversize key + explicit-empty key rejection tests
- MED source-grep regression guards (advisory-removal, helper imports
  across 5 non-push commands)
- MED `prefetch_model_from_hub` outside-cwd cache_root rejection
- LOW empty-list + bool-True tool_calls no-op tests
- LOW push.py uses upload_repo + validate_hub_name regression guard

Test count: 8285 -> 8330 (+45 in tests/test_v05310.py).
Full suite green; ruff clean; on Win+Py3.10.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:58:20 +05:00
Alpamys e8422660a5 fix(tests): strip ANSI codes before help-output substring asserts (v0.53.9)
CI's Rich pipeline emits styled output that splits `--vocab-size` into
multiple ANSI-bracketed spans (e.g. `\x1b[36m-\x1b[0m\x1b[36m-vocab\x1b[0m\x1b[36m-size\x1b[0m`),
breaking naive `"--vocab-size" in result.output` checks. Locally Rich
auto-detects non-TTY and skips the codes, so the regression only shows
on CI (ubuntu/macos/windows × 3.9/3.11/3.12).

Fix: small `_plain()` helper using `re.sub(r"\x1b\[[0-9;]*m", "", ...)`
applied to the 7 failing assertions. Same approach already used in
several other v0.5x test modules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 09:43:45 +05:00
Alpamys bde9d149a0 feat(v0.53.9): Live Dashboard + UX + Bench + Standalone CLIs
Eight features that close out the v0.44.x live-monitoring deferrals
plus a long tail of standalone CLI wins:

- #94  /api/train/stream async SSE with per-subscriber cursor + JS
       EventSource consumer; SoupTrainerCallback pushes TrainEvent on
       each on_log.
- #95  soup ui --public derives LAN IP via SOCK_DGRAM connect-trick,
       prints scannable QR; --auth-token override; SPA bootstrap
       hydrates window._authToken from ?token= + sessionStorage and
       cleans the URL via history.replaceState; CORS regex auto-widens
       to loopback + RFC1918 in public mode; set_auth_token rotation
       race fixed via threading.Lock.
- #98  soup serve --reasoning-parser strips <think>...</think> (and
       OpenThinker tags); pre-compiled regex with marker-token
       fast-path + 1 MiB cap + leading-newline-only strip.
- #100 ToolOutputsBuffer global singleton + /api/tool-outputs JSON
       endpoint; best-effort observation hook in callback.on_log.
- #15  soup tokenizer train: BPE training CLI with raw-path lstat
       symlink rejection, 50 MiB total / 8 KiB per-line caps,
       post-mkdir output-dir re-check, --special-token NUL/oversize
       dedup, vocab bounds [256, 200000].
- #26  soup bench --p50 --p95 renders extra per-prompt tail-latency
       Rich table; --prompts-file gains symlink rejection.
- #28  soup bench --backend auto: MLX weights.npz probe (per-entry
       lstat) -> config.json model_type keyword -> transformers
       fallback; SOUP_BENCH_BACKEND env hint.
- #12  examples/synthetic_workflow.{md,yaml} end-to-end walkthrough.

Review fixes: 0 CRITICAL + 11 HIGH + 14 MEDIUM + 9 LOW across the
python / code / security / tdd review agents. Notable HIGH:
- QR token now consumed by SPA (was unreachable previously).
- set_auth_token rotation lock-protected, 8-thread stress tested.
- Tokenizer input + output symlink TOCTOU defence on raw path.
- SSE generator switched to async (asyncio.sleep) for non-blocking
  multi-subscriber operation.
- CORS regex for --public LAN mode (the old fixed allowlist of
  http://0.0.0.0:port never matched a real Origin header).
- _has_mlx_weights per-entry lstat so a symlinked weights.npz can't
  trigger MLX dispatch.

Test count: 8257 -> 8285 (+57 in tests/test_v0539.py, minus the
relaxed v0.53.8 version-pin asserts in tests/test_v0538.py).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 04:28:19 +05:00
Alpamys 13ffc89058 chore: ignore out/ directory (training run artifacts)
Local training runs write to ./out/ by default (e.g. .checkpoint.json
from soup train). Add it to the existing training-outputs gitignore
block alongside output/ and checkpoints/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 00:33:16 +05:00
Alpamys 53bb82afeb fix(v0.53.8.1): hatch artifacts directive — fix PyPI duplicate-filename 400
v0.53.8 PyPI publish failed with:
  400 Invalid distribution file. ZIP archive not accepted:
  Duplicate filename in local headers

Root cause: `[tool.hatch.build.targets.wheel.force-include]` shipped
`soup_cli/data/_fixtures/` AND `packages = ["soup_cli"]` recursed into
the same path, so both the wheel and sdist contained each JSONL twice.

Fix: switch from force-include to `artifacts = [...]` which adds
non-Python files to the existing package tree exactly once. Standard
hatchling pattern for shipping data files inside an already-packaged
directory.

Version bumped to v0.53.8.1 (patch) — same code surface, just a build
config fix. v0.53.8 GitHub release remains as the feature changelog;
PyPI ships under v0.53.8.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 00:18:20 +05:00
Alpamys 7b98982ab6 fix(test): v0.53.8 CI hardening — strip ANSI + use _repo_root() helper
Five v0.53.8 CI failures (ubuntu/macos × py3.9/3.11/3.12):

1. test_help_lists_hub_flag — Typer's Rich-rendered help wraps long
   option help across ANSI box-drawing lines; "--hub" appears as
   "│ --\nhub" in the CI terminal renderer. Strip ANSI + collapse
   whitespace before asserting.

2-5. test_pyproject_version / test_*_extra_present / test_force_include
   — used `Path("pyproject.toml")` (relative to cwd). CI invokes pytest
   from a different cwd than the repo root on at least one matrix
   entry. Switched to a `_repo_root()` helper that derives from
   `__file__` (matches v0.43.0 Part D demo_bundles approach).

Local re-run: 66/66 v0.53.8 tests pass after the fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 00:04:29 +05:00