@pmelab/gtd 15.2.0 → 15.2.1
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/dist/gtd.bundle.mjs +557 -86
- package/package.json +2 -1
package/dist/gtd.bundle.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { isIP } from "node:net";
|
|
|
8
8
|
import * as Crypto from "node:crypto";
|
|
9
9
|
import { createHash, randomUUID } from "node:crypto";
|
|
10
10
|
import * as NFS from "node:fs";
|
|
11
|
-
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, realpathSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
12
12
|
import * as OS from "node:os";
|
|
13
13
|
import { homedir, tmpdir } from "node:os";
|
|
14
14
|
import * as Path from "node:path";
|
|
@@ -47623,6 +47623,7 @@ const makeGitImpl = (executor, root) => {
|
|
|
47623
47623
|
isAncestor: (a, b) => run$1(root, "git", "merge-base", "--is-ancestor", a, b).pipe(provide$2(succeed$2(CommandExecutor, executor)), map$4(() => true), catchAll(() => succeed$3(false))),
|
|
47624
47624
|
topLevel: () => exec("git", "rev-parse", "--show-toplevel").pipe(map$4((s) => s.trim())),
|
|
47625
47625
|
gitDir: () => exec("git", "rev-parse", "--absolute-git-dir").pipe(map$4((s) => s.trim())),
|
|
47626
|
+
gitCommonDir: () => exec("git", "rev-parse", "--path-format=absolute", "--git-common-dir").pipe(map$4((s) => s.trim())),
|
|
47626
47627
|
commitHistory: (base, head = "HEAD") => {
|
|
47627
47628
|
const range = base !== void 0 ? `${base}..${head}` : head !== "HEAD" ? head : void 0;
|
|
47628
47629
|
const args = [
|
|
@@ -47647,6 +47648,27 @@ const makeGitImpl = (executor, root) => {
|
|
|
47647
47648
|
};
|
|
47648
47649
|
})), catchAll(() => succeed$3([])));
|
|
47649
47650
|
},
|
|
47651
|
+
subjectHistory: (pageSize, skip, head = "HEAD") => {
|
|
47652
|
+
const ref = head !== "HEAD" ? head : void 0;
|
|
47653
|
+
const args = [
|
|
47654
|
+
"git",
|
|
47655
|
+
"log",
|
|
47656
|
+
"--first-parent",
|
|
47657
|
+
"--reverse",
|
|
47658
|
+
"-z",
|
|
47659
|
+
"--format=%H%x02%s",
|
|
47660
|
+
`--skip=${skip}`,
|
|
47661
|
+
...Number.isFinite(pageSize) ? [`--max-count=${pageSize}`] : [],
|
|
47662
|
+
...ref !== void 0 ? [ref] : []
|
|
47663
|
+
];
|
|
47664
|
+
return exec(...args).pipe(map$4((out) => splitNul(out).map((chunk) => {
|
|
47665
|
+
const sep = chunk.indexOf("");
|
|
47666
|
+
return {
|
|
47667
|
+
hash: (sep === -1 ? chunk : chunk.slice(0, sep)).trim(),
|
|
47668
|
+
subject: (sep === -1 ? "" : chunk.slice(sep + 1)).trim()
|
|
47669
|
+
};
|
|
47670
|
+
})), catchAll(() => succeed$3([])));
|
|
47671
|
+
},
|
|
47650
47672
|
commitAllWithPrefix: (prefix) => gen(function* () {
|
|
47651
47673
|
yield* exec("git", "add", "-A");
|
|
47652
47674
|
yield* exec("git", "commit", "--allow-empty", "-m", prefix).pipe(catchAll((error) => error.message.includes("empty git commit") ? exec("git", "commit", "--allow-empty", "--no-verify", "-m", prefix) : fail$2(error)));
|
|
@@ -47676,6 +47698,158 @@ var GitService = class GitService extends Tag("GitService")() {
|
|
|
47676
47698
|
const toError = (e) => e instanceof Error ? e : new Error(String(e));
|
|
47677
47699
|
const MAX_BUFFER = 256 * 1024 * 1024;
|
|
47678
47700
|
const HASH_CHUNK = 500;
|
|
47701
|
+
/** A full sha1 or sha256 object id — the only refs an immutable, disk-cached read is safe to key on. */
|
|
47702
|
+
const isObjectHash = (ref) => /^[0-9a-f]{40}$|^[0-9a-f]{64}$/.test(ref);
|
|
47703
|
+
const GTD_CACHE_SUBDIR = "gtd-cache";
|
|
47704
|
+
const treeCachePath = (commonDir, hash) => join(commonDir, GTD_CACHE_SUBDIR, "trees", hash);
|
|
47705
|
+
const blobCachePath = (commonDir, id) => join(commonDir, GTD_CACHE_SUBDIR, "blobs", id);
|
|
47706
|
+
const readTextCache = (path) => {
|
|
47707
|
+
try {
|
|
47708
|
+
return readFileSync(path, "utf8");
|
|
47709
|
+
} catch {
|
|
47710
|
+
return;
|
|
47711
|
+
}
|
|
47712
|
+
};
|
|
47713
|
+
/**
|
|
47714
|
+
* Atomic write: a temp name in the SAME directory, `rename`d into place —
|
|
47715
|
+
* rename is atomic on POSIX, so a concurrent reader sees either the old file
|
|
47716
|
+
* or the whole new one, never a half-written one. Any failure (full disk, a
|
|
47717
|
+
* read-only git directory, a raced directory removal) is swallowed: the
|
|
47718
|
+
* cache is a best-effort speedup, never a correctness dependency, and the
|
|
47719
|
+
* alternative — failing a gtd command over a cache write — is worse.
|
|
47720
|
+
*/
|
|
47721
|
+
const writeTextCacheAtomic = (path, content) => {
|
|
47722
|
+
try {
|
|
47723
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
47724
|
+
const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
47725
|
+
writeFileSync(tmp, content, "utf8");
|
|
47726
|
+
renameSync(tmp, path);
|
|
47727
|
+
} catch {}
|
|
47728
|
+
};
|
|
47729
|
+
/** A commit's `path → blob id` map, serialized as JSON. `undefined` on ANY read failure — missing, truncated, unparseable — so the caller falls back to asking git. */
|
|
47730
|
+
const readTreeCache = (commonDir, hash) => {
|
|
47731
|
+
const raw = readTextCache(treeCachePath(commonDir, hash));
|
|
47732
|
+
if (raw === void 0) return void 0;
|
|
47733
|
+
try {
|
|
47734
|
+
const parsed = JSON.parse(raw);
|
|
47735
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
47736
|
+
const map = /* @__PURE__ */ new Map();
|
|
47737
|
+
for (const [path, id] of Object.entries(parsed)) {
|
|
47738
|
+
if (typeof id !== "string") return void 0;
|
|
47739
|
+
map.set(path, id);
|
|
47740
|
+
}
|
|
47741
|
+
return map;
|
|
47742
|
+
} catch {
|
|
47743
|
+
return;
|
|
47744
|
+
}
|
|
47745
|
+
};
|
|
47746
|
+
const writeTreeCache = (commonDir, hash, tree) => writeTextCacheAtomic(treeCachePath(commonDir, hash), JSON.stringify(Object.fromEntries(tree)));
|
|
47747
|
+
/** Parses a `:`-prefixed raw-diff header token into its status letter and new blob id. */
|
|
47748
|
+
const parseRawDiffHeader = (header) => {
|
|
47749
|
+
const fields = header.slice(1).split(" ").filter((f) => f.length > 0);
|
|
47750
|
+
const status = fields[4] ?? "";
|
|
47751
|
+
const newId = fields[3] ?? "";
|
|
47752
|
+
return {
|
|
47753
|
+
letter: status[0] ?? "",
|
|
47754
|
+
newId
|
|
47755
|
+
};
|
|
47756
|
+
};
|
|
47757
|
+
/**
|
|
47758
|
+
* Consumes the 1 (or, for rename/copy, 2) NUL-separated path tokens that
|
|
47759
|
+
* follow a raw-diff header, starting at `tokens[i]`. Returns the resulting
|
|
47760
|
+
* entry (`undefined` if a path token is missing) and the next index.
|
|
47761
|
+
*/
|
|
47762
|
+
const consumeRawDiffPaths = (tokens, i, letter, newId) => {
|
|
47763
|
+
if (letter === "R" || letter === "C") {
|
|
47764
|
+
const oldPath = tokens[i];
|
|
47765
|
+
const path = tokens[i + 1];
|
|
47766
|
+
return {
|
|
47767
|
+
entry: oldPath !== void 0 && path !== void 0 ? {
|
|
47768
|
+
status: letter,
|
|
47769
|
+
oldPath,
|
|
47770
|
+
path,
|
|
47771
|
+
newId
|
|
47772
|
+
} : void 0,
|
|
47773
|
+
next: i + 2
|
|
47774
|
+
};
|
|
47775
|
+
}
|
|
47776
|
+
const path = tokens[i];
|
|
47777
|
+
return {
|
|
47778
|
+
entry: path !== void 0 ? {
|
|
47779
|
+
status: letter,
|
|
47780
|
+
path,
|
|
47781
|
+
newId
|
|
47782
|
+
} : void 0,
|
|
47783
|
+
next: i + 1
|
|
47784
|
+
};
|
|
47785
|
+
};
|
|
47786
|
+
/** Parses the NUL-separated body of ONE commit's `--raw -z` diff (the format string's own separator already stripped). */
|
|
47787
|
+
const parseRawDiff = (body) => {
|
|
47788
|
+
const tokens = body.split("\0").filter((t) => t.length > 0);
|
|
47789
|
+
const entries = [];
|
|
47790
|
+
let i = 0;
|
|
47791
|
+
while (i < tokens.length) {
|
|
47792
|
+
const header = tokens[i];
|
|
47793
|
+
i += 1;
|
|
47794
|
+
if (!header.startsWith(":")) continue;
|
|
47795
|
+
const { letter, newId } = parseRawDiffHeader(header);
|
|
47796
|
+
const { entry, next } = consumeRawDiffPaths(tokens, i, letter, newId);
|
|
47797
|
+
i = next;
|
|
47798
|
+
if (entry !== void 0) entries.push(entry);
|
|
47799
|
+
}
|
|
47800
|
+
return entries;
|
|
47801
|
+
};
|
|
47802
|
+
/** Applies one commit's raw-diff entries to a (mutable, working-copy) tree map. */
|
|
47803
|
+
const applyRawDiff = (tree, entries) => {
|
|
47804
|
+
for (const entry of entries) if (entry.status === "D") tree.delete(entry.path);
|
|
47805
|
+
else {
|
|
47806
|
+
if (entry.status === "R") tree.delete(entry.oldPath);
|
|
47807
|
+
tree.set(entry.path, entry.newId);
|
|
47808
|
+
}
|
|
47809
|
+
};
|
|
47810
|
+
/** `git ls-tree -r -z --full-tree <ref>`'s output, parsed into `path → blob id`. */
|
|
47811
|
+
const parseLsTree = (out) => {
|
|
47812
|
+
const entries = /* @__PURE__ */ new Map();
|
|
47813
|
+
for (const record of out.split("\0")) {
|
|
47814
|
+
const tab = record.indexOf(" ");
|
|
47815
|
+
if (tab === -1) continue;
|
|
47816
|
+
const id = record.slice(0, tab).split(" ")[2];
|
|
47817
|
+
if (id !== void 0) entries.set(record.slice(tab + 1), id);
|
|
47818
|
+
}
|
|
47819
|
+
return entries;
|
|
47820
|
+
};
|
|
47821
|
+
/**
|
|
47822
|
+
* Runs `git ls-tree` for `ref` and reports whether it actually succeeded —
|
|
47823
|
+
* `ok: false` on ANY subprocess failure (a transient `EAGAIN`/`ENOMEM` on
|
|
47824
|
+
* fork, a `git gc` race, a signal), never folded into an empty tree here.
|
|
47825
|
+
* The disk/memo cache must only ever hold a git-CONFIRMED tree: an immutable
|
|
47826
|
+
* key can serve a wrong answer forever if what's stored under it came from a
|
|
47827
|
+
* failed read rather than from the key itself, so callers that cache must
|
|
47828
|
+
* check `ok` before doing so.
|
|
47829
|
+
*/
|
|
47830
|
+
const fetchTree = (root, ref) => {
|
|
47831
|
+
try {
|
|
47832
|
+
return {
|
|
47833
|
+
ok: true,
|
|
47834
|
+
entries: parseLsTree(execFileSync("git", [
|
|
47835
|
+
"ls-tree",
|
|
47836
|
+
"-r",
|
|
47837
|
+
"-z",
|
|
47838
|
+
"--full-tree",
|
|
47839
|
+
ref
|
|
47840
|
+
], {
|
|
47841
|
+
cwd: root,
|
|
47842
|
+
encoding: "utf8",
|
|
47843
|
+
maxBuffer: MAX_BUFFER
|
|
47844
|
+
}))
|
|
47845
|
+
};
|
|
47846
|
+
} catch {
|
|
47847
|
+
return {
|
|
47848
|
+
ok: false,
|
|
47849
|
+
entries: /* @__PURE__ */ new Map()
|
|
47850
|
+
};
|
|
47851
|
+
}
|
|
47852
|
+
};
|
|
47679
47853
|
const assertRepoRelative = (path) => {
|
|
47680
47854
|
if (isAbsolute(path)) throw new Error(`Workspace: "${path}" is an absolute path — this member takes repo-relative paths only (use "atPath"/"writeAtPath" to read/write outside the repo)`);
|
|
47681
47855
|
return path;
|
|
@@ -47696,13 +47870,96 @@ const lstatOrAbsent = (path) => {
|
|
|
47696
47870
|
throw e;
|
|
47697
47871
|
}
|
|
47698
47872
|
};
|
|
47699
|
-
|
|
47873
|
+
/**
|
|
47874
|
+
* Runs `git log --raw` for `base..head` (or all of `head`'s first-parent
|
|
47875
|
+
* history when `base` is `undefined`) and splits the output into one
|
|
47876
|
+
* `RawLogEntry` per commit, validating the result against `commits` — same
|
|
47877
|
+
* count, same hashes, in order — since the two disagreeing means the episode
|
|
47878
|
+
* bounds no longer match git's own history.
|
|
47879
|
+
*/
|
|
47880
|
+
const runRawLogReplay = (root, base, head, commits) => {
|
|
47881
|
+
const chunks = execFileSync("git", [
|
|
47882
|
+
"log",
|
|
47883
|
+
"--first-parent",
|
|
47884
|
+
"--reverse",
|
|
47885
|
+
"-z",
|
|
47886
|
+
"--raw",
|
|
47887
|
+
"-r",
|
|
47888
|
+
"--no-abbrev",
|
|
47889
|
+
"--format=%x01%H",
|
|
47890
|
+
base !== void 0 ? `${base}..${head}` : head
|
|
47891
|
+
], {
|
|
47892
|
+
cwd: root,
|
|
47893
|
+
encoding: "utf8",
|
|
47894
|
+
maxBuffer: MAX_BUFFER
|
|
47895
|
+
}).split("").filter((c) => c.length > 0);
|
|
47896
|
+
if (chunks.length !== commits.length) throw new Error(`gtd: episode reconstruction expected ${commits.length} commit(s) from git log, got ${chunks.length} — the episode bounds disagree with git's own history`);
|
|
47897
|
+
return chunks.map((chunk, i) => {
|
|
47898
|
+
const nul = chunk.indexOf("\0");
|
|
47899
|
+
const hash = nul === -1 ? chunk : chunk.slice(0, nul);
|
|
47900
|
+
if (hash !== commits[i]) throw new Error(`gtd: episode reconstruction hash mismatch at position ${i}: expected ${commits[i]}, got ${hash}`);
|
|
47901
|
+
const tail = nul === -1 ? "" : chunk.slice(nul + 1);
|
|
47902
|
+
return {
|
|
47903
|
+
hash,
|
|
47904
|
+
tail: tail.startsWith("\n") ? tail.slice(1) : tail
|
|
47905
|
+
};
|
|
47906
|
+
});
|
|
47907
|
+
};
|
|
47908
|
+
/** Applies each `RawLogEntry`'s diff in order onto `baseTree`, caching (and returning) every resulting tree. */
|
|
47909
|
+
const foldRawDiffs = (baseTree, replay, rememberTree) => {
|
|
47910
|
+
const result = /* @__PURE__ */ new Map();
|
|
47911
|
+
let current = new Map(baseTree);
|
|
47912
|
+
for (const { hash, tail } of replay) {
|
|
47913
|
+
const next = new Map(current);
|
|
47914
|
+
applyRawDiff(next, parseRawDiff(tail));
|
|
47915
|
+
current = next;
|
|
47916
|
+
result.set(hash, current);
|
|
47917
|
+
rememberTree(hash, current);
|
|
47918
|
+
}
|
|
47919
|
+
return result;
|
|
47920
|
+
};
|
|
47921
|
+
const makeWorkspaceOps = (root, git, commonDir) => {
|
|
47700
47922
|
const resolveAny = (path) => isAbsolute(path) ? path : join(root, path);
|
|
47923
|
+
const treeMemo = /* @__PURE__ */ new Map();
|
|
47924
|
+
const blobMemo = /* @__PURE__ */ new Map();
|
|
47925
|
+
/** A commit's tree, from the in-process memo or the on-disk cache — no subprocess. `undefined` on a cold miss. */
|
|
47926
|
+
const peekTree = (hash) => {
|
|
47927
|
+
const memoHit = treeMemo.get(hash);
|
|
47928
|
+
if (memoHit !== void 0) return memoHit;
|
|
47929
|
+
if (commonDir === void 0) return void 0;
|
|
47930
|
+
const cached = readTreeCache(commonDir, hash);
|
|
47931
|
+
if (cached !== void 0) treeMemo.set(hash, cached);
|
|
47932
|
+
return cached;
|
|
47933
|
+
};
|
|
47934
|
+
const rememberTree = (hash, tree) => {
|
|
47935
|
+
treeMemo.set(hash, tree);
|
|
47936
|
+
if (commonDir !== void 0) writeTreeCache(commonDir, hash, tree);
|
|
47937
|
+
};
|
|
47701
47938
|
const readSync = (path) => readFileOrAbsent(join(root, assertRepoRelative(path)));
|
|
47702
|
-
|
|
47703
|
-
|
|
47939
|
+
/**
|
|
47940
|
+
* A blob's content, keyed on its OWN id rather than a (path, ref) pair —
|
|
47941
|
+
* the same content read from two different commits costs one read, not
|
|
47942
|
+
* two. `git cat-file -p` reads any object by id directly, no pathspec
|
|
47943
|
+
* needed. Any failure folds to `undefined`, matching `readCommittedSync`'s
|
|
47944
|
+
* own catch-all-as-absence.
|
|
47945
|
+
*/
|
|
47946
|
+
const readBlobSync = (id) => {
|
|
47947
|
+
const memoHit = blobMemo.get(id);
|
|
47948
|
+
if (memoHit !== void 0) return memoHit;
|
|
47949
|
+
if (commonDir !== void 0) {
|
|
47950
|
+
const cached = readTextCache(blobCachePath(commonDir, id));
|
|
47951
|
+
if (cached !== void 0) {
|
|
47952
|
+
blobMemo.set(id, cached);
|
|
47953
|
+
return cached;
|
|
47954
|
+
}
|
|
47955
|
+
}
|
|
47956
|
+
let content;
|
|
47704
47957
|
try {
|
|
47705
|
-
|
|
47958
|
+
content = execFileSync("git", [
|
|
47959
|
+
"cat-file",
|
|
47960
|
+
"-p",
|
|
47961
|
+
id
|
|
47962
|
+
], {
|
|
47706
47963
|
cwd: root,
|
|
47707
47964
|
encoding: "utf8",
|
|
47708
47965
|
maxBuffer: MAX_BUFFER
|
|
@@ -47710,33 +47967,72 @@ const makeWorkspaceOps = (root, git) => {
|
|
|
47710
47967
|
} catch {
|
|
47711
47968
|
return;
|
|
47712
47969
|
}
|
|
47970
|
+
blobMemo.set(id, content);
|
|
47971
|
+
if (commonDir !== void 0) writeTextCacheAtomic(blobCachePath(commonDir, id), content);
|
|
47972
|
+
return content;
|
|
47713
47973
|
};
|
|
47714
|
-
const
|
|
47715
|
-
const
|
|
47716
|
-
|
|
47974
|
+
const readCommittedSync = (path, ref = "HEAD") => {
|
|
47975
|
+
const relPath = assertRepoRelative(path);
|
|
47976
|
+
if (isObjectHash(ref)) {
|
|
47977
|
+
const id = treeSync(ref).get(relPath);
|
|
47978
|
+
return id === void 0 ? void 0 : readBlobSync(id);
|
|
47979
|
+
}
|
|
47717
47980
|
try {
|
|
47718
|
-
|
|
47719
|
-
"ls-tree",
|
|
47720
|
-
"-r",
|
|
47721
|
-
"-z",
|
|
47722
|
-
"--full-tree",
|
|
47723
|
-
ref
|
|
47724
|
-
], {
|
|
47981
|
+
return execFileSync("git", ["show", `${ref}:${relPath}`], {
|
|
47725
47982
|
cwd: root,
|
|
47726
47983
|
encoding: "utf8",
|
|
47727
47984
|
maxBuffer: MAX_BUFFER
|
|
47728
47985
|
});
|
|
47729
47986
|
} catch {
|
|
47730
|
-
return
|
|
47987
|
+
return;
|
|
47731
47988
|
}
|
|
47732
|
-
|
|
47733
|
-
|
|
47734
|
-
|
|
47735
|
-
const
|
|
47736
|
-
if (
|
|
47989
|
+
};
|
|
47990
|
+
const readTree = (ref, onFailure) => {
|
|
47991
|
+
if (isObjectHash(ref)) {
|
|
47992
|
+
const cached = peekTree(ref);
|
|
47993
|
+
if (cached !== void 0) return cached;
|
|
47994
|
+
}
|
|
47995
|
+
const { ok, entries } = fetchTree(root, ref);
|
|
47996
|
+
if (!ok) {
|
|
47997
|
+
if (onFailure === "throw") throw new Error(`gtd: failed to read the tree at "${ref}" — git ls-tree did not succeed`);
|
|
47998
|
+
return entries;
|
|
47737
47999
|
}
|
|
48000
|
+
if (isObjectHash(ref)) rememberTree(ref, entries);
|
|
47738
48001
|
return entries;
|
|
47739
48002
|
};
|
|
48003
|
+
const treeSync = (ref) => readTree(ref, "empty");
|
|
48004
|
+
/**
|
|
48005
|
+
* `treeSync`'s starting point when a failed read must not silently
|
|
48006
|
+
* proceed: a cache hit or a git-confirmed `ls-tree` succeeds; any
|
|
48007
|
+
* subprocess failure THROWS rather than folding to an empty tree, because
|
|
48008
|
+
* `episodeTrees` folds every subsequent commit's diff on top of this one —
|
|
48009
|
+
* a wrong empty base would poison every commit's cached tree in the
|
|
48010
|
+
* episode, not just this one's.
|
|
48011
|
+
*/
|
|
48012
|
+
const confirmedTree = (ref) => readTree(ref, "throw");
|
|
48013
|
+
/**
|
|
48014
|
+
* The delta-stream reconstruction (package 4's Requirement A): one
|
|
48015
|
+
* `ls-tree` for `base`, one `--raw` `git log` for every commit, applied in
|
|
48016
|
+
* order. Skipped entirely when every requested hash is already known
|
|
48017
|
+
* (memo or on-disk cache) — then this issues NO subprocess at all.
|
|
48018
|
+
*/
|
|
48019
|
+
const episodeTrees = (base, commits) => try_({
|
|
48020
|
+
try: () => {
|
|
48021
|
+
const result = /* @__PURE__ */ new Map();
|
|
48022
|
+
const baseTree = base !== void 0 ? confirmedTree(base) : /* @__PURE__ */ new Map();
|
|
48023
|
+
if (base !== void 0) result.set(base, baseTree);
|
|
48024
|
+
if (commits.length === 0) return result;
|
|
48025
|
+
if (commits.every((hash) => peekTree(hash) !== void 0)) {
|
|
48026
|
+
for (const hash of commits) result.set(hash, treeSync(hash));
|
|
48027
|
+
return result;
|
|
48028
|
+
}
|
|
48029
|
+
const head = commits[commits.length - 1];
|
|
48030
|
+
const replay = runRawLogReplay(root, base, head, commits);
|
|
48031
|
+
for (const [hash, tree] of foldRawDiffs(baseTree, replay, rememberTree)) result.set(hash, tree);
|
|
48032
|
+
return result;
|
|
48033
|
+
},
|
|
48034
|
+
catch: toError
|
|
48035
|
+
});
|
|
47740
48036
|
const gitSync = (args, input) => execFileSync("git", args, {
|
|
47741
48037
|
cwd: root,
|
|
47742
48038
|
encoding: "utf8",
|
|
@@ -47795,6 +48091,7 @@ const makeWorkspaceOps = (root, git) => {
|
|
|
47795
48091
|
return {
|
|
47796
48092
|
treeSync,
|
|
47797
48093
|
worktreeSync,
|
|
48094
|
+
episodeTrees,
|
|
47798
48095
|
readSync,
|
|
47799
48096
|
read: (path) => try_({
|
|
47800
48097
|
try: () => readSync(path),
|
|
@@ -47820,7 +48117,8 @@ var Workspace = class Workspace extends Tag("Workspace")() {
|
|
|
47820
48117
|
static Live = effect$1(Workspace, gen(function* () {
|
|
47821
48118
|
const { root } = yield* Host;
|
|
47822
48119
|
const git = yield* GitService;
|
|
47823
|
-
|
|
48120
|
+
const commonDir = yield* git.gitCommonDir().pipe(catchAll(() => succeed$3(void 0)));
|
|
48121
|
+
return makeWorkspaceOps(root, git, commonDir);
|
|
47824
48122
|
}));
|
|
47825
48123
|
};
|
|
47826
48124
|
//#endregion
|
|
@@ -200431,7 +200729,9 @@ const load = gen(function* () {
|
|
|
200431
200729
|
workflowVars: { ...loaded.defaults },
|
|
200432
200730
|
rcVars: compiled.rcVars,
|
|
200433
200731
|
...compiled.ui !== void 0 ? { ui: compiled.ui } : {},
|
|
200434
|
-
warnings: diagnostics.filter((d) => d.severity === "warning")
|
|
200732
|
+
warnings: diagnostics.filter((d) => d.severity === "warning"),
|
|
200733
|
+
configFiles: levels.map((level) => level.filepath),
|
|
200734
|
+
workflowFiles: loaded.dependencyFiles
|
|
200435
200735
|
};
|
|
200436
200736
|
});
|
|
200437
200737
|
const TRANSFORM_MEMO_SIZE = 512;
|
|
@@ -200480,7 +200780,7 @@ const optionalFunction = (exports, name) => {
|
|
|
200480
200780
|
* Read a workflow module: the default export is the flow; `defaults`,
|
|
200481
200781
|
* `summary`, `base` and `steering` are optional; any other export is ignored.
|
|
200482
200782
|
*/
|
|
200483
|
-
const fromModule = (exported, origin) => {
|
|
200783
|
+
const fromModule = (exported, origin, dependencyFiles) => {
|
|
200484
200784
|
const exports = typeof exported === "object" && exported !== null ? exported : {};
|
|
200485
200785
|
const flow = exports.default;
|
|
200486
200786
|
if (typeof flow !== "function") throw new Error("the default export is not a flow — export default an async function");
|
|
@@ -200490,11 +200790,28 @@ const fromModule = (exported, origin) => {
|
|
|
200490
200790
|
steering: stringRecord(exports, "steering"),
|
|
200491
200791
|
summary: optionalFunction(exports, "summary"),
|
|
200492
200792
|
base: optionalFunction(exports, "base"),
|
|
200493
|
-
origin
|
|
200793
|
+
origin,
|
|
200794
|
+
dependencyFiles
|
|
200494
200795
|
};
|
|
200495
200796
|
};
|
|
200496
|
-
/**
|
|
200497
|
-
|
|
200797
|
+
/**
|
|
200798
|
+
* Evaluate `gtd.config.ts`, or take the bundled default. `cache` is jiti's
|
|
200799
|
+
* OWN per-call module cache (unrelated to the instance's `moduleCache:
|
|
200800
|
+
* false` — that governs reuse ACROSS calls, this one is populated fresh
|
|
200801
|
+
* DURING this one) — passing an empty object in has the side effect of
|
|
200802
|
+
* collecting every real file jiti resolved evaluating this module, module
|
|
200803
|
+
* AND every relative import it pulled in, transitively. Virtual modules
|
|
200804
|
+
* (`@pmelab/gtd/flows`) never touch it — no real file backs them.
|
|
200805
|
+
*/
|
|
200806
|
+
const loadWorkflow = (module) => {
|
|
200807
|
+
if (module === void 0) return fromModule(unified_exports, BUILT_IN_ORIGIN, []);
|
|
200808
|
+
const cache = {};
|
|
200809
|
+
const exported = jiti().evalModule(module.source, {
|
|
200810
|
+
filename: module.filepath,
|
|
200811
|
+
cache
|
|
200812
|
+
});
|
|
200813
|
+
return fromModule(exported, module.filepath, Object.keys(cache));
|
|
200814
|
+
};
|
|
200498
200815
|
const headTree = (workspace) => {
|
|
200499
200816
|
let entries;
|
|
200500
200817
|
const list = () => entries ??= workspace.treeSync("HEAD");
|
|
@@ -206061,8 +206378,86 @@ const runOf = (history, location, closingHash = void 0) => {
|
|
|
206061
206378
|
}
|
|
206062
206379
|
};
|
|
206063
206380
|
};
|
|
206064
|
-
const
|
|
206065
|
-
|
|
206381
|
+
const PAGE_TIERS = [
|
|
206382
|
+
32,
|
|
206383
|
+
128,
|
|
206384
|
+
512,
|
|
206385
|
+
Infinity
|
|
206386
|
+
];
|
|
206387
|
+
/**
|
|
206388
|
+
* `locateEpisode`'s own boundary test, subject-only: a commit that isn't a
|
|
206389
|
+
* gtd step commit, or one landing the flow's initial state, bounds an
|
|
206390
|
+
* episode from below — EXCEPT a bare `gtd(human): <x>` subject (no `→`),
|
|
206391
|
+
* which is NEVER a boundary here, whatever `<x>` is. That bare shape is
|
|
206392
|
+
* `locateEpisode`'s own `opening` branch (an entered episode's first commit),
|
|
206393
|
+
* which takes priority there over the initial-state check no matter what the
|
|
206394
|
+
* entry is named — including an entry named after `def.initial` itself,
|
|
206395
|
+
* where the two branches would otherwise disagree. A bare human commit can
|
|
206396
|
+
* also be a self-loop step landing rather than a real opening (this
|
|
206397
|
+
* subject-only pass can't read the `Gtd-Step` trailer that would tell them
|
|
206398
|
+
* apart), so ruling it out here can only make `findBoundaryBase` walk a FEW
|
|
206399
|
+
* commits further back than strictly needed — never stop short of the
|
|
206400
|
+
* boundary `locateEpisode` would find once it has full commit bodies in
|
|
206401
|
+
* hand. That asymmetry is the only direction this approximation may err in;
|
|
206402
|
+
* it must never call a real boundary "not yet found".
|
|
206403
|
+
*/
|
|
206404
|
+
const subjectIsBoundary = (def, subject) => {
|
|
206405
|
+
const parsed = parseCommitMessage(subject).parsed;
|
|
206406
|
+
if (parsed === void 0 || !ACTORS.has(parsed.actor)) return true;
|
|
206407
|
+
if (parsed.actor === "human" && parsed.from === void 0) return false;
|
|
206408
|
+
return parsed.to === def.initial;
|
|
206409
|
+
};
|
|
206410
|
+
/** The last page index (walking newest→oldest, same direction as `locateEpisode`) whose subject is a boundary, or `undefined` if none in `page`. */
|
|
206411
|
+
const boundaryIndexIn = (def, page) => {
|
|
206412
|
+
for (let i = page.length - 1; i >= 0; i--) if (subjectIsBoundary(def, page[i].subject)) return i;
|
|
206413
|
+
};
|
|
206414
|
+
/**
|
|
206415
|
+
* `findBoundaryBase`'s result: `base` is the hash to pass `commitHistory` so
|
|
206416
|
+
* a `git.commitHistory(base, head)` call reads exactly the episode ending at
|
|
206417
|
+
* `head`, plus whatever a subject-only read couldn't rule out — never the
|
|
206418
|
+
* whole repository. `base` is deliberately the boundary commit's PARENT, not
|
|
206419
|
+
* the boundary commit itself: `commitHistory`'s range excludes `base`, and
|
|
206420
|
+
* `runOf` needs the boundary commit present in the read history
|
|
206421
|
+
* (`episode.base`, `startParentHash`). `base: undefined` means "read from the
|
|
206422
|
+
* repository root" — an empty repository, an unresolvable `head`, or an
|
|
206423
|
+
* episode that reaches the root commit, none of which cost anything extra
|
|
206424
|
+
* since that IS the whole history in those cases.
|
|
206425
|
+
*
|
|
206426
|
+
* `head` is the CONCRETE hash the literal `head` argument resolved to on this
|
|
206427
|
+
* call's first read — `undefined` only when it never resolved (an empty
|
|
206428
|
+
* repository, or an unresolvable ref). The caller must hand this hash, not
|
|
206429
|
+
* the original `head` argument, to its own `commitHistory` call: resolving a
|
|
206430
|
+
* symbolic ref (literal `HEAD`, most callers' default) a second time risks
|
|
206431
|
+
* reading a DIFFERENT commit than this function paged through, if something
|
|
206432
|
+
* moves HEAD in between — pinning both reads to one resolved hash is what
|
|
206433
|
+
* keeps the two-read split from disagreeing with itself.
|
|
206434
|
+
*/
|
|
206435
|
+
const findBoundaryBase = (git, def, head) => gen(function* () {
|
|
206436
|
+
let pinnedHead = head;
|
|
206437
|
+
for (const pageSize of PAGE_TIERS) {
|
|
206438
|
+
const page = yield* git.subjectHistory(pageSize, 0, pinnedHead);
|
|
206439
|
+
if (pinnedHead === void 0) pinnedHead = page[page.length - 1]?.hash;
|
|
206440
|
+
const wholeHistory = !Number.isFinite(pageSize) || page.length < pageSize;
|
|
206441
|
+
const boundaryIdx = boundaryIndexIn(def, page);
|
|
206442
|
+
if (boundaryIdx === void 0 || boundaryIdx === 0) {
|
|
206443
|
+
if (wholeHistory) return {
|
|
206444
|
+
base: void 0,
|
|
206445
|
+
head: pinnedHead
|
|
206446
|
+
};
|
|
206447
|
+
continue;
|
|
206448
|
+
}
|
|
206449
|
+
return {
|
|
206450
|
+
base: page[boundaryIdx - 1].hash,
|
|
206451
|
+
head: pinnedHead
|
|
206452
|
+
};
|
|
206453
|
+
}
|
|
206454
|
+
return {
|
|
206455
|
+
base: void 0,
|
|
206456
|
+
head: pinnedHead
|
|
206457
|
+
};
|
|
206458
|
+
});
|
|
206459
|
+
const historyUpTo = (git, def, head) => flatMap$4(findBoundaryBase(git, def, head), ({ base, head: resolvedHead }) => git.commitHistory(base, resolvedHead));
|
|
206460
|
+
const computeProcessRun = (git, def, head) => map$4(historyUpTo(git, def, head), (history) => runOf(history, locateEpisode(def, history)));
|
|
206066
206461
|
/** The run alone, never replaying — `gtd abandon` must work even when replay would refuse. */
|
|
206067
206462
|
const currentRun = gen(function* () {
|
|
206068
206463
|
const git = yield* GitService;
|
|
@@ -206073,19 +206468,29 @@ const currentRun = gen(function* () {
|
|
|
206073
206468
|
const summaryRun = gen(function* () {
|
|
206074
206469
|
const git = yield* GitService;
|
|
206075
206470
|
const def = (yield* (yield* ConfigService).load).workflow;
|
|
206076
|
-
const
|
|
206077
|
-
|
|
206078
|
-
|
|
206079
|
-
|
|
206471
|
+
const headEntry = (yield* git.subjectHistory(1, 0))[0];
|
|
206472
|
+
if (headEntry === void 0) {
|
|
206473
|
+
const history = yield* historyUpTo(git, def, void 0);
|
|
206474
|
+
return runOf(history, locateEpisode(def, history));
|
|
206475
|
+
}
|
|
206476
|
+
if (!(parseCommitMessage(headEntry.subject).parsed?.to === def.initial)) {
|
|
206477
|
+
const history = yield* historyUpTo(git, def, headEntry.hash);
|
|
206478
|
+
return runOf(history, locateEpisode(def, history));
|
|
206479
|
+
}
|
|
206480
|
+
const parentRef = `${headEntry.hash}~1`;
|
|
206481
|
+
const before = yield* historyUpTo(git, def, parentRef);
|
|
206080
206482
|
const location = locateEpisode(def, before);
|
|
206081
|
-
|
|
206483
|
+
let closing = yield* git.commitHistory(parentRef, headEntry.hash);
|
|
206484
|
+
if (closing.length === 0) closing = yield* git.commitHistory(void 0, headEntry.hash);
|
|
206485
|
+
return runOf([...before, ...closing], { ...location }, headEntry.hash);
|
|
206082
206486
|
});
|
|
206083
206487
|
const commitTree = (workspace, hash) => {
|
|
206084
206488
|
let entries;
|
|
206085
206489
|
const list = () => entries ??= workspace.treeSync(hash);
|
|
206490
|
+
let sortedPaths;
|
|
206086
206491
|
const contents = /* @__PURE__ */ new Map();
|
|
206087
206492
|
return {
|
|
206088
|
-
paths: () => [...list().keys()].sort(),
|
|
206493
|
+
paths: () => sortedPaths ??= [...list().keys()].sort(),
|
|
206089
206494
|
read: (path) => {
|
|
206090
206495
|
if (!list().has(path)) return void 0;
|
|
206091
206496
|
if (!contents.has(path)) contents.set(path, workspace.readCommittedSync(path, hash));
|
|
@@ -206094,9 +206499,13 @@ const commitTree = (workspace, hash) => {
|
|
|
206094
206499
|
id: (path) => list().get(path)
|
|
206095
206500
|
};
|
|
206096
206501
|
};
|
|
206097
|
-
/**
|
|
206098
|
-
|
|
206099
|
-
|
|
206502
|
+
/**
|
|
206503
|
+
* The tree a landing would commit: the working tree, with any rewrite the
|
|
206504
|
+
* landing script applies first. `entries` is `setup.pendingWorktree()` —
|
|
206505
|
+
* already memoized — never a fresh `workspace.worktreeSync()` call; see that
|
|
206506
|
+
* field's own comment for why the memo lives there and not on `Workspace`.
|
|
206507
|
+
*/
|
|
206508
|
+
const pendingTree = (workspace, entries, rewrite) => {
|
|
206100
206509
|
const paths = [...entries.keys()];
|
|
206101
206510
|
const readWorktree = (path) => {
|
|
206102
206511
|
try {
|
|
@@ -206128,32 +206537,18 @@ const judgeBudgetBytes = (vars) => {
|
|
|
206128
206537
|
if (raw.trim() === "" || !Number.isInteger(n) || n <= 0) throw new Error(`"judgeBudgetBytes" must be a positive integer — got ${JSON.stringify(raw)} (blanking this var disables the payload bound rather than the mechanism it guards, so it is refused rather than defaulted)`);
|
|
206129
206538
|
return n;
|
|
206130
206539
|
};
|
|
206131
|
-
const
|
|
206132
|
-
|
|
206133
|
-
|
|
206134
|
-
|
|
206135
|
-
|
|
206136
|
-
|
|
206137
|
-
|
|
206138
|
-
|
|
206139
|
-
|
|
206140
|
-
|
|
206141
|
-
|
|
206142
|
-
|
|
206143
|
-
hash: EMPTY_TREE$1,
|
|
206144
|
-
tree: treeFromRecord({})
|
|
206145
|
-
} : {
|
|
206146
|
-
hash: base,
|
|
206147
|
-
tree: commitTree(setup.workspace, base)
|
|
206148
|
-
},
|
|
206149
|
-
commits: episodeCommits(setup)
|
|
206150
|
-
},
|
|
206151
|
-
vars: setup.vars,
|
|
206152
|
-
start: setup.run.diffBase,
|
|
206153
|
-
budgetBytes: setup.budget,
|
|
206154
|
-
...pending !== void 0 ? { pending } : {}
|
|
206155
|
-
});
|
|
206156
|
-
};
|
|
206540
|
+
const replayFor = (setup, pending) => replay({
|
|
206541
|
+
flow: setup.def.flow,
|
|
206542
|
+
episode: {
|
|
206543
|
+
entry: setup.run.entry,
|
|
206544
|
+
base: setup.base,
|
|
206545
|
+
commits: setup.episode
|
|
206546
|
+
},
|
|
206547
|
+
vars: setup.vars,
|
|
206548
|
+
start: setup.run.diffBase,
|
|
206549
|
+
budgetBytes: setup.budget,
|
|
206550
|
+
...pending !== void 0 ? { pending } : {}
|
|
206551
|
+
});
|
|
206157
206552
|
const replayError = (outcome) => {
|
|
206158
206553
|
if (outcome.kind === "divergence" || outcome.kind === "failed") return new Error(outcome.message);
|
|
206159
206554
|
if (outcome.kind === "refused") return /* @__PURE__ */ new Error(`gtd: ${outcome.message}`);
|
|
@@ -206288,15 +206683,33 @@ const restAt = (ref) => gen(function* () {
|
|
|
206288
206683
|
const def = config.workflow;
|
|
206289
206684
|
const run = yield* computeProcessRun(git, def, ref);
|
|
206290
206685
|
const vars = resolveVars(config.workflowVars, config.rcVars, run.entryVars, host.env);
|
|
206686
|
+
const budget = yield* try_({
|
|
206687
|
+
try: () => judgeBudgetBytes(vars),
|
|
206688
|
+
catch: (e) => e instanceof Error ? e : new Error(String(e))
|
|
206689
|
+
});
|
|
206690
|
+
yield* workspace.episodeTrees(run.episode.base, run.episode.commits.map((c) => c.hash));
|
|
206691
|
+
const base = run.episode.base === void 0 ? {
|
|
206692
|
+
hash: EMPTY_TREE$1,
|
|
206693
|
+
tree: treeFromRecord({})
|
|
206694
|
+
} : {
|
|
206695
|
+
hash: run.episode.base,
|
|
206696
|
+
tree: commitTree(workspace, run.episode.base)
|
|
206697
|
+
};
|
|
206698
|
+
const episode = run.episode.commits.map((c) => ({
|
|
206699
|
+
hash: c.hash,
|
|
206700
|
+
message: c.message,
|
|
206701
|
+
tree: commitTree(workspace, c.hash)
|
|
206702
|
+
}));
|
|
206703
|
+
let pendingMemo;
|
|
206291
206704
|
const setup = {
|
|
206292
206705
|
def,
|
|
206293
206706
|
run,
|
|
206294
206707
|
vars,
|
|
206295
|
-
budget
|
|
206296
|
-
|
|
206297
|
-
|
|
206298
|
-
|
|
206299
|
-
workspace
|
|
206708
|
+
budget,
|
|
206709
|
+
workspace,
|
|
206710
|
+
base,
|
|
206711
|
+
episode,
|
|
206712
|
+
pendingWorktree: () => pendingMemo ??= workspace.worktreeSync()
|
|
206300
206713
|
};
|
|
206301
206714
|
const outcome = yield* promise(() => replayFor(setup));
|
|
206302
206715
|
const error = replayError(outcome);
|
|
@@ -206347,7 +206760,7 @@ const entryRefusal = (rest, name, entryVars) => gen(function* () {
|
|
|
206347
206760
|
entry: name,
|
|
206348
206761
|
base: {
|
|
206349
206762
|
hash: "",
|
|
206350
|
-
tree: pendingTree(workspace, void 0)
|
|
206763
|
+
tree: pendingTree(workspace, rest.setup.pendingWorktree(), void 0)
|
|
206351
206764
|
},
|
|
206352
206765
|
commits: []
|
|
206353
206766
|
},
|
|
@@ -206411,7 +206824,7 @@ const decideLanding = (rest, verdicts) => gen(function* () {
|
|
|
206411
206824
|
const early = cleanLanding(rest);
|
|
206412
206825
|
if (early !== void 0) return early;
|
|
206413
206826
|
const outcome = yield* promise(() => replayFor(rest.setup, {
|
|
206414
|
-
tree: pendingTree(rest.setup.workspace, reviewGateRewrite(rest.stepDef)),
|
|
206827
|
+
tree: pendingTree(rest.setup.workspace, rest.setup.pendingWorktree(), reviewGateRewrite(rest.stepDef)),
|
|
206415
206828
|
...verdicts !== void 0 ? { verdicts } : {}
|
|
206416
206829
|
}));
|
|
206417
206830
|
if (outcome.kind === "refused") return {
|
|
@@ -218205,9 +218618,13 @@ const workspaceLayerForRoot = (root) => Workspace.Live.pipe(provide$1(merge(host
|
|
|
218205
218618
|
const layersForRoot = (root) => mergeAll$1(gitLayerForRoot(root), ConfigService.Live, ConfigDiscovery.Live, workspaceLayerForRoot(root), hostLayerForRoot(root), Narrator.layer(() => {}, false));
|
|
218206
218619
|
/**
|
|
218207
218620
|
* One `ManagedRuntime` per root, memoised — caches the runtime (the
|
|
218208
|
-
* constructed service layer), not the config:
|
|
218209
|
-
*
|
|
218210
|
-
*
|
|
218621
|
+
* constructed service layer), not the config: `currentSteeringFile` and a
|
|
218622
|
+
* `steeringMapFor` cache miss still call `ConfigService.load` fresh through
|
|
218623
|
+
* it. A `steeringMapFor` hit skips that load entirely, but its own memo (see
|
|
218624
|
+
* `makeNodeLspEnv`) is keyed on the `(path, size, mtimeMs)` of every file the
|
|
218625
|
+
* LAST such load reported touching, so an edit to `.gtdrc`, `gtd.config.ts`,
|
|
218626
|
+
* or a module `gtd.config.ts` imports still forces the next request to
|
|
218627
|
+
* reload — only an unrelated repeat request skips it.
|
|
218211
218628
|
*/
|
|
218212
218629
|
const runtimeCache = /* @__PURE__ */ new Map();
|
|
218213
218630
|
const runtimeFor = (root) => {
|
|
@@ -218237,21 +218654,75 @@ const reachedSteeringSteps = gen(function* () {
|
|
|
218237
218654
|
}));
|
|
218238
218655
|
return {
|
|
218239
218656
|
def: config.workflow,
|
|
218240
|
-
steps: [...reached, ...declared]
|
|
218657
|
+
steps: [...reached, ...declared],
|
|
218658
|
+
dependencyFiles: [...config.configFiles, ...config.workflowFiles]
|
|
218241
218659
|
};
|
|
218242
218660
|
});
|
|
218661
|
+
/** A cheap `(size, mtimeMs)` fingerprint of `path` — a stat, never a read. `"missing"` when it doesn't exist, so a file's creation OR deletion still changes the identity. */
|
|
218662
|
+
const fingerprintOf = (path) => {
|
|
218663
|
+
try {
|
|
218664
|
+
const stat = statSync(path);
|
|
218665
|
+
return `${stat.size}:${stat.mtimeMs}`;
|
|
218666
|
+
} catch {
|
|
218667
|
+
return "missing";
|
|
218668
|
+
}
|
|
218669
|
+
};
|
|
218670
|
+
/** `paths`' identity, sorted so the same set always reads the same regardless of build order. `known`'s fingerprint wins over a fresh `fingerprintOf` stat where it has one — see `steeringMapFor`'s miss path for why that matters. */
|
|
218671
|
+
const identityOf = (paths, known) => paths.map((path) => `${path}:${known?.get(path) ?? fingerprintOf(path)}`).sort().join("|");
|
|
218672
|
+
/**
|
|
218673
|
+
* Every path a `.gtdrc`/`gtd.config.ts` COULD live at for `root` — every
|
|
218674
|
+
* `SEARCH_PLACES` name plus `gtd.config.ts` itself, in every directory
|
|
218675
|
+
* `walkUp(root, home)` visits — computed from path strings alone, no
|
|
218676
|
+
* filesystem access. Unlike `ConfigOperations.configFiles`/`workflowFiles`
|
|
218677
|
+
* (which name only what a load actually FOUND), this list is complete
|
|
218678
|
+
* whether or not any of it exists yet, so `identityOf` on it catches a
|
|
218679
|
+
* brand-new `.gtdrc` appearing where there was none, not just an edit to one
|
|
218680
|
+
* that already existed.
|
|
218681
|
+
*/
|
|
218682
|
+
const candidateConfigPaths = (root) => walkUp(root, liveHost.home).flatMap((dir) => [...SEARCH_PLACES.map((name) => resolve(dir, name)), resolve(dir, WORKFLOW_MODULE)]);
|
|
218243
218683
|
/** The Node adapter: the only place `LspEnv`'s Effects/layers get built and run. `startLspServer` is its production caller; most `Lsp.test.ts` coverage exercises a fake `LspEnv` instead, but this is exported so the real wiring (real git/config/repo-files layers) gets exercised against a real temp repo too. */
|
|
218244
|
-
const makeNodeLspEnv = (warn) =>
|
|
218245
|
-
|
|
218246
|
-
|
|
218247
|
-
|
|
218248
|
-
|
|
218249
|
-
|
|
218250
|
-
return
|
|
218251
|
-
}
|
|
218252
|
-
|
|
218253
|
-
|
|
218254
|
-
|
|
218684
|
+
const makeNodeLspEnv = (warn) => {
|
|
218685
|
+
const stepsMemo = /* @__PURE__ */ new Map();
|
|
218686
|
+
const headHashFor = (root) => runtimeFor(root).runPromise(flatMap$4(GitService, (git) => git.resolveRef("HEAD"))).catch(() => "no-head");
|
|
218687
|
+
/** `result.dependencyFiles` beyond `candidateConfigPaths(root)` — a genuine import elsewhere (e.g. a split workflow's sibling module), never `gtd.config.ts`/`.gtdrc` themselves, which the static set already names. */
|
|
218688
|
+
const dynamicOnlyPaths = (root, result) => {
|
|
218689
|
+
const staticSet = new Set(candidateConfigPaths(root));
|
|
218690
|
+
return result.dependencyFiles.filter((path) => !staticSet.has(path));
|
|
218691
|
+
};
|
|
218692
|
+
const queues = /* @__PURE__ */ new Map();
|
|
218693
|
+
const serialized = (root, task) => {
|
|
218694
|
+
const settled = (queues.get(root) ?? Promise.resolve()).then(task, task);
|
|
218695
|
+
queues.set(root, settled.then(() => void 0, () => void 0));
|
|
218696
|
+
return settled;
|
|
218697
|
+
};
|
|
218698
|
+
return {
|
|
218699
|
+
cwd: liveHost.root,
|
|
218700
|
+
steeringMapFor: (root) => serialized(root, async () => {
|
|
218701
|
+
const headHash = await headHashFor(root);
|
|
218702
|
+
const cached = stepsMemo.get(root);
|
|
218703
|
+
if (cached !== void 0 && cached.headHash === headHash && identityOf([...candidateConfigPaths(root), ...dynamicOnlyPaths(root, cached.result)]) === cached.configIdentity) {
|
|
218704
|
+
const { map, warnings } = buildSteeringMap(cached.result.def, cached.result.steps, root);
|
|
218705
|
+
for (const warning of warnings) warn(warning);
|
|
218706
|
+
return map;
|
|
218707
|
+
}
|
|
218708
|
+
const staticPaths = candidateConfigPaths(root);
|
|
218709
|
+
const knownDynamic = cached !== void 0 ? dynamicOnlyPaths(root, cached.result) : [];
|
|
218710
|
+
const before = new Map([...staticPaths, ...knownDynamic].map((path) => [path, fingerprintOf(path)]));
|
|
218711
|
+
const result = await runtimeFor(root).runPromise(reachedSteeringSteps);
|
|
218712
|
+
const configIdentity = identityOf([...staticPaths, ...dynamicOnlyPaths(root, result)], before);
|
|
218713
|
+
stepsMemo.set(root, {
|
|
218714
|
+
headHash,
|
|
218715
|
+
configIdentity,
|
|
218716
|
+
result
|
|
218717
|
+
});
|
|
218718
|
+
const { map, warnings } = buildSteeringMap(result.def, result.steps, root);
|
|
218719
|
+
for (const warning of warnings) warn(warning);
|
|
218720
|
+
return map;
|
|
218721
|
+
}),
|
|
218722
|
+
gitTopLevel: (dir) => runtimeFor(dir).runPromise(flatMap$4(GitService, (git) => git.topLevel())),
|
|
218723
|
+
currentSteeringFile: (root) => runtimeFor(root).runPromise(resolveSteeringFile)
|
|
218724
|
+
};
|
|
218725
|
+
};
|
|
218255
218726
|
/**
|
|
218256
218727
|
* Starts the `gtd lsp` server over stdio. The returned Effect resolves when
|
|
218257
218728
|
* the client disconnects (`exit` notification), so the process exits cleanly
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmelab/gtd",
|
|
3
|
-
"version": "15.2.
|
|
3
|
+
"version": "15.2.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Git-aware CLI that emits the next prompt for an autonomous coding agent based on the current repository state",
|
|
6
6
|
"bin": {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"build": "tsdown --filter web && npx @tailwindcss/cli -i src/web/styles.css -o dist/web/main.css --minify && node scripts/inline-web-client.mjs && tsdown --filter gtd",
|
|
39
39
|
"postbuild": "jiti scripts/generate-schema.ts && node scripts/assert-no-test-doubles.mjs",
|
|
40
40
|
"watch": "tsdown --watch",
|
|
41
|
+
"bench:replay": "node scripts/bench-replay.mjs",
|
|
41
42
|
"test": "turbo run format:check typecheck lint lint:sh lint:boundaries test:unit test:e2e:inmem test:e2e:live test:web analyze",
|
|
42
43
|
"test:unit": "vitest run --project unit",
|
|
43
44
|
"test:watch": "vitest",
|