perttool 0.1.0-alpha.2
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/CHANGELOG.md +53 -0
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/analysis/graph.d.ts +41 -0
- package/dist/analysis/graph.d.ts.map +1 -0
- package/dist/analysis/graph.js +199 -0
- package/dist/analysis/graph.js.map +1 -0
- package/dist/analysis/precedence.d.ts +52 -0
- package/dist/analysis/precedence.d.ts.map +1 -0
- package/dist/analysis/precedence.js +187 -0
- package/dist/analysis/precedence.js.map +1 -0
- package/dist/analysis/resource.d.ts +96 -0
- package/dist/analysis/resource.d.ts.map +1 -0
- package/dist/analysis/resource.js +522 -0
- package/dist/analysis/resource.js.map +1 -0
- package/dist/application/analyze.d.ts +32 -0
- package/dist/application/analyze.d.ts.map +1 -0
- package/dist/application/analyze.js +144 -0
- package/dist/application/analyze.js.map +1 -0
- package/dist/application/check.d.ts +24 -0
- package/dist/application/check.d.ts.map +1 -0
- package/dist/application/check.js +43 -0
- package/dist/application/check.js.map +1 -0
- package/dist/application/format.d.ts +21 -0
- package/dist/application/format.d.ts.map +1 -0
- package/dist/application/format.js +52 -0
- package/dist/application/format.js.map +1 -0
- package/dist/application/mutate.d.ts +4 -0
- package/dist/application/mutate.d.ts.map +1 -0
- package/dist/application/mutate.js +175 -0
- package/dist/application/mutate.js.map +1 -0
- package/dist/application/next.d.ts +86 -0
- package/dist/application/next.d.ts.map +1 -0
- package/dist/application/next.js +310 -0
- package/dist/application/next.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +2103 -0
- package/dist/cli.js.map +1 -0
- package/dist/conversion/mermaid-import.d.ts +26 -0
- package/dist/conversion/mermaid-import.d.ts.map +1 -0
- package/dist/conversion/mermaid-import.js +655 -0
- package/dist/conversion/mermaid-import.js.map +1 -0
- package/dist/conversion/mermaid.d.ts +38 -0
- package/dist/conversion/mermaid.d.ts.map +1 -0
- package/dist/conversion/mermaid.js +362 -0
- package/dist/conversion/mermaid.js.map +1 -0
- package/dist/editing/unified-diff.d.ts +7 -0
- package/dist/editing/unified-diff.d.ts.map +1 -0
- package/dist/editing/unified-diff.js +81 -0
- package/dist/editing/unified-diff.js.map +1 -0
- package/dist/formatter/source-formatter.d.ts +17 -0
- package/dist/formatter/source-formatter.d.ts.map +1 -0
- package/dist/formatter/source-formatter.js +210 -0
- package/dist/formatter/source-formatter.js.map +1 -0
- package/dist/help/registry.d.ts +37 -0
- package/dist/help/registry.d.ts.map +1 -0
- package/dist/help/registry.js +657 -0
- package/dist/help/registry.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/io/document-file.d.ts +9 -0
- package/dist/io/document-file.d.ts.map +1 -0
- package/dist/io/document-file.js +19 -0
- package/dist/io/document-file.js.map +1 -0
- package/dist/io/safe-write.d.ts +33 -0
- package/dist/io/safe-write.d.ts.map +1 -0
- package/dist/io/safe-write.js +280 -0
- package/dist/io/safe-write.js.map +1 -0
- package/dist/model/diagnostics.d.ts +43 -0
- package/dist/model/diagnostics.d.ts.map +1 -0
- package/dist/model/diagnostics.js +47 -0
- package/dist/model/diagnostics.js.map +1 -0
- package/dist/model/rational.d.ts +21 -0
- package/dist/model/rational.d.ts.map +1 -0
- package/dist/model/rational.js +84 -0
- package/dist/model/rational.js.map +1 -0
- package/dist/model/syntax.d.ts +67 -0
- package/dist/model/syntax.d.ts.map +1 -0
- package/dist/model/syntax.js +15 -0
- package/dist/model/syntax.js.map +1 -0
- package/dist/model/units.d.ts +18 -0
- package/dist/model/units.d.ts.map +1 -0
- package/dist/model/units.js +29 -0
- package/dist/model/units.js.map +1 -0
- package/dist/mutation/advance.d.ts +26 -0
- package/dist/mutation/advance.d.ts.map +1 -0
- package/dist/mutation/advance.js +261 -0
- package/dist/mutation/advance.js.map +1 -0
- package/dist/mutation/diagnostics.d.ts +10 -0
- package/dist/mutation/diagnostics.d.ts.map +1 -0
- package/dist/mutation/diagnostics.js +10 -0
- package/dist/mutation/diagnostics.js.map +1 -0
- package/dist/mutation/entity-editor.d.ts +16 -0
- package/dist/mutation/entity-editor.d.ts.map +1 -0
- package/dist/mutation/entity-editor.js +103 -0
- package/dist/mutation/entity-editor.js.map +1 -0
- package/dist/mutation/milestone.d.ts +5 -0
- package/dist/mutation/milestone.d.ts.map +1 -0
- package/dist/mutation/milestone.js +194 -0
- package/dist/mutation/milestone.js.map +1 -0
- package/dist/mutation/resource.d.ts +5 -0
- package/dist/mutation/resource.d.ts.map +1 -0
- package/dist/mutation/resource.js +145 -0
- package/dist/mutation/resource.js.map +1 -0
- package/dist/mutation/source.d.ts +22 -0
- package/dist/mutation/source.d.ts.map +1 -0
- package/dist/mutation/source.js +155 -0
- package/dist/mutation/source.js.map +1 -0
- package/dist/mutation/task.d.ts +10 -0
- package/dist/mutation/task.d.ts.map +1 -0
- package/dist/mutation/task.js +644 -0
- package/dist/mutation/task.js.map +1 -0
- package/dist/mutation/text-edits.d.ts +8 -0
- package/dist/mutation/text-edits.d.ts.map +1 -0
- package/dist/mutation/text-edits.js +33 -0
- package/dist/mutation/text-edits.js.map +1 -0
- package/dist/mutation/types.d.ts +153 -0
- package/dist/mutation/types.d.ts.map +1 -0
- package/dist/mutation/types.js +2 -0
- package/dist/mutation/types.js.map +1 -0
- package/dist/parser/document-parser.d.ts +6 -0
- package/dist/parser/document-parser.d.ts.map +1 -0
- package/dist/parser/document-parser.js +735 -0
- package/dist/parser/document-parser.js.map +1 -0
- package/dist/recommendation/explanation-types.d.ts +226 -0
- package/dist/recommendation/explanation-types.d.ts.map +1 -0
- package/dist/recommendation/explanation-types.js +2 -0
- package/dist/recommendation/explanation-types.js.map +1 -0
- package/dist/recommendation/explanation-validation.d.ts +4 -0
- package/dist/recommendation/explanation-validation.d.ts.map +1 -0
- package/dist/recommendation/explanation-validation.js +745 -0
- package/dist/recommendation/explanation-validation.js.map +1 -0
- package/dist/recommendation/explanation-values.d.ts +7 -0
- package/dist/recommendation/explanation-values.d.ts.map +1 -0
- package/dist/recommendation/explanation-values.js +283 -0
- package/dist/recommendation/explanation-values.js.map +1 -0
- package/dist/recommendation/explanation.d.ts +8 -0
- package/dist/recommendation/explanation.d.ts.map +1 -0
- package/dist/recommendation/explanation.js +1022 -0
- package/dist/recommendation/explanation.js.map +1 -0
- package/dist/recommendation/failure.d.ts +3 -0
- package/dist/recommendation/failure.d.ts.map +1 -0
- package/dist/recommendation/failure.js +4 -0
- package/dist/recommendation/failure.js.map +1 -0
- package/dist/recommendation/json.d.ts +3 -0
- package/dist/recommendation/json.d.ts.map +1 -0
- package/dist/recommendation/json.js +252 -0
- package/dist/recommendation/json.js.map +1 -0
- package/dist/recommendation/override-types.d.ts +117 -0
- package/dist/recommendation/override-types.d.ts.map +1 -0
- package/dist/recommendation/override-types.js +2 -0
- package/dist/recommendation/override-types.js.map +1 -0
- package/dist/recommendation/override.d.ts +7 -0
- package/dist/recommendation/override.d.ts.map +1 -0
- package/dist/recommendation/override.js +717 -0
- package/dist/recommendation/override.js.map +1 -0
- package/dist/recommendation/ranking.d.ts +7 -0
- package/dist/recommendation/ranking.d.ts.map +1 -0
- package/dist/recommendation/ranking.js +391 -0
- package/dist/recommendation/ranking.js.map +1 -0
- package/dist/recommendation/types.d.ts +83 -0
- package/dist/recommendation/types.d.ts.map +1 -0
- package/dist/recommendation/types.js +2 -0
- package/dist/recommendation/types.js.map +1 -0
- package/dist/semantic/validator.d.ts +4 -0
- package/dist/semantic/validator.d.ts.map +1 -0
- package/dist/semantic/validator.js +490 -0
- package/dist/semantic/validator.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
import { compareStableStrings } from "../model/diagnostics.js";
|
|
2
|
+
import { evaluateRecommendationExpression, recommendationDescriptionKeys, recommendationValueKey, renderRecommendationDescription, } from "./explanation-values.js";
|
|
3
|
+
const knownRules = new Set([
|
|
4
|
+
"task_ready",
|
|
5
|
+
"selection_horizon",
|
|
6
|
+
"critical_class",
|
|
7
|
+
"lower_total_float",
|
|
8
|
+
"higher_explicit_priority",
|
|
9
|
+
"higher_new_ready_count",
|
|
10
|
+
"higher_new_gate_count",
|
|
11
|
+
"higher_new_milestone_count",
|
|
12
|
+
"shorter_next_gate_distance",
|
|
13
|
+
"shorter_finish_distance",
|
|
14
|
+
"longer_expected_duration",
|
|
15
|
+
"task_id_tiebreak",
|
|
16
|
+
"joint_resource_feasibility",
|
|
17
|
+
"recommended_set_membership",
|
|
18
|
+
"recommendation_tier",
|
|
19
|
+
]);
|
|
20
|
+
const ruleOrder = [...knownRules];
|
|
21
|
+
const ruleRank = new Map(ruleOrder.map((rule, index) => [rule, index]));
|
|
22
|
+
function compareRules(left, right) {
|
|
23
|
+
return ((ruleRank.get(left) ?? Number.MAX_SAFE_INTEGER) -
|
|
24
|
+
(ruleRank.get(right) ?? Number.MAX_SAFE_INTEGER) ||
|
|
25
|
+
compareStableStrings(left, right));
|
|
26
|
+
}
|
|
27
|
+
const comparisonScopeRank = {
|
|
28
|
+
ranking: 0,
|
|
29
|
+
selection_horizon: 1,
|
|
30
|
+
resource_selection: 2,
|
|
31
|
+
tier: 3,
|
|
32
|
+
};
|
|
33
|
+
const knownFactKinds = new Set([
|
|
34
|
+
"task_classification",
|
|
35
|
+
"recommendation_set_membership",
|
|
36
|
+
"set_start_feasibility",
|
|
37
|
+
"resource_capacity_witness",
|
|
38
|
+
"resource_active_occupants",
|
|
39
|
+
"resource_selected_occupants",
|
|
40
|
+
"resource_occupants",
|
|
41
|
+
"resource_capacity",
|
|
42
|
+
"resource_active_usage",
|
|
43
|
+
"resource_selected_usage",
|
|
44
|
+
"resource_used",
|
|
45
|
+
"resource_required",
|
|
46
|
+
"resource_available",
|
|
47
|
+
"resource_deficit",
|
|
48
|
+
"ranking_rule_application",
|
|
49
|
+
"selection_horizon_membership",
|
|
50
|
+
"recommendation_tier",
|
|
51
|
+
"precedence_total_float",
|
|
52
|
+
"precedence_critical_class",
|
|
53
|
+
"explicit_priority",
|
|
54
|
+
"new_ready_task_count",
|
|
55
|
+
"new_satisfied_gate_count",
|
|
56
|
+
"new_reached_milestone_count",
|
|
57
|
+
"next_gate_task_distance",
|
|
58
|
+
"finish_task_distance",
|
|
59
|
+
"expected_duration",
|
|
60
|
+
"task_id",
|
|
61
|
+
"requirements",
|
|
62
|
+
]);
|
|
63
|
+
const reasonContract = {
|
|
64
|
+
task_ready: { effect: "neutral", roles: ["context"], requiredFactKinds: ["task_classification"] },
|
|
65
|
+
recommended_set_selected: {
|
|
66
|
+
effect: "supporting",
|
|
67
|
+
roles: ["decisive"],
|
|
68
|
+
requiredFactKinds: ["recommendation_set_membership"],
|
|
69
|
+
},
|
|
70
|
+
recommended_set_not_selected: {
|
|
71
|
+
effect: "opposing",
|
|
72
|
+
roles: ["decisive"],
|
|
73
|
+
requiredFactKinds: ["recommendation_set_membership"],
|
|
74
|
+
},
|
|
75
|
+
recommended_set_feasible: {
|
|
76
|
+
effect: "neutral",
|
|
77
|
+
roles: ["context"],
|
|
78
|
+
requiredFactKinds: ["set_start_feasibility"],
|
|
79
|
+
},
|
|
80
|
+
ranking_rule_supports_task: {
|
|
81
|
+
effect: "supporting",
|
|
82
|
+
roles: ["decisive", "contributing"],
|
|
83
|
+
requiredFactKinds: ["ranking_rule_application"],
|
|
84
|
+
},
|
|
85
|
+
ranking_rule_opposes_task: {
|
|
86
|
+
effect: "opposing",
|
|
87
|
+
roles: ["decisive", "contributing"],
|
|
88
|
+
requiredFactKinds: ["ranking_rule_application"],
|
|
89
|
+
},
|
|
90
|
+
ranking_rule_tied: {
|
|
91
|
+
effect: "neutral",
|
|
92
|
+
roles: ["context"],
|
|
93
|
+
requiredFactKinds: ["ranking_rule_application"],
|
|
94
|
+
},
|
|
95
|
+
recommended_set_addition_feasible: {
|
|
96
|
+
effect: "supporting",
|
|
97
|
+
roles: ["decisive", "contributing"],
|
|
98
|
+
requiredFactKinds: ["set_start_feasibility"],
|
|
99
|
+
},
|
|
100
|
+
recommended_set_resource_conflict: {
|
|
101
|
+
effect: "blocking",
|
|
102
|
+
roles: ["decisive", "contributing"],
|
|
103
|
+
requiredFactKinds: ["set_start_feasibility", "resource_capacity_witness"],
|
|
104
|
+
},
|
|
105
|
+
policy_defers_start: {
|
|
106
|
+
effect: "blocking",
|
|
107
|
+
roles: ["decisive", "contributing"],
|
|
108
|
+
requiredFactKinds: ["ranking_rule_application"],
|
|
109
|
+
},
|
|
110
|
+
modeled_negative_fact_applies: {
|
|
111
|
+
effect: "blocking",
|
|
112
|
+
roles: ["decisive"],
|
|
113
|
+
requiredFactKinds: ["modeled_negative_fact"],
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
const phaseRank = {
|
|
117
|
+
eligibility: 0,
|
|
118
|
+
negative_fact_filter: 1,
|
|
119
|
+
selection_horizon: 2,
|
|
120
|
+
candidate_ranking: 3,
|
|
121
|
+
resource_selection: 4,
|
|
122
|
+
set_membership: 5,
|
|
123
|
+
tier_classification: 6,
|
|
124
|
+
};
|
|
125
|
+
function diagnostic(code, message) {
|
|
126
|
+
return { code, severity: "error", message };
|
|
127
|
+
}
|
|
128
|
+
function addOnce(diagnostics, item) {
|
|
129
|
+
if (!diagnostics.some(({ code, message }) => code === item.code && message === item.message)) {
|
|
130
|
+
diagnostics.push(item);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function unitKey(unit) {
|
|
134
|
+
if (unit === null)
|
|
135
|
+
return "-";
|
|
136
|
+
if (unit.kind === "duration")
|
|
137
|
+
return `duration:${unit.value}`;
|
|
138
|
+
if (unit.kind === "resource")
|
|
139
|
+
return `resource:${unit.resource.kind}:${unit.resource.id}`;
|
|
140
|
+
return "ratio";
|
|
141
|
+
}
|
|
142
|
+
function scalarType(value) {
|
|
143
|
+
if (value.type === "enum")
|
|
144
|
+
return `enum:${value.enumType}`;
|
|
145
|
+
if (value.type === "entity")
|
|
146
|
+
return `entity:${value.value.kind}`;
|
|
147
|
+
return value.type;
|
|
148
|
+
}
|
|
149
|
+
function greatestCommonDivisor(left, right) {
|
|
150
|
+
let current = left < 0n ? -left : left;
|
|
151
|
+
let remainder = right < 0n ? -right : right;
|
|
152
|
+
while (remainder !== 0n) {
|
|
153
|
+
[current, remainder] = [remainder, current % remainder];
|
|
154
|
+
}
|
|
155
|
+
return current;
|
|
156
|
+
}
|
|
157
|
+
function validateScalar(value) {
|
|
158
|
+
try {
|
|
159
|
+
switch (value.type) {
|
|
160
|
+
case "boolean":
|
|
161
|
+
return typeof value.value === "boolean" ? null : "boolean value is invalid";
|
|
162
|
+
case "integer":
|
|
163
|
+
return BigInt(value.value).toString() === value.value
|
|
164
|
+
? null
|
|
165
|
+
: "integer value is not canonical";
|
|
166
|
+
case "rational": {
|
|
167
|
+
const numerator = BigInt(value.numerator);
|
|
168
|
+
const denominator = BigInt(value.denominator);
|
|
169
|
+
if (denominator <= 0n)
|
|
170
|
+
return "rational denominator is not positive";
|
|
171
|
+
if (numerator.toString() !== value.numerator || denominator.toString() !== value.denominator) {
|
|
172
|
+
return "rational value is not canonical";
|
|
173
|
+
}
|
|
174
|
+
if (greatestCommonDivisor(numerator, denominator) !== 1n) {
|
|
175
|
+
return "rational value is not reduced";
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
case "enum":
|
|
180
|
+
return value.enumType.length > 0 && value.value.length > 0 ? null : "enum is incomplete";
|
|
181
|
+
case "entity":
|
|
182
|
+
return value.value.id.length > 0 ? null : "entity reference is incomplete";
|
|
183
|
+
default:
|
|
184
|
+
return "unknown scalar value type";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return "numeric value is invalid";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function validateValue(value) {
|
|
192
|
+
if (value.type !== "list" && value.type !== "set" && value.type !== "map") {
|
|
193
|
+
return validateScalar(value);
|
|
194
|
+
}
|
|
195
|
+
if (value.type === "map") {
|
|
196
|
+
const keySet = new Set();
|
|
197
|
+
const keys = [];
|
|
198
|
+
for (const entry of value.entries) {
|
|
199
|
+
const keyIssue = validateScalar(entry.key);
|
|
200
|
+
const valueIssue = validateScalar(entry.value);
|
|
201
|
+
if (keyIssue !== null || valueIssue !== null)
|
|
202
|
+
return keyIssue ?? valueIssue;
|
|
203
|
+
if (scalarType(entry.key) !== value.keyType || scalarType(entry.value) !== value.valueType) {
|
|
204
|
+
return "map entry type does not match its declaration";
|
|
205
|
+
}
|
|
206
|
+
const key = recommendationValueKey(entry.key);
|
|
207
|
+
if (keySet.has(key))
|
|
208
|
+
return "map contains duplicate keys";
|
|
209
|
+
keySet.add(key);
|
|
210
|
+
keys.push(key);
|
|
211
|
+
}
|
|
212
|
+
const sortedKeys = [...keys].sort(compareStableStrings);
|
|
213
|
+
if (keys.some((key, index) => key !== sortedKeys[index])) {
|
|
214
|
+
return "map entries are not in canonical key order";
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const itemKeys = new Set();
|
|
219
|
+
for (const item of value.items) {
|
|
220
|
+
const issue = validateScalar(item);
|
|
221
|
+
if (issue !== null)
|
|
222
|
+
return issue;
|
|
223
|
+
if (scalarType(item) !== value.itemType)
|
|
224
|
+
return `${value.type} item type does not match its declaration`;
|
|
225
|
+
const key = recommendationValueKey(item);
|
|
226
|
+
if (value.type === "set" && itemKeys.has(key))
|
|
227
|
+
return "set contains duplicate items";
|
|
228
|
+
itemKeys.add(key);
|
|
229
|
+
}
|
|
230
|
+
if (value.type === "set") {
|
|
231
|
+
const keys = value.items.map((item) => recommendationValueKey(item));
|
|
232
|
+
const sortedKeys = [...keys].sort(compareStableStrings);
|
|
233
|
+
if (keys.some((key, index) => key !== sortedKeys[index])) {
|
|
234
|
+
return "set items are not in canonical order";
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
function expressionKinds(expression, diagnostics, depth = 1) {
|
|
240
|
+
if (depth > 8) {
|
|
241
|
+
addOnce(diagnostics, diagnostic("PTREC-302", "expression exceeds version 1 maximum depth"));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const kind = expression.kind;
|
|
245
|
+
if (kind !== "compare" && kind !== "all" && kind !== "any") {
|
|
246
|
+
addOnce(diagnostics, diagnostic("PTREC-302", `expression node ${kind ?? "<missing>"} is not registered`));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (kind === "all" || kind === "any") {
|
|
250
|
+
const children = expression.children;
|
|
251
|
+
if (!Array.isArray(children) || children.length === 0) {
|
|
252
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `${kind} expression has no children`));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
for (const child of children)
|
|
256
|
+
expressionKinds(child, diagnostics, depth + 1);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const relation = expression.relation;
|
|
260
|
+
if (![
|
|
261
|
+
"equal",
|
|
262
|
+
"not_equal",
|
|
263
|
+
"less_than",
|
|
264
|
+
"less_or_equal",
|
|
265
|
+
"greater_than",
|
|
266
|
+
"greater_or_equal",
|
|
267
|
+
"contains",
|
|
268
|
+
].includes(relation)) {
|
|
269
|
+
addOnce(diagnostics, diagnostic("PTREC-302", `expression relation ${String(relation)} is not registered`));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function validateExpression(expression, facts, diagnostics, context) {
|
|
273
|
+
const before = diagnostics.length;
|
|
274
|
+
expressionKinds(expression, diagnostics);
|
|
275
|
+
if (diagnostics.length > before && diagnostics.at(-1)?.code === "PTREC-302")
|
|
276
|
+
return null;
|
|
277
|
+
try {
|
|
278
|
+
return evaluateRecommendationExpression(expression, facts);
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `${context} expression is not type-correct: ${error instanceof Error ? error.message : String(error)}`));
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
function entityParameterMatches(name, value) {
|
|
286
|
+
const singularKinds = {
|
|
287
|
+
task_id: "task",
|
|
288
|
+
higher_priority_task_id: "task",
|
|
289
|
+
winner_task_id: "task",
|
|
290
|
+
alternative_task_id: "task",
|
|
291
|
+
resource_id: "resource",
|
|
292
|
+
rule_id: "policy_rule",
|
|
293
|
+
decisive_rule_id: "policy_rule",
|
|
294
|
+
negative_fact_kind: "negative_fact_kind",
|
|
295
|
+
};
|
|
296
|
+
const pluralKinds = {
|
|
297
|
+
resource_ids: "resource",
|
|
298
|
+
higher_priority_task_ids: "task",
|
|
299
|
+
active_blocker_task_ids: "task",
|
|
300
|
+
occupant_task_ids: "task",
|
|
301
|
+
};
|
|
302
|
+
const singular = singularKinds[name];
|
|
303
|
+
if (singular !== undefined) {
|
|
304
|
+
return value.type === "entity" && value.value.kind === singular;
|
|
305
|
+
}
|
|
306
|
+
const plural = pluralKinds[name];
|
|
307
|
+
if (plural !== undefined) {
|
|
308
|
+
return (value.type === "set" &&
|
|
309
|
+
value.items.every((item) => item.type === "entity" && item.value.kind === plural));
|
|
310
|
+
}
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
function validateDescription(description, diagnostics) {
|
|
314
|
+
if (!recommendationDescriptionKeys.includes(description.key)) {
|
|
315
|
+
addOnce(diagnostics, diagnostic("PTREC-303", `description ${description.id} has unknown key ${description.key}`));
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (description.registryVersion !== 1 || description.locale !== "en") {
|
|
319
|
+
addOnce(diagnostics, diagnostic("PTREC-303", `description ${description.id} has an unsupported registry or locale`));
|
|
320
|
+
}
|
|
321
|
+
for (const parameter of description.parameters) {
|
|
322
|
+
if (!/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/.test(parameter.name)) {
|
|
323
|
+
addOnce(diagnostics, diagnostic("PTREC-303", `description ${description.id} has invalid parameter ${parameter.name}`));
|
|
324
|
+
}
|
|
325
|
+
if (!entityParameterMatches(parameter.name, parameter.value)) {
|
|
326
|
+
addOnce(diagnostics, diagnostic("PTREC-303", `description ${description.id} parameter ${parameter.name} has the wrong entity kind`));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
const rendered = renderRecommendationDescription(description.key, description.parameters);
|
|
331
|
+
if (rendered !== description.text) {
|
|
332
|
+
addOnce(diagnostics, diagnostic("PTREC-303", `description ${description.id} text does not match its typed parameters`));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
addOnce(diagnostics, diagnostic("PTREC-303", `description ${description.id} is invalid: ${error instanceof Error ? error.message : String(error)}`));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
function descriptionParameter(description, name) {
|
|
340
|
+
return description.parameters.find((parameter) => parameter.name === name);
|
|
341
|
+
}
|
|
342
|
+
function typedParameterMatchesFact(description, parameterName, fact) {
|
|
343
|
+
const parameter = descriptionParameter(description, parameterName);
|
|
344
|
+
return (parameter !== undefined &&
|
|
345
|
+
fact !== undefined &&
|
|
346
|
+
recommendationValueKey(parameter.value) === recommendationValueKey(fact.value) &&
|
|
347
|
+
unitKey(parameter.unit) === unitKey(fact.unit));
|
|
348
|
+
}
|
|
349
|
+
function entityParameterId(description, name) {
|
|
350
|
+
const value = descriptionParameter(description, name)?.value;
|
|
351
|
+
return value?.type === "entity" ? value.value.id : null;
|
|
352
|
+
}
|
|
353
|
+
function validateDescriptionSources(description, facts, comparisons, reasons, diagnostics) {
|
|
354
|
+
const sourceReasons = description.sourceReasonIds
|
|
355
|
+
.map((id) => reasons.get(id))
|
|
356
|
+
.filter((reason) => reason !== undefined);
|
|
357
|
+
const taskSubject = sourceReasons.find(({ subject }) => subject.kind === "task")?.subject.id;
|
|
358
|
+
if (taskSubject !== undefined &&
|
|
359
|
+
descriptionParameter(description, "task_id") !== undefined &&
|
|
360
|
+
entityParameterId(description, "task_id") !== taskSubject) {
|
|
361
|
+
addOnce(diagnostics, diagnostic("PTREC-303", `description ${description.id} task_id does not match its source reason`));
|
|
362
|
+
}
|
|
363
|
+
if (description.key === "recommendation.reason.ranking_comparison") {
|
|
364
|
+
const comparison = description.sourceComparisonIds
|
|
365
|
+
.map((id) => comparisons.get(id))
|
|
366
|
+
.find((candidate) => candidate !== undefined);
|
|
367
|
+
if (comparison === undefined ||
|
|
368
|
+
entityParameterId(description, "winner_task_id") !== comparison.winnerTaskId ||
|
|
369
|
+
entityParameterId(description, "alternative_task_id") !==
|
|
370
|
+
comparison.alternativeTaskId ||
|
|
371
|
+
entityParameterId(description, "rule_id") !== comparison.decisiveRule.id ||
|
|
372
|
+
comparison.decisiveExpression.kind !== "compare" ||
|
|
373
|
+
comparison.decisiveExpression.left.kind !== "fact" ||
|
|
374
|
+
comparison.decisiveExpression.right.kind !== "fact") {
|
|
375
|
+
addOnce(diagnostics, diagnostic("PTREC-303", `description ${description.id} does not match its source comparison`));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const relation = descriptionParameter(description, "relation")?.value;
|
|
379
|
+
if (relation?.type !== "enum" ||
|
|
380
|
+
relation.value !== comparison.decisiveExpression.relation ||
|
|
381
|
+
!typedParameterMatchesFact(description, "winner_value", facts.get(comparison.decisiveExpression.left.factId)) ||
|
|
382
|
+
!typedParameterMatchesFact(description, "alternative_value", facts.get(comparison.decisiveExpression.right.factId))) {
|
|
383
|
+
addOnce(diagnostics, diagnostic("PTREC-303", `description ${description.id} values do not match its source comparison facts`));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (description.key === "recommendation.reason.resource_conflict") {
|
|
387
|
+
const reason = sourceReasons[0];
|
|
388
|
+
const resourceId = entityParameterId(description, "resource_id");
|
|
389
|
+
const sourceFacts = (reason?.factIds ?? [])
|
|
390
|
+
.map((id) => facts.get(id))
|
|
391
|
+
.filter((fact) => fact !== undefined && fact.subject.kind === "resource" && fact.subject.id === resourceId);
|
|
392
|
+
const expectedKinds = {
|
|
393
|
+
capacity: "resource_capacity",
|
|
394
|
+
used: "resource_used",
|
|
395
|
+
required: "resource_required",
|
|
396
|
+
deficit: "resource_deficit",
|
|
397
|
+
occupant_task_ids: "resource_occupants",
|
|
398
|
+
};
|
|
399
|
+
if (reason === undefined ||
|
|
400
|
+
resourceId === null ||
|
|
401
|
+
Object.entries(expectedKinds).some(([parameterName, factKind]) => !typedParameterMatchesFact(description, parameterName, sourceFacts.find(({ kind }) => kind === factKind)))) {
|
|
402
|
+
addOnce(diagnostics, diagnostic("PTREC-303", `description ${description.id} does not match its resource witness facts`));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
function reasonCodes(reasons) {
|
|
407
|
+
return new Set(reasons.map(({ code }) => code));
|
|
408
|
+
}
|
|
409
|
+
function validateTier(decision, reasons, diagnostics) {
|
|
410
|
+
const codes = reasonCodes(reasons);
|
|
411
|
+
const require = (...required) => {
|
|
412
|
+
for (const code of required) {
|
|
413
|
+
if (!codes.has(code)) {
|
|
414
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `task ${decision.subjectTaskId} tier ${decision.tier} is missing reason ${code}`));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
const forbid = (...forbidden) => {
|
|
419
|
+
for (const code of forbidden) {
|
|
420
|
+
if (codes.has(code)) {
|
|
421
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `task ${decision.subjectTaskId} tier ${decision.tier} forbids reason ${code}`));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
require("task_ready");
|
|
426
|
+
switch (decision.tier) {
|
|
427
|
+
case "recommended":
|
|
428
|
+
require("recommended_set_selected", "ranking_rule_supports_task");
|
|
429
|
+
forbid("recommended_set_not_selected", "policy_defers_start", "modeled_negative_fact_applies");
|
|
430
|
+
break;
|
|
431
|
+
case "allowed":
|
|
432
|
+
require("recommended_set_not_selected", "recommended_set_addition_feasible", "ranking_rule_opposes_task");
|
|
433
|
+
forbid("policy_defers_start", "modeled_negative_fact_applies", "recommended_set_resource_conflict");
|
|
434
|
+
break;
|
|
435
|
+
case "deferred":
|
|
436
|
+
require("recommended_set_not_selected");
|
|
437
|
+
if (!codes.has("policy_defers_start") && !codes.has("recommended_set_resource_conflict")) {
|
|
438
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `task ${decision.subjectTaskId} deferred tier has no blocking reason`));
|
|
439
|
+
}
|
|
440
|
+
break;
|
|
441
|
+
case "discouraged":
|
|
442
|
+
require("recommended_set_not_selected", "modeled_negative_fact_applies");
|
|
443
|
+
addOnce(diagnostics, diagnostic("PTREC-302", "reason taxonomy 1.0 cannot produce discouraged tasks"));
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
export function validateRecommendationAnalysis(analysis) {
|
|
448
|
+
const diagnostics = [];
|
|
449
|
+
if (analysis.algorithm.id !== "perttool.recommendation-ranking.lexicographic-frontier" ||
|
|
450
|
+
analysis.algorithm.version !== 1 ||
|
|
451
|
+
analysis.algorithm.optimal !== false ||
|
|
452
|
+
analysis.reasonTaxonomyVersion !== "1.0" ||
|
|
453
|
+
analysis.explanationModelVersion !== 1 ||
|
|
454
|
+
analysis.expressionVersion !== 1 ||
|
|
455
|
+
analysis.descriptionRegistryVersion !== 1 ||
|
|
456
|
+
analysis.descriptionLocale !== "en") {
|
|
457
|
+
addOnce(diagnostics, diagnostic("PTREC-302", "recommendation version identity is not registered"));
|
|
458
|
+
}
|
|
459
|
+
if (!/^sha256:[0-9a-f]{64}$/.test(analysis.sourceDigest)) {
|
|
460
|
+
addOnce(diagnostics, diagnostic("PTREC-301", "recommendation source digest is invalid"));
|
|
461
|
+
}
|
|
462
|
+
const collections = [
|
|
463
|
+
["decision step", analysis.decisionSteps],
|
|
464
|
+
["fact", analysis.facts],
|
|
465
|
+
["comparison", analysis.comparisons],
|
|
466
|
+
["reason", analysis.reasonOccurrences],
|
|
467
|
+
["description", analysis.descriptions],
|
|
468
|
+
["task decision", analysis.taskDecisions],
|
|
469
|
+
];
|
|
470
|
+
const allRecordIds = new Set();
|
|
471
|
+
allRecordIds.add(analysis.resultDecision.id);
|
|
472
|
+
for (const [label, records] of collections) {
|
|
473
|
+
for (const record of records) {
|
|
474
|
+
if (!/^rec:(decision|step|fact|comparison|reason|description):/.test(record.id)) {
|
|
475
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `${label} ${record.id} has an invalid record ID`));
|
|
476
|
+
}
|
|
477
|
+
if (allRecordIds.has(record.id)) {
|
|
478
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `record ID ${record.id} is duplicated`));
|
|
479
|
+
}
|
|
480
|
+
allRecordIds.add(record.id);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const facts = new Map(analysis.facts.map((fact) => [fact.id, fact]));
|
|
484
|
+
const steps = new Map(analysis.decisionSteps.map((step) => [step.id, step]));
|
|
485
|
+
const comparisons = new Map(analysis.comparisons.map((comparison) => [comparison.id, comparison]));
|
|
486
|
+
const reasons = new Map(analysis.reasonOccurrences.map((reason) => [reason.id, reason]));
|
|
487
|
+
const descriptions = new Map(analysis.descriptions.map((description) => [description.id, description]));
|
|
488
|
+
for (const fact of analysis.facts) {
|
|
489
|
+
if (!knownFactKinds.has(fact.kind)) {
|
|
490
|
+
addOnce(diagnostics, diagnostic("PTREC-302", `fact kind ${fact.kind} is not registered`));
|
|
491
|
+
}
|
|
492
|
+
const valueIssue = validateValue(fact.value);
|
|
493
|
+
if (valueIssue !== null) {
|
|
494
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `fact ${fact.id} is invalid: ${valueIssue}`));
|
|
495
|
+
}
|
|
496
|
+
if (fact.provenance.sourceDigest !== analysis.sourceDigest) {
|
|
497
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `fact ${fact.id} provenance digest differs from the result`));
|
|
498
|
+
}
|
|
499
|
+
if (fact.unit !== null && fact.value.type !== "integer" && fact.value.type !== "rational" && fact.value.type !== "map") {
|
|
500
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `fact ${fact.id} uses a unit with a nonnumeric value`));
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const stepIndex = new Map(analysis.decisionSteps.map(({ id }, index) => [id, index]));
|
|
504
|
+
for (const step of analysis.decisionSteps) {
|
|
505
|
+
if (step.rule.kind !== "policy_rule" || !knownRules.has(step.rule.id)) {
|
|
506
|
+
addOnce(diagnostics, diagnostic("PTREC-302", `step ${step.id} uses unregistered rule ${step.rule.id}`));
|
|
507
|
+
}
|
|
508
|
+
for (const factId of step.inputFactIds) {
|
|
509
|
+
if (!facts.has(factId))
|
|
510
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `step ${step.id} references missing fact ${factId}`));
|
|
511
|
+
}
|
|
512
|
+
for (const reasonId of step.reasonOccurrenceIds) {
|
|
513
|
+
if (!reasons.has(reasonId))
|
|
514
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `step ${step.id} references missing reason ${reasonId}`));
|
|
515
|
+
}
|
|
516
|
+
for (const comparisonId of step.comparisonIds) {
|
|
517
|
+
if (!comparisons.has(comparisonId))
|
|
518
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `step ${step.id} references missing comparison ${comparisonId}`));
|
|
519
|
+
}
|
|
520
|
+
for (const dependencyId of step.dependsOnStepIds) {
|
|
521
|
+
const dependencyIndex = stepIndex.get(dependencyId);
|
|
522
|
+
if (dependencyIndex === undefined || dependencyIndex >= stepIndex.get(step.id)) {
|
|
523
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `step ${step.id} has a missing or non-forward dependency ${dependencyId}`));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const result = validateExpression(step.expression, facts, diagnostics, `step ${step.id}`);
|
|
527
|
+
if (result !== null && result !== step.result) {
|
|
528
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `step ${step.id} result differs from expression evaluation`));
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
for (const comparison of analysis.comparisons) {
|
|
532
|
+
if (comparison.decisiveRule.kind !== "policy_rule" || !knownRules.has(comparison.decisiveRule.id)) {
|
|
533
|
+
addOnce(diagnostics, diagnostic("PTREC-302", `comparison ${comparison.id} uses unregistered rule ${comparison.decisiveRule.id}`));
|
|
534
|
+
}
|
|
535
|
+
for (const factId of comparison.factIds) {
|
|
536
|
+
if (!facts.has(factId))
|
|
537
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `comparison ${comparison.id} references missing fact ${factId}`));
|
|
538
|
+
}
|
|
539
|
+
const result = validateExpression(comparison.decisiveExpression, facts, diagnostics, `comparison ${comparison.id}`);
|
|
540
|
+
if (result === false) {
|
|
541
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `comparison ${comparison.id} decisive expression is false`));
|
|
542
|
+
}
|
|
543
|
+
if (comparison.winnerTaskId === null && comparison.loserTaskId !== null) {
|
|
544
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `comparison ${comparison.id} has a loser without a winner`));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
for (const reason of analysis.reasonOccurrences) {
|
|
548
|
+
const contract = reasonContract[reason.code];
|
|
549
|
+
if (contract === undefined) {
|
|
550
|
+
addOnce(diagnostics, diagnostic("PTREC-302", `reason code ${String(reason.code)} is not registered`));
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (reason.effect !== contract.effect || !contract.roles.includes(reason.role)) {
|
|
554
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `reason ${reason.id} effect or role violates its taxonomy`));
|
|
555
|
+
}
|
|
556
|
+
const step = steps.get(reason.decisionStepId);
|
|
557
|
+
if (step === undefined || !step.reasonOccurrenceIds.includes(reason.id)) {
|
|
558
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `reason ${reason.id} is not closed through its decision step`));
|
|
559
|
+
}
|
|
560
|
+
const reasonFacts = reason.factIds.map((id) => facts.get(id)).filter((fact) => fact !== undefined);
|
|
561
|
+
for (const factId of reason.factIds) {
|
|
562
|
+
if (!facts.has(factId))
|
|
563
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `reason ${reason.id} references missing fact ${factId}`));
|
|
564
|
+
}
|
|
565
|
+
for (const requiredKind of contract.requiredFactKinds) {
|
|
566
|
+
if (!reasonFacts.some(({ kind }) => kind === requiredKind)) {
|
|
567
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `reason ${reason.id} is missing required fact kind ${requiredKind}`));
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
for (const comparisonId of reason.comparisonIds) {
|
|
571
|
+
if (!comparisons.has(comparisonId))
|
|
572
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `reason ${reason.id} references missing comparison ${comparisonId}`));
|
|
573
|
+
}
|
|
574
|
+
const result = validateExpression(reason.emissionExpression, facts, diagnostics, `reason ${reason.id}`);
|
|
575
|
+
if (result === false)
|
|
576
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `reason ${reason.id} emission expression is false`));
|
|
577
|
+
if (reason.descriptionId !== null && !descriptions.has(reason.descriptionId)) {
|
|
578
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `reason ${reason.id} references missing description ${reason.descriptionId}`));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
for (const description of analysis.descriptions) {
|
|
582
|
+
validateDescription(description, diagnostics);
|
|
583
|
+
if (description.sourceReasonIds.length === 0 &&
|
|
584
|
+
description.sourceComparisonIds.length === 0) {
|
|
585
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `description ${description.id} has no semantic source`));
|
|
586
|
+
}
|
|
587
|
+
for (const reasonId of description.sourceReasonIds) {
|
|
588
|
+
if (!reasons.has(reasonId))
|
|
589
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `description ${description.id} references missing reason ${reasonId}`));
|
|
590
|
+
}
|
|
591
|
+
for (const comparisonId of description.sourceComparisonIds) {
|
|
592
|
+
if (!comparisons.has(comparisonId))
|
|
593
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `description ${description.id} references missing comparison ${comparisonId}`));
|
|
594
|
+
}
|
|
595
|
+
validateDescriptionSources(description, facts, comparisons, reasons, diagnostics);
|
|
596
|
+
}
|
|
597
|
+
const resultJointFact = facts.get(analysis.resultDecision.jointFeasibilityFactId);
|
|
598
|
+
if (resultJointFact?.kind !== "set_start_feasibility" ||
|
|
599
|
+
resultJointFact.value.type !== "boolean" ||
|
|
600
|
+
resultJointFact.value.value !== true) {
|
|
601
|
+
addOnce(diagnostics, diagnostic("PTREC-301", "result decision does not reference a true recommended-set feasibility fact"));
|
|
602
|
+
}
|
|
603
|
+
for (const id of analysis.resultDecision.stepIds) {
|
|
604
|
+
if (!steps.has(id))
|
|
605
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `result decision references missing step ${id}`));
|
|
606
|
+
}
|
|
607
|
+
for (const id of analysis.resultDecision.reasonOccurrenceIds) {
|
|
608
|
+
if (!reasons.has(id))
|
|
609
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `result decision references missing reason ${id}`));
|
|
610
|
+
}
|
|
611
|
+
const resultReasons = analysis.resultDecision.reasonOccurrenceIds
|
|
612
|
+
.map((id) => reasons.get(id))
|
|
613
|
+
.filter((reason) => reason !== undefined);
|
|
614
|
+
if (!resultReasons.some(({ code }) => code === "recommended_set_feasible")) {
|
|
615
|
+
addOnce(diagnostics, diagnostic("PTREC-301", "result decision is missing recommended_set_feasible"));
|
|
616
|
+
}
|
|
617
|
+
const taskIds = new Set();
|
|
618
|
+
const selectedTaskIds = [];
|
|
619
|
+
for (const decision of analysis.taskDecisions) {
|
|
620
|
+
if (taskIds.has(decision.subjectTaskId)) {
|
|
621
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `task ${decision.subjectTaskId} has multiple decisions`));
|
|
622
|
+
}
|
|
623
|
+
taskIds.add(decision.subjectTaskId);
|
|
624
|
+
if ((decision.tier === "recommended") !== decision.recommendedSetMember) {
|
|
625
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `task ${decision.subjectTaskId} tier and set membership disagree`));
|
|
626
|
+
}
|
|
627
|
+
if (decision.recommendedSetMember)
|
|
628
|
+
selectedTaskIds.push(decision.subjectTaskId);
|
|
629
|
+
const taskSteps = decision.stepIds.map((id) => steps.get(id));
|
|
630
|
+
if (taskSteps.some((step) => step === undefined) || !decision.stepIds.includes(decision.decisiveStepId)) {
|
|
631
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `task ${decision.subjectTaskId} decision step closure is incomplete`));
|
|
632
|
+
}
|
|
633
|
+
const phases = taskSteps
|
|
634
|
+
.filter((step) => step !== undefined)
|
|
635
|
+
.map(({ phase }) => phaseRank[phase]);
|
|
636
|
+
if (phases.some((phase, index) => index > 0 && phase < phases[index - 1])) {
|
|
637
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `task ${decision.subjectTaskId} decision steps are not phase ordered`));
|
|
638
|
+
}
|
|
639
|
+
const taskReasons = decision.reasonOccurrenceIds
|
|
640
|
+
.map((id) => reasons.get(id))
|
|
641
|
+
.filter((reason) => reason !== undefined);
|
|
642
|
+
if (taskReasons.length !== decision.reasonOccurrenceIds.length) {
|
|
643
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `task ${decision.subjectTaskId} reason closure is incomplete`));
|
|
644
|
+
}
|
|
645
|
+
for (const comparisonId of decision.comparisonIds) {
|
|
646
|
+
if (!comparisons.has(comparisonId))
|
|
647
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `task ${decision.subjectTaskId} references missing comparison ${comparisonId}`));
|
|
648
|
+
}
|
|
649
|
+
const summaryDescription = descriptions.get(decision.summaryDescriptionId);
|
|
650
|
+
if (summaryDescription === undefined) {
|
|
651
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `task ${decision.subjectTaskId} has no summary description`));
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
const expectedSummaryKey = decision.tier === "recommended"
|
|
655
|
+
? "recommendation.summary.recommended"
|
|
656
|
+
: decision.tier === "allowed"
|
|
657
|
+
? "recommendation.summary.allowed"
|
|
658
|
+
: decision.tier === "deferred"
|
|
659
|
+
? "recommendation.summary.deferred_resource"
|
|
660
|
+
: "recommendation.summary.discouraged";
|
|
661
|
+
if (summaryDescription.key !== expectedSummaryKey ||
|
|
662
|
+
entityParameterId(summaryDescription, "task_id") !== decision.subjectTaskId) {
|
|
663
|
+
addOnce(diagnostics, diagnostic("PTREC-303", `task ${decision.subjectTaskId} summary does not match its tier or subject`));
|
|
664
|
+
}
|
|
665
|
+
if (decision.tier === "allowed" &&
|
|
666
|
+
entityParameterId(summaryDescription, "higher_priority_task_id") !==
|
|
667
|
+
decision.primaryHigherPriorityTaskId) {
|
|
668
|
+
addOnce(diagnostics, diagnostic("PTREC-303", `task ${decision.subjectTaskId} summary has the wrong higher-priority task`));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
for (const descriptionId of decision.descriptionIds) {
|
|
672
|
+
if (!descriptions.has(descriptionId))
|
|
673
|
+
addOnce(diagnostics, diagnostic("PTREC-301", `task ${decision.subjectTaskId} references missing description ${descriptionId}`));
|
|
674
|
+
}
|
|
675
|
+
validateTier(decision, taskReasons, diagnostics);
|
|
676
|
+
}
|
|
677
|
+
if (selectedTaskIds.length !== analysis.recommendedTaskIds.length ||
|
|
678
|
+
selectedTaskIds.some((id, index) => id !== analysis.recommendedTaskIds[index]) ||
|
|
679
|
+
analysis.resultDecision.recommendedTaskIds.some((id, index) => id !== analysis.recommendedTaskIds[index])) {
|
|
680
|
+
addOnce(diagnostics, diagnostic("PTREC-301", "recommended task IDs disagree with task decisions"));
|
|
681
|
+
}
|
|
682
|
+
const factOrder = [...analysis.facts].sort((left, right) => compareStableStrings(left.kind, right.kind) ||
|
|
683
|
+
compareStableStrings(left.subject.kind, right.subject.kind) ||
|
|
684
|
+
compareStableStrings(left.subject.id, right.subject.id) ||
|
|
685
|
+
compareStableStrings(left.id, right.id));
|
|
686
|
+
if (factOrder.some(({ id }, index) => id !== analysis.facts[index]?.id)) {
|
|
687
|
+
addOnce(diagnostics, diagnostic("PTREC-301", "facts are not in canonical order"));
|
|
688
|
+
}
|
|
689
|
+
const stepOrder = [...analysis.decisionSteps].sort((left, right) => phaseRank[left.phase] - phaseRank[right.phase] ||
|
|
690
|
+
compareRules(left.rule.id, right.rule.id) ||
|
|
691
|
+
compareStableStrings(left.id, right.id));
|
|
692
|
+
if (stepOrder.some(({ id }, index) => id !== analysis.decisionSteps[index]?.id)) {
|
|
693
|
+
addOnce(diagnostics, diagnostic("PTREC-301", "decision steps are not in canonical order"));
|
|
694
|
+
}
|
|
695
|
+
const comparisonOrder = [...analysis.comparisons].sort((left, right) => comparisonScopeRank[left.scope] - comparisonScopeRank[right.scope] ||
|
|
696
|
+
compareStableStrings(left.subjectTaskId, right.subjectTaskId) ||
|
|
697
|
+
compareStableStrings(left.alternativeTaskId ?? "", right.alternativeTaskId ?? "") ||
|
|
698
|
+
compareRules(left.decisiveRule.id, right.decisiveRule.id) ||
|
|
699
|
+
compareStableStrings(left.id, right.id));
|
|
700
|
+
if (comparisonOrder.some(({ id }, index) => id !== analysis.comparisons[index]?.id)) {
|
|
701
|
+
addOnce(diagnostics, diagnostic("PTREC-301", "comparisons are not in canonical order"));
|
|
702
|
+
}
|
|
703
|
+
const alternativeForReason = (reason) => reason.comparisonIds
|
|
704
|
+
.map((id) => comparisons.get(id)?.alternativeTaskId)
|
|
705
|
+
.find((id) => id !== undefined && id !== null) ?? "";
|
|
706
|
+
const reasonOrder = [...analysis.reasonOccurrences].sort((left, right) => {
|
|
707
|
+
const leftStep = steps.get(left.decisionStepId);
|
|
708
|
+
const rightStep = steps.get(right.decisionStepId);
|
|
709
|
+
return ((leftStep === undefined ? Number.MAX_SAFE_INTEGER : phaseRank[leftStep.phase]) -
|
|
710
|
+
(rightStep === undefined ? Number.MAX_SAFE_INTEGER : phaseRank[rightStep.phase]) ||
|
|
711
|
+
compareRules(leftStep?.rule.id ?? "", rightStep?.rule.id ?? "") ||
|
|
712
|
+
compareStableStrings(left.subject.kind, right.subject.kind) ||
|
|
713
|
+
compareStableStrings(left.subject.id, right.subject.id) ||
|
|
714
|
+
compareStableStrings(alternativeForReason(left), alternativeForReason(right)) ||
|
|
715
|
+
compareStableStrings(left.code, right.code) ||
|
|
716
|
+
compareStableStrings(left.id, right.id));
|
|
717
|
+
});
|
|
718
|
+
if (reasonOrder.some(({ id }, index) => id !== analysis.reasonOccurrences[index]?.id)) {
|
|
719
|
+
addOnce(diagnostics, diagnostic("PTREC-301", "reason occurrences are not in canonical order"));
|
|
720
|
+
}
|
|
721
|
+
const taskOrder = new Map(analysis.taskDecisions.map(({ subjectTaskId }, index) => [subjectTaskId, index]));
|
|
722
|
+
const descriptionTaskRank = (description) => {
|
|
723
|
+
for (const reasonId of description.sourceReasonIds) {
|
|
724
|
+
const subject = reasons.get(reasonId)?.subject;
|
|
725
|
+
if (subject?.kind === "task") {
|
|
726
|
+
return taskOrder.get(subject.id) ?? Number.MAX_SAFE_INTEGER;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
return Number.MAX_SAFE_INTEGER;
|
|
730
|
+
};
|
|
731
|
+
const descriptionOrder = [...analysis.descriptions].sort((left, right) => descriptionTaskRank(left) - descriptionTaskRank(right) ||
|
|
732
|
+
compareStableStrings(left.key, right.key) ||
|
|
733
|
+
compareStableStrings(left.id, right.id));
|
|
734
|
+
if (descriptionOrder.some(({ id }, index) => id !== analysis.descriptions[index]?.id)) {
|
|
735
|
+
addOnce(diagnostics, diagnostic("PTREC-303", "descriptions are not in canonical order"));
|
|
736
|
+
}
|
|
737
|
+
for (const description of analysis.descriptions) {
|
|
738
|
+
const sortedNames = description.parameters.map(({ name }) => name).sort(compareStableStrings);
|
|
739
|
+
if (sortedNames.some((name, index) => name !== description.parameters[index]?.name)) {
|
|
740
|
+
addOnce(diagnostics, diagnostic("PTREC-303", `description ${description.id} parameters are not canonical`));
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return diagnostics.sort((left, right) => compareStableStrings(left.code, right.code) || compareStableStrings(left.message, right.message));
|
|
744
|
+
}
|
|
745
|
+
//# sourceMappingURL=explanation-validation.js.map
|