@pmelab/gtd 29.2.1 → 29.2.3
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 +32 -29
- package/claude/hooks/drive.ts +36 -22
- package/claude/hooks/paste.ts +1 -1
- package/claude/hooks/register.tsx +30 -3
- package/claude/hooks/ship.ts +123 -25
- package/claude/hooks/sketch.ts +6 -0
- package/claude/hooks/wording.ts +2 -0
- package/dist/chunks/{reserved-BzeofpoC.mjs → reserved-BduvUQxQ.mjs} +24 -10
- package/dist/chunks/{tail-eS_ZSImR.mjs → tail-pgq1Fbbv.mjs} +1 -1
- package/dist/workflows/cleanup.mjs +2 -2
- package/dist/workflows/feature.mjs +44 -8
- 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 +66 -16
- package/package.json +1 -1
- package/src/workflows/feature/refactor-review.feature +37 -3
- package/src/workflows/feature/refactor.test.ts +183 -16
- package/src/workflows/feature/refactor.ts +66 -8
- package/src/workflows/shared/text.test.ts +30 -7
- package/src/workflows/shared/text.ts +31 -10
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { answered, changes, changesSince, glob, hasThreadFor, head, human, judge, judgeDiffShape, kept, mark, matches, numeric, quote, read, refuse, removeScript, requireAnswers, requireProgress, requireReplies, requireRevert, requireThreadsClosed, restoreScript, revertScript, run, scope, slice, start, vars } from "../flows.mjs";
|
|
2
2
|
import { lensesOf, review as review$1 } from "../workflow-kit.mjs";
|
|
3
|
-
import { $ as refactorFixPrompt, A as architectureDecomposePrompt, B as designSystem, C as planner, D as agentWithSkills, E as reviewing, F as buildReviewFixNitsPrompt, G as idleMessage, I as buildReviewTriageMessage, J as naivePassPrompt, K as implementBuildingPrompt, L as builderSystem, M as buildReviewAnswerQuestionsPrompt, N as buildReviewAwaitAfterDeferMessage, O as architectSystem, P as buildReviewCollectingPrompt, Q as refactorDroppedRefusal, R as deferNotesScript, S as fixRisks, V as designTriagePrompt, X as packagesItemFixSuitePrompt, Y as packagesItemBuildingPrompt, Z as refactorBuildingPrompt, _ as REVIEW, at as reviewerSystem, b as fixCheck, ct as startGateBlockedMessage, d as healthy, dt as defaults, et as refactorRules, f as ARCHITECTURE, ft as envDefaults, g as REQUIREMENTS, h as REFACTOR, i as skills, it as reviewNotesCapture, j as architectureGateAnswerMessage, k as architectureAuthorPrompt, l as gate, lt as unwindFailedMessage, n as summary, nt as reviewEditsCapture, o as shipped, ot as seamByHandMessage, p as FEEDBACK, pt as testFileGlobs, q as implementFixPrompt, r as access, st as seamSteerMessage, t as steering, tt as reviewEditNotesCapture, u as guarded, ut as unwindFailure, v as awaitReview, w as reviewMissing, y as coder, z as designGateAnswerMessage } from "../chunks/reserved-
|
|
4
|
-
import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-
|
|
3
|
+
import { $ as refactorFixPrompt, A as architectureDecomposePrompt, B as designSystem, C as planner, D as agentWithSkills, E as reviewing, F as buildReviewFixNitsPrompt, G as idleMessage, I as buildReviewTriageMessage, J as naivePassPrompt, K as implementBuildingPrompt, L as builderSystem, M as buildReviewAnswerQuestionsPrompt, N as buildReviewAwaitAfterDeferMessage, O as architectSystem, P as buildReviewCollectingPrompt, Q as refactorDroppedRefusal, R as deferNotesScript, S as fixRisks, V as designTriagePrompt, X as packagesItemFixSuitePrompt, Y as packagesItemBuildingPrompt, Z as refactorBuildingPrompt, _ as REVIEW, at as reviewerSystem, b as fixCheck, ct as startGateBlockedMessage, d as healthy, dt as defaults, et as refactorRules, f as ARCHITECTURE, ft as envDefaults, g as REQUIREMENTS, h as REFACTOR, i as skills, it as reviewNotesCapture, j as architectureGateAnswerMessage, k as architectureAuthorPrompt, l as gate, lt as unwindFailedMessage, n as summary, nt as reviewEditsCapture, o as shipped, ot as seamByHandMessage, p as FEEDBACK, pt as testFileGlobs, q as implementFixPrompt, r as access, st as seamSteerMessage, t as steering, tt as reviewEditNotesCapture, u as guarded, ut as unwindFailure, v as awaitReview, w as reviewMissing, y as coder, z as designGateAnswerMessage } from "../chunks/reserved-BduvUQxQ.mjs";
|
|
4
|
+
import { a as tail, i as refusePendingHandOff, n as closeReview } from "../chunks/tail-pgq1Fbbv.mjs";
|
|
5
5
|
//#region src/workflows/shared/fences.ts
|
|
6
6
|
const markFenced = (text) => {
|
|
7
7
|
let fence;
|
|
@@ -516,6 +516,35 @@ const requireNoTestFiles = () => {
|
|
|
516
516
|
if (tests.length === 0) return;
|
|
517
517
|
refuse(`gtd land: refactor-tests: the refactor may not change a test file — revert these:\n${tests.map((path) => ` - ${path}`).join("\n")}`);
|
|
518
518
|
};
|
|
519
|
+
const holdsCallSites = (path) => !path.endsWith(".feature") && !path.endsWith(".snap");
|
|
520
|
+
const COMMENT = /(?<![*/])\/\*[\s\S]*?\*\/|(?<!:)\/\/.*$/gm;
|
|
521
|
+
const ASSERTION = /\b(?:expect|assert\w*)\s*[.(]/g;
|
|
522
|
+
const TEST_CASE = /(?<![.\w])x?(?:it|test|describe)\s*[.(]/g;
|
|
523
|
+
const MODIFIER = /(?<![.\w])(?:it|test|describe)\.(skip|only|todo|fails|skipIf|runIf)\b|(?<![.\w])(x)(?:it|test|describe)\b/g;
|
|
524
|
+
const counts = (text) => {
|
|
525
|
+
const live = text.replace(COMMENT, "");
|
|
526
|
+
const modifiers = [...live.matchAll(MODIFIER)].map((m) => m[1] ?? m[2]).sort();
|
|
527
|
+
return JSON.stringify([
|
|
528
|
+
[...live.matchAll(ASSERTION)].length,
|
|
529
|
+
[...live.matchAll(TEST_CASE)].length,
|
|
530
|
+
modifiers
|
|
531
|
+
]);
|
|
532
|
+
};
|
|
533
|
+
const tested = (text) => {
|
|
534
|
+
const live = text.replace(COMMENT, "");
|
|
535
|
+
return live.search(ASSERTION) >= 0 || live.search(TEST_CASE) >= 0;
|
|
536
|
+
};
|
|
537
|
+
const callSitesOnly = ({ path, status, before, after }) => status === "modified" && holdsCallSites(path) && before !== void 0 && after !== void 0 && tested(before) && counts(before) === counts(after);
|
|
538
|
+
/**
|
|
539
|
+
* The test-file half of the refactor guard: an existing test may follow a changed call shape,
|
|
540
|
+
* nothing more. Returns the test files it admitted, which the scope check then allows.
|
|
541
|
+
*/
|
|
542
|
+
const requireTestCallSitesOnly = () => {
|
|
543
|
+
const tests = changes().filter((c) => isCode(c.path) && isTestFile(c.path));
|
|
544
|
+
const refused = tests.filter((c) => !callSitesOnly(c)).map((c) => c.path);
|
|
545
|
+
if (refused.length > 0) refuse(`gtd land: refactor-tests: the refactor may update an existing test file's call sites only — never add, delete, skip or comment out a test or an assertion, or touch a snapshot, a .feature file or test data; revert these:\n${refused.map((path) => ` - ${path}`).join("\n")}`);
|
|
546
|
+
return tests.map((c) => c.path);
|
|
547
|
+
};
|
|
519
548
|
const flagExpression = (pattern) => {
|
|
520
549
|
if (pattern.trim() === "") throw new Error("gtd: refactorFlagPattern must not be blank");
|
|
521
550
|
let expression;
|
|
@@ -571,12 +600,16 @@ const packageLines = (strays, pattern, reference) => [
|
|
|
571
600
|
"## Rules",
|
|
572
601
|
"",
|
|
573
602
|
"- The suite's outcome must not change",
|
|
574
|
-
"- Change only the paths above, and never a test
|
|
603
|
+
"- Change only the paths above, plus an existing test file where a changed call shape reaches it: never add or delete a test file, and never add, remove or alter a test or an assertion",
|
|
575
604
|
`- Introduce no flag read and add no second read of an existing one (\`${pattern}\`)`,
|
|
576
605
|
"",
|
|
577
606
|
"## Reference",
|
|
578
607
|
"",
|
|
579
|
-
reference === void 0 ? "None." :
|
|
608
|
+
...reference === void 0 ? ["None."] : [
|
|
609
|
+
`Reading material only, never applied: \`git show ${reference} -- ${strays.map(quote).join(" ")}\``,
|
|
610
|
+
"",
|
|
611
|
+
"Its changes to the existing code above are the seam this refactor exists to land, minus the feature's own behaviour."
|
|
612
|
+
],
|
|
580
613
|
""
|
|
581
614
|
];
|
|
582
615
|
const demoteHeadings = (text) => {
|
|
@@ -609,8 +642,7 @@ const plan = (shape, options) => {
|
|
|
609
642
|
};
|
|
610
643
|
};
|
|
611
644
|
const guardOf = ({ pattern, strays }, entry) => () => {
|
|
612
|
-
|
|
613
|
-
requireWithin(strays);
|
|
645
|
+
requireWithin([...strays, ...requireTestCallSitesOnly()]);
|
|
614
646
|
requireNoNewFlags(pattern, entry);
|
|
615
647
|
};
|
|
616
648
|
/** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
|
|
@@ -651,7 +683,11 @@ const refactorLap = async (p, guard, feedback) => {
|
|
|
651
683
|
});
|
|
652
684
|
await run("closing", removeScript([REFACTOR_PACKAGE]), { label: "Closing out the refactor" });
|
|
653
685
|
};
|
|
654
|
-
const strayAccess = (strays) => ({ write: [
|
|
686
|
+
const strayAccess = (strays) => ({ write: [
|
|
687
|
+
...strays,
|
|
688
|
+
...testFileGlobs,
|
|
689
|
+
".gtd/**"
|
|
690
|
+
] });
|
|
655
691
|
const refactorFirst = async (shape, options = {}) => {
|
|
656
692
|
const p = plan(shape, options);
|
|
657
693
|
if (p === void 0) return false;
|
|
@@ -886,4 +922,4 @@ const feature = shipped(ordinaryStart);
|
|
|
886
922
|
//#region src/workflows/feature/index.ts
|
|
887
923
|
var feature_default = feature;
|
|
888
924
|
//#endregion
|
|
889
|
-
export { REFACTOR_PACKAGE, access, afterTail, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, build, buildTail, collecting, declaredTests, decompose, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, fixSuite, implement, judgeSeam, naivePass, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireDeclaredTests, requireNoNewFlags, requireNoTestFiles, requireWithin, reviewedRefactor, seamBuild, skills, steering, summary, triage, unwind };
|
|
925
|
+
export { REFACTOR_PACKAGE, access, afterTail, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, build, buildTail, collecting, declaredTests, decompose, feature_default as default, defaults, deferNotes, design, envDefaults, feature, fixNits, fixSuite, implement, judgeSeam, naivePass, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireDeclaredTests, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, reviewedRefactor, seamBuild, skills, steering, summary, triage, unwind };
|
package/dist/workflows/fix.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { read, refuse, removeScript, run, scope, start } from "../flows.mjs";
|
|
2
2
|
import { d as reviewNotes, f as reviewRisks } from "../chunks/steering-DVILanVf.mjs";
|
|
3
|
-
import { D as agentWithSkills, H as finisherSystem, U as fixReviewNotesPrompt, _ as REVIEW, dt as defaults, ft as envDefaults, i as skills, n as summary, o as shipped, r as access, s as baseline, t as steering, y as coder } from "../chunks/reserved-
|
|
4
|
-
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-
|
|
3
|
+
import { D as agentWithSkills, H as finisherSystem, U as fixReviewNotesPrompt, _ as REVIEW, dt as defaults, ft as envDefaults, i as skills, n as summary, o as shipped, r as access, s as baseline, t as steering, y as coder } from "../chunks/reserved-BduvUQxQ.mjs";
|
|
4
|
+
import { r as handOffTail, t as REVIEW_ORIG } from "../chunks/tail-pgq1Fbbv.mjs";
|
|
5
5
|
//#region src/workflows/fix/fix.ts
|
|
6
6
|
/**
|
|
7
7
|
* Fix every note a human left in a handed-off `.gtd/REVIEW.md` and every
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { start } from "../flows.mjs";
|
|
2
|
-
import { dt as defaults, ft as envDefaults, i as skills, l as gate, n as summary, o as shipped, r as access, rt as reviewGateBlockedMessage, t as steering } from "../chunks/reserved-
|
|
3
|
-
import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-
|
|
2
|
+
import { dt as defaults, ft as envDefaults, i as skills, l as gate, n as summary, o as shipped, r as access, rt as reviewGateBlockedMessage, t as steering } from "../chunks/reserved-BduvUQxQ.mjs";
|
|
3
|
+
import { i as refusePendingHandOff, r as handOffTail } from "../chunks/tail-pgq1Fbbv.mjs";
|
|
4
4
|
//#region src/workflows/review/review.ts
|
|
5
5
|
/**
|
|
6
6
|
* Pure review of everything since `reviewBase`: a green gate, the quality
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agent, changesSince, glob, head, mark, read, refuse, removeScript, run, scope, vars } from "../flows.mjs";
|
|
2
|
-
import { a as flagShipped, dt as defaults, ft as envDefaults, gt as flipProblems, i as skills, l as gate, mt as flagMentions, n as summary, p as FEEDBACK, r as access, t as steering, vt as requireFlag, y as coder } from "../chunks/reserved-
|
|
2
|
+
import { a as flagShipped, dt as defaults, ft as envDefaults, gt as flipProblems, i as skills, l as gate, mt as flagMentions, n as summary, p as FEEDBACK, r as access, t as steering, vt as requireFlag, y as coder } from "../chunks/reserved-BduvUQxQ.mjs";
|
|
3
3
|
//#region src/workflows/rollout/rollout.ts
|
|
4
4
|
const flippingPrompt = (name, registry) => `Roll out the release flag "${name}": in ${registry}, set \`defaultOn: true\` on its entry. Change nothing else — no other entry, no other field, no other file, no comments.`;
|
|
5
5
|
const suiteBlockedMessage = () => "The suite is red after the rollout. Fix it by hand — a rollout may only touch the flag's registry entry — then land.";
|
package/dist/workflows.mjs
CHANGED
|
@@ -17554,9 +17554,13 @@ ${fixFeedbackPrompt}
|
|
|
17554
17554
|
- Leave everything uncommitted and finish your turn — do not commit
|
|
17555
17555
|
`;
|
|
17556
17556
|
const refactorRules = (strays, pattern) => `- Behaviour-neutral: the suite's outcome must not change
|
|
17557
|
-
- Change only these paths, nothing else
|
|
17558
|
-
when it is one of them:
|
|
17557
|
+
- Change only these paths, nothing else:
|
|
17559
17558
|
${strays.map((path) => ` - \`${displayPath(path)}\``).join("\n")}
|
|
17559
|
+
- An existing test file only where a changed call shape reaches it:
|
|
17560
|
+
update its call sites, inside an assertion too;
|
|
17561
|
+
never add or delete a test file,
|
|
17562
|
+
never add, remove or alter a test or an assertion, and never
|
|
17563
|
+
change what an assertion checks, an expected value or a snapshot
|
|
17560
17564
|
- Introduce no new flag read and add no second read of an existing one:
|
|
17561
17565
|
every match of \`${pattern}\` may move, never increase in number`;
|
|
17562
17566
|
const withRules = (rules) => rules === void 0 ? "" : `${rules}\n`;
|
|
@@ -17566,8 +17570,13 @@ ${feedback ? `- The package carries a \`## Review feedback\` section: a human's
|
|
|
17566
17570
|
last refactor. Act on it, within the rules below
|
|
17567
17571
|
` : ""}${refactorRules(strays, pattern)}
|
|
17568
17572
|
${reference === void 0 ? "" : `- Reference only: \`git show ${reference} -- ${strays.map(quote).join(" ")}\` shows an
|
|
17569
|
-
earlier attempt at this change
|
|
17570
|
-
|
|
17573
|
+
earlier attempt at this change, and \`git show --stat ${reference}\`
|
|
17574
|
+
lists every file it touched, the test call sites included.
|
|
17575
|
+
Read it; never apply, cherry-pick or check it out
|
|
17576
|
+
- Its changes to these paths' existing code are the seam: write that
|
|
17577
|
+
restructuring afresh here, minus the feature's own behaviour. A
|
|
17578
|
+
refactor that touches none of it is wrong, however clean — never
|
|
17579
|
+
swap in an unrelated tidy-up
|
|
17571
17580
|
`}- Leave everything uncommitted and finish your turn — do not commit
|
|
17572
17581
|
`;
|
|
17573
17582
|
const refactorFixPrompt = (strays, pattern) => `${stateFileRules}
|
|
@@ -17883,11 +17892,11 @@ const closedFold = (round, target) => round === "edits" ? `- Fold every hand-edi
|
|
|
17883
17892
|
/** `refactor` retargets the prompt at `.gtd/REFACTOR.md` and fences it to the strays. A concern outside the fence is not folded; if nothing at all is written the landing is refused, but a partly buildable round drops the rest silently. */
|
|
17884
17893
|
const buildReviewCollectingPrompt = (capture, refactor) => {
|
|
17885
17894
|
const target = refactor ? ".gtd/REFACTOR.md" : ".gtd/REQUIREMENTS.md";
|
|
17886
|
-
const fence = refactor ? `- The next refactor may change only these paths,
|
|
17887
|
-
|
|
17895
|
+
const fence = refactor ? `- The next refactor may change only these paths, plus an existing test
|
|
17896
|
+
file's call sites, and never a flag read:
|
|
17888
17897
|
${refactor.strays.map((path) => ` - \`${displayPath(path)}\``).join("\n")}
|
|
17889
|
-
A concern needing any other path, a
|
|
17890
|
-
read, or a change in behaviour (that belongs to the feature, not this
|
|
17898
|
+
A concern needing any other path, a new or changed test or assertion,
|
|
17899
|
+
a new or added flag read, or a change in behaviour (that belongs to the feature, not this
|
|
17891
17900
|
refactor) cannot be built: do not fold it
|
|
17892
17901
|
` : "";
|
|
17893
17902
|
const actionable = refactor ? `The round is actionable if the human left a note on
|
|
@@ -17991,7 +18000,7 @@ ${removed.map(({ path, lines }) => lines.map((a) => ` ${path} — ${a.scenari
|
|
|
17991
18000
|
const lapRange = {
|
|
17992
18001
|
first: "it covers everything since the base.",
|
|
17993
18002
|
feedback: "it starts at the previous review's boundary, so it covers only what is new since the previous review.",
|
|
17994
|
-
refactor: "it covers the whole refactor since it started, on every round."
|
|
18003
|
+
refactor: "it covers the whole refactor since it started, on every round. A refactor may update an existing test's call sites only: give any test-file hunk its own chunk, and say so if it changes a test, an assertion or an expected value."
|
|
17995
18004
|
};
|
|
17996
18005
|
const buildReviewReviewingPrompt = (base, lap, carry, removed = []) => `${styleBlock}
|
|
17997
18006
|
|
|
@@ -18108,7 +18117,12 @@ ${slice === void 0 ? `- Write the closing message for the process HEAD closes or
|
|
|
18108
18117
|
the exact escape or rewrite a committed config needs. A \`!\` on the
|
|
18109
18118
|
type prefix alone (\`feat!:\`/\`refactor!:\`) is NOT enough: this repo's
|
|
18110
18119
|
own commit-analyzer has missed that marker before, cutting no release
|
|
18111
|
-
until a follow-up commit carried the literal footer instead
|
|
18120
|
+
until a follow-up commit carried the literal footer instead${slice === void 0 || slice.index === slice.count ? `
|
|
18121
|
+
- If the process was started for an issue — its entry commit or
|
|
18122
|
+
\`.gtd/REQUIREMENTS.md\` names one as \`#N\`, \`owner/repo#N\` or an issue
|
|
18123
|
+
URL — end the message with a literal \`Closes #N\` footer per issue
|
|
18124
|
+
(\`Closes owner/repo#N\` for another repository). Name only an issue the
|
|
18125
|
+
work resolves, never one it merely mentions` : ""}
|
|
18112
18126
|
|
|
18113
18127
|
${slice !== void 0 && slice.index > 1 ? `The process's entry commit is \`${it.entryCommit}\` — outside this slice's range, context
|
|
18114
18128
|
that states the motivation. ` : `The process's entry commit is \`${it.entryCommit}\`. `}${human}Inspect the range: \`git log ${it.processBase}..${it.processTip}\`
|
|
@@ -38339,6 +38353,35 @@ const requireNoTestFiles = () => {
|
|
|
38339
38353
|
if (tests.length === 0) return;
|
|
38340
38354
|
refuse(`gtd land: refactor-tests: the refactor may not change a test file — revert these:\n${tests.map((path) => ` - ${path}`).join("\n")}`);
|
|
38341
38355
|
};
|
|
38356
|
+
const holdsCallSites = (path) => !path.endsWith(".feature") && !path.endsWith(".snap");
|
|
38357
|
+
const COMMENT = /(?<![*/])\/\*[\s\S]*?\*\/|(?<!:)\/\/.*$/gm;
|
|
38358
|
+
const ASSERTION = /\b(?:expect|assert\w*)\s*[.(]/g;
|
|
38359
|
+
const TEST_CASE = /(?<![.\w])x?(?:it|test|describe)\s*[.(]/g;
|
|
38360
|
+
const MODIFIER = /(?<![.\w])(?:it|test|describe)\.(skip|only|todo|fails|skipIf|runIf)\b|(?<![.\w])(x)(?:it|test|describe)\b/g;
|
|
38361
|
+
const counts = (text) => {
|
|
38362
|
+
const live = text.replace(COMMENT, "");
|
|
38363
|
+
const modifiers = [...live.matchAll(MODIFIER)].map((m) => m[1] ?? m[2]).sort();
|
|
38364
|
+
return JSON.stringify([
|
|
38365
|
+
[...live.matchAll(ASSERTION)].length,
|
|
38366
|
+
[...live.matchAll(TEST_CASE)].length,
|
|
38367
|
+
modifiers
|
|
38368
|
+
]);
|
|
38369
|
+
};
|
|
38370
|
+
const tested = (text) => {
|
|
38371
|
+
const live = text.replace(COMMENT, "");
|
|
38372
|
+
return live.search(ASSERTION) >= 0 || live.search(TEST_CASE) >= 0;
|
|
38373
|
+
};
|
|
38374
|
+
const callSitesOnly = ({ path, status, before, after }) => status === "modified" && holdsCallSites(path) && before !== void 0 && after !== void 0 && tested(before) && counts(before) === counts(after);
|
|
38375
|
+
/**
|
|
38376
|
+
* The test-file half of the refactor guard: an existing test may follow a changed call shape,
|
|
38377
|
+
* nothing more. Returns the test files it admitted, which the scope check then allows.
|
|
38378
|
+
*/
|
|
38379
|
+
const requireTestCallSitesOnly = () => {
|
|
38380
|
+
const tests = changes().filter((c) => isCode(c.path) && isTestFile(c.path));
|
|
38381
|
+
const refused = tests.filter((c) => !callSitesOnly(c)).map((c) => c.path);
|
|
38382
|
+
if (refused.length > 0) refuse(`gtd land: refactor-tests: the refactor may update an existing test file's call sites only — never add, delete, skip or comment out a test or an assertion, or touch a snapshot, a .feature file or test data; revert these:\n${refused.map((path) => ` - ${path}`).join("\n")}`);
|
|
38383
|
+
return tests.map((c) => c.path);
|
|
38384
|
+
};
|
|
38342
38385
|
const flagExpression = (pattern) => {
|
|
38343
38386
|
if (pattern.trim() === "") throw new Error("gtd: refactorFlagPattern must not be blank");
|
|
38344
38387
|
let expression;
|
|
@@ -38394,12 +38437,16 @@ const packageLines = (strays, pattern, reference) => [
|
|
|
38394
38437
|
"## Rules",
|
|
38395
38438
|
"",
|
|
38396
38439
|
"- The suite's outcome must not change",
|
|
38397
|
-
"- Change only the paths above, and never a test
|
|
38440
|
+
"- Change only the paths above, plus an existing test file where a changed call shape reaches it: never add or delete a test file, and never add, remove or alter a test or an assertion",
|
|
38398
38441
|
`- Introduce no flag read and add no second read of an existing one (\`${pattern}\`)`,
|
|
38399
38442
|
"",
|
|
38400
38443
|
"## Reference",
|
|
38401
38444
|
"",
|
|
38402
|
-
reference === void 0 ? "None." :
|
|
38445
|
+
...reference === void 0 ? ["None."] : [
|
|
38446
|
+
`Reading material only, never applied: \`git show ${reference} -- ${strays.map(quote).join(" ")}\``,
|
|
38447
|
+
"",
|
|
38448
|
+
"Its changes to the existing code above are the seam this refactor exists to land, minus the feature's own behaviour."
|
|
38449
|
+
],
|
|
38403
38450
|
""
|
|
38404
38451
|
];
|
|
38405
38452
|
const demoteHeadings = (text) => {
|
|
@@ -38432,8 +38479,7 @@ const plan = (shape, options) => {
|
|
|
38432
38479
|
};
|
|
38433
38480
|
};
|
|
38434
38481
|
const guardOf = ({ pattern, strays }, entry) => () => {
|
|
38435
|
-
|
|
38436
|
-
requireWithin(strays);
|
|
38482
|
+
requireWithin([...strays, ...requireTestCallSitesOnly()]);
|
|
38437
38483
|
requireNoNewFlags(pattern, entry);
|
|
38438
38484
|
};
|
|
38439
38485
|
/** Derive, build, check and close one refactor lap; `feedback` is the collected review text of the lap before. */
|
|
@@ -38474,7 +38520,11 @@ const refactorLap = async (p, guard, feedback) => {
|
|
|
38474
38520
|
});
|
|
38475
38521
|
await run("closing", removeScript([REFACTOR_PACKAGE]), { label: "Closing out the refactor" });
|
|
38476
38522
|
};
|
|
38477
|
-
const strayAccess = (strays) => ({ write: [
|
|
38523
|
+
const strayAccess = (strays) => ({ write: [
|
|
38524
|
+
...strays,
|
|
38525
|
+
...testFileGlobs,
|
|
38526
|
+
".gtd/**"
|
|
38527
|
+
] });
|
|
38478
38528
|
const refactorFirst = async (shape, options = {}) => {
|
|
38479
38529
|
const p = plan(shape, options);
|
|
38480
38530
|
if (p === void 0) return false;
|
|
@@ -38900,4 +38950,4 @@ var workflows_default = feature;
|
|
|
38900
38950
|
*/
|
|
38901
38951
|
const doors = {};
|
|
38902
38952
|
//#endregion
|
|
38903
|
-
export { ARCHITECTURE, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, awaitReview, base, baseline, build, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, declaredTests, decompose, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, fixSuite, gate, guarded, handOffReview, handOffTail, healthy, implement, judgeSeam, lensesOf, naivePass, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireDeclaredTests, requireNoNewFlags, requireNoTestFiles, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, steering, summary, tail, threads, triage, unwind };
|
|
38953
|
+
export { ARCHITECTURE, ESCALATION, FEEDBACK, FIX_CAP, FlagOptions, QUALITY, REFACTOR, REFACTOR_PACKAGE, REQUIREMENTS, REVIEW, REVIEW_ORIG, ShippedOptions, access, afterTail, agentWithSkills, answerProductQuestions, answerReviewQuestions, answerTechnicalQuestions, architecture, author, awaitReview, base, baseline, build, buildTail, builtInLenses, cleanup, cleanupFlow, closeReview, coder, collecting, declaredTests, decompose, workflows_default as default, defaults, deferNotes, describeEscalation, design, doors, envDefaults, escalate, escalation, escalationExhausted, feature, fix, fixCheck, fixFlow, fixNits, fixQuality, fixQualityFindings, fixReviewNotes, fixRisks, fixSuite, gate, guarded, handOffReview, handOffTail, healthy, implement, judgeSeam, lensesOf, naivePass, nextPackage, ordinaryStart, packageItem, packages, planAndBuild, planner, qualityLap, reUnwind, refactorCollecting, refactorFirst, refactorStrays, requireDeclaredTests, requireNoNewFlags, requireNoTestFiles, requireTestCallSitesOnly, requireWithin, retryQuestion, review, reviewFlow, reviewMissing, reviewQuality, reviewedRefactor, reviewing, rollout, rolloutFlow, seamBuild, skills, steering, summary, tail, threads, triage, unwind };
|
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
|
|
|
13
13
|
And an environment variable "GTD_FLAG_NAIVE_PASS" set to "1"
|
|
14
14
|
And an environment variable "GTD_FLAG_RISK_TIER" set to "0"
|
|
15
15
|
|
|
16
|
-
Scenario: with a narrowed seamAllow a test
|
|
16
|
+
Scenario: with a narrowed seamAllow a test file added by the refactor is refused
|
|
17
17
|
Given an environment variable "GTD_REFACTORQUALITYREVIEWS" set to ""
|
|
18
18
|
And an environment variable "GTD_SEAMALLOW" set to "docs/**"
|
|
19
19
|
And a file "src/calc.ts" with:
|
|
@@ -85,12 +85,12 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
|
|
|
85
85
|
import { expect, it } from "vitest"
|
|
86
86
|
it("adds", () => expect(2 + 2).toBe(4))
|
|
87
87
|
"""
|
|
88
|
-
And step "refactor.building" writes "src/calc.
|
|
88
|
+
And step "refactor.building" writes "src/calc.spec.ts" with:
|
|
89
89
|
"""
|
|
90
90
|
import { expect, it } from "vitest"
|
|
91
91
|
it("adds", () => expect(3 + 3).toBe(6))
|
|
92
92
|
"""
|
|
93
|
-
Then driving the process is refused with "
|
|
93
|
+
Then driving the process is refused with "refactor-tests"
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
Scenario: refactorQualityReviews picks the refactor's lenses and qualityReviews does not reach them
|
|
@@ -1607,6 +1607,40 @@ Feature: the refactor slice is reviewed on its own before the feature is impleme
|
|
|
1607
1607
|
And ".gtd/REFACTOR.md" contains "src/calc.ts"
|
|
1608
1608
|
And ".gtd/REFACTOR.md" does not contain "src/calc.test.ts"
|
|
1609
1609
|
|
|
1610
|
+
Scenario: the refactor updates an existing test's call sites to the seam's new shape
|
|
1611
|
+
Given step "seam.naive" writes "src/calc.ts" with:
|
|
1612
|
+
"""
|
|
1613
|
+
export const add = ({ a, b }: { a: number; b: number }) => a + b
|
|
1614
|
+
export const subtract = ({ a, b }: { a: number; b: number }) => a - b
|
|
1615
|
+
"""
|
|
1616
|
+
And step "refactor.deriving" runs its script
|
|
1617
|
+
And step "refactor.building" writes "src/calc.ts" with:
|
|
1618
|
+
"""
|
|
1619
|
+
export const add = ({ a, b }: { a: number; b: number }) => a + b
|
|
1620
|
+
"""
|
|
1621
|
+
And step "refactor.building" writes "src/calc.test.ts" with:
|
|
1622
|
+
"""
|
|
1623
|
+
import { expect, it } from "vitest"
|
|
1624
|
+
import { add } from "./calc"
|
|
1625
|
+
it("adds", () => expect(add({ a: 1, b: 1 })).toBe(2))
|
|
1626
|
+
"""
|
|
1627
|
+
When the process is driven
|
|
1628
|
+
Then the process rests at "refactor.health.check"
|
|
1629
|
+
|
|
1630
|
+
Scenario: the refactor may not drop an assertion from an existing test
|
|
1631
|
+
Given step "seam.naive" writes "src/calc.ts" with:
|
|
1632
|
+
"""
|
|
1633
|
+
export const add = (a: number, b: number) => a + b
|
|
1634
|
+
export const subtract = (a: number, b: number) => a - b
|
|
1635
|
+
"""
|
|
1636
|
+
And step "refactor.deriving" runs its script
|
|
1637
|
+
And step "refactor.building" writes "src/calc.test.ts" with:
|
|
1638
|
+
"""
|
|
1639
|
+
import { it } from "vitest"
|
|
1640
|
+
it("adds", () => {})
|
|
1641
|
+
"""
|
|
1642
|
+
Then driving the process is refused with "refactor-tests"
|
|
1643
|
+
|
|
1610
1644
|
Scenario: a refactor-first whose strays are all test files goes straight to implement
|
|
1611
1645
|
Given step "seam.naive" writes "src/calc.test.ts" with:
|
|
1612
1646
|
"""
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
refactorStrays,
|
|
14
14
|
requireNoNewFlags,
|
|
15
15
|
requireNoTestFiles,
|
|
16
|
+
requireTestCallSitesOnly,
|
|
16
17
|
requireWithin,
|
|
17
18
|
reviewedRefactor,
|
|
18
19
|
REFACTOR_PACKAGE,
|
|
@@ -20,6 +21,15 @@ import {
|
|
|
20
21
|
|
|
21
22
|
afterEach(() => installContext(undefined))
|
|
22
23
|
|
|
24
|
+
const testFileGlobs = [
|
|
25
|
+
"**/*.test.*",
|
|
26
|
+
"**/*.spec.*",
|
|
27
|
+
"**/*.feature",
|
|
28
|
+
"**/tests/**",
|
|
29
|
+
"**/test/**",
|
|
30
|
+
"**/__tests__/**",
|
|
31
|
+
]
|
|
32
|
+
|
|
23
33
|
const change = (
|
|
24
34
|
path: string,
|
|
25
35
|
before: string | undefined,
|
|
@@ -131,6 +141,137 @@ describe("requireNoTestFiles", () => {
|
|
|
131
141
|
})
|
|
132
142
|
})
|
|
133
143
|
|
|
144
|
+
describe("requireTestCallSitesOnly", () => {
|
|
145
|
+
it("refuses an added or deleted test path, naming it", () => {
|
|
146
|
+
const list = [
|
|
147
|
+
change("tests/unit/b.ts", undefined, "x", "added"),
|
|
148
|
+
change("src/__tests__/c.ts", "a", undefined, "deleted"),
|
|
149
|
+
change("src/a.test.ts", "expect(a()).toBe(1)", "expect(a(1)).toBe(1)"),
|
|
150
|
+
]
|
|
151
|
+
const message = withChanges(list, () => requireTestCallSitesOnly())
|
|
152
|
+
expect(message).toMatch(/^gtd land: refactor-tests: /)
|
|
153
|
+
expect(message).toContain(" - tests/unit/b.ts")
|
|
154
|
+
expect(message).toContain(" - src/__tests__/c.ts")
|
|
155
|
+
expect(message).not.toContain("src/a.test.ts")
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it("passes a modified test file's call site, non-test paths and .gtd/ paths", () => {
|
|
159
|
+
const list = [
|
|
160
|
+
change("src/a.test.ts", "expect(a()).toBe(1)", "expect(a(1)).toBe(1)"),
|
|
161
|
+
change("src/new.ts", undefined, "x", "added"),
|
|
162
|
+
change(".gtd/tests/x.md", "", "x", "added"),
|
|
163
|
+
]
|
|
164
|
+
expect(withChanges(list, () => requireTestCallSitesOnly())).toBeUndefined()
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it("refuses a snapshot change and a dropped test or assertion, naming each", () => {
|
|
168
|
+
const list = [
|
|
169
|
+
change("src/__snapshots__/a.test.ts.snap", "a", "b"),
|
|
170
|
+
change("src/b.test.ts", 'it("a", () => {})\nit("b", () => {})', 'it("a", () => {})'),
|
|
171
|
+
change("src/c.test.ts", "expect(c()).toBe(1)\nexpect(c()).toBe(1)", "expect(c()).toBe(1)"),
|
|
172
|
+
change(
|
|
173
|
+
"d.feature",
|
|
174
|
+
"Feature: d\n Scenario: d\n Then it is\n",
|
|
175
|
+
"Feature: d\n Scenario: d\n",
|
|
176
|
+
),
|
|
177
|
+
]
|
|
178
|
+
const message = withChanges(list, () => requireTestCallSitesOnly())
|
|
179
|
+
for (const { path } of list) expect(message).toContain(` - ${path}`)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it.each([
|
|
183
|
+
[
|
|
184
|
+
"a skipped test",
|
|
185
|
+
'it("a", () => expect(a()).toBe(1))',
|
|
186
|
+
'it.skip("a", () => expect(a()).toBe(1))',
|
|
187
|
+
],
|
|
188
|
+
[
|
|
189
|
+
"an x-prefixed test",
|
|
190
|
+
'it("a", () => expect(a()).toBe(1))',
|
|
191
|
+
'xit("a", () => expect(a()).toBe(1))',
|
|
192
|
+
],
|
|
193
|
+
[
|
|
194
|
+
"a line-commented assertion",
|
|
195
|
+
'it("a", () => {\n expect(a()).toBe(1)\n})',
|
|
196
|
+
'it("a", () => {\n // expect(a()).toBe(1)\n})',
|
|
197
|
+
],
|
|
198
|
+
[
|
|
199
|
+
"a block-commented test",
|
|
200
|
+
'it("a", () => expect(a()).toBe(1))',
|
|
201
|
+
'/* it("a", () => expect(a()).toBe(1)) */',
|
|
202
|
+
],
|
|
203
|
+
[
|
|
204
|
+
"a .skip swapped for an .only",
|
|
205
|
+
'it.skip("a", () => expect(a()).toBe(1))\nit("b", () => expect(b()).toBe(1))',
|
|
206
|
+
'it.only("a", () => expect(a()).toBe(1))\nit("b", () => expect(b()).toBe(1))',
|
|
207
|
+
],
|
|
208
|
+
[
|
|
209
|
+
"an assertion dropped after a URL in a test name",
|
|
210
|
+
'it("GET http://x", () => expect(get()).toBe(1))',
|
|
211
|
+
'it("GET http://x", () => {})',
|
|
212
|
+
],
|
|
213
|
+
["fixture data with no test", '{"sum": 2}', '{"sum": 3}'],
|
|
214
|
+
])("refuses %s", (_label, before, after) => {
|
|
215
|
+
const message = withChanges([change("tests/a.test.ts", before, after)], () =>
|
|
216
|
+
requireTestCallSitesOnly(),
|
|
217
|
+
)
|
|
218
|
+
expect(message).toContain(" - tests/a.test.ts")
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it("refuses a modified test file whose content cannot be read", () => {
|
|
222
|
+
const message = withChanges([change("tests/a.test.ts", undefined, "expect(a()).toBe(1)")], () =>
|
|
223
|
+
requireTestCallSitesOnly(),
|
|
224
|
+
)
|
|
225
|
+
expect(message).toContain(" - tests/a.test.ts")
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it("passes a regex .test( call rewritten at a call site", () => {
|
|
229
|
+
const list = [
|
|
230
|
+
change(
|
|
231
|
+
"src/a.test.ts",
|
|
232
|
+
'it("a", () => expect(/x/.test(s)).toBe(true))',
|
|
233
|
+
'it("a", () => expect(has(s)).toBe(true))',
|
|
234
|
+
),
|
|
235
|
+
]
|
|
236
|
+
expect(withChanges(list, () => requireTestCallSitesOnly())).toBeUndefined()
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it("passes a call site rewritten inside an assertion", () => {
|
|
240
|
+
const list = [
|
|
241
|
+
change(
|
|
242
|
+
"src/a.test.ts",
|
|
243
|
+
'it("adds", () => expect(add(1, 1)).toBe(2))',
|
|
244
|
+
'it("adds", async () => expect(await add({ a: 1, b: 1 })).toBe(2))',
|
|
245
|
+
),
|
|
246
|
+
]
|
|
247
|
+
expect(withChanges(list, () => requireTestCallSitesOnly())).toBeUndefined()
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it("is not narrowed by a narrowed seamAllow", () => {
|
|
251
|
+
installContext(
|
|
252
|
+
fixtureContext(
|
|
253
|
+
{ vars: { seamAllow: "docs/**" } },
|
|
254
|
+
{
|
|
255
|
+
changes: () => [change("src/a.test.ts", undefined, "b", "added")],
|
|
256
|
+
matches: globMatches,
|
|
257
|
+
refuse: (message) => {
|
|
258
|
+
throw new Error(message)
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
expect(refusal(() => requireTestCallSitesOnly())).toContain("src/a.test.ts")
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it("does not refuse other naming such as _test.go or test_*.py", () => {
|
|
267
|
+
const list = [
|
|
268
|
+
change("pkg/a_test.go", undefined, "b", "added"),
|
|
269
|
+
change("pkg/test_a.py", undefined, "b", "added"),
|
|
270
|
+
]
|
|
271
|
+
expect(withChanges(list, () => requireTestCallSitesOnly())).toBeUndefined()
|
|
272
|
+
})
|
|
273
|
+
})
|
|
274
|
+
|
|
134
275
|
describe("refactorStrays", () => {
|
|
135
276
|
it("refactorStrays drops test files, keeps the rest and ignores a narrowed seamAllow", () => {
|
|
136
277
|
installContext(fixtureContext({ vars: { seamAllow: "docs/**" } }, { matches: globMatches }))
|
|
@@ -359,7 +500,7 @@ describe("refactorFirst", () => {
|
|
|
359
500
|
const body = derive?.kind === "run" ? derive.body : ""
|
|
360
501
|
expect(body).toContain("`src/a.ts`") // gtd-path-exempt: fixture paths
|
|
361
502
|
expect(body).not.toContain("a.test.ts")
|
|
362
|
-
expect(h.scopes[0]?.access).toEqual({ write: ["src/a.ts", ".gtd/**"] })
|
|
503
|
+
expect(h.scopes[0]?.access).toEqual({ write: ["src/a.ts", ...testFileGlobs, ".gtd/**"] })
|
|
363
504
|
})
|
|
364
505
|
|
|
365
506
|
it("throws before any step when a stray holds a flag read the pattern matches zero-length", async () => {
|
|
@@ -403,10 +544,13 @@ describe("refactorFirst", () => {
|
|
|
403
544
|
expect(lines).toEqual(["- `src/z.ts`", "- `src/a.ts`", "- `it's.ts`"]) // gtd-path-exempt: fixture paths
|
|
404
545
|
})
|
|
405
546
|
|
|
406
|
-
it("runs inside a refactor scope whose write access is the strays
|
|
547
|
+
it("runs inside a refactor scope whose write access is the strays, test files and .gtd/**", async () => {
|
|
407
548
|
const h = await drive(shape())
|
|
408
549
|
expect(h.scopes).toEqual([
|
|
409
|
-
{
|
|
550
|
+
{
|
|
551
|
+
name: "refactor",
|
|
552
|
+
access: { write: ["src/a.ts", "src/b.ts", ...testFileGlobs, ".gtd/**"] },
|
|
553
|
+
},
|
|
410
554
|
])
|
|
411
555
|
})
|
|
412
556
|
|
|
@@ -439,19 +583,38 @@ describe("refactorFirst", () => {
|
|
|
439
583
|
expect(names(h)).not.toContain("health.check")
|
|
440
584
|
})
|
|
441
585
|
|
|
442
|
-
it("
|
|
443
|
-
const h = await drive(shape(
|
|
444
|
-
build: [
|
|
586
|
+
it("lets a build turn update an existing test file's call sites", async () => {
|
|
587
|
+
const h = await drive(shape(), {
|
|
588
|
+
build: [
|
|
589
|
+
change("src/a.ts", "a", "b"),
|
|
590
|
+
change("tests/a.test.ts", "expect(a()).toBe(1)", "expect(await a()).toBe(1)"),
|
|
591
|
+
],
|
|
592
|
+
})
|
|
593
|
+
expect(h.result).toBe(true)
|
|
594
|
+
expect(names(h)).toContain("health.check")
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
it("refuses a build turn adding a test file", async () => {
|
|
598
|
+
const h = await drive(shape(), {
|
|
599
|
+
build: [change("src/a.test.ts", undefined, "b", "added")],
|
|
445
600
|
})
|
|
446
601
|
expect((h.result as Error).message).toContain("refactor-tests")
|
|
447
602
|
expect((h.result as Error).message).toContain("src/a.test.ts")
|
|
448
603
|
expect(names(h)).not.toContain("health.check")
|
|
449
604
|
})
|
|
450
605
|
|
|
451
|
-
it("
|
|
606
|
+
it("lets the package rules admit only an existing test file's call sites", async () => {
|
|
452
607
|
const h = await drive(shape())
|
|
453
|
-
expect(bodyOf(h, 0)).toContain("never a test file")
|
|
454
|
-
expect(bodyOf(h, 0)).
|
|
608
|
+
expect(bodyOf(h, 0)).toContain("never add or delete a test file")
|
|
609
|
+
expect(bodyOf(h, 0)).toContain("never add, remove or alter a test or an assertion")
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
it("names the reference's seam as the change to carry, when there is a reference", async () => {
|
|
613
|
+
const withRef = await drive(shape(), { reference: "abc1234" })
|
|
614
|
+
expect(bodyOf(withRef, 0)).toMatch(/seam/)
|
|
615
|
+
expect(buildingPrompt(withRef)).toMatch(/touches none of it/)
|
|
616
|
+
const bare = await drive(shape())
|
|
617
|
+
expect(buildingPrompt(bare)).not.toMatch(/touches none of it/)
|
|
455
618
|
})
|
|
456
619
|
|
|
457
620
|
it("refuses a build turn adding a flag read", async () => {
|
|
@@ -734,7 +897,7 @@ describe("reviewedRefactor", () => {
|
|
|
734
897
|
const body = derive?.kind === "run" ? derive.body : ""
|
|
735
898
|
expect(body).toContain("`src/a.ts`") // gtd-path-exempt: fixture paths
|
|
736
899
|
expect(body).not.toContain("a.test.ts")
|
|
737
|
-
expect(h.scopes[0]?.access).toEqual({ write: ["src/a.ts", ".gtd/**"] })
|
|
900
|
+
expect(h.scopes[0]?.access).toEqual({ write: ["src/a.ts", ...testFileGlobs, ".gtd/**"] })
|
|
738
901
|
const collecting = h.steps.find((s) => s.kind === "agent" && s.name === "review.collecting")
|
|
739
902
|
const prompt = collecting?.kind === "agent" ? collecting.prompt : ""
|
|
740
903
|
expect(prompt).toContain("`src/a.ts`") // gtd-path-exempt: fixture paths
|
|
@@ -958,14 +1121,18 @@ describe("reviewedRefactor", () => {
|
|
|
958
1121
|
const h = await driveReviewed(shape({ strays: ["src/a.ts", "src/a.test.ts"] }), {
|
|
959
1122
|
notes: ["add — typo"],
|
|
960
1123
|
answers: { "note-1": { answer: "nit", p: 0.9 } },
|
|
961
|
-
turns: {
|
|
1124
|
+
turns: {
|
|
1125
|
+
"refactor.build.review.fix.nits.fixing": [
|
|
1126
|
+
change("src/a.test.ts", undefined, "b", "added"),
|
|
1127
|
+
],
|
|
1128
|
+
},
|
|
962
1129
|
})
|
|
963
1130
|
expect((h.result as Error).message).toContain("refactor-tests")
|
|
964
1131
|
expect(h.names.at(-1)).toBe("refactor.build.review.fix.nits.fixing")
|
|
965
1132
|
})
|
|
966
1133
|
|
|
967
|
-
it("runs the nit-fix turn under the strays
|
|
968
|
-
const write = ["src/a.ts", ".gtd/**"]
|
|
1134
|
+
it("runs the nit-fix turn under the strays, test files and .gtd/** as its write access", async () => {
|
|
1135
|
+
const write = ["src/a.ts", ...testFileGlobs, ".gtd/**"]
|
|
969
1136
|
const h = await driveReviewed(shape({ strays: ["src/a.ts", "src/a.test.ts"] }), {
|
|
970
1137
|
notes: ["add — typo"],
|
|
971
1138
|
answers: { "note-1": { answer: "nit", p: 0.9 } },
|
|
@@ -979,7 +1146,7 @@ describe("reviewedRefactor", () => {
|
|
|
979
1146
|
notes: ["add — typo"],
|
|
980
1147
|
answers: { "note-1": { answer: "nit", p: 0.9 } },
|
|
981
1148
|
reds: 2,
|
|
982
|
-
turns: { "refactor.build.fix": [change("src/a.test.ts", "x", "
|
|
1149
|
+
turns: { "refactor.build.fix": [change("src/a.test.ts", "x", undefined, "deleted")] },
|
|
983
1150
|
})
|
|
984
1151
|
expect(h.names).toContain("refactor.build.fix")
|
|
985
1152
|
expect((h.result as Error).message).toContain("refactor-tests")
|
|
@@ -1031,11 +1198,11 @@ describe("reviewedRefactor's tail prompts", () => {
|
|
|
1031
1198
|
],
|
|
1032
1199
|
["refactor.build.health.describe", { findings: true, reds: 6 }],
|
|
1033
1200
|
] as const)(
|
|
1034
|
-
"%s names every stray and bans test files and new flag reads",
|
|
1201
|
+
"%s names every stray and bans new test files and new flag reads",
|
|
1035
1202
|
async (name, script) => {
|
|
1036
1203
|
const prompt = await promptAt(name, script as ReviewedScript)
|
|
1037
1204
|
for (const stray of strays) expect(prompt).toContain(`\`${stray}\``)
|
|
1038
|
-
expect(prompt).toContain("never a test file")
|
|
1205
|
+
expect(prompt).toContain("never add or delete a test file")
|
|
1039
1206
|
expect(prompt).toContain("no new flag read")
|
|
1040
1207
|
},
|
|
1041
1208
|
)
|