@things-factory/ops-master 10.1.81 → 10.1.106
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/ai-entry/master-entry-rules.d.ts +175 -0
- package/dist-server/service/ai-entry/master-entry-rules.js +302 -0
- package/dist-server/service/ai-entry/master-entry-rules.js.map +1 -0
- package/dist-server/service/ai-entry/master-entry-targets.d.ts +23 -0
- package/dist-server/service/ai-entry/master-entry-targets.js +293 -0
- package/dist-server/service/ai-entry/master-entry-targets.js.map +1 -0
- package/dist-server/service/index.d.ts +2 -0
- package/dist-server/service/index.js +10 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/translations/en.json +21 -1
- package/translations/ja.json +21 -1
- package/translations/ko.json +21 -1
- package/translations/ms.json +21 -1
- package/translations/zh.json +21 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { type MasterSnapshot } from '../master-conformance';
|
|
2
|
+
export type OpsMasterEntryFieldType = 'string' | 'text' | 'number' | 'integer' | 'date' | 'datetime' | 'boolean' | 'enum' | 'reference';
|
|
3
|
+
export interface OpsMasterEntryReference {
|
|
4
|
+
query: string;
|
|
5
|
+
variables(text: string): Record<string, unknown>;
|
|
6
|
+
recordsOf(data: any, text: string): {
|
|
7
|
+
id: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
}[];
|
|
10
|
+
}
|
|
11
|
+
export interface OpsMasterEntryField {
|
|
12
|
+
name: string;
|
|
13
|
+
type: OpsMasterEntryFieldType;
|
|
14
|
+
required?: boolean;
|
|
15
|
+
values?: string[];
|
|
16
|
+
description?: string;
|
|
17
|
+
labelKey?: string;
|
|
18
|
+
valueKeyPrefix?: string;
|
|
19
|
+
reference?: OpsMasterEntryReference;
|
|
20
|
+
}
|
|
21
|
+
export interface OpsMasterEntryIssue {
|
|
22
|
+
field?: string;
|
|
23
|
+
code: string;
|
|
24
|
+
messageKey?: string;
|
|
25
|
+
params?: Record<string, unknown>;
|
|
26
|
+
blocking: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface OpsMasterEntryRow {
|
|
29
|
+
index: number;
|
|
30
|
+
values: Record<string, unknown>;
|
|
31
|
+
sources: {
|
|
32
|
+
attachmentId?: string;
|
|
33
|
+
where?: string;
|
|
34
|
+
person?: boolean;
|
|
35
|
+
}[];
|
|
36
|
+
issues: OpsMasterEntryIssue[];
|
|
37
|
+
}
|
|
38
|
+
export type OpsMasterEntryRecord = {
|
|
39
|
+
id: string;
|
|
40
|
+
} & Record<string, unknown>;
|
|
41
|
+
export interface OpsMasterEntryTarget {
|
|
42
|
+
key: string;
|
|
43
|
+
labelKey: string;
|
|
44
|
+
description: string;
|
|
45
|
+
fields: OpsMasterEntryField[];
|
|
46
|
+
mutation: string;
|
|
47
|
+
variables(row: OpsMasterEntryRow): Record<string, unknown>;
|
|
48
|
+
appliedOf(data: any, row: OpsMasterEntryRow): {
|
|
49
|
+
id?: string;
|
|
50
|
+
label?: string;
|
|
51
|
+
} | undefined;
|
|
52
|
+
maxRows?: number;
|
|
53
|
+
check(rows: OpsMasterEntryRow[], context: {
|
|
54
|
+
state: any;
|
|
55
|
+
}): Promise<OpsMasterEntryIssue[][]>;
|
|
56
|
+
update: {
|
|
57
|
+
read: {
|
|
58
|
+
query: string;
|
|
59
|
+
variables(ids: string[]): Record<string, unknown>;
|
|
60
|
+
recordsOf(data: any, ids: string[]): OpsMasterEntryRecord[];
|
|
61
|
+
};
|
|
62
|
+
find: {
|
|
63
|
+
query: string;
|
|
64
|
+
variables(criteria: Record<string, unknown>): Record<string, unknown>;
|
|
65
|
+
recordsOf(data: any, criteria: Record<string, unknown>): OpsMasterEntryRecord[];
|
|
66
|
+
criteria: OpsMasterEntryField[];
|
|
67
|
+
};
|
|
68
|
+
mutation: string;
|
|
69
|
+
variables(change: {
|
|
70
|
+
id: string;
|
|
71
|
+
record: OpsMasterEntryRecord;
|
|
72
|
+
values: Record<string, unknown>;
|
|
73
|
+
}): Record<string, unknown>;
|
|
74
|
+
labelOf(record: OpsMasterEntryRecord): string;
|
|
75
|
+
fields: string[];
|
|
76
|
+
/** Read-only checks of the changes - asked when the card is made and again when it is pressed. */
|
|
77
|
+
check(changes: {
|
|
78
|
+
id: string;
|
|
79
|
+
record?: OpsMasterEntryRecord;
|
|
80
|
+
values: Record<string, unknown>;
|
|
81
|
+
}[], context: {
|
|
82
|
+
state: any;
|
|
83
|
+
}): Promise<OpsMasterEntryIssue[][]>;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The axes the assistant may enter. The revision-controlled ones (material, recipe, test specification) change
|
|
88
|
+
* through a revision draft and an approval, never a row of a card; operations and routes are not opened yet.
|
|
89
|
+
*/
|
|
90
|
+
export declare const OPS_MASTER_ENTRY_AXES: readonly ["operationalLocation", "equipment", "person", "physicalAsset", "stopReason", "nonconformanceCause", "equipmentClass", "personnelClass", "physicalAssetClass", "materialClass"];
|
|
91
|
+
export type OpsMasterEntryAxis = (typeof OPS_MASTER_ENTRY_AXES)[number];
|
|
92
|
+
/** `operationalLocation` → `operational-location`. An entry key is lowercase (`<app>.<record>`). */
|
|
93
|
+
export declare const kebabOf: (axis: string) => string;
|
|
94
|
+
/** `ops-master.operational-location`. */
|
|
95
|
+
export declare const opsMasterEntryKey: (axis: OpsMasterEntryAxis) => string;
|
|
96
|
+
/** `label.ops-master-entry-operational-location` - the card's title. */
|
|
97
|
+
export declare const opsMasterEntryLabelKey: (axis: OpsMasterEntryAxis) => string;
|
|
98
|
+
/** What a rule needs to know of an axis. */
|
|
99
|
+
export interface MasterEntryRuleSpec {
|
|
100
|
+
axis: OpsMasterEntryAxis;
|
|
101
|
+
fields: OpsMasterEntryField[];
|
|
102
|
+
/** Fields written as a comma list of names of another axis (or this one) - stored as an array. */
|
|
103
|
+
lists: Record<string, OpsMasterEntryAxis>;
|
|
104
|
+
/** The field that names this axis' own parent. */
|
|
105
|
+
parentField?: string;
|
|
106
|
+
/** Where `checkMaster` reads this axis - absent: it has no rule for it. */
|
|
107
|
+
snapshotKey?: keyof MasterSnapshot;
|
|
108
|
+
}
|
|
109
|
+
export declare const idOf: (value: unknown) => string | undefined;
|
|
110
|
+
/** "PC-A, PC-B" · "PC-A;PC-B" · lines → ['PC-A', 'PC-B']. Order kept, a repeated name once. */
|
|
111
|
+
export declare function namesIn(value: unknown): string[];
|
|
112
|
+
/** A row's name, or nothing when it is empty. */
|
|
113
|
+
export declare function nameOf(values: Record<string, unknown>): string | undefined;
|
|
114
|
+
/**
|
|
115
|
+
* The patch of one row - **only the declared fields**, because a change saves `{ ...found, ...values }` and anything
|
|
116
|
+
* else sent would overwrite the record. A reference goes as the name it found, a list as an array of names.
|
|
117
|
+
*
|
|
118
|
+
* `keepEmpty`: a change that empties a field sends null; a new record leaves it out.
|
|
119
|
+
*/
|
|
120
|
+
export declare function masterPatchOf(values: Record<string, unknown>, spec: Pick<MasterEntryRuleSpec, 'fields' | 'lists'>, keepEmpty?: boolean): Record<string, unknown>;
|
|
121
|
+
/**
|
|
122
|
+
* The record a name points at - the same name, or the same description, written with any spacing or case. Nothing
|
|
123
|
+
* else is taken: a master's parent or home found by a near name would be entered as fact. Several near matches are
|
|
124
|
+
* handed back so the person can say which one.
|
|
125
|
+
*/
|
|
126
|
+
export declare function mastersNamed(data: any, list: string, text: string): {
|
|
127
|
+
id: string;
|
|
128
|
+
label?: string;
|
|
129
|
+
}[];
|
|
130
|
+
/** A stored master row as the check reads it - its id, its name, its end, and the axis' own fields. */
|
|
131
|
+
export type MasterRow = {
|
|
132
|
+
id?: string;
|
|
133
|
+
name: string;
|
|
134
|
+
effectiveEnd?: Date | string | null;
|
|
135
|
+
} & Record<string, unknown>;
|
|
136
|
+
/** Of the names asked, the rows on the axis that carry them, by name. A name not in the map does not exist. */
|
|
137
|
+
export type MasterNamesLookup = (axis: OpsMasterEntryAxis, names: string[]) => Promise<Map<string, MasterRow>>;
|
|
138
|
+
/** Retired - its end has come. A future end is still in use. */
|
|
139
|
+
export declare const retiredRow: (row: MasterRow | undefined, now?: number) => boolean;
|
|
140
|
+
/** Two rows of one proposal with one name - the second would collide with the first; it is kept out, naming the row. */
|
|
141
|
+
export declare function repeatedNames(names: (string | undefined)[]): OpsMasterEntryIssue[][];
|
|
142
|
+
/**
|
|
143
|
+
* The fields `checkMaster` would find empty once this row is entered - the conformance screen's own rule. A field
|
|
144
|
+
* the target declares required is said by the framework already and is not said twice. They do not keep the row
|
|
145
|
+
* out: the save takes them, and the conformance screen lists them.
|
|
146
|
+
*/
|
|
147
|
+
export declare function conformanceGaps(spec: MasterEntryRuleSpec, values: Record<string, unknown>): OpsMasterEntryIssue[];
|
|
148
|
+
/**
|
|
149
|
+
* The target's check - read-only, one list per row. `existing` asks the database (scoped to the domain) which names
|
|
150
|
+
* are there; it is an argument so the rules are tested without one.
|
|
151
|
+
*/
|
|
152
|
+
export declare function checkMasterRows(spec: MasterEntryRuleSpec, rows: {
|
|
153
|
+
values: Record<string, unknown>;
|
|
154
|
+
}[], existing: MasterNamesLookup): Promise<OpsMasterEntryIssue[][]>;
|
|
155
|
+
/**
|
|
156
|
+
* The field that says what a record sits under on its own axis - a location's parent, a reason's parent, a class's
|
|
157
|
+
* bases. Absent: the axis is flat.
|
|
158
|
+
*/
|
|
159
|
+
export declare function lineageFieldOf(spec: MasterEntryRuleSpec): string | undefined;
|
|
160
|
+
/**
|
|
161
|
+
* Whether climbing from `start` along the lineage reaches one of `selves` - a location put under its own child, a
|
|
162
|
+
* class inheriting from one that inherits from it. Changes of the same proposal count as if made (`pending`). The
|
|
163
|
+
* climb reads one level at a time and stops at a name seen before, so a cycle already stored does not loop it.
|
|
164
|
+
*/
|
|
165
|
+
export declare function climbReaches(spec: MasterEntryRuleSpec, field: string, start: string[], selves: Set<string>, pending: Map<string, string[]>, lookup: MasterNamesLookup): Promise<boolean>;
|
|
166
|
+
/**
|
|
167
|
+
* The check of changes - one list per change. A new name another record holds (a retired one too - the name is
|
|
168
|
+
* unique on the axis); one new name given to two records; a class named in a list that is not there; a record put
|
|
169
|
+
* under itself or under something below it.
|
|
170
|
+
*/
|
|
171
|
+
export declare function checkMasterChanges(spec: MasterEntryRuleSpec, changes: {
|
|
172
|
+
id: string;
|
|
173
|
+
record?: Record<string, unknown>;
|
|
174
|
+
values: Record<string, unknown>;
|
|
175
|
+
}[], lookup: MasterNamesLookup): Promise<OpsMasterEntryIssue[][]>;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.retiredRow = exports.idOf = exports.opsMasterEntryLabelKey = exports.opsMasterEntryKey = exports.kebabOf = exports.OPS_MASTER_ENTRY_AXES = void 0;
|
|
4
|
+
exports.namesIn = namesIn;
|
|
5
|
+
exports.nameOf = nameOf;
|
|
6
|
+
exports.masterPatchOf = masterPatchOf;
|
|
7
|
+
exports.mastersNamed = mastersNamed;
|
|
8
|
+
exports.repeatedNames = repeatedNames;
|
|
9
|
+
exports.conformanceGaps = conformanceGaps;
|
|
10
|
+
exports.checkMasterRows = checkMasterRows;
|
|
11
|
+
exports.lineageFieldOf = lineageFieldOf;
|
|
12
|
+
exports.climbReaches = climbReaches;
|
|
13
|
+
exports.checkMasterChanges = checkMasterChanges;
|
|
14
|
+
/*
|
|
15
|
+
* AI 입력 — the rules of entering master records, without the framework, so the tests read them bare
|
|
16
|
+
* (master-entry-targets.ts declares the targets; operato-application/design/ai/data-entry.md is the frame).
|
|
17
|
+
*
|
|
18
|
+
* The declarations are plain data in the shape of `@things-factory/ai-assistant`'s `EntryTarget`. This package does
|
|
19
|
+
* not depend on the assistant: an application that has it registers each target with `registerEntryTarget`, and the
|
|
20
|
+
* types below are structural copies of the parts it reads.
|
|
21
|
+
*
|
|
22
|
+
* What is here is what a row cannot say about itself: a name already taken, a name repeated within one proposal, a
|
|
23
|
+
* class named in a list that is not there, and the conformance rules (`checkMaster`) a row would break once entered.
|
|
24
|
+
*/
|
|
25
|
+
const master_conformance_1 = require("../master-conformance");
|
|
26
|
+
/* ── The axes ────────────────────────────────────────────────────────────────────────────────────────── */
|
|
27
|
+
/**
|
|
28
|
+
* The axes the assistant may enter. The revision-controlled ones (material, recipe, test specification) change
|
|
29
|
+
* through a revision draft and an approval, never a row of a card; operations and routes are not opened yet.
|
|
30
|
+
*/
|
|
31
|
+
exports.OPS_MASTER_ENTRY_AXES = [
|
|
32
|
+
'operationalLocation',
|
|
33
|
+
'equipment',
|
|
34
|
+
'person',
|
|
35
|
+
'physicalAsset',
|
|
36
|
+
'stopReason',
|
|
37
|
+
'nonconformanceCause',
|
|
38
|
+
'equipmentClass',
|
|
39
|
+
'personnelClass',
|
|
40
|
+
'physicalAssetClass',
|
|
41
|
+
'materialClass'
|
|
42
|
+
];
|
|
43
|
+
/** `operationalLocation` → `operational-location`. An entry key is lowercase (`<app>.<record>`). */
|
|
44
|
+
const kebabOf = (axis) => axis.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
|
|
45
|
+
exports.kebabOf = kebabOf;
|
|
46
|
+
/** `ops-master.operational-location`. */
|
|
47
|
+
const opsMasterEntryKey = (axis) => `ops-master.${(0, exports.kebabOf)(axis)}`;
|
|
48
|
+
exports.opsMasterEntryKey = opsMasterEntryKey;
|
|
49
|
+
/** `label.ops-master-entry-operational-location` - the card's title. */
|
|
50
|
+
const opsMasterEntryLabelKey = (axis) => `label.ops-master-entry-${(0, exports.kebabOf)(axis)}`;
|
|
51
|
+
exports.opsMasterEntryLabelKey = opsMasterEntryLabelKey;
|
|
52
|
+
/* ── Values ──────────────────────────────────────────────────────────────────────────────────────────── */
|
|
53
|
+
const idOf = (value) => value && typeof value === 'object' && typeof value.id === 'string' ? value.id : undefined;
|
|
54
|
+
exports.idOf = idOf;
|
|
55
|
+
/** "PC-A, PC-B" · "PC-A;PC-B" · lines → ['PC-A', 'PC-B']. Order kept, a repeated name once. */
|
|
56
|
+
function namesIn(value) {
|
|
57
|
+
const parts = Array.isArray(value) ? value.map(String) : typeof value === 'string' ? value.split(/[,;\n]/) : [];
|
|
58
|
+
return [...new Set(parts.map(part => part.trim()).filter(Boolean))];
|
|
59
|
+
}
|
|
60
|
+
/** A row's name, or nothing when it is empty. */
|
|
61
|
+
function nameOf(values) {
|
|
62
|
+
const name = values.name;
|
|
63
|
+
return typeof name === 'string' && name.trim() ? name.trim() : undefined;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The patch of one row - **only the declared fields**, because a change saves `{ ...found, ...values }` and anything
|
|
67
|
+
* else sent would overwrite the record. A reference goes as the name it found, a list as an array of names.
|
|
68
|
+
*
|
|
69
|
+
* `keepEmpty`: a change that empties a field sends null; a new record leaves it out.
|
|
70
|
+
*/
|
|
71
|
+
function masterPatchOf(values, spec, keepEmpty = false) {
|
|
72
|
+
const patch = {};
|
|
73
|
+
for (const field of spec.fields) {
|
|
74
|
+
if (!(field.name in values))
|
|
75
|
+
continue;
|
|
76
|
+
const value = values[field.name];
|
|
77
|
+
if (value === undefined)
|
|
78
|
+
continue;
|
|
79
|
+
if (value === null || value === '') {
|
|
80
|
+
if (keepEmpty)
|
|
81
|
+
patch[field.name] = null;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (field.type === 'reference') {
|
|
85
|
+
/* An unsettled name keeps its row out already; it is never sent as if it were found. */
|
|
86
|
+
const id = (0, exports.idOf)(value);
|
|
87
|
+
if (id !== undefined)
|
|
88
|
+
patch[field.name] = id;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (spec.lists[field.name]) {
|
|
92
|
+
const names = namesIn(value);
|
|
93
|
+
if (names.length)
|
|
94
|
+
patch[field.name] = names;
|
|
95
|
+
else if (keepEmpty)
|
|
96
|
+
patch[field.name] = null;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
patch[field.name] = value;
|
|
100
|
+
}
|
|
101
|
+
return patch;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The record a name points at - the same name, or the same description, written with any spacing or case. Nothing
|
|
105
|
+
* else is taken: a master's parent or home found by a near name would be entered as fact. Several near matches are
|
|
106
|
+
* handed back so the person can say which one.
|
|
107
|
+
*/
|
|
108
|
+
function mastersNamed(data, list, text) {
|
|
109
|
+
const items = data?.[list]?.items ?? [];
|
|
110
|
+
const plain = (value) => String(value ?? '').replace(/\s+/g, '').toLowerCase();
|
|
111
|
+
const wanted = plain(text);
|
|
112
|
+
const named = items.filter(item => item.name && (plain(item.name) === wanted || (item.description && plain(item.description) === wanted)));
|
|
113
|
+
const near = items.filter(item => !!item.name);
|
|
114
|
+
const chosen = named.length ? named : near.length > 1 ? near : [];
|
|
115
|
+
return chosen.map(item => ({ id: item.name, label: item.description ? `${item.name} · ${item.description}` : item.name }));
|
|
116
|
+
}
|
|
117
|
+
/** Retired - its end has come. A future end is still in use. */
|
|
118
|
+
const retiredRow = (row, now = Date.now()) => !!row?.effectiveEnd && new Date(row.effectiveEnd).getTime() <= now;
|
|
119
|
+
exports.retiredRow = retiredRow;
|
|
120
|
+
const issue = (code, field, params, blocking = true) => ({
|
|
121
|
+
...(field ? { field } : {}),
|
|
122
|
+
code,
|
|
123
|
+
messageKey: `text.ops-master-entry-${code}`,
|
|
124
|
+
...(params ? { params } : {}),
|
|
125
|
+
blocking
|
|
126
|
+
});
|
|
127
|
+
/** Names of a comma list that are not there, or retired. */
|
|
128
|
+
function listIssues(field, said, known) {
|
|
129
|
+
const missing = said.filter(one => !known.has(one));
|
|
130
|
+
const retired = said.filter(one => (0, exports.retiredRow)(known.get(one)));
|
|
131
|
+
return [
|
|
132
|
+
...(missing.length ? [issue('names-not-found', field, { names: missing.join(', ') })] : []),
|
|
133
|
+
...(retired.length ? [issue('names-retired', field, { names: retired.join(', ') })] : [])
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
/** Two rows of one proposal with one name - the second would collide with the first; it is kept out, naming the row. */
|
|
137
|
+
function repeatedNames(names) {
|
|
138
|
+
return names.map((name, i) => {
|
|
139
|
+
const earlier = name ? names.slice(0, i).indexOf(name) : -1;
|
|
140
|
+
return earlier < 0 ? [] : [issue('name-repeated-row', 'name', { row: earlier + 1 })];
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function emptySnapshot() {
|
|
144
|
+
return {
|
|
145
|
+
persons: [],
|
|
146
|
+
equipment: [],
|
|
147
|
+
physicalAssets: [],
|
|
148
|
+
materialDefinitions: [],
|
|
149
|
+
operationDefinitions: [],
|
|
150
|
+
routeDefinitions: [],
|
|
151
|
+
recipeDefinitions: [],
|
|
152
|
+
operationalLocations: [],
|
|
153
|
+
stopReasons: [],
|
|
154
|
+
testSpecifications: [],
|
|
155
|
+
personnelClasses: [],
|
|
156
|
+
equipmentClasses: [],
|
|
157
|
+
physicalAssetClasses: [],
|
|
158
|
+
materialClasses: []
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The fields `checkMaster` would find empty once this row is entered - the conformance screen's own rule. A field
|
|
163
|
+
* the target declares required is said by the framework already and is not said twice. They do not keep the row
|
|
164
|
+
* out: the save takes them, and the conformance screen lists them.
|
|
165
|
+
*/
|
|
166
|
+
function conformanceGaps(spec, values) {
|
|
167
|
+
if (!spec.snapshotKey)
|
|
168
|
+
return [];
|
|
169
|
+
const required = new Set(spec.fields.filter(field => field.required).map(field => field.name));
|
|
170
|
+
const declared = new Set(spec.fields.map(field => field.name));
|
|
171
|
+
const snapshot = emptySnapshot();
|
|
172
|
+
snapshot[spec.snapshotKey] = [{ ...masterPatchOf(values, spec), name: nameOf(values) ?? '' }];
|
|
173
|
+
return (0, master_conformance_1.checkMaster)(snapshot)
|
|
174
|
+
.filter(found => found.axis === spec.axis && found.kind === 'missing' && declared.has(found.field) && !required.has(found.field))
|
|
175
|
+
.map(found => issue('conformance-missing', found.field, undefined, false));
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The target's check - read-only, one list per row. `existing` asks the database (scoped to the domain) which names
|
|
179
|
+
* are there; it is an argument so the rules are tested without one.
|
|
180
|
+
*/
|
|
181
|
+
async function checkMasterRows(spec, rows, existing) {
|
|
182
|
+
const names = rows.map(row => nameOf(row.values));
|
|
183
|
+
const own = await existing(spec.axis, [...new Set(names.filter((name) => !!name))]);
|
|
184
|
+
/* Every list name of every row, asked once per axis. */
|
|
185
|
+
const listed = new Map();
|
|
186
|
+
for (const [field, axis] of Object.entries(spec.lists)) {
|
|
187
|
+
const set = listed.get(axis) ?? new Set();
|
|
188
|
+
for (const row of rows)
|
|
189
|
+
for (const name of namesIn(row.values[field]))
|
|
190
|
+
set.add(name);
|
|
191
|
+
listed.set(axis, set);
|
|
192
|
+
}
|
|
193
|
+
const found = new Map();
|
|
194
|
+
for (const [axis, set] of listed)
|
|
195
|
+
found.set(axis, set.size ? await existing(axis, [...set]) : new Map());
|
|
196
|
+
const repeated = repeatedNames(names);
|
|
197
|
+
return rows.map((row, i) => {
|
|
198
|
+
const issues = [...repeated[i]];
|
|
199
|
+
const name = names[i];
|
|
200
|
+
const there = name ? own.get(name) : undefined;
|
|
201
|
+
/* The name is unique on the axis, retired rows included - a new row with it is refused by the database. */
|
|
202
|
+
if (there)
|
|
203
|
+
issues.push(issue((0, exports.retiredRow)(there) ? 'name-retired' : 'name-exists', 'name', { name }));
|
|
204
|
+
for (const [field, axis] of Object.entries(spec.lists)) {
|
|
205
|
+
const said = namesIn(row.values[field]);
|
|
206
|
+
issues.push(...listIssues(field, said, found.get(axis)));
|
|
207
|
+
if (axis === spec.axis && name && said.includes(name))
|
|
208
|
+
issues.push(issue('parent-is-self', field));
|
|
209
|
+
}
|
|
210
|
+
if (spec.parentField && name && (0, exports.idOf)(row.values[spec.parentField]) === name)
|
|
211
|
+
issues.push(issue('parent-is-self', spec.parentField));
|
|
212
|
+
issues.push(...conformanceGaps(spec, row.values));
|
|
213
|
+
return issues;
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
/* ── Changes ─────────────────────────────────────────────────────────────────────────────────────────── */
|
|
217
|
+
/**
|
|
218
|
+
* The field that says what a record sits under on its own axis - a location's parent, a reason's parent, a class's
|
|
219
|
+
* bases. Absent: the axis is flat.
|
|
220
|
+
*/
|
|
221
|
+
function lineageFieldOf(spec) {
|
|
222
|
+
return spec.parentField ?? Object.entries(spec.lists).find(([, axis]) => axis === spec.axis)?.[0];
|
|
223
|
+
}
|
|
224
|
+
/** What a value of the lineage field names - one parent, or the bases of a class. */
|
|
225
|
+
function lineageOf(spec, field, value) {
|
|
226
|
+
if (spec.lists[field])
|
|
227
|
+
return namesIn(value);
|
|
228
|
+
const id = (0, exports.idOf)(value) ?? (typeof value === 'string' && value.trim() ? value.trim() : undefined);
|
|
229
|
+
return id ? [id] : [];
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Whether climbing from `start` along the lineage reaches one of `selves` - a location put under its own child, a
|
|
233
|
+
* class inheriting from one that inherits from it. Changes of the same proposal count as if made (`pending`). The
|
|
234
|
+
* climb reads one level at a time and stops at a name seen before, so a cycle already stored does not loop it.
|
|
235
|
+
*/
|
|
236
|
+
async function climbReaches(spec, field, start, selves, pending, lookup) {
|
|
237
|
+
const seen = new Set();
|
|
238
|
+
let frontier = [...new Set(start)];
|
|
239
|
+
while (frontier.length) {
|
|
240
|
+
if (frontier.some(name => selves.has(name)))
|
|
241
|
+
return true;
|
|
242
|
+
frontier.forEach(name => seen.add(name));
|
|
243
|
+
const stored = frontier.filter(name => !pending.has(name));
|
|
244
|
+
const rows = stored.length ? await lookup(spec.axis, stored) : new Map();
|
|
245
|
+
const next = frontier.flatMap(name => pending.get(name) ?? (rows.has(name) ? lineageOf(spec, field, rows.get(name)[field]) : []));
|
|
246
|
+
frontier = [...new Set(next)].filter(name => !seen.has(name));
|
|
247
|
+
}
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* The check of changes - one list per change. A new name another record holds (a retired one too - the name is
|
|
252
|
+
* unique on the axis); one new name given to two records; a class named in a list that is not there; a record put
|
|
253
|
+
* under itself or under something below it.
|
|
254
|
+
*/
|
|
255
|
+
async function checkMasterChanges(spec, changes, lookup) {
|
|
256
|
+
const currentName = (change) => (typeof change.record?.name === 'string' ? change.record.name : undefined);
|
|
257
|
+
const renamed = changes.map(change => ('name' in change.values ? nameOf(change.values) : undefined)).map((name, i) => (name && name !== currentName(changes[i]) ? name : undefined));
|
|
258
|
+
const taken = await lookup(spec.axis, [...new Set(renamed.filter((name) => !!name))]);
|
|
259
|
+
const listed = new Map();
|
|
260
|
+
for (const [field, axis] of Object.entries(spec.lists)) {
|
|
261
|
+
const set = listed.get(axis) ?? new Set();
|
|
262
|
+
for (const change of changes)
|
|
263
|
+
if (field in change.values)
|
|
264
|
+
for (const name of namesIn(change.values[field]))
|
|
265
|
+
set.add(name);
|
|
266
|
+
listed.set(axis, set);
|
|
267
|
+
}
|
|
268
|
+
const found = new Map();
|
|
269
|
+
for (const [axis, set] of listed)
|
|
270
|
+
found.set(axis, set.size ? await lookup(axis, [...set]) : new Map());
|
|
271
|
+
const lineage = lineageFieldOf(spec);
|
|
272
|
+
/* What the proposal itself puts each record under, by its name now. */
|
|
273
|
+
const pending = new Map();
|
|
274
|
+
if (lineage)
|
|
275
|
+
for (const change of changes)
|
|
276
|
+
if (lineage in change.values && currentName(change))
|
|
277
|
+
pending.set(currentName(change), lineageOf(spec, lineage, change.values[lineage]));
|
|
278
|
+
const repeated = repeatedNames(renamed);
|
|
279
|
+
const out = [];
|
|
280
|
+
for (const [i, change] of changes.entries()) {
|
|
281
|
+
const issues = [...repeated[i]];
|
|
282
|
+
const name = renamed[i];
|
|
283
|
+
const holder = name ? taken.get(name) : undefined;
|
|
284
|
+
if (name && holder && holder.id !== change.id)
|
|
285
|
+
issues.push(issue('name-taken', 'name', { name }));
|
|
286
|
+
for (const [field, axis] of Object.entries(spec.lists))
|
|
287
|
+
if (field in change.values)
|
|
288
|
+
issues.push(...listIssues(field, namesIn(change.values[field]), found.get(axis)));
|
|
289
|
+
if (lineage && lineage in change.values) {
|
|
290
|
+
const selves = new Set([currentName(change), nameOf(change.values)].filter((one) => !!one));
|
|
291
|
+
const under = lineageOf(spec, lineage, change.values[lineage]);
|
|
292
|
+
if (under.some(one => selves.has(one)))
|
|
293
|
+
issues.push(issue('parent-is-self', lineage));
|
|
294
|
+
else if (under.length && (await climbReaches(spec, lineage, under, selves, pending, lookup)))
|
|
295
|
+
issues.push(issue('parent-cycle', lineage));
|
|
296
|
+
}
|
|
297
|
+
issues.push(...conformanceGaps(spec, { name: nameOf(change.values) ?? currentName(change), ...change.values }).filter(gap => gap.field in change.values));
|
|
298
|
+
out.push(issues);
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
//# sourceMappingURL=master-entry-rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"master-entry-rules.js","sourceRoot":"","sources":["../../../server/service/ai-entry/master-entry-rules.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * AI 입력 — the rules of entering master records, without the framework, so the tests read them bare\n * (master-entry-targets.ts declares the targets; operato-application/design/ai/data-entry.md is the frame).\n *\n * The declarations are plain data in the shape of `@things-factory/ai-assistant`'s `EntryTarget`. This package does\n * not depend on the assistant: an application that has it registers each target with `registerEntryTarget`, and the\n * types below are structural copies of the parts it reads.\n *\n * What is here is what a row cannot say about itself: a name already taken, a name repeated within one proposal, a\n * class named in a list that is not there, and the conformance rules (`checkMaster`) a row would break once entered.\n */\nimport { checkMaster, type MasterSnapshot } from '../master-conformance'\n\n/* ── The shapes the assistant reads (structural copies of entry-target.ts) ───────────────────────────── */\n\nexport type OpsMasterEntryFieldType = 'string' | 'text' | 'number' | 'integer' | 'date' | 'datetime' | 'boolean' | 'enum' | 'reference'\n\nexport interface OpsMasterEntryReference {\n query: string\n variables(text: string): Record<string, unknown>\n recordsOf(data: any, text: string): { id: string; label?: string }[]\n}\n\nexport interface OpsMasterEntryField {\n name: string\n type: OpsMasterEntryFieldType\n required?: boolean\n values?: string[]\n description?: string\n labelKey?: string\n valueKeyPrefix?: string\n reference?: OpsMasterEntryReference\n}\n\nexport interface OpsMasterEntryIssue {\n field?: string\n code: string\n messageKey?: string\n params?: Record<string, unknown>\n blocking: boolean\n}\n\nexport interface OpsMasterEntryRow {\n index: number\n values: Record<string, unknown>\n sources: { attachmentId?: string; where?: string; person?: boolean }[]\n issues: OpsMasterEntryIssue[]\n}\n\nexport type OpsMasterEntryRecord = { id: string } & Record<string, unknown>\n\nexport interface OpsMasterEntryTarget {\n key: string\n labelKey: string\n description: string\n fields: OpsMasterEntryField[]\n mutation: string\n variables(row: OpsMasterEntryRow): Record<string, unknown>\n appliedOf(data: any, row: OpsMasterEntryRow): { id?: string; label?: string } | undefined\n maxRows?: number\n check(rows: OpsMasterEntryRow[], context: { state: any }): Promise<OpsMasterEntryIssue[][]>\n update: {\n read: { query: string; variables(ids: string[]): Record<string, unknown>; recordsOf(data: any, ids: string[]): OpsMasterEntryRecord[] }\n find: {\n query: string\n variables(criteria: Record<string, unknown>): Record<string, unknown>\n recordsOf(data: any, criteria: Record<string, unknown>): OpsMasterEntryRecord[]\n criteria: OpsMasterEntryField[]\n }\n mutation: string\n variables(change: { id: string; record: OpsMasterEntryRecord; values: Record<string, unknown> }): Record<string, unknown>\n labelOf(record: OpsMasterEntryRecord): string\n fields: string[]\n /** Read-only checks of the changes - asked when the card is made and again when it is pressed. */\n check(changes: { id: string; record?: OpsMasterEntryRecord; values: Record<string, unknown> }[], context: { state: any }): Promise<OpsMasterEntryIssue[][]>\n }\n}\n\n/* ── The axes ────────────────────────────────────────────────────────────────────────────────────────── */\n\n/**\n * The axes the assistant may enter. The revision-controlled ones (material, recipe, test specification) change\n * through a revision draft and an approval, never a row of a card; operations and routes are not opened yet.\n */\nexport const OPS_MASTER_ENTRY_AXES = [\n 'operationalLocation',\n 'equipment',\n 'person',\n 'physicalAsset',\n 'stopReason',\n 'nonconformanceCause',\n 'equipmentClass',\n 'personnelClass',\n 'physicalAssetClass',\n 'materialClass'\n] as const\n\nexport type OpsMasterEntryAxis = (typeof OPS_MASTER_ENTRY_AXES)[number]\n\n/** `operationalLocation` → `operational-location`. An entry key is lowercase (`<app>.<record>`). */\nexport const kebabOf = (axis: string) => axis.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`)\n\n/** `ops-master.operational-location`. */\nexport const opsMasterEntryKey = (axis: OpsMasterEntryAxis) => `ops-master.${kebabOf(axis)}`\n\n/** `label.ops-master-entry-operational-location` - the card's title. */\nexport const opsMasterEntryLabelKey = (axis: OpsMasterEntryAxis) => `label.ops-master-entry-${kebabOf(axis)}`\n\n/** What a rule needs to know of an axis. */\nexport interface MasterEntryRuleSpec {\n axis: OpsMasterEntryAxis\n fields: OpsMasterEntryField[]\n /** Fields written as a comma list of names of another axis (or this one) - stored as an array. */\n lists: Record<string, OpsMasterEntryAxis>\n /** The field that names this axis' own parent. */\n parentField?: string\n /** Where `checkMaster` reads this axis - absent: it has no rule for it. */\n snapshotKey?: keyof MasterSnapshot\n}\n\n/* ── Values ──────────────────────────────────────────────────────────────────────────────────────────── */\n\nexport const idOf = (value: unknown): string | undefined =>\n value && typeof value === 'object' && typeof (value as any).id === 'string' ? (value as any).id : undefined\n\n/** \"PC-A, PC-B\" · \"PC-A;PC-B\" · lines → ['PC-A', 'PC-B']. Order kept, a repeated name once. */\nexport function namesIn(value: unknown): string[] {\n const parts = Array.isArray(value) ? value.map(String) : typeof value === 'string' ? value.split(/[,;\\n]/) : []\n return [...new Set(parts.map(part => part.trim()).filter(Boolean))]\n}\n\n/** A row's name, or nothing when it is empty. */\nexport function nameOf(values: Record<string, unknown>): string | undefined {\n const name = values.name\n return typeof name === 'string' && name.trim() ? name.trim() : undefined\n}\n\n/**\n * The patch of one row - **only the declared fields**, because a change saves `{ ...found, ...values }` and anything\n * else sent would overwrite the record. A reference goes as the name it found, a list as an array of names.\n *\n * `keepEmpty`: a change that empties a field sends null; a new record leaves it out.\n */\nexport function masterPatchOf(values: Record<string, unknown>, spec: Pick<MasterEntryRuleSpec, 'fields' | 'lists'>, keepEmpty = false): Record<string, unknown> {\n const patch: Record<string, unknown> = {}\n for (const field of spec.fields) {\n if (!(field.name in values)) continue\n const value = values[field.name]\n if (value === undefined) continue\n if (value === null || value === '') {\n if (keepEmpty) patch[field.name] = null\n continue\n }\n if (field.type === 'reference') {\n /* An unsettled name keeps its row out already; it is never sent as if it were found. */\n const id = idOf(value)\n if (id !== undefined) patch[field.name] = id\n continue\n }\n if (spec.lists[field.name]) {\n const names = namesIn(value)\n if (names.length) patch[field.name] = names\n else if (keepEmpty) patch[field.name] = null\n continue\n }\n patch[field.name] = value\n }\n return patch\n}\n\n/**\n * The record a name points at - the same name, or the same description, written with any spacing or case. Nothing\n * else is taken: a master's parent or home found by a near name would be entered as fact. Several near matches are\n * handed back so the person can say which one.\n */\nexport function mastersNamed(data: any, list: string, text: string): { id: string; label?: string }[] {\n const items: { name?: string; description?: string | null }[] = data?.[list]?.items ?? []\n const plain = (value: unknown) => String(value ?? '').replace(/\\s+/g, '').toLowerCase()\n const wanted = plain(text)\n const named = items.filter(item => item.name && (plain(item.name) === wanted || (item.description && plain(item.description) === wanted)))\n const near = items.filter(item => !!item.name)\n const chosen = named.length ? named : near.length > 1 ? near : []\n return chosen.map(item => ({ id: item.name!, label: item.description ? `${item.name} · ${item.description}` : item.name }))\n}\n\n/* ── Checks ──────────────────────────────────────────────────────────────────────────────────────────── */\n\n/** A stored master row as the check reads it - its id, its name, its end, and the axis' own fields. */\nexport type MasterRow = { id?: string; name: string; effectiveEnd?: Date | string | null } & Record<string, unknown>\n\n/** Of the names asked, the rows on the axis that carry them, by name. A name not in the map does not exist. */\nexport type MasterNamesLookup = (axis: OpsMasterEntryAxis, names: string[]) => Promise<Map<string, MasterRow>>\n\n/** Retired - its end has come. A future end is still in use. */\nexport const retiredRow = (row: MasterRow | undefined, now = Date.now()) => !!row?.effectiveEnd && new Date(row.effectiveEnd as any).getTime() <= now\n\nconst issue = (code: string, field: string | undefined, params?: Record<string, unknown>, blocking = true): OpsMasterEntryIssue => ({\n ...(field ? { field } : {}),\n code,\n messageKey: `text.ops-master-entry-${code}`,\n ...(params ? { params } : {}),\n blocking\n})\n\n/** Names of a comma list that are not there, or retired. */\nfunction listIssues(field: string, said: string[], known: Map<string, MasterRow>): OpsMasterEntryIssue[] {\n const missing = said.filter(one => !known.has(one))\n const retired = said.filter(one => retiredRow(known.get(one)))\n return [\n ...(missing.length ? [issue('names-not-found', field, { names: missing.join(', ') })] : []),\n ...(retired.length ? [issue('names-retired', field, { names: retired.join(', ') })] : [])\n ]\n}\n\n/** Two rows of one proposal with one name - the second would collide with the first; it is kept out, naming the row. */\nexport function repeatedNames(names: (string | undefined)[]): OpsMasterEntryIssue[][] {\n return names.map((name, i) => {\n const earlier = name ? names.slice(0, i).indexOf(name) : -1\n return earlier < 0 ? [] : [issue('name-repeated-row', 'name', { row: earlier + 1 })]\n })\n}\n\nfunction emptySnapshot(): MasterSnapshot {\n return {\n persons: [],\n equipment: [],\n physicalAssets: [],\n materialDefinitions: [],\n operationDefinitions: [],\n routeDefinitions: [],\n recipeDefinitions: [],\n operationalLocations: [],\n stopReasons: [],\n testSpecifications: [],\n personnelClasses: [],\n equipmentClasses: [],\n physicalAssetClasses: [],\n materialClasses: []\n }\n}\n\n/**\n * The fields `checkMaster` would find empty once this row is entered - the conformance screen's own rule. A field\n * the target declares required is said by the framework already and is not said twice. They do not keep the row\n * out: the save takes them, and the conformance screen lists them.\n */\nexport function conformanceGaps(spec: MasterEntryRuleSpec, values: Record<string, unknown>): OpsMasterEntryIssue[] {\n if (!spec.snapshotKey) return []\n const required = new Set(spec.fields.filter(field => field.required).map(field => field.name))\n const declared = new Set(spec.fields.map(field => field.name))\n const snapshot = emptySnapshot()\n snapshot[spec.snapshotKey] = [{ ...masterPatchOf(values, spec), name: nameOf(values) ?? '' }]\n return checkMaster(snapshot)\n .filter(found => found.axis === spec.axis && found.kind === 'missing' && declared.has(found.field) && !required.has(found.field))\n .map(found => issue('conformance-missing', found.field, undefined, false))\n}\n\n/**\n * The target's check - read-only, one list per row. `existing` asks the database (scoped to the domain) which names\n * are there; it is an argument so the rules are tested without one.\n */\nexport async function checkMasterRows(spec: MasterEntryRuleSpec, rows: { values: Record<string, unknown> }[], existing: MasterNamesLookup): Promise<OpsMasterEntryIssue[][]> {\n const names = rows.map(row => nameOf(row.values))\n const own = await existing(spec.axis, [...new Set(names.filter((name): name is string => !!name))])\n\n /* Every list name of every row, asked once per axis. */\n const listed = new Map<OpsMasterEntryAxis, Set<string>>()\n for (const [field, axis] of Object.entries(spec.lists)) {\n const set = listed.get(axis) ?? new Set<string>()\n for (const row of rows) for (const name of namesIn(row.values[field])) set.add(name)\n listed.set(axis, set)\n }\n const found = new Map<OpsMasterEntryAxis, Map<string, MasterRow>>()\n for (const [axis, set] of listed) found.set(axis, set.size ? await existing(axis, [...set]) : new Map())\n\n const repeated = repeatedNames(names)\n return rows.map((row, i) => {\n const issues: OpsMasterEntryIssue[] = [...repeated[i]]\n const name = names[i]\n const there = name ? own.get(name) : undefined\n /* The name is unique on the axis, retired rows included - a new row with it is refused by the database. */\n if (there) issues.push(issue(retiredRow(there) ? 'name-retired' : 'name-exists', 'name', { name }))\n\n for (const [field, axis] of Object.entries(spec.lists)) {\n const said = namesIn(row.values[field])\n issues.push(...listIssues(field, said, found.get(axis)!))\n if (axis === spec.axis && name && said.includes(name)) issues.push(issue('parent-is-self', field))\n }\n\n if (spec.parentField && name && idOf(row.values[spec.parentField]) === name) issues.push(issue('parent-is-self', spec.parentField))\n\n issues.push(...conformanceGaps(spec, row.values))\n return issues\n })\n}\n\n/* ── Changes ─────────────────────────────────────────────────────────────────────────────────────────── */\n\n/**\n * The field that says what a record sits under on its own axis - a location's parent, a reason's parent, a class's\n * bases. Absent: the axis is flat.\n */\nexport function lineageFieldOf(spec: MasterEntryRuleSpec): string | undefined {\n return spec.parentField ?? Object.entries(spec.lists).find(([, axis]) => axis === spec.axis)?.[0]\n}\n\n/** What a value of the lineage field names - one parent, or the bases of a class. */\nfunction lineageOf(spec: MasterEntryRuleSpec, field: string, value: unknown): string[] {\n if (spec.lists[field]) return namesIn(value)\n const id = idOf(value) ?? (typeof value === 'string' && value.trim() ? value.trim() : undefined)\n return id ? [id] : []\n}\n\n/**\n * Whether climbing from `start` along the lineage reaches one of `selves` - a location put under its own child, a\n * class inheriting from one that inherits from it. Changes of the same proposal count as if made (`pending`). The\n * climb reads one level at a time and stops at a name seen before, so a cycle already stored does not loop it.\n */\nexport async function climbReaches(\n spec: MasterEntryRuleSpec,\n field: string,\n start: string[],\n selves: Set<string>,\n pending: Map<string, string[]>,\n lookup: MasterNamesLookup\n): Promise<boolean> {\n const seen = new Set<string>()\n let frontier = [...new Set(start)]\n while (frontier.length) {\n if (frontier.some(name => selves.has(name))) return true\n frontier.forEach(name => seen.add(name))\n const stored = frontier.filter(name => !pending.has(name))\n const rows = stored.length ? await lookup(spec.axis, stored) : new Map<string, MasterRow>()\n const next = frontier.flatMap(name => pending.get(name) ?? (rows.has(name) ? lineageOf(spec, field, rows.get(name)![field]) : []))\n frontier = [...new Set(next)].filter(name => !seen.has(name))\n }\n return false\n}\n\n/**\n * The check of changes - one list per change. A new name another record holds (a retired one too - the name is\n * unique on the axis); one new name given to two records; a class named in a list that is not there; a record put\n * under itself or under something below it.\n */\nexport async function checkMasterChanges(\n spec: MasterEntryRuleSpec,\n changes: { id: string; record?: Record<string, unknown>; values: Record<string, unknown> }[],\n lookup: MasterNamesLookup\n): Promise<OpsMasterEntryIssue[][]> {\n const currentName = (change: { record?: Record<string, unknown> }) => (typeof change.record?.name === 'string' ? (change.record.name as string) : undefined)\n const renamed = changes.map(change => ('name' in change.values ? nameOf(change.values) : undefined)).map((name, i) => (name && name !== currentName(changes[i]) ? name : undefined))\n const taken = await lookup(spec.axis, [...new Set(renamed.filter((name): name is string => !!name))])\n\n const listed = new Map<OpsMasterEntryAxis, Set<string>>()\n for (const [field, axis] of Object.entries(spec.lists)) {\n const set = listed.get(axis) ?? new Set<string>()\n for (const change of changes) if (field in change.values) for (const name of namesIn(change.values[field])) set.add(name)\n listed.set(axis, set)\n }\n const found = new Map<OpsMasterEntryAxis, Map<string, MasterRow>>()\n for (const [axis, set] of listed) found.set(axis, set.size ? await lookup(axis, [...set]) : new Map())\n\n const lineage = lineageFieldOf(spec)\n /* What the proposal itself puts each record under, by its name now. */\n const pending = new Map<string, string[]>()\n if (lineage) for (const change of changes) if (lineage in change.values && currentName(change)) pending.set(currentName(change)!, lineageOf(spec, lineage, change.values[lineage]))\n\n const repeated = repeatedNames(renamed)\n const out: OpsMasterEntryIssue[][] = []\n for (const [i, change] of changes.entries()) {\n const issues: OpsMasterEntryIssue[] = [...repeated[i]]\n const name = renamed[i]\n const holder = name ? taken.get(name) : undefined\n if (name && holder && holder.id !== change.id) issues.push(issue('name-taken', 'name', { name }))\n\n for (const [field, axis] of Object.entries(spec.lists)) if (field in change.values) issues.push(...listIssues(field, namesIn(change.values[field]), found.get(axis)!))\n\n if (lineage && lineage in change.values) {\n const selves = new Set([currentName(change), nameOf(change.values)].filter((one): one is string => !!one))\n const under = lineageOf(spec, lineage, change.values[lineage])\n if (under.some(one => selves.has(one))) issues.push(issue('parent-is-self', lineage))\n else if (under.length && (await climbReaches(spec, lineage, under, selves, pending, lookup))) issues.push(issue('parent-cycle', lineage))\n }\n\n issues.push(...conformanceGaps(spec, { name: nameOf(change.values) ?? currentName(change), ...change.values }).filter(gap => gap.field! in change.values))\n out.push(issues)\n }\n return out\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type MasterEntryRuleSpec, type MasterNamesLookup, type OpsMasterEntryTarget } from './master-entry-rules';
|
|
2
|
+
/** The list query an axis is read and searched through, and the grid's mutation. */
|
|
3
|
+
export interface OpsMasterEntryAxisSpec extends MasterEntryRuleSpec {
|
|
4
|
+
/** `operationalLocations` · `equipmentList` … */
|
|
5
|
+
list: string;
|
|
6
|
+
/** `updateMultipleOperationalLocation` … */
|
|
7
|
+
mutation: string;
|
|
8
|
+
/** What one row is, for the model. */
|
|
9
|
+
description: string;
|
|
10
|
+
/** The fields the read query asks besides `id`. */
|
|
11
|
+
recordFields: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare const OPS_MASTER_ENTRY_SPECS: readonly OpsMasterEntryAxisSpec[];
|
|
14
|
+
/** The fields a change may touch - every declared field; the update check reads the names a change gives. */
|
|
15
|
+
export declare const changeableFieldsOf: (spec: Pick<OpsMasterEntryAxisSpec, "fields">) => string[];
|
|
16
|
+
export interface OpsMasterEntryOptions {
|
|
17
|
+
/** Which names exist - the master tables by default; a test gives its own. */
|
|
18
|
+
lookup?: (domain: any) => MasterNamesLookup;
|
|
19
|
+
}
|
|
20
|
+
/** One axis as an entry target. */
|
|
21
|
+
export declare function opsMasterEntryTargetOf(spec: OpsMasterEntryAxisSpec, options?: OpsMasterEntryOptions): OpsMasterEntryTarget;
|
|
22
|
+
/** Every master axis the assistant may enter, as targets to register. */
|
|
23
|
+
export declare function opsMasterEntryTargets(options?: OpsMasterEntryOptions): OpsMasterEntryTarget[];
|