perttool 0.10.6 → 0.11.1

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 (241) hide show
  1. package/CHANGELOG.md +56 -1
  2. package/README.md +273 -609
  3. package/dist/application/cli-facade.d.ts +4 -10
  4. package/dist/application/cli-facade.d.ts.map +1 -1
  5. package/dist/application/cli-facade.js +1 -1
  6. package/dist/application/cli-facade.js.map +1 -1
  7. package/dist/application/contract10-candidate.d.ts +33 -0
  8. package/dist/application/contract10-candidate.d.ts.map +1 -0
  9. package/dist/application/contract10-candidate.js +38 -0
  10. package/dist/application/contract10-candidate.js.map +1 -0
  11. package/dist/application/contract10-planning-history.d.ts +4 -0
  12. package/dist/application/contract10-planning-history.d.ts.map +1 -0
  13. package/dist/application/contract10-planning-history.js +28 -0
  14. package/dist/application/contract10-planning-history.js.map +1 -0
  15. package/dist/application/contract10-planning.d.ts +61 -0
  16. package/dist/application/contract10-planning.d.ts.map +1 -0
  17. package/dist/application/contract10-planning.js +165 -0
  18. package/dist/application/contract10-planning.js.map +1 -0
  19. package/dist/application/contract10-runtime.d.ts +185 -0
  20. package/dist/application/contract10-runtime.d.ts.map +1 -0
  21. package/dist/application/contract10-runtime.js +236 -0
  22. package/dist/application/contract10-runtime.js.map +1 -0
  23. package/dist/application/contract8-milestone-acceptance.d.ts +6 -22
  24. package/dist/application/contract8-milestone-acceptance.d.ts.map +1 -1
  25. package/dist/application/contract8-milestone-acceptance.js +21 -99
  26. package/dist/application/contract8-milestone-acceptance.js.map +1 -1
  27. package/dist/application/contract8-milestone-read.d.ts +25 -0
  28. package/dist/application/contract8-milestone-read.d.ts.map +1 -0
  29. package/dist/application/contract8-milestone-read.js +111 -0
  30. package/dist/application/contract8-milestone-read.js.map +1 -0
  31. package/dist/application/contract9-assurance-evaluate.d.ts +3 -0
  32. package/dist/application/contract9-assurance-evaluate.d.ts.map +1 -0
  33. package/dist/application/contract9-assurance-evaluate.js +75 -0
  34. package/dist/application/contract9-assurance-evaluate.js.map +1 -0
  35. package/dist/application/contract9-assurance.d.ts +1 -2
  36. package/dist/application/contract9-assurance.d.ts.map +1 -1
  37. package/dist/application/contract9-assurance.js +2 -56
  38. package/dist/application/contract9-assurance.js.map +1 -1
  39. package/dist/application/contract9-candidate.d.ts +8 -0
  40. package/dist/application/contract9-candidate.d.ts.map +1 -1
  41. package/dist/application/contract9-candidate.js +36 -11
  42. package/dist/application/contract9-candidate.js.map +1 -1
  43. package/dist/application/contract9-mixed-mutation.d.ts +3 -0
  44. package/dist/application/contract9-mixed-mutation.d.ts.map +1 -1
  45. package/dist/application/contract9-mixed-mutation.js +5 -4
  46. package/dist/application/contract9-mixed-mutation.js.map +1 -1
  47. package/dist/application/contract9-temporal.js +1 -1
  48. package/dist/application/contract9-temporal.js.map +1 -1
  49. package/dist/application/target-actuals-analysis.d.ts.map +1 -1
  50. package/dist/application/target-actuals-analysis.js +10 -6
  51. package/dist/application/target-actuals-analysis.js.map +1 -1
  52. package/dist/application/target-temporal-analysis.d.ts +1 -0
  53. package/dist/application/target-temporal-analysis.d.ts.map +1 -1
  54. package/dist/application/target-temporal-analysis.js +5 -1
  55. package/dist/application/target-temporal-analysis.js.map +1 -1
  56. package/dist/cli.js +761 -87
  57. package/dist/cli.js.map +1 -1
  58. package/dist/command/contract10-discovery.d.ts +17 -0
  59. package/dist/command/contract10-discovery.d.ts.map +1 -0
  60. package/dist/command/contract10-discovery.js +304 -0
  61. package/dist/command/contract10-discovery.js.map +1 -0
  62. package/dist/command/contract10-usage.d.ts +24 -0
  63. package/dist/command/contract10-usage.d.ts.map +1 -0
  64. package/dist/command/contract10-usage.js +49 -0
  65. package/dist/command/contract10-usage.js.map +1 -0
  66. package/dist/core/index.d.ts +11 -2
  67. package/dist/core/index.d.ts.map +1 -1
  68. package/dist/core/index.js +3 -1
  69. package/dist/core/index.js.map +1 -1
  70. package/dist/help/assurance-guide.js +1 -1
  71. package/dist/help/assurance-guide.js.map +1 -1
  72. package/dist/help/contract10-guide.d.ts +10 -0
  73. package/dist/help/contract10-guide.d.ts.map +1 -0
  74. package/dist/help/contract10-guide.js +244 -0
  75. package/dist/help/contract10-guide.js.map +1 -0
  76. package/dist/index.d.ts +18 -8
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +8 -5
  79. package/dist/index.js.map +1 -1
  80. package/dist/milestone-acceptance/advance.d.ts +9 -0
  81. package/dist/milestone-acceptance/advance.d.ts.map +1 -1
  82. package/dist/milestone-acceptance/advance.js +29 -2
  83. package/dist/milestone-acceptance/advance.js.map +1 -1
  84. package/dist/milestone-acceptance/mutation.d.ts +2 -0
  85. package/dist/milestone-acceptance/mutation.d.ts.map +1 -1
  86. package/dist/milestone-acceptance/mutation.js +48 -12
  87. package/dist/milestone-acceptance/mutation.js.map +1 -1
  88. package/dist/model/source-result.d.ts +12 -0
  89. package/dist/model/source-result.d.ts.map +1 -0
  90. package/dist/model/source-result.js +16 -0
  91. package/dist/model/source-result.js.map +1 -0
  92. package/dist/mutation/advance-deletion.d.ts +1 -0
  93. package/dist/mutation/advance-deletion.d.ts.map +1 -1
  94. package/dist/mutation/advance-deletion.js +4 -1
  95. package/dist/mutation/advance-deletion.js.map +1 -1
  96. package/dist/mutation/validated-source.d.ts +30 -0
  97. package/dist/mutation/validated-source.d.ts.map +1 -0
  98. package/dist/mutation/validated-source.js +58 -0
  99. package/dist/mutation/validated-source.js.map +1 -0
  100. package/dist/node/planning-token-store.d.ts +3 -0
  101. package/dist/node/planning-token-store.d.ts.map +1 -0
  102. package/dist/node/planning-token-store.js +90 -0
  103. package/dist/node/planning-token-store.js.map +1 -0
  104. package/dist/parser/document-parser.d.ts +1 -0
  105. package/dist/parser/document-parser.d.ts.map +1 -1
  106. package/dist/parser/document-parser.js +1 -1
  107. package/dist/parser/document-parser.js.map +1 -1
  108. package/dist/perttool-vscode-private-0.0.0-d1578b9.vsix +0 -0
  109. package/dist/planning-pool/advance.d.ts +10 -0
  110. package/dist/planning-pool/advance.d.ts.map +1 -0
  111. package/dist/planning-pool/advance.js +158 -0
  112. package/dist/planning-pool/advance.js.map +1 -0
  113. package/dist/planning-pool/format.d.ts +12 -0
  114. package/dist/planning-pool/format.d.ts.map +1 -0
  115. package/dist/planning-pool/format.js +199 -0
  116. package/dist/planning-pool/format.js.map +1 -0
  117. package/dist/planning-pool/history-guard.d.ts +19 -0
  118. package/dist/planning-pool/history-guard.d.ts.map +1 -0
  119. package/dist/planning-pool/history-guard.js +123 -0
  120. package/dist/planning-pool/history-guard.js.map +1 -0
  121. package/dist/planning-pool/history-types.d.ts +174 -0
  122. package/dist/planning-pool/history-types.d.ts.map +1 -0
  123. package/dist/planning-pool/history-types.js +2 -0
  124. package/dist/planning-pool/history-types.js.map +1 -0
  125. package/dist/planning-pool/history.d.ts +11 -0
  126. package/dist/planning-pool/history.d.ts.map +1 -0
  127. package/dist/planning-pool/history.js +977 -0
  128. package/dist/planning-pool/history.js.map +1 -0
  129. package/dist/planning-pool/human.d.ts +9 -0
  130. package/dist/planning-pool/human.d.ts.map +1 -0
  131. package/dist/planning-pool/human.js +305 -0
  132. package/dist/planning-pool/human.js.map +1 -0
  133. package/dist/planning-pool/intent-types.d.ts +76 -0
  134. package/dist/planning-pool/intent-types.d.ts.map +1 -0
  135. package/dist/planning-pool/intent-types.js +2 -0
  136. package/dist/planning-pool/intent-types.js.map +1 -0
  137. package/dist/planning-pool/intent.d.ts +7 -0
  138. package/dist/planning-pool/intent.d.ts.map +1 -0
  139. package/dist/planning-pool/intent.js +590 -0
  140. package/dist/planning-pool/intent.js.map +1 -0
  141. package/dist/planning-pool/observation-types.d.ts +143 -0
  142. package/dist/planning-pool/observation-types.d.ts.map +1 -0
  143. package/dist/planning-pool/observation-types.js +2 -0
  144. package/dist/planning-pool/observation-types.js.map +1 -0
  145. package/dist/planning-pool/observation.d.ts +8 -0
  146. package/dist/planning-pool/observation.d.ts.map +1 -0
  147. package/dist/planning-pool/observation.js +582 -0
  148. package/dist/planning-pool/observation.js.map +1 -0
  149. package/dist/planning-pool/projection.d.ts +28 -0
  150. package/dist/planning-pool/projection.d.ts.map +1 -0
  151. package/dist/planning-pool/projection.js +148 -0
  152. package/dist/planning-pool/projection.js.map +1 -0
  153. package/dist/planning-pool/public-core.d.ts +32 -0
  154. package/dist/planning-pool/public-core.d.ts.map +1 -0
  155. package/dist/planning-pool/public-core.js +122 -0
  156. package/dist/planning-pool/public-core.js.map +1 -0
  157. package/dist/planning-pool/reshape-normalize.d.ts +15 -0
  158. package/dist/planning-pool/reshape-normalize.d.ts.map +1 -0
  159. package/dist/planning-pool/reshape-normalize.js +688 -0
  160. package/dist/planning-pool/reshape-normalize.js.map +1 -0
  161. package/dist/planning-pool/reshape-token.d.ts +20 -0
  162. package/dist/planning-pool/reshape-token.d.ts.map +1 -0
  163. package/dist/planning-pool/reshape-token.js +193 -0
  164. package/dist/planning-pool/reshape-token.js.map +1 -0
  165. package/dist/planning-pool/reshape-types.d.ts +183 -0
  166. package/dist/planning-pool/reshape-types.d.ts.map +1 -0
  167. package/dist/planning-pool/reshape-types.js +2 -0
  168. package/dist/planning-pool/reshape-types.js.map +1 -0
  169. package/dist/planning-pool/reshape.d.ts +13 -0
  170. package/dist/planning-pool/reshape.d.ts.map +1 -0
  171. package/dist/planning-pool/reshape.js +1434 -0
  172. package/dist/planning-pool/reshape.js.map +1 -0
  173. package/dist/planning-pool/source-lexical.d.ts +30 -0
  174. package/dist/planning-pool/source-lexical.d.ts.map +1 -0
  175. package/dist/planning-pool/source-lexical.js +158 -0
  176. package/dist/planning-pool/source-lexical.js.map +1 -0
  177. package/dist/planning-pool/source-types.d.ts +169 -0
  178. package/dist/planning-pool/source-types.d.ts.map +1 -0
  179. package/dist/planning-pool/source-types.js +2 -0
  180. package/dist/planning-pool/source-types.js.map +1 -0
  181. package/dist/planning-pool/source-values.d.ts +16 -0
  182. package/dist/planning-pool/source-values.d.ts.map +1 -0
  183. package/dist/planning-pool/source-values.js +120 -0
  184. package/dist/planning-pool/source-values.js.map +1 -0
  185. package/dist/planning-pool/source.d.ts +18 -0
  186. package/dist/planning-pool/source.d.ts.map +1 -0
  187. package/dist/planning-pool/source.js +811 -0
  188. package/dist/planning-pool/source.js.map +1 -0
  189. package/dist/planning-pool/window-types.d.ts +133 -0
  190. package/dist/planning-pool/window-types.d.ts.map +1 -0
  191. package/dist/planning-pool/window-types.js +2 -0
  192. package/dist/planning-pool/window-types.js.map +1 -0
  193. package/dist/planning-pool/window.d.ts +6 -0
  194. package/dist/planning-pool/window.d.ts.map +1 -0
  195. package/dist/planning-pool/window.js +625 -0
  196. package/dist/planning-pool/window.js.map +1 -0
  197. package/dist/schema/registry.d.ts +1 -1
  198. package/dist/schema/registry.d.ts.map +1 -1
  199. package/dist/schema/registry.js +17 -3
  200. package/dist/schema/registry.js.map +1 -1
  201. package/dist/temporal-schedule/format.d.ts.map +1 -1
  202. package/dist/temporal-schedule/format.js +4 -54
  203. package/dist/temporal-schedule/format.js.map +1 -1
  204. package/dist/temporal-schedule/source-lexical.d.ts +9 -0
  205. package/dist/temporal-schedule/source-lexical.d.ts.map +1 -1
  206. package/dist/temporal-schedule/source-lexical.js +21 -10
  207. package/dist/temporal-schedule/source-lexical.js.map +1 -1
  208. package/dist/temporal-schedule/source.d.ts.map +1 -1
  209. package/dist/temporal-schedule/source.js +3 -13
  210. package/dist/temporal-schedule/source.js.map +1 -1
  211. package/dist/version.d.ts +1 -1
  212. package/dist/version.js +1 -1
  213. package/package.json +1 -1
  214. package/schemas/{Perttool.AdvanceResult.v3.schema.json → Perttool.AdvanceResult.v4.schema.json} +42 -4
  215. package/schemas/Perttool.AgentGuidanceResult.v1.schema.json +1 -1
  216. package/schemas/Perttool.AnalysisResult.v7.schema.json +1 -1
  217. package/schemas/Perttool.CheckResult.v6.schema.json +1 -1
  218. package/schemas/Perttool.CliError.v1.schema.json +1 -1
  219. package/schemas/Perttool.CommandHelpResult.v1.schema.json +2 -2
  220. package/schemas/Perttool.ExportResult.v1.schema.json +1 -1
  221. package/schemas/Perttool.FormatResult.v1.schema.json +1 -1
  222. package/schemas/Perttool.GuideResult.v1.schema.json +1 -1
  223. package/schemas/Perttool.HistoricalGraphResult.v1.schema.json +1 -1
  224. package/schemas/Perttool.ImportResult.v1.schema.json +1 -1
  225. package/schemas/Perttool.InitResult.v1.schema.json +1 -1
  226. package/schemas/Perttool.MilestoneAcceptanceMigrationResult.v1.schema.json +1 -1
  227. package/schemas/Perttool.MilestoneAcceptanceResult.v1.schema.json +1 -1
  228. package/schemas/Perttool.MutationResult.v6.schema.json +1 -1
  229. package/schemas/Perttool.NextResult.v8.schema.json +7 -4
  230. package/schemas/Perttool.PlanAssuranceResult.v2.schema.json +1 -1
  231. package/schemas/Perttool.PlanningMutationResult.v1.schema.json +567 -0
  232. package/schemas/Perttool.PlanningObservationRequest.v1.schema.json +6 -0
  233. package/schemas/Perttool.PlanningPoolResult.v1.schema.json +469 -0
  234. package/schemas/Perttool.PlanningReshapePreflightResult.v1.schema.json +906 -0
  235. package/schemas/Perttool.PlanningReshapeRequest.v1.schema.json +19 -0
  236. package/schemas/Perttool.ProjectHistoryResult.v1.schema.json +1 -1
  237. package/schemas/Perttool.ProjectResult.v5.schema.json +1 -1
  238. package/schemas/Perttool.SchemaResult.v1.schema.json +1 -1
  239. package/schemas/{Perttool.UnitMigrationResult.v4.schema.json → Perttool.UnitMigrationResult.v5.schema.json} +42 -17
  240. package/schemas/Perttool.VelocityObservationResult.v1.schema.json +1 -1
  241. package/schemas/Perttool.WindowMutationRequest.v1.schema.json +6 -0
@@ -0,0 +1,1434 @@
1
+ import { applyTextEdits, normalizeTextEdits, } from "../mutation/text-edits.js";
2
+ import { fieldLine, scanTemporalDeclarationBlocks, } from "../temporal-schedule/source-lexical.js";
3
+ import { planPlanningPoolSourceMutation } from "./format.js";
4
+ import { compilePlanningIntentRequest, isPlanningIntentRequest, } from "./intent.js";
5
+ import { PLANNING_RESHAPE_NORMALIZATION_CONTRACT, PLANNING_RESHAPE_NORMALIZED_LIMITS, normalizePlanningReshapeRequest, planningReshapeSha256, } from "./reshape-normalize.js";
6
+ import { PlanningReshapeTokenRegistry } from "./reshape-token.js";
7
+ import { planningFields, scanPlanningDeclarationBlocks, } from "./source-lexical.js";
8
+ import { parsePlanningPoolSource, PLANNING_POOL_SOURCE_CAPABILITY, } from "./source.js";
9
+ export const PLANNING_RESHAPE_CORE_CAPABILITY = Object.freeze({
10
+ id: "perttool.planning-reshape-core",
11
+ version: 1,
12
+ normalizationContract: PLANNING_RESHAPE_NORMALIZATION_CONTRACT,
13
+ });
14
+ export const PLANNING_RESHAPE_CORE_LIMITS = PLANNING_RESHAPE_NORMALIZED_LIMITS;
15
+ function reshapeDiagnostic(message, code = "PTPOOL-110", severity = "error") {
16
+ return Object.freeze({
17
+ code,
18
+ severity,
19
+ message,
20
+ data: Object.freeze({}),
21
+ });
22
+ }
23
+ function requireCapability(capability) {
24
+ if (capability !== PLANNING_RESHAPE_CORE_CAPABILITY) {
25
+ throw new TypeError("the private planning reshape Core capability is required");
26
+ }
27
+ }
28
+ function localId(qualifiedId, documentId, label, diagnostics) {
29
+ const prefix = `${documentId}::`;
30
+ if (!qualifiedId.startsWith(prefix)) {
31
+ diagnostics.push(reshapeDiagnostic(`${label} ${qualifiedId} is outside ${documentId}`));
32
+ return null;
33
+ }
34
+ return qualifiedId.slice(prefix.length);
35
+ }
36
+ function relationCount(request) {
37
+ return request.planning_entity_dispositions.length +
38
+ request.association_dispositions.length +
39
+ request.projection_link_dispositions.length +
40
+ request.dependency_dispositions.length +
41
+ request.window_membership_dispositions.length;
42
+ }
43
+ function enforceRequestLimits(request, diagnostics) {
44
+ if (request.affected_work_ids.length > PLANNING_RESHAPE_CORE_LIMITS.affectedWorks) {
45
+ diagnostics.push(reshapeDiagnostic("Planning reshape affected-Work limit exceeded", "PTPOOL-115"));
46
+ }
47
+ if (request.semantic_elements.length > PLANNING_RESHAPE_CORE_LIMITS.semanticRows) {
48
+ diagnostics.push(reshapeDiagnostic("Planning reshape semantic-row limit exceeded", "PTPOOL-115"));
49
+ }
50
+ if (relationCount(request) > PLANNING_RESHAPE_CORE_LIMITS.relationshipDispositions) {
51
+ diagnostics.push(reshapeDiagnostic("Planning reshape relationship-disposition limit exceeded", "PTPOOL-115"));
52
+ }
53
+ }
54
+ function workState(work) {
55
+ return {
56
+ id: work.id,
57
+ qualifiedId: work.qualifiedId,
58
+ title: work.title,
59
+ description: work.description?.value ?? "",
60
+ events: new Set(work.events.map(({ id }) => id)),
61
+ activities: new Set(work.activities.map(({ id }) => id)),
62
+ milestoneLinks: new Set(work.milestoneLinks.map(({ id }) => id)),
63
+ taskLinks: new Set(work.taskLinks.map(({ id }) => id)),
64
+ dependsOn: new Set(work.dependsOn.map(({ id }) => id)),
65
+ };
66
+ }
67
+ function sourceDescriptions(model, affected, elementsByWork) {
68
+ return Object.freeze([...affected].sort().flatMap((id) => {
69
+ const work = model.works.find((candidate) => candidate.id === id);
70
+ return work === undefined ? [] : [Object.freeze({
71
+ workId: work.qualifiedId,
72
+ description: work.description?.value ?? "",
73
+ elementIds: Object.freeze([...(elementsByWork.get(id) ?? [])]),
74
+ })];
75
+ }));
76
+ }
77
+ function appendMapValue(map, key, value) {
78
+ map.set(key, Object.freeze([...(map.get(key) ?? []), value]));
79
+ }
80
+ function collectDescriptionInventory(request, model, affected, finalWorks, removed, diagnostics) {
81
+ const originIds = new Map();
82
+ const existingRows = new Map();
83
+ const destinationRows = new Map();
84
+ for (const element of request.semantic_elements) {
85
+ if (element.origin.kind === "existing") {
86
+ const id = localId(element.origin.work_id, model.documentId, "Semantic origin Work", diagnostics);
87
+ if (id !== null) {
88
+ appendMapValue(existingRows, id, element);
89
+ appendMapValue(originIds, id, element.element_id);
90
+ if (!affected.has(id))
91
+ diagnostics.push(reshapeDiagnostic(`Semantic origin Work ${element.origin.work_id} is not affected`));
92
+ }
93
+ }
94
+ if (element.destination.kind === "work") {
95
+ const id = localId(element.destination.work_id, model.documentId, "Semantic destination Work", diagnostics);
96
+ if (id !== null) {
97
+ appendMapValue(destinationRows, id, element);
98
+ if (!affected.has(id))
99
+ diagnostics.push(reshapeDiagnostic(`Semantic destination Work ${element.destination.work_id} is not affected`));
100
+ if (!finalWorks.has(id) || removed.has(id))
101
+ diagnostics.push(reshapeDiagnostic(`Semantic destination Work ${element.destination.work_id} is not final`));
102
+ }
103
+ }
104
+ }
105
+ return Object.freeze({ originIds, existingRows, destinationRows });
106
+ }
107
+ function validateSourcePartition(work, rows, diagnostics) {
108
+ const description = work.description?.value ?? "";
109
+ const ordered = [...rows].sort((left, right) => (left.origin.kind === "existing" ? left.origin.start_utf16 : 0) -
110
+ (right.origin.kind === "existing" ? right.origin.start_utf16 : 0));
111
+ let cursor = 0;
112
+ for (const row of ordered) {
113
+ if (row.origin.kind !== "existing")
114
+ continue;
115
+ const matches = row.origin.start_utf16 === cursor &&
116
+ row.origin.end_utf16 <= description.length &&
117
+ description.slice(row.origin.start_utf16, row.origin.end_utf16) === row.origin.source_text;
118
+ if (!matches)
119
+ diagnostics.push(reshapeDiagnostic(`Semantic origin partition for ${work.qualifiedId} has a gap, overlap, or text mismatch`));
120
+ cursor = row.origin.end_utf16;
121
+ }
122
+ if (cursor !== description.length)
123
+ diagnostics.push(reshapeDiagnostic(`Semantic origin partition for ${work.qualifiedId} is incomplete`));
124
+ }
125
+ function validateSourcePartitions(model, affected, inventory, diagnostics) {
126
+ const originalById = new Map(model.works.map((work) => [work.id, work]));
127
+ for (const id of affected) {
128
+ const work = originalById.get(id);
129
+ if (work !== undefined)
130
+ validateSourcePartition(work, inventory.existingRows.get(id) ?? [], diagnostics);
131
+ }
132
+ }
133
+ function reconstructDestinationRow(work, rows, diagnostics) {
134
+ const ordered = [...rows].sort((left, right) => (left.destination.kind === "work" ? left.destination.position : 0) -
135
+ (right.destination.kind === "work" ? right.destination.position : 0));
136
+ const destinations = ordered.map(({ destination }) => destination);
137
+ if (destinations.some((destination, index) => destination.kind !== "work" || destination.position !== index)) {
138
+ diagnostics.push(reshapeDiagnostic(`Semantic destination positions for ${work.qualifiedId} are not unique and contiguous`));
139
+ }
140
+ work.description = destinations.map((destination) => destination.kind === "work" ? destination.text : "").join("");
141
+ return Object.freeze({
142
+ workId: work.qualifiedId,
143
+ description: work.description,
144
+ elementIds: Object.freeze(ordered.map(({ element_id }) => element_id)),
145
+ });
146
+ }
147
+ function reconstructDestinationRows(affected, finalWorks, removed, inventory, diagnostics) {
148
+ const after = [];
149
+ for (const id of affected) {
150
+ const work = finalWorks.get(id);
151
+ if (work !== undefined && !removed.has(id)) {
152
+ after.push(reconstructDestinationRow(work, inventory.destinationRows.get(id) ?? [], diagnostics));
153
+ }
154
+ }
155
+ return Object.freeze(after.sort((left, right) => left.workId.localeCompare(right.workId)));
156
+ }
157
+ function validateResidualActions(request, model, affected, finalWorks, removed, diagnostics) {
158
+ for (const action of request.add_residual_description) {
159
+ const id = localId(action.work_id, model.documentId, "Residual-description Work", diagnostics);
160
+ const work = id === null ? undefined : finalWorks.get(id);
161
+ if (id === null || work === undefined || removed.has(id) || !affected.has(id)) {
162
+ diagnostics.push(reshapeDiagnostic(`Residual-description action target ${action.work_id} is not an affected final Work`));
163
+ }
164
+ else if (work.description !== action.text) {
165
+ diagnostics.push(reshapeDiagnostic(`Residual-description action for ${action.work_id} does not match reconstructed text`));
166
+ }
167
+ }
168
+ }
169
+ function affectedSet(request, model, diagnostics) {
170
+ const result = new Set();
171
+ for (const id of request.affected_work_ids) {
172
+ const local = localId(id, model.documentId, "Affected Work", diagnostics);
173
+ if (local !== null)
174
+ result.add(local);
175
+ }
176
+ return result;
177
+ }
178
+ function createdBoundaryIds(created, model, diagnostics) {
179
+ const id = localId(created.work_id, model.documentId, "Created Work", diagnostics);
180
+ const anchor = created.insert_after_work_id === null
181
+ ? null
182
+ : localId(created.insert_after_work_id, model.documentId, "Created Work anchor", diagnostics);
183
+ return id === null || (created.insert_after_work_id !== null && anchor === null)
184
+ ? null
185
+ : Object.freeze([id, anchor]);
186
+ }
187
+ function createdWorks(request, model, affected, works, diagnostics) {
188
+ const result = [];
189
+ const boundaries = [];
190
+ for (const created of request.created_works) {
191
+ const boundary = createdBoundaryIds(created, model, diagnostics);
192
+ if (boundary === null)
193
+ continue;
194
+ const [id, anchor] = boundary;
195
+ if (works.has(id))
196
+ diagnostics.push(reshapeDiagnostic(`Created Work ${created.work_id} already exists`));
197
+ if (!affected.has(id))
198
+ diagnostics.push(reshapeDiagnostic(`Created Work ${created.work_id} is not affected`));
199
+ works.set(id, {
200
+ id,
201
+ qualifiedId: created.work_id,
202
+ title: created.title,
203
+ description: "",
204
+ events: new Set(),
205
+ activities: new Set(),
206
+ milestoneLinks: new Set(),
207
+ taskLinks: new Set(),
208
+ dependsOn: new Set(),
209
+ });
210
+ boundaries.push(Object.freeze({ created, id, anchor }));
211
+ result.push(id);
212
+ }
213
+ for (const { created, anchor } of boundaries) {
214
+ if (anchor !== null && !works.has(anchor))
215
+ diagnostics.push(reshapeDiagnostic(`Created Work anchor ${created.insert_after_work_id} does not exist`));
216
+ }
217
+ return Object.freeze(result.sort());
218
+ }
219
+ function removedWorks(request, model, affected, originalIds, diagnostics) {
220
+ const result = new Set();
221
+ for (const removed of request.removed_work_ids) {
222
+ const id = localId(removed, model.documentId, "Removed Work", diagnostics);
223
+ if (id === null)
224
+ continue;
225
+ if (!originalIds.has(id))
226
+ diagnostics.push(reshapeDiagnostic(`Removed Work ${removed} does not exist`));
227
+ if (!affected.has(id))
228
+ diagnostics.push(reshapeDiagnostic(`Removed Work ${removed} is not affected`));
229
+ result.add(id);
230
+ }
231
+ return result;
232
+ }
233
+ function validateAffectedIdentities(affected, works, diagnostics) {
234
+ for (const id of affected) {
235
+ if (!works.has(id))
236
+ diagnostics.push(reshapeDiagnostic(`Affected Work ${id} does not exist in the final boundary set`));
237
+ }
238
+ }
239
+ function descriptionRows(request, model, affected, finalWorks, removed, diagnostics) {
240
+ const inventory = collectDescriptionInventory(request, model, affected, finalWorks, removed, diagnostics);
241
+ validateSourcePartitions(model, affected, inventory, diagnostics);
242
+ const after = reconstructDestinationRows(affected, finalWorks, removed, inventory, diagnostics);
243
+ validateResidualActions(request, model, affected, finalWorks, removed, diagnostics);
244
+ return Object.freeze({
245
+ before: sourceDescriptions(model, affected, inventory.originIds),
246
+ after,
247
+ });
248
+ }
249
+ function strictQualifiedIds(text, documentId, request) {
250
+ const milestones = new Set();
251
+ const tasks = new Set();
252
+ for (const block of scanTemporalDeclarationBlocks(text)) {
253
+ if (block.kind === "milestone")
254
+ milestones.add(`${documentId}::${block.id}`);
255
+ if (block.kind === "task")
256
+ tasks.add(`${documentId}::${block.id}`);
257
+ }
258
+ if (request?.strict_fragment?.kind === "project") {
259
+ for (const id of request.strict_fragment.event_ids)
260
+ milestones.add(id);
261
+ for (const id of request.strict_fragment.activity_ids)
262
+ tasks.add(id);
263
+ }
264
+ return Object.freeze({ milestones, tasks });
265
+ }
266
+ function strictField(block, name) {
267
+ return block.lines.map(fieldLine).find((field) => field?.name === name)?.rawValue ?? null;
268
+ }
269
+ function qualifiedSelection(ids, documentId, label, diagnostics) {
270
+ const locals = new Set();
271
+ for (const id of ids) {
272
+ const local = localId(id, documentId, label, diagnostics);
273
+ if (local !== null)
274
+ locals.add(local);
275
+ }
276
+ return locals;
277
+ }
278
+ function entityDisposition(request, kind, qualifiedId, action, diagnostics) {
279
+ const matching = request.planning_entity_dispositions.filter((item) => item.entity_kind === kind && item.entity_id === qualifiedId && item.action === action);
280
+ if (matching.length !== 1)
281
+ diagnostics.push(reshapeDiagnostic(`${action} ${kind} ${qualifiedId} requires one exact planning-entity disposition`, action === "project" ? "PTPOOL-108" : "PTPOOL-109"));
282
+ }
283
+ function hasAssociationDisposition(request, kind, entityId, originWorkId, destinationWorkId) {
284
+ return request.association_dispositions.some((item) => item.entity_kind === kind && item.entity_id === entityId &&
285
+ item.origin_work_id === originWorkId && item.destination_work_id === destinationWorkId);
286
+ }
287
+ function hasLinkDisposition(request, kind, strictId, originWorkId, destinationWorkId) {
288
+ return request.projection_link_dispositions.some((item) => item.strict_kind === kind && item.strict_id === strictId &&
289
+ item.origin_work_id === originWorkId && item.destination_work_id === destinationWorkId);
290
+ }
291
+ function associatedWorkIds(model, kind, qualifiedId) {
292
+ return Object.freeze(model.works.filter((work) => (kind === "event" ? work.events : work.activities).some((item) => item.qualifiedId === qualifiedId))
293
+ .map(({ qualifiedId: id }) => id).sort());
294
+ }
295
+ function linkedWorkIds(model, kind, qualifiedId) {
296
+ return Object.freeze(model.works.filter((work) => (kind === "milestone" ? work.milestoneLinks : work.taskLinks)
297
+ .some((item) => item.qualifiedId === qualifiedId))
298
+ .map(({ qualifiedId: id }) => id).sort());
299
+ }
300
+ function requireAffectedWorks(request, workIds, label, code, diagnostics) {
301
+ const affected = new Set(request.affected_work_ids);
302
+ for (const workId of workIds) {
303
+ if (!affected.has(workId))
304
+ diagnostics.push(reshapeDiagnostic(`${label} requires affected Work ${workId}`, code));
305
+ }
306
+ }
307
+ function validateProjectionEntity(request, model, kind, qualifiedId, diagnostics) {
308
+ const entities = kind === "event" ? model.events : model.activities;
309
+ if (!entities.some((item) => item.qualifiedId === qualifiedId)) {
310
+ diagnostics.push(reshapeDiagnostic(`Projected ${kind} ${qualifiedId} does not exist`, "PTPOOL-108"));
311
+ return;
312
+ }
313
+ entityDisposition(request, kind, qualifiedId, "project", diagnostics);
314
+ const workIds = associatedWorkIds(model, kind, qualifiedId);
315
+ requireAffectedWorks(request, workIds, `Projection of ${qualifiedId}`, "PTPOOL-108", diagnostics);
316
+ const strictKind = kind === "event" ? "milestone" : "task";
317
+ for (const workId of workIds) {
318
+ if (!hasAssociationDisposition(request, kind, qualifiedId, workId, null)) {
319
+ diagnostics.push(reshapeDiagnostic(`Projection must remove ${kind} ${qualifiedId} association from ${workId}`, "PTPOOL-108"));
320
+ }
321
+ if (!hasLinkDisposition(request, strictKind, qualifiedId, null, workId)) {
322
+ diagnostics.push(reshapeDiagnostic(`Projection must create ${strictKind} ${qualifiedId} link for ${workId}`, "PTPOOL-108"));
323
+ }
324
+ }
325
+ }
326
+ function validateProjectionFragment(text, request, model, diagnostics) {
327
+ const fragment = request.strict_fragment;
328
+ if (fragment?.kind !== "project")
329
+ return;
330
+ if (fragment.event_ids.length + fragment.activity_ids.length === 0) {
331
+ diagnostics.push(reshapeDiagnostic("Projection strict fragment is empty", "PTPOOL-108"));
332
+ }
333
+ const existing = strictQualifiedIds(text, model.documentId);
334
+ for (const id of fragment.event_ids) {
335
+ if (existing.milestones.has(id))
336
+ diagnostics.push(reshapeDiagnostic(`Projected Event ${id} collides with an existing Milestone`, "PTPOOL-108"));
337
+ validateProjectionEntity(request, model, "event", id, diagnostics);
338
+ const event = model.events.find(({ qualifiedId }) => qualifiedId === id);
339
+ if (event?.source !== null && event?.source !== undefined)
340
+ diagnostics.push(reshapeDiagnostic(`Projected Event ${id} has a source field with no strict Milestone owner`, "PTPOOL-108"));
341
+ }
342
+ const finalMilestones = new Set([...existing.milestones, ...fragment.event_ids]);
343
+ for (const id of fragment.activity_ids) {
344
+ if (existing.tasks.has(id))
345
+ diagnostics.push(reshapeDiagnostic(`Projected Activity ${id} collides with an existing Task`, "PTPOOL-108"));
346
+ validateProjectionEntity(request, model, "activity", id, diagnostics);
347
+ const activity = model.activities.find(({ qualifiedId }) => qualifiedId === id);
348
+ if (activity !== undefined) {
349
+ for (const endpoint of [activity.from.qualifiedId, activity.to.qualifiedId]) {
350
+ if (!finalMilestones.has(endpoint))
351
+ diagnostics.push(reshapeDiagnostic(`Projected Activity ${id} endpoint ${endpoint} is not a final strict Milestone`, "PTPOOL-108"));
352
+ }
353
+ }
354
+ }
355
+ }
356
+ function declarationContainsReference(text, headerPattern, fieldPattern) {
357
+ const lines = text.split(/(?<=\n)/u);
358
+ for (let index = 0; index < lines.length; index += 1) {
359
+ if (!headerPattern.test(lines[index].replace(/\r?\n$/u, "")))
360
+ continue;
361
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
362
+ const line = lines[cursor].replace(/\r?\n$/u, "");
363
+ if (line !== "" && !line.startsWith(" ") && !line.startsWith("\t"))
364
+ break;
365
+ if (fieldPattern.test(line))
366
+ return true;
367
+ }
368
+ }
369
+ return false;
370
+ }
371
+ function taskHasProtectedEvidence(text, id) {
372
+ const escaped = id.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
373
+ return new RegExp(`^plan_seal ${escaped}:`, "mu").test(text) ||
374
+ declarationContainsReference(text, /^task_outcome [A-Za-z][A-Za-z0-9_-]*:$/u, new RegExp(`^ task ${escaped}$`, "u")) ||
375
+ declarationContainsReference(text, /^work_event [A-Za-z][A-Za-z0-9_-]*:$/u, new RegExp(`^ task ${escaped}$`, "u")) ||
376
+ declarationContainsReference(text, /^assurance_receipt [A-Za-z][A-Za-z0-9_-]*:$/u, new RegExp(`^(?: producer| ) ${escaped}(?: |$)`, "u")) ||
377
+ new RegExp(`^task_relation [A-Za-z][A-Za-z0-9_-]* ${escaped} ->|^task_relation [A-Za-z][A-Za-z0-9_-]* [A-Za-z][A-Za-z0-9_-]* -> ${escaped}:`, "mu").test(text);
378
+ }
379
+ function milestoneHasProtectedEvidence(text, id) {
380
+ const escaped = id.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
381
+ return declarationContainsReference(text, /^milestone_criterion_set [A-Za-z][A-Za-z0-9_-]*:$/u, new RegExp(`^ milestone ${escaped}$`, "u"));
382
+ }
383
+ function taskEndpoints(block) {
384
+ const match = /^task [A-Za-z][A-Za-z0-9_-]* ([A-Za-z][A-Za-z0-9_-]*) -> ([A-Za-z][A-Za-z0-9_-]*):$/u.exec(block.header.text);
385
+ return match === null ? null : Object.freeze([match[1], match[2]]);
386
+ }
387
+ function projectFinish(text) {
388
+ const project = scanTemporalDeclarationBlocks(text).find(({ kind }) => kind === "project");
389
+ return project === undefined ? null : strictField(project, "finish");
390
+ }
391
+ function validateDeferralLinkRestoration(request, model, kind, qualifiedId, diagnostics) {
392
+ const planningKind = kind === "milestone" ? "event" : "activity";
393
+ entityDisposition(request, planningKind, qualifiedId, "defer", diagnostics);
394
+ const workIds = linkedWorkIds(model, kind, qualifiedId);
395
+ if (workIds.length === 0)
396
+ diagnostics.push(reshapeDiagnostic(`Deferred ${kind} ${qualifiedId} has no live Work projection link`, "PTPOOL-109"));
397
+ requireAffectedWorks(request, workIds, `Deferral of ${qualifiedId}`, "PTPOOL-109", diagnostics);
398
+ for (const workId of workIds) {
399
+ if (!hasLinkDisposition(request, kind, qualifiedId, workId, null)) {
400
+ diagnostics.push(reshapeDiagnostic(`Deferral must remove ${kind} ${qualifiedId} link from ${workId}`, "PTPOOL-109"));
401
+ }
402
+ if (!hasAssociationDisposition(request, planningKind, qualifiedId, null, workId)) {
403
+ diagnostics.push(reshapeDiagnostic(`Deferral must restore ${planningKind} ${qualifiedId} association for ${workId}`, "PTPOOL-109"));
404
+ }
405
+ }
406
+ }
407
+ function validateDeferredTask(id, context) {
408
+ const { text, request, model, tasks, milestones, milestoneIds, diagnostics } = context;
409
+ const block = tasks.get(id);
410
+ const qualified = `${model.documentId}::${id}`;
411
+ if (block === undefined) {
412
+ diagnostics.push(reshapeDiagnostic(`Deferred Task ${qualified} does not exist`, "PTPOOL-109"));
413
+ return;
414
+ }
415
+ const protectedEvidence = (strictField(block, "status") ?? "planned") !== "planned" ||
416
+ strictField(block, "blocked_reason") !== null || taskHasProtectedEvidence(text, id);
417
+ if (protectedEvidence)
418
+ diagnostics.push(reshapeDiagnostic(`Deferred Task ${qualified} has protected execution or assurance evidence`, "PTPOOL-109"));
419
+ validateDeferralLinkRestoration(request, model, "task", qualified, diagnostics);
420
+ const endpoints = taskEndpoints(block);
421
+ if (endpoints === null) {
422
+ diagnostics.push(reshapeDiagnostic(`Deferred Task ${qualified} has invalid endpoints`, "PTPOOL-109"));
423
+ return;
424
+ }
425
+ for (const endpoint of endpoints) {
426
+ if (!milestones.has(endpoint) && !milestoneIds.has(endpoint))
427
+ diagnostics.push(reshapeDiagnostic(`Deferred Task ${qualified} endpoint ${endpoint} is unavailable`, "PTPOOL-109"));
428
+ }
429
+ }
430
+ function retainedMilestoneConsumer(text, id, blocks, taskIds) {
431
+ const retainedTask = blocks.some((candidate) => {
432
+ if (candidate.kind !== "task" || taskIds.has(candidate.id))
433
+ return false;
434
+ return taskEndpoints(candidate)?.includes(id) ?? false;
435
+ });
436
+ const retainedGate = new RegExp(`^gate [A-Za-z][A-Za-z0-9_-]* (?:${id} ->|[A-Za-z][A-Za-z0-9_-]* -> ${id}:)`, "mu").test(text);
437
+ return retainedTask || retainedGate;
438
+ }
439
+ function validateDeferredMilestone(id, context) {
440
+ const { text, request, model, milestones, blocks, taskIds, diagnostics } = context;
441
+ const block = milestones.get(id);
442
+ const qualified = `${model.documentId}::${id}`;
443
+ if (block === undefined) {
444
+ diagnostics.push(reshapeDiagnostic(`Deferred Milestone ${qualified} does not exist`, "PTPOOL-109"));
445
+ return;
446
+ }
447
+ const protectedEvidence = (strictField(block, "state") ?? "planned") !== "planned" ||
448
+ strictField(block, "deadline") !== null || strictField(block, "when") !== null ||
449
+ milestoneHasProtectedEvidence(text, id) || projectFinish(text) === id;
450
+ if (protectedEvidence)
451
+ diagnostics.push(reshapeDiagnostic(`Deferred Milestone ${qualified} has protected outcome, temporal, or finish evidence`, "PTPOOL-109"));
452
+ if (retainedMilestoneConsumer(text, id, blocks, taskIds))
453
+ diagnostics.push(reshapeDiagnostic(`Deferred Milestone ${qualified} is required by retained strict structure`, "PTPOOL-109"));
454
+ validateDeferralLinkRestoration(request, model, "milestone", qualified, diagnostics);
455
+ }
456
+ function validateDeferralFragment(text, request, model, diagnostics) {
457
+ const fragment = request.strict_fragment;
458
+ if (fragment?.kind !== "defer")
459
+ return;
460
+ if (fragment.task_ids.length + fragment.milestone_ids.length === 0) {
461
+ diagnostics.push(reshapeDiagnostic("Deferral strict fragment is empty", "PTPOOL-109"));
462
+ }
463
+ const taskIds = qualifiedSelection(fragment.task_ids, model.documentId, "Deferred Task", diagnostics);
464
+ const milestoneIds = qualifiedSelection(fragment.milestone_ids, model.documentId, "Deferred Milestone", diagnostics);
465
+ const blocks = scanTemporalDeclarationBlocks(text);
466
+ const tasks = new Map(blocks.filter(({ kind }) => kind === "task").map((block) => [block.id, block]));
467
+ const milestones = new Map(blocks.filter(({ kind }) => kind === "milestone").map((block) => [block.id, block]));
468
+ const context = {
469
+ text, request, model, tasks, milestones, blocks, taskIds, milestoneIds, diagnostics,
470
+ };
471
+ for (const id of taskIds) {
472
+ validateDeferredTask(id, context);
473
+ }
474
+ for (const id of milestoneIds) {
475
+ validateDeferredMilestone(id, context);
476
+ }
477
+ }
478
+ function linkSelectedForDeferral(request, disposition) {
479
+ const fragment = request.strict_fragment;
480
+ if (fragment?.kind !== "defer")
481
+ return false;
482
+ return disposition.strict_kind === "task"
483
+ ? fragment.task_ids.includes(disposition.strict_id)
484
+ : fragment.milestone_ids.includes(disposition.strict_id);
485
+ }
486
+ function linkTargetHasProtectedEvidence(text, disposition, local) {
487
+ const block = scanTemporalDeclarationBlocks(text).find((candidate) => candidate.kind === disposition.strict_kind && candidate.id === local);
488
+ if (block === undefined)
489
+ return false;
490
+ return disposition.strict_kind === "task"
491
+ ? (strictField(block, "status") ?? "planned") !== "planned" || taskHasProtectedEvidence(text, local)
492
+ : (strictField(block, "state") ?? "planned") !== "planned" || milestoneHasProtectedEvidence(text, local);
493
+ }
494
+ function validateProtectedLinkDisposition(text, request, model, disposition, diagnostics) {
495
+ if (disposition.origin_work_id === null || disposition.origin_work_id === disposition.destination_work_id)
496
+ return;
497
+ const local = localId(disposition.strict_id, model.documentId, "Projection-link target", diagnostics);
498
+ if (local === null || linkSelectedForDeferral(request, disposition))
499
+ return;
500
+ if (linkTargetHasProtectedEvidence(text, disposition, local))
501
+ diagnostics.push(reshapeDiagnostic(`Projection link ${disposition.strict_id} cannot be split or reallocated after authoritative evidence exists`, "PTPOOL-109"));
502
+ }
503
+ function validateProtectedLinkReallocation(text, request, model, diagnostics) {
504
+ for (const disposition of request.projection_link_dispositions) {
505
+ validateProtectedLinkDisposition(text, request, model, disposition, diagnostics);
506
+ }
507
+ }
508
+ function workIsArchiveable(work, model) {
509
+ const incidentDependency = model.works.some((candidate) => candidate.qualifiedId !== work.qualifiedId &&
510
+ candidate.dependsOn.some(({ qualifiedId }) => qualifiedId === work.qualifiedId));
511
+ const membership = model.windows.some((window) => window.works.some(({ qualifiedId }) => qualifiedId === work.qualifiedId));
512
+ const ownsMeaning = (work.description?.value ?? "").length > 0 || work.events.length > 0 ||
513
+ work.activities.length > 0 || work.milestoneLinks.length > 0 || work.taskLinks.length > 0;
514
+ return !ownsMeaning && work.dependsOn.length === 0 && !incidentDependency && !membership;
515
+ }
516
+ function validateArchiveIntent(request, model, diagnostics) {
517
+ if (request.intent !== "archive")
518
+ return;
519
+ if (request.removed_work_ids.length === 0 || request.created_works.length > 0 || request.strict_fragment !== null) {
520
+ diagnostics.push(reshapeDiagnostic("Archive requires removed Work and cannot create Work or transfer strict ownership", "PTPOOL-113"));
521
+ }
522
+ const removed = new Set(request.removed_work_ids);
523
+ const affected = new Set(request.affected_work_ids);
524
+ if (removed.size !== affected.size || [...removed].some((id) => !affected.has(id))) {
525
+ diagnostics.push(reshapeDiagnostic("Archive affected Work set must equal the removed Work set", "PTPOOL-113"));
526
+ }
527
+ for (const qualified of removed) {
528
+ const work = model.works.find(({ qualifiedId }) => qualifiedId === qualified);
529
+ if (work === undefined)
530
+ continue;
531
+ if (!workIsArchiveable(work, model)) {
532
+ diagnostics.push(reshapeDiagnostic(`Work ${qualified} is not archiveable in the current source`, "PTPOOL-113"));
533
+ }
534
+ }
535
+ }
536
+ function existingRelations(model) {
537
+ const associations = new Set();
538
+ const projectionLinks = new Set();
539
+ const dependencies = new Set();
540
+ const memberships = new Set();
541
+ for (const work of model.works) {
542
+ for (const event of work.events)
543
+ associations.add(`event|${event.qualifiedId}|${work.qualifiedId}`);
544
+ for (const activity of work.activities)
545
+ associations.add(`activity|${activity.qualifiedId}|${work.qualifiedId}`);
546
+ for (const milestone of work.milestoneLinks)
547
+ projectionLinks.add(`milestone|${milestone.qualifiedId}|${work.qualifiedId}`);
548
+ for (const task of work.taskLinks)
549
+ projectionLinks.add(`task|${task.qualifiedId}|${work.qualifiedId}`);
550
+ for (const dependency of work.dependsOn)
551
+ dependencies.add(`${work.qualifiedId}|${dependency.qualifiedId}`);
552
+ }
553
+ for (const window of model.windows) {
554
+ for (const work of window.works)
555
+ memberships.add(`${window.qualifiedId}|${work.qualifiedId}`);
556
+ }
557
+ return Object.freeze({ associations, projectionLinks, dependencies, memberships });
558
+ }
559
+ function entityDispositionMap(request, dispositions, diagnostics) {
560
+ const result = new Map();
561
+ for (const disposition of dispositions) {
562
+ const projected = request.strict_fragment?.kind === "project" && (disposition.entity_kind === "event"
563
+ ? request.strict_fragment.event_ids.includes(disposition.entity_id)
564
+ : request.strict_fragment.activity_ids.includes(disposition.entity_id));
565
+ const deferred = request.strict_fragment?.kind === "defer" && (disposition.entity_kind === "event"
566
+ ? request.strict_fragment.milestone_ids.includes(disposition.entity_id)
567
+ : request.strict_fragment.task_ids.includes(disposition.entity_id));
568
+ const expected = projected ? "project" : deferred ? "defer" : "retain";
569
+ if (disposition.action !== expected) {
570
+ diagnostics.push(reshapeDiagnostic(`Planning entity action ${disposition.action} belongs to a later Core`));
571
+ }
572
+ result.set(`${disposition.entity_kind}:${disposition.entity_id}`, disposition);
573
+ }
574
+ return result;
575
+ }
576
+ function relationWorkLocal(key, position) {
577
+ const qualified = key.split("|")[position];
578
+ return qualified.slice(qualified.indexOf("::") + 2);
579
+ }
580
+ function unaffectedRelations(relations, affected, workPosition) {
581
+ return new Set([...relations].filter((key) => !affected.has(relationWorkLocal(key, workPosition))));
582
+ }
583
+ function requireAffectedRelationDisposition(relations, affected, seen, label, workPosition, diagnostics) {
584
+ for (const key of relations) {
585
+ if (affected.has(relationWorkLocal(key, workPosition)) && !seen.has(key)) {
586
+ diagnostics.push(reshapeDiagnostic(`Affected ${label} ${key} has no disposition`));
587
+ }
588
+ }
589
+ }
590
+ function applyAssociationDisposition(request, disposition, entities, context, seenOrigins, finalKeys) {
591
+ const { model, works, affected, relations, diagnostics } = context;
592
+ const entitySet = disposition.entity_kind === "event"
593
+ ? new Set(model.events.map(({ qualifiedId }) => qualifiedId))
594
+ : new Set(model.activities.map(({ qualifiedId }) => qualifiedId));
595
+ if (request.strict_fragment?.kind === "defer") {
596
+ for (const id of disposition.entity_kind === "event"
597
+ ? request.strict_fragment.milestone_ids
598
+ : request.strict_fragment.task_ids)
599
+ entitySet.add(id);
600
+ }
601
+ if (!entitySet.has(disposition.entity_id))
602
+ diagnostics.push(reshapeDiagnostic(`Association entity ${disposition.entity_id} does not exist`));
603
+ if (!entities.has(`${disposition.entity_kind}:${disposition.entity_id}`))
604
+ diagnostics.push(reshapeDiagnostic(`Association entity ${disposition.entity_id} has no entity disposition`));
605
+ if (disposition.origin_work_id !== null) {
606
+ const key = `${disposition.entity_kind}|${disposition.entity_id}|${disposition.origin_work_id}`;
607
+ if (!relations.associations.has(key))
608
+ diagnostics.push(reshapeDiagnostic(`Association origin ${key} does not exist`));
609
+ if (seenOrigins.has(key))
610
+ diagnostics.push(reshapeDiagnostic(`Association origin ${key} is disposed more than once`));
611
+ seenOrigins.add(key);
612
+ const local = localId(disposition.origin_work_id, model.documentId, "Association origin Work", diagnostics);
613
+ if (local !== null && !affected.has(local))
614
+ diagnostics.push(reshapeDiagnostic(`Association origin Work ${disposition.origin_work_id} is not affected`));
615
+ }
616
+ if (disposition.destination_work_id !== null) {
617
+ const local = localId(disposition.destination_work_id, model.documentId, "Association destination Work", diagnostics);
618
+ if (local !== null && !works.has(local))
619
+ diagnostics.push(reshapeDiagnostic(`Association destination Work ${disposition.destination_work_id} is not final`));
620
+ finalKeys.add(`${disposition.entity_kind}|${disposition.entity_id}|${disposition.destination_work_id}`);
621
+ }
622
+ }
623
+ function rebuildAssociations(works, finalKeys) {
624
+ for (const work of works.values()) {
625
+ work.events.clear();
626
+ work.activities.clear();
627
+ }
628
+ for (const key of finalKeys) {
629
+ const [kind, entityQualified, workQualified] = key.split("|");
630
+ const work = works.get(workQualified.slice(workQualified.indexOf("::") + 2));
631
+ if (work === undefined)
632
+ continue;
633
+ const entityId = entityQualified.slice(entityQualified.indexOf("::") + 2);
634
+ (kind === "event" ? work.events : work.activities).add(entityId);
635
+ }
636
+ }
637
+ function applyAssociations(request, context) {
638
+ const { works, affected, relations, diagnostics } = context;
639
+ const entities = entityDispositionMap(request, request.planning_entity_dispositions, diagnostics);
640
+ const seenOrigins = new Set();
641
+ const finalKeys = unaffectedRelations(relations.associations, affected, 2);
642
+ for (const disposition of request.association_dispositions) {
643
+ applyAssociationDisposition(request, disposition, entities, context, seenOrigins, finalKeys);
644
+ }
645
+ requireAffectedRelationDisposition(relations.associations, affected, seenOrigins, "association", 2, diagnostics);
646
+ rebuildAssociations(works, finalKeys);
647
+ }
648
+ function applyProjectionLinkDisposition(disposition, strict, context, seenOrigins, finalKeys) {
649
+ const { model, works, affected, relations, diagnostics } = context;
650
+ const ids = disposition.strict_kind === "milestone" ? strict.milestones : strict.tasks;
651
+ if (!ids.has(disposition.strict_id))
652
+ diagnostics.push(reshapeDiagnostic(`Projection-link entity ${disposition.strict_id} does not exist`));
653
+ if (disposition.origin_work_id !== null) {
654
+ const key = `${disposition.strict_kind}|${disposition.strict_id}|${disposition.origin_work_id}`;
655
+ if (!relations.projectionLinks.has(key))
656
+ diagnostics.push(reshapeDiagnostic(`Projection-link origin ${key} does not exist`));
657
+ if (seenOrigins.has(key))
658
+ diagnostics.push(reshapeDiagnostic(`Projection-link origin ${key} is disposed more than once`));
659
+ seenOrigins.add(key);
660
+ const local = localId(disposition.origin_work_id, model.documentId, "Projection-link origin Work", diagnostics);
661
+ if (local !== null && !affected.has(local))
662
+ diagnostics.push(reshapeDiagnostic(`Projection-link origin Work ${disposition.origin_work_id} is not affected`));
663
+ }
664
+ if (disposition.destination_work_id !== null) {
665
+ const local = localId(disposition.destination_work_id, model.documentId, "Projection-link destination Work", diagnostics);
666
+ if (local !== null && !works.has(local))
667
+ diagnostics.push(reshapeDiagnostic(`Projection-link destination Work ${disposition.destination_work_id} is not final`));
668
+ finalKeys.add(`${disposition.strict_kind}|${disposition.strict_id}|${disposition.destination_work_id}`);
669
+ }
670
+ }
671
+ function rebuildProjectionLinks(works, finalKeys) {
672
+ for (const work of works.values()) {
673
+ work.milestoneLinks.clear();
674
+ work.taskLinks.clear();
675
+ }
676
+ for (const key of finalKeys) {
677
+ const [kind, strictQualified, workQualified] = key.split("|");
678
+ const work = works.get(workQualified.slice(workQualified.indexOf("::") + 2));
679
+ if (work === undefined)
680
+ continue;
681
+ const strictLocal = strictQualified.slice(strictQualified.indexOf("::") + 2);
682
+ (kind === "milestone" ? work.milestoneLinks : work.taskLinks).add(strictLocal);
683
+ }
684
+ }
685
+ function applyProjectionLinks(request, context) {
686
+ const { text, model, works, affected, relations, diagnostics } = context;
687
+ const strict = strictQualifiedIds(text, model.documentId, request);
688
+ const seenOrigins = new Set();
689
+ const finalKeys = unaffectedRelations(relations.projectionLinks, affected, 2);
690
+ for (const disposition of request.projection_link_dispositions) {
691
+ applyProjectionLinkDisposition(disposition, strict, context, seenOrigins, finalKeys);
692
+ }
693
+ requireAffectedRelationDisposition(relations.projectionLinks, affected, seenOrigins, "projection link", 2, diagnostics);
694
+ rebuildProjectionLinks(works, finalKeys);
695
+ }
696
+ function dependencyFinalPair(disposition) {
697
+ if (disposition.action === "retain" || disposition.action === "create") {
698
+ return [disposition.dependent_work_id, disposition.prerequisite_work_id];
699
+ }
700
+ if (disposition.action === "rebind")
701
+ return [disposition.final_dependent_work_id, disposition.final_prerequisite_work_id];
702
+ return null;
703
+ }
704
+ function unaffectedDependencies(relations, affected) {
705
+ return new Set([...relations].filter((pair) => {
706
+ const [dependent, prerequisite] = pair.split("|");
707
+ return !affected.has(relationWorkLocal(dependent, 0)) &&
708
+ !affected.has(relationWorkLocal(prerequisite, 0));
709
+ }));
710
+ }
711
+ function planningOwners(request, context) {
712
+ const { text, model } = context;
713
+ const strict = strictQualifiedIds(text, model.documentId, request);
714
+ return new Set([
715
+ ...model.events.map(({ qualifiedId }) => qualifiedId),
716
+ ...model.activities.map(({ qualifiedId }) => qualifiedId),
717
+ ...(request.strict_fragment?.kind === "defer"
718
+ ? [...request.strict_fragment.milestone_ids, ...request.strict_fragment.task_ids]
719
+ : []),
720
+ ...strict.milestones,
721
+ ...strict.tasks,
722
+ ]);
723
+ }
724
+ function applyDependencyDisposition(disposition, owners, context, seen, finalPairs) {
725
+ const { model, works, relations, diagnostics } = context;
726
+ const originKey = `${disposition.dependent_work_id}|${disposition.prerequisite_work_id}`;
727
+ if (disposition.action === "create") {
728
+ if (relations.dependencies.has(originKey))
729
+ diagnostics.push(reshapeDiagnostic(`Created dependency ${originKey} already exists`));
730
+ }
731
+ else {
732
+ if (!relations.dependencies.has(originKey))
733
+ diagnostics.push(reshapeDiagnostic(`Dependency origin ${originKey} does not exist`));
734
+ seen.add(originKey);
735
+ }
736
+ for (const owner of disposition.represented_by ?? []) {
737
+ if (!owners.has(owner))
738
+ diagnostics.push(reshapeDiagnostic(`Represented dependency owner ${owner} does not exist`));
739
+ }
740
+ const pair = dependencyFinalPair(disposition);
741
+ if (pair === null)
742
+ return;
743
+ const dependent = localId(pair[0], model.documentId, "Final dependent Work", diagnostics);
744
+ const prerequisite = localId(pair[1], model.documentId, "Final prerequisite Work", diagnostics);
745
+ if (dependent === null || prerequisite === null)
746
+ return;
747
+ if (!works.has(dependent) || !works.has(prerequisite))
748
+ diagnostics.push(reshapeDiagnostic("Final dependency endpoint does not exist"));
749
+ if (dependent === prerequisite)
750
+ diagnostics.push(reshapeDiagnostic("Rebound dependency cannot become a self-dependency"));
751
+ finalPairs.add(`${pair[0]}|${pair[1]}`);
752
+ }
753
+ function requireIncidentDependencyDispositions(relations, affected, seen, diagnostics) {
754
+ for (const pair of relations) {
755
+ const [dependent, prerequisite] = pair.split("|");
756
+ const incident = affected.has(relationWorkLocal(dependent, 0)) ||
757
+ affected.has(relationWorkLocal(prerequisite, 0));
758
+ if (incident && !seen.has(pair))
759
+ diagnostics.push(reshapeDiagnostic(`Incident dependency ${pair} has no disposition`));
760
+ }
761
+ }
762
+ function rebuildDependencies(works, finalPairs) {
763
+ for (const work of works.values())
764
+ work.dependsOn.clear();
765
+ for (const pair of finalPairs) {
766
+ const [dependentQualified, prerequisiteQualified] = pair.split("|");
767
+ const dependent = relationWorkLocal(dependentQualified, 0);
768
+ const prerequisite = relationWorkLocal(prerequisiteQualified, 0);
769
+ works.get(dependent)?.dependsOn.add(prerequisite);
770
+ }
771
+ }
772
+ function applyDependencies(request, context) {
773
+ const { works, affected, relations, diagnostics } = context;
774
+ const owners = planningOwners(request, context);
775
+ const seen = new Set();
776
+ const finalPairs = unaffectedDependencies(relations.dependencies, affected);
777
+ for (const disposition of request.dependency_dispositions) {
778
+ applyDependencyDisposition(disposition, owners, context, seen, finalPairs);
779
+ }
780
+ requireIncidentDependencyDispositions(relations.dependencies, affected, seen, diagnostics);
781
+ rebuildDependencies(works, finalPairs);
782
+ }
783
+ function mutableWindow(window) {
784
+ return {
785
+ source: window,
786
+ id: window.id,
787
+ qualifiedId: window.qualifiedId,
788
+ title: window.title,
789
+ objective: window.objective,
790
+ start: window.start?.sourceText ?? null,
791
+ end: window.end?.sourceText ?? null,
792
+ works: new Set(window.works.map(({ id }) => id)),
793
+ changed: false,
794
+ };
795
+ }
796
+ function applyMembershipDisposition(disposition, context) {
797
+ const { model, works, affected, relations, windows, seen, diagnostics } = context;
798
+ const windowId = localId(disposition.window_id, model.documentId, "Window", diagnostics);
799
+ const origin = localId(disposition.origin_work_id, model.documentId, "Window origin Work", diagnostics);
800
+ const destination = disposition.destination_work_id === null
801
+ ? null
802
+ : localId(disposition.destination_work_id, model.documentId, "Window destination Work", diagnostics);
803
+ if (windowId === null || origin === null || (disposition.destination_work_id !== null && destination === null))
804
+ return;
805
+ const key = `${disposition.window_id}|${disposition.origin_work_id}`;
806
+ if (!relations.memberships.has(key))
807
+ diagnostics.push(reshapeDiagnostic(`Window membership origin ${key} does not exist`));
808
+ if (!affected.has(origin))
809
+ diagnostics.push(reshapeDiagnostic(`Window membership origin ${disposition.origin_work_id} is not affected`));
810
+ const window = windows.get(windowId);
811
+ if (window === undefined)
812
+ diagnostics.push(reshapeDiagnostic(`Window ${disposition.window_id} does not exist`));
813
+ if (destination !== null && !works.has(destination))
814
+ diagnostics.push(reshapeDiagnostic(`Window destination Work ${disposition.destination_work_id} is not final`));
815
+ window?.works.delete(origin);
816
+ if (destination !== null)
817
+ window?.works.add(destination);
818
+ if (window !== undefined)
819
+ window.changed = true;
820
+ seen.add(key);
821
+ }
822
+ function requireAffectedMembershipDispositions(relations, affected, seen, diagnostics) {
823
+ for (const key of relations.memberships) {
824
+ const origin = key.split("|")[1];
825
+ if (affected.has(origin.slice(origin.indexOf("::") + 2)) && !seen.has(key)) {
826
+ diagnostics.push(reshapeDiagnostic(`Affected Window membership ${key} has no disposition`));
827
+ }
828
+ }
829
+ }
830
+ function applyMemberships(request, model, works, affected, relations, diagnostics) {
831
+ const windows = new Map();
832
+ for (const window of model.windows)
833
+ windows.set(window.id, mutableWindow(window));
834
+ const seen = new Set();
835
+ const context = {
836
+ model, works, affected, relations, windows, seen, diagnostics,
837
+ };
838
+ for (const disposition of request.window_membership_dispositions) {
839
+ applyMembershipDisposition(disposition, context);
840
+ }
841
+ requireAffectedMembershipDispositions(relations, affected, seen, diagnostics);
842
+ return windows;
843
+ }
844
+ function windowSnapshot(window, documentId) {
845
+ return Object.freeze({
846
+ kind: "persisted",
847
+ qualifiedId: window.qualifiedId,
848
+ title: window.title,
849
+ objective: window.objective,
850
+ start: window.start,
851
+ end: window.end,
852
+ workIds: Object.freeze([...window.works].sort().map((id) => `${documentId}::${id}`)),
853
+ });
854
+ }
855
+ function carryTargetWindow(target, model, windows, diagnostics) {
856
+ const targetId = localId(target.window_id, model.documentId, "carry-over Window", diagnostics);
857
+ const existing = targetId === null ? undefined : windows.get(targetId);
858
+ if (target.kind === "existing" && existing === undefined) {
859
+ diagnostics.push(reshapeDiagnostic(`Carry-over Window ${target.window_id} does not exist`));
860
+ }
861
+ if (target.kind === "new" && existing !== undefined) {
862
+ diagnostics.push(reshapeDiagnostic(`New carry-over Window ${target.window_id} already exists`));
863
+ }
864
+ return targetId === null ? null : Object.freeze({ localId: targetId, existing });
865
+ }
866
+ function newCarryWindow(target, local, works) {
867
+ return {
868
+ source: null,
869
+ id: local,
870
+ qualifiedId: target.window_id,
871
+ title: target.title,
872
+ objective: target.objective,
873
+ start: target.start,
874
+ end: target.end,
875
+ works: new Set(works),
876
+ changed: true,
877
+ };
878
+ }
879
+ function validateFinalWindows(windows, diagnostics) {
880
+ for (const window of windows.values()) {
881
+ if (window.works.size === 0)
882
+ diagnostics.push(reshapeDiagnostic(`Persisted Window ${window.qualifiedId} cannot become empty`));
883
+ }
884
+ }
885
+ function compositeCarryWorkIds(close, model, works, source, diagnostics) {
886
+ const result = [];
887
+ for (const qualified of close.carry_over_work_ids) {
888
+ const id = localId(qualified, model.documentId, "carry-over Work", diagnostics);
889
+ if (id === null)
890
+ continue;
891
+ result.push(id);
892
+ if (!works.has(id))
893
+ diagnostics.push(reshapeDiagnostic(`Carry-over Work ${qualified} is not final`));
894
+ if (source !== undefined && !source.works.has(id))
895
+ diagnostics.push(reshapeDiagnostic(`Carry-over Work ${qualified} is not selected by ${close.window_id}`));
896
+ }
897
+ return Object.freeze(result);
898
+ }
899
+ function applyCompositeCarryTarget(target, resolved, carryLocal, windows) {
900
+ if (target?.kind === "existing" && resolved?.existing !== undefined) {
901
+ for (const id of carryLocal)
902
+ resolved.existing.works.add(id);
903
+ resolved.existing.changed = true;
904
+ }
905
+ if (target?.kind !== "new" || resolved === null)
906
+ return Object.freeze([]);
907
+ windows.set(resolved.localId, newCarryWindow(target, resolved.localId, carryLocal));
908
+ return Object.freeze([resolved.localId]);
909
+ }
910
+ function compositeCloseReport(close, source, model, existingWorks) {
911
+ if (source === undefined)
912
+ return null;
913
+ const target = close.carry_over_target;
914
+ return Object.freeze({
915
+ removedWindow: windowSnapshot(source, model.documentId),
916
+ objectiveDisposition: "discard",
917
+ carryOver: Object.freeze(close.carry_over_work_ids.map((workId) => Object.freeze({
918
+ workId,
919
+ targetWindowId: target?.window_id ?? "",
920
+ status: existingWorks.has(workId.slice(workId.indexOf("::") + 2)) ? "already_selected" : "selected",
921
+ }))),
922
+ targetCreated: target?.kind === "new",
923
+ });
924
+ }
925
+ function applyWindowClose(request, model, works, windows, diagnostics) {
926
+ const close = request.window_close;
927
+ if (close === null) {
928
+ validateFinalWindows(windows, diagnostics);
929
+ return Object.freeze({ report: null, createdWindowIds: Object.freeze([]) });
930
+ }
931
+ const sourceId = localId(close.window_id, model.documentId, "closed Window", diagnostics);
932
+ const source = sourceId === null ? undefined : windows.get(sourceId);
933
+ if (source === undefined)
934
+ diagnostics.push(reshapeDiagnostic(`Window ${close.window_id} does not exist`));
935
+ const carryLocal = compositeCarryWorkIds(close, model, works, source, diagnostics);
936
+ const target = close.carry_over_target;
937
+ if (target?.window_id === close.window_id)
938
+ diagnostics.push(reshapeDiagnostic("Window cannot carry over to itself"));
939
+ const resolved = target === null ? null : carryTargetWindow(target, model, windows, diagnostics);
940
+ const existingWorks = new Set(resolved?.existing?.works ?? []);
941
+ const createdWindowIds = applyCompositeCarryTarget(target, resolved, carryLocal, windows);
942
+ if (sourceId !== null)
943
+ windows.delete(sourceId);
944
+ validateFinalWindows(windows, diagnostics);
945
+ return Object.freeze({
946
+ report: compositeCloseReport(close, source, model, existingWorks),
947
+ createdWindowIds,
948
+ });
949
+ }
950
+ function validateFinalOrder(request, model, works, diagnostics) {
951
+ if (request.final_work_order.length === 0) {
952
+ if (request.created_works.length > 0 || request.removed_work_ids.length > 0) {
953
+ diagnostics.push(reshapeDiagnostic("final_work_order is required when Work boundaries change"));
954
+ }
955
+ return Object.freeze(model.workOrder.map(({ id }) => id));
956
+ }
957
+ const order = [];
958
+ for (const qualified of request.final_work_order) {
959
+ const id = localId(qualified, model.documentId, "Final Work order", diagnostics);
960
+ if (id !== null)
961
+ order.push(id);
962
+ }
963
+ const expected = [...works.keys()].sort();
964
+ const actual = [...order].sort();
965
+ if (expected.length !== actual.length || expected.some((id, index) => id !== actual[index])) {
966
+ diagnostics.push(reshapeDiagnostic("final_work_order must contain every final Work exactly once"));
967
+ }
968
+ for (const created of request.created_works) {
969
+ const boundary = createdBoundaryIds(created, model, diagnostics);
970
+ if (boundary === null)
971
+ continue;
972
+ const [id, anchor] = boundary;
973
+ const position = order.indexOf(id);
974
+ const anchorPosition = anchor === null ? -1 : order.indexOf(anchor);
975
+ if (position <= anchorPosition)
976
+ diagnostics.push(reshapeDiagnostic(`Created Work ${created.work_id} does not follow its insertion anchor`));
977
+ }
978
+ return Object.freeze(order);
979
+ }
980
+ function renderReferences(lines, name, values) {
981
+ if (values.size === 0)
982
+ return;
983
+ lines.push(` ${name}:`, ...[...values].sort().map((id) => ` ${id}`));
984
+ }
985
+ function renderWork(work, lineEnding) {
986
+ const lines = [`work ${work.id}:`, ` title ${JSON.stringify(work.title)}`];
987
+ if (work.description.length > 0)
988
+ lines.push(` description ${JSON.stringify(work.description)}`);
989
+ renderReferences(lines, "events", work.events);
990
+ renderReferences(lines, "activities", work.activities);
991
+ renderReferences(lines, "milestone_links", work.milestoneLinks);
992
+ renderReferences(lines, "task_links", work.taskLinks);
993
+ renderReferences(lines, "depends_on", work.dependsOn);
994
+ return `${lines.join(lineEnding)}${lineEnding}`;
995
+ }
996
+ function renderWindow(window, lineEnding) {
997
+ const lines = [
998
+ `window ${window.id}:`,
999
+ ` title ${JSON.stringify(window.title)}`,
1000
+ ` objective ${JSON.stringify(window.objective)}`,
1001
+ ];
1002
+ if (window.start !== null)
1003
+ lines.push(` start ${window.start}`);
1004
+ if (window.end !== null)
1005
+ lines.push(` end ${window.end}`);
1006
+ lines.push(" works:", ...[...window.works].sort().map((id) => ` ${id}`));
1007
+ return `${lines.join(lineEnding)}${lineEnding}`;
1008
+ }
1009
+ function lineEnding(text) {
1010
+ return text.includes("\r\n") ? "\r\n" : "\n";
1011
+ }
1012
+ function planningInsertionOffset(text, blocks) {
1013
+ const firstWork = blocks.find(({ kind }) => kind === "work");
1014
+ if (firstWork !== undefined)
1015
+ return firstWork.span.start.offset;
1016
+ const firstPlanning = blocks[0];
1017
+ if (firstPlanning !== undefined)
1018
+ return firstPlanning.span.start.offset;
1019
+ const firstStrict = scanTemporalDeclarationBlocks(text).find(({ kind }) => kind === "milestone" || kind === "task");
1020
+ return firstStrict?.span.start.offset ?? text.length;
1021
+ }
1022
+ function declarationBody(lines, removedFields) {
1023
+ const result = [];
1024
+ let removing = false;
1025
+ for (const line of lines) {
1026
+ const field = fieldLine(line);
1027
+ if (field !== null)
1028
+ removing = removedFields.has(field.name);
1029
+ if (!removing)
1030
+ result.push(line.text);
1031
+ }
1032
+ while (result.at(-1) === "")
1033
+ result.pop();
1034
+ return Object.freeze(result);
1035
+ }
1036
+ function renderedTransferDeclaration(header, body, ending) {
1037
+ return `${[header, ...body].join(ending)}${ending}`;
1038
+ }
1039
+ function projectionTransferEdits(text, request, model, ending) {
1040
+ const fragment = request.strict_fragment;
1041
+ if (fragment?.kind !== "project")
1042
+ return Object.freeze({ removals: Object.freeze([]), insertion: "" });
1043
+ const eventIds = qualifiedSelection(fragment.event_ids, model.documentId, "Projected Event", []);
1044
+ const activityIds = qualifiedSelection(fragment.activity_ids, model.documentId, "Projected Activity", []);
1045
+ const projectedIncoming = new Set(model.activities
1046
+ .filter(({ id }) => activityIds.has(id))
1047
+ .map(({ to }) => to.id));
1048
+ const removals = [];
1049
+ const declarations = [];
1050
+ for (const block of scanPlanningDeclarationBlocks(text)) {
1051
+ if (block.id === null)
1052
+ continue;
1053
+ if (block.kind === "event" && eventIds.has(block.id)) {
1054
+ removals.push({ startOffset: block.span.start.offset, endOffset: block.span.end.offset, replacement: "" });
1055
+ const body = [...declarationBody(block.lines, new Set(["source"]))];
1056
+ if (!projectedIncoming.has(block.id)) {
1057
+ const tags = body.findIndex((line) => /^ tags /u.test(line));
1058
+ body.splice(tags === -1 ? body.length : tags, 0, " state reached");
1059
+ }
1060
+ declarations.push(renderedTransferDeclaration(`milestone ${block.id}:`, body, ending));
1061
+ }
1062
+ if (block.kind === "activity" && activityIds.has(block.id)) {
1063
+ removals.push({ startOffset: block.span.start.offset, endOffset: block.span.end.offset, replacement: "" });
1064
+ declarations.push(renderedTransferDeclaration(`task ${block.id} ${block.from} -> ${block.to}:`, declarationBody(block.lines, new Set()), ending));
1065
+ }
1066
+ }
1067
+ return Object.freeze({ removals: Object.freeze(removals), insertion: declarations.join(ending) });
1068
+ }
1069
+ function deferralTransferEdits(text, request, model, ending) {
1070
+ const fragment = request.strict_fragment;
1071
+ if (fragment?.kind !== "defer")
1072
+ return Object.freeze({ removals: Object.freeze([]), insertion: "" });
1073
+ const taskIds = qualifiedSelection(fragment.task_ids, model.documentId, "Deferred Task", []);
1074
+ const milestoneIds = qualifiedSelection(fragment.milestone_ids, model.documentId, "Deferred Milestone", []);
1075
+ const removals = [];
1076
+ const events = [];
1077
+ const activities = [];
1078
+ for (const block of scanTemporalDeclarationBlocks(text)) {
1079
+ if (block.kind === "milestone" && milestoneIds.has(block.id)) {
1080
+ removals.push({ startOffset: block.span.start.offset, endOffset: block.span.end.offset, replacement: "" });
1081
+ events.push(renderedTransferDeclaration(`event ${block.id}:`, declarationBody(block.lines, new Set(["state", "deadline", "when"])), ending));
1082
+ }
1083
+ if (block.kind === "task" && taskIds.has(block.id)) {
1084
+ const endpoints = taskEndpoints(block);
1085
+ removals.push({ startOffset: block.span.start.offset, endOffset: block.span.end.offset, replacement: "" });
1086
+ activities.push(renderedTransferDeclaration(`activity ${block.id} ${endpoints[0]} -> ${endpoints[1]}:`, declarationBody(block.lines, new Set(["status", "blocked_reason"])), ending));
1087
+ }
1088
+ }
1089
+ return Object.freeze({ removals: Object.freeze(removals), insertion: [...events, ...activities].join(ending) });
1090
+ }
1091
+ function addCandidateReplacement(replacements, edit) {
1092
+ const existing = replacements.get(edit.startOffset);
1093
+ if (existing === undefined)
1094
+ replacements.set(edit.startOffset, edit);
1095
+ else
1096
+ replacements.set(edit.startOffset, {
1097
+ startOffset: edit.startOffset,
1098
+ endOffset: Math.max(existing.endOffset, edit.endOffset),
1099
+ replacement: `${edit.replacement}${existing.replacement}`,
1100
+ });
1101
+ }
1102
+ function candidateReplacements(blocks, state, ending) {
1103
+ const replacements = new Map();
1104
+ for (const block of blocks) {
1105
+ addWorkCandidateReplacement(replacements, block, state, ending);
1106
+ addWindowCandidateReplacement(replacements, block, state, ending);
1107
+ addOrderCandidateReplacement(replacements, block, state, ending);
1108
+ }
1109
+ return replacements;
1110
+ }
1111
+ function addWorkCandidateReplacement(replacements, block, state, ending) {
1112
+ if (block.kind !== "work" || block.id === null || !state.affectedLocalIds.has(block.id))
1113
+ return;
1114
+ const work = state.works.get(block.id);
1115
+ replacements.set(block.span.start.offset, {
1116
+ startOffset: block.span.start.offset,
1117
+ endOffset: block.span.end.offset,
1118
+ replacement: work === undefined ? "" : renderWork(work, ending),
1119
+ });
1120
+ }
1121
+ function addWindowCandidateReplacement(replacements, block, state, ending) {
1122
+ if (block.kind !== "window" || block.id === null)
1123
+ return;
1124
+ const window = state.windows.get(block.id);
1125
+ if (window !== undefined && !window.changed)
1126
+ return;
1127
+ replacements.set(block.span.start.offset, {
1128
+ startOffset: block.span.start.offset,
1129
+ endOffset: block.span.end.offset,
1130
+ replacement: window === undefined ? "" : renderWindow(window, ending),
1131
+ });
1132
+ }
1133
+ function addOrderCandidateReplacement(replacements, block, state, ending) {
1134
+ if (block.kind !== "work_order" || !state.writeWorkOrder)
1135
+ return;
1136
+ replacements.set(block.span.start.offset, {
1137
+ startOffset: block.span.start.offset,
1138
+ endOffset: block.span.end.offset,
1139
+ replacement: state.order.length === 0
1140
+ ? ""
1141
+ : `work_order:${ending}${state.order.map((id) => ` ${id}`).join(ending)}${ending}`,
1142
+ });
1143
+ }
1144
+ function candidateInsertion(blocks, state, ending) {
1145
+ let insertion = [
1146
+ ...state.createdWindowIds.map((id) => renderWindow(state.windows.get(id), ending)),
1147
+ ...state.createdLocalIds.map((id) => renderWork(state.works.get(id), ending)),
1148
+ ].join(ending);
1149
+ const hasOrder = blocks.some(({ kind }) => kind === "work_order");
1150
+ if (!hasOrder && state.order.length > 0) {
1151
+ if (insertion.length > 0)
1152
+ insertion += ending;
1153
+ insertion += `work_order:${ending}${state.order.map((id) => ` ${id}`).join(ending)}${ending}${ending}`;
1154
+ }
1155
+ return insertion;
1156
+ }
1157
+ function candidateText(text, model, state, request) {
1158
+ const ending = lineEnding(text);
1159
+ const blocks = scanPlanningDeclarationBlocks(text);
1160
+ const replacements = candidateReplacements(blocks, state, ending);
1161
+ const projected = projectionTransferEdits(text, request, model, ending);
1162
+ const deferred = deferralTransferEdits(text, request, model, ending);
1163
+ for (const edit of [...projected.removals, ...deferred.removals])
1164
+ addCandidateReplacement(replacements, edit);
1165
+ const insertion = [candidateInsertion(blocks, state, ending), projected.insertion, deferred.insertion]
1166
+ .filter((value) => value.length > 0).join(ending);
1167
+ if (insertion.length > 0) {
1168
+ const offset = planningInsertionOffset(text, blocks);
1169
+ addCandidateReplacement(replacements, { startOffset: offset, endOffset: offset, replacement: insertion });
1170
+ }
1171
+ const edits = normalizeTextEdits(text, [...replacements.values()], "planning reshape candidate");
1172
+ return applyTextEdits(text, edits);
1173
+ }
1174
+ function buildState(text, request, model, diagnostics) {
1175
+ const originalIds = new Set(model.works.map(({ id }) => id));
1176
+ const works = new Map(model.works.map((work) => [work.id, workState(work)]));
1177
+ const affected = affectedSet(request, model, diagnostics);
1178
+ const created = createdWorks(request, model, affected, works, diagnostics);
1179
+ const removed = removedWorks(request, model, affected, originalIds, diagnostics);
1180
+ validateAffectedIdentities(affected, works, diagnostics);
1181
+ for (const disposition of request.work_title_dispositions ?? []) {
1182
+ const id = localId(disposition.work_id, model.documentId, "Work title disposition", diagnostics);
1183
+ const work = id === null ? undefined : works.get(id);
1184
+ if (id !== null && !affected.has(id))
1185
+ diagnostics.push(reshapeDiagnostic(`Work title disposition ${disposition.work_id} is not affected`));
1186
+ if (work === undefined || removed.has(id ?? ""))
1187
+ diagnostics.push(reshapeDiagnostic(`Work title disposition ${disposition.work_id} is not a final Work`));
1188
+ else
1189
+ work.title = disposition.title;
1190
+ }
1191
+ const descriptions = descriptionRows(request, model, affected, works, removed, diagnostics);
1192
+ for (const id of removed)
1193
+ works.delete(id);
1194
+ const relations = existingRelations(model);
1195
+ const relationContext = {
1196
+ text,
1197
+ model,
1198
+ works,
1199
+ affected,
1200
+ relations,
1201
+ diagnostics,
1202
+ };
1203
+ applyAssociations(request, relationContext);
1204
+ applyProjectionLinks(request, relationContext);
1205
+ applyDependencies(request, relationContext);
1206
+ const windows = applyMemberships(request, model, works, affected, relations, diagnostics);
1207
+ const windowClose = applyWindowClose(request, model, works, windows, diagnostics);
1208
+ const order = validateFinalOrder(request, model, works, diagnostics);
1209
+ return Object.freeze({
1210
+ works,
1211
+ windows,
1212
+ order,
1213
+ affectedLocalIds: affected,
1214
+ removedLocalIds: removed,
1215
+ createdLocalIds: created,
1216
+ createdWindowIds: windowClose.createdWindowIds,
1217
+ writeWorkOrder: request.final_work_order.length > 0 || created.length > 0 || removed.size > 0,
1218
+ beforeDescriptions: descriptions.before,
1219
+ afterDescriptions: descriptions.after,
1220
+ windowCloseReport: windowClose.report,
1221
+ });
1222
+ }
1223
+ function ownerFromSource(text) {
1224
+ const project = scanTemporalDeclarationBlocks(text).find(({ kind }) => kind === "project");
1225
+ return project?.lines.map(fieldLine).find((field) => field?.name === "dag_owner")?.rawValue ?? "user";
1226
+ }
1227
+ function authorityImpact(text, request, changed) {
1228
+ const affectsDag = request.strict_fragment !== null;
1229
+ return Object.freeze({
1230
+ affectedScopes: affectsDag
1231
+ ? Object.freeze(["dag"])
1232
+ : Object.freeze([]),
1233
+ requiredOwner: affectsDag ? ownerFromSource(text) : null,
1234
+ userResponseRequired: affectsDag && changed,
1235
+ });
1236
+ }
1237
+ function failedAudit(sourceDigest, documentId, diagnostics) {
1238
+ return Object.freeze({
1239
+ ok: false,
1240
+ documentId,
1241
+ sourceDigest,
1242
+ normalizedRequest: null,
1243
+ canonicalRequestUtf8: null,
1244
+ preflightHash: null,
1245
+ candidateDigest: null,
1246
+ candidateText: null,
1247
+ changed: false,
1248
+ edits: Object.freeze([]),
1249
+ beforeDescriptions: Object.freeze([]),
1250
+ afterDescriptions: Object.freeze([]),
1251
+ windowCloseReport: null,
1252
+ diagnostics: Object.freeze(diagnostics),
1253
+ });
1254
+ }
1255
+ function residualDescriptionWarnings(request, state) {
1256
+ const acknowledged = new Set(request.add_residual_description.map(({ work_id }) => work_id));
1257
+ const before = new Map(state.beforeDescriptions.map((row) => [row.workId, row.description]));
1258
+ const warnings = [];
1259
+ for (const row of state.afterDescriptions) {
1260
+ const original = before.get(row.workId);
1261
+ const local = row.workId.slice(row.workId.indexOf("::") + 2);
1262
+ const work = state.works.get(local);
1263
+ const materializesReference = work !== undefined && (work.events.size > 0 || work.activities.size > 0 ||
1264
+ work.milestoneLinks.size > 0 || work.taskLinks.size > 0);
1265
+ if (original !== undefined && original.length > 0 && original === row.description &&
1266
+ materializesReference && !acknowledged.has(row.workId)) {
1267
+ warnings.push(reshapeDiagnostic(`Work ${row.workId} keeps an unchanged residual description while materializing planning or strict references; consider Add residual description`, "PTPOOL-112", "warning"));
1268
+ }
1269
+ }
1270
+ return Object.freeze(warnings);
1271
+ }
1272
+ function normalizedReshapeInput(requestInput, model) {
1273
+ if (!isPlanningIntentRequest(requestInput)) {
1274
+ return normalizePlanningReshapeRequest(requestInput);
1275
+ }
1276
+ const compiled = compilePlanningIntentRequest(requestInput, model);
1277
+ if (compiled.ok && compiled.reshapeRequest !== null) {
1278
+ return normalizePlanningReshapeRequest(compiled.reshapeRequest);
1279
+ }
1280
+ return Object.freeze({
1281
+ ok: false,
1282
+ request: null,
1283
+ canonicalUtf8: null,
1284
+ preflightHash: null,
1285
+ diagnostics: Object.freeze([
1286
+ ...compiled.diagnostics,
1287
+ ...(compiled.windowRequest === null ? [] : [reshapeDiagnostic("Window intent request must use a Window mutation command", "PTPOOL-118")]),
1288
+ ]),
1289
+ });
1290
+ }
1291
+ export function auditPlanningReshape(text, requestInput, capability) {
1292
+ requireCapability(capability);
1293
+ const sourceDigest = planningReshapeSha256(text);
1294
+ const source = parsePlanningPoolSource(text, PLANNING_POOL_SOURCE_CAPABILITY);
1295
+ if (!source.ok || source.model === null) {
1296
+ return failedAudit(sourceDigest, source.documentId, source.diagnostics);
1297
+ }
1298
+ const normalized = normalizedReshapeInput(requestInput, source.model);
1299
+ if (!normalized.ok || normalized.request === null || normalized.canonicalUtf8 === null || normalized.preflightHash === null) {
1300
+ return failedAudit(sourceDigest, source.documentId, normalized.diagnostics);
1301
+ }
1302
+ const diagnostics = [];
1303
+ enforceRequestLimits(normalized.request, diagnostics);
1304
+ if (normalized.request.source_digest !== sourceDigest)
1305
+ diagnostics.push(reshapeDiagnostic("Planning reshape source_digest does not match current raw source", "PTPOOL-111"));
1306
+ if (!["reshape", "project", "defer", "archive", "composite"].includes(normalized.request.intent)) {
1307
+ diagnostics.push(reshapeDiagnostic(`Planning reshape intent ${normalized.request.intent} belongs to a later Core`));
1308
+ }
1309
+ validateProjectionFragment(text, normalized.request, source.model, diagnostics);
1310
+ validateDeferralFragment(text, normalized.request, source.model, diagnostics);
1311
+ validateProtectedLinkReallocation(text, normalized.request, source.model, diagnostics);
1312
+ validateArchiveIntent(normalized.request, source.model, diagnostics);
1313
+ const state = buildState(text, normalized.request, source.model, diagnostics);
1314
+ if (diagnostics.some(({ severity }) => severity === "error")) {
1315
+ return Object.freeze({
1316
+ ...failedAudit(sourceDigest, source.documentId, diagnostics),
1317
+ normalizedRequest: normalized.request,
1318
+ canonicalRequestUtf8: normalized.canonicalUtf8,
1319
+ preflightHash: normalized.preflightHash,
1320
+ beforeDescriptions: state.beforeDescriptions,
1321
+ afterDescriptions: state.afterDescriptions,
1322
+ windowCloseReport: state.windowCloseReport,
1323
+ });
1324
+ }
1325
+ const candidate = candidateText(text, source.model, state, normalized.request);
1326
+ const mutation = planPlanningPoolSourceMutation(text, [{ startOffset: 0, endOffset: text.length, replacement: candidate }], PLANNING_POOL_SOURCE_CAPABILITY);
1327
+ if (!mutation.ok || mutation.updatedText === null) {
1328
+ return Object.freeze({
1329
+ ...failedAudit(sourceDigest, source.documentId, [
1330
+ ...mutation.diagnostics,
1331
+ reshapeDiagnostic("Planning reshape reconstruction did not produce a valid final candidate"),
1332
+ ]),
1333
+ normalizedRequest: normalized.request,
1334
+ canonicalRequestUtf8: normalized.canonicalUtf8,
1335
+ preflightHash: normalized.preflightHash,
1336
+ beforeDescriptions: state.beforeDescriptions,
1337
+ afterDescriptions: state.afterDescriptions,
1338
+ windowCloseReport: state.windowCloseReport,
1339
+ });
1340
+ }
1341
+ const changed = mutation.updatedText !== text;
1342
+ const warnings = residualDescriptionWarnings(normalized.request, state);
1343
+ return Object.freeze({
1344
+ ok: true,
1345
+ documentId: source.documentId,
1346
+ sourceDigest,
1347
+ normalizedRequest: normalized.request,
1348
+ canonicalRequestUtf8: normalized.canonicalUtf8,
1349
+ preflightHash: normalized.preflightHash,
1350
+ candidateDigest: planningReshapeSha256(mutation.updatedText),
1351
+ candidateText: mutation.updatedText,
1352
+ changed,
1353
+ edits: mutation.edits,
1354
+ beforeDescriptions: state.beforeDescriptions,
1355
+ afterDescriptions: state.afterDescriptions,
1356
+ windowCloseReport: state.windowCloseReport,
1357
+ diagnostics: Object.freeze([
1358
+ ...warnings,
1359
+ ...(changed ? [] : [reshapeDiagnostic("Planning reshape candidate is byte-identical", "PTPOOL-117", "warning")]),
1360
+ ]),
1361
+ });
1362
+ }
1363
+ function bindingFromAudit(audit) {
1364
+ if (audit.preflightHash === null || audit.candidateDigest === null) {
1365
+ throw new TypeError("successful planning reshape audit bindings are required");
1366
+ }
1367
+ return Object.freeze({
1368
+ normalizationContract: audit.normalizedRequest?.normalization_contract ?? PLANNING_RESHAPE_NORMALIZATION_CONTRACT,
1369
+ preflightHash: audit.preflightHash,
1370
+ sourceDigest: audit.sourceDigest,
1371
+ candidateDigest: audit.candidateDigest,
1372
+ });
1373
+ }
1374
+ export function preflightPlanningReshape(text, requestInput, registry, capability) {
1375
+ const audit = auditPlanningReshape(text, requestInput, capability);
1376
+ if (!audit.ok)
1377
+ return Object.freeze({
1378
+ ...audit,
1379
+ schemaVersion: "Perttool.PlanningReshapePreflightResult.v1",
1380
+ preflightToken: null,
1381
+ tokenExpiresAt: null,
1382
+ authorityImpact: null,
1383
+ });
1384
+ const issue = registry.issue(bindingFromAudit(audit));
1385
+ if (issue === null)
1386
+ return Object.freeze({
1387
+ ...audit,
1388
+ ok: false,
1389
+ schemaVersion: "Perttool.PlanningReshapePreflightResult.v1",
1390
+ preflightToken: null,
1391
+ tokenExpiresAt: null,
1392
+ authorityImpact: authorityImpact(text, audit.normalizedRequest, audit.changed),
1393
+ diagnostics: Object.freeze([...audit.diagnostics, reshapeDiagnostic("Planning reshape token registry is full", "PTPOOL-115")]),
1394
+ });
1395
+ return Object.freeze({
1396
+ ...audit,
1397
+ schemaVersion: "Perttool.PlanningReshapePreflightResult.v1",
1398
+ preflightToken: issue.token,
1399
+ tokenExpiresAt: issue.expiresAt,
1400
+ authorityImpact: authorityImpact(text, audit.normalizedRequest, audit.changed),
1401
+ });
1402
+ }
1403
+ export function preparePlanningReshapeApply(text, requestInput, preflightHash, preflightToken, registry, capability) {
1404
+ const audit = auditPlanningReshape(text, requestInput, capability);
1405
+ const binding = audit.ok ? bindingFromAudit(audit) : null;
1406
+ const tokenDecision = binding === null
1407
+ ? null
1408
+ : registry.verify(preflightToken, binding);
1409
+ const hashMatches = audit.preflightHash === preflightHash;
1410
+ const valid = audit.ok && hashMatches && tokenDecision?.ok === true;
1411
+ const diagnostics = valid
1412
+ ? audit.diagnostics
1413
+ : Object.freeze([
1414
+ ...audit.diagnostics,
1415
+ reshapeDiagnostic("Planning reshape hash, token, source, request, candidate, expiry, or replay binding failed", "PTPOOL-111"),
1416
+ ]);
1417
+ return Object.freeze({
1418
+ ...audit,
1419
+ ok: valid,
1420
+ schemaVersion: "Perttool.PlanningMutationResult.v1",
1421
+ preflightTokenValidated: valid,
1422
+ authorityImpact: audit.documentId === null || audit.normalizedRequest === null
1423
+ ? null
1424
+ : authorityImpact(text, audit.normalizedRequest, audit.changed),
1425
+ diagnostics,
1426
+ });
1427
+ }
1428
+ export function planningReshapeBinding(prepared) {
1429
+ if (!prepared.ok || !prepared.preflightTokenValidated) {
1430
+ throw new TypeError("a successfully rebound planning reshape preparation is required");
1431
+ }
1432
+ return bindingFromAudit(prepared);
1433
+ }
1434
+ //# sourceMappingURL=reshape.js.map