/* * GS1 EPCIS 2.0 적합성 기준 — **물류·제조 트윈이 표준의 어디를 채우고 있나.** * * ── 왜 따로 있나 ──────────────────────────────────────────────────────────── * 적합성 패널이 ISA-95 하나만 말하고 있었다. 그런데 창고·야드·공장 트윈에서 「무슨 일이 있었나」와 * 「이 번호가 무엇을 가리키나」를 정하는 것은 EPCIS 다. ISA-95 표만 보이면 그 트윈이 기대는 기준의 * 절반이 화면에 없다. * * ── 수준은 근거를 보고 적었다 ─────────────────────────────────────────────── * 「있다/없다」를 기억으로 적지 않는다. 각 줄의 판단 근거: * · 타입 선언 — 커널 `epcis.ts`(이벤트·필드의 모양) * · 실제 사용 — 커널 도메인(`mes-kernel`·`face2-adapter`·`observed-reducer`)과 호스트 인제스트 * · 화면 노출 — 저널·계보·엔티티 360 * 모양만 있고 아무도 채우지 않는 필드는 **구조만 있음**으로 적는다(그게 사실이다). * * ISA-95 표와 같은 규율이다(`isa95-coverage.ts` 머리말): 사람이 판단해 적고, 틀리면 여기를 고친다. */ import { reconcileAxes, type Isa95Concept, type CoverageLevel } from './isa95-coverage.js' /* Part: EPCIS 2.0 의 사건 종류 — 무엇이 일어났는가. */ const EVENTS: Isa95Concept[] = [ { std: 'ObjectEvent', part: 'events', label: 'twin.epcis.ObjectEvent', axis: 'items', structure: 'full', behavior: 'full', surface: 'full', note: 'twin.epcis.note.ObjectEvent' }, { std: 'AggregationEvent', part: 'events', label: 'twin.epcis.AggregationEvent', axis: null, shownOn: ['items'], structure: 'full', behavior: 'full', surface: 'partial', note: 'twin.epcis.note.AggregationEvent' }, { std: 'TransactionEvent', part: 'events', label: 'twin.epcis.TransactionEvent', axis: 'orders', structure: 'full', behavior: 'full', surface: 'partial', note: 'twin.epcis.note.TransactionEvent' }, { std: 'TransformationEvent', part: 'events', label: 'twin.epcis.TransformationEvent', axis: null, shownOn: ['items', 'operations'], structure: 'full', behavior: 'full', surface: 'full', note: 'twin.epcis.note.TransformationEvent' }, /* EPCIS 2.0 이 더한 사건 — 우리는 타입조차 두지 않았다. 없는 것은 없다고 적는다. */ { std: 'AssociationEvent', part: 'events', label: 'twin.epcis.AssociationEvent', axis: null, structure: 'none', behavior: 'none', surface: 'none', note: 'twin.epcis.note.AssociationEvent' } ] /* Part: 사건이 답하는 다섯 가지 — 무엇을·어디서·언제·왜, 그리고 어떤 거래로. */ const FIELDS: Isa95Concept[] = [ { /* * **무엇을 한 것인가** — 표준 `action`: `ADD`(생겼다) · `OBSERVE`(보았다) · `DELETE`(없어졌다). * * 이 줄이 표에 없었다. 표준 필드이고 우리가 계약·유입·검증에서 다 쓰는데도 세지 않았습니다. * * ── 2026-08-25 그 뜻이 커졌다 ───────────────────────────────────────────── * `DELETE` 를 낱개 번호가 있는 물품에만 적용하고 있었다. 그래서 **낱개 번호가 없는 현장에서는 * 재고를 지울 방법이 하나도 없었다** — 승화푸드는 모든 재고가 수량으로 오는 현장이고, 한 번 본 * 재고가 영원히 남았다(실제 1,704건인데 트윈이 10,038건). * * 이제 수량으로 온 재고도 없어질 수 있다. 자리를 함께 말하면 그 자리의 것만, 말하지 않으면 * 그 로트를 모든 자리에서 지운다 — 짐작이 아니라 진술의 범위를 읽는다. * * 화면은 아직 없다. 「이것이 없어졌다」를 사람에게 보이는 자리를 만들지 않았다. */ std: 'action', part: 'fields', label: 'twin.epcis.action', axis: null, shownOn: ['items'], structure: 'full', behavior: 'full', surface: 'none' }, { std: 'epcList / quantityList', part: 'fields', label: 'twin.epcis.identity', axis: 'items', structure: 'full', behavior: 'full', surface: 'full', note: 'twin.epcis.note.identity' }, /* * ── 2026-08-24 주장 근거가 달라졌다 ──────────────────────────────────────── * 이 줄은 오래 `full` 이었지만 근거가 **우리 선언**이었다. 이제 **정본 원문 대조**다: * CBV Standard Release 2.0(Ratified Jun 2022) §7.2.3 의 처분 38개를 전수 확인했다. * * 그 대조가 실제로 셋을 바꿨다. * · `expired` — 있는 낱말인데 우리가 쓰지 않고 있었다(기한 경과를 처분으로 말할 수 있게 됐다) * · `conformant` / `non_conformant` — 검사 판정을 처분으로 남기는 낱말. 함께 `inspecting` bizStep * · `non_sellable_expired` 는 표준이 **폐기**하고 `expired` 로 대체한 것을 확인 * * ★ 그리고 **검증 방법에 함정이 있다**: `ref.gs1.org/cbv/…` 로 URN 을 조회하면 **지어낸 값에도 * 똑같은 응답**이 온다. 그것으로 확인했다고 여기면 없는 낱말을 발행한다(한 번 그렇게 했다). * 확인은 정본 문서로만 한다. */ { std: 'bizStep / disposition (CBV)', part: 'fields', label: 'twin.epcis.cbv', axis: null, shownOn: ['items', 'tasks'], structure: 'full', behavior: 'full', surface: 'full', note: 'twin.epcis.note.cbv' }, { std: 'readPoint / bizLocation', part: 'fields', label: 'twin.epcis.where', axis: 'locations', structure: 'full', behavior: 'full', surface: 'full', note: 'twin.epcis.note.where' }, { std: 'bizTransactionList', part: 'fields', label: 'twin.epcis.bizTransaction', axis: 'orders', structure: 'full', behavior: 'full', surface: 'partial', note: 'twin.epcis.note.bizTransaction' }, { std: 'ilmd', part: 'fields', label: 'twin.epcis.ilmd', axis: null, shownOn: ['items'], structure: 'full', behavior: 'full', surface: 'partial', note: 'twin.epcis.note.ilmd' }, { /* * `shownOn` 은 「어디에 속하나」다 — 정정 선언은 물품의 사실이다. 화면에 그려지는지는 `surface` 가 * 따로 답한다(지금은 `none` 이고 `note` 가 그 사실을 말한다). 둘은 다른 물음이다(§`shownOn`). */ shownOn: ['items'], std: 'errorDeclaration', part: 'fields', label: 'twin.epcis.errorDeclaration', axis: null, structure: 'full', behavior: 'partial', surface: 'none', note: 'twin.epcis.note.errorDeclaration' }, /* 모양만 있고 아무도 채우지 않는 것들 — 구조만 있음. */ { /* 어디에 속하나 — 소유·보관의 이전은 그 물품의 사실이다. 화면에 그려지는지는 `surface` 가 따로 답한다(§`shownOn`). */ shownOn: ['items'], std: 'sourceList / destinationList', part: 'fields', label: 'twin.epcis.sourceDestination', axis: null, structure: 'partial', behavior: 'none', surface: 'none', note: 'twin.epcis.note.sourceDestination' }, { /* 어디에 속하나 — 지속 처분은 그 물품에 붙는다. 화면에 그려지는지는 `surface` 가 따로 답한다(§`shownOn`). */ shownOn: ['items'], std: 'persistentDisposition', part: 'fields', label: 'twin.epcis.persistentDisposition', axis: null, structure: 'partial', behavior: 'none', surface: 'none', note: 'twin.epcis.note.persistentDisposition' }, /* * ── 2026-08-24 계측이 어디로 들어오는지 확정됐다 ────────────────────────── * 「계측은 EPCIS 센서 필드로 오지 않는다」는 사실이 이제 **자리를 갖는다**: 자리의 상시 관측은 * ISA-95 `OperationsEvent` 로 들어오고(커널 `location.measured` 채널), 에너지는 그 위의 누적기가 * 받는다. ISA-95 표의 `OperationsEvent` 줄이 그 판정을 든다. * * 왜 EPCIS 가 아닌가: `SensorElement` 는 **개체에 붙는** 관측이고, 냉장실의 온도는 그 안의 물건 * 수백 개와 관계되며 그 수백 개는 시간에 따라 바뀐다. 개체마다 붙이면 같은 사실이 수백 벌이 되고, * 물건이 떠나면 그 방의 온도 이력이 함께 사라진다. 주인은 **자리**다. * * 그래서 이 줄은 `behavior: 'none'` 으로 남는다 — 결손이 아니라 **다른 층으로 들어온다는 사실**이다. * 그 구별을 표가 말하지 않으면 「센서를 못 받는 트윈」으로 읽힌다. */ { /* 어디에 속하나 — 표준에서 센서 관측은 **개체에 붙는다** — 우리가 그것을 자리로 옮긴 것이 다른 이야기다. 화면에 그려지는지는 `surface` 가 따로 답한다(§`shownOn`). */ shownOn: ['items'], std: 'sensorElementList', part: 'fields', label: 'twin.epcis.sensorElement', axis: null, structure: 'partial', behavior: 'none', surface: 'none', note: 'twin.epcis.note.sensorElement' } ] export const EPCIS_CONCEPTS: Isa95Concept[] = [...EVENTS, ...FIELDS] export function epcisCoverage(axes: string[]): { model: string concepts: (Isa95Concept & { axisMissing?: boolean })[] totals: { concepts: number; structure: number; behavior: number; surface: number } } { const concepts = EPCIS_CONCEPTS.map(c => reconcileAxes(c, new Set(axes))) const count = (k: 'structure' | 'behavior' | 'surface') => concepts.filter(c => c[k] === 'full').length return { model: 'GS1 EPCIS 2.0', concepts, totals: { concepts: concepts.length, structure: count('structure'), behavior: count('behavior'), surface: count('surface') } } }