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,717 @@
1
+ import { createHash } from "node:crypto";
2
+ import { compareStableStrings } from "../model/diagnostics.js";
3
+ import { fieldNamed } from "../model/syntax.js";
4
+ import { validateRecommendationAnalysis } from "./explanation-validation.js";
5
+ import { TOOL_VERSION } from "../version.js";
6
+ const overrideSchemaVersion = "Perttool.OverrideDecision.v1";
7
+ const operation = "recommendation.override.validate";
8
+ const sourceSchemaVersion = "Perttool.NextResult.v3";
9
+ const digestPattern = /^sha256:[0-9a-f]{64}$/;
10
+ const overrideReasonCodes = new Set([
11
+ "human_priority_decision",
12
+ "external_commitment",
13
+ "incident_response",
14
+ "plan_correction_pending",
15
+ "resource_reallocation_pending",
16
+ "risk_acceptance",
17
+ "experiment",
18
+ "other_explicit_reason",
19
+ ]);
20
+ const evidenceKinds = new Set([
21
+ "issue",
22
+ "commit",
23
+ "document",
24
+ "url",
25
+ "other",
26
+ ]);
27
+ function failure(code, message, data = {}) {
28
+ return {
29
+ schemaVersion: overrideSchemaVersion,
30
+ toolVersion: TOOL_VERSION,
31
+ operation,
32
+ ok: false,
33
+ diagnostics: [{
34
+ code,
35
+ severity: "error",
36
+ message,
37
+ data,
38
+ }],
39
+ diagnosticsTruncated: false,
40
+ override: null,
41
+ };
42
+ }
43
+ function byteCompare(left, right) {
44
+ return Buffer.compare(Buffer.from(left, "utf8"), Buffer.from(right, "utf8"));
45
+ }
46
+ function sameMembers(left, right) {
47
+ if (left.length !== right.length)
48
+ return false;
49
+ const leftSet = new Set(left);
50
+ const rightSet = new Set(right);
51
+ return (leftSet.size === left.length &&
52
+ rightSet.size === right.length &&
53
+ left.every((id) => rightSet.has(id)));
54
+ }
55
+ function validBoundedText(value, maximumBytes) {
56
+ return (typeof value === "string" &&
57
+ value.isWellFormed() &&
58
+ !value.includes("\0") &&
59
+ Buffer.byteLength(value, "utf8") >= 1 &&
60
+ Buffer.byteLength(value, "utf8") <= maximumBytes &&
61
+ !/^\p{White_Space}/u.test(value) &&
62
+ !/\p{White_Space}$/u.test(value));
63
+ }
64
+ function validUtcSecond(value) {
65
+ if (typeof value !== "string" ||
66
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(value)) {
67
+ return false;
68
+ }
69
+ const milliseconds = Date.parse(value);
70
+ return (Number.isFinite(milliseconds) &&
71
+ new Date(milliseconds).toISOString() === `${value.slice(0, -1)}.000Z`);
72
+ }
73
+ function sourceContractError(source, request) {
74
+ try {
75
+ if (request.sourceSchemaVersion !== sourceSchemaVersion) {
76
+ return `unsupported source schema ${String(request.sourceSchemaVersion)}`;
77
+ }
78
+ if (!source.ok ||
79
+ source.recommendation === null ||
80
+ source.diagnosticsTruncated) {
81
+ return "source NextResult.v3 must be successful and untruncated";
82
+ }
83
+ const recommendation = source.recommendation;
84
+ if (recommendation.algorithm.id !==
85
+ "perttool.recommendation-ranking.lexicographic-frontier" ||
86
+ recommendation.algorithm.version !== 1 ||
87
+ recommendation.algorithm.optimal !== false ||
88
+ recommendation.reasonTaxonomyVersion !== "1.0" ||
89
+ recommendation.explanationModelVersion !== 1 ||
90
+ recommendation.expressionVersion !== 1 ||
91
+ recommendation.descriptionRegistryVersion !== 1 ||
92
+ recommendation.descriptionLocale !== "en" ||
93
+ recommendation.explanationStatus.complete !== true ||
94
+ recommendation.explanationStatus.decisiveChainComplete !== true ||
95
+ recommendation.explanationStatus.truncated !== false) {
96
+ return "source recommendation uses unsupported or incomplete decisive semantics";
97
+ }
98
+ if (!digestPattern.test(recommendation.sourceDigest)) {
99
+ return "source recommendation digest is not canonical SHA-256";
100
+ }
101
+ const invariantDiagnostics = validateRecommendationAnalysis(recommendation);
102
+ if (invariantDiagnostics.length > 0) {
103
+ return `source recommendation invariant failed with ${invariantDiagnostics[0].code}`;
104
+ }
105
+ const readyTasks = source.tasks
106
+ .filter(({ classification }) => classification === "ready")
107
+ .map(({ id }) => id);
108
+ const decisionTasks = recommendation.taskDecisions.map(({ subjectTaskId }) => subjectTaskId);
109
+ if (!sameMembers(source.groups.ready, readyTasks) ||
110
+ !sameMembers(source.groups.ready, decisionTasks)) {
111
+ return "source ready tasks and recommendation decisions do not match";
112
+ }
113
+ const declaredResources = new Set(source.document.declarations
114
+ .filter(({ kind }) => kind === "resource")
115
+ .map(({ id }) => id));
116
+ for (const [resourceId, capacity] of source.capacityOverrides) {
117
+ if (!declaredResources.has(resourceId) ||
118
+ !Number.isSafeInteger(capacity) ||
119
+ capacity < 1) {
120
+ return "source capacity override is invalid";
121
+ }
122
+ }
123
+ return null;
124
+ }
125
+ catch (error) {
126
+ return `source recommendation cannot be validated: ${error instanceof Error ? error.message : String(error)}`;
127
+ }
128
+ }
129
+ function canonicalEvidence(request) {
130
+ if (!Array.isArray(request.evidenceReferences) ||
131
+ request.evidenceReferences.length > 16) {
132
+ return null;
133
+ }
134
+ const byKey = new Map();
135
+ for (const reference of request.evidenceReferences) {
136
+ if (reference === null ||
137
+ typeof reference !== "object" ||
138
+ !evidenceKinds.has(reference.kind) ||
139
+ !validBoundedText(reference.value, 1024)) {
140
+ return null;
141
+ }
142
+ byKey.set(`${reference.kind}\0${reference.value}`, {
143
+ kind: reference.kind,
144
+ value: reference.value,
145
+ });
146
+ }
147
+ return [...byKey.values()].sort((left, right) => compareStableStrings(left.kind, right.kind) ||
148
+ byteCompare(left.value, right.value));
149
+ }
150
+ function validHumanFields(request) {
151
+ if (request.actor === null ||
152
+ typeof request.actor !== "object" ||
153
+ request.actor.kind !== "human" ||
154
+ request.actor.authentication !== "caller_asserted" ||
155
+ !validBoundedText(request.actor.id, 256) ||
156
+ !validUtcSecond(request.decidedAt) ||
157
+ !overrideReasonCodes.has(request.reasonCode) ||
158
+ !validBoundedText(request.reasonText, 4096)) {
159
+ return null;
160
+ }
161
+ const evidence = canonicalEvidence(request);
162
+ return evidence === null ? null : { evidence };
163
+ }
164
+ function triggerCodesForTier(tier, displacesRecommended) {
165
+ const triggers = [];
166
+ if (tier === "allowed" && displacesRecommended) {
167
+ triggers.push("allowed_replaces_recommended");
168
+ }
169
+ if (tier === "deferred")
170
+ triggers.push("deferred_selected");
171
+ if (tier === "discouraged")
172
+ triggers.push("discouraged_selected");
173
+ return triggers;
174
+ }
175
+ function declaredCapacities(source) {
176
+ const capacities = new Map();
177
+ for (const declaration of source.document.declarations) {
178
+ if (declaration.kind !== "resource")
179
+ continue;
180
+ capacities.set(declaration.id, fieldNamed(declaration, "capacity").value);
181
+ }
182
+ for (const [resourceId, capacity] of source.capacityOverrides) {
183
+ capacities.set(resourceId, capacity);
184
+ }
185
+ return capacities;
186
+ }
187
+ function addTaskUsage(usage, capacities, task, occupants) {
188
+ for (const requirement of task.requirements) {
189
+ if (!capacities.has(requirement.resourceId)) {
190
+ throw new Error(`task ${task.id} references unknown resource ${requirement.resourceId}`);
191
+ }
192
+ usage.set(requirement.resourceId, (usage.get(requirement.resourceId) ?? 0) + requirement.units);
193
+ if (occupants !== undefined) {
194
+ const taskIds = occupants.get(requirement.resourceId) ?? [];
195
+ taskIds.push(task.id);
196
+ occupants.set(requirement.resourceId, taskIds);
197
+ }
198
+ }
199
+ }
200
+ function feasibilityExpression(witnesses) {
201
+ if (witnesses.length === 0)
202
+ return null;
203
+ return {
204
+ kind: "all",
205
+ children: witnesses.map((witness) => {
206
+ const unit = {
207
+ kind: "resource",
208
+ resource: { kind: "resource", id: witness.resourceId },
209
+ };
210
+ return {
211
+ kind: "compare",
212
+ left: {
213
+ kind: "literal",
214
+ value: { type: "integer", value: String(witness.used) },
215
+ unit,
216
+ },
217
+ relation: "less_or_equal",
218
+ right: {
219
+ kind: "literal",
220
+ value: { type: "integer", value: String(witness.capacity) },
221
+ unit,
222
+ },
223
+ };
224
+ }),
225
+ };
226
+ }
227
+ function evaluateSelectedSet(source, selectedTaskIds) {
228
+ const taskById = new Map(source.tasks.map((task) => [task.id, task]));
229
+ const capacities = declaredCapacities(source);
230
+ const activeTasks = source.tasks
231
+ .filter(({ classification }) => classification === "active")
232
+ .sort((left, right) => compareStableStrings(left.id, right.id));
233
+ const selectedTasks = selectedTaskIds.map((id) => taskById.get(id));
234
+ const activeUsage = new Map();
235
+ const selectedUsage = new Map();
236
+ const selectedOccupants = new Map();
237
+ for (const task of activeTasks) {
238
+ addTaskUsage(activeUsage, capacities, task);
239
+ }
240
+ for (const task of selectedTasks) {
241
+ addTaskUsage(selectedUsage, capacities, task, selectedOccupants);
242
+ }
243
+ const resourceIds = [...capacities.keys()]
244
+ .filter((id) => (activeUsage.get(id) ?? 0) > 0 ||
245
+ (selectedUsage.get(id) ?? 0) > 0)
246
+ .sort(compareStableStrings);
247
+ const witnesses = resourceIds.map((resourceId) => {
248
+ const capacity = capacities.get(resourceId);
249
+ const active = activeUsage.get(resourceId) ?? 0;
250
+ const selected = selectedUsage.get(resourceId) ?? 0;
251
+ const used = active + selected;
252
+ return {
253
+ resourceId,
254
+ capacity,
255
+ activeUsage: active,
256
+ selectedUsage: selected,
257
+ used,
258
+ availableAfterSelection: capacity - used,
259
+ selectedTaskIds: [...(selectedOccupants.get(resourceId) ?? [])],
260
+ };
261
+ });
262
+ const violatedResources = witnesses
263
+ .filter(({ used, capacity }) => used > capacity)
264
+ .map(({ resourceId }) => resourceId);
265
+ if (violatedResources.length > 0) {
266
+ return { feasible: false, result: null, violatedResources };
267
+ }
268
+ return {
269
+ feasible: true,
270
+ result: {
271
+ selectedSetReference: { kind: "derived_set", id: "O" },
272
+ startFeasible: true,
273
+ activeTaskIds: activeTasks.map(({ id }) => id),
274
+ resourceWitnesses: witnesses,
275
+ expression: feasibilityExpression(witnesses),
276
+ },
277
+ violatedResources: [],
278
+ };
279
+ }
280
+ function negativeReasonIds(source, taskId) {
281
+ const recommendation = source.recommendation;
282
+ const decision = recommendation.taskDecisions.find(({ subjectTaskId }) => subjectTaskId === taskId);
283
+ const reasonById = new Map(recommendation.reasonOccurrences.map((reason) => [reason.id, reason]));
284
+ return decision.reasonOccurrenceIds.filter((id) => {
285
+ const reason = reasonById.get(id);
286
+ return (reason?.code === "modeled_negative_fact_applies" &&
287
+ reason.role === "decisive");
288
+ });
289
+ }
290
+ function acknowledgementError(source, request, selectedTaskIds) {
291
+ if (!Array.isArray(request.acknowledgedNegativeFactReasonIds) ||
292
+ request.acknowledgedNegativeFactReasonIds.some((id) => typeof id !== "string")) {
293
+ return "negative fact acknowledgement must be a list of reason IDs";
294
+ }
295
+ const provided = request.acknowledgedNegativeFactReasonIds;
296
+ if (new Set(provided).size !== provided.length) {
297
+ return "negative fact acknowledgement contains duplicate reason IDs";
298
+ }
299
+ const recommendation = source.recommendation;
300
+ const decisionByTask = new Map(recommendation.taskDecisions.map((decision) => [
301
+ decision.subjectTaskId,
302
+ decision,
303
+ ]));
304
+ const expected = selectedTaskIds.flatMap((taskId) => {
305
+ const decision = decisionByTask.get(taskId);
306
+ if (decision.tier !== "discouraged")
307
+ return [];
308
+ const reasons = negativeReasonIds(source, taskId);
309
+ if (reasons.length === 0) {
310
+ throw new Error(`discouraged task ${taskId} has no decisive negative fact reason`);
311
+ }
312
+ return reasons;
313
+ });
314
+ return sameMembers(provided, expected)
315
+ ? null
316
+ : "negative fact acknowledgement does not match selected discouraged tasks";
317
+ }
318
+ function taskDecisionReferences(source, selectedTaskIds, displacesRecommended) {
319
+ const decisionByTask = new Map(source.recommendation.taskDecisions.map((decision) => [
320
+ decision.subjectTaskId,
321
+ decision,
322
+ ]));
323
+ return selectedTaskIds.map((taskId) => {
324
+ const normal = decisionByTask.get(taskId);
325
+ return {
326
+ taskId,
327
+ normalDecisionId: normal.id,
328
+ normalTier: normal.tier,
329
+ normalDecisiveStepId: normal.decisiveStepId,
330
+ normalReasonOccurrenceIds: normal.reasonOccurrenceIds,
331
+ normalComparisonIds: normal.comparisonIds,
332
+ overrideSelected: true,
333
+ triggerCodes: triggerCodesForTier(normal.tier, displacesRecommended),
334
+ acknowledgedNegativeFactReasonIds: normal.tier === "discouraged"
335
+ ? negativeReasonIds(source, taskId)
336
+ : [],
337
+ };
338
+ });
339
+ }
340
+ export function validateOverride(source, request) {
341
+ const sourceError = sourceContractError(source, request);
342
+ if (sourceError !== null) {
343
+ return failure("PTOVR-101", sourceError);
344
+ }
345
+ const recommendation = source.recommendation;
346
+ if (request.sourceDigest !== recommendation.sourceDigest ||
347
+ request.sourceResultDecisionId !== recommendation.resultDecision.id) {
348
+ return failure("PTOVR-102", "override request does not match the source digest and result decision");
349
+ }
350
+ if (!Array.isArray(request.selectedTaskIds) ||
351
+ request.selectedTaskIds.length === 0 ||
352
+ request.selectedTaskIds.some((id) => typeof id !== "string") ||
353
+ new Set(request.selectedTaskIds).size !== request.selectedTaskIds.length) {
354
+ return failure("PTOVR-103", "selected_task_ids must contain unique ready task IDs");
355
+ }
356
+ const selectedSet = new Set(request.selectedTaskIds);
357
+ const readySet = new Set(source.groups.ready);
358
+ const decisionByTask = new Map(recommendation.taskDecisions.map((decision) => [
359
+ decision.subjectTaskId,
360
+ decision,
361
+ ]));
362
+ const invalidTaskIds = request.selectedTaskIds.filter((id) => !readySet.has(id) || !decisionByTask.has(id));
363
+ if (invalidTaskIds.length > 0) {
364
+ return failure("PTOVR-103", "selected task is not an actual ready task with a normal decision", { task_ids: [...invalidTaskIds].sort(compareStableStrings) });
365
+ }
366
+ const selectedTaskIds = recommendation.taskDecisions
367
+ .map(({ subjectTaskId }) => subjectTaskId)
368
+ .filter((id) => selectedSet.has(id));
369
+ const recommendedSet = new Set(recommendation.recommendedTaskIds);
370
+ const retainedRecommendedTaskIds = recommendation.recommendedTaskIds.filter((id) => selectedSet.has(id));
371
+ const displacedRecommendedTaskIds = recommendation.recommendedTaskIds.filter((id) => !selectedSet.has(id));
372
+ const selectedNonrecommendedTaskIds = selectedTaskIds.filter((id) => !recommendedSet.has(id));
373
+ const displacesRecommended = displacedRecommendedTaskIds.length > 0;
374
+ const eventTriggers = [];
375
+ if (displacesRecommended &&
376
+ selectedTaskIds.some((id) => decisionByTask.get(id).tier === "allowed")) {
377
+ eventTriggers.push("allowed_replaces_recommended");
378
+ }
379
+ if (selectedTaskIds.some((id) => decisionByTask.get(id).tier === "deferred")) {
380
+ eventTriggers.push("deferred_selected");
381
+ }
382
+ if (selectedTaskIds.some((id) => decisionByTask.get(id).tier === "discouraged")) {
383
+ eventTriggers.push("discouraged_selected");
384
+ }
385
+ let feasibility;
386
+ try {
387
+ feasibility = evaluateSelectedSet(source, selectedTaskIds);
388
+ }
389
+ catch (error) {
390
+ return failure("PTOVR-101", `source resource snapshot cannot be validated: ${error instanceof Error ? error.message : String(error)}`);
391
+ }
392
+ if (!feasibility.feasible || feasibility.result === null) {
393
+ return failure("PTOVR-104", "selected task set is not feasible with active allocation and applied capacity", { resource_ids: feasibility.violatedResources });
394
+ }
395
+ if (eventTriggers.length === 0) {
396
+ return failure("PTOVR-106", "selected task set is within normal recommendation authority");
397
+ }
398
+ const human = validHumanFields(request);
399
+ if (human === null) {
400
+ return failure("PTOVR-105", "actor, decision time, reason, or evidence is invalid");
401
+ }
402
+ let acknowledgement;
403
+ try {
404
+ acknowledgement = acknowledgementError(source, request, selectedTaskIds);
405
+ }
406
+ catch (error) {
407
+ return failure("PTOVR-101", `source negative fact trace cannot be validated: ${error instanceof Error ? error.message : String(error)}`);
408
+ }
409
+ if (acknowledgement !== null) {
410
+ return failure("PTOVR-105", acknowledgement);
411
+ }
412
+ const payload = {
413
+ overrideContractVersion: 1,
414
+ source: {
415
+ schemaVersion: sourceSchemaVersion,
416
+ toolVersion: TOOL_VERSION,
417
+ sourceDigest: recommendation.sourceDigest,
418
+ recommendationInterfaceVersion: 1,
419
+ rankingAlgorithmId: recommendation.algorithm.id,
420
+ rankingAlgorithmVersion: recommendation.algorithm.version,
421
+ reasonTaxonomyVersion: recommendation.reasonTaxonomyVersion,
422
+ explanationModelVersion: recommendation.explanationModelVersion,
423
+ expressionVersion: recommendation.expressionVersion,
424
+ descriptionRegistryVersion: recommendation.descriptionRegistryVersion,
425
+ resultDecisionId: recommendation.resultDecision.id,
426
+ recommendedTaskIds: recommendation.recommendedTaskIds,
427
+ capacityOverrides: [...source.capacityOverrides]
428
+ .sort(([left], [right]) => compareStableStrings(left, right))
429
+ .map(([resourceId, capacity]) => ({ resourceId, capacity })),
430
+ },
431
+ actor: {
432
+ kind: "human",
433
+ id: request.actor.id,
434
+ authentication: "caller_asserted",
435
+ },
436
+ decidedAt: request.decidedAt,
437
+ reason: {
438
+ code: request.reasonCode,
439
+ text: request.reasonText,
440
+ evidenceReferences: human.evidence,
441
+ },
442
+ selection: {
443
+ selectedTaskIds,
444
+ retainedRecommendedTaskIds,
445
+ displacedRecommendedTaskIds,
446
+ selectedNonrecommendedTaskIds,
447
+ triggerCodes: eventTriggers,
448
+ },
449
+ taskDecisions: taskDecisionReferences(source, selectedTaskIds, displacesRecommended),
450
+ feasibility: feasibility.result,
451
+ singleUse: true,
452
+ };
453
+ const overrideId = `override:sha256:${createHash("sha256")
454
+ .update(JSON.stringify(overrideDecisionPayloadJson(payload)), "utf8")
455
+ .digest("hex")}`;
456
+ const override = {
457
+ overrideContractVersion: 1,
458
+ overrideId,
459
+ source: payload.source,
460
+ actor: payload.actor,
461
+ decidedAt: payload.decidedAt,
462
+ reason: payload.reason,
463
+ selection: payload.selection,
464
+ taskDecisions: payload.taskDecisions,
465
+ feasibility: payload.feasibility,
466
+ singleUse: true,
467
+ };
468
+ return {
469
+ schemaVersion: overrideSchemaVersion,
470
+ toolVersion: TOOL_VERSION,
471
+ operation,
472
+ ok: true,
473
+ diagnostics: [],
474
+ diagnosticsTruncated: false,
475
+ override,
476
+ };
477
+ }
478
+ function entityJson(value) {
479
+ return { kind: value.kind, id: value.id };
480
+ }
481
+ function scalarValueJson(value) {
482
+ switch (value.type) {
483
+ case "boolean":
484
+ case "integer":
485
+ return { type: value.type, value: value.value };
486
+ case "rational":
487
+ return {
488
+ type: value.type,
489
+ numerator: value.numerator,
490
+ denominator: value.denominator,
491
+ };
492
+ case "enum":
493
+ return {
494
+ type: value.type,
495
+ enum_type: value.enumType,
496
+ value: value.value,
497
+ };
498
+ case "entity":
499
+ return { type: value.type, value: entityJson(value.value) };
500
+ }
501
+ }
502
+ function valueJson(value) {
503
+ switch (value.type) {
504
+ case "boolean":
505
+ case "integer":
506
+ case "rational":
507
+ case "enum":
508
+ case "entity":
509
+ return scalarValueJson(value);
510
+ case "list":
511
+ case "set":
512
+ return {
513
+ type: value.type,
514
+ item_type: value.itemType,
515
+ items: value.items.map(scalarValueJson),
516
+ };
517
+ case "map":
518
+ return {
519
+ type: value.type,
520
+ key_type: value.keyType,
521
+ value_type: value.valueType,
522
+ entries: value.entries.map(({ key, value: entryValue }) => ({
523
+ key: scalarValueJson(key),
524
+ value: scalarValueJson(entryValue),
525
+ })),
526
+ };
527
+ }
528
+ }
529
+ function unitJson(unit) {
530
+ if (unit === null)
531
+ return null;
532
+ switch (unit.kind) {
533
+ case "duration":
534
+ return { kind: unit.kind, value: unit.value };
535
+ case "resource":
536
+ return { kind: unit.kind, resource: entityJson(unit.resource) };
537
+ case "ratio":
538
+ return { kind: unit.kind };
539
+ }
540
+ }
541
+ function expressionTermJson(term) {
542
+ return term.kind === "fact"
543
+ ? { kind: term.kind, fact_id: term.factId }
544
+ : {
545
+ kind: term.kind,
546
+ value: valueJson(term.value),
547
+ unit: unitJson(term.unit),
548
+ };
549
+ }
550
+ function expressionJson(expression) {
551
+ return expression.kind === "compare"
552
+ ? {
553
+ kind: expression.kind,
554
+ left: expressionTermJson(expression.left),
555
+ relation: expression.relation,
556
+ right: expressionTermJson(expression.right),
557
+ }
558
+ : {
559
+ kind: expression.kind,
560
+ children: expression.children.map(expressionJson),
561
+ };
562
+ }
563
+ function sourceJson(source) {
564
+ return {
565
+ schema_version: source.schemaVersion,
566
+ tool_version: source.toolVersion,
567
+ source_digest: source.sourceDigest,
568
+ recommendation_interface_version: source.recommendationInterfaceVersion,
569
+ ranking_algorithm_id: source.rankingAlgorithmId,
570
+ ranking_algorithm_version: source.rankingAlgorithmVersion,
571
+ reason_taxonomy_version: source.reasonTaxonomyVersion,
572
+ explanation_model_version: source.explanationModelVersion,
573
+ expression_version: source.expressionVersion,
574
+ description_registry_version: source.descriptionRegistryVersion,
575
+ result_decision_id: source.resultDecisionId,
576
+ recommended_task_ids: source.recommendedTaskIds,
577
+ capacity_overrides: source.capacityOverrides.map(({ resourceId, capacity }) => ({
578
+ resource_id: resourceId,
579
+ capacity,
580
+ })),
581
+ };
582
+ }
583
+ function actorJson(actor) {
584
+ return {
585
+ kind: actor.kind,
586
+ id: actor.id,
587
+ authentication: actor.authentication,
588
+ };
589
+ }
590
+ function reasonJson(reason) {
591
+ return {
592
+ code: reason.code,
593
+ text: reason.text,
594
+ evidence_references: reason.evidenceReferences.map(({ kind, value }) => ({
595
+ kind,
596
+ value,
597
+ })),
598
+ };
599
+ }
600
+ function selectionJson(selection) {
601
+ return {
602
+ selected_task_ids: selection.selectedTaskIds,
603
+ retained_recommended_task_ids: selection.retainedRecommendedTaskIds,
604
+ displaced_recommended_task_ids: selection.displacedRecommendedTaskIds,
605
+ selected_nonrecommended_task_ids: selection.selectedNonrecommendedTaskIds,
606
+ trigger_codes: selection.triggerCodes,
607
+ };
608
+ }
609
+ function taskDecisionJson(decision) {
610
+ return {
611
+ task_id: decision.taskId,
612
+ normal_decision_id: decision.normalDecisionId,
613
+ normal_tier: decision.normalTier,
614
+ normal_decisive_step_id: decision.normalDecisiveStepId,
615
+ normal_reason_occurrence_ids: decision.normalReasonOccurrenceIds,
616
+ normal_comparison_ids: decision.normalComparisonIds,
617
+ override_selected: decision.overrideSelected,
618
+ trigger_codes: decision.triggerCodes,
619
+ acknowledged_negative_fact_reason_ids: decision.acknowledgedNegativeFactReasonIds,
620
+ };
621
+ }
622
+ function feasibilityJson(feasibility) {
623
+ return {
624
+ selected_set_reference: {
625
+ kind: feasibility.selectedSetReference.kind,
626
+ id: feasibility.selectedSetReference.id,
627
+ },
628
+ start_feasible: feasibility.startFeasible,
629
+ active_task_ids: feasibility.activeTaskIds,
630
+ resource_witnesses: feasibility.resourceWitnesses.map((witness) => ({
631
+ resource_id: witness.resourceId,
632
+ capacity: witness.capacity,
633
+ active_usage: witness.activeUsage,
634
+ selected_usage: witness.selectedUsage,
635
+ used: witness.used,
636
+ available_after_selection: witness.availableAfterSelection,
637
+ selected_task_ids: witness.selectedTaskIds,
638
+ })),
639
+ expression: feasibility.expression === null
640
+ ? null
641
+ : expressionJson(feasibility.expression),
642
+ };
643
+ }
644
+ function overrideDecisionPayloadJson(decision) {
645
+ return {
646
+ override_contract_version: decision.overrideContractVersion,
647
+ source: sourceJson(decision.source),
648
+ actor: actorJson(decision.actor),
649
+ decided_at: decision.decidedAt,
650
+ reason: reasonJson(decision.reason),
651
+ selection: selectionJson(decision.selection),
652
+ task_decisions: decision.taskDecisions.map(taskDecisionJson),
653
+ feasibility: feasibilityJson(decision.feasibility),
654
+ single_use: decision.singleUse,
655
+ };
656
+ }
657
+ export function humanOverrideDecisionToJson(decision) {
658
+ return {
659
+ override_contract_version: decision.overrideContractVersion,
660
+ override_id: decision.overrideId,
661
+ source: sourceJson(decision.source),
662
+ actor: actorJson(decision.actor),
663
+ decided_at: decision.decidedAt,
664
+ reason: reasonJson(decision.reason),
665
+ selection: selectionJson(decision.selection),
666
+ task_decisions: decision.taskDecisions.map(taskDecisionJson),
667
+ feasibility: feasibilityJson(decision.feasibility),
668
+ single_use: decision.singleUse,
669
+ };
670
+ }
671
+ function jsonPosition(position) {
672
+ return {
673
+ offset: position.offset,
674
+ line: position.line + 1,
675
+ column: position.column + 1,
676
+ };
677
+ }
678
+ function jsonSpan(span) {
679
+ return {
680
+ start: jsonPosition(span.start),
681
+ end: jsonPosition(span.end),
682
+ };
683
+ }
684
+ function diagnosticJson(diagnostic) {
685
+ return {
686
+ code: diagnostic.code,
687
+ severity: diagnostic.severity,
688
+ message: diagnostic.message,
689
+ entity_id: diagnostic.entityId ?? null,
690
+ span: diagnostic.span === undefined ? null : jsonSpan(diagnostic.span),
691
+ related: (diagnostic.related ?? []).map((related) => ({
692
+ message: related.message,
693
+ span: jsonSpan(related.span),
694
+ })),
695
+ help_topic: diagnostic.helpTopic ?? null,
696
+ expected_syntax: diagnostic.expectedSyntax ?? null,
697
+ fixes: [],
698
+ data: diagnostic.data ?? {},
699
+ };
700
+ }
701
+ export function overrideValidationResultToJson(result) {
702
+ return {
703
+ schema_version: result.schemaVersion,
704
+ tool_version: result.toolVersion,
705
+ operation: result.operation,
706
+ ok: result.ok,
707
+ diagnostics: result.diagnostics.map(diagnosticJson),
708
+ diagnostics_truncated: result.diagnosticsTruncated,
709
+ override: result.override === null
710
+ ? null
711
+ : humanOverrideDecisionToJson(result.override),
712
+ };
713
+ }
714
+ export function canonicalOverrideArtifact(result) {
715
+ return JSON.stringify(overrideValidationResultToJson(result));
716
+ }
717
+ //# sourceMappingURL=override.js.map