@things-factory/worklist 10.1.18 → 10.1.20
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-fields.d.ts +20 -0
- package/dist-client/components/activity-fields.js +23 -0
- package/dist-client/components/activity-fields.js.map +1 -1
- package/dist-client/components/decision-slots.d.ts +26 -0
- package/dist-client/components/decision-slots.js +24 -0
- package/dist-client/components/decision-slots.js.map +1 -1
- package/dist-client/pages/activity-approval/activity-approval-page.d.ts +13 -0
- package/dist-client/pages/activity-approval/activity-approval-page.js +29 -7
- package/dist-client/pages/activity-approval/activity-approval-page.js.map +1 -1
- package/dist-client/pages/activity-instance/activity-instance-search-page.js +2 -1
- package/dist-client/pages/activity-instance/activity-instance-search-page.js.map +1 -1
- package/dist-client/pages/activity-instance/activity-instance-view.js +2 -1
- package/dist-client/pages/activity-instance/activity-instance-view.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/controllers/activity-approval/check-authority.d.ts +17 -0
- package/dist-server/controllers/activity-approval/check-authority.js +53 -0
- package/dist-server/controllers/activity-approval/check-authority.js.map +1 -1
- package/dist-server/controllers/activity-approval/own-request.d.ts +162 -0
- package/dist-server/controllers/activity-approval/own-request.js +135 -0
- package/dist-server/controllers/activity-approval/own-request.js.map +1 -0
- package/dist-server/controllers/activity-approval/resolve-judgeable-seats.d.ts +21 -0
- package/dist-server/controllers/activity-approval/resolve-judgeable-seats.js +59 -0
- package/dist-server/controllers/activity-approval/resolve-judgeable-seats.js.map +1 -0
- package/dist-server/controllers/activity-installation/activity-installation-controller.js +2 -0
- package/dist-server/controllers/activity-installation/activity-installation-controller.js.map +1 -1
- package/dist-server/controllers/activity-installation/registry-conflict.d.ts +15 -0
- package/dist-server/controllers/activity-installation/registry-conflict.js +26 -0
- package/dist-server/controllers/activity-installation/registry-conflict.js.map +1 -1
- package/dist-server/controllers/activity-instance/approval-expectation.d.ts +1 -1
- package/dist-server/controllers/activity-instance/approval-expectation.js +1 -1
- package/dist-server/controllers/activity-instance/approval-expectation.js.map +1 -1
- package/dist-server/controllers/activity-instance/issue.js +38 -0
- package/dist-server/controllers/activity-instance/issue.js.map +1 -1
- package/dist-server/service/activity/activity-decision-key-item-type.d.ts +49 -0
- package/dist-server/service/activity/activity-decision-key-item-type.js +9 -0
- package/dist-server/service/activity/activity-decision-key-item-type.js.map +1 -1
- package/dist-server/service/activity/decision-key-declaration.d.ts +32 -0
- package/dist-server/service/activity/decision-key-declaration.js +81 -0
- package/dist-server/service/activity/decision-key-declaration.js.map +1 -1
- package/dist-server/service/activity-approval/my-pending-approvals.d.ts +16 -4
- package/dist-server/service/activity-approval/my-pending-approvals.js +19 -4
- package/dist-server/service/activity-approval/my-pending-approvals.js.map +1 -1
- package/dist-server/service/activity-summary/activity-summary-query.js +35 -14
- package/dist-server/service/activity-summary/activity-summary-query.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +11 -11
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.assertNotAlreadyInstalled = assertNotAlreadyInstalled;
|
|
4
4
|
exports.assertNotDeclaredIntended = assertNotDeclaredIntended;
|
|
5
|
+
exports.assertDoorSlugIsFree = assertDoorSlugIsFree;
|
|
5
6
|
/** Called when a declaration arrives. */
|
|
6
7
|
function assertNotAlreadyInstalled(key, installed) {
|
|
7
8
|
if (!installed?.[key]) {
|
|
@@ -18,4 +19,29 @@ function assertNotDeclaredIntended(key, intended) {
|
|
|
18
19
|
throw new Error(`installActivityTemplate: "${key}" is also declared as intended — work identified but not built. ` +
|
|
19
20
|
'Remove the declaration now that the activity exists.');
|
|
20
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Two doors must not fold to the same slug — **their value sentences would overwrite each other.**
|
|
24
|
+
*
|
|
25
|
+
* The i18n key for a decision slot is `approval.<door>.<inputPath>` and `<door>` is the activity
|
|
26
|
+
* name kebab-cased, so two names differing only where the slug does not look (`Batch Release` and
|
|
27
|
+
* `BatchRelease`) land on one key. Then one door's sentences are drawn on the other's card, in
|
|
28
|
+
* whichever order the locale file happened to be written.
|
|
29
|
+
*
|
|
30
|
+
* Nothing throws when that happens and nothing is logged — the card simply says the wrong thing
|
|
31
|
+
* about an approval somebody is about to give. It cannot happen today, which is why it is checked
|
|
32
|
+
* now rather than after a door finds it.
|
|
33
|
+
*
|
|
34
|
+
* Refused where the second template arrives, so the message can name both.
|
|
35
|
+
*/
|
|
36
|
+
function assertDoorSlugIsFree(name, slugOf, installed) {
|
|
37
|
+
const slug = slugOf(name);
|
|
38
|
+
const taken = Object.keys(installed || {}).find(one => one !== name && slugOf(one) === slug);
|
|
39
|
+
if (!taken) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`activity "${name}" folds to the same door key as "${taken}" ("${slug}"). ` +
|
|
43
|
+
`The approval card reads its sentences from approval.${slug}.<inputPath>, so the two would ` +
|
|
44
|
+
`draw each other's words with nothing to say which. Give one of them a name that reads ` +
|
|
45
|
+
`differently, not one that only looks different.`);
|
|
46
|
+
}
|
|
21
47
|
//# sourceMappingURL=registry-conflict.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry-conflict.js","sourceRoot":"","sources":["../../../server/controllers/activity-installation/registry-conflict.ts"],"names":[],"mappings":";;AA2BA,8DASC;AAGD,8DASC;
|
|
1
|
+
{"version":3,"file":"registry-conflict.js","sourceRoot":"","sources":["../../../server/controllers/activity-installation/registry-conflict.ts"],"names":[],"mappings":";;AA2BA,8DASC;AAGD,8DASC;AAgBD,oDAeC;AArDD,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;AAED;;;;;;;;;;;;;GAaG;AACH,SAAgB,oBAAoB,CAAC,IAAY,EAAE,MAAgC,EAAE,SAAsB;IACzG,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,CAAA;IAEzB,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,IAAI,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,CAAA;IAE5F,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAM;IACR,CAAC;IAED,MAAM,IAAI,KAAK,CACb,aAAa,IAAI,oCAAoC,KAAK,OAAO,IAAI,MAAM;QACzE,uDAAuD,IAAI,iCAAiC;QAC5F,wFAAwF;QACxF,iDAAiD,CACpD,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\n/**\n * Two doors must not fold to the same slug — **their value sentences would overwrite each other.**\n *\n * The i18n key for a decision slot is `approval.<door>.<inputPath>` and `<door>` is the activity\n * name kebab-cased, so two names differing only where the slug does not look (`Batch Release` and\n * `BatchRelease`) land on one key. Then one door's sentences are drawn on the other's card, in\n * whichever order the locale file happened to be written.\n *\n * Nothing throws when that happens and nothing is logged — the card simply says the wrong thing\n * about an approval somebody is about to give. It cannot happen today, which is why it is checked\n * now rather than after a door finds it.\n *\n * Refused where the second template arrives, so the message can name both.\n */\nexport function assertDoorSlugIsFree(name: string, slugOf: (name: string) => string, installed: KeyedByName): void {\n const slug = slugOf(name)\n\n const taken = Object.keys(installed || {}).find(one => one !== name && slugOf(one) === slug)\n\n if (!taken) {\n return\n }\n\n throw new Error(\n `activity \"${name}\" folds to the same door key as \"${taken}\" (\"${slug}\"). ` +\n `The approval card reads its sentences from approval.${slug}.<inputPath>, so the two would ` +\n `draw each other's words with nothing to say which. Give one of them a name that reads ` +\n `differently, not one that only looks different.`\n )\n}\n"]}
|
|
@@ -10,7 +10,7 @@ function judgeApprovalExpectation(input) {
|
|
|
10
10
|
if (input.approvalLine?.length) {
|
|
11
11
|
return { ok: true };
|
|
12
12
|
}
|
|
13
|
-
return { ok: false, because: 'approval-line-
|
|
13
|
+
return { ok: false, because: 'approval-line-missing', activityName: input.activityName };
|
|
14
14
|
}
|
|
15
15
|
function expectationBroken(verdict) {
|
|
16
16
|
return !verdict.ok;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval-expectation.js","sourceRoot":"","sources":["../../../server/controllers/activity-instance/approval-expectation.ts"],"names":[],"mappings":";;AAsBA,4DAgBC;AAED,8CAIC;AAtBD,SAAgB,wBAAwB,CAAC,KAKxC;IACC,IAAI,CAAC,KAAK,CAAC,eAAe,EAAE,CAAC;QAC3B,6CAA6C;QAC7C,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;IACrB,CAAC;IAED,IAAI,KAAK,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;QAC/B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;IACrB,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"approval-expectation.js","sourceRoot":"","sources":["../../../server/controllers/activity-instance/approval-expectation.ts"],"names":[],"mappings":";;AAsBA,4DAgBC;AAED,8CAIC;AAtBD,SAAgB,wBAAwB,CAAC,KAKxC;IACC,IAAI,CAAC,KAAK,CAAC,eAAe,EAAE,CAAC;QAC3B,6CAA6C;QAC7C,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;IACrB,CAAC;IAED,IAAI,KAAK,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;QAC/B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAA;IACrB,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,uBAAuB,EAAE,YAAY,EAAE,KAAK,CAAC,YAAY,EAAE,CAAA;AAC1F,CAAC;AAED,SAAgB,iBAAiB,CAC/B,OAA4B;IAE5B,OAAO,CAAC,OAAO,CAAC,EAAE,CAAA;AACpB,CAAC","sourcesContent":["/**\n * 「결재를 받겠다」는 기대를 지킬 수 있나 — **순수 판단.**\n *\n * ── 왜 이 자리가 필요한가 ──────────────────────────────────────────────────\n * `startingType: 'post'` 는 낸 사람에게 스레드를 하나 만들고 곧바로 제출까지 간다. 결재선이 있으면\n * 거기서 상신되고, **없으면 그대로 끝난다.** 끝나면 완료 콜백이 돌아 업무가 확정된다.\n *\n * 그 거동 자체는 틀리지 않았다 — 결재 없이 내면 끝나는 일이 있다(작업 일지 같은 것). 문제는 **내는\n * 쪽이 「이건 결재를 받는다」고 믿고 냈는데 결재선이 비어 조용히 확정되는 경우**다. 오류도 없고\n * 흔적도 없어서, 승인 게이트가 있는 척만 하게 된다.\n *\n * 그래서 기대를 낸 쪽이 말하고 여기서 지킨다. **말하지 않은 건은 지나간다** — 기존에 돌던 것이\n * 하나도 달라지지 않아야 한다.\n *\n * ── 왜 내는 쪽에서 미리 보는 것으로는 부족한가 ────────────────────────────\n * 미리 봐도 되지만 그것만으로는 부족하다. 보고 나서 내기까지 사이에 누가 결재선을 비울 수 있다.\n * 내는 것과 같은 트랜잭션 안에서 보는 자리는 하나뿐이다.\n */\nexport type ApprovalExpectation =\n | { ok: true }\n | { ok: false; because: 'approval-line-missing'; activityName: string }\n\nexport function judgeApprovalExpectation(input: {\n requireApproval?: boolean\n /** 액티비티의 것을 물려받고, 초안을 다시 내는 경우 원본의 것까지 합친 **최종** 결재선. */\n approvalLine?: unknown[] | null\n activityName: string\n}): ApprovalExpectation {\n if (!input.requireApproval) {\n /* 기대를 말하지 않았다. 결재 없이 내면 끝나는 일이 정상인 앱이 있다. */\n return { ok: true }\n }\n\n if (input.approvalLine?.length) {\n return { ok: true }\n }\n\n return { ok: false, because: 'approval-line-missing', activityName: input.activityName }\n}\n\nexport function expectationBroken(\n verdict: ApprovalExpectation\n): verdict is Extract<ApprovalExpectation, { ok: false }> {\n return !verdict.ok\n}\n"]}
|
|
@@ -8,6 +8,8 @@ const common_js_1 = require("../common.js");
|
|
|
8
8
|
const submit_js_1 = require("../activity-thread/submit.js");
|
|
9
9
|
const entitlement_js_1 = require("./entitlement.js");
|
|
10
10
|
const approval_expectation_js_1 = require("./approval-expectation.js");
|
|
11
|
+
const own_request_js_1 = require("../activity-approval/own-request.js");
|
|
12
|
+
const resolve_judgeable_seats_js_1 = require("../activity-approval/resolve-judgeable-seats.js");
|
|
11
13
|
async function issue(activityInstance, context) {
|
|
12
14
|
const { domain, user, tx } = context.state;
|
|
13
15
|
const issuedAt = new Date();
|
|
@@ -113,6 +115,42 @@ async function issue(activityInstance, context) {
|
|
|
113
115
|
'그대로 두면 결재 없이 확정됩니다.'
|
|
114
116
|
}));
|
|
115
117
|
}
|
|
118
|
+
/*
|
|
119
|
+
* **올린 사람 말고 설 사람이 있어야 올릴 수 있다.**
|
|
120
|
+
*
|
|
121
|
+
* 결재는 누르는 자리에서도 막지만(§`own-request.ts`), 거기서만 막으면 결재선에 올린 사람밖에 없는
|
|
122
|
+
* 건이 **아무에게도 안 간다.** 정비 완료 확인이 그 모양이면 설비가 정비중으로 남고 라인이 선다.
|
|
123
|
+
*
|
|
124
|
+
* 그래서 올리는 이 자리에서 먼저 본다. 여기서 걸리면 결재선을 고칠 수 있는 사람이 그 자리에서
|
|
125
|
+
* 무엇을 고쳐야 하는지 읽는다. (ADR-0052 ②)
|
|
126
|
+
*
|
|
127
|
+
* 판단은 `own-request.ts` 가 들고, 자리를 푸는 것은 `resolve-judgeable-seats.ts` 가 한다 — 여기에
|
|
128
|
+
* 조건을 적으면 위의 결재선 검사와 같은 이유로 시험할 수 없다.
|
|
129
|
+
*/
|
|
130
|
+
const standing = (0, own_request_js_1.judgeSomeoneElseCanJudge)({
|
|
131
|
+
issuerId: user?.id,
|
|
132
|
+
seats: await (0, resolve_judgeable_seats_js_1.resolveJudgeableSeats)(activityInstance.approvalLine ?? origin?.approvalLine, context)
|
|
133
|
+
});
|
|
134
|
+
if ((0, own_request_js_1.standingBroken)(standing)) {
|
|
135
|
+
/*
|
|
136
|
+
* 사유마다 한 문장씩 — **자리가 같다고 사유를 합치지 않는다.** 관리자가 할 일이 다르다: 하나는
|
|
137
|
+
* 「그 자리에 다른 결재자를」이고 하나는 「그 역할에 사람을」이다. (ADR-0047 ② 의 낱말)
|
|
138
|
+
*/
|
|
139
|
+
throw new Error(standing.refusals
|
|
140
|
+
.map(refusal => {
|
|
141
|
+
/* 이름이 안 풀린 자리는 결재선에 적힌 값이라도 보인다 — 관리자가 고칠 실마리가 그것뿐이다. */
|
|
142
|
+
const seats = refusal.seats
|
|
143
|
+
.map(seat => `${seat.order}차${seat.name ? ` (${seat.name})` : seat.pointsAt ? ` (${seat.pointsAt})` : ''}`)
|
|
144
|
+
.join(', ');
|
|
145
|
+
const said = {
|
|
146
|
+
'approval-role-empty': `${seats} 결재 자리가 지목한 역할에 사람이 없습니다. 그대로 올리면 아무도 결재할 수 없습니다 — 그 역할에 사람을 넣어 주십시오.`,
|
|
147
|
+
'approval-person-missing': `${seats} 결재 자리가 지목한 사람을 찾을 수 없습니다. 그대로 올리면 아무도 결재할 수 없습니다 — 그 자리에 지금 있는 사람을 다시 지정해 주십시오.`,
|
|
148
|
+
'only-the-issuer-could-judge': `${seats} 결재 자리에 올린 사람 말고 설 사람이 없습니다. 그대로 올리면 아무도 결재할 수 없습니다 — 그 자리에 다른 결재자를 넣거나, 그 역할을 가진 사람을 늘려 주십시오.`
|
|
149
|
+
}[refusal.because];
|
|
150
|
+
return context.t(own_request_js_1.STANDING_REFUSAL_KEYS[refusal.because], { seats, defaultValue: said });
|
|
151
|
+
})
|
|
152
|
+
.join(' '));
|
|
153
|
+
}
|
|
116
154
|
const activitySearchKeys = (0, common_js_1.fillActivitySearchKeys)(activity?.searchKeys, input);
|
|
117
155
|
if (activity.startingType == 'post') {
|
|
118
156
|
/*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"issue.js","sourceRoot":"","sources":["../../../server/controllers/activity-instance/issue.ts"],"names":[],"mappings":"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","sourcesContent":["import { In } from 'typeorm'\nimport { Activity } from '../../service/activity/activity.js'\nimport { ActivityInstance, ActivityInstanceStatus } from '../../service/activity-instance/activity-instance.js'\nimport { ActivityInstanceIssue } from '../../service/activity-instance/activity-instance-type.js'\nimport {\n createActivityThreadWithUnassigned,\n createActivityThreadsForAllRoleUsers,\n createActivityThreadsForUsers,\n getSystemUserFromOrgMemberItem,\n createActivityThreadForUser,\n fillActivitySearchKeys\n} from '../common.js'\nimport { submit } from '../activity-thread/submit.js'\nimport { assertMayWorkOnDraft } from './entitlement.js'\nimport { expectationBroken, judgeApprovalExpectation } from './approval-expectation.js'\n\nexport async function issue(\n activityInstance: ActivityInstanceIssue,\n context: ResolverContext\n): Promise<ActivityInstance> {\n const { domain, user, tx } = context.state\n const issuedAt = new Date()\n var {\n id,\n name,\n description,\n assignees,\n uiType,\n uiSource,\n viewType,\n viewSource,\n dueAt,\n priority,\n approvalLine,\n activityId,\n activityType,\n assigneeRole,\n supervisoryRole,\n input\n } = activityInstance\n\n var origin = id\n ? await tx.getRepository(ActivityInstance).findOne({\n where: { domain: { id: domain.id }, id },\n relations: [\n 'domain',\n 'activity',\n 'assigneeRole',\n 'supervisoryRole',\n 'updater',\n 'creator',\n 'starter',\n 'terminator'\n ]\n })\n : null\n\n /*\n 남의 초안은 못 내보낸다.\n\n 내보내는 순간 결재선과 담당자가 서고, 그것은 **다른 사람들의 의무**가 된다. 새로 시작하는\n 것(id 없음)은 주인이 없으므로 이 검사에 안 걸린다. 자격 규칙은 `entitlement.ts` 에 있다.\n */\n if (origin) {\n await assertMayWorkOnDraft(origin, context)\n }\n\n /* \n Prerequisites for a Task to Be Issued.\n - The previous state of the task should be Draft.\n */\n if (origin && origin.state !== ActivityInstanceStatus.Draft) {\n throw new Error(\n context.t(`error.activity-instance is already issued`, {\n id: id,\n state: origin.state\n })\n )\n }\n\n var repository = tx.getRepository(Activity)\n var activity =\n origin?.activity ||\n (await repository.findOne({\n where: { domain: domain.parentId ? { id: In([domain.id, domain.parentId]) } : { id: domain.id }, id: activityId },\n relations: ['assigneeRole', 'supervisoryRole']\n }))\n\n if (!activity) {\n throw new Error(\n context.t('error.activity not found', {\n activity: activityId\n })\n )\n }\n\n if (!origin) {\n activityInstance.name = name || activity.name\n activityInstance.description = description || activity.description\n activityInstance.activityType = activityType || activity.activityType\n activityInstance.uiType = uiType || activity.uiType\n activityInstance.uiSource = uiSource || activity.uiSource\n activityInstance.viewType = viewType || activity.viewType\n activityInstance.viewSource = viewSource || activity.viewSource\n activityInstance.assigneeRole = assigneeRole || activity.assigneeRole\n activityInstance.supervisoryRole = supervisoryRole || activity.supervisoryRole\n activityInstance.priority = priority ?? activity.priority\n\n activityInstance.adhocType = 'standard'\n activityInstance.refBy = activityId\n\n /*\n * 낸 쪽이 직접 주지 않았으면 액티비티의 선언을 따른다.\n *\n * 위의 `if (!assignees || !approvalLine)` 안에 두지 않는다 — 둘 다 준 건에서는 그 블록에 안 들어가고,\n * 그러면 이 축만 조용히 빠진다.\n */\n if (activityInstance.rejectionIsFinal === undefined || activityInstance.rejectionIsFinal === null) {\n activityInstance.rejectionIsFinal = activity.rejectionIsFinal ?? false\n }\n\n if (!assignees || !approvalLine) {\n if (!assignees) {\n assignees = activityInstance.assignees = activity.assignees\n }\n\n if (!approvalLine) {\n approvalLine = activityInstance.approvalLine = activity.approvalLine\n }\n\n if (!dueAt && activity.standardTime) {\n activityInstance.dueAt = new Date(issuedAt.getTime() + activity.standardTime * 1000)\n }\n }\n }\n\n /*\n * **결재를 받겠다고 낸 건은 결재선이 있어야 한다.**\n *\n * 액티비티의 결재선을 물려받은 뒤에 본다 — 낸 쪽이 직접 주지 않아도 액티비티가 들고 있으면 그것이\n * 이 건의 결재선이다. 물려받기 전에 보면 멀쩡한 건을 막는다.\n *\n * 초안을 다시 내는 경우 결재선은 원본이 들고 있으므로 그것까지 함께 본다.\n *\n * 판단은 `approval-expectation.ts` 가 든다 — 여기서 조건을 적으면 시험할 수 없다.\n */\n const expectation = judgeApprovalExpectation({\n requireApproval: activityInstance.requireApproval,\n approvalLine: activityInstance.approvalLine ?? origin?.approvalLine,\n activityName: activity.name\n })\n\n if (expectationBroken(expectation)) {\n throw new Error(\n context.t('error.approval line is empty but approval was required', {\n activity: expectation.activityName,\n defaultValue:\n `\"${expectation.activityName}\" 에 결재선이 없습니다. 이 건은 결재를 받기로 하고 낸 것이라 올리지 않습니다 — ` +\n '그대로 두면 결재 없이 확정됩니다.'\n })\n )\n }\n\n const activitySearchKeys = fillActivitySearchKeys(activity?.searchKeys, input)\n\n if (activity.startingType == 'post') {\n /* \n startingType이 'post'이면, \n activityInstance 이슈와 동시에 issuer에게 할당된 activityThread가 생성되고 submit까지 모두 완료되게 된다.\n 만일, 결재선이 있다면, 상신되게 된다.\n */\n const posted = await tx.getRepository(ActivityInstance).save({\n version: 1,\n creator: user,\n ...origin,\n ...activityInstance,\n transaction: 'post',\n activity,\n ...activitySearchKeys,\n state: ActivityInstanceStatus.Started,\n domain,\n issuer: user,\n updater: user,\n issuedAt,\n startedAt: issuedAt\n })\n\n const thread = await createActivityThreadForUser('post', posted, user, context)\n\n /* post 방식은 thread가 하나이므로, instance의 output을 그대로 thread의 output이라고 판단할 수 있다. 따라서, 아래 로직은 제거한다. */\n // const data = { ...activityInstance.input, ...activityInstance.output }\n // const output = (activity.model || [])\n // .filter(item => item.inout === 'inout' || item.inout === 'out')\n // .reduce((inout, item) => {\n // inout[item.tag] = data[item.tag]\n // return inout\n // }, {})\n\n await submit({ id: thread.id, output: activityInstance.output }, context)\n\n return await tx.getRepository(ActivityInstance).findOneBy({ id: posted.id })\n } else {\n const issued = await tx.getRepository(ActivityInstance).save({\n version: 1,\n creator: user,\n ...origin,\n ...activityInstance,\n transaction: 'issue',\n activity,\n ...activitySearchKeys,\n state: ActivityInstanceStatus.Issued,\n domain,\n issuer: user,\n updater: user,\n issuedAt\n })\n\n /*\n 1. 하나의 thread가 필요한 경우 (대부분)\n 조건 : activityInstance의 threadsMin이 1 이어야 한다.\n 로직 1 : getSystemUserFromOrgMemberItem 의 결과가 한명이라면 그대로 activityThread.assignee로 직접 할당한다.\n 로직 2 : getSystemUserFromOrgMemberItem 의 결과가 한명이 아니라면, assignee타입이 assigneeRole 인 경우에만 처리가 가능하다.\n 이 경우에는 assignee를 비운다. - createActivityThreadWithUnassigned\n 2. 할당되지 않은 복수개의 thread가 필요한 경우\n 조건 : threadsMin이 1 이상이어야 한다.\n 로직 1 : getSystemUserFromOrgMemberItem 의 결과와 threadsMin 값이 같다면, 그대로 activityThread.assignee로 직접 할당한다. createActivityThreadsForAllRoleUsers\n 로직 2 : createActivityThreadWithUnassigned\n 3. 모든 assignee가 처리를 해야하는 경우\n 조건 : threadsMin이 0 이어야 한다.\n 로직 : getSystemUserFromOrgMemberItem 의 결과로 createActivityThreadsForUsers\n */\n\n // assignees가 복수인 것은 아무 의미가 없다.\n const assignedUsers = await Promise.all(\n (assignees || []).map(assignee => getSystemUserFromOrgMemberItem(assignee, context))\n )\n\n if (assignedUsers.length == 0) {\n if (issued.assigneeRoleId) {\n if (issued.threadsMin === 0) {\n /* assigneeRoleId에 해당하는 모든 assignee 에게 태스크를 할당하는 경우 */\n await createActivityThreadsForAllRoleUsers('issue', issued, context)\n } else {\n /* minimum threads 설정에 따른 pending assignment 상태로 activityThread를 만드는 경우 */\n await createActivityThreadWithUnassigned('issue', issued, context)\n }\n }\n } else {\n await createActivityThreadsForUsers('issue', issued, assignedUsers, context)\n }\n\n return await tx.getRepository(ActivityInstance).findOneBy({ id: issued.id })\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"issue.js","sourceRoot":"","sources":["../../../server/controllers/activity-instance/issue.ts"],"names":[],"mappings":"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","sourcesContent":["import { In } from 'typeorm'\nimport { Activity } from '../../service/activity/activity.js'\nimport { ActivityInstance, ActivityInstanceStatus } from '../../service/activity-instance/activity-instance.js'\nimport { ActivityInstanceIssue } from '../../service/activity-instance/activity-instance-type.js'\nimport {\n createActivityThreadWithUnassigned,\n createActivityThreadsForAllRoleUsers,\n createActivityThreadsForUsers,\n getSystemUserFromOrgMemberItem,\n createActivityThreadForUser,\n fillActivitySearchKeys\n} from '../common.js'\nimport { submit } from '../activity-thread/submit.js'\nimport { assertMayWorkOnDraft } from './entitlement.js'\nimport { expectationBroken, judgeApprovalExpectation } from './approval-expectation.js'\nimport { STANDING_REFUSAL_KEYS, judgeSomeoneElseCanJudge, standingBroken } from '../activity-approval/own-request.js'\nimport { resolveJudgeableSeats } from '../activity-approval/resolve-judgeable-seats.js'\n\nexport async function issue(\n activityInstance: ActivityInstanceIssue,\n context: ResolverContext\n): Promise<ActivityInstance> {\n const { domain, user, tx } = context.state\n const issuedAt = new Date()\n var {\n id,\n name,\n description,\n assignees,\n uiType,\n uiSource,\n viewType,\n viewSource,\n dueAt,\n priority,\n approvalLine,\n activityId,\n activityType,\n assigneeRole,\n supervisoryRole,\n input\n } = activityInstance\n\n var origin = id\n ? await tx.getRepository(ActivityInstance).findOne({\n where: { domain: { id: domain.id }, id },\n relations: [\n 'domain',\n 'activity',\n 'assigneeRole',\n 'supervisoryRole',\n 'updater',\n 'creator',\n 'starter',\n 'terminator'\n ]\n })\n : null\n\n /*\n 남의 초안은 못 내보낸다.\n\n 내보내는 순간 결재선과 담당자가 서고, 그것은 **다른 사람들의 의무**가 된다. 새로 시작하는\n 것(id 없음)은 주인이 없으므로 이 검사에 안 걸린다. 자격 규칙은 `entitlement.ts` 에 있다.\n */\n if (origin) {\n await assertMayWorkOnDraft(origin, context)\n }\n\n /* \n Prerequisites for a Task to Be Issued.\n - The previous state of the task should be Draft.\n */\n if (origin && origin.state !== ActivityInstanceStatus.Draft) {\n throw new Error(\n context.t(`error.activity-instance is already issued`, {\n id: id,\n state: origin.state\n })\n )\n }\n\n var repository = tx.getRepository(Activity)\n var activity =\n origin?.activity ||\n (await repository.findOne({\n where: { domain: domain.parentId ? { id: In([domain.id, domain.parentId]) } : { id: domain.id }, id: activityId },\n relations: ['assigneeRole', 'supervisoryRole']\n }))\n\n if (!activity) {\n throw new Error(\n context.t('error.activity not found', {\n activity: activityId\n })\n )\n }\n\n if (!origin) {\n activityInstance.name = name || activity.name\n activityInstance.description = description || activity.description\n activityInstance.activityType = activityType || activity.activityType\n activityInstance.uiType = uiType || activity.uiType\n activityInstance.uiSource = uiSource || activity.uiSource\n activityInstance.viewType = viewType || activity.viewType\n activityInstance.viewSource = viewSource || activity.viewSource\n activityInstance.assigneeRole = assigneeRole || activity.assigneeRole\n activityInstance.supervisoryRole = supervisoryRole || activity.supervisoryRole\n activityInstance.priority = priority ?? activity.priority\n\n activityInstance.adhocType = 'standard'\n activityInstance.refBy = activityId\n\n /*\n * 낸 쪽이 직접 주지 않았으면 액티비티의 선언을 따른다.\n *\n * 위의 `if (!assignees || !approvalLine)` 안에 두지 않는다 — 둘 다 준 건에서는 그 블록에 안 들어가고,\n * 그러면 이 축만 조용히 빠진다.\n */\n if (activityInstance.rejectionIsFinal === undefined || activityInstance.rejectionIsFinal === null) {\n activityInstance.rejectionIsFinal = activity.rejectionIsFinal ?? false\n }\n\n if (!assignees || !approvalLine) {\n if (!assignees) {\n assignees = activityInstance.assignees = activity.assignees\n }\n\n if (!approvalLine) {\n approvalLine = activityInstance.approvalLine = activity.approvalLine\n }\n\n if (!dueAt && activity.standardTime) {\n activityInstance.dueAt = new Date(issuedAt.getTime() + activity.standardTime * 1000)\n }\n }\n }\n\n /*\n * **결재를 받겠다고 낸 건은 결재선이 있어야 한다.**\n *\n * 액티비티의 결재선을 물려받은 뒤에 본다 — 낸 쪽이 직접 주지 않아도 액티비티가 들고 있으면 그것이\n * 이 건의 결재선이다. 물려받기 전에 보면 멀쩡한 건을 막는다.\n *\n * 초안을 다시 내는 경우 결재선은 원본이 들고 있으므로 그것까지 함께 본다.\n *\n * 판단은 `approval-expectation.ts` 가 든다 — 여기서 조건을 적으면 시험할 수 없다.\n */\n const expectation = judgeApprovalExpectation({\n requireApproval: activityInstance.requireApproval,\n approvalLine: activityInstance.approvalLine ?? origin?.approvalLine,\n activityName: activity.name\n })\n\n if (expectationBroken(expectation)) {\n throw new Error(\n context.t('error.approval line is empty but approval was required', {\n activity: expectation.activityName,\n defaultValue:\n `\"${expectation.activityName}\" 에 결재선이 없습니다. 이 건은 결재를 받기로 하고 낸 것이라 올리지 않습니다 — ` +\n '그대로 두면 결재 없이 확정됩니다.'\n })\n )\n }\n\n /*\n * **올린 사람 말고 설 사람이 있어야 올릴 수 있다.**\n *\n * 결재는 누르는 자리에서도 막지만(§`own-request.ts`), 거기서만 막으면 결재선에 올린 사람밖에 없는\n * 건이 **아무에게도 안 간다.** 정비 완료 확인이 그 모양이면 설비가 정비중으로 남고 라인이 선다.\n *\n * 그래서 올리는 이 자리에서 먼저 본다. 여기서 걸리면 결재선을 고칠 수 있는 사람이 그 자리에서\n * 무엇을 고쳐야 하는지 읽는다. (ADR-0052 ②)\n *\n * 판단은 `own-request.ts` 가 들고, 자리를 푸는 것은 `resolve-judgeable-seats.ts` 가 한다 — 여기에\n * 조건을 적으면 위의 결재선 검사와 같은 이유로 시험할 수 없다.\n */\n const standing = judgeSomeoneElseCanJudge({\n issuerId: user?.id,\n seats: await resolveJudgeableSeats(activityInstance.approvalLine ?? origin?.approvalLine, context)\n })\n\n if (standingBroken(standing)) {\n /*\n * 사유마다 한 문장씩 — **자리가 같다고 사유를 합치지 않는다.** 관리자가 할 일이 다르다: 하나는\n * 「그 자리에 다른 결재자를」이고 하나는 「그 역할에 사람을」이다. (ADR-0047 ② 의 낱말)\n */\n throw new Error(\n standing.refusals\n .map(refusal => {\n /* 이름이 안 풀린 자리는 결재선에 적힌 값이라도 보인다 — 관리자가 고칠 실마리가 그것뿐이다. */\n const seats = refusal.seats\n .map(seat => `${seat.order}차${seat.name ? ` (${seat.name})` : seat.pointsAt ? ` (${seat.pointsAt})` : ''}`)\n .join(', ')\n\n const said = {\n 'approval-role-empty': `${seats} 결재 자리가 지목한 역할에 사람이 없습니다. 그대로 올리면 아무도 결재할 수 없습니다 — 그 역할에 사람을 넣어 주십시오.`,\n 'approval-person-missing': `${seats} 결재 자리가 지목한 사람을 찾을 수 없습니다. 그대로 올리면 아무도 결재할 수 없습니다 — 그 자리에 지금 있는 사람을 다시 지정해 주십시오.`,\n 'only-the-issuer-could-judge': `${seats} 결재 자리에 올린 사람 말고 설 사람이 없습니다. 그대로 올리면 아무도 결재할 수 없습니다 — 그 자리에 다른 결재자를 넣거나, 그 역할을 가진 사람을 늘려 주십시오.`\n }[refusal.because]\n\n return context.t(STANDING_REFUSAL_KEYS[refusal.because], { seats, defaultValue: said })\n })\n .join(' ')\n )\n }\n\n const activitySearchKeys = fillActivitySearchKeys(activity?.searchKeys, input)\n\n if (activity.startingType == 'post') {\n /* \n startingType이 'post'이면, \n activityInstance 이슈와 동시에 issuer에게 할당된 activityThread가 생성되고 submit까지 모두 완료되게 된다.\n 만일, 결재선이 있다면, 상신되게 된다.\n */\n const posted = await tx.getRepository(ActivityInstance).save({\n version: 1,\n creator: user,\n ...origin,\n ...activityInstance,\n transaction: 'post',\n activity,\n ...activitySearchKeys,\n state: ActivityInstanceStatus.Started,\n domain,\n issuer: user,\n updater: user,\n issuedAt,\n startedAt: issuedAt\n })\n\n const thread = await createActivityThreadForUser('post', posted, user, context)\n\n /* post 방식은 thread가 하나이므로, instance의 output을 그대로 thread의 output이라고 판단할 수 있다. 따라서, 아래 로직은 제거한다. */\n // const data = { ...activityInstance.input, ...activityInstance.output }\n // const output = (activity.model || [])\n // .filter(item => item.inout === 'inout' || item.inout === 'out')\n // .reduce((inout, item) => {\n // inout[item.tag] = data[item.tag]\n // return inout\n // }, {})\n\n await submit({ id: thread.id, output: activityInstance.output }, context)\n\n return await tx.getRepository(ActivityInstance).findOneBy({ id: posted.id })\n } else {\n const issued = await tx.getRepository(ActivityInstance).save({\n version: 1,\n creator: user,\n ...origin,\n ...activityInstance,\n transaction: 'issue',\n activity,\n ...activitySearchKeys,\n state: ActivityInstanceStatus.Issued,\n domain,\n issuer: user,\n updater: user,\n issuedAt\n })\n\n /*\n 1. 하나의 thread가 필요한 경우 (대부분)\n 조건 : activityInstance의 threadsMin이 1 이어야 한다.\n 로직 1 : getSystemUserFromOrgMemberItem 의 결과가 한명이라면 그대로 activityThread.assignee로 직접 할당한다.\n 로직 2 : getSystemUserFromOrgMemberItem 의 결과가 한명이 아니라면, assignee타입이 assigneeRole 인 경우에만 처리가 가능하다.\n 이 경우에는 assignee를 비운다. - createActivityThreadWithUnassigned\n 2. 할당되지 않은 복수개의 thread가 필요한 경우\n 조건 : threadsMin이 1 이상이어야 한다.\n 로직 1 : getSystemUserFromOrgMemberItem 의 결과와 threadsMin 값이 같다면, 그대로 activityThread.assignee로 직접 할당한다. createActivityThreadsForAllRoleUsers\n 로직 2 : createActivityThreadWithUnassigned\n 3. 모든 assignee가 처리를 해야하는 경우\n 조건 : threadsMin이 0 이어야 한다.\n 로직 : getSystemUserFromOrgMemberItem 의 결과로 createActivityThreadsForUsers\n */\n\n // assignees가 복수인 것은 아무 의미가 없다.\n const assignedUsers = await Promise.all(\n (assignees || []).map(assignee => getSystemUserFromOrgMemberItem(assignee, context))\n )\n\n if (assignedUsers.length == 0) {\n if (issued.assigneeRoleId) {\n if (issued.threadsMin === 0) {\n /* assigneeRoleId에 해당하는 모든 assignee 에게 태스크를 할당하는 경우 */\n await createActivityThreadsForAllRoleUsers('issue', issued, context)\n } else {\n /* minimum threads 설정에 따른 pending assignment 상태로 activityThread를 만드는 경우 */\n await createActivityThreadWithUnassigned('issue', issued, context)\n }\n }\n } else {\n await createActivityThreadsForUsers('issue', issued, assignedUsers, context)\n }\n\n return await tx.getRepository(ActivityInstance).findOneBy({ id: issued.id })\n }\n}\n"]}
|
|
@@ -142,6 +142,55 @@ export declare class ActivityDecisionKeyItem {
|
|
|
142
142
|
inputPath: string;
|
|
143
143
|
name?: string;
|
|
144
144
|
tKey?: string;
|
|
145
|
+
/**
|
|
146
|
+
* i18n key for the **value**, when the value is a sentence the module would otherwise freeze.
|
|
147
|
+
*
|
|
148
|
+
* ── tKey is the label; this is the sentence ───────────────────────────
|
|
149
|
+
* Two different things were being asked of one field. The screen draws a slot as a label and a
|
|
150
|
+
* value, and they come from different places:
|
|
151
|
+
*
|
|
152
|
+
* label declaredLabel(slot) = t(slot.tKey, { defaultValue: name }) 「설비」 「사유」
|
|
153
|
+
* value fieldValue(slot.value) the frozen sentence
|
|
154
|
+
*
|
|
155
|
+
* Putting a sentence key in `tKey` turns the label into a developer's English name and leaves the
|
|
156
|
+
* frozen Korean where it was — the thing this is meant to remove.
|
|
157
|
+
*
|
|
158
|
+
* ── Why the sentence must not freeze ──────────────────────────────────
|
|
159
|
+
* Eighteen doors build a Korean sentence when the request is raised and `issue()` snapshots it
|
|
160
|
+
* whole. The sentence then stands in the language of whoever raised it, not whoever reads it, and
|
|
161
|
+
* an approver on another language never catches up.
|
|
162
|
+
*
|
|
163
|
+
* Values are right to freeze — an approval is a judgment about facts at a moment, so quantities,
|
|
164
|
+
* dates and counts are the record. The prose describing them is not a fact, and freezing it also
|
|
165
|
+
* loses what it described: maintenance deferral carries `impact.times` as a value but `dueAt`
|
|
166
|
+
* only inside the sentence, so editing that wording would leave no way to say which due date the
|
|
167
|
+
* approval was about. (ADR-0054 ④)
|
|
168
|
+
*
|
|
169
|
+
* ── Computed, never invented ──────────────────────────────────────────
|
|
170
|
+
* `approval.<door>.<inputPath>` — `inputPath` is the one thing unique within a door, which `role`
|
|
171
|
+
* is not (a door repeats `consequence`). Nobody writing a new door picks a key, so no table of
|
|
172
|
+
* keys grows, and a test can count every one of them.
|
|
173
|
+
*
|
|
174
|
+
* Leaving it out is how a slot stays as it is. The frozen sentence remains in `value`, so a
|
|
175
|
+
* consumer that knows nothing about this field — the plain card, the timeline, an export — draws
|
|
176
|
+
* exactly what it drew before, and a missing translation never puts English into a Korean screen.
|
|
177
|
+
*/
|
|
178
|
+
valueTKey?: string;
|
|
179
|
+
/**
|
|
180
|
+
* What the sentence puts into its blanks: **name in the sentence → where the value lives.**
|
|
181
|
+
*
|
|
182
|
+
* { what: 'equipmentName', days: 'impact.days' } → t(valueTKey, { what: …, days: … })
|
|
183
|
+
*
|
|
184
|
+
* Every value here is an `inputPath`, and that is the point rather than a convention: a sentence
|
|
185
|
+
* may only be built from values that were snapshotted. Wanting a value with no field to read it
|
|
186
|
+
* from means the field is missing, not that params needs another source — so the rule is a shape
|
|
187
|
+
* a declaration cannot write around, and a test counts it.
|
|
188
|
+
*
|
|
189
|
+
* The names on the left belong to the sentence and only have to match the locale files.
|
|
190
|
+
*/
|
|
191
|
+
params?: {
|
|
192
|
+
[name: string]: string;
|
|
193
|
+
};
|
|
145
194
|
/**
|
|
146
195
|
* Unit to print after the value, when the activity fixes the unit.
|
|
147
196
|
*
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ActivityDecisionKeyItem = exports.ActivityDecisionRole = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const type_graphql_1 = require("type-graphql");
|
|
6
|
+
const shell_1 = require("@things-factory/shell");
|
|
6
7
|
/**
|
|
7
8
|
* The part an input value plays on the approval screen.
|
|
8
9
|
*
|
|
@@ -166,6 +167,14 @@ tslib_1.__decorate([
|
|
|
166
167
|
(0, type_graphql_1.Field)({ nullable: true, description: 'i18n key for the label. Without it the screen shows no label.' }),
|
|
167
168
|
tslib_1.__metadata("design:type", String)
|
|
168
169
|
], ActivityDecisionKeyItem.prototype, "tKey", void 0);
|
|
170
|
+
tslib_1.__decorate([
|
|
171
|
+
(0, type_graphql_1.Field)({ nullable: true, description: "i18n key for the value sentence: approval.<door>.<inputPath>" }),
|
|
172
|
+
tslib_1.__metadata("design:type", String)
|
|
173
|
+
], ActivityDecisionKeyItem.prototype, "valueTKey", void 0);
|
|
174
|
+
tslib_1.__decorate([
|
|
175
|
+
(0, type_graphql_1.Field)(type => shell_1.ScalarObject, { nullable: true, description: 'sentence placeholder → inputPath' }),
|
|
176
|
+
tslib_1.__metadata("design:type", Object)
|
|
177
|
+
], ActivityDecisionKeyItem.prototype, "params", void 0);
|
|
169
178
|
tslib_1.__decorate([
|
|
170
179
|
(0, type_graphql_1.Field)({ nullable: true, description: 'Unit to print after the value, when the activity fixes the unit' }),
|
|
171
180
|
tslib_1.__metadata("design:type", String)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"activity-decision-key-item-type.js","sourceRoot":"","sources":["../../../server/service/activity/activity-decision-key-item-type.ts"],"names":[],"mappings":";;;;AAAA,+CAAkE;AAElE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,IAAY,oBAkEX;AAlED,WAAY,oBAAoB;IAC9B,mEAAmE;IACnE,6CAAqB,CAAA;IACrB,mDAAmD;IACnD,2CAAmB,CAAA;IACnB,iGAAiG;IACjG,yCAAiB,CAAA;IACjB;;;;;;;;OAQG;IACH,mDAA2B,CAAA;IAC3B,+DAA+D;IAC/D,yCAAiB,CAAA;IACjB;;;;;;;;;;;;OAYG;IACH,qDAA6B,CAAA;IAC7B;;;;;;;;;;;;;;;;OAgBG;IACH,uDAA+B,CAAA;IAC/B;;;;;;;;;;;;;OAaG;IACH,6CAAqB,CAAA;AACvB,CAAC,EAlEW,oBAAoB,oCAApB,oBAAoB,QAkE/B;AAED,IAAA,+BAAgB,EAAC,oBAAoB,EAAE;IACrC,IAAI,EAAE,sBAAsB;IAC5B,WAAW,EAAE,sDAAsD;CACpE,CAAC,CAAA;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEI,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;CA6BnC,CAAA;AA7BY,0DAAuB;AAElC;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,oBAAoB,CAAC;;qDACV;AAG1B;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,2EAA2E,EAAE,CAAC;;0DACnF;AAGjB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,iDAAiD,EAAE,CAAC;;qDAC7E;AAGb;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,+DAA+D,EAAE,CAAC;;qDAC3F;AAiBb;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,iEAAiE,EAAE,CAAC;;qDAC7F;kCA5BF,uBAAuB;IADnC,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,oCAAoC,EAAE,CAAC;GACrD,uBAAuB,CA6BnC","sourcesContent":["import { Field, ObjectType, registerEnumType } from 'type-graphql'\n\n/**\n * The part an input value plays on the approval screen.\n *\n * ── The shape of the set: three tenses, and five others ───────────────────\n * Three of the eight say something about the subject at a point in time, and the other five do not:\n *\n * past evidence what the decision was made from (may repeat)\n * present currentState how things stand right now\n * future consequence what approving would change (may repeat)\n *\n * subject what this is about\n * reason what a person wrote\n * policy why it came up for approval at all\n * headline one sentence built from the values\n * reversibility whether approving can be undone\n *\n * Every mis-slotting found so far was the same mistake in that column: currentState under\n * consequence read the present as the future, and evidence under currentState read the past as the\n * present. So when a ninth role is asked for, the first question is whether the value is the\n * subject's past, present or future — if it is, one of those three already holds it. If it is not,\n * whoever asks should be able to say what it is instead. (2026-09-10, architect's ruling)\n *\n * ── Why the keys and the values are spelled the same ──────────────────────\n * type-graphql publishes an enum by its **keys** when the field names the enum type, and by its\n * **values** when the field leaves the type off. One word in a @Field decorator therefore decides\n * what a client receives, and a schema-improving edit far away can flip it. That has already cost\n * this repository twice: ApprovalLineItem.participation arrives as `Reference` and was compared\n * against `reference`, false every time; the catalog page read `NotWorklist` where its selector,\n * i18n key and stage list all said `notworklist`.\n *\n * Writing each key as its own value takes the choice away — either spelling produces the same\n * string. ActivityModelItemInoutType and ActivityModelItemType are written the same way.\n *\n * ── The set is closed, and widening it is the architect's call ────────────\n * Left open as a free string, a convention becomes suffix-appending: every lane invents its own\n * word for the same slot and the screen ends up switching on a dozen spellings, printing the\n * unmatched ones. Adding a role here is an architecture decision, not a lane's. (2026-09-09,\n * architect's ruling)\n */\nexport enum ActivityDecisionRole {\n /** One sentence built from the values: 「폐기 30 EA」, 「3에서 1로 올림」. */\n headline = 'headline',\n /** What the decision is about — one identifier. */\n subject = 'subject',\n /** What the person who raised it wrote. Absent, the screen says so rather than leaving a gap. */\n reason = 'reason',\n /**\n * What changes on approval: 「주문 넷이 뒤로 밀린다」. The only role that may appear more than once.\n *\n * Named consequence rather than effect because TwinCommand.effect already means the opposite in\n * time: { status, at, seen }, what the source reported back after the command was dispatched.\n * That one is an observation and past; this one is a prediction and future. Using a real term for\n * the wrong meaning is worse than coining one, and plant already writes .consequence for exactly\n * this sense in its stop-reason dialog. (2026-09-10, architect's ruling)\n */\n consequence = 'consequence',\n /** Why this came up for approval at all: 「폐기는 100 이상이면 결재」. */\n policy = 'policy',\n /**\n * What is measured to be true right now — about the world, not about the request.\n *\n * The other roles all describe what is being asked for. This one does not, and putting it under\n * consequence made the screen say the opposite of the truth: the twin's current readings would\n * have read as things the approval causes.\n *\n * Not named observed: ops-contract already has observedBy for which observation backs a declared\n * property, and TwinCommand.effect is an observation too, so observed would sit in the most\n * confusable spot between them. (2026-09-10, architect's ruling) plant never needed it because the subject's present state\n * went into the headline (「폐기 30 EA」); the twin's instruction and its current state are two\n * separate things, and holding them side by side is what this screen is for.\n */\n currentState = 'currentState',\n /**\n * Whether approving can be undone. Drawn in the frame rather than in the body.\n *\n * Not a separate role for want of a place — it fits consequence well enough — but for want of\n * weight. Listed among the consequences it reads with the same force as \"four orders get pushed\n * back\", and on\n * the twin actuation, where approving moves equipment, it is the heaviest line on the screen.\n *\n * ── Not the same as rejectionIsFinal ──────────────────────────────────\n * Two \"cannot be undone\" facts attach to opposite actions, and the twin actuation carries both,\n * which is what makes them look like one:\n *\n * reversibility whether **approving** can be undone (this role, read from the input)\n * rejectionIsFinal whether **rejecting** ends the case (a column on Activity)\n *\n * Merging them in a screen sentence tells the reader that rejection can be taken back.\n */\n reversibility = 'reversibility',\n /**\n * What the decision was made from — which inspection, against which spec, decided when.\n *\n * Neither consequence nor currentState: consequence is the opposite direction in time, and under\n * currentState a past finding reads as the present state of the subject. plant's disposition\n * approval kept its own screen for exactly these three values, and it survived the comparison not\n * because a slot could not hold them but because no slot existed — those are different reasons,\n * and only the second one means the set is short.\n *\n * plant's case for it: someone approving a concession in a regulated plant who cannot see which\n * inspection produced the finding is approving without grounds.\n *\n * Repeats, because grounds come in several — there were three.\n */\n evidence = 'evidence'\n}\n\nregisterEnumType(ActivityDecisionRole, {\n name: 'ActivityDecisionRole',\n description: 'the part an input value plays on the approval screen'\n})\n\n/**\n * One value the approver reads, and the part it plays.\n *\n * ── Why this points at input rather than at a model item ──────────────────\n * inputPath names a value inside the instance's input and does not go through the activity's model.\n * That is the whole reason it is a separate array: the values an approver most needs are computed by\n * the registering module and put on the input, not declared as model items — plant's `policy` and\n * its `shift.passes` / `shift.passedBy` are exactly that. A role hung on a model item could not\n * reach them.\n *\n * ── inputPath, not inputKey ───────────────────────────────────────────────\n * ActivitySearchKeyItem.inputKey names one top-level key, because its value goes into a searchable\n * column and a path into a list is not something that can be indexed. This field takes a path,\n * because it points at a value to draw. The two do different work, so they carry different names —\n * one word with two meanings would make the wrong one silent, and a name that does not exist is\n * read as a mistake instead. (2026-09-09, architect's ruling)\n *\n * 'policy' → input.policy\n * 'shift.passes' → input.shift.passes\n *\n * Dots only. No array index — drawing a list is the slot's job, not the path's — and no wildcard. A\n * path that leads nowhere resolves to undefined and is not folded into an empty value: a `reason`\n * that is missing and a `reason` someone left blank are different facts, and the screen draws them\n * differently.\n *\n * The two declarations also address different readers. model declares the boxes a performer fills;\n * this declares what an approver reads. Putting both on one item leaves \"is this an input or\n * something to judge by\" unanswerable.\n *\n * Order matters. plant wrote four of these screens by hand and all four laid the slots out in the\n * same order, which makes the order the screen's hierarchy rather than a preference — so this is an\n * ordered array and not an object keyed by role.\n */\n@ObjectType({ description: 'Entity for ActivityDecisionKeyItem' })\nexport class ActivityDecisionKeyItem {\n @Field(type => ActivityDecisionRole)\n role: ActivityDecisionRole\n\n @Field({ description: \"Dotted path to the value inside the instance's input, e.g. 'shift.passes'\" })\n inputPath: string\n\n @Field({ nullable: true, description: 'Developer-facing note. Never shown on a screen.' })\n name?: string\n\n @Field({ nullable: true, description: 'i18n key for the label. Without it the screen shows no label.' })\n tKey?: string\n\n /**\n * Unit to print after the value, when the activity fixes the unit.\n *\n * Declared once, so it holds for every instance of the activity — 「30 건」 works, and a unit that\n * varies per record does not: plant's disposition carries the unit alongside the quantity, so the\n * same activity produces 「3 EA」 and 「12.5 KG」. Writing one of them here would attach the wrong\n * unit to the other records, with nothing to catch it.\n *\n * A unit that varies belongs in the headline sentence the module builds, which is what plant did:\n * 「LOT-A 3 EA 를 폐기합니다」. Leave this empty then.\n *\n * Delete this field if no activity ever turns out to fix a unit. A field nobody writes stays\n * forever and the next person reads a meaning into it. (2026-09-10, architect's ruling)\n */\n @Field({ nullable: true, description: 'Unit to print after the value, when the activity fixes the unit' })\n unit?: string\n}\n"]}
|
|
1
|
+
{"version":3,"file":"activity-decision-key-item-type.js","sourceRoot":"","sources":["../../../server/service/activity/activity-decision-key-item-type.ts"],"names":[],"mappings":";;;;AAAA,+CAAkE;AAElE,iDAAoD;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,IAAY,oBAkEX;AAlED,WAAY,oBAAoB;IAC9B,mEAAmE;IACnE,6CAAqB,CAAA;IACrB,mDAAmD;IACnD,2CAAmB,CAAA;IACnB,iGAAiG;IACjG,yCAAiB,CAAA;IACjB;;;;;;;;OAQG;IACH,mDAA2B,CAAA;IAC3B,+DAA+D;IAC/D,yCAAiB,CAAA;IACjB;;;;;;;;;;;;OAYG;IACH,qDAA6B,CAAA;IAC7B;;;;;;;;;;;;;;;;OAgBG;IACH,uDAA+B,CAAA;IAC/B;;;;;;;;;;;;;OAaG;IACH,6CAAqB,CAAA;AACvB,CAAC,EAlEW,oBAAoB,oCAApB,oBAAoB,QAkE/B;AAED,IAAA,+BAAgB,EAAC,oBAAoB,EAAE;IACrC,IAAI,EAAE,sBAAsB;IAC5B,WAAW,EAAE,sDAAsD;CACpE,CAAC,CAAA;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEI,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;CAgFnC,CAAA;AAhFY,0DAAuB;AAElC;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,oBAAoB,CAAC;;qDACV;AAG1B;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,2EAA2E,EAAE,CAAC;;0DACnF;AAGjB;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,iDAAiD,EAAE,CAAC;;qDAC7E;AAGb;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,+DAA+D,EAAE,CAAC;;qDAC3F;AAoCb;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,8DAA8D,EAAE,CAAC;;0DACrF;AAelB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,oBAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,kCAAkC,EAAE,CAAC;;uDAC9D;AAiBnC;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,iEAAiE,EAAE,CAAC;;qDAC7F;kCA/EF,uBAAuB;IADnC,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,oCAAoC,EAAE,CAAC;GACrD,uBAAuB,CAgFnC","sourcesContent":["import { Field, ObjectType, registerEnumType } from 'type-graphql'\n\nimport { ScalarObject } from '@things-factory/shell'\n\n/**\n * The part an input value plays on the approval screen.\n *\n * ── The shape of the set: three tenses, and five others ───────────────────\n * Three of the eight say something about the subject at a point in time, and the other five do not:\n *\n * past evidence what the decision was made from (may repeat)\n * present currentState how things stand right now\n * future consequence what approving would change (may repeat)\n *\n * subject what this is about\n * reason what a person wrote\n * policy why it came up for approval at all\n * headline one sentence built from the values\n * reversibility whether approving can be undone\n *\n * Every mis-slotting found so far was the same mistake in that column: currentState under\n * consequence read the present as the future, and evidence under currentState read the past as the\n * present. So when a ninth role is asked for, the first question is whether the value is the\n * subject's past, present or future — if it is, one of those three already holds it. If it is not,\n * whoever asks should be able to say what it is instead. (2026-09-10, architect's ruling)\n *\n * ── Why the keys and the values are spelled the same ──────────────────────\n * type-graphql publishes an enum by its **keys** when the field names the enum type, and by its\n * **values** when the field leaves the type off. One word in a @Field decorator therefore decides\n * what a client receives, and a schema-improving edit far away can flip it. That has already cost\n * this repository twice: ApprovalLineItem.participation arrives as `Reference` and was compared\n * against `reference`, false every time; the catalog page read `NotWorklist` where its selector,\n * i18n key and stage list all said `notworklist`.\n *\n * Writing each key as its own value takes the choice away — either spelling produces the same\n * string. ActivityModelItemInoutType and ActivityModelItemType are written the same way.\n *\n * ── The set is closed, and widening it is the architect's call ────────────\n * Left open as a free string, a convention becomes suffix-appending: every lane invents its own\n * word for the same slot and the screen ends up switching on a dozen spellings, printing the\n * unmatched ones. Adding a role here is an architecture decision, not a lane's. (2026-09-09,\n * architect's ruling)\n */\nexport enum ActivityDecisionRole {\n /** One sentence built from the values: 「폐기 30 EA」, 「3에서 1로 올림」. */\n headline = 'headline',\n /** What the decision is about — one identifier. */\n subject = 'subject',\n /** What the person who raised it wrote. Absent, the screen says so rather than leaving a gap. */\n reason = 'reason',\n /**\n * What changes on approval: 「주문 넷이 뒤로 밀린다」. The only role that may appear more than once.\n *\n * Named consequence rather than effect because TwinCommand.effect already means the opposite in\n * time: { status, at, seen }, what the source reported back after the command was dispatched.\n * That one is an observation and past; this one is a prediction and future. Using a real term for\n * the wrong meaning is worse than coining one, and plant already writes .consequence for exactly\n * this sense in its stop-reason dialog. (2026-09-10, architect's ruling)\n */\n consequence = 'consequence',\n /** Why this came up for approval at all: 「폐기는 100 이상이면 결재」. */\n policy = 'policy',\n /**\n * What is measured to be true right now — about the world, not about the request.\n *\n * The other roles all describe what is being asked for. This one does not, and putting it under\n * consequence made the screen say the opposite of the truth: the twin's current readings would\n * have read as things the approval causes.\n *\n * Not named observed: ops-contract already has observedBy for which observation backs a declared\n * property, and TwinCommand.effect is an observation too, so observed would sit in the most\n * confusable spot between them. (2026-09-10, architect's ruling) plant never needed it because the subject's present state\n * went into the headline (「폐기 30 EA」); the twin's instruction and its current state are two\n * separate things, and holding them side by side is what this screen is for.\n */\n currentState = 'currentState',\n /**\n * Whether approving can be undone. Drawn in the frame rather than in the body.\n *\n * Not a separate role for want of a place — it fits consequence well enough — but for want of\n * weight. Listed among the consequences it reads with the same force as \"four orders get pushed\n * back\", and on\n * the twin actuation, where approving moves equipment, it is the heaviest line on the screen.\n *\n * ── Not the same as rejectionIsFinal ──────────────────────────────────\n * Two \"cannot be undone\" facts attach to opposite actions, and the twin actuation carries both,\n * which is what makes them look like one:\n *\n * reversibility whether **approving** can be undone (this role, read from the input)\n * rejectionIsFinal whether **rejecting** ends the case (a column on Activity)\n *\n * Merging them in a screen sentence tells the reader that rejection can be taken back.\n */\n reversibility = 'reversibility',\n /**\n * What the decision was made from — which inspection, against which spec, decided when.\n *\n * Neither consequence nor currentState: consequence is the opposite direction in time, and under\n * currentState a past finding reads as the present state of the subject. plant's disposition\n * approval kept its own screen for exactly these three values, and it survived the comparison not\n * because a slot could not hold them but because no slot existed — those are different reasons,\n * and only the second one means the set is short.\n *\n * plant's case for it: someone approving a concession in a regulated plant who cannot see which\n * inspection produced the finding is approving without grounds.\n *\n * Repeats, because grounds come in several — there were three.\n */\n evidence = 'evidence'\n}\n\nregisterEnumType(ActivityDecisionRole, {\n name: 'ActivityDecisionRole',\n description: 'the part an input value plays on the approval screen'\n})\n\n/**\n * One value the approver reads, and the part it plays.\n *\n * ── Why this points at input rather than at a model item ──────────────────\n * inputPath names a value inside the instance's input and does not go through the activity's model.\n * That is the whole reason it is a separate array: the values an approver most needs are computed by\n * the registering module and put on the input, not declared as model items — plant's `policy` and\n * its `shift.passes` / `shift.passedBy` are exactly that. A role hung on a model item could not\n * reach them.\n *\n * ── inputPath, not inputKey ───────────────────────────────────────────────\n * ActivitySearchKeyItem.inputKey names one top-level key, because its value goes into a searchable\n * column and a path into a list is not something that can be indexed. This field takes a path,\n * because it points at a value to draw. The two do different work, so they carry different names —\n * one word with two meanings would make the wrong one silent, and a name that does not exist is\n * read as a mistake instead. (2026-09-09, architect's ruling)\n *\n * 'policy' → input.policy\n * 'shift.passes' → input.shift.passes\n *\n * Dots only. No array index — drawing a list is the slot's job, not the path's — and no wildcard. A\n * path that leads nowhere resolves to undefined and is not folded into an empty value: a `reason`\n * that is missing and a `reason` someone left blank are different facts, and the screen draws them\n * differently.\n *\n * The two declarations also address different readers. model declares the boxes a performer fills;\n * this declares what an approver reads. Putting both on one item leaves \"is this an input or\n * something to judge by\" unanswerable.\n *\n * Order matters. plant wrote four of these screens by hand and all four laid the slots out in the\n * same order, which makes the order the screen's hierarchy rather than a preference — so this is an\n * ordered array and not an object keyed by role.\n */\n@ObjectType({ description: 'Entity for ActivityDecisionKeyItem' })\nexport class ActivityDecisionKeyItem {\n @Field(type => ActivityDecisionRole)\n role: ActivityDecisionRole\n\n @Field({ description: \"Dotted path to the value inside the instance's input, e.g. 'shift.passes'\" })\n inputPath: string\n\n @Field({ nullable: true, description: 'Developer-facing note. Never shown on a screen.' })\n name?: string\n\n @Field({ nullable: true, description: 'i18n key for the label. Without it the screen shows no label.' })\n tKey?: string\n\n /**\n * i18n key for the **value**, when the value is a sentence the module would otherwise freeze.\n *\n * ── tKey is the label; this is the sentence ───────────────────────────\n * Two different things were being asked of one field. The screen draws a slot as a label and a\n * value, and they come from different places:\n *\n * label declaredLabel(slot) = t(slot.tKey, { defaultValue: name }) 「설비」 「사유」\n * value fieldValue(slot.value) the frozen sentence\n *\n * Putting a sentence key in `tKey` turns the label into a developer's English name and leaves the\n * frozen Korean where it was — the thing this is meant to remove.\n *\n * ── Why the sentence must not freeze ──────────────────────────────────\n * Eighteen doors build a Korean sentence when the request is raised and `issue()` snapshots it\n * whole. The sentence then stands in the language of whoever raised it, not whoever reads it, and\n * an approver on another language never catches up.\n *\n * Values are right to freeze — an approval is a judgment about facts at a moment, so quantities,\n * dates and counts are the record. The prose describing them is not a fact, and freezing it also\n * loses what it described: maintenance deferral carries `impact.times` as a value but `dueAt`\n * only inside the sentence, so editing that wording would leave no way to say which due date the\n * approval was about. (ADR-0054 ④)\n *\n * ── Computed, never invented ──────────────────────────────────────────\n * `approval.<door>.<inputPath>` — `inputPath` is the one thing unique within a door, which `role`\n * is not (a door repeats `consequence`). Nobody writing a new door picks a key, so no table of\n * keys grows, and a test can count every one of them.\n *\n * Leaving it out is how a slot stays as it is. The frozen sentence remains in `value`, so a\n * consumer that knows nothing about this field — the plain card, the timeline, an export — draws\n * exactly what it drew before, and a missing translation never puts English into a Korean screen.\n */\n @Field({ nullable: true, description: \"i18n key for the value sentence: approval.<door>.<inputPath>\" })\n valueTKey?: string\n\n /**\n * What the sentence puts into its blanks: **name in the sentence → where the value lives.**\n *\n * { what: 'equipmentName', days: 'impact.days' } → t(valueTKey, { what: …, days: … })\n *\n * Every value here is an `inputPath`, and that is the point rather than a convention: a sentence\n * may only be built from values that were snapshotted. Wanting a value with no field to read it\n * from means the field is missing, not that params needs another source — so the rule is a shape\n * a declaration cannot write around, and a test counts it.\n *\n * The names on the left belong to the sentence and only have to match the locale files.\n */\n @Field(type => ScalarObject, { nullable: true, description: 'sentence placeholder → inputPath' })\n params?: { [name: string]: string }\n\n /**\n * Unit to print after the value, when the activity fixes the unit.\n *\n * Declared once, so it holds for every instance of the activity — 「30 건」 works, and a unit that\n * varies per record does not: plant's disposition carries the unit alongside the quantity, so the\n * same activity produces 「3 EA」 and 「12.5 KG」. Writing one of them here would attach the wrong\n * unit to the other records, with nothing to catch it.\n *\n * A unit that varies belongs in the headline sentence the module builds, which is what plant did:\n * 「LOT-A 3 EA 를 폐기합니다」. Leave this empty then.\n *\n * Delete this field if no activity ever turns out to fix a unit. A field nobody writes stays\n * forever and the next person reads a meaning into it. (2026-09-10, architect's ruling)\n */\n @Field({ nullable: true, description: 'Unit to print after the value, when the activity fixes the unit' })\n unit?: string\n}\n"]}
|
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
type DecisionKeyDeclaration = {
|
|
13
13
|
role?: string;
|
|
14
14
|
inputPath?: string;
|
|
15
|
+
valueTKey?: string;
|
|
16
|
+
params?: {
|
|
17
|
+
[name: string]: string;
|
|
18
|
+
};
|
|
15
19
|
};
|
|
16
20
|
/** Roles a screen knows how to draw. Widening this is an architecture decision, not a lane's. */
|
|
17
21
|
export declare const DECISION_ROLES: readonly ["headline", "subject", "reversibility", "reason", "evidence", "currentState", "consequence", "policy"];
|
|
@@ -29,5 +33,33 @@ export declare const DECISION_ROLES: readonly ["headline", "subject", "reversibi
|
|
|
29
33
|
* — an observation, and past — while this is a prediction, and future.
|
|
30
34
|
*/
|
|
31
35
|
export declare const REPEATABLE_ROLES: readonly ["consequence", "evidence"];
|
|
36
|
+
/**
|
|
37
|
+
* The door's part of a value key — the activity's name, kebab-cased.
|
|
38
|
+
*
|
|
39
|
+
* ── Why computed and not declared ─────────────────────────────────────────
|
|
40
|
+
* A declared key is a key table, and a key table is what a computed key exists to avoid: nobody
|
|
41
|
+
* writing a new door picks a key, so none of them can be picked wrong, and a test can count every
|
|
42
|
+
* key that exists rather than every key somebody remembered to register.
|
|
43
|
+
*
|
|
44
|
+
* It is safe to hang this on `name` because `name` cannot move — it is the key the callback is
|
|
45
|
+
* found by, and renaming it makes an approval finish with nothing happening. A key derived from a
|
|
46
|
+
* fixed name is fixed.
|
|
47
|
+
*
|
|
48
|
+
* ── The boundary rule ─────────────────────────────────────────────────────
|
|
49
|
+
* Activity names come in two shapes and this repository has both: 'MES Disposition Approval' and
|
|
50
|
+
* 'BatchRelease'. Splitting only on spaces would leave `batchrelease` beside `mes-disposition-
|
|
51
|
+
* approval`; splitting on every capital would turn 'MES' into `m-e-s`. So the split is between a
|
|
52
|
+
* lowercase or digit and a capital, and between a run of capitals and a capitalised word — the same
|
|
53
|
+
* rule `secretEnvName` uses, written after HOOKSECRET and HOOK_SECRET drifted apart.
|
|
54
|
+
*/
|
|
55
|
+
export declare function doorSlug(activityName: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* The i18n key for one slot's **value sentence** — `approval.<door>.<inputPath>`.
|
|
58
|
+
*
|
|
59
|
+
* `inputPath` and not `role`: a door repeats `consequence`, so two slots in one door would collide
|
|
60
|
+
* on role and the later sentence would silently take the earlier one's key. `inputPath` is the one
|
|
61
|
+
* thing already unique within a door, and it is already written in the declaration.
|
|
62
|
+
*/
|
|
63
|
+
export declare function decisionValueKey(activityName: string, inputPath: string): string;
|
|
32
64
|
export declare function assertDecisionKeysAreUsable(decisionKeys: DecisionKeyDeclaration[] | undefined, activityName?: string): void;
|
|
33
65
|
export {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.REPEATABLE_ROLES = exports.DECISION_ROLES = void 0;
|
|
4
|
+
exports.doorSlug = doorSlug;
|
|
5
|
+
exports.decisionValueKey = decisionValueKey;
|
|
4
6
|
exports.assertDecisionKeysAreUsable = assertDecisionKeysAreUsable;
|
|
5
7
|
/** Roles a screen knows how to draw. Widening this is an architecture decision, not a lane's. */
|
|
6
8
|
exports.DECISION_ROLES = [
|
|
@@ -27,6 +29,43 @@ exports.DECISION_ROLES = [
|
|
|
27
29
|
* — an observation, and past — while this is a prediction, and future.
|
|
28
30
|
*/
|
|
29
31
|
exports.REPEATABLE_ROLES = ['consequence', 'evidence'];
|
|
32
|
+
/**
|
|
33
|
+
* The door's part of a value key — the activity's name, kebab-cased.
|
|
34
|
+
*
|
|
35
|
+
* ── Why computed and not declared ─────────────────────────────────────────
|
|
36
|
+
* A declared key is a key table, and a key table is what a computed key exists to avoid: nobody
|
|
37
|
+
* writing a new door picks a key, so none of them can be picked wrong, and a test can count every
|
|
38
|
+
* key that exists rather than every key somebody remembered to register.
|
|
39
|
+
*
|
|
40
|
+
* It is safe to hang this on `name` because `name` cannot move — it is the key the callback is
|
|
41
|
+
* found by, and renaming it makes an approval finish with nothing happening. A key derived from a
|
|
42
|
+
* fixed name is fixed.
|
|
43
|
+
*
|
|
44
|
+
* ── The boundary rule ─────────────────────────────────────────────────────
|
|
45
|
+
* Activity names come in two shapes and this repository has both: 'MES Disposition Approval' and
|
|
46
|
+
* 'BatchRelease'. Splitting only on spaces would leave `batchrelease` beside `mes-disposition-
|
|
47
|
+
* approval`; splitting on every capital would turn 'MES' into `m-e-s`. So the split is between a
|
|
48
|
+
* lowercase or digit and a capital, and between a run of capitals and a capitalised word — the same
|
|
49
|
+
* rule `secretEnvName` uses, written after HOOKSECRET and HOOK_SECRET drifted apart.
|
|
50
|
+
*/
|
|
51
|
+
function doorSlug(activityName) {
|
|
52
|
+
return String(activityName ?? '')
|
|
53
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
54
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
|
|
55
|
+
.replace(/[^A-Za-z0-9]+/g, '-')
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.replace(/^-+|-+$/g, '');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The i18n key for one slot's **value sentence** — `approval.<door>.<inputPath>`.
|
|
61
|
+
*
|
|
62
|
+
* `inputPath` and not `role`: a door repeats `consequence`, so two slots in one door would collide
|
|
63
|
+
* on role and the later sentence would silently take the earlier one's key. `inputPath` is the one
|
|
64
|
+
* thing already unique within a door, and it is already written in the declaration.
|
|
65
|
+
*/
|
|
66
|
+
function decisionValueKey(activityName, inputPath) {
|
|
67
|
+
return `approval.${doorSlug(activityName)}.${inputPath}`;
|
|
68
|
+
}
|
|
30
69
|
function assertDecisionKeysAreUsable(decisionKeys, activityName) {
|
|
31
70
|
if (!decisionKeys?.length) {
|
|
32
71
|
return;
|
|
@@ -63,5 +102,47 @@ function assertDecisionKeysAreUsable(decisionKeys, activityName) {
|
|
|
63
102
|
`change several things at once, and grounds come in several. The screen would draw one of the ` +
|
|
64
103
|
`others and could not say which.`);
|
|
65
104
|
}
|
|
105
|
+
/*
|
|
106
|
+
* A sentence may only be built from values that were snapshotted, so every params value is an
|
|
107
|
+
* inputPath and reads by the same rules. Wanting a value with no field behind it means the field
|
|
108
|
+
* is missing, not that params needs a second source — the two hold each other up: without the
|
|
109
|
+
* value the sentence cannot be rebuilt, and a value a sentence uses is guaranteed to stay.
|
|
110
|
+
*
|
|
111
|
+
* Only the right side has a rule. The names on the left belong to the sentence and are the
|
|
112
|
+
* locale file's business.
|
|
113
|
+
*/
|
|
114
|
+
const badParams = [];
|
|
115
|
+
for (const key of decisionKeys) {
|
|
116
|
+
for (const [name, path] of Object.entries(key?.params || {})) {
|
|
117
|
+
const said = String(path ?? '');
|
|
118
|
+
if (!said.trim()) {
|
|
119
|
+
badParams.push(`"${name}" (empty)`);
|
|
120
|
+
}
|
|
121
|
+
else if (/[[\]*]/.test(said)) {
|
|
122
|
+
badParams.push(`"${name}": "${said}"`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (badParams.length) {
|
|
127
|
+
throw new Error(`${named} declares sentence params that are not plain dotted paths into the input: ` +
|
|
128
|
+
`${badParams.join(', ')}. Every params value is an inputPath — a sentence is built only from ` +
|
|
129
|
+
`values the request froze, so a value with nowhere to read it from is a missing field.`);
|
|
130
|
+
}
|
|
131
|
+
if (!activityName) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
/*
|
|
135
|
+
* The key is computed, so a declared one that disagrees is a typo that would draw `approval.…` on
|
|
136
|
+
* the card. Checking it here rather than trusting the writer is the whole reason it is computed.
|
|
137
|
+
*/
|
|
138
|
+
const wrongKey = decisionKeys.filter(key => key?.valueTKey && key.valueTKey !== decisionValueKey(activityName, String(key?.inputPath ?? '')));
|
|
139
|
+
if (wrongKey.length) {
|
|
140
|
+
throw new Error(`${named} declares value keys that are not the computed ones: ` +
|
|
141
|
+
`${wrongKey
|
|
142
|
+
.map(key => `"${key?.valueTKey}" (expected "${decisionValueKey(activityName, String(key?.inputPath ?? ''))}")`)
|
|
143
|
+
.join(', ')}. ` +
|
|
144
|
+
`The key is approval.<door>.<inputPath> and nobody picks it — a key nobody can pick is a key ` +
|
|
145
|
+
`nobody can pick wrong.`);
|
|
146
|
+
}
|
|
66
147
|
}
|
|
67
148
|
//# sourceMappingURL=decision-key-declaration.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decision-key-declaration.js","sourceRoot":"","sources":["../../../server/service/activity/decision-key-declaration.ts"],"names":[],"mappings":";;;AAwCA,kEA2DC;AAtFD,iGAAiG;AACpF,QAAA,cAAc,GAAG;IAC5B,UAAU;IACV,SAAS;IACT,eAAe;IACf,QAAQ;IACR,UAAU;IACV,cAAc;IACd,aAAa;IACb,QAAQ;CACA,CAAA;AAEV;;;;;;;;;;;;GAYG;AACU,QAAA,gBAAgB,GAAG,CAAC,aAAa,EAAE,UAAU,CAAU,CAAA;AAEpE,SAAgB,2BAA2B,CACzC,YAAkD,EAClD,YAAqB;IAErB,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;QAC1B,OAAM;IACR,CAAC;IAED,MAAM,KAAK,GAAG,YAAY,CAAC,CAAC,CAAC,aAAa,YAAY,GAAG,CAAC,CAAC,CAAC,eAAe,CAAA;IAE3E,MAAM,OAAO,GAAG,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAE,sBAAoC,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;IAEpH,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,yEAAyE;YAC/E,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,GAAG,EAAE,IAAI,IAAI,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;YAC5D,mBAAmB,sBAAc,CAAC,IAAI,CAAC,IAAI,CAAC,0CAA0C,CACzF,CAAA;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,EAAE,SAAS,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;IAEjF,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,aAAa,QAAQ,CAAC,MAAM,sCAAsC;YACxE,mFAAmF,CACtF,CAAA;IACH,CAAC;IAED;;;;OAIG;IACH,MAAM,QAAQ,GAAG,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,SAAS,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;IAExF,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,sEAAsE;YAC5E,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,GAAG,EAAE,SAAS,IAAI,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;YAClE,6FAA6F;YAC7F,kBAAkB,CACrB,CAAA;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,sBAAc,CAAC,MAAM,CACpC,IAAI,CAAC,EAAE,CACL,CAAE,wBAAsC,CAAC,QAAQ,CAAC,IAAI,CAAC;QACvD,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAC5D,CAAA;IAED,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;QACpB,MAAM,IAAI,KAAK,CACb,GAAG,KAAK,4BAA4B,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,kBAAkB;YACzE,QAAQ,wBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,gCAAgC;YAC/F,+FAA+F;YAC/F,iCAAiC,CACpC,CAAA;IACH,CAAC;AACH,CAAC","sourcesContent":["/**\n * Refuses a decisionKeys declaration that cannot draw.\n *\n * Same reasoning as search keys: a declaration that does not work should fail where it arrives, not\n * where someone opens an approval. The difference is what \"not working\" means here — nothing is\n * dropped by a save, so the failure would be a screen that quietly draws less than it was told to.\n *\n * The shape is spelled out rather than imported. ActivityDecisionKeyItem carries type-graphql\n * decorators and the test runner strips types without reading them, so importing the declared class\n * pulls type-graphql in and the test file fails to load.\n */\ntype DecisionKeyDeclaration = { role?: string; inputPath?: string }\n\n/** Roles a screen knows how to draw. Widening this is an architecture decision, not a lane's. */\nexport const DECISION_ROLES = [\n 'headline',\n 'subject',\n 'reversibility',\n 'reason',\n 'evidence',\n 'currentState',\n 'consequence',\n 'policy'\n] as const\n\n/**\n * The roles that may appear more than once.\n *\n * An approval can change several things at once — plant's priority change pushes some orders back\n * and moves others ahead — and those are separate consequences, not one merged sentence. Grounds\n * come in several the same way: which inspection, against which spec, decided when.\n *\n * Every other role names a single value. Two headlines would leave the screen picking one with\n * nothing to say why.\n *\n * Named consequence, not effect: TwinCommand.effect is what the source reported back after dispatch\n * — an observation, and past — while this is a prediction, and future.\n */\nexport const REPEATABLE_ROLES = ['consequence', 'evidence'] as const\n\nexport function assertDecisionKeysAreUsable(\n decisionKeys: DecisionKeyDeclaration[] | undefined,\n activityName?: string\n): void {\n if (!decisionKeys?.length) {\n return\n }\n\n const named = activityName ? `activity \"${activityName}\"` : 'this activity'\n\n const unknown = decisionKeys.filter(key => !(DECISION_ROLES as readonly string[]).includes(String(key?.role ?? '')))\n\n if (unknown.length) {\n throw new Error(\n `${named} declares decision keys with a role the approval screen does not know: ` +\n `${unknown.map(key => `\"${key?.role ?? ''}\"`).join(', ')}. ` +\n `Known roles are ${DECISION_ROLES.join(', ')}. A role outside that set draws nothing.`\n )\n }\n\n const pathless = decisionKeys.filter(key => !String(key?.inputPath ?? '').trim())\n\n if (pathless.length) {\n throw new Error(\n `${named} declares ${pathless.length} decision key(s) with no inputPath. ` +\n `A decision key names the value it draws; without a path there is nothing to read.`\n )\n }\n\n /*\n * An index or a wildcard resolves to nothing at draw time and the slot would report \"the path led\n * nowhere\" — which reads as a payload that disagrees with the declaration, when the truth is that\n * the declaration used a path language that does not exist. Refusing here says which it was.\n */\n const notPaths = decisionKeys.filter(key => /[[\\]*]/.test(String(key?.inputPath ?? '')))\n\n if (notPaths.length) {\n throw new Error(\n `${named} declares decision keys whose inputPath is not a plain dotted path: ` +\n `${notPaths.map(key => `\"${key?.inputPath ?? ''}\"`).join(', ')}. ` +\n `Dots only — no array index and no wildcard. Point at the list itself; drawing its items is ` +\n `the slot's work.`\n )\n }\n\n const repeated = DECISION_ROLES.filter(\n role =>\n !(REPEATABLE_ROLES as readonly string[]).includes(role) &&\n decisionKeys.filter(key => key?.role === role).length > 1\n )\n\n if (repeated.length) {\n throw new Error(\n `${named} declares more than one \"${repeated.join('\", \"')}\" decision key. ` +\n `Only ${REPEATABLE_ROLES.map(role => `\"${role}\"`).join(' and ')} may repeat — an approval can ` +\n `change several things at once, and grounds come in several. The screen would draw one of the ` +\n `others and could not say which.`\n )\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"decision-key-declaration.js","sourceRoot":"","sources":["../../../server/service/activity/decision-key-declaration.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Refuses a decisionKeys declaration that cannot draw.\n *\n * Same reasoning as search keys: a declaration that does not work should fail where it arrives, not\n * where someone opens an approval. The difference is what \"not working\" means here — nothing is\n * dropped by a save, so the failure would be a screen that quietly draws less than it was told to.\n *\n * The shape is spelled out rather than imported. ActivityDecisionKeyItem carries type-graphql\n * decorators and the test runner strips types without reading them, so importing the declared class\n * pulls type-graphql in and the test file fails to load.\n */\ntype DecisionKeyDeclaration = {\n role?: string\n inputPath?: string\n valueTKey?: string\n params?: { [name: string]: string }\n}\n\n/** Roles a screen knows how to draw. Widening this is an architecture decision, not a lane's. */\nexport const DECISION_ROLES = [\n 'headline',\n 'subject',\n 'reversibility',\n 'reason',\n 'evidence',\n 'currentState',\n 'consequence',\n 'policy'\n] as const\n\n/**\n * The roles that may appear more than once.\n *\n * An approval can change several things at once — plant's priority change pushes some orders back\n * and moves others ahead — and those are separate consequences, not one merged sentence. Grounds\n * come in several the same way: which inspection, against which spec, decided when.\n *\n * Every other role names a single value. Two headlines would leave the screen picking one with\n * nothing to say why.\n *\n * Named consequence, not effect: TwinCommand.effect is what the source reported back after dispatch\n * — an observation, and past — while this is a prediction, and future.\n */\nexport const REPEATABLE_ROLES = ['consequence', 'evidence'] as const\n\n/**\n * The door's part of a value key — the activity's name, kebab-cased.\n *\n * ── Why computed and not declared ─────────────────────────────────────────\n * A declared key is a key table, and a key table is what a computed key exists to avoid: nobody\n * writing a new door picks a key, so none of them can be picked wrong, and a test can count every\n * key that exists rather than every key somebody remembered to register.\n *\n * It is safe to hang this on `name` because `name` cannot move — it is the key the callback is\n * found by, and renaming it makes an approval finish with nothing happening. A key derived from a\n * fixed name is fixed.\n *\n * ── The boundary rule ─────────────────────────────────────────────────────\n * Activity names come in two shapes and this repository has both: 'MES Disposition Approval' and\n * 'BatchRelease'. Splitting only on spaces would leave `batchrelease` beside `mes-disposition-\n * approval`; splitting on every capital would turn 'MES' into `m-e-s`. So the split is between a\n * lowercase or digit and a capital, and between a run of capitals and a capitalised word — the same\n * rule `secretEnvName` uses, written after HOOKSECRET and HOOK_SECRET drifted apart.\n */\nexport function doorSlug(activityName: string): string {\n return String(activityName ?? '')\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')\n .replace(/[^A-Za-z0-9]+/g, '-')\n .toLowerCase()\n .replace(/^-+|-+$/g, '')\n}\n\n/**\n * The i18n key for one slot's **value sentence** — `approval.<door>.<inputPath>`.\n *\n * `inputPath` and not `role`: a door repeats `consequence`, so two slots in one door would collide\n * on role and the later sentence would silently take the earlier one's key. `inputPath` is the one\n * thing already unique within a door, and it is already written in the declaration.\n */\nexport function decisionValueKey(activityName: string, inputPath: string): string {\n return `approval.${doorSlug(activityName)}.${inputPath}`\n}\n\nexport function assertDecisionKeysAreUsable(\n decisionKeys: DecisionKeyDeclaration[] | undefined,\n activityName?: string\n): void {\n if (!decisionKeys?.length) {\n return\n }\n\n const named = activityName ? `activity \"${activityName}\"` : 'this activity'\n\n const unknown = decisionKeys.filter(key => !(DECISION_ROLES as readonly string[]).includes(String(key?.role ?? '')))\n\n if (unknown.length) {\n throw new Error(\n `${named} declares decision keys with a role the approval screen does not know: ` +\n `${unknown.map(key => `\"${key?.role ?? ''}\"`).join(', ')}. ` +\n `Known roles are ${DECISION_ROLES.join(', ')}. A role outside that set draws nothing.`\n )\n }\n\n const pathless = decisionKeys.filter(key => !String(key?.inputPath ?? '').trim())\n\n if (pathless.length) {\n throw new Error(\n `${named} declares ${pathless.length} decision key(s) with no inputPath. ` +\n `A decision key names the value it draws; without a path there is nothing to read.`\n )\n }\n\n /*\n * An index or a wildcard resolves to nothing at draw time and the slot would report \"the path led\n * nowhere\" — which reads as a payload that disagrees with the declaration, when the truth is that\n * the declaration used a path language that does not exist. Refusing here says which it was.\n */\n const notPaths = decisionKeys.filter(key => /[[\\]*]/.test(String(key?.inputPath ?? '')))\n\n if (notPaths.length) {\n throw new Error(\n `${named} declares decision keys whose inputPath is not a plain dotted path: ` +\n `${notPaths.map(key => `\"${key?.inputPath ?? ''}\"`).join(', ')}. ` +\n `Dots only — no array index and no wildcard. Point at the list itself; drawing its items is ` +\n `the slot's work.`\n )\n }\n\n const repeated = DECISION_ROLES.filter(\n role =>\n !(REPEATABLE_ROLES as readonly string[]).includes(role) &&\n decisionKeys.filter(key => key?.role === role).length > 1\n )\n\n if (repeated.length) {\n throw new Error(\n `${named} declares more than one \"${repeated.join('\", \"')}\" decision key. ` +\n `Only ${REPEATABLE_ROLES.map(role => `\"${role}\"`).join(' and ')} may repeat — an approval can ` +\n `change several things at once, and grounds come in several. The screen would draw one of the ` +\n `others and could not say which.`\n )\n }\n\n /*\n * A sentence may only be built from values that were snapshotted, so every params value is an\n * inputPath and reads by the same rules. Wanting a value with no field behind it means the field\n * is missing, not that params needs a second source — the two hold each other up: without the\n * value the sentence cannot be rebuilt, and a value a sentence uses is guaranteed to stay.\n *\n * Only the right side has a rule. The names on the left belong to the sentence and are the\n * locale file's business.\n */\n const badParams: string[] = []\n\n for (const key of decisionKeys) {\n for (const [name, path] of Object.entries(key?.params || {})) {\n const said = String(path ?? '')\n\n if (!said.trim()) {\n badParams.push(`\"${name}\" (empty)`)\n } else if (/[[\\]*]/.test(said)) {\n badParams.push(`\"${name}\": \"${said}\"`)\n }\n }\n }\n\n if (badParams.length) {\n throw new Error(\n `${named} declares sentence params that are not plain dotted paths into the input: ` +\n `${badParams.join(', ')}. Every params value is an inputPath — a sentence is built only from ` +\n `values the request froze, so a value with nowhere to read it from is a missing field.`\n )\n }\n\n if (!activityName) {\n return\n }\n\n /*\n * The key is computed, so a declared one that disagrees is a typo that would draw `approval.…` on\n * the card. Checking it here rather than trusting the writer is the whole reason it is computed.\n */\n const wrongKey = decisionKeys.filter(\n key => key?.valueTKey && key.valueTKey !== decisionValueKey(activityName, String(key?.inputPath ?? ''))\n )\n\n if (wrongKey.length) {\n throw new Error(\n `${named} declares value keys that are not the computed ones: ` +\n `${wrongKey\n .map(key => `\"${key?.valueTKey}\" (expected \"${decisionValueKey(activityName, String(key?.inputPath ?? ''))}\")`)\n .join(', ')}. ` +\n `The key is approval.<door>.<inputPath> and nobody picks it — a key nobody can pick is a key ` +\n `nobody can pick wrong.`\n )\n }\n}\n"]}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Brackets } from 'typeorm';
|
|
2
2
|
/**
|
|
3
|
-
* The approvals waiting on me — one definition,
|
|
3
|
+
* The approvals waiting on me — one definition, three readers.
|
|
4
4
|
*
|
|
5
|
-
* 내 결재함 asks this, and so does the
|
|
6
|
-
* to get wrong separately:
|
|
5
|
+
* 내 결재함 asks this, the calendar asks it, and so does the badge on the collapsed menu. All three
|
|
6
|
+
* have to agree on three things that are easy to get wrong separately:
|
|
7
7
|
*
|
|
8
8
|
* - **참조는 승인 대기가 아니다.** Asking a reference to approve destroys what a reference is. An
|
|
9
9
|
* empty column is an approval: rows created before the column existed have nothing there.
|
|
@@ -12,8 +12,20 @@ import { Brackets } from 'typeorm';
|
|
|
12
12
|
* everyone holding that role, forever.
|
|
13
13
|
* - **끝난 것의 정의는 한 자리에 있다** (TERMINATED_JUDGMENTS). Counted again here, a new judgment
|
|
14
14
|
* value would land in one reader and not the other.
|
|
15
|
+
* - **내가 올린 것은 내 결재가 아니다** (ADR-0052 ②). 누르는 자리(`checkAuthority`)가 거절하는 것을
|
|
16
|
+
* 여기서도 빼야 한다 — 갈리면 목록에 떴는데 누르면 거절된다. 두 표현은 `own-request.ts` 에 나란히 있다.
|
|
15
17
|
*
|
|
16
|
-
*
|
|
18
|
+
* ── What a caller has to bring ───────────────────────────────────────────────
|
|
19
|
+
* `ap` (ActivityApproval) as the alias, and **`at` and `ai` joined** — the thread and its instance.
|
|
20
|
+
* The clause reads only `ap` today. It is about to read the instance as well, to answer whether the
|
|
21
|
+
* person judging is the one who raised it (ADR-0052 ②), and a caller that has not joined does not
|
|
22
|
+
* return a wrong number — it throws at query time.
|
|
23
|
+
*
|
|
24
|
+
* Two of the three readers already joined, for their own reasons: the inbox list for sorting, the
|
|
25
|
+
* calendar to read the thread it draws. The badge joined nothing, because counting rows needs no
|
|
26
|
+
* columns. So the clause would have started working for two readers and started failing for the
|
|
27
|
+
* third, and the third is the one nobody opens a screen to check. The badge joins too, and
|
|
28
|
+
* `joined-for-the-shared-clause.test.ts` counts that every reader does.
|
|
17
29
|
*/
|
|
18
30
|
export declare function myPendingApprovalsWhere({ userId, roles }: {
|
|
19
31
|
userId: string;
|