Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/client-mode.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: client-mode

# Runs the client-mode suite (checklist §6), which imports lailara_engagement.
# The lib is installed from the private repo pinned to v0.2.1 via the
# LAILARA_ENGAGEMENT_TOKEN secret (fine-grained, read-only). This is the ONLY job
# that depends on the PAT; the demo golden gate stays credential-free, so a token
# expiry degrades client-mode coverage without ever reddening the demo invariant.

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

jobs:
client-mode:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Engagement guard
run: python3 scripts/engagement_guard.py
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Verify engagement token is present
env:
LAILARA_ENGAGEMENT_TOKEN: ${{ secrets.LAILARA_ENGAGEMENT_TOKEN }}
run: |
if [ -z "${LAILARA_ENGAGEMENT_TOKEN}" ] || [ "${LAILARA_ENGAGEMENT_TOKEN}" = "-" ]; then
echo "::error::LAILARA_ENGAGEMENT_TOKEN is empty or unavailable (literal '-' means the secret was set with 'gh secret set --body -'). Re-set via stdin: gh secret set LAILARA_ENGAGEMENT_TOKEN --repo MsShawnP/<repo> < tokenfile"
exit 1
fi
- name: Install (repo runtime + lailara_engagement @ v0.2.1)
env:
LAILARA_ENGAGEMENT_TOKEN: ${{ secrets.LAILARA_ENGAGEMENT_TOKEN }}
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt pytest
pip install "lailara_engagement @ git+https://x-access-token:${LAILARA_ENGAGEMENT_TOKEN}@github.com/MsShawnP/lailara-engagement.git@v0.2.2#subdirectory=lib"
- name: Client-mode suite
run: python -m pytest tests/test_client_mode.py -q
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Engagement guard
run: python3 scripts/engagement_guard.py

- name: Setup Node.js
uses: actions/setup-node@v4
with:
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/golden.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: golden

# Gates the demo golden lock: freezes the deployed demo's byte-level exports and
# headline figures so they cannot drift during the client-mode conversion. Runs
# ONLY the golden (tests/test_demo_golden.py) — it needs base deps only (pytest +
# stdlib, reads committed JSON), never lailara_engagement.

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

jobs:
golden:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install test deps
run: pip install pytest
- name: Demo golden lock
run: python -m pytest tests/test_demo_golden.py -q
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,11 @@ secrets.*

# dbt local user config
dbt/.user.yml

# --- 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)
90 changes: 90 additions & 0 deletions INPUT-SPEC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# INPUT-SPEC — dimension-weight-integrity (client mode)

What to hand the physical-attribute readiness check in a client engagement. Written so
a client's IT/data person can produce the file without a call.

## The file

- **One item-master export: 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; Excel dates/numbers rendered as text.
- **One row per SKU (per case configuration).** Extra columns are ignored.
- This is the client's item master — the case dimensions and weights one system holds.
The full four-system divergence and cost model (ERP vs WMS vs GDSN vs DTC) is the dbt
pipeline's job and takes four extracts; client mode validates one file and computes the
per-SKU physical attributes (cube, density, freight class) it implies.

## Required columns

These are the fields freight class is computed from — the "case dimensions + weights" the
readiness check exists to validate. If any is missing, the run produces a **Data Readiness
Report** naming it instead of results.

| Canonical | Type | Required | Used for |
|---|---|---|---|
| `sku` | identifier (text) | yes | Row key; deduplicated and reported per SKU. §1 |
| `case_length_in` | number | yes | Case length (inches) → cube → density → freight class. §2 |
| `case_width_in` | number | yes | Case width (inches). §2 |
| `case_height_in` | number | yes | Case height (inches). §2 |
| `case_gross_weight_lb` | number (≥ 0) | yes | Case gross weight (lb) → density → freight class. §2 |

- **Identifiers read as text.** `sku` keeps leading zeros; a numeric-looking SKU is never
parsed to a number.
- **Dimensions and weights are numeric and non-negative.** Zero or blank case dimensions
make cube (and therefore density and freight class) uncomputable for that row; those rows
are counted and disclosed, never silently assumed.

## Optional columns

Present → used; absent → skipped and disclosed. None blocks the run.

| Canonical | Type | Used for |
|---|---|---|
| `product_name` | string | Human label in the report. |
| `gtin` | identifier (text) | Carried through for cross-reference; not validated here (see the GTIN validator tool). |
| `unit_net_weight_lb` | number (≥ 0) | DTC parcel billable-weight check, when a DTC parcel gross weight is also supplied. §3 |
| `case_pack_qty` | integer | Units per case; reported for context. |
| `dtc_parcel_gross_lb` | number (≥ 0) | Actual DTC parcel weight → DIM/billable-weight vs listed net → parcel reweigh exposure. §3 |

## Column mapping (engagement.yml)

If the client's headers are not the canonical names, map them. A case/whitespace-insensitive
exact match (e.g. `Case Length (in)` → `case_length_in`) is auto-detected and disclosed;
anything else must be mapped here.

```yaml
client:
name: "Meridian Farms"
engagement:
id: "MER-2026-08"
as_of_date: "2026-07-31"
columns:
sku: "Item #"
case_length_in: "Case L (in)"
case_width_in: "Case W (in)"
case_height_in: "Case H (in)"
case_gross_weight_lb: "Case Wt (lb)"
```

## Run

```bash
# with lailara_engagement installed: pip install -e ../engagement-template/lib
python client_mode.py --config engagement.yml --input client-data/item_master.csv \
--out client-output [--final]
```

Outputs to `client-output/` (gitignored):
- `dimension-readiness-summary.html` — branded, provenance-footed (input SHA-256, row
counts, `as_of_date`, config hash), DRAFT-watermarked until `--final`.
- `dimension-readiness.csv` — per-SKU cube, density, and freight class.
- or `data-readiness-report.html` if a required column is missing.

## What is computed vs configured

- **Computed (physics/standards, never asserted):** case cube, density, NMFC freight
class, DTC DIM weight, billable weight. Same math as the dbt macros (`dimension_physics.py`).
- **Configured (`config/cost_params.yml`):** the DTC box size and DIM divisor used for the
optional parcel check. Rate tables and annual volumes are **not** applied on the
single-file client path — dollar-cost lanes need the four-system divergence the pipeline
builds, so client mode reports physical readiness, not a cost estimate.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,27 @@ python -m pytest tests/ -v
cd frontend && npm test
```

### Client engagement use

To validate a client's item master and compute its physical attributes locally
(no database, no deploy), install the shared scaffold and run client mode:

```
python -m pip install -e ../engagement-template/lib
python client_mode.py --config engagement.yml --input client-data/item_master.csv \
--out client-output [--final]
```

It reads CSV/XLSX tolerantly (SKU kept as text), runs a preflight that emits a
branded **Data Readiness Report** if a required case dimension/weight column is
missing, then computes cube, density, and NMFC freight class per SKU into a
branded, provenance-footed, DRAFT-watermarked report in `client-output/`
(gitignored). Required fields, mapping, and scope are in
[`INPUT-SPEC.md`](INPUT-SPEC.md). `engagement.demo.yml` is a safe-to-deploy
example (`demo: true`); a real `engagement.yml` is runtime-only and never
deploys (`scripts/engagement_guard.py` enforces this). The four-system
divergence and dollar-cost model below is the dbt pipeline and is unchanged.

### Deploy

**Pushing to `main` deploys the site.** The Cloudflare Pages project is
Expand Down
Loading
Loading