@things-factory/headless-twin 10.1.27 → 10.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist-server/index.js +6 -0
  2. package/dist-server/index.js.map +1 -1
  3. package/dist-server/service/reference/actuation-verification.d.ts +22 -0
  4. package/dist-server/service/reference/actuation-verification.js +37 -0
  5. package/dist-server/service/reference/actuation-verification.js.map +1 -1
  6. package/dist-server/service/reference/reference-resolver.js +32 -2
  7. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  8. package/dist-server/service/twin-model/external-resolver.d.ts +13 -3
  9. package/dist-server/service/twin-model/external-resolver.js +2 -2
  10. package/dist-server/service/twin-model/external-resolver.js.map +1 -1
  11. package/dist-server/service/twin-model/twin-lineage-query.js +14 -0
  12. package/dist-server/service/twin-model/twin-lineage-query.js.map +1 -1
  13. package/dist-server/service/twin-model/twin-model-item-query.js +41 -12
  14. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  15. package/dist-server/service/twin-model/twin-model-query.js +14 -0
  16. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  17. package/dist-server/service/twin-space/area-resolver.d.ts +6 -0
  18. package/dist-server/service/twin-space/area-resolver.js +68 -0
  19. package/dist-server/service/twin-space/area-resolver.js.map +1 -0
  20. package/package.json +8 -8
  21. package/server/index.ts +6 -0
  22. package/server/service/reference/actuation-verification.ts +42 -0
  23. package/server/service/reference/reference-resolver.ts +33 -2
  24. package/server/service/twin-model/external-resolver.ts +15 -3
  25. package/server/service/twin-model/twin-lineage-query.ts +13 -0
  26. package/server/service/twin-model/twin-model-item-query.ts +62 -12
  27. package/server/service/twin-model/twin-model-query.ts +14 -0
  28. package/server/service/twin-space/area-resolver.ts +75 -0
  29. package/test/actuation-verification-state.test.ts +46 -1
  30. package/test/area-resolver-db.test.ts +110 -0
  31. package/test/axis-declaration-passthrough-db.test.ts +152 -0
  32. package/test/external-resolver.test.ts +11 -5
  33. package/test/lineage-survives-restart.test.ts +15 -0
  34. package/test/twin-model-item-db.test.ts +18 -1
  35. package/tsconfig.tsbuildinfo +1 -1
@@ -28,6 +28,12 @@ import {
28
28
 
29
29
  const AREA = 'space.area'
30
30
 
31
+ /*
32
+ * 어느 트윈의 관계인지 — `resolve` 는 이것을 함께 받는다(2026-09-18). 밖의 id 는 도메인에서 유일하지
33
+ * 않아서(구역 id 는 공간 안에서만 유일), 문맥 없이 답하면 남의 공간 이름이 이 자리의 부모로 보인다.
34
+ */
35
+ const CTX = { instanceId: 'probe-twin' }
36
+
31
37
  /* ── 못 풀면 못 푼다고 말한다 ────────────────────────────────────────────── */
32
38
 
33
39
  test('★ 꽂힌 것이 없으면 「해소기가 없다」고 말한다 — 「없다」가 아니다', async () => {
@@ -36,7 +42,7 @@ test('★ 꽂힌 것이 없으면 「해소기가 없다」고 말한다 — 「
36
42
  * 사람이 할 일이 다르다 — 앞은 자료를 채울 일이고 뒤는 해소기를 꽂을 일이다.
37
43
  */
38
44
  clearExternalResolvers()
39
- const got = await resolveExternal(AREA, 'd1', ['LINE-CAM'])
45
+ const got = await resolveExternal(AREA, 'd1', ['LINE-CAM'], CTX)
40
46
 
41
47
  assert.equal(got.unresolved, 'no-resolver')
42
48
  assert.equal(got.refs, undefined)
@@ -47,7 +53,7 @@ test('★ 해소기가 터져도 360 을 죽이지 않는다 — 알리고 못
47
53
  const said: string[] = []
48
54
  registerExternalResolver({ entity: AREA, resolve: async () => { throw new Error('DB down') } })
49
55
 
50
- const got = await resolveExternal(AREA, 'd1', ['X'], m => said.push(m))
56
+ const got = await resolveExternal(AREA, 'd1', ['X'], CTX, m => said.push(m))
51
57
 
52
58
  assert.equal(got.unresolved, 'failed')
53
59
  assert.ok(said.some(m => m.includes('DB down')), '사유를 삼켰다')
@@ -62,7 +68,7 @@ test('★ 이름과 갈 곳을 낸다 — 막다른 링크를 만들지 않으
62
68
  resolve: async (_d, ids) => ids.map(id => ({ id, name: `라인 ${id}`, href: `/areas/${id}` }))
63
69
  })
64
70
 
65
- const got = await resolveExternal(AREA, 'd1', ['LINE-CAM'])
71
+ const got = await resolveExternal(AREA, 'd1', ['LINE-CAM'], CTX)
66
72
  assert.equal(got.refs?.[0].name, '라인 LINE-CAM')
67
73
  assert.equal(got.refs?.[0].href, '/areas/LINE-CAM')
68
74
  })
@@ -72,7 +78,7 @@ test('★ 없는 id 는 없다고 말한다 — 답에서 빼지 않는다', asy
72
78
  clearExternalResolvers()
73
79
  registerExternalResolver({ entity: AREA, resolve: async (_d, ids) => ids.map(id => ({ id, missing: true })) })
74
80
 
75
- const got = await resolveExternal(AREA, 'd1', ['사라진구역'])
81
+ const got = await resolveExternal(AREA, 'd1', ['사라진구역'], CTX)
76
82
  assert.equal(got.refs?.length, 1)
77
83
  assert.equal(got.refs?.[0].missing, true)
78
84
  })
@@ -166,6 +172,6 @@ test('★ 이 검사가 실제로 무언가를 잰다 — 견준 것이 0이면
166
172
  clearExternalResolvers()
167
173
  assert.deepEqual(externalResolverEntities(), [], '지우기가 안 듣는다')
168
174
  registerExternalResolver({ entity: AREA, resolve: async (_d, ids) => ids.map(id => ({ id })) })
169
- const got = await resolveExternal(AREA, 'd', ['X'])
175
+ const got = await resolveExternal(AREA, 'd', ['X'], CTX)
170
176
  assert.equal(got.refs?.length, 1, '꽂았는데 안 불린다')
171
177
  })
@@ -209,6 +209,21 @@ test('계보를 거슬러 올라가 선언과 나란히 놓는다 — 그 차이
209
209
  assert.equal(steel.matches, true)
210
210
  })
211
211
 
212
+ test('★ 만든 것을 관측하지 못했으면 「다르다」고 하지 않는다 — 안 본 것과 어긋난 것은 다른 사실이다', async () => {
213
+ const q = new TwinLineageQuery()
214
+ const ctx = { state: { domain: { id: domainId } } } as any
215
+ /*
216
+ * 완제품 클래스의 개체이지만 저널에 그것을 낸 변환이 없다 — 자재로도 이어지고 레시피도 하나다.
217
+ * 예전에는 여기서 셈이 그대로 돌아 관측을 전부 0 으로 적고 「레시피와 다릅니다」를 냈다
218
+ * (2026-09-18 :3000 에서 카메라 로트 23 줄이 그렇게 보였다).
219
+ */
220
+ const lin = await q.twinItemLineage(INSTANCE, `urn:epc:id:sgtin:${CP}.078101.999999`, ctx)
221
+ assert.equal(lin.conformance.comparable, false, `관측이 없는데 견주었다: ${JSON.stringify(lin.conformance?.lines?.slice(0, 2))}`)
222
+ assert.equal(lin.conformance.reason, 'made-not-observed')
223
+ assert.ok(lin.conformance.recipe, '어느 레시피와 견주려 했는지는 말한다 — 사람이 다음 걸음을 안다')
224
+ assert.equal(lin.basis.transformationsRead >= 0, true)
225
+ })
226
+
212
227
  test('바인딩이 없으면 비교하지 않고 이유를 알린다 — 짐작으로 이으면 거짓 진단이 된다', async () => {
213
228
  const q = new TwinLineageQuery()
214
229
  const ctx = { state: { domain: { id: domainId } } } as any
@@ -114,12 +114,29 @@ test('아직 문서에 사는 축도 열린다 — 이주 전이라고 안 보
114
114
 
115
115
  test('검색이 전수 조회라는 사실을 드러낸다 — 감추면 한계를 아무도 모른다', async () => {
116
116
  const r = await q.twinModelEntities(INSTANCE, 'equipment', ctx, 'weld')
117
- assert.equal(r.basis, 'rows-scanned', '아직 DB 로 내리지 않았다는 사실이 응답에 있다')
118
117
  assert.equal(r.total, 2)
118
+ /* 훑어 걸렀다는 사실은 **이 두 칸이** 말한다(근거가 아니라 셈의 한계다). */
119
+ assert.equal(typeof r.scanned, 'number', '몇 줄을 읽었는지 말하지 않는다')
119
120
  assert.equal(r.truncated, false, '상한에 닿지 않았으면 잘리지 않았다고 말한다')
120
121
  assert.deepEqual(r.items.map((i: any) => i.id).sort(), ['welder-1', 'welder-2'])
121
122
  })
122
123
 
124
+ test('★ 근거는 계약이 닫아 둔 다섯 안에서만 답한다', async () => {
125
+ /*
126
+ * 2026-09-18 판정(ADR-0075 결정 3 을 코드에 대어 본 결과): 검색 갈래가 `'rows-scanned'` 를 냈다.
127
+ * 그것은 출처가 아니라 우리가 어떻게 셌나이고, 목록 소비자는 계약에 없는 값을 만난다. 출처는
128
+ * 훑었든 DB 가 골랐든 같은 행이므로 `rows` 다.
129
+ */
130
+ const { READ_BASIS } = req('@operato/ops-contract')
131
+ const answers = [
132
+ await q.twinModelEntities(INSTANCE, 'equipment', ctx, 'weld'),
133
+ await q.twinModelEntities(INSTANCE, 'equipment', ctx),
134
+ await q.twinModelEntities(INSTANCE, 'recipes', ctx)
135
+ ]
136
+ for (const a of answers) assert.ok(READ_BASIS.includes(a.basis), `계약 밖의 근거 '${a.basis}'`)
137
+ assert.equal(answers[0].basis, 'rows', '훑어 세도 값이 온 곳은 행이다')
138
+ })
139
+
123
140
  test('모르는 축은 거절한다 — 빈 목록은 "없다" 로 읽힌다', async () => {
124
141
  await assert.rejects(() => q.twinModelEntities(INSTANCE, 'nosuch', ctx), /unknown concept axis/)
125
142
  await assert.rejects(() => q.twinModelItem(INSTANCE, 'nosuch', 'x', ctx), /unknown concept axis/)