perttool 0.2.0 → 0.3.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 (195) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/README.md +60 -27
  3. package/dist/analysis/temporal-deadline.d.ts +59 -0
  4. package/dist/analysis/temporal-deadline.d.ts.map +1 -0
  5. package/dist/analysis/temporal-deadline.js +387 -0
  6. package/dist/analysis/temporal-deadline.js.map +1 -0
  7. package/dist/analysis/temporal-precedence.d.ts +45 -0
  8. package/dist/analysis/temporal-precedence.d.ts.map +1 -0
  9. package/dist/analysis/temporal-precedence.js +123 -0
  10. package/dist/analysis/temporal-precedence.js.map +1 -0
  11. package/dist/analysis/temporal-resource.d.ts +66 -0
  12. package/dist/analysis/temporal-resource.d.ts.map +1 -0
  13. package/dist/analysis/temporal-resource.js +292 -0
  14. package/dist/analysis/temporal-resource.js.map +1 -0
  15. package/dist/application/check.d.ts +16 -0
  16. package/dist/application/check.d.ts.map +1 -1
  17. package/dist/application/check.js +51 -0
  18. package/dist/application/check.js.map +1 -1
  19. package/dist/application/contract4.d.ts +17 -0
  20. package/dist/application/contract4.d.ts.map +1 -0
  21. package/dist/application/contract4.js +27 -0
  22. package/dist/application/contract4.js.map +1 -0
  23. package/dist/application/init.d.ts +2 -1
  24. package/dist/application/init.d.ts.map +1 -1
  25. package/dist/application/init.js +11 -2
  26. package/dist/application/init.js.map +1 -1
  27. package/dist/application/mutate.d.ts +19 -0
  28. package/dist/application/mutate.d.ts.map +1 -1
  29. package/dist/application/mutate.js +34 -16
  30. package/dist/application/mutate.js.map +1 -1
  31. package/dist/application/next.d.ts +2 -0
  32. package/dist/application/next.d.ts.map +1 -1
  33. package/dist/application/next.js +7 -1
  34. package/dist/application/next.js.map +1 -1
  35. package/dist/application/project.d.ts +3 -1
  36. package/dist/application/project.d.ts.map +1 -1
  37. package/dist/application/project.js +2 -1
  38. package/dist/application/project.js.map +1 -1
  39. package/dist/application/target-check.d.ts +34 -0
  40. package/dist/application/target-check.d.ts.map +1 -0
  41. package/dist/application/target-check.js +90 -0
  42. package/dist/application/target-check.js.map +1 -0
  43. package/dist/application/target-grammar-boundary.d.ts +22 -0
  44. package/dist/application/target-grammar-boundary.d.ts.map +1 -0
  45. package/dist/application/target-grammar-boundary.js +91 -0
  46. package/dist/application/target-grammar-boundary.js.map +1 -0
  47. package/dist/application/target-mutate.d.ts +8 -0
  48. package/dist/application/target-mutate.d.ts.map +1 -0
  49. package/dist/application/target-mutate.js +56 -0
  50. package/dist/application/target-mutate.js.map +1 -0
  51. package/dist/application/target-project.d.ts +28 -0
  52. package/dist/application/target-project.d.ts.map +1 -0
  53. package/dist/application/target-project.js +60 -0
  54. package/dist/application/target-project.js.map +1 -0
  55. package/dist/application/target-temporal-analysis.d.ts +138 -0
  56. package/dist/application/target-temporal-analysis.d.ts.map +1 -0
  57. package/dist/application/target-temporal-analysis.js +409 -0
  58. package/dist/application/target-temporal-analysis.js.map +1 -0
  59. package/dist/application/target-temporal-input.d.ts +86 -0
  60. package/dist/application/target-temporal-input.d.ts.map +1 -0
  61. package/dist/application/target-temporal-input.js +238 -0
  62. package/dist/application/target-temporal-input.js.map +1 -0
  63. package/dist/application/target-unit-migration-candidate.d.ts +60 -0
  64. package/dist/application/target-unit-migration-candidate.d.ts.map +1 -0
  65. package/dist/application/target-unit-migration-candidate.js +290 -0
  66. package/dist/application/target-unit-migration-candidate.js.map +1 -0
  67. package/dist/application/target-unit-migration-request.d.ts +35 -0
  68. package/dist/application/target-unit-migration-request.d.ts.map +1 -0
  69. package/dist/application/target-unit-migration-request.js +68 -0
  70. package/dist/application/target-unit-migration-request.js.map +1 -0
  71. package/dist/application/target-unit-migration-result.d.ts +66 -0
  72. package/dist/application/target-unit-migration-result.d.ts.map +1 -0
  73. package/dist/application/target-unit-migration-result.js +86 -0
  74. package/dist/application/target-unit-migration-result.js.map +1 -0
  75. package/dist/application/unit-migration.d.ts +13 -0
  76. package/dist/application/unit-migration.d.ts.map +1 -0
  77. package/dist/application/unit-migration.js +9 -0
  78. package/dist/application/unit-migration.js.map +1 -0
  79. package/dist/cli.js +264 -26
  80. package/dist/cli.js.map +1 -1
  81. package/dist/command/discovery.d.ts +3 -3
  82. package/dist/command/discovery.d.ts.map +1 -1
  83. package/dist/command/discovery.js +149 -22
  84. package/dist/command/discovery.js.map +1 -1
  85. package/dist/command/registry.d.ts +1 -1
  86. package/dist/command/registry.d.ts.map +1 -1
  87. package/dist/command/registry.js +14 -1
  88. package/dist/command/registry.js.map +1 -1
  89. package/dist/command/usage.js +3 -3
  90. package/dist/formatter/source-formatter.d.ts +12 -0
  91. package/dist/formatter/source-formatter.d.ts.map +1 -1
  92. package/dist/formatter/source-formatter.js +48 -6
  93. package/dist/formatter/source-formatter.js.map +1 -1
  94. package/dist/formatter/target-source-formatter.d.ts +8 -0
  95. package/dist/formatter/target-source-formatter.d.ts.map +1 -0
  96. package/dist/formatter/target-source-formatter.js +36 -0
  97. package/dist/formatter/target-source-formatter.js.map +1 -0
  98. package/dist/help/guide.d.ts +1 -1
  99. package/dist/help/guide.js +1 -1
  100. package/dist/help/registry.d.ts.map +1 -1
  101. package/dist/help/registry.js +124 -13
  102. package/dist/help/registry.js.map +1 -1
  103. package/dist/index.d.ts +9 -6
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +3 -3
  106. package/dist/index.js.map +1 -1
  107. package/dist/io/safe-write.d.ts +7 -0
  108. package/dist/io/safe-write.d.ts.map +1 -1
  109. package/dist/io/safe-write.js +19 -10
  110. package/dist/io/safe-write.js.map +1 -1
  111. package/dist/io/target-safe-write.d.ts +7 -0
  112. package/dist/io/target-safe-write.d.ts.map +1 -0
  113. package/dist/io/target-safe-write.js +33 -0
  114. package/dist/io/target-safe-write.js.map +1 -0
  115. package/dist/migration/conversion.d.ts +40 -0
  116. package/dist/migration/conversion.d.ts.map +1 -0
  117. package/dist/migration/conversion.js +106 -0
  118. package/dist/migration/conversion.js.map +1 -0
  119. package/dist/migration/grammar-boundary.d.ts +20 -0
  120. package/dist/migration/grammar-boundary.d.ts.map +1 -0
  121. package/dist/migration/grammar-boundary.js +92 -0
  122. package/dist/migration/grammar-boundary.js.map +1 -0
  123. package/dist/migration/request.d.ts +79 -0
  124. package/dist/migration/request.d.ts.map +1 -0
  125. package/dist/migration/request.js +308 -0
  126. package/dist/migration/request.js.map +1 -0
  127. package/dist/model/calendar-arithmetic.d.ts +46 -0
  128. package/dist/model/calendar-arithmetic.d.ts.map +1 -0
  129. package/dist/model/calendar-arithmetic.js +236 -0
  130. package/dist/model/calendar-arithmetic.js.map +1 -0
  131. package/dist/model/calendar.d.ts +22 -0
  132. package/dist/model/calendar.d.ts.map +1 -0
  133. package/dist/model/calendar.js +83 -0
  134. package/dist/model/calendar.js.map +1 -0
  135. package/dist/model/declaration-fields.d.ts +15 -0
  136. package/dist/model/declaration-fields.d.ts.map +1 -0
  137. package/dist/model/declaration-fields.js +54 -0
  138. package/dist/model/declaration-fields.js.map +1 -0
  139. package/dist/model/exact-duration-source.d.ts +10 -0
  140. package/dist/model/exact-duration-source.d.ts.map +1 -0
  141. package/dist/model/exact-duration-source.js +69 -0
  142. package/dist/model/exact-duration-source.js.map +1 -0
  143. package/dist/model/rational.d.ts +2 -2
  144. package/dist/model/rational.d.ts.map +1 -1
  145. package/dist/model/rational.js +3 -1
  146. package/dist/model/rational.js.map +1 -1
  147. package/dist/model/syntax.d.ts +9 -2
  148. package/dist/model/syntax.d.ts.map +1 -1
  149. package/dist/model/syntax.js +20 -4
  150. package/dist/model/syntax.js.map +1 -1
  151. package/dist/model/target-calendar.d.ts +25 -0
  152. package/dist/model/target-calendar.d.ts.map +1 -0
  153. package/dist/model/target-calendar.js +56 -0
  154. package/dist/model/target-calendar.js.map +1 -0
  155. package/dist/mutation/advance.d.ts.map +1 -1
  156. package/dist/mutation/advance.js +21 -4
  157. package/dist/mutation/advance.js.map +1 -1
  158. package/dist/mutation/milestone.d.ts +10 -1
  159. package/dist/mutation/milestone.d.ts.map +1 -1
  160. package/dist/mutation/milestone.js +77 -21
  161. package/dist/mutation/milestone.js.map +1 -1
  162. package/dist/mutation/project.d.ts +7 -1
  163. package/dist/mutation/project.d.ts.map +1 -1
  164. package/dist/mutation/project.js +18 -5
  165. package/dist/mutation/project.js.map +1 -1
  166. package/dist/mutation/target-types.d.ts +42 -0
  167. package/dist/mutation/target-types.d.ts.map +1 -0
  168. package/dist/mutation/target-types.js +2 -0
  169. package/dist/mutation/target-types.js.map +1 -0
  170. package/dist/mutation/task.d.ts +12 -1
  171. package/dist/mutation/task.d.ts.map +1 -1
  172. package/dist/mutation/task.js +108 -49
  173. package/dist/mutation/task.js.map +1 -1
  174. package/dist/mutation/types.d.ts +8 -2
  175. package/dist/mutation/types.d.ts.map +1 -1
  176. package/dist/parser/document-parser.d.ts +14 -0
  177. package/dist/parser/document-parser.d.ts.map +1 -1
  178. package/dist/parser/document-parser.js +142 -38
  179. package/dist/parser/document-parser.js.map +1 -1
  180. package/dist/recommendation/override-types.d.ts +2 -2
  181. package/dist/recommendation/override.d.ts +2 -2
  182. package/dist/recommendation/override.d.ts.map +1 -1
  183. package/dist/recommendation/override.js +49 -12
  184. package/dist/recommendation/override.js.map +1 -1
  185. package/dist/semantic/target-validator.d.ts +36 -0
  186. package/dist/semantic/target-validator.d.ts.map +1 -0
  187. package/dist/semantic/target-validator.js +87 -0
  188. package/dist/semantic/target-validator.js.map +1 -0
  189. package/dist/semantic/validator.d.ts +2 -0
  190. package/dist/semantic/validator.d.ts.map +1 -1
  191. package/dist/semantic/validator.js +50 -5
  192. package/dist/semantic/validator.js.map +1 -1
  193. package/dist/version.d.ts +1 -1
  194. package/dist/version.js +1 -1
  195. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2,19 +2,19 @@
2
2
  import { readFile } from "node:fs/promises";
3
3
  import process from "node:process";
4
4
  import { TextDecoder } from "node:util";
5
- import { analyzeDocument } from "./application/analyze.js";
5
+ import { analyzeDocument, selectNextTasks, } from "./application/contract4.js";
6
6
  import { checkDocument } from "./application/check.js";
7
7
  import { planFormat } from "./application/format.js";
8
8
  import { planProjectInit, projectInitResultToJson, renderProjectInitResult, withProjectInitOutput, } from "./application/init.js";
9
9
  import { planBatchMutation, planMutation } from "./application/mutate.js";
10
- import { selectNextTasks } from "./application/next.js";
10
+ import { planUnitMigration, withUnitMigrationWrite, } from "./application/unit-migration.js";
11
11
  import { getProjectMetadata, } from "./application/project.js";
12
12
  import { getAgentHelp } from "./application/agent-help.js";
13
13
  import { exportMermaid, } from "./conversion/mermaid.js";
14
14
  import { importMermaid } from "./conversion/mermaid-import.js";
15
15
  import { getGuide, renderGuideResult, serializeGuideResult, } from "./help/guide.js";
16
16
  import { commandOptionSets, } from "./command/registry.js";
17
- import { CONTRACT3_COMMAND_REGISTRY, getCommandDiscovery, renderCommandHelpResult, serializeCommandHelpResult, } from "./command/discovery.js";
17
+ import { CONTRACT4_COMMAND_REGISTRY, getCommandDiscovery, renderCommandHelpResult, serializeCommandHelpResult, } from "./command/discovery.js";
18
18
  import { handlerCommandUsageError, renderCommandUsageError, serializeCommandUsageError, validateCommandInvocation, } from "./command/usage.js";
19
19
  import { agentGuidanceResultToJson } from "./guidance/projection.js";
20
20
  import { agentGuidanceExitCode, renderAgentGuidanceText, } from "./guidance/text.js";
@@ -79,7 +79,7 @@ function parseOptions(args, valueOptions, flagOptions, repeatableOptions = new S
79
79
  return { positionals, values, repeatedValues, flags };
80
80
  }
81
81
  function parseCommandOptions(operation, args) {
82
- const descriptor = CONTRACT3_COMMAND_REGISTRY.find((candidate) => candidate.operation === operation);
82
+ const descriptor = CONTRACT4_COMMAND_REGISTRY.find((candidate) => candidate.operation === operation);
83
83
  if (descriptor === undefined) {
84
84
  throw new Error(`command descriptor is missing for ${operation}`);
85
85
  }
@@ -182,6 +182,18 @@ async function readStdin() {
182
182
  function writeJson(value) {
183
183
  process.stdout.write(`${JSON.stringify(value)}\n`);
184
184
  }
185
+ function snakeJson(value) {
186
+ if (typeof value === "bigint")
187
+ return value.toString();
188
+ if (Array.isArray(value))
189
+ return value.map(snakeJson);
190
+ if (value === null || typeof value !== "object")
191
+ return value;
192
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
193
+ key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase(),
194
+ snakeJson(item),
195
+ ]));
196
+ }
185
197
  function cliError(error, exitCode, operation, json) {
186
198
  const code = error instanceof UsageError
187
199
  ? error.code
@@ -205,7 +217,7 @@ function cliError(error, exitCode, operation, json) {
205
217
  if (json) {
206
218
  writeJson({
207
219
  schema_version: "Perttool.CliError.v1",
208
- cli_contract_version: 3,
220
+ cli_contract_version: 4,
209
221
  tool_version: TOOL_VERSION,
210
222
  operation,
211
223
  ok: false,
@@ -240,8 +252,8 @@ async function runCheck(args) {
240
252
  const ok = result.ok && !warningFailure;
241
253
  if (format === "json") {
242
254
  writeJson({
243
- schema_version: "Perttool.CheckResult.v1",
244
- cli_contract_version: 3,
255
+ schema_version: "Perttool.CheckResult.v2",
256
+ cli_contract_version: 4,
245
257
  tool_version: TOOL_VERSION,
246
258
  operation: "document.check",
247
259
  ok,
@@ -252,11 +264,12 @@ async function runCheck(args) {
252
264
  diagnostics_truncated: result.diagnosticsTruncated,
253
265
  grammar_version: result.grammarVersion,
254
266
  summary: result.summary,
267
+ temporal_inputs: snakeJson(result.temporalInputs),
255
268
  });
256
269
  }
257
270
  else {
258
271
  if (ok) {
259
- process.stdout.write(`OK ${source} project=${result.documentId ?? "-"} milestones=${result.summary.milestones} tasks=${result.summary.tasks} gates=${result.summary.gates} resources=${result.summary.resources}\n`);
272
+ process.stdout.write(`OK ${source} project=${result.documentId ?? "-"} milestones=${result.summary.milestones} tasks=${result.summary.tasks} gates=${result.summary.gates} resources=${result.summary.resources} temporal=milestone_deadlines:${result.temporalInputs?.milestoneDeadlines.length ?? 0},task_not_before:${result.temporalInputs?.taskConstraints.filter(({ notBefore }) => notBefore !== null).length ?? 0},task_deadlines:${result.temporalInputs?.taskConstraints.filter(({ deadline }) => deadline !== null).length ?? 0}\n`);
260
273
  }
261
274
  for (const diagnostic of result.diagnostics) {
262
275
  process.stderr.write(`${renderDiagnostic(diagnostic, source, color)}\n`);
@@ -273,25 +286,30 @@ function projectJson(project) {
273
286
  version: project.version,
274
287
  title: project.title,
275
288
  description: project.description,
276
- as_of: project.asOf,
289
+ as_of: snakeJson(project.asOf),
277
290
  duration_unit: project.durationUnit,
278
291
  velocity: project.velocity,
279
292
  finish: project.finish,
293
+ finish_deadline: snakeJson(project.finishDeadline),
280
294
  critical_epsilon: project.criticalEpsilon,
281
295
  target_duration: project.targetDuration,
282
296
  };
283
297
  }
284
298
  function renderProjectText(project) {
285
299
  const optional = (value) => value ?? "-";
300
+ const calendar = (value) => value === null
301
+ ? "-"
302
+ : `${value.kind.toUpperCase()} ${value.sourceText ?? "-"}`;
286
303
  return [
287
304
  `PROJECT ${project.id}`,
288
305
  `VERSION ${project.version}`,
289
306
  `TITLE ${JSON.stringify(project.title)}`,
290
307
  `DESCRIPTION ${project.description === null ? "-" : JSON.stringify(project.description)}`,
291
- `AS_OF ${optional(project.asOf)}`,
308
+ `AS_OF ${calendar(project.asOf)}`,
292
309
  `DURATION_UNIT ${project.durationUnit}`,
293
310
  `VELOCITY ${optional(project.velocity)}`,
294
311
  `FINISH ${project.finish}`,
312
+ `FINISH_DEADLINE ${calendar(project.finishDeadline)}`,
295
313
  `CRITICAL_EPSILON ${optional(project.criticalEpsilon)}`,
296
314
  `TARGET_DURATION ${optional(project.targetDuration)}`,
297
315
  "",
@@ -321,8 +339,8 @@ async function runProjectShow(args) {
321
339
  const ok = result.ok && !warningFailure;
322
340
  if (format === "json") {
323
341
  writeJson({
324
- schema_version: "Perttool.ProjectResult.v1",
325
- cli_contract_version: 3,
342
+ schema_version: "Perttool.ProjectResult.v2",
343
+ cli_contract_version: 4,
326
344
  tool_version: TOOL_VERSION,
327
345
  operation: "project.show",
328
346
  ok,
@@ -368,6 +386,11 @@ async function runProjectInit(args) {
368
386
  ...(parsed.values.get("velocity") === undefined
369
387
  ? {}
370
388
  : { velocity: parsed.values.get("velocity") }),
389
+ ...(parsed.values.get("initial-milestone-deadline") === undefined
390
+ ? {}
391
+ : {
392
+ initialMilestoneDeadline: parsed.values.get("initial-milestone-deadline"),
393
+ }),
371
394
  });
372
395
  let writeResult = null;
373
396
  const output = parsed.values.get("out");
@@ -546,7 +569,7 @@ async function runFormat(args) {
546
569
  if (format === "json") {
547
570
  writeJson({
548
571
  schema_version: "Perttool.FormatResult.v1",
549
- cli_contract_version: 3,
572
+ cli_contract_version: 4,
550
573
  tool_version: TOOL_VERSION,
551
574
  operation: "document.format",
552
575
  ok,
@@ -700,6 +723,12 @@ function taskMutationFromOptions(action, parsed) {
700
723
  ...(parsed.values.get("description") === undefined
701
724
  ? {}
702
725
  : { description: parsed.values.get("description") }),
726
+ ...(parsed.values.get("not-before") === undefined
727
+ ? {}
728
+ : { notBefore: parsed.values.get("not-before") }),
729
+ ...(parsed.values.get("deadline") === undefined
730
+ ? {}
731
+ : { deadline: parsed.values.get("deadline") }),
703
732
  ...(status === undefined ? {} : { status }),
704
733
  ...(priority === undefined ? {} : { priority }),
705
734
  ...(requirements.length === 0 ? {} : { requirements }),
@@ -720,7 +749,7 @@ function taskMutationFromOptions(action, parsed) {
720
749
  }
721
750
  const clear = enumRepeated(parsed, "clear", new Set([
722
751
  "description", "status", "priority", "owner", "blocked_reason", "source",
723
- "tags", "requires",
752
+ "tags", "requires", "not_before", "deadline",
724
753
  ]));
725
754
  const addTags = uniqueRepeated(parsed, "add-tag");
726
755
  const removeTags = uniqueRepeated(parsed, "remove-tag");
@@ -735,6 +764,12 @@ function taskMutationFromOptions(action, parsed) {
735
764
  ? {}
736
765
  : { description: parsed.values.get("description") }),
737
766
  ...timing,
767
+ ...(parsed.values.get("not-before") === undefined
768
+ ? {}
769
+ : { notBefore: parsed.values.get("not-before") }),
770
+ ...(parsed.values.get("deadline") === undefined
771
+ ? {}
772
+ : { deadline: parsed.values.get("deadline") }),
738
773
  ...(status === undefined ? {} : { status }),
739
774
  ...(priority === undefined ? {} : { priority }),
740
775
  ...(parsed.values.get("owner") === undefined
@@ -749,6 +784,8 @@ function taskMutationFromOptions(action, parsed) {
749
784
  };
750
785
  const clearConflicts = new Map([
751
786
  ["description", parsed.values.has("description")],
787
+ ["not_before", parsed.values.has("not-before")],
788
+ ["deadline", parsed.values.has("deadline")],
752
789
  ["status", status !== undefined],
753
790
  ["priority", priority !== undefined],
754
791
  ["owner", parsed.values.has("owner")],
@@ -848,18 +885,22 @@ function milestoneMutationFromOptions(action, parsed) {
848
885
  ? {}
849
886
  : { description: parsed.values.get("description") }),
850
887
  ...(state === undefined ? {} : { state }),
888
+ ...(parsed.values.get("deadline") === undefined
889
+ ? {}
890
+ : { deadline: parsed.values.get("deadline") }),
851
891
  ...((parsed.repeatedValues.get("tag") ?? []).length === 0
852
892
  ? {}
853
893
  : { tags: parsed.repeatedValues.get("tag") }),
854
894
  },
855
895
  };
856
896
  }
857
- const clear = enumRepeated(parsed, "clear", new Set(["description", "state", "tags"]));
897
+ const clear = enumRepeated(parsed, "clear", new Set(["description", "state", "deadline", "tags"]));
858
898
  const addTags = uniqueRepeated(parsed, "add-tag");
859
899
  const removeTags = uniqueRepeated(parsed, "remove-tag");
860
900
  ensureDisjoint(addTags, "add-tag", removeTags, "remove-tag");
861
901
  if (clear.some((field) => (field === "description" && parsed.values.has("description")) ||
862
902
  (field === "state" && state !== undefined) ||
903
+ (field === "deadline" && parsed.values.has("deadline")) ||
863
904
  (field === "tags" && (addTags.length > 0 || removeTags.length > 0)))) {
864
905
  throw new UsageError("--clear conflicts with another milestone field option");
865
906
  }
@@ -871,6 +912,9 @@ function milestoneMutationFromOptions(action, parsed) {
871
912
  ? {}
872
913
  : { description: parsed.values.get("description") }),
873
914
  ...(state === undefined ? {} : { state }),
915
+ ...(parsed.values.get("deadline") === undefined
916
+ ? {}
917
+ : { deadline: parsed.values.get("deadline") }),
874
918
  };
875
919
  return {
876
920
  kind: "milestone.set",
@@ -1063,7 +1107,7 @@ async function runMutation(resource, action, args) {
1063
1107
  if (format === "json") {
1064
1108
  writeJson({
1065
1109
  schema_version: "Perttool.MutationResult.v1",
1066
- cli_contract_version: 3,
1110
+ cli_contract_version: 4,
1067
1111
  tool_version: TOOL_VERSION,
1068
1112
  operation,
1069
1113
  ok,
@@ -1096,6 +1140,142 @@ async function runMutation(resource, action, args) {
1096
1140
  }
1097
1141
  return ok ? 0 : 1;
1098
1142
  }
1143
+ function unitMigrationJson(result, source, sourceDigest, ok) {
1144
+ const exact = (value) => ({
1145
+ numerator: value.numerator.toString(),
1146
+ denominator: value.denominator.toString(),
1147
+ unit: value.unit,
1148
+ });
1149
+ return {
1150
+ schema_version: result.schemaVersion,
1151
+ cli_contract_version: 4,
1152
+ tool_version: TOOL_VERSION,
1153
+ operation: "project.migrate-unit",
1154
+ ok,
1155
+ document_id: result.documentId,
1156
+ source,
1157
+ source_digest: sourceDigest,
1158
+ diagnostics: result.diagnostics.map(jsonDiagnostic),
1159
+ diagnostics_truncated: result.diagnosticsTruncated,
1160
+ unit_migration: result.unitMigration,
1161
+ source_grammar_version: result.sourceGrammarVersion,
1162
+ target_grammar_version: result.targetGrammarVersion,
1163
+ grammar_disposition: result.grammarDisposition,
1164
+ source_unit: result.sourceUnit,
1165
+ target_unit: result.targetUnit,
1166
+ effective_velocity: result.effectiveVelocity === null
1167
+ ? null
1168
+ : {
1169
+ points: exact(result.effectiveVelocity.points),
1170
+ period: exact(result.effectiveVelocity.period),
1171
+ },
1172
+ velocity_disposition: result.velocityDisposition,
1173
+ changed: result.changed,
1174
+ converted_fields: result.convertedFields.map((field) => ({
1175
+ entity_kind: field.entityKind,
1176
+ entity_id: field.entityId,
1177
+ field_path: field.fieldPath,
1178
+ original: exact(field.original),
1179
+ converted: exact(field.converted),
1180
+ canonical_token: field.canonicalToken,
1181
+ })),
1182
+ reversibility: result.reversibility,
1183
+ qualifications: result.qualifications,
1184
+ unavailable_causes: result.unavailableCauses.map((cause) => ({
1185
+ cause: cause.cause,
1186
+ diagnostic_code: cause.diagnosticCode,
1187
+ field_paths: cause.fieldPaths,
1188
+ })),
1189
+ original_digest: result.originalDigest,
1190
+ updated_digest: result.updatedDigest,
1191
+ updated_text: result.updatedText,
1192
+ diff: result.diff,
1193
+ edits: result.edits.map((edit) => ({
1194
+ start_offset: edit.startOffset,
1195
+ end_offset: edit.endOffset,
1196
+ replacement: edit.replacement,
1197
+ })),
1198
+ write: {
1199
+ mode: result.write.mode,
1200
+ target: result.write.target,
1201
+ written: result.write.written,
1202
+ },
1203
+ };
1204
+ }
1205
+ async function runUnitMigration(args) {
1206
+ const parsed = parseCommandOptions("project.migrate-unit", args);
1207
+ if (parsed.positionals.length !== 1) {
1208
+ throw new UsageError("project migrate-unit requires exactly one <file>");
1209
+ }
1210
+ const format = outputFormat(parsed.values.get("format"));
1211
+ const color = colorMode(parsed.values.get("color"), format);
1212
+ const targetUnit = enumOption(requiredOption(parsed, "to-unit"), "to-unit", new Set(["day", "hour", "point"]));
1213
+ const maxDiagnostics = boundedInteger(parsed.values.get("max-diagnostics"), "max-diagnostics", 100, 1, 1000);
1214
+ const sourceOperand = parsed.positionals[0];
1215
+ const writeRequest = editingWriteRequest(parsed, sourceOperand);
1216
+ const source = sourceOperand === "-" ? "<stdin>" : sourceOperand;
1217
+ let input;
1218
+ try {
1219
+ input = await readDocument(sourceOperand);
1220
+ }
1221
+ catch (error) {
1222
+ return cliError(error instanceof Error ? error : new Error(String(error)), 3, "project.migrate-unit", format === "json");
1223
+ }
1224
+ let result = planUnitMigration(input.text, {
1225
+ targetUnit,
1226
+ ...(parsed.values.get("replacement-velocity") === undefined
1227
+ ? {}
1228
+ : {
1229
+ replacementVelocity: parsed.values.get("replacement-velocity"),
1230
+ }),
1231
+ }, {
1232
+ maxDiagnostics,
1233
+ originalLabel: source,
1234
+ updatedLabel: "candidate",
1235
+ });
1236
+ const warningFailure = parsed.flags.has("warnings-as-errors") &&
1237
+ (result.diagnosticsTruncated ||
1238
+ result.diagnostics.some(({ severity }) => severity === "warning"));
1239
+ const ok = result.ok && !warningFailure;
1240
+ let writeResult = null;
1241
+ if (result.ok) {
1242
+ try {
1243
+ assertExpectedDigest(writeRequest, input.digest);
1244
+ if (ok && writeRequest.mode !== "preview") {
1245
+ writeResult = await commitCandidate(writeRequest, result.updatedText, input.digest);
1246
+ result = withUnitMigrationWrite(result, writeResult);
1247
+ }
1248
+ }
1249
+ catch (error) {
1250
+ return writeFailureExit(error, "project.migrate-unit", format === "json");
1251
+ }
1252
+ }
1253
+ if (format === "json") {
1254
+ writeJson(unitMigrationJson(result, source, input.digest, ok));
1255
+ }
1256
+ else {
1257
+ if (ok) {
1258
+ if (writeResult !== null) {
1259
+ process.stderr.write(renderWriteSummary("project.migrate-unit", writeResult));
1260
+ }
1261
+ else {
1262
+ process.stdout.write(parsed.flags.has("diff")
1263
+ ? (result.diff ?? "")
1264
+ : (result.updatedText ?? ""));
1265
+ if (!parsed.flags.has("diff")) {
1266
+ process.stderr.write(`PREVIEW project.migrate-unit changed=${result.changed} source_unit=${result.sourceUnit ?? "-"} target_unit=${result.targetUnit} original_digest=${result.originalDigest} updated_digest=${result.updatedDigest ?? "-"}\n`);
1267
+ }
1268
+ }
1269
+ }
1270
+ for (const diagnostic of result.diagnostics) {
1271
+ process.stderr.write(`${renderDiagnostic(diagnostic, source, color)}\n`);
1272
+ }
1273
+ if (result.diagnosticsTruncated) {
1274
+ process.stderr.write(`DIAGNOSTICS_TRUNCATED true limit=${maxDiagnostics}\n`);
1275
+ }
1276
+ }
1277
+ return ok ? 0 : 1;
1278
+ }
1099
1279
  function advanceResultJson(details) {
1100
1280
  return details === null
1101
1281
  ? null
@@ -1159,7 +1339,7 @@ async function runAdvance(args) {
1159
1339
  if (format === "json") {
1160
1340
  writeJson({
1161
1341
  schema_version: "Perttool.MutationResult.v1",
1162
- cli_contract_version: 3,
1342
+ cli_contract_version: 4,
1163
1343
  tool_version: TOOL_VERSION,
1164
1344
  operation: "dag.advance",
1165
1345
  ok,
@@ -1437,6 +1617,41 @@ function renderAnalysisText(result) {
1437
1617
  if (resource.resources.length === 0)
1438
1618
  lines.push("-");
1439
1619
  }
1620
+ if (result.temporal !== null) {
1621
+ const temporalPoint = (point) => {
1622
+ const causes = point.unavailableCauses
1623
+ .map(({ cause }) => cause)
1624
+ .join(",") || "-";
1625
+ return [
1626
+ `state=${point.state}`,
1627
+ `relative=${point.relative === null ? "-" : `${point.relative.numerator}/${point.relative.denominator}${durationSuffix(point.relative.unit)}`}`,
1628
+ `calendar=${point.calendar?.sourceText ?? "-"}`,
1629
+ `causes=${causes}`,
1630
+ ].join(" ");
1631
+ };
1632
+ for (const [title, projection] of [
1633
+ ["TEMPORAL PRECEDENCE", result.temporal.precedence],
1634
+ ["TEMPORAL RESOURCE", result.temporal.resource],
1635
+ ]) {
1636
+ lines.push("", title, `STATE ${projection.state}`, `ALGORITHM ${projection.algorithm === null ? "-" : `${projection.algorithm.id}@${projection.algorithm.version} optimal=${projection.algorithm.optimal}`}`, `CONDITIONAL_ON_BLOCKS_RESOLVED ${projection.conditionalOnBlocksResolved}`, `BLOCKED_TASKS ${projection.blockedTaskIds.join(",") || "-"}`);
1637
+ for (const task of projection.tasks) {
1638
+ lines.push(`${task.taskId} START ${temporalPoint(task.start)} FINISH ${temporalPoint(task.finish)}`);
1639
+ }
1640
+ if (projection.tasks.length === 0)
1641
+ lines.push("-");
1642
+ }
1643
+ lines.push("", "DEADLINES");
1644
+ for (const evaluation of result.temporal.deadlineEvaluations) {
1645
+ const causes = [
1646
+ ...evaluation.current.unavailableCauses,
1647
+ ...evaluation.precedence.unavailableCauses,
1648
+ ...evaluation.resource.unavailableCauses,
1649
+ ].map(({ cause }) => cause).join(",") || "-";
1650
+ lines.push(`${evaluation.subject.kind}:${evaluation.subject.id} assessment=${evaluation.combinedAssessment} precedence=${evaluation.precedence.assessment ?? evaluation.precedence.state} resource=${evaluation.resource.assessment ?? evaluation.resource.state} conditional=${evaluation.conditionalOnBlocksResolved} causes=${causes}`);
1651
+ }
1652
+ if (result.temporal.deadlineEvaluations.length === 0)
1653
+ lines.push("-");
1654
+ }
1440
1655
  return `${lines.join("\n")}\n`;
1441
1656
  }
1442
1657
  async function runAnalyze(args) {
@@ -1473,8 +1688,8 @@ async function runAnalyze(args) {
1473
1688
  const ok = result.ok && !warningFailure;
1474
1689
  if (format === "json") {
1475
1690
  writeJson({
1476
- schema_version: "Perttool.AnalysisResult.v2",
1477
- cli_contract_version: 3,
1691
+ schema_version: "Perttool.AnalysisResult.v3",
1692
+ cli_contract_version: 4,
1478
1693
  tool_version: TOOL_VERSION,
1479
1694
  operation: "dag.analyze",
1480
1695
  ok,
@@ -1483,6 +1698,7 @@ async function runAnalyze(args) {
1483
1698
  source_digest: input.digest,
1484
1699
  diagnostics: result.diagnostics.map(jsonDiagnostic),
1485
1700
  diagnostics_truncated: result.diagnosticsTruncated,
1701
+ grammar_version: result.grammarVersion,
1486
1702
  mode: result.mode,
1487
1703
  precision: result.precision,
1488
1704
  ...(result.durationUnit === null
@@ -1499,6 +1715,7 @@ async function runAnalyze(args) {
1499
1715
  resource: result.resource === null || result.durationUnit === null
1500
1716
  ? null
1501
1717
  : resourceJson(result.resource, result.durationUnit, result.precision),
1718
+ temporal: snakeJson(result.temporal),
1502
1719
  });
1503
1720
  }
1504
1721
  else {
@@ -1586,7 +1803,7 @@ async function runRender(args) {
1586
1803
  if (format === "json") {
1587
1804
  writeJson({
1588
1805
  schema_version: "Perttool.ExportResult.v1",
1589
- cli_contract_version: 3,
1806
+ cli_contract_version: 4,
1590
1807
  tool_version: TOOL_VERSION,
1591
1808
  operation: "dag.render",
1592
1809
  ok,
@@ -1678,7 +1895,7 @@ async function runImport(args) {
1678
1895
  if (format === "json") {
1679
1896
  writeJson({
1680
1897
  schema_version: "Perttool.ImportResult.v1",
1681
- cli_contract_version: 3,
1898
+ cli_contract_version: 4,
1682
1899
  tool_version: TOOL_VERSION,
1683
1900
  operation: "dag.import",
1684
1901
  ok,
@@ -1895,7 +2112,15 @@ function renderExplanation(node, indent) {
1895
2112
  function renderNextText(result) {
1896
2113
  const unit = result.durationUnit;
1897
2114
  const taskById = new Map(result.tasks.map((task) => [task.id, task]));
2115
+ const authority = result.temporal.authority;
1898
2116
  const lines = [
2117
+ "START AUTHORITY",
2118
+ `POLICY ${authority.policy}`,
2119
+ `STARTABLE RECOMMENDED ${authority.startableRecommendedTaskIds.join(",") || "-"}`,
2120
+ `DELAYED RECOMMENDED ${authority.delayedRecommendedTaskIds.join(",") || "-"}`,
2121
+ `UNAVAILABLE RECOMMENDED ${authority.unavailableRecommendedTaskIds.join(",") || "-"}`,
2122
+ "DEADLINE FACTS INFORMATIONAL FOR RANKING v1",
2123
+ "",
1899
2124
  `PERTTOOL NEXT ${result.documentId ?? "-"}`,
1900
2125
  `VELOCITY ${result.velocity === null ? "-" : velocityText(result.velocity, result.precision)}`,
1901
2126
  `VELOCITY_FORECAST_UNIT ${result.velocityForecast?.targetUnit ?? "-"}`,
@@ -1934,6 +2159,15 @@ function renderNextText(result) {
1934
2159
  section("READY / WAITING RESOURCE", result.groups.ready.filter((id) => !result.groups.runnableNow.includes(id)), "rejection");
1935
2160
  section("BLOCKED NOW", result.groups.blockedNow, "blocked");
1936
2161
  section("UPCOMING", result.groups.upcoming, "explanation");
2162
+ lines.push("", "TEMPORAL CONTEXT");
2163
+ for (const temporal of result.temporal.tasks) {
2164
+ const causes = temporal.timeEligibility.unavailableCauses
2165
+ .map(({ cause }) => cause)
2166
+ .join(",") || "-";
2167
+ lines.push(`${temporal.taskId} eligibility=${temporal.timeEligibility.state} release=${temporal.timeEligibility.releaseBound === null ? "-" : `${temporal.timeEligibility.releaseBound.numerator}/${temporal.timeEligibility.releaseBound.denominator}${durationSuffix(temporal.timeEligibility.releaseBound.unit)}`} task_deadline=${temporal.taskDeadline?.sourceText ?? "-"} destination=${temporal.destinationMilestoneId} destination_deadline=${temporal.destinationDeadline?.sourceText ?? "-"} causes=${causes}`);
2168
+ }
2169
+ if (result.temporal.tasks.length === 0)
2170
+ lines.push("-");
1937
2171
  return `${lines.join("\n")}\n`;
1938
2172
  }
1939
2173
  async function runNext(args) {
@@ -1968,7 +2202,7 @@ async function runNext(args) {
1968
2202
  if (format === "json") {
1969
2203
  writeJson({
1970
2204
  schema_version: "Perttool.CliError.v1",
1971
- cli_contract_version: 3,
2205
+ cli_contract_version: 4,
1972
2206
  tool_version: TOOL_VERSION,
1973
2207
  operation: "dag.next",
1974
2208
  ok: false,
@@ -1988,8 +2222,8 @@ async function runNext(args) {
1988
2222
  const ok = result.ok && !warningFailure;
1989
2223
  if (format === "json") {
1990
2224
  writeJson({
1991
- schema_version: "Perttool.NextResult.v3",
1992
- cli_contract_version: 3,
2225
+ schema_version: "Perttool.NextResult.v4",
2226
+ cli_contract_version: 4,
1993
2227
  recommendation_interface_version: 1,
1994
2228
  tool_version: TOOL_VERSION,
1995
2229
  operation: "dag.next",
@@ -1999,7 +2233,9 @@ async function runNext(args) {
1999
2233
  source_digest: input.digest,
2000
2234
  diagnostics: result.diagnostics.map(jsonDiagnostic),
2001
2235
  diagnostics_truncated: result.diagnosticsTruncated,
2236
+ grammar_version: result.grammarVersion,
2002
2237
  ...(result.durationUnit === null ? {} : nextJson(result)),
2238
+ temporal: snakeJson(result.temporal),
2003
2239
  });
2004
2240
  }
2005
2241
  else {
@@ -2082,7 +2318,7 @@ function runAgentHelp(args) {
2082
2318
  const { schema_version: schemaVersion, ...payload } = projected;
2083
2319
  writeJson({
2084
2320
  schema_version: schemaVersion,
2085
- cli_contract_version: 3,
2321
+ cli_contract_version: 4,
2086
2322
  ...payload,
2087
2323
  });
2088
2324
  }
@@ -2120,6 +2356,8 @@ async function dispatchCommand(descriptor, args) {
2120
2356
  return runProjectInit(args);
2121
2357
  case "project.show":
2122
2358
  return runProjectShow(args);
2359
+ case "project.migrate-unit":
2360
+ return runUnitMigration(args);
2123
2361
  case "dag.analyze":
2124
2362
  return runAnalyze(args);
2125
2363
  case "dag.next":
@@ -2138,7 +2376,7 @@ async function dispatchCommand(descriptor, args) {
2138
2376
  const [resource, action] = descriptor.path;
2139
2377
  return runMutation(resource, action, args);
2140
2378
  }
2141
- throw new Error(`no Contract 3 handler for ${descriptor.operation}`);
2379
+ throw new Error(`no Contract 4 handler for ${descriptor.operation}`);
2142
2380
  }
2143
2381
  function emitCommandUsage(error, json) {
2144
2382
  if (json) {