@things-factory/worklist 10.1.29 → 10.1.31
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/pages/work-door/door-words.d.ts +41 -0
- package/dist-client/pages/work-door/door-words.js +54 -0
- package/dist-client/pages/work-door/door-words.js.map +1 -0
- package/dist-client/pages/work-door/work-doors-page.d.ts +37 -0
- package/dist-client/pages/work-door/work-doors-page.js +369 -0
- package/dist-client/pages/work-door/work-doors-page.js.map +1 -0
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js +3 -0
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/activity/index.d.ts +1 -1
- package/dist-server/service/activity-instance/index.d.ts +1 -1
- package/dist-server/service/activity-template/index.d.ts +1 -1
- package/dist-server/service/activity-thread/index.d.ts +1 -1
- package/dist-server/service/index.d.ts +3 -3
- package/dist-server/service/index.js +5 -2
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/work-door/activity-lookup.d.ts +70 -0
- package/dist-server/service/work-door/activity-lookup.js +110 -0
- package/dist-server/service/work-door/activity-lookup.js.map +1 -0
- package/dist-server/service/work-door/approval-line-standing.d.ts +92 -0
- package/dist-server/service/work-door/approval-line-standing.js +47 -0
- package/dist-server/service/work-door/approval-line-standing.js.map +1 -0
- package/dist-server/service/work-door/index.d.ts +15 -0
- package/dist-server/service/work-door/index.js +20 -0
- package/dist-server/service/work-door/index.js.map +1 -0
- package/dist-server/service/work-door/pass-work-door.d.ts +74 -0
- package/dist-server/service/work-door/pass-work-door.js +45 -0
- package/dist-server/service/work-door/pass-work-door.js.map +1 -0
- package/dist-server/service/work-door/work-door-declaration.d.ts +114 -0
- package/dist-server/service/work-door/work-door-declaration.js +67 -0
- package/dist-server/service/work-door/work-door-declaration.js.map +1 -0
- package/dist-server/service/work-door/work-door-event.d.ts +20 -0
- package/dist-server/service/work-door/work-door-event.js +25 -0
- package/dist-server/service/work-door/work-door-event.js.map +1 -0
- package/dist-server/service/work-door/work-door-journal-service.d.ts +47 -0
- package/dist-server/service/work-door/work-door-journal-service.js +75 -0
- package/dist-server/service/work-door/work-door-journal-service.js.map +1 -0
- package/dist-server/service/work-door/work-door-journal.d.ts +30 -0
- package/dist-server/service/work-door/work-door-journal.js +94 -0
- package/dist-server/service/work-door/work-door-journal.js.map +1 -0
- package/dist-server/service/work-door/work-door-not-used.d.ts +39 -0
- package/dist-server/service/work-door/work-door-not-used.js +86 -0
- package/dist-server/service/work-door/work-door-not-used.js.map +1 -0
- package/dist-server/service/work-door/work-door-resolver.d.ts +29 -0
- package/dist-server/service/work-door/work-door-resolver.js +127 -0
- package/dist-server/service/work-door/work-door-resolver.js.map +1 -0
- package/dist-server/service/work-door/work-door-service.d.ts +29 -0
- package/dist-server/service/work-door/work-door-service.js +126 -0
- package/dist-server/service/work-door/work-door-service.js.map +1 -0
- package/dist-server/service/work-door/work-door-type.d.ts +49 -0
- package/dist-server/service/work-door/work-door-type.js +122 -0
- package/dist-server/service/work-door/work-door-type.js.map +1 -0
- package/dist-server/service/work-door/work-door.d.ts +138 -0
- package/dist-server/service/work-door/work-door.js +156 -0
- package/dist-server/service/work-door/work-door.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/tests/pass-work-door.test.ts +133 -0
- package/translations/en.json +12 -1
- package/translations/ja.json +12 -1
- package/translations/ko.json +12 -1
- package/translations/ms.json +12 -1
- package/translations/zh.json +12 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.passWorkDoor = passWorkDoor;
|
|
4
|
+
const approval_line_standing_js_1 = require("./approval-line-standing.js");
|
|
5
|
+
const work_door_event_js_1 = require("./work-door-event.js");
|
|
6
|
+
const work_door_journal_service_js_1 = require("./work-door-journal-service.js");
|
|
7
|
+
const approval_line_standing_js_2 = require("./approval-line-standing.js");
|
|
8
|
+
/** A manager that is not in a transaction commits each statement where it stands. */
|
|
9
|
+
function assertOpenTransaction(tx) {
|
|
10
|
+
if (tx?.queryRunner?.isTransactionActive) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
throw new Error('passWorkDoor: must run inside the transaction that does the work - ' +
|
|
14
|
+
'a plain manager commits the log line on its own, and then the log says something happened that did not');
|
|
15
|
+
}
|
|
16
|
+
async function passWorkDoor(reading, passage) {
|
|
17
|
+
assertOpenTransaction(passage.tx);
|
|
18
|
+
const { door } = reading;
|
|
19
|
+
if (door.kind === 'assignment') {
|
|
20
|
+
throw new Error(`passWorkDoor: "${door.name}" is an assignment door, and an assignment door has no approval line - ` +
|
|
21
|
+
'what it waits for is a role bound to it, which is a different question with a different answer');
|
|
22
|
+
}
|
|
23
|
+
/* Declared unused: the work happens, and the line says it was that and not an empty line. */
|
|
24
|
+
if (reading.notUsed) {
|
|
25
|
+
return await ran(work_door_event_js_1.WorkDoorEvent.RanNotUsed, reading, passage);
|
|
26
|
+
}
|
|
27
|
+
const standing = await (0, approval_line_standing_js_1.judgeApprovalLine)(reading.activity?.approvalLine, passage.seats);
|
|
28
|
+
if (!standing.refusal) {
|
|
29
|
+
return { go: 'raise', standing };
|
|
30
|
+
}
|
|
31
|
+
const whenNoLine = door.whenNoLine === 'as-established'
|
|
32
|
+
? reading.establishedByApproval
|
|
33
|
+
? 'refuse'
|
|
34
|
+
: 'run'
|
|
35
|
+
: door.whenNoLine;
|
|
36
|
+
if (whenNoLine === 'refuse') {
|
|
37
|
+
return { go: 'refuse', refusal: (0, approval_line_standing_js_2.approvalLineRefusal)(standing.refusal) };
|
|
38
|
+
}
|
|
39
|
+
return await ran(work_door_event_js_1.WorkDoorEvent.RanWithoutLine, reading, passage);
|
|
40
|
+
}
|
|
41
|
+
async function ran(kind, reading, passage) {
|
|
42
|
+
const noted = await (0, work_door_journal_service_js_1.noteWorkDoor)(passage.tx, passage.domain, { kind, door: reading.door.name, subject: reading.subject }, passage.user);
|
|
43
|
+
return { go: 'run', noted: noted.id, because: kind };
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=pass-work-door.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pass-work-door.js","sourceRoot":"","sources":["../../../server/service/work-door/pass-work-door.ts"],"names":[],"mappings":";;AA6EA,oCAyCC;AAjHD,2EAAmH;AACnH,6DAAoD;AACpD,iFAA6D;AAC7D,2EAAiE;AAyDjE,qFAAqF;AACrF,SAAS,qBAAqB,CAAC,EAAoC;IACjE,IAAK,EAA0D,EAAE,WAAW,EAAE,mBAAmB,EAAE,CAAC;QAClG,OAAM;IACR,CAAC;IAED,MAAM,IAAI,KAAK,CACb,qEAAqE;QACnE,wGAAwG,CAC3G,CAAA;AACH,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,OAAwB,EACxB,OAAwB;IAExB,qBAAqB,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAEjC,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAA;IAExB,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,kBAAkB,IAAI,CAAC,IAAI,yEAAyE;YAClG,gGAAgG,CACnG,CAAA;IACH,CAAC;IAED,6FAA6F;IAC7F,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO,MAAM,GAAG,CAAC,kCAAa,CAAC,UAAU,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;IAC9D,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,IAAA,6CAAiB,EACtC,OAAO,CAAC,QAAQ,EAAE,YAAuD,EACzE,OAAO,CAAC,KAAK,CACd,CAAA;IAED,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;QACtB,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAA;IAClC,CAAC;IAED,MAAM,UAAU,GACd,IAAI,CAAC,UAAU,KAAK,gBAAgB;QAClC,CAAC,CAAC,OAAO,CAAC,qBAAqB;YAC7B,CAAC,CAAC,QAAQ;YACV,CAAC,CAAC,KAAK;QACT,CAAC,CAAC,IAAI,CAAC,UAAU,CAAA;IAErB,IAAI,UAAU,KAAK,QAAQ,EAAE,CAAC;QAC5B,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAA,+CAAmB,EAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAA;IACzE,CAAC;IAED,OAAO,MAAM,GAAG,CAAC,kCAAa,CAAC,cAAc,EAAE,OAAO,EAAE,OAAO,CAAC,CAAA;AAClE,CAAC;AAED,KAAK,UAAU,GAAG,CAChB,IAA6D,EAC7D,OAAwB,EACxB,OAAwB;IAExB,MAAM,KAAK,GAAG,MAAM,IAAA,2CAAY,EAC9B,OAAO,CAAC,EAAE,EACV,OAAO,CAAC,MAAM,EACd,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,EAC3D,OAAO,CAAC,IAAI,CACb,CAAA;IAED,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AACtD,CAAC","sourcesContent":["import { EntityManager } from 'typeorm'\n\nimport { Domain, Refusal } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\n\nimport { judgeApprovalLine, type ApprovalLineStanding, type ApprovalSeatReader } from './approval-line-standing.js'\nimport { WorkDoorEvent } from './work-door-event.js'\nimport { noteWorkDoor } from './work-door-journal-service.js'\nimport { approvalLineRefusal } from './approval-line-standing.js'\nimport type { WorkDoorDeclaration } from './work-door-declaration.js'\n\n/**\n * **One place decides, and the same place writes it down** (ADR-0077 decision 1).\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── Why judgement and record are one call ─────────────────────────────────\n * Until now each application door read the approval line itself and decided what to do, which is why\n * eight of operato-plant's doors run with no line and none of them leaves a trace: the place that\n * knew a step had been skipped was not the place that wrote anything down, and there were eight of\n * them. Joined here, a door cannot take the `run` branch without the line being written - there is\n * no branch to take that skips it.\n *\n * ── The transaction is not a convenience ──────────────────────────────────\n * `tx` is the caller's **open** transaction, not a manager. The line and the work it describes have\n * to commit together: the line first and the work failing leaves a log that says something happened\n * that did not, and the reverse leaves the silent pass this log exists to end. A plain manager\n * commits each statement where it stands, so passing one is the same bug the outbox sequence has\n * one layer down - and there it burned 200 numbers before anyone noticed.\n *\n * ── `as-established` asks the door, it does not guess ─────────────────────\n * A door whose branch turns on the row's own history (a non-working period withdrawn: declared\n * through approval, withdrawn through approval) answers `establishedByApproval` itself. This\n * function reads that one flag. Putting the judgement here would mean the worklist knew what a\n * non-working period is.\n * ═══════════════════════════════════════════════════════════════════════════\n */\nexport interface WorkDoorReading {\n door: WorkDoorDeclaration\n /** The activity as installed in this domain, if it is installed at all. */\n activity?: { id?: string; approvalLine?: unknown[] } | null\n /** What this is about, as the application names it. Two strings; nothing is read inside them. */\n subject: { kind: string; id: string }\n /** `as-established` doors only: did the row this is about come about through approval? */\n establishedByApproval?: boolean\n /** True when this domain has declared it does not use this door. */\n notUsed?: boolean\n}\n\nexport interface WorkDoorPassage {\n /** The caller's open transaction - the work and the log line commit together. */\n tx: EntityManager\n domain: Domain\n user?: User\n /** Reads whether a seat in the line resolves to a person. */\n seats: ApprovalSeatReader\n}\n\nexport type WorkDoorVerdict =\n /** The line stands. The caller raises it for approval; the instance is the record. */\n | { go: 'raise'; standing: ApprovalLineStanding }\n /** No approval here. The work happens, and this is the id of the line written about it. */\n | { go: 'run'; noted: string; because: WorkDoorEvent.RanWithoutLine | WorkDoorEvent.RanNotUsed }\n /** The caller is refused, in words that say what to go and fix. */\n | { go: 'refuse'; refusal: Refusal }\n\n/** A manager that is not in a transaction commits each statement where it stands. */\nfunction assertOpenTransaction(tx: EntityManager | undefined | null): void {\n if ((tx as { queryRunner?: { isTransactionActive?: boolean } })?.queryRunner?.isTransactionActive) {\n return\n }\n\n throw new Error(\n 'passWorkDoor: must run inside the transaction that does the work - ' +\n 'a plain manager commits the log line on its own, and then the log says something happened that did not'\n )\n}\n\nexport async function passWorkDoor(\n reading: WorkDoorReading,\n passage: WorkDoorPassage\n): Promise<WorkDoorVerdict> {\n assertOpenTransaction(passage.tx)\n\n const { door } = reading\n\n if (door.kind === 'assignment') {\n throw new Error(\n `passWorkDoor: \"${door.name}\" is an assignment door, and an assignment door has no approval line - ` +\n 'what it waits for is a role bound to it, which is a different question with a different answer'\n )\n }\n\n /* Declared unused: the work happens, and the line says it was that and not an empty line. */\n if (reading.notUsed) {\n return await ran(WorkDoorEvent.RanNotUsed, reading, passage)\n }\n\n const standing = await judgeApprovalLine(\n reading.activity?.approvalLine as Parameters<typeof judgeApprovalLine>[0],\n passage.seats\n )\n\n if (!standing.refusal) {\n return { go: 'raise', standing }\n }\n\n const whenNoLine =\n door.whenNoLine === 'as-established'\n ? reading.establishedByApproval\n ? 'refuse'\n : 'run'\n : door.whenNoLine\n\n if (whenNoLine === 'refuse') {\n return { go: 'refuse', refusal: approvalLineRefusal(standing.refusal) }\n }\n\n return await ran(WorkDoorEvent.RanWithoutLine, reading, passage)\n}\n\nasync function ran(\n kind: WorkDoorEvent.RanWithoutLine | WorkDoorEvent.RanNotUsed,\n reading: WorkDoorReading,\n passage: WorkDoorPassage\n): Promise<WorkDoorVerdict> {\n const noted = await noteWorkDoor(\n passage.tx,\n passage.domain,\n { kind, door: reading.door.name, subject: reading.subject },\n passage.user\n )\n\n return { go: 'run', noted: noted.id, because: kind }\n}\n"]}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **What an application declares about its own work doors** (ADR-0069 decision 3, ADR-0077).
|
|
3
|
+
*
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
5
|
+
* ── Who says what ─────────────────────────────────────────────────────────
|
|
6
|
+
* The form is installed by the product at boot, and the plant fills in who approves it. Filling it
|
|
7
|
+
* in is the declaration that this factory uses that step. So two facts have two owners:
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* the list of doors, and what each one is the application - it declares them here
|
|
11
|
+
* whether a door stands in this domain the worklist - it reads the activity and its line
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* An application registers its list; the screens and the judgement live here. Without that split the
|
|
15
|
+
* worklist would have to know operato-plant's activities, and every application would carry its own
|
|
16
|
+
* copy of the same screen (operato-warehouse asked for it second, which is what moved this).
|
|
17
|
+
*
|
|
18
|
+
* ── Registration happens at module import ─────────────────────────────────
|
|
19
|
+
* Not on a bootstrap hook. A hook runs after the schema is built, which is too late for anything the
|
|
20
|
+
* schema or a boot-time install reads - `declareAxes` in auth-base carries the same warning.
|
|
21
|
+
*
|
|
22
|
+
* ── Labels travel as keys, never as words ─────────────────────────────────
|
|
23
|
+
* `labelKey` names a translation in the declaring module's own namespace. A word here would put one
|
|
24
|
+
* product's vocabulary ("plant", "factory floor") on every other product's screen.
|
|
25
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* What happens when the approval line is empty. **This records what each door already does** - it is
|
|
29
|
+
* not a new policy (ADR-0077 decision 1).
|
|
30
|
+
*
|
|
31
|
+
* - `run` - the work happens and the approval step is skipped. The worklist writes a line saying so;
|
|
32
|
+
* until that record existed there was no way to tell it from a factory that approved it.
|
|
33
|
+
* - `refuse` - the caller is refused and told why the line does not stand.
|
|
34
|
+
* - `as-established` - the branch depends on **the row's own history**: a period declared through
|
|
35
|
+
* approval is withdrawn through approval, and one that stood without approval is simply removed.
|
|
36
|
+
* Only a door whose branch turns on the row may use this, and both branches are tested. The
|
|
37
|
+
* judgement stays with the door; the worklist asks it which way this row goes.
|
|
38
|
+
*/
|
|
39
|
+
export type WhenNoLine = 'refuse' | 'run' | 'as-established';
|
|
40
|
+
/**
|
|
41
|
+
* Why a door refuses rather than running. Two doors that both refuse can refuse for different
|
|
42
|
+
* reasons, and an administrator reads the reason to know whether filling the line is the fix.
|
|
43
|
+
*
|
|
44
|
+
* - `irreversible` - what the door does cannot be taken back (`mutation-reversibility`).
|
|
45
|
+
* - `approval-is-the-act` - the door's whole work **is** the approval. Letting it through with no
|
|
46
|
+
* line would put an unsigned controlled document on the floor.
|
|
47
|
+
* - `lowers-a-control` - the door lowers a control that stands: turning document control off, or
|
|
48
|
+
* moving a maintenance due date. Such a door refuses whatever its reversibility says, because the
|
|
49
|
+
* time that passed under the lowered control does not come back.
|
|
50
|
+
*/
|
|
51
|
+
export type WhenNoLineBecause = 'irreversible' | 'approval-is-the-act' | 'lowers-a-control';
|
|
52
|
+
interface DoorBase {
|
|
53
|
+
/** The form name - the same key the callback is found by. Labels change; this does not. */
|
|
54
|
+
name: string;
|
|
55
|
+
/** A translation key in the declaring module's namespace. Never a word. */
|
|
56
|
+
labelKey: string;
|
|
57
|
+
/**
|
|
58
|
+
* This door borrows another door's approval line. Written once, in one place: two doors carrying
|
|
59
|
+
* the same line as a value drift, and then turning a control on and off are approved by different
|
|
60
|
+
* people (ADR-0047 ⑨).
|
|
61
|
+
*/
|
|
62
|
+
lineFollows?: string;
|
|
63
|
+
}
|
|
64
|
+
type ApprovalDoor = DoorBase & {
|
|
65
|
+
kind: 'approval';
|
|
66
|
+
} & ({
|
|
67
|
+
whenNoLine: 'run';
|
|
68
|
+
because?: never;
|
|
69
|
+
} | {
|
|
70
|
+
whenNoLine: 'refuse' | 'as-established';
|
|
71
|
+
because: WhenNoLineBecause;
|
|
72
|
+
});
|
|
73
|
+
/**
|
|
74
|
+
* `whenNoLine?: never` is the point of this type: an assignment door has no approval line, so the
|
|
75
|
+
* field cannot be written here at all. Left optional it would be filled in one day with a value that
|
|
76
|
+
* reads true and means nothing.
|
|
77
|
+
*/
|
|
78
|
+
type AssignmentDoor = DoorBase & {
|
|
79
|
+
kind: 'assignment';
|
|
80
|
+
whenNoLine?: never;
|
|
81
|
+
because?: never;
|
|
82
|
+
};
|
|
83
|
+
export type WorkDoorDeclaration = ApprovalDoor | AssignmentDoor;
|
|
84
|
+
export interface WorkDoorRegistration {
|
|
85
|
+
/** The declaring module, as its package's short name. It groups the screen and names the source. */
|
|
86
|
+
module: string;
|
|
87
|
+
doors: readonly WorkDoorDeclaration[];
|
|
88
|
+
/**
|
|
89
|
+
* The role seats this module offers, by name.
|
|
90
|
+
*
|
|
91
|
+
* An approval line points at roles, so a domain with none of them cannot build one - and today
|
|
92
|
+
* that is found out on the form screen, too late. The door screen counts how many of these stand
|
|
93
|
+
* here and says what to do first. A domain's own roles are not counted: this number answers "of
|
|
94
|
+
* what the product offered", and a line built from other roles works just as well.
|
|
95
|
+
*/
|
|
96
|
+
seats?: readonly string[];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* An application declares its doors. Called at module import time, once per module.
|
|
100
|
+
*
|
|
101
|
+
* Re-registering the same module replaces it rather than appending: a module imported twice under a
|
|
102
|
+
* watch rebuild would otherwise show every door twice, and a screen that double-counts the doors
|
|
103
|
+
* that need attention is one nobody trusts.
|
|
104
|
+
*/
|
|
105
|
+
export declare function registerWorkDoors(registration: WorkDoorRegistration): void;
|
|
106
|
+
/** Every registration in this installation, in registration order. */
|
|
107
|
+
export declare function registeredWorkDoors(): WorkDoorRegistration[];
|
|
108
|
+
/** Every declared door, flattened, in registration order. */
|
|
109
|
+
export declare function declaredDoors(): WorkDoorDeclaration[];
|
|
110
|
+
/** Every role seat the declaring modules offer, by name, without repeats. */
|
|
111
|
+
export declare function offeredSeatNames(): string[];
|
|
112
|
+
/** One door by form name, whichever module declared it. */
|
|
113
|
+
export declare function declaredDoor(name: string): WorkDoorDeclaration | undefined;
|
|
114
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* **What an application declares about its own work doors** (ADR-0069 decision 3, ADR-0077).
|
|
4
|
+
*
|
|
5
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
6
|
+
* ── Who says what ─────────────────────────────────────────────────────────
|
|
7
|
+
* The form is installed by the product at boot, and the plant fills in who approves it. Filling it
|
|
8
|
+
* in is the declaration that this factory uses that step. So two facts have two owners:
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* the list of doors, and what each one is the application - it declares them here
|
|
12
|
+
* whether a door stands in this domain the worklist - it reads the activity and its line
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* An application registers its list; the screens and the judgement live here. Without that split the
|
|
16
|
+
* worklist would have to know operato-plant's activities, and every application would carry its own
|
|
17
|
+
* copy of the same screen (operato-warehouse asked for it second, which is what moved this).
|
|
18
|
+
*
|
|
19
|
+
* ── Registration happens at module import ─────────────────────────────────
|
|
20
|
+
* Not on a bootstrap hook. A hook runs after the schema is built, which is too late for anything the
|
|
21
|
+
* schema or a boot-time install reads - `declareAxes` in auth-base carries the same warning.
|
|
22
|
+
*
|
|
23
|
+
* ── Labels travel as keys, never as words ─────────────────────────────────
|
|
24
|
+
* `labelKey` names a translation in the declaring module's own namespace. A word here would put one
|
|
25
|
+
* product's vocabulary ("plant", "factory floor") on every other product's screen.
|
|
26
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.registerWorkDoors = registerWorkDoors;
|
|
30
|
+
exports.registeredWorkDoors = registeredWorkDoors;
|
|
31
|
+
exports.declaredDoors = declaredDoors;
|
|
32
|
+
exports.offeredSeatNames = offeredSeatNames;
|
|
33
|
+
exports.declaredDoor = declaredDoor;
|
|
34
|
+
const registered = new Map();
|
|
35
|
+
/**
|
|
36
|
+
* An application declares its doors. Called at module import time, once per module.
|
|
37
|
+
*
|
|
38
|
+
* Re-registering the same module replaces it rather than appending: a module imported twice under a
|
|
39
|
+
* watch rebuild would otherwise show every door twice, and a screen that double-counts the doors
|
|
40
|
+
* that need attention is one nobody trusts.
|
|
41
|
+
*/
|
|
42
|
+
function registerWorkDoors(registration) {
|
|
43
|
+
registered.set(registration.module, registration);
|
|
44
|
+
}
|
|
45
|
+
/** Every registration in this installation, in registration order. */
|
|
46
|
+
function registeredWorkDoors() {
|
|
47
|
+
return [...registered.values()];
|
|
48
|
+
}
|
|
49
|
+
/** Every declared door, flattened, in registration order. */
|
|
50
|
+
function declaredDoors() {
|
|
51
|
+
return [...registered.values()].flatMap(one => [...one.doors]);
|
|
52
|
+
}
|
|
53
|
+
/** Every role seat the declaring modules offer, by name, without repeats. */
|
|
54
|
+
function offeredSeatNames() {
|
|
55
|
+
return [...new Set([...registered.values()].flatMap(one => [...(one.seats ?? [])]))];
|
|
56
|
+
}
|
|
57
|
+
/** One door by form name, whichever module declared it. */
|
|
58
|
+
function declaredDoor(name) {
|
|
59
|
+
for (const registration of registered.values()) {
|
|
60
|
+
const found = registration.doors.find(door => door.name === name);
|
|
61
|
+
if (found) {
|
|
62
|
+
return found;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=work-door-declaration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"work-door-declaration.js","sourceRoot":"","sources":["../../../server/service/work-door/work-door-declaration.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;;AAgFH,8CAEC;AAGD,kDAEC;AAGD,sCAEC;AAGD,4CAEC;AAGD,oCAUC;AAvCD,MAAM,UAAU,GAAG,IAAI,GAAG,EAAgC,CAAA;AAE1D;;;;;;GAMG;AACH,SAAgB,iBAAiB,CAAC,YAAkC;IAClE,UAAU,CAAC,GAAG,CAAC,YAAY,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;AACnD,CAAC;AAED,sEAAsE;AACtE,SAAgB,mBAAmB;IACjC,OAAO,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,CAAA;AACjC,CAAC;AAED,6DAA6D;AAC7D,SAAgB,aAAa;IAC3B,OAAO,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAA;AAChE,CAAC;AAED,6EAA6E;AAC7E,SAAgB,gBAAgB;IAC9B,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AACtF,CAAC;AAED,2DAA2D;AAC3D,SAAgB,YAAY,CAAC,IAAY;IACvC,KAAK,MAAM,YAAY,IAAI,UAAU,CAAC,MAAM,EAAE,EAAE,CAAC;QAC/C,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,CAAA;QAEjE,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,KAAK,CAAA;QACd,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAA;AAClB,CAAC","sourcesContent":["/**\n * **What an application declares about its own work doors** (ADR-0069 decision 3, ADR-0077).\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── Who says what ─────────────────────────────────────────────────────────\n * The form is installed by the product at boot, and the plant fills in who approves it. Filling it\n * in is the declaration that this factory uses that step. So two facts have two owners:\n *\n * ```\n * the list of doors, and what each one is the application - it declares them here\n * whether a door stands in this domain the worklist - it reads the activity and its line\n * ```\n *\n * An application registers its list; the screens and the judgement live here. Without that split the\n * worklist would have to know operato-plant's activities, and every application would carry its own\n * copy of the same screen (operato-warehouse asked for it second, which is what moved this).\n *\n * ── Registration happens at module import ─────────────────────────────────\n * Not on a bootstrap hook. A hook runs after the schema is built, which is too late for anything the\n * schema or a boot-time install reads - `declareAxes` in auth-base carries the same warning.\n *\n * ── Labels travel as keys, never as words ─────────────────────────────────\n * `labelKey` names a translation in the declaring module's own namespace. A word here would put one\n * product's vocabulary (\"plant\", \"factory floor\") on every other product's screen.\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\n/**\n * What happens when the approval line is empty. **This records what each door already does** - it is\n * not a new policy (ADR-0077 decision 1).\n *\n * - `run` - the work happens and the approval step is skipped. The worklist writes a line saying so;\n * until that record existed there was no way to tell it from a factory that approved it.\n * - `refuse` - the caller is refused and told why the line does not stand.\n * - `as-established` - the branch depends on **the row's own history**: a period declared through\n * approval is withdrawn through approval, and one that stood without approval is simply removed.\n * Only a door whose branch turns on the row may use this, and both branches are tested. The\n * judgement stays with the door; the worklist asks it which way this row goes.\n */\nexport type WhenNoLine = 'refuse' | 'run' | 'as-established'\n\n/**\n * Why a door refuses rather than running. Two doors that both refuse can refuse for different\n * reasons, and an administrator reads the reason to know whether filling the line is the fix.\n *\n * - `irreversible` - what the door does cannot be taken back (`mutation-reversibility`).\n * - `approval-is-the-act` - the door's whole work **is** the approval. Letting it through with no\n * line would put an unsigned controlled document on the floor.\n * - `lowers-a-control` - the door lowers a control that stands: turning document control off, or\n * moving a maintenance due date. Such a door refuses whatever its reversibility says, because the\n * time that passed under the lowered control does not come back.\n */\nexport type WhenNoLineBecause = 'irreversible' | 'approval-is-the-act' | 'lowers-a-control'\n\ninterface DoorBase {\n /** The form name - the same key the callback is found by. Labels change; this does not. */\n name: string\n /** A translation key in the declaring module's namespace. Never a word. */\n labelKey: string\n /**\n * This door borrows another door's approval line. Written once, in one place: two doors carrying\n * the same line as a value drift, and then turning a control on and off are approved by different\n * people (ADR-0047 ⑨).\n */\n lineFollows?: string\n}\n\ntype ApprovalDoor = DoorBase & { kind: 'approval' } & (\n | { whenNoLine: 'run'; because?: never }\n | { whenNoLine: 'refuse' | 'as-established'; because: WhenNoLineBecause }\n )\n\n/**\n * `whenNoLine?: never` is the point of this type: an assignment door has no approval line, so the\n * field cannot be written here at all. Left optional it would be filled in one day with a value that\n * reads true and means nothing.\n */\ntype AssignmentDoor = DoorBase & { kind: 'assignment'; whenNoLine?: never; because?: never }\n\nexport type WorkDoorDeclaration = ApprovalDoor | AssignmentDoor\n\nexport interface WorkDoorRegistration {\n /** The declaring module, as its package's short name. It groups the screen and names the source. */\n module: string\n doors: readonly WorkDoorDeclaration[]\n /**\n * The role seats this module offers, by name.\n *\n * An approval line points at roles, so a domain with none of them cannot build one - and today\n * that is found out on the form screen, too late. The door screen counts how many of these stand\n * here and says what to do first. A domain's own roles are not counted: this number answers \"of\n * what the product offered\", and a line built from other roles works just as well.\n */\n seats?: readonly string[]\n}\n\nconst registered = new Map<string, WorkDoorRegistration>()\n\n/**\n * An application declares its doors. Called at module import time, once per module.\n *\n * Re-registering the same module replaces it rather than appending: a module imported twice under a\n * watch rebuild would otherwise show every door twice, and a screen that double-counts the doors\n * that need attention is one nobody trusts.\n */\nexport function registerWorkDoors(registration: WorkDoorRegistration): void {\n registered.set(registration.module, registration)\n}\n\n/** Every registration in this installation, in registration order. */\nexport function registeredWorkDoors(): WorkDoorRegistration[] {\n return [...registered.values()]\n}\n\n/** Every declared door, flattened, in registration order. */\nexport function declaredDoors(): WorkDoorDeclaration[] {\n return [...registered.values()].flatMap(one => [...one.doors])\n}\n\n/** Every role seat the declaring modules offer, by name, without repeats. */\nexport function offeredSeatNames(): string[] {\n return [...new Set([...registered.values()].flatMap(one => [...(one.seats ?? [])]))]\n}\n\n/** One door by form name, whichever module declared it. */\nexport function declaredDoor(name: string): WorkDoorDeclaration | undefined {\n for (const registration of registered.values()) {\n const found = registration.doors.find(door => door.name === name)\n\n if (found) {\n return found\n }\n }\n\n return undefined\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What happened at a work door — **the enum on its own, with nothing to load.**
|
|
3
|
+
*
|
|
4
|
+
* The entity that stores it carries TypeORM decorators, and importing those into a unit test pulls
|
|
5
|
+
* in the whole storage layer (the repository's jest config says as much: pure logic goes in its own
|
|
6
|
+
* module and is tested there). So the three names live here, and the entity and the writing side
|
|
7
|
+
* both read them from this file.
|
|
8
|
+
*
|
|
9
|
+
* The three are different events, not three reasons for one event. Read as reasons, the next person
|
|
10
|
+
* adds a fourth string and nothing tells them the row now means something else - which is why
|
|
11
|
+
* `subject` and `detail` follow the kind rather than sitting optional beside it.
|
|
12
|
+
*/
|
|
13
|
+
export declare enum WorkDoorEvent {
|
|
14
|
+
/** The approval line was empty and this door runs anyway - the work happened, unapproved. */
|
|
15
|
+
RanWithoutLine = "ran-without-line",
|
|
16
|
+
/** The domain declared it does not use this door, so the work happened without it. */
|
|
17
|
+
RanNotUsed = "ran-not-used",
|
|
18
|
+
/** A "we do not use this door" declaration was withdrawn; what it said is kept with the line. */
|
|
19
|
+
NotUsedWithdrawn = "not-used-withdrawn"
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WorkDoorEvent = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* What happened at a work door — **the enum on its own, with nothing to load.**
|
|
6
|
+
*
|
|
7
|
+
* The entity that stores it carries TypeORM decorators, and importing those into a unit test pulls
|
|
8
|
+
* in the whole storage layer (the repository's jest config says as much: pure logic goes in its own
|
|
9
|
+
* module and is tested there). So the three names live here, and the entity and the writing side
|
|
10
|
+
* both read them from this file.
|
|
11
|
+
*
|
|
12
|
+
* The three are different events, not three reasons for one event. Read as reasons, the next person
|
|
13
|
+
* adds a fourth string and nothing tells them the row now means something else - which is why
|
|
14
|
+
* `subject` and `detail` follow the kind rather than sitting optional beside it.
|
|
15
|
+
*/
|
|
16
|
+
var WorkDoorEvent;
|
|
17
|
+
(function (WorkDoorEvent) {
|
|
18
|
+
/** The approval line was empty and this door runs anyway - the work happened, unapproved. */
|
|
19
|
+
WorkDoorEvent["RanWithoutLine"] = "ran-without-line";
|
|
20
|
+
/** The domain declared it does not use this door, so the work happened without it. */
|
|
21
|
+
WorkDoorEvent["RanNotUsed"] = "ran-not-used";
|
|
22
|
+
/** A "we do not use this door" declaration was withdrawn; what it said is kept with the line. */
|
|
23
|
+
WorkDoorEvent["NotUsedWithdrawn"] = "not-used-withdrawn";
|
|
24
|
+
})(WorkDoorEvent || (exports.WorkDoorEvent = WorkDoorEvent = {}));
|
|
25
|
+
//# sourceMappingURL=work-door-event.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"work-door-event.js","sourceRoot":"","sources":["../../../server/service/work-door/work-door-event.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;GAWG;AACH,IAAY,aAOX;AAPD,WAAY,aAAa;IACvB,6FAA6F;IAC7F,oDAAmC,CAAA;IACnC,sFAAsF;IACtF,4CAA2B,CAAA;IAC3B,iGAAiG;IACjG,wDAAuC,CAAA;AACzC,CAAC,EAPW,aAAa,6BAAb,aAAa,QAOxB","sourcesContent":["/**\n * What happened at a work door — **the enum on its own, with nothing to load.**\n *\n * The entity that stores it carries TypeORM decorators, and importing those into a unit test pulls\n * in the whole storage layer (the repository's jest config says as much: pure logic goes in its own\n * module and is tested there). So the three names live here, and the entity and the writing side\n * both read them from this file.\n *\n * The three are different events, not three reasons for one event. Read as reasons, the next person\n * adds a fourth string and nothing tells them the row now means something else - which is why\n * `subject` and `detail` follow the kind rather than sitting optional beside it.\n */\nexport enum WorkDoorEvent {\n /** The approval line was empty and this door runs anyway - the work happened, unapproved. */\n RanWithoutLine = 'ran-without-line',\n /** The domain declared it does not use this door, so the work happened without it. */\n RanNotUsed = 'ran-not-used',\n /** A \"we do not use this door\" declaration was withdrawn; what it said is kept with the line. */\n NotUsedWithdrawn = 'not-used-withdrawn'\n}\n"]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { EntityManager } from 'typeorm';
|
|
2
|
+
import { Domain } from '@things-factory/shell';
|
|
3
|
+
import { User } from '@things-factory/auth-base';
|
|
4
|
+
import { WorkDoorEvent } from './work-door-event.js';
|
|
5
|
+
import type { WorkDoorJournal } from './work-door-journal.js';
|
|
6
|
+
/**
|
|
7
|
+
* Writing a line in a work door's log.
|
|
8
|
+
*
|
|
9
|
+
* ── The union is the point ────────────────────────────────────────────────
|
|
10
|
+
* `subject` belongs to something that happened to a row; `detail` belongs to a withdrawn
|
|
11
|
+
* declaration. An entry type with both optional would compile with either missing, and the first
|
|
12
|
+
* caller to forget `subject` would write a line saying that something ran, about nothing. So each
|
|
13
|
+
* kind names exactly what it carries, and `?: never` closes the other field.
|
|
14
|
+
*/
|
|
15
|
+
export type WorkDoorEntry = {
|
|
16
|
+
kind: WorkDoorEvent.RanWithoutLine | WorkDoorEvent.RanNotUsed;
|
|
17
|
+
door: string;
|
|
18
|
+
/** What it was about, as the application names it. The worklist never reads inside these. */
|
|
19
|
+
subject: {
|
|
20
|
+
kind: string;
|
|
21
|
+
id: string;
|
|
22
|
+
};
|
|
23
|
+
detail?: never;
|
|
24
|
+
} | {
|
|
25
|
+
kind: WorkDoorEvent.NotUsedWithdrawn;
|
|
26
|
+
door: string;
|
|
27
|
+
subject?: never;
|
|
28
|
+
/** What the withdrawn declaration said - who declared it, when, and why. */
|
|
29
|
+
detail: Record<string, unknown>;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Write one line.
|
|
33
|
+
*
|
|
34
|
+
* **`manager` has to be the caller's open transaction.** The line and the work it describes commit
|
|
35
|
+
* together or not at all: a line that commits first and work that then fails leaves the log saying
|
|
36
|
+
* something happened that did not, and the other order leaves the work with no trace - which is the
|
|
37
|
+
* hole this log exists to close. `passWorkDoor` takes the transaction for the same reason, and the
|
|
38
|
+
* outbox's sequence carries the same rule one layer down.
|
|
39
|
+
*/
|
|
40
|
+
export declare function noteWorkDoor(manager: EntityManager, domain: Domain, entry: WorkDoorEntry, who?: User): Promise<WorkDoorJournal>;
|
|
41
|
+
/**
|
|
42
|
+
* What the last withdrawn declaration for this door said, if there is one.
|
|
43
|
+
*
|
|
44
|
+
* This is the read that makes the log worth writing: somebody declaring "we do not use this door"
|
|
45
|
+
* again wants to see what was said the previous time.
|
|
46
|
+
*/
|
|
47
|
+
export declare function lastWithdrawnDeclaration(manager: EntityManager, domain: Domain, door: string): Promise<Record<string, unknown> | undefined>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.noteWorkDoor = noteWorkDoor;
|
|
37
|
+
exports.lastWithdrawnDeclaration = lastWithdrawnDeclaration;
|
|
38
|
+
const work_door_event_js_1 = require("./work-door-event.js");
|
|
39
|
+
/**
|
|
40
|
+
* Write one line.
|
|
41
|
+
*
|
|
42
|
+
* **`manager` has to be the caller's open transaction.** The line and the work it describes commit
|
|
43
|
+
* together or not at all: a line that commits first and work that then fails leaves the log saying
|
|
44
|
+
* something happened that did not, and the other order leaves the work with no trace - which is the
|
|
45
|
+
* hole this log exists to close. `passWorkDoor` takes the transaction for the same reason, and the
|
|
46
|
+
* outbox's sequence carries the same rule one layer down.
|
|
47
|
+
*/
|
|
48
|
+
async function noteWorkDoor(manager, domain, entry, who) {
|
|
49
|
+
/* 엔티티는 여기서 늦게 가져온다 — 이 모듈을 부르는 순수 시험이 저장소 계층을 안 들이게. */
|
|
50
|
+
const { WorkDoorJournal } = await Promise.resolve().then(() => __importStar(require('./work-door-journal.js')));
|
|
51
|
+
return await manager.getRepository(WorkDoorJournal).save({
|
|
52
|
+
domain,
|
|
53
|
+
kind: entry.kind,
|
|
54
|
+
door: entry.door,
|
|
55
|
+
subjectKind: entry.subject?.kind,
|
|
56
|
+
subjectId: entry.subject?.id,
|
|
57
|
+
detail: entry.detail,
|
|
58
|
+
who
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* What the last withdrawn declaration for this door said, if there is one.
|
|
63
|
+
*
|
|
64
|
+
* This is the read that makes the log worth writing: somebody declaring "we do not use this door"
|
|
65
|
+
* again wants to see what was said the previous time.
|
|
66
|
+
*/
|
|
67
|
+
async function lastWithdrawnDeclaration(manager, domain, door) {
|
|
68
|
+
const { WorkDoorJournal } = await Promise.resolve().then(() => __importStar(require('./work-door-journal.js')));
|
|
69
|
+
const found = await manager.getRepository(WorkDoorJournal).findOne({
|
|
70
|
+
where: { domain: { id: domain.id }, door, kind: work_door_event_js_1.WorkDoorEvent.NotUsedWithdrawn },
|
|
71
|
+
order: { createdAt: 'DESC' }
|
|
72
|
+
});
|
|
73
|
+
return found?.detail;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=work-door-journal-service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"work-door-journal-service.js","sourceRoot":"","sources":["../../../server/service/work-door/work-door-journal-service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,oCAkBC;AAQD,4DAaC;AA5ED,6DAAoD;AA4BpD;;;;;;;;GAQG;AACI,KAAK,UAAU,YAAY,CAChC,OAAsB,EACtB,MAAc,EACd,KAAoB,EACpB,GAAU;IAEV,wDAAwD;IACxD,MAAM,EAAE,eAAe,EAAE,GAAG,wDAAa,wBAAwB,GAAC,CAAA;IAElE,OAAO,MAAM,OAAO,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC;QACvD,MAAM;QACN,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,WAAW,EAAE,KAAK,CAAC,OAAO,EAAE,IAAI;QAChC,SAAS,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE;QAC5B,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,GAAG;KACJ,CAAC,CAAA;AACJ,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,wBAAwB,CAC5C,OAAsB,EACtB,MAAc,EACd,IAAY;IAEZ,MAAM,EAAE,eAAe,EAAE,GAAG,wDAAa,wBAAwB,GAAC,CAAA;IAElE,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,OAAO,CAAC;QACjE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,kCAAa,CAAC,gBAAgB,EAAE;QAChF,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE;KAC7B,CAAC,CAAA;IAEF,OAAO,KAAK,EAAE,MAAM,CAAA;AACtB,CAAC","sourcesContent":["import { EntityManager } from 'typeorm'\n\nimport { Domain } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\n\nimport { WorkDoorEvent } from './work-door-event.js'\nimport type { WorkDoorJournal } from './work-door-journal.js'\n\n/**\n * Writing a line in a work door's log.\n *\n * ── The union is the point ────────────────────────────────────────────────\n * `subject` belongs to something that happened to a row; `detail` belongs to a withdrawn\n * declaration. An entry type with both optional would compile with either missing, and the first\n * caller to forget `subject` would write a line saying that something ran, about nothing. So each\n * kind names exactly what it carries, and `?: never` closes the other field.\n */\nexport type WorkDoorEntry =\n | {\n kind: WorkDoorEvent.RanWithoutLine | WorkDoorEvent.RanNotUsed\n door: string\n /** What it was about, as the application names it. The worklist never reads inside these. */\n subject: { kind: string; id: string }\n detail?: never\n }\n | {\n kind: WorkDoorEvent.NotUsedWithdrawn\n door: string\n subject?: never\n /** What the withdrawn declaration said - who declared it, when, and why. */\n detail: Record<string, unknown>\n }\n\n/**\n * Write one line.\n *\n * **`manager` has to be the caller's open transaction.** The line and the work it describes commit\n * together or not at all: a line that commits first and work that then fails leaves the log saying\n * something happened that did not, and the other order leaves the work with no trace - which is the\n * hole this log exists to close. `passWorkDoor` takes the transaction for the same reason, and the\n * outbox's sequence carries the same rule one layer down.\n */\nexport async function noteWorkDoor(\n manager: EntityManager,\n domain: Domain,\n entry: WorkDoorEntry,\n who?: User\n): Promise<WorkDoorJournal> {\n /* 엔티티는 여기서 늦게 가져온다 — 이 모듈을 부르는 순수 시험이 저장소 계층을 안 들이게. */\n const { WorkDoorJournal } = await import('./work-door-journal.js')\n\n return await manager.getRepository(WorkDoorJournal).save({\n domain,\n kind: entry.kind,\n door: entry.door,\n subjectKind: entry.subject?.kind,\n subjectId: entry.subject?.id,\n detail: entry.detail,\n who\n })\n}\n\n/**\n * What the last withdrawn declaration for this door said, if there is one.\n *\n * This is the read that makes the log worth writing: somebody declaring \"we do not use this door\"\n * again wants to see what was said the previous time.\n */\nexport async function lastWithdrawnDeclaration(\n manager: EntityManager,\n domain: Domain,\n door: string\n): Promise<Record<string, unknown> | undefined> {\n const { WorkDoorJournal } = await import('./work-door-journal.js')\n\n const found = await manager.getRepository(WorkDoorJournal).findOne({\n where: { domain: { id: domain.id }, door, kind: WorkDoorEvent.NotUsedWithdrawn },\n order: { createdAt: 'DESC' }\n })\n\n return found?.detail\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Domain } from '@things-factory/shell';
|
|
2
|
+
import { WorkDoorEvent } from './work-door-event.js';
|
|
3
|
+
import { User } from '@things-factory/auth-base';
|
|
4
|
+
export declare class WorkDoorJournal {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
domain?: Domain;
|
|
7
|
+
domainId?: string;
|
|
8
|
+
kind: WorkDoorEvent;
|
|
9
|
+
/** The form name, the key the callback is found by - not the label, which changes. */
|
|
10
|
+
door: string;
|
|
11
|
+
/**
|
|
12
|
+
* What it was about, as two strings. **The worklist does not know the application's entities**, so
|
|
13
|
+
* a lot, a job order and a screen all arrive the same way: a kind the application names and the id
|
|
14
|
+
* it knows the row by.
|
|
15
|
+
*
|
|
16
|
+
* Empty for `not-used-withdrawn`: withdrawing a declaration is about the door itself.
|
|
17
|
+
*/
|
|
18
|
+
subjectKind?: string;
|
|
19
|
+
subjectId?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Only `not-used-withdrawn` carries this: what the withdrawn declaration said (who declared it,
|
|
22
|
+
* when, and why they said this factory does not use the door). Deleting the declaration would
|
|
23
|
+
* otherwise take that with it (ADR-0064 ③), and declaring it again is when somebody wants to read
|
|
24
|
+
* what was said last time.
|
|
25
|
+
*/
|
|
26
|
+
detail?: Record<string, unknown>;
|
|
27
|
+
who?: User;
|
|
28
|
+
createdAt: Date;
|
|
29
|
+
}
|
|
30
|
+
export { WorkDoorEvent };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WorkDoorEvent = exports.WorkDoorJournal = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const typeorm_1 = require("typeorm");
|
|
6
|
+
const type_graphql_1 = require("type-graphql");
|
|
7
|
+
const shell_1 = require("@things-factory/shell");
|
|
8
|
+
const work_door_event_js_1 = require("./work-door-event.js");
|
|
9
|
+
Object.defineProperty(exports, "WorkDoorEvent", { enumerable: true, get: function () { return work_door_event_js_1.WorkDoorEvent; } });
|
|
10
|
+
const auth_base_1 = require("@things-factory/auth-base");
|
|
11
|
+
/**
|
|
12
|
+
* **A work door's log** — what happened at a door when no approval instance was left behind
|
|
13
|
+
* (ADR-0077 decision 1).
|
|
14
|
+
*
|
|
15
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
+
* ── Why a row is needed at all ────────────────────────────────────────────
|
|
17
|
+
* A door that was approved leaves an `ActivityInstance`, and the instance is the record. A door
|
|
18
|
+
* whose approval line is empty leaves **nothing**: the work simply happens. Counted on 2026-09-19,
|
|
19
|
+
* eight of operato-plant's approval doors run that way, and none of them writes a trace - a
|
|
20
|
+
* production correction made in a factory with no approval line is stored exactly like one that was
|
|
21
|
+
* approved, and exactly like one made in a factory that does not use approvals at all. Three
|
|
22
|
+
* different situations, one appearance.
|
|
23
|
+
*
|
|
24
|
+
* That matters where it is read: an auditor asking "who approved this correction" gets silence, and
|
|
25
|
+
* silence reads as "nobody looked" whether or not anybody was ever supposed to.
|
|
26
|
+
*
|
|
27
|
+
* ── Why the kinds are a closed list ───────────────────────────────────────
|
|
28
|
+
* The three are different events, not three reasons for one event. Reading them as reasons is how a
|
|
29
|
+
* log turns into a bag: the next person adds a fourth string and nothing tells them the row now
|
|
30
|
+
* means something else. `subject` and `detail` follow the kind (see `work-door-journal-service`),
|
|
31
|
+
* which is why the writing side takes a union rather than an object with everything optional.
|
|
32
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
33
|
+
*/
|
|
34
|
+
(0, type_graphql_1.registerEnumType)(work_door_event_js_1.WorkDoorEvent, {
|
|
35
|
+
name: 'WorkDoorEvent',
|
|
36
|
+
description: 'what happened at a work door that left no approval instance behind'
|
|
37
|
+
});
|
|
38
|
+
let WorkDoorJournal = class WorkDoorJournal {
|
|
39
|
+
};
|
|
40
|
+
exports.WorkDoorJournal = WorkDoorJournal;
|
|
41
|
+
tslib_1.__decorate([
|
|
42
|
+
(0, typeorm_1.Column)({ primary: true, type: 'uuid', generated: 'uuid' }),
|
|
43
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.ID),
|
|
44
|
+
tslib_1.__metadata("design:type", String)
|
|
45
|
+
], WorkDoorJournal.prototype, "id", void 0);
|
|
46
|
+
tslib_1.__decorate([
|
|
47
|
+
(0, typeorm_1.ManyToOne)(type => shell_1.Domain),
|
|
48
|
+
tslib_1.__metadata("design:type", shell_1.Domain)
|
|
49
|
+
], WorkDoorJournal.prototype, "domain", void 0);
|
|
50
|
+
tslib_1.__decorate([
|
|
51
|
+
(0, typeorm_1.RelationId)((row) => row.domain),
|
|
52
|
+
tslib_1.__metadata("design:type", String)
|
|
53
|
+
], WorkDoorJournal.prototype, "domainId", void 0);
|
|
54
|
+
tslib_1.__decorate([
|
|
55
|
+
(0, typeorm_1.Column)(),
|
|
56
|
+
(0, type_graphql_1.Field)(type => work_door_event_js_1.WorkDoorEvent, { description: 'what happened' }),
|
|
57
|
+
tslib_1.__metadata("design:type", String)
|
|
58
|
+
], WorkDoorJournal.prototype, "kind", void 0);
|
|
59
|
+
tslib_1.__decorate([
|
|
60
|
+
(0, typeorm_1.Column)(),
|
|
61
|
+
(0, type_graphql_1.Field)({ description: 'the form name of the door this happened at' }),
|
|
62
|
+
tslib_1.__metadata("design:type", String)
|
|
63
|
+
], WorkDoorJournal.prototype, "door", void 0);
|
|
64
|
+
tslib_1.__decorate([
|
|
65
|
+
(0, typeorm_1.Column)({ nullable: true }),
|
|
66
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'the kind of thing this was about, as the application names it' }),
|
|
67
|
+
tslib_1.__metadata("design:type", String)
|
|
68
|
+
], WorkDoorJournal.prototype, "subjectKind", void 0);
|
|
69
|
+
tslib_1.__decorate([
|
|
70
|
+
(0, typeorm_1.Column)({ nullable: true }),
|
|
71
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'the id of the thing this was about' }),
|
|
72
|
+
tslib_1.__metadata("design:type", String)
|
|
73
|
+
], WorkDoorJournal.prototype, "subjectId", void 0);
|
|
74
|
+
tslib_1.__decorate([
|
|
75
|
+
(0, typeorm_1.Column)({ type: 'simple-json', nullable: true }),
|
|
76
|
+
(0, type_graphql_1.Field)(type => shell_1.ScalarObject, { nullable: true, description: 'what the withdrawn declaration said' }),
|
|
77
|
+
tslib_1.__metadata("design:type", Object)
|
|
78
|
+
], WorkDoorJournal.prototype, "detail", void 0);
|
|
79
|
+
tslib_1.__decorate([
|
|
80
|
+
(0, typeorm_1.ManyToOne)(type => auth_base_1.User, { nullable: true }),
|
|
81
|
+
(0, type_graphql_1.Field)(type => auth_base_1.User, { nullable: true }),
|
|
82
|
+
tslib_1.__metadata("design:type", auth_base_1.User)
|
|
83
|
+
], WorkDoorJournal.prototype, "who", void 0);
|
|
84
|
+
tslib_1.__decorate([
|
|
85
|
+
(0, typeorm_1.CreateDateColumn)(),
|
|
86
|
+
(0, type_graphql_1.Field)({ description: 'when this happened' }),
|
|
87
|
+
tslib_1.__metadata("design:type", Date)
|
|
88
|
+
], WorkDoorJournal.prototype, "createdAt", void 0);
|
|
89
|
+
exports.WorkDoorJournal = WorkDoorJournal = tslib_1.__decorate([
|
|
90
|
+
(0, typeorm_1.Entity)(),
|
|
91
|
+
(0, typeorm_1.Index)('ix_work_door_journal_0', (row) => [row.domain, row.door, row.createdAt]),
|
|
92
|
+
(0, type_graphql_1.ObjectType)({ description: 'What happened at a work door that left no approval instance behind' })
|
|
93
|
+
], WorkDoorJournal);
|
|
94
|
+
//# sourceMappingURL=work-door-journal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"work-door-journal.js","sourceRoot":"","sources":["../../../server/service/work-door/work-door-journal.ts"],"names":[],"mappings":";;;;AAAA,qCAAwF;AACxF,+CAAsE;AAEtE,iDAA4D;AAE5D,6DAAoD;AAyF3C,8FAzFA,kCAAa,OAyFA;AAxFtB,yDAAgD;AAEhD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,IAAA,+BAAgB,EAAC,kCAAa,EAAE;IAC9B,IAAI,EAAE,eAAe;IACrB,WAAW,EAAE,oEAAoE;CAClF,CAAC,CAAA;AAKK,IAAM,eAAe,GAArB,MAAM,eAAe;CAoD3B,CAAA;AApDY,0CAAe;AAGjB;IAFR,IAAA,gBAAM,EAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAC1D,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAE,CAAC;;2CACC;AAGnB;IADC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,CAAC;sCACjB,cAAM;+CAAA;AAGf;IADC,IAAA,oBAAU,EAAC,CAAC,GAAoB,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC;;iDAChC;AAIjB;IAFC,IAAA,gBAAM,GAAE;IACR,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kCAAa,EAAE,EAAE,WAAW,EAAE,eAAe,EAAE,CAAC;;6CAC5C;AAKnB;IAFC,IAAA,gBAAM,GAAE;IACR,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,4CAA4C,EAAE,CAAC;;6CACzD;AAWZ;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,+DAA+D,EAAE,CAAC;;oDACpF;AAIpB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,oCAAoC,EAAE,CAAC;;kDAC3D;AAUlB;IAFC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC/C,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,oBAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,qCAAqC,EAAE,CAAC;;+CACpE;AAIhC;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC3C,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCAClC,gBAAI;4CAAA;AAIV;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,oBAAoB,EAAE,CAAC;sCAClC,IAAI;kDAAA;0BAnDJ,eAAe;IAH3B,IAAA,gBAAM,GAAE;IACR,IAAA,eAAK,EAAC,wBAAwB,EAAE,CAAC,GAAoB,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC;IAChG,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,oEAAoE,EAAE,CAAC;GACrF,eAAe,CAoD3B","sourcesContent":["import { Column, CreateDateColumn, Entity, Index, ManyToOne, RelationId } from 'typeorm'\nimport { Field, ID, ObjectType, registerEnumType } from 'type-graphql'\n\nimport { Domain, ScalarObject } from '@things-factory/shell'\n\nimport { WorkDoorEvent } from './work-door-event.js'\nimport { User } from '@things-factory/auth-base'\n\n/**\n * **A work door's log** — what happened at a door when no approval instance was left behind\n * (ADR-0077 decision 1).\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ── Why a row is needed at all ────────────────────────────────────────────\n * A door that was approved leaves an `ActivityInstance`, and the instance is the record. A door\n * whose approval line is empty leaves **nothing**: the work simply happens. Counted on 2026-09-19,\n * eight of operato-plant's approval doors run that way, and none of them writes a trace - a\n * production correction made in a factory with no approval line is stored exactly like one that was\n * approved, and exactly like one made in a factory that does not use approvals at all. Three\n * different situations, one appearance.\n *\n * That matters where it is read: an auditor asking \"who approved this correction\" gets silence, and\n * silence reads as \"nobody looked\" whether or not anybody was ever supposed to.\n *\n * ── Why the kinds are a closed list ───────────────────────────────────────\n * The three are different events, not three reasons for one event. Reading them as reasons is how a\n * log turns into a bag: the next person adds a fourth string and nothing tells them the row now\n * means something else. `subject` and `detail` follow the kind (see `work-door-journal-service`),\n * which is why the writing side takes a union rather than an object with everything optional.\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\nregisterEnumType(WorkDoorEvent, {\n name: 'WorkDoorEvent',\n description: 'what happened at a work door that left no approval instance behind'\n})\n\n@Entity()\n@Index('ix_work_door_journal_0', (row: WorkDoorJournal) => [row.domain, row.door, row.createdAt])\n@ObjectType({ description: 'What happened at a work door that left no approval instance behind' })\nexport class WorkDoorJournal {\n @Column({ primary: true, type: 'uuid', generated: 'uuid' })\n @Field(type => ID)\n readonly id: string\n\n @ManyToOne(type => Domain)\n domain?: Domain\n\n @RelationId((row: WorkDoorJournal) => row.domain)\n domainId?: string\n\n @Column()\n @Field(type => WorkDoorEvent, { description: 'what happened' })\n kind: WorkDoorEvent\n\n /** The form name, the key the callback is found by - not the label, which changes. */\n @Column()\n @Field({ description: 'the form name of the door this happened at' })\n door: string\n\n /**\n * What it was about, as two strings. **The worklist does not know the application's entities**, so\n * a lot, a job order and a screen all arrive the same way: a kind the application names and the id\n * it knows the row by.\n *\n * Empty for `not-used-withdrawn`: withdrawing a declaration is about the door itself.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'the kind of thing this was about, as the application names it' })\n subjectKind?: string\n\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'the id of the thing this was about' })\n subjectId?: string\n\n /**\n * Only `not-used-withdrawn` carries this: what the withdrawn declaration said (who declared it,\n * when, and why they said this factory does not use the door). Deleting the declaration would\n * otherwise take that with it (ADR-0064 ③), and declaring it again is when somebody wants to read\n * what was said last time.\n */\n @Column({ type: 'simple-json', nullable: true })\n @Field(type => ScalarObject, { nullable: true, description: 'what the withdrawn declaration said' })\n detail?: Record<string, unknown>\n\n @ManyToOne(type => User, { nullable: true })\n @Field(type => User, { nullable: true })\n who?: User\n\n @CreateDateColumn()\n @Field({ description: 'when this happened' })\n createdAt: Date\n}\n\nexport { WorkDoorEvent }\n"]}
|