@things-factory/station 10.1.226 → 10.1.228

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/README.md +3 -3
  2. package/dist-client/tsconfig.tsbuildinfo +1 -1
  3. package/dist-server/activities/activity-station-screen-release.d.ts +3 -3
  4. package/dist-server/activities/activity-station-screen-release.js +7 -7
  5. package/dist-server/activities/activity-station-screen-release.js.map +1 -1
  6. package/dist-server/service/station/screen-model-digest.d.ts +3 -3
  7. package/dist-server/service/station/screen-model-digest.js +3 -3
  8. package/dist-server/service/station/screen-model-digest.js.map +1 -1
  9. package/dist-server/service/station/station-assignment-choice.d.ts +1 -1
  10. package/dist-server/service/station/station-assignment-choice.js.map +1 -1
  11. package/dist-server/service/station/station-assignment-resolver.d.ts +2 -2
  12. package/dist-server/service/station/station-assignment-resolver.js.map +1 -1
  13. package/dist-server/service/station/station-screen-assignment.js.map +1 -1
  14. package/dist-server/service/station/station-screen-control-withdrawal-resolver.d.ts +1 -1
  15. package/dist-server/service/station/station-screen-control-withdrawal-resolver.js +2 -2
  16. package/dist-server/service/station/station-screen-control-withdrawal-resolver.js.map +1 -1
  17. package/dist-server/service/station/station-screen-control-withdrawal.d.ts +1 -1
  18. package/dist-server/service/station/station-screen-control-withdrawal.js +1 -1
  19. package/dist-server/service/station/station-screen-control-withdrawal.js.map +1 -1
  20. package/dist-server/service/station/station-screen-release-record.d.ts +5 -5
  21. package/dist-server/service/station/station-screen-release-record.js +3 -3
  22. package/dist-server/service/station/station-screen-release-record.js.map +1 -1
  23. package/dist-server/service/station/station-screen-spec-resolver.js +2 -2
  24. package/dist-server/service/station/station-screen-spec-resolver.js.map +1 -1
  25. package/dist-server/tsconfig.tsbuildinfo +1 -1
  26. package/package.json +8 -8
@@ -1 +1 @@
1
- {"version":3,"file":"station-screen-control-withdrawal-resolver.js","sourceRoot":"","sources":["../../../server/service/station/station-screen-control-withdrawal-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Mutation, Resolver, UseMiddleware } from 'type-graphql'\nimport { In } from 'typeorm'\n\nimport { getRepository, SpeakRefusals } from '@things-factory/shell'\nimport { Board } from '@things-factory/board-service'\nimport { ActivityInstance, ActivityInstanceStatus, issue } from '@things-factory/worklist'\n\nimport { STATION_SCREEN_CONTROL_WITHDRAWAL_ACTIVITY } from '../../activities/activity-station-screen-control-withdrawal'\nimport { STATION_SCREEN_RELEASE_ACTIVITY } from '../../activities/activity-station-screen-release'\nimport { activityNamed, approvalLineStanding } from '@things-factory/worklist'\nimport type { ApprovalLineStanding } from '@things-factory/worklist'\nimport { assertWithdrawalRaisable, controlWithdrawalRequestName, withdrawalReversibility, stationScreenSpecBefore } from './station-screen-control-withdrawal'\nimport type { ControlWithdrawalAction } from './station-screen-control-withdrawal'\nimport { StationScreenSpec } from './station-screen-spec'\n\n/** 아직 끝나지 않은 상태들 — 끝난 것과 접힌 것만 뺀다(§`disposition-pending`). */\nconst OPEN_STATES = [\n ActivityInstanceStatus.Draft,\n ActivityInstanceStatus.Issued,\n ActivityInstanceStatus.Unassigned,\n ActivityInstanceStatus.Assigned,\n ActivityInstanceStatus.Started,\n ActivityInstanceStatus.Pending\n]\n\n/**\n * **통제를 켜고 끄는 결재선** — 화면 발행 승인의 결재선 하나다.\n *\n * 켤 때(`saveStationScreenSpec`)와 끌 때(`requestStationScreenControlWithdrawal`)가 **같은 함수로**\n * 판정한다. 둘이 따로 읽으면, 켤 수는 없는데 끌 수는 있는(또는 그 반대의) 공장이 생긴다.\n */\nexport async function controlLineStanding(\n context: ResolverContext\n): Promise<{ activity?: { id?: string; approvalLine?: unknown[] }; standing: ApprovalLineStanding }> {\n const activity = await activityNamed(STATION_SCREEN_RELEASE_ACTIVITY, context)\n\n return { activity, standing: await approvalLineStanding(activity, context) }\n}\n\n/**\n * 통제 문서를 **통제 밖으로 내보내자고 결재에 올린다.**\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── 올리기만 한다. spec 은 그대로다 ────────────────────────────────────────\n * 승인 전까지 이 화면은 여전히 통제 문서이고, 서명 없는 판은 그동안에도 발행 관문에서 거절된다\n * (아키텍트 조건 ①). 여기서 spec 을 쓰면 「올렸다」가 곧 「껐다」가 된다 — 이 파일에는 spec 을\n * 쓰는 줄이 없어야 한다(`station-screen-control-withdrawal.test.ts` 가 센다).\n *\n * ── 무엇을 거둘지는 올릴 때 굳힌다 ─────────────────────────────────────────\n * spec 의 **id** 와 방식(끄기 · 지우기)을 싣는다. 승인 콜백은 그것만 한다(조건 ②).\n *\n * ── 결재선은 발행 승인의 것을 **올리는 순간** 읽어 싣는다 ─────────────────\n * 이 문 자신의 결재선 칸은 쓰지 않는다. 같은 결재선을 값으로 두 번 두면 갈린다.\n *\n * ── 이 문을 여는 자리 ─────────────────────────────────────────────────────\n * 화면을 만드는 권한(`station:mutation`)을 든 자리가 올리고, 발행 승인의 결재선(품질)이 정한다 —\n * 작성자는 승인하지 않는다(ADR-0047 ⑦).\n * ═══════════════════════════════════════════════════════════════════════════\n */\n@Resolver()\n@UseMiddleware(SpeakRefusals)\nexport class StationScreenControlWithdrawalResolver {\n @Directive('@privilege(category: \"station\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => String, { description: 'Ask for approval to take one station screen out of document control' })\n async requestStationScreenControlWithdrawal(\n @Arg('boardId') boardId: string,\n @Arg('reason') reason: string,\n @Arg('remove', { nullable: true, description: 'delete the station spec instead of only turning control off' })\n remove: boolean | undefined,\n @Ctx() context: ResolverContext\n ): Promise<string> {\n const { domain, tx } = context.state\n const scope = { domain: { id: domain.id } }\n\n const spec = await tx.getRepository(StationScreenSpec).findOne({ where: { ...scope, boardId } })\n const name = controlWithdrawalRequestName(boardId)\n\n const waiting = await tx.getRepository(ActivityInstance).findOne({\n where: {\n ...scope,\n name,\n activity: { name: STATION_SCREEN_CONTROL_WITHDRAWAL_ACTIVITY },\n state: In(OPEN_STATES)\n },\n relations: ['activity']\n })\n\n const activity = await activityNamed(STATION_SCREEN_CONTROL_WITHDRAWAL_ACTIVITY, context)\n const control = await controlLineStanding(context)\n\n /* 읽기만 여기서 하고 **판단은 한 곳에서** 한다 — 조건이 여기 있으면 거동 시험이 못 본다(§`assertWithdrawalRaisable`). */\n assertWithdrawalRaisable({ reason, spec, waiting: !!waiting, installed: !!activity, line: control.standing })\n\n /* 이름은 **서버가 읽는다.** 무엇을 승인했는지 묻는 자리라 올린 쪽이 보낸 이름을 믿지 않는다. */\n const board = await getRepository(Board).findOne({ where: { id: boardId } })\n const screenName = board?.name ?? boardId\n const action: ControlWithdrawalAction = remove ? 'delete' : 'turn-off'\n\n const instance = await issue(\n {\n name,\n description: `${screenName} — ${reason.trim()}`,\n activityId: activity!.id,\n /* 이 문의 결재선이 아니라 **발행 승인의 결재선**이다(머리말). */\n approvalLine: control.activity?.approvalLine,\n requireApproval: true,\n threadsMin: 1,\n input: {\n boardId,\n headline:\n action === 'delete'\n ? `${screenName} 의 화면 설정을 지우고 통제에서 뺍니다`\n : `${screenName} 를 통제 문서에서 뺍니다`,\n consequences: {\n after: '승인하면 이 화면은 그 뒤로 결재 없이 발행됩니다. 작업자가 보는 판이 서명 없이 바뀔 수 있습니다.'\n },\n /* 방식마다 무게가 다르다 — 끄는 것은 다시 켤 수 있고, 지우는 것은 못 되돌린다. 사유(caveat)와 함께 싣는다. */\n reversibility: withdrawalReversibility(action) as { reversible: boolean; caveat: string },\n /* The settings as they were, so a deletion can be set up again from this record (ADR-0064 ③). */\n before: stationScreenSpecBefore(spec!),\n withdrawal: {\n name,\n /* 승인 때 **이 id** 만 다룬다 — 그 사이 지우고 새로 만든 설정은 결재자가 본 것이 아니다. */\n specId: spec!.id,\n boardId,\n screenName,\n action,\n reason: reason.trim(),\n requestedAt: new Date().toISOString()\n }\n }\n } as any,\n context\n )\n\n return instance.id\n }\n}\n"]}
1
+ {"version":3,"file":"station-screen-control-withdrawal-resolver.js","sourceRoot":"","sources":["../../../server/service/station/station-screen-control-withdrawal-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Mutation, Resolver, UseMiddleware } from 'type-graphql'\nimport { In } from 'typeorm'\n\nimport { getRepository, SpeakRefusals } from '@things-factory/shell'\nimport { Board } from '@things-factory/board-service'\nimport { ActivityInstance, ActivityInstanceStatus, issue } from '@things-factory/worklist'\n\nimport { STATION_SCREEN_CONTROL_WITHDRAWAL_ACTIVITY } from '../../activities/activity-station-screen-control-withdrawal'\nimport { STATION_SCREEN_RELEASE_ACTIVITY } from '../../activities/activity-station-screen-release'\nimport { activityNamed, approvalLineStanding } from '@things-factory/worklist'\nimport type { ApprovalLineStanding } from '@things-factory/worklist'\nimport { assertWithdrawalRaisable, controlWithdrawalRequestName, withdrawalReversibility, stationScreenSpecBefore } from './station-screen-control-withdrawal'\nimport type { ControlWithdrawalAction } from './station-screen-control-withdrawal'\nimport { StationScreenSpec } from './station-screen-spec'\n\n/** 아직 끝나지 않은 상태들 — 끝난 것과 접힌 것만 뺀다(§`disposition-pending`). */\nconst OPEN_STATES = [\n ActivityInstanceStatus.Draft,\n ActivityInstanceStatus.Issued,\n ActivityInstanceStatus.Unassigned,\n ActivityInstanceStatus.Assigned,\n ActivityInstanceStatus.Started,\n ActivityInstanceStatus.Pending\n]\n\n/**\n * **통제를 켜고 끄는 결재선** — 화면 발행 승인의 결재선 하나다.\n *\n * 켤 때(`saveStationScreenSpec`)와 끌 때(`requestStationScreenControlWithdrawal`)가 **같은 함수로**\n * 판정한다. 둘이 따로 읽으면, 켤 수는 없는데 끌 수는 있는(또는 그 반대의) 공장이 생긴다.\n */\nexport async function controlLineStanding(\n context: ResolverContext\n): Promise<{ activity?: { id?: string; approvalLine?: unknown[] }; standing: ApprovalLineStanding }> {\n const activity = await activityNamed(STATION_SCREEN_RELEASE_ACTIVITY, context)\n\n return { activity, standing: await approvalLineStanding(activity, context) }\n}\n\n/**\n * 통제 문서를 **통제 밖으로 내보내자고 결재에 올린다.**\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── 올리기만 한다. spec 은 그대로다 ────────────────────────────────────────\n * 승인 전까지 이 화면은 여전히 통제 문서이고, 서명 없는 버전은 그동안에도 발행 관문에서 거절된다\n * (아키텍트 조건 ①). 여기서 spec 을 쓰면 「올렸다」가 곧 「껐다」가 된다 — 이 파일에는 spec 을\n * 쓰는 줄이 없어야 한다(`station-screen-control-withdrawal.test.ts` 가 센다).\n *\n * ── 무엇을 거둘지는 올릴 때 굳힌다 ─────────────────────────────────────────\n * spec 의 **id** 와 방식(끄기 · 지우기)을 싣는다. 승인 콜백은 그것만 한다(조건 ②).\n *\n * ── 결재선은 발행 승인의 것을 **올리는 순간** 읽어 싣는다 ─────────────────\n * 이 문 자신의 결재선 칸은 쓰지 않는다. 같은 결재선을 값으로 두 번 두면 갈린다.\n *\n * ── 이 문을 여는 자리 ─────────────────────────────────────────────────────\n * 화면을 만드는 권한(`station:mutation`)을 든 자리가 올리고, 발행 승인의 결재선(품질)이 정한다 —\n * 작성자는 승인하지 않는다(ADR-0047 ⑦).\n * ═══════════════════════════════════════════════════════════════════════════\n */\n@Resolver()\n@UseMiddleware(SpeakRefusals)\nexport class StationScreenControlWithdrawalResolver {\n @Directive('@privilege(category: \"station\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => String, { description: 'Ask for approval to take one station screen out of document control' })\n async requestStationScreenControlWithdrawal(\n @Arg('boardId') boardId: string,\n @Arg('reason') reason: string,\n @Arg('remove', { nullable: true, description: 'delete the station spec instead of only turning control off' })\n remove: boolean | undefined,\n @Ctx() context: ResolverContext\n ): Promise<string> {\n const { domain, tx } = context.state\n const scope = { domain: { id: domain.id } }\n\n const spec = await tx.getRepository(StationScreenSpec).findOne({ where: { ...scope, boardId } })\n const name = controlWithdrawalRequestName(boardId)\n\n const waiting = await tx.getRepository(ActivityInstance).findOne({\n where: {\n ...scope,\n name,\n activity: { name: STATION_SCREEN_CONTROL_WITHDRAWAL_ACTIVITY },\n state: In(OPEN_STATES)\n },\n relations: ['activity']\n })\n\n const activity = await activityNamed(STATION_SCREEN_CONTROL_WITHDRAWAL_ACTIVITY, context)\n const control = await controlLineStanding(context)\n\n /* 읽기만 여기서 하고 **판단은 한 곳에서** 한다 — 조건이 여기 있으면 거동 시험이 못 본다(§`assertWithdrawalRaisable`). */\n assertWithdrawalRaisable({ reason, spec, waiting: !!waiting, installed: !!activity, line: control.standing })\n\n /* 이름은 **서버가 읽는다.** 무엇을 승인했는지 묻는 자리라 올린 쪽이 보낸 이름을 믿지 않는다. */\n const board = await getRepository(Board).findOne({ where: { id: boardId } })\n const screenName = board?.name ?? boardId\n const action: ControlWithdrawalAction = remove ? 'delete' : 'turn-off'\n\n const instance = await issue(\n {\n name,\n description: `${screenName} — ${reason.trim()}`,\n activityId: activity!.id,\n /* 이 문의 결재선이 아니라 **발행 승인의 결재선**이다(머리말). */\n approvalLine: control.activity?.approvalLine,\n requireApproval: true,\n threadsMin: 1,\n input: {\n boardId,\n headline:\n action === 'delete'\n ? `${screenName} 의 화면 설정을 지우고 통제에서 뺍니다`\n : `${screenName} 를 통제 문서에서 뺍니다`,\n consequences: {\n after: '승인하면 이 화면은 그 뒤로 결재 없이 발행됩니다. 작업자가 보는 버전이 서명 없이 바뀔 수 있습니다.'\n },\n /* 방식마다 무게가 다르다 — 끄는 것은 다시 켤 수 있고, 지우는 것은 못 되돌린다. 사유(caveat)와 함께 싣는다. */\n reversibility: withdrawalReversibility(action) as { reversible: boolean; caveat: string },\n /* The settings as they were, so a deletion can be set up again from this record (ADR-0064 ③). */\n before: stationScreenSpecBefore(spec!),\n withdrawal: {\n name,\n /* 승인 때 **이 id** 만 다룬다 — 그 사이 지우고 새로 만든 설정은 결재자가 본 것이 아니다. */\n specId: spec!.id,\n boardId,\n screenName,\n action,\n reason: reason.trim(),\n requestedAt: new Date().toISOString()\n }\n }\n } as any,\n context\n )\n\n return instance.id\n }\n}\n"]}
@@ -59,7 +59,7 @@ export declare function whyNotWithdraw(standing: {
59
59
  /**
60
60
  * 되돌릴 수 있는가 — **방식마다 다르다.**
61
61
  *
62
- * 끄는 것은 다시 켤 수 있다. 다만 꺼져 있던 동안 서명 없이 나간 판은 되돌아오지 않는다. 지우는 것은
62
+ * 끄는 것은 다시 켤 수 있다. 다만 꺼져 있던 동안 서명 없이 나간 버전은 되돌아오지 않는다. 지우는 것은
63
63
  * 흐름·요구 축·통제 표시가 함께 사라져 되돌릴 길이 없다 — 다시 만드는 것은 새로 짓는 일이다.
64
64
  */
65
65
  export declare function withdrawalReversibility(action: ControlWithdrawalAction): {
@@ -67,7 +67,7 @@ function whyNotWithdraw(standing) {
67
67
  /**
68
68
  * 되돌릴 수 있는가 — **방식마다 다르다.**
69
69
  *
70
- * 끄는 것은 다시 켤 수 있다. 다만 꺼져 있던 동안 서명 없이 나간 판은 되돌아오지 않는다. 지우는 것은
70
+ * 끄는 것은 다시 켤 수 있다. 다만 꺼져 있던 동안 서명 없이 나간 버전은 되돌아오지 않는다. 지우는 것은
71
71
  * 흐름·요구 축·통제 표시가 함께 사라져 되돌릴 길이 없다 — 다시 만드는 것은 새로 짓는 일이다.
72
72
  */
73
73
  function withdrawalReversibility(action) {
@@ -1 +1 @@
1
- {"version":3,"file":"station-screen-control-withdrawal.js","sourceRoot":"","sources":["../../../server/service/station/station-screen-control-withdrawal.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * **통제 문서의 통제를 거두는 일** — 켜는 것만 막고 끄는 것을 열어 두면 통제가 한 방향으로만 선다.\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── 무엇이 열려 있었나 ─────────────────────────────────────────────────────\n * 통제를 **켤 때**는 결재선이 있어야 했다(`turnsControlOn`). 그런데 `controlled: false` 로 저장하거나\n * spec 을 지우는 데는 관문이 없었고, 발행 관문은 통제가 아닌 화면을 그대로 통과시킨다\n * (`station-screen-publish-subscriber.ts` — `if (!spec?.controlled) return`). 그래서 화면을 만드는\n * 권한만 든 작성자가 통제를 끄고 **서명 없는 도안을 발행**할 수 있었다.\n *\n * 판정(ADR-0047 ⑨): **결재로 세운 것은 결재로 거둔다.** 통제를 끄는 것과 통제 중인 spec 을 지우는\n * 것은 발행 승인과 **같은 결재선**을 지난다. 끄는 것이 결재를 지났다면 발행 관문의 그 return 은\n * 정당하다 — 그래서 관문은 그대로 둔다.\n *\n * ── 여기 있는 것 ──────────────────────────────────────────────────────────\n * 판단만 둔다. DB 를 읽는 것은 부르는 쪽의 일이다 — 이 판단이 틀리면 통제 문서가 뚫리는데, 그것을\n * 확인하는 시험이 DB·보드·spec·결재를 다 세운 뒤에야 물을 수 있으면 안 묻게 된다.\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\nimport { approvalLineRefusal, type ApprovalLineStanding } from '@things-factory/worklist'\nimport { Refusal, type Reversibility } from '@things-factory/shell'\n\n/** 거두는 방식 둘 — 상신 때 정하고, 승인 때 그대로 한다. */\nexport const CONTROL_WITHDRAWAL_ACTIONS = ['turn-off', 'delete'] as const\n\nexport type ControlWithdrawalAction = (typeof CONTROL_WITHDRAWAL_ACTIONS)[number]\n\n/**\n * Both are reversible (ADR-0064, conditions for `reversible`): turning control on again and saving the\n * settings again are the paired operations, releases are kept by board and not by the settings row's id,\n * and the delete raise carries the settings as they were (`stationScreenSpecBefore`), so the approval\n * record holds what to set up again.\n */\nexport const WITHDRAWAL_REVERSIBILITY: Record<ControlWithdrawalAction, Reversibility> = {\n 'turn-off': 'reversible',\n delete: 'reversible'\n}\n\n/**\n * 승인 콜백이 **하지 않고 끝내는** 사유.\n *\n * 상신과 승인 사이에 그 spec 이 바뀔 수 있다. 콜백은 상신 때 굳힌 것만 하고, 그 사이 들어온 변경을\n * 덮지 않는다 — 그러면 조용히 성공하는 대신 이 코드로 멈춘다.\n */\nexport type ControlWithdrawalRefusal =\n /** 상신 때 가리킨 spec 이 이제 없다. */\n | 'control-withdrawal-spec-gone'\n /** 그 spec 은 이미 통제가 아니다 — 거둘 것이 없다. */\n | 'control-already-off'\n\n/**\n * 이 저장이 통제를 **끄는** 저장인가.\n *\n * `after` 가 비어 있으면 끄는 것이 아니다 — 흐름만 저장하는 요청은 `controlled` 를 안 싣는다\n * (§`saveStationScreenSpec`). 그것을 끄는 것으로 읽으면 통제 문서를 고칠 수조차 없게 된다.\n */\nexport function turnsControlOff(before: boolean | null | undefined, after: boolean | null | undefined): boolean {\n return !!before && after === false\n}\n\n/**\n * 승인된 지금 **상신 때 굳힌 것을 그대로 할 수 있는가** — 할 수 없으면 그 사유.\n *\n * `standing` 은 콜백이 **상신 때 굳힌 id 로** 다시 읽은 spec 이다. boardId 로 다시 찾지 않는다 — 그\n * 사이 지우고 새로 만든 spec 은 결재자가 본 것이 아니다.\n */\nexport function whyNotWithdraw(\n standing: { controlled?: boolean | null } | null | undefined\n): ControlWithdrawalRefusal | undefined {\n if (!standing) {\n return 'control-withdrawal-spec-gone'\n }\n\n if (!standing.controlled) {\n return 'control-already-off'\n }\n\n return undefined\n}\n\n/**\n * 되돌릴 수 있는가 — **방식마다 다르다.**\n *\n * 끄는 것은 다시 켤 수 있다. 다만 꺼져 있던 동안 서명 없이 나간 판은 되돌아오지 않는다. 지우는 것은\n * 흐름·요구 축·통제 표시가 함께 사라져 되돌릴 길이 없다 — 다시 만드는 것은 새로 짓는 일이다.\n */\nexport function withdrawalReversibility(action: ControlWithdrawalAction): { reversible: boolean; caveat: string } {\n return {\n reversible: WITHDRAWAL_REVERSIBILITY[action] === 'reversible',\n caveat:\n action === 'delete'\n ? '결재 기록에 남은 화면 설정으로 다시 세울 수 있습니다. 통제를 다시 켜는 것도 결재를 거칩니다.'\n : '통제는 다시 켤 수 있습니다. 꺼져 있는 동안 서명 없이 발행된 버전은 되돌아오지 않습니다.'\n }\n}\n\n/** 상신 이름 — 읽는 쪽과 올리는 쪽이 같은 이름을 써야 겹친 상신을 찾는다. */\nexport function controlWithdrawalRequestName(boardId: string): string {\n return `통제 해제 승인 ${boardId}`\n}\n\n/** 올릴 때 본 것 — 읽는 것은 리졸버가 하고, 이 판단은 DB 를 모른다. */\nexport interface WithdrawalRaiseReading {\n reason?: string | null\n spec?: { controlled?: boolean | null } | null\n /** 같은 화면의 해제가 이미 결재 중인가. */\n waiting: boolean\n /** 통제 해제 승인 양식이 이 공장에 섰나. */\n installed: boolean\n /** **화면 발행 승인**의 결재선 판정 — 켤 때와 같은 함수가 낸 것(`controlLineStanding`). */\n line: ApprovalLineStanding\n}\n\n/**\n * 올려도 되는가 — **안 되면 던진다.** 리졸버에는 조건이 없고 이 함수를 부르기만 한다.\n *\n * ── 왜 판단이 던지나 ───────────────────────────────────────────────────────\n * 리졸버에 `if (refusal) throw` 를 두면, 그 조건이 죽어도(`if (false && …)`) 글자는 남는다. 글자로\n * 문을 세는 분류기(`irreversible-doors-refuse.test.ts`)는 그것을 「거절한다」로 읽는다 — 거절하는 글자가\n * 있는지를 셀 뿐 거절이 **도는지**는 못 본다. 그 층은 거동 시험이 맡는다\n * (`station-screen-control-withdrawal.test.ts`, 아키텍트 2026-09-17). 조건을 이 함수 하나에 모아야 거동\n * 시험이 부르는 것이 곧 상신이 부르는 것이 된다.\n */\nexport function assertWithdrawalRaisable(reading: WithdrawalRaiseReading): void {\n if (!reading.reason?.trim()) {\n throw new Refusal('control-withdrawal-reason-missing', {}, 'enter the reason for withdrawing control.')\n }\n\n if (!reading.spec) {\n throw new Refusal('station-spec-missing', {}, 'the screen settings were not found.')\n }\n\n if (!reading.spec.controlled) {\n throw new Refusal('control-not-on', {}, 'this screen is not a controlled document.')\n }\n\n if (reading.waiting) {\n throw new Refusal('control-withdrawal-already-raised', {}, 'a control withdrawal for this screen is already awaiting approval.')\n }\n\n if (!reading.installed) {\n throw new Refusal('control-withdrawal-door-missing', {}, 'the control withdrawal approval form is not installed in this plant.')\n }\n\n const refusal = reading.line.refusal\n\n if (!refusal) {\n return\n }\n\n /* The line is the screen release line, judged by the same function as turning control on (ADR-0047 ②). */\n throw approvalLineRefusal(refusal)\n}\n\n/** The settings' content columns — what a person needs to set the same screen up again. */\nexport const STATION_SCREEN_SPEC_CONTENT = ['boardId', 'requires', 'target', 'controlled', 'kind', 'flow'] as const\n\n/**\n * The settings as they were before deletion, carried in the approval record. It is what makes the delete\n * branch reversible: the flow, read axes and control mark stay readable after the row is gone\n * (ADR-0064, condition ③ for `reversible`).\n */\nexport function stationScreenSpecBefore(spec: Partial<Record<(typeof STATION_SCREEN_SPEC_CONTENT)[number], unknown>>) {\n return Object.fromEntries(STATION_SCREEN_SPEC_CONTENT.map(column => [column, spec[column] ?? null]))\n}\n"]}
1
+ {"version":3,"file":"station-screen-control-withdrawal.js","sourceRoot":"","sources":["../../../server/service/station/station-screen-control-withdrawal.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * **통제 문서의 통제를 거두는 일** — 켜는 것만 막고 끄는 것을 열어 두면 통제가 한 방향으로만 선다.\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── 무엇이 열려 있었나 ─────────────────────────────────────────────────────\n * 통제를 **켤 때**는 결재선이 있어야 했다(`turnsControlOn`). 그런데 `controlled: false` 로 저장하거나\n * spec 을 지우는 데는 관문이 없었고, 발행 관문은 통제가 아닌 화면을 그대로 통과시킨다\n * (`station-screen-publish-subscriber.ts` — `if (!spec?.controlled) return`). 그래서 화면을 만드는\n * 권한만 든 작성자가 통제를 끄고 **서명 없는 도안을 발행**할 수 있었다.\n *\n * 판정(ADR-0047 ⑨): **결재로 세운 것은 결재로 거둔다.** 통제를 끄는 것과 통제 중인 spec 을 지우는\n * 것은 발행 승인과 **같은 결재선**을 지난다. 끄는 것이 결재를 지났다면 발행 관문의 그 return 은\n * 정당하다 — 그래서 관문은 그대로 둔다.\n *\n * ── 여기 있는 것 ──────────────────────────────────────────────────────────\n * 판단만 둔다. DB 를 읽는 것은 부르는 쪽의 일이다 — 이 판단이 틀리면 통제 문서가 뚫리는데, 그것을\n * 확인하는 시험이 DB·보드·spec·결재를 다 세운 뒤에야 물을 수 있으면 안 묻게 된다.\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\nimport { approvalLineRefusal, type ApprovalLineStanding } from '@things-factory/worklist'\nimport { Refusal, type Reversibility } from '@things-factory/shell'\n\n/** 거두는 방식 둘 — 상신 때 정하고, 승인 때 그대로 한다. */\nexport const CONTROL_WITHDRAWAL_ACTIONS = ['turn-off', 'delete'] as const\n\nexport type ControlWithdrawalAction = (typeof CONTROL_WITHDRAWAL_ACTIONS)[number]\n\n/**\n * Both are reversible (ADR-0064, conditions for `reversible`): turning control on again and saving the\n * settings again are the paired operations, releases are kept by board and not by the settings row's id,\n * and the delete raise carries the settings as they were (`stationScreenSpecBefore`), so the approval\n * record holds what to set up again.\n */\nexport const WITHDRAWAL_REVERSIBILITY: Record<ControlWithdrawalAction, Reversibility> = {\n 'turn-off': 'reversible',\n delete: 'reversible'\n}\n\n/**\n * 승인 콜백이 **하지 않고 끝내는** 사유.\n *\n * 상신과 승인 사이에 그 spec 이 바뀔 수 있다. 콜백은 상신 때 굳힌 것만 하고, 그 사이 들어온 변경을\n * 덮지 않는다 — 그러면 조용히 성공하는 대신 이 코드로 멈춘다.\n */\nexport type ControlWithdrawalRefusal =\n /** 상신 때 가리킨 spec 이 이제 없다. */\n | 'control-withdrawal-spec-gone'\n /** 그 spec 은 이미 통제가 아니다 — 거둘 것이 없다. */\n | 'control-already-off'\n\n/**\n * 이 저장이 통제를 **끄는** 저장인가.\n *\n * `after` 가 비어 있으면 끄는 것이 아니다 — 흐름만 저장하는 요청은 `controlled` 를 안 싣는다\n * (§`saveStationScreenSpec`). 그것을 끄는 것으로 읽으면 통제 문서를 고칠 수조차 없게 된다.\n */\nexport function turnsControlOff(before: boolean | null | undefined, after: boolean | null | undefined): boolean {\n return !!before && after === false\n}\n\n/**\n * 승인된 지금 **상신 때 굳힌 것을 그대로 할 수 있는가** — 할 수 없으면 그 사유.\n *\n * `standing` 은 콜백이 **상신 때 굳힌 id 로** 다시 읽은 spec 이다. boardId 로 다시 찾지 않는다 — 그\n * 사이 지우고 새로 만든 spec 은 결재자가 본 것이 아니다.\n */\nexport function whyNotWithdraw(\n standing: { controlled?: boolean | null } | null | undefined\n): ControlWithdrawalRefusal | undefined {\n if (!standing) {\n return 'control-withdrawal-spec-gone'\n }\n\n if (!standing.controlled) {\n return 'control-already-off'\n }\n\n return undefined\n}\n\n/**\n * 되돌릴 수 있는가 — **방식마다 다르다.**\n *\n * 끄는 것은 다시 켤 수 있다. 다만 꺼져 있던 동안 서명 없이 나간 버전은 되돌아오지 않는다. 지우는 것은\n * 흐름·요구 축·통제 표시가 함께 사라져 되돌릴 길이 없다 — 다시 만드는 것은 새로 짓는 일이다.\n */\nexport function withdrawalReversibility(action: ControlWithdrawalAction): { reversible: boolean; caveat: string } {\n return {\n reversible: WITHDRAWAL_REVERSIBILITY[action] === 'reversible',\n caveat:\n action === 'delete'\n ? '결재 기록에 남은 화면 설정으로 다시 세울 수 있습니다. 통제를 다시 켜는 것도 결재를 거칩니다.'\n : '통제는 다시 켤 수 있습니다. 꺼져 있는 동안 서명 없이 발행된 버전은 되돌아오지 않습니다.'\n }\n}\n\n/** 상신 이름 — 읽는 쪽과 올리는 쪽이 같은 이름을 써야 겹친 상신을 찾는다. */\nexport function controlWithdrawalRequestName(boardId: string): string {\n return `통제 해제 승인 ${boardId}`\n}\n\n/** 올릴 때 본 것 — 읽는 것은 리졸버가 하고, 이 판단은 DB 를 모른다. */\nexport interface WithdrawalRaiseReading {\n reason?: string | null\n spec?: { controlled?: boolean | null } | null\n /** 같은 화면의 해제가 이미 결재 중인가. */\n waiting: boolean\n /** 통제 해제 승인 양식이 이 공장에 섰나. */\n installed: boolean\n /** **화면 발행 승인**의 결재선 판정 — 켤 때와 같은 함수가 낸 것(`controlLineStanding`). */\n line: ApprovalLineStanding\n}\n\n/**\n * 올려도 되는가 — **안 되면 던진다.** 리졸버에는 조건이 없고 이 함수를 부르기만 한다.\n *\n * ── 왜 판단이 던지나 ───────────────────────────────────────────────────────\n * 리졸버에 `if (refusal) throw` 를 두면, 그 조건이 죽어도(`if (false && …)`) 글자는 남는다. 글자로\n * 문을 세는 분류기(`irreversible-doors-refuse.test.ts`)는 그것을 「거절한다」로 읽는다 — 거절하는 글자가\n * 있는지를 셀 뿐 거절이 **도는지**는 못 본다. 그 층은 거동 시험이 맡는다\n * (`station-screen-control-withdrawal.test.ts`, 아키텍트 2026-09-17). 조건을 이 함수 하나에 모아야 거동\n * 시험이 부르는 것이 곧 상신이 부르는 것이 된다.\n */\nexport function assertWithdrawalRaisable(reading: WithdrawalRaiseReading): void {\n if (!reading.reason?.trim()) {\n throw new Refusal('control-withdrawal-reason-missing', {}, 'enter the reason for withdrawing control.')\n }\n\n if (!reading.spec) {\n throw new Refusal('station-spec-missing', {}, 'the screen settings were not found.')\n }\n\n if (!reading.spec.controlled) {\n throw new Refusal('control-not-on', {}, 'this screen is not a controlled document.')\n }\n\n if (reading.waiting) {\n throw new Refusal('control-withdrawal-already-raised', {}, 'a control withdrawal for this screen is already awaiting approval.')\n }\n\n if (!reading.installed) {\n throw new Refusal('control-withdrawal-door-missing', {}, 'the control withdrawal approval form is not installed in this plant.')\n }\n\n const refusal = reading.line.refusal\n\n if (!refusal) {\n return\n }\n\n /* The line is the screen release line, judged by the same function as turning control on (ADR-0047 ②). */\n throw approvalLineRefusal(refusal)\n}\n\n/** The settings' content columns — what a person needs to set the same screen up again. */\nexport const STATION_SCREEN_SPEC_CONTENT = ['boardId', 'requires', 'target', 'controlled', 'kind', 'flow'] as const\n\n/**\n * The settings as they were before deletion, carried in the approval record. It is what makes the delete\n * branch reversible: the flow, read axes and control mark stay readable after the row is gone\n * (ADR-0064, condition ③ for `reversible`).\n */\nexport function stationScreenSpecBefore(spec: Partial<Record<(typeof STATION_SCREEN_SPEC_CONTENT)[number], unknown>>) {\n return Object.fromEntries(STATION_SCREEN_SPEC_CONTENT.map(column => [column, spec[column] ?? null]))\n}\n"]}
@@ -1,13 +1,13 @@
1
1
  import { Domain } from '@things-factory/shell';
2
2
  import { User } from '@things-factory/auth-base';
3
3
  /**
4
- * 화면 발행 승인 — **통제 문서 한 판에 서명한 자리.**
4
+ * 화면 발행 승인 — **통제 문서 한 버전에 서명한 자리.**
5
5
  *
6
6
  * ═══════════════════════════════════════════════════════════════════════════
7
7
  * ── 왜 표가 필요한가 ──────────────────────────────────────────────────────
8
- * `controlled` 한 화면은 발행이 결재를 지나야 하고, **판마다 누가 승인했는지 나중에 찾을 수 있어야**
8
+ * `controlled` 한 화면은 발행이 결재를 지나야 하고, **버전마다 누가 승인했는지 나중에 찾을 수 있어야**
9
9
  * 한다(ISO 9001 7.5.3). 그 「찾을 수 있음」이 이 표다. 결재 기록만으로는 부족하다 — 발행 관문이
10
- * 「이 판에 승인이 있나」를 묻고, 그 물음의 답이 여기 있어야 `approval-missing` 을 가릴 수 있다.
10
+ * 「이 버전에 승인이 있나」를 묻고, 그 물음의 답이 여기 있어야 `approval-missing` 을 가릴 수 있다.
11
11
  *
12
12
  * ── 이름이 아니라 `boardId` 로 건다 ───────────────────────────────────────
13
13
  * 화면 이름은 바뀐다. 이름으로 걸면 개명한 날 **승인이 없는 화면**이 되고, 통제 문서가 조용히 통제
@@ -32,10 +32,10 @@ export declare class StationScreenRelease {
32
32
  screenName?: string;
33
33
  stationName?: string;
34
34
  /**
35
- * **어느 판을 승인했나** — 상신 시점 도안의 지문(§`screen-model-digest`).
35
+ * **어느 버전을 승인했나** — 상신 시점 도안의 지문(§`screen-model-digest`).
36
36
  *
37
37
  * 이 칸이 없으면 서명이 화면에만 걸리고 **v1 을 승인한 서명 위로 v2 가 그대로 나간다.** 통제 문서는
38
- * 판마다 승인받는 것이라 그 순간 통제가 이름만 남는다. 번호로는 못 묶는다 — `Board.version` 은
38
+ * 버전마다 승인받는 것이라 그 순간 통제가 이름만 남는다. 번호로는 못 묶는다 — `Board.version` 은
39
39
  * 발행할 때 정해지고 상신 시점에는 없다.
40
40
  *
41
41
  * 발행 관문이 지금 도안의 지문을 같은 함수로 만들어 이 값과 견준다. 다르면 `approval-stale` 이다.
@@ -7,13 +7,13 @@ const type_graphql_1 = require("type-graphql");
7
7
  const shell_1 = require("@things-factory/shell");
8
8
  const auth_base_1 = require("@things-factory/auth-base");
9
9
  /**
10
- * 화면 발행 승인 — **통제 문서 한 판에 서명한 자리.**
10
+ * 화면 발행 승인 — **통제 문서 한 버전에 서명한 자리.**
11
11
  *
12
12
  * ═══════════════════════════════════════════════════════════════════════════
13
13
  * ── 왜 표가 필요한가 ──────────────────────────────────────────────────────
14
- * `controlled` 한 화면은 발행이 결재를 지나야 하고, **판마다 누가 승인했는지 나중에 찾을 수 있어야**
14
+ * `controlled` 한 화면은 발행이 결재를 지나야 하고, **버전마다 누가 승인했는지 나중에 찾을 수 있어야**
15
15
  * 한다(ISO 9001 7.5.3). 그 「찾을 수 있음」이 이 표다. 결재 기록만으로는 부족하다 — 발행 관문이
16
- * 「이 판에 승인이 있나」를 묻고, 그 물음의 답이 여기 있어야 `approval-missing` 을 가릴 수 있다.
16
+ * 「이 버전에 승인이 있나」를 묻고, 그 물음의 답이 여기 있어야 `approval-missing` 을 가릴 수 있다.
17
17
  *
18
18
  * ── 이름이 아니라 `boardId` 로 건다 ───────────────────────────────────────
19
19
  * 화면 이름은 바뀐다. 이름으로 걸면 개명한 날 **승인이 없는 화면**이 되고, 통제 문서가 조용히 통제
@@ -1 +1 @@
1
- {"version":3,"file":"station-screen-release-record.js","sourceRoot":"","sources":["../../../server/service/station/station-screen-release-record.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\n\nimport { Domain } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\n\n/**\n * 화면 발행 승인 — **통제 문서 한 판에 서명한 자리.**\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── 왜 표가 필요한가 ──────────────────────────────────────────────────────\n * `controlled` 한 화면은 발행이 결재를 지나야 하고, **판마다 누가 승인했는지 나중에 찾을 수 있어야**\n * 한다(ISO 9001 7.5.3). 그 「찾을 수 있음」이 이 표다. 결재 기록만으로는 부족하다 — 발행 관문이\n * 「이 판에 승인이 있나」를 묻고, 그 물음의 답이 여기 있어야 `approval-missing` 을 가릴 수 있다.\n *\n * ── 이름이 아니라 `boardId` 로 건다 ───────────────────────────────────────\n * 화면 이름은 바뀐다. 이름으로 걸면 개명한 날 **승인이 없는 화면**이 되고, 통제 문서가 조용히 통제\n * 밖으로 나간다. 사람이 읽는 이름은 서명 시점의 것으로 함께 굳힌다 — 그때 무엇을 승인했는지는\n * 그때의 이름으로 말해야 한다.\n *\n * ── 발행 관문은 이 표를 읽는다 ────────────────────────────────────────────\n * 관문은 station 레인이 든다. 이 표는 그 관문이 묻는 자리이고, 여기 줄이 없으면 관문이 거절한다\n * (`approval-missing`). 문(양식·결재선·결재 화면)은 이 레인이 든다.\n *\n * [합의했다] operato-application/adr/ADR-0047.md ②④⑦\n * ═══════════════════════════════════════════════════════════════════════════\n */\n@Entity({ name: 'station_screen_releases' })\n/* A signature is for one drawing: the identity is (boardId, modelDigest), not the request name (ADR-0047 ④). */\n@Index('ix_station_screen_releases_0', (release: StationScreenRelease) => [release.domain, release.boardId, release.modelDigest], {\n unique: true\n})\n@Index('ix_station_screen_releases_1', (release: StationScreenRelease) => [release.domain, release.boardId])\n@ObjectType({ description: 'A signed decision that one controlled screen may be released' })\nexport class StationScreenRelease {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID)\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain)\n domain?: Domain\n\n @RelationId((release: StationScreenRelease) => release.domain)\n domainId?: string\n\n @Column()\n @Field({ description: 'the name of this release decision' })\n name: string\n\n /** 어느 화면인가 — **개명을 견디는 식별자.** */\n @Column()\n @Field({ description: 'the board this screen is, which survives renaming' })\n boardId: string\n\n /** 서명할 때 사람이 읽은 이름. 지금 이름이 아니라 **그때 이름**이다. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'what the screen was called when this was signed' })\n screenName?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'which station this screen was drawn for' })\n stationName?: string\n\n /**\n * **어느 판을 승인했나** — 상신 시점 도안의 지문(§`screen-model-digest`).\n *\n * 이 칸이 없으면 서명이 화면에만 걸리고 **v1 을 승인한 서명 위로 v2 가 그대로 나간다.** 통제 문서는\n * 판마다 승인받는 것이라 그 순간 통제가 이름만 남는다. 번호로는 못 묶는다 — `Board.version` 은\n * 발행할 때 정해지고 상신 시점에는 없다.\n *\n * 발행 관문이 지금 도안의 지문을 같은 함수로 만들어 이 값과 견준다. 다르면 `approval-stale` 이다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'digest of the drawing this signature was given to' })\n modelDigest?: string\n\n @Column()\n @Field({ description: 'when the last approver signed' })\n decidedAt: Date\n\n /** 누가 정했나 — **서버가 로그인한 사람으로 적는다.** 손으로 받지 않는다. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'who decided — filled by the server, never accepted from the caller' })\n decidedBy?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'when it was sent for approval' })\n requestedAt?: Date\n\n /** 무엇을 바꿨나 — 상신한 사람이 적는다. 통제 문서의 변경 이력이 이 칸이다. */\n @Column({ nullable: true })\n @Field({ nullable: true })\n reason?: string\n\n /**\n * 결재자가 **보고 서명한 문장 그대로.**\n *\n * 나중에 문구가 바뀌어도 옛 서명의 뜻이 안 흔들리게 굳힌다(§`batch-release-record` 와 같은 규율).\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => [String], { nullable: true, description: 'what the approver was shown before signing' })\n shownAtSigning?: string[]\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt?: Date\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n creator?: User\n\n @RelationId((release: StationScreenRelease) => release.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n updater?: User\n\n @RelationId((release: StationScreenRelease) => release.updater)\n updaterId?: string\n}\n"]}
1
+ {"version":3,"file":"station-screen-release-record.js","sourceRoot":"","sources":["../../../server/service/station/station-screen-release-record.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Column,\n CreateDateColumn,\n Entity,\n Index,\n ManyToOne,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\n\nimport { Domain } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\n\n/**\n * 화면 발행 승인 — **통제 문서 한 버전에 서명한 자리.**\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── 왜 표가 필요한가 ──────────────────────────────────────────────────────\n * `controlled` 한 화면은 발행이 결재를 지나야 하고, **버전마다 누가 승인했는지 나중에 찾을 수 있어야**\n * 한다(ISO 9001 7.5.3). 그 「찾을 수 있음」이 이 표다. 결재 기록만으로는 부족하다 — 발행 관문이\n * 「이 버전에 승인이 있나」를 묻고, 그 물음의 답이 여기 있어야 `approval-missing` 을 가릴 수 있다.\n *\n * ── 이름이 아니라 `boardId` 로 건다 ───────────────────────────────────────\n * 화면 이름은 바뀐다. 이름으로 걸면 개명한 날 **승인이 없는 화면**이 되고, 통제 문서가 조용히 통제\n * 밖으로 나간다. 사람이 읽는 이름은 서명 시점의 것으로 함께 굳힌다 — 그때 무엇을 승인했는지는\n * 그때의 이름으로 말해야 한다.\n *\n * ── 발행 관문은 이 표를 읽는다 ────────────────────────────────────────────\n * 관문은 station 레인이 든다. 이 표는 그 관문이 묻는 자리이고, 여기 줄이 없으면 관문이 거절한다\n * (`approval-missing`). 문(양식·결재선·결재 화면)은 이 레인이 든다.\n *\n * [합의했다] operato-application/adr/ADR-0047.md ②④⑦\n * ═══════════════════════════════════════════════════════════════════════════\n */\n@Entity({ name: 'station_screen_releases' })\n/* A signature is for one drawing: the identity is (boardId, modelDigest), not the request name (ADR-0047 ④). */\n@Index('ix_station_screen_releases_0', (release: StationScreenRelease) => [release.domain, release.boardId, release.modelDigest], {\n unique: true\n})\n@Index('ix_station_screen_releases_1', (release: StationScreenRelease) => [release.domain, release.boardId])\n@ObjectType({ description: 'A signed decision that one controlled screen may be released' })\nexport class StationScreenRelease {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID)\n readonly id: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain)\n domain?: Domain\n\n @RelationId((release: StationScreenRelease) => release.domain)\n domainId?: string\n\n @Column()\n @Field({ description: 'the name of this release decision' })\n name: string\n\n /** 어느 화면인가 — **개명을 견디는 식별자.** */\n @Column()\n @Field({ description: 'the board this screen is, which survives renaming' })\n boardId: string\n\n /** 서명할 때 사람이 읽은 이름. 지금 이름이 아니라 **그때 이름**이다. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'what the screen was called when this was signed' })\n screenName?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'which station this screen was drawn for' })\n stationName?: string\n\n /**\n * **어느 버전을 승인했나** — 상신 시점 도안의 지문(§`screen-model-digest`).\n *\n * 이 칸이 없으면 서명이 화면에만 걸리고 **v1 을 승인한 서명 위로 v2 가 그대로 나간다.** 통제 문서는\n * 버전마다 승인받는 것이라 그 순간 통제가 이름만 남는다. 번호로는 못 묶는다 — `Board.version` 은\n * 발행할 때 정해지고 상신 시점에는 없다.\n *\n * 발행 관문이 지금 도안의 지문을 같은 함수로 만들어 이 값과 견준다. 다르면 `approval-stale` 이다.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'digest of the drawing this signature was given to' })\n modelDigest?: string\n\n @Column()\n @Field({ description: 'when the last approver signed' })\n decidedAt: Date\n\n /** 누가 정했나 — **서버가 로그인한 사람으로 적는다.** 손으로 받지 않는다. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'who decided — filled by the server, never accepted from the caller' })\n decidedBy?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'when it was sent for approval' })\n requestedAt?: Date\n\n /** 무엇을 바꿨나 — 상신한 사람이 적는다. 통제 문서의 변경 이력이 이 칸이다. */\n @Column({ nullable: true })\n @Field({ nullable: true })\n reason?: string\n\n /**\n * 결재자가 **보고 서명한 문장 그대로.**\n *\n * 나중에 문구가 바뀌어도 옛 서명의 뜻이 안 흔들리게 굳힌다(§`batch-release-record` 와 같은 규율).\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => [String], { nullable: true, description: 'what the approver was shown before signing' })\n shownAtSigning?: string[]\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt?: Date\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n creator?: User\n\n @RelationId((release: StationScreenRelease) => release.creator)\n creatorId?: string\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n updater?: User\n\n @RelationId((release: StationScreenRelease) => release.updater)\n updaterId?: string\n}\n"]}
@@ -15,7 +15,7 @@ const worklist_1 = require("@things-factory/worklist");
15
15
  * 통제를 **이 저장으로는** 거둘 수 없다 — 결재로 세운 것은 결재로 거둔다(ADR-0047 ⑨).
16
16
  *
17
17
  * 저장과 삭제는 작성자의 권한(`station:mutation`)으로 열린다. 그 문으로 통제를 끄거나 통제 중인 설정을
18
- * 지우면, 발행 관문은 통제가 아닌 화면을 그대로 내보낸다 — 작성자가 서명 없는 판을 내보내는 길이다.
18
+ * 지우면, 발행 관문은 통제가 아닌 화면을 그대로 내보낸다 — 작성자가 서명 없는 버전을 내보내는 길이다.
19
19
  * 그래서 이 둘은 막고 「통제 해제 승인」으로 보낸다.
20
20
  */
21
21
  function controlWithdrawalNeedsApproval() {
@@ -113,7 +113,7 @@ let StationScreenSpecResolver = class StationScreenSpecResolver {
113
113
  const { domain, tx } = context.state;
114
114
  const repository = tx.getRepository(station_screen_spec_1.StationScreenSpec);
115
115
  /*
116
- * **통제 중인 설정은 여기서 안 지운다.** 지우면 발행 관문이 「통제 아님」으로 읽고 서명 없는 판을
116
+ * **통제 중인 설정은 여기서 안 지운다.** 지우면 발행 관문이 「통제 아님」으로 읽고 서명 없는 버전을
117
117
  * 내보낸다 — 통제를 끄는 것과 같은 일이다(ADR-0047 ⑨). 통제가 아닌 설정은 지금처럼 바로 지운다.
118
118
  */
119
119
  const existing = await repository.findOne({ where: { domain: { id: domain.id }, boardId } });
@@ -1 +1 @@
1
- {"version":3,"file":"station-screen-spec-resolver.js","sourceRoot":"","sources":["../../../server/service/station/station-screen-spec-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Field, InputType, Mutation, Query, Resolver, UseMiddleware } from 'type-graphql'\n\nimport { In } from 'typeorm'\n\nimport { ScalarObject, getRepository, Refusal, SpeakRefusals } from '@things-factory/shell'\n\nimport { axesRead } from './station-flow'\nimport type { StationFlow } from './station-flow'\nimport { StationScreenSpec } from './station-screen-spec'\nimport { turnsControlOn } from './station-screen-release-gate'\nimport { turnsControlOff } from './station-screen-control-withdrawal'\nimport { controlLineStanding } from './station-screen-control-withdrawal-resolver'\nimport { approvalLineRefusal } from '@things-factory/worklist'\n\n/**\n * 통제를 **이 저장으로는** 거둘 수 없다 — 결재로 세운 것은 결재로 거둔다(ADR-0047 ⑨).\n *\n * 저장과 삭제는 작성자의 권한(`station:mutation`)으로 열린다. 그 문으로 통제를 끄거나 통제 중인 설정을\n * 지우면, 발행 관문은 통제가 아닌 화면을 그대로 내보낸다 — 작성자가 서명 없는 판을 내보내는 길이다.\n * 그래서 이 둘은 막고 「통제 해제 승인」으로 보낸다.\n */\nfunction controlWithdrawalNeedsApproval(): Refusal {\n return new Refusal(\n 'control-withdrawal-needs-approval',\n {},\n 'document control can only be removed through a control withdrawal approval.'\n )\n}\n\n@InputType({ description: 'what a board needs in order to work as a station screen' })\nexport class StationScreenSpecInput {\n @Field({ description: \"the board's id in board-service\" })\n boardId: string\n\n @Field(type => [String], { nullable: true, description: 'station context axes the drawing reads' })\n requires?: string[]\n\n @Field(type => ScalarObject, { nullable: true })\n target?: Record<string, unknown>\n\n @Field({ nullable: true })\n controlled?: boolean\n\n @Field({ nullable: true, description: \"'work' or 'signal'\" })\n kind?: 'work' | 'signal'\n\n @Field(type => ScalarObject, { nullable: true, description: 'the steps, and where each argument comes from' })\n flow?: StationFlow\n}\n\n/**\n * Reading and writing the manufacturing side of a board.\n *\n * The drawing stays in board-service; this is only what the drawing needs in order to stand in\n * front of an operator. Written by the modeller when a screen is saved, read by the assignment\n * screen and by whoever is deciding whether a release needs approval.\n */\n@Resolver(StationScreenSpec)\n@UseMiddleware(SpeakRefusals)\nexport class StationScreenSpecResolver {\n @Directive('@privilege(category: \"station\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => StationScreenSpec, { nullable: true, description: 'the station spec for one board' })\n async stationScreenSpec(@Arg('boardId') boardId: string, @Ctx() context: any): Promise<StationScreenSpec | null> {\n const { domain } = context.state\n\n return await getRepository(StationScreenSpec).findOne({ where: { domain: { id: domain.id }, boardId } })\n }\n\n /**\n * Specs for several boards at once.\n *\n * The assignment screen needs one per row. Asked one at a time it is a round trip per assignment,\n * and that list is the screen a plant manager keeps open.\n */\n @Directive('@privilege(category: \"station\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => [StationScreenSpec], { description: 'station specs for these boards' })\n async stationScreenSpecs(@Arg('boardIds', type => [String]) boardIds: string[], @Ctx() context: any): Promise<StationScreenSpec[]> {\n const { domain } = context.state\n\n if (!boardIds.length) {\n return []\n }\n\n return await getRepository(StationScreenSpec).find({ where: { domain: { id: domain.id }, boardId: In(boardIds) } })\n }\n\n /**\n * Write the spec for a board — one row per board, replaced rather than appended.\n *\n * There is no version history here on purpose. The spec describes whatever the board currently\n * is; the board keeps its own versions, and a spec that drifted from the drawing would answer a\n * question nobody asked.\n */\n @Directive('@transaction')\n @Directive('@privilege(category: \"station\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => StationScreenSpec, { description: 'record what a board needs as a station screen' })\n async saveStationScreenSpec(@Arg('patch', type => StationScreenSpecInput) patch: StationScreenSpecInput, @Ctx() context: any): Promise<StationScreenSpec> {\n const { domain, user, tx } = context.state\n const repository = tx.getRepository(StationScreenSpec)\n\n const existing = await repository.findOne({ where: { domain: { id: domain.id }, boardId: patch.boardId } })\n\n /*\n * `requires` 는 **흐름에서 나온다.** 저작자가 따로 적지 않는다 — 손으로 적으면 부품을 고칠 때\n * 어긋나고, 그러면 배정 검증이 이미 사실이 아닌 것을 검사한다.\n *\n * 흐름을 안 보낸 저장(예: `controlled` 만 켜는 것)에서는 손대지 않는다. 그때 다시 계산하면\n * 이미 있던 축이 빈 흐름 때문에 지워진다.\n */\n const derived = patch.flow ? { requires: axesRead(patch.flow) } : {}\n\n if (turnsControlOn(existing?.controlled, patch.controlled)) {\n await refuseControlWithoutApprovers(context)\n }\n\n if (turnsControlOff(existing?.controlled, patch.controlled)) {\n throw controlWithdrawalNeedsApproval()\n }\n\n return await repository.save({\n domain,\n ...(existing ?? { creator: user }),\n ...patch,\n ...derived,\n updater: user\n })\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"station\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Boolean, { description: 'forget the station spec for a board' })\n async deleteStationScreenSpec(@Arg('boardId') boardId: string, @Ctx() context: any): Promise<boolean> {\n const { domain, tx } = context.state\n const repository = tx.getRepository(StationScreenSpec)\n\n /*\n * **통제 중인 설정은 여기서 안 지운다.** 지우면 발행 관문이 「통제 아님」으로 읽고 서명 없는 판을\n * 내보낸다 — 통제를 끄는 것과 같은 일이다(ADR-0047 ⑨). 통제가 아닌 설정은 지금처럼 바로 지운다.\n */\n const existing = await repository.findOne({ where: { domain: { id: domain.id }, boardId } })\n\n if (existing?.controlled) {\n throw controlWithdrawalNeedsApproval()\n }\n\n await repository.delete({ domain: { id: domain.id }, boardId })\n\n return true\n }\n}\n\n/**\n * 통제 문서로 켜려면 **결재할 사람이 실제로 있어야 한다** (ADR-0047 ②).\n *\n * ── 왜 못 켜게 하는 쪽인가 ─────────────────────────────────────────────────\n * 「켜 두되 발행할 때 막는다」도 규칙이 될 수 있었다. 그러면 결재선이 없는 공장에서 켠 화면은 영원히\n * 발행이 안 되고, 현장이 낼 수 있는 답은 하나뿐이다 — 통제를 끈다. **통제를 위해 넣은 칸이 통제를\n * 끄는 손잡이가 된다.** 그래서 못 지킬 약속은 애초에 못 켜게 한다.\n *\n * ── 왜 서버인가 ────────────────────────────────────────────────────────────\n * 저작면도 이 판정을 보여 준다. 그러나 **브라우저가 건너뛸 수 있는 검사는 닫힌 것이 아니다** —\n * `saveStationScreenSpec` 은 공개된 mutation 이라 화면을 거치지 않고 부를 수 있다.\n *\n * ── 왜 거절이 셋인가 ───────────────────────────────────────────────────────\n * 관리자가 할 일이 셋 다 다르다. 결재선을 세우는 것 · 역할에 사람을 넣는 것 · 지목된 사람을 이 공장에\n * 들이는 것. 하나로 묶으면 무엇을 해야 하는지 말하지 않고 막기만 한다. 그리고 이 말은 화면을 만드는\n * 저작자가 아니라 **관리자**에게 가야 하는 말이라, 문장이 그렇게 되어 있다.\n */\nasync function refuseControlWithoutApprovers(context: any): Promise<void> {\n /* 끌 때(`requestStationScreenControlWithdrawal`)와 **같은 함수로** 판정한다. */\n const { standing } = await controlLineStanding(context)\n\n /*\n * 판정은 **한 객체에 선택 칸**이다 — `refusal` 이 있으면 그 결재선은 안 선다(ADR-0047 ⑤ 개정).\n * 앞서는 `{ok:true}|{ok:false,…}` union 이었는데 이 패키지의 서버가 `strict: false` 로 컴파일해\n * **boolean 판별자로 안 좁혀졌고**, 이 자리에서 오늘 서버 빌드가 한 번 섰다. 그때 넓은 타입으로\n * 받아 두었던 것을 지금 지운다.\n */\n\n if (!standing.refusal) {\n return\n }\n\n /* 켤 수 없는 까닭은 결재선이 안 서는 까닭 그대로다 — 관리자가 할 일이 셋 다르다. */\n throw approvalLineRefusal(standing.refusal)\n}\n"]}
1
+ {"version":3,"file":"station-screen-spec-resolver.js","sourceRoot":"","sources":["../../../server/service/station/station-screen-spec-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Field, InputType, Mutation, Query, Resolver, UseMiddleware } from 'type-graphql'\n\nimport { In } from 'typeorm'\n\nimport { ScalarObject, getRepository, Refusal, SpeakRefusals } from '@things-factory/shell'\n\nimport { axesRead } from './station-flow'\nimport type { StationFlow } from './station-flow'\nimport { StationScreenSpec } from './station-screen-spec'\nimport { turnsControlOn } from './station-screen-release-gate'\nimport { turnsControlOff } from './station-screen-control-withdrawal'\nimport { controlLineStanding } from './station-screen-control-withdrawal-resolver'\nimport { approvalLineRefusal } from '@things-factory/worklist'\n\n/**\n * 통제를 **이 저장으로는** 거둘 수 없다 — 결재로 세운 것은 결재로 거둔다(ADR-0047 ⑨).\n *\n * 저장과 삭제는 작성자의 권한(`station:mutation`)으로 열린다. 그 문으로 통제를 끄거나 통제 중인 설정을\n * 지우면, 발행 관문은 통제가 아닌 화면을 그대로 내보낸다 — 작성자가 서명 없는 버전을 내보내는 길이다.\n * 그래서 이 둘은 막고 「통제 해제 승인」으로 보낸다.\n */\nfunction controlWithdrawalNeedsApproval(): Refusal {\n return new Refusal(\n 'control-withdrawal-needs-approval',\n {},\n 'document control can only be removed through a control withdrawal approval.'\n )\n}\n\n@InputType({ description: 'what a board needs in order to work as a station screen' })\nexport class StationScreenSpecInput {\n @Field({ description: \"the board's id in board-service\" })\n boardId: string\n\n @Field(type => [String], { nullable: true, description: 'station context axes the drawing reads' })\n requires?: string[]\n\n @Field(type => ScalarObject, { nullable: true })\n target?: Record<string, unknown>\n\n @Field({ nullable: true })\n controlled?: boolean\n\n @Field({ nullable: true, description: \"'work' or 'signal'\" })\n kind?: 'work' | 'signal'\n\n @Field(type => ScalarObject, { nullable: true, description: 'the steps, and where each argument comes from' })\n flow?: StationFlow\n}\n\n/**\n * Reading and writing the manufacturing side of a board.\n *\n * The drawing stays in board-service; this is only what the drawing needs in order to stand in\n * front of an operator. Written by the modeller when a screen is saved, read by the assignment\n * screen and by whoever is deciding whether a release needs approval.\n */\n@Resolver(StationScreenSpec)\n@UseMiddleware(SpeakRefusals)\nexport class StationScreenSpecResolver {\n @Directive('@privilege(category: \"station\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => StationScreenSpec, { nullable: true, description: 'the station spec for one board' })\n async stationScreenSpec(@Arg('boardId') boardId: string, @Ctx() context: any): Promise<StationScreenSpec | null> {\n const { domain } = context.state\n\n return await getRepository(StationScreenSpec).findOne({ where: { domain: { id: domain.id }, boardId } })\n }\n\n /**\n * Specs for several boards at once.\n *\n * The assignment screen needs one per row. Asked one at a time it is a round trip per assignment,\n * and that list is the screen a plant manager keeps open.\n */\n @Directive('@privilege(category: \"station\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => [StationScreenSpec], { description: 'station specs for these boards' })\n async stationScreenSpecs(@Arg('boardIds', type => [String]) boardIds: string[], @Ctx() context: any): Promise<StationScreenSpec[]> {\n const { domain } = context.state\n\n if (!boardIds.length) {\n return []\n }\n\n return await getRepository(StationScreenSpec).find({ where: { domain: { id: domain.id }, boardId: In(boardIds) } })\n }\n\n /**\n * Write the spec for a board — one row per board, replaced rather than appended.\n *\n * There is no version history here on purpose. The spec describes whatever the board currently\n * is; the board keeps its own versions, and a spec that drifted from the drawing would answer a\n * question nobody asked.\n */\n @Directive('@transaction')\n @Directive('@privilege(category: \"station\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => StationScreenSpec, { description: 'record what a board needs as a station screen' })\n async saveStationScreenSpec(@Arg('patch', type => StationScreenSpecInput) patch: StationScreenSpecInput, @Ctx() context: any): Promise<StationScreenSpec> {\n const { domain, user, tx } = context.state\n const repository = tx.getRepository(StationScreenSpec)\n\n const existing = await repository.findOne({ where: { domain: { id: domain.id }, boardId: patch.boardId } })\n\n /*\n * `requires` 는 **흐름에서 나온다.** 저작자가 따로 적지 않는다 — 손으로 적으면 부품을 고칠 때\n * 어긋나고, 그러면 배정 검증이 이미 사실이 아닌 것을 검사한다.\n *\n * 흐름을 안 보낸 저장(예: `controlled` 만 켜는 것)에서는 손대지 않는다. 그때 다시 계산하면\n * 이미 있던 축이 빈 흐름 때문에 지워진다.\n */\n const derived = patch.flow ? { requires: axesRead(patch.flow) } : {}\n\n if (turnsControlOn(existing?.controlled, patch.controlled)) {\n await refuseControlWithoutApprovers(context)\n }\n\n if (turnsControlOff(existing?.controlled, patch.controlled)) {\n throw controlWithdrawalNeedsApproval()\n }\n\n return await repository.save({\n domain,\n ...(existing ?? { creator: user }),\n ...patch,\n ...derived,\n updater: user\n })\n }\n\n @Directive('@transaction')\n @Directive('@privilege(category: \"station\", privilege: \"mutation\", domainOwnerGranted: true)')\n @Mutation(returns => Boolean, { description: 'forget the station spec for a board' })\n async deleteStationScreenSpec(@Arg('boardId') boardId: string, @Ctx() context: any): Promise<boolean> {\n const { domain, tx } = context.state\n const repository = tx.getRepository(StationScreenSpec)\n\n /*\n * **통제 중인 설정은 여기서 안 지운다.** 지우면 발행 관문이 「통제 아님」으로 읽고 서명 없는 버전을\n * 내보낸다 — 통제를 끄는 것과 같은 일이다(ADR-0047 ⑨). 통제가 아닌 설정은 지금처럼 바로 지운다.\n */\n const existing = await repository.findOne({ where: { domain: { id: domain.id }, boardId } })\n\n if (existing?.controlled) {\n throw controlWithdrawalNeedsApproval()\n }\n\n await repository.delete({ domain: { id: domain.id }, boardId })\n\n return true\n }\n}\n\n/**\n * 통제 문서로 켜려면 **결재할 사람이 실제로 있어야 한다** (ADR-0047 ②).\n *\n * ── 왜 못 켜게 하는 쪽인가 ─────────────────────────────────────────────────\n * 「켜 두되 발행할 때 막는다」도 규칙이 될 수 있었다. 그러면 결재선이 없는 공장에서 켠 화면은 영원히\n * 발행이 안 되고, 현장이 낼 수 있는 답은 하나뿐이다 — 통제를 끈다. **통제를 위해 넣은 칸이 통제를\n * 끄는 손잡이가 된다.** 그래서 못 지킬 약속은 애초에 못 켜게 한다.\n *\n * ── 왜 서버인가 ────────────────────────────────────────────────────────────\n * 저작면도 이 판정을 보여 준다. 그러나 **브라우저가 건너뛸 수 있는 검사는 닫힌 것이 아니다** —\n * `saveStationScreenSpec` 은 공개된 mutation 이라 화면을 거치지 않고 부를 수 있다.\n *\n * ── 왜 거절이 셋인가 ───────────────────────────────────────────────────────\n * 관리자가 할 일이 셋 다 다르다. 결재선을 세우는 것 · 역할에 사람을 넣는 것 · 지목된 사람을 이 공장에\n * 들이는 것. 하나로 묶으면 무엇을 해야 하는지 말하지 않고 막기만 한다. 그리고 이 말은 화면을 만드는\n * 저작자가 아니라 **관리자**에게 가야 하는 말이라, 문장이 그렇게 되어 있다.\n */\nasync function refuseControlWithoutApprovers(context: any): Promise<void> {\n /* 끌 때(`requestStationScreenControlWithdrawal`)와 **같은 함수로** 판정한다. */\n const { standing } = await controlLineStanding(context)\n\n /*\n * 판정은 **한 객체에 선택 칸**이다 — `refusal` 이 있으면 그 결재선은 안 선다(ADR-0047 ⑤ 개정).\n * 앞서는 `{ok:true}|{ok:false,…}` union 이었는데 이 패키지의 서버가 `strict: false` 로 컴파일해\n * **boolean 판별자로 안 좁혀졌고**, 이 자리에서 오늘 서버 빌드가 한 번 섰다. 그때 넓은 타입으로\n * 받아 두었던 것을 지금 지운다.\n */\n\n if (!standing.refusal) {\n return\n }\n\n /* 켤 수 없는 까닭은 결재선이 안 서는 까닭 그대로다 — 관리자가 할 일이 셋 다르다. */\n throw approvalLineRefusal(standing.refusal)\n}\n"]}