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.
Files changed (176) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/LICENSE +21 -0
  3. package/README.md +134 -0
  4. package/dist/analysis/graph.d.ts +41 -0
  5. package/dist/analysis/graph.d.ts.map +1 -0
  6. package/dist/analysis/graph.js +199 -0
  7. package/dist/analysis/graph.js.map +1 -0
  8. package/dist/analysis/precedence.d.ts +52 -0
  9. package/dist/analysis/precedence.d.ts.map +1 -0
  10. package/dist/analysis/precedence.js +187 -0
  11. package/dist/analysis/precedence.js.map +1 -0
  12. package/dist/analysis/resource.d.ts +96 -0
  13. package/dist/analysis/resource.d.ts.map +1 -0
  14. package/dist/analysis/resource.js +522 -0
  15. package/dist/analysis/resource.js.map +1 -0
  16. package/dist/application/analyze.d.ts +32 -0
  17. package/dist/application/analyze.d.ts.map +1 -0
  18. package/dist/application/analyze.js +144 -0
  19. package/dist/application/analyze.js.map +1 -0
  20. package/dist/application/check.d.ts +24 -0
  21. package/dist/application/check.d.ts.map +1 -0
  22. package/dist/application/check.js +43 -0
  23. package/dist/application/check.js.map +1 -0
  24. package/dist/application/format.d.ts +21 -0
  25. package/dist/application/format.d.ts.map +1 -0
  26. package/dist/application/format.js +52 -0
  27. package/dist/application/format.js.map +1 -0
  28. package/dist/application/mutate.d.ts +4 -0
  29. package/dist/application/mutate.d.ts.map +1 -0
  30. package/dist/application/mutate.js +175 -0
  31. package/dist/application/mutate.js.map +1 -0
  32. package/dist/application/next.d.ts +86 -0
  33. package/dist/application/next.d.ts.map +1 -0
  34. package/dist/application/next.js +310 -0
  35. package/dist/application/next.js.map +1 -0
  36. package/dist/cli.d.ts +3 -0
  37. package/dist/cli.d.ts.map +1 -0
  38. package/dist/cli.js +2103 -0
  39. package/dist/cli.js.map +1 -0
  40. package/dist/conversion/mermaid-import.d.ts +26 -0
  41. package/dist/conversion/mermaid-import.d.ts.map +1 -0
  42. package/dist/conversion/mermaid-import.js +655 -0
  43. package/dist/conversion/mermaid-import.js.map +1 -0
  44. package/dist/conversion/mermaid.d.ts +38 -0
  45. package/dist/conversion/mermaid.d.ts.map +1 -0
  46. package/dist/conversion/mermaid.js +362 -0
  47. package/dist/conversion/mermaid.js.map +1 -0
  48. package/dist/editing/unified-diff.d.ts +7 -0
  49. package/dist/editing/unified-diff.d.ts.map +1 -0
  50. package/dist/editing/unified-diff.js +81 -0
  51. package/dist/editing/unified-diff.js.map +1 -0
  52. package/dist/formatter/source-formatter.d.ts +17 -0
  53. package/dist/formatter/source-formatter.d.ts.map +1 -0
  54. package/dist/formatter/source-formatter.js +210 -0
  55. package/dist/formatter/source-formatter.js.map +1 -0
  56. package/dist/help/registry.d.ts +37 -0
  57. package/dist/help/registry.d.ts.map +1 -0
  58. package/dist/help/registry.js +657 -0
  59. package/dist/help/registry.js.map +1 -0
  60. package/dist/index.d.ts +44 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +23 -0
  63. package/dist/index.js.map +1 -0
  64. package/dist/io/document-file.d.ts +9 -0
  65. package/dist/io/document-file.d.ts.map +1 -0
  66. package/dist/io/document-file.js +19 -0
  67. package/dist/io/document-file.js.map +1 -0
  68. package/dist/io/safe-write.d.ts +33 -0
  69. package/dist/io/safe-write.d.ts.map +1 -0
  70. package/dist/io/safe-write.js +280 -0
  71. package/dist/io/safe-write.js.map +1 -0
  72. package/dist/model/diagnostics.d.ts +43 -0
  73. package/dist/model/diagnostics.d.ts.map +1 -0
  74. package/dist/model/diagnostics.js +47 -0
  75. package/dist/model/diagnostics.js.map +1 -0
  76. package/dist/model/rational.d.ts +21 -0
  77. package/dist/model/rational.d.ts.map +1 -0
  78. package/dist/model/rational.js +84 -0
  79. package/dist/model/rational.js.map +1 -0
  80. package/dist/model/syntax.d.ts +67 -0
  81. package/dist/model/syntax.d.ts.map +1 -0
  82. package/dist/model/syntax.js +15 -0
  83. package/dist/model/syntax.js.map +1 -0
  84. package/dist/model/units.d.ts +18 -0
  85. package/dist/model/units.d.ts.map +1 -0
  86. package/dist/model/units.js +29 -0
  87. package/dist/model/units.js.map +1 -0
  88. package/dist/mutation/advance.d.ts +26 -0
  89. package/dist/mutation/advance.d.ts.map +1 -0
  90. package/dist/mutation/advance.js +261 -0
  91. package/dist/mutation/advance.js.map +1 -0
  92. package/dist/mutation/diagnostics.d.ts +10 -0
  93. package/dist/mutation/diagnostics.d.ts.map +1 -0
  94. package/dist/mutation/diagnostics.js +10 -0
  95. package/dist/mutation/diagnostics.js.map +1 -0
  96. package/dist/mutation/entity-editor.d.ts +16 -0
  97. package/dist/mutation/entity-editor.d.ts.map +1 -0
  98. package/dist/mutation/entity-editor.js +103 -0
  99. package/dist/mutation/entity-editor.js.map +1 -0
  100. package/dist/mutation/milestone.d.ts +5 -0
  101. package/dist/mutation/milestone.d.ts.map +1 -0
  102. package/dist/mutation/milestone.js +194 -0
  103. package/dist/mutation/milestone.js.map +1 -0
  104. package/dist/mutation/resource.d.ts +5 -0
  105. package/dist/mutation/resource.d.ts.map +1 -0
  106. package/dist/mutation/resource.js +145 -0
  107. package/dist/mutation/resource.js.map +1 -0
  108. package/dist/mutation/source.d.ts +22 -0
  109. package/dist/mutation/source.d.ts.map +1 -0
  110. package/dist/mutation/source.js +155 -0
  111. package/dist/mutation/source.js.map +1 -0
  112. package/dist/mutation/task.d.ts +10 -0
  113. package/dist/mutation/task.d.ts.map +1 -0
  114. package/dist/mutation/task.js +644 -0
  115. package/dist/mutation/task.js.map +1 -0
  116. package/dist/mutation/text-edits.d.ts +8 -0
  117. package/dist/mutation/text-edits.d.ts.map +1 -0
  118. package/dist/mutation/text-edits.js +33 -0
  119. package/dist/mutation/text-edits.js.map +1 -0
  120. package/dist/mutation/types.d.ts +153 -0
  121. package/dist/mutation/types.d.ts.map +1 -0
  122. package/dist/mutation/types.js +2 -0
  123. package/dist/mutation/types.js.map +1 -0
  124. package/dist/parser/document-parser.d.ts +6 -0
  125. package/dist/parser/document-parser.d.ts.map +1 -0
  126. package/dist/parser/document-parser.js +735 -0
  127. package/dist/parser/document-parser.js.map +1 -0
  128. package/dist/recommendation/explanation-types.d.ts +226 -0
  129. package/dist/recommendation/explanation-types.d.ts.map +1 -0
  130. package/dist/recommendation/explanation-types.js +2 -0
  131. package/dist/recommendation/explanation-types.js.map +1 -0
  132. package/dist/recommendation/explanation-validation.d.ts +4 -0
  133. package/dist/recommendation/explanation-validation.d.ts.map +1 -0
  134. package/dist/recommendation/explanation-validation.js +745 -0
  135. package/dist/recommendation/explanation-validation.js.map +1 -0
  136. package/dist/recommendation/explanation-values.d.ts +7 -0
  137. package/dist/recommendation/explanation-values.d.ts.map +1 -0
  138. package/dist/recommendation/explanation-values.js +283 -0
  139. package/dist/recommendation/explanation-values.js.map +1 -0
  140. package/dist/recommendation/explanation.d.ts +8 -0
  141. package/dist/recommendation/explanation.d.ts.map +1 -0
  142. package/dist/recommendation/explanation.js +1022 -0
  143. package/dist/recommendation/explanation.js.map +1 -0
  144. package/dist/recommendation/failure.d.ts +3 -0
  145. package/dist/recommendation/failure.d.ts.map +1 -0
  146. package/dist/recommendation/failure.js +4 -0
  147. package/dist/recommendation/failure.js.map +1 -0
  148. package/dist/recommendation/json.d.ts +3 -0
  149. package/dist/recommendation/json.d.ts.map +1 -0
  150. package/dist/recommendation/json.js +252 -0
  151. package/dist/recommendation/json.js.map +1 -0
  152. package/dist/recommendation/override-types.d.ts +117 -0
  153. package/dist/recommendation/override-types.d.ts.map +1 -0
  154. package/dist/recommendation/override-types.js +2 -0
  155. package/dist/recommendation/override-types.js.map +1 -0
  156. package/dist/recommendation/override.d.ts +7 -0
  157. package/dist/recommendation/override.d.ts.map +1 -0
  158. package/dist/recommendation/override.js +717 -0
  159. package/dist/recommendation/override.js.map +1 -0
  160. package/dist/recommendation/ranking.d.ts +7 -0
  161. package/dist/recommendation/ranking.d.ts.map +1 -0
  162. package/dist/recommendation/ranking.js +391 -0
  163. package/dist/recommendation/ranking.js.map +1 -0
  164. package/dist/recommendation/types.d.ts +83 -0
  165. package/dist/recommendation/types.d.ts.map +1 -0
  166. package/dist/recommendation/types.js +2 -0
  167. package/dist/recommendation/types.js.map +1 -0
  168. package/dist/semantic/validator.d.ts +4 -0
  169. package/dist/semantic/validator.d.ts.map +1 -0
  170. package/dist/semantic/validator.js +490 -0
  171. package/dist/semantic/validator.js.map +1 -0
  172. package/dist/version.d.ts +2 -0
  173. package/dist/version.d.ts.map +1 -0
  174. package/dist/version.js +2 -0
  175. package/dist/version.js.map +1 -0
  176. package/package.json +61 -0
@@ -0,0 +1,1022 @@
1
+ import { compareStableStrings } from "../model/diagnostics.js";
2
+ import { recommendationValueKey, renderRecommendationDescription, } from "./explanation-values.js";
3
+ import { explainRecommendationCandidateComparison } from "./ranking.js";
4
+ import { validateRecommendationAnalysis } from "./explanation-validation.js";
5
+ export const RECOMMENDATION_REASON_TAXONOMY_VERSION = "1.0";
6
+ export const RECOMMENDATION_EXPLANATION_MODEL_VERSION = 1;
7
+ export const RECOMMENDATION_EXPRESSION_VERSION = 1;
8
+ export const RECOMMENDATION_DESCRIPTION_REGISTRY_VERSION = 1;
9
+ export const RECOMMENDATION_DESCRIPTION_LOCALE = "en";
10
+ const phaseRank = {
11
+ eligibility: 0,
12
+ negative_fact_filter: 1,
13
+ selection_horizon: 2,
14
+ candidate_ranking: 3,
15
+ resource_selection: 4,
16
+ set_membership: 5,
17
+ tier_classification: 6,
18
+ };
19
+ const comparisonScopeRank = {
20
+ ranking: 0,
21
+ selection_horizon: 1,
22
+ resource_selection: 2,
23
+ tier: 3,
24
+ };
25
+ const ruleOrder = [
26
+ "task_ready",
27
+ "selection_horizon",
28
+ "critical_class",
29
+ "lower_total_float",
30
+ "higher_explicit_priority",
31
+ "higher_new_ready_count",
32
+ "higher_new_gate_count",
33
+ "higher_new_milestone_count",
34
+ "shorter_next_gate_distance",
35
+ "shorter_finish_distance",
36
+ "longer_expected_duration",
37
+ "task_id_tiebreak",
38
+ "joint_resource_feasibility",
39
+ "recommended_set_membership",
40
+ "recommendation_tier",
41
+ ];
42
+ const ruleRank = new Map(ruleOrder.map((rule, index) => [rule, index]));
43
+ function compareRules(left, right) {
44
+ return ((ruleRank.get(left) ?? Number.MAX_SAFE_INTEGER) -
45
+ (ruleRank.get(right) ?? Number.MAX_SAFE_INTEGER) ||
46
+ compareStableStrings(left, right));
47
+ }
48
+ function encodeRecordComponent(component) {
49
+ let encoded = "";
50
+ for (const byte of new TextEncoder().encode(component)) {
51
+ const character = String.fromCharCode(byte);
52
+ if (/[A-Za-z0-9._~-]/.test(character)) {
53
+ encoded += character;
54
+ }
55
+ else {
56
+ encoded += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
57
+ }
58
+ }
59
+ return encoded;
60
+ }
61
+ function recordId(kind, ...components) {
62
+ return `rec:${kind}:${components.map(encodeRecordComponent).join(":")}`;
63
+ }
64
+ function entity(kind, id) {
65
+ return { kind, id };
66
+ }
67
+ function booleanValue(value) {
68
+ return { type: "boolean", value };
69
+ }
70
+ function integerValue(value) {
71
+ if (!Number.isSafeInteger(value))
72
+ throw new Error(`unsafe recommendation integer ${value}`);
73
+ return { type: "integer", value: value.toString() };
74
+ }
75
+ function rationalValue(value) {
76
+ return {
77
+ type: "rational",
78
+ numerator: value.numerator.toString(),
79
+ denominator: value.denominator.toString(),
80
+ };
81
+ }
82
+ function enumValue(enumType, value) {
83
+ return { type: "enum", enumType, value };
84
+ }
85
+ function entityValue(kind, id) {
86
+ return { type: "entity", value: entity(kind, id) };
87
+ }
88
+ function setValue(itemType, items) {
89
+ const unique = new Map(items.map((item) => [recommendationValueKey(item), item]));
90
+ return {
91
+ type: "set",
92
+ itemType,
93
+ items: [...unique.entries()]
94
+ .sort(([left], [right]) => compareStableStrings(left, right))
95
+ .map(([, item]) => item),
96
+ };
97
+ }
98
+ function literal(value, unit = null) {
99
+ return { kind: "literal", value, unit };
100
+ }
101
+ function factEquals(factId, value, unit = null) {
102
+ return {
103
+ kind: "compare",
104
+ left: { kind: "fact", factId },
105
+ relation: "equal",
106
+ right: literal(value, unit),
107
+ };
108
+ }
109
+ function projectUnit(unit) {
110
+ return { kind: "duration", value: unit };
111
+ }
112
+ function resourceUnit(resourceId) {
113
+ return { kind: "resource", resource: entity("resource", resourceId) };
114
+ }
115
+ function sortedReferences(references) {
116
+ const unique = new Map(references.map((reference) => [`${reference.kind}:${reference.id}`, reference]));
117
+ return [...unique.values()].sort((left, right) => compareStableStrings(left.kind, right.kind) || compareStableStrings(left.id, right.id));
118
+ }
119
+ function provenance(input, kind, producerId, producerVersion, references) {
120
+ return {
121
+ kind,
122
+ sourceDigest: input.sourceDigest,
123
+ entityReferences: sortedReferences(references),
124
+ producer: { id: producerId, version: producerVersion },
125
+ sourceSpan: null,
126
+ };
127
+ }
128
+ function factorProjection(candidate, rule, durationUnit) {
129
+ switch (rule) {
130
+ case "critical_class":
131
+ return {
132
+ kind: "precedence_critical_class",
133
+ value: enumValue("precedence_critical_class", candidate.precedenceCriticalClass),
134
+ unit: null,
135
+ };
136
+ case "lower_total_float":
137
+ return {
138
+ kind: "precedence_total_float",
139
+ value: rationalValue(candidate.precedenceTotalFloat),
140
+ unit: projectUnit(durationUnit),
141
+ };
142
+ case "higher_explicit_priority":
143
+ return { kind: "explicit_priority", value: integerValue(candidate.explicitPriority), unit: null };
144
+ case "higher_new_ready_count":
145
+ return { kind: "new_ready_task_count", value: integerValue(candidate.newReadyTaskCount), unit: null };
146
+ case "higher_new_gate_count":
147
+ return {
148
+ kind: "new_satisfied_gate_count",
149
+ value: integerValue(candidate.newSatisfiedGateCount),
150
+ unit: null,
151
+ };
152
+ case "higher_new_milestone_count":
153
+ return {
154
+ kind: "new_reached_milestone_count",
155
+ value: integerValue(candidate.newReachedMilestoneCount),
156
+ unit: null,
157
+ };
158
+ case "shorter_next_gate_distance":
159
+ return {
160
+ kind: "next_gate_task_distance",
161
+ value: enumValue("structural_distance", String(candidate.nextGateTaskDistance)),
162
+ unit: null,
163
+ };
164
+ case "shorter_finish_distance":
165
+ return {
166
+ kind: "finish_task_distance",
167
+ value: integerValue(candidate.finishTaskDistance),
168
+ unit: null,
169
+ };
170
+ case "longer_expected_duration":
171
+ return {
172
+ kind: "expected_duration",
173
+ value: rationalValue(candidate.expectedDuration),
174
+ unit: projectUnit(durationUnit),
175
+ };
176
+ case "task_id_tiebreak":
177
+ return { kind: "task_id", value: enumValue("task_id_ascii", candidate.taskId), unit: null };
178
+ }
179
+ }
180
+ function winnerRelation(rule) {
181
+ switch (rule) {
182
+ case "higher_explicit_priority":
183
+ case "higher_new_ready_count":
184
+ case "higher_new_gate_count":
185
+ case "higher_new_milestone_count":
186
+ case "longer_expected_duration":
187
+ return "greater_than";
188
+ default:
189
+ return "less_than";
190
+ }
191
+ }
192
+ function descriptionParameters(parameters) {
193
+ return [...parameters].sort((left, right) => compareStableStrings(left.name, right.name));
194
+ }
195
+ function addFact(state, semanticKey, fact) {
196
+ const existing = state.factsBySemanticKey.get(semanticKey);
197
+ if (existing !== undefined)
198
+ return existing;
199
+ const id = recordId("fact", ...semanticKey.split(":"));
200
+ const result = { id, ...fact };
201
+ state.facts.push(result);
202
+ state.factsBySemanticKey.set(semanticKey, result);
203
+ return result;
204
+ }
205
+ function addDescription(state, taskId, key, parameters, sourceReasonIds, sourceComparisonIds, occurrence = 0) {
206
+ const sorted = descriptionParameters(parameters);
207
+ const description = {
208
+ id: recordId("description", taskId, key, occurrence.toString()),
209
+ key,
210
+ registryVersion: RECOMMENDATION_DESCRIPTION_REGISTRY_VERSION,
211
+ parameters: sorted,
212
+ sourceReasonIds,
213
+ sourceComparisonIds,
214
+ locale: RECOMMENDATION_DESCRIPTION_LOCALE,
215
+ text: renderRecommendationDescription(key, sorted),
216
+ renderStatus: "rendered",
217
+ };
218
+ state.descriptions.push(description);
219
+ state.descriptionTaskIds.set(description.id, taskId);
220
+ return description;
221
+ }
222
+ function applicationFact(state, taskId, ruleId, outcome) {
223
+ return addFact(state, `${taskId}:ranking_rule_application:${ruleId}`, {
224
+ kind: "ranking_rule_application",
225
+ subject: entity("task", taskId),
226
+ value: enumValue("ranking_rule_application", outcome),
227
+ unit: null,
228
+ provenance: provenance(state.input, "ranking_algorithm", state.input.ranking.algorithmId, state.input.ranking.algorithmVersion.toString(), [entity("task", taskId), entity("policy_rule", ruleId)]),
229
+ });
230
+ }
231
+ function addStepWithReason(state, options) {
232
+ const occurrence = options.occurrence ?? 0;
233
+ const stepId = recordId("step", options.taskId, options.phase, options.ruleId, occurrence.toString());
234
+ const comparisonIds = options.comparisonIds ?? [];
235
+ let reason = null;
236
+ if (options.reason !== undefined) {
237
+ const reasonId = recordId("reason", options.taskId, options.reason.code, options.role, (options.reason.occurrence ?? occurrence).toString());
238
+ reason = {
239
+ id: reasonId,
240
+ code: options.reason.code,
241
+ subject: options.reason.subject ?? entity("task", options.taskId),
242
+ effect: options.effect,
243
+ role: options.role,
244
+ factIds: options.reason.factIds,
245
+ emissionExpression: options.reason.emissionExpression ?? options.expression,
246
+ decisionStepId: stepId,
247
+ comparisonIds,
248
+ descriptionId: options.reason.descriptionId ?? null,
249
+ };
250
+ state.reasons.push(reason);
251
+ }
252
+ const step = {
253
+ id: stepId,
254
+ phase: options.phase,
255
+ rule: entity("policy_rule", options.ruleId),
256
+ inputFactIds: options.inputFactIds,
257
+ expression: options.expression,
258
+ result: true,
259
+ effect: options.effect,
260
+ role: options.role,
261
+ reasonOccurrenceIds: reason === null ? [] : [reason.id],
262
+ comparisonIds,
263
+ dependsOnStepIds: options.dependsOnStepIds,
264
+ };
265
+ state.steps.push(step);
266
+ return { step, reason };
267
+ }
268
+ function candidateFact(state, candidate, rule) {
269
+ const projected = factorProjection(candidate, rule, state.durationUnit);
270
+ const provenanceKind = projected.kind === "explicit_priority" || projected.kind === "task_id"
271
+ ? "document"
272
+ : projected.kind === "precedence_total_float" ||
273
+ projected.kind === "precedence_critical_class" ||
274
+ projected.kind === "expected_duration"
275
+ ? "precedence_analysis"
276
+ : "ranking_algorithm";
277
+ return addFact(state, `${candidate.taskId}:${projected.kind}:0`, {
278
+ kind: projected.kind,
279
+ subject: entity("task", candidate.taskId),
280
+ value: projected.value,
281
+ unit: projected.unit,
282
+ provenance: provenance(state.input, provenanceKind, provenanceKind === "ranking_algorithm"
283
+ ? state.input.ranking.algorithmId
284
+ : `perttool.${provenanceKind}`, provenanceKind === "ranking_algorithm"
285
+ ? state.input.ranking.algorithmVersion.toString()
286
+ : "1", [entity("task", candidate.taskId), entity("ranking_factor", projected.kind)]),
287
+ });
288
+ }
289
+ function addCandidateFacts(state, candidate) {
290
+ for (const rule of ruleOrder.filter((candidateRule) => [
291
+ "critical_class",
292
+ "lower_total_float",
293
+ "higher_explicit_priority",
294
+ "higher_new_ready_count",
295
+ "higher_new_gate_count",
296
+ "higher_new_milestone_count",
297
+ "shorter_next_gate_distance",
298
+ "shorter_finish_distance",
299
+ "longer_expected_duration",
300
+ "task_id_tiebreak",
301
+ ].includes(candidateRule))) {
302
+ candidateFact(state, candidate, rule);
303
+ }
304
+ addFact(state, `${candidate.taskId}:requirements:0`, {
305
+ kind: "requirements",
306
+ subject: entity("task", candidate.taskId),
307
+ value: {
308
+ type: "map",
309
+ keyType: "entity:resource",
310
+ valueType: "integer",
311
+ entries: candidate.requirements.map(({ resourceId, units }) => ({
312
+ key: entityValue("resource", resourceId),
313
+ value: integerValue(units),
314
+ })),
315
+ },
316
+ unit: null,
317
+ provenance: provenance(state.input, "document", "perttool.document", "1", [entity("task", candidate.taskId), ...candidate.requirements.map(({ resourceId }) => entity("resource", resourceId))]),
318
+ });
319
+ }
320
+ function addRankingComparison(state, winner, alternative, scope) {
321
+ const semantic = explainRecommendationCandidateComparison(winner, alternative);
322
+ const relevantRules = [
323
+ ...semantic.priorTiedRuleIds,
324
+ semantic.decisiveRuleId,
325
+ ...semantic.contributingRuleIds,
326
+ ];
327
+ const factIds = [];
328
+ for (const rule of relevantRules) {
329
+ factIds.push(candidateFact(state, winner, rule).id, candidateFact(state, alternative, rule).id);
330
+ }
331
+ const winnerFact = candidateFact(state, winner, semantic.decisiveRuleId);
332
+ const alternativeFact = candidateFact(state, alternative, semantic.decisiveRuleId);
333
+ const decisiveExpression = {
334
+ kind: "compare",
335
+ left: { kind: "fact", factId: winnerFact.id },
336
+ relation: winnerRelation(semantic.decisiveRuleId),
337
+ right: { kind: "fact", factId: alternativeFact.id },
338
+ };
339
+ const comparison = {
340
+ id: recordId("comparison", scope, winner.taskId, alternative.taskId, semantic.decisiveRuleId, "0"),
341
+ scope,
342
+ subjectTaskId: winner.taskId,
343
+ alternativeTaskId: alternative.taskId,
344
+ winnerTaskId: winner.taskId,
345
+ loserTaskId: alternative.taskId,
346
+ decisiveRule: entity("policy_rule", semantic.decisiveRuleId),
347
+ decisiveExpression,
348
+ priorTiedRuleIds: semantic.priorTiedRuleIds,
349
+ contributingRuleIds: semantic.contributingRuleIds,
350
+ factIds: [...new Set(factIds)],
351
+ };
352
+ state.comparisons.push(comparison);
353
+ return comparison;
354
+ }
355
+ function addResourceWitness(state, taskId, resource, context) {
356
+ const refs = [
357
+ entity("task", taskId),
358
+ entity("resource", resource.resourceId),
359
+ ...resource.activeTaskIds.map((id) => entity("task", id)),
360
+ ...resource.selectedTaskIds.map((id) => entity("task", id)),
361
+ ];
362
+ const commonProvenance = provenance(state.input, "resource_snapshot", "perttool.recommendation-resource-snapshot", "1", refs);
363
+ const numeric = addFact(state, `${taskId}:${resource.resourceId}:${context}:resource_capacity_witness:0`, {
364
+ kind: "resource_capacity_witness",
365
+ subject: entity("resource", resource.resourceId),
366
+ value: {
367
+ type: "map",
368
+ keyType: "enum:resource_witness_field",
369
+ valueType: "integer",
370
+ entries: [
371
+ ["active_usage", resource.activeUsage],
372
+ ["available", resource.available],
373
+ ["capacity", resource.capacity],
374
+ ["deficit", resource.deficit],
375
+ ["required", resource.required],
376
+ ["selected_usage", resource.selectedUsage],
377
+ ].map(([name, value]) => ({
378
+ key: enumValue("resource_witness_field", String(name)),
379
+ value: integerValue(value),
380
+ })),
381
+ },
382
+ unit: resourceUnit(resource.resourceId),
383
+ provenance: commonProvenance,
384
+ });
385
+ const numericFields = {
386
+ resource_capacity: resource.capacity,
387
+ resource_active_usage: resource.activeUsage,
388
+ resource_selected_usage: resource.selectedUsage,
389
+ resource_used: resource.activeUsage + resource.selectedUsage,
390
+ resource_required: resource.required,
391
+ resource_available: resource.available,
392
+ resource_deficit: resource.deficit,
393
+ };
394
+ const scalarFacts = Object.fromEntries(Object.entries(numericFields).map(([kind, value]) => [
395
+ kind,
396
+ addFact(state, `${taskId}:${resource.resourceId}:${context}:${kind}:0`, {
397
+ kind,
398
+ subject: entity("resource", resource.resourceId),
399
+ value: integerValue(value),
400
+ unit: resourceUnit(resource.resourceId),
401
+ provenance: commonProvenance,
402
+ }),
403
+ ]));
404
+ const active = addFact(state, `${taskId}:${resource.resourceId}:${context}:resource_active_occupants:0`, {
405
+ kind: "resource_active_occupants",
406
+ subject: entity("resource", resource.resourceId),
407
+ value: setValue("entity:task", resource.activeTaskIds.map((id) => entityValue("task", id))),
408
+ unit: null,
409
+ provenance: commonProvenance,
410
+ });
411
+ const selected = addFact(state, `${taskId}:${resource.resourceId}:${context}:resource_selected_occupants:0`, {
412
+ kind: "resource_selected_occupants",
413
+ subject: entity("resource", resource.resourceId),
414
+ value: setValue("entity:task", resource.selectedTaskIds.map((id) => entityValue("task", id))),
415
+ unit: null,
416
+ provenance: commonProvenance,
417
+ });
418
+ const occupants = addFact(state, `${taskId}:${resource.resourceId}:${context}:resource_occupants:0`, {
419
+ kind: "resource_occupants",
420
+ subject: entity("resource", resource.resourceId),
421
+ value: setValue("entity:task", [...resource.activeTaskIds, ...resource.selectedTaskIds].map((id) => entityValue("task", id))),
422
+ unit: null,
423
+ provenance: commonProvenance,
424
+ });
425
+ return {
426
+ factIds: [
427
+ numeric.id,
428
+ ...Object.values(scalarFacts).map(({ id }) => id),
429
+ active.id,
430
+ selected.id,
431
+ occupants.id,
432
+ ],
433
+ capacityFact: scalarFacts.resource_capacity,
434
+ usedFact: scalarFacts.resource_used,
435
+ requiredFact: scalarFacts.resource_required,
436
+ deficitFact: scalarFacts.resource_deficit,
437
+ occupantFact: occupants,
438
+ };
439
+ }
440
+ function addComparisonDescription(state, taskId, comparison, reasonId) {
441
+ const [winnerFactId, alternativeFactId] = comparison.factIds.filter((factId) => {
442
+ const fact = state.facts.find(({ id }) => id === factId);
443
+ return fact?.kind === factorProjection(state.input.ranking.candidates.find(({ taskId: id }) => id === comparison.winnerTaskId), comparison.decisiveRule.id, state.durationUnit).kind;
444
+ });
445
+ const winnerFact = state.facts.find(({ id }) => id === winnerFactId);
446
+ const alternativeFact = state.facts.find(({ id }) => id === alternativeFactId);
447
+ return addDescription(state, taskId, "recommendation.reason.ranking_comparison", [
448
+ { name: "winner_task_id", value: entityValue("task", comparison.winnerTaskId), unit: null },
449
+ { name: "alternative_task_id", value: entityValue("task", comparison.alternativeTaskId), unit: null },
450
+ { name: "rule_id", value: entityValue("policy_rule", comparison.decisiveRule.id), unit: null },
451
+ { name: "winner_value", value: winnerFact.value, unit: winnerFact.unit },
452
+ { name: "alternative_value", value: alternativeFact.value, unit: alternativeFact.unit },
453
+ {
454
+ name: "relation",
455
+ value: enumValue("recommendation_relation", comparison.decisiveExpression.kind === "compare"
456
+ ? comparison.decisiveExpression.relation
457
+ : "equal"),
458
+ unit: null,
459
+ },
460
+ ], [reasonId], [comparison.id]);
461
+ }
462
+ function buildTaskDecision(state, rankingDecision, horizonFirst) {
463
+ const taskId = rankingDecision.facts.taskId;
464
+ const stepIds = [];
465
+ const reasonIds = [];
466
+ const comparisonIds = [];
467
+ const descriptionIds = [];
468
+ const readyFact = addFact(state, `${taskId}:task_classification:0`, {
469
+ kind: "task_classification",
470
+ subject: entity("task", taskId),
471
+ value: enumValue("task_classification", "ready"),
472
+ unit: null,
473
+ provenance: provenance(state.input, "recommendation_model", "perttool.recommendation-model", "1", [entity("task", taskId)]),
474
+ });
475
+ const ready = addStepWithReason(state, {
476
+ taskId,
477
+ phase: "eligibility",
478
+ ruleId: "task_ready",
479
+ expression: factEquals(readyFact.id, enumValue("task_classification", "ready")),
480
+ inputFactIds: [readyFact.id],
481
+ effect: "neutral",
482
+ role: "context",
483
+ dependsOnStepIds: [],
484
+ reason: { code: "task_ready", factIds: [readyFact.id] },
485
+ });
486
+ stepIds.push(ready.step.id);
487
+ reasonIds.push(ready.reason.id);
488
+ const horizonFact = addFact(state, `${taskId}:selection_horizon_membership:0`, {
489
+ kind: "selection_horizon_membership",
490
+ subject: entity("task", taskId),
491
+ value: booleanValue(rankingDecision.horizonMember),
492
+ unit: null,
493
+ provenance: provenance(state.input, "ranking_algorithm", state.input.ranking.algorithmId, state.input.ranking.algorithmVersion.toString(), [entity("task", taskId), entity("derived_set", "H"), entity("policy_rule", "selection_horizon")]),
494
+ });
495
+ const horizonApplication = applicationFact(state, taskId, "selection_horizon", rankingDecision.horizonMember ? "supports" : "opposes");
496
+ const horizonExpression = factEquals(horizonFact.id, booleanValue(rankingDecision.horizonMember));
497
+ const horizonStep = addStepWithReason(state, {
498
+ taskId,
499
+ phase: "selection_horizon",
500
+ ruleId: "selection_horizon",
501
+ expression: horizonExpression,
502
+ inputFactIds: [horizonFact.id, horizonApplication.id],
503
+ effect: rankingDecision.horizonMember ? "supporting" : "opposing",
504
+ role: rankingDecision.recommendedSetMember ? "decisive" : "contributing",
505
+ dependsOnStepIds: [ready.step.id],
506
+ ...(rankingDecision.horizonMember
507
+ ? {
508
+ reason: {
509
+ code: "ranking_rule_supports_task",
510
+ factIds: [horizonApplication.id, horizonFact.id],
511
+ emissionExpression: factEquals(horizonApplication.id, enumValue("ranking_rule_application", "supports")),
512
+ },
513
+ }
514
+ : {}),
515
+ });
516
+ stepIds.push(horizonStep.step.id);
517
+ if (horizonStep.reason !== null)
518
+ reasonIds.push(horizonStep.reason.id);
519
+ let priorStepId = horizonStep.step.id;
520
+ let rankingComparison = null;
521
+ let rankingReason = null;
522
+ if (!rankingDecision.horizonMember && horizonFirst !== undefined) {
523
+ rankingComparison = addRankingComparison(state, horizonFirst, rankingDecision.facts, "selection_horizon");
524
+ comparisonIds.push(rankingComparison.id);
525
+ const application = applicationFact(state, taskId, rankingComparison.decisiveRule.id, "opposes");
526
+ const rankStep = addStepWithReason(state, {
527
+ taskId,
528
+ phase: "candidate_ranking",
529
+ ruleId: rankingComparison.decisiveRule.id,
530
+ expression: rankingComparison.decisiveExpression,
531
+ inputFactIds: [...rankingComparison.factIds, application.id],
532
+ effect: "opposing",
533
+ role: "decisive",
534
+ dependsOnStepIds: [priorStepId],
535
+ comparisonIds: [rankingComparison.id],
536
+ reason: {
537
+ code: "ranking_rule_opposes_task",
538
+ factIds: [application.id, ...rankingComparison.factIds],
539
+ emissionExpression: factEquals(application.id, enumValue("ranking_rule_application", "opposes")),
540
+ },
541
+ });
542
+ stepIds.push(rankStep.step.id);
543
+ reasonIds.push(rankStep.reason.id);
544
+ priorStepId = rankStep.step.id;
545
+ const emittedRankingReason = rankStep.reason;
546
+ rankingReason = emittedRankingReason;
547
+ const comparisonDescription = addComparisonDescription(state, taskId, rankingComparison, emittedRankingReason.id);
548
+ rankingReason = {
549
+ ...emittedRankingReason,
550
+ descriptionId: comparisonDescription.id,
551
+ };
552
+ state.reasons[state.reasons.findIndex(({ id }) => id === emittedRankingReason.id)] = rankingReason;
553
+ descriptionIds.push(comparisonDescription.id);
554
+ }
555
+ const selectionContext = rankingDecision.selection.evaluated
556
+ ? "selection"
557
+ : "addition";
558
+ const selectionFeasibility = rankingDecision.selection.feasibility ?? rankingDecision.tierFeasibility;
559
+ const selectionFeasibilityFact = addFact(state, `${taskId}:set_start_feasibility:${selectionContext}`, {
560
+ kind: "set_start_feasibility",
561
+ subject: entity("task", taskId),
562
+ value: booleanValue(selectionFeasibility.feasible),
563
+ unit: null,
564
+ provenance: provenance(state.input, "resource_snapshot", "perttool.recommendation-resource-snapshot", "1", [
565
+ entity("task", taskId),
566
+ entity("derived_set", "R"),
567
+ ...selectionFeasibility.resources.map(({ resourceId }) => entity("resource", resourceId)),
568
+ ]),
569
+ });
570
+ const selectionViolated = selectionFeasibility.resources.filter(({ deficit }) => deficit > 0);
571
+ const selectionWitnesses = selectionViolated.map((resource) => ({
572
+ resource,
573
+ witness: addResourceWitness(state, taskId, resource, selectionContext),
574
+ }));
575
+ const tierFeasibility = rankingDecision.recommendedSetMember
576
+ ? selectionFeasibility
577
+ : rankingDecision.tierFeasibility;
578
+ const tierContext = rankingDecision.recommendedSetMember
579
+ ? selectionContext
580
+ : "addition";
581
+ const tierFeasibilityFact = tierContext === selectionContext
582
+ ? selectionFeasibilityFact
583
+ : addFact(state, `${taskId}:set_start_feasibility:${tierContext}`, {
584
+ kind: "set_start_feasibility",
585
+ subject: entity("task", taskId),
586
+ value: booleanValue(tierFeasibility.feasible),
587
+ unit: null,
588
+ provenance: provenance(state.input, "resource_snapshot", "perttool.recommendation-resource-snapshot", "1", [
589
+ entity("task", taskId),
590
+ entity("derived_set", "R"),
591
+ ...tierFeasibility.resources.map(({ resourceId }) => entity("resource", resourceId)),
592
+ ]),
593
+ });
594
+ const tierViolated = tierFeasibility.resources.filter(({ deficit }) => deficit > 0);
595
+ const tierWitnesses = tierContext === selectionContext
596
+ ? selectionWitnesses
597
+ : tierViolated.map((resource) => ({
598
+ resource,
599
+ witness: addResourceWitness(state, taskId, resource, tierContext),
600
+ }));
601
+ let resourceComparison = null;
602
+ if (!selectionFeasibility.feasible) {
603
+ const resourceWinner = selectionWitnesses.flatMap(({ resource }) => resource.selectedTaskIds)[0] ?? null;
604
+ resourceComparison = {
605
+ id: recordId("comparison", "resource_selection", taskId, resourceWinner ?? "-", "joint_resource_feasibility", "0"),
606
+ scope: "resource_selection",
607
+ subjectTaskId: taskId,
608
+ alternativeTaskId: resourceWinner,
609
+ winnerTaskId: resourceWinner,
610
+ loserTaskId: resourceWinner === null ? null : taskId,
611
+ decisiveRule: entity("policy_rule", "joint_resource_feasibility"),
612
+ decisiveExpression: factEquals(selectionFeasibilityFact.id, booleanValue(false)),
613
+ priorTiedRuleIds: [],
614
+ contributingRuleIds: [],
615
+ factIds: [
616
+ selectionFeasibilityFact.id,
617
+ ...selectionWitnesses.flatMap(({ witness }) => witness.factIds),
618
+ ],
619
+ };
620
+ state.comparisons.push(resourceComparison);
621
+ comparisonIds.push(resourceComparison.id);
622
+ }
623
+ const resourceStep = addStepWithReason(state, {
624
+ taskId,
625
+ phase: "resource_selection",
626
+ ruleId: "joint_resource_feasibility",
627
+ expression: factEquals(selectionFeasibilityFact.id, booleanValue(selectionFeasibility.feasible)),
628
+ inputFactIds: [
629
+ selectionFeasibilityFact.id,
630
+ ...selectionWitnesses.flatMap(({ witness }) => witness.factIds),
631
+ ],
632
+ effect: selectionFeasibility.feasible ? "supporting" : "blocking",
633
+ role: "contributing",
634
+ dependsOnStepIds: [priorStepId],
635
+ comparisonIds: resourceComparison === null ? [] : [resourceComparison.id],
636
+ });
637
+ stepIds.push(resourceStep.step.id);
638
+ priorStepId = resourceStep.step.id;
639
+ const membershipFact = addFact(state, `${taskId}:recommendation_set_membership:0`, {
640
+ kind: "recommendation_set_membership",
641
+ subject: entity("task", taskId),
642
+ value: booleanValue(rankingDecision.recommendedSetMember),
643
+ unit: null,
644
+ provenance: provenance(state.input, "recommendation_model", "perttool.recommendation-model", "1", [entity("task", taskId), entity("derived_set", "R")]),
645
+ });
646
+ const membership = addStepWithReason(state, {
647
+ taskId,
648
+ phase: "set_membership",
649
+ ruleId: "recommended_set_membership",
650
+ expression: factEquals(membershipFact.id, booleanValue(rankingDecision.recommendedSetMember)),
651
+ inputFactIds: [membershipFact.id],
652
+ effect: rankingDecision.recommendedSetMember ? "supporting" : "opposing",
653
+ role: "decisive",
654
+ dependsOnStepIds: [priorStepId],
655
+ reason: {
656
+ code: rankingDecision.recommendedSetMember
657
+ ? "recommended_set_selected"
658
+ : "recommended_set_not_selected",
659
+ factIds: [membershipFact.id],
660
+ },
661
+ });
662
+ stepIds.push(membership.step.id);
663
+ reasonIds.push(membership.reason.id);
664
+ priorStepId = membership.step.id;
665
+ const tierFact = addFact(state, `${taskId}:recommendation_tier:0`, {
666
+ kind: "recommendation_tier",
667
+ subject: entity("task", taskId),
668
+ value: enumValue("recommendation_tier", rankingDecision.tier),
669
+ unit: null,
670
+ provenance: provenance(state.input, "recommendation_model", "perttool.recommendation-model", "1", [entity("task", taskId), entity("derived_set", "R")]),
671
+ });
672
+ let decisiveStepId = membership.step.id;
673
+ let tierReasonIds = [];
674
+ if (rankingDecision.tier === "allowed") {
675
+ const tierStep = addStepWithReason(state, {
676
+ taskId,
677
+ phase: "tier_classification",
678
+ ruleId: "recommendation_tier",
679
+ expression: {
680
+ kind: "all",
681
+ children: [
682
+ factEquals(tierFeasibilityFact.id, booleanValue(true)),
683
+ factEquals(tierFact.id, enumValue("recommendation_tier", "allowed")),
684
+ ],
685
+ },
686
+ inputFactIds: [tierFeasibilityFact.id, tierFact.id],
687
+ effect: "supporting",
688
+ role: "decisive",
689
+ dependsOnStepIds: [priorStepId],
690
+ reason: {
691
+ code: "recommended_set_addition_feasible",
692
+ factIds: [tierFeasibilityFact.id],
693
+ emissionExpression: factEquals(tierFeasibilityFact.id, booleanValue(true)),
694
+ },
695
+ });
696
+ stepIds.push(tierStep.step.id);
697
+ reasonIds.push(tierStep.reason.id);
698
+ tierReasonIds = [tierStep.reason.id];
699
+ decisiveStepId = tierStep.step.id;
700
+ }
701
+ else if (rankingDecision.tier === "deferred") {
702
+ const resourceReasonIds = [];
703
+ for (const [index, { resource, witness }] of tierWitnesses.entries()) {
704
+ const resourceDescription = addDescription(state, taskId, "recommendation.reason.resource_conflict", [
705
+ { name: "task_id", value: entityValue("task", taskId), unit: null },
706
+ { name: "resource_id", value: entityValue("resource", resource.resourceId), unit: null },
707
+ {
708
+ name: "capacity",
709
+ value: witness.capacityFact.value,
710
+ unit: witness.capacityFact.unit,
711
+ },
712
+ {
713
+ name: "used",
714
+ value: witness.usedFact.value,
715
+ unit: witness.usedFact.unit,
716
+ },
717
+ {
718
+ name: "required",
719
+ value: witness.requiredFact.value,
720
+ unit: witness.requiredFact.unit,
721
+ },
722
+ {
723
+ name: "deficit",
724
+ value: witness.deficitFact.value,
725
+ unit: witness.deficitFact.unit,
726
+ },
727
+ {
728
+ name: "occupant_task_ids",
729
+ value: witness.occupantFact.value,
730
+ unit: witness.occupantFact.unit,
731
+ },
732
+ ], [], resourceComparison === null ? [] : [resourceComparison.id], index);
733
+ const reasonId = recordId("reason", taskId, "recommended_set_resource_conflict", index === 0 ? "decisive" : "contributing", index.toString());
734
+ const emission = {
735
+ kind: "all",
736
+ children: [
737
+ factEquals(tierFeasibilityFact.id, booleanValue(false)),
738
+ {
739
+ kind: "compare",
740
+ left: { kind: "fact", factId: witness.deficitFact.id },
741
+ relation: "greater_than",
742
+ right: literal(integerValue(0), resourceUnit(resource.resourceId)),
743
+ },
744
+ ],
745
+ };
746
+ const reason = {
747
+ id: reasonId,
748
+ code: "recommended_set_resource_conflict",
749
+ subject: entity("task", taskId),
750
+ effect: "blocking",
751
+ role: index === 0 ? "decisive" : "contributing",
752
+ factIds: [tierFeasibilityFact.id, ...witness.factIds],
753
+ emissionExpression: emission,
754
+ decisionStepId: recordId("step", taskId, "tier_classification", "joint_resource_feasibility", "0"),
755
+ comparisonIds: resourceComparison === null ? [] : [resourceComparison.id],
756
+ descriptionId: resourceDescription.id,
757
+ };
758
+ state.reasons.push(reason);
759
+ resourceReasonIds.push(reason.id);
760
+ state.descriptions[state.descriptions.findIndex(({ id }) => id === resourceDescription.id)] = {
761
+ ...resourceDescription,
762
+ sourceReasonIds: [reason.id],
763
+ };
764
+ descriptionIds.push(resourceDescription.id);
765
+ }
766
+ const tierExpression = {
767
+ kind: "all",
768
+ children: [
769
+ factEquals(tierFeasibilityFact.id, booleanValue(false)),
770
+ factEquals(tierFact.id, enumValue("recommendation_tier", "deferred")),
771
+ ],
772
+ };
773
+ const tierStep = {
774
+ id: recordId("step", taskId, "tier_classification", "joint_resource_feasibility", "0"),
775
+ phase: "tier_classification",
776
+ rule: entity("policy_rule", "joint_resource_feasibility"),
777
+ inputFactIds: [
778
+ tierFeasibilityFact.id,
779
+ tierFact.id,
780
+ ...tierWitnesses.flatMap(({ witness }) => witness.factIds),
781
+ ],
782
+ expression: tierExpression,
783
+ result: true,
784
+ effect: "blocking",
785
+ role: "decisive",
786
+ reasonOccurrenceIds: resourceReasonIds,
787
+ comparisonIds: resourceComparison === null ? [] : [resourceComparison.id],
788
+ dependsOnStepIds: [priorStepId],
789
+ };
790
+ state.steps.push(tierStep);
791
+ stepIds.push(tierStep.id);
792
+ reasonIds.push(...resourceReasonIds);
793
+ tierReasonIds = resourceReasonIds;
794
+ decisiveStepId = tierStep.id;
795
+ }
796
+ else {
797
+ const tierStep = addStepWithReason(state, {
798
+ taskId,
799
+ phase: "tier_classification",
800
+ ruleId: "recommendation_tier",
801
+ expression: {
802
+ kind: "all",
803
+ children: [
804
+ factEquals(membershipFact.id, booleanValue(true)),
805
+ factEquals(tierFact.id, enumValue("recommendation_tier", "recommended")),
806
+ ],
807
+ },
808
+ inputFactIds: [membershipFact.id, tierFact.id],
809
+ effect: "supporting",
810
+ role: "context",
811
+ dependsOnStepIds: [priorStepId],
812
+ });
813
+ stepIds.push(tierStep.step.id);
814
+ }
815
+ let summary;
816
+ if (rankingDecision.tier === "recommended") {
817
+ const decisiveRule = rankingDecision.selection.scanIndex === 0
818
+ ? "selection_horizon"
819
+ : "joint_resource_feasibility";
820
+ summary = addDescription(state, taskId, "recommendation.summary.recommended", [
821
+ { name: "task_id", value: entityValue("task", taskId), unit: null },
822
+ { name: "decisive_rule_id", value: entityValue("policy_rule", decisiveRule), unit: null },
823
+ ], [horizonStep.reason.id, membership.reason.id], []);
824
+ }
825
+ else if (rankingDecision.tier === "allowed") {
826
+ summary = addDescription(state, taskId, "recommendation.summary.allowed", [
827
+ { name: "task_id", value: entityValue("task", taskId), unit: null },
828
+ {
829
+ name: "higher_priority_task_id",
830
+ value: entityValue("task", rankingDecision.primaryHigherPriorityTaskId),
831
+ unit: null,
832
+ },
833
+ {
834
+ name: "decisive_rule_id",
835
+ value: entityValue("policy_rule", rankingComparison.decisiveRule.id),
836
+ unit: null,
837
+ },
838
+ ], [rankingReason.id, ...tierReasonIds], [rankingComparison.id]);
839
+ }
840
+ else {
841
+ summary = addDescription(state, taskId, "recommendation.summary.deferred_resource", [
842
+ { name: "task_id", value: entityValue("task", taskId), unit: null },
843
+ {
844
+ name: "resource_ids",
845
+ value: setValue("entity:resource", tierViolated.map(({ resourceId }) => entityValue("resource", resourceId))),
846
+ unit: null,
847
+ },
848
+ {
849
+ name: "higher_priority_task_ids",
850
+ value: setValue("entity:task", rankingDecision.selectedBlockerTaskIds.map((id) => entityValue("task", id))),
851
+ unit: null,
852
+ },
853
+ {
854
+ name: "active_blocker_task_ids",
855
+ value: setValue("entity:task", rankingDecision.activeBlockerTaskIds.map((id) => entityValue("task", id))),
856
+ unit: null,
857
+ },
858
+ ], tierReasonIds, resourceComparison === null ? [] : [resourceComparison.id]);
859
+ }
860
+ descriptionIds.push(summary.id);
861
+ return {
862
+ id: recordId("decision", "task", taskId),
863
+ subjectTaskId: taskId,
864
+ action: "start",
865
+ classification: "ready",
866
+ tier: rankingDecision.tier,
867
+ recommendedSetMember: rankingDecision.recommendedSetMember,
868
+ stepIds,
869
+ decisiveStepId,
870
+ reasonOccurrenceIds: reasonIds,
871
+ comparisonIds,
872
+ primaryHigherPriorityTaskId: rankingDecision.primaryHigherPriorityTaskId,
873
+ summaryDescriptionId: summary.id,
874
+ descriptionIds,
875
+ };
876
+ }
877
+ function buildRecommendationExplanationUnchecked(input) {
878
+ const state = {
879
+ input,
880
+ durationUnit: input.graph.durationUnit,
881
+ facts: [],
882
+ factsBySemanticKey: new Map(),
883
+ steps: [],
884
+ comparisons: [],
885
+ reasons: [],
886
+ descriptions: [],
887
+ descriptionTaskIds: new Map(),
888
+ };
889
+ for (const candidate of input.ranking.candidates)
890
+ addCandidateFacts(state, candidate);
891
+ const jointFact = addFact(state, "R:set_start_feasibility:0", {
892
+ kind: "set_start_feasibility",
893
+ subject: entity("derived_set", "R"),
894
+ value: booleanValue(input.ranking.jointFeasibility.feasible),
895
+ unit: null,
896
+ provenance: provenance(input, "resource_snapshot", "perttool.recommendation-resource-snapshot", "1", [entity("derived_set", "R"), ...input.ranking.jointFeasibility.resources.map(({ resourceId }) => entity("resource", resourceId))]),
897
+ });
898
+ const resultTrace = addStepWithReason(state, {
899
+ taskId: "R",
900
+ phase: "resource_selection",
901
+ ruleId: "joint_resource_feasibility",
902
+ expression: factEquals(jointFact.id, booleanValue(true)),
903
+ inputFactIds: [jointFact.id],
904
+ effect: "neutral",
905
+ role: "context",
906
+ dependsOnStepIds: [],
907
+ reason: {
908
+ code: "recommended_set_feasible",
909
+ subject: entity("derived_set", "R"),
910
+ factIds: [jointFact.id],
911
+ },
912
+ });
913
+ const horizonFirst = input.ranking.candidates.find(({ taskId }) => input.ranking.horizonTaskIds.includes(taskId));
914
+ const taskDecisions = input.ranking.taskDecisions.map((decision) => buildTaskDecision(state, decision, horizonFirst));
915
+ const taskOrder = new Map(taskDecisions.map(({ subjectTaskId }, index) => [subjectTaskId, index]));
916
+ state.steps.sort((left, right) => phaseRank[left.phase] - phaseRank[right.phase] ||
917
+ compareRules(left.rule.id, right.rule.id) ||
918
+ compareStableStrings(left.id, right.id));
919
+ state.comparisons.sort((left, right) => comparisonScopeRank[left.scope] - comparisonScopeRank[right.scope] ||
920
+ compareStableStrings(left.subjectTaskId, right.subjectTaskId) ||
921
+ compareStableStrings(left.alternativeTaskId ?? "", right.alternativeTaskId ?? "") ||
922
+ compareRules(left.decisiveRule.id, right.decisiveRule.id) ||
923
+ compareStableStrings(left.id, right.id));
924
+ state.facts.sort((left, right) => compareStableStrings(left.kind, right.kind) ||
925
+ compareStableStrings(left.subject.kind, right.subject.kind) ||
926
+ compareStableStrings(left.subject.id, right.subject.id) ||
927
+ compareStableStrings(left.id, right.id));
928
+ const stepById = new Map(state.steps.map((step) => [step.id, step]));
929
+ const comparisonById = new Map(state.comparisons.map((comparison) => [comparison.id, comparison]));
930
+ state.reasons.sort((left, right) => {
931
+ const leftStep = stepById.get(left.decisionStepId);
932
+ const rightStep = stepById.get(right.decisionStepId);
933
+ const leftAlternative = left.comparisonIds
934
+ .map((id) => comparisonById.get(id)?.alternativeTaskId)
935
+ .find((id) => id !== undefined && id !== null) ?? "";
936
+ const rightAlternative = right.comparisonIds
937
+ .map((id) => comparisonById.get(id)?.alternativeTaskId)
938
+ .find((id) => id !== undefined && id !== null) ?? "";
939
+ return (phaseRank[leftStep.phase] - phaseRank[rightStep.phase] ||
940
+ compareRules(leftStep.rule.id, rightStep.rule.id) ||
941
+ compareStableStrings(left.subject.kind, right.subject.kind) ||
942
+ compareStableStrings(left.subject.id, right.subject.id) ||
943
+ compareStableStrings(leftAlternative, rightAlternative) ||
944
+ compareStableStrings(left.code, right.code) ||
945
+ compareStableStrings(left.id, right.id));
946
+ });
947
+ state.descriptions.sort((left, right) => (taskOrder.get(state.descriptionTaskIds.get(left.id)) ?? Number.MAX_SAFE_INTEGER) -
948
+ (taskOrder.get(state.descriptionTaskIds.get(right.id)) ?? Number.MAX_SAFE_INTEGER) ||
949
+ compareStableStrings(left.key, right.key) ||
950
+ compareStableStrings(left.id, right.id));
951
+ const analysis = {
952
+ action: "start",
953
+ algorithm: {
954
+ id: input.ranking.algorithmId,
955
+ version: input.ranking.algorithmVersion,
956
+ optimal: input.ranking.optimal,
957
+ },
958
+ reasonTaxonomyVersion: RECOMMENDATION_REASON_TAXONOMY_VERSION,
959
+ explanationModelVersion: RECOMMENDATION_EXPLANATION_MODEL_VERSION,
960
+ expressionVersion: RECOMMENDATION_EXPRESSION_VERSION,
961
+ descriptionRegistryVersion: RECOMMENDATION_DESCRIPTION_REGISTRY_VERSION,
962
+ descriptionLocale: RECOMMENDATION_DESCRIPTION_LOCALE,
963
+ sourceDigest: input.sourceDigest,
964
+ recommendedTaskIds: input.ranking.recommendedTaskIds,
965
+ resultDecision: {
966
+ id: recordId("decision", "result", "R"),
967
+ action: "start",
968
+ recommendedTaskIds: input.ranking.recommendedTaskIds,
969
+ jointFeasibilityFactId: jointFact.id,
970
+ stepIds: [resultTrace.step.id],
971
+ reasonOccurrenceIds: [resultTrace.reason.id],
972
+ },
973
+ taskDecisions,
974
+ decisionSteps: state.steps,
975
+ facts: state.facts,
976
+ comparisons: state.comparisons,
977
+ reasonOccurrences: state.reasons,
978
+ descriptions: state.descriptions,
979
+ explanationStatus: {
980
+ level: "full",
981
+ complete: true,
982
+ decisiveChainComplete: true,
983
+ truncated: false,
984
+ omittedCounts: {
985
+ decisionSteps: 0,
986
+ facts: 0,
987
+ comparisons: 0,
988
+ reasonOccurrences: 0,
989
+ descriptions: 0,
990
+ },
991
+ },
992
+ };
993
+ const diagnostics = validateRecommendationAnalysis(analysis);
994
+ return diagnostics.length === 0
995
+ ? { ok: true, analysis, diagnostics: [] }
996
+ : { ok: false, analysis: null, diagnostics };
997
+ }
998
+ export function buildRecommendationExplanation(input) {
999
+ try {
1000
+ return buildRecommendationExplanationUnchecked(input);
1001
+ }
1002
+ catch (error) {
1003
+ const message = error instanceof Error ? error.message : String(error);
1004
+ const code = message.includes("description")
1005
+ ? "PTREC-303"
1006
+ : message.includes("unknown") || message.includes("registered")
1007
+ ? "PTREC-302"
1008
+ : "PTREC-301";
1009
+ return {
1010
+ ok: false,
1011
+ analysis: null,
1012
+ diagnostics: [
1013
+ {
1014
+ code,
1015
+ severity: "error",
1016
+ message: `recommendation explanation invariant failure: ${message}`,
1017
+ },
1018
+ ],
1019
+ };
1020
+ }
1021
+ }
1022
+ //# sourceMappingURL=explanation.js.map