perttool 0.1.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +53 -0
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/analysis/graph.d.ts +41 -0
- package/dist/analysis/graph.d.ts.map +1 -0
- package/dist/analysis/graph.js +199 -0
- package/dist/analysis/graph.js.map +1 -0
- package/dist/analysis/precedence.d.ts +52 -0
- package/dist/analysis/precedence.d.ts.map +1 -0
- package/dist/analysis/precedence.js +187 -0
- package/dist/analysis/precedence.js.map +1 -0
- package/dist/analysis/resource.d.ts +96 -0
- package/dist/analysis/resource.d.ts.map +1 -0
- package/dist/analysis/resource.js +522 -0
- package/dist/analysis/resource.js.map +1 -0
- package/dist/application/analyze.d.ts +32 -0
- package/dist/application/analyze.d.ts.map +1 -0
- package/dist/application/analyze.js +144 -0
- package/dist/application/analyze.js.map +1 -0
- package/dist/application/check.d.ts +24 -0
- package/dist/application/check.d.ts.map +1 -0
- package/dist/application/check.js +43 -0
- package/dist/application/check.js.map +1 -0
- package/dist/application/format.d.ts +21 -0
- package/dist/application/format.d.ts.map +1 -0
- package/dist/application/format.js +52 -0
- package/dist/application/format.js.map +1 -0
- package/dist/application/mutate.d.ts +4 -0
- package/dist/application/mutate.d.ts.map +1 -0
- package/dist/application/mutate.js +175 -0
- package/dist/application/mutate.js.map +1 -0
- package/dist/application/next.d.ts +86 -0
- package/dist/application/next.d.ts.map +1 -0
- package/dist/application/next.js +310 -0
- package/dist/application/next.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +2103 -0
- package/dist/cli.js.map +1 -0
- package/dist/conversion/mermaid-import.d.ts +26 -0
- package/dist/conversion/mermaid-import.d.ts.map +1 -0
- package/dist/conversion/mermaid-import.js +655 -0
- package/dist/conversion/mermaid-import.js.map +1 -0
- package/dist/conversion/mermaid.d.ts +38 -0
- package/dist/conversion/mermaid.d.ts.map +1 -0
- package/dist/conversion/mermaid.js +362 -0
- package/dist/conversion/mermaid.js.map +1 -0
- package/dist/editing/unified-diff.d.ts +7 -0
- package/dist/editing/unified-diff.d.ts.map +1 -0
- package/dist/editing/unified-diff.js +81 -0
- package/dist/editing/unified-diff.js.map +1 -0
- package/dist/formatter/source-formatter.d.ts +17 -0
- package/dist/formatter/source-formatter.d.ts.map +1 -0
- package/dist/formatter/source-formatter.js +210 -0
- package/dist/formatter/source-formatter.js.map +1 -0
- package/dist/help/registry.d.ts +37 -0
- package/dist/help/registry.d.ts.map +1 -0
- package/dist/help/registry.js +657 -0
- package/dist/help/registry.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/io/document-file.d.ts +9 -0
- package/dist/io/document-file.d.ts.map +1 -0
- package/dist/io/document-file.js +19 -0
- package/dist/io/document-file.js.map +1 -0
- package/dist/io/safe-write.d.ts +33 -0
- package/dist/io/safe-write.d.ts.map +1 -0
- package/dist/io/safe-write.js +280 -0
- package/dist/io/safe-write.js.map +1 -0
- package/dist/model/diagnostics.d.ts +43 -0
- package/dist/model/diagnostics.d.ts.map +1 -0
- package/dist/model/diagnostics.js +47 -0
- package/dist/model/diagnostics.js.map +1 -0
- package/dist/model/rational.d.ts +21 -0
- package/dist/model/rational.d.ts.map +1 -0
- package/dist/model/rational.js +84 -0
- package/dist/model/rational.js.map +1 -0
- package/dist/model/syntax.d.ts +67 -0
- package/dist/model/syntax.d.ts.map +1 -0
- package/dist/model/syntax.js +15 -0
- package/dist/model/syntax.js.map +1 -0
- package/dist/model/units.d.ts +18 -0
- package/dist/model/units.d.ts.map +1 -0
- package/dist/model/units.js +29 -0
- package/dist/model/units.js.map +1 -0
- package/dist/mutation/advance.d.ts +26 -0
- package/dist/mutation/advance.d.ts.map +1 -0
- package/dist/mutation/advance.js +261 -0
- package/dist/mutation/advance.js.map +1 -0
- package/dist/mutation/diagnostics.d.ts +10 -0
- package/dist/mutation/diagnostics.d.ts.map +1 -0
- package/dist/mutation/diagnostics.js +10 -0
- package/dist/mutation/diagnostics.js.map +1 -0
- package/dist/mutation/entity-editor.d.ts +16 -0
- package/dist/mutation/entity-editor.d.ts.map +1 -0
- package/dist/mutation/entity-editor.js +103 -0
- package/dist/mutation/entity-editor.js.map +1 -0
- package/dist/mutation/milestone.d.ts +5 -0
- package/dist/mutation/milestone.d.ts.map +1 -0
- package/dist/mutation/milestone.js +194 -0
- package/dist/mutation/milestone.js.map +1 -0
- package/dist/mutation/resource.d.ts +5 -0
- package/dist/mutation/resource.d.ts.map +1 -0
- package/dist/mutation/resource.js +145 -0
- package/dist/mutation/resource.js.map +1 -0
- package/dist/mutation/source.d.ts +22 -0
- package/dist/mutation/source.d.ts.map +1 -0
- package/dist/mutation/source.js +155 -0
- package/dist/mutation/source.js.map +1 -0
- package/dist/mutation/task.d.ts +10 -0
- package/dist/mutation/task.d.ts.map +1 -0
- package/dist/mutation/task.js +644 -0
- package/dist/mutation/task.js.map +1 -0
- package/dist/mutation/text-edits.d.ts +8 -0
- package/dist/mutation/text-edits.d.ts.map +1 -0
- package/dist/mutation/text-edits.js +33 -0
- package/dist/mutation/text-edits.js.map +1 -0
- package/dist/mutation/types.d.ts +153 -0
- package/dist/mutation/types.d.ts.map +1 -0
- package/dist/mutation/types.js +2 -0
- package/dist/mutation/types.js.map +1 -0
- package/dist/parser/document-parser.d.ts +6 -0
- package/dist/parser/document-parser.d.ts.map +1 -0
- package/dist/parser/document-parser.js +735 -0
- package/dist/parser/document-parser.js.map +1 -0
- package/dist/recommendation/explanation-types.d.ts +226 -0
- package/dist/recommendation/explanation-types.d.ts.map +1 -0
- package/dist/recommendation/explanation-types.js +2 -0
- package/dist/recommendation/explanation-types.js.map +1 -0
- package/dist/recommendation/explanation-validation.d.ts +4 -0
- package/dist/recommendation/explanation-validation.d.ts.map +1 -0
- package/dist/recommendation/explanation-validation.js +745 -0
- package/dist/recommendation/explanation-validation.js.map +1 -0
- package/dist/recommendation/explanation-values.d.ts +7 -0
- package/dist/recommendation/explanation-values.d.ts.map +1 -0
- package/dist/recommendation/explanation-values.js +283 -0
- package/dist/recommendation/explanation-values.js.map +1 -0
- package/dist/recommendation/explanation.d.ts +8 -0
- package/dist/recommendation/explanation.d.ts.map +1 -0
- package/dist/recommendation/explanation.js +1022 -0
- package/dist/recommendation/explanation.js.map +1 -0
- package/dist/recommendation/failure.d.ts +3 -0
- package/dist/recommendation/failure.d.ts.map +1 -0
- package/dist/recommendation/failure.js +4 -0
- package/dist/recommendation/failure.js.map +1 -0
- package/dist/recommendation/json.d.ts +3 -0
- package/dist/recommendation/json.d.ts.map +1 -0
- package/dist/recommendation/json.js +252 -0
- package/dist/recommendation/json.js.map +1 -0
- package/dist/recommendation/override-types.d.ts +117 -0
- package/dist/recommendation/override-types.d.ts.map +1 -0
- package/dist/recommendation/override-types.js +2 -0
- package/dist/recommendation/override-types.js.map +1 -0
- package/dist/recommendation/override.d.ts +7 -0
- package/dist/recommendation/override.d.ts.map +1 -0
- package/dist/recommendation/override.js +717 -0
- package/dist/recommendation/override.js.map +1 -0
- package/dist/recommendation/ranking.d.ts +7 -0
- package/dist/recommendation/ranking.d.ts.map +1 -0
- package/dist/recommendation/ranking.js +391 -0
- package/dist/recommendation/ranking.js.map +1 -0
- package/dist/recommendation/types.d.ts +83 -0
- package/dist/recommendation/types.d.ts.map +1 -0
- package/dist/recommendation/types.js +2 -0
- package/dist/recommendation/types.js.map +1 -0
- package/dist/semantic/validator.d.ts +4 -0
- package/dist/semantic/validator.d.ts.map +1 -0
- package/dist/semantic/validator.js +490 -0
- package/dist/semantic/validator.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/package.json +61 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { TextDecoder } from "node:util";
|
|
5
|
+
import { analyzeDocument } from "./application/analyze.js";
|
|
6
|
+
import { checkDocument } from "./application/check.js";
|
|
7
|
+
import { planFormat } from "./application/format.js";
|
|
8
|
+
import { planBatchMutation, planMutation } from "./application/mutate.js";
|
|
9
|
+
import { selectNextTasks } from "./application/next.js";
|
|
10
|
+
import { exportMermaid, } from "./conversion/mermaid.js";
|
|
11
|
+
import { importMermaid } from "./conversion/mermaid-import.js";
|
|
12
|
+
import { getHelp } from "./help/registry.js";
|
|
13
|
+
import { documentContentFromBytes, readDocumentFile, } from "./io/document-file.js";
|
|
14
|
+
import { createArtifactFile, createDocumentFile, replaceDocumentFile, SafeWriteConflictError, SafeWriteVerificationError, } from "./io/safe-write.js";
|
|
15
|
+
import { formatDecimal } from "./model/rational.js";
|
|
16
|
+
import { convertWithVelocity, durationSuffix } from "./model/units.js";
|
|
17
|
+
import { recommendationInvariantExitCode } from "./recommendation/failure.js";
|
|
18
|
+
import { recommendationAnalysisToJson } from "./recommendation/json.js";
|
|
19
|
+
import { planAdvance, } from "./mutation/advance.js";
|
|
20
|
+
import { TOOL_VERSION } from "./version.js";
|
|
21
|
+
class UsageError extends Error {
|
|
22
|
+
code = "PTCLI-001";
|
|
23
|
+
}
|
|
24
|
+
function topLevelHelp() {
|
|
25
|
+
return [
|
|
26
|
+
"perttool - document-based PERT/CPM task management",
|
|
27
|
+
"",
|
|
28
|
+
"Usage:",
|
|
29
|
+
" perttool --version",
|
|
30
|
+
" perttool --help",
|
|
31
|
+
" perttool dsl check <file> [--format text|json]",
|
|
32
|
+
" perttool dsl format <file> [--check] [--diff] [--format text|json]",
|
|
33
|
+
" perttool dsl help [topic [subtopic]] [--level index|quick|detail] [--format text|json]",
|
|
34
|
+
" perttool dag analyze <file> [--schedule precedence|resource|both] [--format text|json]",
|
|
35
|
+
" perttool dag next <file> [--capacity <resource-id>=<integer>] [--format text|json]",
|
|
36
|
+
" perttool dag advance <file> [--diff] [--write | --out <path>] [--format text|json]",
|
|
37
|
+
" perttool dag render <file> --to mermaid [--profile perttool|plain] [--format text|json]",
|
|
38
|
+
" perttool dag import <file> --from mermaid [--strict-loss] [--out <path>] [--format text|json]",
|
|
39
|
+
" perttool task add|set|remove|finish ...",
|
|
40
|
+
" perttool milestone add|set|remove ...",
|
|
41
|
+
" perttool resource add|set|remove ...",
|
|
42
|
+
" perttool mutation apply <file> --request <json-file|-> [--diff] [--format text|json]",
|
|
43
|
+
"",
|
|
44
|
+
"Format and mutation commands preview by default; use --write or --out for explicit writes.",
|
|
45
|
+
].join("\n");
|
|
46
|
+
}
|
|
47
|
+
function commandHelp(resource, action) {
|
|
48
|
+
if (resource === "dsl" && action === "check") {
|
|
49
|
+
return [
|
|
50
|
+
"Usage: perttool dsl check <file>",
|
|
51
|
+
" [--warnings-as-errors]",
|
|
52
|
+
" [--max-diagnostics <integer>]",
|
|
53
|
+
" [--format text|json]",
|
|
54
|
+
" [--color auto|always|never]",
|
|
55
|
+
].join("\n");
|
|
56
|
+
}
|
|
57
|
+
if (resource === "dsl" && action === "format")
|
|
58
|
+
return [
|
|
59
|
+
"Usage: perttool dsl format <file>",
|
|
60
|
+
" [--check] [--diff]",
|
|
61
|
+
" [--write [--expect-digest <digest>] | --out <path>]",
|
|
62
|
+
" [--max-diagnostics <integer>] [--warnings-as-errors]",
|
|
63
|
+
" [--format text|json] [--color auto|always|never]",
|
|
64
|
+
].join("\n");
|
|
65
|
+
if (resource === "dsl" && action === "help")
|
|
66
|
+
return [
|
|
67
|
+
"Usage: perttool dsl help [topic [subtopic]]",
|
|
68
|
+
" [--level index|quick|detail]",
|
|
69
|
+
" [--format text|json]",
|
|
70
|
+
" [--color auto|always|never]",
|
|
71
|
+
].join("\n");
|
|
72
|
+
if (resource === "dag" && action === "analyze")
|
|
73
|
+
return [
|
|
74
|
+
"Usage: perttool dag analyze <file>",
|
|
75
|
+
" [--schedule precedence|resource|both]",
|
|
76
|
+
" [--capacity <resource-id>=<integer>]...",
|
|
77
|
+
" [--max-paths <integer>] [--precision <integer>]",
|
|
78
|
+
" [--max-diagnostics <integer>]",
|
|
79
|
+
" [--warnings-as-errors]",
|
|
80
|
+
" [--format text|json] [--color auto|always|never]",
|
|
81
|
+
].join("\n");
|
|
82
|
+
if (resource === "dag" && action === "next")
|
|
83
|
+
return [
|
|
84
|
+
"Usage: perttool dag next <file>",
|
|
85
|
+
" [--capacity <resource-id>=<integer>]...",
|
|
86
|
+
" [--explain-depth <integer>] [--precision <integer>]",
|
|
87
|
+
" [--max-diagnostics <integer>]",
|
|
88
|
+
" [--warnings-as-errors]",
|
|
89
|
+
" [--format text|json] [--color auto|always|never]",
|
|
90
|
+
"Output: Perttool.NextResult.v3 with a complete recommendation graph in JSON.",
|
|
91
|
+
"Consumers must inspect schema_version before using recommendation authority.",
|
|
92
|
+
].join("\n");
|
|
93
|
+
if (resource === "dag" && action === "advance")
|
|
94
|
+
return [
|
|
95
|
+
"Usage: perttool dag advance <file>",
|
|
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 === "render")
|
|
101
|
+
return [
|
|
102
|
+
"Usage: perttool dag render <file> --to mermaid",
|
|
103
|
+
" [--profile perttool|plain] [--analysis none|precedence|resource|both]",
|
|
104
|
+
" [--capacity <resource-id>=<integer>]... [--strict-loss] [--out <path>]",
|
|
105
|
+
" [--max-diagnostics <integer>] [--warnings-as-errors]",
|
|
106
|
+
" [--format text|json] [--color auto|always|never]",
|
|
107
|
+
].join("\n");
|
|
108
|
+
if (resource === "dag" && action === "import")
|
|
109
|
+
return [
|
|
110
|
+
"Usage: perttool dag import <file> --from mermaid",
|
|
111
|
+
" [--strict-loss] [--out <path>]",
|
|
112
|
+
" [--max-diagnostics <integer>] [--warnings-as-errors]",
|
|
113
|
+
" [--format text|json] [--color auto|always|never]",
|
|
114
|
+
].join("\n");
|
|
115
|
+
const preview = " [--diff] [--write [--expect-digest <digest>] | --out <path>] [--max-diagnostics <integer>] [--warnings-as-errors] [--format text|json] [--color auto|always|never]";
|
|
116
|
+
if (resource === "task" && action === "add")
|
|
117
|
+
return [
|
|
118
|
+
"Usage: perttool task add <file> <id> <from> <to>",
|
|
119
|
+
" --title <text> (--duration <duration> | --optimistic <duration> --most-likely <duration> --pessimistic <duration>)",
|
|
120
|
+
" [--description <text>] [--status planned|active|blocked|done] [--priority <integer>]",
|
|
121
|
+
" [--owner <text>] [--blocked-reason <text>] [--source <text>] [--tag <tag>]... [--require <resource-id>=<integer>]...",
|
|
122
|
+
preview,
|
|
123
|
+
].join("\n");
|
|
124
|
+
if (resource === "task" && action === "set")
|
|
125
|
+
return [
|
|
126
|
+
"Usage: perttool task set <file> <id> [field options]",
|
|
127
|
+
" [--from <id>] [--to <id>] [--title <text>] [--description <text>] [--duration <duration>]",
|
|
128
|
+
" [--optimistic <duration> --most-likely <duration> --pessimistic <duration>]",
|
|
129
|
+
" [--status planned|active|blocked|done] [--priority <integer>] [--owner <text>]",
|
|
130
|
+
" [--blocked-reason <text>] [--source <text>] [--require <resource-id>=<integer>]...",
|
|
131
|
+
" [--add-tag <tag>]... [--remove-tag <tag>]... [--remove-require <resource-id>]... [--clear <field>]...",
|
|
132
|
+
preview,
|
|
133
|
+
].join("\n");
|
|
134
|
+
if (resource === "task")
|
|
135
|
+
return [
|
|
136
|
+
`Usage: perttool task ${action} <file> <id>`,
|
|
137
|
+
preview,
|
|
138
|
+
].join("\n");
|
|
139
|
+
if (resource === "milestone" && action === "add")
|
|
140
|
+
return [
|
|
141
|
+
"Usage: perttool milestone add <file> <id> --title <text>",
|
|
142
|
+
" [--description <text>] [--state planned|reached] [--tag <tag>]...",
|
|
143
|
+
preview,
|
|
144
|
+
].join("\n");
|
|
145
|
+
if (resource === "milestone" && action === "set")
|
|
146
|
+
return [
|
|
147
|
+
"Usage: perttool milestone set <file> <id> [--title <text>] [--description <text>] [--state planned|reached]",
|
|
148
|
+
" [--add-tag <tag>]... [--remove-tag <tag>]... [--clear description|state|tags]...",
|
|
149
|
+
preview,
|
|
150
|
+
].join("\n");
|
|
151
|
+
if (resource === "milestone")
|
|
152
|
+
return [
|
|
153
|
+
`Usage: perttool milestone ${action} <file> <id>`,
|
|
154
|
+
preview,
|
|
155
|
+
].join("\n");
|
|
156
|
+
if (resource === "resource" && action === "add")
|
|
157
|
+
return [
|
|
158
|
+
"Usage: perttool resource add <file> <id> --title <text> --capacity <integer>",
|
|
159
|
+
" [--description <text>]",
|
|
160
|
+
preview,
|
|
161
|
+
].join("\n");
|
|
162
|
+
if (resource === "resource" && action === "set")
|
|
163
|
+
return [
|
|
164
|
+
"Usage: perttool resource set <file> <id> [--title <text>] [--description <text>] [--capacity <integer>] [--clear description]",
|
|
165
|
+
preview,
|
|
166
|
+
].join("\n");
|
|
167
|
+
if (resource === "resource")
|
|
168
|
+
return [
|
|
169
|
+
`Usage: perttool resource ${action} <file> <id>`,
|
|
170
|
+
preview,
|
|
171
|
+
].join("\n");
|
|
172
|
+
return [
|
|
173
|
+
"Usage: perttool mutation apply <file> --request <json-file|->",
|
|
174
|
+
preview,
|
|
175
|
+
].join("\n");
|
|
176
|
+
}
|
|
177
|
+
function parseOptions(args, valueOptions, flagOptions, repeatableOptions = new Set()) {
|
|
178
|
+
const positionals = [];
|
|
179
|
+
const values = new Map();
|
|
180
|
+
const repeatedValues = new Map();
|
|
181
|
+
const flags = new Set();
|
|
182
|
+
let optionsEnded = false;
|
|
183
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
184
|
+
const argument = args[index];
|
|
185
|
+
if (!optionsEnded && argument === "--") {
|
|
186
|
+
optionsEnded = true;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (optionsEnded || !argument.startsWith("--")) {
|
|
190
|
+
positionals.push(argument);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const equals = argument.indexOf("=");
|
|
194
|
+
const name = equals === -1 ? argument.slice(2) : argument.slice(2, equals);
|
|
195
|
+
if (flagOptions.has(name)) {
|
|
196
|
+
if (equals !== -1)
|
|
197
|
+
throw new UsageError(`boolean option --${name} does not take a value`);
|
|
198
|
+
if (flags.has(name))
|
|
199
|
+
throw new UsageError(`duplicate option --${name}`);
|
|
200
|
+
flags.add(name);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (!valueOptions.has(name) && !repeatableOptions.has(name)) {
|
|
204
|
+
throw new UsageError(`unknown option --${name}`);
|
|
205
|
+
}
|
|
206
|
+
const isRepeatable = repeatableOptions.has(name);
|
|
207
|
+
if (values.has(name))
|
|
208
|
+
throw new UsageError(`duplicate option --${name}`);
|
|
209
|
+
const value = equals === -1 ? args[index + 1] : argument.slice(equals + 1);
|
|
210
|
+
if (value === undefined || value.startsWith("--")) {
|
|
211
|
+
throw new UsageError(`option --${name} requires a value`);
|
|
212
|
+
}
|
|
213
|
+
if (equals === -1)
|
|
214
|
+
index += 1;
|
|
215
|
+
if (isRepeatable) {
|
|
216
|
+
const entries = repeatedValues.get(name) ?? [];
|
|
217
|
+
entries.push(value);
|
|
218
|
+
repeatedValues.set(name, entries);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
values.set(name, value);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return { positionals, values, repeatedValues, flags };
|
|
225
|
+
}
|
|
226
|
+
function outputFormat(value) {
|
|
227
|
+
if (value === undefined || value === "text")
|
|
228
|
+
return "text";
|
|
229
|
+
if (value === "json")
|
|
230
|
+
return "json";
|
|
231
|
+
throw new UsageError("--format must be text or json");
|
|
232
|
+
}
|
|
233
|
+
function colorMode(value, format) {
|
|
234
|
+
const color = value ?? "auto";
|
|
235
|
+
if (color !== "auto" && color !== "always" && color !== "never") {
|
|
236
|
+
throw new UsageError("--color must be auto, always, or never");
|
|
237
|
+
}
|
|
238
|
+
if (format === "json" && color === "always") {
|
|
239
|
+
throw new UsageError("--color always cannot be used with --format json");
|
|
240
|
+
}
|
|
241
|
+
return color;
|
|
242
|
+
}
|
|
243
|
+
function helpLevel(value, hasTopic) {
|
|
244
|
+
if (value === undefined)
|
|
245
|
+
return hasTopic ? "quick" : "index";
|
|
246
|
+
if (value === "index" || value === "quick" || value === "detail")
|
|
247
|
+
return value;
|
|
248
|
+
throw new UsageError("--level must be index, quick, or detail");
|
|
249
|
+
}
|
|
250
|
+
function jsonRequested(args) {
|
|
251
|
+
return args.some((argument, index) => argument === "--format=json" ||
|
|
252
|
+
(argument === "--format" && args[index + 1] === "json"));
|
|
253
|
+
}
|
|
254
|
+
function jsonPosition(position) {
|
|
255
|
+
return {
|
|
256
|
+
offset: position.offset,
|
|
257
|
+
line: position.line + 1,
|
|
258
|
+
column: position.column + 1,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function jsonSpan(span) {
|
|
262
|
+
return { start: jsonPosition(span.start), end: jsonPosition(span.end) };
|
|
263
|
+
}
|
|
264
|
+
function jsonDiagnostic(diagnostic) {
|
|
265
|
+
return {
|
|
266
|
+
code: diagnostic.code,
|
|
267
|
+
severity: diagnostic.severity,
|
|
268
|
+
message: diagnostic.message,
|
|
269
|
+
entity_id: diagnostic.entityId ?? null,
|
|
270
|
+
span: diagnostic.span === undefined ? null : jsonSpan(diagnostic.span),
|
|
271
|
+
related: (diagnostic.related ?? []).map((related) => ({
|
|
272
|
+
message: related.message,
|
|
273
|
+
span: jsonSpan(related.span),
|
|
274
|
+
})),
|
|
275
|
+
help_topic: diagnostic.helpTopic ?? null,
|
|
276
|
+
expected_syntax: diagnostic.expectedSyntax ?? null,
|
|
277
|
+
fixes: [],
|
|
278
|
+
data: diagnostic.data ?? {},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function colorEnabled(mode) {
|
|
282
|
+
return mode === "always" || (mode === "auto" && Boolean(process.stderr.isTTY));
|
|
283
|
+
}
|
|
284
|
+
function renderDiagnostic(diagnostic, source, mode) {
|
|
285
|
+
const severity = colorEnabled(mode)
|
|
286
|
+
? diagnostic.severity === "error"
|
|
287
|
+
? `\u001b[31m${diagnostic.severity}\u001b[0m`
|
|
288
|
+
: diagnostic.severity === "warning"
|
|
289
|
+
? `\u001b[33m${diagnostic.severity}\u001b[0m`
|
|
290
|
+
: diagnostic.severity
|
|
291
|
+
: diagnostic.severity;
|
|
292
|
+
const lines = [`${diagnostic.code} ${severity}: ${diagnostic.message}`];
|
|
293
|
+
if (diagnostic.span !== undefined) {
|
|
294
|
+
lines.push(` --> ${source}:${diagnostic.span.start.line + 1}:${diagnostic.span.start.column + 1}`);
|
|
295
|
+
}
|
|
296
|
+
for (const related of diagnostic.related ?? []) {
|
|
297
|
+
lines.push(` related: ${source}:${related.span.start.line + 1}:${related.span.start.column + 1} ${related.message}`);
|
|
298
|
+
}
|
|
299
|
+
if (diagnostic.helpTopic !== undefined) {
|
|
300
|
+
const [topic, subtopic] = diagnostic.helpTopic.split(".", 2);
|
|
301
|
+
lines.push(` help: perttool dsl help ${topic}${subtopic === undefined ? "" : ` ${subtopic}`} --level quick`);
|
|
302
|
+
}
|
|
303
|
+
return lines.join("\n");
|
|
304
|
+
}
|
|
305
|
+
async function readDocument(source) {
|
|
306
|
+
const content = source === "-"
|
|
307
|
+
? documentContentFromBytes(await readStdin())
|
|
308
|
+
: await readDocumentFile(source);
|
|
309
|
+
return { text: content.text, digest: content.digest };
|
|
310
|
+
}
|
|
311
|
+
async function readStdin() {
|
|
312
|
+
const chunks = [];
|
|
313
|
+
for await (const chunk of process.stdin) {
|
|
314
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
315
|
+
}
|
|
316
|
+
return Buffer.concat(chunks);
|
|
317
|
+
}
|
|
318
|
+
function writeJson(value) {
|
|
319
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
320
|
+
}
|
|
321
|
+
function cliError(error, exitCode, operation, json) {
|
|
322
|
+
const code = error instanceof UsageError
|
|
323
|
+
? error.code
|
|
324
|
+
: error instanceof SafeWriteConflictError
|
|
325
|
+
? "PTIO-501"
|
|
326
|
+
: error instanceof SafeWriteVerificationError
|
|
327
|
+
? "PTIO-502"
|
|
328
|
+
: exitCode === 3
|
|
329
|
+
? "PTCLI-003"
|
|
330
|
+
: "PTCLI-070";
|
|
331
|
+
const diagnostic = {
|
|
332
|
+
code,
|
|
333
|
+
severity: "error",
|
|
334
|
+
message: error.message,
|
|
335
|
+
helpTopic: "errors",
|
|
336
|
+
...((error instanceof SafeWriteConflictError ||
|
|
337
|
+
error instanceof SafeWriteVerificationError)
|
|
338
|
+
? { data: { reason: error.reason } }
|
|
339
|
+
: {}),
|
|
340
|
+
};
|
|
341
|
+
if (json) {
|
|
342
|
+
writeJson({
|
|
343
|
+
schema_version: "Perttool.CliError.v1",
|
|
344
|
+
tool_version: TOOL_VERSION,
|
|
345
|
+
operation,
|
|
346
|
+
ok: false,
|
|
347
|
+
diagnostics: [jsonDiagnostic(diagnostic)],
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
process.stderr.write(`${renderDiagnostic(diagnostic, "<cli>", "never")}\n`);
|
|
352
|
+
}
|
|
353
|
+
return exitCode;
|
|
354
|
+
}
|
|
355
|
+
async function runCheck(args) {
|
|
356
|
+
const parsed = parseOptions(args, new Set(["format", "color", "max-diagnostics"]), new Set(["warnings-as-errors"]));
|
|
357
|
+
if (parsed.positionals.length !== 1) {
|
|
358
|
+
throw new UsageError("dsl check requires exactly one <file>");
|
|
359
|
+
}
|
|
360
|
+
const format = outputFormat(parsed.values.get("format"));
|
|
361
|
+
const color = colorMode(parsed.values.get("color"), format);
|
|
362
|
+
const sourceOperand = parsed.positionals[0];
|
|
363
|
+
const maxDiagnostics = boundedInteger(parsed.values.get("max-diagnostics"), "max-diagnostics", 100, 1, 1000);
|
|
364
|
+
const source = sourceOperand === "-" ? "<stdin>" : sourceOperand;
|
|
365
|
+
let input;
|
|
366
|
+
try {
|
|
367
|
+
input = await readDocument(sourceOperand);
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
return cliError(error instanceof Error ? error : new Error(String(error)), 3, "dsl.check", format === "json");
|
|
371
|
+
}
|
|
372
|
+
const result = checkDocument(input.text, { maxDiagnostics });
|
|
373
|
+
const warningsAsErrors = parsed.flags.has("warnings-as-errors");
|
|
374
|
+
const warningFailure = warningsAsErrors && result.summary.warnings > 0;
|
|
375
|
+
const ok = result.ok && !warningFailure;
|
|
376
|
+
if (format === "json") {
|
|
377
|
+
writeJson({
|
|
378
|
+
schema_version: "Perttool.CheckResult.v1",
|
|
379
|
+
tool_version: TOOL_VERSION,
|
|
380
|
+
operation: "dsl.check",
|
|
381
|
+
ok,
|
|
382
|
+
document_id: result.documentId,
|
|
383
|
+
source,
|
|
384
|
+
source_digest: input.digest,
|
|
385
|
+
diagnostics: result.diagnostics.map(jsonDiagnostic),
|
|
386
|
+
diagnostics_truncated: result.diagnosticsTruncated,
|
|
387
|
+
grammar_version: result.grammarVersion,
|
|
388
|
+
summary: result.summary,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
if (ok) {
|
|
393
|
+
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`);
|
|
394
|
+
}
|
|
395
|
+
for (const diagnostic of result.diagnostics) {
|
|
396
|
+
process.stderr.write(`${renderDiagnostic(diagnostic, source, color)}\n`);
|
|
397
|
+
}
|
|
398
|
+
if (result.diagnosticsTruncated) {
|
|
399
|
+
process.stderr.write(`DIAGNOSTICS_TRUNCATED true limit=${maxDiagnostics}\n`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return ok ? 0 : 1;
|
|
403
|
+
}
|
|
404
|
+
function boundedInteger(raw, option, defaultValue, minimum, maximum) {
|
|
405
|
+
if (raw === undefined)
|
|
406
|
+
return defaultValue;
|
|
407
|
+
if (!/^[0-9]+$/.test(raw)) {
|
|
408
|
+
throw new UsageError(`--${option} must be an integer from ${minimum} to ${maximum}`);
|
|
409
|
+
}
|
|
410
|
+
const value = Number(raw);
|
|
411
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
412
|
+
throw new UsageError(`--${option} must be an integer from ${minimum} to ${maximum}`);
|
|
413
|
+
}
|
|
414
|
+
return value;
|
|
415
|
+
}
|
|
416
|
+
function analysisMode(raw) {
|
|
417
|
+
if (raw === undefined || raw === "both")
|
|
418
|
+
return "both";
|
|
419
|
+
if (raw === "precedence" || raw === "resource")
|
|
420
|
+
return raw;
|
|
421
|
+
throw new UsageError("--schedule must be precedence, resource, or both");
|
|
422
|
+
}
|
|
423
|
+
function capacityOverrides(values) {
|
|
424
|
+
const overrides = new Map();
|
|
425
|
+
for (const value of values) {
|
|
426
|
+
const match = /^([A-Za-z][A-Za-z0-9_-]*)=([0-9]+)$/.exec(value);
|
|
427
|
+
if (match === null) {
|
|
428
|
+
throw new UsageError("--capacity must be <resource-id>=<integer>");
|
|
429
|
+
}
|
|
430
|
+
const id = match[1];
|
|
431
|
+
const capacity = Number(match[2]);
|
|
432
|
+
if (!Number.isSafeInteger(capacity) || capacity < 1 || capacity > 2_147_483_647) {
|
|
433
|
+
throw new UsageError("--capacity integer must be from 1 to 2147483647");
|
|
434
|
+
}
|
|
435
|
+
if (overrides.has(id))
|
|
436
|
+
throw new UsageError(`duplicate --capacity for ${id}`);
|
|
437
|
+
overrides.set(id, capacity);
|
|
438
|
+
}
|
|
439
|
+
return overrides;
|
|
440
|
+
}
|
|
441
|
+
const mutationCommonValueOptions = [
|
|
442
|
+
"format",
|
|
443
|
+
"color",
|
|
444
|
+
"max-diagnostics",
|
|
445
|
+
"out",
|
|
446
|
+
"expect-digest",
|
|
447
|
+
];
|
|
448
|
+
const mutationCommonFlagOptions = ["diff", "write", "warnings-as-errors"];
|
|
449
|
+
function editingWriteRequest(parsed, sourceOperand, check = false) {
|
|
450
|
+
const write = parsed.flags.has("write");
|
|
451
|
+
const out = parsed.values.get("out");
|
|
452
|
+
const expectedDigest = parsed.values.get("expect-digest");
|
|
453
|
+
if (write && out !== undefined) {
|
|
454
|
+
throw new UsageError("--write and --out are mutually exclusive");
|
|
455
|
+
}
|
|
456
|
+
if (check && (write || out !== undefined)) {
|
|
457
|
+
throw new UsageError("--check cannot be used with --write or --out");
|
|
458
|
+
}
|
|
459
|
+
if (parsed.flags.has("diff") && (write || out !== undefined)) {
|
|
460
|
+
throw new UsageError("--diff is preview-only and cannot be used with --write or --out");
|
|
461
|
+
}
|
|
462
|
+
if (expectedDigest !== undefined && !write) {
|
|
463
|
+
throw new UsageError("--expect-digest can only be used with --write");
|
|
464
|
+
}
|
|
465
|
+
if (expectedDigest !== undefined && !/^sha256:[0-9a-f]{64}$/.test(expectedDigest)) {
|
|
466
|
+
throw new UsageError("--expect-digest must be sha256 followed by 64 lowercase hex digits");
|
|
467
|
+
}
|
|
468
|
+
if (write && sourceOperand === "-") {
|
|
469
|
+
throw new UsageError("--write cannot be used with stdin");
|
|
470
|
+
}
|
|
471
|
+
if (out !== undefined && out.length === 0) {
|
|
472
|
+
throw new UsageError("--out path must not be empty");
|
|
473
|
+
}
|
|
474
|
+
if (write) {
|
|
475
|
+
return {
|
|
476
|
+
mode: "in_place",
|
|
477
|
+
target: sourceOperand,
|
|
478
|
+
...(expectedDigest === undefined ? {} : { expectedDigest }),
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
if (out !== undefined)
|
|
482
|
+
return { mode: "out", target: out };
|
|
483
|
+
return { mode: "preview", target: null };
|
|
484
|
+
}
|
|
485
|
+
function assertExpectedDigest(request, initialDigest) {
|
|
486
|
+
if (request.mode === "in_place" &&
|
|
487
|
+
request.expectedDigest !== undefined &&
|
|
488
|
+
request.expectedDigest !== initialDigest) {
|
|
489
|
+
throw new SafeWriteConflictError("expected_digest_mismatch", "--expect-digestがinitial document digestと一致しません");
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
async function commitCandidate(request, candidateText, initialDigest) {
|
|
493
|
+
if (candidateText === null) {
|
|
494
|
+
throw new SafeWriteVerificationError("invalid_candidate", "successful editing resultにcandidate textがありません");
|
|
495
|
+
}
|
|
496
|
+
return request.mode === "in_place"
|
|
497
|
+
? replaceDocumentFile(request.target, candidateText, {
|
|
498
|
+
initialDigest,
|
|
499
|
+
...(request.expectedDigest === undefined
|
|
500
|
+
? {}
|
|
501
|
+
: { expectedDigest: request.expectedDigest }),
|
|
502
|
+
})
|
|
503
|
+
: createDocumentFile(request.target, candidateText);
|
|
504
|
+
}
|
|
505
|
+
function writeFailureExit(error, operation, json) {
|
|
506
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
507
|
+
const exitCode = normalized instanceof SafeWriteConflictError
|
|
508
|
+
? 5
|
|
509
|
+
: normalized instanceof SafeWriteVerificationError
|
|
510
|
+
? 70
|
|
511
|
+
: 3;
|
|
512
|
+
return cliError(normalized, exitCode, operation, json);
|
|
513
|
+
}
|
|
514
|
+
function renderWriteSummary(operation, result) {
|
|
515
|
+
return `WRITE ${operation} mode=${result.mode} target=${result.target} digest=${result.digest} written=${result.written}\n`;
|
|
516
|
+
}
|
|
517
|
+
async function runFormat(args) {
|
|
518
|
+
const parsed = parseOptions(args, new Set(["format", "color", "max-diagnostics", "out", "expect-digest"]), new Set(["check", "diff", "write", "warnings-as-errors"]));
|
|
519
|
+
if (parsed.positionals.length !== 1) {
|
|
520
|
+
throw new UsageError("dsl format requires exactly one <file>");
|
|
521
|
+
}
|
|
522
|
+
const format = outputFormat(parsed.values.get("format"));
|
|
523
|
+
const color = colorMode(parsed.values.get("color"), format);
|
|
524
|
+
const maxDiagnostics = boundedInteger(parsed.values.get("max-diagnostics"), "max-diagnostics", 100, 1, 1000);
|
|
525
|
+
const sourceOperand = parsed.positionals[0];
|
|
526
|
+
const writeRequest = editingWriteRequest(parsed, sourceOperand, parsed.flags.has("check"));
|
|
527
|
+
const source = sourceOperand === "-" ? "<stdin>" : sourceOperand;
|
|
528
|
+
let input;
|
|
529
|
+
try {
|
|
530
|
+
input = await readDocument(sourceOperand);
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
return cliError(error instanceof Error ? error : new Error(String(error)), 3, "dsl.format", format === "json");
|
|
534
|
+
}
|
|
535
|
+
const result = planFormat(input.text, {
|
|
536
|
+
maxDiagnostics,
|
|
537
|
+
originalLabel: source,
|
|
538
|
+
updatedLabel: "candidate",
|
|
539
|
+
});
|
|
540
|
+
const warningFailure = parsed.flags.has("warnings-as-errors") &&
|
|
541
|
+
(result.diagnosticsTruncated ||
|
|
542
|
+
result.diagnostics.some((diagnostic) => diagnostic.severity === "warning"));
|
|
543
|
+
const checkFailure = parsed.flags.has("check") && result.ok && result.changed;
|
|
544
|
+
const ok = result.ok && !warningFailure && !checkFailure;
|
|
545
|
+
let writeResult = null;
|
|
546
|
+
if (result.ok) {
|
|
547
|
+
try {
|
|
548
|
+
assertExpectedDigest(writeRequest, input.digest);
|
|
549
|
+
if (ok && writeRequest.mode !== "preview") {
|
|
550
|
+
writeResult = await commitCandidate(writeRequest, result.updatedText, input.digest);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
return writeFailureExit(error, "dsl.format", format === "json");
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (format === "json") {
|
|
558
|
+
writeJson({
|
|
559
|
+
schema_version: "Perttool.FormatResult.v1",
|
|
560
|
+
tool_version: TOOL_VERSION,
|
|
561
|
+
operation: "dsl.format",
|
|
562
|
+
ok,
|
|
563
|
+
document_id: result.documentId,
|
|
564
|
+
source,
|
|
565
|
+
source_digest: input.digest,
|
|
566
|
+
diagnostics: result.diagnostics.map(jsonDiagnostic),
|
|
567
|
+
diagnostics_truncated: result.diagnosticsTruncated,
|
|
568
|
+
...previewResultJson(result, result.ok, writeRequest, writeResult),
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
const candidateAllowed = result.ok && !warningFailure;
|
|
573
|
+
if (candidateAllowed) {
|
|
574
|
+
if (writeResult !== null) {
|
|
575
|
+
process.stderr.write(renderWriteSummary("dsl.format", writeResult));
|
|
576
|
+
}
|
|
577
|
+
else if (parsed.flags.has("check")) {
|
|
578
|
+
if (parsed.flags.has("diff"))
|
|
579
|
+
process.stdout.write(result.diff ?? "");
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
process.stdout.write(parsed.flags.has("diff") ? (result.diff ?? "") : (result.updatedText ?? ""));
|
|
583
|
+
if (!parsed.flags.has("diff")) {
|
|
584
|
+
process.stderr.write(`PREVIEW dsl.format changed=${result.changed} original_digest=${result.originalDigest} updated_digest=${result.updatedDigest}\n`);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
for (const diagnostic of result.diagnostics) {
|
|
589
|
+
process.stderr.write(`${renderDiagnostic(diagnostic, source, color)}\n`);
|
|
590
|
+
}
|
|
591
|
+
if (result.diagnosticsTruncated) {
|
|
592
|
+
process.stderr.write(`DIAGNOSTICS_TRUNCATED true limit=${maxDiagnostics}\n`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return ok ? 0 : 1;
|
|
596
|
+
}
|
|
597
|
+
function mutationOptionSets(resource, action) {
|
|
598
|
+
const values = new Set(mutationCommonValueOptions);
|
|
599
|
+
const flags = new Set(mutationCommonFlagOptions);
|
|
600
|
+
const repeatable = new Set();
|
|
601
|
+
const addValues = (...names) => {
|
|
602
|
+
for (const name of names)
|
|
603
|
+
values.add(name);
|
|
604
|
+
};
|
|
605
|
+
const addRepeatable = (...names) => {
|
|
606
|
+
for (const name of names)
|
|
607
|
+
repeatable.add(name);
|
|
608
|
+
};
|
|
609
|
+
if (resource === "task" && action === "add") {
|
|
610
|
+
addValues("title", "description", "duration", "optimistic", "most-likely", "pessimistic", "status", "priority", "owner", "blocked-reason", "source");
|
|
611
|
+
addRepeatable("tag", "require");
|
|
612
|
+
}
|
|
613
|
+
else if (resource === "task" && action === "set") {
|
|
614
|
+
addValues("from", "to", "title", "description", "duration", "optimistic", "most-likely", "pessimistic", "status", "priority", "owner", "blocked-reason", "source");
|
|
615
|
+
addRepeatable("require", "add-tag", "remove-tag", "remove-require", "clear");
|
|
616
|
+
}
|
|
617
|
+
else if (resource === "milestone" && action === "add") {
|
|
618
|
+
addValues("title", "description", "state");
|
|
619
|
+
addRepeatable("tag");
|
|
620
|
+
}
|
|
621
|
+
else if (resource === "milestone" && action === "set") {
|
|
622
|
+
addValues("title", "description", "state");
|
|
623
|
+
addRepeatable("add-tag", "remove-tag", "clear");
|
|
624
|
+
}
|
|
625
|
+
else if (resource === "resource" && action === "add") {
|
|
626
|
+
addValues("title", "description", "capacity");
|
|
627
|
+
}
|
|
628
|
+
else if (resource === "resource" && action === "set") {
|
|
629
|
+
addValues("title", "description", "capacity");
|
|
630
|
+
addRepeatable("clear");
|
|
631
|
+
}
|
|
632
|
+
else if (resource === "mutation" && action === "apply") {
|
|
633
|
+
addValues("request");
|
|
634
|
+
}
|
|
635
|
+
return { values, flags, repeatable };
|
|
636
|
+
}
|
|
637
|
+
function requiredOption(parsed, name) {
|
|
638
|
+
const value = parsed.values.get(name);
|
|
639
|
+
if (value === undefined)
|
|
640
|
+
throw new UsageError(`option --${name} is required`);
|
|
641
|
+
return value;
|
|
642
|
+
}
|
|
643
|
+
function optionalInteger(parsed, name, minimum, maximum) {
|
|
644
|
+
const raw = parsed.values.get(name);
|
|
645
|
+
return raw === undefined
|
|
646
|
+
? undefined
|
|
647
|
+
: boundedInteger(raw, name, minimum, minimum, maximum);
|
|
648
|
+
}
|
|
649
|
+
function enumOption(raw, option, allowed) {
|
|
650
|
+
if (raw === undefined)
|
|
651
|
+
return undefined;
|
|
652
|
+
if (!allowed.has(raw)) {
|
|
653
|
+
throw new UsageError(`--${option} must be one of ${[...allowed].join(", ")}`);
|
|
654
|
+
}
|
|
655
|
+
return raw;
|
|
656
|
+
}
|
|
657
|
+
function uniqueRepeated(parsed, option) {
|
|
658
|
+
const values = parsed.repeatedValues.get(option) ?? [];
|
|
659
|
+
const seen = new Set();
|
|
660
|
+
for (const value of values) {
|
|
661
|
+
if (seen.has(value))
|
|
662
|
+
throw new UsageError(`duplicate --${option} value ${value}`);
|
|
663
|
+
seen.add(value);
|
|
664
|
+
}
|
|
665
|
+
return values;
|
|
666
|
+
}
|
|
667
|
+
function enumRepeated(parsed, option, allowed) {
|
|
668
|
+
return uniqueRepeated(parsed, option).map((value) => {
|
|
669
|
+
if (!allowed.has(value)) {
|
|
670
|
+
throw new UsageError(`--${option} must be one of ${[...allowed].join(", ")}`);
|
|
671
|
+
}
|
|
672
|
+
return value;
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
function ensureDisjoint(left, leftOption, right, rightOption) {
|
|
676
|
+
const rightSet = new Set(right);
|
|
677
|
+
const duplicate = left.find((value) => rightSet.has(value));
|
|
678
|
+
if (duplicate !== undefined) {
|
|
679
|
+
throw new UsageError(`--${leftOption} and --${rightOption} conflict for ${duplicate}`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function taskTiming(parsed, required) {
|
|
683
|
+
const duration = parsed.values.get("duration");
|
|
684
|
+
const optimistic = parsed.values.get("optimistic");
|
|
685
|
+
const mostLikely = parsed.values.get("most-likely");
|
|
686
|
+
const pessimistic = parsed.values.get("pessimistic");
|
|
687
|
+
const estimateValues = [optimistic, mostLikely, pessimistic];
|
|
688
|
+
const hasEstimate = estimateValues.some((value) => value !== undefined);
|
|
689
|
+
if (duration !== undefined && hasEstimate) {
|
|
690
|
+
throw new UsageError("--duration and three-point estimate options are mutually exclusive");
|
|
691
|
+
}
|
|
692
|
+
if (hasEstimate && estimateValues.some((value) => value === undefined)) {
|
|
693
|
+
throw new UsageError("--optimistic, --most-likely, and --pessimistic must be specified together");
|
|
694
|
+
}
|
|
695
|
+
if (duration !== undefined)
|
|
696
|
+
return { duration };
|
|
697
|
+
if (optimistic !== undefined && mostLikely !== undefined && pessimistic !== undefined) {
|
|
698
|
+
return { estimate: { optimistic, mostLikely, pessimistic } };
|
|
699
|
+
}
|
|
700
|
+
if (required) {
|
|
701
|
+
throw new UsageError("--duration or a complete three-point estimate is required");
|
|
702
|
+
}
|
|
703
|
+
return undefined;
|
|
704
|
+
}
|
|
705
|
+
function requirementOptions(parsed) {
|
|
706
|
+
const requirements = [];
|
|
707
|
+
const seen = new Set();
|
|
708
|
+
for (const raw of parsed.repeatedValues.get("require") ?? []) {
|
|
709
|
+
const match = /^(.+)=([0-9]+)$/.exec(raw);
|
|
710
|
+
if (match === null) {
|
|
711
|
+
throw new UsageError("--require must be <resource-id>=<integer>");
|
|
712
|
+
}
|
|
713
|
+
const resourceId = match[1];
|
|
714
|
+
const units = Number(match[2]);
|
|
715
|
+
if (!Number.isSafeInteger(units) || units < 1 || units > 2_147_483_647) {
|
|
716
|
+
throw new UsageError("--require integer must be from 1 to 2147483647");
|
|
717
|
+
}
|
|
718
|
+
if (seen.has(resourceId)) {
|
|
719
|
+
throw new UsageError(`duplicate --require for ${resourceId}`);
|
|
720
|
+
}
|
|
721
|
+
seen.add(resourceId);
|
|
722
|
+
requirements.push({ resourceId, units });
|
|
723
|
+
}
|
|
724
|
+
return requirements;
|
|
725
|
+
}
|
|
726
|
+
function taskMutationFromOptions(action, parsed) {
|
|
727
|
+
const expectedPositionals = action === "add" ? 4 : 2;
|
|
728
|
+
if (parsed.positionals.length !== expectedPositionals) {
|
|
729
|
+
throw new UsageError(`task ${action} requires ${action === "add" ? "<file> <id> <from> <to>" : "<file> <id>"}`);
|
|
730
|
+
}
|
|
731
|
+
const id = parsed.positionals[1];
|
|
732
|
+
if (action === "remove")
|
|
733
|
+
return { kind: "task.remove", id };
|
|
734
|
+
if (action === "finish")
|
|
735
|
+
return { kind: "task.finish", id };
|
|
736
|
+
const status = enumOption(parsed.values.get("status"), "status", new Set(["planned", "active", "blocked", "done"]));
|
|
737
|
+
const priority = optionalInteger(parsed, "priority", 0, 2_147_483_647);
|
|
738
|
+
const timing = taskTiming(parsed, action === "add");
|
|
739
|
+
const requirements = requirementOptions(parsed);
|
|
740
|
+
if (action === "add") {
|
|
741
|
+
const title = requiredOption(parsed, "title");
|
|
742
|
+
return {
|
|
743
|
+
kind: "task.add",
|
|
744
|
+
id,
|
|
745
|
+
from: parsed.positionals[2],
|
|
746
|
+
to: parsed.positionals[3],
|
|
747
|
+
task: {
|
|
748
|
+
title,
|
|
749
|
+
...timing,
|
|
750
|
+
...(parsed.values.get("description") === undefined
|
|
751
|
+
? {}
|
|
752
|
+
: { description: parsed.values.get("description") }),
|
|
753
|
+
...(status === undefined ? {} : { status }),
|
|
754
|
+
...(priority === undefined ? {} : { priority }),
|
|
755
|
+
...(requirements.length === 0 ? {} : { requirements }),
|
|
756
|
+
...(parsed.values.get("owner") === undefined
|
|
757
|
+
? {}
|
|
758
|
+
: { owner: parsed.values.get("owner") }),
|
|
759
|
+
...((parsed.repeatedValues.get("tag") ?? []).length === 0
|
|
760
|
+
? {}
|
|
761
|
+
: { tags: parsed.repeatedValues.get("tag") }),
|
|
762
|
+
...(parsed.values.get("blocked-reason") === undefined
|
|
763
|
+
? {}
|
|
764
|
+
: { blockedReason: parsed.values.get("blocked-reason") }),
|
|
765
|
+
...(parsed.values.get("source") === undefined
|
|
766
|
+
? {}
|
|
767
|
+
: { source: parsed.values.get("source") }),
|
|
768
|
+
},
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
const clear = enumRepeated(parsed, "clear", new Set([
|
|
772
|
+
"description", "status", "priority", "owner", "blocked_reason", "source",
|
|
773
|
+
"tags", "requires",
|
|
774
|
+
]));
|
|
775
|
+
const addTags = uniqueRepeated(parsed, "add-tag");
|
|
776
|
+
const removeTags = uniqueRepeated(parsed, "remove-tag");
|
|
777
|
+
const removeRequirements = uniqueRepeated(parsed, "remove-require");
|
|
778
|
+
ensureDisjoint(addTags, "add-tag", removeTags, "remove-tag");
|
|
779
|
+
ensureDisjoint(requirements.map(({ resourceId }) => resourceId), "require", removeRequirements, "remove-require");
|
|
780
|
+
const set = {
|
|
781
|
+
...(parsed.values.get("title") === undefined
|
|
782
|
+
? {}
|
|
783
|
+
: { title: parsed.values.get("title") }),
|
|
784
|
+
...(parsed.values.get("description") === undefined
|
|
785
|
+
? {}
|
|
786
|
+
: { description: parsed.values.get("description") }),
|
|
787
|
+
...timing,
|
|
788
|
+
...(status === undefined ? {} : { status }),
|
|
789
|
+
...(priority === undefined ? {} : { priority }),
|
|
790
|
+
...(parsed.values.get("owner") === undefined
|
|
791
|
+
? {}
|
|
792
|
+
: { owner: parsed.values.get("owner") }),
|
|
793
|
+
...(parsed.values.get("blocked-reason") === undefined
|
|
794
|
+
? {}
|
|
795
|
+
: { blockedReason: parsed.values.get("blocked-reason") }),
|
|
796
|
+
...(parsed.values.get("source") === undefined
|
|
797
|
+
? {}
|
|
798
|
+
: { source: parsed.values.get("source") }),
|
|
799
|
+
};
|
|
800
|
+
const clearConflicts = new Map([
|
|
801
|
+
["description", parsed.values.has("description")],
|
|
802
|
+
["status", status !== undefined],
|
|
803
|
+
["priority", priority !== undefined],
|
|
804
|
+
["owner", parsed.values.has("owner")],
|
|
805
|
+
["blocked_reason", parsed.values.has("blocked-reason")],
|
|
806
|
+
["source", parsed.values.has("source")],
|
|
807
|
+
["tags", addTags.length > 0 || removeTags.length > 0],
|
|
808
|
+
["requires", requirements.length > 0 || removeRequirements.length > 0],
|
|
809
|
+
]);
|
|
810
|
+
const conflict = clear.find((field) => clearConflicts.get(field) === true);
|
|
811
|
+
if (conflict !== undefined) {
|
|
812
|
+
throw new UsageError(`--clear ${conflict} conflicts with another field option`);
|
|
813
|
+
}
|
|
814
|
+
return {
|
|
815
|
+
kind: "task.set",
|
|
816
|
+
id,
|
|
817
|
+
...(parsed.values.get("from") === undefined ? {} : { from: parsed.values.get("from") }),
|
|
818
|
+
...(parsed.values.get("to") === undefined ? {} : { to: parsed.values.get("to") }),
|
|
819
|
+
...(Object.keys(set).length === 0 ? {} : { set }),
|
|
820
|
+
...(clear.length === 0 ? {} : { clear }),
|
|
821
|
+
...(addTags.length === 0 ? {} : { addTags }),
|
|
822
|
+
...(removeTags.length === 0 ? {} : { removeTags }),
|
|
823
|
+
...(requirements.length === 0 ? {} : { upsertRequirements: requirements }),
|
|
824
|
+
...(removeRequirements.length === 0 ? {} : { removeRequirements }),
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
function milestoneMutationFromOptions(action, parsed) {
|
|
828
|
+
if (parsed.positionals.length !== 2) {
|
|
829
|
+
throw new UsageError(`milestone ${action} requires <file> <id>`);
|
|
830
|
+
}
|
|
831
|
+
const id = parsed.positionals[1];
|
|
832
|
+
if (action === "remove")
|
|
833
|
+
return { kind: "milestone.remove", id };
|
|
834
|
+
const state = enumOption(parsed.values.get("state"), "state", new Set(["planned", "reached"]));
|
|
835
|
+
if (action === "add") {
|
|
836
|
+
return {
|
|
837
|
+
kind: "milestone.add",
|
|
838
|
+
id,
|
|
839
|
+
milestone: {
|
|
840
|
+
title: requiredOption(parsed, "title"),
|
|
841
|
+
...(parsed.values.get("description") === undefined
|
|
842
|
+
? {}
|
|
843
|
+
: { description: parsed.values.get("description") }),
|
|
844
|
+
...(state === undefined ? {} : { state }),
|
|
845
|
+
...((parsed.repeatedValues.get("tag") ?? []).length === 0
|
|
846
|
+
? {}
|
|
847
|
+
: { tags: parsed.repeatedValues.get("tag") }),
|
|
848
|
+
},
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
const clear = enumRepeated(parsed, "clear", new Set(["description", "state", "tags"]));
|
|
852
|
+
const addTags = uniqueRepeated(parsed, "add-tag");
|
|
853
|
+
const removeTags = uniqueRepeated(parsed, "remove-tag");
|
|
854
|
+
ensureDisjoint(addTags, "add-tag", removeTags, "remove-tag");
|
|
855
|
+
if (clear.some((field) => (field === "description" && parsed.values.has("description")) ||
|
|
856
|
+
(field === "state" && state !== undefined) ||
|
|
857
|
+
(field === "tags" && (addTags.length > 0 || removeTags.length > 0)))) {
|
|
858
|
+
throw new UsageError("--clear conflicts with another milestone field option");
|
|
859
|
+
}
|
|
860
|
+
const set = {
|
|
861
|
+
...(parsed.values.get("title") === undefined
|
|
862
|
+
? {}
|
|
863
|
+
: { title: parsed.values.get("title") }),
|
|
864
|
+
...(parsed.values.get("description") === undefined
|
|
865
|
+
? {}
|
|
866
|
+
: { description: parsed.values.get("description") }),
|
|
867
|
+
...(state === undefined ? {} : { state }),
|
|
868
|
+
};
|
|
869
|
+
return {
|
|
870
|
+
kind: "milestone.set",
|
|
871
|
+
id,
|
|
872
|
+
...(Object.keys(set).length === 0 ? {} : { set }),
|
|
873
|
+
...(clear.length === 0 ? {} : { clear }),
|
|
874
|
+
...(addTags.length === 0 ? {} : { addTags }),
|
|
875
|
+
...(removeTags.length === 0 ? {} : { removeTags }),
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
function resourceMutationFromOptions(action, parsed) {
|
|
879
|
+
if (parsed.positionals.length !== 2) {
|
|
880
|
+
throw new UsageError(`resource ${action} requires <file> <id>`);
|
|
881
|
+
}
|
|
882
|
+
const id = parsed.positionals[1];
|
|
883
|
+
if (action === "remove")
|
|
884
|
+
return { kind: "resource.remove", id };
|
|
885
|
+
const capacity = optionalInteger(parsed, "capacity", 1, 2_147_483_647);
|
|
886
|
+
if (action === "add") {
|
|
887
|
+
if (capacity === undefined)
|
|
888
|
+
throw new UsageError("option --capacity is required");
|
|
889
|
+
return {
|
|
890
|
+
kind: "resource.add",
|
|
891
|
+
id,
|
|
892
|
+
resource: {
|
|
893
|
+
title: requiredOption(parsed, "title"),
|
|
894
|
+
capacity,
|
|
895
|
+
...(parsed.values.get("description") === undefined
|
|
896
|
+
? {}
|
|
897
|
+
: { description: parsed.values.get("description") }),
|
|
898
|
+
},
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
const clear = enumRepeated(parsed, "clear", new Set(["description"]));
|
|
902
|
+
if (clear.length > 0 && parsed.values.has("description")) {
|
|
903
|
+
throw new UsageError("--clear description conflicts with --description");
|
|
904
|
+
}
|
|
905
|
+
const set = {
|
|
906
|
+
...(parsed.values.get("title") === undefined
|
|
907
|
+
? {}
|
|
908
|
+
: { title: parsed.values.get("title") }),
|
|
909
|
+
...(parsed.values.get("description") === undefined
|
|
910
|
+
? {}
|
|
911
|
+
: { description: parsed.values.get("description") }),
|
|
912
|
+
...(capacity === undefined ? {} : { capacity }),
|
|
913
|
+
};
|
|
914
|
+
return {
|
|
915
|
+
kind: "resource.set",
|
|
916
|
+
id,
|
|
917
|
+
...(Object.keys(set).length === 0 ? {} : { set }),
|
|
918
|
+
...(clear.length === 0 ? {} : { clear }),
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
function previewResultJson(result, exposeCandidate, writeRequest = { mode: "preview", target: null }, writeResult = null) {
|
|
922
|
+
return {
|
|
923
|
+
changed: exposeCandidate ? result.changed : false,
|
|
924
|
+
original_digest: result.originalDigest,
|
|
925
|
+
updated_digest: exposeCandidate ? result.updatedDigest : null,
|
|
926
|
+
updated_text: exposeCandidate ? result.updatedText : null,
|
|
927
|
+
diff: exposeCandidate ? result.diff : null,
|
|
928
|
+
edits: (exposeCandidate ? result.edits : []).map((edit) => ({
|
|
929
|
+
start_offset: edit.startOffset,
|
|
930
|
+
end_offset: edit.endOffset,
|
|
931
|
+
replacement: edit.replacement,
|
|
932
|
+
})),
|
|
933
|
+
write: {
|
|
934
|
+
mode: writeRequest.mode,
|
|
935
|
+
target: writeRequest.target,
|
|
936
|
+
written: writeResult?.written ?? false,
|
|
937
|
+
},
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
async function readMutationRequest(source) {
|
|
941
|
+
const bytes = source === "-" ? await readStdin() : await readFile(source);
|
|
942
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
943
|
+
try {
|
|
944
|
+
return JSON.parse(text);
|
|
945
|
+
}
|
|
946
|
+
catch (error) {
|
|
947
|
+
throw new UsageError(`mutation request is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
async function runMutation(resource, action, args) {
|
|
951
|
+
const config = mutationOptionSets(resource, action);
|
|
952
|
+
const parsed = parseOptions(args, config.values, config.flags, config.repeatable);
|
|
953
|
+
const format = outputFormat(parsed.values.get("format"));
|
|
954
|
+
const color = colorMode(parsed.values.get("color"), format);
|
|
955
|
+
const maxDiagnostics = boundedInteger(parsed.values.get("max-diagnostics"), "max-diagnostics", 100, 1, 1000);
|
|
956
|
+
const operation = `${resource}.${action}`;
|
|
957
|
+
let sourceOperand;
|
|
958
|
+
let writeRequest;
|
|
959
|
+
let mutation;
|
|
960
|
+
if (resource === "mutation") {
|
|
961
|
+
if (parsed.positionals.length !== 1) {
|
|
962
|
+
throw new UsageError("mutation apply requires exactly one <file>");
|
|
963
|
+
}
|
|
964
|
+
sourceOperand = parsed.positionals[0];
|
|
965
|
+
writeRequest = editingWriteRequest(parsed, sourceOperand);
|
|
966
|
+
const requestSource = requiredOption(parsed, "request");
|
|
967
|
+
if (sourceOperand === "-" && requestSource === "-") {
|
|
968
|
+
throw new UsageError("document and mutation request cannot both use stdin");
|
|
969
|
+
}
|
|
970
|
+
let request;
|
|
971
|
+
try {
|
|
972
|
+
request = await readMutationRequest(requestSource);
|
|
973
|
+
}
|
|
974
|
+
catch (error) {
|
|
975
|
+
if (error instanceof UsageError)
|
|
976
|
+
throw error;
|
|
977
|
+
return cliError(error instanceof Error ? error : new Error(String(error)), 3, operation, format === "json");
|
|
978
|
+
}
|
|
979
|
+
mutation = request;
|
|
980
|
+
}
|
|
981
|
+
else {
|
|
982
|
+
mutation =
|
|
983
|
+
resource === "task"
|
|
984
|
+
? taskMutationFromOptions(action, parsed)
|
|
985
|
+
: resource === "milestone"
|
|
986
|
+
? milestoneMutationFromOptions(action, parsed)
|
|
987
|
+
: resourceMutationFromOptions(action, parsed);
|
|
988
|
+
sourceOperand = parsed.positionals[0];
|
|
989
|
+
writeRequest = editingWriteRequest(parsed, sourceOperand);
|
|
990
|
+
}
|
|
991
|
+
const source = sourceOperand === "-" ? "<stdin>" : sourceOperand;
|
|
992
|
+
let input;
|
|
993
|
+
try {
|
|
994
|
+
input = await readDocument(sourceOperand);
|
|
995
|
+
}
|
|
996
|
+
catch (error) {
|
|
997
|
+
return cliError(error instanceof Error ? error : new Error(String(error)), 3, operation, format === "json");
|
|
998
|
+
}
|
|
999
|
+
const mutationOptions = {
|
|
1000
|
+
maxDiagnostics,
|
|
1001
|
+
originalLabel: source,
|
|
1002
|
+
updatedLabel: "candidate",
|
|
1003
|
+
};
|
|
1004
|
+
const result = resource === "mutation"
|
|
1005
|
+
? planBatchMutation(input.text, mutation, mutationOptions)
|
|
1006
|
+
: planMutation(input.text, mutation, mutationOptions);
|
|
1007
|
+
const warningFailure = parsed.flags.has("warnings-as-errors") &&
|
|
1008
|
+
(result.diagnosticsTruncated ||
|
|
1009
|
+
result.diagnostics.some((diagnostic) => diagnostic.severity === "warning"));
|
|
1010
|
+
const ok = result.ok && !warningFailure;
|
|
1011
|
+
let writeResult = null;
|
|
1012
|
+
if (result.ok) {
|
|
1013
|
+
try {
|
|
1014
|
+
assertExpectedDigest(writeRequest, input.digest);
|
|
1015
|
+
if (ok && writeRequest.mode !== "preview") {
|
|
1016
|
+
writeResult = await commitCandidate(writeRequest, result.updatedText, input.digest);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
catch (error) {
|
|
1020
|
+
return writeFailureExit(error, operation, format === "json");
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
if (format === "json") {
|
|
1024
|
+
writeJson({
|
|
1025
|
+
schema_version: "Perttool.MutationResult.v1",
|
|
1026
|
+
tool_version: TOOL_VERSION,
|
|
1027
|
+
operation,
|
|
1028
|
+
ok,
|
|
1029
|
+
document_id: result.documentId,
|
|
1030
|
+
source,
|
|
1031
|
+
source_digest: input.digest,
|
|
1032
|
+
diagnostics: result.diagnostics.map(jsonDiagnostic),
|
|
1033
|
+
diagnostics_truncated: result.diagnosticsTruncated,
|
|
1034
|
+
...previewResultJson(result, result.ok, writeRequest, writeResult),
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
if (ok) {
|
|
1039
|
+
if (writeResult !== null) {
|
|
1040
|
+
process.stderr.write(renderWriteSummary(operation, writeResult));
|
|
1041
|
+
}
|
|
1042
|
+
else {
|
|
1043
|
+
process.stdout.write(parsed.flags.has("diff") ? (result.diff ?? "") : (result.updatedText ?? ""));
|
|
1044
|
+
if (!parsed.flags.has("diff")) {
|
|
1045
|
+
process.stderr.write(`PREVIEW ${operation} changed=${result.changed} original_digest=${result.originalDigest} updated_digest=${result.updatedDigest}\n`);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
for (const diagnostic of result.diagnostics) {
|
|
1050
|
+
process.stderr.write(`${renderDiagnostic(diagnostic, source, color)}\n`);
|
|
1051
|
+
}
|
|
1052
|
+
if (result.diagnosticsTruncated) {
|
|
1053
|
+
process.stderr.write(`DIAGNOSTICS_TRUNCATED true limit=${maxDiagnostics}\n`);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return ok ? 0 : 1;
|
|
1057
|
+
}
|
|
1058
|
+
function advanceResultJson(details) {
|
|
1059
|
+
return details === null
|
|
1060
|
+
? null
|
|
1061
|
+
: {
|
|
1062
|
+
removed_task_ids: details.removedTaskIds,
|
|
1063
|
+
removed_gate_ids: details.removedGateIds,
|
|
1064
|
+
removed_milestone_ids: details.removedMilestoneIds,
|
|
1065
|
+
frontier_before: details.frontierBefore,
|
|
1066
|
+
frontier_after: details.frontierAfter,
|
|
1067
|
+
ready_before: details.readyBefore,
|
|
1068
|
+
ready_after: details.readyAfter,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
function renderAdvanceSummary(details) {
|
|
1072
|
+
const list = (ids) => ids.join(",") || "-";
|
|
1073
|
+
return [
|
|
1074
|
+
`ADVANCE removed_tasks=${list(details.removedTaskIds)} removed_gates=${list(details.removedGateIds)} removed_milestones=${list(details.removedMilestoneIds)}`,
|
|
1075
|
+
`ADVANCE frontier_before=${list(details.frontierBefore)} frontier_after=${list(details.frontierAfter)} ready_before=${list(details.readyBefore)} ready_after=${list(details.readyAfter)}`,
|
|
1076
|
+
"",
|
|
1077
|
+
].join("\n");
|
|
1078
|
+
}
|
|
1079
|
+
async function runAdvance(args) {
|
|
1080
|
+
const parsed = parseOptions(args, new Set(["format", "color", "max-diagnostics", "out", "expect-digest"]), new Set(["diff", "write", "warnings-as-errors"]));
|
|
1081
|
+
if (parsed.positionals.length !== 1) {
|
|
1082
|
+
throw new UsageError("dag advance requires exactly one <file>");
|
|
1083
|
+
}
|
|
1084
|
+
const format = outputFormat(parsed.values.get("format"));
|
|
1085
|
+
const color = colorMode(parsed.values.get("color"), format);
|
|
1086
|
+
const maxDiagnostics = boundedInteger(parsed.values.get("max-diagnostics"), "max-diagnostics", 100, 1, 1000);
|
|
1087
|
+
const sourceOperand = parsed.positionals[0];
|
|
1088
|
+
const writeRequest = editingWriteRequest(parsed, sourceOperand);
|
|
1089
|
+
const source = sourceOperand === "-" ? "<stdin>" : sourceOperand;
|
|
1090
|
+
let input;
|
|
1091
|
+
try {
|
|
1092
|
+
input = await readDocument(sourceOperand);
|
|
1093
|
+
}
|
|
1094
|
+
catch (error) {
|
|
1095
|
+
return cliError(error instanceof Error ? error : new Error(String(error)), 3, "dag.advance", format === "json");
|
|
1096
|
+
}
|
|
1097
|
+
const result = planAdvance(input.text, {
|
|
1098
|
+
maxDiagnostics,
|
|
1099
|
+
originalLabel: source,
|
|
1100
|
+
updatedLabel: "candidate",
|
|
1101
|
+
});
|
|
1102
|
+
const warningFailure = parsed.flags.has("warnings-as-errors") &&
|
|
1103
|
+
(result.diagnosticsTruncated ||
|
|
1104
|
+
result.diagnostics.some((diagnostic) => diagnostic.severity === "warning"));
|
|
1105
|
+
const ok = result.ok && !warningFailure;
|
|
1106
|
+
let writeResult = null;
|
|
1107
|
+
if (result.ok) {
|
|
1108
|
+
try {
|
|
1109
|
+
assertExpectedDigest(writeRequest, input.digest);
|
|
1110
|
+
if (ok && writeRequest.mode !== "preview") {
|
|
1111
|
+
writeResult = await commitCandidate(writeRequest, result.updatedText, input.digest);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
catch (error) {
|
|
1115
|
+
return writeFailureExit(error, "dag.advance", format === "json");
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
if (format === "json") {
|
|
1119
|
+
writeJson({
|
|
1120
|
+
schema_version: "Perttool.MutationResult.v1",
|
|
1121
|
+
tool_version: TOOL_VERSION,
|
|
1122
|
+
operation: "dag.advance",
|
|
1123
|
+
ok,
|
|
1124
|
+
document_id: result.documentId,
|
|
1125
|
+
source,
|
|
1126
|
+
source_digest: input.digest,
|
|
1127
|
+
diagnostics: result.diagnostics.map(jsonDiagnostic),
|
|
1128
|
+
diagnostics_truncated: result.diagnosticsTruncated,
|
|
1129
|
+
...previewResultJson(result, result.ok, writeRequest, writeResult),
|
|
1130
|
+
advance: advanceResultJson(result.advance),
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
else {
|
|
1134
|
+
if (ok && result.advance !== null) {
|
|
1135
|
+
if (writeResult !== null) {
|
|
1136
|
+
process.stderr.write(renderWriteSummary("dag.advance", writeResult));
|
|
1137
|
+
}
|
|
1138
|
+
else {
|
|
1139
|
+
process.stdout.write(parsed.flags.has("diff") ? (result.diff ?? "") : (result.updatedText ?? ""));
|
|
1140
|
+
if (!parsed.flags.has("diff")) {
|
|
1141
|
+
process.stderr.write(`PREVIEW dag.advance changed=${result.changed} original_digest=${result.originalDigest} updated_digest=${result.updatedDigest}\n`);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
process.stderr.write(renderAdvanceSummary(result.advance));
|
|
1145
|
+
}
|
|
1146
|
+
for (const diagnostic of result.diagnostics) {
|
|
1147
|
+
process.stderr.write(`${renderDiagnostic(diagnostic, source, color)}\n`);
|
|
1148
|
+
}
|
|
1149
|
+
if (result.diagnosticsTruncated) {
|
|
1150
|
+
process.stderr.write(`DIAGNOSTICS_TRUNCATED true limit=${maxDiagnostics}\n`);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return ok ? 0 : 1;
|
|
1154
|
+
}
|
|
1155
|
+
function rationalJson(value, unit, precision) {
|
|
1156
|
+
return {
|
|
1157
|
+
numerator: value.numerator.toString(),
|
|
1158
|
+
denominator: value.denominator.toString(),
|
|
1159
|
+
unit,
|
|
1160
|
+
display: formatDecimal(value, precision),
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
function precedenceJson(result, unit, precision) {
|
|
1164
|
+
const varianceUnit = `${unit}^2`;
|
|
1165
|
+
const path = (value) => ({
|
|
1166
|
+
edge_ids: value.edgeIds,
|
|
1167
|
+
task_ids: value.taskIds,
|
|
1168
|
+
gate_ids: value.gateIds,
|
|
1169
|
+
variance: rationalJson(value.variance, varianceUnit, precision),
|
|
1170
|
+
});
|
|
1171
|
+
return {
|
|
1172
|
+
makespan: rationalJson(result.makespan, unit, precision),
|
|
1173
|
+
conditional_on_blocks_resolved: result.conditionalOnBlocksResolved,
|
|
1174
|
+
blocked_task_ids: result.blockedTaskIds,
|
|
1175
|
+
milestones: result.milestones.map((milestone) => ({
|
|
1176
|
+
id: milestone.id,
|
|
1177
|
+
earliest: rationalJson(milestone.earliest, unit, precision),
|
|
1178
|
+
latest: rationalJson(milestone.latest, unit, precision),
|
|
1179
|
+
slack: rationalJson(milestone.slack, unit, precision),
|
|
1180
|
+
})),
|
|
1181
|
+
edges: result.edges.map((edge) => ({
|
|
1182
|
+
id: edge.id,
|
|
1183
|
+
source: edge.source,
|
|
1184
|
+
target: edge.target,
|
|
1185
|
+
kind: edge.kind,
|
|
1186
|
+
status: edge.status,
|
|
1187
|
+
expected: rationalJson(edge.expected, unit, precision),
|
|
1188
|
+
variance: rationalJson(edge.variance, varianceUnit, precision),
|
|
1189
|
+
es: rationalJson(edge.es, unit, precision),
|
|
1190
|
+
ef: rationalJson(edge.ef, unit, precision),
|
|
1191
|
+
ls: rationalJson(edge.ls, unit, precision),
|
|
1192
|
+
lf: rationalJson(edge.lf, unit, precision),
|
|
1193
|
+
total_float: rationalJson(edge.totalFloat, unit, precision),
|
|
1194
|
+
free_float: rationalJson(edge.freeFloat, unit, precision),
|
|
1195
|
+
is_critical: edge.isCritical,
|
|
1196
|
+
is_driving: edge.isDriving,
|
|
1197
|
+
})),
|
|
1198
|
+
critical: {
|
|
1199
|
+
milestone_ids: result.critical.milestoneIds,
|
|
1200
|
+
task_ids: result.critical.taskIds,
|
|
1201
|
+
gate_ids: result.critical.gateIds,
|
|
1202
|
+
driving_edge_ids: result.critical.drivingEdgeIds,
|
|
1203
|
+
representative_path: path(result.critical.representativePath),
|
|
1204
|
+
path_count: result.critical.pathCount.toString(),
|
|
1205
|
+
paths: result.critical.paths.map(path),
|
|
1206
|
+
paths_truncated: result.critical.pathsTruncated,
|
|
1207
|
+
},
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
function resourceJson(result, unit, precision) {
|
|
1211
|
+
const varianceUnit = `${unit}^2`;
|
|
1212
|
+
const schedulePath = (path) => ({
|
|
1213
|
+
task_ids: path.taskIds,
|
|
1214
|
+
constraints: path.constraints.map((constraint) => ({
|
|
1215
|
+
from_task_id: constraint.fromTaskId,
|
|
1216
|
+
to_task_id: constraint.toTaskId,
|
|
1217
|
+
kind: constraint.kind,
|
|
1218
|
+
resource_arc_id: constraint.resourceArcId,
|
|
1219
|
+
})),
|
|
1220
|
+
connector_ids: path.connectorIds,
|
|
1221
|
+
});
|
|
1222
|
+
return {
|
|
1223
|
+
algorithm: result.algorithm,
|
|
1224
|
+
conditional_on_blocks_resolved: result.conditionalOnBlocksResolved,
|
|
1225
|
+
blocked_task_ids: result.blockedTaskIds,
|
|
1226
|
+
capacities: result.capacities.map((capacity) => ({
|
|
1227
|
+
id: capacity.id,
|
|
1228
|
+
declared: capacity.declared,
|
|
1229
|
+
override: capacity.override,
|
|
1230
|
+
effective: capacity.effective,
|
|
1231
|
+
})),
|
|
1232
|
+
precedence_lower_bound: rationalJson(result.precedenceLowerBound, unit, precision),
|
|
1233
|
+
makespan: rationalJson(result.makespan, unit, precision),
|
|
1234
|
+
resource_delay: rationalJson(result.resourceDelay, unit, precision),
|
|
1235
|
+
tasks: result.tasks.map((task) => ({
|
|
1236
|
+
id: task.id,
|
|
1237
|
+
status: task.status,
|
|
1238
|
+
expected: rationalJson(task.expected, unit, precision),
|
|
1239
|
+
variance: rationalJson(task.variance, varianceUnit, precision),
|
|
1240
|
+
eligible_time: rationalJson(task.eligibleTime, unit, precision),
|
|
1241
|
+
start: rationalJson(task.start, unit, precision),
|
|
1242
|
+
finish: rationalJson(task.finish, unit, precision),
|
|
1243
|
+
resource_wait: rationalJson(task.resourceWait, unit, precision),
|
|
1244
|
+
requirements: task.requirements.map((requirement) => ({
|
|
1245
|
+
resource_id: requirement.resourceId,
|
|
1246
|
+
units: requirement.units,
|
|
1247
|
+
})),
|
|
1248
|
+
priority_key: {
|
|
1249
|
+
priority: task.priorityKey.priority,
|
|
1250
|
+
precedence_total_float: rationalJson(task.priorityKey.precedenceTotalFloat, unit, precision),
|
|
1251
|
+
expected: rationalJson(task.priorityKey.expected, unit, precision),
|
|
1252
|
+
task_id: task.priorityKey.taskId,
|
|
1253
|
+
},
|
|
1254
|
+
conditional_blocked: task.conditionalBlocked,
|
|
1255
|
+
})),
|
|
1256
|
+
resources: result.resources.map((resource) => ({
|
|
1257
|
+
id: resource.id,
|
|
1258
|
+
capacity: resource.capacity,
|
|
1259
|
+
amount_time: rationalJson(resource.amountTime, unit, precision),
|
|
1260
|
+
utilization: rationalJson(resource.utilization, "ratio", precision),
|
|
1261
|
+
peak_usage: resource.peakUsage,
|
|
1262
|
+
last_release: rationalJson(resource.lastRelease, unit, precision),
|
|
1263
|
+
timeline: resource.timeline.map((entry) => ({
|
|
1264
|
+
task_id: entry.taskId,
|
|
1265
|
+
start: rationalJson(entry.start, unit, precision),
|
|
1266
|
+
finish: rationalJson(entry.finish, unit, precision),
|
|
1267
|
+
units: entry.units,
|
|
1268
|
+
})),
|
|
1269
|
+
})),
|
|
1270
|
+
resource_arcs: result.resourceArcs.map((arc) => ({
|
|
1271
|
+
id: arc.id,
|
|
1272
|
+
from_task_id: arc.fromTaskId,
|
|
1273
|
+
to_task_id: arc.toTaskId,
|
|
1274
|
+
at_time: rationalJson(arc.atTime, unit, precision),
|
|
1275
|
+
wait_from: rationalJson(arc.waitFrom, unit, precision),
|
|
1276
|
+
resources: [...arc.resources]
|
|
1277
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
1278
|
+
.map(([resourceId, contributedUnits]) => ({
|
|
1279
|
+
resource_id: resourceId,
|
|
1280
|
+
contributed_units: contributedUnits,
|
|
1281
|
+
})),
|
|
1282
|
+
schedule_float: rationalJson(arc.scheduleFloat, unit, precision),
|
|
1283
|
+
is_critical: arc.isCritical,
|
|
1284
|
+
is_driving: arc.isDriving,
|
|
1285
|
+
})),
|
|
1286
|
+
constraint_graph_replay: result.constraintGraphReplay,
|
|
1287
|
+
schedule_critical: {
|
|
1288
|
+
task_ids: result.scheduleCritical.taskIds,
|
|
1289
|
+
resource_arc_ids: result.scheduleCritical.resourceArcIds,
|
|
1290
|
+
driving_constraint_ids: result.scheduleCritical.drivingConstraintIds,
|
|
1291
|
+
representative_path: schedulePath(result.scheduleCritical.representativePath),
|
|
1292
|
+
path_count: result.scheduleCritical.pathCount.toString(),
|
|
1293
|
+
paths: result.scheduleCritical.paths.map(schedulePath),
|
|
1294
|
+
paths_truncated: result.scheduleCritical.pathsTruncated,
|
|
1295
|
+
},
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
function durationText(value, unit, precision) {
|
|
1299
|
+
return `${formatDecimal(value, precision)}${durationSuffix(unit)}`;
|
|
1300
|
+
}
|
|
1301
|
+
function velocityJson(velocity, precision) {
|
|
1302
|
+
if (velocity === null)
|
|
1303
|
+
return null;
|
|
1304
|
+
return {
|
|
1305
|
+
points: rationalJson(velocity.points, "point", precision),
|
|
1306
|
+
period: rationalJson(velocity.period, velocity.periodUnit, precision),
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
function velocityText(velocity, precision) {
|
|
1310
|
+
return `${durationText(velocity.points, "point", precision)}/${durationText(velocity.period, velocity.periodUnit, precision)}`;
|
|
1311
|
+
}
|
|
1312
|
+
function analysisVelocityForecastJson(result) {
|
|
1313
|
+
const forecast = result.velocityForecast;
|
|
1314
|
+
if (forecast === null)
|
|
1315
|
+
return null;
|
|
1316
|
+
return {
|
|
1317
|
+
qualifier: forecast.qualifier,
|
|
1318
|
+
source_unit: forecast.sourceUnit,
|
|
1319
|
+
target_unit: forecast.targetUnit,
|
|
1320
|
+
precedence_makespan: result.precedence === null
|
|
1321
|
+
? null
|
|
1322
|
+
: rationalJson(convertWithVelocity(result.precedence.makespan, forecast), forecast.targetUnit, result.precision),
|
|
1323
|
+
resource_makespan: result.resource === null
|
|
1324
|
+
? null
|
|
1325
|
+
: rationalJson(convertWithVelocity(result.resource.makespan, forecast), forecast.targetUnit, result.precision),
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
function renderAnalysisText(result) {
|
|
1329
|
+
const unit = result.durationUnit;
|
|
1330
|
+
const lines = [`PERTTOOL ANALYSIS ${result.documentId ?? "-"}`, "", "QUALIFIERS"];
|
|
1331
|
+
const conditional = result.precedence?.conditionalOnBlocksResolved === true ||
|
|
1332
|
+
result.resource?.conditionalOnBlocksResolved === true;
|
|
1333
|
+
const blockedTaskIds = result.precedence?.blockedTaskIds ?? result.resource?.blockedTaskIds ?? [];
|
|
1334
|
+
const pathsTruncated = result.precedence?.critical.pathsTruncated === true ||
|
|
1335
|
+
result.resource?.scheduleCritical.pathsTruncated === true;
|
|
1336
|
+
const overrides = [...result.capacityOverrides]
|
|
1337
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
|
|
1338
|
+
lines.push(`CONDITIONAL_ON_BLOCKS_RESOLVED ${conditional}`, `BLOCKED_TASKS ${blockedTaskIds.join(", ") || "-"}`, `PATHS_TRUNCATED ${pathsTruncated}`, `CAPACITY_OVERRIDES ${overrides.map(([id, capacity]) => `${id}=${capacity}`).join(", ") || "-"}`, `VELOCITY ${result.velocity === null ? "-" : velocityText(result.velocity, result.precision)}`, `VELOCITY_FORECAST_UNIT ${result.velocityForecast?.targetUnit ?? "-"}`);
|
|
1339
|
+
if (result.precedence !== null) {
|
|
1340
|
+
const precedence = result.precedence;
|
|
1341
|
+
lines.push("", "PRECEDENCE", `MAKESPAN ${durationText(precedence.makespan, unit, result.precision)}`, ...(result.velocityForecast === null
|
|
1342
|
+
? []
|
|
1343
|
+
: [
|
|
1344
|
+
`VELOCITY FORECAST ${durationText(convertWithVelocity(precedence.makespan, result.velocityForecast), result.velocityForecast.targetUnit, result.precision)}`,
|
|
1345
|
+
]), "EDGES", "ID EXPECTED ES EF LS LF TF FF CRITICAL");
|
|
1346
|
+
for (const edge of precedence.edges) {
|
|
1347
|
+
lines.push([
|
|
1348
|
+
edge.id,
|
|
1349
|
+
durationText(edge.expected, unit, result.precision),
|
|
1350
|
+
durationText(edge.es, unit, result.precision),
|
|
1351
|
+
durationText(edge.ef, unit, result.precision),
|
|
1352
|
+
durationText(edge.ls, unit, result.precision),
|
|
1353
|
+
durationText(edge.lf, unit, result.precision),
|
|
1354
|
+
durationText(edge.totalFloat, unit, result.precision),
|
|
1355
|
+
durationText(edge.freeFloat, unit, result.precision),
|
|
1356
|
+
edge.isCritical ? "yes" : "no",
|
|
1357
|
+
].join(" "));
|
|
1358
|
+
}
|
|
1359
|
+
lines.push("", "PRECEDENCE CRITICAL", `TASKS ${precedence.critical.taskIds.join(", ") || "-"}`, `GATES ${precedence.critical.gateIds.join(", ") || "-"}`, `REPRESENTATIVE PATH ${precedence.critical.representativePath.edgeIds.join(" -> ") || "(complete)"}`, `PATH COUNT ${precedence.critical.pathCount.toString()}${precedence.critical.pathsTruncated ? " (truncated)" : ""}`);
|
|
1360
|
+
}
|
|
1361
|
+
if (result.resource !== null) {
|
|
1362
|
+
const resource = result.resource;
|
|
1363
|
+
lines.push("", "RESOURCE SCHEDULE", `ALGORITHM ${resource.algorithm.id}@${resource.algorithm.version} optimal=${resource.algorithm.optimal}`, `PRECEDENCE LOWER BOUND ${durationText(resource.precedenceLowerBound, unit, result.precision)}`, `MAKESPAN ${durationText(resource.makespan, unit, result.precision)}`, ...(result.velocityForecast === null
|
|
1364
|
+
? []
|
|
1365
|
+
: [
|
|
1366
|
+
`VELOCITY FORECAST ${durationText(convertWithVelocity(resource.makespan, result.velocityForecast), result.velocityForecast.targetUnit, result.precision)}`,
|
|
1367
|
+
]), `DELAY ${durationText(resource.resourceDelay, unit, result.precision)}`, "TASKS", "ID ELIGIBLE START FINISH WAIT REQUIREMENTS");
|
|
1368
|
+
for (const task of resource.tasks) {
|
|
1369
|
+
lines.push([
|
|
1370
|
+
task.id,
|
|
1371
|
+
durationText(task.eligibleTime, unit, result.precision),
|
|
1372
|
+
durationText(task.start, unit, result.precision),
|
|
1373
|
+
durationText(task.finish, unit, result.precision),
|
|
1374
|
+
durationText(task.resourceWait, unit, result.precision),
|
|
1375
|
+
task.requirements.map((requirement) => `${requirement.resourceId}=${requirement.units}`).join(",") || "-",
|
|
1376
|
+
].join(" "));
|
|
1377
|
+
}
|
|
1378
|
+
lines.push("RESOURCE ARCS");
|
|
1379
|
+
for (const arc of resource.resourceArcs) {
|
|
1380
|
+
lines.push(`${arc.id} at=${durationText(arc.atTime, unit, result.precision)} resources=${[...arc.resources].map(([id, units]) => `${id}=${units}`).join(",")}`);
|
|
1381
|
+
}
|
|
1382
|
+
if (resource.resourceArcs.length === 0)
|
|
1383
|
+
lines.push("-");
|
|
1384
|
+
lines.push("", "RESOURCE CRITICAL", `TASKS ${resource.scheduleCritical.taskIds.join(", ") || "-"}`, `RESOURCE ARCS ${resource.scheduleCritical.resourceArcIds.join(", ") || "-"}`, `REPRESENTATIVE PATH ${resource.scheduleCritical.representativePath.taskIds.join(" -> ") || "(complete)"}`, `PATH COUNT ${resource.scheduleCritical.pathCount.toString()}${resource.scheduleCritical.pathsTruncated ? " (truncated)" : ""}`, "", "RESOURCE UTILIZATION", "ID CAPACITY AMOUNT_TIME UTILIZATION PEAK LAST_RELEASE");
|
|
1385
|
+
for (const statistic of resource.resources) {
|
|
1386
|
+
lines.push([
|
|
1387
|
+
statistic.id,
|
|
1388
|
+
statistic.capacity.toString(),
|
|
1389
|
+
durationText(statistic.amountTime, unit, result.precision),
|
|
1390
|
+
formatDecimal(statistic.utilization, result.precision),
|
|
1391
|
+
statistic.peakUsage.toString(),
|
|
1392
|
+
durationText(statistic.lastRelease, unit, result.precision),
|
|
1393
|
+
].join(" "));
|
|
1394
|
+
}
|
|
1395
|
+
if (resource.resources.length === 0)
|
|
1396
|
+
lines.push("-");
|
|
1397
|
+
}
|
|
1398
|
+
return `${lines.join("\n")}\n`;
|
|
1399
|
+
}
|
|
1400
|
+
async function runAnalyze(args) {
|
|
1401
|
+
const parsed = parseOptions(args, new Set([
|
|
1402
|
+
"schedule",
|
|
1403
|
+
"max-paths",
|
|
1404
|
+
"precision",
|
|
1405
|
+
"max-diagnostics",
|
|
1406
|
+
"format",
|
|
1407
|
+
"color",
|
|
1408
|
+
]), new Set(["warnings-as-errors"]), new Set(["capacity"]));
|
|
1409
|
+
if (parsed.positionals.length !== 1) {
|
|
1410
|
+
throw new UsageError("dag analyze requires exactly one <file>");
|
|
1411
|
+
}
|
|
1412
|
+
const format = outputFormat(parsed.values.get("format"));
|
|
1413
|
+
const color = colorMode(parsed.values.get("color"), format);
|
|
1414
|
+
const mode = analysisMode(parsed.values.get("schedule"));
|
|
1415
|
+
const maxPaths = boundedInteger(parsed.values.get("max-paths"), "max-paths", 1, 0, 1000);
|
|
1416
|
+
const precision = boundedInteger(parsed.values.get("precision"), "precision", 3, 0, 9);
|
|
1417
|
+
const maxDiagnostics = boundedInteger(parsed.values.get("max-diagnostics"), "max-diagnostics", 100, 1, 1000);
|
|
1418
|
+
const overrides = capacityOverrides(parsed.repeatedValues.get("capacity") ?? []);
|
|
1419
|
+
const sourceOperand = parsed.positionals[0];
|
|
1420
|
+
const source = sourceOperand === "-" ? "<stdin>" : sourceOperand;
|
|
1421
|
+
let input;
|
|
1422
|
+
try {
|
|
1423
|
+
input = await readDocument(sourceOperand);
|
|
1424
|
+
}
|
|
1425
|
+
catch (error) {
|
|
1426
|
+
return cliError(error instanceof Error ? error : new Error(String(error)), 3, "dag.analyze", format === "json");
|
|
1427
|
+
}
|
|
1428
|
+
const result = analyzeDocument(input.text, {
|
|
1429
|
+
mode,
|
|
1430
|
+
capacityOverrides: overrides,
|
|
1431
|
+
maxPaths,
|
|
1432
|
+
precision,
|
|
1433
|
+
maxDiagnostics,
|
|
1434
|
+
});
|
|
1435
|
+
const warningFailure = parsed.flags.has("warnings-as-errors") &&
|
|
1436
|
+
(result.diagnosticsTruncated ||
|
|
1437
|
+
result.diagnostics.some((diagnostic) => diagnostic.severity === "warning"));
|
|
1438
|
+
const ok = result.ok && !warningFailure;
|
|
1439
|
+
if (format === "json") {
|
|
1440
|
+
writeJson({
|
|
1441
|
+
schema_version: "Perttool.AnalysisResult.v2",
|
|
1442
|
+
tool_version: TOOL_VERSION,
|
|
1443
|
+
operation: "dag.analyze",
|
|
1444
|
+
ok,
|
|
1445
|
+
document_id: result.documentId,
|
|
1446
|
+
source,
|
|
1447
|
+
source_digest: input.digest,
|
|
1448
|
+
diagnostics: result.diagnostics.map(jsonDiagnostic),
|
|
1449
|
+
diagnostics_truncated: result.diagnosticsTruncated,
|
|
1450
|
+
mode: result.mode,
|
|
1451
|
+
precision: result.precision,
|
|
1452
|
+
...(result.durationUnit === null
|
|
1453
|
+
? {}
|
|
1454
|
+
: {
|
|
1455
|
+
duration_unit: result.durationUnit,
|
|
1456
|
+
critical_epsilon: rationalJson(result.criticalEpsilon, result.durationUnit, result.precision),
|
|
1457
|
+
velocity: velocityJson(result.velocity, result.precision),
|
|
1458
|
+
velocity_forecast: analysisVelocityForecastJson(result),
|
|
1459
|
+
}),
|
|
1460
|
+
precedence: result.precedence === null || result.durationUnit === null
|
|
1461
|
+
? null
|
|
1462
|
+
: precedenceJson(result.precedence, result.durationUnit, result.precision),
|
|
1463
|
+
resource: result.resource === null || result.durationUnit === null
|
|
1464
|
+
? null
|
|
1465
|
+
: resourceJson(result.resource, result.durationUnit, result.precision),
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
else {
|
|
1469
|
+
if (ok)
|
|
1470
|
+
process.stdout.write(renderAnalysisText(result));
|
|
1471
|
+
for (const diagnostic of result.diagnostics) {
|
|
1472
|
+
process.stderr.write(`${renderDiagnostic(diagnostic, source, color)}\n`);
|
|
1473
|
+
}
|
|
1474
|
+
if (result.diagnosticsTruncated) {
|
|
1475
|
+
process.stderr.write(`DIAGNOSTICS_TRUNCATED true limit=${maxDiagnostics}\n`);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
return ok ? 0 : 1;
|
|
1479
|
+
}
|
|
1480
|
+
function conversionLossJson(loss) {
|
|
1481
|
+
return {
|
|
1482
|
+
code: loss.code,
|
|
1483
|
+
severity: loss.severity,
|
|
1484
|
+
message: loss.message,
|
|
1485
|
+
element_id: loss.elementId,
|
|
1486
|
+
span: loss.span === null ? null : jsonSpan(loss.span),
|
|
1487
|
+
lossy: loss.lossy,
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
function renderConversionLoss(loss, source, color) {
|
|
1491
|
+
return renderDiagnostic({
|
|
1492
|
+
code: loss.code,
|
|
1493
|
+
severity: loss.severity,
|
|
1494
|
+
message: loss.message,
|
|
1495
|
+
...(loss.elementId === null ? {} : { entityId: loss.elementId }),
|
|
1496
|
+
...(loss.span === null ? {} : { span: loss.span }),
|
|
1497
|
+
helpTopic: "mermaid",
|
|
1498
|
+
}, source, color);
|
|
1499
|
+
}
|
|
1500
|
+
async function runRender(args) {
|
|
1501
|
+
const parsed = parseOptions(args, new Set([
|
|
1502
|
+
"to",
|
|
1503
|
+
"profile",
|
|
1504
|
+
"analysis",
|
|
1505
|
+
"out",
|
|
1506
|
+
"max-diagnostics",
|
|
1507
|
+
"format",
|
|
1508
|
+
"color",
|
|
1509
|
+
]), new Set(["strict-loss", "warnings-as-errors"]), new Set(["capacity"]));
|
|
1510
|
+
if (parsed.positionals.length !== 1) {
|
|
1511
|
+
throw new UsageError("dag render requires exactly one <file>");
|
|
1512
|
+
}
|
|
1513
|
+
enumOption(requiredOption(parsed, "to"), "to", new Set(["mermaid"]));
|
|
1514
|
+
const profile = enumOption(parsed.values.get("profile"), "profile", new Set(["perttool", "plain"])) ?? "perttool";
|
|
1515
|
+
const analysis = enumOption(parsed.values.get("analysis"), "analysis", new Set(["none", "precedence", "resource", "both"])) ?? "none";
|
|
1516
|
+
const format = outputFormat(parsed.values.get("format"));
|
|
1517
|
+
const color = colorMode(parsed.values.get("color"), format);
|
|
1518
|
+
const maxDiagnostics = boundedInteger(parsed.values.get("max-diagnostics"), "max-diagnostics", 100, 1, 1000);
|
|
1519
|
+
const overrides = capacityOverrides(parsed.repeatedValues.get("capacity") ?? []);
|
|
1520
|
+
if (overrides.size > 0 && analysis !== "resource" && analysis !== "both") {
|
|
1521
|
+
throw new UsageError("--capacity requires --analysis resource or both");
|
|
1522
|
+
}
|
|
1523
|
+
const sourceOperand = parsed.positionals[0];
|
|
1524
|
+
const source = sourceOperand === "-" ? "<stdin>" : sourceOperand;
|
|
1525
|
+
const out = parsed.values.get("out") ?? null;
|
|
1526
|
+
if (out !== null && out.length === 0) {
|
|
1527
|
+
throw new UsageError("--out path must not be empty");
|
|
1528
|
+
}
|
|
1529
|
+
let input;
|
|
1530
|
+
try {
|
|
1531
|
+
input = await readDocument(sourceOperand);
|
|
1532
|
+
}
|
|
1533
|
+
catch (error) {
|
|
1534
|
+
return cliError(error instanceof Error ? error : new Error(String(error)), 3, "dag.render", format === "json");
|
|
1535
|
+
}
|
|
1536
|
+
const result = exportMermaid(input.text, {
|
|
1537
|
+
profile,
|
|
1538
|
+
analysis,
|
|
1539
|
+
capacityOverrides: overrides,
|
|
1540
|
+
maxDiagnostics,
|
|
1541
|
+
});
|
|
1542
|
+
const warningFailure = parsed.flags.has("warnings-as-errors") &&
|
|
1543
|
+
(result.diagnosticsTruncated ||
|
|
1544
|
+
result.diagnostics.some((diagnostic) => diagnostic.severity === "warning"));
|
|
1545
|
+
const strictFailure = parsed.flags.has("strict-loss") &&
|
|
1546
|
+
result.lossReport.records.some((record) => record.lossy);
|
|
1547
|
+
const ok = result.ok && !warningFailure && !strictFailure;
|
|
1548
|
+
const exposeArtifact = result.ok && !strictFailure;
|
|
1549
|
+
let writeResult = null;
|
|
1550
|
+
if (ok && out !== null) {
|
|
1551
|
+
try {
|
|
1552
|
+
writeResult = await createArtifactFile(out, result.artifact);
|
|
1553
|
+
}
|
|
1554
|
+
catch (error) {
|
|
1555
|
+
return writeFailureExit(error, "dag.render", format === "json");
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
if (format === "json") {
|
|
1559
|
+
writeJson({
|
|
1560
|
+
schema_version: "Perttool.ExportResult.v1",
|
|
1561
|
+
tool_version: TOOL_VERSION,
|
|
1562
|
+
operation: "dag.render",
|
|
1563
|
+
ok,
|
|
1564
|
+
document_id: result.documentId,
|
|
1565
|
+
source,
|
|
1566
|
+
source_digest: input.digest,
|
|
1567
|
+
diagnostics: result.diagnostics.map(jsonDiagnostic),
|
|
1568
|
+
diagnostics_truncated: result.diagnosticsTruncated,
|
|
1569
|
+
artifact_format: "mermaid",
|
|
1570
|
+
profile: result.profile,
|
|
1571
|
+
analysis: result.analysis,
|
|
1572
|
+
capacity_overrides: [...result.capacityOverrides]
|
|
1573
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
1574
|
+
.map(([resourceId, capacity]) => ({ resource_id: resourceId, capacity })),
|
|
1575
|
+
artifact: exposeArtifact ? result.artifact : null,
|
|
1576
|
+
artifact_digest: exposeArtifact ? result.artifactDigest : null,
|
|
1577
|
+
loss_report: {
|
|
1578
|
+
lossless: result.lossReport.lossless,
|
|
1579
|
+
records: result.lossReport.records.map(conversionLossJson),
|
|
1580
|
+
},
|
|
1581
|
+
generated_ids: [],
|
|
1582
|
+
write: {
|
|
1583
|
+
mode: out === null ? "preview" : "out",
|
|
1584
|
+
target: out,
|
|
1585
|
+
written: writeResult?.written ?? false,
|
|
1586
|
+
},
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
else {
|
|
1590
|
+
if (ok) {
|
|
1591
|
+
if (writeResult === null) {
|
|
1592
|
+
process.stdout.write(result.artifact);
|
|
1593
|
+
}
|
|
1594
|
+
else {
|
|
1595
|
+
process.stderr.write(renderWriteSummary("dag.render", writeResult));
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
for (const diagnostic of result.diagnostics) {
|
|
1599
|
+
process.stderr.write(`${renderDiagnostic(diagnostic, source, color)}\n`);
|
|
1600
|
+
}
|
|
1601
|
+
for (const loss of result.lossReport.records) {
|
|
1602
|
+
process.stderr.write(`${renderConversionLoss(loss, source, color)}\n`);
|
|
1603
|
+
}
|
|
1604
|
+
if (result.diagnosticsTruncated) {
|
|
1605
|
+
process.stderr.write(`DIAGNOSTICS_TRUNCATED true limit=${maxDiagnostics}\n`);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
return strictFailure ? 4 : ok ? 0 : 1;
|
|
1609
|
+
}
|
|
1610
|
+
async function runImport(args) {
|
|
1611
|
+
const parsed = parseOptions(args, new Set(["from", "out", "max-diagnostics", "format", "color"]), new Set(["strict-loss", "warnings-as-errors"]));
|
|
1612
|
+
if (parsed.positionals.length !== 1) {
|
|
1613
|
+
throw new UsageError("dag import requires exactly one <file>");
|
|
1614
|
+
}
|
|
1615
|
+
enumOption(requiredOption(parsed, "from"), "from", new Set(["mermaid"]));
|
|
1616
|
+
const format = outputFormat(parsed.values.get("format"));
|
|
1617
|
+
const color = colorMode(parsed.values.get("color"), format);
|
|
1618
|
+
const maxDiagnostics = boundedInteger(parsed.values.get("max-diagnostics"), "max-diagnostics", 100, 1, 1000);
|
|
1619
|
+
const sourceOperand = parsed.positionals[0];
|
|
1620
|
+
const source = sourceOperand === "-" ? "<stdin>" : sourceOperand;
|
|
1621
|
+
const out = parsed.values.get("out") ?? null;
|
|
1622
|
+
if (out !== null && out.length === 0) {
|
|
1623
|
+
throw new UsageError("--out path must not be empty");
|
|
1624
|
+
}
|
|
1625
|
+
let input;
|
|
1626
|
+
try {
|
|
1627
|
+
input = await readDocument(sourceOperand);
|
|
1628
|
+
}
|
|
1629
|
+
catch (error) {
|
|
1630
|
+
return cliError(error instanceof Error ? error : new Error(String(error)), 3, "dag.import", format === "json");
|
|
1631
|
+
}
|
|
1632
|
+
const result = importMermaid(input.text, { maxDiagnostics });
|
|
1633
|
+
const warningFailure = parsed.flags.has("warnings-as-errors") &&
|
|
1634
|
+
(result.diagnosticsTruncated ||
|
|
1635
|
+
result.diagnostics.some((diagnostic) => diagnostic.severity === "warning"));
|
|
1636
|
+
const strictFailure = parsed.flags.has("strict-loss") &&
|
|
1637
|
+
result.lossReport.records.some((record) => record.lossy);
|
|
1638
|
+
const ok = result.ok && !warningFailure && !strictFailure;
|
|
1639
|
+
const exposeArtifact = result.ok && !strictFailure;
|
|
1640
|
+
let writeResult = null;
|
|
1641
|
+
if (ok && out !== null) {
|
|
1642
|
+
try {
|
|
1643
|
+
writeResult = await createDocumentFile(out, result.artifact);
|
|
1644
|
+
}
|
|
1645
|
+
catch (error) {
|
|
1646
|
+
return writeFailureExit(error, "dag.import", format === "json");
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
if (format === "json") {
|
|
1650
|
+
writeJson({
|
|
1651
|
+
schema_version: "Perttool.ImportResult.v1",
|
|
1652
|
+
tool_version: TOOL_VERSION,
|
|
1653
|
+
operation: "dag.import",
|
|
1654
|
+
ok,
|
|
1655
|
+
document_id: result.documentId,
|
|
1656
|
+
source,
|
|
1657
|
+
source_digest: input.digest,
|
|
1658
|
+
diagnostics: result.diagnostics.map(jsonDiagnostic),
|
|
1659
|
+
diagnostics_truncated: result.diagnosticsTruncated,
|
|
1660
|
+
artifact_format: "pert",
|
|
1661
|
+
profile: result.profile,
|
|
1662
|
+
analysis: result.analysis,
|
|
1663
|
+
capacity_overrides: [...result.capacityOverrides]
|
|
1664
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
1665
|
+
.map(([resourceId, capacity]) => ({ resource_id: resourceId, capacity })),
|
|
1666
|
+
artifact: exposeArtifact ? result.artifact : null,
|
|
1667
|
+
artifact_digest: exposeArtifact ? result.artifactDigest : null,
|
|
1668
|
+
loss_report: {
|
|
1669
|
+
lossless: result.lossReport.lossless,
|
|
1670
|
+
records: result.lossReport.records.map(conversionLossJson),
|
|
1671
|
+
},
|
|
1672
|
+
generated_ids: result.generatedIds.map(({ sourceElement, generatedId }) => ({
|
|
1673
|
+
source_element: sourceElement,
|
|
1674
|
+
generated_id: generatedId,
|
|
1675
|
+
})),
|
|
1676
|
+
write: {
|
|
1677
|
+
mode: out === null ? "preview" : "out",
|
|
1678
|
+
target: out,
|
|
1679
|
+
written: writeResult?.written ?? false,
|
|
1680
|
+
},
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
else {
|
|
1684
|
+
if (ok) {
|
|
1685
|
+
if (writeResult === null) {
|
|
1686
|
+
process.stdout.write(result.artifact);
|
|
1687
|
+
}
|
|
1688
|
+
else {
|
|
1689
|
+
process.stderr.write(renderWriteSummary("dag.import", writeResult));
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
for (const diagnostic of result.diagnostics) {
|
|
1693
|
+
process.stderr.write(`${renderDiagnostic(diagnostic, source, color)}\n`);
|
|
1694
|
+
}
|
|
1695
|
+
for (const conversionLoss of result.lossReport.records) {
|
|
1696
|
+
process.stderr.write(`${renderConversionLoss(conversionLoss, source, color)}\n`);
|
|
1697
|
+
}
|
|
1698
|
+
if (result.diagnosticsTruncated) {
|
|
1699
|
+
process.stderr.write(`DIAGNOSTICS_TRUNCATED true limit=${maxDiagnostics}\n`);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
return strictFailure ? 4 : ok ? 0 : 1;
|
|
1703
|
+
}
|
|
1704
|
+
function explanationJson(node) {
|
|
1705
|
+
return {
|
|
1706
|
+
milestone_id: node.milestoneId,
|
|
1707
|
+
reached: node.reached,
|
|
1708
|
+
unsatisfied_edges: node.unsatisfiedEdges.map((edge) => ({
|
|
1709
|
+
edge_id: edge.edgeId,
|
|
1710
|
+
kind: edge.kind,
|
|
1711
|
+
status: edge.status,
|
|
1712
|
+
source_milestone_id: edge.sourceMilestoneId,
|
|
1713
|
+
source_reached: edge.sourceReached,
|
|
1714
|
+
})),
|
|
1715
|
+
children: node.children.map(explanationJson),
|
|
1716
|
+
truncated: node.truncated,
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
function nextJson(result) {
|
|
1720
|
+
const unit = result.durationUnit;
|
|
1721
|
+
return {
|
|
1722
|
+
precision: result.precision,
|
|
1723
|
+
duration_unit: unit,
|
|
1724
|
+
velocity: velocityJson(result.velocity, result.precision),
|
|
1725
|
+
velocity_forecast: result.velocityForecast === null
|
|
1726
|
+
? null
|
|
1727
|
+
: {
|
|
1728
|
+
qualifier: result.velocityForecast.qualifier,
|
|
1729
|
+
source_unit: result.velocityForecast.sourceUnit,
|
|
1730
|
+
target_unit: result.velocityForecast.targetUnit,
|
|
1731
|
+
},
|
|
1732
|
+
capacity_overrides: [...result.capacityOverrides]
|
|
1733
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
1734
|
+
.map(([resourceId, capacity]) => ({ resource_id: resourceId, capacity })),
|
|
1735
|
+
recommendation: recommendationAnalysisToJson(result.recommendation),
|
|
1736
|
+
groups: {
|
|
1737
|
+
active: result.groups.active,
|
|
1738
|
+
ready: result.groups.ready,
|
|
1739
|
+
runnable_now: result.groups.runnableNow,
|
|
1740
|
+
blocked_now: result.groups.blockedNow,
|
|
1741
|
+
upcoming: result.groups.upcoming,
|
|
1742
|
+
},
|
|
1743
|
+
tasks: result.tasks.map((task) => ({
|
|
1744
|
+
id: task.id,
|
|
1745
|
+
title: task.title,
|
|
1746
|
+
status: task.status,
|
|
1747
|
+
classification: task.classification,
|
|
1748
|
+
runnable_now: task.runnableNow,
|
|
1749
|
+
priority: task.priority,
|
|
1750
|
+
owner: task.owner,
|
|
1751
|
+
blocked_reason: task.blockedReason,
|
|
1752
|
+
expected: rationalJson(task.expected, unit, result.precision),
|
|
1753
|
+
total_float: rationalJson(task.totalFloat, unit, result.precision),
|
|
1754
|
+
earliest_start: rationalJson(task.earliestStart, unit, result.precision),
|
|
1755
|
+
forecast_expected: task.forecastExpected === null || result.velocityForecast === null
|
|
1756
|
+
? null
|
|
1757
|
+
: rationalJson(task.forecastExpected, result.velocityForecast.targetUnit, result.precision),
|
|
1758
|
+
forecast_total_float: task.forecastTotalFloat === null || result.velocityForecast === null
|
|
1759
|
+
? null
|
|
1760
|
+
: rationalJson(task.forecastTotalFloat, result.velocityForecast.targetUnit, result.precision),
|
|
1761
|
+
forecast_earliest_start: task.forecastEarliestStart === null || result.velocityForecast === null
|
|
1762
|
+
? null
|
|
1763
|
+
: rationalJson(task.forecastEarliestStart, result.velocityForecast.targetUnit, result.precision),
|
|
1764
|
+
precedence_critical: task.precedenceCritical,
|
|
1765
|
+
schedule_critical: task.scheduleCritical,
|
|
1766
|
+
requirements: task.requirements.map((requirement) => ({
|
|
1767
|
+
resource_id: requirement.resourceId,
|
|
1768
|
+
units: requirement.units,
|
|
1769
|
+
})),
|
|
1770
|
+
resource_rejections: task.resourceRejections.map((rejection) => ({
|
|
1771
|
+
resource_id: rejection.resourceId,
|
|
1772
|
+
capacity: rejection.capacity,
|
|
1773
|
+
active_usage: rejection.activeUsage,
|
|
1774
|
+
earlier_selected_usage: rejection.earlierSelectedUsage,
|
|
1775
|
+
used_before_decision: rejection.usedBeforeDecision,
|
|
1776
|
+
required: rejection.required,
|
|
1777
|
+
available: rejection.available,
|
|
1778
|
+
deficit: rejection.deficit,
|
|
1779
|
+
active_task_ids: rejection.activeTaskIds,
|
|
1780
|
+
earlier_selected_task_ids: rejection.earlierSelectedTaskIds,
|
|
1781
|
+
})),
|
|
1782
|
+
explanation: task.explanation.map(explanationJson),
|
|
1783
|
+
})),
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
function recommendationParameterTaskIds(description, name) {
|
|
1787
|
+
const parameter = description.parameters.find((item) => item.name === name);
|
|
1788
|
+
if (parameter === undefined)
|
|
1789
|
+
return [];
|
|
1790
|
+
if (parameter.value.type === "entity" && parameter.value.value.kind === "task") {
|
|
1791
|
+
return [parameter.value.value.id];
|
|
1792
|
+
}
|
|
1793
|
+
if (parameter.value.type !== "list" && parameter.value.type !== "set")
|
|
1794
|
+
return [];
|
|
1795
|
+
return parameter.value.items.flatMap((item) => item.type === "entity" && item.value.kind === "task" ? [item.value.id] : []);
|
|
1796
|
+
}
|
|
1797
|
+
function recommendationParameterEntityId(description, name) {
|
|
1798
|
+
const parameter = description.parameters.find((item) => item.name === name);
|
|
1799
|
+
return parameter?.value.type === "entity" ? parameter.value.value.id : null;
|
|
1800
|
+
}
|
|
1801
|
+
function renderRecommendationSummary(recommendation) {
|
|
1802
|
+
const steps = new Map(recommendation.decisionSteps.map((step) => [step.id, step]));
|
|
1803
|
+
const reasons = new Map(recommendation.reasonOccurrences.map((reason) => [reason.id, reason]));
|
|
1804
|
+
const descriptions = new Map(recommendation.descriptions.map((description) => [description.id, description]));
|
|
1805
|
+
const lines = [
|
|
1806
|
+
"RECOMMENDATION",
|
|
1807
|
+
`ALGORITHM ${recommendation.algorithm.id}@${recommendation.algorithm.version} optimal=${recommendation.algorithm.optimal}`,
|
|
1808
|
+
'EXPLANATION detail=summary complete=false machine_trace="--format json"',
|
|
1809
|
+
`RECOMMENDED SET ${recommendation.recommendedTaskIds.join(",") || "-"}`,
|
|
1810
|
+
];
|
|
1811
|
+
const sections = [
|
|
1812
|
+
["RECOMMENDED START", "recommended"],
|
|
1813
|
+
["ALLOWED ADDITIONAL START", "allowed"],
|
|
1814
|
+
["DEFERRED START", "deferred"],
|
|
1815
|
+
["DISCOURAGED START", "discouraged"],
|
|
1816
|
+
];
|
|
1817
|
+
for (const [title, tier] of sections) {
|
|
1818
|
+
lines.push("", title);
|
|
1819
|
+
const decisions = recommendation.taskDecisions.filter((decision) => decision.tier === tier);
|
|
1820
|
+
if (decisions.length === 0) {
|
|
1821
|
+
lines.push("-");
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
for (const decision of decisions) {
|
|
1825
|
+
const description = descriptions.get(decision.summaryDescriptionId);
|
|
1826
|
+
const primaryReason = reasons.get(description.sourceReasonIds[0]);
|
|
1827
|
+
const rule = recommendationParameterEntityId(description, "decisive_rule_id") ??
|
|
1828
|
+
steps.get(primaryReason.decisionStepId).rule.id;
|
|
1829
|
+
const blockerIds = [
|
|
1830
|
+
...recommendationParameterTaskIds(description, "higher_priority_task_ids"),
|
|
1831
|
+
...recommendationParameterTaskIds(description, "active_blocker_task_ids"),
|
|
1832
|
+
];
|
|
1833
|
+
lines.push(`${decision.subjectTaskId} tier=${decision.tier} rule=${rule} higher_priority=${decision.primaryHigherPriorityTaskId ?? "-"} blockers=${blockerIds.join(",") || "-"}`, ` reason=${primaryReason.code}`, ` why: ${description.text}`);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
return lines;
|
|
1837
|
+
}
|
|
1838
|
+
function renderNextTask(task, unit, precision, forecastUnit) {
|
|
1839
|
+
const resources = task.requirements
|
|
1840
|
+
.map((requirement) => `${requirement.resourceId}=${requirement.units}`)
|
|
1841
|
+
.join(",");
|
|
1842
|
+
return [
|
|
1843
|
+
task.id,
|
|
1844
|
+
`priority=${task.priority}`,
|
|
1845
|
+
`expected=${durationText(task.expected, unit, precision)}`,
|
|
1846
|
+
...(task.forecastExpected === null || forecastUnit === null
|
|
1847
|
+
? []
|
|
1848
|
+
: [`forecast=${durationText(task.forecastExpected, forecastUnit, precision)}`]),
|
|
1849
|
+
`TF=${durationText(task.totalFloat, unit, precision)}`,
|
|
1850
|
+
`precedence_critical=${task.precedenceCritical}`,
|
|
1851
|
+
`schedule_critical=${task.scheduleCritical}`,
|
|
1852
|
+
`owner=${task.owner ?? "-"}`,
|
|
1853
|
+
`resources=${resources || "-"}`,
|
|
1854
|
+
].join(" ");
|
|
1855
|
+
}
|
|
1856
|
+
function renderExplanation(node, indent) {
|
|
1857
|
+
const edges = node.unsatisfiedEdges.map((edge) => edge.edgeId).join(",") || "-";
|
|
1858
|
+
const lines = [
|
|
1859
|
+
`${indent}waiting milestone=${node.milestoneId} unsatisfied=${edges} truncated=${node.truncated}`,
|
|
1860
|
+
];
|
|
1861
|
+
for (const child of node.children)
|
|
1862
|
+
lines.push(...renderExplanation(child, `${indent} `));
|
|
1863
|
+
return lines;
|
|
1864
|
+
}
|
|
1865
|
+
function renderNextText(result) {
|
|
1866
|
+
const unit = result.durationUnit;
|
|
1867
|
+
const taskById = new Map(result.tasks.map((task) => [task.id, task]));
|
|
1868
|
+
const lines = [
|
|
1869
|
+
`PERTTOOL NEXT ${result.documentId ?? "-"}`,
|
|
1870
|
+
`VELOCITY ${result.velocity === null ? "-" : velocityText(result.velocity, result.precision)}`,
|
|
1871
|
+
`VELOCITY_FORECAST_UNIT ${result.velocityForecast?.targetUnit ?? "-"}`,
|
|
1872
|
+
];
|
|
1873
|
+
lines.push("", ...renderRecommendationSummary(result.recommendation));
|
|
1874
|
+
const section = (title, ids, details = "none") => {
|
|
1875
|
+
lines.push("", title);
|
|
1876
|
+
if (ids.length === 0) {
|
|
1877
|
+
lines.push("-");
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
for (const id of ids) {
|
|
1881
|
+
const task = taskById.get(id);
|
|
1882
|
+
lines.push(renderNextTask(task, unit, result.precision, result.velocityForecast?.targetUnit ?? null));
|
|
1883
|
+
if (details === "rejection") {
|
|
1884
|
+
for (const rejection of task.resourceRejections) {
|
|
1885
|
+
const occupants = [
|
|
1886
|
+
...rejection.activeTaskIds,
|
|
1887
|
+
...rejection.earlierSelectedTaskIds,
|
|
1888
|
+
].join(",") || "-";
|
|
1889
|
+
lines.push(` ${rejection.resourceId} capacity=${rejection.capacity} used=${rejection.usedBeforeDecision} required=${rejection.required} available=${rejection.available} deficit=${rejection.deficit} occupants=${occupants}`);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
else if (details === "blocked") {
|
|
1893
|
+
lines.push(` blocked_reason=${task.blockedReason ?? "-"}`);
|
|
1894
|
+
}
|
|
1895
|
+
else if (details === "explanation") {
|
|
1896
|
+
for (const explanation of task.explanation) {
|
|
1897
|
+
lines.push(...renderExplanation(explanation, " "));
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
section("ACTIVE", result.groups.active);
|
|
1903
|
+
section("RUNNABLE NOW", result.groups.runnableNow);
|
|
1904
|
+
section("READY / WAITING RESOURCE", result.groups.ready.filter((id) => !result.groups.runnableNow.includes(id)), "rejection");
|
|
1905
|
+
section("BLOCKED NOW", result.groups.blockedNow, "blocked");
|
|
1906
|
+
section("UPCOMING", result.groups.upcoming, "explanation");
|
|
1907
|
+
return `${lines.join("\n")}\n`;
|
|
1908
|
+
}
|
|
1909
|
+
async function runNext(args) {
|
|
1910
|
+
const parsed = parseOptions(args, new Set(["explain-depth", "precision", "max-diagnostics", "format", "color"]), new Set(["warnings-as-errors"]), new Set(["capacity"]));
|
|
1911
|
+
if (parsed.positionals.length !== 1) {
|
|
1912
|
+
throw new UsageError("dag next requires exactly one <file>");
|
|
1913
|
+
}
|
|
1914
|
+
const format = outputFormat(parsed.values.get("format"));
|
|
1915
|
+
const color = colorMode(parsed.values.get("color"), format);
|
|
1916
|
+
const explainDepth = boundedInteger(parsed.values.get("explain-depth"), "explain-depth", 1, 0, 32);
|
|
1917
|
+
const precision = boundedInteger(parsed.values.get("precision"), "precision", 3, 0, 9);
|
|
1918
|
+
const maxDiagnostics = boundedInteger(parsed.values.get("max-diagnostics"), "max-diagnostics", 100, 1, 1000);
|
|
1919
|
+
const overrides = capacityOverrides(parsed.repeatedValues.get("capacity") ?? []);
|
|
1920
|
+
const sourceOperand = parsed.positionals[0];
|
|
1921
|
+
const source = sourceOperand === "-" ? "<stdin>" : sourceOperand;
|
|
1922
|
+
let input;
|
|
1923
|
+
try {
|
|
1924
|
+
input = await readDocument(sourceOperand);
|
|
1925
|
+
}
|
|
1926
|
+
catch (error) {
|
|
1927
|
+
return cliError(error instanceof Error ? error : new Error(String(error)), 3, "dag.next", format === "json");
|
|
1928
|
+
}
|
|
1929
|
+
const result = selectNextTasks(input.text, {
|
|
1930
|
+
capacityOverrides: overrides,
|
|
1931
|
+
explainDepth,
|
|
1932
|
+
precision,
|
|
1933
|
+
maxDiagnostics,
|
|
1934
|
+
sourceDigest: input.digest,
|
|
1935
|
+
});
|
|
1936
|
+
const invariantExitCode = recommendationInvariantExitCode(result.diagnostics);
|
|
1937
|
+
if (invariantExitCode !== null) {
|
|
1938
|
+
if (format === "json") {
|
|
1939
|
+
writeJson({
|
|
1940
|
+
schema_version: "Perttool.CliError.v1",
|
|
1941
|
+
tool_version: TOOL_VERSION,
|
|
1942
|
+
operation: "dag.next",
|
|
1943
|
+
ok: false,
|
|
1944
|
+
diagnostics: result.diagnostics.map(jsonDiagnostic),
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
else {
|
|
1948
|
+
for (const diagnostic of result.diagnostics) {
|
|
1949
|
+
process.stderr.write(`${renderDiagnostic(diagnostic, source, color)}\n`);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
return invariantExitCode;
|
|
1953
|
+
}
|
|
1954
|
+
const warningFailure = parsed.flags.has("warnings-as-errors") &&
|
|
1955
|
+
(result.diagnosticsTruncated ||
|
|
1956
|
+
result.diagnostics.some((diagnostic) => diagnostic.severity === "warning"));
|
|
1957
|
+
const ok = result.ok && !warningFailure;
|
|
1958
|
+
if (format === "json") {
|
|
1959
|
+
writeJson({
|
|
1960
|
+
schema_version: "Perttool.NextResult.v3",
|
|
1961
|
+
recommendation_interface_version: 1,
|
|
1962
|
+
tool_version: TOOL_VERSION,
|
|
1963
|
+
operation: "dag.next",
|
|
1964
|
+
ok,
|
|
1965
|
+
document_id: result.documentId,
|
|
1966
|
+
source,
|
|
1967
|
+
source_digest: input.digest,
|
|
1968
|
+
diagnostics: result.diagnostics.map(jsonDiagnostic),
|
|
1969
|
+
diagnostics_truncated: result.diagnosticsTruncated,
|
|
1970
|
+
...(result.durationUnit === null ? {} : nextJson(result)),
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
else {
|
|
1974
|
+
if (ok)
|
|
1975
|
+
process.stdout.write(renderNextText(result));
|
|
1976
|
+
for (const diagnostic of result.diagnostics) {
|
|
1977
|
+
process.stderr.write(`${renderDiagnostic(diagnostic, source, color)}\n`);
|
|
1978
|
+
}
|
|
1979
|
+
if (result.diagnosticsTruncated) {
|
|
1980
|
+
process.stderr.write(`DIAGNOSTICS_TRUNCATED true limit=${maxDiagnostics}\n`);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
return ok ? 0 : 1;
|
|
1984
|
+
}
|
|
1985
|
+
function renderHelpText(result) {
|
|
1986
|
+
const lines = [result.title, "", result.summary];
|
|
1987
|
+
if (result.topics.length > 0) {
|
|
1988
|
+
lines.push("", "Topics:");
|
|
1989
|
+
for (const topic of result.topics)
|
|
1990
|
+
lines.push(` ${topic.id.padEnd(12)} ${topic.summary}`);
|
|
1991
|
+
}
|
|
1992
|
+
for (const section of result.sections) {
|
|
1993
|
+
lines.push("", section.title, section.body);
|
|
1994
|
+
}
|
|
1995
|
+
if (result.syntax.length > 0)
|
|
1996
|
+
lines.push("", "Syntax:", ...result.syntax.map((line) => ` ${line}`));
|
|
1997
|
+
if (result.examples.length > 0) {
|
|
1998
|
+
lines.push("", "Examples:", ...result.examples.map((example) => ` ${example.id}: ${example.text}`));
|
|
1999
|
+
}
|
|
2000
|
+
if (result.related.length > 0)
|
|
2001
|
+
lines.push("", `Related: ${result.related.join(", ")}`);
|
|
2002
|
+
return `${lines.join("\n")}\n`;
|
|
2003
|
+
}
|
|
2004
|
+
function runHelp(args) {
|
|
2005
|
+
const parsed = parseOptions(args, new Set(["level", "format", "color"]), new Set());
|
|
2006
|
+
if (parsed.positionals.length > 2) {
|
|
2007
|
+
throw new UsageError("dsl help accepts at most <topic> <subtopic>");
|
|
2008
|
+
}
|
|
2009
|
+
const format = outputFormat(parsed.values.get("format"));
|
|
2010
|
+
colorMode(parsed.values.get("color"), format);
|
|
2011
|
+
const topicId = parsed.positionals.length === 0 ? null : parsed.positionals.join(".");
|
|
2012
|
+
const level = helpLevel(parsed.values.get("level"), topicId !== null);
|
|
2013
|
+
const result = getHelp(topicId, level);
|
|
2014
|
+
if (format === "json") {
|
|
2015
|
+
writeJson({
|
|
2016
|
+
schema_version: "Perttool.HelpResult.v1",
|
|
2017
|
+
tool_version: TOOL_VERSION,
|
|
2018
|
+
operation: "dsl.help",
|
|
2019
|
+
ok: result.ok,
|
|
2020
|
+
diagnostics: result.diagnostics.map(jsonDiagnostic),
|
|
2021
|
+
topic_id: result.topicId,
|
|
2022
|
+
level: result.level,
|
|
2023
|
+
title: result.title,
|
|
2024
|
+
summary: result.summary,
|
|
2025
|
+
sections: result.sections,
|
|
2026
|
+
syntax: result.syntax,
|
|
2027
|
+
examples: result.examples,
|
|
2028
|
+
related: result.related,
|
|
2029
|
+
topics: result.topics,
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
else if (result.ok) {
|
|
2033
|
+
process.stdout.write(renderHelpText(result));
|
|
2034
|
+
}
|
|
2035
|
+
else {
|
|
2036
|
+
for (const diagnostic of result.diagnostics) {
|
|
2037
|
+
process.stderr.write(`${renderDiagnostic(diagnostic, "<help>", "never")}\n`);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
return result.ok ? 0 : 1;
|
|
2041
|
+
}
|
|
2042
|
+
async function main(argv) {
|
|
2043
|
+
if (argv.length === 1 && argv[0] === "--version") {
|
|
2044
|
+
process.stdout.write(`perttool ${TOOL_VERSION}\n`);
|
|
2045
|
+
return 0;
|
|
2046
|
+
}
|
|
2047
|
+
if (argv.length === 1 && argv[0] === "--help") {
|
|
2048
|
+
process.stdout.write(`${topLevelHelp()}\n`);
|
|
2049
|
+
return 0;
|
|
2050
|
+
}
|
|
2051
|
+
if (argv.length < 2)
|
|
2052
|
+
throw new UsageError("expected <resource> <action>");
|
|
2053
|
+
const resource = argv[0];
|
|
2054
|
+
const action = argv[1];
|
|
2055
|
+
const entityActions = new Map([
|
|
2056
|
+
["task", new Set(["add", "set", "remove", "finish"])],
|
|
2057
|
+
["milestone", new Set(["add", "set", "remove"])],
|
|
2058
|
+
["resource", new Set(["add", "set", "remove"])],
|
|
2059
|
+
["mutation", new Set(["apply"])],
|
|
2060
|
+
]);
|
|
2061
|
+
const isMutationCommand = entityActions.get(resource)?.has(action) === true;
|
|
2062
|
+
if (argv.length === 3 &&
|
|
2063
|
+
argv[2] === "--help" &&
|
|
2064
|
+
((resource === "dsl" && ["check", "format", "help"].includes(action)) ||
|
|
2065
|
+
(resource === "dag" && ["analyze", "next", "advance", "render", "import"].includes(action)) ||
|
|
2066
|
+
isMutationCommand)) {
|
|
2067
|
+
process.stdout.write(`${commandHelp(resource, action)}\n`);
|
|
2068
|
+
return 0;
|
|
2069
|
+
}
|
|
2070
|
+
if (resource === "dag" && action === "analyze") {
|
|
2071
|
+
return runAnalyze(argv.slice(2));
|
|
2072
|
+
}
|
|
2073
|
+
if (resource === "dag" && action === "next") {
|
|
2074
|
+
return runNext(argv.slice(2));
|
|
2075
|
+
}
|
|
2076
|
+
if (resource === "dag" && action === "advance") {
|
|
2077
|
+
return runAdvance(argv.slice(2));
|
|
2078
|
+
}
|
|
2079
|
+
if (resource === "dag" && action === "render") {
|
|
2080
|
+
return runRender(argv.slice(2));
|
|
2081
|
+
}
|
|
2082
|
+
if (resource === "dag" && action === "import") {
|
|
2083
|
+
return runImport(argv.slice(2));
|
|
2084
|
+
}
|
|
2085
|
+
if (resource === "dsl" && action === "format") {
|
|
2086
|
+
return runFormat(argv.slice(2));
|
|
2087
|
+
}
|
|
2088
|
+
if (isMutationCommand) {
|
|
2089
|
+
return runMutation(resource, action, argv.slice(2));
|
|
2090
|
+
}
|
|
2091
|
+
if (resource !== "dsl" || (action !== "check" && action !== "help")) {
|
|
2092
|
+
throw new UsageError(`unknown or not-yet-implemented command: ${resource} ${action}`);
|
|
2093
|
+
}
|
|
2094
|
+
return action === "check" ? runCheck(argv.slice(2)) : runHelp(argv.slice(2));
|
|
2095
|
+
}
|
|
2096
|
+
const args = process.argv.slice(2);
|
|
2097
|
+
try {
|
|
2098
|
+
process.exitCode = await main(args);
|
|
2099
|
+
}
|
|
2100
|
+
catch (error) {
|
|
2101
|
+
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));
|
|
2102
|
+
}
|
|
2103
|
+
//# sourceMappingURL=cli.js.map
|