@things-factory/station 10.1.33
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/README.md +124 -0
- package/dist-client/index.d.ts +11 -0
- package/dist-client/index.js +13 -0
- package/dist-client/index.js.map +1 -0
- package/dist-client/station/scene/index.d.ts +11 -0
- package/dist-client/station/scene/index.js +12 -0
- package/dist-client/station/scene/index.js.map +1 -0
- package/dist-client/station/scene/station-context-binding.d.ts +25 -0
- package/dist-client/station/scene/station-context-binding.js +39 -0
- package/dist-client/station/scene/station-context-binding.js.map +1 -0
- package/dist-client/station/scene/station-context-scene.d.ts +28 -0
- package/dist-client/station/scene/station-context-scene.js +138 -0
- package/dist-client/station/scene/station-context-scene.js.map +1 -0
- package/dist-client/station/scene/station-counts-scene.d.ts +6 -0
- package/dist-client/station/scene/station-counts-scene.js +51 -0
- package/dist-client/station/scene/station-counts-scene.js.map +1 -0
- package/dist-client/station/scene/station-field-scene.d.ts +6 -0
- package/dist-client/station/scene/station-field-scene.js +89 -0
- package/dist-client/station/scene/station-field-scene.js.map +1 -0
- package/dist-client/station/scene/station-icons.d.ts +82 -0
- package/dist-client/station/scene/station-icons.js +131 -0
- package/dist-client/station/scene/station-icons.js.map +1 -0
- package/dist-client/station/scene/station-numpad-scene.d.ts +9 -0
- package/dist-client/station/scene/station-numpad-scene.js +144 -0
- package/dist-client/station/scene/station-numpad-scene.js.map +1 -0
- package/dist-client/station/scene/station-scene-style.d.ts +51 -0
- package/dist-client/station/scene/station-scene-style.js +108 -0
- package/dist-client/station/scene/station-scene-style.js.map +1 -0
- package/dist-client/station/scene/station-scene.d.ts +295 -0
- package/dist-client/station/scene/station-scene.js +68 -0
- package/dist-client/station/scene/station-scene.js.map +1 -0
- package/dist-client/station/scene/station-who-scene.d.ts +6 -0
- package/dist-client/station/scene/station-who-scene.js +48 -0
- package/dist-client/station/scene/station-who-scene.js.map +1 -0
- package/dist-client/station/scene/station-work-panel-scene.d.ts +28 -0
- package/dist-client/station/scene/station-work-panel-scene.js +37 -0
- package/dist-client/station/scene/station-work-panel-scene.js.map +1 -0
- package/dist-client/station/station-board-binding.d.ts +16 -0
- package/dist-client/station/station-board-binding.js +20 -0
- package/dist-client/station/station-board-binding.js.map +1 -0
- package/dist-client/station/station-context.d.ts +178 -0
- package/dist-client/station/station-context.js +30 -0
- package/dist-client/station/station-context.js.map +1 -0
- package/dist-client/station/station-palette.d.ts +34 -0
- package/dist-client/station/station-palette.js +119 -0
- package/dist-client/station/station-palette.js.map +1 -0
- package/dist-client/station/station-runtime.d.ts +47 -0
- package/dist-client/station/station-runtime.js +47 -0
- package/dist-client/station/station-runtime.js.map +1 -0
- package/dist-client/station/station-scene-context.d.ts +68 -0
- package/dist-client/station/station-scene-context.js +62 -0
- package/dist-client/station/station-scene-context.js.map +1 -0
- package/dist-client/station/station-screen-flow.d.ts +16 -0
- package/dist-client/station/station-screen-flow.js +58 -0
- package/dist-client/station/station-screen-flow.js.map +1 -0
- package/dist-client/station/station-tile-registry.d.ts +89 -0
- package/dist-client/station/station-tile-registry.js +86 -0
- package/dist-client/station/station-tile-registry.js.map +1 -0
- package/dist-client/station/station-work-panel-view.d.ts +23 -0
- package/dist-client/station/station-work-panel-view.js +125 -0
- package/dist-client/station/station-work-panel-view.js.map +1 -0
- package/dist-client/station/station-work-runtime.d.ts +47 -0
- package/dist-client/station/station-work-runtime.js +161 -0
- package/dist-client/station/station-work-runtime.js.map +1 -0
- package/dist-client/station/station-work-scenarios.d.ts +41 -0
- package/dist-client/station/station-work-scenarios.js +59 -0
- package/dist-client/station/station-work-scenarios.js.map +1 -0
- package/dist-client/station/task-counts.d.ts +32 -0
- package/dist-client/station/task-counts.js +26 -0
- package/dist-client/station/task-counts.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -0
- package/dist-server/activities/activity-station-screen-control-withdrawal.d.ts +62 -0
- package/dist-server/activities/activity-station-screen-control-withdrawal.js +147 -0
- package/dist-server/activities/activity-station-screen-control-withdrawal.js.map +1 -0
- package/dist-server/activities/activity-station-screen-release.d.ts +81 -0
- package/dist-server/activities/activity-station-screen-release.js +147 -0
- package/dist-server/activities/activity-station-screen-release.js.map +1 -0
- package/dist-server/activities/index.d.ts +49 -0
- package/dist-server/activities/index.js +21 -0
- package/dist-server/activities/index.js.map +1 -0
- package/dist-server/index.d.ts +3 -0
- package/dist-server/index.js +8 -0
- package/dist-server/index.js.map +1 -0
- package/dist-server/service/index.d.ts +23 -0
- package/dist-server/service/index.js +41 -0
- package/dist-server/service/index.js.map +1 -0
- package/dist-server/service/station/deletion-changes.d.ts +14 -0
- package/dist-server/service/station/deletion-changes.js +25 -0
- package/dist-server/service/station/deletion-changes.js.map +1 -0
- package/dist-server/service/station/index.d.ts +13 -0
- package/dist-server/service/station/index.js +25 -0
- package/dist-server/service/station/index.js.map +1 -0
- package/dist-server/service/station/reversibility.d.ts +21 -0
- package/dist-server/service/station/reversibility.js +24 -0
- package/dist-server/service/station/reversibility.js.map +1 -0
- package/dist-server/service/station/screen-model-digest.d.ts +60 -0
- package/dist-server/service/station/screen-model-digest.js +97 -0
- package/dist-server/service/station/screen-model-digest.js.map +1 -0
- package/dist-server/service/station/staged-material.d.ts +62 -0
- package/dist-server/service/station/staged-material.js +44 -0
- package/dist-server/service/station/staged-material.js.map +1 -0
- package/dist-server/service/station/station-argument-sources.d.ts +31 -0
- package/dist-server/service/station/station-argument-sources.js +66 -0
- package/dist-server/service/station/station-argument-sources.js.map +1 -0
- package/dist-server/service/station/station-assignment-choice.d.ts +81 -0
- package/dist-server/service/station/station-assignment-choice.js +105 -0
- package/dist-server/service/station/station-assignment-choice.js.map +1 -0
- package/dist-server/service/station/station-assignment-resolver.d.ts +104 -0
- package/dist-server/service/station/station-assignment-resolver.js +426 -0
- package/dist-server/service/station/station-assignment-resolver.js.map +1 -0
- package/dist-server/service/station/station-assignment-service.d.ts +33 -0
- package/dist-server/service/station/station-assignment-service.js +64 -0
- package/dist-server/service/station/station-assignment-service.js.map +1 -0
- package/dist-server/service/station/station-axes.d.ts +9 -0
- package/dist-server/service/station/station-axes.js +16 -0
- package/dist-server/service/station/station-axes.js.map +1 -0
- package/dist-server/service/station/station-flow.d.ts +156 -0
- package/dist-server/service/station/station-flow.js +255 -0
- package/dist-server/service/station/station-flow.js.map +1 -0
- package/dist-server/service/station/station-generate.d.ts +76 -0
- package/dist-server/service/station/station-generate.js +178 -0
- package/dist-server/service/station/station-generate.js.map +1 -0
- package/dist-server/service/station/station-product.d.ts +123 -0
- package/dist-server/service/station/station-product.js +77 -0
- package/dist-server/service/station/station-product.js.map +1 -0
- package/dist-server/service/station/station-runtime-binding.d.ts +6 -0
- package/dist-server/service/station/station-runtime-binding.js +46 -0
- package/dist-server/service/station/station-runtime-binding.js.map +1 -0
- package/dist-server/service/station/station-screen-assignment.d.ts +94 -0
- package/dist-server/service/station/station-screen-assignment.js +154 -0
- package/dist-server/service/station/station-screen-assignment.js.map +1 -0
- package/dist-server/service/station/station-screen-control-withdrawal-resolver.d.ts +37 -0
- package/dist-server/service/station/station-screen-control-withdrawal-resolver.js +130 -0
- package/dist-server/service/station/station-screen-control-withdrawal-resolver.js.map +1 -0
- package/dist-server/service/station/station-screen-control-withdrawal.d.ts +104 -0
- package/dist-server/service/station/station-screen-control-withdrawal.js +128 -0
- package/dist-server/service/station/station-screen-control-withdrawal.js.map +1 -0
- package/dist-server/service/station/station-screen-draft-resolver.d.ts +47 -0
- package/dist-server/service/station/station-screen-draft-resolver.js +153 -0
- package/dist-server/service/station/station-screen-draft-resolver.js.map +1 -0
- package/dist-server/service/station/station-screen-layout.d.ts +119 -0
- package/dist-server/service/station/station-screen-layout.js +202 -0
- package/dist-server/service/station/station-screen-layout.js.map +1 -0
- package/dist-server/service/station/station-screen-list-resolver.d.ts +69 -0
- package/dist-server/service/station/station-screen-list-resolver.js +215 -0
- package/dist-server/service/station/station-screen-list-resolver.js.map +1 -0
- package/dist-server/service/station/station-screen-publish-subscriber.d.ts +22 -0
- package/dist-server/service/station/station-screen-publish-subscriber.js +90 -0
- package/dist-server/service/station/station-screen-publish-subscriber.js.map +1 -0
- package/dist-server/service/station/station-screen-release-gate.d.ts +66 -0
- package/dist-server/service/station/station-screen-release-gate.js +67 -0
- package/dist-server/service/station/station-screen-release-gate.js.map +1 -0
- package/dist-server/service/station/station-screen-release-record.d.ts +62 -0
- package/dist-server/service/station/station-screen-release-record.js +139 -0
- package/dist-server/service/station/station-screen-release-record.js.map +1 -0
- package/dist-server/service/station/station-screen-release-signature.d.ts +23 -0
- package/dist-server/service/station/station-screen-release-signature.js +32 -0
- package/dist-server/service/station/station-screen-release-signature.js.map +1 -0
- package/dist-server/service/station/station-screen-scorecard.d.ts +15 -0
- package/dist-server/service/station/station-screen-scorecard.js +35 -0
- package/dist-server/service/station/station-screen-scorecard.js.map +1 -0
- package/dist-server/service/station/station-screen-spec-resolver.d.ts +36 -0
- package/dist-server/service/station/station-screen-spec-resolver.js +202 -0
- package/dist-server/service/station/station-screen-spec-resolver.js.map +1 -0
- package/dist-server/service/station/station-screen-spec.d.ts +104 -0
- package/dist-server/service/station/station-screen-spec.js +114 -0
- package/dist-server/service/station/station-screen-spec.js.map +1 -0
- package/dist-server/service/station/station-screen-thumbnail.d.ts +11 -0
- package/dist-server/service/station/station-screen-thumbnail.js +36 -0
- package/dist-server/service/station/station-screen-thumbnail.js.map +1 -0
- package/dist-server/service/station/station-service-resolver.d.ts +27 -0
- package/dist-server/service/station/station-service-resolver.js +95 -0
- package/dist-server/service/station/station-service-resolver.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -0
- package/package.json +44 -0
- package/shared/station-axes.d.ts +4 -0
- package/shared/station-axes.js +155 -0
- package/shared/station-screen-flow-check.d.ts +12 -0
- package/shared/station-screen-flow-check.js +74 -0
- package/things-scene.config.js +15 -0
- package/translations/en.json +42 -0
- package/translations/ja.json +42 -0
- package/translations/ko.json +42 -0
- package/translations/ms.json +42 -0
- package/translations/zh.json +42 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StationScreenPublishSubscriber = void 0;
|
|
4
|
+
exports.whyNotReleaseBoard = whyNotReleaseBoard;
|
|
5
|
+
const tslib_1 = require("tslib");
|
|
6
|
+
const typeorm_1 = require("typeorm");
|
|
7
|
+
const board_service_1 = require("@things-factory/board-service");
|
|
8
|
+
const station_screen_release_gate_1 = require("./station-screen-release-gate");
|
|
9
|
+
const screen_model_digest_1 = require("./screen-model-digest");
|
|
10
|
+
const station_screen_release_record_1 = require("./station-screen-release-record");
|
|
11
|
+
const station_screen_spec_1 = require("./station-screen-spec");
|
|
12
|
+
const shell_1 = require("@things-factory/shell");
|
|
13
|
+
/** Enforce at persistence, including the common Board release API. Drafts remain editable. */
|
|
14
|
+
let StationScreenPublishSubscriber = class StationScreenPublishSubscriber {
|
|
15
|
+
listenTo() { return board_service_1.Board; }
|
|
16
|
+
async beforeInsert(event) {
|
|
17
|
+
if (event.entity?.state !== 'released')
|
|
18
|
+
return;
|
|
19
|
+
refuseUnpublishableFlow(event.entity.model);
|
|
20
|
+
await refuseUnapproved(event.manager, event.entity.id, event.entity.model);
|
|
21
|
+
}
|
|
22
|
+
async beforeUpdate(event) {
|
|
23
|
+
const state = event.entity?.state ?? event.databaseEntity?.state;
|
|
24
|
+
if (state === undefined && event.entity?.model !== undefined) {
|
|
25
|
+
throw new Error('StationScreenPublishSubscriber: checking a board model change needs the current release state');
|
|
26
|
+
}
|
|
27
|
+
if (state !== 'released')
|
|
28
|
+
return;
|
|
29
|
+
const model = event.entity?.model ?? event.databaseEntity?.model;
|
|
30
|
+
// releaseBoard uses save with the complete entity. Reject incomplete release writes.
|
|
31
|
+
if (model === undefined)
|
|
32
|
+
throw new Error('StationScreenPublishSubscriber: checking a release needs the whole model');
|
|
33
|
+
refuseUnpublishableFlow(model);
|
|
34
|
+
await refuseUnapproved(event.manager, event.entity?.id ?? event.databaseEntity?.id, model);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
exports.StationScreenPublishSubscriber = StationScreenPublishSubscriber;
|
|
38
|
+
exports.StationScreenPublishSubscriber = StationScreenPublishSubscriber = tslib_1.__decorate([
|
|
39
|
+
(0, typeorm_1.EventSubscriber)()
|
|
40
|
+
], StationScreenPublishSubscriber);
|
|
41
|
+
/**
|
|
42
|
+
* 통제 문서로 켠 화면은 **결재받은 그 도안일 때만** 나간다.
|
|
43
|
+
*
|
|
44
|
+
* ── 왜 여기인가 ────────────────────────────────────────────────────────────
|
|
45
|
+
* 위의 흐름 검사와 같은 이유다. board 는 우리 spec 을 모르고, `releaseBoard` 는 board-service 의
|
|
46
|
+
* 공용 문이다. 저작면에서만 막으면 그 문으로 그냥 나간다.
|
|
47
|
+
*
|
|
48
|
+
* ── 왜 boardId 하나로 찾나 ─────────────────────────────────────────────────
|
|
49
|
+
* 서명 줄의 색인은 `(domain, boardId)` 지만 `boardId` 는 uuid 라 그 자체로 한 화면을 가리킨다.
|
|
50
|
+
* 구독자가 받는 이벤트에 도메인이 실려 있다는 보장은 없어서, 있을지 없을지 모르는 것으로 범위를
|
|
51
|
+
* 좁히는 대신 확실한 것 하나로 찾는다. 발행 한 번에 한 번 도는 질의다.
|
|
52
|
+
*/
|
|
53
|
+
async function whyNotReleaseBoard(manager, boardId, model) {
|
|
54
|
+
if (!boardId) {
|
|
55
|
+
/*
|
|
56
|
+
* 아직 id 가 없는 것은 **삽입과 동시에 발행**하는 경우뿐이고, 그러면 이 보드를 가리키는 spec 도
|
|
57
|
+
* 서명도 있을 수 없다. 통제 문서가 아니므로 통과시킨다 — 없는 것을 근거로 막지 않는다.
|
|
58
|
+
*/
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const spec = await manager.getRepository(station_screen_spec_1.StationScreenSpec).findOne({ where: { boardId } });
|
|
62
|
+
if (!spec?.controlled) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const signed = await manager.getRepository(station_screen_release_record_1.StationScreenRelease).find({ where: { boardId } });
|
|
66
|
+
return (0, station_screen_release_gate_1.whyNotRelease)(spec.controlled, (0, screen_model_digest_1.screenModelDigest)(model), signed.map(one => one.modelDigest));
|
|
67
|
+
}
|
|
68
|
+
/** Same place, same limit as the signature gate below: no request, so the English fallback with the code. */
|
|
69
|
+
function refuseUnpublishableFlow(model) {
|
|
70
|
+
const refusal = (0, station_screen_release_gate_1.flowRefusal)(model);
|
|
71
|
+
if (refusal) {
|
|
72
|
+
throw (0, shell_1.speakRefusal)(refusal, {});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function refuseUnapproved(manager, boardId, model) {
|
|
76
|
+
const refusal = await whyNotReleaseBoard(manager, boardId, model);
|
|
77
|
+
if (!refusal) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
/*
|
|
81
|
+
* **This place does not know the request's language.** It runs inside board-service's mutation, and
|
|
82
|
+
* the transaction carries no request, so the sentence stays the English fallback with the code in
|
|
83
|
+
* `extensions.refusal` (speakRefusal without a translator). The sentence in the reader's language
|
|
84
|
+
* comes before this, from `checkStationScreenRelease`, which plant's release screen asks first
|
|
85
|
+
* (chief architect, 2026-09-17). This gate stays here because `releaseBoard` is the one way a
|
|
86
|
+
* screen reaches the floor (ADR-0047 ④).
|
|
87
|
+
*/
|
|
88
|
+
throw (0, shell_1.speakRefusal)((0, station_screen_release_gate_1.releaseRefusal)(refusal), {});
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=station-screen-publish-subscriber.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"station-screen-publish-subscriber.js","sourceRoot":"","sources":["../../../server/service/station/station-screen-publish-subscriber.ts"],"names":[],"mappings":"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","sourcesContent":["import { EventSubscriber, EntitySubscriberInterface, InsertEvent, UpdateEvent, EntityManager } from 'typeorm'\nimport { Board } from '@things-factory/board-service'\nimport { flowRefusal, releaseRefusal, whyNotRelease, type ReleaseRefusal } from './station-screen-release-gate'\nimport { screenModelDigest } from './screen-model-digest'\nimport { StationScreenRelease } from './station-screen-release-record'\nimport { StationScreenSpec } from './station-screen-spec'\nimport { speakRefusal } from '@things-factory/shell'\n\n/** Enforce at persistence, including the common Board release API. Drafts remain editable. */\n@EventSubscriber()\nexport class StationScreenPublishSubscriber implements EntitySubscriberInterface<Board> {\n listenTo() { return Board }\n\n async beforeInsert(event: InsertEvent<Board>) {\n if (event.entity?.state !== 'released') return\n refuseUnpublishableFlow(event.entity.model)\n await refuseUnapproved(event.manager, event.entity.id, event.entity.model)\n }\n\n async beforeUpdate(event: UpdateEvent<Board>) {\n const state = event.entity?.state ?? event.databaseEntity?.state\n if (state === undefined && event.entity?.model !== undefined) {\n throw new Error('StationScreenPublishSubscriber: checking a board model change needs the current release state')\n }\n if (state !== 'released') return\n const model = event.entity?.model ?? event.databaseEntity?.model\n // releaseBoard uses save with the complete entity. Reject incomplete release writes.\n if (model === undefined) throw new Error('StationScreenPublishSubscriber: checking a release needs the whole model')\n refuseUnpublishableFlow(model)\n await refuseUnapproved(event.manager, event.entity?.id ?? event.databaseEntity?.id, model)\n }\n}\n\n/**\n * 통제 문서로 켠 화면은 **결재받은 그 도안일 때만** 나간다.\n *\n * ── 왜 여기인가 ────────────────────────────────────────────────────────────\n * 위의 흐름 검사와 같은 이유다. board 는 우리 spec 을 모르고, `releaseBoard` 는 board-service 의\n * 공용 문이다. 저작면에서만 막으면 그 문으로 그냥 나간다.\n *\n * ── 왜 boardId 하나로 찾나 ─────────────────────────────────────────────────\n * 서명 줄의 색인은 `(domain, boardId)` 지만 `boardId` 는 uuid 라 그 자체로 한 화면을 가리킨다.\n * 구독자가 받는 이벤트에 도메인이 실려 있다는 보장은 없어서, 있을지 없을지 모르는 것으로 범위를\n * 좁히는 대신 확실한 것 하나로 찾는다. 발행 한 번에 한 번 도는 질의다.\n */\nexport async function whyNotReleaseBoard(\n manager: EntityManager,\n boardId: string | undefined,\n model: unknown\n): Promise<ReleaseRefusal | undefined> {\n if (!boardId) {\n /*\n * 아직 id 가 없는 것은 **삽입과 동시에 발행**하는 경우뿐이고, 그러면 이 보드를 가리키는 spec 도\n * 서명도 있을 수 없다. 통제 문서가 아니므로 통과시킨다 — 없는 것을 근거로 막지 않는다.\n */\n return undefined\n }\n\n const spec = await manager.getRepository(StationScreenSpec).findOne({ where: { boardId } })\n\n if (!spec?.controlled) {\n return undefined\n }\n\n const signed = await manager.getRepository(StationScreenRelease).find({ where: { boardId } })\n\n return whyNotRelease(spec.controlled, screenModelDigest(model), signed.map(one => one.modelDigest))\n}\n\n/** Same place, same limit as the signature gate below: no request, so the English fallback with the code. */\nfunction refuseUnpublishableFlow(model: unknown) {\n const refusal = flowRefusal(model)\n\n if (refusal) {\n throw speakRefusal(refusal, {})\n }\n}\n\nasync function refuseUnapproved(manager: EntityManager, boardId: string | undefined, model: unknown) {\n const refusal = await whyNotReleaseBoard(manager, boardId, model)\n\n if (!refusal) {\n return\n }\n\n /*\n * **This place does not know the request's language.** It runs inside board-service's mutation, and\n * the transaction carries no request, so the sentence stays the English fallback with the code in\n * `extensions.refusal` (speakRefusal without a translator). The sentence in the reader's language\n * comes before this, from `checkStationScreenRelease`, which plant's release screen asks first\n * (chief architect, 2026-09-17). This gate stays here because `releaseBoard` is the one way a\n * screen reaches the floor (ADR-0047 ④).\n */\n throw speakRefusal(releaseRefusal(refusal), {})\n}\n"]}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Refusal } from '@things-factory/shell';
|
|
2
|
+
/**
|
|
3
|
+
* May this controlled screen go out — **is there a signature for the drawing being released?**
|
|
4
|
+
*
|
|
5
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
6
|
+
* ## What this refuses, and why it is two answers and not one
|
|
7
|
+
*
|
|
8
|
+
* `spec.controlled` says a release goes through approval (ADR-0047). Raising and signing happen in
|
|
9
|
+
* the worklist; this is the other end, where the board is actually written. Two different things go
|
|
10
|
+
* wrong here and the person has to do something different about each:
|
|
11
|
+
*
|
|
12
|
+
* ```
|
|
13
|
+
* approval-missing nothing was ever signed for this screen → raise it
|
|
14
|
+
* approval-stale it was signed, then the drawing changed → raise it again
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* One code for both would tell an author who is looking at a screen they had approved last week
|
|
18
|
+
* that it was never approved, and they would go looking for the approval they remember instead of
|
|
19
|
+
* for the edit they made after it.
|
|
20
|
+
*
|
|
21
|
+
* ## Why it compares drawings and not counts
|
|
22
|
+
*
|
|
23
|
+
* A signature is recorded against a `boardId`, so «has this board ever been approved» is the easy
|
|
24
|
+
* check and the wrong one — approve v1, change a torque figure, release v2, and it passes while the
|
|
25
|
+
* screen says «controlled document». So the signature carries the digest of what the approver was
|
|
26
|
+
* shown, and this compares it with what is going out (`screen-model-digest.ts` is that one function;
|
|
27
|
+
* the raise stores what it returns and this recomputes it).
|
|
28
|
+
*
|
|
29
|
+
* ## Why it is pure
|
|
30
|
+
*
|
|
31
|
+
* The gate runs inside a persistence hook, where a test would need a database, a board, a spec and
|
|
32
|
+
* a signed row before it could ask the one question that matters. Reading the rows is the caller's
|
|
33
|
+
* work; deciding is this function's, and deciding is what has to be right.
|
|
34
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
35
|
+
*/
|
|
36
|
+
export type ReleaseRefusal = 'approval-missing' | 'approval-stale';
|
|
37
|
+
/**
|
|
38
|
+
* The refusal for a screen flow that cannot be published: the first blocking issue, under
|
|
39
|
+
* `error.station-flow-<code>`, with the screen and component it is on.
|
|
40
|
+
*/
|
|
41
|
+
export declare function flowRefusal(model: unknown): Refusal | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* The refusal for a release that would go out unsigned. Two codes because the author's next step
|
|
44
|
+
* differs: a screen never approved needs a first approval, a changed one needs approval again.
|
|
45
|
+
*/
|
|
46
|
+
export declare function releaseRefusal(code: ReleaseRefusal): Refusal;
|
|
47
|
+
/**
|
|
48
|
+
* Why this release may not go out — **nothing back means it may.**
|
|
49
|
+
*
|
|
50
|
+
* An `{ ok: boolean }` shape would read more like the other gates in this package, and it was
|
|
51
|
+
* written that way first. It does not survive this build: the server compiles with `strict: false`
|
|
52
|
+
* (`server/tsconfig.json`), and without `strictNullChecks` TypeScript will not narrow a union by a
|
|
53
|
+
* boolean discriminant — so the caller that had just returned on `ok` still could not reach `code`,
|
|
54
|
+
* and the server stopped compiling. Carrying a phantom `code?: undefined` on the passing branch
|
|
55
|
+
* would buy the narrowing back and leave a field in the type that exists for the compiler and not
|
|
56
|
+
* for the reader. Answering with the reason itself needs no narrowing anywhere.
|
|
57
|
+
*/
|
|
58
|
+
export declare function whyNotRelease(controlled: boolean | null | undefined, digest: string, signedDigests: readonly (string | null | undefined)[]): ReleaseRefusal | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* 이 저장이 통제를 **켜는** 저장인가.
|
|
61
|
+
*
|
|
62
|
+
* 켤 때만 결재선을 본다. 이미 켜져 있는 화면을 저장할 때마다 다시 보면, 그 뒤에 결재선이 비워진
|
|
63
|
+
* 공장에서 **통제 문서를 고칠 수조차 없게** 된다 — 고치지 못하면 결재를 받으러 갈 수도 없고, 남는
|
|
64
|
+
* 길은 통제를 끄는 것뿐이다. 켠 뒤에 결재선이 사라진 경우는 발행 관문이 잡는다(§`whyNotRelease`).
|
|
65
|
+
*/
|
|
66
|
+
export declare function turnsControlOn(before: boolean | null | undefined, after: boolean | null | undefined): boolean;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.flowRefusal = flowRefusal;
|
|
4
|
+
exports.releaseRefusal = releaseRefusal;
|
|
5
|
+
exports.whyNotRelease = whyNotRelease;
|
|
6
|
+
exports.turnsControlOn = turnsControlOn;
|
|
7
|
+
const station_screen_flow_check_js_1 = require("../../../shared/station-screen-flow-check.js");
|
|
8
|
+
const shell_1 = require("@things-factory/shell");
|
|
9
|
+
/**
|
|
10
|
+
* The refusal for a screen flow that cannot be published: the first blocking issue, under
|
|
11
|
+
* `error.station-flow-<code>`, with the screen and component it is on.
|
|
12
|
+
*/
|
|
13
|
+
function flowRefusal(model) {
|
|
14
|
+
const issue = (0, station_screen_flow_check_js_1.blockingFlowIssue)(model);
|
|
15
|
+
if (!issue) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return new shell_1.Refusal(`station-${issue.code}`, { page: issue.page, component: issue.component ?? null, ...issue.params }, issue.message);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The refusal for a release that would go out unsigned. Two codes because the author's next step
|
|
22
|
+
* differs: a screen never approved needs a first approval, a changed one needs approval again.
|
|
23
|
+
*/
|
|
24
|
+
function releaseRefusal(code) {
|
|
25
|
+
return code === 'approval-missing'
|
|
26
|
+
? new shell_1.Refusal('station-release-approval-missing', {}, 'this screen is a controlled document with no approved version. submit a release approval first.')
|
|
27
|
+
: new shell_1.Refusal('station-release-approval-stale', {}, 'this screen is a controlled document and was changed after approval. submit a release approval again.');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Why this release may not go out — **nothing back means it may.**
|
|
31
|
+
*
|
|
32
|
+
* An `{ ok: boolean }` shape would read more like the other gates in this package, and it was
|
|
33
|
+
* written that way first. It does not survive this build: the server compiles with `strict: false`
|
|
34
|
+
* (`server/tsconfig.json`), and without `strictNullChecks` TypeScript will not narrow a union by a
|
|
35
|
+
* boolean discriminant — so the caller that had just returned on `ok` still could not reach `code`,
|
|
36
|
+
* and the server stopped compiling. Carrying a phantom `code?: undefined` on the passing branch
|
|
37
|
+
* would buy the narrowing back and leave a field in the type that exists for the compiler and not
|
|
38
|
+
* for the reader. Answering with the reason itself needs no narrowing anywhere.
|
|
39
|
+
*/
|
|
40
|
+
function whyNotRelease(controlled, digest, signedDigests) {
|
|
41
|
+
/* An ordinary screen is released by whoever may edit it. Only a controlled one needs a signature. */
|
|
42
|
+
if (!controlled) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const signed = signedDigests.filter((one) => !!one);
|
|
46
|
+
if (!signed.length) {
|
|
47
|
+
return 'approval-missing';
|
|
48
|
+
}
|
|
49
|
+
/*
|
|
50
|
+
* **Any** signature matching this drawing is enough — not the newest one. A screen that is edited,
|
|
51
|
+
* approved, reverted and released again is going out as a drawing somebody signed, and that is the
|
|
52
|
+
* question a controlled document asks. Requiring the latest row would refuse a release of content
|
|
53
|
+
* that was approved, which is a refusal with no fact behind it.
|
|
54
|
+
*/
|
|
55
|
+
return signed.includes(digest) ? undefined : 'approval-stale';
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 이 저장이 통제를 **켜는** 저장인가.
|
|
59
|
+
*
|
|
60
|
+
* 켤 때만 결재선을 본다. 이미 켜져 있는 화면을 저장할 때마다 다시 보면, 그 뒤에 결재선이 비워진
|
|
61
|
+
* 공장에서 **통제 문서를 고칠 수조차 없게** 된다 — 고치지 못하면 결재를 받으러 갈 수도 없고, 남는
|
|
62
|
+
* 길은 통제를 끄는 것뿐이다. 켠 뒤에 결재선이 사라진 경우는 발행 관문이 잡는다(§`whyNotRelease`).
|
|
63
|
+
*/
|
|
64
|
+
function turnsControlOn(before, after) {
|
|
65
|
+
return !!after && !before;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=station-screen-release-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"station-screen-release-gate.js","sourceRoot":"","sources":["../../../server/service/station/station-screen-release-gate.ts"],"names":[],"mappings":";;AA2CA,kCAQC;AAMD,wCAYC;AAaD,sCAuBC;AASD,wCAEC;AApHD,+FAAgF;AAChF,iDAA+C;AAsC/C;;;GAGG;AACH,SAAgB,WAAW,CAAC,KAAc;IACxC,MAAM,KAAK,GAAG,IAAA,gDAAiB,EAAC,KAAK,CAAC,CAAA;IAEtC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,OAAO,IAAI,eAAO,CAAC,WAAW,KAAK,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,IAAI,IAAI,EAAE,GAAG,KAAK,CAAC,MAAM,EAAE,EAAE,KAAK,CAAC,OAAO,CAAC,CAAA;AACvI,CAAC;AAED;;;GAGG;AACH,SAAgB,cAAc,CAAC,IAAoB;IACjD,OAAO,IAAI,KAAK,kBAAkB;QAChC,CAAC,CAAC,IAAI,eAAO,CACT,kCAAkC,EAClC,EAAE,EACF,iGAAiG,CAClG;QACH,CAAC,CAAC,IAAI,eAAO,CACT,gCAAgC,EAChC,EAAE,EACF,uGAAuG,CACxG,CAAA;AACP,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,aAAa,CAC3B,UAAsC,EACtC,MAAc,EACd,aAAqD;IAErD,qGAAqG;IACrG,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,EAAiB,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IAElE,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QACnB,OAAO,kBAAkB,CAAA;IAC3B,CAAC;IAED;;;;;OAKG;IACH,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,gBAAgB,CAAA;AAC/D,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,cAAc,CAAC,MAAkC,EAAE,KAAiC;IAClG,OAAO,CAAC,CAAC,KAAK,IAAI,CAAC,MAAM,CAAA;AAC3B,CAAC","sourcesContent":["import { blockingFlowIssue } from '../../../shared/station-screen-flow-check.js'\nimport { Refusal } from '@things-factory/shell'\n\n/**\n * May this controlled screen go out — **is there a signature for the drawing being released?**\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ## What this refuses, and why it is two answers and not one\n *\n * `spec.controlled` says a release goes through approval (ADR-0047). Raising and signing happen in\n * the worklist; this is the other end, where the board is actually written. Two different things go\n * wrong here and the person has to do something different about each:\n *\n * ```\n * approval-missing nothing was ever signed for this screen → raise it\n * approval-stale it was signed, then the drawing changed → raise it again\n * ```\n *\n * One code for both would tell an author who is looking at a screen they had approved last week\n * that it was never approved, and they would go looking for the approval they remember instead of\n * for the edit they made after it.\n *\n * ## Why it compares drawings and not counts\n *\n * A signature is recorded against a `boardId`, so «has this board ever been approved» is the easy\n * check and the wrong one — approve v1, change a torque figure, release v2, and it passes while the\n * screen says «controlled document». So the signature carries the digest of what the approver was\n * shown, and this compares it with what is going out (`screen-model-digest.ts` is that one function;\n * the raise stores what it returns and this recomputes it).\n *\n * ## Why it is pure\n *\n * The gate runs inside a persistence hook, where a test would need a database, a board, a spec and\n * a signed row before it could ask the one question that matters. Reading the rows is the caller's\n * work; deciding is this function's, and deciding is what has to be right.\n * ═══════════════════════════════════════════════════════════════════════════\n */\nexport type ReleaseRefusal = 'approval-missing' | 'approval-stale'\n\n/**\n * The refusal for a screen flow that cannot be published: the first blocking issue, under\n * `error.station-flow-<code>`, with the screen and component it is on.\n */\nexport function flowRefusal(model: unknown): Refusal | undefined {\n const issue = blockingFlowIssue(model)\n\n if (!issue) {\n return undefined\n }\n\n return new Refusal(`station-${issue.code}`, { page: issue.page, component: issue.component ?? null, ...issue.params }, issue.message)\n}\n\n/**\n * The refusal for a release that would go out unsigned. Two codes because the author's next step\n * differs: a screen never approved needs a first approval, a changed one needs approval again.\n */\nexport function releaseRefusal(code: ReleaseRefusal): Refusal {\n return code === 'approval-missing'\n ? new Refusal(\n 'station-release-approval-missing',\n {},\n 'this screen is a controlled document with no approved version. submit a release approval first.'\n )\n : new Refusal(\n 'station-release-approval-stale',\n {},\n 'this screen is a controlled document and was changed after approval. submit a release approval again.'\n )\n}\n\n/**\n * Why this release may not go out — **nothing back means it may.**\n *\n * An `{ ok: boolean }` shape would read more like the other gates in this package, and it was\n * written that way first. It does not survive this build: the server compiles with `strict: false`\n * (`server/tsconfig.json`), and without `strictNullChecks` TypeScript will not narrow a union by a\n * boolean discriminant — so the caller that had just returned on `ok` still could not reach `code`,\n * and the server stopped compiling. Carrying a phantom `code?: undefined` on the passing branch\n * would buy the narrowing back and leave a field in the type that exists for the compiler and not\n * for the reader. Answering with the reason itself needs no narrowing anywhere.\n */\nexport function whyNotRelease(\n controlled: boolean | null | undefined,\n digest: string,\n signedDigests: readonly (string | null | undefined)[]\n): ReleaseRefusal | undefined {\n /* An ordinary screen is released by whoever may edit it. Only a controlled one needs a signature. */\n if (!controlled) {\n return undefined\n }\n\n const signed = signedDigests.filter((one): one is string => !!one)\n\n if (!signed.length) {\n return 'approval-missing'\n }\n\n /*\n * **Any** signature matching this drawing is enough — not the newest one. A screen that is edited,\n * approved, reverted and released again is going out as a drawing somebody signed, and that is the\n * question a controlled document asks. Requiring the latest row would refuse a release of content\n * that was approved, which is a refusal with no fact behind it.\n */\n return signed.includes(digest) ? undefined : 'approval-stale'\n}\n\n/**\n * 이 저장이 통제를 **켜는** 저장인가.\n *\n * 켤 때만 결재선을 본다. 이미 켜져 있는 화면을 저장할 때마다 다시 보면, 그 뒤에 결재선이 비워진\n * 공장에서 **통제 문서를 고칠 수조차 없게** 된다 — 고치지 못하면 결재를 받으러 갈 수도 없고, 남는\n * 길은 통제를 끄는 것뿐이다. 켠 뒤에 결재선이 사라진 경우는 발행 관문이 잡는다(§`whyNotRelease`).\n */\nexport function turnsControlOn(before: boolean | null | undefined, after: boolean | null | undefined): boolean {\n return !!after && !before\n}\n"]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Domain } from '@things-factory/shell';
|
|
2
|
+
import { User } from '@things-factory/auth-base';
|
|
3
|
+
/**
|
|
4
|
+
* 화면 발행 승인 — **통제 문서 한 판에 서명한 자리.**
|
|
5
|
+
*
|
|
6
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
7
|
+
* ── 왜 표가 필요한가 ──────────────────────────────────────────────────────
|
|
8
|
+
* `controlled` 한 화면은 발행이 결재를 지나야 하고, **판마다 누가 승인했는지 나중에 찾을 수 있어야**
|
|
9
|
+
* 한다(ISO 9001 7.5.3). 그 「찾을 수 있음」이 이 표다. 결재 기록만으로는 부족하다 — 발행 관문이
|
|
10
|
+
* 「이 판에 승인이 있나」를 묻고, 그 물음의 답이 여기 있어야 `approval-missing` 을 가릴 수 있다.
|
|
11
|
+
*
|
|
12
|
+
* ── 이름이 아니라 `boardId` 로 건다 ───────────────────────────────────────
|
|
13
|
+
* 화면 이름은 바뀐다. 이름으로 걸면 개명한 날 **승인이 없는 화면**이 되고, 통제 문서가 조용히 통제
|
|
14
|
+
* 밖으로 나간다. 사람이 읽는 이름은 서명 시점의 것으로 함께 굳힌다 — 그때 무엇을 승인했는지는
|
|
15
|
+
* 그때의 이름으로 말해야 한다.
|
|
16
|
+
*
|
|
17
|
+
* ── 발행 관문은 이 표를 읽는다 ────────────────────────────────────────────
|
|
18
|
+
* 관문은 station 레인이 든다. 이 표는 그 관문이 묻는 자리이고, 여기 줄이 없으면 관문이 거절한다
|
|
19
|
+
* (`approval-missing`). 문(양식·결재선·결재 화면)은 이 레인이 든다.
|
|
20
|
+
*
|
|
21
|
+
* [합의했다] operato-application/adr/ADR-0047.md ②④⑦
|
|
22
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
23
|
+
*/
|
|
24
|
+
export declare class StationScreenRelease {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
domain?: Domain;
|
|
27
|
+
domainId?: string;
|
|
28
|
+
name: string;
|
|
29
|
+
/** 어느 화면인가 — **개명을 견디는 식별자.** */
|
|
30
|
+
boardId: string;
|
|
31
|
+
/** 서명할 때 사람이 읽은 이름. 지금 이름이 아니라 **그때 이름**이다. */
|
|
32
|
+
screenName?: string;
|
|
33
|
+
stationName?: string;
|
|
34
|
+
/**
|
|
35
|
+
* **어느 판을 승인했나** — 상신 시점 도안의 지문(§`screen-model-digest`).
|
|
36
|
+
*
|
|
37
|
+
* 이 칸이 없으면 서명이 화면에만 걸리고 **v1 을 승인한 서명 위로 v2 가 그대로 나간다.** 통제 문서는
|
|
38
|
+
* 판마다 승인받는 것이라 그 순간 통제가 이름만 남는다. 번호로는 못 묶는다 — `Board.version` 은
|
|
39
|
+
* 발행할 때 정해지고 상신 시점에는 없다.
|
|
40
|
+
*
|
|
41
|
+
* 발행 관문이 지금 도안의 지문을 같은 함수로 만들어 이 값과 견준다. 다르면 `approval-stale` 이다.
|
|
42
|
+
*/
|
|
43
|
+
modelDigest?: string;
|
|
44
|
+
decidedAt: Date;
|
|
45
|
+
/** 누가 정했나 — **서버가 로그인한 사람으로 적는다.** 손으로 받지 않는다. */
|
|
46
|
+
decidedBy?: string;
|
|
47
|
+
requestedAt?: Date;
|
|
48
|
+
/** 무엇을 바꿨나 — 상신한 사람이 적는다. 통제 문서의 변경 이력이 이 칸이다. */
|
|
49
|
+
reason?: string;
|
|
50
|
+
/**
|
|
51
|
+
* 결재자가 **보고 서명한 문장 그대로.**
|
|
52
|
+
*
|
|
53
|
+
* 나중에 문구가 바뀌어도 옛 서명의 뜻이 안 흔들리게 굳힌다(§`batch-release-record` 와 같은 규율).
|
|
54
|
+
*/
|
|
55
|
+
shownAtSigning?: string[];
|
|
56
|
+
createdAt?: Date;
|
|
57
|
+
updatedAt?: Date;
|
|
58
|
+
creator?: User;
|
|
59
|
+
creatorId?: string;
|
|
60
|
+
updater?: User;
|
|
61
|
+
updaterId?: string;
|
|
62
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StationScreenRelease = 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 auth_base_1 = require("@things-factory/auth-base");
|
|
9
|
+
/**
|
|
10
|
+
* 화면 발행 승인 — **통제 문서 한 판에 서명한 자리.**
|
|
11
|
+
*
|
|
12
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
* ── 왜 표가 필요한가 ──────────────────────────────────────────────────────
|
|
14
|
+
* `controlled` 한 화면은 발행이 결재를 지나야 하고, **판마다 누가 승인했는지 나중에 찾을 수 있어야**
|
|
15
|
+
* 한다(ISO 9001 7.5.3). 그 「찾을 수 있음」이 이 표다. 결재 기록만으로는 부족하다 — 발행 관문이
|
|
16
|
+
* 「이 판에 승인이 있나」를 묻고, 그 물음의 답이 여기 있어야 `approval-missing` 을 가릴 수 있다.
|
|
17
|
+
*
|
|
18
|
+
* ── 이름이 아니라 `boardId` 로 건다 ───────────────────────────────────────
|
|
19
|
+
* 화면 이름은 바뀐다. 이름으로 걸면 개명한 날 **승인이 없는 화면**이 되고, 통제 문서가 조용히 통제
|
|
20
|
+
* 밖으로 나간다. 사람이 읽는 이름은 서명 시점의 것으로 함께 굳힌다 — 그때 무엇을 승인했는지는
|
|
21
|
+
* 그때의 이름으로 말해야 한다.
|
|
22
|
+
*
|
|
23
|
+
* ── 발행 관문은 이 표를 읽는다 ────────────────────────────────────────────
|
|
24
|
+
* 관문은 station 레인이 든다. 이 표는 그 관문이 묻는 자리이고, 여기 줄이 없으면 관문이 거절한다
|
|
25
|
+
* (`approval-missing`). 문(양식·결재선·결재 화면)은 이 레인이 든다.
|
|
26
|
+
*
|
|
27
|
+
* [합의했다] operato-application/adr/ADR-0047.md ②④⑦
|
|
28
|
+
* ═══════════════════════════════════════════════════════════════════════════
|
|
29
|
+
*/
|
|
30
|
+
let StationScreenRelease = class StationScreenRelease {
|
|
31
|
+
};
|
|
32
|
+
exports.StationScreenRelease = StationScreenRelease;
|
|
33
|
+
tslib_1.__decorate([
|
|
34
|
+
(0, typeorm_1.PrimaryGeneratedColumn)('uuid'),
|
|
35
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.ID),
|
|
36
|
+
tslib_1.__metadata("design:type", String)
|
|
37
|
+
], StationScreenRelease.prototype, "id", void 0);
|
|
38
|
+
tslib_1.__decorate([
|
|
39
|
+
(0, typeorm_1.ManyToOne)(type => shell_1.Domain),
|
|
40
|
+
(0, type_graphql_1.Field)(type => shell_1.Domain),
|
|
41
|
+
tslib_1.__metadata("design:type", shell_1.Domain)
|
|
42
|
+
], StationScreenRelease.prototype, "domain", void 0);
|
|
43
|
+
tslib_1.__decorate([
|
|
44
|
+
(0, typeorm_1.RelationId)((release) => release.domain),
|
|
45
|
+
tslib_1.__metadata("design:type", String)
|
|
46
|
+
], StationScreenRelease.prototype, "domainId", void 0);
|
|
47
|
+
tslib_1.__decorate([
|
|
48
|
+
(0, typeorm_1.Column)(),
|
|
49
|
+
(0, type_graphql_1.Field)({ description: 'the name of this release decision' }),
|
|
50
|
+
tslib_1.__metadata("design:type", String)
|
|
51
|
+
], StationScreenRelease.prototype, "name", void 0);
|
|
52
|
+
tslib_1.__decorate([
|
|
53
|
+
(0, typeorm_1.Column)(),
|
|
54
|
+
(0, type_graphql_1.Field)({ description: 'the board this screen is, which survives renaming' }),
|
|
55
|
+
tslib_1.__metadata("design:type", String)
|
|
56
|
+
], StationScreenRelease.prototype, "boardId", void 0);
|
|
57
|
+
tslib_1.__decorate([
|
|
58
|
+
(0, typeorm_1.Column)({ nullable: true }),
|
|
59
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'what the screen was called when this was signed' }),
|
|
60
|
+
tslib_1.__metadata("design:type", String)
|
|
61
|
+
], StationScreenRelease.prototype, "screenName", void 0);
|
|
62
|
+
tslib_1.__decorate([
|
|
63
|
+
(0, typeorm_1.Column)({ nullable: true }),
|
|
64
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'which station this screen was drawn for' }),
|
|
65
|
+
tslib_1.__metadata("design:type", String)
|
|
66
|
+
], StationScreenRelease.prototype, "stationName", void 0);
|
|
67
|
+
tslib_1.__decorate([
|
|
68
|
+
(0, typeorm_1.Column)({ nullable: true }),
|
|
69
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'digest of the drawing this signature was given to' }),
|
|
70
|
+
tslib_1.__metadata("design:type", String)
|
|
71
|
+
], StationScreenRelease.prototype, "modelDigest", void 0);
|
|
72
|
+
tslib_1.__decorate([
|
|
73
|
+
(0, typeorm_1.Column)(),
|
|
74
|
+
(0, type_graphql_1.Field)({ description: 'when the last approver signed' }),
|
|
75
|
+
tslib_1.__metadata("design:type", Date
|
|
76
|
+
/** 누가 정했나 — **서버가 로그인한 사람으로 적는다.** 손으로 받지 않는다. */
|
|
77
|
+
)
|
|
78
|
+
], StationScreenRelease.prototype, "decidedAt", void 0);
|
|
79
|
+
tslib_1.__decorate([
|
|
80
|
+
(0, typeorm_1.Column)({ nullable: true }),
|
|
81
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'who decided — filled by the server, never accepted from the caller' }),
|
|
82
|
+
tslib_1.__metadata("design:type", String)
|
|
83
|
+
], StationScreenRelease.prototype, "decidedBy", void 0);
|
|
84
|
+
tslib_1.__decorate([
|
|
85
|
+
(0, typeorm_1.Column)({ nullable: true }),
|
|
86
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'when it was sent for approval' }),
|
|
87
|
+
tslib_1.__metadata("design:type", Date
|
|
88
|
+
/** 무엇을 바꿨나 — 상신한 사람이 적는다. 통제 문서의 변경 이력이 이 칸이다. */
|
|
89
|
+
)
|
|
90
|
+
], StationScreenRelease.prototype, "requestedAt", void 0);
|
|
91
|
+
tslib_1.__decorate([
|
|
92
|
+
(0, typeorm_1.Column)({ nullable: true }),
|
|
93
|
+
(0, type_graphql_1.Field)({ nullable: true }),
|
|
94
|
+
tslib_1.__metadata("design:type", String)
|
|
95
|
+
], StationScreenRelease.prototype, "reason", void 0);
|
|
96
|
+
tslib_1.__decorate([
|
|
97
|
+
(0, typeorm_1.Column)({ type: 'simple-json', nullable: true }),
|
|
98
|
+
(0, type_graphql_1.Field)(type => [String], { nullable: true, description: 'what the approver was shown before signing' }),
|
|
99
|
+
tslib_1.__metadata("design:type", Array)
|
|
100
|
+
], StationScreenRelease.prototype, "shownAtSigning", void 0);
|
|
101
|
+
tslib_1.__decorate([
|
|
102
|
+
(0, typeorm_1.CreateDateColumn)(),
|
|
103
|
+
(0, type_graphql_1.Field)({ nullable: true }),
|
|
104
|
+
tslib_1.__metadata("design:type", Date)
|
|
105
|
+
], StationScreenRelease.prototype, "createdAt", void 0);
|
|
106
|
+
tslib_1.__decorate([
|
|
107
|
+
(0, typeorm_1.UpdateDateColumn)(),
|
|
108
|
+
(0, type_graphql_1.Field)({ nullable: true }),
|
|
109
|
+
tslib_1.__metadata("design:type", Date)
|
|
110
|
+
], StationScreenRelease.prototype, "updatedAt", void 0);
|
|
111
|
+
tslib_1.__decorate([
|
|
112
|
+
(0, typeorm_1.ManyToOne)(type => auth_base_1.User, { nullable: true }),
|
|
113
|
+
(0, type_graphql_1.Field)(type => auth_base_1.User, { nullable: true }),
|
|
114
|
+
tslib_1.__metadata("design:type", auth_base_1.User)
|
|
115
|
+
], StationScreenRelease.prototype, "creator", void 0);
|
|
116
|
+
tslib_1.__decorate([
|
|
117
|
+
(0, typeorm_1.RelationId)((release) => release.creator),
|
|
118
|
+
tslib_1.__metadata("design:type", String)
|
|
119
|
+
], StationScreenRelease.prototype, "creatorId", void 0);
|
|
120
|
+
tslib_1.__decorate([
|
|
121
|
+
(0, typeorm_1.ManyToOne)(type => auth_base_1.User, { nullable: true }),
|
|
122
|
+
(0, type_graphql_1.Field)(type => auth_base_1.User, { nullable: true }),
|
|
123
|
+
tslib_1.__metadata("design:type", auth_base_1.User)
|
|
124
|
+
], StationScreenRelease.prototype, "updater", void 0);
|
|
125
|
+
tslib_1.__decorate([
|
|
126
|
+
(0, typeorm_1.RelationId)((release) => release.updater),
|
|
127
|
+
tslib_1.__metadata("design:type", String)
|
|
128
|
+
], StationScreenRelease.prototype, "updaterId", void 0);
|
|
129
|
+
exports.StationScreenRelease = StationScreenRelease = tslib_1.__decorate([
|
|
130
|
+
(0, typeorm_1.Entity)({ name: 'station_screen_releases' })
|
|
131
|
+
/* A signature is for one drawing: the identity is (boardId, modelDigest), not the request name (ADR-0047 ④). */
|
|
132
|
+
,
|
|
133
|
+
(0, typeorm_1.Index)('ix_station_screen_releases_0', (release) => [release.domain, release.boardId, release.modelDigest], {
|
|
134
|
+
unique: true
|
|
135
|
+
}),
|
|
136
|
+
(0, typeorm_1.Index)('ix_station_screen_releases_1', (release) => [release.domain, release.boardId]),
|
|
137
|
+
(0, type_graphql_1.ObjectType)({ description: 'A signed decision that one controlled screen may be released' })
|
|
138
|
+
], StationScreenRelease);
|
|
139
|
+
//# sourceMappingURL=station-screen-release-record.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **A signature is for one drawing of one screen** — `(boardId, modelDigest)`, ADR-0047 ④.
|
|
3
|
+
*
|
|
4
|
+
* The request name used to be fixed per board and unique, and a raise was refused once any signature
|
|
5
|
+
* stood. A controlled screen could then be approved once in its life: after its drawing changed, the
|
|
6
|
+
* release gate refused the unsigned drawing and the raise refused a new approval. The identity is now the
|
|
7
|
+
* drawing, and the name only helps a person tell versions apart in the approval inbox (chief architect,
|
|
8
|
+
* 2026-09-17). Earlier signatures stay: a signature is a record, not a state.
|
|
9
|
+
*/
|
|
10
|
+
/** Short enough to read in an inbox, long enough to tell two drawings apart there. */
|
|
11
|
+
export declare function versionMark(modelDigest: string): string;
|
|
12
|
+
/** What the approval inbox shows. Not the identity; the fields are. */
|
|
13
|
+
export declare function releaseRequestName(boardId: string, modelDigest: string): string;
|
|
14
|
+
/** Every request for this screen, whatever the drawing — one open request per screen at a time. */
|
|
15
|
+
export declare function releaseRequestPrefix(boardId: string): string;
|
|
16
|
+
/** Where a signature for this drawing is looked up. Both the raise and the approval callback use it. */
|
|
17
|
+
export declare function signatureOf(domainId: string, boardId: string, modelDigest: string): {
|
|
18
|
+
domain: {
|
|
19
|
+
id: string;
|
|
20
|
+
};
|
|
21
|
+
boardId: string;
|
|
22
|
+
modelDigest: string;
|
|
23
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* **A signature is for one drawing of one screen** — `(boardId, modelDigest)`, ADR-0047 ④.
|
|
4
|
+
*
|
|
5
|
+
* The request name used to be fixed per board and unique, and a raise was refused once any signature
|
|
6
|
+
* stood. A controlled screen could then be approved once in its life: after its drawing changed, the
|
|
7
|
+
* release gate refused the unsigned drawing and the raise refused a new approval. The identity is now the
|
|
8
|
+
* drawing, and the name only helps a person tell versions apart in the approval inbox (chief architect,
|
|
9
|
+
* 2026-09-17). Earlier signatures stay: a signature is a record, not a state.
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.versionMark = versionMark;
|
|
13
|
+
exports.releaseRequestName = releaseRequestName;
|
|
14
|
+
exports.releaseRequestPrefix = releaseRequestPrefix;
|
|
15
|
+
exports.signatureOf = signatureOf;
|
|
16
|
+
/** Short enough to read in an inbox, long enough to tell two drawings apart there. */
|
|
17
|
+
function versionMark(modelDigest) {
|
|
18
|
+
return String(modelDigest ?? '').slice(0, 8);
|
|
19
|
+
}
|
|
20
|
+
/** What the approval inbox shows. Not the identity; the fields are. */
|
|
21
|
+
function releaseRequestName(boardId, modelDigest) {
|
|
22
|
+
return `화면 발행 승인 ${boardId} · ${versionMark(modelDigest)}`;
|
|
23
|
+
}
|
|
24
|
+
/** Every request for this screen, whatever the drawing — one open request per screen at a time. */
|
|
25
|
+
function releaseRequestPrefix(boardId) {
|
|
26
|
+
return `화면 발행 승인 ${boardId} · `;
|
|
27
|
+
}
|
|
28
|
+
/** Where a signature for this drawing is looked up. Both the raise and the approval callback use it. */
|
|
29
|
+
function signatureOf(domainId, boardId, modelDigest) {
|
|
30
|
+
return { domain: { id: domainId }, boardId, modelDigest };
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=station-screen-release-signature.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"station-screen-release-signature.js","sourceRoot":"","sources":["../../../server/service/station/station-screen-release-signature.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;AAGH,kCAEC;AAGD,gDAEC;AAGD,oDAEC;AAGD,kCAEC;AAlBD,sFAAsF;AACtF,SAAgB,WAAW,CAAC,WAAmB;IAC7C,OAAO,MAAM,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;AAC9C,CAAC;AAED,uEAAuE;AACvE,SAAgB,kBAAkB,CAAC,OAAe,EAAE,WAAmB;IACrE,OAAO,YAAY,OAAO,MAAM,WAAW,CAAC,WAAW,CAAC,EAAE,CAAA;AAC5D,CAAC;AAED,mGAAmG;AACnG,SAAgB,oBAAoB,CAAC,OAAe;IAClD,OAAO,YAAY,OAAO,KAAK,CAAA;AACjC,CAAC;AAED,wGAAwG;AACxG,SAAgB,WAAW,CAAC,QAAgB,EAAE,OAAe,EAAE,WAAmB;IAChF,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE,CAAA;AAC3D,CAAC","sourcesContent":["/**\n * **A signature is for one drawing of one screen** — `(boardId, modelDigest)`, ADR-0047 ④.\n *\n * The request name used to be fixed per board and unique, and a raise was refused once any signature\n * stood. A controlled screen could then be approved once in its life: after its drawing changed, the\n * release gate refused the unsigned drawing and the raise refused a new approval. The identity is now the\n * drawing, and the name only helps a person tell versions apart in the approval inbox (chief architect,\n * 2026-09-17). Earlier signatures stay: a signature is a record, not a state.\n */\n\n/** Short enough to read in an inbox, long enough to tell two drawings apart there. */\nexport function versionMark(modelDigest: string): string {\n return String(modelDigest ?? '').slice(0, 8)\n}\n\n/** What the approval inbox shows. Not the identity; the fields are. */\nexport function releaseRequestName(boardId: string, modelDigest: string): string {\n return `화면 발행 승인 ${boardId} · ${versionMark(modelDigest)}`\n}\n\n/** Every request for this screen, whatever the drawing — one open request per screen at a time. */\nexport function releaseRequestPrefix(boardId: string): string {\n return `화면 발행 승인 ${boardId} · `\n}\n\n/** Where a signature for this drawing is looked up. Both the raise and the approval callback use it. */\nexport function signatureOf(domainId: string, boardId: string, modelDigest: string) {\n return { domain: { id: domainId }, boardId, modelDigest }\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StationFlow, StationProblem } from './station-flow.js';
|
|
2
|
+
export interface StationScreenScoreDimension {
|
|
3
|
+
key: 'flow' | 'context' | 'safety' | 'usability' | 'recovery' | 'release' | 'maintainability';
|
|
4
|
+
score: number;
|
|
5
|
+
max: number;
|
|
6
|
+
evidence: string;
|
|
7
|
+
}
|
|
8
|
+
export interface StationScreenScorecard {
|
|
9
|
+
score: number;
|
|
10
|
+
grade: 'BLOCKED' | 'D' | 'C' | 'B' | 'A';
|
|
11
|
+
blockers: string[];
|
|
12
|
+
dimensions: StationScreenScoreDimension[];
|
|
13
|
+
}
|
|
14
|
+
/** Server-derived evidence for one screen draft; unknown operational proof earns no credit. */
|
|
15
|
+
export declare function inspectStationScreen(flow: StationFlow | null | undefined, problems: readonly StationProblem[]): StationScreenScorecard;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inspectStationScreen = inspectStationScreen;
|
|
4
|
+
const station_flow_js_1 = require("./station-flow.js");
|
|
5
|
+
/** Server-derived evidence for one screen draft; unknown operational proof earns no credit. */
|
|
6
|
+
function inspectStationScreen(flow, problems) {
|
|
7
|
+
const safe = { steps: Array.isArray(flow?.steps) ? flow.steps : [] };
|
|
8
|
+
const errors = problems.filter(one => one.level === 'error');
|
|
9
|
+
const axes = (0, station_flow_js_1.axesRead)(safe);
|
|
10
|
+
const labelled = safe.steps.filter(step => !!step.label?.trim()).length;
|
|
11
|
+
const runs = safe.steps.filter(step => step.kind === 'run');
|
|
12
|
+
const hasInput = safe.steps.some(step => step.kind === 'scan' || step.kind === 'enter');
|
|
13
|
+
const hasDisplay = safe.steps.some(step => step.kind === 'show');
|
|
14
|
+
const blockers = [
|
|
15
|
+
...(safe.steps.length ? [] : ['EMPTY_FLOW']),
|
|
16
|
+
...(errors.length ? ['FLOW_ERROR'] : []),
|
|
17
|
+
...(axes.length ? [] : ['UNBOUND_REQUIRED_CONTEXT']),
|
|
18
|
+
...runs.filter(step => !step.service).map(() => 'UNSAFE_COMMAND')
|
|
19
|
+
];
|
|
20
|
+
const dimensions = [
|
|
21
|
+
{ key: 'flow', max: 15, score: safe.steps.length ? Math.min(15, 6 + (runs.length ? 5 : 0) + Math.round((labelled / safe.steps.length) * 4)) : 0, evidence: safe.steps.length ? `steps:${safe.steps.length};run:${runs.length};labelled:${labelled}` : 'no-flow' },
|
|
22
|
+
{ key: 'context', max: 20, score: axes.length ? 12 + (errors.some(one => one.code.includes('axis')) ? 0 : 8) : 0, evidence: axes.length ? `axes:${axes.join(',')}` : 'no-context-axis' },
|
|
23
|
+
{ key: 'safety', max: 25, score: errors.length ? 0 : Math.min(25, 15 + (runs.length && runs.every(step => step.service) ? 10 : 0)), evidence: errors.length ? `flow-errors:${errors.length}` : runs.length ? 'static-service-mapping' : 'read-only-flow' },
|
|
24
|
+
{ key: 'usability', max: 15, score: Math.min(15, (hasDisplay ? 5 : 0) + (hasInput ? 5 : 0) + (labelled === safe.steps.length && safe.steps.length ? 5 : 0)), evidence: `display:${hasDisplay};input:${hasInput};labelled:${labelled}/${safe.steps.length}` },
|
|
25
|
+
{ key: 'recovery', max: 10, score: 0, evidence: 'no-screen-specific-recovery-e2e' },
|
|
26
|
+
{ key: 'release', max: 10, score: 0, evidence: 'draft-has-no-release-or-assignment-evidence' },
|
|
27
|
+
{ key: 'maintainability', max: 5, score: safe.steps.length && labelled === safe.steps.length ? 5 : 0, evidence: safe.steps.length && labelled === safe.steps.length ? 'every-step-labelled' : 'unlabelled-or-empty-flow' }
|
|
28
|
+
];
|
|
29
|
+
const score = dimensions.reduce((sum, one) => sum + one.score, 0);
|
|
30
|
+
/* A requires evidence the draft itself cannot manufacture: recovery and released-assignment proof. */
|
|
31
|
+
const operationalProof = dimensions.find(one => one.key === 'recovery').score > 0 && dimensions.find(one => one.key === 'release').score > 0;
|
|
32
|
+
const grade = blockers.length ? 'BLOCKED' : score >= 80 && operationalProof ? 'A' : score >= 60 ? 'B' : score >= 40 ? 'C' : 'D';
|
|
33
|
+
return { score, grade, blockers: [...new Set(blockers)], dimensions };
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=station-screen-scorecard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"station-screen-scorecard.js","sourceRoot":"","sources":["../../../server/service/station/station-screen-scorecard.ts"],"names":[],"mappings":"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","sourcesContent":["import { axesRead } from './station-flow.js'\nimport type { StationFlow, StationProblem } from './station-flow.js'\n\nexport interface StationScreenScoreDimension {\n key: 'flow' | 'context' | 'safety' | 'usability' | 'recovery' | 'release' | 'maintainability'\n score: number\n max: number\n evidence: string\n}\n\nexport interface StationScreenScorecard {\n score: number\n grade: 'BLOCKED' | 'D' | 'C' | 'B' | 'A'\n blockers: string[]\n dimensions: StationScreenScoreDimension[]\n}\n\n/** Server-derived evidence for one screen draft; unknown operational proof earns no credit. */\nexport function inspectStationScreen(flow: StationFlow | null | undefined, problems: readonly StationProblem[]): StationScreenScorecard {\n const safe: StationFlow = { steps: Array.isArray(flow?.steps) ? flow!.steps : [] }\n const errors = problems.filter(one => one.level === 'error')\n const axes = axesRead(safe)\n const labelled = safe.steps.filter(step => !!step.label?.trim()).length\n const runs = safe.steps.filter(step => step.kind === 'run')\n const hasInput = safe.steps.some(step => step.kind === 'scan' || step.kind === 'enter')\n const hasDisplay = safe.steps.some(step => step.kind === 'show')\n const blockers = [\n ...(safe.steps.length ? [] : ['EMPTY_FLOW']),\n ...(errors.length ? ['FLOW_ERROR'] : []),\n ...(axes.length ? [] : ['UNBOUND_REQUIRED_CONTEXT']),\n ...runs.filter(step => !step.service).map(() => 'UNSAFE_COMMAND')\n ]\n const dimensions: StationScreenScoreDimension[] = [\n { key: 'flow', max: 15, score: safe.steps.length ? Math.min(15, 6 + (runs.length ? 5 : 0) + Math.round((labelled / safe.steps.length) * 4)) : 0, evidence: safe.steps.length ? `steps:${safe.steps.length};run:${runs.length};labelled:${labelled}` : 'no-flow' },\n { key: 'context', max: 20, score: axes.length ? 12 + (errors.some(one => one.code.includes('axis')) ? 0 : 8) : 0, evidence: axes.length ? `axes:${axes.join(',')}` : 'no-context-axis' },\n { key: 'safety', max: 25, score: errors.length ? 0 : Math.min(25, 15 + (runs.length && runs.every(step => step.service) ? 10 : 0)), evidence: errors.length ? `flow-errors:${errors.length}` : runs.length ? 'static-service-mapping' : 'read-only-flow' },\n { key: 'usability', max: 15, score: Math.min(15, (hasDisplay ? 5 : 0) + (hasInput ? 5 : 0) + (labelled === safe.steps.length && safe.steps.length ? 5 : 0)), evidence: `display:${hasDisplay};input:${hasInput};labelled:${labelled}/${safe.steps.length}` },\n { key: 'recovery', max: 10, score: 0, evidence: 'no-screen-specific-recovery-e2e' },\n { key: 'release', max: 10, score: 0, evidence: 'draft-has-no-release-or-assignment-evidence' },\n { key: 'maintainability', max: 5, score: safe.steps.length && labelled === safe.steps.length ? 5 : 0, evidence: safe.steps.length && labelled === safe.steps.length ? 'every-step-labelled' : 'unlabelled-or-empty-flow' }\n ]\n const score = dimensions.reduce((sum, one) => sum + one.score, 0)\n /* A requires evidence the draft itself cannot manufacture: recovery and released-assignment proof. */\n const operationalProof = dimensions.find(one => one.key === 'recovery')!.score > 0 && dimensions.find(one => one.key === 'release')!.score > 0\n const grade = blockers.length ? 'BLOCKED' : score >= 80 && operationalProof ? 'A' : score >= 60 ? 'B' : score >= 40 ? 'C' : 'D'\n return { score, grade, blockers: [...new Set(blockers)], dimensions }\n}\n"]}
|