tldr-experts 0.9.2 → 0.11.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +286 -0
  2. package/README.md +6 -2
  3. package/dist/hooks/answer-capture.js +297 -25
  4. package/dist/hooks/budget-gate.js +7 -10
  5. package/dist/hooks/{chunk-hg43zexg.js → chunk-1c43349s.js} +65 -51
  6. package/dist/hooks/{chunk-x3zy7zrn.js → chunk-6c84kjd7.js} +1 -1
  7. package/dist/hooks/{chunk-2y3kwfrx.js → chunk-6ws778wq.js} +1 -1
  8. package/dist/hooks/{chunk-fv66tdsm.js → chunk-9v45j71e.js} +23 -8
  9. package/dist/hooks/chunk-bk7rkdzw.js +103 -0
  10. package/dist/hooks/{chunk-4w03q2kj.js → chunk-d20x2aew.js} +27 -19
  11. package/dist/hooks/{chunk-rk1nm39z.js → chunk-ecwes7kd.js} +1 -1
  12. package/dist/hooks/{chunk-f6484ykg.js → chunk-kqvbzyc0.js} +24 -10
  13. package/dist/hooks/{chunk-y3cxqvnx.js → chunk-r1efatxk.js} +411 -10
  14. package/dist/hooks/{chunk-cf4az0j5.js → chunk-stw0dxna.js} +1 -1
  15. package/dist/hooks/{chunk-8awygmqw.js → chunk-tg7ppsxr.js} +4 -1
  16. package/dist/hooks/{chunk-vyvjbbmb.js → chunk-wzzywf6c.js} +32 -2
  17. package/dist/hooks/claim-sources.js +5 -5
  18. package/dist/hooks/dod-gate.js +8 -99
  19. package/dist/hooks/no-reask.js +8 -9
  20. package/dist/hooks/session-start.js +38 -123
  21. package/dist/hooks/statusline.js +6 -9
  22. package/dist/tldrx.js +3503 -1648
  23. package/package.json +1 -1
  24. package/plugin/.claude-plugin/plugin.json +1 -1
  25. package/stages/build/stage.yml +14 -0
  26. package/stages/how/stage.yml +6 -0
  27. package/stages/plan/stage.yml +6 -0
  28. package/stages/watch/stage.yml +6 -0
  29. package/stages/what/stage.yml +6 -0
  30. package/templates/experts/stack/dotnet.md +4 -3
  31. package/templates/experts/stack/javascript.md +4 -3
  32. package/templates/experts/stack/python.md +4 -3
  33. package/templates/experts/stack/typescript.md +4 -3
  34. package/templates/workspace.yml +6 -0
  35. package/workflows/bugfix.yml +7 -0
  36. package/workflows/docs.yml +7 -0
  37. package/workflows/feature.yml +7 -0
  38. package/workflows/hotfix.yml +7 -0
  39. package/workflows/integration.yml +7 -0
  40. package/workflows/migration.yml +7 -0
  41. package/workflows/performance.yml +7 -0
  42. package/workflows/prototype.yml +7 -0
  43. package/workflows/refactor.yml +7 -0
  44. package/workflows/retro.yml +7 -0
  45. package/workflows/security-patch.yml +7 -0
  46. package/workflows/spike.yml +7 -0
  47. package/workflows/upgrade.yml +7 -0
  48. package/dist/hooks/chunk-39zh2e44.js +0 -97
  49. package/dist/hooks/chunk-5w5eg00k.js +0 -136
  50. package/dist/hooks/chunk-rrkdfk7s.js +0 -30
@@ -1,33 +1,41 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- FactsStore
4
- } from "./chunk-f6484ykg.js";
3
+ conflictOf
4
+ } from "./chunk-bk7rkdzw.js";
5
+ import {
6
+ FactsStore,
7
+ formatJaccard
8
+ } from "./chunk-kqvbzyc0.js";
5
9
  import {
6
10
  parseHookInput,
7
11
  readStdin
8
- } from "./chunk-x3zy7zrn.js";
12
+ } from "./chunk-6c84kjd7.js";
9
13
  import {
10
14
  EventLog,
11
15
  PHASE_ID_RE
12
- } from "./chunk-fv66tdsm.js";
13
- import"./chunk-5w5eg00k.js";
14
- import"./chunk-8awygmqw.js";
16
+ } from "./chunk-9v45j71e.js";
17
+ import"./chunk-tg7ppsxr.js";
15
18
  import {
19
+ ADVISORY_KEY,
16
20
  MAX_FACT_CHARS,
17
21
  detectAnswered,
18
22
  parseQuestions,
19
23
  recordAnswer,
24
+ renderQuestionBlock,
20
25
  replaceBlock,
21
26
  serializeQuestions
22
- } from "./chunk-vyvjbbmb.js";
23
- import"./chunk-39zh2e44.js";
27
+ } from "./chunk-wzzywf6c.js";
24
28
  import {
29
+ ITERATION_ONLY_SLOT,
30
+ PROJECT_FRAMEWORK_DIR,
25
31
  PROJECT_WORK_DIR,
26
- factsPath
27
- } from "./chunk-y3cxqvnx.js";
32
+ commandProbeIssues,
33
+ factsPath,
34
+ parseYaml
35
+ } from "./chunk-r1efatxk.js";
28
36
 
29
37
  // src/hooks/answer-capture.ts
30
- import { existsSync as existsSync3 } from "fs";
38
+ import { existsSync as existsSync5 } from "fs";
31
39
 
32
40
  // src/hooks/lib/decide.ts
33
41
  function allow() {
@@ -69,6 +77,7 @@ function filePathOf(payload) {
69
77
  }
70
78
 
71
79
  // src/hooks/lib/workspace.ts
80
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
72
81
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
73
82
  function locateWork(filePath) {
74
83
  if (filePath === "")
@@ -88,6 +97,111 @@ function locateWork(filePath) {
88
97
  relative: parts.slice(at + 2).join("/")
89
98
  };
90
99
  }
100
+ var FALLBACK_DEFAULT_BRANCH = "main";
101
+ function commandRolesOf(doc) {
102
+ const out = new Map;
103
+ const list = doc?.repos;
104
+ if (!Array.isArray(list))
105
+ return out;
106
+ for (const entry of list) {
107
+ if (typeof entry?.name !== "string")
108
+ continue;
109
+ const roles = new Map;
110
+ const cmds = entry.commands;
111
+ if (cmds !== null && typeof cmds === "object") {
112
+ for (const [role, value] of Object.entries(cmds)) {
113
+ if (typeof value !== "string" || value.trim() === "")
114
+ continue;
115
+ roles.set(role, value);
116
+ }
117
+ }
118
+ out.set(entry.name, roles);
119
+ }
120
+ return out;
121
+ }
122
+ function commandProbesOf(doc) {
123
+ const out = new Map;
124
+ const list = doc?.repos;
125
+ if (!Array.isArray(list))
126
+ return out;
127
+ for (const entry of list) {
128
+ if (typeof entry?.name !== "string")
129
+ continue;
130
+ const probes = new Map;
131
+ const raw = entry.command_probes;
132
+ if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
133
+ for (const [slot, value] of Object.entries(raw)) {
134
+ if (commandProbeIssues(value, slot).length > 0)
135
+ continue;
136
+ probes.set(slot, value);
137
+ }
138
+ }
139
+ out.set(entry.name, probes);
140
+ }
141
+ return out;
142
+ }
143
+ function loadWorkspace(root) {
144
+ const repos = new Map;
145
+ const commands = new Set;
146
+ const repoCommands = new Map;
147
+ const commandRoles = new Map;
148
+ const commandProbes = new Map;
149
+ const iterationCommands = new Set;
150
+ const defaultBranches = new Map;
151
+ let seedTriageThresholdTokens = null;
152
+ const empty = () => ({
153
+ root,
154
+ repos,
155
+ commands,
156
+ repoCommands,
157
+ commandRoles,
158
+ commandProbes,
159
+ iterationCommands,
160
+ defaultBranches,
161
+ seedTriageThresholdTokens
162
+ });
163
+ const path = join(root, PROJECT_FRAMEWORK_DIR, "workspace.yml");
164
+ if (!existsSync(path))
165
+ return empty();
166
+ let doc;
167
+ try {
168
+ doc = parseYaml(readFileSync(path, "utf8"));
169
+ } catch {
170
+ return empty();
171
+ }
172
+ const triage = doc?.seed_triage;
173
+ if (triage !== null && typeof triage === "object") {
174
+ const tokens = triage.threshold_tokens;
175
+ if (typeof tokens === "number" && Number.isFinite(tokens) && tokens > 0) {
176
+ seedTriageThresholdTokens = tokens;
177
+ }
178
+ }
179
+ const list = doc?.repos;
180
+ if (!Array.isArray(list))
181
+ return empty();
182
+ const declared = commandRolesOf(doc);
183
+ const measured = commandProbesOf(doc);
184
+ for (const entry of list) {
185
+ if (typeof entry?.name !== "string")
186
+ continue;
187
+ repos.set(entry.name, typeof entry.path === "string" ? entry.path : ".");
188
+ defaultBranches.set(entry.name, typeof entry.default_branch === "string" && entry.default_branch !== "" ? entry.default_branch : FALLBACK_DEFAULT_BRANCH);
189
+ const own = [];
190
+ const roles = declared.get(entry.name) ?? new Map;
191
+ for (const value of roles.values()) {
192
+ commands.add(value);
193
+ if (!own.includes(value))
194
+ own.push(value);
195
+ }
196
+ repoCommands.set(entry.name, own);
197
+ commandRoles.set(entry.name, roles);
198
+ const fast = roles.get(ITERATION_ONLY_SLOT);
199
+ if (fast !== undefined)
200
+ iterationCommands.add(fast);
201
+ commandProbes.set(entry.name, measured.get(entry.name) ?? new Map);
202
+ }
203
+ return empty();
204
+ }
91
205
 
92
206
  // src/hooks/lib/actor.ts
93
207
  function currentActor() {
@@ -99,10 +213,10 @@ function nowRfc3339() {
99
213
  }
100
214
 
101
215
  // src/core/answers/captureAnswers.ts
102
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
216
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync } from "node:fs";
103
217
 
104
218
  // src/core/answers/stampSuperseded.ts
105
- import { appendFileSync, existsSync, readFileSync, statSync } from "node:fs";
219
+ import { appendFileSync, existsSync as existsSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
106
220
  import { isAbsolute as isAbsolute2, join as join2, relative } from "node:path";
107
221
  var STAMP_MARKER = "tldrx:superseded";
108
222
  var AFFECTS_KEY = "affects";
@@ -142,7 +256,7 @@ function stampSuperseded(runDir, questionsPath, block, fact, at) {
142
256
  const questionsRel = runRelative(runDir, questionsPath) ?? "questions.md";
143
257
  const stamped = [];
144
258
  for (const doc of affectedDocs(runDir, questionsPath, block)) {
145
- const text = readFileSync(doc.abs, "utf8");
259
+ const text = readFileSync2(doc.abs, "utf8");
146
260
  if (isStamped(text, fact))
147
261
  continue;
148
262
  const lead = text === "" ? "" : text.endsWith(`
@@ -167,7 +281,7 @@ function resolveInRun(runDir, raw) {
167
281
  const rel = runRelative(runDir, candidate);
168
282
  if (rel === null)
169
283
  continue;
170
- if (!existsSync(candidate) || !statSync(candidate).isFile())
284
+ if (!existsSync2(candidate) || !statSync2(candidate).isFile())
171
285
  continue;
172
286
  return rel;
173
287
  }
@@ -187,7 +301,113 @@ function isEarlier(phase, than) {
187
301
  return Number(phase.slice(0, 2)) < Number(than.slice(0, 2));
188
302
  }
189
303
 
304
+ // src/core/answers/reposFromAffects.ts
305
+ function uniqueRepos(names) {
306
+ return [...new Set(names)];
307
+ }
308
+ function reposFromAffects(affects, repoNames) {
309
+ const repos = [];
310
+ const unresolved = [];
311
+ for (const raw of affects) {
312
+ const entry = raw.trim();
313
+ if (entry === "")
314
+ continue;
315
+ if (repoNames.has(entry)) {
316
+ repos.push(entry);
317
+ continue;
318
+ }
319
+ const colon = entry.indexOf(":");
320
+ if (colon <= 0)
321
+ continue;
322
+ const prefix = entry.slice(0, colon);
323
+ if (repoNames.has(prefix)) {
324
+ repos.push(prefix);
325
+ continue;
326
+ }
327
+ unresolved.push(entry);
328
+ }
329
+ return { repos: uniqueRepos(repos), unresolved };
330
+ }
331
+
332
+ // src/core/answers/raiseConflict.ts
333
+ import { appendFileSync as appendFileSync2, existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
334
+ import { join as join3 } from "node:path";
335
+
336
+ // src/core/run/questionCards.ts
337
+ var QUESTION_PHASES = ["01-what", "02-how", "03-plan", "04-build", "05-watch"];
338
+
339
+ // src/core/answers/raiseConflict.ts
340
+ var RAISED_BY = "tldrx";
341
+ function nextQuestionId(runDir) {
342
+ let highest = 0;
343
+ for (const phase of QUESTION_PHASES) {
344
+ const path = join3(runDir, phase, "questions.md");
345
+ if (!existsSync3(path))
346
+ continue;
347
+ let blocks;
348
+ try {
349
+ blocks = parseQuestions(readFileSync3(path, "utf8")).blocks;
350
+ } catch {
351
+ continue;
352
+ }
353
+ for (const block of blocks) {
354
+ const n = Number.parseInt(block.id.slice(1), 10);
355
+ if (Number.isFinite(n) && n > highest)
356
+ highest = n;
357
+ }
358
+ }
359
+ return `Q${String(highest + 1)}`;
360
+ }
361
+ function raiseConflictQuestion(args) {
362
+ const id = nextQuestionId(args.runDir);
363
+ const block = renderQuestionBlock({
364
+ id,
365
+ title: `Which is right about ${args.area}: ${args.newFactId} or ${args.oldFactId}?`,
366
+ metadata: {
367
+ id,
368
+ status: "open",
369
+ area: args.area,
370
+ asked_by: RAISED_BY,
371
+ asked_at: args.at,
372
+ extra: [[ADVISORY_KEY, "true"]]
373
+ },
374
+ metadataIndex: -1,
375
+ whyAsked: `answering ${args.answeredQ} recorded ${args.newFactId}, which overlaps ${args.oldFactId}'s ` + `text at Jaccard ${formatJaccard(args.score)} on the question's wording, in the same area, ` + `and answers it differently — ${args.oldFactId}: "${oneLine(args.oldFactText)}" ` + `[src: ${args.oldFactId}]`,
376
+ whySrc: null,
377
+ options: [
378
+ { letter: "A", text: `${args.newFactId} is right — supersede ${args.oldFactId}` },
379
+ { letter: "B", text: `${args.oldFactId} is right — supersede ${args.newFactId}` },
380
+ { letter: "C", text: "Both are partly right — write the correction below" }
381
+ ],
382
+ answer: "",
383
+ answerIndex: -1,
384
+ footer: null,
385
+ startLine: -1,
386
+ lines: []
387
+ });
388
+ appendFileSync2(args.questionsPath, `
389
+ ${block}
390
+ `, "utf8");
391
+ return { q: id, fact: args.newFactId, conflictsWith: args.oldFactId, score: args.score };
392
+ }
393
+ function oneLine(text) {
394
+ return text.replace(/\s+/g, " ").trim();
395
+ }
396
+
190
397
  // src/core/answers/captureAnswers.ts
398
+ function answerProvenance(block, overrides, repoNames, fallbackRepos) {
399
+ const override = overrides?.get(block.id);
400
+ const named = reposFromAffects(declaredAffects(block), repoNames ?? new Set);
401
+ const repos = override?.repos ?? (named.repos.length > 0 ? named.repos : fallbackRepos);
402
+ return {
403
+ repos,
404
+ unresolved: named.unresolved,
405
+ source: override?.decidedBy === undefined ? {} : { decided_by: override.decidedBy }
406
+ };
407
+ }
408
+ function unresolvedEntries(captured) {
409
+ return captured.flatMap((c) => c.unresolvedAffects.map((entry) => `${c.q}: affects: ${entry} names no repo in this workspace — it scoped nothing`));
410
+ }
191
411
  var TRUNCATION_MARK = " …";
192
412
  function factTextFor(title, answer) {
193
413
  const whole = `${title} — ${answer}`;
@@ -199,28 +419,35 @@ function factWasTruncated(title, answer) {
199
419
  return `${title} — ${answer}`.length > MAX_FACT_CHARS;
200
420
  }
201
421
  function captureAnswers(questionsPath, ctx) {
202
- if (!existsSync2(questionsPath))
422
+ if (!existsSync4(questionsPath))
203
423
  return [];
204
- let doc = parseQuestions(readFileSync2(questionsPath, "utf8"));
424
+ let doc = parseQuestions(readFileSync4(questionsPath, "utf8"));
205
425
  const answered = detectAnswered(doc.blocks);
206
426
  if (answered.length === 0)
207
427
  return [];
208
428
  const log = EventLog.forRun(ctx.runDir);
209
429
  const captured = [];
210
430
  const recorded = [];
431
+ const clashes = [];
211
432
  FactsStore.update(factsPath(ctx.root), (store) => {
212
433
  for (const block of answered) {
213
434
  const area = block.metadata?.area ?? "unscoped";
214
435
  const truncated = factWasTruncated(block.title, block.answer);
436
+ const prov = answerProvenance(block, ctx.overrides, ctx.repoNames, []);
437
+ const text = factTextFor(block.title, block.answer);
438
+ const clash = conflictOf({ match: block.title, area, text }, store.active);
215
439
  const fact = store.append({
216
- fact: factTextFor(block.title, block.answer),
440
+ fact: text,
217
441
  ...truncated ? { truncated: true } : {},
442
+ ...clash === null ? {} : { conflicts_with: [clash.fact.id] },
218
443
  area,
219
- repos: [],
444
+ repos: prov.repos,
220
445
  kind: "answer",
221
446
  confidence: "stated",
222
- source: { who: ctx.actor, when: ctx.at, run: ctx.run, q: block.id }
447
+ source: { who: ctx.actor, when: ctx.at, run: ctx.run, q: block.id, ...prov.source }
223
448
  });
449
+ if (clash !== null)
450
+ clashes.push({ block, area, factId: fact.id, hit: clash });
224
451
  doc = replaceBlock(doc, recordAnswer(block, { answered_by: ctx.actor, answered_at: ctx.at, fact: fact.id }));
225
452
  log.tryAppend({
226
453
  ts: ctx.at,
@@ -240,15 +467,58 @@ function captureAnswers(questionsPath, ctx) {
240
467
  cost_usd: 0,
241
468
  payload: { fact: fact.id, area: fact.area, kind: fact.kind, q: block.id }
242
469
  });
243
- captured.push({ q: block.id, fact: fact.id, answer: block.answer, area });
470
+ captured.push({
471
+ q: block.id,
472
+ fact: fact.id,
473
+ answer: block.answer,
474
+ area,
475
+ unresolvedAffects: prov.unresolved
476
+ });
244
477
  recorded.push({ block, fact: fact.id });
245
478
  }
246
479
  });
247
480
  writeFileSync(questionsPath, serializeQuestions(doc), "utf8");
481
+ const raised = raiseAll(log, ctx, questionsPath, clashes);
248
482
  for (const item of recorded) {
249
483
  markSuperseded(log, ctx, questionsPath, item.block, item.fact);
250
484
  }
251
- return captured;
485
+ return captured.map((answer) => {
486
+ const conflict = raised.get(answer.q);
487
+ return conflict === undefined ? answer : { ...answer, conflict };
488
+ });
489
+ }
490
+ function raiseAll(log, ctx, questionsPath, clashes) {
491
+ const raised = new Map;
492
+ for (const clash of clashes) {
493
+ const conflict = raiseConflictQuestion({
494
+ runDir: ctx.runDir,
495
+ questionsPath,
496
+ area: clash.area,
497
+ at: ctx.at,
498
+ newFactId: clash.factId,
499
+ oldFactId: clash.hit.fact.id,
500
+ oldFactText: clash.hit.fact.fact,
501
+ score: clash.hit.score,
502
+ answeredQ: clash.block.id
503
+ });
504
+ raised.set(clash.block.id, conflict);
505
+ log.tryAppend({
506
+ ts: ctx.at,
507
+ run: ctx.run,
508
+ stage: null,
509
+ type: "fact.conflict_raised",
510
+ actor: ctx.actor,
511
+ cost_usd: 0,
512
+ payload: {
513
+ fact: clash.factId,
514
+ conflicts_with: clash.hit.fact.id,
515
+ score: clash.hit.score,
516
+ q: clash.block.id,
517
+ raised: conflict.q
518
+ }
519
+ });
520
+ }
521
+ return raised;
252
522
  }
253
523
  function markSuperseded(log, ctx, questionsPath, block, fact) {
254
524
  try {
@@ -287,7 +557,7 @@ await runHook("answer-capture", async () => {
287
557
  if (event === "PostToolUse" && payload.tool_name !== "Write" && payload.tool_name !== "Edit")
288
558
  return;
289
559
  const filePath = filePathOf(payload);
290
- if (!filePath.endsWith("questions.md") || !existsSync3(filePath))
560
+ if (!filePath.endsWith("questions.md") || !existsSync5(filePath))
291
561
  return;
292
562
  const location = locateWork(filePath);
293
563
  if (location === null)
@@ -297,10 +567,12 @@ await runHook("answer-capture", async () => {
297
567
  runDir: location.runDir,
298
568
  run: location.run,
299
569
  actor: currentActor(),
300
- at: nowRfc3339()
570
+ at: nowRfc3339(),
571
+ repoNames: new Set(loadWorkspace(location.root).repos.keys())
301
572
  });
302
573
  if (captured.length === 0)
303
574
  return;
304
- postContext(`tldrx: recorded ${captured.map((c) => `${c.q} \u2192 ${c.fact}`).join(", ")}`);
575
+ const unresolved = unresolvedEntries(captured);
576
+ postContext(`tldrx: recorded ${captured.map((c) => `${c.q} \u2192 ${c.fact}`).join(", ")}` + (unresolved.length === 0 ? "" : ` \u2014 ${unresolved.join("; ")}`));
305
577
  });
306
578
  allow();
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  budgetGateDeny
4
- } from "./chunk-cf4az0j5.js";
4
+ } from "./chunk-stw0dxna.js";
5
5
  import {
6
6
  allow,
7
7
  deny,
8
8
  readPayload,
9
9
  runHook,
10
10
  toolInput
11
- } from "./chunk-2y3kwfrx.js";
12
- import"./chunk-x3zy7zrn.js";
11
+ } from "./chunk-6ws778wq.js";
12
+ import"./chunk-6c84kjd7.js";
13
13
  import {
14
14
  asRunBudget,
15
15
  currentActor,
@@ -29,23 +29,20 @@ import {
29
29
  validateRunBudget,
30
30
  wouldExceed,
31
31
  wouldExceedHostTokens
32
- } from "./chunk-hg43zexg.js";
32
+ } from "./chunk-1c43349s.js";
33
33
  import {
34
34
  EventLog
35
- } from "./chunk-fv66tdsm.js";
36
- import"./chunk-rrkdfk7s.js";
37
- import"./chunk-5w5eg00k.js";
35
+ } from "./chunk-9v45j71e.js";
38
36
  import {
39
37
  noteDeprecations
40
- } from "./chunk-vyvjbbmb.js";
41
- import"./chunk-39zh2e44.js";
38
+ } from "./chunk-wzzywf6c.js";
42
39
  import {
43
40
  PROJECT_WORK_DIR,
44
41
  findWorkspaceRoot,
45
42
  locateWork,
46
43
  parseYaml,
47
44
  stageYamlPath
48
- } from "./chunk-y3cxqvnx.js";
45
+ } from "./chunk-r1efatxk.js";
49
46
 
50
47
  // src/hooks/budget-gate.ts
51
48
  import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";