@pmelab/gtd 23.2.0 → 24.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +44 -11
- package/claude/hooks/access.ts +1 -1
- package/dist/chunks/Schema-Dh-mkwkq.mjs +25353 -0
- package/dist/chunks/reserved-ejn-LVYO.mjs +1388 -0
- package/dist/chunks/rolldown-runtime-8H4AJuhK.mjs +14 -0
- package/dist/chunks/steering-D-PvRC5W.mjs +16296 -0
- package/dist/chunks/wire-DA7q2ikx.mjs +473 -0
- package/dist/flows.mjs +525 -0
- package/dist/gtd.bundle.mjs +43830 -56044
- package/dist/steering.mjs +2 -0
- package/dist/testkit.mjs +13006 -0
- package/dist/wire.mjs +2 -0
- package/dist/workflow-kit.mjs +352 -0
- package/dist/workflows/feature.mjs +315 -0
- package/dist/workflows/fix.mjs +40 -0
- package/dist/workflows/review.mjs +25 -0
- package/dist/workflows.mjs +1953 -1724
- package/package.json +70 -8
- package/skills/authoring/SKILL.md +5 -4
- package/src/cli/Cli.test.ts +1329 -0
- package/src/cli/Cli.ts +1348 -0
- package/src/cli/Program.test.ts +2262 -0
- package/src/cli/Program.ts +1331 -0
- package/src/cli/ToolCommands.ts +43 -0
- package/src/cli/index.ts +14 -0
- package/src/edge/Edge.test.ts +1016 -0
- package/src/edge/Edge.ts +1225 -0
- package/src/edge/index.ts +16 -0
- package/src/emit/Emit.test.ts +425 -0
- package/src/emit/Emit.ts +139 -0
- package/src/emit/GitScript.test.ts +260 -0
- package/src/emit/GitScript.ts +131 -0
- package/src/emit/ModeContradiction.test.ts +120 -0
- package/src/emit/ModeContradiction.ts +66 -0
- package/src/emit/OutcomeScript.test.ts +238 -0
- package/src/emit/OutcomeScript.ts +73 -0
- package/src/emit/SteeringMode.test.ts +274 -0
- package/src/emit/SteeringMode.ts +256 -0
- package/src/emit/index.ts +40 -0
- package/src/flows/factory.ts +5 -1
- package/src/flows/runtime.ts +3 -0
- package/src/install/ConfigScaffold.ts +19 -0
- package/src/install/Install.test.ts +426 -0
- package/src/install/Install.ts +634 -0
- package/src/install/Version.ts +27 -0
- package/src/install/index.ts +3 -0
- package/src/platform/CommandRunner.ts +74 -0
- package/src/platform/Commentary.test.ts +126 -0
- package/src/platform/Commentary.ts +92 -0
- package/src/platform/Git.test.ts +50 -0
- package/src/platform/Git.ts +552 -0
- package/src/platform/Host.ts +42 -0
- package/src/platform/RetainedHistory.test.ts +65 -0
- package/src/platform/RetainedHistory.ts +34 -0
- package/src/platform/Workspace.test.ts +669 -0
- package/src/platform/Workspace.ts +631 -0
- package/src/platform/WorktreeState.test.ts +162 -0
- package/src/platform/WorktreeState.ts +50 -0
- package/src/platform/index.ts +22 -0
- package/src/replay/Access.test.ts +53 -0
- package/src/replay/Access.ts +46 -0
- package/src/replay/Format.test.ts +20 -0
- package/src/replay/Format.ts +14 -0
- package/src/replay/Replay.test.ts +887 -0
- package/src/replay/Replay.ts +669 -0
- package/src/replay/Trailers.test.ts +117 -0
- package/src/replay/Trailers.ts +213 -0
- package/src/replay/Tree.test.ts +25 -0
- package/src/replay/Tree.ts +45 -0
- package/src/replay/index.ts +11 -0
- package/src/steering/Blocks.fixture.ts +6 -0
- package/src/steering/Blocks.test.ts +167 -0
- package/src/steering/Blocks.ts +321 -0
- package/src/steering/CodeThreads.test.ts +127 -0
- package/src/steering/CodeThreads.ts +197 -0
- package/src/steering/Edits.test.ts +19 -0
- package/src/steering/Edits.ts +20 -0
- package/src/steering/Eol.test.ts +16 -0
- package/src/steering/Eol.ts +2 -0
- package/src/steering/FORMATS.md +58 -0
- package/src/steering/Footnotes.test.ts +977 -0
- package/src/steering/Footnotes.ts +661 -0
- package/src/steering/Inline.fixture.ts +7 -0
- package/src/steering/Inline.test.ts +227 -0
- package/src/steering/Inline.ts +195 -0
- package/src/steering/MarkdownTree.test.ts +247 -0
- package/src/steering/MarkdownTree.ts +260 -0
- package/src/steering/Outline.ts +19 -0
- package/src/steering/SteeringFormat.test.ts +91 -0
- package/src/steering/SteeringFormat.ts +267 -0
- package/src/steering/SteeringFormats.test.ts +163 -0
- package/src/steering/Threads.ts +163 -0
- package/src/steering/contract.test.ts +264 -0
- package/src/steering/fixtures/code-threads/cache.stripped.ts.txt +6 -0
- package/src/steering/fixtures/code-threads/cache.ts.md +15 -0
- package/src/steering/fixtures/free-form/notes.md +12 -0
- package/src/steering/fixtures/free-form/notes.read.json +42 -0
- package/src/steering/fixtures/qa/answered.md +23 -0
- package/src/steering/fixtures/qa/answered.read.json +119 -0
- package/src/steering/fixtures/qa/requirements.md +160 -0
- package/src/steering/fixtures/qa/requirements.read.json +475 -0
- package/src/steering/fixtures/review/review.md +76 -0
- package/src/steering/fixtures/review/review.read.json +237 -0
- package/src/steering/fixtures/review/ticked.md +23 -0
- package/src/steering/fixtures/review/ticked.read.json +99 -0
- package/src/steering/fixtures/review-notes/baseline.md +14 -0
- package/src/steering/fixtures/review-notes/noted.md +19 -0
- package/src/steering/freeform.test.ts +311 -0
- package/src/steering/freeform.ts +148 -0
- package/src/steering/index.test.ts +62 -0
- package/src/steering/index.ts +140 -0
- package/src/steering/qa.test.ts +2160 -0
- package/src/steering/qa.ts +1104 -0
- package/src/steering/review.test.ts +1829 -0
- package/src/steering/review.ts +1180 -0
- package/src/step/LandStep.ts +31 -0
- package/src/step/RepoSnapshot.ts +29 -0
- package/src/step/accessRefusal.test.ts +58 -0
- package/src/step/accessRefusal.ts +26 -0
- package/src/step/index.ts +5 -0
- package/src/step/planStart.test.ts +202 -0
- package/src/step/planStart.ts +167 -0
- package/src/step/planStep.test.ts +104 -0
- package/src/step/planStep.ts +89 -0
- package/src/step/snapshot.fixture.ts +28 -0
- package/src/testkit/EmittedScriptRecognizer.test.ts +639 -0
- package/src/testkit/EmittedScriptRecognizer.ts +642 -0
- package/src/testkit/FakeGitOperations.ts +95 -0
- package/src/testkit/GitTiers.test.ts +11 -0
- package/src/testkit/GitTiers.ts +1052 -0
- package/src/testkit/InMemRepo.ts +467 -0
- package/src/testkit/Layers.test.ts +58 -0
- package/src/testkit/Layers.ts +241 -0
- package/src/testkit/StrictGitOperations.ts +31 -0
- package/src/testkit/TestRepo.test.ts +119 -0
- package/src/testkit/TestRepo.ts +274 -0
- package/src/testkit/cliIo.ts +36 -0
- package/src/testkit/index.ts +9 -0
- package/src/testkit/steps.ts +177 -0
- package/src/testkit/testkit.feature +65 -0
- package/src/wire/BeatDocument.test.ts +419 -0
- package/src/wire/BeatDocument.ts +114 -0
- package/src/wire/BeatPlain.test.ts +113 -0
- package/src/wire/BeatPlain.ts +127 -0
- package/src/wire/BeatStatus.test.ts +59 -0
- package/src/wire/BeatStatus.ts +81 -0
- package/src/wire/Demand.test.ts +142 -0
- package/src/wire/Demand.ts +108 -0
- package/src/wire/ExitCodes.test.ts +21 -0
- package/src/wire/ExitCodes.ts +16 -0
- package/src/wire/Glob.ts +29 -0
- package/src/wire/Judge.ts +22 -0
- package/src/wire/Land.test.ts +120 -0
- package/src/wire/Land.ts +75 -0
- package/src/wire/Select.test.ts +148 -0
- package/src/wire/Select.ts +78 -0
- package/src/wire/Sessions.test.ts +59 -0
- package/src/wire/Sessions.ts +39 -0
- package/src/wire/WireSchema.test.ts +162 -0
- package/src/wire/constants.ts +22 -0
- package/src/wire/golden/1/beat-capture.json +34 -0
- package/src/wire/golden/1/beat-message.json +34 -0
- package/src/wire/golden/1/beat-prompt.json +44 -0
- package/src/wire/golden/1/beat-script.json +34 -0
- package/src/wire/golden/1/beat-stalled.json +34 -0
- package/src/wire/golden/1/judge.json +14 -0
- package/src/wire/golden/1/land-noop.json +10 -0
- package/src/wire/golden/1/land.json +10 -0
- package/src/wire/index.ts +12 -0
- package/src/wire/types.ts +46 -0
- package/src/workflow/Diagnostic.test.ts +93 -0
- package/src/workflow/Diagnostic.ts +54 -0
- package/src/workflow/SteeringFormats.test.ts +27 -0
- package/src/workflow/SteeringFormats.ts +6 -0
- package/src/workflow/Workflow.ts +77 -0
- package/src/workflow/compile.test.ts +53 -0
- package/src/workflow/compile.ts +56 -0
- package/src/workflow/discovery.ts +122 -0
- package/src/workflow/fingerprint.test.ts +52 -0
- package/src/workflow/fingerprint.ts +53 -0
- package/src/workflow/index.ts +33 -0
- package/src/workflow/load.test.ts +1017 -0
- package/src/workflow/load.ts +836 -0
- package/src/workflow/skills.test.ts +109 -0
- package/src/workflow/skills.ts +90 -0
- package/src/workflow/vars.ts +33 -0
- package/src/workflow-kit/health.test.ts +30 -0
- package/src/workflow-kit/health.ts +86 -0
- package/src/workflow-kit/index.ts +7 -0
- package/src/workflow-kit/quality.test.ts +17 -0
- package/src/workflow-kit/quality.ts +45 -0
- package/src/workflow-kit/review.ts +324 -0
- package/src/{workflows → workflow-kit}/scenarios.ts +21 -28
- package/src/workflows/feature/feature.test.ts +32 -0
- package/src/workflows/{bundled.ts → feature/feature.ts} +11 -63
- package/src/workflows/feature/index.ts +10 -0
- package/src/workflows/{packages.test.ts → feature/packages.test.ts} +2 -2
- package/src/workflows/{packages.ts → feature/packages.ts} +3 -3
- package/src/workflows/{planning.ts → feature/planning.ts} +2 -3
- package/src/workflows/{review.test.ts → feature/review.test.ts} +9 -44
- package/src/workflows/feature/review.ts +106 -0
- package/src/workflows/feature/steps.test.ts +82 -0
- package/src/workflows/feature/steps.ts +92 -0
- package/src/workflows/fix/fix.feature +80 -0
- package/src/workflows/fix/fix.test.ts +74 -0
- package/src/workflows/fix/fix.ts +52 -0
- package/src/workflows/fix/index.ts +6 -0
- package/src/workflows/index.ts +36 -2
- package/src/workflows/review/index.ts +6 -0
- package/src/workflows/review/review.test.ts +99 -0
- package/src/workflows/review/review.ts +33 -0
- package/src/workflows/{access.test.ts → shared/access.test.ts} +1 -1
- package/src/workflows/{access.ts → shared/access.ts} +3 -5
- package/src/workflows/shared/config.fixture.ts +12 -0
- package/src/workflows/{health.test.ts → shared/health.test.ts} +1 -1
- package/src/workflows/{health.ts → shared/health.ts} +38 -55
- package/src/workflows/{options.test.ts → shared/options.test.ts} +2 -2
- package/src/workflows/{options.ts → shared/options.ts} +14 -2
- package/src/workflows/shared/reserved.ts +11 -0
- package/src/workflows/{scenarios.test.ts → shared/scenarios.test.ts} +1 -1
- package/src/workflows/shared/scenarios.ts +35 -0
- package/src/workflows/{skills.test.ts → shared/skills.test.ts} +3 -1
- package/src/workflows/{skills.ts → shared/skills.ts} +7 -6
- package/src/workflows/{steps.test.ts → shared/steps.test.ts} +1 -66
- package/src/workflows/{steps.ts → shared/steps.ts} +8 -98
- package/src/workflows/shared/tail.test.ts +205 -0
- package/src/workflows/shared/tail.ts +135 -0
- package/src/workflows/{text.fixture.ts → shared/text.fixture.ts} +2 -2
- package/src/workflows/{text.ts → shared/text.ts} +48 -7
- package/src/workflows/{threads.test.ts → shared/threads.test.ts} +1 -1
- package/src/workflows/shared/workflows.steps.ts +7 -0
- package/src/workflows/review.ts +0 -362
- /package/src/workflows/{prose.ts → shared/prose.ts} +0 -0
- /package/src/workflows/{text.test.ts → shared/text.test.ts} +0 -0
- /package/src/workflows/{vars.ts → shared/vars.ts} +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export {
|
|
2
|
+
currentRun,
|
|
3
|
+
summaryRun,
|
|
4
|
+
restAt,
|
|
5
|
+
currentRest,
|
|
6
|
+
startRefusal,
|
|
7
|
+
reviewBaseFor,
|
|
8
|
+
type RenderedRest,
|
|
9
|
+
renderRest,
|
|
10
|
+
stalledAt,
|
|
11
|
+
noProcessUnderway,
|
|
12
|
+
restIsIdle,
|
|
13
|
+
previewLanding,
|
|
14
|
+
snapshotFromRest,
|
|
15
|
+
summaryFor,
|
|
16
|
+
} from "./Edge.js"
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { execFileSync, execSync, spawnSync } from "node:child_process"
|
|
2
|
+
import { existsSync, mkdtempSync } from "node:fs"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import { describe, expect, it } from "vitest"
|
|
6
|
+
import {
|
|
7
|
+
binaryGuard,
|
|
8
|
+
combinedScript,
|
|
9
|
+
DID_NOT_RUN_COMMENT,
|
|
10
|
+
emitScripts,
|
|
11
|
+
extractLeadingBinary,
|
|
12
|
+
fileExistsGuard,
|
|
13
|
+
type EmitStep,
|
|
14
|
+
} from "./Emit.js"
|
|
15
|
+
import { commitOutcome, noteOutcome } from "./index.js"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `sh -n` (POSIX syntax check), not `bash -n` — a script that only passes
|
|
19
|
+
* under `bash -n` could still be using a bashism `dash` rejects (`local`,
|
|
20
|
+
* `$RANDOM`, `$'...'`, process substitution, …), which is exactly the class
|
|
21
|
+
* of bug this port is fixing. `sh` on this machine is a real POSIX shell, so
|
|
22
|
+
* a pass here is evidence the emitted script is portable, not just
|
|
23
|
+
* bash-flavored.
|
|
24
|
+
*/
|
|
25
|
+
const runShCheckSyntax = (script: string): number => {
|
|
26
|
+
try {
|
|
27
|
+
execFileSync("sh", ["-n"], { input: script })
|
|
28
|
+
return 0
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return (error as { status?: number }).status ?? 1
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("emitScripts — empty inputs", () => {
|
|
35
|
+
it("is the empty string when both halves are omitted", () => {
|
|
36
|
+
const { required, optional } = emitScripts()
|
|
37
|
+
expect(required).toBe("")
|
|
38
|
+
expect(optional).toBe("")
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("is the empty string when both halves are explicitly []", () => {
|
|
42
|
+
const { required, optional } = emitScripts([], [])
|
|
43
|
+
expect(required).toBe("")
|
|
44
|
+
expect(optional).toBe("")
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe("emitScripts — required only", () => {
|
|
49
|
+
const steps: ReadonlyArray<EmitStep> = [
|
|
50
|
+
{ kind: "gitWrite", command: "git commit --allow-empty -m 'gtd(agent): x'" },
|
|
51
|
+
]
|
|
52
|
+
const { required, optional } = emitScripts(steps)
|
|
53
|
+
|
|
54
|
+
it("leaves optional empty", () => {
|
|
55
|
+
expect(optional).toBe("")
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("begins with set -eu as the literal first line", () => {
|
|
59
|
+
expect(required.split("\n")[0]).toBe("set -eu")
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it("carries no precondition guard — the script is exactly set -eu plus its steps", () => {
|
|
63
|
+
expect(required).not.toContain("git rev-parse")
|
|
64
|
+
expect(required.split("\n\n")).toEqual([
|
|
65
|
+
"set -eu",
|
|
66
|
+
"git commit --allow-empty -m 'gtd(agent): x'",
|
|
67
|
+
])
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it("is syntactically valid POSIX sh", () => {
|
|
71
|
+
expect(runShCheckSyntax(required)).toBe(0)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe("emitScripts — both halves populated", () => {
|
|
76
|
+
const required: ReadonlyArray<EmitStep> = [
|
|
77
|
+
{ kind: "gitWrite", command: "git commit --allow-empty -m 'gtd(agent): x'" },
|
|
78
|
+
]
|
|
79
|
+
const optional: ReadonlyArray<EmitStep> = [
|
|
80
|
+
{ kind: "command", command: "some-format-command --check FEEDBACK.md" },
|
|
81
|
+
]
|
|
82
|
+
const result = emitScripts(required, optional)
|
|
83
|
+
|
|
84
|
+
it("both halves are non-empty", () => {
|
|
85
|
+
expect(result.required).not.toBe("")
|
|
86
|
+
expect(result.optional).not.toBe("")
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("both halves independently begin with set -eu", () => {
|
|
90
|
+
expect(result.required.split("\n")[0]).toBe("set -eu")
|
|
91
|
+
expect(result.optional.split("\n")[0]).toBe("set -eu")
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it("both halves are syntactically valid POSIX sh", () => {
|
|
95
|
+
expect(runShCheckSyntax(result.required)).toBe(0)
|
|
96
|
+
expect(runShCheckSyntax(result.optional)).toBe(0)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("a plain command step is emitted verbatim", () => {
|
|
100
|
+
expect(result.optional).toContain("some-format-command --check FEEDBACK.md")
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("a gitWrite step is emitted verbatim — no wrapper, so a failing git write fails in the user's shell", () => {
|
|
104
|
+
expect(result.required).toContain("git commit --allow-empty -m 'gtd(agent): x'")
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe("emitScripts — outcome steps", () => {
|
|
109
|
+
it("renders an outcome step verbatim — no preamble, the step carries its own printf", () => {
|
|
110
|
+
const { required } = emitScripts([{ kind: "outcome", command: commitOutcome("gtd(agent): x") }])
|
|
111
|
+
expect(required).toContain(commitOutcome("gtd(agent): x"))
|
|
112
|
+
expect(required.split("\n\n").length).toBe(2)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("is syntactically valid POSIX sh", () => {
|
|
116
|
+
const { required } = emitScripts([
|
|
117
|
+
{ kind: "outcome", command: noteOutcome('nothing to do at "idle"') },
|
|
118
|
+
])
|
|
119
|
+
expect(runShCheckSyntax(required)).toBe(0)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe("emitScripts — POSIX sh portability of the emitted output", () => {
|
|
124
|
+
const steps: ReadonlyArray<EmitStep> = [
|
|
125
|
+
{ kind: "gitWrite", command: "git commit --allow-empty -m 'gtd(agent): x'" },
|
|
126
|
+
{ kind: "outcome", command: commitOutcome("gtd(agent): x") },
|
|
127
|
+
]
|
|
128
|
+
const { required } = emitScripts(steps)
|
|
129
|
+
|
|
130
|
+
it("never contains 'pipefail'", () => {
|
|
131
|
+
expect(required).not.toContain("pipefail")
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it("never contains 'RANDOM'", () => {
|
|
135
|
+
expect(required).not.toContain("RANDOM")
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it("contains no ' local ' token", () => {
|
|
139
|
+
expect(required).not.toContain(" local ")
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
describe("fileExistsGuard", () => {
|
|
144
|
+
it("renders a plain OR-list test, never an if/fi block", () => {
|
|
145
|
+
expect(fileExistsGuard(".gtd/REVIEW.md")).toBe(`[ -f '.gtd/REVIEW.md' ] || exit 0`)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it("shell-quotes a file path containing a single quote", () => {
|
|
149
|
+
expect(fileExistsGuard("it's.md")).toBe(`[ -f 'it'\\''s.md' ] || exit 0`)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it("is syntactically valid POSIX sh on its own", () => {
|
|
153
|
+
expect(runShCheckSyntax(fileExistsGuard(".gtd/REVIEW.md"))).toBe(0)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it("exits 0 (not 1) when the guard trips — a missing steering file is not a failure", () => {
|
|
157
|
+
const result = spawnSync("sh", ["-c", fileExistsGuard("/no/such/file-for-sure")])
|
|
158
|
+
expect(result.status).toBe(0)
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe("extractLeadingBinary", () => {
|
|
163
|
+
it("returns the leading word of a plain command", () => {
|
|
164
|
+
expect(extractLeadingBinary("adr-lint docs/adr.md")).toBe("adr-lint")
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it("returns the whole command when it is a single bare word", () => {
|
|
168
|
+
expect(extractLeadingBinary("true")).toBe("true")
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it("returns undefined for a VAR=x-prefixed command", () => {
|
|
172
|
+
expect(extractLeadingBinary("FOO=1 some-tool docs/adr.md")).toBeUndefined()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it("returns undefined for a pipeline", () => {
|
|
176
|
+
expect(extractLeadingBinary("cat docs/adr.md | grep -q Decision")).toBeUndefined()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it("returns undefined for a subshell", () => {
|
|
180
|
+
expect(extractLeadingBinary("(cd docs && adr-lint adr.md)")).toBeUndefined()
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it("returns undefined for a multi-line command", () => {
|
|
184
|
+
expect(extractLeadingBinary("adr-lint docs/adr.md\necho done")).toBeUndefined()
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it("returns undefined for a leading word spelled with characters outside [A-Za-z0-9_./-]", () => {
|
|
188
|
+
expect(extractLeadingBinary("$(which adr-lint) docs/adr.md")).toBeUndefined()
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
describe("binaryGuard", () => {
|
|
193
|
+
it("renders a command -v probe naming the mode, key, and binary, exiting 127", () => {
|
|
194
|
+
expect(binaryGuard("adr-lint", "adr", "validate")).toBe(
|
|
195
|
+
[
|
|
196
|
+
`command -v 'adr-lint' >/dev/null 2>&1 || {`,
|
|
197
|
+
` printf '%s\\n%s\\n' 'gtd: mode "adr": "validate" command not found: adr-lint' '$PATH: '"$PATH"`,
|
|
198
|
+
` exit 127`,
|
|
199
|
+
`}`,
|
|
200
|
+
].join("\n"),
|
|
201
|
+
)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it("is syntactically valid POSIX sh", () => {
|
|
205
|
+
expect(runShCheckSyntax(binaryGuard("adr-lint", "adr", "validate"))).toBe(0)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it("$PATH is expanded by the driver's shell, never resolved in-process — a literal '$PATH: ' label plus a double-quoted $PATH survive in the rendered text", () => {
|
|
209
|
+
expect(binaryGuard("adr-lint", "adr", "validate")).toContain(`'$PATH: '"$PATH"`)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it("exits 0 (not 127) when the named binary IS on PATH", () => {
|
|
213
|
+
const result = spawnSync("sh", ["-c", binaryGuard("true", "adr", "validate")])
|
|
214
|
+
expect(result.status).toBe(0)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it("exits 127 and names the binary plus the resolved $PATH when it is NOT on PATH", () => {
|
|
218
|
+
const result = spawnSync(
|
|
219
|
+
"/bin/sh",
|
|
220
|
+
["-c", binaryGuard("gtd-nonexistent-tool", "adr", "validate")],
|
|
221
|
+
{
|
|
222
|
+
encoding: "utf8",
|
|
223
|
+
env: { ...process.env, PATH: "/no/such/dir" },
|
|
224
|
+
},
|
|
225
|
+
)
|
|
226
|
+
expect(result.status).toBe(127)
|
|
227
|
+
expect(result.stdout).toContain(
|
|
228
|
+
'gtd: mode "adr": "validate" command not found: gtd-nonexistent-tool',
|
|
229
|
+
)
|
|
230
|
+
expect(result.stdout).toContain("$PATH: /no/such/dir")
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
describe("emitScripts — a command step with onFailure", () => {
|
|
235
|
+
const initRepo = (): string => {
|
|
236
|
+
const dir = mkdtempSync(join(tmpdir(), "emit-onfailure-repo-"))
|
|
237
|
+
execFileSync("git", ["init", "-q"], { cwd: dir })
|
|
238
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: dir })
|
|
239
|
+
execFileSync("git", ["config", "user.name", "Test"], { cwd: dir })
|
|
240
|
+
execFileSync("git", ["config", "commit.gpgsign", "false"], { cwd: dir })
|
|
241
|
+
execFileSync("git", ["commit", "--allow-empty", "-q", "-m", "initial"], { cwd: dir })
|
|
242
|
+
return dir
|
|
243
|
+
}
|
|
244
|
+
const runIn = (dir: string, script: string): { status: number; output: string } => {
|
|
245
|
+
try {
|
|
246
|
+
const output = execSync("sh", { input: script, cwd: dir, encoding: "utf8" })
|
|
247
|
+
return { status: 0, output }
|
|
248
|
+
} catch (error) {
|
|
249
|
+
const e = error as { status?: number; stdout?: string }
|
|
250
|
+
return { status: e.status ?? 1, output: e.stdout ?? "" }
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
it("is syntactically valid POSIX sh", () => {
|
|
255
|
+
const steps: ReadonlyArray<EmitStep> = [
|
|
256
|
+
{ kind: "command", command: "true", onFailure: "fix it" },
|
|
257
|
+
]
|
|
258
|
+
const { required } = emitScripts(steps)
|
|
259
|
+
expect(runShCheckSyntax(required)).toBe(0)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it("a succeeding wrapped command exits 0 and prints nothing of its own", () => {
|
|
263
|
+
const dir = initRepo()
|
|
264
|
+
const steps: ReadonlyArray<EmitStep> = [
|
|
265
|
+
{ kind: "command", command: "true", onFailure: "fix it" },
|
|
266
|
+
]
|
|
267
|
+
const { required } = emitScripts(steps)
|
|
268
|
+
const { status, output } = runIn(dir, required)
|
|
269
|
+
expect(status).toBe(0)
|
|
270
|
+
expect(output).toBe("")
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it("a failing command prints the instruction, a blank line, then its combined output, and exits with its own code", () => {
|
|
274
|
+
const dir = initRepo()
|
|
275
|
+
const steps: ReadonlyArray<EmitStep> = [
|
|
276
|
+
{
|
|
277
|
+
kind: "command",
|
|
278
|
+
command: "echo boom >&2; exit 3",
|
|
279
|
+
onFailure: "Fix this violation",
|
|
280
|
+
},
|
|
281
|
+
]
|
|
282
|
+
const { required } = emitScripts(steps)
|
|
283
|
+
const { status, output } = runIn(dir, required)
|
|
284
|
+
expect(status).toBe(3)
|
|
285
|
+
expect(output).toBe("Fix this violation\n\nboom\n")
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it("a multi-line inner command round-trips through the { … } group", () => {
|
|
289
|
+
const dir = initRepo()
|
|
290
|
+
const steps: ReadonlyArray<EmitStep> = [
|
|
291
|
+
{
|
|
292
|
+
kind: "command",
|
|
293
|
+
command: "echo line1 >&2\necho line2 >&2\nexit 5",
|
|
294
|
+
onFailure: "Fix multi",
|
|
295
|
+
},
|
|
296
|
+
]
|
|
297
|
+
const { required } = emitScripts(steps)
|
|
298
|
+
const { status, output } = runIn(dir, required)
|
|
299
|
+
expect(status).toBe(5)
|
|
300
|
+
expect(output).toBe("Fix multi\n\nline1\nline2\n")
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it("a command step without onFailure is still emitted verbatim", () => {
|
|
304
|
+
const steps: ReadonlyArray<EmitStep> = [{ kind: "command", command: "some-command --flag" }]
|
|
305
|
+
const { required } = emitScripts(steps)
|
|
306
|
+
expect(required).toContain("some-command --flag")
|
|
307
|
+
expect(required).not.toContain("gtd_validate_status")
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
describe("real repo — an assembled script end to end", () => {
|
|
312
|
+
const initRepo = (): string => {
|
|
313
|
+
const dir = mkdtempSync(join(tmpdir(), "emit-realrepo-"))
|
|
314
|
+
execFileSync("git", ["init", "-q"], { cwd: dir })
|
|
315
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: dir })
|
|
316
|
+
execFileSync("git", ["config", "user.name", "Test"], { cwd: dir })
|
|
317
|
+
execFileSync("git", ["config", "commit.gpgsign", "false"], { cwd: dir })
|
|
318
|
+
execFileSync("git", ["commit", "--allow-empty", "-q", "-m", "initial"], { cwd: dir })
|
|
319
|
+
return dir
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const commitCount = (dir: string): number =>
|
|
323
|
+
Number(
|
|
324
|
+
execFileSync("git", ["rev-list", "--count", "HEAD"], { cwd: dir, encoding: "utf8" }).trim(),
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
it("the git command actually lands", () => {
|
|
328
|
+
const dir = initRepo()
|
|
329
|
+
const before = commitCount(dir)
|
|
330
|
+
const { required } = emitScripts([
|
|
331
|
+
{ kind: "gitWrite", command: "git commit --allow-empty -m 'gtd(agent): landed'" },
|
|
332
|
+
])
|
|
333
|
+
execSync("sh", { input: required, cwd: dir })
|
|
334
|
+
expect(commitCount(dir)).toBe(before + 1)
|
|
335
|
+
expect(
|
|
336
|
+
execFileSync("git", ["log", "-1", "--pretty=%s"], { cwd: dir, encoding: "utf8" }).trim(),
|
|
337
|
+
).toBe("gtd(agent): landed")
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it("a failing git write aborts the script non-zero, leaving later steps unrun", () => {
|
|
341
|
+
const dir = initRepo()
|
|
342
|
+
const before = commitCount(dir)
|
|
343
|
+
const { required } = emitScripts([
|
|
344
|
+
{ kind: "gitWrite", command: "git commit --allow-empty -m 'x' --this-flag-does-not-exist" },
|
|
345
|
+
{ kind: "command", command: "touch later-step-ran" },
|
|
346
|
+
])
|
|
347
|
+
let status = 0
|
|
348
|
+
try {
|
|
349
|
+
execSync("sh", { input: required, cwd: dir, stdio: ["pipe", "pipe", "pipe"] })
|
|
350
|
+
} catch (error) {
|
|
351
|
+
status = (error as { status?: number }).status ?? 1
|
|
352
|
+
}
|
|
353
|
+
expect(status).not.toBe(0)
|
|
354
|
+
expect(commitCount(dir)).toBe(before)
|
|
355
|
+
expect(existsSync(join(dir, "later-step-ran"))).toBe(false)
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
describe("combinedScript — the plain-text write commands' single pasteable script", () => {
|
|
360
|
+
it("DID_NOT_RUN_COMMENT points the reader at sh, not bash", () => {
|
|
361
|
+
expect(DID_NOT_RUN_COMMENT).toBe(
|
|
362
|
+
"# gtd emitted this and did NOT run it — pipe it into `sh` to land the turn",
|
|
363
|
+
)
|
|
364
|
+
expect(DID_NOT_RUN_COMMENT).not.toContain("bash")
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it("is the empty string when required is empty", () => {
|
|
368
|
+
expect(combinedScript("", "")).toBe("")
|
|
369
|
+
expect(combinedScript("", "echo optional")).toBe("")
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it("prepends the leading 'did not run it' comment ahead of required, with no optional block", () => {
|
|
373
|
+
const script = combinedScript("echo required", "")
|
|
374
|
+
expect(script.startsWith("# gtd emitted this and did NOT run it")).toBe(true)
|
|
375
|
+
expect(script).toContain("echo required")
|
|
376
|
+
expect(script).not.toContain("presentation-only follow-up failed")
|
|
377
|
+
expect(runShCheckSyntax(script)).toBe(0)
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it("wraps a non-empty optional in a subshell that never fails the whole script", () => {
|
|
381
|
+
const script = combinedScript("echo required", "echo optional")
|
|
382
|
+
expect(script.startsWith("# gtd emitted this and did NOT run it")).toBe(true)
|
|
383
|
+
expect(script).toContain("echo required")
|
|
384
|
+
expect(script).toContain("(\necho optional\n) || printf")
|
|
385
|
+
expect(script).toContain("presentation-only follow-up failed — continuing")
|
|
386
|
+
expect(runShCheckSyntax(script)).toBe(0)
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it("the presentation-only failure warning prints via printf, not echo, and its wording is stable", () => {
|
|
390
|
+
const script = combinedScript("echo required", "echo optional")
|
|
391
|
+
expect(script).toContain("printf 'gtd: presentation-only follow-up failed — continuing\\n' >&2")
|
|
392
|
+
expect(script).not.toMatch(/echo\s+["']?gtd: presentation-only/)
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it("required runs before optional, and a failing required aborts before optional runs", () => {
|
|
396
|
+
const dir = mkdtempSync(join(tmpdir(), "emit-combined-"))
|
|
397
|
+
const script = combinedScript("exit 1", "touch optional-ran")
|
|
398
|
+
let status = 0
|
|
399
|
+
try {
|
|
400
|
+
execSync("sh", { input: script, cwd: dir, stdio: ["pipe", "pipe", "pipe"] })
|
|
401
|
+
} catch (error) {
|
|
402
|
+
status = (error as { status?: number }).status ?? 1
|
|
403
|
+
}
|
|
404
|
+
expect(status).not.toBe(0)
|
|
405
|
+
expect(existsSync(join(dir, "optional-ran"))).toBe(false)
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it("a failing optional is swallowed — the whole script still exits 0", () => {
|
|
409
|
+
const dir = mkdtempSync(join(tmpdir(), "emit-combined-"))
|
|
410
|
+
const script = combinedScript("touch required-ran", "exit 1")
|
|
411
|
+
// stderr piped, not inherited: the optional half's own failure warning is
|
|
412
|
+
// the point of this case, and must not leak into the test run's output.
|
|
413
|
+
execSync("sh", { input: script, cwd: dir, stdio: ["pipe", "pipe", "pipe"] })
|
|
414
|
+
expect(existsSync(join(dir, "required-ran"))).toBe(true)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it("the discarded-optional-failure warning lands on stderr, never stdout — this channel's own shape, pinned alongside Commentary.test.ts", () => {
|
|
418
|
+
const dir = mkdtempSync(join(tmpdir(), "emit-combined-"))
|
|
419
|
+
const script = combinedScript("touch required-ran", "exit 1")
|
|
420
|
+
const result = spawnSync("sh", [], { input: script, cwd: dir, encoding: "utf8" })
|
|
421
|
+
expect(result.status).toBe(0)
|
|
422
|
+
expect(result.stderr).toContain("gtd: presentation-only follow-up failed — continuing")
|
|
423
|
+
expect(result.stdout).not.toContain("gtd: presentation-only follow-up failed")
|
|
424
|
+
})
|
|
425
|
+
})
|
package/src/emit/Emit.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { shellQuote } from "./GitScript.js"
|
|
2
|
+
|
|
3
|
+
export interface EmittedScripts {
|
|
4
|
+
readonly required: string
|
|
5
|
+
readonly optional: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Every kind renders its `command` verbatim (the one exception is a
|
|
10
|
+
* `command` step carrying an `onFailure` prompt); the discriminant records
|
|
11
|
+
* WHAT a step is, and nothing else. A failing git write is left to fail in
|
|
12
|
+
* the user's shell — gtd never writes git itself, so there is nothing to
|
|
13
|
+
* retry on its behalf — and an `outcome` step carries its own complete
|
|
14
|
+
* `printf`, so no shared preamble is emitted for it either.
|
|
15
|
+
*/
|
|
16
|
+
export type EmitStep =
|
|
17
|
+
| { readonly kind: "gitWrite"; readonly command: string }
|
|
18
|
+
| {
|
|
19
|
+
readonly kind: "command"
|
|
20
|
+
readonly command: string
|
|
21
|
+
/**
|
|
22
|
+
* Prompt text printed (with the command's captured output) on a
|
|
23
|
+
* non-zero exit before propagating that exit code. Omitted for a step
|
|
24
|
+
* that should fail raw — e.g. a `format:` command's own broken-tooling
|
|
25
|
+
* failure, which an agent can't fix by editing the steering file.
|
|
26
|
+
*/
|
|
27
|
+
readonly onFailure?: string
|
|
28
|
+
}
|
|
29
|
+
| { readonly kind: "outcome"; readonly command: string }
|
|
30
|
+
|
|
31
|
+
/** Exits 0 when the declared steering file is absent — nothing to format or validate yet. An OR list, not `if`: its left side failing never trips `set -e`. */
|
|
32
|
+
export const fileExistsGuard = (file: string): string => `[ -f ${shellQuote(file)} ] || exit 0`
|
|
33
|
+
|
|
34
|
+
/** A mode or `ui.format` command finds the file it works on in `$GTD_FILE`. */
|
|
35
|
+
export const withFileVar = (command: string, file: string): string =>
|
|
36
|
+
`export GTD_FILE=${shellQuote(file)}\n${command}`
|
|
37
|
+
|
|
38
|
+
const SIMPLE_LEADING_WORD_RE = /^[A-Za-z0-9_./-]+(?=[ \t]|$)/
|
|
39
|
+
const SHELL_METACHARACTER_RE = /[|&;$<>(){}`]/
|
|
40
|
+
// The file variable is an argument, never a second command.
|
|
41
|
+
const FILE_VAR_RE = /"\$GTD_FILE"|"\$\{GTD_FILE\}"|\$GTD_FILE\b|\$\{GTD_FILE\}/g
|
|
42
|
+
|
|
43
|
+
/** `command` with `file` written in for `$GTD_FILE` — one line an agent can copy, equivalent to running it under `withFileVar`. */
|
|
44
|
+
export const commandForFile = (command: string, file: string): string =>
|
|
45
|
+
command.replace(FILE_VAR_RE, shellQuote(file))
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The leading word of a rendered `format:`/`validate:` command, when the
|
|
49
|
+
* command has exactly one unambiguous binary to probe — `undefined` for
|
|
50
|
+
* anything a shell metacharacter could hide a second command behind. A missing
|
|
51
|
+
* guard degrades to a raw exit; a wrong guard would refuse a working command.
|
|
52
|
+
*/
|
|
53
|
+
export const extractLeadingBinary = (command: string): string | undefined => {
|
|
54
|
+
if (command.includes("\n")) return undefined
|
|
55
|
+
if (SHELL_METACHARACTER_RE.test(command.replace(FILE_VAR_RE, ""))) return undefined
|
|
56
|
+
const match = SIMPLE_LEADING_WORD_RE.exec(command)
|
|
57
|
+
return match === null ? undefined : match[0]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Names a typo'd or uninstalled binary and the `$PATH` it was looked up on.
|
|
62
|
+
* `$PATH` is a shell word the DRIVER's shell expands at run time, so gtd never
|
|
63
|
+
* resolves it in-process. Exits 127, matching bash's "command not found".
|
|
64
|
+
*/
|
|
65
|
+
export const binaryGuard = (binary: string, mode: string, key: "format" | "validate"): string => {
|
|
66
|
+
const messageQ = shellQuote(`gtd: mode "${mode}": "${key}" command not found: ${binary}`)
|
|
67
|
+
return [
|
|
68
|
+
`command -v ${shellQuote(binary)} >/dev/null 2>&1 || {`,
|
|
69
|
+
` printf '%s\\n%s\\n' ${messageQ} '$PATH: '"$PATH"`,
|
|
70
|
+
` exit 127`,
|
|
71
|
+
`}`,
|
|
72
|
+
].join("\n")
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Prints `prompt` plus the captured output on a non-zero exit, then propagates
|
|
77
|
+
* it. The `{ … }` group inlines a multi-line `command` verbatim; the
|
|
78
|
+
* assignment sits left of `||` so `set -e` never trips on the failure itself.
|
|
79
|
+
*/
|
|
80
|
+
export const failurePromptWrapper = (command: string, prompt: string): string => {
|
|
81
|
+
const promptQ = shellQuote(prompt)
|
|
82
|
+
return [
|
|
83
|
+
`gtd_validate_status=0`,
|
|
84
|
+
`gtd_validate_out="$( {`,
|
|
85
|
+
command,
|
|
86
|
+
`} 2>&1 )" || gtd_validate_status=$?`,
|
|
87
|
+
`if [ "$gtd_validate_status" -ne 0 ]; then`,
|
|
88
|
+
` printf '%s\\n\\n%s\\n' ${promptQ} "$gtd_validate_out"`,
|
|
89
|
+
` exit "$gtd_validate_status"`,
|
|
90
|
+
`fi`,
|
|
91
|
+
].join("\n")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const renderStep = (step: EmitStep): string => {
|
|
95
|
+
if (step.kind === "command" && step.onFailure !== undefined) {
|
|
96
|
+
return failurePromptWrapper(step.command, step.onFailure)
|
|
97
|
+
}
|
|
98
|
+
return step.command
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const assembleScript = (steps: ReadonlyArray<EmitStep>): string =>
|
|
102
|
+
steps.length === 0 ? "" : ["set -eu", ...steps.map(renderStep)].join("\n\n")
|
|
103
|
+
|
|
104
|
+
/** Build both halves from already-rendered command strings; an empty (or omitted) array produces the empty-string result, so a driver checking `if [ -n "$required" ]` never has to special-case a bare preamble. */
|
|
105
|
+
export const emitScripts = (
|
|
106
|
+
required: ReadonlyArray<EmitStep> = [],
|
|
107
|
+
optional: ReadonlyArray<EmitStep> = [],
|
|
108
|
+
): EmittedScripts => ({
|
|
109
|
+
required: assembleScript(required),
|
|
110
|
+
optional: assembleScript(optional),
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
/** Exported so `src/testkit/EmittedScriptRecognizer.ts` can recognize these by exact string comparison. `printf`, not `echo`, avoids shell-specific backslash-escape quirks. */
|
|
114
|
+
export const DID_NOT_RUN_COMMENT =
|
|
115
|
+
"# gtd emitted this and did NOT run it — pipe it into `sh` to land the turn"
|
|
116
|
+
|
|
117
|
+
export const PRESENTATION_ONLY_COMMENT = "# presentation only — safe to skip"
|
|
118
|
+
|
|
119
|
+
export const PRESENTATION_FAILURE_WARNING =
|
|
120
|
+
"printf 'gtd: presentation-only follow-up failed — continuing\\n' >&2"
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A write command's single pasteable script (`gtd abandon | sh`,
|
|
124
|
+
* `gtd land --json=script`'s own field). `optional`, when non-empty, is
|
|
125
|
+
* wrapped in a subshell whose failure is swallowed — presentation-only, so it
|
|
126
|
+
* must never turn a landed turn into a non-zero exit.
|
|
127
|
+
*/
|
|
128
|
+
export const combinedScript = (required: string, optional: string): string => {
|
|
129
|
+
if (required.length === 0) return ""
|
|
130
|
+
if (optional.length === 0) return `${DID_NOT_RUN_COMMENT}\n\n${required}`
|
|
131
|
+
return [
|
|
132
|
+
DID_NOT_RUN_COMMENT,
|
|
133
|
+
"",
|
|
134
|
+
required,
|
|
135
|
+
"",
|
|
136
|
+
PRESENTATION_ONLY_COMMENT,
|
|
137
|
+
`(\n${optional}\n) || ${PRESENTATION_FAILURE_WARNING}`,
|
|
138
|
+
].join("\n")
|
|
139
|
+
}
|