@pmelab/gtd 35.0.0 → 37.0.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 +45 -29
- package/claude/hooks/context.ts +66 -0
- package/claude/hooks/drive.ts +3 -0
- package/claude/hooks/register.tsx +69 -6
- package/dist/chunks/{reserved-C8Fm3s72.mjs → reserved-zAfoJtR7.mjs} +62 -24
- package/dist/chunks/{tail-oaYewBN_.mjs → tail-BRC4DGdV.mjs} +1 -1
- package/dist/chunks/{wire-D3Rc5r22.mjs → wire-CSsa3Wxd.mjs} +117 -2
- package/dist/flows.mjs +15 -10
- package/dist/gtd.bundle.mjs +511 -64
- package/dist/testkit.mjs +158 -26
- package/dist/wire.mjs +2 -2
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +138 -38
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +2 -2
- package/dist/workflows/rollout.mjs +1 -1
- package/dist/workflows.mjs +208 -66
- package/package.json +1 -1
- package/skills/authoring/SKILL.md +3 -3
- package/src/cli/Cli.test.ts +10 -3
- package/src/cli/Cli.ts +76 -1
- package/src/cli/Program.test.ts +1 -0
- package/src/cli/Program.ts +28 -0
- package/src/cli/ToolCommands.ts +3 -0
- package/src/edge/Edge.ts +11 -0
- package/src/flows/runtime.ts +11 -0
- package/src/flows/scripts.ts +31 -9
- package/src/replay/Replay.ts +51 -2
- package/src/testkit/FlowScripts.test.ts +58 -0
- package/src/testkit/FlowScripts.ts +61 -24
- package/src/testkit/Layers.ts +1 -0
- package/src/wire/BeatDocument.ts +12 -2
- package/src/wire/BeatStatus.ts +3 -0
- package/src/wire/ContextLimit.fixture.ts +30 -0
- package/src/wire/ContextLimit.test.ts +96 -0
- package/src/wire/ContextLimit.ts +169 -0
- package/src/wire/index.ts +1 -0
- package/src/wire/types.ts +7 -0
- package/src/workflow/Workflow.ts +9 -1
- package/src/workflows/feature/acceptance-criteria.feature +5 -1
- package/src/workflows/feature/context-limit.feature +37 -0
- package/src/workflows/feature/coverage.feature +6 -3
- package/src/workflows/feature/coverage.ts +8 -2
- package/src/workflows/feature/fast-path.feature +11 -19
- package/src/workflows/feature/feature.test.ts +24 -4
- package/src/workflows/feature/feature.ts +14 -10
- package/src/workflows/feature/flag-mode.feature +17 -9
- package/src/workflows/feature/lens-models.feature +7 -1
- package/src/workflows/feature/naive-acceptance.feature +2 -2
- package/src/workflows/feature/one-step-land.feature +10 -12
- package/src/workflows/feature/quality-unresolved.feature +7 -1
- package/src/workflows/feature/refactor-absorbs.feature +12 -5
- package/src/workflows/feature/refactor-first.feature +11 -2
- package/src/workflows/feature/refactor-lens-models.feature +4 -0
- package/src/workflows/feature/refactor-rebuild.feature +269 -0
- package/src/workflows/feature/refactor-review.feature +45 -1
- package/src/workflows/feature/refactor.ts +9 -3
- package/src/workflows/feature/risk-tier.feature +12 -4
- package/src/workflows/feature/seam-rejection.feature +4 -9
- package/src/workflows/feature/seam.feature +29 -26
- package/src/workflows/feature/seam.test.ts +117 -6
- package/src/workflows/feature/seam.ts +158 -39
- package/src/workflows/review/review-door.feature +12 -4
- package/src/workflows/shared/options.test.ts +11 -2
- package/src/workflows/shared/options.ts +18 -4
- package/src/workflows/shared/steps.ts +1 -0
- package/src/workflows/shared/text.test.ts +8 -0
- package/src/workflows/shared/text.ts +91 -22
package/dist/workflows.mjs
CHANGED
|
@@ -214,25 +214,21 @@ const checkScript = (command, options) => {
|
|
|
214
214
|
""
|
|
215
215
|
].join("\n");
|
|
216
216
|
};
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
* patch no longer applies — writes `failure` and git's error to `report`,
|
|
220
|
-
* since a failed revert and one with nothing to undo both leave the tree clean.
|
|
221
|
-
*/
|
|
222
|
-
const revertScript = (commit, report, failure) => {
|
|
217
|
+
const patchScript = (commit, report, failure, reverse, exclude = []) => {
|
|
218
|
+
const paths = exclude.length === 0 ? "" : ` . ${exclude.map((p) => quote(`:(exclude,literal)${p}`)).join(" ")}`;
|
|
223
219
|
const range = `${quote(`${commit}^`)} ${quote(commit)}`;
|
|
224
220
|
const error = quote(`${report}.output`);
|
|
225
221
|
return [
|
|
226
222
|
"#!/usr/bin/env sh",
|
|
227
223
|
"set +e",
|
|
228
|
-
`if ! git diff --quiet ${range}
|
|
224
|
+
`if ! git diff --quiet ${range} --${paths}; then`,
|
|
229
225
|
` mkdir -p "$(dirname ${quote(report)})"`,
|
|
230
|
-
` git diff --binary ${range}
|
|
226
|
+
` git diff --binary --no-color --no-ext-diff --no-textconv --src-prefix=a/ --dst-prefix=b/ ${range} --${paths} | git apply --whitespace=nowarn${reverse ? " -R" : ""} 2> ${error}`,
|
|
231
227
|
" code=$?",
|
|
232
228
|
" if [ \"$code\" -ne 0 ]; then",
|
|
233
229
|
" {",
|
|
234
230
|
` printf '%s\\n\\n' ${quote(failure)}`,
|
|
235
|
-
` if [ -s ${error} ]; then cat ${error}; else printf 'Reverting it exited %s and produced no output.\\n' "$code"; fi`,
|
|
231
|
+
` if [ -s ${error} ]; then cat ${error}; else printf '${reverse ? "Reverting" : "Applying"} it exited %s and produced no output.\\n' "$code"; fi`,
|
|
236
232
|
` } > ${quote(report)}`,
|
|
237
233
|
" fi",
|
|
238
234
|
` rm -f ${error}`,
|
|
@@ -241,6 +237,14 @@ const revertScript = (commit, report, failure) => {
|
|
|
241
237
|
].join("\n");
|
|
242
238
|
};
|
|
243
239
|
/**
|
|
240
|
+
* Reverse-apply `commit`'s changes to the working tree. A failure — the
|
|
241
|
+
* patch no longer applies — writes `failure` and git's error to `report`,
|
|
242
|
+
* since a failed revert and one with nothing to undo both leave the tree clean.
|
|
243
|
+
*/
|
|
244
|
+
const revertScript = (commit, report, failure) => patchScript(commit, report, failure, true);
|
|
245
|
+
/** Apply `commit`'s changes outside `exclude` to the working tree; a failure is reported as `revertScript`'s is. */
|
|
246
|
+
const applyScript = (commit, report, failure, exclude = []) => patchScript(commit, report, failure, false, exclude);
|
|
247
|
+
/**
|
|
244
248
|
* Undo the edits one commit made to `paths`: each goes back to its content
|
|
245
249
|
* before `commit` — or away, when `commit` added it — but only while it is
|
|
246
250
|
* still exactly as `commit` left it. A path changed since is left alone.
|
|
@@ -17188,6 +17192,7 @@ const agentWithSkills = (name, prompt, options = {}) => {
|
|
|
17188
17192
|
return agent(name, withSkills(skillsFor(name).join(", "), body), options);
|
|
17189
17193
|
};
|
|
17190
17194
|
const unwindFailure = (commit) => `gtd could not unwind ${commit} out of your working tree.`;
|
|
17195
|
+
const naiveApplyFailure = (commit) => `gtd could not apply the naive pass ${commit} to the working tree; it is built fresh instead.`;
|
|
17191
17196
|
const idleMessage = () => `No active gtd process.
|
|
17192
17197
|
|
|
17193
17198
|
To start one, make ANY change — a hand-edit to real code, a scratch
|
|
@@ -17431,9 +17436,11 @@ ${strays.map((path) => ` - \`${displayPath(path)}\``).join("\n")}
|
|
|
17431
17436
|
- Introduce no new flag read and add no second read of an existing one:
|
|
17432
17437
|
every match of \`${pattern}\` may move, never increase in number`;
|
|
17433
17438
|
const withRules = (rules) => rules === void 0 ? "" : `${rules}\n`;
|
|
17434
|
-
const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false }) => `${stateFileRules}
|
|
17439
|
+
const refactorBuildingPrompt = ({ pkg, strays, pattern, reference, feedback = false, note = false }) => `${stateFileRules}
|
|
17435
17440
|
- The package to implement is \`${pkg}\`; never delete it
|
|
17436
|
-
${
|
|
17441
|
+
${note ? `- The package's \`## Refactor note\` section names the seams to open and the
|
|
17442
|
+
shape each takes, written from the whole naive pass: build exactly those
|
|
17443
|
+
` : ""}${feedback ? `- The package carries a \`## Review feedback\` section: a human's review of the
|
|
17437
17444
|
last refactor. Act on it, within the rules below
|
|
17438
17445
|
` : ""}${refactorRules(strays, pattern)}
|
|
17439
17446
|
${reference === void 0 ? "" : `- Reference only: \`git show ${reference} -- ${strays.map(quote).join(" ")}\` shows an
|
|
@@ -18036,8 +18043,8 @@ const naivePassPrompt = (retry) => `${stateFileRules}
|
|
|
18036
18043
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18037
18044
|
${criteriaBinding(false)}
|
|
18038
18045
|
- Implement them the quickest way that works — the naive version. Do not
|
|
18039
|
-
restructure, abstract or refactor around the change;
|
|
18040
|
-
|
|
18046
|
+
restructure, abstract or refactor around the change; it may land exactly as
|
|
18047
|
+
you leave it, and a reviewer makes it fit
|
|
18041
18048
|
- Work breadth-first: rough out every part of the change before finishing any
|
|
18042
18049
|
one, so a run cut off midway still shows the whole shape
|
|
18043
18050
|
- Write behaviour-level acceptance tests, at least one per criterion, each
|
|
@@ -18052,21 +18059,50 @@ ${criteriaBinding(false)}
|
|
|
18052
18059
|
- Leave \`.gtd/\` untouched
|
|
18053
18060
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
18054
18061
|
${retry === void 0 ? "" : naiveRetry(retry)}`;
|
|
18055
|
-
const
|
|
18062
|
+
const carries = (rebuild) => (rebuild.acceptance?.ids.length ?? 0) > 0;
|
|
18063
|
+
const rebuildLines = ({ note, acceptance }) => `${note === void 0 ? "" : `- The refactor note (\`.gtd/REFACTOR-NOTE.md\`) named the seams the refactor
|
|
18064
|
+
opened; plug the change into them:
|
|
18065
|
+
|
|
18066
|
+
${note.trimEnd().split("\n").map((line) => line === "" ? "" : ` > ${line}`).join("\n")}
|
|
18067
|
+
|
|
18068
|
+
`}${acceptance === void 0 || !carries({ acceptance }) ? "" : `- Carry over the naive pass's acceptance tests for ${acceptance.ids.join(", ")}, the
|
|
18069
|
+
one part of it you take as it is: \`git show ${acceptance.hash} -- ${acceptance.tests.map(quote).join(" ")}\`.
|
|
18070
|
+
Copy each test with its \`[AC-<n>]\` name into the tree as the refactor left
|
|
18071
|
+
it — check a file out of the tip whole only where the refactor did not touch
|
|
18072
|
+
it — changing only what reaches the release flag. A turn whose changed tests
|
|
18073
|
+
leave an ID out is refused, and the suite must be green with them
|
|
18074
|
+
`}`;
|
|
18075
|
+
const refactorNotePrompt = (reference, strays) => `${stateFileRules}
|
|
18076
|
+
- The seam judge ruled the naive pass refactor-first: it is never applied.
|
|
18077
|
+
A behaviour-neutral refactor lands first, then the change is rebuilt on
|
|
18078
|
+
the refactored base. Both turns read the note you write
|
|
18079
|
+
- Read the whole naive pass: \`git show ${reference}\`. It changed these
|
|
18080
|
+
existing files outside the allowlist:
|
|
18081
|
+
${listed(strays)}
|
|
18082
|
+
- Write \`.gtd/REFACTOR-NOTE.md\`: which seams the refactor opens (the existing
|
|
18083
|
+
code to restructure, by path and symbol) and the shape each takes — the
|
|
18084
|
+
signatures and where the new behaviour will plug in. Name only the files
|
|
18085
|
+
listed above: the refactor may change no other path. Never the feature's own
|
|
18086
|
+
behaviour, and no flag read: the refactor adds neither
|
|
18087
|
+
- Change nothing else — a turn that does is refused
|
|
18088
|
+
- Leave everything uncommitted and finish your turn — do not commit
|
|
18089
|
+
`;
|
|
18090
|
+
const afterRefactorLine = (rebuild) => `- A reviewed refactor has just landed and may already carry part of the change,
|
|
18091
|
+
though never a release flag. Build only what is still missing, held to every
|
|
18092
|
+
rule below; if nothing is, ${carries(rebuild) ? "carry over the acceptance tests below and change nothing else" : "change nothing and finish your turn"}
|
|
18093
|
+
`;
|
|
18094
|
+
const implementBuildingPrompt = (reference, tier, amend, afterRefactor, flagMode = true, rebuild = {}) => `${stateFileRules}
|
|
18056
18095
|
- Read \`.gtd/REQUIREMENTS.md\`: the settled requirements for this change
|
|
18057
18096
|
${criteriaBinding(true)}
|
|
18058
|
-
${afterRefactor === true ?
|
|
18059
|
-
though never a release flag. Build only what is still missing, held to every
|
|
18060
|
-
rule below; if nothing is, change nothing and finish your turn
|
|
18061
|
-
` : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
|
|
18097
|
+
${afterRefactor === true ? afterRefactorLine(rebuild) : ""}${amend === true ? `- The change already exists in the tree from an earlier lap; amend it to meet
|
|
18062
18098
|
the re-derived requirements instead of rewriting it
|
|
18063
18099
|
` : flagMode ? `- Implement them properly, fitting the codebase's seams and conventions
|
|
18064
18100
|
` : `- Implement them the quickest way that works — the naive version. Do not
|
|
18065
18101
|
restructure, abstract or refactor around the change
|
|
18066
18102
|
`}${reference === void 0 || amend === true ? "" : `- Reference only: \`git show ${reference}\` shows an earlier naive attempt.
|
|
18067
18103
|
Read it for ideas; never apply, cherry-pick or check it out — write
|
|
18068
|
-
the change fresh
|
|
18069
|
-
`}${!flagMode ? "- Add no release flag — this project ships without release flags" : tier === "low" ? "- Add no release flag — this change is low risk tier" : amend === true ? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag" : `- Put the new behaviour behind a new release flag, following the
|
|
18104
|
+
the change fresh${carries(rebuild) ? ", its acceptance tests excepted (below)" : ""}
|
|
18105
|
+
`}${rebuildLines(rebuild)}${!flagMode ? "- Add no release flag — this project ships without release flags" : tier === "low" ? "- Add no release flag — this change is low risk tier" : amend === true ? "- Keep the release flag the earlier lap added, if it added one; otherwise put\n the behaviour behind a new release flag" : `- Put the new behaviour behind a new release flag, following the
|
|
18070
18106
|
repository's flag conventions`}
|
|
18071
18107
|
- Write the tests first, then the code that makes them pass. Name each
|
|
18072
18108
|
criterion's ID in the test that covers it (\`[AC-3] …\`): once the suite is
|
|
@@ -18087,12 +18123,13 @@ An edit together with an approval counts as steer. Only
|
|
|
18087
18123
|
\`.gtd/REQUIREMENTS.md\` is meant to change here: any other edit lands on the
|
|
18088
18124
|
branch unjudged.
|
|
18089
18125
|
`;
|
|
18090
|
-
|
|
18126
|
+
/** `judged`: whether an overruled pass still meets the seam judge (`flagMode` on). */
|
|
18127
|
+
const seamSteerMessage = (retry, judged = true) => isFault(retry) ? `The naive pass failed three times and the retries are spent. The last
|
|
18091
18128
|
attempt's tests (\`git show ${retry.hash}\`) fall short:
|
|
18092
18129
|
${retry.fault}
|
|
18093
18130
|
|
|
18094
18131
|
${steerChoices(`land a clean tree to go on with the last attempt as it is:
|
|
18095
|
-
\`seam.check\` runs its suite and the seam judge rules on its shape. Use it
|
|
18132
|
+
\`seam.check\` runs its suite and ${judged ? "the seam judge rules on its shape" : "it lands as the implementation"}. Use it
|
|
18096
18133
|
when the rule cannot fit this change (tests the globs do not see, or no
|
|
18097
18134
|
entry point that existed before). Risk: a criterion may have no test that
|
|
18098
18135
|
survives a later refactor — the choice is recorded in history.`)}` : isRed(retry) ? `The naive pass failed three times and the retries are spent. The last
|
|
@@ -18103,7 +18140,7 @@ Two ways forward (then run \`gtd land\`):
|
|
|
18103
18140
|
- **Steer** — edit \`.gtd/REQUIREMENTS.md\` to redirect the change; the naive
|
|
18104
18141
|
pass runs again with a fresh retry budget
|
|
18105
18142
|
- **Overrule** — land a clean tree to go on with the last attempt as it is:
|
|
18106
|
-
the seam judge rules on its shape, and the implementation's health loop
|
|
18143
|
+
${judged ? "the seam judge rules on its shape" : "it lands as the implementation"}, and the implementation's health loop
|
|
18107
18144
|
must turn the suite green. Risk: the criteria's tests never passed on the
|
|
18108
18145
|
naive pass — the choice is recorded in history.
|
|
18109
18146
|
|
|
@@ -18119,9 +18156,8 @@ Two ways forward (then run \`gtd land\`):
|
|
|
18119
18156
|
- **Steer** — edit \`.gtd/REQUIREMENTS.md\` to redirect the change; the naive
|
|
18120
18157
|
pass runs again with a fresh retry budget (the risk tier stays as the
|
|
18121
18158
|
design gate settled it; a tier edit here is not applied)
|
|
18122
|
-
- **Overrule** — land a clean tree to accept the last attempt
|
|
18123
|
-
the implementation
|
|
18124
|
-
and no refactor. Risk: this ships the seam the judge objected to,
|
|
18159
|
+
- **Overrule** — land a clean tree to accept the last attempt as it is: it
|
|
18160
|
+
lands as the implementation, with no seam gate and no refactor. Risk: this ships the seam the judge objected to,
|
|
18125
18161
|
unaddressed — the choice is recorded in history.
|
|
18126
18162
|
|
|
18127
18163
|
An edit together with an approval counts as steer. Only
|
|
@@ -18175,6 +18211,7 @@ const ESCALATION = ".gtd/ESCALATION.md";
|
|
|
18175
18211
|
const REQUIREMENTS = ".gtd/REQUIREMENTS.md";
|
|
18176
18212
|
const REVIEW = ".gtd/REVIEW.md";
|
|
18177
18213
|
const REFACTOR = ".gtd/REFACTOR.md";
|
|
18214
|
+
const REFACTOR_NOTE = ".gtd/REFACTOR-NOTE.md";
|
|
18178
18215
|
const QUALITY = ".gtd/QUALITY.md";
|
|
18179
18216
|
const planner = () => env.plannerModel ?? "";
|
|
18180
18217
|
const coder = () => env.coderModel ?? "";
|
|
@@ -37692,7 +37729,11 @@ const ShippedOptions = Struct({
|
|
|
37692
37729
|
testCommand: setting,
|
|
37693
37730
|
plannerModel: setting,
|
|
37694
37731
|
coderModel: setting,
|
|
37695
|
-
flagMode: optionalWith(Boolean$, { exact: true })
|
|
37732
|
+
flagMode: optionalWith(Boolean$, { exact: true }),
|
|
37733
|
+
contextLimit: optionalWith(Struct({
|
|
37734
|
+
soft: optionalWith(Number$, { exact: true }),
|
|
37735
|
+
hard: optionalWith(Number$, { exact: true })
|
|
37736
|
+
}), { exact: true })
|
|
37696
37737
|
});
|
|
37697
37738
|
const over = (base, options) => ({
|
|
37698
37739
|
...base,
|
|
@@ -37704,12 +37745,13 @@ const flagModeOn = () => {
|
|
|
37704
37745
|
if (value !== "true" && value !== "false") refuse(`gtd: flagMode must be "true" or "false", got ${JSON.stringify(value)}`);
|
|
37705
37746
|
return value === "true";
|
|
37706
37747
|
};
|
|
37748
|
+
const limited = (flow, context) => context === void 0 ? flow : () => scope({ context }, flow);
|
|
37707
37749
|
/** A shipped flow as a factory: its options override the settings it declares, each in its own kind. */
|
|
37708
37750
|
const shipped = (flow, doors, base) => defineWorkflow(standardSchemaV1(ShippedOptions, {
|
|
37709
37751
|
onExcessProperty: "error",
|
|
37710
37752
|
errors: "all"
|
|
37711
|
-
}), (options) => ({
|
|
37712
|
-
flow,
|
|
37753
|
+
}), ({ contextLimit, ...options }) => ({
|
|
37754
|
+
flow: limited(flow, contextLimit),
|
|
37713
37755
|
defaults: over(defaults, options),
|
|
37714
37756
|
envDefaults: over(envDefaults, options),
|
|
37715
37757
|
doors,
|
|
@@ -37722,14 +37764,14 @@ const FlagOptions = Struct({
|
|
|
37722
37764
|
const flagShipped = (flow, name) => defineWorkflow(standardSchemaV1(FlagOptions, {
|
|
37723
37765
|
onExcessProperty: "error",
|
|
37724
37766
|
errors: "all"
|
|
37725
|
-
}), (options) => {
|
|
37767
|
+
}), ({ contextLimit, ...options }) => {
|
|
37726
37768
|
const flagDefaults = {
|
|
37727
37769
|
...defaults,
|
|
37728
37770
|
flag: "",
|
|
37729
37771
|
flagRegistry: defaultFlagRegistry
|
|
37730
37772
|
};
|
|
37731
37773
|
return {
|
|
37732
|
-
flow,
|
|
37774
|
+
flow: limited(flow, contextLimit),
|
|
37733
37775
|
defaults: over(flagDefaults, options),
|
|
37734
37776
|
envDefaults: over(envDefaults, options),
|
|
37735
37777
|
doors: { [name]: {
|
|
@@ -38468,7 +38510,8 @@ const plan = (shape, options) => {
|
|
|
38468
38510
|
return {
|
|
38469
38511
|
pattern,
|
|
38470
38512
|
strays,
|
|
38471
|
-
reference: options.reference?.trim() ? options.reference : void 0
|
|
38513
|
+
reference: options.reference?.trim() ? options.reference : void 0,
|
|
38514
|
+
note: options.note?.trim() ? options.note : void 0
|
|
38472
38515
|
};
|
|
38473
38516
|
};
|
|
38474
38517
|
const guardOf = ({ pattern, strays }, entry) => () => {
|
|
@@ -38477,13 +38520,22 @@ const guardOf = ({ pattern, strays }, entry) => () => {
|
|
|
38477
38520
|
};
|
|
38478
38521
|
/** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
|
|
38479
38522
|
const refactorLap = async (p, guard, feedback) => {
|
|
38480
|
-
const { pattern, strays, reference } = p;
|
|
38481
|
-
await run("deriving", deriveScript([
|
|
38482
|
-
|
|
38483
|
-
|
|
38484
|
-
|
|
38485
|
-
|
|
38486
|
-
|
|
38523
|
+
const { pattern, strays, reference, note } = p;
|
|
38524
|
+
await run("deriving", deriveScript([
|
|
38525
|
+
...packageLines(strays, pattern, reference),
|
|
38526
|
+
...note === void 0 ? [] : [
|
|
38527
|
+
"## Refactor note",
|
|
38528
|
+
"",
|
|
38529
|
+
...demoteHeadings(note),
|
|
38530
|
+
""
|
|
38531
|
+
],
|
|
38532
|
+
...feedback === void 0 ? [] : [
|
|
38533
|
+
"## Review feedback",
|
|
38534
|
+
"",
|
|
38535
|
+
...demoteHeadings(feedback),
|
|
38536
|
+
""
|
|
38537
|
+
]
|
|
38538
|
+
]), {
|
|
38487
38539
|
label: "Deriving the refactor package",
|
|
38488
38540
|
file: REFACTOR_PACKAGE
|
|
38489
38541
|
});
|
|
@@ -38492,7 +38544,8 @@ const refactorLap = async (p, guard, feedback) => {
|
|
|
38492
38544
|
strays,
|
|
38493
38545
|
pattern,
|
|
38494
38546
|
reference,
|
|
38495
|
-
feedback: feedback !== void 0
|
|
38547
|
+
feedback: feedback !== void 0,
|
|
38548
|
+
note: note !== void 0
|
|
38496
38549
|
}), {
|
|
38497
38550
|
label: "Refactoring",
|
|
38498
38551
|
model: coder(),
|
|
@@ -38578,13 +38631,16 @@ const COVERED_MIN_P = .7;
|
|
|
38578
38631
|
/** Covering turns before the process rests at `uncovered`; a human edit there refills them. */
|
|
38579
38632
|
const COVERING_RETRIES = 2;
|
|
38580
38633
|
const frozen = () => requireCriteriaFrozen(REQUIREMENTS);
|
|
38581
|
-
const green = () => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
38634
|
+
const green = (guard) => healthy(() => agentWithSkills("fix.suite.fixing", implementFixPrompt(), {
|
|
38582
38635
|
label: "Fixing the check",
|
|
38583
38636
|
file: FEEDBACK,
|
|
38584
38637
|
model: coder(),
|
|
38585
38638
|
system: builderSystem()
|
|
38586
38639
|
}), {
|
|
38587
|
-
guard:
|
|
38640
|
+
guard: () => {
|
|
38641
|
+
frozen();
|
|
38642
|
+
guard?.();
|
|
38643
|
+
},
|
|
38588
38644
|
guardRests: false
|
|
38589
38645
|
});
|
|
38590
38646
|
const testsSince = (base) => changesSince(base).filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path)).sort((a, b) => Number(/\bAC-\d/.test(a.after)) - Number(/\bAC-\d/.test(b.after))).map((c) => `=== ${c.path} (${c.status})\n${c.after}`).join("\n");
|
|
@@ -38698,16 +38754,20 @@ const addedLines = (change) => {
|
|
|
38698
38754
|
const before = new Set((change.before ?? "").split("\n"));
|
|
38699
38755
|
return change.after.split("\n").filter((line) => !before.has(line)).join("\n");
|
|
38700
38756
|
};
|
|
38757
|
+
const acceptanceTests = (changes) => changes.filter((c) => c.after !== void 0 && isCode$1(c.path) && isTestFile(c.path));
|
|
38758
|
+
const idsOf = (file) => (criteriaOf(read(file) ?? "") ?? []).map((c) => c.id).filter((id) => id !== "");
|
|
38759
|
+
const unnamed = (ids, tests) => {
|
|
38760
|
+
const lines = tests.map(addedLines);
|
|
38761
|
+
return ids.filter((id) => !lines.some((text) => named(id, text)));
|
|
38762
|
+
};
|
|
38701
38763
|
/**
|
|
38702
38764
|
* Why `tip`'s tests fall short, or `undefined`: a criterion ID of `file` no
|
|
38703
38765
|
* test line of the pass names, or a test importing a file the pass added —
|
|
38704
38766
|
* naive structure a reset and refactor replaces, which the tests must survive.
|
|
38705
38767
|
*/
|
|
38706
38768
|
const acceptanceFault = (file, tip) => {
|
|
38707
|
-
const
|
|
38708
|
-
const
|
|
38709
|
-
const lines = tests.map(addedLines);
|
|
38710
|
-
const untested = ids.filter((id) => !lines.some((text) => named(id, text)));
|
|
38769
|
+
const tests = acceptanceTests(tip.changes);
|
|
38770
|
+
const untested = unnamed(idsOf(file), tests);
|
|
38711
38771
|
if (untested.length > 0) return `acceptance-tests: no test of the naive pass names ${untested.join(", ")} — write a behaviour-level test for each criterion with its ID in the test or scenario name, e.g. "[${untested[0]}] …"`;
|
|
38712
38772
|
const added = tip.changes.filter((c) => c.status === "added" && isCode$1(c.path) && !isTestFile(c.path)).map((c) => c.path);
|
|
38713
38773
|
const reaching = tests.filter((test) => holdsCallSites(test.path)).flatMap((test) => {
|
|
@@ -38716,6 +38776,41 @@ const acceptanceFault = (file, tip) => {
|
|
|
38716
38776
|
});
|
|
38717
38777
|
if (reaching.length > 0) return `acceptance-internals: these tests import files the naive pass added — test through code that existed before it (a public entry point, an existing module's exports):\n${reaching.join("\n")}`;
|
|
38718
38778
|
};
|
|
38779
|
+
/** What a rebuild carries over from the naive pass `tip`: the IDs its tests name, and those test files. */
|
|
38780
|
+
const naiveAcceptance = (tip) => {
|
|
38781
|
+
const ids = idsOf(REQUIREMENTS);
|
|
38782
|
+
const perTest = acceptanceTests(tip.changes).map((c) => {
|
|
38783
|
+
const lines = addedLines(c);
|
|
38784
|
+
return {
|
|
38785
|
+
path: c.path,
|
|
38786
|
+
ids: ids.filter((id) => named(id, lines))
|
|
38787
|
+
};
|
|
38788
|
+
});
|
|
38789
|
+
return {
|
|
38790
|
+
hash: tip.hash,
|
|
38791
|
+
tests: perTest.filter((c) => c.ids.length > 0).map((c) => c.path),
|
|
38792
|
+
ids: ids.filter((id) => perTest.some((c) => c.ids.includes(id)))
|
|
38793
|
+
};
|
|
38794
|
+
};
|
|
38795
|
+
/**
|
|
38796
|
+
* Refuse a rebuild whose test changes since `since` leave out an ID the naive
|
|
38797
|
+
* pass's tests named. A skipped test still counts: the gate reads text, never runs it.
|
|
38798
|
+
*/
|
|
38799
|
+
const requireAcceptance = (acceptance, since) => {
|
|
38800
|
+
const missing = unnamed(acceptance.ids, acceptanceTests(changesSince(since)));
|
|
38801
|
+
if (missing.length === 0) return;
|
|
38802
|
+
refuse(`gtd land: rebuild-acceptance: the rebuild's tests leave out ${missing.join(", ")} — carry over the naive pass's acceptance tests (\`git show ${acceptance.hash} -- ${acceptance.tests.map(quote).join(" ")}\`), each with its ID in the test or scenario name`);
|
|
38803
|
+
};
|
|
38804
|
+
/**
|
|
38805
|
+
* Refuse a fix turn that, on its own, drops an ID the naive pass's tests named
|
|
38806
|
+
* from a test file. Turn-local, so a person's edit at an escalation rest never
|
|
38807
|
+
* wedges later fix turns.
|
|
38808
|
+
*/
|
|
38809
|
+
const requireAcceptanceKept = (acceptance) => {
|
|
38810
|
+
const dropped = acceptance.ids.filter((id) => changes().filter((c) => isCode$1(c.path) && isTestFile(c.path)).some((c) => named(id, c.before ?? "") && !named(id, c.after ?? "")));
|
|
38811
|
+
if (dropped.length === 0) return;
|
|
38812
|
+
refuse(`gtd land: rebuild-acceptance: this turn drops ${dropped.join(", ")} from the tests — the naive pass's acceptance tests stay; fix the code, not them`);
|
|
38813
|
+
};
|
|
38719
38814
|
const restore = (path) => `if git cat-file -e ${quote(`HEAD:${path}`)} 2>/dev/null; then git checkout -q HEAD -- ${quote(path)}; else rm -f -- ${quote(path)}; fi`;
|
|
38720
38815
|
/**
|
|
38721
38816
|
* Run the suite with `tip`'s diff applied, then take it out again; resolves
|
|
@@ -38728,7 +38823,7 @@ const naiveGreen = (tip) => {
|
|
|
38728
38823
|
return check("check", [
|
|
38729
38824
|
...paths,
|
|
38730
38825
|
"dirty=$(git -c core.quotePath=false ls-files -m -d -o --exclude-standard)",
|
|
38731
|
-
`if ! git diff --binary ${quote(`${tip.hash}^`)} ${quote(tip.hash)} -- | git apply; then`,
|
|
38826
|
+
`if ! git diff --binary --no-color --no-ext-diff --no-textconv --src-prefix=a/ --dst-prefix=b/ ${quote(`${tip.hash}^`)} ${quote(tip.hash)} -- | git apply --whitespace=nowarn; then`,
|
|
38732
38827
|
" echo 'gtd: the kept naive pass no longer applies to HEAD; no test ran'",
|
|
38733
38828
|
" exit 1",
|
|
38734
38829
|
"fi",
|
|
@@ -38747,6 +38842,11 @@ const naiveGreen = (tip) => {
|
|
|
38747
38842
|
label: "Running the naive pass's tests"
|
|
38748
38843
|
});
|
|
38749
38844
|
};
|
|
38845
|
+
/** Resolves `false` when the tip no longer applies (the failure is in `.gtd/FEEDBACK.md`) or has nothing outside `.gtd/` to apply. */
|
|
38846
|
+
const applied = async (tip) => {
|
|
38847
|
+
await run("applying", applyScript(tip, FEEDBACK, naiveApplyFailure(tip), [".gtd"]), { label: "Applying the naive pass" });
|
|
38848
|
+
return !wrote(".gtd/FEEDBACK.md") && codeChanges().length > 0;
|
|
38849
|
+
};
|
|
38750
38850
|
function implement(first, tier, amend) {
|
|
38751
38851
|
const options = typeof first === "object" && first !== null ? first : {
|
|
38752
38852
|
reference: first,
|
|
@@ -38754,22 +38854,26 @@ function implement(first, tier, amend) {
|
|
|
38754
38854
|
amend
|
|
38755
38855
|
};
|
|
38756
38856
|
return scope("implement", async () => {
|
|
38757
|
-
|
|
38857
|
+
const since = head$1();
|
|
38858
|
+
const acceptance = options.rebuild?.acceptance;
|
|
38859
|
+
const built = options.naive === void 0 || !await applied(options.naive);
|
|
38860
|
+
if (built) await agentWithSkills("building", implementBuildingPrompt(options.reference, options.tier, options.amend, options.afterRefactor, flagModeOn(), options.rebuild), {
|
|
38758
38861
|
label: "Implementing",
|
|
38759
38862
|
model: coder(),
|
|
38760
38863
|
system: builderSystem(),
|
|
38761
38864
|
...options.afterRefactor === true ? { allowEmpty: true } : {}
|
|
38762
38865
|
});
|
|
38763
38866
|
requireCriteriaFrozen(REQUIREMENTS);
|
|
38764
|
-
|
|
38867
|
+
if (built && acceptance !== void 0) requireAcceptance(acceptance, since);
|
|
38868
|
+
await green(built && acceptance !== void 0 ? () => requireAcceptanceKept(acceptance) : void 0);
|
|
38765
38869
|
await requireCoverage();
|
|
38766
|
-
await run("closing", removeScript([REQUIREMENTS]), { label: "Closing out the implementation" });
|
|
38870
|
+
await run("closing", removeScript([REQUIREMENTS, REFACTOR_NOTE]), { label: "Closing out the implementation" });
|
|
38767
38871
|
});
|
|
38768
38872
|
}
|
|
38769
38873
|
/** Resolves `true` when the human steered (edited), `false` on a clean overrule. */
|
|
38770
38874
|
const steer = async (retry) => {
|
|
38771
38875
|
await human("steer", {
|
|
38772
|
-
message: seamSteerMessage(retry),
|
|
38876
|
+
message: seamSteerMessage(retry, flagModeOn()),
|
|
38773
38877
|
label: "Steer or overrule the naive pass",
|
|
38774
38878
|
file: REQUIREMENTS,
|
|
38775
38879
|
acceptClean: true
|
|
@@ -38813,16 +38917,23 @@ const vetted = async (tip, vetting) => {
|
|
|
38813
38917
|
/** The judge's ruling on a vetted pass: the decision, or the next attempt's verdict. */
|
|
38814
38918
|
const ruled = async (tip, again) => {
|
|
38815
38919
|
const shape = await judgeSeam(tip);
|
|
38816
|
-
if (shape.choice === "land") return {
|
|
38920
|
+
if (shape.choice === "land") return {
|
|
38921
|
+
reference: tip.hash,
|
|
38922
|
+
tip
|
|
38923
|
+
};
|
|
38817
38924
|
if (shape.choice === "refactor-first") return {
|
|
38818
38925
|
reference: tip.hash,
|
|
38926
|
+
tip,
|
|
38819
38927
|
shape
|
|
38820
38928
|
};
|
|
38821
38929
|
const next = await again({
|
|
38822
38930
|
strays: shape.strays,
|
|
38823
38931
|
p: shape.judgment?.answers.shape?.p ?? 0
|
|
38824
38932
|
});
|
|
38825
|
-
return next.kind === "overruled" ? {
|
|
38933
|
+
return next.kind === "overruled" ? {
|
|
38934
|
+
reference: tip.hash,
|
|
38935
|
+
tip
|
|
38936
|
+
} : next;
|
|
38826
38937
|
};
|
|
38827
38938
|
const decide = () => scope("seam", async () => {
|
|
38828
38939
|
const vetting = {
|
|
@@ -38833,16 +38944,33 @@ const decide = () => scope("seam", async () => {
|
|
|
38833
38944
|
for (;;) {
|
|
38834
38945
|
await naivePass(retry);
|
|
38835
38946
|
const tip = kept("naive");
|
|
38836
|
-
const outcome = await vetted(tip, vetting) ?? await ruled(tip, vetting.again)
|
|
38947
|
+
const outcome = await vetted(tip, vetting) ?? (flagModeOn() ? await ruled(tip, vetting.again) : {
|
|
38948
|
+
reference: tip.hash,
|
|
38949
|
+
tip
|
|
38950
|
+
});
|
|
38837
38951
|
if (!("kind" in outcome)) return outcome;
|
|
38838
38952
|
retry = outcome.kind === "retry" ? outcome.retry : void 0;
|
|
38839
38953
|
}
|
|
38840
38954
|
});
|
|
38955
|
+
/** Write the refactor note from the whole naive pass; a turn that changes anything else is refused. */
|
|
38956
|
+
const refactorNote = async (reference, strays) => {
|
|
38957
|
+
await agent("note", refactorNotePrompt(reference, strays), {
|
|
38958
|
+
label: "Writing the refactor note",
|
|
38959
|
+
file: REFACTOR_NOTE,
|
|
38960
|
+
model: coder(),
|
|
38961
|
+
system: builderSystem()
|
|
38962
|
+
});
|
|
38963
|
+
const others = changes().map((c) => c.path).filter((path) => path !== REFACTOR_NOTE);
|
|
38964
|
+
const note = read(".gtd/REFACTOR-NOTE.md") ?? "";
|
|
38965
|
+
if (others.length > 0 || note.trim() === "") refuse(`gtd land: refactor-note: write ${REFACTOR_NOTE} and change nothing else${others.length > 0 ? ` — revert these:\n${others.map((path) => ` - ${path}`).join("\n")}` : ""}`);
|
|
38966
|
+
return note;
|
|
38967
|
+
};
|
|
38841
38968
|
const seamBuild = async (base) => {
|
|
38842
|
-
const { reference, shape } = await decide();
|
|
38969
|
+
const { reference, tip, shape } = await decide();
|
|
38843
38970
|
if (shape === void 0) return {
|
|
38844
38971
|
reference,
|
|
38845
|
-
base
|
|
38972
|
+
base,
|
|
38973
|
+
land: true
|
|
38846
38974
|
};
|
|
38847
38975
|
if (shape.unreadable.length > 0) {
|
|
38848
38976
|
await scope("seam", () => human("by-hand", {
|
|
@@ -38852,18 +38980,29 @@ const seamBuild = async (base) => {
|
|
|
38852
38980
|
}));
|
|
38853
38981
|
return {
|
|
38854
38982
|
reference,
|
|
38855
|
-
base
|
|
38983
|
+
base,
|
|
38984
|
+
rebuild: { acceptance: naiveAcceptance(tip) }
|
|
38856
38985
|
};
|
|
38857
38986
|
}
|
|
38858
|
-
|
|
38987
|
+
const strays = refactorStrays(shape);
|
|
38988
|
+
if (strays.length === 0) return {
|
|
38859
38989
|
reference,
|
|
38860
|
-
base
|
|
38990
|
+
base,
|
|
38991
|
+
rebuild: { acceptance: naiveAcceptance(tip) }
|
|
38861
38992
|
};
|
|
38862
|
-
const
|
|
38993
|
+
const note = await scope("seam", () => refactorNote(reference, strays));
|
|
38994
|
+
const refactored = await slice("refactor", () => reviewedRefactor(shape, {
|
|
38995
|
+
reference,
|
|
38996
|
+
note
|
|
38997
|
+
}));
|
|
38863
38998
|
return {
|
|
38864
38999
|
reference,
|
|
38865
39000
|
base: head$1(),
|
|
38866
|
-
refactored
|
|
39001
|
+
refactored,
|
|
39002
|
+
rebuild: {
|
|
39003
|
+
note,
|
|
39004
|
+
acceptance: naiveAcceptance(tip)
|
|
39005
|
+
}
|
|
38867
39006
|
};
|
|
38868
39007
|
};
|
|
38869
39008
|
//#endregion
|
|
@@ -38873,17 +39012,20 @@ const unwind = () => {
|
|
|
38873
39012
|
const commit = head$1();
|
|
38874
39013
|
return run("unwind", revertScript(commit, FEEDBACK, unwindFailure(commit)), { label: "Unwinding your input" });
|
|
38875
39014
|
};
|
|
38876
|
-
/** A
|
|
39015
|
+
/** A naive pass (judged only with `flagMode` on), then implement and the quality lap; design must have run since `firstBase`. */
|
|
38877
39016
|
const buildFeature = async (firstBase) => {
|
|
38878
39017
|
const tier = riskTierOn() ? recordRisk() : void 0;
|
|
38879
|
-
const seam =
|
|
39018
|
+
const seam = await seamBuild(firstBase);
|
|
38880
39019
|
await slice("feature", async () => {
|
|
38881
39020
|
await implement({
|
|
38882
39021
|
reference: seam.reference,
|
|
38883
39022
|
tier,
|
|
38884
|
-
afterRefactor: seam.refactored
|
|
39023
|
+
afterRefactor: seam.refactored,
|
|
39024
|
+
naive: seam.land === true ? seam.reference : void 0,
|
|
39025
|
+
rebuild: seam.rebuild
|
|
38885
39026
|
});
|
|
38886
|
-
const
|
|
39027
|
+
const carried = seam.rebuild?.acceptance?.tests ?? [];
|
|
39028
|
+
const base = seam.refactored === true && !changesSince(seam.base).some((c) => isCode$1(c.path) && !carried.includes(c.path)) ? firstBase : seam.base;
|
|
38887
39029
|
await tail(settleQuality, {
|
|
38888
39030
|
green: true,
|
|
38889
39031
|
lensBase: base
|
|
@@ -39126,4 +39268,4 @@ var workflows_default = feature;
|
|
|
39126
39268
|
*/
|
|
39127
39269
|
const doors = {};
|
|
39128
39270
|
//#endregion
|
|
39129
|
-
export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, criteriaOf, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireCriteria, requireCriteriaFrozen, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, statedCriteria, steering, summary, tail, threads, triage, unwind };
|
|
39271
|
+
export { COMMENT, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR, REFACTOR_NOTE, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, acceptanceFault, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, awaitReview, base, baseline, buildFeature, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, criteriaOf, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, gate, guarded, handOffReview, handOffTail, healthy, holdsCallSites, implement, isTestFile, judgeSeam, lensEntriesOf, lensOf, lensesOf, naiveAcceptance, naivePass, ordinaryStart, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorNote, refactorStrays, requireAcceptance, requireAcceptanceKept, requireCriteria, requireCriteriaFrozen, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, skipDesign, statedCriteria, steering, summary, tail, threads, triage, unwind };
|
package/package.json
CHANGED
|
@@ -223,9 +223,9 @@ workflow.
|
|
|
223
223
|
## Worked example: add an approval gate before building
|
|
224
224
|
|
|
225
225
|
In the bundled default, `planAndBuild` runs the design phase, then
|
|
226
|
-
`buildFeature`: the naive pass and its seam judge, then the implementation
|
|
227
|
-
the quality lap, ending with no human review. Add a
|
|
228
|
-
build:
|
|
226
|
+
`buildFeature`: the naive pass and its seam judge, then the implementation (the
|
|
227
|
+
pass itself on `land`) and the quality lap, ending with no human review. Add a
|
|
228
|
+
human sign-off before the build:
|
|
229
229
|
|
|
230
230
|
```ts
|
|
231
231
|
const planAndBuild = async (firstBase: string): Promise<void> => {
|
package/src/cli/Cli.test.ts
CHANGED
|
@@ -1106,6 +1106,7 @@ describe("standaloneKinds / needsOf", () => {
|
|
|
1106
1106
|
"uncheck",
|
|
1107
1107
|
"install",
|
|
1108
1108
|
"loop",
|
|
1109
|
+
"contextHook",
|
|
1109
1110
|
"judgeRun",
|
|
1110
1111
|
"strays",
|
|
1111
1112
|
])
|
|
@@ -1117,6 +1118,7 @@ describe("standaloneKinds / needsOf", () => {
|
|
|
1117
1118
|
expect(needsOf("uncheck")).toBe("fs")
|
|
1118
1119
|
expect(needsOf("install")).toBe("none")
|
|
1119
1120
|
expect(needsOf("loop")).toBe("none")
|
|
1121
|
+
expect(needsOf("contextHook")).toBe("none")
|
|
1120
1122
|
expect(needsOf("judgeRun")).toBe("none")
|
|
1121
1123
|
expect(needsOf("strays")).toBe("none")
|
|
1122
1124
|
for (const kind of [
|
|
@@ -1202,19 +1204,24 @@ describe("renderHelp", () => {
|
|
|
1202
1204
|
|
|
1203
1205
|
it("README.md and COMPATIBILITY.md list exactly the commands", () => {
|
|
1204
1206
|
const commands = new Set(
|
|
1205
|
-
[...renderHelp().matchAll(/^ {2}([a-z]+(?: [a-z]+)?)(?= )/gm)].map(
|
|
1207
|
+
[...renderHelp().matchAll(/^ {2}([a-z]+(?:-[a-z]+)?(?: [a-z]+)?)(?= )/gm)].map(
|
|
1208
|
+
([, name]) => name,
|
|
1209
|
+
),
|
|
1206
1210
|
)
|
|
1207
1211
|
expect(commands).toContain("judge answer")
|
|
1212
|
+
expect(commands).toContain("context-hook")
|
|
1208
1213
|
const listed = (file: string, list: RegExp): Set<string> => {
|
|
1209
1214
|
const doc = readFileSync(resolve(import.meta.dirname, file), "utf8")
|
|
1210
1215
|
const match = doc.match(list)
|
|
1211
1216
|
expect(match, file).not.toBeNull()
|
|
1212
|
-
return new Set(
|
|
1217
|
+
return new Set(
|
|
1218
|
+
[...match![1]!.matchAll(/`([a-z]+(?:-[a-z]+)?(?: [a-z]+)?)`/g)].map(([, name]) => name!),
|
|
1219
|
+
)
|
|
1213
1220
|
}
|
|
1214
1221
|
expect(
|
|
1215
1222
|
listed("../../README.md", /The `gtd` bin's commands — ([\s\S]*?) — are\s+listed/),
|
|
1216
1223
|
).toEqual(commands)
|
|
1217
|
-
expect(listed("../../COMPATIBILITY.md", /every command \(([\s\S]*?)\)
|
|
1224
|
+
expect(listed("../../COMPATIBILITY.md", /every command \(([\s\S]*?)\),\s+option/)).toEqual(
|
|
1218
1225
|
commands,
|
|
1219
1226
|
)
|
|
1220
1227
|
})
|