/* * 개체 → 품목 참조 — **정체성 모양이 하나가 아니고, 구분자는 우리가 정하지 않는다** (2026-08-23). * * ── 무엇이 알리지 않고 비어 있었나 ─────────────────────────────────────────────── * 화면이 「이 개체는 선언된 자재가 아닙니다」라고 말했다. 그런데 그 품목은 원본의 선언에 이름까지 있었고 * (「냉동그린빈스」), 대응 표도 멀쩡했다. 원인은 참조를 꺼내는 규칙이 GS1 SGTIN 하나만 알았던 것이다. * * ── 그리고 고치는 방향을 한 번 더 고쳤다 ──────────────────────────────────── * 처음에는 「`@` 뒤는 개체다」를 규칙으로 넣었다. 그것은 **한 원본(chef)의 관행**이고, 커널 계약이 그 * 구분자를 정한 적이 없다 — 사용자가 「이 화면이 특정 레거시를 기준으로 해서는 안 된다」고 짚었다. * 그래서 자르는 판단을 걷고, **그 트윈의 선언과 비교해** 답한다. * * 이 시험이 지키는 것: * ① SGTIN 은 표준이 정한 자리에서 품목 참조를 꺼낸다. * ② 그 밖의 모양은 **손대지 않는다**(구분자를 모른다). * ③ 선언과 비교할 때는 두 방향을 본다 — 관측이 선언을 확장한 경우, 화면이 짧게 줄인 경우. * ④ 후보가 둘 이상이면 **답하지 않는다**(모르는 것이 틀린 답보다 낫다). */ import { test } from 'node:test' import assert from 'node:assert/strict' import { itemRefOf, resolveByDeclaration } from '../server/service/twin-model/item-ref.ts' test('GS1 SGTIN 은 품목 참조 숫자다 — 표준이 자리를 정해 두었다', () => { assert.equal(itemRefOf('urn:epc:id:sgtin:0614141.077101.42'), '077101') assert.equal(itemRefOf('urn:epc:idpat:sgtin:0614141.078101.*'), '078101') }) test('그 밖의 정체성은 손대지 않는다 — 어느 부호가 개체인지 우리가 정하지 않는다', () => { assert.equal(itemRefOf('https://x.net/class/product-M1@M2R0001'), 'https://x.net/class/product-M1@M2R0001') assert.equal(itemRefOf('https://x.net/class/product-M1'), 'https://x.net/class/product-M1') assert.equal(itemRefOf(' https://x.net/class/p-1 '), 'https://x.net/class/p-1', '앞뒤 공백만 다듬는다') }) test('읽을 수 없는 값은 null 이다', () => { assert.equal(itemRefOf(''), null) assert.equal(itemRefOf(' '), null) assert.equal(itemRefOf(null), null) assert.equal(itemRefOf(undefined), null) assert.equal(itemRefOf(42), null) }) const DECLARED = new Map([ ['https://x.net/class/product-M0000438', 'M0000438'], ['https://x.net/class/product-B0000033', 'B0000033'] ]) test('그대로 같으면 그것이다', () => { assert.equal(resolveByDeclaration('https://x.net/class/product-M0000438', DECLARED), 'M0000438') }) test('관측이 선언을 확장한 경우 — 개체는 자기 클래스로 시작한다(부호를 모른다)', () => { for (const sep of ['@', '#', '/', ':', '']) { assert.equal( resolveByDeclaration(`https://x.net/class/product-M0000438${sep}M2R0001`, DECLARED), 'M0000438', `구분자 「${sep}」 에서도 같은 답이어야 한다` ) } }) test('화면이 짧게 줄인 경우 — 선언이 그것으로 끝난다', () => { assert.equal(resolveByDeclaration('net/class/product-B0000033', DECLARED), 'B0000033') assert.equal(resolveByDeclaration('product-M0000438', DECLARED), 'M0000438') }) test('후보가 둘 이상이면 답하지 않는다 — 남의 레시피와 비교하지 않게', () => { const ambiguous = new Map([ ['https://a.net/class/product-1', 'A1'], ['https://b.net/class/product-1', 'B1'] ]) assert.equal(resolveByDeclaration('class/product-1', ambiguous), undefined) }) test('선언에 없으면 없는 것이다 — 비슷한 것을 이어 주지 않는다', () => { assert.equal(resolveByDeclaration('https://x.net/class/product-ZZZ', DECLARED), undefined) assert.equal(resolveByDeclaration(null, DECLARED), undefined) })