perttool 0.7.1 → 0.8.0

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 (229) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/README.md +94 -18
  3. package/dist/actuals/lifecycle.d.ts +6 -20
  4. package/dist/actuals/lifecycle.d.ts.map +1 -1
  5. package/dist/actuals/lifecycle.js +5 -176
  6. package/dist/actuals/lifecycle.js.map +1 -1
  7. package/dist/actuals/reduction.d.ts +20 -0
  8. package/dist/actuals/reduction.d.ts.map +1 -0
  9. package/dist/actuals/reduction.js +175 -0
  10. package/dist/actuals/reduction.js.map +1 -0
  11. package/dist/analysis/service.d.ts +33 -0
  12. package/dist/analysis/service.d.ts.map +1 -0
  13. package/dist/analysis/service.js +153 -0
  14. package/dist/analysis/service.js.map +1 -0
  15. package/dist/analysis/target-temporal-input.d.ts +88 -0
  16. package/dist/analysis/target-temporal-input.d.ts.map +1 -0
  17. package/dist/analysis/target-temporal-input.js +249 -0
  18. package/dist/analysis/target-temporal-input.js.map +1 -0
  19. package/dist/analysis/temporal-deadline.d.ts +1 -1
  20. package/dist/analysis/temporal-deadline.d.ts.map +1 -1
  21. package/dist/analysis/temporal-precedence.d.ts +1 -1
  22. package/dist/analysis/temporal-precedence.d.ts.map +1 -1
  23. package/dist/analysis/temporal-precedence.js +1 -1
  24. package/dist/analysis/temporal-precedence.js.map +1 -1
  25. package/dist/analysis/temporal-resource.d.ts +1 -1
  26. package/dist/analysis/temporal-resource.d.ts.map +1 -1
  27. package/dist/analysis/temporal-resource.js +1 -1
  28. package/dist/analysis/temporal-resource.js.map +1 -1
  29. package/dist/application/advance-history.d.ts +2 -1
  30. package/dist/application/advance-history.d.ts.map +1 -1
  31. package/dist/application/advance-history.js +1 -1
  32. package/dist/application/advance-history.js.map +1 -1
  33. package/dist/application/analyze.d.ts +1 -32
  34. package/dist/application/analyze.d.ts.map +1 -1
  35. package/dist/application/analyze.js +1 -152
  36. package/dist/application/analyze.js.map +1 -1
  37. package/dist/application/check.d.ts +1 -42
  38. package/dist/application/check.d.ts.map +1 -1
  39. package/dist/application/check.js +1 -109
  40. package/dist/application/check.js.map +1 -1
  41. package/dist/application/cli-facade.d.ts +85 -0
  42. package/dist/application/cli-facade.d.ts.map +1 -0
  43. package/dist/application/cli-facade.js +108 -0
  44. package/dist/application/cli-facade.js.map +1 -0
  45. package/dist/application/contract6-mutation.js +2 -2
  46. package/dist/application/contract6-mutation.js.map +1 -1
  47. package/dist/application/contract7-mermaid.d.ts.map +1 -1
  48. package/dist/application/contract7-mermaid.js +3 -3
  49. package/dist/application/contract7-mermaid.js.map +1 -1
  50. package/dist/application/contract7-source.d.ts +1 -7
  51. package/dist/application/contract7-source.d.ts.map +1 -1
  52. package/dist/application/contract7-source.js +4 -12
  53. package/dist/application/contract7-source.js.map +1 -1
  54. package/dist/application/editor-dag-focus.d.ts +52 -0
  55. package/dist/application/editor-dag-focus.d.ts.map +1 -0
  56. package/dist/application/editor-dag-focus.js +132 -0
  57. package/dist/application/editor-dag-focus.js.map +1 -0
  58. package/dist/application/format.js +2 -2
  59. package/dist/application/format.js.map +1 -1
  60. package/dist/application/mutate.d.ts +1 -26
  61. package/dist/application/mutate.d.ts.map +1 -1
  62. package/dist/application/mutate.js +1 -205
  63. package/dist/application/mutate.js.map +1 -1
  64. package/dist/application/next.js +2 -2
  65. package/dist/application/next.js.map +1 -1
  66. package/dist/application/target-actuals-advance.js +2 -2
  67. package/dist/application/target-actuals-advance.js.map +1 -1
  68. package/dist/application/target-actuals-analysis.js +2 -2
  69. package/dist/application/target-actuals-analysis.js.map +1 -1
  70. package/dist/application/target-actuals-mutation.d.ts.map +1 -1
  71. package/dist/application/target-actuals-mutation.js +6 -5
  72. package/dist/application/target-actuals-mutation.js.map +1 -1
  73. package/dist/application/target-assurance-analysis.js +2 -2
  74. package/dist/application/target-assurance-analysis.js.map +1 -1
  75. package/dist/application/target-assurance-write.d.ts +2 -1
  76. package/dist/application/target-assurance-write.d.ts.map +1 -1
  77. package/dist/application/target-assurance-write.js +36 -12
  78. package/dist/application/target-assurance-write.js.map +1 -1
  79. package/dist/application/target-governance-mutation.js +2 -2
  80. package/dist/application/target-governance-mutation.js.map +1 -1
  81. package/dist/application/target-historical-graph.d.ts +124 -0
  82. package/dist/application/target-historical-graph.d.ts.map +1 -0
  83. package/dist/application/target-historical-graph.js +547 -0
  84. package/dist/application/target-historical-graph.js.map +1 -0
  85. package/dist/application/target-mutate.d.ts +1 -17
  86. package/dist/application/target-mutate.d.ts.map +1 -1
  87. package/dist/application/target-mutate.js +1 -159
  88. package/dist/application/target-mutate.js.map +1 -1
  89. package/dist/application/target-project-history.d.ts +2 -1
  90. package/dist/application/target-project-history.d.ts.map +1 -1
  91. package/dist/application/target-project-history.js +3 -3
  92. package/dist/application/target-project-history.js.map +1 -1
  93. package/dist/application/target-temporal-analysis.js +2 -2
  94. package/dist/application/target-temporal-analysis.js.map +1 -1
  95. package/dist/application/target-temporal-input.d.ts +1 -87
  96. package/dist/application/target-temporal-input.d.ts.map +1 -1
  97. package/dist/application/target-temporal-input.js +1 -248
  98. package/dist/application/target-temporal-input.js.map +1 -1
  99. package/dist/application/target-unit-migration-candidate.js +2 -2
  100. package/dist/application/target-unit-migration-candidate.js.map +1 -1
  101. package/dist/assurance/advance.js +2 -2
  102. package/dist/assurance/advance.js.map +1 -1
  103. package/dist/assurance/canonical.js +2 -2
  104. package/dist/assurance/canonical.js.map +1 -1
  105. package/dist/assurance/compatibility.js +2 -2
  106. package/dist/assurance/compatibility.js.map +1 -1
  107. package/dist/assurance/mermaid.d.ts +4 -2
  108. package/dist/assurance/mermaid.d.ts.map +1 -1
  109. package/dist/assurance/mermaid.js +11 -7
  110. package/dist/assurance/mermaid.js.map +1 -1
  111. package/dist/assurance/mutation.d.ts.map +1 -1
  112. package/dist/assurance/mutation.js +9 -23
  113. package/dist/assurance/mutation.js.map +1 -1
  114. package/dist/cli.js +66 -26
  115. package/dist/cli.js.map +1 -1
  116. package/dist/command/assurance-discovery.d.ts.map +1 -1
  117. package/dist/command/assurance-discovery.js +62 -2
  118. package/dist/command/assurance-discovery.js.map +1 -1
  119. package/dist/command/assurance-usage.d.ts.map +1 -1
  120. package/dist/command/assurance-usage.js +11 -0
  121. package/dist/command/assurance-usage.js.map +1 -1
  122. package/dist/conversion/mermaid-import.js +3 -3
  123. package/dist/conversion/mermaid-import.js.map +1 -1
  124. package/dist/conversion/mermaid.d.ts +1 -1
  125. package/dist/conversion/mermaid.d.ts.map +1 -1
  126. package/dist/conversion/mermaid.js +4 -4
  127. package/dist/conversion/mermaid.js.map +1 -1
  128. package/dist/core/index.d.ts +33 -0
  129. package/dist/core/index.d.ts.map +1 -0
  130. package/dist/core/index.js +13 -0
  131. package/dist/core/index.js.map +1 -0
  132. package/dist/core/source.d.ts +8 -0
  133. package/dist/core/source.d.ts.map +1 -0
  134. package/dist/core/source.js +11 -0
  135. package/dist/core/source.js.map +1 -0
  136. package/dist/formatter/source-formatter.d.ts +1 -1
  137. package/dist/formatter/source-formatter.d.ts.map +1 -1
  138. package/dist/formatter/source-formatter.js +1 -1
  139. package/dist/formatter/source-formatter.js.map +1 -1
  140. package/dist/guidance/validator.js +2 -2
  141. package/dist/guidance/validator.js.map +1 -1
  142. package/dist/help/assurance-guide.d.ts.map +1 -1
  143. package/dist/help/assurance-guide.js +78 -0
  144. package/dist/help/assurance-guide.js.map +1 -1
  145. package/dist/history/git-probe.d.ts +71 -0
  146. package/dist/history/git-probe.d.ts.map +1 -1
  147. package/dist/history/git-probe.js +517 -0
  148. package/dist/history/git-probe.js.map +1 -1
  149. package/dist/history/historical-graph.d.ts +174 -0
  150. package/dist/history/historical-graph.d.ts.map +1 -0
  151. package/dist/history/historical-graph.js +870 -0
  152. package/dist/history/historical-graph.js.map +1 -0
  153. package/dist/history/historical-transition.d.ts +311 -0
  154. package/dist/history/historical-transition.d.ts.map +1 -0
  155. package/dist/history/historical-transition.js +998 -0
  156. package/dist/history/historical-transition.js.map +1 -0
  157. package/dist/history/project-history.d.ts.map +1 -1
  158. package/dist/history/project-history.js +2 -2
  159. package/dist/history/project-history.js.map +1 -1
  160. package/dist/index.d.ts +2 -0
  161. package/dist/index.d.ts.map +1 -1
  162. package/dist/index.js +1 -0
  163. package/dist/index.js.map +1 -1
  164. package/dist/io/document-file.js +2 -2
  165. package/dist/io/document-file.js.map +1 -1
  166. package/dist/io/safe-write.js +1 -1
  167. package/dist/io/safe-write.js.map +1 -1
  168. package/dist/model/sha256.d.ts +5 -0
  169. package/dist/model/sha256.d.ts.map +1 -0
  170. package/dist/model/sha256.js +105 -0
  171. package/dist/model/sha256.js.map +1 -0
  172. package/dist/mutation/advance.js +3 -3
  173. package/dist/mutation/advance.js.map +1 -1
  174. package/dist/mutation/diagnostics.d.ts +2 -2
  175. package/dist/mutation/diagnostics.d.ts.map +1 -1
  176. package/dist/mutation/diagnostics.js.map +1 -1
  177. package/dist/mutation/planner.d.ts +27 -0
  178. package/dist/mutation/planner.d.ts.map +1 -0
  179. package/dist/mutation/planner.js +206 -0
  180. package/dist/mutation/planner.js.map +1 -0
  181. package/dist/mutation/source.d.ts +2 -1
  182. package/dist/mutation/source.d.ts.map +1 -1
  183. package/dist/mutation/source.js +14 -0
  184. package/dist/mutation/source.js.map +1 -1
  185. package/dist/mutation/target-planner.d.ts +18 -0
  186. package/dist/mutation/target-planner.d.ts.map +1 -0
  187. package/dist/mutation/target-planner.js +160 -0
  188. package/dist/mutation/target-planner.js.map +1 -0
  189. package/dist/mutation/task.d.ts.map +1 -1
  190. package/dist/mutation/task.js +19 -11
  191. package/dist/mutation/task.js.map +1 -1
  192. package/dist/mutation/types.d.ts +1 -1
  193. package/dist/mutation/types.d.ts.map +1 -1
  194. package/dist/node/historical-host.d.ts +6 -0
  195. package/dist/node/historical-host.d.ts.map +1 -0
  196. package/dist/node/historical-host.js +8 -0
  197. package/dist/node/historical-host.js.map +1 -0
  198. package/dist/node/host.d.ts +3 -0
  199. package/dist/node/host.d.ts.map +1 -0
  200. package/dist/node/host.js +48 -0
  201. package/dist/node/host.js.map +1 -0
  202. package/dist/node/index.d.ts +2 -0
  203. package/dist/node/index.d.ts.map +1 -0
  204. package/dist/node/index.js +2 -0
  205. package/dist/node/index.js.map +1 -0
  206. package/dist/perttool-vscode-private-0.0.0-d1578b9.vsix +0 -0
  207. package/dist/ports/node-host.d.ts +41 -0
  208. package/dist/ports/node-host.d.ts.map +1 -0
  209. package/dist/ports/node-host.js +2 -0
  210. package/dist/ports/node-host.js.map +1 -0
  211. package/dist/recommendation/override.d.ts +40 -2
  212. package/dist/recommendation/override.d.ts.map +1 -1
  213. package/dist/recommendation/override.js +5 -6
  214. package/dist/recommendation/override.js.map +1 -1
  215. package/dist/schema/registry.d.ts.map +1 -1
  216. package/dist/schema/registry.js +1 -0
  217. package/dist/schema/registry.js.map +1 -1
  218. package/dist/semantic/check.d.ts +43 -0
  219. package/dist/semantic/check.d.ts.map +1 -0
  220. package/dist/semantic/check.js +110 -0
  221. package/dist/semantic/check.js.map +1 -0
  222. package/dist/session/document-session.d.ts +109 -0
  223. package/dist/session/document-session.d.ts.map +1 -0
  224. package/dist/session/document-session.js +432 -0
  225. package/dist/session/document-session.js.map +1 -0
  226. package/dist/version.d.ts +1 -1
  227. package/dist/version.js +1 -1
  228. package/package.json +18 -4
  229. package/schemas/Perttool.HistoricalGraphResult.v1.schema.json +654 -0
@@ -0,0 +1,998 @@
1
+ import { canonicalJson, sha256Canonical, } from "../assurance/canonical.js";
2
+ import { governanceMetadataFromDocument } from "../governance/source.js";
3
+ import { parseDeclaredCalendarValue, } from "../model/calendar.js";
4
+ import { rational, rationalFromDuration, } from "../model/rational.js";
5
+ import { sha256DigestUtf8 } from "../model/sha256.js";
6
+ import { fieldNamed } from "../model/syntax.js";
7
+ export const HISTORICAL_TRANSITION_MODEL_VERSION = 1;
8
+ export const HISTORICAL_TRANSITION_MODEL_ID = "Perttool.HistoricalTransitionModel.v1";
9
+ const entityKindOrder = {
10
+ milestone: 0,
11
+ task: 1,
12
+ gate: 2,
13
+ resource: 3,
14
+ };
15
+ const trustworthyTransitionClasses = new Set([
16
+ "representation_only",
17
+ "evidence_extension",
18
+ "lifecycle_projection",
19
+ "future_plan_edit",
20
+ "canonical_advance",
21
+ ]);
22
+ const fullObjectIdPattern = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
23
+ function compareUnicodeScalars(left, right) {
24
+ const leftScalars = Array.from(left, (value) => value.codePointAt(0));
25
+ const rightScalars = Array.from(right, (value) => value.codePointAt(0));
26
+ const common = Math.min(leftScalars.length, rightScalars.length);
27
+ for (let index = 0; index < common; index += 1) {
28
+ const difference = leftScalars[index] - rightScalars[index];
29
+ if (difference !== 0)
30
+ return difference;
31
+ }
32
+ return leftScalars.length - rightScalars.length;
33
+ }
34
+ function compareEntity(left, right) {
35
+ return entityKindOrder[left.kind] - entityKindOrder[right.kind] ||
36
+ compareUnicodeScalars(left.id, right.id);
37
+ }
38
+ function deepFreeze(value) {
39
+ if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
40
+ for (const child of Object.values(value)) {
41
+ deepFreeze(child);
42
+ }
43
+ Object.freeze(value);
44
+ }
45
+ return value;
46
+ }
47
+ function requiredField(declaration, name) {
48
+ const field = fieldNamed(declaration, name);
49
+ if (field === undefined) {
50
+ throw new Error(`validated ${declaration.kind} ${declaration.id} is missing ${name}`);
51
+ }
52
+ return field;
53
+ }
54
+ function requiredString(declaration, name) {
55
+ const value = requiredField(declaration, name).value;
56
+ if (typeof value !== "string") {
57
+ throw new Error(`validated ${declaration.kind} ${declaration.id}.${name} is not a string`);
58
+ }
59
+ return value;
60
+ }
61
+ function optionalString(declaration, name) {
62
+ const value = fieldNamed(declaration, name)?.value;
63
+ if (value === undefined)
64
+ return null;
65
+ if (typeof value !== "string") {
66
+ throw new Error(`validated ${declaration.kind} ${declaration.id}.${name} is not a string`);
67
+ }
68
+ return value;
69
+ }
70
+ function durationValue(field) {
71
+ const value = field.value;
72
+ if (value === null ||
73
+ typeof value !== "object" ||
74
+ !("suffix" in value) ||
75
+ (!("digits" in value) && !("numerator" in value))) {
76
+ throw new Error(`validated ${field.name} is not an exact duration`);
77
+ }
78
+ return value;
79
+ }
80
+ function personHoursValue(field) {
81
+ const value = field.value;
82
+ if (value === null ||
83
+ typeof value !== "object" ||
84
+ !("suffix" in value) ||
85
+ value.suffix !== "ph" ||
86
+ (!("digits" in value) && !("numerator" in value))) {
87
+ throw new Error(`validated ${field.name} is not exact person-hours`);
88
+ }
89
+ return value;
90
+ }
91
+ function exactRecord(value, unit) {
92
+ return {
93
+ numerator: value.numerator.toString(),
94
+ denominator: value.denominator.toString(),
95
+ unit,
96
+ };
97
+ }
98
+ function durationUnit(suffix) {
99
+ return suffix === "d" ? "day" : suffix === "h" ? "hour" : "point";
100
+ }
101
+ function exactDuration(field) {
102
+ const value = durationValue(field);
103
+ return exactRecord(rationalFromDuration(value), durationUnit(value.suffix));
104
+ }
105
+ function exactPersonHours(field) {
106
+ const value = personHoursValue(field);
107
+ const exact = "numerator" in value
108
+ ? rational(value.numerator, value.denominator)
109
+ : rational(value.digits, 10n ** BigInt(value.scale));
110
+ return exactRecord(exact, "person_hour");
111
+ }
112
+ function calendarValue(field) {
113
+ if (field === undefined)
114
+ return null;
115
+ const value = typeof field.value === "string"
116
+ ? parseDeclaredCalendarValue(field.value)
117
+ : field.value;
118
+ if (value === undefined) {
119
+ throw new Error(`validated ${field.name} is not a calendar value`);
120
+ }
121
+ if (value.kind === "date") {
122
+ return {
123
+ kind: "date",
124
+ year: value.year,
125
+ month: value.month,
126
+ day: value.day,
127
+ };
128
+ }
129
+ if (value.kind !== "date_time") {
130
+ throw new Error(`validated ${field.name} is not a calendar value`);
131
+ }
132
+ return {
133
+ kind: "date_time",
134
+ year: value.year,
135
+ month: value.month,
136
+ day: value.day,
137
+ hour: value.hour,
138
+ minute: value.minute,
139
+ second: {
140
+ numerator: value.second.numerator.toString(),
141
+ denominator: value.second.denominator.toString(),
142
+ },
143
+ offset_minutes: value.offsetMinutes,
144
+ };
145
+ }
146
+ function stringList(declaration, name) {
147
+ const value = fieldNamed(declaration, name)?.value ?? [];
148
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
149
+ throw new Error(`validated ${declaration.kind}.${name} is not a string list`);
150
+ }
151
+ return [...value].sort(compareUnicodeScalars);
152
+ }
153
+ function timing(task) {
154
+ const duration = fieldNamed(task, "duration");
155
+ if (duration !== undefined) {
156
+ return { kind: "duration", value: exactDuration(duration) };
157
+ }
158
+ const estimate = requiredField(task, "estimate");
159
+ const component = (name) => {
160
+ const child = estimate.children?.find((candidate) => candidate.name === name);
161
+ if (child === undefined) {
162
+ throw new Error(`validated task ${task.id} estimate is missing ${name}`);
163
+ }
164
+ return child;
165
+ };
166
+ return {
167
+ kind: "estimate",
168
+ optimistic: exactDuration(component("optimistic")),
169
+ most_likely: exactDuration(component("most_likely")),
170
+ pessimistic: exactDuration(component("pessimistic")),
171
+ };
172
+ }
173
+ function projectSemantic(validated, project) {
174
+ const unit = requiredString(project, "duration_unit");
175
+ if (unit !== "day" && unit !== "hour" && unit !== "point") {
176
+ throw new Error("validated project has an invalid duration unit");
177
+ }
178
+ const velocityField = fieldNamed(project, "velocity");
179
+ const velocity = velocityField?.value;
180
+ const metadata = governanceMetadataFromDocument(validated.document);
181
+ const critical = fieldNamed(project, "critical_epsilon");
182
+ const assuranceModel = fieldNamed(project, "plan_assurance_model")?.value;
183
+ const assuranceHashModel = fieldNamed(project, "plan_assurance_hash_model")?.value;
184
+ return {
185
+ id: project.id,
186
+ grammar_version: validated.grammarVersion,
187
+ title: requiredString(project, "title"),
188
+ description: optionalString(project, "description"),
189
+ as_of: calendarValue(fieldNamed(project, "as_of")),
190
+ duration_unit: unit,
191
+ velocity: velocity === undefined
192
+ ? null
193
+ : {
194
+ points: exactRecord(rationalFromDuration(velocity.points), "point"),
195
+ period: exactRecord(rationalFromDuration(velocity.period), durationUnit(velocity.period.suffix)),
196
+ },
197
+ finish_milestone_id: requiredString(project, "finish"),
198
+ critical_epsilon: critical === undefined
199
+ ? exactRecord(rational(0n), unit)
200
+ : exactDuration(critical),
201
+ target_duration: fieldNamed(project, "target_duration") === undefined
202
+ ? null
203
+ : exactDuration(fieldNamed(project, "target_duration")),
204
+ governance: {
205
+ declared: {
206
+ goal_owner: metadata.declared.goalOwner,
207
+ goal_delegates: metadata.declared.goalDelegates === null
208
+ ? null
209
+ : [...metadata.declared.goalDelegates].sort(compareUnicodeScalars),
210
+ dag_owner: metadata.declared.dagOwner,
211
+ dag_delegates: metadata.declared.dagDelegates === null
212
+ ? null
213
+ : [...metadata.declared.dagDelegates].sort(compareUnicodeScalars),
214
+ },
215
+ effective: {
216
+ goal_owner: metadata.effective.goalOwner,
217
+ goal_delegates: [...metadata.effective.goalDelegates]
218
+ .sort(compareUnicodeScalars),
219
+ dag_owner: metadata.effective.dagOwner,
220
+ dag_delegates: [...metadata.effective.dagDelegates]
221
+ .sort(compareUnicodeScalars),
222
+ },
223
+ },
224
+ plan_assurance: {
225
+ model_version: typeof assuranceModel === "number" ? assuranceModel : null,
226
+ hash_model_version: typeof assuranceHashModel === "number"
227
+ ? assuranceHashModel
228
+ : null,
229
+ },
230
+ };
231
+ }
232
+ function resourceSemantic(declaration) {
233
+ const capacity = requiredField(declaration, "capacity").value;
234
+ if (typeof capacity !== "number") {
235
+ throw new Error(`validated resource ${declaration.id} has invalid capacity`);
236
+ }
237
+ return {
238
+ id: declaration.id,
239
+ title: requiredString(declaration, "title"),
240
+ description: optionalString(declaration, "description"),
241
+ capacity,
242
+ tags: stringList(declaration, "tags"),
243
+ };
244
+ }
245
+ function milestoneSemantic(declaration) {
246
+ const state = fieldNamed(declaration, "state")?.value ?? "planned";
247
+ if (state !== "planned" && state !== "reached") {
248
+ throw new Error(`validated milestone ${declaration.id} has invalid state`);
249
+ }
250
+ return {
251
+ id: declaration.id,
252
+ title: requiredString(declaration, "title"),
253
+ description: optionalString(declaration, "description"),
254
+ state,
255
+ deadline: calendarValue(fieldNamed(declaration, "deadline")),
256
+ tags: stringList(declaration, "tags"),
257
+ };
258
+ }
259
+ function taskSemantic(declaration) {
260
+ const status = fieldNamed(declaration, "status")?.value ?? "planned";
261
+ if (status !== "planned" &&
262
+ status !== "active" &&
263
+ status !== "blocked" &&
264
+ status !== "suspended" &&
265
+ status !== "done") {
266
+ throw new Error(`validated task ${declaration.id} has invalid status`);
267
+ }
268
+ const priority = fieldNamed(declaration, "priority")?.value ?? 0;
269
+ const requirements = fieldNamed(declaration, "requires")?.value ?? [];
270
+ if (typeof priority !== "number" || !Array.isArray(requirements)) {
271
+ throw new Error(`validated task ${declaration.id} has invalid plan values`);
272
+ }
273
+ return {
274
+ id: declaration.id,
275
+ from_milestone_id: declaration.from,
276
+ to_milestone_id: declaration.to,
277
+ plan: {
278
+ title: requiredString(declaration, "title"),
279
+ description: optionalString(declaration, "description"),
280
+ timing: timing(declaration),
281
+ not_before: calendarValue(fieldNamed(declaration, "not_before")),
282
+ deadline: calendarValue(fieldNamed(declaration, "deadline")),
283
+ priority,
284
+ requirements: requirements
285
+ .map(({ resourceId, units }) => ({ resource_id: resourceId, units }))
286
+ .sort((left, right) => compareUnicodeScalars(left.resource_id, right.resource_id)),
287
+ owner: optionalString(declaration, "owner"),
288
+ tags: stringList(declaration, "tags"),
289
+ source: optionalString(declaration, "source"),
290
+ },
291
+ lifecycle: {
292
+ status,
293
+ blocked_reason: optionalString(declaration, "blocked_reason"),
294
+ },
295
+ };
296
+ }
297
+ function gateSemantic(declaration) {
298
+ return {
299
+ id: declaration.id,
300
+ from_milestone_id: declaration.from,
301
+ to_milestone_id: declaration.to,
302
+ reason: requiredString(declaration, "reason"),
303
+ };
304
+ }
305
+ function workEventSemantic(declaration) {
306
+ const model = requiredField(declaration, "model").value;
307
+ const kind = requiredField(declaration, "kind").value;
308
+ if (model !== 1 ||
309
+ (kind !== "start" &&
310
+ kind !== "suspend" &&
311
+ kind !== "resume" &&
312
+ kind !== "finish")) {
313
+ throw new Error(`validated work event ${declaration.id} is invalid`);
314
+ }
315
+ const occurredAt = calendarValue(requiredField(declaration, "occurred_at"));
316
+ if (occurredAt === null || occurredAt.kind !== "date_time") {
317
+ throw new Error(`validated work event ${declaration.id} has no date-time`);
318
+ }
319
+ return {
320
+ id: declaration.id,
321
+ model,
322
+ task_id: requiredString(declaration, "task"),
323
+ kind,
324
+ occurred_at: occurredAt,
325
+ planned_value: fieldNamed(declaration, "planned_value") === undefined
326
+ ? null
327
+ : exactDuration(fieldNamed(declaration, "planned_value")),
328
+ active_time: fieldNamed(declaration, "active_time") === undefined
329
+ ? null
330
+ : exactDuration(fieldNamed(declaration, "active_time")),
331
+ effort: fieldNamed(declaration, "effort") === undefined
332
+ ? null
333
+ : exactPersonHours(fieldNamed(declaration, "effort")),
334
+ reason: optionalString(declaration, "reason"),
335
+ };
336
+ }
337
+ function taskRelationSemantic(declaration) {
338
+ const mode = requiredField(declaration, "mode").value;
339
+ if (mode !== "both" &&
340
+ mode !== "execution_only" &&
341
+ mode !== "planning_only") {
342
+ throw new Error(`validated task relation ${declaration.id} has invalid mode`);
343
+ }
344
+ return {
345
+ id: declaration.id,
346
+ predecessor_task_id: declaration.from,
347
+ successor_task_id: declaration.to,
348
+ mode,
349
+ reason: optionalString(declaration, "reason"),
350
+ };
351
+ }
352
+ function planSealSemantic(declaration) {
353
+ const inputs = fieldNamed(declaration, "accepted_inputs")?.value ?? [];
354
+ if (!Array.isArray(inputs)) {
355
+ throw new Error(`validated plan seal ${declaration.id} has invalid inputs`);
356
+ }
357
+ return {
358
+ task_id: declaration.id,
359
+ accepted_contract: requiredString(declaration, "accepted_contract"),
360
+ accepted_basis: requiredString(declaration, "accepted_basis"),
361
+ accepted_inputs: inputs
362
+ .map((input) => ({
363
+ predecessor_task_id: input.predecessorTaskId,
364
+ relation_mode: input.relationMode,
365
+ assurance_hash: input.assuranceHash,
366
+ }))
367
+ .sort((left, right) => compareUnicodeScalars(left.predecessor_task_id, right.predecessor_task_id)),
368
+ reason: optionalString(declaration, "reason"),
369
+ };
370
+ }
371
+ function taskOutcomeSemantic(declaration) {
372
+ const model = requiredField(declaration, "model").value;
373
+ const status = requiredField(declaration, "status").value;
374
+ if (model !== 1 || (status !== "conformant" && status !== "changed")) {
375
+ throw new Error(`validated task outcome ${declaration.id} is invalid`);
376
+ }
377
+ return {
378
+ id: declaration.id,
379
+ model,
380
+ task_id: requiredString(declaration, "task"),
381
+ against_basis: requiredString(declaration, "against_basis"),
382
+ status,
383
+ summary: optionalString(declaration, "summary"),
384
+ reason: requiredString(declaration, "reason"),
385
+ };
386
+ }
387
+ function assuranceReceiptSemantic(declaration) {
388
+ const model = requiredField(declaration, "model").value;
389
+ const outcome = requiredField(declaration, "outcome").value;
390
+ const consumers = requiredField(declaration, "consumers").value;
391
+ if (model !== 1 ||
392
+ (outcome !== "conformant" && outcome !== "changed") ||
393
+ !Array.isArray(consumers)) {
394
+ throw new Error(`validated assurance receipt ${declaration.id} is invalid`);
395
+ }
396
+ return {
397
+ id: declaration.id,
398
+ model,
399
+ receipt_hash: requiredString(declaration, "receipt_hash"),
400
+ producer_task_id: requiredString(declaration, "producer"),
401
+ producer_contract_hash: requiredString(declaration, "producer_contract_hash"),
402
+ producer_assurance_hash: requiredString(declaration, "producer_assurance_hash"),
403
+ outcome,
404
+ source_milestone_id: optionalString(declaration, "source_milestone"),
405
+ consumers: consumers
406
+ .map((consumer) => ({
407
+ consumer_task_id: consumer.consumerTaskId,
408
+ relation_mode: consumer.relationMode,
409
+ }))
410
+ .sort((left, right) => compareUnicodeScalars(left.consumer_task_id, right.consumer_task_id)),
411
+ };
412
+ }
413
+ function cloneRange(span) {
414
+ return {
415
+ start: { ...span.start },
416
+ end: { ...span.end },
417
+ };
418
+ }
419
+ function sourceFidelity(validated) {
420
+ return {
421
+ source_digest: sha256DigestUtf8(validated.document.text),
422
+ utf16_length: validated.document.text.length,
423
+ declarations: validated.document.declarations.map((declaration, ordinal) => ({
424
+ kind: declaration.kind,
425
+ id: declaration.id,
426
+ ordinal: ordinal + 1,
427
+ from_id: declaration.from ?? null,
428
+ to_id: declaration.to ?? null,
429
+ range: cloneRange(declaration.span),
430
+ id_range: cloneRange(declaration.idSpan),
431
+ fields: declaration.fields.map((field, fieldOrdinal) => ({
432
+ name: field.name,
433
+ ordinal: fieldOrdinal + 1,
434
+ raw_value: field.rawValue,
435
+ range: cloneRange(field.span),
436
+ value_range: cloneRange(field.valueSpan),
437
+ child_ranges: (field.children ?? []).map((child, childOrdinal) => ({
438
+ name: child.name,
439
+ ordinal: childOrdinal + 1,
440
+ raw_value: child.rawValue,
441
+ range: cloneRange(child.span),
442
+ value_range: cloneRange(child.valueSpan),
443
+ })),
444
+ })),
445
+ })),
446
+ };
447
+ }
448
+ function projectAxisRecords(semantic) {
449
+ return {
450
+ planning: {
451
+ model: HISTORICAL_TRANSITION_MODEL_ID,
452
+ project: {
453
+ id: semantic.project.id,
454
+ grammar_version: semantic.project.grammar_version,
455
+ title: semantic.project.title,
456
+ description: semantic.project.description,
457
+ as_of: semantic.project.as_of,
458
+ duration_unit: semantic.project.duration_unit,
459
+ velocity: semantic.project.velocity,
460
+ finish_milestone_id: semantic.project.finish_milestone_id,
461
+ critical_epsilon: semantic.project.critical_epsilon,
462
+ target_duration: semantic.project.target_duration,
463
+ },
464
+ resources: semantic.resources,
465
+ milestones: semantic.milestones.map(({ state: _state, ...milestone }) => milestone),
466
+ tasks: semantic.tasks.map(({ lifecycle: _lifecycle, ...task }) => task),
467
+ gates: semantic.gates,
468
+ task_relations: semantic.task_relations,
469
+ },
470
+ lifecycle: {
471
+ model: HISTORICAL_TRANSITION_MODEL_ID,
472
+ milestones: semantic.milestones.map(({ id, state }) => ({ id, state })),
473
+ tasks: semantic.tasks.map(({ id, lifecycle }) => ({ id, lifecycle })),
474
+ },
475
+ actual_evidence: {
476
+ model: HISTORICAL_TRANSITION_MODEL_ID,
477
+ work_events: semantic.work_events,
478
+ },
479
+ governance: {
480
+ model: HISTORICAL_TRANSITION_MODEL_ID,
481
+ governance: semantic.project.governance,
482
+ },
483
+ assurance: {
484
+ model: HISTORICAL_TRANSITION_MODEL_ID,
485
+ plan_assurance: semantic.project.plan_assurance,
486
+ task_relations: semantic.task_relations,
487
+ plan_seals: semantic.plan_seals,
488
+ task_outcomes: semantic.task_outcomes,
489
+ assurance_receipts: semantic.assurance_receipts,
490
+ },
491
+ topology: {
492
+ model: HISTORICAL_TRANSITION_MODEL_ID,
493
+ milestones: semantic.milestones.map(({ id }) => ({ id })),
494
+ tasks: semantic.tasks.map((task) => ({
495
+ id: task.id,
496
+ from_milestone_id: task.from_milestone_id,
497
+ to_milestone_id: task.to_milestone_id,
498
+ })),
499
+ gates: semantic.gates.map((gate) => ({
500
+ id: gate.id,
501
+ from_milestone_id: gate.from_milestone_id,
502
+ to_milestone_id: gate.to_milestone_id,
503
+ })),
504
+ },
505
+ };
506
+ }
507
+ export function projectHistoricalTransitionModel(validated) {
508
+ const declarations = validated.document.declarations;
509
+ const project = declarations.find(({ kind }) => kind === "project");
510
+ if (project === undefined)
511
+ throw new Error("validated document has no project");
512
+ const sorted = (values) => [...values].sort((left, right) => compareUnicodeScalars(left.id, right.id));
513
+ const semantic = {
514
+ model: HISTORICAL_TRANSITION_MODEL_ID,
515
+ model_version: HISTORICAL_TRANSITION_MODEL_VERSION,
516
+ project: projectSemantic(validated, project),
517
+ resources: sorted(declarations.filter(({ kind }) => kind === "resource").map(resourceSemantic)),
518
+ milestones: sorted(declarations.filter(({ kind }) => kind === "milestone").map(milestoneSemantic)),
519
+ tasks: sorted(declarations.filter(({ kind }) => kind === "task").map(taskSemantic)),
520
+ gates: sorted(declarations.filter(({ kind }) => kind === "gate").map(gateSemantic)),
521
+ work_events: sorted(declarations.filter(({ kind }) => kind === "work_event")
522
+ .map(workEventSemantic)),
523
+ task_relations: sorted(declarations.filter(({ kind }) => kind === "task_relation")
524
+ .map(taskRelationSemantic)),
525
+ plan_seals: [...declarations.filter(({ kind }) => kind === "plan_seal")
526
+ .map(planSealSemantic)].sort((left, right) => compareUnicodeScalars(left.task_id, right.task_id)),
527
+ task_outcomes: sorted(declarations.filter(({ kind }) => kind === "task_outcome")
528
+ .map(taskOutcomeSemantic)),
529
+ assurance_receipts: sorted(declarations.filter(({ kind }) => kind === "assurance_receipt")
530
+ .map(assuranceReceiptSemantic)),
531
+ };
532
+ const axes = projectAxisRecords(semantic);
533
+ return deepFreeze({
534
+ model_version: HISTORICAL_TRANSITION_MODEL_VERSION,
535
+ semantic_digest: sha256Canonical(semantic),
536
+ semantic,
537
+ axes: {
538
+ planning: sha256Canonical(axes.planning),
539
+ lifecycle: sha256Canonical(axes.lifecycle),
540
+ actual_evidence: sha256Canonical(axes.actual_evidence),
541
+ governance: sha256Canonical(axes.governance),
542
+ assurance: sha256Canonical(axes.assurance),
543
+ topology: sha256Canonical(axes.topology),
544
+ },
545
+ source_fidelity: sourceFidelity(validated),
546
+ });
547
+ }
548
+ export function historicalOccurrenceId(key) {
549
+ if (key.projectId.length === 0 ||
550
+ key.sourceId.length === 0 ||
551
+ !(key.entityKind in entityKindOrder) ||
552
+ !fullObjectIdPattern.test(key.introducedCommitId)) {
553
+ throw new Error("historical occurrence key is invalid");
554
+ }
555
+ const digest = sha256Canonical({
556
+ projectId: key.projectId,
557
+ entityKind: key.entityKind,
558
+ sourceId: key.sourceId,
559
+ introducedCommitId: key.introducedCommitId,
560
+ });
561
+ return `HDGE-${digest.slice("sha256:".length)}`;
562
+ }
563
+ export function historicalTopologyEpochId(entries) {
564
+ const ordered = [...entries].sort((left, right) => (left.entity_kind === right.entity_kind
565
+ ? 0
566
+ : left.entity_kind === "task"
567
+ ? -1
568
+ : 1) || compareUnicodeScalars(left.source_id, right.source_id));
569
+ const digest = sha256Canonical({
570
+ model: HISTORICAL_TRANSITION_MODEL_ID,
571
+ model_version: HISTORICAL_TRANSITION_MODEL_VERSION,
572
+ topology: ordered,
573
+ });
574
+ return `HDGT-${digest.slice("sha256:".length)}`;
575
+ }
576
+ function changedAxes(previous, current) {
577
+ const order = [
578
+ "planning",
579
+ "lifecycle",
580
+ "actual_evidence",
581
+ "governance",
582
+ "assurance",
583
+ "topology",
584
+ ];
585
+ return order.filter((axis) => previous.axes[axis] !== current.axes[axis]);
586
+ }
587
+ function recordsById(records) {
588
+ return new Map(records.map((record) => [record.id, record]));
589
+ }
590
+ function removedIds(previous, current) {
591
+ const currentIds = new Set(current.map(({ id }) => id));
592
+ return previous.map(({ id }) => id).filter((id) => !currentIds.has(id));
593
+ }
594
+ function changedStableRecordIds(previous, current) {
595
+ const currentById = recordsById(current);
596
+ return previous.flatMap((record) => {
597
+ const candidate = currentById.get(record.id);
598
+ return candidate !== undefined &&
599
+ canonicalJson(record) !== canonicalJson(candidate)
600
+ ? [record.id]
601
+ : [];
602
+ });
603
+ }
604
+ function validAdvanceCandidate(previous, current, candidate) {
605
+ return candidate !== undefined &&
606
+ candidate.planner_version.length > 0 &&
607
+ candidate.base_semantic_digest === previous.semantic_digest &&
608
+ candidate.candidate.semantic_digest === current.semantic_digest &&
609
+ candidate.complete &&
610
+ !candidate.force_requested &&
611
+ !candidate.owner_assertion_used &&
612
+ !candidate.repository_proof_assumed &&
613
+ !candidate.persistence_assumed;
614
+ }
615
+ function hasOnlyAdditions(previous, current) {
616
+ const currentById = recordsById(current);
617
+ return previous.every((record) => {
618
+ const candidate = currentById.get(record.id);
619
+ return candidate !== undefined && canonicalJson(record) === canonicalJson(candidate);
620
+ });
621
+ }
622
+ function frozenTaskIds(semantic) {
623
+ return new Set([
624
+ ...semantic.work_events.map(({ task_id }) => task_id),
625
+ ...semantic.task_outcomes.map(({ task_id }) => task_id),
626
+ ...semantic.assurance_receipts.map(({ producer_task_id }) => producer_task_id),
627
+ ]);
628
+ }
629
+ function frozenTaskPlanChanged(previous, current) {
630
+ const frozen = frozenTaskIds(previous);
631
+ const previousTasks = recordsById(previous.tasks);
632
+ const currentTasks = recordsById(current.tasks);
633
+ for (const taskId of frozen) {
634
+ const before = previousTasks.get(taskId);
635
+ const after = currentTasks.get(taskId);
636
+ if (before === undefined || after === undefined)
637
+ return true;
638
+ if (canonicalJson({
639
+ from_milestone_id: before.from_milestone_id,
640
+ to_milestone_id: before.to_milestone_id,
641
+ plan: before.plan,
642
+ }) !== canonicalJson({
643
+ from_milestone_id: after.from_milestone_id,
644
+ to_milestone_id: after.to_milestone_id,
645
+ plan: after.plan,
646
+ }))
647
+ return true;
648
+ }
649
+ return false;
650
+ }
651
+ export function classifyHistoricalTransition(previous, current, options = {}) {
652
+ const isMergeCommit = options.isMergeCommit ?? false;
653
+ if (previous === null) {
654
+ return deepFreeze({
655
+ class: "initial",
656
+ causes: [],
657
+ semantic_changed: true,
658
+ source_changed: true,
659
+ changed_axes: [
660
+ "planning",
661
+ "lifecycle",
662
+ "actual_evidence",
663
+ "governance",
664
+ "assurance",
665
+ "topology",
666
+ ],
667
+ is_merge_commit: isMergeCommit,
668
+ canonical_advance: null,
669
+ });
670
+ }
671
+ const axes = changedAxes(previous, current);
672
+ const sourceChanged = previous.source_fidelity.source_digest !==
673
+ current.source_fidelity.source_digest;
674
+ if (previous.semantic_digest === current.semantic_digest) {
675
+ return deepFreeze({
676
+ class: "representation_only",
677
+ causes: [],
678
+ semantic_changed: false,
679
+ source_changed: sourceChanged,
680
+ changed_axes: [],
681
+ is_merge_commit: isMergeCommit,
682
+ canonical_advance: null,
683
+ });
684
+ }
685
+ const previousSemantic = previous.semantic;
686
+ const currentSemantic = current.semantic;
687
+ const eventConflicts = changedStableRecordIds(previousSemantic.work_events, currentSemantic.work_events);
688
+ const immutableAssuranceConflicts = [
689
+ ...changedStableRecordIds(previousSemantic.task_outcomes, currentSemantic.task_outcomes),
690
+ ...changedStableRecordIds(previousSemantic.assurance_receipts, currentSemantic.assurance_receipts),
691
+ ];
692
+ if (previousSemantic.project.id !== currentSemantic.project.id ||
693
+ eventConflicts.length > 0 ||
694
+ immutableAssuranceConflicts.length > 0) {
695
+ return deepFreeze({
696
+ class: "conflict",
697
+ causes: eventConflicts.length > 0
698
+ ? ["event_payload_changed"]
699
+ : ["topology_conflict"],
700
+ semantic_changed: true,
701
+ source_changed: sourceChanged,
702
+ changed_axes: axes,
703
+ is_merge_commit: isMergeCommit,
704
+ canonical_advance: null,
705
+ });
706
+ }
707
+ if (validAdvanceCandidate(previous, current, options.canonicalAdvanceCandidate)) {
708
+ return deepFreeze({
709
+ class: "canonical_advance",
710
+ causes: [],
711
+ semantic_changed: true,
712
+ source_changed: sourceChanged,
713
+ changed_axes: axes,
714
+ is_merge_commit: isMergeCommit,
715
+ canonical_advance: {
716
+ planner_version: options.canonicalAdvanceCandidate.planner_version,
717
+ candidate_semantic_digest: options.canonicalAdvanceCandidate.candidate.semantic_digest,
718
+ },
719
+ });
720
+ }
721
+ const removed = [
722
+ ...removedIds(previousSemantic.resources, currentSemantic.resources),
723
+ ...removedIds(previousSemantic.milestones, currentSemantic.milestones),
724
+ ...removedIds(previousSemantic.tasks, currentSemantic.tasks),
725
+ ...removedIds(previousSemantic.gates, currentSemantic.gates),
726
+ ...removedIds(previousSemantic.work_events, currentSemantic.work_events),
727
+ ...removedIds(previousSemantic.task_relations, currentSemantic.task_relations),
728
+ ...previousSemantic.plan_seals
729
+ .map(({ task_id }) => task_id)
730
+ .filter((taskId) => !currentSemantic.plan_seals.some(({ task_id }) => task_id === taskId)),
731
+ ...removedIds(previousSemantic.task_outcomes, currentSemantic.task_outcomes),
732
+ ...removedIds(previousSemantic.assurance_receipts, currentSemantic.assurance_receipts),
733
+ ];
734
+ if (removed.length > 0) {
735
+ return deepFreeze({
736
+ class: "ambiguous_edit",
737
+ causes: ["noncanonical_removal"],
738
+ semantic_changed: true,
739
+ source_changed: sourceChanged,
740
+ changed_axes: axes,
741
+ is_merge_commit: isMergeCommit,
742
+ canonical_advance: null,
743
+ });
744
+ }
745
+ if (frozenTaskPlanChanged(previousSemantic, currentSemantic)) {
746
+ return deepFreeze({
747
+ class: "conflict",
748
+ causes: ["topology_conflict"],
749
+ semantic_changed: true,
750
+ source_changed: sourceChanged,
751
+ changed_axes: axes,
752
+ is_merge_commit: isMergeCommit,
753
+ canonical_advance: null,
754
+ });
755
+ }
756
+ const evidenceOnlyAdditions = hasOnlyAdditions(previousSemantic.work_events, currentSemantic.work_events) && hasOnlyAdditions(previousSemantic.task_outcomes, currentSemantic.task_outcomes) && hasOnlyAdditions(previousSemantic.assurance_receipts, currentSemantic.assurance_receipts);
757
+ const planningChanged = axes.includes("planning") ||
758
+ axes.includes("governance") || axes.includes("topology");
759
+ const lifecycleChanged = axes.includes("lifecycle");
760
+ const evidenceChanged = axes.includes("actual_evidence") ||
761
+ axes.includes("assurance");
762
+ const classification = !planningChanged && !lifecycleChanged && evidenceChanged &&
763
+ evidenceOnlyAdditions
764
+ ? "evidence_extension"
765
+ : !planningChanged && lifecycleChanged && evidenceOnlyAdditions
766
+ ? "lifecycle_projection"
767
+ : "future_plan_edit";
768
+ return deepFreeze({
769
+ class: classification,
770
+ causes: [],
771
+ semantic_changed: true,
772
+ source_changed: sourceChanged,
773
+ changed_axes: axes,
774
+ is_merge_commit: isMergeCommit,
775
+ canonical_advance: null,
776
+ });
777
+ }
778
+ function entityRecords(semantic) {
779
+ return [
780
+ ...semantic.milestones.map((value) => ({
781
+ kind: "milestone",
782
+ id: value.id,
783
+ value,
784
+ fromId: null,
785
+ toId: null,
786
+ })),
787
+ ...semantic.tasks.map((value) => ({
788
+ kind: "task",
789
+ id: value.id,
790
+ value,
791
+ fromId: value.from_milestone_id,
792
+ toId: value.to_milestone_id,
793
+ })),
794
+ ...semantic.gates.map((value) => ({
795
+ kind: "gate",
796
+ id: value.id,
797
+ value,
798
+ fromId: value.from_milestone_id,
799
+ toId: value.to_milestone_id,
800
+ })),
801
+ ...semantic.resources.map((value) => ({
802
+ kind: "resource",
803
+ id: value.id,
804
+ value,
805
+ fromId: null,
806
+ toId: null,
807
+ })),
808
+ ].sort(compareEntity);
809
+ }
810
+ function identityKey(kind, id) {
811
+ return `${kind}\u0000${id}`;
812
+ }
813
+ function sequenceCauseKey(cause) {
814
+ return [
815
+ cause.cause,
816
+ cause.commit_id,
817
+ cause.entity_kind ?? "",
818
+ cause.source_id ?? "",
819
+ ].join("\u0000");
820
+ }
821
+ export function projectHistoricalTransitionSequence(inputs) {
822
+ if (inputs.length === 0) {
823
+ return deepFreeze({
824
+ model_version: HISTORICAL_TRANSITION_MODEL_VERSION,
825
+ checkpoints: [],
826
+ causes: [],
827
+ });
828
+ }
829
+ const commitIds = new Set();
830
+ for (const [index, input] of inputs.entries()) {
831
+ if (!fullObjectIdPattern.test(input.commit_id) ||
832
+ commitIds.has(input.commit_id) ||
833
+ (index === 0 && input.connected_to_previous)) {
834
+ throw new Error("historical transition sequence input is invalid");
835
+ }
836
+ commitIds.add(input.commit_id);
837
+ }
838
+ const memory = new Map();
839
+ const causes = [];
840
+ const checkpoints = [];
841
+ let previous = null;
842
+ let previousEntityKeys = new Set();
843
+ for (const [index, input] of inputs.entries()) {
844
+ const connected = index > 0 && input.connected_to_previous;
845
+ const identityConnected = connected && previous !== null &&
846
+ previous.semantic.project.id === input.projection.semantic.project.id;
847
+ const transition = classifyHistoricalTransition(connected ? previous : null, input.projection, {
848
+ isMergeCommit: input.is_merge_commit,
849
+ ...(input.canonical_advance_candidate === undefined
850
+ ? {}
851
+ : { canonicalAdvanceCandidate: input.canonical_advance_candidate }),
852
+ });
853
+ for (const cause of transition.causes) {
854
+ causes.push({
855
+ cause,
856
+ commit_id: input.commit_id,
857
+ entity_kind: null,
858
+ source_id: null,
859
+ });
860
+ }
861
+ const records = entityRecords(input.projection.semantic);
862
+ const currentEntityKeys = new Set(records.map(({ kind, id }) => identityKey(kind, id)));
863
+ if (!identityConnected && index > 0) {
864
+ for (const entry of memory.values()) {
865
+ if (entry.seen)
866
+ entry.uncertainWhileAbsent = true;
867
+ entry.active = null;
868
+ }
869
+ }
870
+ else if (connected) {
871
+ for (const [key, entry] of memory) {
872
+ if (previousEntityKeys.has(key) && !currentEntityKeys.has(key)) {
873
+ entry.active = null;
874
+ if (!trustworthyTransitionClasses.has(transition.class)) {
875
+ entry.uncertainWhileAbsent = true;
876
+ }
877
+ }
878
+ else if (entry.active === null &&
879
+ entry.seen &&
880
+ !trustworthyTransitionClasses.has(transition.class)) {
881
+ entry.uncertainWhileAbsent = true;
882
+ }
883
+ }
884
+ }
885
+ const epochs = [];
886
+ const currentOccurrences = new Map();
887
+ for (const record of records) {
888
+ const key = identityKey(record.kind, record.id);
889
+ const valueDigest = sha256Canonical(record.value);
890
+ const entry = memory.get(key) ?? {
891
+ active: null,
892
+ seen: false,
893
+ uncertainWhileAbsent: false,
894
+ };
895
+ let active = entry.active;
896
+ if (active === null) {
897
+ const ambiguousOtherKind = [...memory.entries()].some(([otherKey, other]) => otherKey !== key &&
898
+ otherKey.endsWith(`\u0000${record.id}`) &&
899
+ other.seen &&
900
+ (other.active !== null || other.uncertainWhileAbsent));
901
+ if ((entry.seen && entry.uncertainWhileAbsent) ||
902
+ ambiguousOtherKind) {
903
+ active = {
904
+ occurrenceId: null,
905
+ valueDigest,
906
+ valueEpochOrdinal: null,
907
+ };
908
+ causes.push({
909
+ cause: "identity_ambiguous",
910
+ commit_id: input.commit_id,
911
+ entity_kind: record.kind,
912
+ source_id: record.id,
913
+ });
914
+ }
915
+ else {
916
+ active = {
917
+ occurrenceId: historicalOccurrenceId({
918
+ projectId: input.projection.semantic.project.id,
919
+ entityKind: record.kind,
920
+ sourceId: record.id,
921
+ introducedCommitId: input.commit_id,
922
+ }),
923
+ valueDigest,
924
+ valueEpochOrdinal: 1,
925
+ };
926
+ }
927
+ }
928
+ else if (active.valueDigest !== valueDigest) {
929
+ active = {
930
+ occurrenceId: active.occurrenceId,
931
+ valueDigest,
932
+ valueEpochOrdinal: active.valueEpochOrdinal === null
933
+ ? null
934
+ : active.valueEpochOrdinal + 1,
935
+ };
936
+ }
937
+ entry.active = active;
938
+ entry.seen = true;
939
+ entry.uncertainWhileAbsent = false;
940
+ memory.set(key, entry);
941
+ currentOccurrences.set(key, active.occurrenceId);
942
+ epochs.push({
943
+ entity_kind: record.kind,
944
+ source_id: record.id,
945
+ occurrence_id: active.occurrenceId,
946
+ value_epoch_ordinal: active.valueEpochOrdinal,
947
+ value_digest: active.valueDigest,
948
+ });
949
+ }
950
+ const topologyEntries = [];
951
+ let topologyComplete = true;
952
+ for (const record of records.filter((candidate) => candidate.kind === "task" || candidate.kind === "gate")) {
953
+ const occurrenceId = currentOccurrences.get(identityKey(record.kind, record.id));
954
+ const fromOccurrenceId = currentOccurrences.get(identityKey("milestone", record.fromId));
955
+ const toOccurrenceId = currentOccurrences.get(identityKey("milestone", record.toId));
956
+ if (occurrenceId === null || occurrenceId === undefined ||
957
+ fromOccurrenceId === null || fromOccurrenceId === undefined ||
958
+ toOccurrenceId === null || toOccurrenceId === undefined) {
959
+ topologyComplete = false;
960
+ continue;
961
+ }
962
+ topologyEntries.push({
963
+ entity_kind: record.kind,
964
+ source_id: record.id,
965
+ occurrence_id: occurrenceId,
966
+ from_occurrence_id: fromOccurrenceId,
967
+ to_occurrence_id: toOccurrenceId,
968
+ });
969
+ }
970
+ checkpoints.push({
971
+ commit_id: input.commit_id,
972
+ semantic_digest: input.projection.semantic_digest,
973
+ transition,
974
+ entity_value_epochs: epochs,
975
+ topology_epoch_id: topologyComplete
976
+ ? historicalTopologyEpochId(topologyEntries)
977
+ : null,
978
+ });
979
+ previous = input.projection;
980
+ previousEntityKeys = currentEntityKeys;
981
+ }
982
+ const uniqueCauses = new Map(causes.map((cause) => [sequenceCauseKey(cause), cause]));
983
+ return deepFreeze({
984
+ model_version: HISTORICAL_TRANSITION_MODEL_VERSION,
985
+ checkpoints,
986
+ causes: [...uniqueCauses.values()].sort((left, right) => compareUnicodeScalars(left.commit_id, right.commit_id) ||
987
+ compareUnicodeScalars(left.cause, right.cause) ||
988
+ (left.entity_kind === null || right.entity_kind === null
989
+ ? left.entity_kind === right.entity_kind
990
+ ? 0
991
+ : left.entity_kind === null
992
+ ? -1
993
+ : 1
994
+ : entityKindOrder[left.entity_kind] - entityKindOrder[right.entity_kind]) ||
995
+ compareUnicodeScalars(left.source_id ?? "", right.source_id ?? "")),
996
+ });
997
+ }
998
+ //# sourceMappingURL=historical-transition.js.map