@pmelab/gtd 24.3.0 → 24.4.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 +15 -4
- package/dist/chunks/{reserved-DJwU5C4z.mjs → reserved-B9pG93Fc.mjs} +52 -12
- package/dist/chunks/{steering-D-PvRC5W.mjs → steering-DVILanVf.mjs} +16 -5
- package/dist/flows.mjs +9 -1
- package/dist/gtd.bundle.mjs +779 -812
- package/dist/steering.mjs +2 -2
- package/dist/testkit.mjs +663 -158
- package/dist/workflow-kit.mjs +9 -7
- package/dist/workflows/feature.mjs +152 -35
- package/dist/workflows/fix.mjs +2 -2
- package/dist/workflows/review.mjs +1 -1
- package/dist/workflows.mjs +223 -54
- package/package.json +1 -1
- package/src/cli/Program.test.ts +3 -8
- package/src/cli/Program.ts +1 -8
- package/src/edge/Edge.test.ts +5 -2
- package/src/edge/Edge.ts +3 -4
- package/src/emit/index.ts +1 -0
- package/src/flows/scripts.ts +6 -0
- package/src/platform/Git.test.ts +6 -9
- package/src/steering/FORMATS.md +1 -1
- package/src/steering/SteeringFormat.test.ts +9 -1
- package/src/steering/SteeringFormat.ts +7 -0
- package/src/steering/contract.test.ts +3 -0
- package/src/steering/index.ts +1 -0
- package/src/steering/review.test.ts +30 -0
- package/src/steering/review.ts +8 -2
- package/src/testkit/EmittedScriptRecognizer.test.ts +29 -30
- package/src/testkit/EmittedScriptRecognizer.ts +148 -51
- package/src/testkit/EmittedScripts.test.ts +166 -0
- package/src/testkit/FlowScripts.test.ts +242 -0
- package/src/testkit/FlowScripts.ts +326 -0
- package/src/testkit/GitTiers.ts +140 -49
- package/src/testkit/InMemRepo.ts +35 -2
- package/src/testkit/Layers.ts +10 -2
- package/src/testkit/TestRepo.ts +4 -1
- package/src/testkit/index.ts +1 -1
- package/src/workflow/discovery.test.ts +147 -0
- package/src/workflow/discovery.ts +5 -0
- package/src/workflow/fingerprint.test.ts +20 -4
- package/src/workflow/fingerprint.ts +8 -9
- package/src/workflow/index.ts +1 -6
- package/src/workflow/load.test.ts +5 -19
- package/src/workflow/load.ts +94 -10
- package/src/workflow-kit/review.test.ts +84 -0
- package/src/workflow-kit/review.ts +9 -6
- package/src/workflows/feature/review.test.ts +238 -12
- package/src/workflows/feature/review.ts +172 -31
- package/src/workflows/feature/steps.test.ts +12 -0
- package/src/workflows/feature/steps.ts +8 -1
- package/src/workflows/shared/access.test.ts +4 -2
- package/src/workflows/shared/access.ts +1 -0
- package/src/workflows/shared/options.test.ts +8 -0
- package/src/workflows/shared/options.ts +1 -0
- package/src/workflows/shared/steps.ts +2 -0
- package/src/workflows/shared/tail.ts +3 -11
- package/src/workflows/shared/text.test.ts +106 -0
- package/src/workflows/shared/text.ts +57 -1
- package/src/workflows/shared/vars.ts +1 -0
package/dist/testkit.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { configProblems, configure, installContext, isFactoryWorkflow, protocolMismatch, t as flows_exports } from "./flows.mjs";
|
|
1
|
+
import { checkScript, configProblems, configure, installContext, isFactoryWorkflow, moveScript, protocolMismatch, removeScript, restoreScript, revertScript, showScript, t as flows_exports } from "./flows.mjs";
|
|
2
2
|
import { O as renderFormat, T as ACCESS_REFUSAL, _ as demandOf, a as landFields, c as renderLandJson, d as renderBeatPlain, f as beatDocument, g as beatKindOf, i as judgeJson, l as renderLandPlain, m as statusOf, n as resolveSession, p as renderBeatJson, r as selectPath, s as noopText, t as globMatches } from "./chunks/wire-DA7q2ikx.mjs";
|
|
3
|
-
import { C as headingSections, S as headingSectionBodies, a as unansweredQuestions, b as parseThreadsWithFindings, n as openThreads, r as steeringFormatFor, s as parseCodeThreads, t as BUILT_IN_MODE_NAMES } from "./chunks/steering-
|
|
3
|
+
import { C as headingSections, S as headingSectionBodies, a as unansweredQuestions, b as parseThreadsWithFindings, n as openThreads, r as steeringFormatFor, s as parseCodeThreads, t as BUILT_IN_MODE_NAMES, w as formatFinding } from "./chunks/steering-DVILanVf.mjs";
|
|
4
4
|
import { $ as makeSemaphore, $i as toJSON, $n as mapBoth, $r as none$1, $t as provideMerge$1, A as catchTag, Ai as getOption, An as scopeTag, Ar as interrupt$2, At as sync$2, B as flatten$4, Bi as isSome, Bn as make$16, Br as tap$1, Bt as unsafeMakeSemaphore$1, C as all$1, Ci as unsafeFromArray, Cn as isInterruptedOnly, Cr as exitFailCause, Ct as scope, D as async, Di as Tag, Dn as ensuring$3, Dr as fiberId, Dt as serviceOption, E as asVoid$1, Ei as GenericTag, En as squash, Er as exitZipRight, Et as scopedWith$1, F as exit, Fi as filter$1, Fn as sequential, Fr as onInterrupt, Ft as tryPromise, G as forkScoped, Gi as isLeft, Gn as Class, Gr as withFiberRuntime, Gt as zipRight$3, H as forEach$2, Hi as match$4, Hn as set$1, Hr as uninterruptibleMask$1, Ht as withFiberRuntime$1, I as fail$5, Ii as flatMap$5, In as bind, Ir as scopeClose, It as try_, J as interrupt$4, Ji as right, Jn as die, Jr as empty$6, Jt as TaggedError$1, K as gen, Ki as left, Kn as spanTag, Kr as zipRight$1, Kt as zipWith, L as failCause$4, Li as fromNullable, Ln as bindTo, Lr as succeed$4, Lt as uninterruptible$1, M as dieMessage, Mi as empty$5, Mn as inheritAll$1, Mr as interruptFiber, Mt as tapErrorCause, N as either, Ni as fromIterable, Nn as interruptAllAs$1, Nr as interruptible$1, Nt as timeout, O as catchAll$1, Oi as add, On as forEachConcurrentDiscard, Or as fiberRefLocally, Ot as succeed$6, P as ensuring$2, Pi as splitAt, Pn as join$3, Pr as map$3, Pt as tryMap, Q as logWarning, Qi as format, Qn as map$5, Qr as make$19, Qt as provide$1, R as fiberIdWith, Ri as getOrElse, Rn as sleep, Rr as suspend$4, Rt as uninterruptibleMask, S as addFinalizer$1, Si as toArray, Sn as isInterrupted, Sr as exitFail, St as sandbox, T as as, Ti as unsafeHead, Tn as reduceWithContext, Tr as exitVoid, Tt as scoped$4, U as forkDaemon, Ui as none, Un as update$1, Ur as void_$2, Ut as zip$1, V as fnUntraced, Vi as map$8, Vn as modify$1, Vr as uninterruptible, Vt as when, W as forkIn, Wi as some, Wn as TypeId$10, Wr as whenEffect, Wt as zipLeft, X as intoDeferred, Xi as BaseProto, Xn as failCause$3, Xr as combineAll, Xt as fromEffect$5, Y as interruptible, Yi as pipeArguments, Yn as fail$4, Yr as combine, Yt as tagged, Z as isEffect, Zi as NodeInspectSymbol, Zn as isFailure, Zr as ids, Zt as mergeAll$2, _ as decodeSync, _i as prepend, _n as failureOrCause, _r as deferredMake, _t as repeat, a as Literal, aa as constVoid, ai as decode, an as forkIn$1, ar as as$1, at as matchCauseEffect, b as acquireRelease, bi as take$2, bn as interruptors, br as deferredUnsafeMake, bt as runPromiseExit$1, c as String$, ca as pipe, ci as appendAll, cn as addFinalizer, cr as catchAll, ct as onError, d as Union, di as filter, dn as extend, dr as deferredComplete, dt as orElseSucceed, ea as equals, ei as get$4, en as scoped$3, er as match$3, et as map$4, f as between, fi as fromIterable$1, fn as fork, fr as deferredFail, ft as promise, gi as of, gn as fail$3, gr as deferredIsDone, gt as raceWith, hi as map$7, hn as die$1, hr as deferredInterruptWith, ht as race, i as Defect, ia as constUndefined, ii as size$2, in as updateContext$1, ir as zipRight$2, it as matchCause, j as die$2, ji as omit, jn as _await$2, jr as interruptAsFiber, jt as tap, k as catchAllCause, ki as get$5, kn as scopeMake, kr as flatMap$3, kt as suspend$5, l as Struct, li as drop, ln as addFinalizerExit, lr as currentContext$1, lt as orDie, m as optional$1, mi as isEmpty$2, mn as NoSuchElementException, mr as deferredInterrupt, mt as provideService, n as Boolean$, na as constFalse, ni as set$2, nn as unsafeFork, nr as void_$3, nt as mapError, o as Never, oa as dual, oi as isFinite, on as unsafeMakeSemaphore, or as asVoid, ot as matchEffect, p as decodeUnknown, pi as isChunk, pn as make$18, pr as deferredFailCause, pt as provide$2, q as ignore, qi as match$2, qn as all, qr as DeferredTypeId$1, qt as spaced$1, r as Cause, ra as constTrue, ri as has, rn as unsafeRunPromiseExit, rr as zip$2, rt as match$5, s as Number$, sa as identity, si as append$1, sn as Scope, sr as asyncInterrupt, st as negate, t as Array$, ta as hasProperty, ti as make$17, tn as succeed$3, tr as succeed$5, tt as mapBoth$1, u as TaggedError, ui as empty$4, un as close, ur as deferredAwait, ut as orElse, v as encodeSync, vi as reduce, vn as flipCauseOption, vr as deferredSucceed, vt as retry, w as annotateLogs, wi as unsafeGet, wn as map$6, wr as exitSucceed, wt as scopeWith, x as acquireUseRelease$2, xi as takeRight, xn as isDieType, xr as exitAs, xt as runtime, y as _void, yi as reverse, yn as interrupt$3, yr as deferredUnsafeDone, yt as runPromise, z as flatMap$4, zi as isNone, zn as get$3, zr as sync$1, zt as unsafeMakeLatch } from "./chunks/Schema-B5yK7QrT.mjs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import * as Crypto from "node:crypto";
|
|
@@ -9,11 +9,11 @@ import * as Path from "node:path";
|
|
|
9
9
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
10
10
|
import { PassThrough } from "node:stream";
|
|
11
11
|
import * as NFS from "node:fs";
|
|
12
|
-
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
13
13
|
import * as OS from "node:os";
|
|
14
14
|
import { homedir, tmpdir } from "node:os";
|
|
15
15
|
import * as ChildProcess from "node:child_process";
|
|
16
|
-
import { execFileSync, execSync } from "node:child_process";
|
|
16
|
+
import { execFileSync, execSync, spawnSync } from "node:child_process";
|
|
17
17
|
import * as NodeUrl from "node:url";
|
|
18
18
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
19
19
|
import * as readline from "node:readline";
|
|
@@ -2438,9 +2438,9 @@ const pipeToOrFail = /*#__PURE__*/ dual(2, (self, that) => suspend$3(() => {
|
|
|
2438
2438
|
return pipeTo$1(pipeTo$1(pipeTo$1(self, reader), that), writer);
|
|
2439
2439
|
}));
|
|
2440
2440
|
/** @internal */
|
|
2441
|
-
const run$
|
|
2441
|
+
const run$4 = (self) => scopedWith$1((scope) => runIn(self, scope));
|
|
2442
2442
|
/** @internal */
|
|
2443
|
-
const runDrain = (self) => run$
|
|
2443
|
+
const runDrain = (self) => run$4(drain$2(self));
|
|
2444
2444
|
/** @internal */
|
|
2445
2445
|
const runScoped = (self) => scopeWith((scope) => runIn(self, scope));
|
|
2446
2446
|
/** @internal */
|
|
@@ -3013,7 +3013,7 @@ const map = /*#__PURE__*/ dual(2, (self, f) => new StreamImpl(pipe(toChannel$2(s
|
|
|
3013
3013
|
/** @internal */
|
|
3014
3014
|
const mapChunks$1 = /*#__PURE__*/ dual(2, (self, f) => new StreamImpl(pipe(toChannel$2(self), mapOut(f))));
|
|
3015
3015
|
/** @internal */
|
|
3016
|
-
const mkString$1 = (self) => run$
|
|
3016
|
+
const mkString$1 = (self) => run$3(self, mkString$2);
|
|
3017
3017
|
/** @internal */
|
|
3018
3018
|
const pipeThroughChannel = /*#__PURE__*/ dual(2, (self, channel) => new StreamImpl(pipeTo$1(toChannel$2(self), channel)));
|
|
3019
3019
|
/** @internal */
|
|
@@ -3022,9 +3022,9 @@ const repeatEffectChunkOption = (effect) => unfoldChunkEffect(effect, (effect) =
|
|
|
3022
3022
|
onSome: fail$5
|
|
3023
3023
|
}))));
|
|
3024
3024
|
/** @internal */
|
|
3025
|
-
const run$
|
|
3025
|
+
const run$3 = /*#__PURE__*/ dual(2, (self, sink) => toChannel$2(self).pipe(pipeToOrFail(toChannel$3(sink)), runDrain));
|
|
3026
3026
|
/** @internal */
|
|
3027
|
-
const runCollect$1 = (self) => run$
|
|
3027
|
+
const runCollect$1 = (self) => run$3(self, collectAll());
|
|
3028
3028
|
/** @internal */
|
|
3029
3029
|
const runIntoQueueScoped = /*#__PURE__*/ dual(2, (self, queue) => {
|
|
3030
3030
|
const writer = readWithCause({
|
|
@@ -3764,7 +3764,7 @@ const mkString = mkString$1;
|
|
|
3764
3764
|
* @since 2.0.0
|
|
3765
3765
|
* @category destructors
|
|
3766
3766
|
*/
|
|
3767
|
-
const run$
|
|
3767
|
+
const run$2 = run$3;
|
|
3768
3768
|
/**
|
|
3769
3769
|
* Runs the stream and collects all of its elements to a chunk.
|
|
3770
3770
|
*
|
|
@@ -4726,7 +4726,7 @@ const makeExecutor$1 = (start) => {
|
|
|
4726
4726
|
stream,
|
|
4727
4727
|
string: (command, encoding = "utf-8") => {
|
|
4728
4728
|
const decoder = new TextDecoder(encoding);
|
|
4729
|
-
return pipe(start(command), flatMap$4((process) => run$
|
|
4729
|
+
return pipe(start(command), flatMap$4((process) => run$2(process.stdout, collectUint8Array)), map$4((bytes) => decoder.decode(bytes)), scoped$4);
|
|
4730
4730
|
},
|
|
4731
4731
|
lines: (command, encoding = "utf-8") => {
|
|
4732
4732
|
return pipe(streamLines(command, encoding), runCollect, map$4(toArray));
|
|
@@ -4959,7 +4959,7 @@ const ProcessProto = {
|
|
|
4959
4959
|
};
|
|
4960
4960
|
}
|
|
4961
4961
|
};
|
|
4962
|
-
const runCommand$
|
|
4962
|
+
const runCommand$2 = (fileSystem) => (command) => {
|
|
4963
4963
|
switch (command._tag) {
|
|
4964
4964
|
case "StandardCommand": {
|
|
4965
4965
|
const spawn = flatMap$4(make$15(), (exitCode) => async((resume) => {
|
|
@@ -5031,17 +5031,17 @@ const runCommand$1 = (fileSystem) => (command) => {
|
|
|
5031
5031
|
stderr,
|
|
5032
5032
|
stdout
|
|
5033
5033
|
});
|
|
5034
|
-
}), typeof command.stdin === "string" ? identity : tap((process) => forkDaemon(run$
|
|
5034
|
+
}), typeof command.stdin === "string" ? identity : tap((process) => forkDaemon(run$2(command.stdin, process.stdin))));
|
|
5035
5035
|
}
|
|
5036
5036
|
case "PipedCommand": {
|
|
5037
5037
|
const flattened = flatten(command);
|
|
5038
|
-
if (flattened.length === 1) return pipe(flattened[0], runCommand$
|
|
5038
|
+
if (flattened.length === 1) return pipe(flattened[0], runCommand$2(fileSystem));
|
|
5039
5039
|
const head = flattened[0];
|
|
5040
5040
|
const tail = flattened.slice(1);
|
|
5041
5041
|
const initial = tail.slice(0, tail.length - 1);
|
|
5042
5042
|
const last = tail[tail.length - 1];
|
|
5043
|
-
const stream = initial.reduce((stdin$2, command) => pipe(stdin(command, stdin$2), runCommand$
|
|
5044
|
-
return pipe(stdin(last, stream), runCommand$
|
|
5043
|
+
const stream = initial.reduce((stdin$2, command) => pipe(stdin(command, stdin$2), runCommand$2(fileSystem), map$4((process) => process.stdout), unwrapScoped), pipe(runCommand$2(fileSystem)(head), map$4((process) => process.stdout), unwrapScoped));
|
|
5044
|
+
return pipe(stdin(last, stream), runCommand$2(fileSystem));
|
|
5045
5045
|
}
|
|
5046
5046
|
}
|
|
5047
5047
|
};
|
|
@@ -5051,7 +5051,7 @@ const runCommand$1 = (fileSystem) => (command) => {
|
|
|
5051
5051
|
* @since 1.0.0
|
|
5052
5052
|
* @category layer
|
|
5053
5053
|
*/
|
|
5054
|
-
const layer$5 = /* @__PURE__ */ effect$1(CommandExecutor, /*#__PURE__*/ pipe(FileSystem, /*#__PURE__*/ map$4((fileSystem) => makeExecutor(runCommand$
|
|
5054
|
+
const layer$5 = /* @__PURE__ */ effect$1(CommandExecutor, /*#__PURE__*/ pipe(FileSystem, /*#__PURE__*/ map$4((fileSystem) => makeExecutor(runCommand$2(fileSystem)))));
|
|
5055
5055
|
//#endregion
|
|
5056
5056
|
//#region node_modules/@effect/platform-node-shared/dist/esm/internal/path.js
|
|
5057
5057
|
const fromFileUrl = (url) => try_({
|
|
@@ -5352,7 +5352,7 @@ const classifyUntracked = (exec, ref, untrackedPaths) => gen(function* () {
|
|
|
5352
5352
|
* `git add`. Callers that expect a probe to fail handle it with an explicit
|
|
5353
5353
|
* `catchAll`.
|
|
5354
5354
|
*/
|
|
5355
|
-
const run = (root, ...args) => scoped$4(gen(function* () {
|
|
5355
|
+
const run$1 = (root, ...args) => scoped$4(gen(function* () {
|
|
5356
5356
|
const process = yield* (yield* CommandExecutor).start(make$1(...args).pipe(workingDirectory(root), stdout("pipe"), stderr("pipe")));
|
|
5357
5357
|
const collect = (stream) => stream.pipe(decodeText(), mkString);
|
|
5358
5358
|
const [stdout$3, stderr$3, exitCode] = yield* all$1([
|
|
@@ -5386,7 +5386,7 @@ var InvalidRefError = class extends Error {
|
|
|
5386
5386
|
*/
|
|
5387
5387
|
const resolvedRefOrCorrupted = (ref, hash) => /^[0-9a-f]{40}$/.test(hash) ? succeed$6(hash) : fail$5(new InvalidRefError(`Invalid ref: ${ref}`, [`ref: ${ref}`]));
|
|
5388
5388
|
const makeGitImpl = (executor, root) => {
|
|
5389
|
-
const exec = (...args) => run(root, ...args).pipe(provide$2(succeed$1(CommandExecutor, executor)));
|
|
5389
|
+
const exec = (...args) => run$1(root, ...args).pipe(provide$2(succeed$1(CommandExecutor, executor)));
|
|
5390
5390
|
return {
|
|
5391
5391
|
lastCommitSubject: (ref = "HEAD") => exec("git", "log", "-1", "--pretty=%s", "--end-of-options", ref).pipe(map$4((s) => s.trim())),
|
|
5392
5392
|
lastCommitMessage: (ref = "HEAD") => exec("git", "log", "-1", "--pretty=%B", "--end-of-options", ref).pipe(map$4((s) => s.trim())),
|
|
@@ -5420,7 +5420,7 @@ const makeGitImpl = (executor, root) => {
|
|
|
5420
5420
|
}),
|
|
5421
5421
|
resolveRef: (ref) => exec("git", "rev-parse", "--verify", "--end-of-options", ref).pipe(map$4((s) => s.trim()), flatMap$4((hash) => resolvedRefOrCorrupted(ref, hash))),
|
|
5422
5422
|
readRefOption: (ref) => exec("git", "rev-parse", "--verify", "--quiet", ref).pipe(map$4((s) => some(s.trim())), catchAll$1(() => succeed$6(none()))),
|
|
5423
|
-
isAncestor: (a, b) => run(root, "git", "merge-base", "--is-ancestor", a, b).pipe(provide$2(succeed$1(CommandExecutor, executor)), map$4(() => true), catchAll$1(() => succeed$6(false))),
|
|
5423
|
+
isAncestor: (a, b) => run$1(root, "git", "merge-base", "--is-ancestor", a, b).pipe(provide$2(succeed$1(CommandExecutor, executor)), map$4(() => true), catchAll$1(() => succeed$6(false))),
|
|
5424
5424
|
mergeBase: (a, b) => exec("git", "merge-base", a, b).pipe(map$4((s) => some(s.trim())), catchAll$1(() => succeed$6(none()))),
|
|
5425
5425
|
defaultBranch: () => exec("git", "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD").pipe(map$4((s) => s.trim()), catchAll$1(() => succeed$6("")), map$4((name) => name === "" ? "main" : name)),
|
|
5426
5426
|
topLevel: () => exec("git", "rev-parse", "--show-toplevel").pipe(map$4((s) => s.trim())),
|
|
@@ -7503,6 +7503,30 @@ var ConfigDiscovery = class ConfigDiscovery extends Tag("ConfigDiscovery")() {
|
|
|
7503
7503
|
});
|
|
7504
7504
|
};
|
|
7505
7505
|
//#endregion
|
|
7506
|
+
//#region src/workflow/fingerprint.ts
|
|
7507
|
+
const HASH_LENGTH = 16;
|
|
7508
|
+
const CONTROL_CHARACTER = /[\x00-\x1f\x7f-\x9f]/;
|
|
7509
|
+
const contentHash = (content) => createHash("sha256").update(content).digest("hex").slice(0, HASH_LENGTH);
|
|
7510
|
+
const realOr = (path) => {
|
|
7511
|
+
try {
|
|
7512
|
+
return realpathSync(path);
|
|
7513
|
+
} catch {
|
|
7514
|
+
return path;
|
|
7515
|
+
}
|
|
7516
|
+
};
|
|
7517
|
+
const configFingerprint = (files, root, read) => {
|
|
7518
|
+
const fingerprint = {};
|
|
7519
|
+
const realRoot = realOr(root);
|
|
7520
|
+
for (const file of files) {
|
|
7521
|
+
const path = relative(realRoot, realOr(file)).split(sep).join("/");
|
|
7522
|
+
if (CONTROL_CHARACTER.test(path)) continue;
|
|
7523
|
+
const content = read(file);
|
|
7524
|
+
if (content !== void 0) fingerprint[path] = contentHash(content);
|
|
7525
|
+
}
|
|
7526
|
+
return fingerprint;
|
|
7527
|
+
};
|
|
7528
|
+
const changedConfigFiles = (recorded, current) => [.../* @__PURE__ */ new Set([...Object.keys(recorded), ...Object.keys(current)])].filter((path) => recorded[path] !== current[path]).sort();
|
|
7529
|
+
//#endregion
|
|
7506
7530
|
//#region src/workflow/Diagnostic.ts
|
|
7507
7531
|
const compareSegment = (a, b) => {
|
|
7508
7532
|
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
@@ -7637,9 +7661,16 @@ const load = gen(function* () {
|
|
|
7637
7661
|
},
|
|
7638
7662
|
...compiled.ui !== void 0 ? { ui: compiled.ui } : {},
|
|
7639
7663
|
warnings: diagnostics.filter((d) => d.severity === "warning"),
|
|
7640
|
-
|
|
7664
|
+
fingerprint: configFingerprint(loaded.file.dependencyFiles, host.root, module.read ?? readFromDisk)
|
|
7641
7665
|
};
|
|
7642
7666
|
});
|
|
7667
|
+
const readFromDisk = (path) => {
|
|
7668
|
+
try {
|
|
7669
|
+
return readFileSync(path, "utf8");
|
|
7670
|
+
} catch {
|
|
7671
|
+
return;
|
|
7672
|
+
}
|
|
7673
|
+
};
|
|
7643
7674
|
const TRANSFORM_MEMO_SIZE = 512;
|
|
7644
7675
|
const memoized = (transform) => {
|
|
7645
7676
|
const memo = /* @__PURE__ */ new Map();
|
|
@@ -7656,12 +7687,18 @@ const memoized = (transform) => {
|
|
|
7656
7687
|
};
|
|
7657
7688
|
let jitiModule;
|
|
7658
7689
|
let transform;
|
|
7659
|
-
const jiti = (shipped) => {
|
|
7690
|
+
const jiti = (shipped, local) => {
|
|
7660
7691
|
const require = createRequire(import.meta.url);
|
|
7661
7692
|
jitiModule ??= require("jiti");
|
|
7662
7693
|
transform ??= memoized(require(join(dirname(require.resolve("jiti/package.json")), "dist", "babel.cjs")));
|
|
7663
7694
|
return jitiModule.createJiti(import.meta.url, {
|
|
7664
|
-
virtualModules: shipped.modules,
|
|
7695
|
+
virtualModules: new Proxy(shipped.modules, {
|
|
7696
|
+
has: (modules, id) => id in modules || typeof id === "string" && local?.resolve(id) !== void 0,
|
|
7697
|
+
get: (modules, id) => {
|
|
7698
|
+
const path = typeof id === "string" ? local?.resolve(id) : void 0;
|
|
7699
|
+
return path === void 0 ? modules[id] : local.load(path);
|
|
7700
|
+
}
|
|
7701
|
+
}),
|
|
7665
7702
|
alias: shipped.missing,
|
|
7666
7703
|
fsCache: false,
|
|
7667
7704
|
moduleCache: false,
|
|
@@ -7858,12 +7895,9 @@ const namespaceOf = (exported) => typeof exported === "object" && exported !== n
|
|
|
7858
7895
|
* pulled in, transitively.
|
|
7859
7896
|
*/
|
|
7860
7897
|
const loadWorkflows = (module, shipped) => {
|
|
7861
|
-
const
|
|
7862
|
-
const exports = namespaceOf(
|
|
7863
|
-
|
|
7864
|
-
cache
|
|
7865
|
-
}));
|
|
7866
|
-
const file = fileFrom(exports, module.filepath, Object.keys(cache));
|
|
7898
|
+
const { exports: evaluated, files } = module.read === void 0 ? evaluateFromDisk(module, shipped) : evaluateRead(module, module.read, shipped);
|
|
7899
|
+
const exports = namespaceOf(evaluated);
|
|
7900
|
+
const file = fileFrom(exports, module.filepath, files);
|
|
7867
7901
|
const exported = Object.entries(exports).filter(([name, value]) => !RESERVED_EXPORTS.has(name) && isWorkflow(value));
|
|
7868
7902
|
const value = defaultExport(exports);
|
|
7869
7903
|
const defaultName = exported.find(([, other]) => other === value)?.[0] ?? "default";
|
|
@@ -7887,6 +7921,49 @@ const loadWorkflows = (module, shipped) => {
|
|
|
7887
7921
|
diagnostics: nameRejections(rejections)
|
|
7888
7922
|
};
|
|
7889
7923
|
};
|
|
7924
|
+
const evaluateFromDisk = (module, shipped) => {
|
|
7925
|
+
const cache = {};
|
|
7926
|
+
return {
|
|
7927
|
+
exports: jiti(shipped).evalModule(module.source, {
|
|
7928
|
+
filename: module.filepath,
|
|
7929
|
+
cache
|
|
7930
|
+
}),
|
|
7931
|
+
files: Object.keys(cache)
|
|
7932
|
+
};
|
|
7933
|
+
};
|
|
7934
|
+
const moduleCandidates = (path) => [
|
|
7935
|
+
path,
|
|
7936
|
+
path.replace(/\.js$/, ".ts"),
|
|
7937
|
+
path.replace(/\.mjs$/, ".mts"),
|
|
7938
|
+
`${path}.ts`,
|
|
7939
|
+
join(path, "index.ts")
|
|
7940
|
+
];
|
|
7941
|
+
/**
|
|
7942
|
+
* Evaluates the config with every local module it imports taken from `read`
|
|
7943
|
+
* instead of the disk. jiti matches a virtual module on the specifier as
|
|
7944
|
+
* written, so each module gets its own instance resolving against its own
|
|
7945
|
+
* directory. A specifier `read` cannot serve falls through to the disk, and
|
|
7946
|
+
* fails the way a missing file does.
|
|
7947
|
+
*/
|
|
7948
|
+
const evaluateRead = (module, read, shipped) => {
|
|
7949
|
+
const files = [module.filepath];
|
|
7950
|
+
const evaluated = /* @__PURE__ */ new Map();
|
|
7951
|
+
const evaluate = (filepath, source) => jiti(shipped, {
|
|
7952
|
+
resolve: (specifier) => /^\.\.?\//.test(specifier) ? moduleCandidates(join(dirname(filepath), specifier)).find((candidate) => read(candidate) !== void 0) : void 0,
|
|
7953
|
+
load: (path) => {
|
|
7954
|
+
if (!evaluated.has(path)) {
|
|
7955
|
+
evaluated.set(path, {});
|
|
7956
|
+
files.push(path);
|
|
7957
|
+
evaluated.set(path, evaluate(path, read(path)));
|
|
7958
|
+
}
|
|
7959
|
+
return evaluated.get(path);
|
|
7960
|
+
}
|
|
7961
|
+
}).evalModule(source, { filename: filepath });
|
|
7962
|
+
return {
|
|
7963
|
+
exports: evaluate(module.filepath, module.source),
|
|
7964
|
+
files
|
|
7965
|
+
};
|
|
7966
|
+
};
|
|
7890
7967
|
const isWorkflow = (value) => typeof value === "function" || isFactoryWorkflow(value);
|
|
7891
7968
|
const defaultExport = (exports) => {
|
|
7892
7969
|
const value = exports.default;
|
|
@@ -8114,35 +8191,6 @@ const resolveScopeAccess = (def, skills, vars) => gen(function* () {
|
|
|
8114
8191
|
//#region src/workflow/Workflow.ts
|
|
8115
8192
|
const knownModes = (def) => Object.keys(def.modes);
|
|
8116
8193
|
//#endregion
|
|
8117
|
-
//#region src/workflow/fingerprint.ts
|
|
8118
|
-
const HASH_LENGTH = 16;
|
|
8119
|
-
const CONTROL_CHARACTER = /[\x00-\x1f\x7f-\x9f]/;
|
|
8120
|
-
const contentHash = (content) => createHash("sha256").update(content).digest("hex").slice(0, HASH_LENGTH);
|
|
8121
|
-
const realOr = (path) => {
|
|
8122
|
-
try {
|
|
8123
|
-
return realpathSync(path);
|
|
8124
|
-
} catch {
|
|
8125
|
-
return path;
|
|
8126
|
-
}
|
|
8127
|
-
};
|
|
8128
|
-
const configFingerprint = (files, root) => {
|
|
8129
|
-
const fingerprint = {};
|
|
8130
|
-
const realRoot = realOr(root);
|
|
8131
|
-
for (const file of files) {
|
|
8132
|
-
const path = relative(realRoot, realOr(file)).split(sep).join("/");
|
|
8133
|
-
if (CONTROL_CHARACTER.test(path)) continue;
|
|
8134
|
-
let content;
|
|
8135
|
-
try {
|
|
8136
|
-
content = readFileSync(file);
|
|
8137
|
-
} catch {
|
|
8138
|
-
continue;
|
|
8139
|
-
}
|
|
8140
|
-
fingerprint[path] = contentHash(content);
|
|
8141
|
-
}
|
|
8142
|
-
return fingerprint;
|
|
8143
|
-
};
|
|
8144
|
-
const changedConfigFiles = (recorded, current) => [.../* @__PURE__ */ new Set([...Object.keys(recorded), ...Object.keys(current)])].filter((path) => recorded[path] !== current[path]).sort();
|
|
8145
|
-
//#endregion
|
|
8146
8194
|
//#region src/step/planStep.ts
|
|
8147
8195
|
/**
|
|
8148
8196
|
* At the human review gate an unconditional `gtd uncheck '<file>'` runs
|
|
@@ -8858,7 +8906,7 @@ const restAt = (ref, startOverrides = {}) => gen(function* () {
|
|
|
8858
8906
|
path: e.path
|
|
8859
8907
|
})) : [];
|
|
8860
8908
|
const memory = memoryOf(outcome.trace, run);
|
|
8861
|
-
const configNow =
|
|
8909
|
+
const configNow = config.fingerprint;
|
|
8862
8910
|
if (ref === void 0) yield* warnConfigChanges(run, configNow, workspace);
|
|
8863
8911
|
return {
|
|
8864
8912
|
def,
|
|
@@ -9087,7 +9135,7 @@ const inRepoConfigPaths = (root, files, recorded) => {
|
|
|
9087
9135
|
const configRefusal = (tip, paths) => /* @__PURE__ */ new Error(`gtd history: refused — config differs at ${tip.slice(0, 7)}: ${paths.join(", ")}. A history ref replays under today's config, so it is unreadable after the config changes`);
|
|
9088
9136
|
const differingConfigPaths = (git, workspace, root, config, run, hash) => gen(function* () {
|
|
9089
9137
|
const differing = [];
|
|
9090
|
-
for (const path of inRepoConfigPaths(root, config.
|
|
9138
|
+
for (const path of inRepoConfigPaths(root, Object.keys(config.fingerprint), run.pinnedConfig?.files ?? {})) if ((yield* orElseSucceed(git.readFileAtRef(hash, path), () => void 0)) !== workspace.readSync(path)) differing.push(path);
|
|
9091
9139
|
return differing;
|
|
9092
9140
|
});
|
|
9093
9141
|
const notClosedError = (hash, outcome) => outcome.kind === "rest" ? /* @__PURE__ */ new Error(`gtd history: ${hash.slice(0, 7)} does not close a process under today's config`) : replayError(outcome) ?? /* @__PURE__ */ new Error("gtd history: replay failed");
|
|
@@ -10079,12 +10127,6 @@ const runValidateCommand = (out) => gen(function* () {
|
|
|
10079
10127
|
const resolved = yield* resolveValidateScript(rest);
|
|
10080
10128
|
out.write(resolved !== void 0 && resolved.script.length > 0 ? `${resolved.script}\n` : `nothing to validate at "${rest.state}"\n`);
|
|
10081
10129
|
});
|
|
10082
|
-
/** `file:line:col: message` — the shape editors and grep already jump on. Stored 0-based, printed 1-based. A finding with a `line` but no `range`, or none at all, degrades to the shorter forms. */
|
|
10083
|
-
const formatFinding = (file, finding) => {
|
|
10084
|
-
if (finding.line === void 0) return finding.message;
|
|
10085
|
-
const col = finding.range !== void 0 ? `:${finding.range.start.character + 1}` : "";
|
|
10086
|
-
return `${file}:${finding.line + 1}${col}: ${finding.message}`;
|
|
10087
|
-
};
|
|
10088
10130
|
/**
|
|
10089
10131
|
* Runs `<mode>`'s parser over `<file>`, exiting non-zero on any finding.
|
|
10090
10132
|
* Resolves no workflow state and reads no config — this is what a seeded
|
|
@@ -10300,7 +10342,7 @@ const dispatchVoidCommand = (command, json, out) => {
|
|
|
10300
10342
|
* (the `standaloneKinds` run bare). Both guard checks run before `dispatch`,
|
|
10301
10343
|
* so a refusal writes nothing and emits no script by construction.
|
|
10302
10344
|
*/
|
|
10303
|
-
const runCommand = (command, json, out) => {
|
|
10345
|
+
const runCommand$1 = (command, json, out) => {
|
|
10304
10346
|
const dispatch = dispatchVoidCommand(command, json, out);
|
|
10305
10347
|
if (needsOf(command.kind) !== "state") return dispatch;
|
|
10306
10348
|
return gen(function* () {
|
|
@@ -11261,7 +11303,7 @@ const runCli = (argv, io) => {
|
|
|
11261
11303
|
io.exit(2);
|
|
11262
11304
|
});
|
|
11263
11305
|
const out = bufferedArtifactOut(io);
|
|
11264
|
-
return runCommand(plan.command, plan.json, out).pipe(map$4(() => {
|
|
11306
|
+
return runCommand$1(plan.command, plan.json, out).pipe(map$4(() => {
|
|
11265
11307
|
out.flush();
|
|
11266
11308
|
io.exit(0);
|
|
11267
11309
|
}), provide$2(io.layers(plan.verbose)), sandbox, catchAll$1((cause) => isInterruptedOnly(cause) ? failCause$4(cause) : report(io, plan.json.kind !== "off")(cause)));
|
|
@@ -11340,10 +11382,10 @@ var InMemRepo = class InMemRepo {
|
|
|
11340
11382
|
}
|
|
11341
11383
|
resolveRef(ref) {
|
|
11342
11384
|
if (/^[0-9a-f]{40}$/.test(ref)) return this.commits.has(ref) ? ref : null;
|
|
11343
|
-
const tildeMatch = /^(HEAD|[0-9a-f]{40})(~(\d+))?$/.exec(ref);
|
|
11385
|
+
const tildeMatch = /^(HEAD|[0-9a-f]{40})(~(\d+)|\^)?$/.exec(ref);
|
|
11344
11386
|
if (tildeMatch) {
|
|
11345
11387
|
const base = tildeMatch[1];
|
|
11346
|
-
const steps = tildeMatch[3] !== void 0 ? parseInt(tildeMatch[3], 10) : 0;
|
|
11388
|
+
const steps = tildeMatch[3] !== void 0 ? parseInt(tildeMatch[3], 10) : tildeMatch[2] === "^" ? 1 : 0;
|
|
11347
11389
|
let cur = base === "HEAD" ? this.head : this.commits.has(base) ? base : null;
|
|
11348
11390
|
for (let i = 0; i < steps; i++) {
|
|
11349
11391
|
if (cur === null) return null;
|
|
@@ -11480,6 +11522,33 @@ var InMemRepo = class InMemRepo {
|
|
|
11480
11522
|
const dirPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
|
|
11481
11523
|
return [...this.worktree.keys()].filter((key) => key === prefix || key.startsWith(dirPrefix)).sort();
|
|
11482
11524
|
}
|
|
11525
|
+
/** Everything git would keep on disk, serialized — two snapshots compare equal exactly when nothing was written. */
|
|
11526
|
+
snapshot() {
|
|
11527
|
+
const sorted = (m) => [...m.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
11528
|
+
return JSON.stringify({
|
|
11529
|
+
head: this.head,
|
|
11530
|
+
branch: this.currentBranch,
|
|
11531
|
+
commits: [...this.commits.keys()].sort(),
|
|
11532
|
+
branches: sorted(this.branches),
|
|
11533
|
+
refs: sorted(this.refs),
|
|
11534
|
+
index: sorted(this.index),
|
|
11535
|
+
worktree: sorted(this.worktree)
|
|
11536
|
+
});
|
|
11537
|
+
}
|
|
11538
|
+
/** Every ref name under `prefix` (`refs/…`), sorted — `git for-each-ref <prefix>`. */
|
|
11539
|
+
refNamesUnder(prefix) {
|
|
11540
|
+
return [...this.refs.keys()].filter((ref) => ref.startsWith(prefix)).sort();
|
|
11541
|
+
}
|
|
11542
|
+
/** Whether `path` is in the index — what `git diff <commit> -- <path>` compares against the worktree. */
|
|
11543
|
+
isTracked(path) {
|
|
11544
|
+
return this.index.has(path);
|
|
11545
|
+
}
|
|
11546
|
+
/** `git add <path>` — the index takes `path` as the worktree has it, or drops it. */
|
|
11547
|
+
stage(path) {
|
|
11548
|
+
const content = this.worktree.get(path);
|
|
11549
|
+
if (content === void 0) this.index.delete(path);
|
|
11550
|
+
else this.index.set(path, content);
|
|
11551
|
+
}
|
|
11483
11552
|
/** `git add -A` — the index becomes exactly the current worktree. */
|
|
11484
11553
|
stageAll() {
|
|
11485
11554
|
this.index = new Map(this.worktree);
|
|
@@ -11809,7 +11878,8 @@ const makeInMemoryConfigDiscovery = (repo, root) => {
|
|
|
11809
11878
|
const source = workspace.atPath(filepath);
|
|
11810
11879
|
if (source !== void 0) return {
|
|
11811
11880
|
filepath,
|
|
11812
|
-
source
|
|
11881
|
+
source,
|
|
11882
|
+
read: workspace.atPath
|
|
11813
11883
|
};
|
|
11814
11884
|
}
|
|
11815
11885
|
},
|
|
@@ -11872,6 +11942,269 @@ const makeCapturingCliIo = (repo, env = {}, tools = {}) => {
|
|
|
11872
11942
|
};
|
|
11873
11943
|
};
|
|
11874
11944
|
//#endregion
|
|
11945
|
+
//#region src/testkit/FlowScripts.ts
|
|
11946
|
+
const SHEBANG = "#!/usr/bin/env sh";
|
|
11947
|
+
const words$1 = (text) => [...text.matchAll(/'((?:[^']|'\\'')*)'/g)].map((m) => m[1].replaceAll("'\\''", "'"));
|
|
11948
|
+
const ok = (output = "") => ({
|
|
11949
|
+
ok: true,
|
|
11950
|
+
output
|
|
11951
|
+
});
|
|
11952
|
+
/** `rm -rf -- <paths>`. `InMemRepo.pathsUnder("")` is the whole tree, where a real `rm -- ''` removes nothing. */
|
|
11953
|
+
const removeAll = (repo, paths) => {
|
|
11954
|
+
if (paths.includes("")) throw new Error("testkit: rm: refusing an empty path");
|
|
11955
|
+
for (const path of paths) for (const file of repo.pathsUnder(path)) repo.deleteFile(file);
|
|
11956
|
+
};
|
|
11957
|
+
const deleteIfPresent = (repo, path) => {
|
|
11958
|
+
if (repo.readFile(path) !== void 0) repo.deleteFile(path);
|
|
11959
|
+
};
|
|
11960
|
+
const recognizeRemove = (repo, script) => {
|
|
11961
|
+
const line = script.split("\n").find((l) => l.startsWith("rm -rf -- ")) ?? "";
|
|
11962
|
+
const paths = words$1(line);
|
|
11963
|
+
if (removeScript(paths) !== script) return void 0;
|
|
11964
|
+
removeAll(repo, paths);
|
|
11965
|
+
return ok();
|
|
11966
|
+
};
|
|
11967
|
+
const recognizeMove = (repo, script) => {
|
|
11968
|
+
const line = script.split("\n").find((l) => l.startsWith("mv -- ")) ?? "";
|
|
11969
|
+
const [from, to] = words$1(line);
|
|
11970
|
+
if (from === void 0 || to === void 0 || moveScript(from, to) !== script) return void 0;
|
|
11971
|
+
const content = repo.readFile(from);
|
|
11972
|
+
if (content === void 0) return {
|
|
11973
|
+
ok: false,
|
|
11974
|
+
error: `mv: ${from}: No such file`,
|
|
11975
|
+
output: ""
|
|
11976
|
+
};
|
|
11977
|
+
repo.deleteFile(from);
|
|
11978
|
+
repo.writeFile(to, content);
|
|
11979
|
+
return ok();
|
|
11980
|
+
};
|
|
11981
|
+
const recognizeShow = (repo, script) => {
|
|
11982
|
+
const line = script.split("\n").find((l) => l.startsWith("git show ")) ?? "";
|
|
11983
|
+
const [source, to] = words$1(line);
|
|
11984
|
+
const at = source?.indexOf(":") ?? -1;
|
|
11985
|
+
if (source === void 0 || to === void 0 || at === -1) return void 0;
|
|
11986
|
+
const ref = source.slice(0, at);
|
|
11987
|
+
const path = source.slice(at + 1);
|
|
11988
|
+
if (showScript(ref, path, to) !== script) return void 0;
|
|
11989
|
+
const content = repo.fileAtRef(ref, path);
|
|
11990
|
+
if (content === null) return {
|
|
11991
|
+
ok: false,
|
|
11992
|
+
error: `git show: ${source} does not exist`,
|
|
11993
|
+
output: ""
|
|
11994
|
+
};
|
|
11995
|
+
repo.writeFile(to, content);
|
|
11996
|
+
return ok();
|
|
11997
|
+
};
|
|
11998
|
+
/** `git diff --quiet <commit> -- <path>`: the worktree copy of a tracked path, or nothing for an untracked one, against `commit`'s. */
|
|
11999
|
+
const unchangedSince = (repo, commit, path) => {
|
|
12000
|
+
return (repo.isTracked(path) ? repo.readFile(path) ?? null : null) === repo.fileAtRef(commit, path);
|
|
12001
|
+
};
|
|
12002
|
+
const RESTORE_LINE = / && git restore --source=/;
|
|
12003
|
+
const REMOVE_LINE = / && rm -f -- /;
|
|
12004
|
+
const parseRestoreLine = (line) => {
|
|
12005
|
+
const [commit, path, source] = words$1(line);
|
|
12006
|
+
if (commit === void 0 || path === void 0) return void 0;
|
|
12007
|
+
if (RESTORE_LINE.test(line) && source !== void 0) return {
|
|
12008
|
+
commit,
|
|
12009
|
+
path,
|
|
12010
|
+
source
|
|
12011
|
+
};
|
|
12012
|
+
return REMOVE_LINE.test(line) ? {
|
|
12013
|
+
commit,
|
|
12014
|
+
path
|
|
12015
|
+
} : void 0;
|
|
12016
|
+
};
|
|
12017
|
+
/** Applies one line, returning its exit status as `sh` would see it. */
|
|
12018
|
+
const applyRestoreLine = (repo, { commit, path, source }) => {
|
|
12019
|
+
if (!unchangedSince(repo, commit, path)) return false;
|
|
12020
|
+
if (source !== void 0 && repo.resolveRef(source) === null) return false;
|
|
12021
|
+
const content = source === void 0 ? null : repo.fileAtRef(source, path);
|
|
12022
|
+
if (content !== null) repo.writeFile(path, content);
|
|
12023
|
+
else if (source === void 0 || repo.isTracked(path)) deleteIfPresent(repo, path);
|
|
12024
|
+
return true;
|
|
12025
|
+
};
|
|
12026
|
+
const recognizeRestore = (repo, script) => {
|
|
12027
|
+
const parsed = script.split("\n").slice(2, -1).map(parseRestoreLine);
|
|
12028
|
+
if (parsed.includes(void 0)) return void 0;
|
|
12029
|
+
const lines = parsed;
|
|
12030
|
+
const restoring = lines.filter((l) => l.source !== void 0);
|
|
12031
|
+
const paths = {
|
|
12032
|
+
restore: restoring.map((l) => l.path),
|
|
12033
|
+
remove: lines.filter((l) => l.source === void 0).map((l) => l.path)
|
|
12034
|
+
};
|
|
12035
|
+
if (restoreScript(lines[0]?.commit ?? "", paths, restoring[0]?.source) !== script) return void 0;
|
|
12036
|
+
if (lines.map((line) => applyRestoreLine(repo, line)).at(-1) === false) return {
|
|
12037
|
+
ok: false,
|
|
12038
|
+
error: "the last path changed since the commit",
|
|
12039
|
+
output: ""
|
|
12040
|
+
};
|
|
12041
|
+
return ok();
|
|
12042
|
+
};
|
|
12043
|
+
const PATCH_FAILED = "error: patch does not apply";
|
|
12044
|
+
/**
|
|
12045
|
+
* `git diff C^ C | git apply -R`, which is all or nothing. A file still
|
|
12046
|
+
* exactly as `C` left it reverts; a file gone, or one `C` added and that
|
|
12047
|
+
* changed since, fails the whole patch. A file `C` modified and that changed
|
|
12048
|
+
* since may still apply through its context — that needs real git, so it
|
|
12049
|
+
* throws rather than guess.
|
|
12050
|
+
*/
|
|
12051
|
+
const reverseApply = (repo, commit) => {
|
|
12052
|
+
const parent = `${commit}^`;
|
|
12053
|
+
const changed = [.../* @__PURE__ */ new Set([...repo.pathsAtRef(parent), ...repo.pathsAtRef(commit)])].filter((p) => repo.fileAtRef(parent, p) !== repo.fileAtRef(commit, p));
|
|
12054
|
+
for (const path of changed) {
|
|
12055
|
+
const after = repo.fileAtRef(commit, path);
|
|
12056
|
+
const current = repo.readFile(path) ?? null;
|
|
12057
|
+
if (current === after) continue;
|
|
12058
|
+
if (current === null || after === null || repo.fileAtRef(parent, path) === null) return `${PATCH_FAILED}: ${path}`;
|
|
12059
|
+
throw new Error(`testkit: cannot tell in memory whether reverting ${commit} applies to the hand-edited ${path} — run this @live`);
|
|
12060
|
+
}
|
|
12061
|
+
for (const path of changed) {
|
|
12062
|
+
const before = repo.fileAtRef(parent, path);
|
|
12063
|
+
if (before === null) repo.deleteFile(path);
|
|
12064
|
+
else repo.writeFile(path, before);
|
|
12065
|
+
}
|
|
12066
|
+
};
|
|
12067
|
+
const recognizeRevert = (repo, script) => {
|
|
12068
|
+
const lines = script.split("\n");
|
|
12069
|
+
const [, commit] = words$1(lines.find((l) => l.startsWith("if ! git diff --quiet ")) ?? "");
|
|
12070
|
+
const [, failure] = words$1(lines.find((l) => l.includes("printf '%s\\n\\n' ")) ?? "");
|
|
12071
|
+
const [report] = words$1(lines.find((l) => l.startsWith(" } > ")) ?? "");
|
|
12072
|
+
if (commit === void 0 || failure === void 0 || report === void 0) return void 0;
|
|
12073
|
+
if (revertScript(commit, report, failure) !== script) return void 0;
|
|
12074
|
+
const error = reverseApply(repo, commit);
|
|
12075
|
+
if (error !== void 0) repo.writeFile(report, `${failure}\n\n${error}\n`);
|
|
12076
|
+
return ok();
|
|
12077
|
+
};
|
|
12078
|
+
/** One preamble line — the closed vocabulary the bundled workflows write there. */
|
|
12079
|
+
const runPreambleLine = (repo, line) => {
|
|
12080
|
+
const quoted = words$1(line);
|
|
12081
|
+
if (line === "exit 0") return {
|
|
12082
|
+
exit: true,
|
|
12083
|
+
output: ""
|
|
12084
|
+
};
|
|
12085
|
+
if (line.startsWith("mkdir -p ")) return {
|
|
12086
|
+
exit: false,
|
|
12087
|
+
output: ""
|
|
12088
|
+
};
|
|
12089
|
+
if (line.startsWith("rm -f ")) {
|
|
12090
|
+
for (const path of quoted) deleteIfPresent(repo, path);
|
|
12091
|
+
return {
|
|
12092
|
+
exit: false,
|
|
12093
|
+
output: ""
|
|
12094
|
+
};
|
|
12095
|
+
}
|
|
12096
|
+
if (line.startsWith("rm -rf -- ")) {
|
|
12097
|
+
removeAll(repo, quoted);
|
|
12098
|
+
return {
|
|
12099
|
+
exit: false,
|
|
12100
|
+
output: ""
|
|
12101
|
+
};
|
|
12102
|
+
}
|
|
12103
|
+
const printf = /^printf '%s\\n' '(?:[^']|'\\'')*' (>&2|> '(?:[^']|'\\'')*')$/.exec(line);
|
|
12104
|
+
if (printf !== null) {
|
|
12105
|
+
const [, text, file] = quoted;
|
|
12106
|
+
if (printf[1] === ">&2") return {
|
|
12107
|
+
exit: false,
|
|
12108
|
+
output: `${text}\n`
|
|
12109
|
+
};
|
|
12110
|
+
repo.writeFile(file, `${text}\n`);
|
|
12111
|
+
return {
|
|
12112
|
+
exit: false,
|
|
12113
|
+
output: ""
|
|
12114
|
+
};
|
|
12115
|
+
}
|
|
12116
|
+
throw new Error(`testkit: cannot run the preamble line \`${line}\` in memory`);
|
|
12117
|
+
};
|
|
12118
|
+
/** Rejoins lines a single-quoted word spans, so a preamble entry carrying a newline stays one line. */
|
|
12119
|
+
const shellLines = (lines) => {
|
|
12120
|
+
const joined = [];
|
|
12121
|
+
let open = false;
|
|
12122
|
+
for (const line of lines) {
|
|
12123
|
+
if (open) joined[joined.length - 1] += `\n${line}`;
|
|
12124
|
+
else joined.push(line);
|
|
12125
|
+
for (let i = 0; i < line.length; i++) if (line[i] === "'" && (open || line[i - 1] !== "\\")) open = !open;
|
|
12126
|
+
}
|
|
12127
|
+
return joined;
|
|
12128
|
+
};
|
|
12129
|
+
const outsideCommand = (lines, mkdir) => {
|
|
12130
|
+
const before = shellLines(lines.slice(2, mkdir));
|
|
12131
|
+
const swept = before.at(-1)?.startsWith("rm -rf -- ") === true;
|
|
12132
|
+
const green = lines.find((l) => l.startsWith(" rm -rf -- "));
|
|
12133
|
+
return {
|
|
12134
|
+
sweep: swept ? words$1(before.at(-1)) : [],
|
|
12135
|
+
sweepOnGreen: green === void 0 ? [] : words$1(green),
|
|
12136
|
+
preamble: swept ? before.slice(0, -1) : before
|
|
12137
|
+
};
|
|
12138
|
+
};
|
|
12139
|
+
const parseCheck = (script) => {
|
|
12140
|
+
const lines = script.split("\n");
|
|
12141
|
+
const mkdir = lines.findIndex((l) => l.startsWith("mkdir -p \"$(dirname "));
|
|
12142
|
+
const close = lines.findIndex((l, i) => i > mkdir && /^\) > '.*' 2>&1$/.test(l));
|
|
12143
|
+
if (mkdir === -1 || lines[mkdir + 1] !== "(" || close === -1) return void 0;
|
|
12144
|
+
const [report] = words$1(lines[mkdir]);
|
|
12145
|
+
const [, stamp] = words$1(lines.find((l) => l.includes("<!-- gtd check %s -->")) ?? "");
|
|
12146
|
+
if (report === void 0 || stamp === void 0) return void 0;
|
|
12147
|
+
return {
|
|
12148
|
+
command: lines.slice(mkdir + 2, close).join("\n"),
|
|
12149
|
+
report,
|
|
12150
|
+
stamp,
|
|
12151
|
+
...outsideCommand(lines, mkdir)
|
|
12152
|
+
};
|
|
12153
|
+
};
|
|
12154
|
+
const runCommand = (repo, commands, command) => {
|
|
12155
|
+
const scripted = commands.get(command);
|
|
12156
|
+
if (scripted === void 0) throw new Error(`testkit: cannot run \`${command}\` in memory — script it ("the shell command … exits …")`);
|
|
12157
|
+
if (scripted.kind === "exit") return scripted;
|
|
12158
|
+
if (scripted.kind === "missing") return {
|
|
12159
|
+
status: 127,
|
|
12160
|
+
output: `sh: ${command}: not found\n`
|
|
12161
|
+
};
|
|
12162
|
+
repo.writeFile(scripted.file, rewritten(repo, scripted));
|
|
12163
|
+
return {
|
|
12164
|
+
status: 0,
|
|
12165
|
+
output: ""
|
|
12166
|
+
};
|
|
12167
|
+
};
|
|
12168
|
+
/** What a scripted rewrite leaves in its file. */
|
|
12169
|
+
const rewritten = (repo, command) => command.append === true ? `${repo.readFile(command.file) ?? ""}${command.content}` : command.content;
|
|
12170
|
+
const recognizeCheck = (repo, commands, script) => {
|
|
12171
|
+
const parsed = parseCheck(script);
|
|
12172
|
+
if (parsed === void 0) return void 0;
|
|
12173
|
+
const { command, report, stamp, sweep, sweepOnGreen, preamble } = parsed;
|
|
12174
|
+
if (checkScript(command, {
|
|
12175
|
+
report,
|
|
12176
|
+
stamp,
|
|
12177
|
+
sweep,
|
|
12178
|
+
sweepOnGreen,
|
|
12179
|
+
preamble
|
|
12180
|
+
}) !== script) return;
|
|
12181
|
+
let printed = "";
|
|
12182
|
+
for (const line of preamble) {
|
|
12183
|
+
const outcome = runPreambleLine(repo, line);
|
|
12184
|
+
printed += outcome.output;
|
|
12185
|
+
if (outcome.exit) return ok(printed);
|
|
12186
|
+
}
|
|
12187
|
+
removeAll(repo, sweep);
|
|
12188
|
+
const { status, output } = runCommand(repo, commands, command);
|
|
12189
|
+
if (status === 0) {
|
|
12190
|
+
deleteIfPresent(repo, report);
|
|
12191
|
+
removeAll(repo, sweepOnGreen);
|
|
12192
|
+
return ok(printed);
|
|
12193
|
+
}
|
|
12194
|
+
const body = output.length > 0 ? output : `the test command failed with exit code ${status} and produced no output.`;
|
|
12195
|
+
repo.writeFile(report, `${body}\n<!-- gtd check ${stamp} -->\n`);
|
|
12196
|
+
return ok(printed);
|
|
12197
|
+
};
|
|
12198
|
+
/**
|
|
12199
|
+
* Applies a `src/flows/scripts.ts` script to `repo`, or `undefined` when
|
|
12200
|
+
* `script` is none of them. `commands` stands in for the real programs a
|
|
12201
|
+
* check runs, keyed by the exact command string.
|
|
12202
|
+
*/
|
|
12203
|
+
const applyFlowScript = (repo, commands, script) => {
|
|
12204
|
+
if (!script.startsWith(`${SHEBANG}\n`)) return void 0;
|
|
12205
|
+
return recognizeRemove(repo, script) ?? recognizeMove(repo, script) ?? recognizeShow(repo, script) ?? recognizeRevert(repo, script) ?? recognizeRestore(repo, script) ?? recognizeCheck(repo, commands, script);
|
|
12206
|
+
};
|
|
12207
|
+
//#endregion
|
|
11875
12208
|
//#region src/testkit/EmittedScriptRecognizer.ts
|
|
11876
12209
|
/**
|
|
11877
12210
|
* Reverses `GitScript.ts`'s `shellQuote`: extracts every `'...'` token from
|
|
@@ -11908,6 +12241,8 @@ const extractQuotedTokens = (text) => {
|
|
|
11908
12241
|
}
|
|
11909
12242
|
return tokens;
|
|
11910
12243
|
};
|
|
12244
|
+
/** What the in-memory tier prints for `$PATH` — there is none to name. */
|
|
12245
|
+
const IN_MEMORY_PATH = "(in-memory: no PATH)";
|
|
11911
12246
|
const SET_FLAGS_RE = /^set\s+-\S+(\s+\S+)*$/;
|
|
11912
12247
|
const recognizeSetFlags = (block) => SET_FLAGS_RE.test(block) ? { kind: "noop" } : void 0;
|
|
11913
12248
|
const PRECONDITION_RE = /^\[ "\$\(git rev-parse HEAD\)" = '([0-9a-f]{40})' \] \|\| \{ echo "precondition failed: HEAD moved" >&2; exit 1; \}$/;
|
|
@@ -11915,11 +12250,9 @@ const recognizePrecondition = (repo, block) => {
|
|
|
11915
12250
|
const match = PRECONDITION_RE.exec(block);
|
|
11916
12251
|
if (!match) return void 0;
|
|
11917
12252
|
const expected = match[1];
|
|
11918
|
-
|
|
11919
|
-
if (actual === expected) return { kind: "noop" };
|
|
11920
|
-
return {
|
|
12253
|
+
return repo.resolveRef("HEAD") === expected ? { kind: "noop" } : {
|
|
11921
12254
|
kind: "failed",
|
|
11922
|
-
error:
|
|
12255
|
+
error: "precondition failed: HEAD moved"
|
|
11923
12256
|
};
|
|
11924
12257
|
};
|
|
11925
12258
|
/**
|
|
@@ -12006,9 +12339,9 @@ const recognizeFileExistsGuard = (repo, block) => {
|
|
|
12006
12339
|
if (file === void 0 || fileExistsGuard(file) !== block) return void 0;
|
|
12007
12340
|
return repo.readFile(file) !== void 0 ? { kind: "noop" } : { kind: "stopped" };
|
|
12008
12341
|
};
|
|
12009
|
-
/** `binaryGuard`. The in-memory tier has no
|
|
12342
|
+
/** `binaryGuard`. The in-memory tier has no `$PATH`: a binary is present unless declared `missing`. */
|
|
12010
12343
|
const BINARY_GUARD_MESSAGE_RE = /^gtd: mode "([^"]+)": "(format|validate)" command not found: (.+)$/;
|
|
12011
|
-
const recognizeBinaryGuard = (block) => {
|
|
12344
|
+
const recognizeBinaryGuard = (commands, block) => {
|
|
12012
12345
|
const [binary, , message] = extractQuotedTokens(block);
|
|
12013
12346
|
if (binary === void 0 || message === void 0) return void 0;
|
|
12014
12347
|
const match = BINARY_GUARD_MESSAGE_RE.exec(message);
|
|
@@ -12016,10 +12349,29 @@ const recognizeBinaryGuard = (block) => {
|
|
|
12016
12349
|
const [, mode, key, messageBinary] = match;
|
|
12017
12350
|
if (messageBinary !== binary) return void 0;
|
|
12018
12351
|
if (binaryGuard(binary, mode, key) !== block) return void 0;
|
|
12019
|
-
return { kind: "noop" };
|
|
12352
|
+
if (commands.get(binary)?.kind !== "missing") return { kind: "noop" };
|
|
12353
|
+
return {
|
|
12354
|
+
kind: "failed",
|
|
12355
|
+
error: `${message}\n$PATH: ${IN_MEMORY_PATH}`,
|
|
12356
|
+
status: 127
|
|
12357
|
+
};
|
|
12020
12358
|
};
|
|
12021
12359
|
/** Recognized by `OUTCOME_MARKER` rather than re-derived and string-compared: an outcome only prints, so no git effect a loose match could miss — unlike `recognizeGitBuilders`, where a near-miss would silently skip a real mutation. */
|
|
12022
|
-
const recognizeOutcome = (block) =>
|
|
12360
|
+
const recognizeOutcome = (repo, block) => {
|
|
12361
|
+
if (!block.startsWith(`# gtd: outcome (print-only)\nprintf `)) return void 0;
|
|
12362
|
+
const line = block.slice(28);
|
|
12363
|
+
const [format, ...args] = shellWords(repo, line.slice(7));
|
|
12364
|
+
return {
|
|
12365
|
+
kind: "noop",
|
|
12366
|
+
output: renderFormat(format.replace(/\\n/g, "\n"), ...args)
|
|
12367
|
+
};
|
|
12368
|
+
};
|
|
12369
|
+
/** The word list of `printf`'s arguments: quoted literals, and the `"$(git …)"` lookups outcome lines carry. */
|
|
12370
|
+
const shellWords = (repo, text) => [...text.matchAll(/'((?:[^']|'\\'')*)'|"\$\(git (rev-parse --short|log -1 --format=%s) '([^']*)'\)"/g)].map((m) => {
|
|
12371
|
+
if (m[1] !== void 0) return m[1].replaceAll("'\\''", "'");
|
|
12372
|
+
const hash = repo.resolveRef(m[3]) ?? "";
|
|
12373
|
+
return m[2] === "rev-parse --short" ? hash.slice(0, 7) : repo.lastCommitSubject(hash) ?? "";
|
|
12374
|
+
});
|
|
12023
12375
|
/**
|
|
12024
12376
|
* `failurePromptWrapper`. The constants below bracket the template's one
|
|
12025
12377
|
* variable part; the extracted pair is confirmed by RE-RUNNING the real
|
|
@@ -12036,8 +12388,27 @@ const expandFileVar = (text) => {
|
|
|
12036
12388
|
if (file === void 0) return void 0;
|
|
12037
12389
|
return text.slice(match[0].length).replace(/"\$GTD_FILE"|"\$\{GTD_FILE\}"/g, shellQuote(file)).replace(/\$\{GTD_FILE\}|\$GTD_FILE\b/g, file);
|
|
12038
12390
|
};
|
|
12039
|
-
/**
|
|
12040
|
-
|
|
12391
|
+
/**
|
|
12392
|
+
* A mode's `format:`/`validate:` command, under `withFileVar` or not:
|
|
12393
|
+
* `gtd check`, or a scripted command — keyed by the command as expanded, or
|
|
12394
|
+
* as written in the config (`fmt $GTD_FILE`), which is the only key a command
|
|
12395
|
+
* run on a scratch path can have.
|
|
12396
|
+
*/
|
|
12397
|
+
const recognizeModeCommand = (repo, commands, block) => {
|
|
12398
|
+
const command = expandFileVar(block) ?? block;
|
|
12399
|
+
const exact = recognizeGtdCheck(repo, command) ?? recognizeScriptedCommand(repo, commands, command);
|
|
12400
|
+
if (exact !== void 0) return exact;
|
|
12401
|
+
const match = FILE_VAR_PREFIX_RE.exec(block);
|
|
12402
|
+
if (match === null) return void 0;
|
|
12403
|
+
const scripted = commands.get(block.slice(match[0].length));
|
|
12404
|
+
const [file] = extractQuotedTokens(match[1]);
|
|
12405
|
+
if (scripted === void 0 || file === void 0) return void 0;
|
|
12406
|
+
const bound = scripted.kind === "rewrite" && scripted.file === "$GTD_FILE" ? {
|
|
12407
|
+
...scripted,
|
|
12408
|
+
file
|
|
12409
|
+
} : scripted;
|
|
12410
|
+
return recognizeScriptedCommand(repo, /* @__PURE__ */ new Map([[command, bound]]), command);
|
|
12411
|
+
};
|
|
12041
12412
|
const FAILURE_PROMPT_HEADER = "gtd_validate_status=0\ngtd_validate_out=\"$( {\n";
|
|
12042
12413
|
const FAILURE_PROMPT_MIDDLE = "\n} 2>&1 )\" || gtd_validate_status=$?\nif [ \"$gtd_validate_status\" -ne 0 ]; then\n printf '%s\\n\\n%s\\n' ";
|
|
12043
12414
|
const FAILURE_PROMPT_FOOTER = " \"$gtd_validate_out\"\n exit \"$gtd_validate_status\"\nfi";
|
|
@@ -12049,11 +12420,12 @@ const recognizeFailurePromptWrapper = (repo, commands, block) => {
|
|
|
12049
12420
|
const promptQuoted = block.slice(middleIndex + 101, block.length - 53);
|
|
12050
12421
|
const [prompt] = extractQuotedTokens(promptQuoted);
|
|
12051
12422
|
if (prompt === void 0 || failurePromptWrapper(inner, prompt) !== block) return void 0;
|
|
12052
|
-
const innerOutcome = recognizeModeCommand(repo, commands,
|
|
12423
|
+
const innerOutcome = recognizeModeCommand(repo, commands, inner);
|
|
12053
12424
|
if (innerOutcome === void 0) return void 0;
|
|
12054
12425
|
if (innerOutcome.kind === "failed") return {
|
|
12055
12426
|
kind: "failed",
|
|
12056
|
-
error: `${prompt}\n\n${innerOutcome.error}
|
|
12427
|
+
error: `${prompt}\n\n${innerOutcome.error.replace(/\n+$/, "")}`,
|
|
12428
|
+
status: innerOutcome.status ?? 1
|
|
12057
12429
|
};
|
|
12058
12430
|
return innerOutcome;
|
|
12059
12431
|
};
|
|
@@ -12066,7 +12438,10 @@ const recognizeModeContradictionSkipNotice = (block) => {
|
|
|
12066
12438
|
if (!match) return void 0;
|
|
12067
12439
|
const mode = match[1];
|
|
12068
12440
|
if (modeContradictionSkipNotice(mode) !== block) return void 0;
|
|
12069
|
-
return {
|
|
12441
|
+
return {
|
|
12442
|
+
kind: "noop",
|
|
12443
|
+
output: `${message}\n`
|
|
12444
|
+
};
|
|
12070
12445
|
};
|
|
12071
12446
|
/** The printf line's own pieces — the sample bytes, the scratch path, and the literal PREFIX text (up to and including that path) — or `undefined` when `block` doesn't open with `printf '%s' ...`. */
|
|
12072
12447
|
const parsePrintfPrefix = (block) => {
|
|
@@ -12129,7 +12504,7 @@ const simulateModeContradictionCheck = (repo, commands, parsed) => {
|
|
|
12129
12504
|
const { mode, samplePath, sample, formatCommand, format } = parsed;
|
|
12130
12505
|
repo.writeFile(samplePath, sample);
|
|
12131
12506
|
const command = expandFileVar(formatCommand) ?? formatCommand;
|
|
12132
|
-
const formatOutcome =
|
|
12507
|
+
const formatOutcome = recognizeModeCommand(repo, commands, formatCommand);
|
|
12133
12508
|
if (formatOutcome === void 0) {
|
|
12134
12509
|
repo.deleteFile(samplePath);
|
|
12135
12510
|
return {
|
|
@@ -12171,16 +12546,16 @@ const recognizeGtdCheck = (repo, block) => {
|
|
|
12171
12546
|
const format = steeringFormatFor(mode);
|
|
12172
12547
|
if (format === void 0 || file === void 0) return {
|
|
12173
12548
|
kind: "failed",
|
|
12174
|
-
error: `gtd check:
|
|
12549
|
+
error: `gtd check: unknown mode "${mode}" — known modes: ${BUILT_IN_MODE_NAMES.join(", ")}`
|
|
12175
12550
|
};
|
|
12176
12551
|
const content = repo.readFile(file);
|
|
12177
12552
|
if (content === void 0) return { kind: "noop" };
|
|
12178
|
-
const findings = format.validate(content);
|
|
12179
|
-
if (findings.length
|
|
12553
|
+
const findings = format.validate(content).map((finding) => formatFinding(file, finding));
|
|
12554
|
+
if (findings.length === 0) return { kind: "noop" };
|
|
12555
|
+
return {
|
|
12180
12556
|
kind: "failed",
|
|
12181
|
-
error: `gtd check ${
|
|
12557
|
+
error: `gtd check: ${file} is not valid under mode "${mode}" (${findings.length} finding(s)):\n` + findings.join("\n")
|
|
12182
12558
|
};
|
|
12183
|
-
return { kind: "noop" };
|
|
12184
12559
|
};
|
|
12185
12560
|
const GTD_UNCHECK_RE = /^gtd uncheck (.+)$/;
|
|
12186
12561
|
/** `gtd uncheck <file>` — the review-gate reset prepended ahead of the human's commit, so no tick ever reaches it. Re-runs the REAL `clearTicks`, writing back only on an actual change. */
|
|
@@ -12204,27 +12579,44 @@ const recognizeGtdUncheck = (repo, block) => {
|
|
|
12204
12579
|
*/
|
|
12205
12580
|
const recognizeDidNotRunComment = (block) => block === "# gtd emitted this and did NOT run it — pipe it into `sh` to land the turn" ? { kind: "noop" } : void 0;
|
|
12206
12581
|
/** `combinedScript`'s optional half. ALWAYS reports `noop` whatever the inner result, mirroring bash's `( … ) || <warning>`, where the subshell's exit status is swallowed and must never fail the outer script. */
|
|
12582
|
+
const PRESENTATION_FAILURE_TEXT = `${extractQuotedTokens(PRESENTATION_FAILURE_WARNING)[0].replace(/\\n$/, "")}\n`;
|
|
12207
12583
|
const PRESENTATION_SUBSHELL_PREFIX = `${PRESENTATION_ONLY_COMMENT}\n(\n`;
|
|
12208
12584
|
const PRESENTATION_SUBSHELL_SUFFIX = `\n) || ${PRESENTATION_FAILURE_WARNING}`;
|
|
12209
12585
|
const recognizePresentationSubshell = (repo, commands, block) => {
|
|
12210
12586
|
if (!block.startsWith(PRESENTATION_SUBSHELL_PREFIX) || !block.endsWith(PRESENTATION_SUBSHELL_SUFFIX)) return;
|
|
12211
12587
|
const inner = block.slice(PRESENTATION_SUBSHELL_PREFIX.length, block.length - PRESENTATION_SUBSHELL_SUFFIX.length);
|
|
12212
|
-
applyEmittedScript(repo, commands, inner);
|
|
12213
|
-
return {
|
|
12588
|
+
const result = applyEmittedScript(repo, commands, inner);
|
|
12589
|
+
if (result.ok) return {
|
|
12590
|
+
kind: "noop",
|
|
12591
|
+
output: result.output
|
|
12592
|
+
};
|
|
12593
|
+
return {
|
|
12594
|
+
kind: "noop",
|
|
12595
|
+
output: `${result.output}${PRESENTATION_FAILURE_TEXT}`
|
|
12596
|
+
};
|
|
12214
12597
|
};
|
|
12215
12598
|
/** Anything left over must be an EXACT hit in the scripted-command table — mirrors `ScriptedCommand`'s two `kind`s (`Layers.ts`). */
|
|
12216
12599
|
const recognizeScriptedCommand = (repo, commands, block) => {
|
|
12217
12600
|
const scripted = commands.get(block);
|
|
12218
12601
|
if (scripted === void 0) return void 0;
|
|
12219
12602
|
if (scripted.kind === "rewrite") {
|
|
12220
|
-
repo.writeFile(scripted.file, scripted
|
|
12603
|
+
repo.writeFile(scripted.file, rewritten(repo, scripted));
|
|
12221
12604
|
return { kind: "applied" };
|
|
12222
12605
|
}
|
|
12606
|
+
if (scripted.kind === "missing") return {
|
|
12607
|
+
kind: "failed",
|
|
12608
|
+
error: `sh: ${block.split(" ")[0]}: not found`,
|
|
12609
|
+
status: 127
|
|
12610
|
+
};
|
|
12223
12611
|
if (scripted.status !== 0) return {
|
|
12224
12612
|
kind: "failed",
|
|
12225
|
-
error:
|
|
12613
|
+
error: scripted.output,
|
|
12614
|
+
status: scripted.status
|
|
12615
|
+
};
|
|
12616
|
+
return {
|
|
12617
|
+
kind: "noop",
|
|
12618
|
+
output: scripted.output
|
|
12226
12619
|
};
|
|
12227
|
-
return { kind: "noop" };
|
|
12228
12620
|
};
|
|
12229
12621
|
/**
|
|
12230
12622
|
* Splits on blank lines, IGNORING those inside a single-quoted string: every
|
|
@@ -12280,14 +12672,27 @@ const splitBlocks = (script) => {
|
|
|
12280
12672
|
if (current.trim().length > 0) blocks.push(current.trim());
|
|
12281
12673
|
return blocks;
|
|
12282
12674
|
};
|
|
12675
|
+
/** `deferNotesScript`: append its one section to `.gtd/TODO.md`, reading the section's lines off the final `printf`. */
|
|
12676
|
+
const DEFER_HEAD = "mkdir -p .gtd\nif [ -s .gtd/TODO.md ]; then";
|
|
12677
|
+
const recognizeDeferNotes = (repo, block) => {
|
|
12678
|
+
if (!block.startsWith(DEFER_HEAD)) return void 0;
|
|
12679
|
+
const at = block.lastIndexOf("\nprintf '%s\\n' ");
|
|
12680
|
+
if (at === -1) return void 0;
|
|
12681
|
+
const lines = extractQuotedTokens(block.slice(at + 15));
|
|
12682
|
+
const old = repo.readFile(".gtd/TODO.md") ?? "";
|
|
12683
|
+
const sep = old === "" ? "" : `${old.endsWith("\n") ? "" : "\n"}\n`;
|
|
12684
|
+
repo.writeFile(".gtd/TODO.md", `${old}${sep}${lines.join("\n")}\n`);
|
|
12685
|
+
return { kind: "applied" };
|
|
12686
|
+
};
|
|
12283
12687
|
/** Every recognizer `applyEmittedScript` tries, first match wins — `recognizeScriptedCommand` stays last since it's the exact-hit fallback for anything the rest don't claim. */
|
|
12284
12688
|
const recognizersFor = (repo, commands) => [
|
|
12285
12689
|
(block) => recognizeSetFlags(block),
|
|
12286
12690
|
(block) => recognizeDidNotRunComment(block),
|
|
12287
12691
|
(block) => recognizePrecondition(repo, block),
|
|
12288
12692
|
(block) => recognizeFileExistsGuard(repo, block),
|
|
12289
|
-
(block) => recognizeBinaryGuard(block),
|
|
12693
|
+
(block) => recognizeBinaryGuard(commands, block),
|
|
12290
12694
|
(block) => recognizeGitBuilders(repo, block),
|
|
12695
|
+
(block) => recognizeDeferNotes(repo, block),
|
|
12291
12696
|
(block) => recognizeFailurePromptWrapper(repo, commands, block),
|
|
12292
12697
|
(block) => recognizePresentationSubshell(repo, commands, block),
|
|
12293
12698
|
(block) => recognizeModeContradictionSkipNotice(block),
|
|
@@ -12295,10 +12700,9 @@ const recognizersFor = (repo, commands) => [
|
|
|
12295
12700
|
(block) => recognizeGtdCheck(repo, block),
|
|
12296
12701
|
(block) => recognizeGtdUncheck(repo, block),
|
|
12297
12702
|
(block) => {
|
|
12298
|
-
|
|
12299
|
-
return command === void 0 ? void 0 : recognizeModeCommand(repo, commands, command);
|
|
12703
|
+
return FILE_VAR_PREFIX_RE.test(block) ? recognizeModeCommand(repo, commands, block) : void 0;
|
|
12300
12704
|
},
|
|
12301
|
-
(block) => recognizeOutcome(block),
|
|
12705
|
+
(block) => recognizeOutcome(repo, block),
|
|
12302
12706
|
(block) => recognizeScriptedCommand(repo, commands, block)
|
|
12303
12707
|
];
|
|
12304
12708
|
const recognizeBlock = (recognizers, block) => {
|
|
@@ -12314,21 +12718,50 @@ const recognizeBlock = (recognizers, block) => {
|
|
|
12314
12718
|
* real script. No block after the failing one is applied.
|
|
12315
12719
|
*/
|
|
12316
12720
|
const applyEmittedScript = (repo, commands, script) => {
|
|
12721
|
+
const flow = applyFlowScript(repo, commands, script);
|
|
12722
|
+
if (flow !== void 0) return flow.ok ? {
|
|
12723
|
+
ok: true,
|
|
12724
|
+
status: 0,
|
|
12725
|
+
output: flow.output
|
|
12726
|
+
} : {
|
|
12727
|
+
ok: false,
|
|
12728
|
+
status: 1,
|
|
12729
|
+
output: flow.output,
|
|
12730
|
+
error: flow.error ?? ""
|
|
12731
|
+
};
|
|
12732
|
+
return script.startsWith("#!") ? unrecognized(script) : applyBlocks(repo, commands, script);
|
|
12733
|
+
};
|
|
12734
|
+
const applyBlocks = (repo, commands, script) => {
|
|
12317
12735
|
const recognizers = recognizersFor(repo, commands);
|
|
12736
|
+
let output = "";
|
|
12318
12737
|
for (const block of splitBlocks(script)) {
|
|
12319
12738
|
const outcome = recognizeBlock(recognizers, block);
|
|
12320
|
-
if (outcome === void 0) return
|
|
12321
|
-
|
|
12322
|
-
|
|
12323
|
-
|
|
12324
|
-
if (outcome.kind === "stopped") return { ok: true };
|
|
12325
|
-
if (outcome.kind === "failed") return {
|
|
12326
|
-
ok: false,
|
|
12327
|
-
error: outcome.error
|
|
12328
|
-
};
|
|
12739
|
+
if (outcome === void 0) return unrecognized(block, output);
|
|
12740
|
+
if (outcome.kind === "noop") output += outcome.output ?? "";
|
|
12741
|
+
if (outcome.kind === "stopped") break;
|
|
12742
|
+
if (outcome.kind === "failed") return failedAt(outcome, output);
|
|
12329
12743
|
}
|
|
12330
|
-
return {
|
|
12744
|
+
return {
|
|
12745
|
+
ok: true,
|
|
12746
|
+
status: 0,
|
|
12747
|
+
output
|
|
12748
|
+
};
|
|
12331
12749
|
};
|
|
12750
|
+
const failedAt = (outcome, output) => {
|
|
12751
|
+
const printed = outcome.error.length === 0 || outcome.error.endsWith("\n") ? "" : "\n";
|
|
12752
|
+
return {
|
|
12753
|
+
ok: false,
|
|
12754
|
+
status: outcome.status ?? 1,
|
|
12755
|
+
output: `${output}${outcome.error}${printed}`,
|
|
12756
|
+
error: outcome.error
|
|
12757
|
+
};
|
|
12758
|
+
};
|
|
12759
|
+
const unrecognized = (block, output = "") => ({
|
|
12760
|
+
ok: false,
|
|
12761
|
+
status: 1,
|
|
12762
|
+
output,
|
|
12763
|
+
error: `unrecognized script block: ${block.split("\n")[0]}`
|
|
12764
|
+
});
|
|
12332
12765
|
//#endregion
|
|
12333
12766
|
//#region src/testkit/TestRepo.ts
|
|
12334
12767
|
const REGISTRY = Symbol.for("@pmelab/gtd/testkit:modules");
|
|
@@ -12463,7 +12896,9 @@ const testRepo = (options) => {
|
|
|
12463
12896
|
commit: (message) => repo.commitAllWithPrefix(message),
|
|
12464
12897
|
show: (ref, path) => repo.fileAtRef(ref, path) ?? void 0,
|
|
12465
12898
|
runScript: (script) => {
|
|
12466
|
-
|
|
12899
|
+
const flow = applyFlowScript(repo, /* @__PURE__ */ new Map(), script);
|
|
12900
|
+
if (flow === void 0) for (const line of script.split("\n")) runLine(repo, line.trim());
|
|
12901
|
+
else if (!flow.ok) throw new Error(`testkit: ${flow.error ?? ""}`);
|
|
12467
12902
|
},
|
|
12468
12903
|
gtd,
|
|
12469
12904
|
start: (workflow, ...args) => begin([
|
|
@@ -12549,7 +12984,8 @@ const makeLiveTier = (initialCommit = true) => {
|
|
|
12549
12984
|
onDiskContent: true,
|
|
12550
12985
|
commitHooks: true,
|
|
12551
12986
|
linkedWorktrees: true,
|
|
12552
|
-
directoryChainConfig: true
|
|
12987
|
+
directoryChainConfig: true,
|
|
12988
|
+
remotes: true
|
|
12553
12989
|
},
|
|
12554
12990
|
provide,
|
|
12555
12991
|
provideExit,
|
|
@@ -12588,7 +13024,37 @@ const makeLiveTier = (initialCommit = true) => {
|
|
|
12588
13024
|
}
|
|
12589
13025
|
},
|
|
12590
13026
|
readWorktreeFile: (path) => readFileSync(join(root, path), "utf8"),
|
|
12591
|
-
existsPath: (path) => existsSync(join(root, path))
|
|
13027
|
+
existsPath: (path) => existsSync(join(root, path)),
|
|
13028
|
+
snapshot: async () => {
|
|
13029
|
+
gitExec("status", "--porcelain");
|
|
13030
|
+
await new Promise((r) => setTimeout(r, 1100));
|
|
13031
|
+
gitExec("status", "--porcelain");
|
|
13032
|
+
const files = readdirSync(join(root, ".git"), {
|
|
13033
|
+
recursive: true,
|
|
13034
|
+
withFileTypes: true
|
|
13035
|
+
}).filter((entry) => entry.isFile()).map((entry) => join(entry.parentPath, entry.name)).filter((path) => !path.includes(`${sep}gtd-cache${sep}`)).sort().map((path) => `${path}:${statSync(path).size}:${statSync(path).mtimeMs}`);
|
|
13036
|
+
return JSON.stringify({
|
|
13037
|
+
files,
|
|
13038
|
+
status: gitExec("status", "--porcelain", "-uall")
|
|
13039
|
+
});
|
|
13040
|
+
},
|
|
13041
|
+
headSubject: () => gitExec("log", "-1", "--pretty=%s"),
|
|
13042
|
+
headTouchedPaths: () => gitExec("show", "--name-only", "--pretty=format:", "HEAD").split("\n").filter((line) => line.length > 0)
|
|
13043
|
+
},
|
|
13044
|
+
runScript: (script) => {
|
|
13045
|
+
const result = spawnSync("sh", ["-c", `exec 2>&1\n${script}`], {
|
|
13046
|
+
cwd: root,
|
|
13047
|
+
encoding: "utf8",
|
|
13048
|
+
env: {
|
|
13049
|
+
...process.env,
|
|
13050
|
+
PATH: `${gtdShimDir()}:${process.env["PATH"] ?? ""}`
|
|
13051
|
+
}
|
|
13052
|
+
});
|
|
13053
|
+
return {
|
|
13054
|
+
ok: result.status === 0,
|
|
13055
|
+
status: result.status ?? 1,
|
|
13056
|
+
output: result.stdout
|
|
13057
|
+
};
|
|
12592
13058
|
},
|
|
12593
13059
|
dispose: () => rmSync(root, {
|
|
12594
13060
|
recursive: true,
|
|
@@ -12596,6 +13062,15 @@ const makeLiveTier = (initialCommit = true) => {
|
|
|
12596
13062
|
})
|
|
12597
13063
|
};
|
|
12598
13064
|
};
|
|
13065
|
+
/** A dir whose `gtd` runs this build's bundle, so a script's bare `gtd` is the code under test. */
|
|
13066
|
+
let shimDir;
|
|
13067
|
+
const gtdShimDir = () => {
|
|
13068
|
+
if (shimDir !== void 0) return shimDir;
|
|
13069
|
+
shimDir = mkdtempSync(join(tmpdir(), "gtd-tier-shim-"));
|
|
13070
|
+
const bundle = join(import.meta.dirname, "../../dist/gtd.bundle.mjs");
|
|
13071
|
+
writeFileSync(join(shimDir, "gtd"), `#!/bin/sh\nexec node "${bundle}" "$@"\n`, { mode: 493 });
|
|
13072
|
+
return shimDir;
|
|
13073
|
+
};
|
|
12599
13074
|
const IN_MEM_ROOT = "/repo";
|
|
12600
13075
|
const makeInMemTier = (initialCommit = true) => {
|
|
12601
13076
|
const repo = new InMemRepo();
|
|
@@ -12613,7 +13088,8 @@ const makeInMemTier = (initialCommit = true) => {
|
|
|
12613
13088
|
onDiskContent: false,
|
|
12614
13089
|
commitHooks: false,
|
|
12615
13090
|
linkedWorktrees: false,
|
|
12616
|
-
directoryChainConfig: true
|
|
13091
|
+
directoryChainConfig: true,
|
|
13092
|
+
remotes: false
|
|
12617
13093
|
},
|
|
12618
13094
|
provide,
|
|
12619
13095
|
provideExit,
|
|
@@ -12646,7 +13122,21 @@ const makeInMemTier = (initialCommit = true) => {
|
|
|
12646
13122
|
if (content === void 0) throw new Error(`ENOENT: no such file or directory, open '${path}'`);
|
|
12647
13123
|
return content;
|
|
12648
13124
|
},
|
|
12649
|
-
existsPath: (path) => repo.hasPath(path)
|
|
13125
|
+
existsPath: (path) => repo.hasPath(path),
|
|
13126
|
+
snapshot: async () => repo.snapshot(),
|
|
13127
|
+
headSubject: () => repo.lastCommitSubject() ?? "",
|
|
13128
|
+
headTouchedPaths: () => {
|
|
13129
|
+
return [.../* @__PURE__ */ new Set([...repo.pathsAtRef("HEAD~1"), ...repo.pathsAtRef("HEAD")])].filter((p) => repo.fileAtRef("HEAD~1", p) !== repo.fileAtRef("HEAD", p)).sort();
|
|
13130
|
+
}
|
|
13131
|
+
},
|
|
13132
|
+
runScript: (script, commands = /* @__PURE__ */ new Map()) => {
|
|
13133
|
+
const { ok, status, output, error } = applyEmittedScript(repo, commands, script);
|
|
13134
|
+
if (error?.startsWith("unrecognized script block:") === true) throw new Error(error);
|
|
13135
|
+
return {
|
|
13136
|
+
ok,
|
|
13137
|
+
status,
|
|
13138
|
+
output
|
|
13139
|
+
};
|
|
12650
13140
|
},
|
|
12651
13141
|
dispose: () => {}
|
|
12652
13142
|
};
|
|
@@ -12693,6 +13183,32 @@ const runGitServiceContract = (makeTier) => {
|
|
|
12693
13183
|
afterEach(() => {
|
|
12694
13184
|
t.dispose();
|
|
12695
13185
|
});
|
|
13186
|
+
it("no reader writes to the repository", async () => {
|
|
13187
|
+
t.seed.commit("feat: second", { "second.txt": "two" });
|
|
13188
|
+
t.seed.writeFile("dirty.txt", "dirty");
|
|
13189
|
+
const before = await t.observe.snapshot();
|
|
13190
|
+
await runGit(t, (g) => all$1([
|
|
13191
|
+
g.lastCommitSubject(),
|
|
13192
|
+
g.lastCommitMessage(),
|
|
13193
|
+
g.hasCommits(),
|
|
13194
|
+
g.resolveRef("HEAD"),
|
|
13195
|
+
g.readRefOption("refs/gtd/none"),
|
|
13196
|
+
g.isAncestor("HEAD~1", "HEAD"),
|
|
13197
|
+
g.mergeBase("HEAD~1", "HEAD"),
|
|
13198
|
+
g.defaultBranch(),
|
|
13199
|
+
g.topLevel(),
|
|
13200
|
+
g.gitDir(),
|
|
13201
|
+
g.gitCommonDir(),
|
|
13202
|
+
g.commitHistory(),
|
|
13203
|
+
g.subjectHistory(10, 0),
|
|
13204
|
+
g.readFileAtRef("HEAD", "second.txt"),
|
|
13205
|
+
g.changedPaths(),
|
|
13206
|
+
g.changedPaths("HEAD~1")
|
|
13207
|
+
]));
|
|
13208
|
+
expect(await t.observe.snapshot()).toBe(before);
|
|
13209
|
+
await runGit(t, (g) => g.updateRef("refs/gtd/marker", t.observe.resolveRef("HEAD")));
|
|
13210
|
+
expect(await t.observe.snapshot()).not.toBe(before);
|
|
13211
|
+
});
|
|
12696
13212
|
describe("resolveRef", () => {
|
|
12697
13213
|
it("resolves HEAD to a 40-char hash", async () => {
|
|
12698
13214
|
expect(await runGit(t, (g) => g.resolveRef("HEAD"))).toMatch(/^[0-9a-f]{40}$/);
|
|
@@ -13228,31 +13744,18 @@ const runGitServiceContract = (makeTier) => {
|
|
|
13228
13744
|
});
|
|
13229
13745
|
};
|
|
13230
13746
|
/**
|
|
13231
|
-
*
|
|
13232
|
-
*
|
|
13233
|
-
*
|
|
13234
|
-
* `&&`, a misquoted path) fails here exactly as it would in production.
|
|
13747
|
+
* The SAME writer postconditions `runGitServiceContract` asserts against
|
|
13748
|
+
* `GitOperations`, asserted here against `GitScript.ts`'s builders as a driver
|
|
13749
|
+
* runs them: real `sh` on Live, the in-memory twin on InMemory.
|
|
13235
13750
|
*/
|
|
13236
|
-
const
|
|
13237
|
-
|
|
13238
|
-
|
|
13239
|
-
stdio: "pipe"
|
|
13240
|
-
});
|
|
13751
|
+
const run = (t, script) => {
|
|
13752
|
+
const result = t.runScript(script);
|
|
13753
|
+
if (!result.ok) throw new Error(`script failed on ${t.name}: ${result.output}`);
|
|
13241
13754
|
};
|
|
13242
|
-
|
|
13243
|
-
const headTouchedPaths = (t) => gitExecIn(t.root, "show", "--name-only", "--pretty=format:", "HEAD").split("\n").filter((line) => line.length > 0);
|
|
13244
|
-
const headSubject = (t) => gitExecIn(t.root, "log", "-1", "--pretty=%s");
|
|
13245
|
-
/**
|
|
13246
|
-
* The SAME 9 writer postconditions `runGitServiceContract` asserts against
|
|
13247
|
-
* `GitOperations`, asserted here against `GitScript.ts`'s bash builders
|
|
13248
|
-
* instead — driven through real `bash` against a real repo (`makeLiveTier`),
|
|
13249
|
-
* never the in-memory fake, since a fake root (`/repo`) has no shell to run
|
|
13250
|
-
* against.
|
|
13251
|
-
*/
|
|
13252
|
-
const runGitScriptContract = () => {
|
|
13755
|
+
const runGitScriptContract = (makeTier) => {
|
|
13253
13756
|
let t;
|
|
13254
13757
|
beforeEach(() => {
|
|
13255
|
-
t =
|
|
13758
|
+
t = makeTier();
|
|
13256
13759
|
});
|
|
13257
13760
|
afterEach(() => {
|
|
13258
13761
|
t.dispose();
|
|
@@ -13261,17 +13764,17 @@ const runGitScriptContract = () => {
|
|
|
13261
13764
|
it("stages (git add -A) before committing — a new untracked file lands in the commit", () => {
|
|
13262
13765
|
t.seed.writeFile("new.ts", "export const x = 1");
|
|
13263
13766
|
const headBefore = t.observe.resolveRef("HEAD");
|
|
13264
|
-
|
|
13767
|
+
run(t, commitAll("gtd: building"));
|
|
13265
13768
|
expect(t.observe.resolveRef("HEAD")).not.toBe(headBefore);
|
|
13266
|
-
expect(headSubject(
|
|
13267
|
-
expect(headTouchedPaths(
|
|
13769
|
+
expect(t.observe.headSubject()).toBe("gtd: building");
|
|
13770
|
+
expect(t.observe.headTouchedPaths()).toEqual(["new.ts"]);
|
|
13268
13771
|
expect(t.observe.statusPorcelain().trim()).toBe("");
|
|
13269
13772
|
});
|
|
13270
13773
|
it("commits even on a clean tree (--allow-empty), never 'nothing to commit'", () => {
|
|
13271
13774
|
const headBefore = t.observe.resolveRef("HEAD");
|
|
13272
|
-
|
|
13775
|
+
run(t, commitAll("gtd: grilled"));
|
|
13273
13776
|
expect(t.observe.resolveRef("HEAD")).not.toBe(headBefore);
|
|
13274
|
-
expect(headSubject(
|
|
13777
|
+
expect(t.observe.headSubject()).toBe("gtd: grilled");
|
|
13275
13778
|
});
|
|
13276
13779
|
});
|
|
13277
13780
|
describe("commitAsIs", () => {
|
|
@@ -13279,22 +13782,22 @@ const runGitScriptContract = () => {
|
|
|
13279
13782
|
t.seed.writeFile("a.txt", "a");
|
|
13280
13783
|
t.seed.stageAll();
|
|
13281
13784
|
t.seed.writeFile("b.txt", "b");
|
|
13282
|
-
|
|
13283
|
-
expect(headSubject(
|
|
13284
|
-
expect(headTouchedPaths(
|
|
13785
|
+
run(t, commitAsIs("feat: squash"));
|
|
13786
|
+
expect(t.observe.headSubject()).toBe("feat: squash");
|
|
13787
|
+
expect(t.observe.headTouchedPaths()).toEqual(["a.txt"]);
|
|
13285
13788
|
});
|
|
13286
13789
|
it("commits even on a clean index (--allow-empty), never 'nothing to commit'", () => {
|
|
13287
13790
|
const headBefore = t.observe.resolveRef("HEAD");
|
|
13288
|
-
|
|
13791
|
+
run(t, commitAsIs("gtd: empty squash"));
|
|
13289
13792
|
expect(t.observe.resolveRef("HEAD")).not.toBe(headBefore);
|
|
13290
|
-
expect(headSubject(
|
|
13793
|
+
expect(t.observe.headSubject()).toBe("gtd: empty squash");
|
|
13291
13794
|
});
|
|
13292
13795
|
});
|
|
13293
13796
|
describe("softResetTo", () => {
|
|
13294
13797
|
it("moves HEAD back but keeps worktree changes from the second commit", () => {
|
|
13295
13798
|
const firstHash = t.observe.resolveRef("HEAD");
|
|
13296
13799
|
t.seed.commit("feat: second", { "second.txt": "second content" });
|
|
13297
|
-
|
|
13800
|
+
run(t, softResetTo(firstHash));
|
|
13298
13801
|
expect(t.observe.resolveRef("HEAD")).toBe(firstHash);
|
|
13299
13802
|
expect(t.observe.statusPorcelain()).toContain("second.txt");
|
|
13300
13803
|
});
|
|
@@ -13303,7 +13806,7 @@ const runGitScriptContract = () => {
|
|
|
13303
13806
|
it("moves HEAD, index, and working tree content back to the target ref", () => {
|
|
13304
13807
|
const firstHash = t.observe.resolveRef("HEAD");
|
|
13305
13808
|
t.seed.commit("feat: second", { "readme.txt": "modified content" });
|
|
13306
|
-
|
|
13809
|
+
run(t, hardResetTo(firstHash));
|
|
13307
13810
|
expect(t.observe.resolveRef("HEAD")).toBe(firstHash);
|
|
13308
13811
|
expect(t.observe.statusPorcelain()).toBe("");
|
|
13309
13812
|
expect(t.observe.readWorktreeFile("readme.txt")).toBe("hello");
|
|
@@ -13313,7 +13816,7 @@ const runGitScriptContract = () => {
|
|
|
13313
13816
|
it("moves HEAD and index, leaving the working tree untouched", () => {
|
|
13314
13817
|
const firstHash = t.observe.resolveRef("HEAD");
|
|
13315
13818
|
t.seed.commit("feat: second", { "second.txt": "second content" });
|
|
13316
|
-
|
|
13819
|
+
run(t, mixedResetTo(firstHash));
|
|
13317
13820
|
expect(t.observe.resolveRef("HEAD")).toBe(firstHash);
|
|
13318
13821
|
expect(t.observe.statusPorcelain()).toContain("second.txt");
|
|
13319
13822
|
});
|
|
@@ -13322,36 +13825,37 @@ const runGitScriptContract = () => {
|
|
|
13322
13825
|
it("drops both tracked modifications and untracked files", () => {
|
|
13323
13826
|
t.seed.writeFile("readme.txt", "modified");
|
|
13324
13827
|
t.seed.writeFile("untracked.txt", "new");
|
|
13325
|
-
|
|
13828
|
+
run(t, discardPending());
|
|
13326
13829
|
expect(t.observe.statusPorcelain()).toBe("");
|
|
13327
13830
|
});
|
|
13328
13831
|
});
|
|
13329
13832
|
describe("updateRef / deleteRef", () => {
|
|
13330
13833
|
it("updateRef points a ref at a commit", () => {
|
|
13331
13834
|
const head = t.observe.resolveRef("HEAD");
|
|
13332
|
-
|
|
13835
|
+
run(t, updateRef("refs/gtd/marker", head));
|
|
13333
13836
|
expect(t.observe.refExists("refs/gtd/marker")).toBe(true);
|
|
13334
13837
|
expect(t.observe.resolveRef("refs/gtd/marker")).toBe(head);
|
|
13335
13838
|
});
|
|
13336
13839
|
it("updateRef with HEAD points the ref at the commit just made", () => {
|
|
13337
|
-
|
|
13338
|
-
|
|
13840
|
+
run(t, commitAll("feat: just made"));
|
|
13841
|
+
run(t, updateRef("refs/gtd/history/x", "HEAD"));
|
|
13339
13842
|
expect(t.observe.resolveRef("refs/gtd/history/x")).toBe(t.observe.resolveRef("HEAD"));
|
|
13340
13843
|
});
|
|
13341
13844
|
it("deleteRef removes an existing ref, and tolerates deleting it again", () => {
|
|
13342
13845
|
const head = t.observe.resolveRef("HEAD");
|
|
13343
|
-
|
|
13344
|
-
|
|
13345
|
-
|
|
13846
|
+
run(t, updateRef("refs/gtd/marker", head));
|
|
13847
|
+
run(t, deleteRef("refs/gtd/marker"));
|
|
13848
|
+
run(t, deleteRef("refs/gtd/marker"));
|
|
13346
13849
|
expect(t.observe.refExists("refs/gtd/marker")).toBe(false);
|
|
13347
13850
|
});
|
|
13348
13851
|
it("deleteRef tolerates a ref that was never created", () => {
|
|
13349
|
-
expect(() =>
|
|
13852
|
+
expect(() => run(t, deleteRef("refs/gtd/never-created"))).not.toThrow();
|
|
13350
13853
|
expect(t.observe.refExists("refs/gtd/never-created")).toBe(false);
|
|
13351
13854
|
});
|
|
13352
13855
|
});
|
|
13353
13856
|
describe("fetchHistoryRefs", () => {
|
|
13354
|
-
it("pulls every refs/gtd/history/* from a bare remote and prints the count", () => {
|
|
13857
|
+
it("pulls every refs/gtd/history/* from a bare remote and prints the count", (ctx) => {
|
|
13858
|
+
if (!t.capabilities.remotes) ctx.skip();
|
|
13355
13859
|
const bare = realpathSync(mkdtempSync(join(tmpdir(), "gtd-git-tier-bare-")));
|
|
13356
13860
|
try {
|
|
13357
13861
|
gitExecIn(bare, "init", "-q", "--bare");
|
|
@@ -13375,7 +13879,8 @@ const runGitScriptContract = () => {
|
|
|
13375
13879
|
});
|
|
13376
13880
|
}
|
|
13377
13881
|
});
|
|
13378
|
-
it("fails loudly when a local history ref conflicts with the remote's", () => {
|
|
13882
|
+
it("fails loudly when a local history ref conflicts with the remote's", (ctx) => {
|
|
13883
|
+
if (!t.capabilities.remotes) ctx.skip();
|
|
13379
13884
|
const bare = realpathSync(mkdtempSync(join(tmpdir(), "gtd-git-tier-bare-")));
|
|
13380
13885
|
try {
|
|
13381
13886
|
gitExecIn(bare, "init", "-q", "--bare");
|