@things-factory/station 10.1.39 → 10.1.40

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.
@@ -8,6 +8,6 @@ import { StationServiceResolver } from './station-service-resolver.js';
8
8
  import { StationAssignmentResolver } from './station-assignment-resolver.js';
9
9
  import { StationScreenControlWithdrawalResolver } from './station-screen-control-withdrawal-resolver.js';
10
10
  import { StationScreenPublishSubscriber } from './station-screen-publish-subscriber.js';
11
- export declare const entities: (typeof StationScreenAssignment | typeof StationScreenSpec | typeof StationScreenRelease)[];
12
- export declare const resolvers: (typeof StationAssignmentResolver | typeof StationScreenSpecResolver | typeof StationScreenListResolver | typeof StationScreenDraftResolver | typeof StationServiceResolver | typeof StationScreenControlWithdrawalResolver)[];
11
+ export declare const entities: (typeof StationScreenRelease | typeof StationScreenSpec | typeof StationScreenAssignment)[];
12
+ export declare const resolvers: (typeof StationScreenControlWithdrawalResolver | typeof StationScreenSpecResolver | typeof StationScreenDraftResolver | typeof StationScreenListResolver | typeof StationServiceResolver | typeof StationAssignmentResolver)[];
13
13
  export declare const subscribers: (typeof StationScreenPublishSubscriber)[];
@@ -6,4 +6,4 @@
6
6
  * the set is closed at all and why it lives there; this one exists so the server keeps importing
7
7
  * the axes by the name it always did, and so `checkFlow` has a local place to reach for them.
8
8
  */
9
- export { STATION_AXES, isStationAxis, nearestAxes } from '../../../shared/station-axes.js';
9
+ export { STATION_AXES, STATION_CORE_AXES, STATION_TRANSITIONAL_AXES, isStationAxis, nearestAxes } from '../../../shared/station-axes.js';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.nearestAxes = exports.isStationAxis = exports.STATION_AXES = void 0;
3
+ exports.nearestAxes = exports.isStationAxis = exports.STATION_TRANSITIONAL_AXES = exports.STATION_CORE_AXES = exports.STATION_AXES = void 0;
4
4
  /**
5
5
  * The closed set of context axes, as the server sees it.
6
6
  *
@@ -11,6 +11,8 @@ exports.nearestAxes = exports.isStationAxis = exports.STATION_AXES = void 0;
11
11
  */
12
12
  var station_axes_js_1 = require("../../../shared/station-axes.js");
13
13
  Object.defineProperty(exports, "STATION_AXES", { enumerable: true, get: function () { return station_axes_js_1.STATION_AXES; } });
14
+ Object.defineProperty(exports, "STATION_CORE_AXES", { enumerable: true, get: function () { return station_axes_js_1.STATION_CORE_AXES; } });
15
+ Object.defineProperty(exports, "STATION_TRANSITIONAL_AXES", { enumerable: true, get: function () { return station_axes_js_1.STATION_TRANSITIONAL_AXES; } });
14
16
  Object.defineProperty(exports, "isStationAxis", { enumerable: true, get: function () { return station_axes_js_1.isStationAxis; } });
15
17
  Object.defineProperty(exports, "nearestAxes", { enumerable: true, get: function () { return station_axes_js_1.nearestAxes; } });
16
18
  //# sourceMappingURL=station-axes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"station-axes.js","sourceRoot":"","sources":["../../../server/service/station/station-axes.ts"],"names":[],"mappings":";;;AAAA;;;;;;;GAOG;AACH,mEAA0F;AAAjF,+GAAA,YAAY,OAAA;AAAE,gHAAA,aAAa,OAAA;AAAE,8GAAA,WAAW,OAAA","sourcesContent":["/**\n * The closed set of context axes, as the server sees it.\n *\n * The names themselves are declared once, in `shared/station-axes.js`, so that the browser's\n * authoring surface and this gate cannot hold two lists that drift apart. Read that file for why\n * the set is closed at all and why it lives there; this one exists so the server keeps importing\n * the axes by the name it always did, and so `checkFlow` has a local place to reach for them.\n */\nexport { STATION_AXES, isStationAxis, nearestAxes } from '../../../shared/station-axes.js'\n"]}
1
+ {"version":3,"file":"station-axes.js","sourceRoot":"","sources":["../../../server/service/station/station-axes.ts"],"names":[],"mappings":";;;AAAA;;;;;;;GAOG;AACH,mEAAwI;AAA/H,+GAAA,YAAY,OAAA;AAAE,oHAAA,iBAAiB,OAAA;AAAE,4HAAA,yBAAyB,OAAA;AAAE,gHAAA,aAAa,OAAA;AAAE,8GAAA,WAAW,OAAA","sourcesContent":["/**\n * The closed set of context axes, as the server sees it.\n *\n * The names themselves are declared once, in `shared/station-axes.js`, so that the browser's\n * authoring surface and this gate cannot hold two lists that drift apart. Read that file for why\n * the set is closed at all and why it lives there; this one exists so the server keeps importing\n * the axes by the name it always did, and so `checkFlow` has a local place to reach for them.\n */\nexport { STATION_AXES, STATION_CORE_AXES, STATION_TRANSITIONAL_AXES, isStationAxis, nearestAxes } from '../../../shared/station-axes.js'\n"]}
@@ -153,4 +153,6 @@ export declare function axesRead(flow: StationFlow): string[];
153
153
  *
154
154
  * No database, no running station. Everything here is answerable at the moment somebody is drawing.
155
155
  */
156
- export declare function checkFlow(flow: StationFlow): StationProblem[];
156
+ export declare function checkFlow(flow: StationFlow, options?: {
157
+ axes?: readonly string[];
158
+ }): StationProblem[];
@@ -44,8 +44,10 @@ function axesRead(flow) {
44
44
  *
45
45
  * No database, no running station. Everything here is answerable at the moment somebody is drawing.
46
46
  */
47
- function checkFlow(flow) {
47
+ function checkFlow(flow, options = {}) {
48
48
  const problems = [];
49
+ /* The product's own axes join the machine's closed set (ADR-0083 ruling ①). */
50
+ const declared = options.axes ?? [];
49
51
  /*
50
52
  * ── 축 이름부터 본다 ──────────────────────────────────────────────────
51
53
  * 이것이 없던 동안 오타 축이 모든 검사를 지나 **영원히 빈 칸**으로 현장에 섰다. 미리보기에서도
@@ -54,13 +56,13 @@ function checkFlow(flow) {
54
56
  */
55
57
  for (const step of flow.steps) {
56
58
  for (const axis of step.reads ?? []) {
57
- if (!(0, station_axes_js_1.isStationAxis)(axis)) {
58
- problems.push(unknownAxis(step, axis));
59
+ if (!(0, station_axes_js_1.isStationAxis)(axis, declared)) {
60
+ problems.push(unknownAxis(step, axis, declared));
59
61
  }
60
62
  }
61
63
  for (const [field, source] of Object.entries(step.arguments ?? {})) {
62
- if (source.from === 'context' && !(0, station_axes_js_1.isStationAxis)(source.axis)) {
63
- problems.push({ ...unknownAxis(step, source.axis), field });
64
+ if (source.from === 'context' && !(0, station_axes_js_1.isStationAxis)(source.axis, declared)) {
65
+ problems.push({ ...unknownAxis(step, source.axis, declared), field });
64
66
  }
65
67
  }
66
68
  }
@@ -233,8 +235,8 @@ function label(step) {
233
235
  * 쉰다섯이라 그 남은 일이 대부분이다. 한 글자 틀린 것이 흔한 경우이고, 그것이 답할 값어치가 있는
234
236
  * 경우다.
235
237
  */
236
- function unknownAxis(step, axis) {
237
- const near = (0, station_axes_js_1.nearestAxes)(axis);
238
+ function unknownAxis(step, axis, declared = []) {
239
+ const near = (0, station_axes_js_1.nearestAxes)(axis, 3, declared);
238
240
  const said = axis.trim() ? axis : '(빈 이름)';
239
241
  return near.length
240
242
  ? {
@@ -1 +1 @@
1
- {"version":3,"file":"station-flow.js","sourceRoot":"","sources":["../../../server/service/station/station-flow.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a station screen does — **the flow, and the checks that read it.**\n *\n * Pure on purpose. The value this authoring surface sells is catching a mistake before the screen\n * hangs on a bench, and a check nobody can run without a database and a logged-in browser is a\n * check nobody runs.\n *\n * ── Why a flow and not a list of fields ────────────────────────────────────\n * Half of a bench screen is not display. The operator scans, starts, counts, records a stop reason,\n * finishes — each of those reads something and most of them **write** something. A design that only\n * describes what is shown cannot say whether the finish button will produce a usable record.\n *\n * ── Where the safety comes from ────────────────────────────────────────────\n * Not from the prompt, and not from care. From three closed sets — the step kinds below, the\n * context axes (`stationBindingNames()` is their only source), and the roles — plus the shape of\n * the service being called, which comes from the schema rather than from anybody's memory.\n */\n\nimport { isStationAxis, nearestAxes } from './station-axes.js'\n\n/**\n * What a step is, as the operator experiences it.\n *\n * Five, and deliberately not one per scene component. These are **the unit the checks ask about** —\n * \"does the run step have a source for every required argument\", \"is there an enter step feeding\n * it\" — and those questions cannot be asked of ten drawing parts. How a step is drawn is a separate\n * matter that the layout decides.\n */\nexport type StationStepKind =\n /** Reads a badge or a label and changes what the station knows. Writes nothing. */\n | 'scan'\n /** Shows values. Reads only. */\n | 'show'\n /** The operator puts a number in. This is what an argument can take its value from. */\n | 'enter'\n /** Calls a mutation. Decides `status` and the clock fields from its own meaning. */\n | 'run'\n /** Not drawn. How often this screen reads, and what it reads. */\n | 'settings'\n /** A standard display block retained while a pre-Composer screen is migrated. */\n | 'legacy'\n\n/**\n * Where an argument's value comes from. **Three, and no others.**\n *\n * A field with no source is the defect this module exists to name.\n */\nexport type StationArgumentSource =\n /** The harness already knows it — equipment, operation, the scanned person. */\n | { from: 'context'; axis: string }\n /** The step's own meaning decides it — `status`, `endTime`. */\n | { from: 'step' }\n /** A person put it in, at the named step. */\n | { from: 'input'; stepId: string }\n /** Written down here on purpose. Rare, and visible when it happens. */\n | { from: 'fixed'; value: string | number | boolean }\n\n/** One argument of the service a step calls, as the schema describes it. */\nexport interface StationServiceField {\n name: string\n /** What the field's own owner wrote about it, when they wrote anything. Shown instead of the name. */\n describes?: string\n /** `!` in the schema. Without a source the call fails. */\n required: boolean\n /**\n * Absent means \"nobody wrote it down\", not zero.\n *\n * `ProductionReportPatch.goodCount` is the case this exists for: it is not required, so a finish\n * button with no number pad is syntactically fine and produces a report that reads as if nothing\n * was made. Which fields are like that is domain knowledge, so it is **told to this module**\n * rather than guessed by it.\n */\n notable?: boolean\n}\n\n/** The mutation a step calls, in the shape the schema gives. */\nexport interface StationService {\n /** The mutation name, exactly as in the schema. */\n name: string\n fields: StationServiceField[]\n}\n\nexport interface StationStep {\n id: string\n kind: StationStepKind\n /** 실행 의미. 이름표를 바꾸어도 수행 업무는 바뀌지 않는다. */\n action?: 'start' | 'finish' | 'pause' | 'resume'\n /** What the operator sees on it. Absent means the axis name is used — never nothing. */\n label?: string\n /** Context axes this step displays. Names come from `stationBindingNames()`. */\n reads?: string[]\n /**\n * For `settings` steps — how often the screen re-reads.\n *\n * Nothing is drawn for it, but it has to survive into the model: without a home here the value\n * a generator computes from the operation's duration is dropped on the way to the board, and\n * every screen ends up on the same default with nothing saying why.\n */\n refreshSeconds?: number\n /** For `run` steps. */\n service?: StationService\n /** Field name → where its value comes from. Only meaningful with a service. */\n arguments?: Record<string, StationArgumentSource>\n /** A rendered Station block not yet expressible by the five authoring step kinds. */\n legacyPart?: Record<string, unknown>\n}\n\nexport interface StationFlow {\n steps: StationStep[]\n}\n\nexport type StationProblemLevel = 'error' | 'warning'\n\nexport interface StationProblem {\n level: StationProblemLevel\n /** Which step, when it is about one. */\n stepId?: string\n /** Which argument, when it is about one. */\n field?: string\n /**\n * Which check this is — the screen says it in the reader's language.\n *\n * ── Why a code after all ────────────────────────────────────────────────\n * This used to be prose only, on the reasoning that an author reads it and nothing matches on\n * it. That held right up until the composer put it on screen: the panel was in English and\n * these sentences came out in Korean, because the server had written them. A sentence the\n * server composes can only ever be in one language.\n *\n * So the server states **which** check failed and **about what**; the screen says it.\n */\n code: string\n /** Values to fill into that sentence — a field name, a step's name, a service name. */\n params?: Record<string, string>\n /**\n * The same thing in Korean, composed here.\n *\n * Not for the screen. This is what `npm run test:station` and the logs print, where there is\n * no i18n and no reader to ask.\n */\n says: string\n}\n\n/**\n * Every context axis this flow reads.\n *\n * This is what `StationScreenSpec.requires` is filled from — derived, never typed. An author who\n * has to keep a list in step with the screen will not, and then the assignment check is checking\n * something that used to be true.\n */\nexport function axesRead(flow: StationFlow): string[] {\n const seen = new Set<string>()\n\n for (const step of flow.steps) {\n step.reads?.forEach(axis => seen.add(axis))\n\n for (const source of Object.values(step.arguments ?? {})) {\n if (source.from === 'context') {\n seen.add(source.axis)\n }\n }\n }\n\n return [...seen].sort()\n}\n\n/**\n * What is wrong with this flow — **from the flow and the schema alone.**\n *\n * No database, no running station. Everything here is answerable at the moment somebody is drawing.\n */\nexport function checkFlow(flow: StationFlow): StationProblem[] {\n const problems: StationProblem[] = []\n\n /*\n * ── 축 이름부터 본다 ──────────────────────────────────────────────────\n * 이것이 없던 동안 오타 축이 모든 검사를 지나 **영원히 빈 칸**으로 현장에 섰다. 미리보기에서도\n * 「지금 값이 없는 정상 축」과 똑같이 보여서, 벤치에 걸기 전에 알 방법이 없었다 — 이 저작면이\n * 없애려던 바로 그 혼동이다.\n */\n for (const step of flow.steps) {\n for (const axis of step.reads ?? []) {\n if (!isStationAxis(axis)) {\n problems.push(unknownAxis(step, axis))\n }\n }\n\n for (const [field, source] of Object.entries(step.arguments ?? {})) {\n if (source.from === 'context' && !isStationAxis(source.axis)) {\n problems.push({ ...unknownAxis(step, source.axis), field })\n }\n }\n }\n const byId = new Map(flow.steps.map(step => [step.id, step]))\n /* 차례가 뜻을 갖는다 — 값을 받는 자리는 그 값을 쓰는 자리보다 앞에 있어야 한다. */\n const order = flow.steps.map(step => step.id)\n\n for (const step of flow.steps) {\n if (!step.service) {\n /*\n * A step that writes nothing has nothing to check here. It is not an error to have one —\n * a screen that only shows is a perfectly good screen.\n */\n continue\n }\n\n const sources = step.arguments ?? {}\n\n for (const field of step.service.fields) {\n const source = sources[field.name]\n\n if (!source) {\n if (field.required) {\n problems.push({\n level: 'error',\n stepId: step.id,\n field: field.name,\n code: 'text.check-required-has-no-source',\n params: { step: label(step), field: field.name },\n says: `「${label(step)}」의 필수 칸 ${field.name} 에 출처가 없습니다 — 누르면 실패합니다`\n })\n } else if (field.notable && field.name === 'goodCount') {\n /*\n * 양품 수는 **결과가 구체적이다.** 이 작업대가 지시의 마지막 공정이면 서버가 수 없는 완료를 거절한다\n * (`report-completed-without-counts`). 저작 시점에는 어느 공정의 작업대인지 모르므로 경고로 두되,\n * 막연한 말 대신 그 결과를 말한다(아키텍트 판정 2026-09-17).\n */\n problems.push({\n level: 'warning',\n stepId: step.id,\n field: field.name,\n code: 'text.check-final-count-has-no-source',\n params: { step: label(step) },\n says: `「${label(step)}」을 눌러도 양품 수 없이 기록됩니다. 이 작업대가 지시의 마지막 공정이면 서버가 완료를 거절합니다. 양품 수를 받는 단계를 연결하십시오.`\n })\n } else if (field.notable) {\n problems.push({\n level: 'warning',\n stepId: step.id,\n field: field.name,\n code: 'text.check-notable-has-no-source',\n params: { step: label(step), field: field.name },\n says: `「${label(step)}」을 눌러도 ${field.name} 없이 기록됩니다. 비워 두면 0 이 아니라 적지 않은 것으로 남습니다.`\n })\n }\n\n continue\n }\n\n /*\n * An argument can name a step that is not there, or one that never asks anybody for anything.\n * Both look fine in the editor and both fail on the bench.\n */\n if (source.from === 'input') {\n const feeding = byId.get(source.stepId)\n\n if (!feeding) {\n problems.push({\n level: 'error',\n stepId: step.id,\n field: field.name,\n code: 'text.check-source-step-gone',\n params: { field: field.name },\n says: `${field.name} 이 지워진 단계에서 값을 받게 되어 있습니다 — 그 단계를 다시 세우거나 출처를 바꾸십시오`\n })\n } else if (feeding.kind === 'enter' && order.indexOf(feeding.id) > order.indexOf(step.id)) {\n /*\n * The pad comes **after** the button that reads it.\n *\n * Nothing about this is wrong to the type system and the argument does point at a real\n * entry step, so every other check here passes. On the bench the operator presses finish\n * and only then reaches the box, and the count they type lands nowhere — the report is\n * already filed. Reordering steps is what makes this reachable, which is why it is here.\n */\n problems.push({\n level: 'error',\n stepId: step.id,\n field: field.name,\n code: 'text.check-source-comes-later',\n params: { field: field.name, from: label(feeding) },\n says: `${field.name} 이 「${label(feeding)}」에서 값을 받는데 그 단계가 이 단계보다 뒤에 있습니다. 값을 주는 단계를 앞으로 옮기십시오.`\n })\n } else if (feeding.kind !== 'enter') {\n problems.push({\n level: 'error',\n stepId: step.id,\n field: field.name,\n code: 'text.check-source-does-not-ask',\n params: { field: field.name, from: label(feeding) },\n says: `${field.name} 이 「${label(feeding)}」에서 값을 받는데, 그 단계는 값을 받는 자리가 아닙니다`\n })\n }\n }\n }\n\n /* An argument mapped to a field the service does not have — usually a renamed service. */\n const known = new Set(step.service.fields.map(field => field.name))\n\n for (const name of Object.keys(sources)) {\n if (!known.has(name)) {\n problems.push({\n level: 'error',\n stepId: step.id,\n field: name,\n code: 'text.check-service-has-no-such-field',\n params: { service: step.service.name, field: name },\n says: `${step.service.name} 에 ${name} 칸이 없습니다 — 서비스가 바뀌었는지 보십시오`\n })\n }\n }\n }\n\n /*\n * A number pad nobody reads.\n *\n * Not an error — the screen works. But somebody put it there meaning it to go somewhere, and\n * on the bench an operator will type into it and watch it do nothing.\n */\n const consumed = new Set<string>()\n\n for (const step of flow.steps) {\n for (const source of Object.values(step.arguments ?? {})) {\n if (source.from === 'input') {\n consumed.add(source.stepId)\n }\n }\n }\n\n for (const step of flow.steps) {\n if (step.kind === 'enter' && !consumed.has(step.id)) {\n problems.push({\n level: 'warning',\n stepId: step.id,\n code: 'text.check-nobody-reads-this-entry',\n params: { step: label(step) },\n says: `「${label(step)}」에 넣은 값을 쓰는 단계가 없습니다 — 작업자가 적어도 아무 데도 안 갑니다`\n })\n }\n }\n\n return problems\n}\n\n/**\n * 이 단계를 사람에게 뭐라고 부르나.\n *\n * ── 만들어진 id 를 문장에 넣지 않는다 ────────────────────────────────────\n * 예전에는 이름표가 없으면 `step.id` 로 떨어졌다. 그래서 저작자가 「수량 입력」을 막 더한 순간\n * 「step-1789052869630-2 에 넣은 값을 쓰는 단계가 없습니다」를 읽었다 — 자기가 방금 누른 것이\n * 그것인 줄 알 방법이 없다. 이름표를 아직 안 적은 것은 흔한 일이고, 잘못도 아니다.\n *\n * 이름표가 없으면 **그 단계가 무슨 종류인지**로 부른다. 화면의 목록에도 같은 낱말이 서 있어서\n * 저작자가 눈으로 짚을 수 있다. 몇 번째인지는 화면이 앞에 붙인다 — 차례는 화면이 알고 있다.\n */\nconst KIND_SAID: Record<StationStepKind, string> = {\n scan: '사번 스캔',\n show: '값 보여주기',\n enter: '수량 입력',\n run: '실행 단추',\n settings: '화면 설정',\n legacy: '보존된 표준 블록'\n}\n\nfunction label(step: StationStep): string {\n return step.label?.trim() || KIND_SAID[step.kind]\n}\n\n/**\n * 모르는 축 하나 — **무엇이 맞는지까지 말한다.**\n *\n * 「그런 축은 없습니다」에서 멈추면 저작자에게 틀렸다는 것만 알리고 남은 일은 그대로 남긴다. 이름이\n * 쉰다섯이라 그 남은 일이 대부분이다. 한 글자 틀린 것이 흔한 경우이고, 그것이 답할 값어치가 있는\n * 경우다.\n */\nfunction unknownAxis(step: StationStep, axis: string): StationProblem {\n const near = nearestAxes(axis)\n const said = axis.trim() ? axis : '(빈 이름)'\n\n return near.length\n ? {\n level: 'error',\n stepId: step.id,\n code: 'text.check-unknown-axis-did-you-mean',\n params: { axis: said, near: near.join(' · ') },\n says: `${said} 라는 값은 없습니다 — 혹시 ${near.join(' · ')} 입니까`\n }\n : {\n level: 'error',\n stepId: step.id,\n code: 'text.check-unknown-axis',\n params: { axis: said },\n says: `${said} 라는 값은 없습니다 — 고를 수 있는 것에서 골라 주십시오`\n }\n}\n"]}
1
+ {"version":3,"file":"station-flow.js","sourceRoot":"","sources":["../../../server/service/station/station-flow.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What a station screen does — **the flow, and the checks that read it.**\n *\n * Pure on purpose. The value this authoring surface sells is catching a mistake before the screen\n * hangs on a bench, and a check nobody can run without a database and a logged-in browser is a\n * check nobody runs.\n *\n * ── Why a flow and not a list of fields ────────────────────────────────────\n * Half of a bench screen is not display. The operator scans, starts, counts, records a stop reason,\n * finishes — each of those reads something and most of them **write** something. A design that only\n * describes what is shown cannot say whether the finish button will produce a usable record.\n *\n * ── Where the safety comes from ────────────────────────────────────────────\n * Not from the prompt, and not from care. From three closed sets — the step kinds below, the\n * context axes (`stationBindingNames()` is their only source), and the roles — plus the shape of\n * the service being called, which comes from the schema rather than from anybody's memory.\n */\n\nimport { isStationAxis, nearestAxes } from './station-axes.js'\n\n/**\n * What a step is, as the operator experiences it.\n *\n * Five, and deliberately not one per scene component. These are **the unit the checks ask about** —\n * \"does the run step have a source for every required argument\", \"is there an enter step feeding\n * it\" — and those questions cannot be asked of ten drawing parts. How a step is drawn is a separate\n * matter that the layout decides.\n */\nexport type StationStepKind =\n /** Reads a badge or a label and changes what the station knows. Writes nothing. */\n | 'scan'\n /** Shows values. Reads only. */\n | 'show'\n /** The operator puts a number in. This is what an argument can take its value from. */\n | 'enter'\n /** Calls a mutation. Decides `status` and the clock fields from its own meaning. */\n | 'run'\n /** Not drawn. How often this screen reads, and what it reads. */\n | 'settings'\n /** A standard display block retained while a pre-Composer screen is migrated. */\n | 'legacy'\n\n/**\n * Where an argument's value comes from. **Three, and no others.**\n *\n * A field with no source is the defect this module exists to name.\n */\nexport type StationArgumentSource =\n /** The harness already knows it — equipment, operation, the scanned person. */\n | { from: 'context'; axis: string }\n /** The step's own meaning decides it — `status`, `endTime`. */\n | { from: 'step' }\n /** A person put it in, at the named step. */\n | { from: 'input'; stepId: string }\n /** Written down here on purpose. Rare, and visible when it happens. */\n | { from: 'fixed'; value: string | number | boolean }\n\n/** One argument of the service a step calls, as the schema describes it. */\nexport interface StationServiceField {\n name: string\n /** What the field's own owner wrote about it, when they wrote anything. Shown instead of the name. */\n describes?: string\n /** `!` in the schema. Without a source the call fails. */\n required: boolean\n /**\n * Absent means \"nobody wrote it down\", not zero.\n *\n * `ProductionReportPatch.goodCount` is the case this exists for: it is not required, so a finish\n * button with no number pad is syntactically fine and produces a report that reads as if nothing\n * was made. Which fields are like that is domain knowledge, so it is **told to this module**\n * rather than guessed by it.\n */\n notable?: boolean\n}\n\n/** The mutation a step calls, in the shape the schema gives. */\nexport interface StationService {\n /** The mutation name, exactly as in the schema. */\n name: string\n fields: StationServiceField[]\n}\n\nexport interface StationStep {\n id: string\n kind: StationStepKind\n /** 실행 의미. 이름표를 바꾸어도 수행 업무는 바뀌지 않는다. */\n action?: 'start' | 'finish' | 'pause' | 'resume'\n /** What the operator sees on it. Absent means the axis name is used — never nothing. */\n label?: string\n /** Context axes this step displays. Names come from `stationBindingNames()`. */\n reads?: string[]\n /**\n * For `settings` steps — how often the screen re-reads.\n *\n * Nothing is drawn for it, but it has to survive into the model: without a home here the value\n * a generator computes from the operation's duration is dropped on the way to the board, and\n * every screen ends up on the same default with nothing saying why.\n */\n refreshSeconds?: number\n /** For `run` steps. */\n service?: StationService\n /** Field name → where its value comes from. Only meaningful with a service. */\n arguments?: Record<string, StationArgumentSource>\n /** A rendered Station block not yet expressible by the five authoring step kinds. */\n legacyPart?: Record<string, unknown>\n}\n\nexport interface StationFlow {\n steps: StationStep[]\n}\n\nexport type StationProblemLevel = 'error' | 'warning'\n\nexport interface StationProblem {\n level: StationProblemLevel\n /** Which step, when it is about one. */\n stepId?: string\n /** Which argument, when it is about one. */\n field?: string\n /**\n * Which check this is — the screen says it in the reader's language.\n *\n * ── Why a code after all ────────────────────────────────────────────────\n * This used to be prose only, on the reasoning that an author reads it and nothing matches on\n * it. That held right up until the composer put it on screen: the panel was in English and\n * these sentences came out in Korean, because the server had written them. A sentence the\n * server composes can only ever be in one language.\n *\n * So the server states **which** check failed and **about what**; the screen says it.\n */\n code: string\n /** Values to fill into that sentence — a field name, a step's name, a service name. */\n params?: Record<string, string>\n /**\n * The same thing in Korean, composed here.\n *\n * Not for the screen. This is what `npm run test:station` and the logs print, where there is\n * no i18n and no reader to ask.\n */\n says: string\n}\n\n/**\n * Every context axis this flow reads.\n *\n * This is what `StationScreenSpec.requires` is filled from — derived, never typed. An author who\n * has to keep a list in step with the screen will not, and then the assignment check is checking\n * something that used to be true.\n */\nexport function axesRead(flow: StationFlow): string[] {\n const seen = new Set<string>()\n\n for (const step of flow.steps) {\n step.reads?.forEach(axis => seen.add(axis))\n\n for (const source of Object.values(step.arguments ?? {})) {\n if (source.from === 'context') {\n seen.add(source.axis)\n }\n }\n }\n\n return [...seen].sort()\n}\n\n/**\n * What is wrong with this flow — **from the flow and the schema alone.**\n *\n * No database, no running station. Everything here is answerable at the moment somebody is drawing.\n */\nexport function checkFlow(flow: StationFlow, options: { axes?: readonly string[] } = {}): StationProblem[] {\n const problems: StationProblem[] = []\n /* The product's own axes join the machine's closed set (ADR-0083 ruling ①). */\n const declared = options.axes ?? []\n\n /*\n * ── 축 이름부터 본다 ──────────────────────────────────────────────────\n * 이것이 없던 동안 오타 축이 모든 검사를 지나 **영원히 빈 칸**으로 현장에 섰다. 미리보기에서도\n * 「지금 값이 없는 정상 축」과 똑같이 보여서, 벤치에 걸기 전에 알 방법이 없었다 — 이 저작면이\n * 없애려던 바로 그 혼동이다.\n */\n for (const step of flow.steps) {\n for (const axis of step.reads ?? []) {\n if (!isStationAxis(axis, declared)) {\n problems.push(unknownAxis(step, axis, declared))\n }\n }\n\n for (const [field, source] of Object.entries(step.arguments ?? {})) {\n if (source.from === 'context' && !isStationAxis(source.axis, declared)) {\n problems.push({ ...unknownAxis(step, source.axis, declared), field })\n }\n }\n }\n const byId = new Map(flow.steps.map(step => [step.id, step]))\n /* 차례가 뜻을 갖는다 — 값을 받는 자리는 그 값을 쓰는 자리보다 앞에 있어야 한다. */\n const order = flow.steps.map(step => step.id)\n\n for (const step of flow.steps) {\n if (!step.service) {\n /*\n * A step that writes nothing has nothing to check here. It is not an error to have one —\n * a screen that only shows is a perfectly good screen.\n */\n continue\n }\n\n const sources = step.arguments ?? {}\n\n for (const field of step.service.fields) {\n const source = sources[field.name]\n\n if (!source) {\n if (field.required) {\n problems.push({\n level: 'error',\n stepId: step.id,\n field: field.name,\n code: 'text.check-required-has-no-source',\n params: { step: label(step), field: field.name },\n says: `「${label(step)}」의 필수 칸 ${field.name} 에 출처가 없습니다 — 누르면 실패합니다`\n })\n } else if (field.notable && field.name === 'goodCount') {\n /*\n * 양품 수는 **결과가 구체적이다.** 이 작업대가 지시의 마지막 공정이면 서버가 수 없는 완료를 거절한다\n * (`report-completed-without-counts`). 저작 시점에는 어느 공정의 작업대인지 모르므로 경고로 두되,\n * 막연한 말 대신 그 결과를 말한다(아키텍트 판정 2026-09-17).\n */\n problems.push({\n level: 'warning',\n stepId: step.id,\n field: field.name,\n code: 'text.check-final-count-has-no-source',\n params: { step: label(step) },\n says: `「${label(step)}」을 눌러도 양품 수 없이 기록됩니다. 이 작업대가 지시의 마지막 공정이면 서버가 완료를 거절합니다. 양품 수를 받는 단계를 연결하십시오.`\n })\n } else if (field.notable) {\n problems.push({\n level: 'warning',\n stepId: step.id,\n field: field.name,\n code: 'text.check-notable-has-no-source',\n params: { step: label(step), field: field.name },\n says: `「${label(step)}」을 눌러도 ${field.name} 없이 기록됩니다. 비워 두면 0 이 아니라 적지 않은 것으로 남습니다.`\n })\n }\n\n continue\n }\n\n /*\n * An argument can name a step that is not there, or one that never asks anybody for anything.\n * Both look fine in the editor and both fail on the bench.\n */\n if (source.from === 'input') {\n const feeding = byId.get(source.stepId)\n\n if (!feeding) {\n problems.push({\n level: 'error',\n stepId: step.id,\n field: field.name,\n code: 'text.check-source-step-gone',\n params: { field: field.name },\n says: `${field.name} 이 지워진 단계에서 값을 받게 되어 있습니다 — 그 단계를 다시 세우거나 출처를 바꾸십시오`\n })\n } else if (feeding.kind === 'enter' && order.indexOf(feeding.id) > order.indexOf(step.id)) {\n /*\n * The pad comes **after** the button that reads it.\n *\n * Nothing about this is wrong to the type system and the argument does point at a real\n * entry step, so every other check here passes. On the bench the operator presses finish\n * and only then reaches the box, and the count they type lands nowhere — the report is\n * already filed. Reordering steps is what makes this reachable, which is why it is here.\n */\n problems.push({\n level: 'error',\n stepId: step.id,\n field: field.name,\n code: 'text.check-source-comes-later',\n params: { field: field.name, from: label(feeding) },\n says: `${field.name} 이 「${label(feeding)}」에서 값을 받는데 그 단계가 이 단계보다 뒤에 있습니다. 값을 주는 단계를 앞으로 옮기십시오.`\n })\n } else if (feeding.kind !== 'enter') {\n problems.push({\n level: 'error',\n stepId: step.id,\n field: field.name,\n code: 'text.check-source-does-not-ask',\n params: { field: field.name, from: label(feeding) },\n says: `${field.name} 이 「${label(feeding)}」에서 값을 받는데, 그 단계는 값을 받는 자리가 아닙니다`\n })\n }\n }\n }\n\n /* An argument mapped to a field the service does not have — usually a renamed service. */\n const known = new Set(step.service.fields.map(field => field.name))\n\n for (const name of Object.keys(sources)) {\n if (!known.has(name)) {\n problems.push({\n level: 'error',\n stepId: step.id,\n field: name,\n code: 'text.check-service-has-no-such-field',\n params: { service: step.service.name, field: name },\n says: `${step.service.name} 에 ${name} 칸이 없습니다 — 서비스가 바뀌었는지 보십시오`\n })\n }\n }\n }\n\n /*\n * A number pad nobody reads.\n *\n * Not an error — the screen works. But somebody put it there meaning it to go somewhere, and\n * on the bench an operator will type into it and watch it do nothing.\n */\n const consumed = new Set<string>()\n\n for (const step of flow.steps) {\n for (const source of Object.values(step.arguments ?? {})) {\n if (source.from === 'input') {\n consumed.add(source.stepId)\n }\n }\n }\n\n for (const step of flow.steps) {\n if (step.kind === 'enter' && !consumed.has(step.id)) {\n problems.push({\n level: 'warning',\n stepId: step.id,\n code: 'text.check-nobody-reads-this-entry',\n params: { step: label(step) },\n says: `「${label(step)}」에 넣은 값을 쓰는 단계가 없습니다 — 작업자가 적어도 아무 데도 안 갑니다`\n })\n }\n }\n\n return problems\n}\n\n/**\n * 이 단계를 사람에게 뭐라고 부르나.\n *\n * ── 만들어진 id 를 문장에 넣지 않는다 ────────────────────────────────────\n * 예전에는 이름표가 없으면 `step.id` 로 떨어졌다. 그래서 저작자가 「수량 입력」을 막 더한 순간\n * 「step-1789052869630-2 에 넣은 값을 쓰는 단계가 없습니다」를 읽었다 — 자기가 방금 누른 것이\n * 그것인 줄 알 방법이 없다. 이름표를 아직 안 적은 것은 흔한 일이고, 잘못도 아니다.\n *\n * 이름표가 없으면 **그 단계가 무슨 종류인지**로 부른다. 화면의 목록에도 같은 낱말이 서 있어서\n * 저작자가 눈으로 짚을 수 있다. 몇 번째인지는 화면이 앞에 붙인다 — 차례는 화면이 알고 있다.\n */\nconst KIND_SAID: Record<StationStepKind, string> = {\n scan: '사번 스캔',\n show: '값 보여주기',\n enter: '수량 입력',\n run: '실행 단추',\n settings: '화면 설정',\n legacy: '보존된 표준 블록'\n}\n\nfunction label(step: StationStep): string {\n return step.label?.trim() || KIND_SAID[step.kind]\n}\n\n/**\n * 모르는 축 하나 — **무엇이 맞는지까지 말한다.**\n *\n * 「그런 축은 없습니다」에서 멈추면 저작자에게 틀렸다는 것만 알리고 남은 일은 그대로 남긴다. 이름이\n * 쉰다섯이라 그 남은 일이 대부분이다. 한 글자 틀린 것이 흔한 경우이고, 그것이 답할 값어치가 있는\n * 경우다.\n */\nfunction unknownAxis(step: StationStep, axis: string, declared: readonly string[] = []): StationProblem {\n const near = nearestAxes(axis, 3, declared)\n const said = axis.trim() ? axis : '(빈 이름)'\n\n return near.length\n ? {\n level: 'error',\n stepId: step.id,\n code: 'text.check-unknown-axis-did-you-mean',\n params: { axis: said, near: near.join(' · ') },\n says: `${said} 라는 값은 없습니다 — 혹시 ${near.join(' · ')} 입니까`\n }\n : {\n level: 'error',\n stepId: step.id,\n code: 'text.check-unknown-axis',\n params: { axis: said },\n says: `${said} 라는 값은 없습니다 — 고를 수 있는 것에서 골라 주십시오`\n }\n}\n"]}
@@ -40,8 +40,14 @@ export interface StationServiceDeclaration {
40
40
  name: string;
41
41
  /** What an author is doing when they pick this — their words, not the mutation's. */
42
42
  describes: string;
43
- /** The `@InputType` whose fields this call takes. */
44
- input: Function;
43
+ /**
44
+ * The `@InputType` whose fields this call takes — **when it takes one.**
45
+ *
46
+ * Absent, the machine reads the mutation's own arguments by `name` (ADR-0083 ruling ②): a warehouse's
47
+ * floor doors take a task name and a scanned place, not one input object, and wrapping them in one would
48
+ * give the same door two shapes.
49
+ */
50
+ input?: Function;
45
51
  /**
46
52
  * Nullable fields that still must not be left empty.
47
53
  *
@@ -91,6 +97,12 @@ export type StationChangeRecorder = (manager: EntityManager, domain: Domain, wha
91
97
  export interface StationProduct {
92
98
  /** For the message when something is missing — the product says its own name. */
93
99
  product: string;
100
+ /**
101
+ * The axes this product's screens read beyond the machine's own (ADR-0083 ruling ①) — `group.name`,
102
+ * e.g. `unit.epc` · `task.kind`. The closed set a screen is checked against is the machine's list and
103
+ * this. Absent means the product reads only the machine's.
104
+ */
105
+ axes?: readonly string[];
94
106
  services: StationServiceDeclaration[];
95
107
  readGround: StationGroundReader;
96
108
  recordChange: StationChangeRecorder;
@@ -104,6 +116,11 @@ export interface StationProduct {
104
116
  * 낫다(ADR-0083 결정 2 「선언은 필수이고 모듈에 기본값은 없다」).
105
117
  */
106
118
  export declare function declareStationProduct(product: StationProduct): void;
119
+ /**
120
+ * Every axis a screen of this product may read — the machine's and the product's (ADR-0083 ruling ①).
121
+ * The authoring surface lists these; `checkFlow` refuses anything else.
122
+ */
123
+ export declare function stationAxesOf(product: Pick<StationProduct, 'axes'>): string[];
107
124
  /**
108
125
  * What the product declared — **or an error saying it has not.**
109
126
  *
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.declareStationProduct = declareStationProduct;
4
+ exports.stationAxesOf = stationAxesOf;
4
5
  exports.stationProduct = stationProduct;
5
6
  exports.forgetStationProduct = forgetStationProduct;
6
7
  exports.fieldsOfService = fieldsOfService;
7
8
  const type_graphql_1 = require("type-graphql");
9
+ const station_axes_js_1 = require("./station-axes.js");
8
10
  let declared;
9
11
  /**
10
12
  * Called once at boot, from the product's own declaration file.
@@ -19,8 +21,26 @@ function declareStationProduct(product) {
19
21
  if (missing.length) {
20
22
  throw new Error(`station: ${product?.product ?? '이 제품'} 의 선언에 ${missing.join(' · ')} 이(가) 없습니다 — 현장 화면이 서려면 다 채워야 합니다.`);
21
23
  }
24
+ const axes = product.axes ?? [];
25
+ const shape = /^[a-z][A-Za-z0-9]*\.[a-z][A-Za-z0-9]*$/;
26
+ const wrong = axes.filter((axis, at) => !shape.test(axis ?? '') || station_axes_js_1.STATION_AXES.includes(axis) || axes.indexOf(axis) !== at);
27
+ if (wrong.length) {
28
+ /*
29
+ * A misspelt axis would pass every screen that reads it and stay blank on the bench; one the machine
30
+ * already has is two owners of one name; a repeat is harmless but says the list was not read.
31
+ */
32
+ throw new Error(`station: ${product.product} 의 axes 선언이 틀렸습니다 — ${wrong.map(one => JSON.stringify(one)).join(' · ')}. ` +
33
+ '축은 `무리.이름` 모양이고, 기계가 이미 가진 축은 다시 선언하지 않습니다.');
34
+ }
22
35
  declared = product;
23
36
  }
37
+ /**
38
+ * Every axis a screen of this product may read — the machine's and the product's (ADR-0083 ruling ①).
39
+ * The authoring surface lists these; `checkFlow` refuses anything else.
40
+ */
41
+ function stationAxesOf(product) {
42
+ return [...station_axes_js_1.STATION_AXES, ...(product.axes ?? [])];
43
+ }
24
44
  /**
25
45
  * What the product declared — **or an error saying it has not.**
26
46
  *
@@ -47,6 +67,9 @@ function forgetStationProduct() {
47
67
  function fieldsOfService(declared) {
48
68
  const storage = (0, type_graphql_1.getMetadataStorage)();
49
69
  const notable = new Set(declared.notable ?? []);
70
+ if (!declared.input) {
71
+ return argumentsOfMutation(declared.name, notable);
72
+ }
50
73
  /*
51
74
  * Fields are stored against the class, and a class that extends another has its parent's fields
52
75
  * under the parent's target. Walking the prototype chain keeps an inherited field from going
@@ -74,4 +97,33 @@ function fieldsOfService(declared) {
74
97
  ...(notable.has(field.name) ? { notable: true } : {})
75
98
  }));
76
99
  }
100
+ /**
101
+ * A door that takes no input object — **its own arguments, as the schema has them** (ADR-0083 ruling ②).
102
+ *
103
+ * The same metadata the schema is built from, read in the same place as the input-type fields, so a door's
104
+ * arguments cannot drift from what the running server takes. `@Ctx()` and other non-argument parameters are
105
+ * not fields an author fills.
106
+ *
107
+ * A name the schema does not have is said. Answered with no fields, a misspelt door would look like a door
108
+ * that needs nothing, and the screen built on it would call something that is not there.
109
+ */
110
+ function argumentsOfMutation(name, notable) {
111
+ const storage = (0, type_graphql_1.getMetadataStorage)();
112
+ const mutation = storage.mutations.find(one => one.schemaName === name);
113
+ if (!mutation) {
114
+ throw new Error(`station: 선언한 문 ${name} 이(가) 스키마에 없습니다 — 이름이 틀렸거나 그 리졸버가 등록되지 않았습니다.`);
115
+ }
116
+ return storage.params
117
+ .filter(param => param.kind === 'arg' && param.target === mutation.target && param.methodName === mutation.methodName)
118
+ .sort((a, b) => a.index - b.index)
119
+ .map(param => {
120
+ const arg = param;
121
+ return {
122
+ name: arg.name,
123
+ required: !arg.typeOptions?.nullable,
124
+ ...(arg.description ? { describes: arg.description } : {}),
125
+ ...(notable.has(arg.name) ? { notable: true } : {})
126
+ };
127
+ });
128
+ }
77
129
  //# sourceMappingURL=station-product.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"station-product.js","sourceRoot":"","sources":["../../../server/service/station/station-product.ts"],"names":[],"mappings":";;AAqHA,sDAUC;AAQD,wCAQC;AAGD,oDAEC;AASD,0CAgCC;AA7LD,+CAAiD;AA2GjD,IAAI,QAAoC,CAAA;AAExC;;;;;;;GAOG;AACH,SAAgB,qBAAqB,CAAC,OAAuB;IAC3D,MAAM,OAAO,GAAI,CAAC,UAAU,EAAE,YAAY,EAAE,cAAc,CAAW,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC,CAAA;IAEpG,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CACb,YAAY,OAAO,EAAE,OAAO,IAAI,MAAM,UAAU,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,oCAAoC,CACxG,CAAA;IACH,CAAC;IAED,QAAQ,GAAG,OAAO,CAAA;AACpB,CAAC;AAED;;;;;GAKG;AACH,SAAgB,cAAc;IAC5B,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CACb,yGAAyG,CAC1G,CAAA;IACH,CAAC;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,oEAAoE;AACpE,SAAgB,oBAAoB;IAClC,QAAQ,GAAG,SAAS,CAAA;AACtB,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,eAAe,CAAC,QAAmC;IACjE,MAAM,OAAO,GAAG,IAAA,iCAAkB,GAAE,CAAA;IACpC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,IAAI,EAAE,CAAC,CAAA;IAE/C;;;;OAIG;IACH,MAAM,OAAO,GAAG,IAAI,GAAG,EAAY,CAAA;IAEnC,KAAK,IAAI,EAAE,GAAoB,QAAQ,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,KAAK,QAAQ,CAAC,SAAS,EAAE,EAAE,GAAG,MAAM,CAAC,cAAc,CAAC,EAAE,CAAC,EAAE,CAAC;QAC/G,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IACjB,CAAC;IAED,OAAO,OAAO,CAAC,MAAM;SAClB,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAkB,CAAC,CAAC;SACtD,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACb,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,QAAQ,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,QAAQ;QACtC;;;;;;;;WAQG;QACH,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9D,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACtD,CAAC,CAAC,CAAA;AACP,CAAC","sourcesContent":["import { getMetadataStorage } from 'type-graphql'\nimport type { EntityManager } from 'typeorm'\nimport type { Domain } from '@things-factory/shell'\n\nimport type { StationAssignmentSituation } from './station-assignment-choice.js'\nimport type { StationServiceField } from './station-flow.js'\n\n/**\n * What a product has to say before the station machinery will run for it (ADR-0083 decision 2).\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ## Why this file exists\n *\n * The station machinery is going to `@things-factory/station`, where operato-warehouse, and later\n * a yard and an energy product, will use the same authoring, the same flow model and the same\n * release approval. Nothing of that is about a factory. What *is* about a factory is small: which\n * values a screen may read, which mutations a step may call, who signs a release, and where those\n * values actually come from.\n *\n * This is that boundary, drawn **before** anything moves. Splitting while moving would leave the\n * first failure unattributable — was it the split or the move? — which is the order ADR-0078 used\n * when it lifted the outbox judgement out one layer at a time.\n *\n * ## Declared, never defaulted\n *\n * A product that has not declared gets an error naming what is missing, not an empty list. A\n * default here would mean a warehouse screen quietly offering a factory's mutations, or offering\n * none and reading as \"this product has no services\" — the silent empty this codebase keeps\n * finding. The same reason `registerWorkDoors` has no default door list.\n *\n * ## The assignment columns are the standard's general words (ADR-0083 decision 5)\n *\n * They used to be a factory's: `productionLineName` became `workCenterName` because the contract\n * already says `WorkCenter` is the general term over `ProcessCell` · `ProductionLine` ·\n * `ProductionUnit` · `StorageZone`. A warehouse zone and a yard block have somewhere to stand now.\n * `routeKey` and `operationKey` stay — a warehouse has a picking route too. No union of both\n * spellings: that would grow four products every time an axis is added.\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\n/** Something a station step can call. The product owns the list; the machinery reads the shape. */\nexport interface StationServiceDeclaration {\n /** The mutation name, exactly as the schema spells it. */\n name: string\n /** What an author is doing when they pick this — their words, not the mutation's. */\n describes: string\n /** The `@InputType` whose fields this call takes. */\n input: Function\n /**\n * Nullable fields that still must not be left empty.\n *\n * Absent means \"nobody wrote it down\", not zero — and the two are different facts on a\n * production report.\n */\n notable?: string[]\n}\n\n/**\n * The facts this product knows about one station, in the words assignment selects on.\n *\n * The machinery reads what the platform owns (the equipment and its classes) itself. This answers\n * only what the product's own domain knows — for a factory, which line it stands on and what the\n * order running there is made of.\n */\nexport type StationGroundReader = (\n manager: EntityManager,\n domain: Domain,\n stationName: string,\n at?: { operationKey?: string; operatorName?: string; jobOrderName?: string }\n) => Promise<Pick<StationAssignmentSituation, 'workCenterName' | 'routeKey' | 'materialClassIds' | 'personnelClassIds'>>\n\n/**\n * 기계가 만든 변경 목록을 제품의 이력으로 보내는 자리 (ADR-0083 결정 7, 선언 다섯).\n *\n * **무엇이 바뀌었는지는 기계가 만든다** — 자기 엔티티를 자기가 안다. 화면 정의가 지워질 때 어느 칸이\n * 무엇이었는지는 `deletionChanges` 가 이 모듈 안에서 만든다.\n *\n * **그것이 어느 이력으로 가는지는 제품이 정한다.** plant 는 `fact-change` 로 보내고, 창고는 자기\n * 이력을 갖는다. 그 표와 그 낱말(`source` 같은 것)은 제품의 것이라 모듈이 알 까닭이 없다.\n *\n * 필수다. 선언하지 않으면 통제 문서를 지운 이력이 **오류 없이 안 남는다** — 통제 문서에서 그것은\n * 「누가 언제 무엇을 지웠나」를 못 답하는 것이고, 감사에서 드러날 때는 이미 늦다.\n */\nexport type StationChangeRecorder = (\n manager: EntityManager,\n domain: Domain,\n what: {\n factKind: string\n factName: string\n changes: { field: string; fromValue?: string; toValue?: string }[]\n at?: Date\n },\n by: {\n user?: unknown\n /** 결재를 지나 온 것이면 그 건. 누가 승인했는지는 워크리스트가 들고 이것으로 찾아간다. */\n activityInstanceId?: string\n }\n) => Promise<unknown>\n\nexport interface StationProduct {\n /** For the message when something is missing — the product says its own name. */\n product: string\n services: StationServiceDeclaration[]\n readGround: StationGroundReader\n recordChange: StationChangeRecorder\n}\n\nlet declared: StationProduct | undefined\n\n/**\n * Called once at boot, from the product's own declaration file.\n *\n * 빠진 자리를 여기서 잡는다. 타입이 이미 다섯을 다 요구하지만 서버 쪽은 `strict: false` 로 돌고\n * 선언 파일에 `as any` 가 한 번 끼면 타입이 못 막는다. 그때 빠진 자리는 **부를 때**에야 드러나고,\n * 그 자리가 `recordChange` 면 통제 문서를 지운 이력이 오류 없이 안 남는다 — 기동 때 말하는 쪽이\n * 낫다(ADR-0083 결정 2 「선언은 필수이고 모듈에 기본값은 없다」).\n */\nexport function declareStationProduct(product: StationProduct): void {\n const missing = (['services', 'readGround', 'recordChange'] as const).filter(one => !product?.[one])\n\n if (missing.length) {\n throw new Error(\n `station: ${product?.product ?? '이 제품'} 의 선언에 ${missing.join(' · ')} 이(가) 없습니다 — 현장 화면이 서려면 다 채워야 합니다.`\n )\n }\n\n declared = product\n}\n\n/**\n * What the product declared — **or an error saying it has not.**\n *\n * Reading this before the declaration is loaded is a wiring mistake and shows as one. Answering\n * with an empty product instead would draw a screen with no services on it and no reason given.\n */\nexport function stationProduct(): StationProduct {\n if (!declared) {\n throw new Error(\n 'station: 이 제품이 선언을 하지 않았습니다 — 서비스 목록 · 바탕을 읽는 자리 · 기록을 남기는 자리를 declareStationProduct 로 등록해야 현장 화면이 섭니다.'\n )\n }\n\n return declared\n}\n\n/** Tests only — a declaration is meant to be made once, at boot. */\nexport function forgetStationProduct(): void {\n declared = undefined\n}\n\n/**\n * The fields of one service, as the running schema has them.\n *\n * Reads type-graphql's own metadata rather than issuing an introspection query: it is the same\n * declaration the schema is built from, it needs no round trip, and it works before the server\n * has finished starting.\n */\nexport function fieldsOfService(declared: StationServiceDeclaration): StationServiceField[] {\n const storage = getMetadataStorage()\n const notable = new Set(declared.notable ?? [])\n\n /*\n * Fields are stored against the class, and a class that extends another has its parent's fields\n * under the parent's target. Walking the prototype chain keeps an inherited field from going\n * missing — which would look exactly like the schema not having it.\n */\n const targets = new Set<Function>()\n\n for (let at: Function | null = declared.input; at && at !== Function.prototype; at = Object.getPrototypeOf(at)) {\n targets.add(at)\n }\n\n return storage.fields\n .filter(field => targets.has(field.target as Function))\n .map(field => ({\n name: field.name,\n required: !field.typeOptions?.nullable,\n /*\n * The description the field's own owner wrote.\n *\n * `goodCount` on a screen is a raw schema name to a plant manager. The input type already\n * carries `@Field({ description })` for many of them — the person who added the field is the\n * one who can say what it means, and they already did. Carrying it through beats a second\n * table here that goes stale the day a field is added: a field with no description shows its\n * name, which is what happened anyway before.\n */\n ...(field.description ? { describes: field.description } : {}),\n ...(notable.has(field.name) ? { notable: true } : {})\n }))\n}\n"]}
1
+ {"version":3,"file":"station-product.js","sourceRoot":"","sources":["../../../server/service/station/station-product.ts"],"names":[],"mappings":"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","sourcesContent":["import { getMetadataStorage } from 'type-graphql'\nimport type { EntityManager } from 'typeorm'\nimport type { Domain } from '@things-factory/shell'\n\nimport type { StationAssignmentSituation } from './station-assignment-choice.js'\nimport type { StationServiceField } from './station-flow.js'\nimport { STATION_AXES } from './station-axes.js'\n\n/**\n * What a product has to say before the station machinery will run for it (ADR-0083 decision 2).\n *\n * ═══════════════════════════════════════════════════════════════════════════\n * ## Why this file exists\n *\n * The station machinery is going to `@things-factory/station`, where operato-warehouse, and later\n * a yard and an energy product, will use the same authoring, the same flow model and the same\n * release approval. Nothing of that is about a factory. What *is* about a factory is small: which\n * values a screen may read, which mutations a step may call, who signs a release, and where those\n * values actually come from.\n *\n * This is that boundary, drawn **before** anything moves. Splitting while moving would leave the\n * first failure unattributable — was it the split or the move? — which is the order ADR-0078 used\n * when it lifted the outbox judgement out one layer at a time.\n *\n * ## Declared, never defaulted\n *\n * A product that has not declared gets an error naming what is missing, not an empty list. A\n * default here would mean a warehouse screen quietly offering a factory's mutations, or offering\n * none and reading as \"this product has no services\" — the silent empty this codebase keeps\n * finding. The same reason `registerWorkDoors` has no default door list.\n *\n * ## The assignment columns are the standard's general words (ADR-0083 decision 5)\n *\n * They used to be a factory's: `productionLineName` became `workCenterName` because the contract\n * already says `WorkCenter` is the general term over `ProcessCell` · `ProductionLine` ·\n * `ProductionUnit` · `StorageZone`. A warehouse zone and a yard block have somewhere to stand now.\n * `routeKey` and `operationKey` stay — a warehouse has a picking route too. No union of both\n * spellings: that would grow four products every time an axis is added.\n * ═══════════════════════════════════════════════════════════════════════════\n */\n\n/** Something a station step can call. The product owns the list; the machinery reads the shape. */\nexport interface StationServiceDeclaration {\n /** The mutation name, exactly as the schema spells it. */\n name: string\n /** What an author is doing when they pick this — their words, not the mutation's. */\n describes: string\n /**\n * The `@InputType` whose fields this call takes — **when it takes one.**\n *\n * Absent, the machine reads the mutation's own arguments by `name` (ADR-0083 ruling ②): a warehouse's\n * floor doors take a task name and a scanned place, not one input object, and wrapping them in one would\n * give the same door two shapes.\n */\n input?: Function\n /**\n * Nullable fields that still must not be left empty.\n *\n * Absent means \"nobody wrote it down\", not zero — and the two are different facts on a\n * production report.\n */\n notable?: string[]\n}\n\n/**\n * The facts this product knows about one station, in the words assignment selects on.\n *\n * The machinery reads what the platform owns (the equipment and its classes) itself. This answers\n * only what the product's own domain knows — for a factory, which line it stands on and what the\n * order running there is made of.\n */\nexport type StationGroundReader = (\n manager: EntityManager,\n domain: Domain,\n stationName: string,\n at?: { operationKey?: string; operatorName?: string; jobOrderName?: string }\n) => Promise<Pick<StationAssignmentSituation, 'workCenterName' | 'routeKey' | 'materialClassIds' | 'personnelClassIds'>>\n\n/**\n * 기계가 만든 변경 목록을 제품의 이력으로 보내는 자리 (ADR-0083 결정 7, 선언 다섯).\n *\n * **무엇이 바뀌었는지는 기계가 만든다** — 자기 엔티티를 자기가 안다. 화면 정의가 지워질 때 어느 칸이\n * 무엇이었는지는 `deletionChanges` 가 이 모듈 안에서 만든다.\n *\n * **그것이 어느 이력으로 가는지는 제품이 정한다.** plant 는 `fact-change` 로 보내고, 창고는 자기\n * 이력을 갖는다. 그 표와 그 낱말(`source` 같은 것)은 제품의 것이라 모듈이 알 까닭이 없다.\n *\n * 필수다. 선언하지 않으면 통제 문서를 지운 이력이 **오류 없이 안 남는다** — 통제 문서에서 그것은\n * 「누가 언제 무엇을 지웠나」를 못 답하는 것이고, 감사에서 드러날 때는 이미 늦다.\n */\nexport type StationChangeRecorder = (\n manager: EntityManager,\n domain: Domain,\n what: {\n factKind: string\n factName: string\n changes: { field: string; fromValue?: string; toValue?: string }[]\n at?: Date\n },\n by: {\n user?: unknown\n /** 결재를 지나 온 것이면 그 건. 누가 승인했는지는 워크리스트가 들고 이것으로 찾아간다. */\n activityInstanceId?: string\n }\n) => Promise<unknown>\n\nexport interface StationProduct {\n /** For the message when something is missing — the product says its own name. */\n product: string\n /**\n * The axes this product's screens read beyond the machine's own (ADR-0083 ruling ①) — `group.name`,\n * e.g. `unit.epc` · `task.kind`. The closed set a screen is checked against is the machine's list and\n * this. Absent means the product reads only the machine's.\n */\n axes?: readonly string[]\n services: StationServiceDeclaration[]\n readGround: StationGroundReader\n recordChange: StationChangeRecorder\n}\n\nlet declared: StationProduct | undefined\n\n/**\n * Called once at boot, from the product's own declaration file.\n *\n * 빠진 자리를 여기서 잡는다. 타입이 이미 다섯을 다 요구하지만 서버 쪽은 `strict: false` 로 돌고\n * 선언 파일에 `as any` 가 한 번 끼면 타입이 못 막는다. 그때 빠진 자리는 **부를 때**에야 드러나고,\n * 그 자리가 `recordChange` 면 통제 문서를 지운 이력이 오류 없이 안 남는다 — 기동 때 말하는 쪽이\n * 낫다(ADR-0083 결정 2 「선언은 필수이고 모듈에 기본값은 없다」).\n */\nexport function declareStationProduct(product: StationProduct): void {\n const missing = (['services', 'readGround', 'recordChange'] as const).filter(one => !product?.[one])\n\n if (missing.length) {\n throw new Error(\n `station: ${product?.product ?? '이 제품'} 의 선언에 ${missing.join(' · ')} 이(가) 없습니다 — 현장 화면이 서려면 다 채워야 합니다.`\n )\n }\n\n const axes = product.axes ?? []\n const shape = /^[a-z][A-Za-z0-9]*\\.[a-z][A-Za-z0-9]*$/\n const wrong = axes.filter((axis, at) => !shape.test(axis ?? '') || STATION_AXES.includes(axis) || axes.indexOf(axis) !== at)\n\n if (wrong.length) {\n /*\n * A misspelt axis would pass every screen that reads it and stay blank on the bench; one the machine\n * already has is two owners of one name; a repeat is harmless but says the list was not read.\n */\n throw new Error(\n `station: ${product.product} 의 axes 선언이 틀렸습니다 — ${wrong.map(one => JSON.stringify(one)).join(' · ')}. ` +\n '축은 `무리.이름` 모양이고, 기계가 이미 가진 축은 다시 선언하지 않습니다.'\n )\n }\n\n declared = product\n}\n\n/**\n * Every axis a screen of this product may read — the machine's and the product's (ADR-0083 ruling ①).\n * The authoring surface lists these; `checkFlow` refuses anything else.\n */\nexport function stationAxesOf(product: Pick<StationProduct, 'axes'>): string[] {\n return [...STATION_AXES, ...(product.axes ?? [])]\n}\n\n/**\n * What the product declared — **or an error saying it has not.**\n *\n * Reading this before the declaration is loaded is a wiring mistake and shows as one. Answering\n * with an empty product instead would draw a screen with no services on it and no reason given.\n */\nexport function stationProduct(): StationProduct {\n if (!declared) {\n throw new Error(\n 'station: 이 제품이 선언을 하지 않았습니다 — 서비스 목록 · 바탕을 읽는 자리 · 기록을 남기는 자리를 declareStationProduct 로 등록해야 현장 화면이 섭니다.'\n )\n }\n\n return declared\n}\n\n/** Tests only — a declaration is meant to be made once, at boot. */\nexport function forgetStationProduct(): void {\n declared = undefined\n}\n\n/**\n * The fields of one service, as the running schema has them.\n *\n * Reads type-graphql's own metadata rather than issuing an introspection query: it is the same\n * declaration the schema is built from, it needs no round trip, and it works before the server\n * has finished starting.\n */\nexport function fieldsOfService(declared: StationServiceDeclaration): StationServiceField[] {\n const storage = getMetadataStorage()\n const notable = new Set(declared.notable ?? [])\n\n if (!declared.input) {\n return argumentsOfMutation(declared.name, notable)\n }\n\n /*\n * Fields are stored against the class, and a class that extends another has its parent's fields\n * under the parent's target. Walking the prototype chain keeps an inherited field from going\n * missing — which would look exactly like the schema not having it.\n */\n const targets = new Set<Function>()\n\n for (let at: Function | null = declared.input; at && at !== Function.prototype; at = Object.getPrototypeOf(at)) {\n targets.add(at)\n }\n\n return storage.fields\n .filter(field => targets.has(field.target as Function))\n .map(field => ({\n name: field.name,\n required: !field.typeOptions?.nullable,\n /*\n * The description the field's own owner wrote.\n *\n * `goodCount` on a screen is a raw schema name to a plant manager. The input type already\n * carries `@Field({ description })` for many of them — the person who added the field is the\n * one who can say what it means, and they already did. Carrying it through beats a second\n * table here that goes stale the day a field is added: a field with no description shows its\n * name, which is what happened anyway before.\n */\n ...(field.description ? { describes: field.description } : {}),\n ...(notable.has(field.name) ? { notable: true } : {})\n }))\n}\n\n/**\n * A door that takes no input object — **its own arguments, as the schema has them** (ADR-0083 ruling ②).\n *\n * The same metadata the schema is built from, read in the same place as the input-type fields, so a door's\n * arguments cannot drift from what the running server takes. `@Ctx()` and other non-argument parameters are\n * not fields an author fills.\n *\n * A name the schema does not have is said. Answered with no fields, a misspelt door would look like a door\n * that needs nothing, and the screen built on it would call something that is not there.\n */\nfunction argumentsOfMutation(name: string, notable: Set<string>): StationServiceField[] {\n const storage = getMetadataStorage()\n const mutation = storage.mutations.find(one => one.schemaName === name)\n\n if (!mutation) {\n throw new Error(`station: 선언한 문 ${name} 이(가) 스키마에 없습니다 — 이름이 틀렸거나 그 리졸버가 등록되지 않았습니다.`)\n }\n\n return storage.params\n .filter(param => param.kind === 'arg' && param.target === mutation.target && param.methodName === mutation.methodName)\n .sort((a, b) => a.index - b.index)\n .map(param => {\n const arg = param as Extract<typeof param, { kind: 'arg' }>\n return {\n name: arg.name,\n required: !arg.typeOptions?.nullable,\n ...(arg.description ? { describes: arg.description } : {}),\n ...(notable.has(arg.name) ? { notable: true } : {})\n }\n })\n}\n\n"]}
@@ -5,6 +5,7 @@ const tslib_1 = require("tslib");
5
5
  const type_graphql_1 = require("type-graphql");
6
6
  const shell_1 = require("@things-factory/shell");
7
7
  const station_screen_layout_1 = require("./station-screen-layout");
8
+ const station_product_1 = require("./station-product");
8
9
  const station_screen_scorecard_1 = require("./station-screen-scorecard");
9
10
  let StationScreenScoreDimensionType = class StationScreenScoreDimensionType {
10
11
  };
@@ -121,7 +122,8 @@ exports.StationScreenDraft = StationScreenDraft = tslib_1.__decorate([
121
122
  */
122
123
  let StationScreenDraftResolver = class StationScreenDraftResolver {
123
124
  async stationScreenDraft(flow) {
124
- const drafted = (0, station_screen_layout_1.draftScreen)(flow);
125
+ /* The product's declared axes are part of the closed set the draft is checked against (ADR-0083 ruling ①). */
126
+ const drafted = (0, station_screen_layout_1.draftScreen)(flow, { axes: (0, station_product_1.stationProduct)().axes });
125
127
  return {
126
128
  model: drafted.model,
127
129
  requires: drafted.requires,
@@ -1 +1 @@
1
- {"version":3,"file":"station-screen-draft-resolver.js","sourceRoot":"","sources":["../../../server/service/station/station-screen-draft-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Directive, Field, ObjectType, Query, Resolver, UseMiddleware } from 'type-graphql'\n\nimport { ScalarObject, SpeakRefusals } from '@things-factory/shell'\n\nimport type { StationFlow } from './station-flow'\nimport { draftScreen } from './station-screen-layout'\nimport { inspectStationScreen } from './station-screen-scorecard'\n\n@ObjectType({ description: 'one evidence-bound dimension of a station screen scorecard' })\nexport class StationScreenScoreDimensionType {\n @Field()\n key: string\n\n @Field()\n score: number\n\n @Field()\n max: number\n\n @Field()\n evidence: string\n}\n\n@ObjectType({ description: 'quality evidence for this screen draft; it is recomputed by the server' })\nexport class StationScreenScorecardType {\n @Field()\n score: number\n\n @Field()\n grade: string\n\n @Field(type => [String])\n blockers: string[]\n\n @Field(type => [StationScreenScoreDimensionType])\n dimensions: StationScreenScoreDimensionType[]\n}\n\n@ObjectType({ description: 'something the author should know before this flow goes in front of an operator' })\nexport class StationFlowProblem {\n @Field({ description: \"'error' — this will not work · 'warning' — this will work and mislead\" })\n level: string\n\n @Field({ nullable: true, description: 'the step it is about' })\n stepId?: string\n\n @Field({ nullable: true, description: 'the service field it is about' })\n field?: string\n\n @Field({ description: \"which check this is — the screen says it in the reader's language\" })\n code: string\n\n @Field(type => ScalarObject, { nullable: true, description: 'values to fill into that sentence' })\n params?: Record<string, string>\n\n @Field({ description: 'the same thing in Korean, for logs and the scenario tool — not for the screen' })\n says: string\n}\n\n@ObjectType({ description: 'what this flow would draw, without saving anything' })\nexport class StationScreenDraft {\n @Field({ description: 'the board model the flow lays out' })\n model: string\n\n @Field(type => [String], { description: 'station context axes the drawing would read' })\n requires: string[]\n\n @Field(type => [StationFlowProblem], { description: 'what the author should know' })\n problems: StationFlowProblem[]\n\n @Field(type => StationScreenScorecardType, { description: 'server-derived quality evidence for this draft' })\n scorecard: StationScreenScorecardType\n}\n\n/**\n * What a flow would draw — **asked before it is saved.**\n *\n * ── Why the server draws the preview ──────────────────────────────────────\n * The seed lays screens out with `screenModel`, and so does saving. If the composer laid its own\n * preview out in the browser there would be two calculators, and the day they part the author sees\n * one screen while the plant gets another. That kind of difference does not announce itself — it\n * shows up as an operator saying the button moved.\n *\n * So the composer sends the flow and gets back exactly what would be stored.\n *\n * ── The checks ride along ─────────────────────────────────────────────────\n * `checkFlow` runs here for the same reason, and because the author should not have to press save\n * to find out that a required argument has nowhere to come from.\n *\n * There is no repository here and nothing is written. The whole answer is `draftScreen`.\n */\n@Resolver()\n@UseMiddleware(SpeakRefusals)\nexport class StationScreenDraftResolver {\n @Directive('@privilege(category: \"station\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => StationScreenDraft, { description: 'lay out a flow without saving it' })\n async stationScreenDraft(@Arg('flow', type => ScalarObject) flow: StationFlow): Promise<StationScreenDraft> {\n const drafted = draftScreen(flow)\n\n return {\n model: drafted.model,\n requires: drafted.requires,\n problems: drafted.problems.map(problem => ({\n level: problem.level,\n stepId: problem.stepId,\n field: problem.field,\n code: problem.code,\n params: problem.params,\n says: problem.says\n })),\n scorecard: inspectStationScreen(flow, drafted.problems)\n }\n }\n}\n"]}
1
+ {"version":3,"file":"station-screen-draft-resolver.js","sourceRoot":"","sources":["../../../server/service/station/station-screen-draft-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Directive, Field, ObjectType, Query, Resolver, UseMiddleware } from 'type-graphql'\n\nimport { ScalarObject, SpeakRefusals } from '@things-factory/shell'\n\nimport type { StationFlow } from './station-flow'\nimport { draftScreen } from './station-screen-layout'\nimport { stationProduct } from './station-product'\nimport { inspectStationScreen } from './station-screen-scorecard'\n\n@ObjectType({ description: 'one evidence-bound dimension of a station screen scorecard' })\nexport class StationScreenScoreDimensionType {\n @Field()\n key: string\n\n @Field()\n score: number\n\n @Field()\n max: number\n\n @Field()\n evidence: string\n}\n\n@ObjectType({ description: 'quality evidence for this screen draft; it is recomputed by the server' })\nexport class StationScreenScorecardType {\n @Field()\n score: number\n\n @Field()\n grade: string\n\n @Field(type => [String])\n blockers: string[]\n\n @Field(type => [StationScreenScoreDimensionType])\n dimensions: StationScreenScoreDimensionType[]\n}\n\n@ObjectType({ description: 'something the author should know before this flow goes in front of an operator' })\nexport class StationFlowProblem {\n @Field({ description: \"'error' — this will not work · 'warning' — this will work and mislead\" })\n level: string\n\n @Field({ nullable: true, description: 'the step it is about' })\n stepId?: string\n\n @Field({ nullable: true, description: 'the service field it is about' })\n field?: string\n\n @Field({ description: \"which check this is — the screen says it in the reader's language\" })\n code: string\n\n @Field(type => ScalarObject, { nullable: true, description: 'values to fill into that sentence' })\n params?: Record<string, string>\n\n @Field({ description: 'the same thing in Korean, for logs and the scenario tool — not for the screen' })\n says: string\n}\n\n@ObjectType({ description: 'what this flow would draw, without saving anything' })\nexport class StationScreenDraft {\n @Field({ description: 'the board model the flow lays out' })\n model: string\n\n @Field(type => [String], { description: 'station context axes the drawing would read' })\n requires: string[]\n\n @Field(type => [StationFlowProblem], { description: 'what the author should know' })\n problems: StationFlowProblem[]\n\n @Field(type => StationScreenScorecardType, { description: 'server-derived quality evidence for this draft' })\n scorecard: StationScreenScorecardType\n}\n\n/**\n * What a flow would draw — **asked before it is saved.**\n *\n * ── Why the server draws the preview ──────────────────────────────────────\n * The seed lays screens out with `screenModel`, and so does saving. If the composer laid its own\n * preview out in the browser there would be two calculators, and the day they part the author sees\n * one screen while the plant gets another. That kind of difference does not announce itself — it\n * shows up as an operator saying the button moved.\n *\n * So the composer sends the flow and gets back exactly what would be stored.\n *\n * ── The checks ride along ─────────────────────────────────────────────────\n * `checkFlow` runs here for the same reason, and because the author should not have to press save\n * to find out that a required argument has nowhere to come from.\n *\n * There is no repository here and nothing is written. The whole answer is `draftScreen`.\n */\n@Resolver()\n@UseMiddleware(SpeakRefusals)\nexport class StationScreenDraftResolver {\n @Directive('@privilege(category: \"station\", privilege: \"query\", domainOwnerGranted: true)')\n @Query(returns => StationScreenDraft, { description: 'lay out a flow without saving it' })\n async stationScreenDraft(@Arg('flow', type => ScalarObject) flow: StationFlow): Promise<StationScreenDraft> {\n /* The product's declared axes are part of the closed set the draft is checked against (ADR-0083 ruling ①). */\n const drafted = draftScreen(flow, { axes: stationProduct().axes })\n\n return {\n model: drafted.model,\n requires: drafted.requires,\n problems: drafted.problems.map(problem => ({\n level: problem.level,\n stepId: problem.stepId,\n field: problem.field,\n code: problem.code,\n params: problem.params,\n says: problem.says\n })),\n scorecard: inspectStationScreen(flow, drafted.problems)\n }\n }\n}\n"]}
@@ -106,7 +106,9 @@ export interface StationScreenDraftResult {
106
106
  * 저작면은 고칠 때마다 묻고, 그중에는 마지막 단계를 지운 편집도 있다. 그때 던지면 화면이 「그리지
107
107
  * 못했습니다」를 말하는데 사실은 그릴 것이 없는 것뿐이다.
108
108
  */
109
- export declare function draftScreen(flow: StationFlow | null | undefined): StationScreenDraftResult;
109
+ export declare function draftScreen(flow: StationFlow | null | undefined, options?: {
110
+ axes?: readonly string[];
111
+ }): StationScreenDraftResult;
110
112
  /**
111
113
  * 이 부품들이 읽는 컨텍스트 축.
112
114
  *
@@ -169,12 +169,12 @@ function partsOfStep(step, flow) {
169
169
  * 저작면은 고칠 때마다 묻고, 그중에는 마지막 단계를 지운 편집도 있다. 그때 던지면 화면이 「그리지
170
170
  * 못했습니다」를 말하는데 사실은 그릴 것이 없는 것뿐이다.
171
171
  */
172
- function draftScreen(flow) {
172
+ function draftScreen(flow, options = {}) {
173
173
  const safe = { steps: Array.isArray(flow?.steps) ? flow.steps : [] };
174
174
  return {
175
175
  model: screenModel(partsOfFlow(safe)),
176
176
  requires: (0, station_flow_1.axesRead)(safe),
177
- problems: [...(0, station_flow_1.checkFlow)(safe), ...safe.steps.filter(step => (step.kind === 'run' && !(0, station_runtime_binding_1.runAction)(step)) || (step.kind === 'enter' && !(0, station_runtime_binding_1.entryCounts)(safe, step.id))).map(step => ({
177
+ problems: [...(0, station_flow_1.checkFlow)(safe, options), ...safe.steps.filter(step => (step.kind === 'run' && !(0, station_runtime_binding_1.runAction)(step)) || (step.kind === 'enter' && !(0, station_runtime_binding_1.entryCounts)(safe, step.id))).map(step => ({
178
178
  level: 'error', stepId: step.id, code: 'text.station-runtime-not-connected',
179
179
  params: { step: step.label ?? step.id },
180
180
  says: `「${step.label ?? step.id}」의 실행 연결이 없습니다. 다른 업무로 대신 실행하지 않습니다.`
@@ -1 +1 @@
1
- {"version":3,"file":"station-screen-layout.js","sourceRoot":"","sources":["../../../server/service/station/station-screen-layout.ts"],"names":[],"mappings":";;AA+EA,kCAoDC;AA8DD,kCAQC;AAyDD,kCAcC;AAWD,kCAUC;AArSD,iDAAoD;AAEpD,uEAAkE;AAqClE,qCAAqC;AACrC,MAAM,KAAK,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAA;AAE1C,MAAM,GAAG,GAAG,EAAE,CAAA;AACd,MAAM,GAAG,GAAG,EAAE,CAAA;AAEd,iDAAiD;AACjD,MAAM,MAAM,GAA2B;IACrC,KAAK,EAAE,EAAE;IACT,QAAQ,EAAE,GAAG;IACb,GAAG,EAAE,GAAG;IACR,KAAK,EAAE,GAAG;IACV,MAAM,EAAE,GAAG;IACX,MAAM,EAAE,GAAG;IACX,MAAM,EAAE,EAAE;IACV,MAAM,EAAE,GAAG;IACX,SAAS,EAAE,GAAG;IACd,WAAW,EAAE,GAAG;IAChB,OAAO,EAAE,CAAC;CACX,CAAA;AAED,qCAAqC;AACrC,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAA;AAYlD;;;;;GAKG;AACH,SAAgB,WAAW,CAAC,KAA0B;IACpD,MAAM,UAAU,GAAa,EAAE,CAAA;IAC/B,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,GAAG,GAAG,GAAG,CAAC,CAAA;IAEnC,IAAI,GAAG,GAAG,GAAG,CAAA;IACb,IAAI,KAAK,GAAG,CAAC,CAAA;IAEb,2BAA2B;IAC3B,KAAK,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,GAAG,KAAK,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC;QACzC,MAAM,IAAI,GAAG,KAAK,CAAC,EAAE,CAAC,CAAA;QAEtB,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC5B,UAAU,CAAC,IAAI,CAAC;gBACd,IAAI,EAAE,iBAAiB;gBACvB,EAAE,EAAE,QAAQ,KAAK,EAAE,EAAE;gBACrB,wCAAwC;gBACxC,IAAI,EAAE,GAAG;gBACT,GAAG,EAAE,KAAK,CAAC,MAAM,GAAG,GAAG;gBACvB,KAAK,EAAE,GAAG;gBACV,MAAM,EAAE,EAAE;gBACV,cAAc,EAAE,IAAI,CAAC,cAAc,IAAI,EAAE;aAC1C,CAAC,CAAA;YACF,SAAQ;QACV,CAAC;QAED,0CAA0C;QAC1C,MAAM,GAAG,GAAwB,CAAC,IAAI,CAAC,CAAA;QAEvC,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAChC,OAAO,EAAE,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;gBACjE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;YACvB,CAAC;QACH,CAAC;QAED,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,CAAA;QACvE,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;QAE/B,GAAG,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE;YAC1B,UAAU,CAAC,IAAI,CAAC;gBACd,GAAI,OAAO,CAAC,GAAG,CAAsB;gBACrC,EAAE,EAAE,QAAQ,KAAK,EAAE,EAAE;gBACrB,IAAI,EAAE,GAAG,GAAG,MAAM,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC;gBAClC,GAAG;gBACH,KAAK;gBACL,MAAM;aACP,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;QAEF,GAAG,IAAI,MAAM,GAAG,GAAG,CAAA;IACrB,CAAC;IAED,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC,EAAE,UAAU,EAAE,CAAC,CAAA;AACtG,CAAC;AAED,SAAS,QAAQ,CAAC,IAAuB;IACvC,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAA;IAClD,CAAC;IAED,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,KAAK,CAAA;AAC1C,CAAC;AAED;;;;;GAKG;AACH,SAAS,OAAO,CAAC,IAAuB;IACtC,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,KAAK,OAAO;YACV,OAAO;gBACL,IAAI,EAAE,eAAe;gBACrB,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE;gBACvB,2BAA2B;gBAC3B,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;aAC9B,CAAA;QAEH,KAAK,KAAK;YACR,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,EAAE,EAAE,CAAA;QAEpF,KAAK,QAAQ;YACX,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAA;QAEzI,KAAK,QAAQ;YACX,OAAO;gBACL,IAAI,EAAE,gBAAgB;gBACtB,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE;gBACvB,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC5C,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC5D,CAAA;QAEH,KAAK,QAAQ;YACX,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,IAAI,KAAK,EAAE,CAAA;QAEhG;YACE,2DAA2D;YAC3D,OAAO,EAAE,IAAI,EAAE,WAAW,IAAI,CAAC,IAAI,EAAE,EAAE,KAAK,EAAG,IAA2B,CAAC,KAAK,IAAI,EAAE,EAAE,CAAA;IAC5F,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,WAAW,CAAC,IAAiB;IAC3C,MAAM,KAAK,GAAwB,EAAE,CAAA;IAErC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;QAC9B,KAAK,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;IACxC,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,WAAW,CAAC,IAAiB,EAAE,IAAiB;IACvD,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,KAAK,MAAM;YACT,OAAO,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAA;QAEzD,KAAK,OAAO;YACV;;;eAGG;YACH,OAAO,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAA,qCAAW,EAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,MAAM,EAAE,QAAQ,EAAE,CAAC,IAAA,qCAAW,EAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;QAErI,KAAK,KAAK;YACR,OAAO,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAA,mCAAS,EAAC,IAAI,CAAC,IAAI,OAAO,EAAE,QAAQ,EAAE,CAAC,IAAA,mCAAS,EAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAEhH,KAAK,UAAU;YACb,sDAAsD;YACtD,OAAO,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;QAEvG,KAAK,QAAQ;YACX,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,UAA+B,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QAEtE;YACE,wCAAwC;YACxC,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI,EAAE,CAAC,CAAC,CAAA;IACtG,CAAC;AACH,CAAC;AAiBD;;;;;;;;;;;GAWG;AACH,SAAgB,WAAW,CAAC,IAAoC;IAC9D,MAAM,IAAI,GAAgB,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,IAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAA;IAElF,OAAO;QACL,KAAK,EAAE,WAAW,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QACrC,QAAQ,EAAE,IAAA,uBAAQ,EAAC,IAAI,CAAC;QACxB,QAAQ,EAAE,CAAC,GAAG,IAAA,wBAAS,EAAC,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CACzD,CAAC,IAAI,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC,IAAA,mCAAS,EAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,OAAO,IAAI,CAAC,IAAA,qCAAW,EAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,CACpG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBACb,KAAK,EAAE,OAAgB,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,oCAAoC;gBACpF,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,EAAE,EAAE;gBACvC,IAAI,EAAE,IAAI,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,EAAE,sCAAsC;aACtE,CAAC,CAAC,CAAC;KACL,CAAA;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,SAAgB,WAAW,CAAC,KAA0B;IACpD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAE9B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAC1B,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACtB,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,EAAE,CAAA;AACzB,CAAC","sourcesContent":["import { axesRead, checkFlow } from './station-flow'\nimport type { StationFlow, StationProblem, StationStep } from './station-flow'\nimport { entryCounts, runAction } from './station-runtime-binding'\n\n/**\n * 부품 목록 → things-scene 모델. **자리는 여기서 계산한다.**\n *\n * ── 왜 아무도 좌표를 안 드나 ───────────────────────────────────────────────\n * 부품 하나를 더할 때마다 나머지를 손으로 밀어야 하고, 그렇게 만든 판은 2px 씩 어긋난다. 폼에서\n * 눈에 걸리는 것이 대개 그 어긋남이다.\n *\n * ── 계산기는 **한 벌이다** ─────────────────────────────────────────────────\n * 시드도 저작면도 여기를 지난다. 두 벌을 두면 시드로 세운 화면과 사람이 만든 화면이 다르게 생기고,\n * 그때 어느 쪽이 맞는지 아무도 못 정한다. 그래서 이 파일은 시드가 아니라 **station 어휘**에 산다.\n *\n * ── 규칙은 하나다: 세로로 쌓고, 짝지을 것만 나란히 ─────────────────────────\n * 숫자패드와 단추는 여럿이 나란히 놓인다 — 작업자가 손을 옮기는 거리가 짧아야 한다. 나머지는\n * 한 줄에 하나다.\n */\n\n/**\n * 화면에 놓이는 부품 하나 — **그려지는 것만.**\n *\n * 흐름의 단계(`StationStep`)와 다르다. 단계는 「사람이 무엇을 하나」이고 여기는 「무엇이 그려지나」다.\n * 한 단계가 부품 둘을 낳기도 하고(값 넣기 → 숫자패드), 하나도 안 낳기도 한다(설정).\n */\nexport type StationScreenPart =\n /** 컨텍스트 축 하나를 값으로. `stationBindingNames()` 가 이름의 유일한 원본이다. */\n | { part: 'field'; field: string; label?: string; big?: boolean }\n | { part: 'who'; label?: string; prompt?: string }\n | { part: 'order'; label?: string }\n | { part: 'counts'; label?: string }\n | { part: 'numpad'; label?: string; counts?: 'good' | 'scrap'; disabled?: boolean }\n | { part: 'action'; label?: string; action: 'start' | 'finish' | 'pause' | 'resume'; reasonCode?: string; disabled?: boolean }\n | { part: 'staged'; label?: string; removable?: boolean }\n | { part: 'directive'; label?: string }\n | { part: 'maintenance'; label?: string }\n | { part: 'context'; refreshSeconds?: number }\n\n/** 작업대 화면의 판 크기. 벤치 태블릿의 가로 기준이다. */\nconst BOARD = { width: 1280, height: 720 }\n\nconst PAD = 24\nconst GAP = 16\n\n/** 부품마다의 높이. 읽는 거리에서 정해진다 — 한 걸음 떨어져 보는 화면이다. */\nconst HEIGHT: Record<string, number> = {\n field: 96,\n fieldBig: 132,\n who: 120,\n order: 132,\n counts: 132,\n numpad: 260,\n action: 88,\n staged: 200,\n directive: 220,\n maintenance: 120,\n context: 0\n}\n\n/** 한 줄에 여럿이 서는 것들. 나머지는 한 줄에 하나다. */\nconst SIDE_BY_SIDE = new Set(['numpad', 'action'])\n\ninterface Placed {\n type: string\n id: string\n left: number\n top: number\n width: number\n height: number\n [more: string]: unknown\n}\n\n/**\n * 부품에서 모델을 만든다.\n *\n * `context` 는 그려지지 않지만 **모델에는 들어간다** — 갱신 주기를 든 부품이라, 없으면 그 화면이\n * 무엇을 얼마나 자주 읽을지 아무 데도 안 적힌다. 자리는 판 밖에 둔다.\n */\nexport function screenModel(parts: StationScreenPart[]): string {\n const components: Placed[] = []\n const inner = BOARD.width - PAD * 2\n\n let top = PAD\n let index = 0\n\n /* 나란히 설 것들을 묶어 가며 내려간다. */\n for (let at = 0; at < parts.length; at++) {\n const part = parts[at]\n\n if (part.part === 'context') {\n components.push({\n type: 'station-context',\n id: `part-${index++}`,\n /* 판 아래에 둔다 — 그려지지 않지만 저작면에서는 집어야 한다. */\n left: PAD,\n top: BOARD.height + GAP,\n width: 240,\n height: 60,\n refreshSeconds: part.refreshSeconds ?? 10\n })\n continue\n }\n\n /* 같은 종류가 이어지고 그것이 나란히 서는 것이면 한 줄에 담는다. */\n const row: StationScreenPart[] = [part]\n\n if (SIDE_BY_SIDE.has(part.part)) {\n while (at + 1 < parts.length && parts[at + 1].part === part.part) {\n row.push(parts[++at])\n }\n }\n\n const width = Math.floor((inner - GAP * (row.length - 1)) / row.length)\n const height = heightOf(row[0])\n\n row.forEach((one, column) => {\n components.push({\n ...(propsOf(one) as { type: string }),\n id: `part-${index++}`,\n left: PAD + column * (width + GAP),\n top,\n width,\n height\n })\n })\n\n top += height + GAP\n }\n\n return JSON.stringify({ width: BOARD.width, height: Math.max(BOARD.height, top + PAD), components })\n}\n\nfunction heightOf(part: StationScreenPart): number {\n if (part.part === 'field') {\n return part.big ? HEIGHT.fieldBig : HEIGHT.field\n }\n\n return HEIGHT[part.part] ?? HEIGHT.field\n}\n\n/**\n * 부품 선언 → 씬 컴포넌트의 속성.\n *\n * `equipment` 를 넣지 않는다. 비우면 그 화면이 걸린 작업대를 따르고, 적으면 그 설비에 못 박힌다 —\n * 데모 화면은 복제해 고치는 첫 장이라 못 박으면 안 된다.\n */\nfunction propsOf(part: StationScreenPart): Record<string, unknown> {\n switch (part.part) {\n case 'field':\n return {\n type: 'station-field',\n field: part.field,\n label: part.label ?? '',\n /* 큰 값은 한 걸음 떨어져 읽는 자리다. */\n valueSize: part.big ? 42 : 26\n }\n\n case 'who':\n return { type: 'station-who', label: part.label ?? '', prompt: part.prompt ?? '' }\n\n case 'numpad':\n return { type: 'station-numpad', label: part.label ?? '', counts: part.counts ?? 'good', ...(part.disabled ? { disabled: true } : {}) }\n\n case 'action':\n return {\n type: 'station-action',\n label: part.label ?? '',\n action: part.action,\n ...(part.disabled ? { disabled: true } : {}),\n ...(part.reasonCode ? { reasonCode: part.reasonCode } : {})\n }\n\n case 'staged':\n return { type: 'station-staged', label: part.label ?? '', removable: part.removable ?? false }\n\n default:\n /* order · counts · directive · maintenance — 이름표 하나뿐이다. */\n return { type: `station-${part.part}`, label: (part as { label?: string }).label ?? '' }\n }\n}\n\n/**\n * 흐름 → 그려질 부품.\n *\n * ── 단계와 부품은 하나가 아니다 ────────────────────────────────────────────\n * 「값 넣기」 한 단계가 숫자패드 하나를 낳고, 「보기」 한 단계가 읽는 축 수만큼 값을 낳고, 「설정」은\n * 하나도 안 낳는다(그려지지 않지만 모델에는 남는다 — 갱신 주기를 든 부품이라).\n *\n * ── 이름표가 없으면 축 이름을 쓴다 ─────────────────────────────────────────\n * 빈 이름표로 두지 않는다. 작업대에서 맨숫자는 가장 자주 오독되는 것이고, 그 오독은 수량을 잘못\n * 적는 것으로 이어진다.\n */\nexport function partsOfFlow(flow: StationFlow): StationScreenPart[] {\n const parts: StationScreenPart[] = []\n\n for (const step of flow.steps) {\n parts.push(...partsOfStep(step, flow))\n }\n\n return parts\n}\n\nfunction partsOfStep(step: StationStep, flow: StationFlow): StationScreenPart[] {\n switch (step.kind) {\n case 'scan':\n return [{ part: 'who', label: step.label, prompt: '' }]\n\n case 'enter':\n /*\n * 무엇을 받는 자리인지는 이 단계를 **쓰는 쪽**이 안다(`{ from: 'input' }`). 여기서는 모르므로\n * 기본값을 두고, 저작면이 고른 뒤에 바뀐다.\n */\n return [{ part: 'numpad', label: step.label, counts: entryCounts(flow, step.id) ?? 'good', disabled: !entryCounts(flow, step.id) }]\n\n case 'run':\n return [{ part: 'action', label: step.label, action: runAction(step) ?? 'start', disabled: !runAction(step) }]\n\n case 'settings':\n /* 갱신 주기를 여기서 떨어뜨리면, 계산해 둔 값이 판까지 안 가고 전부 기본값으로 선다. */\n return [{ part: 'context', ...(step.refreshSeconds ? { refreshSeconds: step.refreshSeconds } : {}) }]\n\n case 'legacy':\n return step.legacyPart ? [step.legacyPart as StationScreenPart] : []\n\n default:\n /* 보기 — 읽는 축마다 값 하나. 축이 없으면 그릴 것도 없다. */\n return (step.reads ?? []).map(axis => ({ part: 'field', field: axis, label: step.label ?? axis }))\n }\n}\n\n/**\n * 이 실행 단계가 어떤 단추인가.\n *\n * 단계의 `status` 출처가 `{ from: 'step' }` 이라는 것은 「단계의 뜻이 정한다」는 말이고, 그 뜻이\n * 여기서 단추 모양으로도 나온다. 아직 흐름이 그것을 따로 적지 않으므로 이름표에서 읽는다 —\n * 흐름에 칸이 생기면 그때 그것을 본다.\n */\n\n/** 저장하지 않고 흐름이 무엇을 그릴지 — 저작면의 미리보기가 이것을 받는다. */\nexport interface StationScreenDraftResult {\n model: string\n requires: string[]\n problems: StationProblem[]\n}\n\n/**\n * 흐름 하나가 낳는 **모든 것**을 한 번에 — 도안 · 읽는 축 · 저작자가 알아야 할 것.\n *\n * ── 왜 셋을 묶었나 ────────────────────────────────────────────────────────\n * 저작면의 미리보기와 저장이 같은 계산을 봐야 한다. 미리보기가 도안만 받고 저장이 축을 따로 세면,\n * 저작자가 본 화면과 배정 검증이 검사하는 축이 갈릴 수 있다 — 그리고 그 차이는 배정하는 날에야\n * 드러난다.\n *\n * ── 빈 흐름은 오류가 아니다 ───────────────────────────────────────────────\n * 저작면은 고칠 때마다 묻고, 그중에는 마지막 단계를 지운 편집도 있다. 그때 던지면 화면이 「그리지\n * 못했습니다」를 말하는데 사실은 그릴 것이 없는 것뿐이다.\n */\nexport function draftScreen(flow: StationFlow | null | undefined): StationScreenDraftResult {\n const safe: StationFlow = { steps: Array.isArray(flow?.steps) ? flow!.steps : [] }\n\n return {\n model: screenModel(partsOfFlow(safe)),\n requires: axesRead(safe),\n problems: [...checkFlow(safe), ...safe.steps.filter(step =>\n (step.kind === 'run' && !runAction(step)) || (step.kind === 'enter' && !entryCounts(safe, step.id))\n ).map(step => ({\n level: 'error' as const, stepId: step.id, code: 'text.station-runtime-not-connected',\n params: { step: step.label ?? step.id },\n says: `「${step.label ?? step.id}」의 실행 연결이 없습니다. 다른 업무로 대신 실행하지 않습니다.`\n }))]\n }\n}\n\n/**\n * 이 부품들이 읽는 컨텍스트 축.\n *\n * `StationScreenSpec.requires` 는 **파생값이다.** 손으로 적으면 부품을 고칠 때 어긋나고, 그러면\n * 배정 검증이 이미 사실이 아닌 것을 검사한다.\n *\n * 흐름 쪽에는 같은 일을 하는 `axesRead` 가 따로 있다 — 들어오는 어휘가 다르기 때문이다(시드는\n * 부품으로, 저작면은 단계로 들어온다). 규칙이 갈리지 않게 둘 다 여기 계산기 옆에 산다.\n */\nexport function axesOfParts(parts: StationScreenPart[]): string[] {\n const seen = new Set<string>()\n\n for (const part of parts) {\n if (part.part === 'field') {\n seen.add(part.field)\n }\n }\n\n return [...seen].sort()\n}\n"]}
1
+ {"version":3,"file":"station-screen-layout.js","sourceRoot":"","sources":["../../../server/service/station/station-screen-layout.ts"],"names":[],"mappings":"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","sourcesContent":["import { axesRead, checkFlow } from './station-flow'\nimport type { StationFlow, StationProblem, StationStep } from './station-flow'\nimport { entryCounts, runAction } from './station-runtime-binding'\n\n/**\n * 부품 목록 → things-scene 모델. **자리는 여기서 계산한다.**\n *\n * ── 왜 아무도 좌표를 안 드나 ───────────────────────────────────────────────\n * 부품 하나를 더할 때마다 나머지를 손으로 밀어야 하고, 그렇게 만든 판은 2px 씩 어긋난다. 폼에서\n * 눈에 걸리는 것이 대개 그 어긋남이다.\n *\n * ── 계산기는 **한 벌이다** ─────────────────────────────────────────────────\n * 시드도 저작면도 여기를 지난다. 두 벌을 두면 시드로 세운 화면과 사람이 만든 화면이 다르게 생기고,\n * 그때 어느 쪽이 맞는지 아무도 못 정한다. 그래서 이 파일은 시드가 아니라 **station 어휘**에 산다.\n *\n * ── 규칙은 하나다: 세로로 쌓고, 짝지을 것만 나란히 ─────────────────────────\n * 숫자패드와 단추는 여럿이 나란히 놓인다 — 작업자가 손을 옮기는 거리가 짧아야 한다. 나머지는\n * 한 줄에 하나다.\n */\n\n/**\n * 화면에 놓이는 부품 하나 — **그려지는 것만.**\n *\n * 흐름의 단계(`StationStep`)와 다르다. 단계는 「사람이 무엇을 하나」이고 여기는 「무엇이 그려지나」다.\n * 한 단계가 부품 둘을 낳기도 하고(값 넣기 → 숫자패드), 하나도 안 낳기도 한다(설정).\n */\nexport type StationScreenPart =\n /** 컨텍스트 축 하나를 값으로. `stationBindingNames()` 가 이름의 유일한 원본이다. */\n | { part: 'field'; field: string; label?: string; big?: boolean }\n | { part: 'who'; label?: string; prompt?: string }\n | { part: 'order'; label?: string }\n | { part: 'counts'; label?: string }\n | { part: 'numpad'; label?: string; counts?: 'good' | 'scrap'; disabled?: boolean }\n | { part: 'action'; label?: string; action: 'start' | 'finish' | 'pause' | 'resume'; reasonCode?: string; disabled?: boolean }\n | { part: 'staged'; label?: string; removable?: boolean }\n | { part: 'directive'; label?: string }\n | { part: 'maintenance'; label?: string }\n | { part: 'context'; refreshSeconds?: number }\n\n/** 작업대 화면의 판 크기. 벤치 태블릿의 가로 기준이다. */\nconst BOARD = { width: 1280, height: 720 }\n\nconst PAD = 24\nconst GAP = 16\n\n/** 부품마다의 높이. 읽는 거리에서 정해진다 — 한 걸음 떨어져 보는 화면이다. */\nconst HEIGHT: Record<string, number> = {\n field: 96,\n fieldBig: 132,\n who: 120,\n order: 132,\n counts: 132,\n numpad: 260,\n action: 88,\n staged: 200,\n directive: 220,\n maintenance: 120,\n context: 0\n}\n\n/** 한 줄에 여럿이 서는 것들. 나머지는 한 줄에 하나다. */\nconst SIDE_BY_SIDE = new Set(['numpad', 'action'])\n\ninterface Placed {\n type: string\n id: string\n left: number\n top: number\n width: number\n height: number\n [more: string]: unknown\n}\n\n/**\n * 부품에서 모델을 만든다.\n *\n * `context` 는 그려지지 않지만 **모델에는 들어간다** — 갱신 주기를 든 부품이라, 없으면 그 화면이\n * 무엇을 얼마나 자주 읽을지 아무 데도 안 적힌다. 자리는 판 밖에 둔다.\n */\nexport function screenModel(parts: StationScreenPart[]): string {\n const components: Placed[] = []\n const inner = BOARD.width - PAD * 2\n\n let top = PAD\n let index = 0\n\n /* 나란히 설 것들을 묶어 가며 내려간다. */\n for (let at = 0; at < parts.length; at++) {\n const part = parts[at]\n\n if (part.part === 'context') {\n components.push({\n type: 'station-context',\n id: `part-${index++}`,\n /* 판 아래에 둔다 — 그려지지 않지만 저작면에서는 집어야 한다. */\n left: PAD,\n top: BOARD.height + GAP,\n width: 240,\n height: 60,\n refreshSeconds: part.refreshSeconds ?? 10\n })\n continue\n }\n\n /* 같은 종류가 이어지고 그것이 나란히 서는 것이면 한 줄에 담는다. */\n const row: StationScreenPart[] = [part]\n\n if (SIDE_BY_SIDE.has(part.part)) {\n while (at + 1 < parts.length && parts[at + 1].part === part.part) {\n row.push(parts[++at])\n }\n }\n\n const width = Math.floor((inner - GAP * (row.length - 1)) / row.length)\n const height = heightOf(row[0])\n\n row.forEach((one, column) => {\n components.push({\n ...(propsOf(one) as { type: string }),\n id: `part-${index++}`,\n left: PAD + column * (width + GAP),\n top,\n width,\n height\n })\n })\n\n top += height + GAP\n }\n\n return JSON.stringify({ width: BOARD.width, height: Math.max(BOARD.height, top + PAD), components })\n}\n\nfunction heightOf(part: StationScreenPart): number {\n if (part.part === 'field') {\n return part.big ? HEIGHT.fieldBig : HEIGHT.field\n }\n\n return HEIGHT[part.part] ?? HEIGHT.field\n}\n\n/**\n * 부품 선언 → 씬 컴포넌트의 속성.\n *\n * `equipment` 를 넣지 않는다. 비우면 그 화면이 걸린 작업대를 따르고, 적으면 그 설비에 못 박힌다 —\n * 데모 화면은 복제해 고치는 첫 장이라 못 박으면 안 된다.\n */\nfunction propsOf(part: StationScreenPart): Record<string, unknown> {\n switch (part.part) {\n case 'field':\n return {\n type: 'station-field',\n field: part.field,\n label: part.label ?? '',\n /* 큰 값은 한 걸음 떨어져 읽는 자리다. */\n valueSize: part.big ? 42 : 26\n }\n\n case 'who':\n return { type: 'station-who', label: part.label ?? '', prompt: part.prompt ?? '' }\n\n case 'numpad':\n return { type: 'station-numpad', label: part.label ?? '', counts: part.counts ?? 'good', ...(part.disabled ? { disabled: true } : {}) }\n\n case 'action':\n return {\n type: 'station-action',\n label: part.label ?? '',\n action: part.action,\n ...(part.disabled ? { disabled: true } : {}),\n ...(part.reasonCode ? { reasonCode: part.reasonCode } : {})\n }\n\n case 'staged':\n return { type: 'station-staged', label: part.label ?? '', removable: part.removable ?? false }\n\n default:\n /* order · counts · directive · maintenance — 이름표 하나뿐이다. */\n return { type: `station-${part.part}`, label: (part as { label?: string }).label ?? '' }\n }\n}\n\n/**\n * 흐름 → 그려질 부품.\n *\n * ── 단계와 부품은 하나가 아니다 ────────────────────────────────────────────\n * 「값 넣기」 한 단계가 숫자패드 하나를 낳고, 「보기」 한 단계가 읽는 축 수만큼 값을 낳고, 「설정」은\n * 하나도 안 낳는다(그려지지 않지만 모델에는 남는다 — 갱신 주기를 든 부품이라).\n *\n * ── 이름표가 없으면 축 이름을 쓴다 ─────────────────────────────────────────\n * 빈 이름표로 두지 않는다. 작업대에서 맨숫자는 가장 자주 오독되는 것이고, 그 오독은 수량을 잘못\n * 적는 것으로 이어진다.\n */\nexport function partsOfFlow(flow: StationFlow): StationScreenPart[] {\n const parts: StationScreenPart[] = []\n\n for (const step of flow.steps) {\n parts.push(...partsOfStep(step, flow))\n }\n\n return parts\n}\n\nfunction partsOfStep(step: StationStep, flow: StationFlow): StationScreenPart[] {\n switch (step.kind) {\n case 'scan':\n return [{ part: 'who', label: step.label, prompt: '' }]\n\n case 'enter':\n /*\n * 무엇을 받는 자리인지는 이 단계를 **쓰는 쪽**이 안다(`{ from: 'input' }`). 여기서는 모르므로\n * 기본값을 두고, 저작면이 고른 뒤에 바뀐다.\n */\n return [{ part: 'numpad', label: step.label, counts: entryCounts(flow, step.id) ?? 'good', disabled: !entryCounts(flow, step.id) }]\n\n case 'run':\n return [{ part: 'action', label: step.label, action: runAction(step) ?? 'start', disabled: !runAction(step) }]\n\n case 'settings':\n /* 갱신 주기를 여기서 떨어뜨리면, 계산해 둔 값이 판까지 안 가고 전부 기본값으로 선다. */\n return [{ part: 'context', ...(step.refreshSeconds ? { refreshSeconds: step.refreshSeconds } : {}) }]\n\n case 'legacy':\n return step.legacyPart ? [step.legacyPart as StationScreenPart] : []\n\n default:\n /* 보기 — 읽는 축마다 값 하나. 축이 없으면 그릴 것도 없다. */\n return (step.reads ?? []).map(axis => ({ part: 'field', field: axis, label: step.label ?? axis }))\n }\n}\n\n/**\n * 이 실행 단계가 어떤 단추인가.\n *\n * 단계의 `status` 출처가 `{ from: 'step' }` 이라는 것은 「단계의 뜻이 정한다」는 말이고, 그 뜻이\n * 여기서 단추 모양으로도 나온다. 아직 흐름이 그것을 따로 적지 않으므로 이름표에서 읽는다 —\n * 흐름에 칸이 생기면 그때 그것을 본다.\n */\n\n/** 저장하지 않고 흐름이 무엇을 그릴지 — 저작면의 미리보기가 이것을 받는다. */\nexport interface StationScreenDraftResult {\n model: string\n requires: string[]\n problems: StationProblem[]\n}\n\n/**\n * 흐름 하나가 낳는 **모든 것**을 한 번에 — 도안 · 읽는 축 · 저작자가 알아야 할 것.\n *\n * ── 왜 셋을 묶었나 ────────────────────────────────────────────────────────\n * 저작면의 미리보기와 저장이 같은 계산을 봐야 한다. 미리보기가 도안만 받고 저장이 축을 따로 세면,\n * 저작자가 본 화면과 배정 검증이 검사하는 축이 갈릴 수 있다 — 그리고 그 차이는 배정하는 날에야\n * 드러난다.\n *\n * ── 빈 흐름은 오류가 아니다 ───────────────────────────────────────────────\n * 저작면은 고칠 때마다 묻고, 그중에는 마지막 단계를 지운 편집도 있다. 그때 던지면 화면이 「그리지\n * 못했습니다」를 말하는데 사실은 그릴 것이 없는 것뿐이다.\n */\nexport function draftScreen(flow: StationFlow | null | undefined, options: { axes?: readonly string[] } = {}): StationScreenDraftResult {\n const safe: StationFlow = { steps: Array.isArray(flow?.steps) ? flow!.steps : [] }\n\n return {\n model: screenModel(partsOfFlow(safe)),\n requires: axesRead(safe),\n problems: [...checkFlow(safe, options), ...safe.steps.filter(step =>\n (step.kind === 'run' && !runAction(step)) || (step.kind === 'enter' && !entryCounts(safe, step.id))\n ).map(step => ({\n level: 'error' as const, stepId: step.id, code: 'text.station-runtime-not-connected',\n params: { step: step.label ?? step.id },\n says: `「${step.label ?? step.id}」의 실행 연결이 없습니다. 다른 업무로 대신 실행하지 않습니다.`\n }))]\n }\n}\n\n/**\n * 이 부품들이 읽는 컨텍스트 축.\n *\n * `StationScreenSpec.requires` 는 **파생값이다.** 손으로 적으면 부품을 고칠 때 어긋나고, 그러면\n * 배정 검증이 이미 사실이 아닌 것을 검사한다.\n *\n * 흐름 쪽에는 같은 일을 하는 `axesRead` 가 따로 있다 — 들어오는 어휘가 다르기 때문이다(시드는\n * 부품으로, 저작면은 단계로 들어온다). 규칙이 갈리지 않게 둘 다 여기 계산기 옆에 산다.\n */\nexport function axesOfParts(parts: StationScreenPart[]): string[] {\n const seen = new Set<string>()\n\n for (const part of parts) {\n if (part.part === 'field') {\n seen.add(part.field)\n }\n }\n\n return [...seen].sort()\n}\n"]}
@@ -24,4 +24,13 @@ export declare class StationServiceType {
24
24
  */
25
25
  export declare class StationServiceResolver {
26
26
  stationServices(): Promise<StationServiceType[]>;
27
+ /**
28
+ * The names a screen of this product may read — the machine's and what the product declared (ADR-0083
29
+ * ruling ①). The same union `stationScreenDraft` checks against.
30
+ *
31
+ * ⚠ The browser's field pickers still read the machine's list directly (`stationBindingNames()`), so a
32
+ * product's own axes pass the check but are not offered in the picker until the authoring pages read this
33
+ * (step 3-라). Said here so the gap is not mistaken for the axes not existing.
34
+ */
35
+ stationAxes(): Promise<string[]>;
27
36
  }