perttool 0.1.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 (213) hide show
  1. package/CHANGELOG.md +112 -33
  2. package/README.md +206 -111
  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/analyze.js +5 -5
  16. package/dist/application/analyze.js.map +1 -1
  17. package/dist/application/check.d.ts +16 -0
  18. package/dist/application/check.d.ts.map +1 -1
  19. package/dist/application/check.js +51 -0
  20. package/dist/application/check.js.map +1 -1
  21. package/dist/application/contract4.d.ts +17 -0
  22. package/dist/application/contract4.d.ts.map +1 -0
  23. package/dist/application/contract4.js +27 -0
  24. package/dist/application/contract4.js.map +1 -0
  25. package/dist/application/init.d.ts +43 -0
  26. package/dist/application/init.d.ts.map +1 -0
  27. package/dist/application/init.js +226 -0
  28. package/dist/application/init.js.map +1 -0
  29. package/dist/application/mutate.d.ts +19 -0
  30. package/dist/application/mutate.d.ts.map +1 -1
  31. package/dist/application/mutate.js +47 -25
  32. package/dist/application/mutate.js.map +1 -1
  33. package/dist/application/next.d.ts +2 -0
  34. package/dist/application/next.d.ts.map +1 -1
  35. package/dist/application/next.js +7 -1
  36. package/dist/application/next.js.map +1 -1
  37. package/dist/application/project.d.ts +3 -1
  38. package/dist/application/project.d.ts.map +1 -1
  39. package/dist/application/project.js +2 -1
  40. package/dist/application/project.js.map +1 -1
  41. package/dist/application/target-check.d.ts +34 -0
  42. package/dist/application/target-check.d.ts.map +1 -0
  43. package/dist/application/target-check.js +90 -0
  44. package/dist/application/target-check.js.map +1 -0
  45. package/dist/application/target-grammar-boundary.d.ts +22 -0
  46. package/dist/application/target-grammar-boundary.d.ts.map +1 -0
  47. package/dist/application/target-grammar-boundary.js +91 -0
  48. package/dist/application/target-grammar-boundary.js.map +1 -0
  49. package/dist/application/target-mutate.d.ts +8 -0
  50. package/dist/application/target-mutate.d.ts.map +1 -0
  51. package/dist/application/target-mutate.js +56 -0
  52. package/dist/application/target-mutate.js.map +1 -0
  53. package/dist/application/target-project.d.ts +28 -0
  54. package/dist/application/target-project.d.ts.map +1 -0
  55. package/dist/application/target-project.js +60 -0
  56. package/dist/application/target-project.js.map +1 -0
  57. package/dist/application/target-temporal-analysis.d.ts +138 -0
  58. package/dist/application/target-temporal-analysis.d.ts.map +1 -0
  59. package/dist/application/target-temporal-analysis.js +409 -0
  60. package/dist/application/target-temporal-analysis.js.map +1 -0
  61. package/dist/application/target-temporal-input.d.ts +86 -0
  62. package/dist/application/target-temporal-input.d.ts.map +1 -0
  63. package/dist/application/target-temporal-input.js +238 -0
  64. package/dist/application/target-temporal-input.js.map +1 -0
  65. package/dist/application/target-unit-migration-candidate.d.ts +60 -0
  66. package/dist/application/target-unit-migration-candidate.d.ts.map +1 -0
  67. package/dist/application/target-unit-migration-candidate.js +290 -0
  68. package/dist/application/target-unit-migration-candidate.js.map +1 -0
  69. package/dist/application/target-unit-migration-request.d.ts +35 -0
  70. package/dist/application/target-unit-migration-request.d.ts.map +1 -0
  71. package/dist/application/target-unit-migration-request.js +68 -0
  72. package/dist/application/target-unit-migration-request.js.map +1 -0
  73. package/dist/application/target-unit-migration-result.d.ts +66 -0
  74. package/dist/application/target-unit-migration-result.d.ts.map +1 -0
  75. package/dist/application/target-unit-migration-result.js +86 -0
  76. package/dist/application/target-unit-migration-result.js.map +1 -0
  77. package/dist/application/unit-migration.d.ts +13 -0
  78. package/dist/application/unit-migration.d.ts.map +1 -0
  79. package/dist/application/unit-migration.js +9 -0
  80. package/dist/application/unit-migration.js.map +1 -0
  81. package/dist/cli.js +494 -383
  82. package/dist/cli.js.map +1 -1
  83. package/dist/command/discovery.d.ts +30 -0
  84. package/dist/command/discovery.d.ts.map +1 -0
  85. package/dist/command/discovery.js +752 -0
  86. package/dist/command/discovery.js.map +1 -0
  87. package/dist/command/registry.d.ts +84 -0
  88. package/dist/command/registry.d.ts.map +1 -0
  89. package/dist/command/registry.js +964 -0
  90. package/dist/command/registry.js.map +1 -0
  91. package/dist/command/usage.d.ts +42 -0
  92. package/dist/command/usage.d.ts.map +1 -0
  93. package/dist/command/usage.js +337 -0
  94. package/dist/command/usage.js.map +1 -0
  95. package/dist/conversion/mermaid-import.js +56 -56
  96. package/dist/conversion/mermaid-import.js.map +1 -1
  97. package/dist/conversion/mermaid.js +1 -1
  98. package/dist/conversion/mermaid.js.map +1 -1
  99. package/dist/formatter/source-formatter.d.ts +12 -0
  100. package/dist/formatter/source-formatter.d.ts.map +1 -1
  101. package/dist/formatter/source-formatter.js +48 -6
  102. package/dist/formatter/source-formatter.js.map +1 -1
  103. package/dist/formatter/target-source-formatter.d.ts +8 -0
  104. package/dist/formatter/target-source-formatter.d.ts.map +1 -0
  105. package/dist/formatter/target-source-formatter.js +36 -0
  106. package/dist/formatter/target-source-formatter.js.map +1 -0
  107. package/dist/help/guide.d.ts +12 -0
  108. package/dist/help/guide.d.ts.map +1 -0
  109. package/dist/help/guide.js +103 -0
  110. package/dist/help/guide.js.map +1 -0
  111. package/dist/help/registry.d.ts +0 -6
  112. package/dist/help/registry.d.ts.map +1 -1
  113. package/dist/help/registry.js +186 -86
  114. package/dist/help/registry.js.map +1 -1
  115. package/dist/index.d.ts +20 -7
  116. package/dist/index.d.ts.map +1 -1
  117. package/dist/index.js +8 -3
  118. package/dist/index.js.map +1 -1
  119. package/dist/io/safe-write.d.ts +7 -0
  120. package/dist/io/safe-write.d.ts.map +1 -1
  121. package/dist/io/safe-write.js +36 -27
  122. package/dist/io/safe-write.js.map +1 -1
  123. package/dist/io/target-safe-write.d.ts +7 -0
  124. package/dist/io/target-safe-write.d.ts.map +1 -0
  125. package/dist/io/target-safe-write.js +33 -0
  126. package/dist/io/target-safe-write.js.map +1 -0
  127. package/dist/migration/conversion.d.ts +40 -0
  128. package/dist/migration/conversion.d.ts.map +1 -0
  129. package/dist/migration/conversion.js +106 -0
  130. package/dist/migration/conversion.js.map +1 -0
  131. package/dist/migration/grammar-boundary.d.ts +20 -0
  132. package/dist/migration/grammar-boundary.d.ts.map +1 -0
  133. package/dist/migration/grammar-boundary.js +92 -0
  134. package/dist/migration/grammar-boundary.js.map +1 -0
  135. package/dist/migration/request.d.ts +79 -0
  136. package/dist/migration/request.d.ts.map +1 -0
  137. package/dist/migration/request.js +308 -0
  138. package/dist/migration/request.js.map +1 -0
  139. package/dist/model/calendar-arithmetic.d.ts +46 -0
  140. package/dist/model/calendar-arithmetic.d.ts.map +1 -0
  141. package/dist/model/calendar-arithmetic.js +236 -0
  142. package/dist/model/calendar-arithmetic.js.map +1 -0
  143. package/dist/model/calendar.d.ts +22 -0
  144. package/dist/model/calendar.d.ts.map +1 -0
  145. package/dist/model/calendar.js +83 -0
  146. package/dist/model/calendar.js.map +1 -0
  147. package/dist/model/declaration-fields.d.ts +15 -0
  148. package/dist/model/declaration-fields.d.ts.map +1 -0
  149. package/dist/model/declaration-fields.js +54 -0
  150. package/dist/model/declaration-fields.js.map +1 -0
  151. package/dist/model/exact-duration-source.d.ts +10 -0
  152. package/dist/model/exact-duration-source.d.ts.map +1 -0
  153. package/dist/model/exact-duration-source.js +69 -0
  154. package/dist/model/exact-duration-source.js.map +1 -0
  155. package/dist/model/rational.d.ts +2 -2
  156. package/dist/model/rational.d.ts.map +1 -1
  157. package/dist/model/rational.js +3 -1
  158. package/dist/model/rational.js.map +1 -1
  159. package/dist/model/syntax.d.ts +9 -2
  160. package/dist/model/syntax.d.ts.map +1 -1
  161. package/dist/model/syntax.js +20 -4
  162. package/dist/model/syntax.js.map +1 -1
  163. package/dist/model/target-calendar.d.ts +25 -0
  164. package/dist/model/target-calendar.d.ts.map +1 -0
  165. package/dist/model/target-calendar.js +56 -0
  166. package/dist/model/target-calendar.js.map +1 -0
  167. package/dist/mutation/advance.d.ts.map +1 -1
  168. package/dist/mutation/advance.js +21 -4
  169. package/dist/mutation/advance.js.map +1 -1
  170. package/dist/mutation/gate.d.ts +5 -0
  171. package/dist/mutation/gate.d.ts.map +1 -0
  172. package/dist/mutation/gate.js +156 -0
  173. package/dist/mutation/gate.js.map +1 -0
  174. package/dist/mutation/milestone.d.ts +10 -1
  175. package/dist/mutation/milestone.d.ts.map +1 -1
  176. package/dist/mutation/milestone.js +104 -48
  177. package/dist/mutation/milestone.js.map +1 -1
  178. package/dist/mutation/project.d.ts +7 -1
  179. package/dist/mutation/project.d.ts.map +1 -1
  180. package/dist/mutation/project.js +32 -19
  181. package/dist/mutation/project.js.map +1 -1
  182. package/dist/mutation/resource.js +22 -22
  183. package/dist/mutation/resource.js.map +1 -1
  184. package/dist/mutation/target-types.d.ts +42 -0
  185. package/dist/mutation/target-types.d.ts.map +1 -0
  186. package/dist/mutation/target-types.js +2 -0
  187. package/dist/mutation/target-types.js.map +1 -0
  188. package/dist/mutation/task.d.ts +12 -1
  189. package/dist/mutation/task.d.ts.map +1 -1
  190. package/dist/mutation/task.js +148 -89
  191. package/dist/mutation/task.js.map +1 -1
  192. package/dist/mutation/types.d.ts +34 -3
  193. package/dist/mutation/types.d.ts.map +1 -1
  194. package/dist/parser/document-parser.d.ts +14 -0
  195. package/dist/parser/document-parser.d.ts.map +1 -1
  196. package/dist/parser/document-parser.js +159 -55
  197. package/dist/parser/document-parser.js.map +1 -1
  198. package/dist/recommendation/override-types.d.ts +2 -2
  199. package/dist/recommendation/override.d.ts +2 -2
  200. package/dist/recommendation/override.d.ts.map +1 -1
  201. package/dist/recommendation/override.js +49 -12
  202. package/dist/recommendation/override.js.map +1 -1
  203. package/dist/semantic/target-validator.d.ts +36 -0
  204. package/dist/semantic/target-validator.d.ts.map +1 -0
  205. package/dist/semantic/target-validator.js +87 -0
  206. package/dist/semantic/target-validator.js.map +1 -0
  207. package/dist/semantic/validator.d.ts +2 -0
  208. package/dist/semantic/validator.d.ts.map +1 -1
  209. package/dist/semantic/validator.js +90 -45
  210. package/dist/semantic/validator.js.map +1 -1
  211. package/dist/version.d.ts +1 -1
  212. package/dist/version.js +1 -1
  213. package/package.json +3 -3
package/dist/cli.js CHANGED
@@ -2,17 +2,21 @@
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
+ import { planProjectInit, projectInitResultToJson, renderProjectInitResult, withProjectInitOutput, } from "./application/init.js";
8
9
  import { planBatchMutation, planMutation } from "./application/mutate.js";
9
- import { selectNextTasks } from "./application/next.js";
10
+ import { planUnitMigration, withUnitMigrationWrite, } from "./application/unit-migration.js";
10
11
  import { getProjectMetadata, } from "./application/project.js";
11
12
  import { getAgentHelp } from "./application/agent-help.js";
12
13
  import { exportMermaid, } from "./conversion/mermaid.js";
13
14
  import { importMermaid } from "./conversion/mermaid-import.js";
14
- import { getAgentHelpCommandHelp, getHelp, } from "./help/registry.js";
15
- import { serializeAgentGuidanceResult } from "./guidance/projection.js";
15
+ import { getGuide, renderGuideResult, serializeGuideResult, } from "./help/guide.js";
16
+ import { commandOptionSets, } from "./command/registry.js";
17
+ import { CONTRACT4_COMMAND_REGISTRY, getCommandDiscovery, renderCommandHelpResult, serializeCommandHelpResult, } from "./command/discovery.js";
18
+ import { handlerCommandUsageError, renderCommandUsageError, serializeCommandUsageError, validateCommandInvocation, } from "./command/usage.js";
19
+ import { agentGuidanceResultToJson } from "./guidance/projection.js";
16
20
  import { agentGuidanceExitCode, renderAgentGuidanceText, } from "./guidance/text.js";
17
21
  import { documentContentFromBytes, readDocumentFile, } from "./io/document-file.js";
18
22
  import { createArtifactFile, createDocumentFile, replaceDocumentFile, SafeWriteConflictError, SafeWriteVerificationError, } from "./io/safe-write.js";
@@ -25,183 +29,6 @@ import { TOOL_VERSION } from "./version.js";
25
29
  class UsageError extends Error {
26
30
  code = "PTCLI-001";
27
31
  }
28
- function topLevelHelp() {
29
- return [
30
- "perttool - document-based PERT/CPM task management",
31
- "",
32
- "Usage:",
33
- " perttool --version",
34
- " perttool --help",
35
- " perttool dsl check <file> [--format text|json]",
36
- " perttool dsl format <file> [--check] [--diff] [--format text|json]",
37
- " perttool dsl help [topic [subtopic]] [--level index|quick|detail] [--format text|json]",
38
- " perttool agent help [provider [surface]] [--level index|quick|detail] [--format text|json]",
39
- " perttool project show <file> [--format text|json]",
40
- " perttool project set <file> [field options] [--diff] [--write | --out <path>]",
41
- " perttool dag analyze <file> [--schedule precedence|resource|both] [--format text|json]",
42
- " perttool dag next <file> [--capacity <resource-id>=<integer>] [--format text|json]",
43
- " perttool dag advance <file> [--diff] [--write | --out <path>] [--format text|json]",
44
- " perttool dag render <file> --to mermaid [--profile perttool|plain] [--format text|json]",
45
- " perttool dag import <file> --from mermaid [--strict-loss] [--out <path>] [--format text|json]",
46
- " perttool task add|set|remove|finish ...",
47
- " perttool milestone add|set|remove ...",
48
- " perttool resource add|set|remove ...",
49
- " perttool mutation apply <file> --request <json-file|-> [--diff] [--format text|json]",
50
- "",
51
- "Format and mutation commands preview by default; use --write or --out for explicit writes.",
52
- ].join("\n");
53
- }
54
- function commandHelp(resource, action) {
55
- if (resource === "agent" && action === "help") {
56
- return getAgentHelpCommandHelp().syntax.join("\n");
57
- }
58
- if (resource === "dsl" && action === "check") {
59
- return [
60
- "Usage: perttool dsl check <file>",
61
- " [--warnings-as-errors]",
62
- " [--max-diagnostics <integer>]",
63
- " [--format text|json]",
64
- " [--color auto|always|never]",
65
- ].join("\n");
66
- }
67
- if (resource === "dsl" && action === "format")
68
- return [
69
- "Usage: perttool dsl format <file>",
70
- " [--check] [--diff]",
71
- " [--write [--expect-digest <digest>] | --out <path>]",
72
- " [--max-diagnostics <integer>] [--warnings-as-errors]",
73
- " [--format text|json] [--color auto|always|never]",
74
- ].join("\n");
75
- if (resource === "dsl" && action === "help")
76
- return [
77
- "Usage: perttool dsl help [topic [subtopic]]",
78
- " [--level index|quick|detail]",
79
- " [--format text|json]",
80
- " [--color auto|always|never]",
81
- ].join("\n");
82
- if (resource === "project" && action === "show")
83
- return [
84
- "Usage: perttool project show <file>",
85
- " [--max-diagnostics <integer>] [--warnings-as-errors]",
86
- " [--format text|json] [--color auto|always|never]",
87
- ].join("\n");
88
- if (resource === "project" && action === "set")
89
- return [
90
- "Usage: perttool project set <file> [field options]",
91
- " [--id <id>] [--version <integer>] [--title <text>] [--description <text>]",
92
- " [--as-of <date-or-date-time>] [--duration-unit day|hour|point]",
93
- " [--velocity <velocity>] [--finish <milestone-id>]",
94
- " [--critical-epsilon <duration>] [--target-duration <duration>]",
95
- " [--clear description|as_of|velocity|critical_epsilon|target_duration]...",
96
- " [--diff] [--write [--expect-digest <digest>] | --out <path>]",
97
- " [--max-diagnostics <integer>] [--warnings-as-errors]",
98
- " [--format text|json] [--color auto|always|never]",
99
- ].join("\n");
100
- if (resource === "dag" && action === "analyze")
101
- return [
102
- "Usage: perttool dag analyze <file>",
103
- " [--schedule precedence|resource|both]",
104
- " [--capacity <resource-id>=<integer>]...",
105
- " [--max-paths <integer>] [--precision <integer>]",
106
- " [--max-diagnostics <integer>]",
107
- " [--warnings-as-errors]",
108
- " [--format text|json] [--color auto|always|never]",
109
- ].join("\n");
110
- if (resource === "dag" && action === "next")
111
- return [
112
- "Usage: perttool dag next <file>",
113
- " [--capacity <resource-id>=<integer>]...",
114
- " [--explain-depth <integer>] [--precision <integer>]",
115
- " [--max-diagnostics <integer>]",
116
- " [--warnings-as-errors]",
117
- " [--format text|json] [--color auto|always|never]",
118
- "Output: Perttool.NextResult.v3 with a complete recommendation graph in JSON.",
119
- "Consumers must inspect schema_version before using recommendation authority.",
120
- ].join("\n");
121
- if (resource === "dag" && action === "advance")
122
- return [
123
- "Usage: perttool dag advance <file>",
124
- " [--diff] [--write [--expect-digest <digest>] | --out <path>]",
125
- " [--max-diagnostics <integer>] [--warnings-as-errors]",
126
- " [--format text|json] [--color auto|always|never]",
127
- ].join("\n");
128
- if (resource === "dag" && action === "render")
129
- return [
130
- "Usage: perttool dag render <file> --to mermaid",
131
- " [--profile perttool|plain] [--analysis none|precedence|resource|both]",
132
- " [--capacity <resource-id>=<integer>]... [--strict-loss] [--out <path>]",
133
- " [--max-diagnostics <integer>] [--warnings-as-errors]",
134
- " [--format text|json] [--color auto|always|never]",
135
- ].join("\n");
136
- if (resource === "dag" && action === "import")
137
- return [
138
- "Usage: perttool dag import <file> --from mermaid",
139
- " [--strict-loss] [--out <path>]",
140
- " [--max-diagnostics <integer>] [--warnings-as-errors]",
141
- " [--format text|json] [--color auto|always|never]",
142
- ].join("\n");
143
- const preview = " [--diff] [--write [--expect-digest <digest>] | --out <path>] [--max-diagnostics <integer>] [--warnings-as-errors] [--format text|json] [--color auto|always|never]";
144
- if (resource === "task" && action === "add")
145
- return [
146
- "Usage: perttool task add <file> <id> <from> <to>",
147
- " --title <text> (--duration <duration> | --optimistic <duration> --most-likely <duration> --pessimistic <duration>)",
148
- " [--description <text>] [--status planned|active|blocked|done] [--priority <integer>]",
149
- " [--owner <text>] [--blocked-reason <text>] [--source <text>] [--tag <tag>]... [--require <resource-id>=<integer>]...",
150
- preview,
151
- ].join("\n");
152
- if (resource === "task" && action === "set")
153
- return [
154
- "Usage: perttool task set <file> <id> [field options]",
155
- " [--from <id>] [--to <id>] [--title <text>] [--description <text>] [--duration <duration>]",
156
- " [--optimistic <duration> --most-likely <duration> --pessimistic <duration>]",
157
- " [--status planned|active|blocked|done] [--priority <integer>] [--owner <text>]",
158
- " [--blocked-reason <text>] [--source <text>] [--require <resource-id>=<integer>]...",
159
- " [--add-tag <tag>]... [--remove-tag <tag>]... [--remove-require <resource-id>]... [--clear <field>]...",
160
- preview,
161
- ].join("\n");
162
- if (resource === "task")
163
- return [
164
- `Usage: perttool task ${action} <file> <id>`,
165
- preview,
166
- ].join("\n");
167
- if (resource === "milestone" && action === "add")
168
- return [
169
- "Usage: perttool milestone add <file> <id> --title <text>",
170
- " [--description <text>] [--state planned|reached] [--tag <tag>]...",
171
- preview,
172
- ].join("\n");
173
- if (resource === "milestone" && action === "set")
174
- return [
175
- "Usage: perttool milestone set <file> <id> [--title <text>] [--description <text>] [--state planned|reached]",
176
- " [--add-tag <tag>]... [--remove-tag <tag>]... [--clear description|state|tags]...",
177
- preview,
178
- ].join("\n");
179
- if (resource === "milestone")
180
- return [
181
- `Usage: perttool milestone ${action} <file> <id>`,
182
- preview,
183
- ].join("\n");
184
- if (resource === "resource" && action === "add")
185
- return [
186
- "Usage: perttool resource add <file> <id> --title <text> --capacity <integer>",
187
- " [--description <text>]",
188
- preview,
189
- ].join("\n");
190
- if (resource === "resource" && action === "set")
191
- return [
192
- "Usage: perttool resource set <file> <id> [--title <text>] [--description <text>] [--capacity <integer>] [--clear description]",
193
- preview,
194
- ].join("\n");
195
- if (resource === "resource")
196
- return [
197
- `Usage: perttool resource ${action} <file> <id>`,
198
- preview,
199
- ].join("\n");
200
- return [
201
- "Usage: perttool mutation apply <file> --request <json-file|->",
202
- preview,
203
- ].join("\n");
204
- }
205
32
  function parseOptions(args, valueOptions, flagOptions, repeatableOptions = new Set()) {
206
33
  const positionals = [];
207
34
  const values = new Map();
@@ -251,6 +78,14 @@ function parseOptions(args, valueOptions, flagOptions, repeatableOptions = new S
251
78
  }
252
79
  return { positionals, values, repeatedValues, flags };
253
80
  }
81
+ function parseCommandOptions(operation, args) {
82
+ const descriptor = CONTRACT4_COMMAND_REGISTRY.find((candidate) => candidate.operation === operation);
83
+ if (descriptor === undefined) {
84
+ throw new Error(`command descriptor is missing for ${operation}`);
85
+ }
86
+ const optionSets = commandOptionSets(descriptor);
87
+ return parseOptions(args, optionSets.values, optionSets.flags, optionSets.repeatable);
88
+ }
254
89
  function outputFormat(value) {
255
90
  if (value === undefined || value === "text")
256
91
  return "text";
@@ -300,7 +135,8 @@ function jsonDiagnostic(diagnostic) {
300
135
  message: related.message,
301
136
  span: jsonSpan(related.span),
302
137
  })),
303
- help_topic: diagnostic.helpTopic ?? null,
138
+ help_topic: null,
139
+ guide_topic: diagnostic.helpTopic ?? null,
304
140
  expected_syntax: diagnostic.expectedSyntax ?? null,
305
141
  fixes: [],
306
142
  data: diagnostic.data ?? {},
@@ -326,7 +162,7 @@ function renderDiagnostic(diagnostic, source, mode) {
326
162
  }
327
163
  if (diagnostic.helpTopic !== undefined) {
328
164
  const [topic, subtopic] = diagnostic.helpTopic.split(".", 2);
329
- lines.push(` help: perttool dsl help ${topic}${subtopic === undefined ? "" : ` ${subtopic}`} --level quick`);
165
+ lines.push(` guide: perttool guide ${topic}${subtopic === undefined ? "" : ` ${subtopic}`} --level quick`);
330
166
  }
331
167
  return lines.join("\n");
332
168
  }
@@ -346,6 +182,18 @@ async function readStdin() {
346
182
  function writeJson(value) {
347
183
  process.stdout.write(`${JSON.stringify(value)}\n`);
348
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
+ }
349
197
  function cliError(error, exitCode, operation, json) {
350
198
  const code = error instanceof UsageError
351
199
  ? error.code
@@ -369,6 +217,7 @@ function cliError(error, exitCode, operation, json) {
369
217
  if (json) {
370
218
  writeJson({
371
219
  schema_version: "Perttool.CliError.v1",
220
+ cli_contract_version: 4,
372
221
  tool_version: TOOL_VERSION,
373
222
  operation,
374
223
  ok: false,
@@ -381,9 +230,9 @@ function cliError(error, exitCode, operation, json) {
381
230
  return exitCode;
382
231
  }
383
232
  async function runCheck(args) {
384
- const parsed = parseOptions(args, new Set(["format", "color", "max-diagnostics"]), new Set(["warnings-as-errors"]));
233
+ const parsed = parseCommandOptions("document.check", args);
385
234
  if (parsed.positionals.length !== 1) {
386
- throw new UsageError("dsl check requires exactly one <file>");
235
+ throw new UsageError("document check requires exactly one <file>");
387
236
  }
388
237
  const format = outputFormat(parsed.values.get("format"));
389
238
  const color = colorMode(parsed.values.get("color"), format);
@@ -395,7 +244,7 @@ async function runCheck(args) {
395
244
  input = await readDocument(sourceOperand);
396
245
  }
397
246
  catch (error) {
398
- return cliError(error instanceof Error ? error : new Error(String(error)), 3, "dsl.check", format === "json");
247
+ return cliError(error instanceof Error ? error : new Error(String(error)), 3, "document.check", format === "json");
399
248
  }
400
249
  const result = checkDocument(input.text, { maxDiagnostics });
401
250
  const warningsAsErrors = parsed.flags.has("warnings-as-errors");
@@ -403,9 +252,10 @@ async function runCheck(args) {
403
252
  const ok = result.ok && !warningFailure;
404
253
  if (format === "json") {
405
254
  writeJson({
406
- schema_version: "Perttool.CheckResult.v1",
255
+ schema_version: "Perttool.CheckResult.v2",
256
+ cli_contract_version: 4,
407
257
  tool_version: TOOL_VERSION,
408
- operation: "dsl.check",
258
+ operation: "document.check",
409
259
  ok,
410
260
  document_id: result.documentId,
411
261
  source,
@@ -414,11 +264,12 @@ async function runCheck(args) {
414
264
  diagnostics_truncated: result.diagnosticsTruncated,
415
265
  grammar_version: result.grammarVersion,
416
266
  summary: result.summary,
267
+ temporal_inputs: snakeJson(result.temporalInputs),
417
268
  });
418
269
  }
419
270
  else {
420
271
  if (ok) {
421
- 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`);
422
273
  }
423
274
  for (const diagnostic of result.diagnostics) {
424
275
  process.stderr.write(`${renderDiagnostic(diagnostic, source, color)}\n`);
@@ -435,32 +286,37 @@ function projectJson(project) {
435
286
  version: project.version,
436
287
  title: project.title,
437
288
  description: project.description,
438
- as_of: project.asOf,
289
+ as_of: snakeJson(project.asOf),
439
290
  duration_unit: project.durationUnit,
440
291
  velocity: project.velocity,
441
292
  finish: project.finish,
293
+ finish_deadline: snakeJson(project.finishDeadline),
442
294
  critical_epsilon: project.criticalEpsilon,
443
295
  target_duration: project.targetDuration,
444
296
  };
445
297
  }
446
298
  function renderProjectText(project) {
447
299
  const optional = (value) => value ?? "-";
300
+ const calendar = (value) => value === null
301
+ ? "-"
302
+ : `${value.kind.toUpperCase()} ${value.sourceText ?? "-"}`;
448
303
  return [
449
304
  `PROJECT ${project.id}`,
450
305
  `VERSION ${project.version}`,
451
306
  `TITLE ${JSON.stringify(project.title)}`,
452
307
  `DESCRIPTION ${project.description === null ? "-" : JSON.stringify(project.description)}`,
453
- `AS_OF ${optional(project.asOf)}`,
308
+ `AS_OF ${calendar(project.asOf)}`,
454
309
  `DURATION_UNIT ${project.durationUnit}`,
455
310
  `VELOCITY ${optional(project.velocity)}`,
456
311
  `FINISH ${project.finish}`,
312
+ `FINISH_DEADLINE ${calendar(project.finishDeadline)}`,
457
313
  `CRITICAL_EPSILON ${optional(project.criticalEpsilon)}`,
458
314
  `TARGET_DURATION ${optional(project.targetDuration)}`,
459
315
  "",
460
316
  ].join("\n");
461
317
  }
462
318
  async function runProjectShow(args) {
463
- const parsed = parseOptions(args, new Set(["format", "color", "max-diagnostics"]), new Set(["warnings-as-errors"]));
319
+ const parsed = parseCommandOptions("project.show", args);
464
320
  if (parsed.positionals.length !== 1) {
465
321
  throw new UsageError("project show requires exactly one <file>");
466
322
  }
@@ -483,7 +339,8 @@ async function runProjectShow(args) {
483
339
  const ok = result.ok && !warningFailure;
484
340
  if (format === "json") {
485
341
  writeJson({
486
- schema_version: "Perttool.ProjectResult.v1",
342
+ schema_version: "Perttool.ProjectResult.v2",
343
+ cli_contract_version: 4,
487
344
  tool_version: TOOL_VERSION,
488
345
  operation: "project.show",
489
346
  ok,
@@ -509,6 +366,61 @@ async function runProjectShow(args) {
509
366
  }
510
367
  return ok ? 0 : 1;
511
368
  }
369
+ async function runProjectInit(args) {
370
+ const parsed = parseCommandOptions("project.init", args);
371
+ const format = outputFormat(parsed.values.get("format"));
372
+ const color = colorMode(parsed.values.get("color"), format);
373
+ const durationUnit = enumOption(requiredOption(parsed, "duration-unit"), "duration-unit", new Set(["day", "hour", "point"]));
374
+ const version = optionalInteger(parsed, "version", 0, 2_147_483_647);
375
+ let result = planProjectInit({
376
+ projectId: parsed.positionals[0],
377
+ title: requiredOption(parsed, "title"),
378
+ durationUnit,
379
+ initialMilestone: requiredOption(parsed, "initial-milestone"),
380
+ initialMilestoneTitle: requiredOption(parsed, "initial-milestone-title"),
381
+ finish: requiredOption(parsed, "finish"),
382
+ ...(version === undefined ? {} : { version }),
383
+ ...(parsed.values.get("as-of") === undefined
384
+ ? {}
385
+ : { asOf: parsed.values.get("as-of") }),
386
+ ...(parsed.values.get("velocity") === undefined
387
+ ? {}
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
+ }),
394
+ });
395
+ let writeResult = null;
396
+ const output = parsed.values.get("out");
397
+ if (result.ok && output !== undefined) {
398
+ try {
399
+ writeResult = await createDocumentFile(output, result.candidateText);
400
+ result = withProjectInitOutput(result, writeResult);
401
+ }
402
+ catch (error) {
403
+ return writeFailureExit(error, "project.init", format === "json");
404
+ }
405
+ }
406
+ if (format === "json") {
407
+ writeJson(projectInitResultToJson(result));
408
+ }
409
+ else {
410
+ if (result.ok) {
411
+ if (writeResult === null) {
412
+ process.stdout.write(renderProjectInitResult(result));
413
+ }
414
+ else {
415
+ process.stderr.write(renderWriteSummary("project.init", writeResult));
416
+ }
417
+ }
418
+ for (const diagnostic of result.diagnostics) {
419
+ process.stderr.write(`${renderDiagnostic(diagnostic, "<project-init>", color)}\n`);
420
+ }
421
+ }
422
+ return result.ok ? 0 : 1;
423
+ }
512
424
  function boundedInteger(raw, option, defaultValue, minimum, maximum) {
513
425
  if (raw === undefined)
514
426
  return defaultValue;
@@ -546,14 +458,6 @@ function capacityOverrides(values) {
546
458
  }
547
459
  return overrides;
548
460
  }
549
- const mutationCommonValueOptions = [
550
- "format",
551
- "color",
552
- "max-diagnostics",
553
- "out",
554
- "expect-digest",
555
- ];
556
- const mutationCommonFlagOptions = ["diff", "write", "warnings-as-errors"];
557
461
  function editingWriteRequest(parsed, sourceOperand, check = false) {
558
462
  const write = parsed.flags.has("write");
559
463
  const out = parsed.values.get("out");
@@ -594,12 +498,12 @@ function assertExpectedDigest(request, initialDigest) {
594
498
  if (request.mode === "in_place" &&
595
499
  request.expectedDigest !== undefined &&
596
500
  request.expectedDigest !== initialDigest) {
597
- throw new SafeWriteConflictError("expected_digest_mismatch", "--expect-digestがinitial document digestと一致しません");
501
+ throw new SafeWriteConflictError("expected_digest_mismatch", "--expect-digest does not match the initial document digest");
598
502
  }
599
503
  }
600
504
  async function commitCandidate(request, candidateText, initialDigest) {
601
505
  if (candidateText === null) {
602
- throw new SafeWriteVerificationError("invalid_candidate", "successful editing resultにcandidate textがありません");
506
+ throw new SafeWriteVerificationError("invalid_candidate", "successful editing result has no candidate text");
603
507
  }
604
508
  return request.mode === "in_place"
605
509
  ? replaceDocumentFile(request.target, candidateText, {
@@ -623,9 +527,9 @@ function renderWriteSummary(operation, result) {
623
527
  return `WRITE ${operation} mode=${result.mode} target=${result.target} digest=${result.digest} written=${result.written}\n`;
624
528
  }
625
529
  async function runFormat(args) {
626
- const parsed = parseOptions(args, new Set(["format", "color", "max-diagnostics", "out", "expect-digest"]), new Set(["check", "diff", "write", "warnings-as-errors"]));
530
+ const parsed = parseCommandOptions("document.format", args);
627
531
  if (parsed.positionals.length !== 1) {
628
- throw new UsageError("dsl format requires exactly one <file>");
532
+ throw new UsageError("document format requires exactly one <file>");
629
533
  }
630
534
  const format = outputFormat(parsed.values.get("format"));
631
535
  const color = colorMode(parsed.values.get("color"), format);
@@ -638,7 +542,7 @@ async function runFormat(args) {
638
542
  input = await readDocument(sourceOperand);
639
543
  }
640
544
  catch (error) {
641
- return cliError(error instanceof Error ? error : new Error(String(error)), 3, "dsl.format", format === "json");
545
+ return cliError(error instanceof Error ? error : new Error(String(error)), 3, "document.format", format === "json");
642
546
  }
643
547
  const result = planFormat(input.text, {
644
548
  maxDiagnostics,
@@ -659,14 +563,15 @@ async function runFormat(args) {
659
563
  }
660
564
  }
661
565
  catch (error) {
662
- return writeFailureExit(error, "dsl.format", format === "json");
566
+ return writeFailureExit(error, "document.format", format === "json");
663
567
  }
664
568
  }
665
569
  if (format === "json") {
666
570
  writeJson({
667
571
  schema_version: "Perttool.FormatResult.v1",
572
+ cli_contract_version: 4,
668
573
  tool_version: TOOL_VERSION,
669
- operation: "dsl.format",
574
+ operation: "document.format",
670
575
  ok,
671
576
  document_id: result.documentId,
672
577
  source,
@@ -680,7 +585,7 @@ async function runFormat(args) {
680
585
  const candidateAllowed = result.ok && !warningFailure;
681
586
  if (candidateAllowed) {
682
587
  if (writeResult !== null) {
683
- process.stderr.write(renderWriteSummary("dsl.format", writeResult));
588
+ process.stderr.write(renderWriteSummary("document.format", writeResult));
684
589
  }
685
590
  else if (parsed.flags.has("check")) {
686
591
  if (parsed.flags.has("diff"))
@@ -689,7 +594,7 @@ async function runFormat(args) {
689
594
  else {
690
595
  process.stdout.write(parsed.flags.has("diff") ? (result.diff ?? "") : (result.updatedText ?? ""));
691
596
  if (!parsed.flags.has("diff")) {
692
- process.stderr.write(`PREVIEW dsl.format changed=${result.changed} original_digest=${result.originalDigest} updated_digest=${result.updatedDigest}\n`);
597
+ process.stderr.write(`PREVIEW document.format changed=${result.changed} original_digest=${result.originalDigest} updated_digest=${result.updatedDigest}\n`);
693
598
  }
694
599
  }
695
600
  }
@@ -702,50 +607,6 @@ async function runFormat(args) {
702
607
  }
703
608
  return ok ? 0 : 1;
704
609
  }
705
- function mutationOptionSets(resource, action) {
706
- const values = new Set(mutationCommonValueOptions);
707
- const flags = new Set(mutationCommonFlagOptions);
708
- const repeatable = new Set();
709
- const addValues = (...names) => {
710
- for (const name of names)
711
- values.add(name);
712
- };
713
- const addRepeatable = (...names) => {
714
- for (const name of names)
715
- repeatable.add(name);
716
- };
717
- if (resource === "project" && action === "set") {
718
- addValues("id", "version", "title", "description", "as-of", "duration-unit", "velocity", "finish", "critical-epsilon", "target-duration");
719
- addRepeatable("clear");
720
- }
721
- else if (resource === "task" && action === "add") {
722
- addValues("title", "description", "duration", "optimistic", "most-likely", "pessimistic", "status", "priority", "owner", "blocked-reason", "source");
723
- addRepeatable("tag", "require");
724
- }
725
- else if (resource === "task" && action === "set") {
726
- addValues("from", "to", "title", "description", "duration", "optimistic", "most-likely", "pessimistic", "status", "priority", "owner", "blocked-reason", "source");
727
- addRepeatable("require", "add-tag", "remove-tag", "remove-require", "clear");
728
- }
729
- else if (resource === "milestone" && action === "add") {
730
- addValues("title", "description", "state");
731
- addRepeatable("tag");
732
- }
733
- else if (resource === "milestone" && action === "set") {
734
- addValues("title", "description", "state");
735
- addRepeatable("add-tag", "remove-tag", "clear");
736
- }
737
- else if (resource === "resource" && action === "add") {
738
- addValues("title", "description", "capacity");
739
- }
740
- else if (resource === "resource" && action === "set") {
741
- addValues("title", "description", "capacity");
742
- addRepeatable("clear");
743
- }
744
- else if (resource === "mutation" && action === "apply") {
745
- addValues("request");
746
- }
747
- return { values, flags, repeatable };
748
- }
749
610
  function requiredOption(parsed, name) {
750
611
  const value = parsed.values.get(name);
751
612
  if (value === undefined)
@@ -862,6 +723,12 @@ function taskMutationFromOptions(action, parsed) {
862
723
  ...(parsed.values.get("description") === undefined
863
724
  ? {}
864
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") }),
865
732
  ...(status === undefined ? {} : { status }),
866
733
  ...(priority === undefined ? {} : { priority }),
867
734
  ...(requirements.length === 0 ? {} : { requirements }),
@@ -882,7 +749,7 @@ function taskMutationFromOptions(action, parsed) {
882
749
  }
883
750
  const clear = enumRepeated(parsed, "clear", new Set([
884
751
  "description", "status", "priority", "owner", "blocked_reason", "source",
885
- "tags", "requires",
752
+ "tags", "requires", "not_before", "deadline",
886
753
  ]));
887
754
  const addTags = uniqueRepeated(parsed, "add-tag");
888
755
  const removeTags = uniqueRepeated(parsed, "remove-tag");
@@ -897,6 +764,12 @@ function taskMutationFromOptions(action, parsed) {
897
764
  ? {}
898
765
  : { description: parsed.values.get("description") }),
899
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") }),
900
773
  ...(status === undefined ? {} : { status }),
901
774
  ...(priority === undefined ? {} : { priority }),
902
775
  ...(parsed.values.get("owner") === undefined
@@ -911,6 +784,8 @@ function taskMutationFromOptions(action, parsed) {
911
784
  };
912
785
  const clearConflicts = new Map([
913
786
  ["description", parsed.values.has("description")],
787
+ ["not_before", parsed.values.has("not-before")],
788
+ ["deadline", parsed.values.has("deadline")],
914
789
  ["status", status !== undefined],
915
790
  ["priority", priority !== undefined],
916
791
  ["owner", parsed.values.has("owner")],
@@ -1010,18 +885,22 @@ function milestoneMutationFromOptions(action, parsed) {
1010
885
  ? {}
1011
886
  : { description: parsed.values.get("description") }),
1012
887
  ...(state === undefined ? {} : { state }),
888
+ ...(parsed.values.get("deadline") === undefined
889
+ ? {}
890
+ : { deadline: parsed.values.get("deadline") }),
1013
891
  ...((parsed.repeatedValues.get("tag") ?? []).length === 0
1014
892
  ? {}
1015
893
  : { tags: parsed.repeatedValues.get("tag") }),
1016
894
  },
1017
895
  };
1018
896
  }
1019
- const clear = enumRepeated(parsed, "clear", new Set(["description", "state", "tags"]));
897
+ const clear = enumRepeated(parsed, "clear", new Set(["description", "state", "deadline", "tags"]));
1020
898
  const addTags = uniqueRepeated(parsed, "add-tag");
1021
899
  const removeTags = uniqueRepeated(parsed, "remove-tag");
1022
900
  ensureDisjoint(addTags, "add-tag", removeTags, "remove-tag");
1023
901
  if (clear.some((field) => (field === "description" && parsed.values.has("description")) ||
1024
902
  (field === "state" && state !== undefined) ||
903
+ (field === "deadline" && parsed.values.has("deadline")) ||
1025
904
  (field === "tags" && (addTags.length > 0 || removeTags.length > 0)))) {
1026
905
  throw new UsageError("--clear conflicts with another milestone field option");
1027
906
  }
@@ -1033,6 +912,9 @@ function milestoneMutationFromOptions(action, parsed) {
1033
912
  ? {}
1034
913
  : { description: parsed.values.get("description") }),
1035
914
  ...(state === undefined ? {} : { state }),
915
+ ...(parsed.values.get("deadline") === undefined
916
+ ? {}
917
+ : { deadline: parsed.values.get("deadline") }),
1036
918
  };
1037
919
  return {
1038
920
  kind: "milestone.set",
@@ -1086,6 +968,37 @@ function resourceMutationFromOptions(action, parsed) {
1086
968
  ...(clear.length === 0 ? {} : { clear }),
1087
969
  };
1088
970
  }
971
+ function gateMutationFromOptions(action, parsed) {
972
+ const expectedPositionals = action === "add" ? 4 : 2;
973
+ if (parsed.positionals.length !== expectedPositionals) {
974
+ throw new UsageError(`gate ${action} requires ${action === "add" ? "<file> <id> <from> <to>" : "<file> <id>"}`);
975
+ }
976
+ const id = parsed.positionals[1];
977
+ if (action === "remove")
978
+ return { kind: "gate.remove", id };
979
+ if (action === "add") {
980
+ return {
981
+ kind: "gate.add",
982
+ id,
983
+ from: parsed.positionals[2],
984
+ to: parsed.positionals[3],
985
+ gate: { reason: requiredOption(parsed, "reason") },
986
+ };
987
+ }
988
+ const from = parsed.values.get("from");
989
+ const to = parsed.values.get("to");
990
+ const reason = parsed.values.get("reason");
991
+ if (from === undefined && to === undefined && reason === undefined) {
992
+ throw new UsageError("gate set requires --from, --to, or --reason");
993
+ }
994
+ return {
995
+ kind: "gate.set",
996
+ id,
997
+ ...(from === undefined ? {} : { from }),
998
+ ...(to === undefined ? {} : { to }),
999
+ ...(reason === undefined ? {} : { set: { reason } }),
1000
+ };
1001
+ }
1089
1002
  function previewResultJson(result, exposeCandidate, writeRequest = { mode: "preview", target: null }, writeResult = null) {
1090
1003
  return {
1091
1004
  changed: exposeCandidate ? result.changed : false,
@@ -1116,8 +1029,7 @@ async function readMutationRequest(source) {
1116
1029
  }
1117
1030
  }
1118
1031
  async function runMutation(resource, action, args) {
1119
- const config = mutationOptionSets(resource, action);
1120
- const parsed = parseOptions(args, config.values, config.flags, config.repeatable);
1032
+ const parsed = parseCommandOptions(`${resource}.${action}`, args);
1121
1033
  const format = outputFormat(parsed.values.get("format"));
1122
1034
  const color = colorMode(parsed.values.get("color"), format);
1123
1035
  const maxDiagnostics = boundedInteger(parsed.values.get("max-diagnostics"), "max-diagnostics", 100, 1, 1000);
@@ -1125,9 +1037,9 @@ async function runMutation(resource, action, args) {
1125
1037
  let sourceOperand;
1126
1038
  let writeRequest;
1127
1039
  let mutation;
1128
- if (resource === "mutation") {
1040
+ if (resource === "batch") {
1129
1041
  if (parsed.positionals.length !== 1) {
1130
- throw new UsageError("mutation apply requires exactly one <file>");
1042
+ throw new UsageError("batch apply requires exactly one <file>");
1131
1043
  }
1132
1044
  sourceOperand = parsed.positionals[0];
1133
1045
  writeRequest = editingWriteRequest(parsed, sourceOperand);
@@ -1152,9 +1064,11 @@ async function runMutation(resource, action, args) {
1152
1064
  ? projectMutationFromOptions(parsed)
1153
1065
  : resource === "task"
1154
1066
  ? taskMutationFromOptions(action, parsed)
1155
- : resource === "milestone"
1156
- ? milestoneMutationFromOptions(action, parsed)
1157
- : resourceMutationFromOptions(action, parsed);
1067
+ : resource === "gate"
1068
+ ? gateMutationFromOptions(action, parsed)
1069
+ : resource === "milestone"
1070
+ ? milestoneMutationFromOptions(action, parsed)
1071
+ : resourceMutationFromOptions(action, parsed);
1158
1072
  sourceOperand = parsed.positionals[0];
1159
1073
  writeRequest = editingWriteRequest(parsed, sourceOperand);
1160
1074
  }
@@ -1171,7 +1085,7 @@ async function runMutation(resource, action, args) {
1171
1085
  originalLabel: source,
1172
1086
  updatedLabel: "candidate",
1173
1087
  };
1174
- const result = resource === "mutation"
1088
+ const result = resource === "batch"
1175
1089
  ? planBatchMutation(input.text, mutation, mutationOptions)
1176
1090
  : planMutation(input.text, mutation, mutationOptions);
1177
1091
  const warningFailure = parsed.flags.has("warnings-as-errors") &&
@@ -1193,6 +1107,7 @@ async function runMutation(resource, action, args) {
1193
1107
  if (format === "json") {
1194
1108
  writeJson({
1195
1109
  schema_version: "Perttool.MutationResult.v1",
1110
+ cli_contract_version: 4,
1196
1111
  tool_version: TOOL_VERSION,
1197
1112
  operation,
1198
1113
  ok,
@@ -1225,6 +1140,142 @@ async function runMutation(resource, action, args) {
1225
1140
  }
1226
1141
  return ok ? 0 : 1;
1227
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
+ }
1228
1279
  function advanceResultJson(details) {
1229
1280
  return details === null
1230
1281
  ? null
@@ -1247,7 +1298,7 @@ function renderAdvanceSummary(details) {
1247
1298
  ].join("\n");
1248
1299
  }
1249
1300
  async function runAdvance(args) {
1250
- const parsed = parseOptions(args, new Set(["format", "color", "max-diagnostics", "out", "expect-digest"]), new Set(["diff", "write", "warnings-as-errors"]));
1301
+ const parsed = parseCommandOptions("dag.advance", args);
1251
1302
  if (parsed.positionals.length !== 1) {
1252
1303
  throw new UsageError("dag advance requires exactly one <file>");
1253
1304
  }
@@ -1288,6 +1339,7 @@ async function runAdvance(args) {
1288
1339
  if (format === "json") {
1289
1340
  writeJson({
1290
1341
  schema_version: "Perttool.MutationResult.v1",
1342
+ cli_contract_version: 4,
1291
1343
  tool_version: TOOL_VERSION,
1292
1344
  operation: "dag.advance",
1293
1345
  ok,
@@ -1565,17 +1617,45 @@ function renderAnalysisText(result) {
1565
1617
  if (resource.resources.length === 0)
1566
1618
  lines.push("-");
1567
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
+ }
1568
1655
  return `${lines.join("\n")}\n`;
1569
1656
  }
1570
1657
  async function runAnalyze(args) {
1571
- const parsed = parseOptions(args, new Set([
1572
- "schedule",
1573
- "max-paths",
1574
- "precision",
1575
- "max-diagnostics",
1576
- "format",
1577
- "color",
1578
- ]), new Set(["warnings-as-errors"]), new Set(["capacity"]));
1658
+ const parsed = parseCommandOptions("dag.analyze", args);
1579
1659
  if (parsed.positionals.length !== 1) {
1580
1660
  throw new UsageError("dag analyze requires exactly one <file>");
1581
1661
  }
@@ -1608,7 +1688,8 @@ async function runAnalyze(args) {
1608
1688
  const ok = result.ok && !warningFailure;
1609
1689
  if (format === "json") {
1610
1690
  writeJson({
1611
- schema_version: "Perttool.AnalysisResult.v2",
1691
+ schema_version: "Perttool.AnalysisResult.v3",
1692
+ cli_contract_version: 4,
1612
1693
  tool_version: TOOL_VERSION,
1613
1694
  operation: "dag.analyze",
1614
1695
  ok,
@@ -1617,6 +1698,7 @@ async function runAnalyze(args) {
1617
1698
  source_digest: input.digest,
1618
1699
  diagnostics: result.diagnostics.map(jsonDiagnostic),
1619
1700
  diagnostics_truncated: result.diagnosticsTruncated,
1701
+ grammar_version: result.grammarVersion,
1620
1702
  mode: result.mode,
1621
1703
  precision: result.precision,
1622
1704
  ...(result.durationUnit === null
@@ -1633,6 +1715,7 @@ async function runAnalyze(args) {
1633
1715
  resource: result.resource === null || result.durationUnit === null
1634
1716
  ? null
1635
1717
  : resourceJson(result.resource, result.durationUnit, result.precision),
1718
+ temporal: snakeJson(result.temporal),
1636
1719
  });
1637
1720
  }
1638
1721
  else {
@@ -1668,15 +1751,7 @@ function renderConversionLoss(loss, source, color) {
1668
1751
  }, source, color);
1669
1752
  }
1670
1753
  async function runRender(args) {
1671
- const parsed = parseOptions(args, new Set([
1672
- "to",
1673
- "profile",
1674
- "analysis",
1675
- "out",
1676
- "max-diagnostics",
1677
- "format",
1678
- "color",
1679
- ]), new Set(["strict-loss", "warnings-as-errors"]), new Set(["capacity"]));
1754
+ const parsed = parseCommandOptions("dag.render", args);
1680
1755
  if (parsed.positionals.length !== 1) {
1681
1756
  throw new UsageError("dag render requires exactly one <file>");
1682
1757
  }
@@ -1728,6 +1803,7 @@ async function runRender(args) {
1728
1803
  if (format === "json") {
1729
1804
  writeJson({
1730
1805
  schema_version: "Perttool.ExportResult.v1",
1806
+ cli_contract_version: 4,
1731
1807
  tool_version: TOOL_VERSION,
1732
1808
  operation: "dag.render",
1733
1809
  ok,
@@ -1778,7 +1854,7 @@ async function runRender(args) {
1778
1854
  return strictFailure ? 4 : ok ? 0 : 1;
1779
1855
  }
1780
1856
  async function runImport(args) {
1781
- const parsed = parseOptions(args, new Set(["from", "out", "max-diagnostics", "format", "color"]), new Set(["strict-loss", "warnings-as-errors"]));
1857
+ const parsed = parseCommandOptions("dag.import", args);
1782
1858
  if (parsed.positionals.length !== 1) {
1783
1859
  throw new UsageError("dag import requires exactly one <file>");
1784
1860
  }
@@ -1819,6 +1895,7 @@ async function runImport(args) {
1819
1895
  if (format === "json") {
1820
1896
  writeJson({
1821
1897
  schema_version: "Perttool.ImportResult.v1",
1898
+ cli_contract_version: 4,
1822
1899
  tool_version: TOOL_VERSION,
1823
1900
  operation: "dag.import",
1824
1901
  ok,
@@ -2035,7 +2112,15 @@ function renderExplanation(node, indent) {
2035
2112
  function renderNextText(result) {
2036
2113
  const unit = result.durationUnit;
2037
2114
  const taskById = new Map(result.tasks.map((task) => [task.id, task]));
2115
+ const authority = result.temporal.authority;
2038
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
+ "",
2039
2124
  `PERTTOOL NEXT ${result.documentId ?? "-"}`,
2040
2125
  `VELOCITY ${result.velocity === null ? "-" : velocityText(result.velocity, result.precision)}`,
2041
2126
  `VELOCITY_FORECAST_UNIT ${result.velocityForecast?.targetUnit ?? "-"}`,
@@ -2074,10 +2159,19 @@ function renderNextText(result) {
2074
2159
  section("READY / WAITING RESOURCE", result.groups.ready.filter((id) => !result.groups.runnableNow.includes(id)), "rejection");
2075
2160
  section("BLOCKED NOW", result.groups.blockedNow, "blocked");
2076
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("-");
2077
2171
  return `${lines.join("\n")}\n`;
2078
2172
  }
2079
2173
  async function runNext(args) {
2080
- const parsed = parseOptions(args, new Set(["explain-depth", "precision", "max-diagnostics", "format", "color"]), new Set(["warnings-as-errors"]), new Set(["capacity"]));
2174
+ const parsed = parseCommandOptions("dag.next", args);
2081
2175
  if (parsed.positionals.length !== 1) {
2082
2176
  throw new UsageError("dag next requires exactly one <file>");
2083
2177
  }
@@ -2108,6 +2202,7 @@ async function runNext(args) {
2108
2202
  if (format === "json") {
2109
2203
  writeJson({
2110
2204
  schema_version: "Perttool.CliError.v1",
2205
+ cli_contract_version: 4,
2111
2206
  tool_version: TOOL_VERSION,
2112
2207
  operation: "dag.next",
2113
2208
  ok: false,
@@ -2127,7 +2222,8 @@ async function runNext(args) {
2127
2222
  const ok = result.ok && !warningFailure;
2128
2223
  if (format === "json") {
2129
2224
  writeJson({
2130
- schema_version: "Perttool.NextResult.v3",
2225
+ schema_version: "Perttool.NextResult.v4",
2226
+ cli_contract_version: 4,
2131
2227
  recommendation_interface_version: 1,
2132
2228
  tool_version: TOOL_VERSION,
2133
2229
  operation: "dag.next",
@@ -2137,7 +2233,9 @@ async function runNext(args) {
2137
2233
  source_digest: input.digest,
2138
2234
  diagnostics: result.diagnostics.map(jsonDiagnostic),
2139
2235
  diagnostics_truncated: result.diagnosticsTruncated,
2236
+ grammar_version: result.grammarVersion,
2140
2237
  ...(result.durationUnit === null ? {} : nextJson(result)),
2238
+ temporal: snakeJson(result.temporal),
2141
2239
  });
2142
2240
  }
2143
2241
  else {
@@ -2152,65 +2250,56 @@ async function runNext(args) {
2152
2250
  }
2153
2251
  return ok ? 0 : 1;
2154
2252
  }
2155
- function renderHelpText(result) {
2156
- const lines = [result.title, "", result.summary];
2157
- if (result.topics.length > 0) {
2158
- lines.push("", "Topics:");
2159
- for (const topic of result.topics)
2160
- lines.push(` ${topic.id.padEnd(12)} ${topic.summary}`);
2161
- }
2162
- for (const section of result.sections) {
2163
- lines.push("", section.title, section.body);
2164
- }
2165
- if (result.syntax.length > 0)
2166
- lines.push("", "Syntax:", ...result.syntax.map((line) => ` ${line}`));
2167
- if (result.examples.length > 0) {
2168
- lines.push("", "Examples:", ...result.examples.map((example) => ` ${example.id}: ${example.text}`));
2169
- }
2170
- if (result.related.length > 0)
2171
- lines.push("", `Related: ${result.related.join(", ")}`);
2172
- return `${lines.join("\n")}\n`;
2173
- }
2174
- function runHelp(args) {
2175
- const parsed = parseOptions(args, new Set(["level", "format", "color"]), new Set());
2253
+ function runGuide(args) {
2254
+ const parsed = parseCommandOptions("guide", args);
2176
2255
  if (parsed.positionals.length > 2) {
2177
- throw new UsageError("dsl help accepts at most <topic> <subtopic>");
2256
+ throw new UsageError("guide accepts at most <topic> <subtopic>");
2178
2257
  }
2179
2258
  const format = outputFormat(parsed.values.get("format"));
2180
2259
  colorMode(parsed.values.get("color"), format);
2181
2260
  const topicId = parsed.positionals.length === 0 ? null : parsed.positionals.join(".");
2182
2261
  const level = helpLevel(parsed.values.get("level"), topicId !== null);
2183
- const result = getHelp(topicId, level);
2262
+ const result = getGuide(topicId, level);
2184
2263
  if (format === "json") {
2185
- writeJson({
2186
- schema_version: "Perttool.HelpResult.v1",
2187
- tool_version: TOOL_VERSION,
2188
- operation: "dsl.help",
2189
- ok: result.ok,
2190
- diagnostics: result.diagnostics.map(jsonDiagnostic),
2191
- topic_id: result.topicId,
2192
- level: result.level,
2193
- title: result.title,
2194
- summary: result.summary,
2195
- sections: result.sections,
2196
- syntax: result.syntax,
2197
- examples: result.examples,
2198
- related: result.related,
2199
- topics: result.topics,
2200
- });
2264
+ process.stdout.write(serializeGuideResult(result));
2265
+ }
2266
+ else {
2267
+ const rendered = renderGuideResult(result);
2268
+ if (result.ok) {
2269
+ process.stdout.write(rendered);
2270
+ }
2271
+ else {
2272
+ process.stderr.write(rendered);
2273
+ }
2201
2274
  }
2202
- else if (result.ok) {
2203
- process.stdout.write(renderHelpText(result));
2275
+ return result.ok ? 0 : 1;
2276
+ }
2277
+ function runCommandHelp(args) {
2278
+ const parsed = parseCommandOptions("help", args);
2279
+ if (parsed.positionals.length > 2) {
2280
+ throw new UsageError("help accepts at most <resource> <action>");
2281
+ }
2282
+ const format = outputFormat(parsed.values.get("format"));
2283
+ const result = getCommandDiscovery({
2284
+ resource: parsed.positionals[0] ?? null,
2285
+ action: parsed.positionals[1] ?? null,
2286
+ });
2287
+ if (format === "json") {
2288
+ process.stdout.write(serializeCommandHelpResult(result));
2204
2289
  }
2205
2290
  else {
2206
- for (const diagnostic of result.diagnostics) {
2207
- process.stderr.write(`${renderDiagnostic(diagnostic, "<help>", "never")}\n`);
2291
+ const rendered = renderCommandHelpResult(result);
2292
+ if (result.ok) {
2293
+ process.stdout.write(rendered);
2294
+ }
2295
+ else {
2296
+ process.stderr.write(rendered);
2208
2297
  }
2209
2298
  }
2210
2299
  return result.ok ? 0 : 1;
2211
2300
  }
2212
2301
  function runAgentHelp(args) {
2213
- const parsed = parseOptions(args, new Set(["level", "format", "color"]), new Set());
2302
+ const parsed = parseCommandOptions("agent.help", args);
2214
2303
  if (parsed.positionals.length > 2) {
2215
2304
  throw new UsageError("agent help accepts at most <provider> <surface>");
2216
2305
  }
@@ -2225,7 +2314,13 @@ function runAgentHelp(args) {
2225
2314
  level,
2226
2315
  });
2227
2316
  if (format === "json") {
2228
- process.stdout.write(serializeAgentGuidanceResult(result));
2317
+ const projected = agentGuidanceResultToJson(result);
2318
+ const { schema_version: schemaVersion, ...payload } = projected;
2319
+ writeJson({
2320
+ schema_version: schemaVersion,
2321
+ cli_contract_version: 4,
2322
+ ...payload,
2323
+ });
2229
2324
  }
2230
2325
  else {
2231
2326
  if (result.ok) {
@@ -2245,74 +2340,90 @@ function runAgentHelp(args) {
2245
2340
  }
2246
2341
  return agentGuidanceExitCode(result.diagnostics);
2247
2342
  }
2343
+ async function dispatchCommand(descriptor, args) {
2344
+ switch (descriptor.operation) {
2345
+ case "help":
2346
+ return runCommandHelp(args);
2347
+ case "guide":
2348
+ return runGuide(args);
2349
+ case "document.check":
2350
+ return runCheck(args);
2351
+ case "document.format":
2352
+ return runFormat(args);
2353
+ case "agent.help":
2354
+ return runAgentHelp(args);
2355
+ case "project.init":
2356
+ return runProjectInit(args);
2357
+ case "project.show":
2358
+ return runProjectShow(args);
2359
+ case "project.migrate-unit":
2360
+ return runUnitMigration(args);
2361
+ case "dag.analyze":
2362
+ return runAnalyze(args);
2363
+ case "dag.next":
2364
+ return runNext(args);
2365
+ case "dag.advance":
2366
+ return runAdvance(args);
2367
+ case "dag.render":
2368
+ return runRender(args);
2369
+ case "dag.import":
2370
+ return runImport(args);
2371
+ }
2372
+ if (descriptor.path.length === 2
2373
+ && (descriptor.operation === "project.set"
2374
+ || descriptor.operation === "batch.apply"
2375
+ || /^(?:task|gate|milestone|resource)\.(?:add|set|remove|finish)$/.test(descriptor.operation))) {
2376
+ const [resource, action] = descriptor.path;
2377
+ return runMutation(resource, action, args);
2378
+ }
2379
+ throw new Error(`no Contract 4 handler for ${descriptor.operation}`);
2380
+ }
2381
+ function emitCommandUsage(error, json) {
2382
+ if (json) {
2383
+ process.stdout.write(serializeCommandUsageError(error));
2384
+ }
2385
+ else {
2386
+ process.stderr.write(renderCommandUsageError(error));
2387
+ }
2388
+ return 2;
2389
+ }
2248
2390
  async function main(argv) {
2249
2391
  if (argv.length === 1 && argv[0] === "--version") {
2250
2392
  process.stdout.write(`perttool ${TOOL_VERSION}\n`);
2251
2393
  return 0;
2252
2394
  }
2253
2395
  if (argv.length === 1 && argv[0] === "--help") {
2254
- process.stdout.write(`${topLevelHelp()}\n`);
2255
- return 0;
2256
- }
2257
- if (argv.length < 2)
2258
- throw new UsageError("expected <resource> <action>");
2259
- const resource = argv[0];
2260
- const action = argv[1];
2261
- const entityActions = new Map([
2262
- ["project", new Set(["set"])],
2263
- ["task", new Set(["add", "set", "remove", "finish"])],
2264
- ["milestone", new Set(["add", "set", "remove"])],
2265
- ["resource", new Set(["add", "set", "remove"])],
2266
- ["mutation", new Set(["apply"])],
2267
- ]);
2268
- const isMutationCommand = entityActions.get(resource)?.has(action) === true;
2269
- if (argv.length === 3 &&
2270
- argv[2] === "--help" &&
2271
- ((resource === "agent" && action === "help") ||
2272
- (resource === "dsl" && ["check", "format", "help"].includes(action)) ||
2273
- (resource === "project" && action === "show") ||
2274
- (resource === "dag" && ["analyze", "next", "advance", "render", "import"].includes(action)) ||
2275
- isMutationCommand)) {
2276
- process.stdout.write(`${commandHelp(resource, action)}\n`);
2277
- return 0;
2278
- }
2279
- if (resource === "dag" && action === "analyze") {
2280
- return runAnalyze(argv.slice(2));
2281
- }
2282
- if (resource === "dag" && action === "next") {
2283
- return runNext(argv.slice(2));
2284
- }
2285
- if (resource === "dag" && action === "advance") {
2286
- return runAdvance(argv.slice(2));
2287
- }
2288
- if (resource === "dag" && action === "render") {
2289
- return runRender(argv.slice(2));
2290
- }
2291
- if (resource === "dag" && action === "import") {
2292
- return runImport(argv.slice(2));
2293
- }
2294
- if (resource === "agent" && action === "help") {
2295
- return runAgentHelp(argv.slice(2));
2396
+ return runCommandHelp([]);
2296
2397
  }
2297
- if (resource === "project" && action === "show") {
2298
- return runProjectShow(argv.slice(2));
2398
+ const validation = validateCommandInvocation(argv);
2399
+ if (!validation.ok) {
2400
+ if (jsonRequested(argv)) {
2401
+ process.stdout.write(serializeCommandUsageError(validation.error));
2402
+ }
2403
+ else {
2404
+ process.stderr.write(renderCommandUsageError(validation.error));
2405
+ }
2406
+ return 2;
2299
2407
  }
2300
- if (resource === "dsl" && action === "format") {
2301
- return runFormat(argv.slice(2));
2408
+ const descriptor = validation.descriptor;
2409
+ if (validation.helpAlias) {
2410
+ return runCommandHelp([...descriptor.path]);
2302
2411
  }
2303
- if (isMutationCommand) {
2304
- return runMutation(resource, action, argv.slice(2));
2412
+ try {
2413
+ return await dispatchCommand(descriptor, argv.slice(descriptor.path.length));
2305
2414
  }
2306
- if (resource !== "dsl" || (action !== "check" && action !== "help")) {
2307
- throw new UsageError(`unknown or not-yet-implemented command: ${resource} ${action}`);
2415
+ catch (error) {
2416
+ if (error instanceof UsageError) {
2417
+ return emitCommandUsage(handlerCommandUsageError(descriptor, error.message), jsonRequested(argv));
2418
+ }
2419
+ throw error;
2308
2420
  }
2309
- return action === "check" ? runCheck(argv.slice(2)) : runHelp(argv.slice(2));
2310
2421
  }
2311
2422
  const args = process.argv.slice(2);
2312
2423
  try {
2313
2424
  process.exitCode = await main(args);
2314
2425
  }
2315
2426
  catch (error) {
2316
- process.exitCode = cliError(error instanceof Error ? error : new Error(String(error)), error instanceof UsageError ? 2 : 70, args.length >= 2 ? `${args[0]}.${args[1]}` : null, jsonRequested(args));
2427
+ process.exitCode = cliError(error instanceof Error ? error : new Error(String(error)), 70, null, jsonRequested(args));
2317
2428
  }
2318
2429
  //# sourceMappingURL=cli.js.map