From df78bd2dd1d9251f641e68bdde86192d81e4a8fa Mon Sep 17 00:00:00 2001 From: MsShawnP Date: Mon, 3 Aug 2026 13:17:34 -0400 Subject: [PATCH 1/6] Engagement scaffold: gitignore + deploy guard --- .dockerignore | 5 +++++ .gitignore | 8 ++++++++ scripts/engagement_guard.py | 25 +++++++++++++++++++++++++ scripts/git-hooks/pre-push | 24 ++++++++++++++++++++++++ 4 files changed, 62 insertions(+) create mode 100644 .dockerignore create mode 100644 scripts/engagement_guard.py create mode 100755 scripts/git-hooks/pre-push diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cc580a6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +# --- lailara engagement scaffold --- +client-data/ +client-output/ +engagement.yml +engagement.yaml diff --git a/.gitignore b/.gitignore index e304d4f..aae8cb2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,11 @@ node_modules/ # Security - secret leak prevention *.pem secrets.* + +# --- lailara engagement scaffold --- +# Client engagement data is runtime-only: never commit it, never deploy it. +client-data/ +client-output/ +/engagement.yml +/engagement.yaml +# (engagement.demo.yml and engagement.example.yml stay committable) diff --git a/scripts/engagement_guard.py b/scripts/engagement_guard.py new file mode 100644 index 0000000..ec77677 --- /dev/null +++ b/scripts/engagement_guard.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +"""Lailara engagement deploy guard (Python, stdlib-only). + +Exit 2 if an ACTIVE (non-demo) client engagement.yml is present in the current +directory. No-op otherwise, so demo builds and clean CI checkouts are unaffected. +Self-contained (no dependency on the installed lailara_engagement package) so it can +run in any repo's deploy/build environment. +""" +import os +import re +import sys + +for _f in ("engagement.yml", "engagement.yaml"): + if os.path.isfile(_f): + with open(_f, encoding="utf-8-sig") as _fh: + _txt = _fh.read() + if re.search(r"^\s*demo:\s*true\s*$", _txt, re.M): + continue # demo config -> safe + sys.stderr.write( + f"ENGAGEMENT GUARD: active client engagement config present ({_f}). " + "Client mode is runtime-only and must never deploy. Deactivate it " + "(set 'demo: true', or use engagement.demo.yml) before deploying.\n" + ) + raise SystemExit(2) +raise SystemExit(0) diff --git a/scripts/git-hooks/pre-push b/scripts/git-hooks/pre-push new file mode 100755 index 0000000..9be9de9 --- /dev/null +++ b/scripts/git-hooks/pre-push @@ -0,0 +1,24 @@ +#!/bin/sh +# Lailara engagement deploy guard (git pre-push hook). +# +# Refuses to push while an ACTIVE (non-demo) client engagement.yml is present in +# the working tree. Every tool repo auto-deploys on push, so blocking the push +# blocks the deploy — client mode is runtime-only and must never ship. +# +# No-op when no engagement.yml exists (demo builds and clean CI checkouts push +# normally), so demo behavior is unchanged. +# +# Activated per repo with: git config core.hooksPath scripts/git-hooks +set -e +for f in engagement.yml engagement.yaml; do + if [ -f "$f" ]; then + if grep -Eq '^[[:space:]]*demo:[[:space:]]*true[[:space:]]*$' "$f"; then + continue # demo config -> safe + fi + echo "ENGAGEMENT GUARD: active client engagement config present ($f)." >&2 + echo "Client mode is runtime-only and must never deploy. Deactivate it" >&2 + echo "(set 'demo: true', or remove/rename to engagement.demo.yml) before pushing." >&2 + exit 2 + fi +done +exit 0 From fb94d38f36992f9afbaa189f190d4a772ca3cd9e Mon Sep 17 00:00:00 2001 From: MsShawnP Date: Mon, 3 Aug 2026 14:26:55 -0400 Subject: [PATCH 2/6] Lock demo golden + P1 regressions (gtin-validator) Golden-file test pins the shipped sample dataset's batch summary (46 GTINs, 36 clean, score 82/B) so the deployed demo cannot drift during the client-mode conversion. Regression tests lock the two 07-31 audit P1s (already fixed on disk, verified with the audit's reproduction): - UPC-A and its case GTIN-14 share a company prefix (no false PREFIX_MISMATCH on sample rows 37-39). - INFO advisories (UPC_NOT_GTIN13) do not zero the clean count / grade. Co-Authored-By: Claude Opus 4.8 --- tests_golden.py | 78 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 tests_golden.py diff --git a/tests_golden.py b/tests_golden.py new file mode 100644 index 0000000..e7f6997 --- /dev/null +++ b/tests_golden.py @@ -0,0 +1,78 @@ +"""Demo golden-file lock + P1 regression tests for gtin-validator. + +The golden test pins the shipped sample dataset's batch summary so the deployed +demo experience cannot drift during the client-mode conversion. The regression +tests lock the two 07-31 audit P1s: + 1. A UPC-A and its case-level GTIN-14 share a company prefix (no false + PREFIX_MISMATCH on textbook unit/case pairs — sample rows 37-39). + 2. INFO advisories (UPC_NOT_GTIN13) do not zero the clean count. +""" + +import csv +import io + +from gtin_core import validate_batch, validate_single_gtin +from sample_data import SAMPLE_DATA + + +def _sample_gtins(): + rows = list(csv.reader(io.StringIO(SAMPLE_DATA.strip())))[1:] + return [r[0] for r in rows] + + +class TestDemoGolden: + """Locks the deployed demo output. If this changes, the live site changes.""" + + def test_sample_summary_is_locked(self): + batch = validate_batch(_sample_gtins()) + assert batch["summary"] == { + "total_gtins": 46, + "valid": 40, + "critical_issues": 6, + "warnings": 4, + "clean": 36, + "duplicate_groups": 2, + "unique_prefixes": 3, + } + + def test_sample_score_is_locked(self): + batch = validate_batch(_sample_gtins()) + assert batch["score"]["score"] == 82 + assert batch["score"]["grade"] == "B" + + +class TestPrefixParityRegression: + """07-31 P1: unit UPC and its case GTIN-14 must share a company prefix.""" + + def test_sample_case_rows_37_39_no_false_prefix_mismatch(self): + batch = validate_batch(_sample_gtins()) + for r in batch["results"]: + if r.row_number in (37, 38, 39): # the GTIN-14 case rows + assert not any(i.code == "PREFIX_MISMATCH" for i in r.issues), ( + f"row {r.row_number} ({r.cleaned}) false-failed PREFIX_MISMATCH" + ) + + def test_upc_and_case_gtin14_share_prefix(self): + unit = validate_single_gtin("614141000012", 1) + case = validate_single_gtin("10614141000019", 2) + assert unit.company_prefix == case.company_prefix == "0614141" + + +class TestCleanCountRegression: + """07-31 P1: INFO advisories must not zero the clean count / grade.""" + + def test_all_upc_file_counts_as_clean(self): + # Every valid UPC-A gets an INFO UPC_NOT_GTIN13 advisory; the file must + # still read as clean and score Grade A, not "0 passed with no issues". + valid_upcs = ["614141000012", "614141000029", "614141000036", "614141000043"] + batch = validate_batch(valid_upcs) + assert batch["summary"]["clean"] == 4 + assert batch["summary"]["total_gtins"] == 4 + assert batch["score"]["grade"] == "A" + + def test_info_only_row_is_clean(self): + batch = validate_batch(["614141000012"]) # one valid UPC-A -> INFO only + r = batch["results"][0] + assert r.issues # it does carry the INFO advisory + assert all(i.severity.value == "Info" for i in r.issues) + assert batch["summary"]["clean"] == 1 From 0f790a3e6d293de4c4d43f80e2837402c2d57dce Mon Sep 17 00:00:00 2001 From: MsShawnP Date: Mon, 3 Aug 2026 14:28:45 -0400 Subject: [PATCH 3/6] Add gtin-validator client mode (engagement.yml + provenance report) Client-mode CLI wraps the existing gtin_core engine with lailara_engagement: tolerant CSV/XLSX intake (GTIN read as text), preflight that names the GTIN column via engagement.yml (Data Readiness Report if missing), and a branded, provenance-footed, draft-watermarked readiness summary + CSV report written to client-output/ only. INPUT-SPEC.md documents the required column and mapping. 4 client-mode tests (clean file, missing-column blocked, BOM+semicolon GTIN as text, --final). 74 tests pass; demo golden unchanged. Co-Authored-By: Claude Opus 4.8 --- INPUT-SPEC.md | 54 +++++++++++ client_mode.py | 216 +++++++++++++++++++++++++++++++++++++++++++ engagement.demo.yml | 10 ++ tests_client_mode.py | 77 +++++++++++++++ 4 files changed, 357 insertions(+) create mode 100644 INPUT-SPEC.md create mode 100644 client_mode.py create mode 100644 engagement.demo.yml create mode 100644 tests_client_mode.py diff --git a/INPUT-SPEC.md b/INPUT-SPEC.md new file mode 100644 index 0000000..2f3ba00 --- /dev/null +++ b/INPUT-SPEC.md @@ -0,0 +1,54 @@ +# INPUT-SPEC — gtin-validator (client mode) + +What to hand the validator in a client engagement. Written so a client's IT person can +produce the file without a call. + +## The file + +- **CSV or XLSX.** Read via `lailara_engagement`'s tolerant reader: UTF-8 / UTF-8-BOM / + latin-1; comma / semicolon / tab; leading blank rows and trailing junk dropped; header + whitespace trimmed. +- One row per product. Extra columns are ignored. + +## Required column + +| Canonical | Type | Required | Used for | +|---|---|---|---| +| `gtin` | identifier (text) | yes | The GTIN/UPC/barcode validated against GS1 standards. §1 | + +- **Read as text.** GTINs keep leading zeros; `012345678905` is never parsed to `12345678905`. +- Accepted GTIN lengths: 8, 12 (UPC-A), 13 (EAN), 14 (ITF-14 case). The engine checks the + mod-10 check digit, GTIN-14 indicator rules, duplicates, company-prefix consistency, and + unit→case hierarchy. + +## Column mapping (engagement.yml) + +If the client's header isn't literally `gtin`, map it: + +```yaml +client: + name: "Meridian Farms" +engagement: + id: "MER-2026-08" +as_of_date: "2026-07-31" +columns: + gtin: "UPC / Barcode" # client header -> canonical +``` + +A case/whitespace-insensitive match (e.g. `GTIN`, `UPC`, `barcode`) is auto-detected and +disclosed; anything else must be mapped here. If no GTIN column resolves, the run produces a +**Data Readiness Report** naming the missing column instead of results. + +## Run + +```bash +# with lailara_engagement installed: pip install -e ../engagement-template/lib +python client_mode.py --config engagement.yml --input client-data/items.csv \ + --out client-output [--final] +``` + +Outputs to `client-output/` (gitignored): +- `gtin-readiness-summary.html` — branded, provenance-footed (input SHA-256, row counts, + `as_of_date`, config hash), DRAFT-watermarked until `--final`. +- `gtin-validation.csv` — the full per-GTIN report. +- or `data-readiness-report.html` if the GTIN column is missing. diff --git a/client_mode.py b/client_mode.py new file mode 100644 index 0000000..0db3758 --- /dev/null +++ b/client_mode.py @@ -0,0 +1,216 @@ +"""Client-mode CLI for the GTIN validator. + +Wraps the existing validation engine (`gtin_core`) with the shared +``lailara_engagement`` scaffold so a client's product-master export can be +validated locally: tolerant CSV/XLSX intake (GTIN read as text), a preflight +that names the GTIN column via ``engagement.yml`` (Data Readiness Report if it's +missing), and a branded, provenance-footed, draft-watermarked readiness summary +plus the standard CSV report — all written to ``client-output/`` only. + +Usage: + python client_mode.py --config engagement.yml --input client-data/items.csv \ + --out client-output [--final] +""" + +from __future__ import annotations + +import argparse +import html +import sys +from pathlib import Path + +from lailara_engagement import ( + ColumnSpec, + PreflightSpec, + build_provenance, + load_config, + read_table, + render_html, + run_preflight, + validation_status_label, + write_report, +) +from lailara_engagement import palette as P +from lailara_engagement.provenance import Provenance + +from csv_report import generate_csv_report +from gtin_core import validate_batch + +TOOL = "gtin-validator" +TOOL_VERSION = "1.0" + + +def _gtin_spec() -> PreflightSpec: + return PreflightSpec( + tool=TOOL, + version=TOOL_VERSION, + columns=[ + ColumnSpec( + name="gtin", + dtype="identifier", + required=True, + description="the product GTIN/UPC/barcode to validate", + spec_ref="INPUT-SPEC §1", + ) + ], + ) + + +def _summary_html(config, batch, provenance: Provenance, *, draft: bool) -> str: + esc = html.escape + s = batch["summary"] + score = batch["score"] + draft_class = " ll-draft" if draft else "" + # top issue types (non-INFO), by count + from collections import Counter + codes = Counter( + i.code for r in batch["results"] for i in r.issues if i.severity.value != "Info" + ) + issue_rows = "".join( + f"{esc(code)}{n}" + for code, n in codes.most_common(10) + ) or "No critical or warning issues." + + grade_fill = P.LL_HK_SURFACE if score["grade"] in ("A", "B") else P.LL_SG_SURFACE + grade_text = P.LL_HK_DARK if score["grade"] in ("A", "B") else P.LL_SG_DARK + + return f""" + +GTIN Readiness — {esc(config.client_name)} + +
+
+
Lailara LLC · GTIN Validation
+

GTIN Readiness Summary

+
+
Client {esc(config.client_name)}
+
Engagement {esc(config.engagement_id)}
+
As of {esc(config.as_of_date.isoformat())}
+
+
+
+
Score {score['score']}/100 · Grade {esc(score['grade'])}
+
{esc(score['interpretation'])}
+
+
+

Batch summary

+ + + + + + +
Total GTINs{s['total_gtins']:,}
Clean (no critical/warning){s['clean']:,}
Critical issues{s['critical_issues']:,}
Warnings{s['warnings']:,}
Duplicate groups{s['duplicate_groups']:,}
+
+
+

Issues by type

+ + {issue_rows}
CodeCount
+
+{provenance.to_html()} +
""" + + +def _css(draft: bool) -> str: + draft_css = ( + ".ll-draft::before{content:'DRAFT';position:fixed;top:50%;left:50%;" + "transform:translate(-50%,-50%) rotate(-32deg);font-family:var(--s);" + "font-size:22vw;font-weight:700;color:rgba(204,16,10,.06);z-index:0;" + "pointer-events:none;white-space:nowrap}" if draft else "" + ) + return f""" +:root{{--s:{P.LL_SERIF};--f:{P.LL_SANS}}} +*{{box-sizing:border-box}} +body{{margin:0;background:{P.LL_CANVAS};color:{P.LL_TEXT};font-family:var(--f);line-height:1.6}} +.ll-page{{position:relative;z-index:1;max-width:{P.LL_MAX_WIDTH};margin:0 auto;padding:48px 24px}} +.ll-header{{border-bottom:1px solid {P.LL_GRIDLINE};padding-bottom:24px;margin-bottom:24px}} +.ll-eyebrow{{font-size:12px;letter-spacing:.04em;text-transform:uppercase;color:{P.LL_RED};font-weight:600}} +.ll-title{{font-family:var(--s);font-weight:700;color:{P.LL_INK};font-size:34px;margin:8px 0 16px}} +.ll-client{{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px 24px;font-size:14px}} +.ll-k{{display:block;color:{P.LL_TEXT_SEC};font-size:11px;text-transform:uppercase;letter-spacing:.04em}} +.ll-banner{{border-radius:2px;padding:16px 20px;margin-bottom:32px}} +.ll-score{{font-family:var(--s);font-weight:700;font-size:22px}} +.ll-h2{{font-family:var(--s);font-weight:700;color:{P.LL_INK};font-size:22px;margin:0 0 12px;padding-bottom:6px;border-bottom:1px solid {P.LL_GRIDLINE}}} +.ll-table{{width:100%;border-collapse:collapse;font-size:14px}} +.ll-table th{{text-align:left;background:{P.LL_CHICAGO};color:#fff;padding:8px 12px}} +.ll-table td{{padding:8px 12px;border-bottom:1px solid {P.LL_GRIDLINE}}} +.mono{{font-family:ui-monospace,Consolas,monospace;font-size:12px}} +.num{{text-align:right;font-variant-numeric:tabular-nums}} +.ll-provenance{{margin-top:40px;background:{P.LL_CARD_BG};color:{P.LL_CARD_TEXT};padding:20px 24px;border-radius:2px;font-size:13px}} +.ll-prov-title{{font-family:var(--s);font-weight:700;font-size:16px;margin-bottom:8px}} +.ll-provenance div{{margin-bottom:4px;color:{P.LL_CARD_SUBTITLE}}} +.ll-provenance strong{{color:{P.LL_CARD_TEXT}}} +.ll-prov-inputs{{width:100%;border-collapse:collapse;margin-top:8px}} +.ll-prov-inputs th{{text-align:left;border-bottom:1px solid rgba(255,255,255,.12);padding:4px 8px;color:{P.LL_CARD_MUTED}}} +.ll-prov-inputs td{{padding:4px 8px;border-bottom:1px solid rgba(255,255,255,.08);color:{P.LL_CARD_SUBTITLE}}} +.ll-prov-brand{{margin-top:12px;font-family:var(--s);color:{P.LL_CARD_MUTED}}} +{draft_css} +@media print{{body{{background:#fff}}}} +""" + + +def run(config_path: str, input_path: str, out_dir: str, *, final: bool = False) -> dict: + config = load_config(config_path) + read = read_table(input_path) + spec = _gtin_spec() + report = run_preflight(read, spec, config) + + out = Path(out_dir) + out.mkdir(parents=True, exist_ok=True) + + provenance = build_provenance( + tool=TOOL, tool_version=TOOL_VERSION, inputs=[read], config=config, + validation_status=validation_status_label(report.status, report.n_warnings), + ) + + # Preflight gate: no GTIN column -> Data Readiness Report, no results. + if not report.passed: + paths = write_report(report, config, str(out), provenance=provenance, + draft=not final, basename="data-readiness-report", + title="GTIN Data Readiness Report") + return {"status": "blocked", "readiness_report": paths["html"], "report": paths["html"]} + + gtin_col = report.column_mapping["gtin"] + gtins = [v for v in read.frame[gtin_col].astype(str)] + batch = validate_batch(gtins) + + # CSV report (reuse the existing generator) + csv_path = out / "gtin-validation.csv" + csv_path.write_text(generate_csv_report(batch), encoding="utf-8") + + # Branded, provenance-footed readiness summary + summary_path = out / "gtin-readiness-summary.html" + summary_path.write_text(_summary_html(config, batch, provenance, draft=not final), + encoding="utf-8") + + return { + "status": "ok", + "score": batch["score"]["score"], + "grade": batch["score"]["grade"], + "clean": batch["summary"]["clean"], + "total": batch["summary"]["total_gtins"], + "csv": str(csv_path), + "report": str(summary_path), + } + + +def main(argv=None) -> int: + ap = argparse.ArgumentParser(prog="gtin client mode", + description="Validate a client GTIN file in engagement mode.") + ap.add_argument("--config", required=True) + ap.add_argument("--input", required=True) + ap.add_argument("--out", default="client-output") + ap.add_argument("--final", action="store_true") + args = ap.parse_args(argv) + result = run(args.config, args.input, args.out, final=args.final) + if result["status"] == "blocked": + print(f"BLOCKED — data not ready. See {result['readiness_report']}") + return 3 + print(f"scored {result['score']}/100 (Grade {result['grade']}); " + f"{result['clean']}/{result['total']} clean") + print(f"report -> {result['report']}\ncsv -> {result['csv']}") + return 0 + + +if __name__ == "__main__": # pragma: no cover + raise SystemExit(main()) diff --git a/engagement.demo.yml b/engagement.demo.yml new file mode 100644 index 0000000..36d952d --- /dev/null +++ b/engagement.demo.yml @@ -0,0 +1,10 @@ +# Demo engagement config (safe to deploy: demo: true). +client: + name: "Cinderhaven Foods (demo)" +engagement: + id: "DEMO-GTIN-001" +as_of_date: "2026-01-31" +prepared_by: "Lailara LLC" +demo: true +columns: + gtin: "GTIN" diff --git a/tests_client_mode.py b/tests_client_mode.py new file mode 100644 index 0000000..7e2840c --- /dev/null +++ b/tests_client_mode.py @@ -0,0 +1,77 @@ +"""Client-mode tests for gtin-validator: intake, preflight, provenance report. + +Adversarial fixtures per checklist §6: missing GTIN column (blocked path), +BOM+semicolon, Excel-mangled GTIN read as text, and a clean file. +Skipped if lailara_engagement isn't installed. +""" + +import pytest + +pytest.importorskip("lailara_engagement") + +import client_mode # noqa: E402 + +_CONFIG = """ +client: {name: Meridian Farms} +engagement: {id: MER-2026-08} +as_of_date: 2026-07-31 +demo: true +columns: {gtin: "UPC / Barcode"} +""" + + +@pytest.fixture +def cfg(tmp_path): + p = tmp_path / "engagement.demo.yml" + p.write_text(_CONFIG, encoding="utf-8") + return str(p) + + +def _write(tmp_path, name, text, encoding="utf-8"): + p = tmp_path / name + p.write_bytes(text.encode(encoding) if isinstance(text, str) else text) + return str(p) + + +def test_clean_file_scores_and_reports(cfg, tmp_path): + src = _write(tmp_path, "items.csv", + "UPC / Barcode\n614141000012\n614141000029\n614141000036\n") + out = str(tmp_path / "client-output") + result = client_mode.run(cfg, src, out) + assert result["status"] == "ok" + assert result["grade"] == "A" # all valid UPCs -> Grade A (INFO not counted) + assert result["clean"] == 3 + html = open(result["report"], encoding="utf-8").read() + assert "Meridian Farms" in html + assert "#f5f3ee" in html # branded canvas + assert "SHA-256" in html # provenance footer + assert "DRAFT" in html + + +def test_missing_gtin_column_is_blocked(cfg, tmp_path): + # No column maps to gtin -> Data Readiness Report, no results. + src = _write(tmp_path, "bad.csv", "product,price\nA,1\nB,2\n") + out = str(tmp_path / "out") + result = client_mode.run(cfg, src, out) + assert result["status"] == "blocked" + html = open(result["readiness_report"], encoding="utf-8").read() + assert "gtin" in html.lower() + + +def test_bom_semicolon_and_gtin_as_text(cfg, tmp_path): + body = "UPC / Barcode;name\n0614141000012;A\n614141000029;B\n" + src = _write(tmp_path, "bom.csv", body) + out = str(tmp_path / "out") + result = client_mode.run(cfg, src, out) + assert result["status"] == "ok" + # leading-zero GTIN preserved as text through the CSV report + csv_text = open(result["csv"], encoding="utf-8").read() + assert "0614141000012" in csv_text + + +def test_final_flag_drops_watermark(cfg, tmp_path): + src = _write(tmp_path, "items.csv", "UPC / Barcode\n614141000012\n") + out = str(tmp_path / "out") + result = client_mode.run(cfg, src, out, final=True) + html = open(result["report"], encoding="utf-8").read() + assert "ll-draft" not in html From 851231b8c3fe2df007bf7b56fcfc734f4c321588 Mon Sep 17 00:00:00 2001 From: MsShawnP Date: Mon, 3 Aug 2026 14:36:46 -0400 Subject: [PATCH 4/6] Wire demo golden into CI (gtin-validator) Rename tests_golden.py/tests_client_mode.py to test_golden.py/test_client_mode.py and add test_golden.py to the CI core-test command so the demo golden + P1 regressions run on every commit (CI invokes files explicitly). Client-mode tests stay local (require the path-installed lailara_engagement). Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 2 +- tests_client_mode.py => test_client_mode.py | 0 tests_golden.py => test_golden.py | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename tests_client_mode.py => test_client_mode.py (100%) rename tests_golden.py => test_golden.py (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b281af4..dec8951 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: pip install -r requirements.txt pytest - name: Run core tests - run: pytest tests.py -v + run: pytest tests.py test_golden.py -v test-api: runs-on: ubuntu-latest diff --git a/tests_client_mode.py b/test_client_mode.py similarity index 100% rename from tests_client_mode.py rename to test_client_mode.py diff --git a/tests_golden.py b/test_golden.py similarity index 100% rename from tests_golden.py rename to test_golden.py From a83d837bfa37d2760aa99b4a990523985c5d3800 Mon Sep 17 00:00:00 2001 From: MsShawnP Date: Tue, 4 Aug 2026 12:26:16 -0400 Subject: [PATCH 5/6] Rename demo client name to canonical "Cinderhaven Provisions" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit engagement.demo.yml carried "Cinderhaven Foods (demo)" — a retired brand-name error (canonical is Cinderhaven Provisions; canonical_values.json flags "Foods"). The canonical-drift gate correctly blocked it. Rename to match the roster's other demo configs. Deployed demo golden is unaffected — client_mode reads this config, the deployed app does not (test_golden.py still passes, unchanged). Co-Authored-By: Claude Opus 4.8 --- engagement.demo.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/engagement.demo.yml b/engagement.demo.yml index 36d952d..f542438 100644 --- a/engagement.demo.yml +++ b/engagement.demo.yml @@ -1,6 +1,6 @@ # Demo engagement config (safe to deploy: demo: true). client: - name: "Cinderhaven Foods (demo)" + name: "Cinderhaven Provisions (demo)" engagement: id: "DEMO-GTIN-001" as_of_date: "2026-01-31" From 24fc99ee3ec671487f3f54455e6163b6bfbd6ff9 Mon Sep 17 00:00:00 2001 From: MsShawnP Date: Tue, 4 Aug 2026 12:26:17 -0400 Subject: [PATCH 6/6] Fix ruff lint in client_mode.py Remove unused imports (sys, render_html) and split three CSS-in-f-string rules at property boundaries so each physical line is <=120. CSS is whitespace-insensitive between properties, so the rendered report is unchanged; no golden covers client_mode output. ruff check . clean. Co-Authored-By: Claude Opus 4.8 --- client_mode.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/client_mode.py b/client_mode.py index 0db3758..00bb4bc 100644 --- a/client_mode.py +++ b/client_mode.py @@ -16,7 +16,6 @@ import argparse import html -import sys from pathlib import Path from lailara_engagement import ( @@ -25,7 +24,6 @@ build_provenance, load_config, read_table, - render_html, run_preflight, validation_status_label, write_report, @@ -130,18 +128,21 @@ def _css(draft: bool) -> str: .ll-k{{display:block;color:{P.LL_TEXT_SEC};font-size:11px;text-transform:uppercase;letter-spacing:.04em}} .ll-banner{{border-radius:2px;padding:16px 20px;margin-bottom:32px}} .ll-score{{font-family:var(--s);font-weight:700;font-size:22px}} -.ll-h2{{font-family:var(--s);font-weight:700;color:{P.LL_INK};font-size:22px;margin:0 0 12px;padding-bottom:6px;border-bottom:1px solid {P.LL_GRIDLINE}}} +.ll-h2{{font-family:var(--s);font-weight:700;color:{P.LL_INK};font-size:22px; +margin:0 0 12px;padding-bottom:6px;border-bottom:1px solid {P.LL_GRIDLINE}}} .ll-table{{width:100%;border-collapse:collapse;font-size:14px}} .ll-table th{{text-align:left;background:{P.LL_CHICAGO};color:#fff;padding:8px 12px}} .ll-table td{{padding:8px 12px;border-bottom:1px solid {P.LL_GRIDLINE}}} .mono{{font-family:ui-monospace,Consolas,monospace;font-size:12px}} .num{{text-align:right;font-variant-numeric:tabular-nums}} -.ll-provenance{{margin-top:40px;background:{P.LL_CARD_BG};color:{P.LL_CARD_TEXT};padding:20px 24px;border-radius:2px;font-size:13px}} +.ll-provenance{{margin-top:40px;background:{P.LL_CARD_BG};color:{P.LL_CARD_TEXT}; +padding:20px 24px;border-radius:2px;font-size:13px}} .ll-prov-title{{font-family:var(--s);font-weight:700;font-size:16px;margin-bottom:8px}} .ll-provenance div{{margin-bottom:4px;color:{P.LL_CARD_SUBTITLE}}} .ll-provenance strong{{color:{P.LL_CARD_TEXT}}} .ll-prov-inputs{{width:100%;border-collapse:collapse;margin-top:8px}} -.ll-prov-inputs th{{text-align:left;border-bottom:1px solid rgba(255,255,255,.12);padding:4px 8px;color:{P.LL_CARD_MUTED}}} +.ll-prov-inputs th{{text-align:left;border-bottom:1px solid rgba(255,255,255,.12); +padding:4px 8px;color:{P.LL_CARD_MUTED}}} .ll-prov-inputs td{{padding:4px 8px;border-bottom:1px solid rgba(255,255,255,.08);color:{P.LL_CARD_SUBTITLE}}} .ll-prov-brand{{margin-top:12px;font-family:var(--s);color:{P.LL_CARD_MUTED}}} {draft_css}