Skip to content

feat(epcis): SEL-598 lossless lot identity normalizer (H2 Preview) - #3728

Closed
PetrefiedThunder wants to merge 1 commit into
mainfrom
pi/epcis-h2-sel598-lot
Closed

PetrefiedThunder wants to merge 1 commit into
mainfrom
pi/epcis-h2-sel598-lot

Conversation

@PetrefiedThunder

@PetrefiedThunder PetrefiedThunder commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Phase B H2 (SEL-598): one shared lossless lot identity normalizer so legacy extraction, canonical ingest, and EPCIS export agree.

  • Canonical stored id is the full LGTIN URN or GS1 Digital Link (01+10) when present.
  • Bare lot token is derived for UI, gap reports, and Digital Link rebuild.
  • Export _epc_class prefers a stored class URI unchanged and never nests a full URI under /10/.
  • Rollback: LOT_IDENTITY_LOSSLESS=0 restores segment-only canonical writes.
  • Synthetic fixtures only (sel598-both-pass, sel598-lenient-only). No partner data. No production ingest.

A-2165 fixture tests

  • Spec §7.12 / §9 SEL-598 cases live in this PR (tests/shared/test_epcis_lot_identity_sel598.py plus extraction/export extensions).

Tests

  • pytest tests/shared/test_epcis_lot_identity_sel598.py services/ingestion/tests/test_epcis_extraction_1342.py services/ingestion/tests/test_epcis_export_gs1_2.py::test_export_epc_class_equals_stored_canonical_full_id --override-ini=addopts=
  • Broader shared epcis/lot filter: 163 passed, 5 skipped

Out of scope

  • H3 ilmd / bizLocation / bizTransactionList (hard gate: this normalizer first)
  • Partner-mode strict TLC as gap wiring (spec §9.2; when SEL-549 partner runs enable)
  • Railway redeploy / production ingest

Files

  • services/shared/epcis_lot_identity.py (new)
  • services/shared/canonical_event.py
  • services/ingestion/app/epcis/extraction.py
  • services/ingestion/app/epcis_export.py
  • services/ingestion/tests/fixtures/sel598/*
  • tests/shared/test_epcis_lot_identity_sel598.py
  • extraction / export test extensions

Note

Medium Risk
Changes what gets persisted in traceability_lot_code and export epcClass for EPCIS events (full URI vs bare segment), which can affect lot matching and downstream consumers until rollout is understood; rollback via LOT_IDENTITY_LOSSLESS=0 limits blast radius.

Overview
Introduces shared.epcis_lot_identity so legacy EPCIS extraction, canonical ingest, and export use one SEL-598 normalizer: stored TLC becomes the full LGTIN URN or GS1 Digital Link when the event carries it, with a derived bare lot token for UI, validation, and rebuilding /01/.../10/... links.

Canonical path (canonical_event) now resolves lot codes via normalize_from_epcis_event instead of inline ILMD/EPC parsing; transformation inputs add lot_token and honor lossless keys. LOT_IDENTITY_LOSSLESS=0 rolls back to segment-only canonical writes.

Legacy extraction delegates LGTIN/Digital Link parsing to parse_class_uri but still returns the full identifier as TLC. Export _epc_class parses stored class URIs unchanged, derives tokens before GTIN+lot rebuilds, and blocks nesting a full URI under /10/.

Synthetic sel598 fixtures and tests cover round-trip, strict vs lenient TLC, flag rollback, and export edge cases.

Reviewed by Cursor Bugbot for commit 44c5c9a. Bugbot is set up for automated code reviews on this repo. Configure here.

Add shared.epcis_lot_identity so canonical, legacy extraction, and export
agree on full LGTIN / Digital Link as the stored identity. Bare lot is
derived for rebuild and UI. LOT_IDENTITY_LOSSLESS=0 restores segment-only
writes. Synthetic sel598 fixtures cover both-pass and lenient-only TLC.

Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
@vercel

vercel Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
regengine Ready Ready Preview Oct 28, 2026 3:37pm UTC

Request Review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-28T15:37:34.316983Z 44c5c9a PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Caution

Review failed

The pull request is closed.

Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 44c5c9af4d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +20 to +22
_LGTIN_RE = re.compile(
r"^urn:epc:class:lgtin:(\d{6,12})\.(\d+)\.([A-Za-z0-9_~!$&'()*+,;=:@%\-]+)$",
re.IGNORECASE,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow dots inside the LGTIN lot component

For an LGTIN such as urn:epc:class:lgtin:0614141.107346.Lot.A, the new character class rejects the dot in the lot component. Previously, legacy extraction captured everything after the company-prefix and item-reference separators and returned Lot.A with the full URI. Now _lot_from_identifier returns None, and canonical normalization raises ValueError when this is the event's only lot identifier. Preserve support for dots in the lot component so previously accepted lot identifiers remain ingestible.

Useful? React with 👍 / 👎.

Comment on lines +175 to +179
def _prefer_class_uri(ident: LotIdentity) -> LotIdentity:
if parse_class_uri(ident.canonical_id):
return ident
for other in class_idents:
if other.lot_token == ident.lot_token:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Apply the rollback flag before returning an ILMD identity

When an event contains ILMD lotNumber: "LotA" and a matching quantity class, setting LOT_IDENTITY_LOSSLESS=0 still writes a full URI. With a Digital Link, identity_from_parts builds a URI and this early return bypasses the flag; with an LGTIN, the matching-class return below also bypasses it. Consequently, the advertised rollback only works for events without ILMD or flattened lot fields, leaving common events on the new storage format. Apply the flag before either full-URI return and add rollback coverage for events containing ILMD.

Useful? React with 👍 / 👎.

@PetrefiedThunder

Copy link
Copy Markdown
Collaborator Author

Closing as duplicate of #3727 (cursor/sel598-phase-b).

VS Code Lead coordination: SEL-598 agent owns the lossless lot normalizer files (epcis_lot_identity.py, canonical_event.py, epcis_export.py lot path). DeepSeek reviews that PR. This branch (pi/epcis-h2-sel598-lot) was the Phase B H2 attempt from Pi and overlaps those files.

A-2165 §7.12/§9 fixture coverage should land via #3727 (or a follow-up on that head), not a second normalizer PR.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 44c5c9a. Configure here.

_LGTIN_RE = re.compile(
r"^urn:epc:class:lgtin:(\d{6,12})\.(\d+)\.([A-Za-z0-9_~!$&'()*+,;=:@%\-]+)$",
re.IGNORECASE,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTIN parser drops dotted lots

Medium Severity

parse_class_uri rejects valid LGTIN lots that contain ., so extraction no longer returns those identifiers and canonical ingest raises if ILMD is absent. The old extraction regex accepted any lot segment, and Digital Links still have a permissive fallback, but LGTINs do not.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 44c5c9a. Configure here.

if value is not None and str(value).strip():
ident = identity_from_parts(tlc=str(value).strip(), gtin=gtin_hint)
if ident:
return _prefer_class_uri(ident)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ILMD path fabricates Digital Links

High Severity

normalize_from_epcis_event feeds the first Digital Link GTIN on the event into identity_from_parts with the ILMD TLC. That helper invents a new Digital Link, and _prefer_class_uri then returns it without checking that the lot tokens match, so a stored canonical id can pair the wrong GTIN with the ILMD lot.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 44c5c9a. Configure here.

return value
parsed = parse_class_uri(value)
if parsed:
return parsed.canonical_id

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Export emits placeholder class URIs

Medium Severity

_epc_class still rejects placeholder LGTINs containing 0000000.000000 in _valid_class_uri, then immediately accepts the same value through parse_class_uri and returns it. Retailer EPCIS export can now emit dummy class URIs that this filter was meant to block.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 44c5c9a. Configure here.

This branch was successfully deployed

1 active deployment
Preview — 44c5c9af Deployed Oct 28, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant