@things-factory/worklist 10.1.4 → 10.1.6
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-client/components/activity-approval-ribon.js +17 -2
- package/dist-client/components/activity-approval-ribon.js.map +1 -1
- package/dist-client/components/decision-slots.d.ts +89 -0
- package/dist-client/components/decision-slots.js +111 -0
- package/dist-client/components/decision-slots.js.map +1 -0
- package/dist-client/pages/activity-approval/activity-approval-page.d.ts +72 -0
- package/dist-client/pages/activity-approval/activity-approval-page.js +425 -43
- package/dist-client/pages/activity-approval/activity-approval-page.js.map +1 -1
- package/dist-client/pages/activity-catalog/activity-catalog-page.d.ts +24 -0
- package/dist-client/pages/activity-catalog/activity-catalog-page.js +101 -6
- package/dist-client/pages/activity-catalog/activity-catalog-page.js.map +1 -1
- package/dist-client/pages/installable-activity/installable-activity-list-page.d.ts +27 -0
- package/dist-client/pages/installable-activity/installable-activity-list-page.js +70 -11
- package/dist-client/pages/installable-activity/installable-activity-list-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/controllers/activity-approval/approval-line.d.ts +32 -0
- package/dist-server/controllers/activity-approval/approval-line.js +35 -0
- package/dist-server/controllers/activity-approval/approval-line.js.map +1 -1
- package/dist-server/controllers/activity-approval/approve.js +17 -0
- package/dist-server/controllers/activity-approval/approve.js.map +1 -1
- package/dist-server/controllers/activity-approval/reject.js +21 -1
- package/dist-server/controllers/activity-approval/reject.js.map +1 -1
- package/dist-server/controllers/activity-installation/activity-installation-controller.js +5 -9
- package/dist-server/controllers/activity-installation/activity-installation-controller.js.map +1 -1
- package/dist-server/controllers/activity-installation/installable-to-activity.d.ts +49 -0
- package/dist-server/controllers/activity-installation/installable-to-activity.js +40 -0
- package/dist-server/controllers/activity-installation/installable-to-activity.js.map +1 -0
- package/dist-server/controllers/activity-installation/registry-conflict.d.ts +32 -0
- package/dist-server/controllers/activity-installation/registry-conflict.js +21 -0
- package/dist-server/controllers/activity-installation/registry-conflict.js.map +1 -0
- package/dist-server/service/activity/activity-decision-key-item-type.d.ts +160 -0
- package/dist-server/service/activity/activity-decision-key-item-type.js +176 -0
- package/dist-server/service/activity/activity-decision-key-item-type.js.map +1 -0
- package/dist-server/service/activity/activity-type.d.ts +3 -0
- package/dist-server/service/activity/activity-type.js +8 -0
- package/dist-server/service/activity/activity-type.js.map +1 -1
- package/dist-server/service/activity/activity.d.ts +30 -0
- package/dist-server/service/activity/activity.js +14 -0
- package/dist-server/service/activity/activity.js.map +1 -1
- package/dist-server/service/activity/decision-key-declaration.d.ts +33 -0
- package/dist-server/service/activity/decision-key-declaration.js +67 -0
- package/dist-server/service/activity/decision-key-declaration.js.map +1 -0
- package/dist-server/service/activity/{credential-guard-subscriber.d.ts → declaration-guard-subscriber.d.ts} +6 -1
- package/dist-server/service/activity/declaration-guard-subscriber.js +46 -0
- package/dist-server/service/activity/declaration-guard-subscriber.js.map +1 -0
- package/dist-server/service/activity/index.d.ts +3 -3
- package/dist-server/service/activity/index.js +2 -2
- package/dist-server/service/activity/index.js.map +1 -1
- package/dist-server/service/activity/search-key-declaration.d.ts +38 -0
- package/dist-server/service/activity/search-key-declaration.js +49 -0
- package/dist-server/service/activity/search-key-declaration.js.map +1 -0
- package/dist-server/service/activity-instance/activity-instance-type.js.map +1 -1
- package/dist-server/service/activity-instance/index.d.ts +1 -1
- package/dist-server/service/activity-template/index.d.ts +1 -1
- package/dist-server/service/activity-thread/index.d.ts +1 -1
- package/dist-server/service/index.d.ts +3 -3
- package/dist-server/service/installable-activity/installable-activity-mutation.js +24 -0
- package/dist-server/service/installable-activity/installable-activity-mutation.js.map +1 -1
- package/dist-server/service/installable-activity/installable-activity.d.ts +9 -0
- package/dist-server/service/installable-activity/installable-activity.js +5 -0
- package/dist-server/service/installable-activity/installable-activity.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -9
- package/translations/en.json +28 -6
- package/translations/ja.json +22 -0
- package/translations/ko.json +28 -6
- package/translations/ms.json +22 -0
- package/translations/zh.json +22 -0
- package/dist-server/service/activity/credential-guard-subscriber.js +0 -35
- package/dist-server/service/activity/credential-guard-subscriber.js.map +0 -1
|
@@ -104,3 +104,35 @@ export declare const TERMINATED_JUDGMENTS: readonly ["approved", "escalated", "d
|
|
|
104
104
|
* 목록에서 사라지지도 않는다. 닫는 쪽은 제대로 닫았는데 읽는 쪽이 그것을 못 읽었다.
|
|
105
105
|
*/
|
|
106
106
|
export declare function isTerminated(judgment: string | undefined): boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Is the seat at this turn declared as a **reference** — someone who comments but does not decide?
|
|
109
|
+
*
|
|
110
|
+
* ── Why this is a function and not two inline calls ────────────────────────
|
|
111
|
+
* ApprovalParticipation has said from the start that a reference only comments and does not affect
|
|
112
|
+
* the judgment. create-approval, the escalation in approve(), and both list queries honour that.
|
|
113
|
+
* The two places where a person presses a button did not: approve() and reject() never asked whether
|
|
114
|
+
* the row they are about to stamp is an approver's row.
|
|
115
|
+
*
|
|
116
|
+
* A reference row is created with judgment Pending, so it passes every check those two functions do
|
|
117
|
+
* make. Approving one stamps it as a real approval and then nextApproval() picks the approver that
|
|
118
|
+
* sits *after* that seat, which moves the approval a step forward. With no approver after it, one
|
|
119
|
+
* reference ends the whole thread. Rejecting is worse: an activity with rejectionIsFinal cannot be
|
|
120
|
+
* restarted, so the case closes for good.
|
|
121
|
+
*
|
|
122
|
+
* Writing the check inline in both callers would put the rule back in two places, which is the
|
|
123
|
+
* reason this file exists.
|
|
124
|
+
*
|
|
125
|
+
* ── The approval line is the authority, not the row ───────────────────────
|
|
126
|
+
* The capacity is also copied onto ActivityApproval.participation. approve() already lets the
|
|
127
|
+
* approval line decide who comes next (nextApproval(approvalLine, order)); letting the row decide
|
|
128
|
+
* who may judge would put two authorities in one function, with nothing to settle a disagreement.
|
|
129
|
+
* The line is loaded either way, so this costs no extra read. (2026-09-09, architect's ruling)
|
|
130
|
+
*
|
|
131
|
+
* ── False when the seat is not in the line ────────────────────────────────
|
|
132
|
+
* That answers "not declared a reference", which is not the same as "is an approver". One boolean
|
|
133
|
+
* cannot carry both facts, so this function reports only what is written down and the caller
|
|
134
|
+
* refuses on a definite yes. A missing seat means the line grew shorter after submission; telling
|
|
135
|
+
* that person they are a reference would state something the data does not say. Whether the line
|
|
136
|
+
* can be edited after submission is being measured separately.
|
|
137
|
+
*/
|
|
138
|
+
export declare function isReferenceOrder(approvalLine: ApprovalLineItem[] | undefined, order: number): boolean;
|
|
@@ -12,6 +12,7 @@ exports.isLastApproval = isLastApproval;
|
|
|
12
12
|
exports.needsApproval = needsApproval;
|
|
13
13
|
exports.isApproved = isApproved;
|
|
14
14
|
exports.isTerminated = isTerminated;
|
|
15
|
+
exports.isReferenceOrder = isReferenceOrder;
|
|
15
16
|
/**
|
|
16
17
|
* 결재선에서 다음 결재자를 찾는다.
|
|
17
18
|
*
|
|
@@ -148,4 +149,38 @@ exports.TERMINATED_JUDGMENTS = ['approved', 'escalated', 'delegated', 'rejected'
|
|
|
148
149
|
function isTerminated(judgment) {
|
|
149
150
|
return exports.TERMINATED_JUDGMENTS.includes(judgment ?? '');
|
|
150
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Is the seat at this turn declared as a **reference** — someone who comments but does not decide?
|
|
154
|
+
*
|
|
155
|
+
* ── Why this is a function and not two inline calls ────────────────────────
|
|
156
|
+
* ApprovalParticipation has said from the start that a reference only comments and does not affect
|
|
157
|
+
* the judgment. create-approval, the escalation in approve(), and both list queries honour that.
|
|
158
|
+
* The two places where a person presses a button did not: approve() and reject() never asked whether
|
|
159
|
+
* the row they are about to stamp is an approver's row.
|
|
160
|
+
*
|
|
161
|
+
* A reference row is created with judgment Pending, so it passes every check those two functions do
|
|
162
|
+
* make. Approving one stamps it as a real approval and then nextApproval() picks the approver that
|
|
163
|
+
* sits *after* that seat, which moves the approval a step forward. With no approver after it, one
|
|
164
|
+
* reference ends the whole thread. Rejecting is worse: an activity with rejectionIsFinal cannot be
|
|
165
|
+
* restarted, so the case closes for good.
|
|
166
|
+
*
|
|
167
|
+
* Writing the check inline in both callers would put the rule back in two places, which is the
|
|
168
|
+
* reason this file exists.
|
|
169
|
+
*
|
|
170
|
+
* ── The approval line is the authority, not the row ───────────────────────
|
|
171
|
+
* The capacity is also copied onto ActivityApproval.participation. approve() already lets the
|
|
172
|
+
* approval line decide who comes next (nextApproval(approvalLine, order)); letting the row decide
|
|
173
|
+
* who may judge would put two authorities in one function, with nothing to settle a disagreement.
|
|
174
|
+
* The line is loaded either way, so this costs no extra read. (2026-09-09, architect's ruling)
|
|
175
|
+
*
|
|
176
|
+
* ── False when the seat is not in the line ────────────────────────────────
|
|
177
|
+
* That answers "not declared a reference", which is not the same as "is an approver". One boolean
|
|
178
|
+
* cannot carry both facts, so this function reports only what is written down and the caller
|
|
179
|
+
* refuses on a definite yes. A missing seat means the line grew shorter after submission; telling
|
|
180
|
+
* that person they are a reference would state something the data does not say. Whether the line
|
|
181
|
+
* can be edited after submission is being measured separately.
|
|
182
|
+
*/
|
|
183
|
+
function isReferenceOrder(approvalLine, order) {
|
|
184
|
+
return isReferenceStep(lineItemFor(approvalLine, order));
|
|
185
|
+
}
|
|
151
186
|
//# sourceMappingURL=approval-line.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval-line.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/approval-line.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **타입만 가져온다.** 값으로 가져오면 `@things-factory/organization` 이 실려 오고, 그것이 auth-base 의\n * 라우터까지 끌고 온다. 순수 규칙 하나를 시험하려는데 서버가 통째로 뜬다.\n */\nimport type { ApprovalLineItem } from '@things-factory/organization'\n\n/**\n * 결재선에서 다음 결재자를 찾는다.\n *\n * ── 왜 따로 뺐나 ───────────────────────────────────────────────────────────\n * 같은 규칙이 두 파일에 나뉘어 있었다.\n *\n * submit.ts 첫 결재를 `order: 1` 로 만들고 `approvalLine[0]` 을 넣는다\n * approve.ts `order` 를 승인하면 `approvalLine[order]` 를 다음 결재자로 삼는다\n *\n * `order` 와 배열 자리가 1만큼 다르다. 지금은 맞게 짝이 맞는데, 한쪽만 고치면 결재자가 한 명 빠지거나\n * 같은 사람이 두 번 들어간다. 그리고 **오류가 안 난다** — 결재가 다 끝나고 나서야 안다.\n *\n * 그래서 한 자리에 적고 양쪽이 이것을 부른다.\n *\n * `order` 는 사람이 세는 수다 — 1차 결재, 2차 결재. 배열은 0부터이므로 그 차이를 여기서만 다룬다.\n */\n\n/**\n * 이 항목이 **결재**인가 — 비어 있으면 결재다.\n *\n * 지금 도는 결재선에는 이 칸이 없다(gangsters·dataset·quotation·accounting). 그 항목들이 전부 결재자로\n * 읽혀야 거동이 안 바뀐다. 그래서 「참조라고 적힌 것만 참조」다.\n */\nexport function isApprovalStep(item: ApprovalLineItem | undefined): boolean {\n return !!item && (item as any).participation !== 'reference'\n}\n\n/** 이 항목이 참조인가 — 의견만 내고 결재를 막지 않는다. */\nexport function isReferenceStep(item: ApprovalLineItem | undefined): boolean {\n return !!item && (item as any).participation === 'reference'\n}\n\n/**\n * 이 차례의 결재자는 결재선의 어느 줄인가. `order` 는 1부터.\n *\n * **참조 항목도 자리를 차지한다.** `order` 는 배열 자리 그대로다 — 다시 매기면 이미 쌓인 행의 뜻이\n * 바뀐다. 그래서 결재자의 차례가 1·3·4 처럼 띄엄띄엄해질 수 있고, 그것이 맞다.\n *\n * 이 함수는 그 자리에 무엇이 있든 돌려준다. 결재인지 아닌지는 부르는 쪽이 본다(§`nextApproval`).\n */\nexport function lineItemFor(approvalLine: ApprovalLineItem[] | undefined, order: number): ApprovalLineItem | undefined {\n if (!approvalLine?.length || order < 1) {\n return undefined\n }\n\n return approvalLine[order - 1]\n}\n\n/**\n * 첫 **결재자**는 몇 번째 자리인가 — 앞에 참조가 있으면 건너뛴다.\n *\n * 결재선이 참조로만 이루어져 있으면 `undefined` 다. 그때는 결재 없이 끝난다(§`needsApproval`).\n */\nexport function firstApprovalOrder(approvalLine: ApprovalLineItem[] | undefined): number | undefined {\n if (!approvalLine?.length) return undefined\n\n const index = approvalLine.findIndex(isApprovalStep)\n return index < 0 ? undefined : index + 1\n}\n\n/**\n * 이 결재선의 참조들 — **언제 알릴지와 함께.**\n *\n * `notifyAfter` 가 없으면 `0` 으로 답한다. 0 은 「상신 즉시」다 — 아직 아무 결재도 끝나지 않은 시점.\n */\nexport function referenceSteps(\n approvalLine: ApprovalLineItem[] | undefined\n): { item: ApprovalLineItem; order: number; notifyAfter: number }[] {\n if (!approvalLine?.length) return []\n\n return approvalLine\n .map((item, index) => ({ item, order: index + 1, notifyAfter: Number((item as any).notifyAfter ?? 0) || 0 }))\n .filter(x => isReferenceStep(x.item))\n}\n\n/** 이번에 알려야 할 참조들 — `afterOrder` 번째 결재가 막 끝났을 때. 상신 직후는 `0`. */\nexport function referencesToNotify(\n approvalLine: ApprovalLineItem[] | undefined,\n afterOrder: number\n): { item: ApprovalLineItem; order: number }[] {\n return referenceSteps(approvalLine)\n .filter(x => x.notifyAfter === afterOrder)\n .map(({ item, order }) => ({ item, order }))\n}\n\n/** 첫 결재의 차례. */\nexport const FIRST_ORDER = 1\n\n/**\n * 이 차례를 승인하면 다음 결재자가 있나. 없으면 이것이 마지막이다.\n *\n * **참조는 건너뛴다.** 참조자가 아직 안 읽었어도 결재는 다음 결재자로 넘어간다 — 막게 만들면 그 사람이\n * 휴가일 때 결재가 선다. 막아야 하는 자격은 합의이고, 그것은 아직 없다.\n */\nexport function nextApproval(\n approvalLine: ApprovalLineItem[] | undefined,\n order: number\n): { item: ApprovalLineItem; order: number } | undefined {\n if (!approvalLine?.length) return undefined\n\n for (let next = order + 1; next <= approvalLine.length; next++) {\n const item = lineItemFor(approvalLine, next)\n if (isApprovalStep(item)) return { item: item!, order: next }\n }\n\n return undefined\n}\n\n/** 이 차례가 마지막인가. */\nexport function isLastApproval(approvalLine: ApprovalLineItem[] | undefined, order: number): boolean {\n return !nextApproval(approvalLine, order)\n}\n\n/**\n * 결재가 필요한가 — **결재자가 하나라도 있어야 한다.**\n *\n * 비어 있으면 결재 없이 끝난다. 그리고 **참조만 있는 결재선도 결재 없이 끝난다** — 참조는 막지 않으므로\n * 기다릴 것이 없다. 참조가 있다고 결재를 기다리게 하면, 그 사람이 승인 권한도 없이 결재를 세운다.\n */\nexport function needsApproval(approvalLine: ApprovalLineItem[] | undefined): boolean {\n return firstApprovalOrder(approvalLine) !== undefined\n}\n\n/**\n * 이 판정이 「승인했다」인가.\n *\n * ── 옛 표기를 여기서 흡수한다 ──────────────────────────────────────────────\n * 2026-08-31 전에는 다음 결재자가 있으면 승인을 `escalated` 로 적었다. **그 행은 고치지 않는다** —\n * 승인 기록을 뒤에서 고쳐 쓰는 것은 규제 산업이 금지하는 바로 그 행위다.\n *\n * 대신 읽는 자리 하나에서 옛 표기를 새 뜻으로 읽는다. 이 규율이 두 곳으로 늘어나면 한쪽만 고쳐지는\n * 날이 온다. 「승인했나」를 묻는 코드는 전부 이것을 부른다.\n */\nexport function isApproved(judgment: string | undefined): boolean {\n return judgment === 'approved' || judgment === 'escalated'\n}\n\n/**\n * 더 기다릴 것이 없는 판정들 — **목록 질의와 이 파일이 같은 값을 본다.**\n *\n * 목록 질의가 이 배열을 그대로 쓴다. 두 벌로 두면 값이 늘 때 한쪽만 늘고, 그 판정을 받은 행이 대기\n * 목록에 영원히 남는다 — 실제로 그렇게 됐다(아래).\n */\nexport const TERMINATED_JUDGMENTS = ['approved', 'escalated', 'delegated', 'rejected', 'aborted', 'closed'] as const\n\n/**\n * 이 판정이 끝난 것인가 — 더 기다릴 것이 없나.\n *\n * ── `aborted` 가 빠져 있었다 (2026-09-01) ─────────────────────────────────\n * 스레드를 중단하면 그 스레드의 대기 결재가 `aborted` 로 닫힌다(§`activity-thread/_abort.ts`).\n * 그런데 **목록 질의도 이 함수도 그 값을 「끝난 것」으로 세지 않았다.**\n *\n * 그래서 중단된 건의 결재가 그 사람의 **승인 대기 목록에 영원히 남았다.** 눌러 봐야 이미 끝난 건이고,\n * 목록에서 사라지지도 않는다. 닫는 쪽은 제대로 닫았는데 읽는 쪽이 그것을 못 읽었다.\n */\nexport function isTerminated(judgment: string | undefined): boolean {\n return (TERMINATED_JUDGMENTS as readonly string[]).includes(judgment ?? '')\n}\n"]}
|
|
1
|
+
{"version":3,"file":"approval-line.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/approval-line.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **타입만 가져온다.** 값으로 가져오면 `@things-factory/organization` 이 실려 오고, 그것이 auth-base 의\n * 라우터까지 끌고 온다. 순수 규칙 하나를 시험하려는데 서버가 통째로 뜬다.\n */\nimport type { ApprovalLineItem } from '@things-factory/organization'\n\n/**\n * 결재선에서 다음 결재자를 찾는다.\n *\n * ── 왜 따로 뺐나 ───────────────────────────────────────────────────────────\n * 같은 규칙이 두 파일에 나뉘어 있었다.\n *\n * submit.ts 첫 결재를 `order: 1` 로 만들고 `approvalLine[0]` 을 넣는다\n * approve.ts `order` 를 승인하면 `approvalLine[order]` 를 다음 결재자로 삼는다\n *\n * `order` 와 배열 자리가 1만큼 다르다. 지금은 맞게 짝이 맞는데, 한쪽만 고치면 결재자가 한 명 빠지거나\n * 같은 사람이 두 번 들어간다. 그리고 **오류가 안 난다** — 결재가 다 끝나고 나서야 안다.\n *\n * 그래서 한 자리에 적고 양쪽이 이것을 부른다.\n *\n * `order` 는 사람이 세는 수다 — 1차 결재, 2차 결재. 배열은 0부터이므로 그 차이를 여기서만 다룬다.\n */\n\n/**\n * 이 항목이 **결재**인가 — 비어 있으면 결재다.\n *\n * 지금 도는 결재선에는 이 칸이 없다(gangsters·dataset·quotation·accounting). 그 항목들이 전부 결재자로\n * 읽혀야 거동이 안 바뀐다. 그래서 「참조라고 적힌 것만 참조」다.\n */\nexport function isApprovalStep(item: ApprovalLineItem | undefined): boolean {\n return !!item && (item as any).participation !== 'reference'\n}\n\n/** 이 항목이 참조인가 — 의견만 내고 결재를 막지 않는다. */\nexport function isReferenceStep(item: ApprovalLineItem | undefined): boolean {\n return !!item && (item as any).participation === 'reference'\n}\n\n/**\n * 이 차례의 결재자는 결재선의 어느 줄인가. `order` 는 1부터.\n *\n * **참조 항목도 자리를 차지한다.** `order` 는 배열 자리 그대로다 — 다시 매기면 이미 쌓인 행의 뜻이\n * 바뀐다. 그래서 결재자의 차례가 1·3·4 처럼 띄엄띄엄해질 수 있고, 그것이 맞다.\n *\n * 이 함수는 그 자리에 무엇이 있든 돌려준다. 결재인지 아닌지는 부르는 쪽이 본다(§`nextApproval`).\n */\nexport function lineItemFor(approvalLine: ApprovalLineItem[] | undefined, order: number): ApprovalLineItem | undefined {\n if (!approvalLine?.length || order < 1) {\n return undefined\n }\n\n return approvalLine[order - 1]\n}\n\n/**\n * 첫 **결재자**는 몇 번째 자리인가 — 앞에 참조가 있으면 건너뛴다.\n *\n * 결재선이 참조로만 이루어져 있으면 `undefined` 다. 그때는 결재 없이 끝난다(§`needsApproval`).\n */\nexport function firstApprovalOrder(approvalLine: ApprovalLineItem[] | undefined): number | undefined {\n if (!approvalLine?.length) return undefined\n\n const index = approvalLine.findIndex(isApprovalStep)\n return index < 0 ? undefined : index + 1\n}\n\n/**\n * 이 결재선의 참조들 — **언제 알릴지와 함께.**\n *\n * `notifyAfter` 가 없으면 `0` 으로 답한다. 0 은 「상신 즉시」다 — 아직 아무 결재도 끝나지 않은 시점.\n */\nexport function referenceSteps(\n approvalLine: ApprovalLineItem[] | undefined\n): { item: ApprovalLineItem; order: number; notifyAfter: number }[] {\n if (!approvalLine?.length) return []\n\n return approvalLine\n .map((item, index) => ({ item, order: index + 1, notifyAfter: Number((item as any).notifyAfter ?? 0) || 0 }))\n .filter(x => isReferenceStep(x.item))\n}\n\n/** 이번에 알려야 할 참조들 — `afterOrder` 번째 결재가 막 끝났을 때. 상신 직후는 `0`. */\nexport function referencesToNotify(\n approvalLine: ApprovalLineItem[] | undefined,\n afterOrder: number\n): { item: ApprovalLineItem; order: number }[] {\n return referenceSteps(approvalLine)\n .filter(x => x.notifyAfter === afterOrder)\n .map(({ item, order }) => ({ item, order }))\n}\n\n/** 첫 결재의 차례. */\nexport const FIRST_ORDER = 1\n\n/**\n * 이 차례를 승인하면 다음 결재자가 있나. 없으면 이것이 마지막이다.\n *\n * **참조는 건너뛴다.** 참조자가 아직 안 읽었어도 결재는 다음 결재자로 넘어간다 — 막게 만들면 그 사람이\n * 휴가일 때 결재가 선다. 막아야 하는 자격은 합의이고, 그것은 아직 없다.\n */\nexport function nextApproval(\n approvalLine: ApprovalLineItem[] | undefined,\n order: number\n): { item: ApprovalLineItem; order: number } | undefined {\n if (!approvalLine?.length) return undefined\n\n for (let next = order + 1; next <= approvalLine.length; next++) {\n const item = lineItemFor(approvalLine, next)\n if (isApprovalStep(item)) return { item: item!, order: next }\n }\n\n return undefined\n}\n\n/** 이 차례가 마지막인가. */\nexport function isLastApproval(approvalLine: ApprovalLineItem[] | undefined, order: number): boolean {\n return !nextApproval(approvalLine, order)\n}\n\n/**\n * 결재가 필요한가 — **결재자가 하나라도 있어야 한다.**\n *\n * 비어 있으면 결재 없이 끝난다. 그리고 **참조만 있는 결재선도 결재 없이 끝난다** — 참조는 막지 않으므로\n * 기다릴 것이 없다. 참조가 있다고 결재를 기다리게 하면, 그 사람이 승인 권한도 없이 결재를 세운다.\n */\nexport function needsApproval(approvalLine: ApprovalLineItem[] | undefined): boolean {\n return firstApprovalOrder(approvalLine) !== undefined\n}\n\n/**\n * 이 판정이 「승인했다」인가.\n *\n * ── 옛 표기를 여기서 흡수한다 ──────────────────────────────────────────────\n * 2026-08-31 전에는 다음 결재자가 있으면 승인을 `escalated` 로 적었다. **그 행은 고치지 않는다** —\n * 승인 기록을 뒤에서 고쳐 쓰는 것은 규제 산업이 금지하는 바로 그 행위다.\n *\n * 대신 읽는 자리 하나에서 옛 표기를 새 뜻으로 읽는다. 이 규율이 두 곳으로 늘어나면 한쪽만 고쳐지는\n * 날이 온다. 「승인했나」를 묻는 코드는 전부 이것을 부른다.\n */\nexport function isApproved(judgment: string | undefined): boolean {\n return judgment === 'approved' || judgment === 'escalated'\n}\n\n/**\n * 더 기다릴 것이 없는 판정들 — **목록 질의와 이 파일이 같은 값을 본다.**\n *\n * 목록 질의가 이 배열을 그대로 쓴다. 두 벌로 두면 값이 늘 때 한쪽만 늘고, 그 판정을 받은 행이 대기\n * 목록에 영원히 남는다 — 실제로 그렇게 됐다(아래).\n */\nexport const TERMINATED_JUDGMENTS = ['approved', 'escalated', 'delegated', 'rejected', 'aborted', 'closed'] as const\n\n/**\n * 이 판정이 끝난 것인가 — 더 기다릴 것이 없나.\n *\n * ── `aborted` 가 빠져 있었다 (2026-09-01) ─────────────────────────────────\n * 스레드를 중단하면 그 스레드의 대기 결재가 `aborted` 로 닫힌다(§`activity-thread/_abort.ts`).\n * 그런데 **목록 질의도 이 함수도 그 값을 「끝난 것」으로 세지 않았다.**\n *\n * 그래서 중단된 건의 결재가 그 사람의 **승인 대기 목록에 영원히 남았다.** 눌러 봐야 이미 끝난 건이고,\n * 목록에서 사라지지도 않는다. 닫는 쪽은 제대로 닫았는데 읽는 쪽이 그것을 못 읽었다.\n */\nexport function isTerminated(judgment: string | undefined): boolean {\n return (TERMINATED_JUDGMENTS as readonly string[]).includes(judgment ?? '')\n}\n\n/**\n * Is the seat at this turn declared as a **reference** — someone who comments but does not decide?\n *\n * ── Why this is a function and not two inline calls ────────────────────────\n * ApprovalParticipation has said from the start that a reference only comments and does not affect\n * the judgment. create-approval, the escalation in approve(), and both list queries honour that.\n * The two places where a person presses a button did not: approve() and reject() never asked whether\n * the row they are about to stamp is an approver's row.\n *\n * A reference row is created with judgment Pending, so it passes every check those two functions do\n * make. Approving one stamps it as a real approval and then nextApproval() picks the approver that\n * sits *after* that seat, which moves the approval a step forward. With no approver after it, one\n * reference ends the whole thread. Rejecting is worse: an activity with rejectionIsFinal cannot be\n * restarted, so the case closes for good.\n *\n * Writing the check inline in both callers would put the rule back in two places, which is the\n * reason this file exists.\n *\n * ── The approval line is the authority, not the row ───────────────────────\n * The capacity is also copied onto ActivityApproval.participation. approve() already lets the\n * approval line decide who comes next (nextApproval(approvalLine, order)); letting the row decide\n * who may judge would put two authorities in one function, with nothing to settle a disagreement.\n * The line is loaded either way, so this costs no extra read. (2026-09-09, architect's ruling)\n *\n * ── False when the seat is not in the line ────────────────────────────────\n * That answers \"not declared a reference\", which is not the same as \"is an approver\". One boolean\n * cannot carry both facts, so this function reports only what is written down and the caller\n * refuses on a definite yes. A missing seat means the line grew shorter after submission; telling\n * that person they are a reference would state something the data does not say. Whether the line\n * can be edited after submission is being measured separately.\n */\nexport function isReferenceOrder(approvalLine: ApprovalLineItem[] | undefined, order: number): boolean {\n return isReferenceStep(lineItemFor(approvalLine, order))\n}\n"]}
|
|
@@ -47,6 +47,23 @@ async function approve({ id, comment }, context) {
|
|
|
47
47
|
}
|
|
48
48
|
const approvalLine = activityThread.activityInstance.approvalLine;
|
|
49
49
|
const order = activityApproval.order;
|
|
50
|
+
/*
|
|
51
|
+
* A reference does not judge.
|
|
52
|
+
*
|
|
53
|
+
* The three checks above all pass for a reference row: it exists, checkAuthority says it belongs
|
|
54
|
+
* to this user, and its judgment is still Pending. Without this refusal the approval is recorded
|
|
55
|
+
* as real and nextApproval() then picks the approver sitting after that seat, moving the approval
|
|
56
|
+
* a step forward; with no approver after it, one reference ends the thread.
|
|
57
|
+
*
|
|
58
|
+
* This stands before claimPendingApproval, the first write. Throwing after the claim would leave
|
|
59
|
+
* the row already stamped with a judgment.
|
|
60
|
+
*
|
|
61
|
+
* The message says what this person can do instead of reporting a missing permission. A reference
|
|
62
|
+
* seat is a different seat, not a blocked one — comments are theirs to leave.
|
|
63
|
+
*/
|
|
64
|
+
if ((0, approval_line_js_1.isReferenceOrder)(approvalLine, order)) {
|
|
65
|
+
throw new Error(context.t('error.activity-approval is reference only'));
|
|
66
|
+
}
|
|
50
67
|
/* 다음 결재자가 누구인지는 한 자리가 정한다 — `approval-line.ts`. */
|
|
51
68
|
const next = (0, approval_line_js_1.nextApproval)(approvalLine, order);
|
|
52
69
|
/*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approve.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/approve.ts"],"names":[],"mappings":";;AAUA,
|
|
1
|
+
{"version":3,"file":"approve.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/approve.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApprovalLineItem } from '@things-factory/organization'\nimport { stampedComment } from './opinion.js'\nimport { ActivityThread, ActivityThreadStatus } from '../../service/activity-thread/activity-thread.js'\nimport { ActivityApproval, ActivityApprovalJudgment } from '../../service/activity-approval/activity-approval.js'\nimport { updateActivityInstanceState } from '../common.js'\nimport { checkAuthority } from './check-authority.js'\nimport { isReferenceOrder, nextApproval, referencesToNotify } from './approval-line.js'\nimport { createApproval, createReferences } from './create-approval.js'\nimport { claimPendingApproval } from './claim.js'\n\nexport async function approve(\n { id, comment }: { id: string; comment: string },\n context: ResolverContext\n): Promise<ActivityApproval> {\n const { domain, user, tx } = context.state\n\n var repository = tx.getRepository(ActivityApproval)\n\n var activityApproval = await repository.findOne({\n where: { domain: { id: domain.id }, id }\n })\n\n if (!activityApproval) {\n throw new Error(\n context.t('error.activity-approval not found', {\n activityApproval: id\n })\n )\n }\n\n await checkAuthority(activityApproval, context)\n\n /* \n Prerequisite of Approval\n - Previous judgment should be 'pending'\n */\n if (activityApproval.judgment !== ActivityApprovalJudgment.Pending) {\n throw new Error(\n context.t(`error.activity-approval is already terminated`, {\n activityApproval: id,\n actual: activityApproval.judgment\n })\n )\n }\n\n var activityThread = await tx.getRepository(ActivityThread).findOne({\n where: { domain: { id: domain.id }, id: activityApproval.activityThreadId },\n relations: ['activityInstance']\n })\n\n /*\n * 여기까지 오면 스레드가 있어야 한다. 없는 채로 아래를 읽으면 `TypeError` 가 나고, 그 오류로는\n * 무엇이 없는지 알 수 없다.\n */\n if (!activityThread) {\n throw new Error(context.t('error.activity-thread not found', { activityThread: activityApproval.activityThreadId }))\n }\n\n if (!activityThread.activityInstance) {\n throw new Error(context.t('error.activity-instance not found', { activityThread: activityThread.id }))\n }\n\n const approvalLine: ApprovalLineItem[] = activityThread.activityInstance.approvalLine\n const order = activityApproval.order\n\n /*\n * A reference does not judge.\n *\n * The three checks above all pass for a reference row: it exists, checkAuthority says it belongs\n * to this user, and its judgment is still Pending. Without this refusal the approval is recorded\n * as real and nextApproval() then picks the approver sitting after that seat, moving the approval\n * a step forward; with no approver after it, one reference ends the thread.\n *\n * This stands before claimPendingApproval, the first write. Throwing after the claim would leave\n * the row already stamped with a judgment.\n *\n * The message says what this person can do instead of reporting a missing permission. A reference\n * seat is a different seat, not a blocked one — comments are theirs to leave.\n */\n if (isReferenceOrder(approvalLine, order)) {\n throw new Error(context.t('error.activity-approval is reference only'))\n }\n\n /* 다음 결재자가 누구인지는 한 자리가 정한다 — `approval-line.ts`. */\n const next = nextApproval(approvalLine, order)\n\n /*\n * **여기서 잡는다.** 위의 검사는 읽은 시점의 것이라 그 사이에 남이 처리할 수 있다. 잡기가 첫 쓰기여야\n * 진 쪽이 다음 결재자를 만들기 전에 물러난다.\n */\n if (!(await claimPendingApproval(tx, activityApproval, ActivityApprovalJudgment.Approved))) {\n /* 「이미 끝났다」보다 **그 사람이 할 일**을 말한다 — 목록을 새로 읽으면 지금 상태가 보인다. */\n throw new Error(context.t('error.activity-approval taken by someone else', { activityApproval: id }))\n }\n\n /*\n * **승인한 사람은 승인한 것이다** (2026-08-31, 아키텍트 확인).\n *\n * 앞서는 다음 결재자가 있으면 `Escalated` 로 적었다. 그러면 「누가 승인했나」를 물을 때 중간 결재자가\n * 전부 「상신함」으로 나오고, **승인한 것과 그냥 넘긴 것이 구별되지 않는다.** 규제 산업에서는 결재\n * 이력이 곧 근거라 이것이 결함이다.\n *\n * 다음으로 넘어간 사실은 `transaction` 이 그대로 들고 있으므로 잃는 것이 없다.\n */\n const result = await tx.getRepository(ActivityApproval).save({\n ...activityApproval,\n judgment: ActivityApprovalJudgment.Approved,\n ...stampedComment(comment),\n transaction: next ? 'escalate' : 'approve',\n /*\n * **누가 처리했는지 남긴다.**\n *\n * 역할로 걸린 결재는 `approver` 가 비어 있고 `approverRole` 만 있다. 처리해도 그 자리가 비어 있으면\n * 두 가지가 함께 무너진다 — 그 역할을 가진 사람들의 **대기 목록에 영원히 남고**(`approver IS NULL`\n * 로 걸리므로), 정작 처리한 사람의 **완료 목록에는 안 나온다**(`approver = 나` 로 거르므로).\n *\n * 그리고 결재 이력에서 「누가 승인했나」는 역할이 아니라 사람이어야 한다.\n */\n approver: activityApproval.approver ?? user,\n updater: user,\n terminatedAt: new Date(),\n terminator: user\n })\n\n /*\n * **이번 결재가 끝나서 알릴 참조들.** `notifyAfter` 가 이 차례를 가리키는 참조가 여기 온다.\n *\n * 다음 결재자를 세우는 것과 **무관하게** 만든다 — 참조는 막지 않으므로, 이것이 실패해도 결재는 간다.\n */\n const refs = await createReferences(\n { references: referencesToNotify(approvalLine, order), activityThread },\n context\n )\n for (const f of refs.failed) {\n console.warn(`[worklist] 참조자를 세우지 못했다 (결재선 ${f.order}번째) — ${f.reason}`)\n }\n\n if (next) {\n /* 다음 결재자에게 넘긴다 — 행을 만드는 자리는 한 곳이다(§`create-approval.ts`). */\n await createApproval(\n { item: next.item, order: next.order, participation: 'approval', activityThread, transaction: 'escalate' },\n context\n )\n\n activityThread = await tx.getRepository(ActivityThread).save({\n ...activityThread,\n transaction: 'escalate',\n reason: comment,\n state: ActivityThreadStatus.Escalated,\n updater: user\n })\n } else {\n activityThread = await tx.getRepository(ActivityThread).save({\n ...activityThread,\n transaction: 'approve',\n reason: comment,\n state: ActivityThreadStatus.Ended,\n terminatedAt: new Date(),\n terminator: user,\n updater: user\n })\n }\n\n await updateActivityInstanceState(activityThread.activityInstanceId, { causedBy: [activityThread] }, context)\n\n return result\n}\n"]}
|
|
@@ -7,12 +7,18 @@ const activity_approval_js_1 = require("../../service/activity-approval/activity
|
|
|
7
7
|
const common_js_1 = require("../common.js");
|
|
8
8
|
const check_authority_js_1 = require("./check-authority.js");
|
|
9
9
|
const claim_js_1 = require("./claim.js");
|
|
10
|
+
const approval_line_js_1 = require("./approval-line.js");
|
|
10
11
|
async function reject({ id, comment }, context) {
|
|
11
12
|
const { domain, user, tx } = context.state;
|
|
12
13
|
var repository = tx.getRepository(activity_approval_js_1.ActivityApproval);
|
|
13
14
|
var activityApproval = await repository.findOne({
|
|
14
15
|
where: { domain: { id: domain.id }, id },
|
|
15
|
-
|
|
16
|
+
/*
|
|
17
|
+
* The approval line comes along, because the refusal below asks the line whether this seat is a
|
|
18
|
+
* reference. approve() reads the thread in a separate query to reach the same value; this
|
|
19
|
+
* function already holds the thread, so it takes one more level instead.
|
|
20
|
+
*/
|
|
21
|
+
relations: ['activityThread', 'activityThread.activityInstance']
|
|
16
22
|
});
|
|
17
23
|
if (!activityApproval) {
|
|
18
24
|
throw new Error(context.t('error.activity-approval not found', {
|
|
@@ -30,6 +36,20 @@ async function reject({ id, comment }, context) {
|
|
|
30
36
|
actual: activityApproval.judgment
|
|
31
37
|
}));
|
|
32
38
|
}
|
|
39
|
+
/*
|
|
40
|
+
* A reference does not judge, and rejecting carries more than approving.
|
|
41
|
+
*
|
|
42
|
+
* This function did not look at the approval line at all. A reference pressing reject had it
|
|
43
|
+
* recorded as a real rejection and the thread closed as Rejected. Activities that set
|
|
44
|
+
* rejectionIsFinal — the twin actuation and the disposition approval — cannot be restarted, so
|
|
45
|
+
* someone seated to comment closed the case for good.
|
|
46
|
+
*
|
|
47
|
+
* This stands before claimPendingApproval, the first write. Throwing after the claim would leave
|
|
48
|
+
* the row already stamped as rejected.
|
|
49
|
+
*/
|
|
50
|
+
if ((0, approval_line_js_1.isReferenceOrder)(activityApproval.activityThread?.activityInstance?.approvalLine, activityApproval.order)) {
|
|
51
|
+
throw new Error(context.t('error.activity-approval is reference only'));
|
|
52
|
+
}
|
|
33
53
|
/*
|
|
34
54
|
* **여기서 잡는다.** 위의 검사는 읽은 시점의 것이라 그 사이에 남이 처리할 수 있다. 잡기가 첫 쓰기여야
|
|
35
55
|
* 진 쪽이 다음 결재자를 만들기 전에 물러난다.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reject.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/reject.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"reject.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/reject.ts"],"names":[],"mappings":"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","sourcesContent":["import { ActivityThread, ActivityThreadStatus } from '../../service/activity-thread/activity-thread.js'\nimport { stampedComment } from './opinion.js'\nimport { ActivityApproval, ActivityApprovalJudgment } from '../../service/activity-approval/activity-approval.js'\nimport { updateActivityInstanceState } from '../common.js'\nimport { checkAuthority } from './check-authority.js'\nimport { claimPendingApproval } from './claim.js'\nimport { isReferenceOrder } from './approval-line.js'\n\nexport async function reject(\n { id, comment }: { id: string; comment: string },\n context: ResolverContext\n): Promise<ActivityApproval> {\n const { domain, user, tx } = context.state\n\n var repository = tx.getRepository(ActivityApproval)\n\n var activityApproval = await repository.findOne({\n where: { domain: { id: domain.id }, id },\n /*\n * The approval line comes along, because the refusal below asks the line whether this seat is a\n * reference. approve() reads the thread in a separate query to reach the same value; this\n * function already holds the thread, so it takes one more level instead.\n */\n relations: ['activityThread', 'activityThread.activityInstance']\n })\n\n if (!activityApproval) {\n throw new Error(\n context.t('error.activity-approval not found', {\n activityApproval: id\n })\n )\n }\n\n await checkAuthority(activityApproval, context)\n\n /* \n Prerequisite of Rejection\n - Previous judgment should be 'pending'\n */\n if (activityApproval.judgment !== ActivityApprovalJudgment.Pending) {\n throw new Error(\n context.t(`error.activity-approval is already terminated`, {\n activityInstance: id,\n actual: activityApproval.judgment\n })\n )\n }\n\n\n /*\n * A reference does not judge, and rejecting carries more than approving.\n *\n * This function did not look at the approval line at all. A reference pressing reject had it\n * recorded as a real rejection and the thread closed as Rejected. Activities that set\n * rejectionIsFinal — the twin actuation and the disposition approval — cannot be restarted, so\n * someone seated to comment closed the case for good.\n *\n * This stands before claimPendingApproval, the first write. Throwing after the claim would leave\n * the row already stamped as rejected.\n */\n if (isReferenceOrder(activityApproval.activityThread?.activityInstance?.approvalLine, activityApproval.order)) {\n throw new Error(context.t('error.activity-approval is reference only'))\n }\n\n /*\n * **여기서 잡는다.** 위의 검사는 읽은 시점의 것이라 그 사이에 남이 처리할 수 있다. 잡기가 첫 쓰기여야\n * 진 쪽이 다음 결재자를 만들기 전에 물러난다.\n */\n if (!(await claimPendingApproval(tx, activityApproval, ActivityApprovalJudgment.Rejected))) {\n /* 「이미 끝났다」보다 **그 사람이 할 일**을 말한다 — 목록을 새로 읽으면 지금 상태가 보인다. */\n throw new Error(context.t('error.activity-approval taken by someone else', { activityApproval: id }))\n }\n\n var result = await repository.save({\n ...activityApproval,\n judgment: ActivityApprovalJudgment.Rejected,\n ...stampedComment(comment),\n transaction: 'reject',\n /*\n * **누가 처리했는지 남긴다.**\n *\n * 역할로 걸린 결재는 `approver` 가 비어 있고 `approverRole` 만 있다. 처리해도 그 자리가 비어 있으면\n * 두 가지가 함께 무너진다 — 그 역할을 가진 사람들의 **대기 목록에 영원히 남고**(`approver IS NULL`\n * 로 걸리므로), 정작 처리한 사람의 **완료 목록에는 안 나온다**(`approver = 나` 로 거르므로).\n *\n * 그리고 결재 이력에서 「누가 승인했나」는 역할이 아니라 사람이어야 한다.\n */\n approver: activityApproval.approver ?? user,\n updater: user,\n terminatedAt: new Date(),\n terminator: user\n })\n\n var activityThread = await tx.getRepository(ActivityThread).save({\n ...activityApproval.activityThread,\n transaction: 'reject',\n reason: comment,\n state: ActivityThreadStatus.Rejected,\n updater: user\n })\n\n await updateActivityInstanceState(\n activityApproval.activityThread.activityInstanceId,\n { causedBy: [activityThread] },\n context\n )\n\n return result\n}\n"]}
|
|
@@ -4,6 +4,8 @@ exports.ActivityInstallations = void 0;
|
|
|
4
4
|
exports.installActivity = installActivity;
|
|
5
5
|
const shell_1 = require("@things-factory/shell");
|
|
6
6
|
const activity_js_1 = require("../../service/activity/activity.js");
|
|
7
|
+
const registry_conflict_js_1 = require("./registry-conflict.js");
|
|
8
|
+
const installable_to_activity_js_1 = require("./installable-to-activity.js");
|
|
7
9
|
/**
|
|
8
10
|
* 등록된 양식을 **도메인에 실제로 세운다.**
|
|
9
11
|
*
|
|
@@ -39,7 +41,7 @@ async function installActivity(name, domain, options = {}) {
|
|
|
39
41
|
const existing = await repository.findOneBy({ domain: { id: domain.id }, name });
|
|
40
42
|
return await repository.save({
|
|
41
43
|
...(existing ?? {}),
|
|
42
|
-
...template,
|
|
44
|
+
...(0, installable_to_activity_js_1.activityRowFrom)(template),
|
|
43
45
|
state: activity_js_1.ActivityStatus.Released,
|
|
44
46
|
domain,
|
|
45
47
|
creator: existing ? existing.creator : user,
|
|
@@ -51,6 +53,7 @@ class ActivityInstallations {
|
|
|
51
53
|
/** Keyed separately from `templates`, so nothing here can be installed by mistake. */
|
|
52
54
|
static { this.intended = {}; }
|
|
53
55
|
static installActivityTemplate(template) {
|
|
56
|
+
(0, registry_conflict_js_1.assertNotDeclaredIntended)(template.name, ActivityInstallations.intended);
|
|
54
57
|
ActivityInstallations.templates[template.name] = template;
|
|
55
58
|
}
|
|
56
59
|
/**
|
|
@@ -61,14 +64,7 @@ class ActivityInstallations {
|
|
|
61
64
|
* own.
|
|
62
65
|
*/
|
|
63
66
|
static declareIntendedActivity(intended) {
|
|
64
|
-
|
|
65
|
-
/*
|
|
66
|
-
* Both at once would draw the same work twice and let a reader believe the built one is the
|
|
67
|
-
* plan. Say so rather than picking a winner.
|
|
68
|
-
*/
|
|
69
|
-
throw new Error(`declareIntendedActivity: "${intended.key}" is already installed as an activity. ` +
|
|
70
|
-
'Remove the declaration now that the activity exists.');
|
|
71
|
-
}
|
|
67
|
+
(0, registry_conflict_js_1.assertNotAlreadyInstalled)(intended.key, ActivityInstallations.templates);
|
|
72
68
|
ActivityInstallations.intended[intended.key] = intended;
|
|
73
69
|
}
|
|
74
70
|
static get(name) {
|
package/dist-server/controllers/activity-installation/activity-installation-controller.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"activity-installation-controller.js","sourceRoot":"","sources":["../../../server/controllers/activity-installation/activity-installation-controller.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"activity-installation-controller.js","sourceRoot":"","sources":["../../../server/controllers/activity-installation/activity-installation-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import { EntityManager } from 'typeorm'\n\nimport { Domain, getRepository } from '@things-factory/shell'\n\nimport { Activity, ActivityStatus } from '../../service/activity/activity.js'\nimport { InstallableActivity } from '../../service/installable-activity/installable-activity.js'\nimport { ActivityReadiness } from '../../service/installable-activity/activity-readiness.js'\nimport { assertNotAlreadyInstalled, assertNotDeclaredIntended } from './registry-conflict.js'\nimport { activityRowFrom } from './installable-to-activity.js'\n\n/**\n * 등록된 양식을 **도메인에 실제로 세운다.**\n *\n * ── 왜 함수로 내놓나 ───────────────────────────────────────────────────────\n * 앞서는 이 일이 화면 뮤테이션 안에만 있었다. 그래서 양식을 세우는 길이 **사람이 화면에서 누르는 것\n * 하나뿐**이었고, 새로 배포하거나 도메인을 하나 더 만들 때마다 그 클릭을 기억해야 했다. 기억하지\n * 못하면 그 기능이 오류 보고 없이 없다.\n *\n * 앱이 「내 기능이 서려면 이 양식이 있어야 한다」를 아는 경우가 있다. 그런 앱은 기동할 때 스스로\n * 세울 수 있어야 한다. 언제 세울지는 앱이 정하고, 세우는 방법은 여기가 한 벌만 갖는다.\n *\n * ── 있는 것을 덮되 결재선은 건드리지 않는다 ────────────────────────────────\n * 이미 있으면 양식의 선언(모델 · 보는 화면 · 시작 방식)으로 덮는다. 판이 오르면 그 값이 따라와야\n * 하기 때문이다.\n *\n * 그런데 **결재선은 양식의 것이 아니라 그 현장의 것**이다. 누가 결재하는지는 공장이 정한다. 그래서\n * 양식에 결재선 선언이 없으면 이미 있는 결재선이 그대로 남는다 — 판을 올릴 때마다 결재자가 지워지면\n * 그 뒤로 상신이 곧 완료가 된다.\n */\nexport async function installActivity(\n name: string,\n domain: Domain,\n options: { manager?: EntityManager; user?: any } = {}\n): Promise<Activity> {\n const template = ActivityInstallations.get(name)\n\n if (!template) {\n /*\n * 없는 이름을 세울 수는 없다. 조용히 넘기면 부르는 쪽은 세운 줄 알고, 그 기능은 없는 채로 선다.\n */\n throw new Error(\n `installActivity: there is no installable activity named \"${name}\". ` +\n `known: ${ActivityInstallations.list()\n .map(one => one.name)\n .join(', ')}`\n )\n }\n\n const { manager, user } = options\n const repository = getRepository(Activity, manager)\n\n const existing = await repository.findOneBy({ domain: { id: domain.id }, name })\n\n return await repository.save({\n ...(existing ?? {}),\n ...activityRowFrom(template),\n state: ActivityStatus.Released,\n domain,\n creator: existing ? (existing as any).creator : user,\n updater: user\n } as Activity)\n}\n\n/**\n * Work a module has identified as a person's decision and **has not built.**\n *\n * Deliberately not an InstallableActivity. A template with no callback can be installed and\n * issued, and then someone approves a request and nothing happens — on the twin side that means a\n * person presses approve and no equipment moves. So these live in their own registry: the catalog\n * reads them, `installActivity` cannot reach them.\n *\n * They belong on the screen because the catalog is the plan of record. The question \"what is left\"\n * should be answered where people look, not in one person's head.\n */\nexport type IntendedActivity = {\n /** What the activity's `name` will be once it exists. */\n key: string\n /** What the catalog shows. The activity's own name is usually not a sentence a person reads. */\n label: string\n category?: string\n /** What this work is, for the person who will receive it. Draft wording is fine. */\n guide?: string[]\n readiness: ActivityReadiness\n}\n\nexport class ActivityInstallations {\n static templates: { [name: string]: InstallableActivity } = {}\n\n /** Keyed separately from `templates`, so nothing here can be installed by mistake. */\n static intended: { [key: string]: IntendedActivity } = {}\n\n static installActivityTemplate(template: InstallableActivity) {\n assertNotDeclaredIntended(template.name, ActivityInstallations.intended)\n\n ActivityInstallations.templates[template.name] = template\n }\n\n /**\n * Declare work that should reach someone's worklist and does not yet.\n *\n * A module calls this for each item it has identified. When the real thing is built, delete the\n * call and register the template instead — the catalog row moves from 예정 to 동작 확인됨 on its\n * own.\n */\n static declareIntendedActivity(intended: IntendedActivity) {\n assertNotAlreadyInstalled(intended.key, ActivityInstallations.templates)\n\n ActivityInstallations.intended[intended.key] = intended\n }\n\n static get(name: string): InstallableActivity {\n return ActivityInstallations.templates[name]\n }\n\n static list(): InstallableActivity[] {\n return Object.values(ActivityInstallations.templates)\n }\n\n static listIntended(): IntendedActivity[] {\n return Object.values(ActivityInstallations.intended)\n }\n\n /**\n * 액티비티인스턴스의 변화가 발생했을 때, 호출되는 콜백함수\n * @param name 액티비티인스턴스가 속한 액티비티명\n * @param activityInstance 해당 액티비티인스턴스\n * @param activityThreads 액티비티인스턴스의 변화를 일으킨 원인이 되는 액티비티쓰레드들\n * @param context 서비스 컨텍스트 : domain, user, translation, ..\n * @returns\n */\n static async callback(name: string, activityInstance: any, activityThreads: any[], context: ResolverContext) {\n const installableActivity = ActivityInstallations.templates[name]\n if (!installableActivity) {\n return\n }\n\n const { callback } = installableActivity\n\n callback && (await callback(activityInstance, { causedBy: activityThreads }, context))\n }\n}\n"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The template fields that belong on the Activity row, named one by one.
|
|
3
|
+
*
|
|
4
|
+
* ── Why not spread the template ───────────────────────────────────────────
|
|
5
|
+
* installActivity used `{ ...(template as any) }`. A template field with no column on Activity was
|
|
6
|
+
* dropped by the save, which reported success — and `as any` meant the types said nothing about it.
|
|
7
|
+
* Nine fields were being handed over with no home: activity, approvalSteps, callback, category,
|
|
8
|
+
* guide, label, provider, readiness, release.
|
|
9
|
+
*
|
|
10
|
+
* `label` was the one that hurt. It is the display name a person reads, and it reached exactly one
|
|
11
|
+
* screen — the catalog, which reads the template registry directly — while every screen reading the
|
|
12
|
+
* row showed the instance name instead. decisionKeys only worked because a column happened to be
|
|
13
|
+
* added for it in the same change; the next field would have been left to the same luck.
|
|
14
|
+
*
|
|
15
|
+
* Naming the fields buys three things: a new template field has to be wired here on purpose, the
|
|
16
|
+
* eight that stay behind are recorded as catalog-only rather than lost, and the two `as any` casts
|
|
17
|
+
* go away. (2026-09-10, architect's ruling)
|
|
18
|
+
*
|
|
19
|
+
* ── What stays in the template registry, and why ──────────────────────────
|
|
20
|
+
* activity the installed row itself, attached by the catalog query. Writing it back would
|
|
21
|
+
* be a row pointing at itself
|
|
22
|
+
* approvalSteps counted per domain by the catalog query from the line that is actually set up
|
|
23
|
+
* callback a function
|
|
24
|
+
* category how the catalog groups it
|
|
25
|
+
* guide what the work is, for the catalog's reader
|
|
26
|
+
* provider which module registered it
|
|
27
|
+
* readiness how far along the registering module says it is
|
|
28
|
+
* release which version registered it
|
|
29
|
+
*
|
|
30
|
+
* Each of those is answered by the registry, which is loaded in every process that serves the
|
|
31
|
+
* catalog. Copying them to the row would make a second copy that goes stale on the next release.
|
|
32
|
+
*
|
|
33
|
+
* ── The template shape is spelled out, not imported ───────────────────────
|
|
34
|
+
* InstallableActivity carries type-graphql decorators, and the test runner strips types without
|
|
35
|
+
* reading them, so importing it takes the whole test file down. That happened three times in one
|
|
36
|
+
* day — to search-key-declaration, to decision-key-declaration, and here.
|
|
37
|
+
*/
|
|
38
|
+
type TemplateFields = {
|
|
39
|
+
[field: string]: unknown;
|
|
40
|
+
};
|
|
41
|
+
export declare const ACTIVITY_ROW_FIELDS: readonly ["name", "label", "description", "activityType", "priority", "startingType", "multiple", "model", "decisionKeys", "uiType", "uiSource", "viewType", "viewSource", "reportType", "reportSource", "thumbnail"];
|
|
42
|
+
/**
|
|
43
|
+
* The part of a template that is stored on the Activity row.
|
|
44
|
+
*
|
|
45
|
+
* A field the template left undefined is not copied, so installing again does not blank a value
|
|
46
|
+
* somebody edited on the row — `save` with an explicit undefined writes null.
|
|
47
|
+
*/
|
|
48
|
+
export declare function activityRowFrom(template: object | undefined): TemplateFields;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ACTIVITY_ROW_FIELDS = void 0;
|
|
4
|
+
exports.activityRowFrom = activityRowFrom;
|
|
5
|
+
exports.ACTIVITY_ROW_FIELDS = [
|
|
6
|
+
'name',
|
|
7
|
+
'label',
|
|
8
|
+
'description',
|
|
9
|
+
'activityType',
|
|
10
|
+
'priority',
|
|
11
|
+
'startingType',
|
|
12
|
+
'multiple',
|
|
13
|
+
'model',
|
|
14
|
+
'decisionKeys',
|
|
15
|
+
'uiType',
|
|
16
|
+
'uiSource',
|
|
17
|
+
'viewType',
|
|
18
|
+
'viewSource',
|
|
19
|
+
'reportType',
|
|
20
|
+
'reportSource',
|
|
21
|
+
'thumbnail'
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* The part of a template that is stored on the Activity row.
|
|
25
|
+
*
|
|
26
|
+
* A field the template left undefined is not copied, so installing again does not blank a value
|
|
27
|
+
* somebody edited on the row — `save` with an explicit undefined writes null.
|
|
28
|
+
*/
|
|
29
|
+
function activityRowFrom(template) {
|
|
30
|
+
const fields = (template ?? {});
|
|
31
|
+
const row = {};
|
|
32
|
+
for (const field of exports.ACTIVITY_ROW_FIELDS) {
|
|
33
|
+
const value = fields[field];
|
|
34
|
+
if (value !== undefined) {
|
|
35
|
+
row[field] = value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return row;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=installable-to-activity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"installable-to-activity.js","sourceRoot":"","sources":["../../../server/controllers/activity-installation/installable-to-activity.ts"],"names":[],"mappings":";;;AA+DA,0CAaC;AAtCY,QAAA,mBAAmB,GAAG;IACjC,MAAM;IACN,OAAO;IACP,aAAa;IACb,cAAc;IACd,UAAU;IACV,cAAc;IACd,UAAU;IACV,OAAO;IACP,cAAc;IACd,QAAQ;IACR,UAAU;IACV,UAAU;IACV,YAAY;IACZ,YAAY;IACZ,cAAc;IACd,WAAW;CACH,CAAA;AAEV;;;;;GAKG;AACH,SAAgB,eAAe,CAAC,QAA4B;IAC1D,MAAM,MAAM,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAmB,CAAA;IACjD,MAAM,GAAG,GAAmB,EAAE,CAAA;IAE9B,KAAK,MAAM,KAAK,IAAI,2BAAmB,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;QAE3B,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,GAAG,CAAC,KAAK,CAAC,GAAG,KAAK,CAAA;QACpB,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAA;AACZ,CAAC","sourcesContent":["/**\n * The template fields that belong on the Activity row, named one by one.\n *\n * ── Why not spread the template ───────────────────────────────────────────\n * installActivity used `{ ...(template as any) }`. A template field with no column on Activity was\n * dropped by the save, which reported success — and `as any` meant the types said nothing about it.\n * Nine fields were being handed over with no home: activity, approvalSteps, callback, category,\n * guide, label, provider, readiness, release.\n *\n * `label` was the one that hurt. It is the display name a person reads, and it reached exactly one\n * screen — the catalog, which reads the template registry directly — while every screen reading the\n * row showed the instance name instead. decisionKeys only worked because a column happened to be\n * added for it in the same change; the next field would have been left to the same luck.\n *\n * Naming the fields buys three things: a new template field has to be wired here on purpose, the\n * eight that stay behind are recorded as catalog-only rather than lost, and the two `as any` casts\n * go away. (2026-09-10, architect's ruling)\n *\n * ── What stays in the template registry, and why ──────────────────────────\n * activity the installed row itself, attached by the catalog query. Writing it back would\n * be a row pointing at itself\n * approvalSteps counted per domain by the catalog query from the line that is actually set up\n * callback a function\n * category how the catalog groups it\n * guide what the work is, for the catalog's reader\n * provider which module registered it\n * readiness how far along the registering module says it is\n * release which version registered it\n *\n * Each of those is answered by the registry, which is loaded in every process that serves the\n * catalog. Copying them to the row would make a second copy that goes stale on the next release.\n *\n * ── The template shape is spelled out, not imported ───────────────────────\n * InstallableActivity carries type-graphql decorators, and the test runner strips types without\n * reading them, so importing it takes the whole test file down. That happened three times in one\n * day — to search-key-declaration, to decision-key-declaration, and here.\n */\ntype TemplateFields = { [field: string]: unknown }\nexport const ACTIVITY_ROW_FIELDS = [\n 'name',\n 'label',\n 'description',\n 'activityType',\n 'priority',\n 'startingType',\n 'multiple',\n 'model',\n 'decisionKeys',\n 'uiType',\n 'uiSource',\n 'viewType',\n 'viewSource',\n 'reportType',\n 'reportSource',\n 'thumbnail'\n] as const\n\n/**\n * The part of a template that is stored on the Activity row.\n *\n * A field the template left undefined is not copied, so installing again does not blank a value\n * somebody edited on the row — `save` with an explicit undefined writes null.\n */\nexport function activityRowFrom(template: object | undefined): TemplateFields {\n const fields = (template ?? {}) as TemplateFields\n const row: TemplateFields = {}\n\n for (const field of ACTIVITY_ROW_FIELDS) {\n const value = fields[field]\n\n if (value !== undefined) {\n row[field] = value\n }\n }\n\n return row\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One key cannot be both a built activity and a declaration that it is not built yet.
|
|
3
|
+
*
|
|
4
|
+
* ── Why both directions ───────────────────────────────────────────────────
|
|
5
|
+
* declareIntendedActivity already refused a key that was installed. installActivityTemplate did not
|
|
6
|
+
* refuse a key that was declared, so which order a module happened to boot in decided whether the
|
|
7
|
+
* conflict was caught at all — declare-then-install slipped through and left the key in both
|
|
8
|
+
* registries.
|
|
9
|
+
*
|
|
10
|
+
* The catalog reads both and concatenates them, so the same work is drawn twice: once as working and
|
|
11
|
+
* once as planned, with the count strip off by one. A reader takes the planned row for work still to
|
|
12
|
+
* come. Nothing throws and nothing is logged.
|
|
13
|
+
*
|
|
14
|
+
* Deduplicating at the read would hide the broken state instead of preventing it, so the two
|
|
15
|
+
* refusals are made symmetric and the read is left alone.
|
|
16
|
+
*
|
|
17
|
+
* ── Which one wins ───────────────────────────────────────────────────────
|
|
18
|
+
* The built activity. A declaration says "this does not exist yet", and once it exists the
|
|
19
|
+
* declaration is simply stale — so both messages say to delete the declaration, and neither
|
|
20
|
+
* suggests removing the activity.
|
|
21
|
+
*
|
|
22
|
+
* The shapes are spelled out as key presence rather than imported: InstallableActivity carries
|
|
23
|
+
* type-graphql decorators, and the test runner strips types without reading them.
|
|
24
|
+
*/
|
|
25
|
+
type KeyedByName = {
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
};
|
|
28
|
+
/** Called when a declaration arrives. */
|
|
29
|
+
export declare function assertNotAlreadyInstalled(key: string, installed: KeyedByName): void;
|
|
30
|
+
/** Called when a template arrives. */
|
|
31
|
+
export declare function assertNotDeclaredIntended(key: string, intended: KeyedByName): void;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assertNotAlreadyInstalled = assertNotAlreadyInstalled;
|
|
4
|
+
exports.assertNotDeclaredIntended = assertNotDeclaredIntended;
|
|
5
|
+
/** Called when a declaration arrives. */
|
|
6
|
+
function assertNotAlreadyInstalled(key, installed) {
|
|
7
|
+
if (!installed?.[key]) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
throw new Error(`declareIntendedActivity: "${key}" is already installed as an activity. ` +
|
|
11
|
+
'Remove the declaration now that the activity exists.');
|
|
12
|
+
}
|
|
13
|
+
/** Called when a template arrives. */
|
|
14
|
+
function assertNotDeclaredIntended(key, intended) {
|
|
15
|
+
if (!intended?.[key]) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
throw new Error(`installActivityTemplate: "${key}" is also declared as intended — work identified but not built. ` +
|
|
19
|
+
'Remove the declaration now that the activity exists.');
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=registry-conflict.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry-conflict.js","sourceRoot":"","sources":["../../../server/controllers/activity-installation/registry-conflict.ts"],"names":[],"mappings":";;AA2BA,8DASC;AAGD,8DASC;AAtBD,yCAAyC;AACzC,SAAgB,yBAAyB,CAAC,GAAW,EAAE,SAAsB;IAC3E,IAAI,CAAC,SAAS,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;QACtB,OAAM;IACR,CAAC;IAED,MAAM,IAAI,KAAK,CACb,6BAA6B,GAAG,yCAAyC;QACvE,sDAAsD,CACzD,CAAA;AACH,CAAC;AAED,sCAAsC;AACtC,SAAgB,yBAAyB,CAAC,GAAW,EAAE,QAAqB;IAC1E,IAAI,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;QACrB,OAAM;IACR,CAAC;IAED,MAAM,IAAI,KAAK,CACb,6BAA6B,GAAG,kEAAkE;QAChG,sDAAsD,CACzD,CAAA;AACH,CAAC","sourcesContent":["/**\n * One key cannot be both a built activity and a declaration that it is not built yet.\n *\n * ── Why both directions ───────────────────────────────────────────────────\n * declareIntendedActivity already refused a key that was installed. installActivityTemplate did not\n * refuse a key that was declared, so which order a module happened to boot in decided whether the\n * conflict was caught at all — declare-then-install slipped through and left the key in both\n * registries.\n *\n * The catalog reads both and concatenates them, so the same work is drawn twice: once as working and\n * once as planned, with the count strip off by one. A reader takes the planned row for work still to\n * come. Nothing throws and nothing is logged.\n *\n * Deduplicating at the read would hide the broken state instead of preventing it, so the two\n * refusals are made symmetric and the read is left alone.\n *\n * ── Which one wins ───────────────────────────────────────────────────────\n * The built activity. A declaration says \"this does not exist yet\", and once it exists the\n * declaration is simply stale — so both messages say to delete the declaration, and neither\n * suggests removing the activity.\n *\n * The shapes are spelled out as key presence rather than imported: InstallableActivity carries\n * type-graphql decorators, and the test runner strips types without reading them.\n */\ntype KeyedByName = { [key: string]: unknown }\n\n/** Called when a declaration arrives. */\nexport function assertNotAlreadyInstalled(key: string, installed: KeyedByName): void {\n if (!installed?.[key]) {\n return\n }\n\n throw new Error(\n `declareIntendedActivity: \"${key}\" is already installed as an activity. ` +\n 'Remove the declaration now that the activity exists.'\n )\n}\n\n/** Called when a template arrives. */\nexport function assertNotDeclaredIntended(key: string, intended: KeyedByName): void {\n if (!intended?.[key]) {\n return\n }\n\n throw new Error(\n `installActivityTemplate: \"${key}\" is also declared as intended — work identified but not built. ` +\n 'Remove the declaration now that the activity exists.'\n )\n}\n"]}
|