@things-factory/headless-twin 10.1.24 → 10.1.26
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.
- package/dist-server/service/actuation/actuation-advisor.d.ts +2 -0
- package/dist-server/service/actuation/actuation-advisor.js +1 -1
- package/dist-server/service/actuation/actuation-advisor.js.map +1 -1
- package/dist-server/service/actuation/twin-command-resolver.js +1 -1
- package/dist-server/service/actuation/twin-command-resolver.js.map +1 -1
- package/dist-server/service/twin-model/twin-model-item-query.js +38 -2
- package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
- package/package.json +8 -8
- package/server/service/actuation/actuation-advisor.ts +8 -1
- package/server/service/actuation/twin-command-resolver.ts +1 -1
- package/server/service/twin-model/twin-model-item-query.ts +37 -2
- package/test/actuation-advisor.test.ts +55 -0
- package/test/twin-model-parent-relation-db.test.ts +125 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -24,6 +24,7 @@ export interface ActuationAdvisor {
|
|
|
24
24
|
* 실패·시간초과면 `undefined` 를 돌려준다. 지어내지 않는다.
|
|
25
25
|
*/
|
|
26
26
|
advise(input: {
|
|
27
|
+
domainId: string;
|
|
27
28
|
command: StoredCommand;
|
|
28
29
|
twinNow: TwinNow;
|
|
29
30
|
locale?: string;
|
|
@@ -53,6 +54,7 @@ export declare const ADVISOR_BUDGET_MS = 8000;
|
|
|
53
54
|
* 사실이고, 승인하는 사람에게 그 차이가 크다.
|
|
54
55
|
*/
|
|
55
56
|
export declare function askAdvisor(args: {
|
|
57
|
+
domainId: string;
|
|
56
58
|
command: StoredCommand;
|
|
57
59
|
twinNow: TwinNow;
|
|
58
60
|
locale?: string;
|
|
@@ -28,7 +28,7 @@ async function askAdvisor(args) {
|
|
|
28
28
|
let timer;
|
|
29
29
|
try {
|
|
30
30
|
const opinion = await Promise.race([
|
|
31
|
-
a.advise({ command: args.command, twinNow: args.twinNow, ...(args.locale ? { locale: args.locale } : {}) }),
|
|
31
|
+
a.advise({ domainId: args.domainId, command: args.command, twinNow: args.twinNow, ...(args.locale ? { locale: args.locale } : {}) }),
|
|
32
32
|
new Promise(resolve => {
|
|
33
33
|
timer = setTimeout(() => resolve(TIMED_OUT), budget);
|
|
34
34
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actuation-advisor.js","sourceRoot":"","sources":["../../../server/service/actuation/actuation-advisor.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"actuation-advisor.js","sourceRoot":"","sources":["../../../server/service/actuation/actuation-advisor.ts"],"names":[],"mappings":";;;AAkEA,kDAEC;AAED,4CAEC;AAwBD,gCA+BC;AAhED,IAAI,OAAqC,CAAA;AAEzC,uBAAuB;AACvB,SAAgB,mBAAmB,CAAC,IAAsB;IACxD,OAAO,GAAG,IAAI,CAAA;AAChB,CAAC;AAED,SAAgB,gBAAgB;IAC9B,OAAO,OAAO,CAAA;AAChB,CAAC;AAeD,6CAA6C;AAChC,QAAA,iBAAiB,GAAG,KAAK,CAAA;AAEtC;;;;;GAKG;AACI,KAAK,UAAU,UAAU,CAAC,IAMhC;IACC,MAAM,CAAC,GAAG,OAAO,CAAA;IACjB,IAAI,CAAC,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,0BAA0B,EAAE,CAAA;IAErD,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,IAAI,yBAAiB,CAAA;IACjD,IAAI,KAAU,CAAA;IACd,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;YACjC,CAAC,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;YACpI,IAAI,OAAO,CAAY,OAAO,CAAC,EAAE;gBAC/B,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,SAAgB,CAAC,EAAE,MAAM,CAAC,CAAA;YAC7D,CAAC,CAAC;SACH,CAAC,CAAA;QAEF,IAAK,OAAe,KAAK,SAAS,EAAE,CAAC;YACnC,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,kBAAkB,EAAE,CAAA;QAChF,CAAC;QACD,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE;YAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAA;QAEjF,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,CAAA;IACjC,CAAC;IAAC,OAAO,CAAM,EAAE,CAAC;QAChB,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,kBAAkB,MAAM,CAAC,CAAC,EAAE,OAAO,IAAI,CAAC,CAAC,EAAE,EAAE,CAAA;IAC7E,CAAC;YAAS,CAAC;QACT,IAAI,KAAK;YAAE,YAAY,CAAC,KAAK,CAAC,CAAA;IAChC,CAAC;AACH,CAAC;AAED,uDAAuD;AACvD,MAAM,SAAS,GAAG,MAAM,CAAC,iBAAiB,CAAC,CAAA","sourcesContent":["/*\n * **AI 가 의견을 내는 자리** — 판정이 아니라 발언.\n *\n * ── 왜 필요한가 (2026-09-01, 사용자 지적) ────────────────────────────────\n * 사용자가 물었다 — 「AI 자신은 결재/승인의 담당이 될 수 없나. 최소한 의견제시자라도 될 수 있지 않나.」\n *\n * 승인자는 안 된다. 승인 기록의 값어치는 사고 뒤에 **책임을 물을 수 있는 것**이고, 「AI 가 승인했다」로는\n * 아무에게도 묻지 못한다. 그리고 낸 쪽이 승인하면 두 사람이 아니다 — 자기 승인을 막는 규칙과 같은 이유다.\n *\n * 의견은 내야 한다. `twinNow` 로 「도금 대기가 여섯 배다」는 사실을 승인 화면에 실었지만, 그건 계산값이다.\n * 「그러니 더 만들면 대기가 늘어난다」가 의견이고 그 자리가 없었다.\n *\n * ── 언제 묻나 — 승인 화면을 열 때다 ─────────────────────────────────────\n * 결재선에 올릴 때 묻지 않는다. 그 자리는 트랜잭션 안이고(`raiseTwinCommand` 의 `@transaction`),\n * 거기서 LLM 을 기다리면 **DB 연결 하나를 몇 초 붙잡는다**(sqlite 는 연결이 하나다).\n *\n * 그리고 화면을 열 때 묻는 것이 더 옳다. 승인하는 사람은 **지금** 판단하고, 올린 시각의 의견은 낡았다.\n *\n * ── 판정에 쓰지 않는다 ───────────────────────────────────────────────────\n * 이 값을 읽어 무엇을 막는 코드가 없다. 승인·거절은 사람이 한다. AI 가 반대해도 승인되고, 동의해도\n * 거절된다. 그 사실이 코드에 드러나 있어야 한다 — 판정에 쓰면 AI 가 사실상 승인자가 된다.\n */\nimport type { StoredCommand } from './command-dispatcher.js'\nimport type { TwinNow } from './twin-now.js'\n\n/**\n * AI 가 어떤 태도를 냈나 — **세 갈래여야 한다.**\n *\n * 「모르겠다」가 없으면 모를 때 동의로 답한다. 그리고 승인자가 그것을 「AI 가 괜찮다고 했다」로 읽는다.\n * 자동 승인 편향이 거기서 시작한다.\n */\nexport type AdvisorStance = 'object' | 'concur' | 'unsure'\n\nexport interface AdvisorOpinion {\n stance: AdvisorStance\n /** 사람이 읽는 문장. AI 가 쓴 그대로 — 우리가 다듬지 않는다(다듬으면 누가 말한 것인지 흐려진다). */\n text: string\n /** 무엇을 보고 말했나 — 주의 식별자·값. 승인자가 근거를 되짚을 수 있게. */\n basis?: string[]\n}\n\n/** 의견을 내는 자리 — 앱이 채운다(headless-twin 은 `twin-ai` 를 부르지 않는다). */\nexport interface ActuationAdvisor {\n /** 화면이 출처를 말해야 한다 — 「AI 의견」. 사람의 의견과 섞이면 안 된다. */\n label: string\n /**\n * 이 조치에 대해 한마디.\n *\n * 실패·시간초과면 `undefined` 를 돌려준다. 지어내지 않는다.\n */\n advise(input: {\n /*\n * The tenant the command belongs to. Required: the advisor's tools read the twin through it, and without it\n * every tool answers \"no domain context\" while the opinion is still written from the prompt alone\n * (2026-09-17 — the twin app passed nothing because this input had no place for it).\n */\n domainId: string\n command: StoredCommand\n twinNow: TwinNow\n locale?: string\n }): Promise<AdvisorOpinion | undefined>\n}\n\nlet advisor: ActuationAdvisor | undefined\n\n/** 앱이 부팅에서 한 번 부른다. */\nexport function setActuationAdvisor(next: ActuationAdvisor): void {\n advisor = next\n}\n\nexport function actuationAdvisor(): ActuationAdvisor | undefined {\n return advisor\n}\n\n/** 의견을 물은 결과 — **없음도 사실이다.** */\nexport interface AdvisorResult {\n /** 누가 말했나. 없으면 아무도 말하지 않았다. */\n by?: string\n opinion?: AdvisorOpinion\n /**\n * 의견이 없으면 왜 없나.\n *\n * 빈칸을 「AI 가 반대하지 않았다」로 읽으면 안 된다. 화면이 이 문장을 그대로 보여야 한다.\n */\n absent?: string\n}\n\n/** 몇 초까지 기다리나 — 승인 화면이 이것 때문에 안 열리면 안 된다. */\nexport const ADVISOR_BUDGET_MS = 8_000\n\n/**\n * 의견을 묻는다 — **없으면 없다고 답한다.**\n *\n * 실패를 삼키지 않는다. AI 가 죽어 있어서 의견이 없는 것과 AI 가 반대할 것이 없어서 조용한 것은 다른\n * 사실이고, 승인하는 사람에게 그 차이가 크다.\n */\nexport async function askAdvisor(args: {\n domainId: string\n command: StoredCommand\n twinNow: TwinNow\n locale?: string\n budgetMs?: number\n}): Promise<AdvisorResult> {\n const a = advisor\n if (!a) return { absent: 'AI 의견을 내는 자리가 설치되지 않았습니다' }\n\n const budget = args.budgetMs ?? ADVISOR_BUDGET_MS\n let timer: any\n try {\n const opinion = await Promise.race([\n a.advise({ domainId: args.domainId, command: args.command, twinNow: args.twinNow, ...(args.locale ? { locale: args.locale } : {}) }),\n new Promise<undefined>(resolve => {\n timer = setTimeout(() => resolve(TIMED_OUT as any), budget)\n })\n ])\n\n if ((opinion as any) === TIMED_OUT) {\n return { by: a.label, absent: `${Math.round(budget / 1000)}초 안에 답이 오지 않았습니다` }\n }\n if (!opinion?.text?.trim()) return { by: a.label, absent: 'AI 가 할 말이 없다고 답했습니다' }\n\n return { by: a.label, opinion }\n } catch (e: any) {\n return { by: a.label, absent: `의견을 받지 못했습니다 — ${String(e?.message ?? e)}` }\n } finally {\n if (timer) clearTimeout(timer)\n }\n}\n\n/** 시간초과를 값으로 구별한다 — `undefined` 로 두면 「할 말 없음」과 섞인다. */\nconst TIMED_OUT = Symbol('advisor-timeout')\n"]}
|
|
@@ -83,7 +83,7 @@ let TwinCommandResolver = class TwinCommandResolver {
|
|
|
83
83
|
* 오히려 낡은 값이다. 그리고 이 값을 읽어 승인·거절을 막는 코드가 없다 — 판정에 쓰면 AI 가 사실상
|
|
84
84
|
* 승인자가 되고, 사고 뒤에 책임을 물을 자리가 사라진다.
|
|
85
85
|
*/
|
|
86
|
-
const advice = await (0, actuation_advisor_js_1.askAdvisor)({ command, twinNow: now, ...(locale ? { locale } : {}) });
|
|
86
|
+
const advice = await (0, actuation_advisor_js_1.askAdvisor)({ domainId, command, twinNow: now, ...(locale ? { locale } : {}) });
|
|
87
87
|
return { command, twinNow: now, advice };
|
|
88
88
|
}
|
|
89
89
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"twin-command-resolver.js","sourceRoot":"","sources":["../../../server/service/actuation/twin-command-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Args, Ctx, Directive, Mutation, Query, Resolver } from 'type-graphql'\n\nimport { MES_COMMANDS, nextCommandState } from '@operato/ops-contract'\n\nimport { randomUUID } from 'node:crypto'\n\nimport { ListParam, ScalarObject, getQueryBuilderFromListParams, getRepository } from '@things-factory/shell'\n\nimport { askAdvisor } from './actuation-advisor.js'\nimport { approvalGateway } from './approval-gateway.js'\nimport { approve, dispatch, linkApproval, propose, reject } from './command-dispatcher.js'\nimport { actuationReadinessOf, actuationTargetOf, routeActuation } from '../reference/actuation-routing.js'\n\nimport { actuationOutcomeOf } from './actuation-outcome.js'\nimport { databaseCommandStore } from './command-store.js'\nimport { TwinCommandRecord } from './twin-command.js'\nimport { twinNow } from './twin-now.js'\n\n/*\n * 승인 대기 줄과 그 결정 — **사람이 쓰는 자리**.\n *\n * ── 왜 필요한가 (2026-08-31) ─────────────────────────────────────────────\n * 게이트를 세우고 승인할 화면을 만들지 않았다. 그래서 되돌릴 수 없는 조치는 전부 대기 상태로 쌓이고\n * **사람이 승인할 방법이 없었다.** 서버만 있고 사람이 쓸 자리가 없는 상태다.\n *\n * ── 왜 조회와 결정을 한 파일에 두나 ──────────────────────────────────────\n * 대기 줄을 여는 것과 그 줄에서 결정하는 것은 한 화면의 일이다. 나누면 화면이 두 곳을 부르게 되고,\n * 그중 하나만 권한이 바뀌는 날 목록은 보이는데 결정이 안 되거나 그 반대가 된다.\n */\n\n@Resolver()\nexport class TwinCommandResolver {\n /**\n * 커맨드 목록 — 기본은 **결정을 기다리는 것부터**.\n *\n * 정렬을 최근순으로 두지 않는다. 승인 대기 줄에서 사람이 찾는 것은 「새로 온 것」이 아니라\n * **「오래 기다린 것」**이다 — 오래 기다린 조치가 뒤로 밀리면 그것이 가장 늦게 처리된다.\n */\n @Query(returns => ScalarObject, {\n nullable: true,\n description: 'List actuation commands with the standard list contract. Defaults to the oldest waiting first.'\n })\n async twinCommands(\n /* 표준 목록 계약은 `@Args` 로 펼쳐 받는다 — `@Arg` 로 받으면 스키마 조립이 실패한다. */\n @Args(type => ListParam) params: ListParam,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n const domainId = context.state.domain?.id\n if (!domainId) return { items: [], total: 0 }\n\n const effective: ListParam = {\n ...(params ?? ({} as ListParam)),\n sortings: params?.sortings?.length ? params.sortings : [{ name: 'createdAt', desc: false }]\n }\n\n const qb = getQueryBuilderFromListParams({\n repository: getRepository(TwinCommandRecord),\n params: effective,\n domain: context.state.domain,\n alias: 'c',\n /* 전부 실컬럼이다 — 질의 빌더는 없는 컬럼의 LIKE 를 경고 후 무시한다(인덱스 없는 전체 스캔 방지). */\n searchables: ['instanceId', 'type', 'origin', 'state', 'approvalRef', 'decidedBy', 'error'],\n sortables: ['createdAt', 'updatedAt', 'state', 'type', 'instanceId'],\n /* 사람이 읽는 속도로 본다. */\n defaultLimit: 50,\n maxLimit: 200\n })\n\n const [items, total] = await qb.getManyAndCount()\n return { items, total }\n }\n\n /**\n * **이 조치에 대해 트윈이 지금 보는 것과 AI 의견** — 승인 화면이 부른다.\n *\n * ── 왜 조회인가 (2026-09-01) ───────────────────────────────────────────\n * 결재선에 올릴 때 묻지 않는다. 그 자리는 트랜잭션 안이고 거기서 LLM 을 기다리면 DB 연결 하나를 몇 초\n * 붙잡는다. 그리고 **승인하는 사람은 지금 판단한다** — 올린 시각의 의견은 낡았다.\n *\n * AI 는 판정하지 않는다. 이 값을 읽어 승인·거절을 막는 코드가 없다. 반대해도 승인되고 동의해도\n * 거절된다 — 판정에 쓰면 AI 가 사실상 승인자가 되고, 그러면 사고 뒤에 책임을 물을 자리가 사라진다.\n */\n @Query(returns => ScalarObject, {\n nullable: true,\n description: 'What the twin sees now for one command, plus an AI opinion when asked. Advisory only — never gates the decision.'\n })\n async twinActuationOpinion(\n @Arg('id') id: string,\n /**\n * AI 에게도 물을 것인가 — **누르면 묻는다.**\n *\n * 기본은 안 묻는다. 화면을 열 때마다 부르면 승인자 다섯 명이 두 번씩 열 때 한 건에 열 번이고,\n * 대부분의 지시는 평범해서 그중 아홉 번은 같은 말을 다시 받는 것이다.\n */\n @Arg('askAi', { nullable: true }) askAi: boolean,\n @Arg('locale', { nullable: true }) locale: string,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n const domainId = context.state.domain?.id\n if (!domainId) throw new Error('테넌트를 알 수 없습니다')\n\n const command = await databaseCommandStore().load(domainId, id)\n if (!command) throw new Error(`모르는 조치입니다 (${id})`)\n\n /* 트윈의 지금 상태는 늘 새로 읽는다 — 계산값이라 싸고, 승인자는 지금을 봐야 한다. */\n const now = twinNow(domainId, command.instanceId, command.causeRef)\n\n if (!askAi) return { command, twinNow: now }\n\n /*\n * **의견을 저장하지 않는다.**\n *\n * 의견은 「지금」에 대한 것이고 승인자마다 여는 시각이 다르다. 저장해서 같은 문장을 돌려주면 그것이\n * 오히려 낡은 값이다. 그리고 이 값을 읽어 승인·거절을 막는 코드가 없다 — 판정에 쓰면 AI 가 사실상\n * 승인자가 되고, 사고 뒤에 책임을 물을 자리가 사라진다.\n */\n const advice = await askAdvisor({ command, twinNow: now, ...(locale ? { locale } : {}) })\n\n return { command, twinNow: now, advice }\n }\n\n /**\n * 조치를 낸다 — **대기 줄에 무언가가 들어오는 유일한 자리.**\n *\n * 결재선이 등록된 배포에서는 여기서 바로 결재선에 올라간다(§`ApprovalGateway`). 등록되지 않았으면\n * 제안 상태로 남고, 이 리졸버의 직접 승인이 유일한 문이 된다. 어느 쪽인지는 답에 적어 준다 — 화면이\n * 「어디서 승인하는가」를 사람에게 말해야 한다.\n *\n * 되돌릴 수 있는 조치는 결재선에 올리지 않는다. 계약이 그 판단을 든다(§`requiresApproval`).\n */\n @Directive('@transaction')\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, { description: 'Propose one actuation command against a twin. Raises it for approval when an approval gateway is installed.' })\n async proposeTwinCommand(\n @Arg('instanceId') instanceId: string,\n @Arg('type') type: string,\n @Arg('payload', type => ScalarObject, { nullable: true }) payload: any,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n const { domain, user } = context.state\n const by = String(user?.email ?? user?.name ?? '').trim()\n if (!by) throw new Error('누가 내는지 알 수 없습니다 — 로그인한 사용자가 없습니다')\n\n const store = databaseCommandStore(context.state.tx)\n const command = await propose(store, domain.id, MES_COMMANDS, {\n id: randomUUID(),\n instanceId,\n type,\n /*\n * 사람이 낸 것이다. 규칙·AI 가 내는 길은 아직 없다 — 그 길은 그것을 내는 쪽이 붙인다.\n * 여기서 인자로 받으면 사람이 「규칙이 냈다」로 적어 승인자를 오판하게 만들 수 있다.\n */\n origin: 'operator',\n /* 낸 사람을 적는다 — 이 값이 있어야 자기 것을 자기가 승인하는 것을 막을 수 있다. */\n proposedBy: by,\n proposedAt: new Date().toISOString(),\n ...(payload ? { payload } : {})\n })\n\n if (command.reversible === true) {\n return { command, gateway: null, note: '되돌릴 수 있는 조치입니다 — 승인 없이 넘길 수 있습니다' }\n }\n\n const gateway = approvalGateway()\n if (!gateway) {\n return {\n command,\n gateway: null,\n note: '결재선이 설치되지 않은 배포입니다 — 이 목록에서 직접 승인합니다'\n }\n }\n\n /*\n * 올리기와 적기가 한 트랜잭션에 있다(`@transaction`). 올렸는데 적지 못하면 결재선에는 올라가 있고\n * 커맨드는 직접 결정도 되는 상태가 남는다 — 그때 승인이 두 곳에 갈린다.\n */\n const approvalRef = await gateway.request(command, context)\n const linked = await linkApproval(store, domain.id, command.id, approvalRef)\n\n return { command: linked, gateway: gateway.label, note: `${gateway.label} 에 올렸습니다 — 승인은 그곳에서 합니다` }\n }\n\n /**\n * **사람이 결재선에 올린다** — 규칙이 낸 조치가 승인 앞에 서는 유일한 길.\n *\n * ── 왜 규칙이 직접 올리지 않나 ─────────────────────────────────────────\n * 업무 목록의 결재선은 **올린 사람 기준**으로 풀린다 — `Myself` · `MyDepartment` · `MySupervisor` 가\n * 전부 로그인한 사람을 본다. 배경 루프에는 「나」가 없으므로, 시스템 사용자를 지어내 올리면 그 셋이\n * 시스템 사용자를 가리키고 결재선이 뜻을 잃는다.\n *\n * 그래서 규칙은 내는 것까지 하고, 여기서 사람이 올린다. **올린 사람이 「이것을 승인자들 앞에 놓았다」의\n * 주체**가 되고, 그 사람 기준으로 결재선이 풀린다.\n *\n * 올린 사람은 자기가 올린 것을 승인할 수 있다 — 규칙이 낸 조치에는 낸 사람이 없으므로 승인자가 곧\n * 두 번째 사람이다. 올린 것과 낸 것은 다르다.\n */\n @Directive('@transaction')\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, { description: 'Raise one proposed command to the approval line. The signed-in user is the submitter.' })\n async raiseTwinCommand(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<any> {\n const { domain, user } = context.state\n if (!user?.id) throw new Error('누가 올리는지 알 수 없습니다 — 로그인한 사용자가 없습니다')\n\n const store = databaseCommandStore(context.state.tx)\n const command = await store.load(domain.id, id)\n if (!command) throw new Error(`모르는 조치입니다 (${id})`)\n\n if (command.approvalRef) {\n throw new Error(`이미 결재선에 올라가 있습니다 (${command.approvalRef}) — 승인은 업무 목록에서 합니다`)\n }\n if (command.state !== 'proposed') {\n throw new Error(`${command.state} 상태의 조치는 올릴 수 없습니다 — 이미 결정되었거나 나갔습니다`)\n }\n\n const gateway = approvalGateway()\n if (!gateway) {\n throw new Error('결재선이 설치되지 않은 배포입니다 — 이 목록에서 직접 승인합니다')\n }\n\n const approvalRef = await gateway.request(command, context)\n const linked = await linkApproval(store, domain.id, command.id, approvalRef)\n\n return { command: linked, gateway: gateway.label, note: `${gateway.label} 에 올렸습니다 — 승인은 그곳에서 합니다` }\n }\n\n /**\n * 승인한다 — **누가 승인했는지는 로그인한 사람에서 온다.**\n *\n * 인자로 받지 않는다. 받으면 다른 사람 이름으로 승인할 수 있고, 그러면 그 기록이 사고 뒤에 쓸 수\n * 없어진다. 승인 기록의 값어치가 거기서 나온다.\n */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, { description: 'Approve one actuation command. The approver is the signed-in user.' })\n async approveTwinCommand(\n @Arg('id') id: string,\n @Arg('note', { nullable: true }) note: string,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n const { domain, user } = context.state\n const by = String(user?.email ?? user?.name ?? '').trim()\n if (!by) throw new Error('누가 승인하는지 알 수 없습니다 — 로그인한 사용자가 없습니다')\n\n return approve(databaseCommandStore(context.state.tx), domain.id, id, {\n by,\n at: new Date().toISOString(),\n ...(note?.trim() ? { note: note.trim() } : {})\n })\n }\n\n /**\n * 거절한다 — 여기서 끝난다.\n *\n * 사유를 요구한다. 거절은 현장이 기다리던 조치를 취소하는 것이고, 이유 없이 거절되면 낸 쪽이 다시\n * 낸다. 그러면 같은 것이 대기 줄에 계속 돌아온다.\n */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, { description: 'Reject one actuation command. A reason is required.' })\n async rejectTwinCommand(\n @Arg('id') id: string,\n @Arg('reason') reason: string,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n const { domain, user } = context.state\n const by = String(user?.email ?? user?.name ?? '').trim()\n if (!by) throw new Error('누가 거절하는지 알 수 없습니다 — 로그인한 사용자가 없습니다')\n if (!reason?.trim()) throw new Error('거절 사유가 필요합니다 — 이유 없이 거절되면 같은 조치가 다시 올라옵니다')\n\n return reject(databaseCommandStore(context.state.tx), domain.id, id, { by, at: new Date().toISOString(), note: reason.trim() })\n }\n\n /**\n * 어댑터에 넘긴다 — **승인을 지나지 않은 것은 여기서 막힌다.**\n *\n * ── 이름이 왜 `dispatchTwinCommand` 가 아닌가 (2026-09-01) ─────────────\n * 그 이름은 이미 **커널 자극**이 쓴다(`TwinControlMutation.dispatchTwinCommand` —\n * `order.hold`·`resource.repair` 를 커널에 넣는다). 같은 이름을 두 번 선언하면 type-graphql 이\n * 오류 없이 하나만 남기고, 실제로 이 뮤테이션이 통째로 가려져 있었다 — 걸어 보고 알았다.\n *\n * 자극과 조치가 다른 어휘라는 것을 계약에서 갈라 놓고(§`propose` 의 거절), 정작 API 이름에서 겹쳐\n * 두었다. 여기서는 조치임을 이름에 적는다.\n *\n * 화면이 이것을 따로 부르는 이유는, 승인과 넘김이 다른 순간이기 때문이다. 승인한 사람과 넘긴 시각이\n * 같아야 할 이유가 없고, 어댑터가 죽어 있으면 승인은 남아 있어야 한다.\n *\n * ── 여기가 끊겨 있었다 (2026-09-02) ──────────────────────────────────\n * 이 자리가 **어댑터를 찾지도 않고** 「구현이 없습니다」를 답했다. MES 커넥터는 그때 이미 `actuate`\n * 를 다 만들어 두었다. 그 답을 사실로 읽어서 설계 문서에도, 다른 레인에도 그렇게 알렸다 — 둘 다\n * 틀렸다.\n *\n * 이제 원본을 찾아 그 원본이 선언한 능력으로 판단한다(§`routeActuation`). 보내지 못하면 **무엇이\n * 없어서인지** 말한다. 조용히 성공을 답하면 화면은 나간 줄로 읽고, 현장은 오지 않는 지시를 기다린다.\n *\n * ── 여기에는 트랜잭션을 걸지 않는다 ──────────────────────────────────\n * 넘기기 전에 「넘기는 중」을 먼저 적는 것이 이 함수의 요점이다. 한 트랜잭션에 묶으면 그 기록이\n * 커밋 전까지 아무에게도 보이지 않고, 넘기는 사이에 프로세스가 죽으면 그대로 사라진다. 그러면 저쪽은\n * 받았는데 우리는 안 보낸 줄 알고 다시 보낸다 — 현장이 두 번 움직인다.\n */\n /**\n * **이 조치를 넘길 수 있나** — 누르기 전에 답한다.\n *\n * ── 왜 이 자리가 필요한가 (2026-09-03) ────────────────────────────────────\n * 판정 함수 둘을 만들어 두고 **부르는 자리를 안 만들었다**(`actuationTargetOf`·\n * `actuationReadinessOf`). 그래서 화면이 물을 방법이 없었고, 못 넘기는 조치도 눌러 보고 알게 됐다.\n * 인티그레이션 레인이 그 사실을 두 번 짚어 줬다 — 자리는 있고 길이 없는 그 부류다.\n *\n * 그동안 값이 실제로 없어졌다. 저쪽에 비밀값이 없던 날, 사람이 승인 화면까지 가서 누르고 실패를\n * 보고 다시 로그인했다. **승인은 살아 있지만 사람의 시간은 돌아오지 않는다.**\n *\n * ── 둘을 함께 답한다 ──────────────────────────────────────────────────────\n * 화면이 묻는 것은 하나다 — 「지금 넘겨도 되나」. 그 답이 두 겹이라 둘을 함께 싣는다.\n *\n * target 보낼 대상이 정해지나 — 망을 타지 않는다(원본·능력·사이트)\n * readiness 저쪽이 받을 준비가 됐나 — 커넥터가 망을 탈 수 있다\n *\n * **문이 아니다.** 이 답으로 넘김을 막지 않는다(§`actuationReadinessOf`) — 화면이 미리 말하는\n * 데까지다. 그래서 조회이고 뮤테이션이 아니다.\n *\n * 화면이 **목록을 그릴 때 부르지 말 것.** 커넥터가 저쪽에 물을 수 있어서 목록 한 줄마다 망을 탄다.\n * 사람이 「넘기기」를 열 때 한 번이다.\n */\n /*\n * 권한 지시자를 달지 않는다 — 이 파일의 다른 조회 둘과 같다. **쓰기만 게이트를 지난다**(ADR-0027),\n * 그리고 조회는 테넌트로 좁혀진다. 처음에 `privilege: \"query\"` 를 달았더니 권한 문장 검사가\n * 「정본과 다르다」로 잡았다.\n */\n @Query(returns => ScalarObject, {\n nullable: true,\n description: 'Whether one approved actuation can be dispatched now — target and far-side readiness. Advisory, not a gate.'\n })\n async twinActuationReadiness(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<any> {\n const domainId = context.state.domain?.id\n if (!domainId) throw new Error('테넌트를 알 수 없습니다')\n\n const command = await databaseCommandStore().load(domainId, id)\n if (!command) throw new Error(`모르는 조치입니다 (${id})`)\n\n /*\n * 상태를 함께 낸다 — **이 조치가 넘길 수 있는 상태인가**는 저쪽 사정과 다른 물음이다. 이미\n * `acked` 인 것은 저쪽이 아무리 준비돼 있어도 다시 넘기지 않는다(종착).\n */\n const dispatchable = nextCommandState(command.state, 'dispatch') !== undefined\n\n const [target, readiness] = await Promise.all([\n actuationTargetOf(domainId, command.instanceId),\n actuationReadinessOf(domainId, command.instanceId)\n ])\n\n return { id, state: command.state, dispatchable, target, readiness }\n }\n\n /*\n * 권한 지시자를 달지 않는다 — 이 파일의 다른 조회들과 같다(ADR-0027: 쓰기만 게이트를 지난다).\n */\n @Query(returns => ScalarObject, {\n nullable: true,\n description: 'What happened to one actuation — for the person who just approved it. Includes whether work remains at the source.'\n })\n async twinActuationOutcome(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<any> {\n const domainId = context.state.domain?.id\n if (!domainId) throw new Error('테넌트를 알 수 없습니다')\n\n const command = await databaseCommandStore().load(domainId, id)\n if (!command) throw new Error(`모르는 조치입니다 (${id})`)\n\n /*\n * **승인 직후에 부르는 자리다.** 발신은 승인 응답이 나간 다음 틱에 일어나므로(§\n * `dispatchAfterApproval`), 그 순간에는 아직 `approved` 일 수 있다. 그것을 감추지 않는다 —\n * 화면이 「보내는 중」을 그리고 짧게 다시 묻는다(UX 레인 규칙: 대략 0.5초·2초 두 번, 그래도\n * 안 끝나면 조치 목록으로 안내).\n *\n * 여기서 기다리지 않는 이유: 기다리면 이 조회가 저쪽 왕복만큼 멈추고, 사람이 화면을 떠난\n * 뒤에도 붙잡는다.\n */\n return actuationOutcomeOf(command, nextCommandState(command.state, 'dispatch') !== undefined)\n }\n\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, { description: 'Send one approved actuation command to its connector. Not a kernel stimulus.' })\n async dispatchTwinActuation(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<any> {\n const { domain } = context.state\n\n /* 트랜잭션을 넘기지 않는다 — 위의 이유다. 지시자가 뒤에 붙어도 이 자리는 바뀌지 않아야 한다. */\n return dispatch(databaseCommandStore(), domain.id, id, command =>\n routeActuation(domain.id, command.instanceId, command)\n )\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"twin-command-resolver.js","sourceRoot":"","sources":["../../../server/service/actuation/twin-command-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Args, Ctx, Directive, Mutation, Query, Resolver } from 'type-graphql'\n\nimport { MES_COMMANDS, nextCommandState } from '@operato/ops-contract'\n\nimport { randomUUID } from 'node:crypto'\n\nimport { ListParam, ScalarObject, getQueryBuilderFromListParams, getRepository } from '@things-factory/shell'\n\nimport { askAdvisor } from './actuation-advisor.js'\nimport { approvalGateway } from './approval-gateway.js'\nimport { approve, dispatch, linkApproval, propose, reject } from './command-dispatcher.js'\nimport { actuationReadinessOf, actuationTargetOf, routeActuation } from '../reference/actuation-routing.js'\n\nimport { actuationOutcomeOf } from './actuation-outcome.js'\nimport { databaseCommandStore } from './command-store.js'\nimport { TwinCommandRecord } from './twin-command.js'\nimport { twinNow } from './twin-now.js'\n\n/*\n * 승인 대기 줄과 그 결정 — **사람이 쓰는 자리**.\n *\n * ── 왜 필요한가 (2026-08-31) ─────────────────────────────────────────────\n * 게이트를 세우고 승인할 화면을 만들지 않았다. 그래서 되돌릴 수 없는 조치는 전부 대기 상태로 쌓이고\n * **사람이 승인할 방법이 없었다.** 서버만 있고 사람이 쓸 자리가 없는 상태다.\n *\n * ── 왜 조회와 결정을 한 파일에 두나 ──────────────────────────────────────\n * 대기 줄을 여는 것과 그 줄에서 결정하는 것은 한 화면의 일이다. 나누면 화면이 두 곳을 부르게 되고,\n * 그중 하나만 권한이 바뀌는 날 목록은 보이는데 결정이 안 되거나 그 반대가 된다.\n */\n\n@Resolver()\nexport class TwinCommandResolver {\n /**\n * 커맨드 목록 — 기본은 **결정을 기다리는 것부터**.\n *\n * 정렬을 최근순으로 두지 않는다. 승인 대기 줄에서 사람이 찾는 것은 「새로 온 것」이 아니라\n * **「오래 기다린 것」**이다 — 오래 기다린 조치가 뒤로 밀리면 그것이 가장 늦게 처리된다.\n */\n @Query(returns => ScalarObject, {\n nullable: true,\n description: 'List actuation commands with the standard list contract. Defaults to the oldest waiting first.'\n })\n async twinCommands(\n /* 표준 목록 계약은 `@Args` 로 펼쳐 받는다 — `@Arg` 로 받으면 스키마 조립이 실패한다. */\n @Args(type => ListParam) params: ListParam,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n const domainId = context.state.domain?.id\n if (!domainId) return { items: [], total: 0 }\n\n const effective: ListParam = {\n ...(params ?? ({} as ListParam)),\n sortings: params?.sortings?.length ? params.sortings : [{ name: 'createdAt', desc: false }]\n }\n\n const qb = getQueryBuilderFromListParams({\n repository: getRepository(TwinCommandRecord),\n params: effective,\n domain: context.state.domain,\n alias: 'c',\n /* 전부 실컬럼이다 — 질의 빌더는 없는 컬럼의 LIKE 를 경고 후 무시한다(인덱스 없는 전체 스캔 방지). */\n searchables: ['instanceId', 'type', 'origin', 'state', 'approvalRef', 'decidedBy', 'error'],\n sortables: ['createdAt', 'updatedAt', 'state', 'type', 'instanceId'],\n /* 사람이 읽는 속도로 본다. */\n defaultLimit: 50,\n maxLimit: 200\n })\n\n const [items, total] = await qb.getManyAndCount()\n return { items, total }\n }\n\n /**\n * **이 조치에 대해 트윈이 지금 보는 것과 AI 의견** — 승인 화면이 부른다.\n *\n * ── 왜 조회인가 (2026-09-01) ───────────────────────────────────────────\n * 결재선에 올릴 때 묻지 않는다. 그 자리는 트랜잭션 안이고 거기서 LLM 을 기다리면 DB 연결 하나를 몇 초\n * 붙잡는다. 그리고 **승인하는 사람은 지금 판단한다** — 올린 시각의 의견은 낡았다.\n *\n * AI 는 판정하지 않는다. 이 값을 읽어 승인·거절을 막는 코드가 없다. 반대해도 승인되고 동의해도\n * 거절된다 — 판정에 쓰면 AI 가 사실상 승인자가 되고, 그러면 사고 뒤에 책임을 물을 자리가 사라진다.\n */\n @Query(returns => ScalarObject, {\n nullable: true,\n description: 'What the twin sees now for one command, plus an AI opinion when asked. Advisory only — never gates the decision.'\n })\n async twinActuationOpinion(\n @Arg('id') id: string,\n /**\n * AI 에게도 물을 것인가 — **누르면 묻는다.**\n *\n * 기본은 안 묻는다. 화면을 열 때마다 부르면 승인자 다섯 명이 두 번씩 열 때 한 건에 열 번이고,\n * 대부분의 지시는 평범해서 그중 아홉 번은 같은 말을 다시 받는 것이다.\n */\n @Arg('askAi', { nullable: true }) askAi: boolean,\n @Arg('locale', { nullable: true }) locale: string,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n const domainId = context.state.domain?.id\n if (!domainId) throw new Error('테넌트를 알 수 없습니다')\n\n const command = await databaseCommandStore().load(domainId, id)\n if (!command) throw new Error(`모르는 조치입니다 (${id})`)\n\n /* 트윈의 지금 상태는 늘 새로 읽는다 — 계산값이라 싸고, 승인자는 지금을 봐야 한다. */\n const now = twinNow(domainId, command.instanceId, command.causeRef)\n\n if (!askAi) return { command, twinNow: now }\n\n /*\n * **의견을 저장하지 않는다.**\n *\n * 의견은 「지금」에 대한 것이고 승인자마다 여는 시각이 다르다. 저장해서 같은 문장을 돌려주면 그것이\n * 오히려 낡은 값이다. 그리고 이 값을 읽어 승인·거절을 막는 코드가 없다 — 판정에 쓰면 AI 가 사실상\n * 승인자가 되고, 사고 뒤에 책임을 물을 자리가 사라진다.\n */\n const advice = await askAdvisor({ domainId, command, twinNow: now, ...(locale ? { locale } : {}) })\n\n return { command, twinNow: now, advice }\n }\n\n /**\n * 조치를 낸다 — **대기 줄에 무언가가 들어오는 유일한 자리.**\n *\n * 결재선이 등록된 배포에서는 여기서 바로 결재선에 올라간다(§`ApprovalGateway`). 등록되지 않았으면\n * 제안 상태로 남고, 이 리졸버의 직접 승인이 유일한 문이 된다. 어느 쪽인지는 답에 적어 준다 — 화면이\n * 「어디서 승인하는가」를 사람에게 말해야 한다.\n *\n * 되돌릴 수 있는 조치는 결재선에 올리지 않는다. 계약이 그 판단을 든다(§`requiresApproval`).\n */\n @Directive('@transaction')\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, { description: 'Propose one actuation command against a twin. Raises it for approval when an approval gateway is installed.' })\n async proposeTwinCommand(\n @Arg('instanceId') instanceId: string,\n @Arg('type') type: string,\n @Arg('payload', type => ScalarObject, { nullable: true }) payload: any,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n const { domain, user } = context.state\n const by = String(user?.email ?? user?.name ?? '').trim()\n if (!by) throw new Error('누가 내는지 알 수 없습니다 — 로그인한 사용자가 없습니다')\n\n const store = databaseCommandStore(context.state.tx)\n const command = await propose(store, domain.id, MES_COMMANDS, {\n id: randomUUID(),\n instanceId,\n type,\n /*\n * 사람이 낸 것이다. 규칙·AI 가 내는 길은 아직 없다 — 그 길은 그것을 내는 쪽이 붙인다.\n * 여기서 인자로 받으면 사람이 「규칙이 냈다」로 적어 승인자를 오판하게 만들 수 있다.\n */\n origin: 'operator',\n /* 낸 사람을 적는다 — 이 값이 있어야 자기 것을 자기가 승인하는 것을 막을 수 있다. */\n proposedBy: by,\n proposedAt: new Date().toISOString(),\n ...(payload ? { payload } : {})\n })\n\n if (command.reversible === true) {\n return { command, gateway: null, note: '되돌릴 수 있는 조치입니다 — 승인 없이 넘길 수 있습니다' }\n }\n\n const gateway = approvalGateway()\n if (!gateway) {\n return {\n command,\n gateway: null,\n note: '결재선이 설치되지 않은 배포입니다 — 이 목록에서 직접 승인합니다'\n }\n }\n\n /*\n * 올리기와 적기가 한 트랜잭션에 있다(`@transaction`). 올렸는데 적지 못하면 결재선에는 올라가 있고\n * 커맨드는 직접 결정도 되는 상태가 남는다 — 그때 승인이 두 곳에 갈린다.\n */\n const approvalRef = await gateway.request(command, context)\n const linked = await linkApproval(store, domain.id, command.id, approvalRef)\n\n return { command: linked, gateway: gateway.label, note: `${gateway.label} 에 올렸습니다 — 승인은 그곳에서 합니다` }\n }\n\n /**\n * **사람이 결재선에 올린다** — 규칙이 낸 조치가 승인 앞에 서는 유일한 길.\n *\n * ── 왜 규칙이 직접 올리지 않나 ─────────────────────────────────────────\n * 업무 목록의 결재선은 **올린 사람 기준**으로 풀린다 — `Myself` · `MyDepartment` · `MySupervisor` 가\n * 전부 로그인한 사람을 본다. 배경 루프에는 「나」가 없으므로, 시스템 사용자를 지어내 올리면 그 셋이\n * 시스템 사용자를 가리키고 결재선이 뜻을 잃는다.\n *\n * 그래서 규칙은 내는 것까지 하고, 여기서 사람이 올린다. **올린 사람이 「이것을 승인자들 앞에 놓았다」의\n * 주체**가 되고, 그 사람 기준으로 결재선이 풀린다.\n *\n * 올린 사람은 자기가 올린 것을 승인할 수 있다 — 규칙이 낸 조치에는 낸 사람이 없으므로 승인자가 곧\n * 두 번째 사람이다. 올린 것과 낸 것은 다르다.\n */\n @Directive('@transaction')\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, { description: 'Raise one proposed command to the approval line. The signed-in user is the submitter.' })\n async raiseTwinCommand(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<any> {\n const { domain, user } = context.state\n if (!user?.id) throw new Error('누가 올리는지 알 수 없습니다 — 로그인한 사용자가 없습니다')\n\n const store = databaseCommandStore(context.state.tx)\n const command = await store.load(domain.id, id)\n if (!command) throw new Error(`모르는 조치입니다 (${id})`)\n\n if (command.approvalRef) {\n throw new Error(`이미 결재선에 올라가 있습니다 (${command.approvalRef}) — 승인은 업무 목록에서 합니다`)\n }\n if (command.state !== 'proposed') {\n throw new Error(`${command.state} 상태의 조치는 올릴 수 없습니다 — 이미 결정되었거나 나갔습니다`)\n }\n\n const gateway = approvalGateway()\n if (!gateway) {\n throw new Error('결재선이 설치되지 않은 배포입니다 — 이 목록에서 직접 승인합니다')\n }\n\n const approvalRef = await gateway.request(command, context)\n const linked = await linkApproval(store, domain.id, command.id, approvalRef)\n\n return { command: linked, gateway: gateway.label, note: `${gateway.label} 에 올렸습니다 — 승인은 그곳에서 합니다` }\n }\n\n /**\n * 승인한다 — **누가 승인했는지는 로그인한 사람에서 온다.**\n *\n * 인자로 받지 않는다. 받으면 다른 사람 이름으로 승인할 수 있고, 그러면 그 기록이 사고 뒤에 쓸 수\n * 없어진다. 승인 기록의 값어치가 거기서 나온다.\n */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, { description: 'Approve one actuation command. The approver is the signed-in user.' })\n async approveTwinCommand(\n @Arg('id') id: string,\n @Arg('note', { nullable: true }) note: string,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n const { domain, user } = context.state\n const by = String(user?.email ?? user?.name ?? '').trim()\n if (!by) throw new Error('누가 승인하는지 알 수 없습니다 — 로그인한 사용자가 없습니다')\n\n return approve(databaseCommandStore(context.state.tx), domain.id, id, {\n by,\n at: new Date().toISOString(),\n ...(note?.trim() ? { note: note.trim() } : {})\n })\n }\n\n /**\n * 거절한다 — 여기서 끝난다.\n *\n * 사유를 요구한다. 거절은 현장이 기다리던 조치를 취소하는 것이고, 이유 없이 거절되면 낸 쪽이 다시\n * 낸다. 그러면 같은 것이 대기 줄에 계속 돌아온다.\n */\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, { description: 'Reject one actuation command. A reason is required.' })\n async rejectTwinCommand(\n @Arg('id') id: string,\n @Arg('reason') reason: string,\n @Ctx() context: ResolverContext\n ): Promise<any> {\n const { domain, user } = context.state\n const by = String(user?.email ?? user?.name ?? '').trim()\n if (!by) throw new Error('누가 거절하는지 알 수 없습니다 — 로그인한 사용자가 없습니다')\n if (!reason?.trim()) throw new Error('거절 사유가 필요합니다 — 이유 없이 거절되면 같은 조치가 다시 올라옵니다')\n\n return reject(databaseCommandStore(context.state.tx), domain.id, id, { by, at: new Date().toISOString(), note: reason.trim() })\n }\n\n /**\n * 어댑터에 넘긴다 — **승인을 지나지 않은 것은 여기서 막힌다.**\n *\n * ── 이름이 왜 `dispatchTwinCommand` 가 아닌가 (2026-09-01) ─────────────\n * 그 이름은 이미 **커널 자극**이 쓴다(`TwinControlMutation.dispatchTwinCommand` —\n * `order.hold`·`resource.repair` 를 커널에 넣는다). 같은 이름을 두 번 선언하면 type-graphql 이\n * 오류 없이 하나만 남기고, 실제로 이 뮤테이션이 통째로 가려져 있었다 — 걸어 보고 알았다.\n *\n * 자극과 조치가 다른 어휘라는 것을 계약에서 갈라 놓고(§`propose` 의 거절), 정작 API 이름에서 겹쳐\n * 두었다. 여기서는 조치임을 이름에 적는다.\n *\n * 화면이 이것을 따로 부르는 이유는, 승인과 넘김이 다른 순간이기 때문이다. 승인한 사람과 넘긴 시각이\n * 같아야 할 이유가 없고, 어댑터가 죽어 있으면 승인은 남아 있어야 한다.\n *\n * ── 여기가 끊겨 있었다 (2026-09-02) ──────────────────────────────────\n * 이 자리가 **어댑터를 찾지도 않고** 「구현이 없습니다」를 답했다. MES 커넥터는 그때 이미 `actuate`\n * 를 다 만들어 두었다. 그 답을 사실로 읽어서 설계 문서에도, 다른 레인에도 그렇게 알렸다 — 둘 다\n * 틀렸다.\n *\n * 이제 원본을 찾아 그 원본이 선언한 능력으로 판단한다(§`routeActuation`). 보내지 못하면 **무엇이\n * 없어서인지** 말한다. 조용히 성공을 답하면 화면은 나간 줄로 읽고, 현장은 오지 않는 지시를 기다린다.\n *\n * ── 여기에는 트랜잭션을 걸지 않는다 ──────────────────────────────────\n * 넘기기 전에 「넘기는 중」을 먼저 적는 것이 이 함수의 요점이다. 한 트랜잭션에 묶으면 그 기록이\n * 커밋 전까지 아무에게도 보이지 않고, 넘기는 사이에 프로세스가 죽으면 그대로 사라진다. 그러면 저쪽은\n * 받았는데 우리는 안 보낸 줄 알고 다시 보낸다 — 현장이 두 번 움직인다.\n */\n /**\n * **이 조치를 넘길 수 있나** — 누르기 전에 답한다.\n *\n * ── 왜 이 자리가 필요한가 (2026-09-03) ────────────────────────────────────\n * 판정 함수 둘을 만들어 두고 **부르는 자리를 안 만들었다**(`actuationTargetOf`·\n * `actuationReadinessOf`). 그래서 화면이 물을 방법이 없었고, 못 넘기는 조치도 눌러 보고 알게 됐다.\n * 인티그레이션 레인이 그 사실을 두 번 짚어 줬다 — 자리는 있고 길이 없는 그 부류다.\n *\n * 그동안 값이 실제로 없어졌다. 저쪽에 비밀값이 없던 날, 사람이 승인 화면까지 가서 누르고 실패를\n * 보고 다시 로그인했다. **승인은 살아 있지만 사람의 시간은 돌아오지 않는다.**\n *\n * ── 둘을 함께 답한다 ──────────────────────────────────────────────────────\n * 화면이 묻는 것은 하나다 — 「지금 넘겨도 되나」. 그 답이 두 겹이라 둘을 함께 싣는다.\n *\n * target 보낼 대상이 정해지나 — 망을 타지 않는다(원본·능력·사이트)\n * readiness 저쪽이 받을 준비가 됐나 — 커넥터가 망을 탈 수 있다\n *\n * **문이 아니다.** 이 답으로 넘김을 막지 않는다(§`actuationReadinessOf`) — 화면이 미리 말하는\n * 데까지다. 그래서 조회이고 뮤테이션이 아니다.\n *\n * 화면이 **목록을 그릴 때 부르지 말 것.** 커넥터가 저쪽에 물을 수 있어서 목록 한 줄마다 망을 탄다.\n * 사람이 「넘기기」를 열 때 한 번이다.\n */\n /*\n * 권한 지시자를 달지 않는다 — 이 파일의 다른 조회 둘과 같다. **쓰기만 게이트를 지난다**(ADR-0027),\n * 그리고 조회는 테넌트로 좁혀진다. 처음에 `privilege: \"query\"` 를 달았더니 권한 문장 검사가\n * 「정본과 다르다」로 잡았다.\n */\n @Query(returns => ScalarObject, {\n nullable: true,\n description: 'Whether one approved actuation can be dispatched now — target and far-side readiness. Advisory, not a gate.'\n })\n async twinActuationReadiness(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<any> {\n const domainId = context.state.domain?.id\n if (!domainId) throw new Error('테넌트를 알 수 없습니다')\n\n const command = await databaseCommandStore().load(domainId, id)\n if (!command) throw new Error(`모르는 조치입니다 (${id})`)\n\n /*\n * 상태를 함께 낸다 — **이 조치가 넘길 수 있는 상태인가**는 저쪽 사정과 다른 물음이다. 이미\n * `acked` 인 것은 저쪽이 아무리 준비돼 있어도 다시 넘기지 않는다(종착).\n */\n const dispatchable = nextCommandState(command.state, 'dispatch') !== undefined\n\n const [target, readiness] = await Promise.all([\n actuationTargetOf(domainId, command.instanceId),\n actuationReadinessOf(domainId, command.instanceId)\n ])\n\n return { id, state: command.state, dispatchable, target, readiness }\n }\n\n /*\n * 권한 지시자를 달지 않는다 — 이 파일의 다른 조회들과 같다(ADR-0027: 쓰기만 게이트를 지난다).\n */\n @Query(returns => ScalarObject, {\n nullable: true,\n description: 'What happened to one actuation — for the person who just approved it. Includes whether work remains at the source.'\n })\n async twinActuationOutcome(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<any> {\n const domainId = context.state.domain?.id\n if (!domainId) throw new Error('테넌트를 알 수 없습니다')\n\n const command = await databaseCommandStore().load(domainId, id)\n if (!command) throw new Error(`모르는 조치입니다 (${id})`)\n\n /*\n * **승인 직후에 부르는 자리다.** 발신은 승인 응답이 나간 다음 틱에 일어나므로(§\n * `dispatchAfterApproval`), 그 순간에는 아직 `approved` 일 수 있다. 그것을 감추지 않는다 —\n * 화면이 「보내는 중」을 그리고 짧게 다시 묻는다(UX 레인 규칙: 대략 0.5초·2초 두 번, 그래도\n * 안 끝나면 조치 목록으로 안내).\n *\n * 여기서 기다리지 않는 이유: 기다리면 이 조회가 저쪽 왕복만큼 멈추고, 사람이 화면을 떠난\n * 뒤에도 붙잡는다.\n */\n return actuationOutcomeOf(command, nextCommandState(command.state, 'dispatch') !== undefined)\n }\n\n @Directive('@privilege(category: \"twin\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Mutation(returns => ScalarObject, { description: 'Send one approved actuation command to its connector. Not a kernel stimulus.' })\n async dispatchTwinActuation(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<any> {\n const { domain } = context.state\n\n /* 트랜잭션을 넘기지 않는다 — 위의 이유다. 지시자가 뒤에 붙어도 이 자리는 바뀌지 않아야 한다. */\n return dispatch(databaseCommandStore(), domain.id, id, command =>\n routeActuation(domain.id, command.instanceId, command)\n )\n }\n}\n"]}
|
|
@@ -18,7 +18,7 @@ const twin_operation_js_1 = require("./twin-operation.js");
|
|
|
18
18
|
const twin_area_js_1 = require("../twin-space/twin-area.js");
|
|
19
19
|
const name_index_js_1 = require("./name-index.js");
|
|
20
20
|
/* 커널 런타임 로드 — 축·관계·속성 선언의 SSOT. 이 파일은 도메인 모양을 알지 않는다. */
|
|
21
|
-
const { TWIN_AXES, TWIN_RELATIONS, axisInfo, axisSource, documentPath, isCacheableTense, relationsFrom, relationsTo, propertiesOf, tenseOf } = require('@operato/ops-contract');
|
|
21
|
+
const { TWIN_AXES, TWIN_RELATIONS, axisInfo, axisSource, documentPath, isCacheableTense, relationsFrom, relationsTo, propertiesOf, splitRelationIds, tenseOf } = require('@operato/ops-contract');
|
|
22
22
|
const external_resolver_js_1 = require("./external-resolver.js");
|
|
23
23
|
/**
|
|
24
24
|
* 축 → 그 축이 이미 **행으로 옮겨졌나**, 그리고 행의 어느 컬럼이 id 인가.
|
|
@@ -724,6 +724,25 @@ let TwinModelItemQuery = class TwinModelItemQuery {
|
|
|
724
724
|
const rawSelf = axisPath ? (0, name_index_js_1.at)(model, axisPath) : undefined;
|
|
725
725
|
const selfAll = Array.isArray(rawSelf) ? rawSelf : rawSelf ? [rawSelf] : [];
|
|
726
726
|
const self = self0 ?? selfAll.find((o) => (0, name_index_js_1.idOf)(o) === itemId);
|
|
727
|
+
/*
|
|
728
|
+
* ── 한 필드가 두 곳을 가리킬 때 (계약 0.9.24, 2026-09-17) ──────────────────────
|
|
729
|
+
* `locations.parentId` 는 다른 자리(라인 · 셀 · 존)이거나, 그 id 의 자리가 없으면 공간의 구역이다
|
|
730
|
+
* (`axis-or-external`). 어느 쪽인지는 **조회로** 갈린다 — 규칙은 계약의 `splitRelationIds` 하나다.
|
|
731
|
+
*
|
|
732
|
+
* 화면에는 두 줄로 낸다: 자리 쪽은 그 자리로 걷는 링크(`target.kind: 'axis'`), 나머지는 구역으로
|
|
733
|
+
* 풀린 것(`external`). 한 줄에 섞으면 화면이 그 줄 전체를 외부로 그리거나 전체를 링크로 그려 둘 중
|
|
734
|
+
* 하나가 틀린다. 선언은 `declaredTarget` 으로 함께 남긴다.
|
|
735
|
+
*/
|
|
736
|
+
const axisIdSets = new Map();
|
|
737
|
+
for (const r of relationsFrom(axis)) {
|
|
738
|
+
if (r.target?.kind !== 'axis-or-external' || axisIdSets.has(r.target.axis))
|
|
739
|
+
continue;
|
|
740
|
+
const tInfo = axisInfo(r.target.axis);
|
|
741
|
+
const raw = tInfo && axisSource(r.target.axis) === 'document' ? (0, name_index_js_1.at)(model, tInfo.path) : await observedBucket(r.target.axis);
|
|
742
|
+
const all = Array.isArray(raw) ? raw : raw ? [raw] : [];
|
|
743
|
+
axisIdSets.set(r.target.axis, new Set(all.map((o) => (0, name_index_js_1.idOf)(o)).filter(Boolean).map(String)));
|
|
744
|
+
}
|
|
745
|
+
const inAxis = (a, id) => axisIdSets.get(a)?.has(id) ?? false;
|
|
727
746
|
/* 나가는 쪽 — 내 필드가 가리키는 것. 선언이 필드 이름을 알려 준다. */
|
|
728
747
|
const out = relationsFrom(axis).map((r) => {
|
|
729
748
|
const raw = self ? this.fieldValue(self, r.field) : [];
|
|
@@ -749,6 +768,18 @@ let TwinModelItemQuery = class TwinModelItemQuery {
|
|
|
749
768
|
ids: ids.slice(0, NEIGHBOR_MAX),
|
|
750
769
|
total: ids.length
|
|
751
770
|
};
|
|
771
|
+
}).flatMap((l) => {
|
|
772
|
+
if (l.target?.kind !== 'axis-or-external')
|
|
773
|
+
return [l];
|
|
774
|
+
const all = self ? this.fieldValue(self, l.field).filter((v) => v != null).map(String) : [];
|
|
775
|
+
const split = splitRelationIds(l.target, all, inAxis);
|
|
776
|
+
const part = (target, ids) => ({ ...l, target, declaredTarget: l.target, ids: ids.slice(0, NEIGHBOR_MAX), total: ids.length });
|
|
777
|
+
const parts = [
|
|
778
|
+
...(split.axis?.ids.length ? [part({ kind: 'axis', axis: split.axis.axis }, split.axis.ids)] : []),
|
|
779
|
+
...(split.external?.ids.length ? [part({ kind: 'external', entity: split.external.entity }, split.external.ids)] : [])
|
|
780
|
+
];
|
|
781
|
+
/* 비어 있는 관계도 낸다(아래 주석) — 가리키는 것이 없으면 선언 그대로 한 줄. */
|
|
782
|
+
return parts.length ? parts : [l];
|
|
752
783
|
});
|
|
753
784
|
/*
|
|
754
785
|
* ── 밖을 가리키는 관계를 푼다 (2026-09-05) ─────────────────────────────────
|
|
@@ -789,7 +820,12 @@ let TwinModelItemQuery = class TwinModelItemQuery {
|
|
|
789
820
|
if (!fromInfo)
|
|
790
821
|
continue;
|
|
791
822
|
const hits = [];
|
|
792
|
-
|
|
823
|
+
/*
|
|
824
|
+
* 행은 `axis` 관계만 담는다. `axis-or-external`(자리 → 부모 자리 또는 구역)은 행에 구역 FK(`area`)만
|
|
825
|
+
* 있고 부모 **자리**를 담는 칸이 없어서, 행에서 세면 「이 존 아래 자리들」이 조용히 0이 된다.
|
|
826
|
+
* 그 관계는 선언 어휘가 사는 문서에서 센다.
|
|
827
|
+
*/
|
|
828
|
+
const row = r.target?.kind === 'axis' ? ROW_BACKED[r.from] : undefined;
|
|
793
829
|
if (row) {
|
|
794
830
|
const repo = (0, shell_1.getRepository)(row.entity);
|
|
795
831
|
/*
|